mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-04 01:18:20 +02:00
feat: metrics calculation limit (#5853)
This commit is contained in:
parent
8ae267ea25
commit
8eb5a53ad9
@ -186,3 +186,37 @@ test('clear daily metrics', async () => {
|
|||||||
.select('*');
|
.select('*');
|
||||||
expect(variantResults.length).toBe(2);
|
expect(variantResults.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('count previous day metrics', async () => {
|
||||||
|
const yesterday = subDays(new Date(), 1);
|
||||||
|
await clientMetricsStore.batchInsertMetrics([
|
||||||
|
{
|
||||||
|
appName: 'test',
|
||||||
|
featureName: 'feature',
|
||||||
|
environment: 'development',
|
||||||
|
timestamp: setHours(yesterday, 10),
|
||||||
|
no: 0,
|
||||||
|
yes: 1,
|
||||||
|
variants: {
|
||||||
|
a: 1,
|
||||||
|
b: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
appName: 'test',
|
||||||
|
featureName: 'feature',
|
||||||
|
environment: 'development',
|
||||||
|
timestamp: setHours(yesterday, 11),
|
||||||
|
no: 1,
|
||||||
|
yes: 1,
|
||||||
|
variants: {
|
||||||
|
a: 0,
|
||||||
|
b: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await clientMetricsStore.countPreviousDayMetrics();
|
||||||
|
|
||||||
|
expect(result).toMatchObject({ enabledCount: 2, variantCount: 4 });
|
||||||
|
});
|
||||||
|
@ -31,9 +31,9 @@ interface ClientMetricsEnvVariantTable extends ClientMetricsBaseTable {
|
|||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TABLE = 'client_metrics_env';
|
const HOURLY_TABLE = 'client_metrics_env';
|
||||||
const DAILY_TABLE = 'client_metrics_env_daily';
|
const DAILY_TABLE = 'client_metrics_env_daily';
|
||||||
const TABLE_VARIANTS = 'client_metrics_env_variants';
|
const HOURLY_TABLE_VARIANTS = 'client_metrics_env_variants';
|
||||||
const DAILY_TABLE_VARIANTS = 'client_metrics_env_variants_daily';
|
const DAILY_TABLE_VARIANTS = 'client_metrics_env_variants_daily';
|
||||||
|
|
||||||
const fromRow = (row: ClientMetricsEnvTable) => ({
|
const fromRow = (row: ClientMetricsEnvTable) => ({
|
||||||
@ -142,7 +142,7 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async get(key: IClientMetricsEnvKey): Promise<IClientMetricsEnv> {
|
async get(key: IClientMetricsEnvKey): Promise<IClientMetricsEnv> {
|
||||||
const row = await this.db<ClientMetricsEnvTable>(TABLE)
|
const row = await this.db<ClientMetricsEnvTable>(HOURLY_TABLE)
|
||||||
.where({
|
.where({
|
||||||
feature_name: key.featureName,
|
feature_name: key.featureName,
|
||||||
app_name: key.appName,
|
app_name: key.appName,
|
||||||
@ -157,7 +157,7 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAll(query: Object = {}): Promise<IClientMetricsEnv[]> {
|
async getAll(query: Object = {}): Promise<IClientMetricsEnv[]> {
|
||||||
const rows = await this.db<ClientMetricsEnvTable>(TABLE)
|
const rows = await this.db<ClientMetricsEnvTable>(HOURLY_TABLE)
|
||||||
.select('*')
|
.select('*')
|
||||||
.where(query);
|
.where(query);
|
||||||
return rows.map(fromRow);
|
return rows.map(fromRow);
|
||||||
@ -173,7 +173,7 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(key: IClientMetricsEnvKey): Promise<void> {
|
async delete(key: IClientMetricsEnvKey): Promise<void> {
|
||||||
return this.db<ClientMetricsEnvTable>(TABLE)
|
return this.db<ClientMetricsEnvTable>(HOURLY_TABLE)
|
||||||
.where({
|
.where({
|
||||||
feature_name: key.featureName,
|
feature_name: key.featureName,
|
||||||
app_name: key.appName,
|
app_name: key.appName,
|
||||||
@ -184,7 +184,7 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteAll(): Promise<void> {
|
deleteAll(): Promise<void> {
|
||||||
return this.db(TABLE).del();
|
return this.db(HOURLY_TABLE).del();
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
@ -207,7 +207,7 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Consider rewriting to SQL batch!
|
// Consider rewriting to SQL batch!
|
||||||
const insert = this.db<ClientMetricsEnvTable>(TABLE)
|
const insert = this.db<ClientMetricsEnvTable>(HOURLY_TABLE)
|
||||||
.insert(sortedRows)
|
.insert(sortedRows)
|
||||||
.toQuery();
|
.toQuery();
|
||||||
const query = `${insert.toString()} ON CONFLICT (feature_name, app_name, environment, timestamp) DO UPDATE SET "yes" = "client_metrics_env"."yes" + EXCLUDED.yes, "no" = "client_metrics_env"."no" + EXCLUDED.no`;
|
const query = `${insert.toString()} ON CONFLICT (feature_name, app_name, environment, timestamp) DO UPDATE SET "yes" = "client_metrics_env"."yes" + EXCLUDED.yes, "no" = "client_metrics_env"."no" + EXCLUDED.no`;
|
||||||
@ -226,7 +226,7 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
|
|||||||
|
|
||||||
if (sortedVariantRows.length > 0) {
|
if (sortedVariantRows.length > 0) {
|
||||||
const insertVariants = this.db<ClientMetricsEnvVariantTable>(
|
const insertVariants = this.db<ClientMetricsEnvVariantTable>(
|
||||||
TABLE_VARIANTS,
|
HOURLY_TABLE_VARIANTS,
|
||||||
)
|
)
|
||||||
.insert(sortedVariantRows)
|
.insert(sortedVariantRows)
|
||||||
.toQuery();
|
.toQuery();
|
||||||
@ -239,20 +239,29 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
|
|||||||
featureName: string,
|
featureName: string,
|
||||||
hoursBack: number = 24,
|
hoursBack: number = 24,
|
||||||
): Promise<IClientMetricsEnv[]> {
|
): Promise<IClientMetricsEnv[]> {
|
||||||
const rows = await this.db<ClientMetricsEnvTable>(TABLE)
|
const rows = await this.db<ClientMetricsEnvTable>(HOURLY_TABLE)
|
||||||
.select([`${TABLE}.*`, 'variant', 'count'])
|
.select([`${HOURLY_TABLE}.*`, 'variant', 'count'])
|
||||||
.leftJoin(TABLE_VARIANTS, function () {
|
.leftJoin(HOURLY_TABLE_VARIANTS, function () {
|
||||||
this.on(
|
this.on(
|
||||||
`${TABLE_VARIANTS}.feature_name`,
|
`${HOURLY_TABLE_VARIANTS}.feature_name`,
|
||||||
`${TABLE}.feature_name`,
|
`${HOURLY_TABLE}.feature_name`,
|
||||||
)
|
)
|
||||||
.on(`${TABLE_VARIANTS}.app_name`, `${TABLE}.app_name`)
|
.on(
|
||||||
.on(`${TABLE_VARIANTS}.environment`, `${TABLE}.environment`)
|
`${HOURLY_TABLE_VARIANTS}.app_name`,
|
||||||
.on(`${TABLE_VARIANTS}.timestamp`, `${TABLE}.timestamp`);
|
`${HOURLY_TABLE}.app_name`,
|
||||||
|
)
|
||||||
|
.on(
|
||||||
|
`${HOURLY_TABLE_VARIANTS}.environment`,
|
||||||
|
`${HOURLY_TABLE}.environment`,
|
||||||
|
)
|
||||||
|
.on(
|
||||||
|
`${HOURLY_TABLE_VARIANTS}.timestamp`,
|
||||||
|
`${HOURLY_TABLE}.timestamp`,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.where(`${TABLE}.feature_name`, featureName)
|
.where(`${HOURLY_TABLE}.feature_name`, featureName)
|
||||||
.andWhereRaw(
|
.andWhereRaw(
|
||||||
`${TABLE}.timestamp >= NOW() - INTERVAL '${hoursBack} hours'`,
|
`${HOURLY_TABLE}.timestamp >= NOW() - INTERVAL '${hoursBack} hours'`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const tokens = rows.reduce(variantRowReducer, {});
|
const tokens = rows.reduce(variantRowReducer, {});
|
||||||
@ -263,9 +272,9 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
|
|||||||
featureName: string,
|
featureName: string,
|
||||||
hoursBack: number = 24,
|
hoursBack: number = 24,
|
||||||
): Promise<IClientMetricsEnv[]> {
|
): Promise<IClientMetricsEnv[]> {
|
||||||
const mainTable = hoursBack <= 48 ? TABLE : DAILY_TABLE;
|
const mainTable = hoursBack <= 48 ? HOURLY_TABLE : DAILY_TABLE;
|
||||||
const variantsTable =
|
const variantsTable =
|
||||||
hoursBack <= 48 ? TABLE_VARIANTS : DAILY_TABLE_VARIANTS;
|
hoursBack <= 48 ? HOURLY_TABLE_VARIANTS : DAILY_TABLE_VARIANTS;
|
||||||
const dateTime = hoursBack <= 48 ? 'timestamp' : 'date';
|
const dateTime = hoursBack <= 48 ? 'timestamp' : 'date';
|
||||||
|
|
||||||
const rows = await this.db<ClientMetricsEnvTable>(mainTable)
|
const rows = await this.db<ClientMetricsEnvTable>(mainTable)
|
||||||
@ -298,7 +307,7 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
|
|||||||
featureName: string,
|
featureName: string,
|
||||||
hoursBack: number = 24,
|
hoursBack: number = 24,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
return this.db<ClientMetricsEnvTable>(TABLE)
|
return this.db<ClientMetricsEnvTable>(HOURLY_TABLE)
|
||||||
.distinct()
|
.distinct()
|
||||||
.where({ feature_name: featureName })
|
.where({ feature_name: featureName })
|
||||||
.andWhereRaw(`timestamp >= NOW() - INTERVAL '${hoursBack} hours'`)
|
.andWhereRaw(`timestamp >= NOW() - INTERVAL '${hoursBack} hours'`)
|
||||||
@ -310,7 +319,7 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
|
|||||||
appName: string,
|
appName: string,
|
||||||
hoursBack: number = 24,
|
hoursBack: number = 24,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
return this.db<ClientMetricsEnvTable>(TABLE)
|
return this.db<ClientMetricsEnvTable>(HOURLY_TABLE)
|
||||||
.distinct()
|
.distinct()
|
||||||
.where({ app_name: appName })
|
.where({ app_name: appName })
|
||||||
.andWhereRaw(`timestamp >= NOW() - INTERVAL '${hoursBack} hours'`)
|
.andWhereRaw(`timestamp >= NOW() - INTERVAL '${hoursBack} hours'`)
|
||||||
@ -319,7 +328,7 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async clearMetrics(hoursAgo: number): Promise<void> {
|
async clearMetrics(hoursAgo: number): Promise<void> {
|
||||||
return this.db<ClientMetricsEnvTable>(TABLE)
|
return this.db<ClientMetricsEnvTable>(HOURLY_TABLE)
|
||||||
.whereRaw(`timestamp <= NOW() - INTERVAL '${hoursAgo} hours'`)
|
.whereRaw(`timestamp <= NOW() - INTERVAL '${hoursAgo} hours'`)
|
||||||
.del();
|
.del();
|
||||||
}
|
}
|
||||||
@ -330,6 +339,30 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
|
|||||||
.del();
|
.del();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async countPreviousDayMetrics(): Promise<{
|
||||||
|
enabledCount: number;
|
||||||
|
variantCount: number;
|
||||||
|
}> {
|
||||||
|
const enabledCountQuery = this.db(HOURLY_TABLE)
|
||||||
|
.whereRaw("timestamp >= CURRENT_DATE - INTERVAL '1 day'")
|
||||||
|
.andWhereRaw('timestamp < CURRENT_DATE')
|
||||||
|
.count()
|
||||||
|
.first();
|
||||||
|
const variantCountQuery = this.db(HOURLY_TABLE_VARIANTS)
|
||||||
|
.whereRaw("timestamp >= CURRENT_DATE - INTERVAL '1 day'")
|
||||||
|
.andWhereRaw('timestamp < CURRENT_DATE')
|
||||||
|
.count()
|
||||||
|
.first();
|
||||||
|
const [enabledCount, variantCount] = await Promise.all([
|
||||||
|
enabledCountQuery,
|
||||||
|
variantCountQuery,
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
enabledCount: Number(enabledCount?.count || 0),
|
||||||
|
variantCount: Number(variantCount?.count || 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// aggregates all hourly metrics from a previous day into daily metrics
|
// aggregates all hourly metrics from a previous day into daily metrics
|
||||||
async aggregateDailyMetrics(): Promise<void> {
|
async aggregateDailyMetrics(): Promise<void> {
|
||||||
const rawQuery: string = `
|
const rawQuery: string = `
|
||||||
@ -342,7 +375,7 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
|
|||||||
SUM(yes) as yes,
|
SUM(yes) as yes,
|
||||||
SUM(no) as no
|
SUM(no) as no
|
||||||
FROM
|
FROM
|
||||||
${TABLE}
|
${HOURLY_TABLE}
|
||||||
WHERE
|
WHERE
|
||||||
timestamp >= CURRENT_DATE - INTERVAL '1 day'
|
timestamp >= CURRENT_DATE - INTERVAL '1 day'
|
||||||
AND timestamp < CURRENT_DATE
|
AND timestamp < CURRENT_DATE
|
||||||
@ -361,7 +394,7 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
|
|||||||
variant,
|
variant,
|
||||||
SUM(count) as count
|
SUM(count) as count
|
||||||
FROM
|
FROM
|
||||||
${TABLE_VARIANTS}
|
${HOURLY_TABLE_VARIANTS}
|
||||||
WHERE
|
WHERE
|
||||||
timestamp >= CURRENT_DATE - INTERVAL '1 day'
|
timestamp >= CURRENT_DATE - INTERVAL '1 day'
|
||||||
AND timestamp < CURRENT_DATE
|
AND timestamp < CURRENT_DATE
|
||||||
|
@ -234,3 +234,57 @@ test('get hourly client metrics for a toggle', async () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('aggregate previous day metrics when metrics count is below limit', async () => {
|
||||||
|
const enabledCount = 2;
|
||||||
|
const variantCount = 4;
|
||||||
|
let limit = 5;
|
||||||
|
let aggregationCalled = false;
|
||||||
|
let recordedWarning = '';
|
||||||
|
const clientMetricsStoreV2 = {
|
||||||
|
aggregateDailyMetrics() {
|
||||||
|
aggregationCalled = true;
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
countPreviousDayMetrics() {
|
||||||
|
return { enabledCount, variantCount };
|
||||||
|
},
|
||||||
|
} as unknown as IClientMetricsStoreV2;
|
||||||
|
const config = {
|
||||||
|
flagResolver: {
|
||||||
|
isEnabled() {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
getVariant() {
|
||||||
|
return { payload: { value: limit } };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getLogger() {
|
||||||
|
return {
|
||||||
|
warn(message: string) {
|
||||||
|
recordedWarning = message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
} as unknown as IUnleashConfig;
|
||||||
|
const lastSeenService = {} as LastSeenService;
|
||||||
|
const service = new ClientMetricsServiceV2(
|
||||||
|
{ clientMetricsStoreV2 },
|
||||||
|
config,
|
||||||
|
lastSeenService,
|
||||||
|
);
|
||||||
|
|
||||||
|
await service.aggregateDailyMetrics();
|
||||||
|
|
||||||
|
expect(recordedWarning).toBe(
|
||||||
|
'Skipping previous day metrics aggregation. Too many results. Expected max value: 5, Actual value: 6',
|
||||||
|
);
|
||||||
|
expect(aggregationCalled).toBe(false);
|
||||||
|
|
||||||
|
recordedWarning = '';
|
||||||
|
limit = 6;
|
||||||
|
await service.aggregateDailyMetrics();
|
||||||
|
|
||||||
|
expect(recordedWarning).toBe('');
|
||||||
|
expect(aggregationCalled).toBe(true);
|
||||||
|
});
|
||||||
|
@ -31,7 +31,7 @@ export default class ClientMetricsServiceV2 {
|
|||||||
|
|
||||||
private lastSeenService: LastSeenService;
|
private lastSeenService: LastSeenService;
|
||||||
|
|
||||||
private flagResolver: Pick<IFlagResolver, 'isEnabled'>;
|
private flagResolver: Pick<IFlagResolver, 'isEnabled' | 'getVariant'>;
|
||||||
|
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
@ -61,7 +61,26 @@ export default class ClientMetricsServiceV2 {
|
|||||||
|
|
||||||
async aggregateDailyMetrics() {
|
async aggregateDailyMetrics() {
|
||||||
if (this.flagResolver.isEnabled('extendedUsageMetrics')) {
|
if (this.flagResolver.isEnabled('extendedUsageMetrics')) {
|
||||||
await this.clientMetricsStoreV2.aggregateDailyMetrics();
|
const { enabledCount, variantCount } =
|
||||||
|
await this.clientMetricsStoreV2.countPreviousDayMetrics();
|
||||||
|
const { payload } = this.flagResolver.getVariant(
|
||||||
|
'extendedUsageMetrics',
|
||||||
|
);
|
||||||
|
|
||||||
|
const limit =
|
||||||
|
payload?.value && Number.isInteger(parseInt(payload?.value))
|
||||||
|
? parseInt(payload?.value)
|
||||||
|
: 3600000;
|
||||||
|
|
||||||
|
const totalCount = enabledCount + variantCount;
|
||||||
|
|
||||||
|
if (totalCount <= limit) {
|
||||||
|
await this.clientMetricsStoreV2.aggregateDailyMetrics();
|
||||||
|
} else {
|
||||||
|
this.logger.warn(
|
||||||
|
`Skipping previous day metrics aggregation. Too many results. Expected max value: ${limit}, Actual value: ${totalCount}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,5 +39,9 @@ export interface IClientMetricsStoreV2
|
|||||||
): Promise<string[]>;
|
): Promise<string[]>;
|
||||||
clearMetrics(hoursAgo: number): Promise<void>;
|
clearMetrics(hoursAgo: number): Promise<void>;
|
||||||
clearDailyMetrics(daysAgo: number): Promise<void>;
|
clearDailyMetrics(daysAgo: number): Promise<void>;
|
||||||
|
countPreviousDayMetrics(): Promise<{
|
||||||
|
enabledCount: number;
|
||||||
|
variantCount: number;
|
||||||
|
}>;
|
||||||
aggregateDailyMetrics(): Promise<void>;
|
aggregateDailyMetrics(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,12 @@ export default class FakeClientMetricsStoreV2
|
|||||||
clearDailyMetrics(daysBack: number): Promise<void> {
|
clearDailyMetrics(daysBack: number): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
countPreviousDayMetrics(): Promise<{
|
||||||
|
enabledCount: number;
|
||||||
|
variantCount: number;
|
||||||
|
}> {
|
||||||
|
return Promise.resolve({ enabledCount: 0, variantCount: 0 });
|
||||||
|
}
|
||||||
aggregateDailyMetrics(): Promise<void> {
|
aggregateDailyMetrics(): Promise<void> {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user