From a4cc1d8daa587191651d26a35f0718937029e89a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Wed, 22 Oct 2025 10:32:14 +0100 Subject: [PATCH] chore: last month and month before last --- .../getEdgeInstances.e2e.test.ts | 132 ++++++++++++++++++ .../instance-stats/getEdgeInstances.ts | 52 ++++--- .../instance-stats/instance-stats-service.ts | 5 +- .../spec/instance-admin-stats-schema.ts | 17 +-- src/lib/routes/admin-api/instance-admin.ts | 5 +- src/lib/services/version-service.test.ts | 5 +- src/lib/services/version-service.ts | 5 +- 7 files changed, 178 insertions(+), 43 deletions(-) create mode 100644 src/lib/features/instance-stats/getEdgeInstances.e2e.test.ts diff --git a/src/lib/features/instance-stats/getEdgeInstances.e2e.test.ts b/src/lib/features/instance-stats/getEdgeInstances.e2e.test.ts new file mode 100644 index 0000000000..ca45ded3ae --- /dev/null +++ b/src/lib/features/instance-stats/getEdgeInstances.e2e.test.ts @@ -0,0 +1,132 @@ +import { + createGetEdgeInstances, + type GetEdgeInstances, +} from './getEdgeInstances.js'; +import dbInit, { + type ITestDb, +} from '../../../test/e2e/helpers/database-init.js'; +import getLogger from '../../../test/fixtures/no-logger.js'; + +let db: ITestDb; +let getEdgeInstances: GetEdgeInstances; + +const TABLE = 'edge_node_presence'; + +const firstDayOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1); +const addMonths = (d: Date, n: number) => + new Date(d.getFullYear(), d.getMonth() + n, 1); + +const monthWindows = () => { + const now = new Date(); + const thisMonthStart = firstDayOfMonth(now); + const lastMonthStart = addMonths(thisMonthStart, -1); + const monthBeforeLastStart = addMonths(thisMonthStart, -2); + const lastMonthEnd = thisMonthStart; + const monthBeforeLastEnd = lastMonthStart; + return { + monthBeforeLastStart, + monthBeforeLastEnd, + lastMonthStart, + lastMonthEnd, + thisMonthStart, + }; +}; + +const atMidMonth = (start: Date) => + new Date(start.getFullYear(), start.getMonth(), 15); +const atLateMonth = (start: Date) => + new Date(start.getFullYear(), start.getMonth(), 25); + +const rowsForBucket = (count: number, when: Date) => + Array.from({ length: count }, (_, i) => ({ + bucket_ts: when, + node_ephem_id: `node-${when.getTime()}-${i}`, + })); + +beforeAll(async () => { + db = await dbInit('edge_instances_e2e', getLogger); + getEdgeInstances = createGetEdgeInstances(db.rawDatabase); +}); + +afterEach(async () => { + await db.rawDatabase(TABLE).delete(); +}); + +afterAll(async () => { + await db.destroy(); +}); + +test('returns 0 for both months when no data', async () => { + await expect(getEdgeInstances()).resolves.toEqual({ + lastMonth: 0, + monthBeforeLast: 0, + }); +}); + +test('counts only last full month', async () => { + const { lastMonthStart } = monthWindows(); + const mid = atMidMonth(lastMonthStart); + const late = atLateMonth(lastMonthStart); + await db + .rawDatabase(TABLE) + .insert([...rowsForBucket(3, mid), ...rowsForBucket(7, late)]); + await expect(getEdgeInstances()).resolves.toEqual({ + lastMonth: 5, + monthBeforeLast: 0, + }); +}); + +test('counts only month before last', async () => { + const { monthBeforeLastStart } = monthWindows(); + const mid = atMidMonth(monthBeforeLastStart); + const late = atLateMonth(monthBeforeLastStart); + await db + .rawDatabase(TABLE) + .insert([...rowsForBucket(2, mid), ...rowsForBucket(5, late)]); + await expect(getEdgeInstances()).resolves.toEqual({ + lastMonth: 0, + monthBeforeLast: 4, + }); +}); + +test('separates months correctly when both have data', async () => { + const { monthBeforeLastStart, lastMonthStart } = monthWindows(); + const pMid = atMidMonth(monthBeforeLastStart); + const pLate = atLateMonth(monthBeforeLastStart); + const lMid = atMidMonth(lastMonthStart); + const lLate = atLateMonth(lastMonthStart); + + await db + .rawDatabase(TABLE) + .insert([ + ...rowsForBucket(4, pMid), + ...rowsForBucket(6, pLate), + ...rowsForBucket(3, lMid), + ...rowsForBucket(7, lLate), + ]); + + await expect(getEdgeInstances()).resolves.toEqual({ + lastMonth: 5, + monthBeforeLast: 5, + }); +}); + +test('ignores current month data', async () => { + const { thisMonthStart, lastMonthStart } = monthWindows(); + const lMid = atMidMonth(lastMonthStart); + const lLate = atLateMonth(lastMonthStart); + const tMid = atMidMonth(thisMonthStart); + + await db + .rawDatabase(TABLE) + .insert([ + ...rowsForBucket(10, tMid), + ...rowsForBucket(2, lMid), + ...rowsForBucket(4, lLate), + ]); + + await expect(getEdgeInstances()).resolves.toEqual({ + lastMonth: 3, + monthBeforeLast: 0, + }); +}); diff --git a/src/lib/features/instance-stats/getEdgeInstances.ts b/src/lib/features/instance-stats/getEdgeInstances.ts index f9336e39b5..ee7581c54a 100644 --- a/src/lib/features/instance-stats/getEdgeInstances.ts +++ b/src/lib/features/instance-stats/getEdgeInstances.ts @@ -3,9 +3,8 @@ import type { Db } from '../../types/index.js'; const TABLE = 'edge_node_presence'; export type GetEdgeInstances = () => Promise<{ - last30: number; - last60: number; - last90: number; + lastMonth: number; + monthBeforeLast: number; }>; export const createGetEdgeInstances = @@ -15,7 +14,9 @@ export const createGetEdgeInstances = .with('buckets', (qb) => qb .from(TABLE) - .whereRaw("bucket_ts >= NOW() - INTERVAL '90 days'") + .whereRaw( + "bucket_ts >= date_trunc('month', NOW()) - INTERVAL '2 months'", + ) .groupBy('bucket_ts') .select( db.raw('bucket_ts'), @@ -24,31 +25,44 @@ export const createGetEdgeInstances = ) .from('buckets') .select({ - last30: db.raw( - `COALESCE(CEIL(AVG(active_nodes) FILTER (WHERE bucket_ts >= NOW() - INTERVAL '30 days'))::int, 0)`, - ), - last60: db.raw( - `COALESCE(CEIL(AVG(active_nodes) FILTER (WHERE bucket_ts >= NOW() - INTERVAL '60 days'))::int, 0)`, - ), - last90: db.raw( - `COALESCE(CEIL(AVG(active_nodes) FILTER (WHERE bucket_ts >= NOW() - INTERVAL '90 days'))::int, 0)`, - ), + lastMonth: db.raw(` + COALESCE( + CEIL( + AVG(active_nodes) + FILTER ( + WHERE bucket_ts >= date_trunc('month', NOW()) - INTERVAL '1 month' + AND bucket_ts < date_trunc('month', NOW()) + ) + )::int, + 0 + ) + `), + monthBeforeLast: db.raw(` + COALESCE( + CEIL( + AVG(active_nodes) + FILTER ( + WHERE bucket_ts >= date_trunc('month', NOW()) - INTERVAL '2 months' + AND bucket_ts < date_trunc('month', NOW()) - INTERVAL '1 month' + ) + )::int, + 0 + ) + `), }) .first(); return { - last30: Number(result?.last30 ?? 0), - last60: Number(result?.last60 ?? 0), - last90: Number(result?.last90 ?? 0), + lastMonth: Number(result?.lastMonth ?? 0), + monthBeforeLast: Number(result?.monthBeforeLast ?? 0), }; }; export const createFakeGetEdgeInstances = ( edgeInstances: Awaited> = { - last30: 0, - last60: 0, - last90: 0, + lastMonth: 0, + monthBeforeLast: 0, }, ): GetEdgeInstances => () => diff --git a/src/lib/features/instance-stats/instance-stats-service.ts b/src/lib/features/instance-stats/instance-stats-service.ts index 6dd0452dd5..5790d58caf 100644 --- a/src/lib/features/instance-stats/instance-stats-service.ts +++ b/src/lib/features/instance-stats/instance-stats-service.ts @@ -545,9 +545,8 @@ export class InstanceStatsService { hostedBy, releaseTemplates, releasePlans, - edgeInstances30: edgeInstances.last30, - edgeInstances60: edgeInstances.last60, - edgeInstances90: edgeInstances.last90, + edgeInstancesLastMonth: edgeInstances.lastMonth, + edgeInstancesMonthBeforeLast: edgeInstances.monthBeforeLast, }; return featureInfo; } diff --git a/src/lib/openapi/spec/instance-admin-stats-schema.ts b/src/lib/openapi/spec/instance-admin-stats-schema.ts index ddb20bd50e..b83e6d5ded 100644 --- a/src/lib/openapi/spec/instance-admin-stats-schema.ts +++ b/src/lib/openapi/spec/instance-admin-stats-schema.ts @@ -287,29 +287,22 @@ export const instanceAdminStatsSchema = { edgeInstances: { type: 'object', description: - 'The billable number of edge instances in the last 30, 60 and 90 days', + 'The rounded up average number of edge instances in the last month and month before last', properties: { - last30: { + lastMonth: { type: 'integer', description: - 'The billable number of edge instances in the last 30 days', + 'The rounded up average number of edge instances in the last month', example: 10, minimum: 0, }, - last60: { + monthBeforeLast: { type: 'integer', description: - 'The billable number of edge instances in the last 60 days', + 'The rounded up average number of edge instances in the month before last', example: 12, minimum: 0, }, - last90: { - type: 'integer', - description: - 'The billable number of edge instances in the last 90 days', - example: 15, - minimum: 0, - }, }, }, sum: { diff --git a/src/lib/routes/admin-api/instance-admin.ts b/src/lib/routes/admin-api/instance-admin.ts index 193f73d20f..55aa37d13f 100644 --- a/src/lib/routes/admin-api/instance-admin.ts +++ b/src/lib/routes/admin-api/instance-admin.ts @@ -134,9 +134,8 @@ class InstanceAdminController extends Controller { releaseTemplates: 3, releasePlans: 5, edgeInstances: { - last30: 10, - last60: 15, - last90: 20, + lastMonth: 10, + monthBeforeLast: 15, }, }; } diff --git a/src/lib/services/version-service.test.ts b/src/lib/services/version-service.test.ts index c0a0ca9aab..a442b704c1 100644 --- a/src/lib/services/version-service.test.ts +++ b/src/lib/services/version-service.test.ts @@ -45,9 +45,8 @@ const fakeTelemetryData = { hostedBy: 'self-hosted', releaseTemplates: 2, releasePlans: 4, - edgeInstances30: 0, - edgeInstances60: 0, - edgeInstances90: 0, + edgeInstancesLastMonth: 0, + edgeInstancesMonthBeforeLast: 0, }; test('yields current versions', async () => { diff --git a/src/lib/services/version-service.ts b/src/lib/services/version-service.ts index 585e6a04ab..2dcdf96a2e 100644 --- a/src/lib/services/version-service.ts +++ b/src/lib/services/version-service.ts @@ -54,9 +54,8 @@ export interface IFeatureUsageInfo { hostedBy: string; releaseTemplates: number; releasePlans: number; - edgeInstances30?: number; - edgeInstances60?: number; - edgeInstances90?: number; + edgeInstancesLastMonth?: number; + edgeInstancesMonthBeforeLast?: number; } export default class VersionService {