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

fix: generate all hour buckets if missing (#2319)

This commit is contained in:
Ivar Conradi Østhus 2022-11-04 09:30:02 +01:00 committed by GitHub
parent 46076fcbc8
commit 2d2d6f268a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 105 additions and 22 deletions

View File

@ -73,6 +73,7 @@ exports[`should create default config 1`] = `
"cloneEnvironment": false, "cloneEnvironment": false,
"embedProxy": false, "embedProxy": false,
"embedProxyFrontend": false, "embedProxyFrontend": false,
"fixHourMetrics": false,
"publicSignup": false, "publicSignup": false,
"responseTimeWithAppName": false, "responseTimeWithAppName": false,
"syncSSOGroups": false, "syncSSOGroups": false,
@ -87,6 +88,7 @@ exports[`should create default config 1`] = `
"cloneEnvironment": false, "cloneEnvironment": false,
"embedProxy": false, "embedProxy": false,
"embedProxyFrontend": false, "embedProxyFrontend": false,
"fixHourMetrics": false,
"publicSignup": false, "publicSignup": false,
"responseTimeWithAppName": false, "responseTimeWithAppName": false,
"syncSSOGroups": false, "syncSSOGroups": false,

View File

@ -8,7 +8,11 @@ import {
IClientMetricsStoreV2, IClientMetricsStoreV2,
} from '../../types/stores/client-metrics-store-v2'; } from '../../types/stores/client-metrics-store-v2';
import { clientMetricsSchema } from './schema'; import { clientMetricsSchema } from './schema';
import { hoursToMilliseconds, secondsToMilliseconds } from 'date-fns'; import {
compareAsc,
hoursToMilliseconds,
secondsToMilliseconds,
} from 'date-fns';
import { IFeatureToggleStore } from '../../types/stores/feature-toggle-store'; import { IFeatureToggleStore } from '../../types/stores/feature-toggle-store';
import { CLIENT_METRICS } from '../../types/events'; import { CLIENT_METRICS } from '../../types/events';
import ApiUser from '../../types/api-user'; import ApiUser from '../../types/api-user';
@ -16,6 +20,8 @@ import { ALL } from '../../types/models/api-token';
import User from '../../types/user'; import User from '../../types/user';
import { collapseHourlyMetrics } from '../../util/collapseHourlyMetrics'; import { collapseHourlyMetrics } from '../../util/collapseHourlyMetrics';
import { LastSeenService } from './last-seen-service'; import { LastSeenService } from './last-seen-service';
import { generateHourBuckets } from '../../util/time-utils';
import { IFlagResolver } from '../../types/experimental';
export default class ClientMetricsServiceV2 { export default class ClientMetricsServiceV2 {
private config: IUnleashConfig; private config: IUnleashConfig;
@ -30,6 +36,8 @@ export default class ClientMetricsServiceV2 {
private lastSeenService: LastSeenService; private lastSeenService: LastSeenService;
private flagResolver: IFlagResolver;
private logger: Logger; private logger: Logger;
constructor( constructor(
@ -45,6 +53,7 @@ export default class ClientMetricsServiceV2 {
this.clientMetricsStoreV2 = clientMetricsStoreV2; this.clientMetricsStoreV2 = clientMetricsStoreV2;
this.lastSeenService = lastSeenService; this.lastSeenService = lastSeenService;
this.config = config; this.config = config;
this.flagResolver = config.flagResolver;
this.logger = config.getLogger( this.logger = config.getLogger(
'/services/client-metrics/client-metrics-service-v2.ts', '/services/client-metrics/client-metrics-service-v2.ts',
); );
@ -149,13 +158,56 @@ export default class ClientMetricsServiceV2 {
} }
async getClientMetricsForToggle( async getClientMetricsForToggle(
toggleName: string, featureName: string,
hoursBack?: number, hoursBack: number = 24,
): Promise<IClientMetricsEnv[]> { ): Promise<IClientMetricsEnv[]> {
return this.clientMetricsStoreV2.getMetricsForFeatureToggle( const metrics =
toggleName, await this.clientMetricsStoreV2.getMetricsForFeatureToggle(
hoursBack, featureName,
); hoursBack,
);
if (this.flagResolver.isEnabled('fixHourMetrics')) {
const hours = generateHourBuckets(hoursBack);
const environments = [
...new Set(metrics.map((x) => x.environment)),
];
const applications = [
...new Set(metrics.map((x) => x.appName)),
].slice(0, 100);
const result = environments.flatMap((environment) =>
applications.flatMap((appName) =>
hours.flatMap((hourBucket) => {
const metric = metrics.find(
(item) =>
compareAsc(
hourBucket.timestamp,
item.timestamp,
) === 0 &&
item.appName === appName &&
item.environment === environment,
);
return (
metric || {
timestamp: hourBucket.timestamp,
no: 0,
yes: 0,
appName,
environment,
featureName,
}
);
}),
),
);
return result.sort((a, b) => compareAsc(a.timestamp, b.timestamp));
} else {
return metrics;
}
} }
resolveMetricsEnvironment(user: User | ApiUser, data: IClientApp): string { resolveMetricsEnvironment(user: User | ApiUser, data: IClientApp): string {

View File

@ -38,6 +38,10 @@ export const defaultExperimentalOptions = {
process.env.UNLEASH_EXPERIMENTAL_CLONE_ENVIRONMENT, process.env.UNLEASH_EXPERIMENTAL_CLONE_ENVIRONMENT,
false, false,
), ),
fixHourMetrics: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_FIX_HOUR_METRICS,
false,
),
}, },
externalResolver: { isEnabled: (): boolean => false }, externalResolver: { isEnabled: (): boolean => false },
}; };

View File

@ -0,0 +1,7 @@
import { generateHourBuckets } from './time-utils';
test('generateHourBuckets', () => {
const result = generateHourBuckets(24);
expect(result).toHaveLength(24);
});

View File

@ -0,0 +1,16 @@
import { startOfHour, subHours } from 'date-fns';
export interface HourBucket {
timestamp: Date;
}
export function generateHourBuckets(hours: number): HourBucket[] {
const start = startOfHour(new Date());
const result = [];
for (let i = 0; i < hours; i++) {
result.push({ timestamp: subHours(start, i) });
}
return result;
}

View File

@ -41,6 +41,7 @@ process.nextTick(async () => {
syncSSOGroups: true, syncSSOGroups: true,
changeRequests: true, changeRequests: true,
cloneEnvironment: true, cloneEnvironment: true,
fixHourMetrics: true,
}, },
}, },
authentication: { authentication: {

View File

@ -30,6 +30,7 @@ export function createTestConfig(config?: IUnleashOptions): IUnleashConfig {
syncSSOGroups: true, syncSSOGroups: true,
changeRequests: true, changeRequests: true,
cloneEnvironment: true, cloneEnvironment: true,
fixHourMetrics: true,
}, },
}, },
}; };

View File

@ -79,18 +79,18 @@ test('should return raw metrics, aggregated on key', async () => {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.expect(200); .expect(200);
expect(demo.data).toHaveLength(2); expect(demo.data).toHaveLength(48);
expect(demo.data[0].environment).toBe('default'); expect(demo.data[46].environment).toBe('default');
expect(demo.data[0].yes).toBe(5); expect(demo.data[46].yes).toBe(5);
expect(demo.data[0].no).toBe(4); expect(demo.data[46].no).toBe(4);
expect(demo.data[1].environment).toBe('test'); expect(demo.data[47].environment).toBe('test');
expect(demo.data[1].yes).toBe(1); expect(demo.data[47].yes).toBe(1);
expect(demo.data[1].no).toBe(3); expect(demo.data[47].no).toBe(3);
expect(t2.data).toHaveLength(1); expect(t2.data).toHaveLength(24);
expect(t2.data[0].environment).toBe('default'); expect(t2.data[23].environment).toBe('default');
expect(t2.data[0].yes).toBe(7); expect(t2.data[23].yes).toBe(7);
expect(t2.data[0].no).toBe(104); expect(t2.data[23].no).toBe(104);
}); });
test('should support the hoursBack query param for raw metrics', async () => { test('should support the hoursBack query param for raw metrics', async () => {
@ -141,10 +141,10 @@ test('should support the hoursBack query param for raw metrics', async () => {
const hoursTooMany = await fetchHoursBack(999); const hoursTooMany = await fetchHoursBack(999);
expect(hours1.data).toHaveLength(1); expect(hours1.data).toHaveLength(1);
expect(hours24.data).toHaveLength(2); expect(hours24.data).toHaveLength(24);
expect(hours48.data).toHaveLength(3); expect(hours48.data).toHaveLength(48);
expect(hoursTooFew.data).toHaveLength(2); expect(hoursTooFew.data).toHaveLength(24);
expect(hoursTooMany.data).toHaveLength(2); expect(hoursTooMany.data).toHaveLength(24);
}); });
test('should return toggle summary', async () => { test('should return toggle summary', async () => {