1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: Metrics api returning daily data (#5830)

This commit is contained in:
Mateusz Kwasniewski 2024-01-11 10:39:41 +01:00 committed by GitHub
parent 0b5ac19d9a
commit ca3b4c5057
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 178 additions and 21 deletions

View File

@ -1,7 +1,7 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import Controller from '../controller'; import Controller from '../controller';
import { IUnleashConfig } from '../../types/option'; import { IUnleashConfig } from '../../types/option';
import { IUnleashServices } from '../../types'; import { IFlagResolver, IUnleashServices } from '../../types';
import { Logger } from '../../logger'; import { Logger } from '../../logger';
import ClientMetricsServiceV2 from '../../services/client-metrics/metrics-service-v2'; import ClientMetricsServiceV2 from '../../services/client-metrics/metrics-service-v2';
import { NONE } from '../../types/permissions'; import { NONE } from '../../types/permissions';
@ -33,10 +33,14 @@ class ClientMetricsController extends Controller {
private openApiService: OpenApiService; private openApiService: OpenApiService;
private flagResolver: Pick<IFlagResolver, 'isEnabled'>;
private static HOURS_BACK_MIN = 1; private static HOURS_BACK_MIN = 1;
private static HOURS_BACK_MAX = 48; private static HOURS_BACK_MAX = 48;
private static HOURS_BACK_MAX_V2 = 24 * 31 * 3; // 3 months
constructor( constructor(
config: IUnleashConfig, config: IUnleashConfig,
{ {
@ -49,6 +53,7 @@ class ClientMetricsController extends Controller {
this.metrics = clientMetricsServiceV2; this.metrics = clientMetricsServiceV2;
this.openApiService = openApiService; this.openApiService = openApiService;
this.flagResolver = config.flagResolver;
this.route({ this.route({
method: 'get', method: 'get',
@ -130,11 +135,11 @@ class ClientMetricsController extends Controller {
} }
const parsed = Number(param); const parsed = Number(param);
const max = this.flagResolver.isEnabled('extendedUsageMetrics')
? ClientMetricsController.HOURS_BACK_MAX_V2
: ClientMetricsController.HOURS_BACK_MAX;
if ( if (parsed >= ClientMetricsController.HOURS_BACK_MIN && parsed <= max) {
parsed >= ClientMetricsController.HOURS_BACK_MIN &&
parsed <= ClientMetricsController.HOURS_BACK_MAX
) {
return parsed; return parsed;
} }
} }

View File

@ -5,7 +5,9 @@ import getLogger from '../../../test/fixtures/no-logger';
import createStores from '../../../test/fixtures/store'; import createStores from '../../../test/fixtures/store';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { LastSeenService } from './last-seen/last-seen-service'; import { LastSeenService } from './last-seen/last-seen-service';
import { IUnleashConfig } from 'lib/types'; import { IClientMetricsStoreV2, IUnleashConfig } from 'lib/types';
import { endOfDay, startOfHour, subDays, subHours } from 'date-fns';
import { IClientMetricsEnv } from '../../types/stores/client-metrics-store-v2';
function initClientMetrics(flagEnabled = true) { function initClientMetrics(flagEnabled = true) {
const stores = createStores(); const stores = createStores();
@ -120,3 +122,115 @@ test('process metrics properly even when some names are not url friendly, with d
expect(eventBus.emit).toHaveBeenCalledTimes(1); expect(eventBus.emit).toHaveBeenCalledTimes(1);
expect(lastSeenService.updateLastSeen).toHaveBeenCalledTimes(1); expect(lastSeenService.updateLastSeen).toHaveBeenCalledTimes(1);
}); });
test('get daily client metrics for a toggle', async () => {
const yesterday = subDays(new Date(), 1);
const twoDaysAgo = subDays(new Date(), 2);
const threeDaysAgo = subDays(new Date(), 3);
const baseData = {
featureName: 'feature',
appName: 'test',
environment: 'development',
yes: 0,
no: 0,
};
const clientMetricsStoreV2 = {
getMetricsForFeatureToggleV2(
featureName: string,
hoursBack?: number,
): Promise<IClientMetricsEnv[]> {
return Promise.resolve([
{
...baseData,
timestamp: endOfDay(yesterday),
yes: 2,
no: 1,
variants: { a: 1, b: 1 },
},
]);
},
} as IClientMetricsStoreV2;
const config = {
flagResolver: {
isEnabled() {
return true;
},
},
getLogger() {},
} as unknown as IUnleashConfig;
const lastSeenService = {} as LastSeenService;
const service = new ClientMetricsServiceV2(
{ clientMetricsStoreV2 },
config,
lastSeenService,
);
const metrics = await service.getClientMetricsForToggle('feature', 3 * 24);
expect(metrics).toMatchObject([
{ ...baseData, timestamp: endOfDay(threeDaysAgo) },
{ ...baseData, timestamp: endOfDay(twoDaysAgo) },
{
...baseData,
timestamp: endOfDay(yesterday),
yes: 2,
no: 1,
variants: { a: 1, b: 1 },
},
]);
});
test('get hourly client metrics for a toggle', async () => {
const hourAgo = startOfHour(subHours(new Date(), 1));
const thisHour = startOfHour(new Date());
const baseData = {
featureName: 'feature',
appName: 'test',
environment: 'development',
yes: 0,
no: 0,
};
const clientMetricsStoreV2 = {
getMetricsForFeatureToggleV2(
featureName: string,
hoursBack?: number,
): Promise<IClientMetricsEnv[]> {
return Promise.resolve([
{
...baseData,
timestamp: thisHour,
yes: 2,
no: 1,
variants: { a: 1, b: 1 },
},
]);
},
} as IClientMetricsStoreV2;
const config = {
flagResolver: {
isEnabled() {
return true;
},
},
getLogger() {},
} as unknown as IUnleashConfig;
const lastSeenService = {} as LastSeenService;
const service = new ClientMetricsServiceV2(
{ clientMetricsStoreV2 },
config,
lastSeenService,
);
const metrics = await service.getClientMetricsForToggle('feature', 2);
expect(metrics).toMatchObject([
{ ...baseData, timestamp: hourAgo },
{
...baseData,
timestamp: thisHour,
yes: 2,
no: 1,
variants: { a: 1, b: 1 },
},
]);
});

View File

@ -14,7 +14,11 @@ import { ALL } from '../../types/models/api-token';
import { IUser } from '../../types/user'; import { IUser } from '../../types/user';
import { collapseHourlyMetrics } from '../../util/collapseHourlyMetrics'; import { collapseHourlyMetrics } from '../../util/collapseHourlyMetrics';
import { LastSeenService } from './last-seen/last-seen-service'; import { LastSeenService } from './last-seen/last-seen-service';
import { generateHourBuckets } from '../../util/time-utils'; import {
generateDayBuckets,
generateHourBuckets,
HourBucket,
} from '../../util/time-utils';
import { ClientMetricsSchema } from 'lib/openapi'; import { ClientMetricsSchema } from 'lib/openapi';
import { nameSchema } from '../../schema/feature-schema'; import { nameSchema } from '../../schema/feature-schema';
@ -183,17 +187,26 @@ export default class ClientMetricsServiceV2 {
featureName: string, featureName: string,
hoursBack: number = 24, hoursBack: number = 24,
): Promise<IClientMetricsEnv[]> { ): Promise<IClientMetricsEnv[]> {
const metrics = this.flagResolver.isEnabled('extendedUsageMetrics') let hours: HourBucket[];
? await this.clientMetricsStoreV2.getMetricsForFeatureToggleV2( let metrics: IClientMetricsEnv[];
featureName, if (this.flagResolver.isEnabled('extendedUsageMetrics')) {
hoursBack, metrics =
) await this.clientMetricsStoreV2.getMetricsForFeatureToggleV2(
: await this.clientMetricsStoreV2.getMetricsForFeatureToggle(
featureName, featureName,
hoursBack, hoursBack,
); );
hours =
const hours = generateHourBuckets(hoursBack); hoursBack > 48
? generateDayBuckets(Math.floor(hoursBack / 24))
: generateHourBuckets(hoursBack);
} else {
metrics =
await this.clientMetricsStoreV2.getMetricsForFeatureToggle(
featureName,
hoursBack,
);
hours = generateHourBuckets(hoursBack);
}
const environments = [...new Set(metrics.map((x) => x.environment))]; const environments = [...new Set(metrics.map((x) => x.environment))];

View File

@ -1,7 +1,16 @@
import { generateHourBuckets } from './time-utils'; import { generateDayBuckets, generateHourBuckets } from './time-utils';
import { endOfDay, subDays } from 'date-fns';
test('generateHourBuckets', () => { test('generateHourBuckets', () => {
const result = generateHourBuckets(24); const result = generateHourBuckets(24);
expect(result).toHaveLength(24); expect(result).toHaveLength(24);
}); });
test('generateDayBuckets', () => {
const result = generateDayBuckets(7);
const endOfDayYesterday = endOfDay(subDays(new Date(), 1));
expect(result).toHaveLength(7);
expect(result[0]).toMatchObject({ timestamp: endOfDayYesterday });
});

View File

@ -1,4 +1,4 @@
import { startOfHour, subHours } from 'date-fns'; import { endOfDay, startOfHour, subDays, subHours } from 'date-fns';
export interface HourBucket { export interface HourBucket {
timestamp: Date; timestamp: Date;
@ -14,3 +14,15 @@ export function generateHourBuckets(hours: number): HourBucket[] {
} }
return result; return result;
} }
// Generate last x days starting from end of yesterday
export function generateDayBuckets(days: number): HourBucket[] {
const start = endOfDay(subDays(new Date(), 1));
const result = [];
for (let i = 0; i < days; i++) {
result.push({ timestamp: subDays(start, i) });
}
return result;
}

View File

@ -25,6 +25,7 @@ beforeAll(async () => {
experimental: { experimental: {
flags: { flags: {
strictSchemaValidation: true, strictSchemaValidation: true,
extendedUsageMetrics: true,
}, },
}, },
}, },
@ -143,18 +144,21 @@ test('should support the hoursBack query param for raw metrics', async () => {
]; ];
await db.stores.clientMetricsStoreV2.batchInsertMetrics(metrics); await db.stores.clientMetricsStoreV2.batchInsertMetrics(metrics);
await db.stores.clientMetricsStoreV2.aggregateDailyMetrics();
const hours1 = await fetchHoursBack(1); const hours1 = await fetchHoursBack(1);
const hours24 = await fetchHoursBack(24); const hours24 = await fetchHoursBack(24);
const hours48 = await fetchHoursBack(48); const hours48 = await fetchHoursBack(48);
const hoursTooFew = await fetchHoursBack(-999); const hoursTooFew = await fetchHoursBack(-999);
const hoursTooMany = await fetchHoursBack(999); const hoursTooMany = await fetchHoursBack(24 * 31 * 3 + 1); // 3 months + 1 hour
const days = await fetchHoursBack(48 + 1); // switch to days after 48 hours
expect(hours1.data).toHaveLength(1); expect(hours1.data).toHaveLength(1);
expect(hours24.data).toHaveLength(24); expect(hours24.data).toHaveLength(24);
expect(hours48.data).toHaveLength(48); expect(hours48.data).toHaveLength(48);
expect(hoursTooFew.data).toHaveLength(24); expect(hoursTooFew.data).toHaveLength(24);
expect(hoursTooMany.data).toHaveLength(24); expect(hoursTooMany.data).toHaveLength(24);
expect(days.data).toHaveLength(2); // two days of data
}); });
test('should return toggle summary', async () => { test('should return toggle summary', async () => {