diff --git a/src/lib/db/client-metrics-store-v2.ts b/src/lib/db/client-metrics-store-v2.ts index ad818c0ff7..8e77e88db8 100644 --- a/src/lib/db/client-metrics-store-v2.ts +++ b/src/lib/db/client-metrics-store-v2.ts @@ -7,6 +7,7 @@ import { IClientMetricsEnvKey, IClientMetricsStoreV2, } from '../types/stores/client-metrics-store-v2'; +import NotFoundError from '../error/notfound-error'; interface ClientMetricsEnvTable { feature_name: string; @@ -53,8 +54,19 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 { this.logger = getLogger('client-metrics-store-v2.js'); } - get(key: IClientMetricsEnvKey): Promise { - throw new Error('Method not implemented.'); + async get(key: IClientMetricsEnvKey): Promise { + const row = await this.db(TABLE) + .where({ + feature_name: key.featureName, + app_name: key.appName, + environment: key.environment, + timestamp: roundDownToHour(key.timestamp), + }) + .first(); + if (row) { + return fromRow(row); + } + throw new NotFoundError(`Could not find metric`); } async getAll(query: Object = {}): Promise { @@ -64,12 +76,24 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 { return rows.map(fromRow); } - exists(key: IClientMetricsEnvKey): Promise { - throw new Error('Method not implemented.'); + async exists(key: IClientMetricsEnvKey): Promise { + try { + await this.get(key); + return true; + } catch (e) { + return false; + } } - delete(key: IClientMetricsEnvKey): Promise { - throw new Error('Method not implemented.'); + async delete(key: IClientMetricsEnvKey): Promise { + return this.db(TABLE) + .where({ + feature_name: key.featureName, + app_name: key.appName, + environment: key.environment, + timestamp: roundDownToHour(key.timestamp), + }) + .del(); } deleteAll(): Promise { @@ -130,4 +154,10 @@ export class ClientMetricsStoreV2 implements IClientMetricsStoreV2 { .pluck('app_name') .orderBy('app_name'); } + + async clearMetrics(hoursAgo: number): Promise { + return this.db(TABLE) + .whereRaw(`timestamp <= NOW() - INTERVAL '${hoursAgo} hours'`) + .del(); + } } diff --git a/src/lib/server-impl.ts b/src/lib/server-impl.ts index 1613365242..0c892e6c87 100644 --- a/src/lib/server-impl.ts +++ b/src/lib/server-impl.ts @@ -49,6 +49,7 @@ async function createApp( metricsMonitor.stopMonitoring(); stores.clientInstanceStore.destroy(); stores.clientMetricsStore.destroy(); + services.clientMetricsServiceV2.destroy(); await db.destroy(); }; 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 0bd2a91bff..f54b7d6d96 100644 --- a/src/lib/services/client-metrics/client-metrics-service-v2.ts +++ b/src/lib/services/client-metrics/client-metrics-service-v2.ts @@ -10,9 +10,10 @@ import { import { clientMetricsSchema } from './client-metrics-schema'; const FIVE_MINUTES = 5 * 60 * 1000; +const ONE_DAY = 24 * 60 * 60 * 1000; export default class ClientMetricsServiceV2 { - private timers: NodeJS.Timeout[] = []; + private timer: NodeJS.Timeout; private clientMetricsStoreV2: IClientMetricsStoreV2; @@ -30,6 +31,11 @@ export default class ClientMetricsServiceV2 { this.logger = getLogger('/services/client-metrics/index.ts'); this.bulkInterval = bulkInterval; + this.timer = setInterval(() => { + console.log('Clear metrics'); + this.clientMetricsStoreV2.clearMetrics(48); + }, ONE_DAY); + this.timer.unref(); } async registerClientMetrics( @@ -97,4 +103,9 @@ export default class ClientMetricsServiceV2 { ): Promise { return this.clientMetricsStoreV2.getMetricsForFeatureToggle(toggleName); } + + destroy(): void { + clearInterval(this.timer); + this.timer = null; + } } diff --git a/src/lib/types/stores/client-metrics-store-v2.ts b/src/lib/types/stores/client-metrics-store-v2.ts index c64cca4731..3856605999 100644 --- a/src/lib/types/stores/client-metrics-store-v2.ts +++ b/src/lib/types/stores/client-metrics-store-v2.ts @@ -23,4 +23,5 @@ export interface IClientMetricsStoreV2 featureName: string, hoursBack?: number, ): Promise; + clearMetrics(hoursAgo: number): Promise; } diff --git a/src/test/e2e/stores/client-metrics-store-v2.e2e.test.ts b/src/test/e2e/stores/client-metrics-store-v2.e2e.test.ts index 82ea813a40..271bd4bb18 100644 --- a/src/test/e2e/stores/client-metrics-store-v2.e2e.test.ts +++ b/src/test/e2e/stores/client-metrics-store-v2.e2e.test.ts @@ -262,3 +262,123 @@ test('Should not fail on undefined list of metrics', async () => { expect(all).toHaveLength(0); }); + +test('Should return delete old metric', async () => { + const twoDaysAgo = new Date(); + twoDaysAgo.setHours(-48); + + const metrics: IClientMetricsEnv[] = [ + { + featureName: 'demo1', + appName: 'web', + environment: 'dev', + timestamp: new Date(), + yes: 2, + no: 2, + }, + { + featureName: 'demo2', + appName: 'backend-api', + environment: 'dev', + timestamp: new Date(), + yes: 1, + no: 3, + }, + { + featureName: 'demo3', + appName: 'backend-api', + environment: 'dev', + timestamp: twoDaysAgo, + yes: 1, + no: 3, + }, + { + featureName: 'demo4', + appName: 'backend-api', + environment: 'dev', + timestamp: twoDaysAgo, + yes: 1, + no: 3, + }, + ]; + await clientMetricsStore.batchInsertMetrics(metrics); + await clientMetricsStore.clearMetrics(24); + const all = await clientMetricsStore.getAll(); + + expect(all).toHaveLength(2); + expect(all[0].featureName).toBe('demo1'); + expect(all[1].featureName).toBe('demo2'); +}); + +test('Should get metric', async () => { + const twoDaysAgo = new Date(); + twoDaysAgo.setHours(-48); + + const metrics: IClientMetricsEnv[] = [ + { + featureName: 'demo1', + appName: 'web', + environment: 'dev', + timestamp: new Date(), + yes: 2, + no: 2, + }, + { + featureName: 'demo2', + appName: 'backend-api', + environment: 'dev', + timestamp: new Date(), + yes: 1, + no: 3, + }, + { + featureName: 'demo3', + appName: 'backend-api', + environment: 'dev', + timestamp: twoDaysAgo, + yes: 1, + no: 3, + }, + { + featureName: 'demo4', + appName: 'backend-api', + environment: 'dev', + timestamp: twoDaysAgo, + yes: 41, + no: 42, + }, + ]; + await clientMetricsStore.batchInsertMetrics(metrics); + const metric = await clientMetricsStore.get({ + featureName: 'demo4', + timestamp: twoDaysAgo, + appName: 'backend-api', + environment: 'dev', + }); + + expect(metric.featureName).toBe('demo4'); + expect(metric.yes).toBe(41); + expect(metric.no).toBe(42); +}); + +test('Should not exists after delete', async () => { + const metric = { + featureName: 'demo4', + appName: 'backend-api', + environment: 'dev', + timestamp: new Date(), + yes: 41, + no: 42, + }; + + const metrics: IClientMetricsEnv[] = [metric]; + await clientMetricsStore.batchInsertMetrics(metrics); + + const existBefore = await clientMetricsStore.exists(metric); + expect(existBefore).toBe(true); + + await clientMetricsStore.delete(metric); + + const existAfter = await clientMetricsStore.exists(metric); + expect(existAfter).toBe(false); +}); diff --git a/src/test/fixtures/fake-client-metrics-store-v2.ts b/src/test/fixtures/fake-client-metrics-store-v2.ts index 310b5f9115..5e919a1d81 100644 --- a/src/test/fixtures/fake-client-metrics-store-v2.ts +++ b/src/test/fixtures/fake-client-metrics-store-v2.ts @@ -18,6 +18,9 @@ export default class FakeClientMetricsStoreV2 super(); this.setMaxListeners(0); } + clearMetrics(hoursBack: number): Promise { + return Promise.resolve(); + } getSeenAppsForFeatureToggle( featureName: string, hoursBack?: number,