diff --git a/src/lib/db/client-metrics-store-v2.ts b/src/lib/db/client-metrics-store-v2.ts index bea2fdfd04..dcf080f73c 100644 --- a/src/lib/db/client-metrics-store-v2.ts +++ b/src/lib/db/client-metrics-store-v2.ts @@ -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 { - throw new Error('Method not implemented.'); + return this.db(TABLE).del(); } destroy(): void { diff --git a/src/lib/routes/admin-api/client-metrics.ts b/src/lib/routes/admin-api/client-metrics.ts index ccac1dd1e7..4998f504a8 100644 --- a/src/lib/routes/admin-api/client-metrics.ts +++ b/src/lib/routes/admin-api/client-metrics.ts @@ -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 { + async getRawToggleMetrics(req: Request, res: Response): Promise { 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 { + const { name } = req.params; + const data = await this.metrics.getFeatureToggleMetricsSummary(name); + res.json({ + version: 1, + maturity: 'experimental', + ...data, + }); + } } export default ClientMetricsController; diff --git a/src/lib/services/client-metrics/client-metrics-service-v2.ts b/src/lib/services/client-metrics/client-metrics-service-v2.ts index 0f8bef6db3..0bd2a91bff 100644 --- a/src/lib/services/client-metrics/client-metrics-service-v2.ts +++ b/src/lib/services/client-metrics/client-metrics-service-v2.ts @@ -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 { + // Overview over usage last "hour" bucket and all applications using the toggle + async getFeatureToggleMetricsSummary( + featureName: string, + ): Promise { 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 { + return this.clientMetricsStoreV2.getMetricsForFeatureToggle(toggleName); } } diff --git a/src/lib/services/client-metrics/util.test.ts b/src/lib/services/client-metrics/util.test.ts index e746e29e1d..98a405fc96 100644 --- a/src/lib/services/client-metrics/util.test.ts +++ b/src/lib/services/client-metrics/util.test.ts @@ -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, - }); -}); diff --git a/src/lib/services/client-metrics/util.ts b/src/lib/services/client-metrics/util.ts index 0ee1dfa5e9..b7edc00321 100644 --- a/src/lib/services/client-metrics/util.ts +++ b/src/lib/services/client-metrics/util.ts @@ -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); -} diff --git a/src/lib/types/models/metrics.ts b/src/lib/types/models/metrics.ts index 22d14aa9f8..efc2558c32 100644 --- a/src/lib/types/models/metrics.ts +++ b/src/lib/types/models/metrics.ts @@ -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[]; } diff --git a/src/test/e2e/api/admin/client-metrics.e2e.test.ts b/src/test/e2e/api/admin/client-metrics.e2e.test.ts index 6e54010d98..3a38c70025 100644 --- a/src/test/e2e/api/admin/client-metrics.e2e.test.ts +++ b/src/test/e2e/api/admin/client-metrics.e2e.test.ts @@ -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']); });