mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-05 17:53:12 +02:00
fix: add feature metrics summary endpoint
This commit is contained in:
parent
5f40560b23
commit
9a3404fc01
@ -19,7 +19,8 @@ interface ClientMetricsEnvTable {
|
||||
|
||||
const TABLE = 'client_metrics_env';
|
||||
|
||||
function roundDownToHour(date) {
|
||||
// Unsure if this would be better be done by the service?
|
||||
export function roundDownToHour(date: Date): Date {
|
||||
let p = 60 * 60 * 1000; // milliseconds in an hour
|
||||
return new Date(Math.floor(date.getTime() / p) * p);
|
||||
}
|
||||
@ -72,7 +73,7 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
|
||||
}
|
||||
|
||||
deleteAll(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
return this.db(TABLE).del();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
|
@ -21,10 +21,11 @@ class ClientMetricsController extends Controller {
|
||||
|
||||
this.metrics = clientMetricsServiceV2;
|
||||
|
||||
this.get('/features/:name', this.getFeatureToggleMetrics);
|
||||
this.get('/features/:name/raw', this.getRawToggleMetrics);
|
||||
this.get('/features/:name', this.getToggleMetricsSummary);
|
||||
}
|
||||
|
||||
async getFeatureToggleMetrics(req: Request, res: Response): Promise<void> {
|
||||
async getRawToggleMetrics(req: Request, res: Response): Promise<void> {
|
||||
const { name } = req.params;
|
||||
const data = await this.metrics.getClientMetricsForToggle(name);
|
||||
res.json({
|
||||
@ -33,5 +34,15 @@ class ClientMetricsController extends Controller {
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
async getToggleMetricsSummary(req: Request, res: Response): Promise<void> {
|
||||
const { name } = req.params;
|
||||
const data = await this.metrics.getFeatureToggleMetricsSummary(name);
|
||||
res.json({
|
||||
version: 1,
|
||||
maturity: 'experimental',
|
||||
...data,
|
||||
});
|
||||
}
|
||||
}
|
||||
export default ClientMetricsController;
|
||||
|
@ -2,13 +2,12 @@ import { Logger } from '../../logger';
|
||||
import { IUnleashConfig } from '../../server-impl';
|
||||
import { IUnleashStores } from '../../types';
|
||||
import { IClientApp } from '../../types/model';
|
||||
import { GroupedClientMetrics } from '../../types/models/metrics';
|
||||
import { ToggleMetricsSummary } from '../../types/models/metrics';
|
||||
import {
|
||||
IClientMetricsEnv,
|
||||
IClientMetricsStoreV2,
|
||||
} from '../../types/stores/client-metrics-store-v2';
|
||||
import { clientMetricsSchema } from './client-metrics-schema';
|
||||
import { groupMetricsOnEnv } from './util';
|
||||
|
||||
const FIVE_MINUTES = 5 * 60 * 1000;
|
||||
|
||||
@ -57,14 +56,45 @@ export default class ClientMetricsServiceV2 {
|
||||
await this.clientMetricsStoreV2.batchInsertMetrics(clientMetrics);
|
||||
}
|
||||
|
||||
async getClientMetricsForToggle(
|
||||
toggleName: string,
|
||||
): Promise<GroupedClientMetrics[]> {
|
||||
// Overview over usage last "hour" bucket and all applications using the toggle
|
||||
async getFeatureToggleMetricsSummary(
|
||||
featureName: string,
|
||||
): Promise<ToggleMetricsSummary> {
|
||||
const metrics =
|
||||
await this.clientMetricsStoreV2.getMetricsForFeatureToggle(
|
||||
toggleName,
|
||||
featureName,
|
||||
1,
|
||||
);
|
||||
const seenApplications =
|
||||
await this.clientMetricsStoreV2.getSeenAppsForFeatureToggle(
|
||||
featureName,
|
||||
);
|
||||
|
||||
return groupMetricsOnEnv(metrics);
|
||||
const groupedMetrics = metrics.reduce((prev, curr) => {
|
||||
if (prev[curr.environment]) {
|
||||
prev[curr.environment].yes += curr.yes;
|
||||
prev[curr.environment].no += curr.no;
|
||||
} else {
|
||||
prev[curr.environment] = {
|
||||
environment: curr.environment,
|
||||
timestamp: curr.timestamp,
|
||||
yes: curr.yes,
|
||||
no: curr.no,
|
||||
};
|
||||
}
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
featureName,
|
||||
lastHourUsage: Object.values(groupedMetrics),
|
||||
seenApplications,
|
||||
};
|
||||
}
|
||||
|
||||
async getClientMetricsForToggle(
|
||||
toggleName: string,
|
||||
): Promise<IClientMetricsEnv[]> {
|
||||
return this.clientMetricsStoreV2.getMetricsForFeatureToggle(toggleName);
|
||||
}
|
||||
}
|
||||
|
@ -10,48 +10,3 @@ test('should return list of 24 horus', () => {
|
||||
expect(hours[2]).toStrictEqual(new Date(2021, 10, 10, 13, 0, 0));
|
||||
expect(hours[23]).toStrictEqual(new Date(2021, 10, 9, 16, 0, 0));
|
||||
});
|
||||
|
||||
test('should group metrics together', () => {
|
||||
const date = roundDownToHour(new Date());
|
||||
const metrics: IClientMetricsEnv[] = [
|
||||
{
|
||||
featureName: 'demo',
|
||||
appName: 'web',
|
||||
environment: 'default',
|
||||
timestamp: date,
|
||||
yes: 2,
|
||||
no: 2,
|
||||
},
|
||||
{
|
||||
featureName: 'demo',
|
||||
appName: 'web',
|
||||
environment: 'default',
|
||||
timestamp: date,
|
||||
yes: 3,
|
||||
no: 2,
|
||||
},
|
||||
{
|
||||
featureName: 'demo',
|
||||
appName: 'web',
|
||||
environment: 'test',
|
||||
timestamp: date,
|
||||
yes: 1,
|
||||
no: 3,
|
||||
},
|
||||
];
|
||||
|
||||
const grouped = groupMetricsOnEnv(metrics);
|
||||
|
||||
expect(grouped[0]).toStrictEqual({
|
||||
timestamp: date,
|
||||
environment: 'default',
|
||||
yes_count: 5,
|
||||
no_count: 4,
|
||||
});
|
||||
expect(grouped[1]).toStrictEqual({
|
||||
timestamp: date,
|
||||
environment: 'test',
|
||||
yes_count: 1,
|
||||
no_count: 3,
|
||||
});
|
||||
});
|
||||
|
@ -1,48 +1,2 @@
|
||||
import { GroupedClientMetrics } from '../../types/models/metrics';
|
||||
import { IClientMetricsEnv } from '../../types/stores/client-metrics-store-v2';
|
||||
|
||||
//duplicate from client-metrics-store-v2.ts
|
||||
export function roundDownToHour(date: Date): Date {
|
||||
let p = 60 * 60 * 1000; // milliseconds in an hour
|
||||
return new Date(Math.floor(date.getTime() / p) * p);
|
||||
}
|
||||
|
||||
export function generateLastNHours(n: number, start: Date): Date[] {
|
||||
const nHours: Date[] = [];
|
||||
nHours.push(roundDownToHour(start));
|
||||
for (let i = 1; i < n; i++) {
|
||||
const prev = nHours[i - 1];
|
||||
const next = new Date(prev);
|
||||
next.setHours(prev.getHours() - 1);
|
||||
nHours.push(next);
|
||||
}
|
||||
|
||||
return nHours;
|
||||
}
|
||||
|
||||
export function groupMetricsOnEnv(
|
||||
metrics: IClientMetricsEnv[],
|
||||
): GroupedClientMetrics[] {
|
||||
const hours = generateLastNHours(24, new Date());
|
||||
const environments = metrics.map((m) => m.environment);
|
||||
|
||||
const grouped = {};
|
||||
|
||||
hours.forEach((time) => {
|
||||
environments.forEach((environment) => {
|
||||
grouped[`${time}:${environment}`] = {
|
||||
timestamp: time,
|
||||
environment,
|
||||
yes_count: 0,
|
||||
no_count: 0,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
metrics.forEach((m) => {
|
||||
grouped[`${m.timestamp}:${m.environment}`].yes_count += m.yes;
|
||||
grouped[`${m.timestamp}:${m.environment}`].no_count += m.no;
|
||||
});
|
||||
|
||||
return Object.values(grouped);
|
||||
}
|
||||
|
@ -1,6 +1,12 @@
|
||||
export interface GroupedClientMetrics {
|
||||
environment: string;
|
||||
timestamp: Date;
|
||||
yes_count: number;
|
||||
no_count: number;
|
||||
yes: number;
|
||||
no: number;
|
||||
}
|
||||
|
||||
export interface ToggleMetricsSummary {
|
||||
featureName: string;
|
||||
lastHourUsage: GroupedClientMetrics[];
|
||||
seenApplications: string[];
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import dbInit, { ITestDb } from '../../helpers/database-init';
|
||||
import { setupAppWithCustomConfig } from '../../helpers/test-helper';
|
||||
import getLogger from '../../../fixtures/no-logger';
|
||||
import { roundDownToHour } from '../../../../lib/services/client-metrics/util';
|
||||
import { IClientMetricsEnv } from '../../../../lib/types/stores/client-metrics-store-v2';
|
||||
|
||||
let app;
|
||||
@ -22,10 +21,11 @@ afterAll(async () => {
|
||||
|
||||
afterEach(async () => {
|
||||
await db.reset();
|
||||
await db.stores.clientMetricsStoreV2.deleteAll();
|
||||
});
|
||||
|
||||
test('should return grouped metrics', async () => {
|
||||
const date = roundDownToHour(new Date());
|
||||
test('should return raw metrics, aggregated on key', async () => {
|
||||
const date = new Date();
|
||||
const metrics: IClientMetricsEnv[] = [
|
||||
{
|
||||
featureName: 'demo',
|
||||
@ -72,24 +72,95 @@ test('should return grouped metrics', async () => {
|
||||
await db.stores.clientMetricsStoreV2.batchInsertMetrics(metrics);
|
||||
|
||||
const { body: demo } = await app.request
|
||||
.get('/api/admin/client-metrics/features/demo')
|
||||
.get('/api/admin/client-metrics/features/demo/raw')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
const { body: t2 } = await app.request
|
||||
.get('/api/admin/client-metrics/features/t2')
|
||||
.get('/api/admin/client-metrics/features/t2/raw')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
|
||||
expect(demo.data).toHaveLength(48);
|
||||
expect(demo.data).toHaveLength(2);
|
||||
expect(demo.data[0].environment).toBe('default');
|
||||
expect(demo.data[0].yes_count).toBe(5);
|
||||
expect(demo.data[0].no_count).toBe(4);
|
||||
expect(demo.data[0].yes).toBe(5);
|
||||
expect(demo.data[0].no).toBe(4);
|
||||
expect(demo.data[1].environment).toBe('test');
|
||||
expect(demo.data[1].yes_count).toBe(1);
|
||||
expect(demo.data[1].no_count).toBe(3);
|
||||
expect(demo.data[1].yes).toBe(1);
|
||||
expect(demo.data[1].no).toBe(3);
|
||||
|
||||
expect(t2.data).toHaveLength(24);
|
||||
expect(t2.data).toHaveLength(1);
|
||||
expect(t2.data[0].environment).toBe('default');
|
||||
expect(t2.data[0].yes_count).toBe(7);
|
||||
expect(t2.data[0].no_count).toBe(104);
|
||||
expect(t2.data[0].yes).toBe(7);
|
||||
expect(t2.data[0].no).toBe(104);
|
||||
});
|
||||
|
||||
test('should toggle summary', async () => {
|
||||
const date = new Date();
|
||||
const metrics: IClientMetricsEnv[] = [
|
||||
{
|
||||
featureName: 'demo',
|
||||
appName: 'web',
|
||||
environment: 'default',
|
||||
timestamp: date,
|
||||
yes: 2,
|
||||
no: 2,
|
||||
},
|
||||
{
|
||||
featureName: 't2',
|
||||
appName: 'web',
|
||||
environment: 'default',
|
||||
timestamp: date,
|
||||
yes: 5,
|
||||
no: 5,
|
||||
},
|
||||
{
|
||||
featureName: 't2',
|
||||
appName: 'web',
|
||||
environment: 'default',
|
||||
timestamp: date,
|
||||
yes: 2,
|
||||
no: 99,
|
||||
},
|
||||
{
|
||||
featureName: 'demo',
|
||||
appName: 'web',
|
||||
environment: 'default',
|
||||
timestamp: date,
|
||||
yes: 3,
|
||||
no: 2,
|
||||
},
|
||||
{
|
||||
featureName: 'demo',
|
||||
appName: 'web',
|
||||
environment: 'test',
|
||||
timestamp: date,
|
||||
yes: 1,
|
||||
no: 3,
|
||||
},
|
||||
{
|
||||
featureName: 'demo',
|
||||
appName: 'backend-api',
|
||||
environment: 'test',
|
||||
timestamp: date,
|
||||
yes: 1,
|
||||
no: 3,
|
||||
},
|
||||
];
|
||||
|
||||
await db.stores.clientMetricsStoreV2.batchInsertMetrics(metrics);
|
||||
|
||||
const { body: demo } = await app.request
|
||||
.get('/api/admin/client-metrics/features/demo')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
|
||||
expect(demo.featureName).toBe('demo');
|
||||
expect(demo.lastHourUsage).toHaveLength(2);
|
||||
expect(demo.lastHourUsage[0].environment).toBe('default');
|
||||
expect(demo.lastHourUsage[0].yes).toBe(5);
|
||||
expect(demo.lastHourUsage[0].no).toBe(4);
|
||||
expect(demo.lastHourUsage[1].environment).toBe('test');
|
||||
expect(demo.lastHourUsage[1].yes).toBe(2);
|
||||
expect(demo.lastHourUsage[1].no).toBe(6);
|
||||
expect(demo.seenApplications).toStrictEqual(['backend-api', 'web']);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user