From 43efaf7c47a8ea67ba2a52ea71f68068626d9848 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Fri, 9 May 2025 09:39:15 +0200 Subject: [PATCH] feat: report feature links by domain (#9936) --- src/lib/db/index.ts | 2 ++ .../feature-links/feature-link.e2e.test.ts | 5 +++++ .../feature-links/feature-links-read-model.ts | 18 ++++++++++++++++-- src/lib/metrics.ts | 19 +++++++++++++++++++ src/lib/types/stores.ts | 3 +++ src/test/fixtures/store.ts | 2 ++ 6 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 3b2972b874..aa6740d14a 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -67,6 +67,7 @@ import { UniqueConnectionStore } from '../features/unique-connection/unique-conn import { UniqueConnectionReadModel } from '../features/unique-connection/unique-connection-read-model'; import { FeatureLinkStore } from '../features/feature-links/feature-link-store'; import { UnknownFlagsStore } from '../features/metrics/unknown-flags/unknown-flags-store'; +import { FeatureLinksReadModel } from '../features/feature-links/feature-links-read-model'; export const createStores = ( config: IUnleashConfig, @@ -205,6 +206,7 @@ export const createStores = ( new ReleasePlanMilestoneStrategyStore(db, config), featureLinkStore: new FeatureLinkStore(db, config), unknownFlagsStore: new UnknownFlagsStore(db), + featureLinkReadModel: new FeatureLinksReadModel(db, eventBus), }; }; 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 406e517058..edfaf43fd8 100644 --- a/src/lib/features/feature-links/feature-link.e2e.test.ts +++ b/src/lib/features/feature-links/feature-link.e2e.test.ts @@ -154,6 +154,11 @@ test('should manage feature links', async () => { domain: 'example_another', }, ]); + const topDomainsMemoized = await featureLinkReadModel.getTopDomains(); + expect(topDomainsMemoized).toMatchObject([ + { domain: 'example_another', count: 1 }, + { domain: 'example', count: 1 }, + ]); const [event1, event2, event3] = await eventStore.getEvents(); expect([event1, event2, event3]).toMatchObject([ 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 0701d190de..d8d32c8299 100644 --- a/src/lib/features/feature-links/feature-links-read-model.ts +++ b/src/lib/features/feature-links/feature-links-read-model.ts @@ -6,10 +6,15 @@ import type { import metricsHelper from '../../util/metrics-helper'; import { DB_TIME } from '../../metric-events'; import type EventEmitter from 'events'; +import memoizee from 'memoizee'; +import { hoursToMilliseconds } from 'date-fns'; export class FeatureLinksReadModel implements IFeatureLinksReadModel { private db: Db; private timer: Function; + private _getTopDomainsMemoized: () => Promise< + { domain: string; count: number }[] + >; constructor(db: Db, eventBus: EventEmitter) { this.db = db; @@ -18,9 +23,18 @@ export class FeatureLinksReadModel implements IFeatureLinksReadModel { store: 'feature_links', action, }); + + this._getTopDomainsMemoized = memoizee(this._getTopDomains.bind(this), { + promise: true, + maxAge: hoursToMilliseconds(1), + }); } - async getTopDomains(): Promise<{ domain: string; count: number }[]> { + public getTopDomains(): Promise<{ domain: string; count: number }[]> { + return this._getTopDomainsMemoized(); + } + + async _getTopDomains(): Promise<{ domain: string; count: number }[]> { const stopTimer = this.timer('getTopDomains'); const topDomains = await this.db .from('feature_link') @@ -29,7 +43,7 @@ export class FeatureLinksReadModel implements IFeatureLinksReadModel { .whereNotNull('domain') .groupBy('domain') .orderBy('count', 'desc') - .limit(20); + .limit(3); stopTimer(); return topDomains.map(({ domain, count }) => ({ diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index cc7e04433f..403bc657d7 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -656,6 +656,25 @@ export function registerPrometheusMetrics( })), }); + dbMetrics.registerGaugeDbMetric({ + name: 'feature_link_by_domain', + help: 'Count most popular domains used in feature links', + labelNames: ['domain'], + query: () => { + if (flagResolver.isEnabled('featureLinks')) { + return stores.featureLinkReadModel.getTopDomains(); + } + return Promise.resolve([]); + }, + map: (result) => + result.map(({ domain, count }) => ({ + value: count, + labels: { + domain, + }, + })), + }); + const featureLifecycleStageEnteredCounter = createCounter({ name: 'feature_lifecycle_stage_entered', help: 'Count how many features entered a given stage', diff --git a/src/lib/types/stores.ts b/src/lib/types/stores.ts index 6c0b6b823f..cb2c5d9bf1 100644 --- a/src/lib/types/stores.ts +++ b/src/lib/types/stores.ts @@ -61,6 +61,7 @@ import { ReleasePlanMilestoneStore } from '../features/release-plans/release-pla import { ReleasePlanMilestoneStrategyStore } from '../features/release-plans/release-plan-milestone-strategy-store'; import type { IFeatureLinkStore } from '../features/feature-links/feature-link-store-type'; import { IUnknownFlagsStore } from '../features/metrics/unknown-flags/unknown-flags-store'; +import { IFeatureLinksReadModel } from '../features/feature-links/feature-links-read-model-type'; export interface IUnleashStores { accessStore: IAccessStore; @@ -126,6 +127,7 @@ export interface IUnleashStores { releasePlanMilestoneStrategyStore: ReleasePlanMilestoneStrategyStore; featureLinkStore: IFeatureLinkStore; unknownFlagsStore: IUnknownFlagsStore; + featureLinkReadModel: IFeatureLinksReadModel; } export { @@ -189,4 +191,5 @@ export { ReleasePlanMilestoneStrategyStore, type IFeatureLinkStore, IUnknownFlagsStore, + IFeatureLinksReadModel, }; diff --git a/src/test/fixtures/store.ts b/src/test/fixtures/store.ts index 4449f37eeb..918811aa27 100644 --- a/src/test/fixtures/store.ts +++ b/src/test/fixtures/store.ts @@ -64,6 +64,7 @@ import { FakeUniqueConnectionStore } from '../../lib/features/unique-connection/ import { UniqueConnectionReadModel } from '../../lib/features/unique-connection/unique-connection-read-model'; import FakeFeatureLinkStore from '../../lib/features/feature-links/fake-feature-link-store'; import { FakeUnknownFlagsStore } from '../../lib/features/metrics/unknown-flags/fake-unknown-flags-store'; +import { FakeFeatureLinksReadModel } from '../../lib/features/feature-links/fake-feature-links-read-model'; const db = { select: () => ({ @@ -143,6 +144,7 @@ const createStores: () => IUnleashStores = () => { {} as ReleasePlanMilestoneStrategyStore, featureLinkStore: new FakeFeatureLinkStore(), unknownFlagsStore, + featureLinkReadModel: new FakeFeatureLinksReadModel(), }; };