From a3ac624deb30ca85044dca1dbc756b2953148e8b Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Thu, 8 May 2025 14:06:10 +0200 Subject: [PATCH] feat: report top used domains (#9934) --- .../fake-feature-links-read-model.ts | 4 +++ .../feature-links/feature-link-store.ts | 1 + .../feature-links/feature-link.e2e.test.ts | 16 ++++++++++ .../feature-links-read-model-type.ts | 1 + .../feature-links/feature-links-read-model.ts | 29 ++++++++++++++++++- .../createFeatureToggleService.ts | 2 +- src/lib/services/index.ts | 2 +- 7 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/lib/features/feature-links/fake-feature-links-read-model.ts b/src/lib/features/feature-links/fake-feature-links-read-model.ts index 691a849d48..f2760a1600 100644 --- a/src/lib/features/feature-links/fake-feature-links-read-model.ts +++ b/src/lib/features/feature-links/fake-feature-links-read-model.ts @@ -4,6 +4,10 @@ import type { } from './feature-links-read-model-type'; export class FakeFeatureLinksReadModel implements IFeatureLinksReadModel { + async getTopDomains(): Promise<{ domain: string; count: number }[]> { + return []; + } + async getLinks(feature: string): Promise { return []; } diff --git a/src/lib/features/feature-links/feature-link-store.ts b/src/lib/features/feature-links/feature-link-store.ts index 8661f5d6f9..f33c0561d1 100644 --- a/src/lib/features/feature-links/feature-link-store.ts +++ b/src/lib/features/feature-links/feature-link-store.ts @@ -26,6 +26,7 @@ export class FeatureLinkStore feature_name: item.featureName, url: item.url, title: item.title, + domain: item.domain, }; await this.db('feature_link').insert(featureLink); return { ...item, id }; diff --git a/src/lib/features/feature-links/feature-link.e2e.test.ts b/src/lib/features/feature-links/feature-link.e2e.test.ts index 5098c12494..406e517058 100644 --- a/src/lib/features/feature-links/feature-link.e2e.test.ts +++ b/src/lib/features/feature-links/feature-link.e2e.test.ts @@ -6,11 +6,14 @@ import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init'; import type { IEventStore, IFeatureLinkStore } from '../../types'; import getLogger from '../../../test/fixtures/no-logger'; import type { FeatureLinkSchema } from '../../openapi/spec/feature-link-schema'; +import type { IFeatureLinksReadModel } from './feature-links-read-model-type'; +import { FeatureLinksReadModel } from './feature-links-read-model'; let app: IUnleashTest; let db: ITestDb; let featureLinkStore: IFeatureLinkStore; let eventStore: IEventStore; +let featureLinkReadModel: IFeatureLinksReadModel; beforeAll(async () => { db = await dbInit('feature_link', getLogger, { @@ -29,6 +32,10 @@ beforeAll(async () => { ); eventStore = db.stores.eventStore; featureLinkStore = db.stores.featureLinkStore; + featureLinkReadModel = new FeatureLinksReadModel( + db.rawDatabase, + app.config.eventBus, + ); await app.request .post(`/auth/demo/login`) @@ -99,13 +106,20 @@ test('should manage feature links', async () => { url: 'https://example.com', title: 'feature link', featureName: 'my_feature', + domain: 'example', }, { url: 'https://example_another.com', title: 'another feature link', featureName: 'my_feature', + domain: 'example_another', }, ]); + const topDomains = await featureLinkReadModel.getTopDomains(); + expect(topDomains).toMatchObject([ + { domain: 'example_another', count: 1 }, + { domain: 'example', count: 1 }, + ]); const { body } = await app.getProjectFeatures('default', 'my_feature'); expect(body.links).toMatchObject([ { id: links[0].id, title: 'feature link', url: 'https://example.com' }, @@ -126,6 +140,7 @@ test('should manage feature links', async () => { url: 'https://example_updated.com', title: 'feature link updated', featureName: 'my_feature', + domain: 'example_updated', }); await deleteLink('my_feature', links[0].id); @@ -136,6 +151,7 @@ test('should manage feature links', async () => { id: links[1].id, title: 'another feature link', url: 'https://example_another.com', + domain: 'example_another', }, ]); diff --git a/src/lib/features/feature-links/feature-links-read-model-type.ts b/src/lib/features/feature-links/feature-links-read-model-type.ts index 26d89ee5b8..b8abeb1136 100644 --- a/src/lib/features/feature-links/feature-links-read-model-type.ts +++ b/src/lib/features/feature-links/feature-links-read-model-type.ts @@ -6,4 +6,5 @@ export interface IFeatureLink { export interface IFeatureLinksReadModel { getLinks(feature: string): Promise; + getTopDomains(): Promise<{ domain: string; count: number }[]>; } diff --git a/src/lib/features/feature-links/feature-links-read-model.ts b/src/lib/features/feature-links/feature-links-read-model.ts index 36bec14b18..0701d190de 100644 --- a/src/lib/features/feature-links/feature-links-read-model.ts +++ b/src/lib/features/feature-links/feature-links-read-model.ts @@ -3,12 +3,39 @@ import type { IFeatureLink, IFeatureLinksReadModel, } from './feature-links-read-model-type'; +import metricsHelper from '../../util/metrics-helper'; +import { DB_TIME } from '../../metric-events'; +import type EventEmitter from 'events'; export class FeatureLinksReadModel implements IFeatureLinksReadModel { private db: Db; + private timer: Function; - constructor(db: Db) { + constructor(db: Db, eventBus: EventEmitter) { this.db = db; + this.timer = (action) => + metricsHelper.wrapTimer(eventBus, DB_TIME, { + store: 'feature_links', + action, + }); + } + + async getTopDomains(): Promise<{ domain: string; count: number }[]> { + const stopTimer = this.timer('getTopDomains'); + const topDomains = await this.db + .from('feature_link') + .select('domain') + .count('* as count') + .whereNotNull('domain') + .groupBy('domain') + .orderBy('count', 'desc') + .limit(20); + stopTimer(); + + return topDomains.map(({ domain, count }) => ({ + domain, + count: Number.parseInt(count, 10), + })); } async getLinks(feature: string): Promise { diff --git a/src/lib/features/feature-toggle/createFeatureToggleService.ts b/src/lib/features/feature-toggle/createFeatureToggleService.ts index d1bc77a5c1..ef6921b827 100644 --- a/src/lib/features/feature-toggle/createFeatureToggleService.ts +++ b/src/lib/features/feature-toggle/createFeatureToggleService.ts @@ -130,7 +130,7 @@ export const createFeatureToggleService = ( const featureCollaboratorsReadModel = new FeatureCollaboratorsReadModel(db); - const featureLinksReadModel = new FeatureLinksReadModel(db); + const featureLinksReadModel = new FeatureLinksReadModel(db, eventBus); const featureToggleService = new FeatureToggleService( { diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 063e77bd59..3f65976751 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -292,7 +292,7 @@ export const createServices = ( : new FakeFeatureCollaboratorsReadModel(); const featureLinkReadModel = db - ? new FeatureLinksReadModel(db) + ? new FeatureLinksReadModel(db, config.eventBus) : new FakeFeatureLinksReadModel(); const featureToggleService = new FeatureToggleService(