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';
|
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
|
let p = 60 * 60 * 1000; // milliseconds in an hour
|
||||||
return new Date(Math.floor(date.getTime() / p) * p);
|
return new Date(Math.floor(date.getTime() / p) * p);
|
||||||
}
|
}
|
||||||
@ -72,7 +73,7 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteAll(): Promise<void> {
|
deleteAll(): Promise<void> {
|
||||||
throw new Error('Method not implemented.');
|
return this.db(TABLE).del();
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
|
@ -21,10 +21,11 @@ class ClientMetricsController extends Controller {
|
|||||||
|
|
||||||
this.metrics = clientMetricsServiceV2;
|
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 { name } = req.params;
|
||||||
const data = await this.metrics.getClientMetricsForToggle(name);
|
const data = await this.metrics.getClientMetricsForToggle(name);
|
||||||
res.json({
|
res.json({
|
||||||
@ -33,5 +34,15 @@ class ClientMetricsController extends Controller {
|
|||||||
data,
|
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;
|
export default ClientMetricsController;
|
||||||
|
@ -2,13 +2,12 @@ import { Logger } from '../../logger';
|
|||||||
import { IUnleashConfig } from '../../server-impl';
|
import { IUnleashConfig } from '../../server-impl';
|
||||||
import { IUnleashStores } from '../../types';
|
import { IUnleashStores } from '../../types';
|
||||||
import { IClientApp } from '../../types/model';
|
import { IClientApp } from '../../types/model';
|
||||||
import { GroupedClientMetrics } from '../../types/models/metrics';
|
import { ToggleMetricsSummary } from '../../types/models/metrics';
|
||||||
import {
|
import {
|
||||||
IClientMetricsEnv,
|
IClientMetricsEnv,
|
||||||
IClientMetricsStoreV2,
|
IClientMetricsStoreV2,
|
||||||
} from '../../types/stores/client-metrics-store-v2';
|
} from '../../types/stores/client-metrics-store-v2';
|
||||||
import { clientMetricsSchema } from './client-metrics-schema';
|
import { clientMetricsSchema } from './client-metrics-schema';
|
||||||
import { groupMetricsOnEnv } from './util';
|
|
||||||
|
|
||||||
const FIVE_MINUTES = 5 * 60 * 1000;
|
const FIVE_MINUTES = 5 * 60 * 1000;
|
||||||
|
|
||||||
@ -57,14 +56,45 @@ export default class ClientMetricsServiceV2 {
|
|||||||
await this.clientMetricsStoreV2.batchInsertMetrics(clientMetrics);
|
await this.clientMetricsStoreV2.batchInsertMetrics(clientMetrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getClientMetricsForToggle(
|
// Overview over usage last "hour" bucket and all applications using the toggle
|
||||||
toggleName: string,
|
async getFeatureToggleMetricsSummary(
|
||||||
): Promise<GroupedClientMetrics[]> {
|
featureName: string,
|
||||||
|
): Promise<ToggleMetricsSummary> {
|
||||||
const metrics =
|
const metrics =
|
||||||
await this.clientMetricsStoreV2.getMetricsForFeatureToggle(
|
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[2]).toStrictEqual(new Date(2021, 10, 10, 13, 0, 0));
|
||||||
expect(hours[23]).toStrictEqual(new Date(2021, 10, 9, 16, 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
|
//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 {
|
export interface GroupedClientMetrics {
|
||||||
environment: string;
|
environment: string;
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
yes_count: number;
|
yes: number;
|
||||||
no_count: number;
|
no: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToggleMetricsSummary {
|
||||||
|
featureName: string;
|
||||||
|
lastHourUsage: GroupedClientMetrics[];
|
||||||
|
seenApplications: string[];
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import dbInit, { ITestDb } from '../../helpers/database-init';
|
import dbInit, { ITestDb } from '../../helpers/database-init';
|
||||||
import { setupAppWithCustomConfig } from '../../helpers/test-helper';
|
import { setupAppWithCustomConfig } from '../../helpers/test-helper';
|
||||||
import getLogger from '../../../fixtures/no-logger';
|
import getLogger from '../../../fixtures/no-logger';
|
||||||
import { roundDownToHour } from '../../../../lib/services/client-metrics/util';
|
|
||||||
import { IClientMetricsEnv } from '../../../../lib/types/stores/client-metrics-store-v2';
|
import { IClientMetricsEnv } from '../../../../lib/types/stores/client-metrics-store-v2';
|
||||||
|
|
||||||
let app;
|
let app;
|
||||||
@ -22,10 +21,11 @@ afterAll(async () => {
|
|||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await db.reset();
|
await db.reset();
|
||||||
|
await db.stores.clientMetricsStoreV2.deleteAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return grouped metrics', async () => {
|
test('should return raw metrics, aggregated on key', async () => {
|
||||||
const date = roundDownToHour(new Date());
|
const date = new Date();
|
||||||
const metrics: IClientMetricsEnv[] = [
|
const metrics: IClientMetricsEnv[] = [
|
||||||
{
|
{
|
||||||
featureName: 'demo',
|
featureName: 'demo',
|
||||||
@ -72,24 +72,95 @@ test('should return grouped metrics', async () => {
|
|||||||
await db.stores.clientMetricsStoreV2.batchInsertMetrics(metrics);
|
await db.stores.clientMetricsStoreV2.batchInsertMetrics(metrics);
|
||||||
|
|
||||||
const { body: demo } = await app.request
|
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('Content-Type', /json/)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
const { body: t2 } = await app.request
|
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('Content-Type', /json/)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(demo.data).toHaveLength(48);
|
expect(demo.data).toHaveLength(2);
|
||||||
expect(demo.data[0].environment).toBe('default');
|
expect(demo.data[0].environment).toBe('default');
|
||||||
expect(demo.data[0].yes_count).toBe(5);
|
expect(demo.data[0].yes).toBe(5);
|
||||||
expect(demo.data[0].no_count).toBe(4);
|
expect(demo.data[0].no).toBe(4);
|
||||||
expect(demo.data[1].environment).toBe('test');
|
expect(demo.data[1].environment).toBe('test');
|
||||||
expect(demo.data[1].yes_count).toBe(1);
|
expect(demo.data[1].yes).toBe(1);
|
||||||
expect(demo.data[1].no_count).toBe(3);
|
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].environment).toBe('default');
|
||||||
expect(t2.data[0].yes_count).toBe(7);
|
expect(t2.data[0].yes).toBe(7);
|
||||||
expect(t2.data[0].no_count).toBe(104);
|
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