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 './instanceAdminStatsSchemaApiTokens.js';
|
||||||
export * from './instanceAdminStatsSchemaClientAppsItem.js';
|
export * from './instanceAdminStatsSchemaClientAppsItem.js';
|
||||||
export * from './instanceAdminStatsSchemaClientAppsItemRange.js';
|
export * from './instanceAdminStatsSchemaClientAppsItemRange.js';
|
||||||
export * from './instanceAdminStatsSchemaEdgeInstances.js';
|
export * from './instanceAdminStatsSchemaEdgeInstanceUsage.js';
|
||||||
export * from './instanceAdminStatsSchemaPreviousDayMetricsBucketsCount.js';
|
export * from './instanceAdminStatsSchemaPreviousDayMetricsBucketsCount.js';
|
||||||
export * from './instanceAdminStatsSchemaProductionChanges.js';
|
export * from './instanceAdminStatsSchemaProductionChanges.js';
|
||||||
export * from './instanceInsightsSchema.js';
|
export * from './instanceInsightsSchema.js';
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
import type { InstanceAdminStatsSchemaActiveUsers } from './instanceAdminStatsSchemaActiveUsers.js';
|
import type { InstanceAdminStatsSchemaActiveUsers } from './instanceAdminStatsSchemaActiveUsers.js';
|
||||||
import type { InstanceAdminStatsSchemaApiTokens } from './instanceAdminStatsSchemaApiTokens.js';
|
import type { InstanceAdminStatsSchemaApiTokens } from './instanceAdminStatsSchemaApiTokens.js';
|
||||||
import type { InstanceAdminStatsSchemaClientAppsItem } from './instanceAdminStatsSchemaClientAppsItem.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 { InstanceAdminStatsSchemaPreviousDayMetricsBucketsCount } from './instanceAdminStatsSchemaPreviousDayMetricsBucketsCount.js';
|
||||||
import type { InstanceAdminStatsSchemaProductionChanges } from './instanceAdminStatsSchemaProductionChanges.js';
|
import type { InstanceAdminStatsSchemaProductionChanges } from './instanceAdminStatsSchemaProductionChanges.js';
|
||||||
|
|
||||||
@ -25,8 +25,8 @@ export interface InstanceAdminStatsSchema {
|
|||||||
* @minimum 0
|
* @minimum 0
|
||||||
*/
|
*/
|
||||||
contextFields?: number;
|
contextFields?: number;
|
||||||
/** The rounded up average number of edge instances in the last month and month before last */
|
/** The average number of edge instances, per month, in the last 12 months, rounded to 3 decimal places */
|
||||||
edgeInstances?: InstanceAdminStatsSchemaEdgeInstances;
|
edgeInstanceUsage?: InstanceAdminStatsSchemaEdgeInstanceUsage;
|
||||||
/**
|
/**
|
||||||
* The number of environments defined in this instance
|
* The number of environments defined in this instance
|
||||||
* @minimum 0
|
* @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 TABLE = 'edge_node_presence';
|
||||||
|
|
||||||
const firstDayOfMonth = (d: Date) => new Date(d.getFullYear(), d.getMonth(), 1);
|
const getMonthRange = async (raw: ITestDb['rawDatabase'], offset: number) => {
|
||||||
const addMonths = (d: Date, n: number) =>
|
const { rows } = await raw.raw(
|
||||||
new Date(d.getFullYear(), d.getMonth() + n, 1);
|
`
|
||||||
|
SELECT
|
||||||
const monthWindows = () => {
|
(date_trunc('month', NOW()) - (? * INTERVAL '1 month'))::timestamptz AS start_ts,
|
||||||
const now = new Date();
|
(date_trunc('month', NOW()) - ((? - 1) * INTERVAL '1 month'))::timestamptz AS end_ts,
|
||||||
const thisMonthStart = firstDayOfMonth(now);
|
to_char((date_trunc('month', NOW()) - (? * INTERVAL '1 month')), 'YYYY-MM') AS key
|
||||||
const lastMonthStart = addMonths(thisMonthStart, -1);
|
`,
|
||||||
const monthBeforeLastStart = addMonths(thisMonthStart, -2);
|
[offset, offset, offset],
|
||||||
const lastMonthEnd = thisMonthStart;
|
);
|
||||||
const monthBeforeLastEnd = lastMonthStart;
|
return rows[0] as { start_ts: string; end_ts: string; key: string };
|
||||||
return {
|
|
||||||
monthBeforeLastStart,
|
|
||||||
monthBeforeLastEnd,
|
|
||||||
lastMonthStart,
|
|
||||||
lastMonthEnd,
|
|
||||||
thisMonthStart,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const atMidMonth = (start: Date) =>
|
const expectedForWindow = async (
|
||||||
new Date(start.getFullYear(), start.getMonth(), 15);
|
raw: ITestDb['rawDatabase'],
|
||||||
const atLateMonth = (start: Date) =>
|
startIso: string,
|
||||||
new Date(start.getFullYear(), start.getMonth(), 25);
|
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) =>
|
const countRowsInWindow = async (
|
||||||
Array.from({ length: count }, (_, i) => ({
|
raw: ITestDb['rawDatabase'],
|
||||||
bucket_ts: when,
|
startIso: string,
|
||||||
node_ephem_id: `node-${when.getTime()}-${i}`,
|
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 () => {
|
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);
|
getEdgeInstances = createGetEdgeInstances(db.rawDatabase);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -56,77 +122,99 @@ afterAll(async () => {
|
|||||||
await db.destroy();
|
await db.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns 0 for both months when no data', async () => {
|
test('returns 12 months with zeros when no data and keys match exact last-12 range', async () => {
|
||||||
await expect(getEdgeInstances()).resolves.toEqual({
|
const res = await getEdgeInstances();
|
||||||
lastMonth: 0,
|
const keys = Object.keys(res);
|
||||||
monthBeforeLast: 0,
|
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 () => {
|
test('computes average for last month and guarantees data actually inserted', async () => {
|
||||||
const { lastMonthStart } = monthWindows();
|
const { start_ts, end_ts, key } = await getMonthRange(db.rawDatabase, 1);
|
||||||
const mid = atMidMonth(lastMonthStart);
|
await insertSpreadAcrossMonth(db.rawDatabase, start_ts, end_ts, 2, 3);
|
||||||
const late = atLateMonth(lastMonthStart);
|
const inserted = await countRowsInWindow(db.rawDatabase, start_ts, end_ts);
|
||||||
await db
|
expect(inserted).toBeGreaterThan(0);
|
||||||
.rawDatabase(TABLE)
|
const expected = await expectedForWindow(db.rawDatabase, start_ts, end_ts);
|
||||||
.insert([...rowsForBucket(3, mid), ...rowsForBucket(7, late)]);
|
expect(expected).toBeGreaterThan(0);
|
||||||
await expect(getEdgeInstances()).resolves.toEqual({
|
const res = await getEdgeInstances();
|
||||||
lastMonth: 5,
|
expect(res[key]).toBe(expected);
|
||||||
monthBeforeLast: 0,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('counts only month before last', async () => {
|
test('separates last two months with different loads and at least one non-zero in series', async () => {
|
||||||
const { monthBeforeLastStart } = monthWindows();
|
const m1 = await getMonthRange(db.rawDatabase, 1);
|
||||||
const mid = atMidMonth(monthBeforeLastStart);
|
const m2 = await getMonthRange(db.rawDatabase, 2);
|
||||||
const late = atLateMonth(monthBeforeLastStart);
|
await insertSpreadAcrossMonth(db.rawDatabase, m2.start_ts, m2.end_ts, 4, 9);
|
||||||
await db
|
await insertSpreadAcrossMonth(db.rawDatabase, m1.start_ts, m1.end_ts, 3, 4);
|
||||||
.rawDatabase(TABLE)
|
const insertedM1 = await countRowsInWindow(
|
||||||
.insert([...rowsForBucket(2, mid), ...rowsForBucket(5, late)]);
|
db.rawDatabase,
|
||||||
await expect(getEdgeInstances()).resolves.toEqual({
|
m1.start_ts,
|
||||||
lastMonth: 0,
|
m1.end_ts,
|
||||||
monthBeforeLast: 4,
|
);
|
||||||
});
|
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 () => {
|
test('ignores current month data; verifies current-month insert happened but key is absent and previous month has non-zero', async () => {
|
||||||
const { monthBeforeLastStart, lastMonthStart } = monthWindows();
|
const current = await getMonthRange(db.rawDatabase, 0);
|
||||||
const pMid = atMidMonth(monthBeforeLastStart);
|
const m1 = await getMonthRange(db.rawDatabase, 1);
|
||||||
const pLate = atLateMonth(monthBeforeLastStart);
|
const m2 = await getMonthRange(db.rawDatabase, 2);
|
||||||
const lMid = atMidMonth(lastMonthStart);
|
await insertSpreadAcrossMonth(
|
||||||
const lLate = atLateMonth(lastMonthStart);
|
db.rawDatabase,
|
||||||
|
current.start_ts,
|
||||||
await db
|
current.end_ts,
|
||||||
.rawDatabase(TABLE)
|
1,
|
||||||
.insert([
|
12,
|
||||||
...rowsForBucket(4, pMid),
|
);
|
||||||
...rowsForBucket(6, pLate),
|
await insertSpreadAcrossMonth(
|
||||||
...rowsForBucket(3, lMid),
|
db.rawDatabase,
|
||||||
...rowsForBucket(7, lLate),
|
m1.start_ts,
|
||||||
]);
|
m1.end_ts,
|
||||||
|
5,
|
||||||
await expect(getEdgeInstances()).resolves.toEqual({
|
11,
|
||||||
lastMonth: 5,
|
);
|
||||||
monthBeforeLast: 5,
|
const currentInserted = await countRowsInWindow(
|
||||||
});
|
db.rawDatabase,
|
||||||
});
|
current.start_ts,
|
||||||
|
current.end_ts,
|
||||||
test('ignores current month data', async () => {
|
);
|
||||||
const { thisMonthStart, lastMonthStart } = monthWindows();
|
expect(currentInserted).toBeGreaterThan(0);
|
||||||
const lMid = atMidMonth(lastMonthStart);
|
const expectedM1 = await expectedForWindow(
|
||||||
const lLate = atLateMonth(lastMonthStart);
|
db.rawDatabase,
|
||||||
const tMid = atMidMonth(thisMonthStart);
|
m1.start_ts,
|
||||||
|
m1.end_ts,
|
||||||
await db
|
);
|
||||||
.rawDatabase(TABLE)
|
const expectedM2 = await expectedForWindow(
|
||||||
.insert([
|
db.rawDatabase,
|
||||||
...rowsForBucket(10, tMid),
|
m2.start_ts,
|
||||||
...rowsForBucket(2, lMid),
|
m2.end_ts,
|
||||||
...rowsForBucket(4, lLate),
|
);
|
||||||
]);
|
const res = await getEdgeInstances();
|
||||||
|
expect(Object.keys(res)).not.toContain(current.key);
|
||||||
await expect(getEdgeInstances()).resolves.toEqual({
|
expect(expectedM1).toBeGreaterThan(0);
|
||||||
lastMonth: 3,
|
expect(res[m1.key]).toBe(expectedM1);
|
||||||
monthBeforeLast: 0,
|
expect(res[m2.key]).toBe(expectedM2);
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,68 +2,83 @@ import type { Db } from '../../types/index.js';
|
|||||||
|
|
||||||
const TABLE = 'edge_node_presence';
|
const TABLE = 'edge_node_presence';
|
||||||
|
|
||||||
export type GetEdgeInstances = () => Promise<{
|
export type EdgeInstanceUsage = Record<string, number>;
|
||||||
lastMonth: number;
|
export type GetEdgeInstances = () => Promise<EdgeInstanceUsage>;
|
||||||
monthBeforeLast: number;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export const createGetEdgeInstances =
|
export const createGetEdgeInstances =
|
||||||
(db: Db): GetEdgeInstances =>
|
(db: Db): GetEdgeInstances =>
|
||||||
async () => {
|
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) =>
|
.with('buckets', (qb) =>
|
||||||
qb
|
qb
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
|
.joinRaw('CROSS JOIN range')
|
||||||
.whereRaw(
|
.whereRaw(
|
||||||
"bucket_ts >= date_trunc('month', NOW()) - INTERVAL '2 months'",
|
'bucket_ts >= range.min_start AND bucket_ts < range.max_end',
|
||||||
)
|
)
|
||||||
.groupBy('bucket_ts')
|
.groupBy('bucket_ts')
|
||||||
.select(
|
.select(
|
||||||
db.raw('bucket_ts'),
|
db.raw('bucket_ts'),
|
||||||
db.raw('COUNT(*)::int AS active_nodes'),
|
db.raw(
|
||||||
|
'COUNT(DISTINCT node_ephem_id)::int AS active_nodes',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.from('buckets')
|
.from('months as m')
|
||||||
.select({
|
.joinRaw(
|
||||||
lastMonth: db.raw(`
|
'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(
|
COALESCE(
|
||||||
CEIL(
|
ROUND((SUM(b.active_nodes)::numeric) / NULLIF(m.expected, 0), 3),
|
||||||
AVG(active_nodes)
|
|
||||||
FILTER (
|
|
||||||
WHERE bucket_ts >= date_trunc('month', NOW()) - INTERVAL '1 month'
|
|
||||||
AND bucket_ts < date_trunc('month', NOW())
|
|
||||||
)
|
|
||||||
)::int,
|
|
||||||
0
|
0
|
||||||
)
|
) AS value
|
||||||
`),
|
`,
|
||||||
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 {
|
const series: EdgeInstanceUsage = {};
|
||||||
lastMonth: Number(result?.lastMonth ?? 0),
|
for (const r of rows as Array<{ key: string; value: number }>) {
|
||||||
monthBeforeLast: Number(result?.monthBeforeLast ?? 0),
|
series[r.key] = Number(r.value ?? 0);
|
||||||
};
|
}
|
||||||
|
return series;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createFakeGetEdgeInstances =
|
export const createFakeGetEdgeInstances =
|
||||||
(
|
(
|
||||||
edgeInstances: Awaited<ReturnType<GetEdgeInstances>> = {
|
edgeInstances: Awaited<ReturnType<GetEdgeInstances>> = {},
|
||||||
lastMonth: 0,
|
|
||||||
monthBeforeLast: 0,
|
|
||||||
},
|
|
||||||
): GetEdgeInstances =>
|
): GetEdgeInstances =>
|
||||||
() =>
|
() =>
|
||||||
Promise.resolve(edgeInstances);
|
Promise.resolve(edgeInstances);
|
||||||
|
|||||||
@ -75,7 +75,7 @@ export interface InstanceStats {
|
|||||||
maxConstraintValues: number;
|
maxConstraintValues: number;
|
||||||
releaseTemplates?: number;
|
releaseTemplates?: number;
|
||||||
releasePlans?: number;
|
releasePlans?: number;
|
||||||
edgeInstances?: Awaited<ReturnType<GetEdgeInstances>>;
|
edgeInstanceUsage?: Awaited<ReturnType<GetEdgeInstances>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InstanceStatsSigned = Omit<InstanceStats, 'projects'> & {
|
export type InstanceStatsSigned = Omit<InstanceStats, 'projects'> & {
|
||||||
@ -363,7 +363,7 @@ export class InstanceStatsService {
|
|||||||
maxConstraints,
|
maxConstraints,
|
||||||
releaseTemplates,
|
releaseTemplates,
|
||||||
releasePlans,
|
releasePlans,
|
||||||
edgeInstances,
|
edgeInstanceUsage,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.getToggleCount(),
|
this.getToggleCount(),
|
||||||
this.getArchivedToggleCount(),
|
this.getArchivedToggleCount(),
|
||||||
@ -451,7 +451,7 @@ export class InstanceStatsService {
|
|||||||
maxConstraints: maxConstraints?.count ?? 0,
|
maxConstraints: maxConstraints?.count ?? 0,
|
||||||
releaseTemplates,
|
releaseTemplates,
|
||||||
releasePlans,
|
releasePlans,
|
||||||
edgeInstances,
|
edgeInstanceUsage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -481,7 +481,7 @@ export class InstanceStatsService {
|
|||||||
hostedBy,
|
hostedBy,
|
||||||
releaseTemplates,
|
releaseTemplates,
|
||||||
releasePlans,
|
releasePlans,
|
||||||
edgeInstances,
|
edgeInstanceUsage,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.getToggleCount(),
|
this.getToggleCount(),
|
||||||
this.getRegisteredUsers(),
|
this.getRegisteredUsers(),
|
||||||
@ -545,8 +545,7 @@ export class InstanceStatsService {
|
|||||||
hostedBy,
|
hostedBy,
|
||||||
releaseTemplates,
|
releaseTemplates,
|
||||||
releasePlans,
|
releasePlans,
|
||||||
edgeInstancesLastMonth: edgeInstances.lastMonth,
|
edgeInstanceUsage,
|
||||||
edgeInstancesMonthBeforeLast: edgeInstances.monthBeforeLast,
|
|
||||||
};
|
};
|
||||||
return featureInfo;
|
return featureInfo;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -284,25 +284,18 @@ export const instanceAdminStatsSchema = {
|
|||||||
example: 1,
|
example: 1,
|
||||||
description: 'The number of release plans in this instance',
|
description: 'The number of release plans in this instance',
|
||||||
},
|
},
|
||||||
edgeInstances: {
|
edgeInstanceUsage: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
description:
|
description:
|
||||||
'The rounded up average number of edge instances in the last month and month before last',
|
'The average number of edge instances, per month, in the last 12 months, rounded to 3 decimal places',
|
||||||
properties: {
|
additionalProperties: {
|
||||||
lastMonth: {
|
type: 'number',
|
||||||
type: 'integer',
|
minimum: 0,
|
||||||
description:
|
},
|
||||||
'The rounded up average number of edge instances in the last month',
|
example: {
|
||||||
example: 10,
|
'2025-09': 2.25,
|
||||||
minimum: 0,
|
'2025-08': 1.75,
|
||||||
},
|
'2024-10': 0.45,
|
||||||
monthBeforeLast: {
|
|
||||||
type: 'integer',
|
|
||||||
description:
|
|
||||||
'The rounded up average number of edge instances in the month before last',
|
|
||||||
example: 12,
|
|
||||||
minimum: 0,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sum: {
|
sum: {
|
||||||
|
|||||||
@ -133,9 +133,10 @@ class InstanceAdminController extends Controller {
|
|||||||
maxConstraintValues: 123,
|
maxConstraintValues: 123,
|
||||||
releaseTemplates: 3,
|
releaseTemplates: 3,
|
||||||
releasePlans: 5,
|
releasePlans: 5,
|
||||||
edgeInstances: {
|
edgeInstanceUsage: {
|
||||||
lastMonth: 10,
|
'2022-06': 2.345,
|
||||||
monthBeforeLast: 15,
|
'2022-07': 2.567,
|
||||||
|
'2022-08': 2.789,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,8 +45,7 @@ const fakeTelemetryData = {
|
|||||||
hostedBy: 'self-hosted',
|
hostedBy: 'self-hosted',
|
||||||
releaseTemplates: 2,
|
releaseTemplates: 2,
|
||||||
releasePlans: 4,
|
releasePlans: 4,
|
||||||
edgeInstancesLastMonth: 0,
|
edgeInstanceUsage: {},
|
||||||
edgeInstancesMonthBeforeLast: 0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
test('yields current versions', async () => {
|
test('yields current versions', async () => {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import type { IUnleashConfig } from '../types/option.js';
|
|||||||
import version from '../util/version.js';
|
import version from '../util/version.js';
|
||||||
import type { Logger } from '../logger.js';
|
import type { Logger } from '../logger.js';
|
||||||
import type { ISettingStore } from '../types/stores/settings-store.js';
|
import type { ISettingStore } from '../types/stores/settings-store.js';
|
||||||
|
import type { EdgeInstanceUsage } from '../features/instance-stats/getEdgeInstances.js';
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
oss: string;
|
oss: string;
|
||||||
@ -54,8 +55,7 @@ export interface IFeatureUsageInfo {
|
|||||||
hostedBy: string;
|
hostedBy: string;
|
||||||
releaseTemplates: number;
|
releaseTemplates: number;
|
||||||
releasePlans: number;
|
releasePlans: number;
|
||||||
edgeInstancesLastMonth?: number;
|
edgeInstanceUsage?: EdgeInstanceUsage;
|
||||||
edgeInstancesMonthBeforeLast?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class VersionService {
|
export default class VersionService {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user