mirror of
https://github.com/Unleash/unleash.git
synced 2025-11-10 01:19:53 +01:00
fix: enterprise edge stats should take into account full month (#10898)
https://linear.app/unleash/issue/2-3993/fix-enterprise-edge-stats Fixes Enterprise Edge stats to correctly reflect the average across the whole month. Now returns a rounded average with 3 decimal places. Also includes the average of the last 12 months.
This commit is contained in:
parent
bbff52eb7b
commit
699f9e6ce2
@ -967,7 +967,7 @@ export * from './instanceAdminStatsSchemaActiveUsers.js';
|
||||
export * from './instanceAdminStatsSchemaApiTokens.js';
|
||||
export * from './instanceAdminStatsSchemaClientAppsItem.js';
|
||||
export * from './instanceAdminStatsSchemaClientAppsItemRange.js';
|
||||
export * from './instanceAdminStatsSchemaEdgeInstances.js';
|
||||
export * from './instanceAdminStatsSchemaEdgeInstanceUsage.js';
|
||||
export * from './instanceAdminStatsSchemaPreviousDayMetricsBucketsCount.js';
|
||||
export * from './instanceAdminStatsSchemaProductionChanges.js';
|
||||
export * from './instanceInsightsSchema.js';
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import type { InstanceAdminStatsSchemaActiveUsers } from './instanceAdminStatsSchemaActiveUsers.js';
|
||||
import type { InstanceAdminStatsSchemaApiTokens } from './instanceAdminStatsSchemaApiTokens.js';
|
||||
import type { InstanceAdminStatsSchemaClientAppsItem } from './instanceAdminStatsSchemaClientAppsItem.js';
|
||||
import type { InstanceAdminStatsSchemaEdgeInstances } from './instanceAdminStatsSchemaEdgeInstances.js';
|
||||
import type { InstanceAdminStatsSchemaEdgeInstanceUsage } from './instanceAdminStatsSchemaEdgeInstanceUsage.js';
|
||||
import type { InstanceAdminStatsSchemaPreviousDayMetricsBucketsCount } from './instanceAdminStatsSchemaPreviousDayMetricsBucketsCount.js';
|
||||
import type { InstanceAdminStatsSchemaProductionChanges } from './instanceAdminStatsSchemaProductionChanges.js';
|
||||
|
||||
@ -25,8 +25,8 @@ export interface InstanceAdminStatsSchema {
|
||||
* @minimum 0
|
||||
*/
|
||||
contextFields?: number;
|
||||
/** The rounded up average number of edge instances in the last month and month before last */
|
||||
edgeInstances?: InstanceAdminStatsSchemaEdgeInstances;
|
||||
/** The average number of edge instances, per month, in the last 12 months, rounded to 3 decimal places */
|
||||
edgeInstanceUsage?: InstanceAdminStatsSchemaEdgeInstanceUsage;
|
||||
/**
|
||||
* The number of environments defined in this instance
|
||||
* @minimum 0
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
|
||||
/**
|
||||
* The average number of edge instances, per month, in the last 12 months, rounded to 3 decimal places
|
||||
*/
|
||||
export type InstanceAdminStatsSchemaEdgeInstanceUsage = {
|
||||
[key: string]: number;
|
||||
};
|
||||
@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Generated by Orval
|
||||
* Do not edit manually.
|
||||
* See `gen:api` script in package.json
|
||||
*/
|
||||
|
||||
/**
|
||||
* The rounded up average number of edge instances in the last month and month before last
|
||||
*/
|
||||
export type InstanceAdminStatsSchemaEdgeInstances = {
|
||||
/**
|
||||
* The rounded up average number of edge instances in the last month
|
||||
* @minimum 0
|
||||
*/
|
||||
lastMonth?: number;
|
||||
/**
|
||||
* The rounded up average number of edge instances in the month before last
|
||||
* @minimum 0
|
||||
*/
|
||||
monthBeforeLast?: number;
|
||||
};
|
||||
@ -12,39 +12,105 @@ 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 getMonthRange = async (raw: ITestDb['rawDatabase'], offset: number) => {
|
||||
const { rows } = await raw.raw(
|
||||
`
|
||||
SELECT
|
||||
(date_trunc('month', NOW()) - (? * INTERVAL '1 month'))::timestamptz AS start_ts,
|
||||
(date_trunc('month', NOW()) - ((? - 1) * INTERVAL '1 month'))::timestamptz AS end_ts,
|
||||
to_char((date_trunc('month', NOW()) - (? * INTERVAL '1 month')), 'YYYY-MM') AS key
|
||||
`,
|
||||
[offset, offset, offset],
|
||||
);
|
||||
return rows[0] as { start_ts: string; end_ts: string; key: string };
|
||||
};
|
||||
|
||||
const atMidMonth = (start: Date) =>
|
||||
new Date(start.getFullYear(), start.getMonth(), 15);
|
||||
const atLateMonth = (start: Date) =>
|
||||
new Date(start.getFullYear(), start.getMonth(), 25);
|
||||
const expectedForWindow = async (
|
||||
raw: ITestDb['rawDatabase'],
|
||||
startIso: string,
|
||||
endIso: string,
|
||||
) => {
|
||||
const { rows } = await raw.raw(
|
||||
`
|
||||
WITH mw AS (
|
||||
SELECT ?::timestamptz AS start_ts, ?::timestamptz AS end_ts
|
||||
),
|
||||
buckets AS (
|
||||
SELECT bucket_ts, COUNT(DISTINCT node_ephem_id)::int AS active_nodes
|
||||
FROM ${TABLE}
|
||||
CROSS JOIN mw
|
||||
WHERE bucket_ts >= mw.start_ts AND bucket_ts < mw.end_ts
|
||||
GROUP BY bucket_ts
|
||||
),
|
||||
totals AS (
|
||||
SELECT COALESCE(SUM(active_nodes), 0) AS total FROM buckets
|
||||
)
|
||||
SELECT COALESCE(
|
||||
ROUND(
|
||||
(totals.total::numeric)
|
||||
/ NULLIF(FLOOR(EXTRACT(EPOCH FROM (mw.end_ts - mw.start_ts)) / 300)::int, 0),
|
||||
3),
|
||||
0
|
||||
) AS val
|
||||
FROM totals CROSS JOIN mw
|
||||
`,
|
||||
[startIso, endIso],
|
||||
);
|
||||
return Number(rows[0].val);
|
||||
};
|
||||
|
||||
const rowsForBucket = (count: number, when: Date) =>
|
||||
Array.from({ length: count }, (_, i) => ({
|
||||
bucket_ts: when,
|
||||
node_ephem_id: `node-${when.getTime()}-${i}`,
|
||||
}));
|
||||
const countRowsInWindow = async (
|
||||
raw: ITestDb['rawDatabase'],
|
||||
startIso: string,
|
||||
endIso: string,
|
||||
) => {
|
||||
const { rows } = await raw.raw(
|
||||
`
|
||||
SELECT COUNT(*)::int AS c
|
||||
FROM ${TABLE}
|
||||
WHERE bucket_ts >= ?::timestamptz AND bucket_ts < ?::timestamptz
|
||||
`,
|
||||
[startIso, endIso],
|
||||
);
|
||||
return Number(rows[0].c);
|
||||
};
|
||||
|
||||
const insertSpreadAcrossMonth = async (
|
||||
raw: ITestDb['rawDatabase'],
|
||||
startIso: string,
|
||||
endIso: string,
|
||||
everyNth: number,
|
||||
nodesPerBucket: number,
|
||||
) => {
|
||||
await raw.raw(
|
||||
`
|
||||
WITH series AS (
|
||||
SELECT generate_series(
|
||||
?::timestamptz,
|
||||
(?::timestamptz - INTERVAL '5 minutes'),
|
||||
INTERVAL '5 minutes'
|
||||
) AS ts
|
||||
),
|
||||
picked AS (
|
||||
SELECT ts
|
||||
FROM series
|
||||
WHERE (EXTRACT(EPOCH FROM ts) / 300)::bigint % ? = 0
|
||||
)
|
||||
INSERT INTO ${TABLE} (bucket_ts, node_ephem_id)
|
||||
SELECT
|
||||
p.ts,
|
||||
'node-' || to_char(p.ts AT TIME ZONE 'UTC', 'YYYYMMDDHH24MI') || '-' || i::text
|
||||
FROM picked p
|
||||
CROSS JOIN generate_series(1, ?) AS i
|
||||
ON CONFLICT (bucket_ts, node_ephem_id) DO NOTHING
|
||||
`,
|
||||
[startIso, endIso, everyNth, nodesPerBucket],
|
||||
);
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
db = await dbInit('edge_instances_e2e', getLogger);
|
||||
db = await dbInit('edge_instances_series_e2e', getLogger);
|
||||
await db.rawDatabase.raw(`SET TIME ZONE 'UTC'`);
|
||||
getEdgeInstances = createGetEdgeInstances(db.rawDatabase);
|
||||
});
|
||||
|
||||
@ -56,77 +122,99 @@ 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('returns 12 months with zeros when no data and keys match exact last-12 range', async () => {
|
||||
const res = await getEdgeInstances();
|
||||
const keys = Object.keys(res);
|
||||
expect(keys.length).toBe(12);
|
||||
for (let i = 1; i <= 12; i++) {
|
||||
const { key } = await getMonthRange(db.rawDatabase, i);
|
||||
expect(keys).toContain(key);
|
||||
expect(res[key]).toBe(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('computes average for last month and guarantees data actually inserted', async () => {
|
||||
const { start_ts, end_ts, key } = await getMonthRange(db.rawDatabase, 1);
|
||||
await insertSpreadAcrossMonth(db.rawDatabase, start_ts, end_ts, 2, 3);
|
||||
const inserted = await countRowsInWindow(db.rawDatabase, start_ts, end_ts);
|
||||
expect(inserted).toBeGreaterThan(0);
|
||||
const expected = await expectedForWindow(db.rawDatabase, start_ts, end_ts);
|
||||
expect(expected).toBeGreaterThan(0);
|
||||
const res = await getEdgeInstances();
|
||||
expect(res[key]).toBe(expected);
|
||||
});
|
||||
|
||||
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 last two months with different loads and at least one non-zero in series', async () => {
|
||||
const m1 = await getMonthRange(db.rawDatabase, 1);
|
||||
const m2 = await getMonthRange(db.rawDatabase, 2);
|
||||
await insertSpreadAcrossMonth(db.rawDatabase, m2.start_ts, m2.end_ts, 4, 9);
|
||||
await insertSpreadAcrossMonth(db.rawDatabase, m1.start_ts, m1.end_ts, 3, 4);
|
||||
const insertedM1 = await countRowsInWindow(
|
||||
db.rawDatabase,
|
||||
m1.start_ts,
|
||||
m1.end_ts,
|
||||
);
|
||||
const insertedM2 = await countRowsInWindow(
|
||||
db.rawDatabase,
|
||||
m2.start_ts,
|
||||
m2.end_ts,
|
||||
);
|
||||
expect(insertedM1).toBeGreaterThan(0);
|
||||
expect(insertedM2).toBeGreaterThan(0);
|
||||
const expectedM1 = await expectedForWindow(
|
||||
db.rawDatabase,
|
||||
m1.start_ts,
|
||||
m1.end_ts,
|
||||
);
|
||||
const expectedM2 = await expectedForWindow(
|
||||
db.rawDatabase,
|
||||
m2.start_ts,
|
||||
m2.end_ts,
|
||||
);
|
||||
const res = await getEdgeInstances();
|
||||
expect(res[m1.key]).toBe(expectedM1);
|
||||
expect(res[m2.key]).toBe(expectedM2);
|
||||
const nonZero = Object.values(res).filter((v) => v > 0).length;
|
||||
expect(nonZero).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
test('ignores current month data; verifies current-month insert happened but key is absent and previous month has non-zero', async () => {
|
||||
const current = await getMonthRange(db.rawDatabase, 0);
|
||||
const m1 = await getMonthRange(db.rawDatabase, 1);
|
||||
const m2 = await getMonthRange(db.rawDatabase, 2);
|
||||
await insertSpreadAcrossMonth(
|
||||
db.rawDatabase,
|
||||
current.start_ts,
|
||||
current.end_ts,
|
||||
1,
|
||||
12,
|
||||
);
|
||||
await insertSpreadAcrossMonth(
|
||||
db.rawDatabase,
|
||||
m1.start_ts,
|
||||
m1.end_ts,
|
||||
5,
|
||||
11,
|
||||
);
|
||||
const currentInserted = await countRowsInWindow(
|
||||
db.rawDatabase,
|
||||
current.start_ts,
|
||||
current.end_ts,
|
||||
);
|
||||
expect(currentInserted).toBeGreaterThan(0);
|
||||
const expectedM1 = await expectedForWindow(
|
||||
db.rawDatabase,
|
||||
m1.start_ts,
|
||||
m1.end_ts,
|
||||
);
|
||||
const expectedM2 = await expectedForWindow(
|
||||
db.rawDatabase,
|
||||
m2.start_ts,
|
||||
m2.end_ts,
|
||||
);
|
||||
const res = await getEdgeInstances();
|
||||
expect(Object.keys(res)).not.toContain(current.key);
|
||||
expect(expectedM1).toBeGreaterThan(0);
|
||||
expect(res[m1.key]).toBe(expectedM1);
|
||||
expect(res[m2.key]).toBe(expectedM2);
|
||||
});
|
||||
|
||||
@ -2,68 +2,83 @@ import type { Db } from '../../types/index.js';
|
||||
|
||||
const TABLE = 'edge_node_presence';
|
||||
|
||||
export type GetEdgeInstances = () => Promise<{
|
||||
lastMonth: number;
|
||||
monthBeforeLast: number;
|
||||
}>;
|
||||
export type EdgeInstanceUsage = Record<string, number>;
|
||||
export type GetEdgeInstances = () => Promise<EdgeInstanceUsage>;
|
||||
|
||||
export const createGetEdgeInstances =
|
||||
(db: Db): GetEdgeInstances =>
|
||||
async () => {
|
||||
const result = await db
|
||||
const rows = await db
|
||||
.with('months', (qb) =>
|
||||
qb.fromRaw('generate_series(1, 12) AS gs').select(
|
||||
db.raw(
|
||||
"(date_trunc('month', NOW()) - (gs * INTERVAL '1 month'))::timestamptz AS mon_start",
|
||||
),
|
||||
db.raw(
|
||||
"(date_trunc('month', NOW()) - ((gs - 1) * INTERVAL '1 month'))::timestamptz AS mon_end",
|
||||
),
|
||||
db.raw(
|
||||
"to_char((date_trunc('month', NOW()) - (gs * INTERVAL '1 month')),'YYYY-MM') AS key",
|
||||
),
|
||||
db.raw(`
|
||||
FLOOR(EXTRACT(EPOCH FROM (
|
||||
(date_trunc('month', NOW()) - ((gs - 1) * INTERVAL '1 month'))
|
||||
- (date_trunc('month', NOW()) - (gs * INTERVAL '1 month'))
|
||||
)) / 300)::int AS expected
|
||||
`),
|
||||
),
|
||||
)
|
||||
.with('range', (qb) =>
|
||||
qb
|
||||
.from('months')
|
||||
.select(
|
||||
db.raw('MIN(mon_start) AS min_start'),
|
||||
db.raw('MAX(mon_end) AS max_end'),
|
||||
),
|
||||
)
|
||||
.with('buckets', (qb) =>
|
||||
qb
|
||||
.from(TABLE)
|
||||
.joinRaw('CROSS JOIN range')
|
||||
.whereRaw(
|
||||
"bucket_ts >= date_trunc('month', NOW()) - INTERVAL '2 months'",
|
||||
'bucket_ts >= range.min_start AND bucket_ts < range.max_end',
|
||||
)
|
||||
.groupBy('bucket_ts')
|
||||
.select(
|
||||
db.raw('bucket_ts'),
|
||||
db.raw('COUNT(*)::int AS active_nodes'),
|
||||
db.raw(
|
||||
'COUNT(DISTINCT node_ephem_id)::int AS active_nodes',
|
||||
),
|
||||
),
|
||||
)
|
||||
.from('buckets')
|
||||
.select({
|
||||
lastMonth: db.raw(`
|
||||
.from('months as m')
|
||||
.joinRaw(
|
||||
'LEFT JOIN buckets b ON b.bucket_ts >= m.mon_start AND b.bucket_ts < m.mon_end',
|
||||
)
|
||||
.groupBy('m.key', 'm.expected')
|
||||
.orderBy('m.key', 'desc')
|
||||
.select(
|
||||
db.raw('m.key'),
|
||||
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,
|
||||
ROUND((SUM(b.active_nodes)::numeric) / NULLIF(m.expected, 0), 3),
|
||||
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();
|
||||
) AS value
|
||||
`,
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
lastMonth: Number(result?.lastMonth ?? 0),
|
||||
monthBeforeLast: Number(result?.monthBeforeLast ?? 0),
|
||||
};
|
||||
const series: EdgeInstanceUsage = {};
|
||||
for (const r of rows as Array<{ key: string; value: number }>) {
|
||||
series[r.key] = Number(r.value ?? 0);
|
||||
}
|
||||
return series;
|
||||
};
|
||||
|
||||
export const createFakeGetEdgeInstances =
|
||||
(
|
||||
edgeInstances: Awaited<ReturnType<GetEdgeInstances>> = {
|
||||
lastMonth: 0,
|
||||
monthBeforeLast: 0,
|
||||
},
|
||||
edgeInstances: Awaited<ReturnType<GetEdgeInstances>> = {},
|
||||
): GetEdgeInstances =>
|
||||
() =>
|
||||
Promise.resolve(edgeInstances);
|
||||
|
||||
@ -75,7 +75,7 @@ export interface InstanceStats {
|
||||
maxConstraintValues: number;
|
||||
releaseTemplates?: number;
|
||||
releasePlans?: number;
|
||||
edgeInstances?: Awaited<ReturnType<GetEdgeInstances>>;
|
||||
edgeInstanceUsage?: Awaited<ReturnType<GetEdgeInstances>>;
|
||||
}
|
||||
|
||||
export type InstanceStatsSigned = Omit<InstanceStats, 'projects'> & {
|
||||
@ -363,7 +363,7 @@ export class InstanceStatsService {
|
||||
maxConstraints,
|
||||
releaseTemplates,
|
||||
releasePlans,
|
||||
edgeInstances,
|
||||
edgeInstanceUsage,
|
||||
] = await Promise.all([
|
||||
this.getToggleCount(),
|
||||
this.getArchivedToggleCount(),
|
||||
@ -451,7 +451,7 @@ export class InstanceStatsService {
|
||||
maxConstraints: maxConstraints?.count ?? 0,
|
||||
releaseTemplates,
|
||||
releasePlans,
|
||||
edgeInstances,
|
||||
edgeInstanceUsage,
|
||||
};
|
||||
}
|
||||
|
||||
@ -481,7 +481,7 @@ export class InstanceStatsService {
|
||||
hostedBy,
|
||||
releaseTemplates,
|
||||
releasePlans,
|
||||
edgeInstances,
|
||||
edgeInstanceUsage,
|
||||
] = await Promise.all([
|
||||
this.getToggleCount(),
|
||||
this.getRegisteredUsers(),
|
||||
@ -545,8 +545,7 @@ export class InstanceStatsService {
|
||||
hostedBy,
|
||||
releaseTemplates,
|
||||
releasePlans,
|
||||
edgeInstancesLastMonth: edgeInstances.lastMonth,
|
||||
edgeInstancesMonthBeforeLast: edgeInstances.monthBeforeLast,
|
||||
edgeInstanceUsage,
|
||||
};
|
||||
return featureInfo;
|
||||
}
|
||||
|
||||
@ -284,25 +284,18 @@ export const instanceAdminStatsSchema = {
|
||||
example: 1,
|
||||
description: 'The number of release plans in this instance',
|
||||
},
|
||||
edgeInstances: {
|
||||
edgeInstanceUsage: {
|
||||
type: 'object',
|
||||
description:
|
||||
'The rounded up average number of edge instances in the last month and month before last',
|
||||
properties: {
|
||||
lastMonth: {
|
||||
type: 'integer',
|
||||
description:
|
||||
'The rounded up average number of edge instances in the last month',
|
||||
example: 10,
|
||||
minimum: 0,
|
||||
},
|
||||
monthBeforeLast: {
|
||||
type: 'integer',
|
||||
description:
|
||||
'The rounded up average number of edge instances in the month before last',
|
||||
example: 12,
|
||||
'The average number of edge instances, per month, in the last 12 months, rounded to 3 decimal places',
|
||||
additionalProperties: {
|
||||
type: 'number',
|
||||
minimum: 0,
|
||||
},
|
||||
example: {
|
||||
'2025-09': 2.25,
|
||||
'2025-08': 1.75,
|
||||
'2024-10': 0.45,
|
||||
},
|
||||
},
|
||||
sum: {
|
||||
|
||||
@ -133,9 +133,10 @@ class InstanceAdminController extends Controller {
|
||||
maxConstraintValues: 123,
|
||||
releaseTemplates: 3,
|
||||
releasePlans: 5,
|
||||
edgeInstances: {
|
||||
lastMonth: 10,
|
||||
monthBeforeLast: 15,
|
||||
edgeInstanceUsage: {
|
||||
'2022-06': 2.345,
|
||||
'2022-07': 2.567,
|
||||
'2022-08': 2.789,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -45,8 +45,7 @@ const fakeTelemetryData = {
|
||||
hostedBy: 'self-hosted',
|
||||
releaseTemplates: 2,
|
||||
releasePlans: 4,
|
||||
edgeInstancesLastMonth: 0,
|
||||
edgeInstancesMonthBeforeLast: 0,
|
||||
edgeInstanceUsage: {},
|
||||
};
|
||||
|
||||
test('yields current versions', async () => {
|
||||
|
||||
@ -4,6 +4,7 @@ import type { IUnleashConfig } from '../types/option.js';
|
||||
import version from '../util/version.js';
|
||||
import type { Logger } from '../logger.js';
|
||||
import type { ISettingStore } from '../types/stores/settings-store.js';
|
||||
import type { EdgeInstanceUsage } from '../features/instance-stats/getEdgeInstances.js';
|
||||
|
||||
export interface IVersionInfo {
|
||||
oss: string;
|
||||
@ -54,8 +55,7 @@ export interface IFeatureUsageInfo {
|
||||
hostedBy: string;
|
||||
releaseTemplates: number;
|
||||
releasePlans: number;
|
||||
edgeInstancesLastMonth?: number;
|
||||
edgeInstancesMonthBeforeLast?: number;
|
||||
edgeInstanceUsage?: EdgeInstanceUsage;
|
||||
}
|
||||
|
||||
export default class VersionService {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user