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:
parent
0b5ac19d9a
commit
ca3b4c5057
@ -1,7 +1,7 @@
|
||||
import { Request, Response } from 'express';
|
||||
import Controller from '../controller';
|
||||
import { IUnleashConfig } from '../../types/option';
|
||||
import { IUnleashServices } from '../../types';
|
||||
import { IFlagResolver, IUnleashServices } from '../../types';
|
||||
import { Logger } from '../../logger';
|
||||
import ClientMetricsServiceV2 from '../../services/client-metrics/metrics-service-v2';
|
||||
import { NONE } from '../../types/permissions';
|
||||
@ -33,10 +33,14 @@ class ClientMetricsController extends Controller {
|
||||
|
||||
private openApiService: OpenApiService;
|
||||
|
||||
private flagResolver: Pick<IFlagResolver, 'isEnabled'>;
|
||||
|
||||
private static HOURS_BACK_MIN = 1;
|
||||
|
||||
private static HOURS_BACK_MAX = 48;
|
||||
|
||||
private static HOURS_BACK_MAX_V2 = 24 * 31 * 3; // 3 months
|
||||
|
||||
constructor(
|
||||
config: IUnleashConfig,
|
||||
{
|
||||
@ -49,6 +53,7 @@ class ClientMetricsController extends Controller {
|
||||
|
||||
this.metrics = clientMetricsServiceV2;
|
||||
this.openApiService = openApiService;
|
||||
this.flagResolver = config.flagResolver;
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
@ -130,11 +135,11 @@ class ClientMetricsController extends Controller {
|
||||
}
|
||||
|
||||
const parsed = Number(param);
|
||||
const max = this.flagResolver.isEnabled('extendedUsageMetrics')
|
||||
? ClientMetricsController.HOURS_BACK_MAX_V2
|
||||
: ClientMetricsController.HOURS_BACK_MAX;
|
||||
|
||||
if (
|
||||
parsed >= ClientMetricsController.HOURS_BACK_MIN &&
|
||||
parsed <= ClientMetricsController.HOURS_BACK_MAX
|
||||
) {
|
||||
if (parsed >= ClientMetricsController.HOURS_BACK_MIN && parsed <= max) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ import getLogger from '../../../test/fixtures/no-logger';
|
||||
import createStores from '../../../test/fixtures/store';
|
||||
import EventEmitter from 'events';
|
||||
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) {
|
||||
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(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 },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -14,7 +14,11 @@ import { ALL } from '../../types/models/api-token';
|
||||
import { IUser } from '../../types/user';
|
||||
import { collapseHourlyMetrics } from '../../util/collapseHourlyMetrics';
|
||||
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 { nameSchema } from '../../schema/feature-schema';
|
||||
|
||||
@ -183,17 +187,26 @@ export default class ClientMetricsServiceV2 {
|
||||
featureName: string,
|
||||
hoursBack: number = 24,
|
||||
): Promise<IClientMetricsEnv[]> {
|
||||
const metrics = this.flagResolver.isEnabled('extendedUsageMetrics')
|
||||
? await this.clientMetricsStoreV2.getMetricsForFeatureToggleV2(
|
||||
featureName,
|
||||
hoursBack,
|
||||
)
|
||||
: await this.clientMetricsStoreV2.getMetricsForFeatureToggle(
|
||||
featureName,
|
||||
hoursBack,
|
||||
);
|
||||
|
||||
const hours = generateHourBuckets(hoursBack);
|
||||
let hours: HourBucket[];
|
||||
let metrics: IClientMetricsEnv[];
|
||||
if (this.flagResolver.isEnabled('extendedUsageMetrics')) {
|
||||
metrics =
|
||||
await this.clientMetricsStoreV2.getMetricsForFeatureToggleV2(
|
||||
featureName,
|
||||
hoursBack,
|
||||
);
|
||||
hours =
|
||||
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))];
|
||||
|
||||
|
@ -1,7 +1,16 @@
|
||||
import { generateHourBuckets } from './time-utils';
|
||||
import { generateDayBuckets, generateHourBuckets } from './time-utils';
|
||||
import { endOfDay, subDays } from 'date-fns';
|
||||
|
||||
test('generateHourBuckets', () => {
|
||||
const result = generateHourBuckets(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 });
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { startOfHour, subHours } from 'date-fns';
|
||||
import { endOfDay, startOfHour, subDays, subHours } from 'date-fns';
|
||||
|
||||
export interface HourBucket {
|
||||
timestamp: Date;
|
||||
@ -14,3 +14,15 @@ export function generateHourBuckets(hours: number): HourBucket[] {
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ beforeAll(async () => {
|
||||
experimental: {
|
||||
flags: {
|
||||
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.aggregateDailyMetrics();
|
||||
|
||||
const hours1 = await fetchHoursBack(1);
|
||||
const hours24 = await fetchHoursBack(24);
|
||||
const hours48 = await fetchHoursBack(48);
|
||||
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(hours24.data).toHaveLength(24);
|
||||
expect(hours48.data).toHaveLength(48);
|
||||
expect(hoursTooFew.data).toHaveLength(24);
|
||||
expect(hoursTooMany.data).toHaveLength(24);
|
||||
expect(days.data).toHaveLength(2); // two days of data
|
||||
});
|
||||
|
||||
test('should return toggle summary', async () => {
|
||||
|
Loading…
Reference in New Issue
Block a user