From e7b757d7e109dd267d96c90e1224cf3eca4a46a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Fri, 29 Aug 2025 10:20:43 +0100 Subject: [PATCH] chore: unknown flags should not include flags that exist in Unleash (#10568) https://linear.app/unleash/issue/2-3825/fetching-unknown-flags-should-not-include-flags-that-currently-exist Fetching unknown flags should not include flags that currently exist in Unleash. This is especially noticeable if you create a flag with an unknown flag's name, since it won't go away from the list right away. --- .../unknown-flags/unknown-flags-store.ts | 24 +-- .../unknown-flags/unknown-flags.e2e.test.ts | 137 ++++++++++++++++++ 2 files changed, 152 insertions(+), 9 deletions(-) diff --git a/src/lib/features/metrics/unknown-flags/unknown-flags-store.ts b/src/lib/features/metrics/unknown-flags/unknown-flags-store.ts index f665f75a3d..9ac4d8098f 100644 --- a/src/lib/features/metrics/unknown-flags/unknown-flags-store.ts +++ b/src/lib/features/metrics/unknown-flags/unknown-flags-store.ts @@ -66,18 +66,24 @@ export class UnknownFlagsStore implements IUnknownFlagsStore { } async getAll({ limit, orderBy }: QueryParams = {}): Promise { - let query = this.db(`${TABLE} AS uf`).select( - 'uf.name', - 'uf.app_name', - 'uf.seen_at', - 'uf.environment', - this.db.raw( - `(SELECT MAX(e.created_at) + let query = this.db(`${TABLE} AS uf`) + .select( + 'uf.name', + 'uf.app_name', + 'uf.seen_at', + 'uf.environment', + this.db.raw( + `(SELECT MAX(e.created_at) FROM ${TABLE_EVENTS} AS e WHERE e.feature_name = uf.name) AS last_event_at`, - ), - ); + ), + ) + .whereNotExists( + this.db('features as f') + .select(this.db.raw('1')) + .whereRaw('f.name = uf.name'), + ); if (orderBy) { query = query.orderBy(orderBy); diff --git a/src/lib/features/metrics/unknown-flags/unknown-flags.e2e.test.ts b/src/lib/features/metrics/unknown-flags/unknown-flags.e2e.test.ts index 898356a3dd..be89d2811d 100644 --- a/src/lib/features/metrics/unknown-flags/unknown-flags.e2e.test.ts +++ b/src/lib/features/metrics/unknown-flags/unknown-flags.e2e.test.ts @@ -182,3 +182,140 @@ describe('should register unknown flags', () => { ); }); }); + +describe('should fetch unknown flags', () => { + test('returns empty list by default', async () => { + const res = await request + .get('/api/admin/metrics/unknown-flags') + .expect(200) + .expect('Content-Type', /json/); + + expect(res.body).toMatchObject({ + unknownFlags: [], + }); + }); + + test('returns list of unknown flags', async () => { + const unknownFlag: BulkRegistrationSchema = { + appName: 'demo', + instanceId: '1', + environment: 'development', + sdkVersion: 'unleash-client-js:1.0.0', + sdkType: 'frontend', + }; + + await request + .post('/api/client/metrics/bulk') + .send({ + applications: [unknownFlag], + metrics: [ + { + featureName: 'unknown_flag_1', + environment: 'development', + appName: 'demo', + timestamp: startOfHour(new Date()), + yes: 1337, + no: 0, + variants: {}, + }, + { + featureName: 'unknown_flag_2', + environment: 'development', + appName: 'demo', + timestamp: startOfHour(new Date()), + yes: 200, + no: 100, + variants: {}, + }, + ], + }) + .expect(202); + + await services.unknownFlagsService.flush(); + + const res = await request + .get('/api/admin/metrics/unknown-flags') + .expect(200) + .expect('Content-Type', /json/); + + expect(res.body.unknownFlags).toHaveLength(2); + expect(res.body.unknownFlags).toEqual([ + expect.objectContaining({ + name: 'unknown_flag_1', + environment: 'development', + appName: 'demo', + lastEventAt: null, + }), + expect.objectContaining({ + name: 'unknown_flag_2', + environment: 'development', + appName: 'demo', + lastEventAt: null, + }), + ]); + }); + + test('does not include flags that have since been created in Unleash', async () => { + const unknownFlag: BulkRegistrationSchema = { + appName: 'demo', + instanceId: '1', + environment: 'development', + sdkVersion: 'unleash-client-js:1.0.0', + sdkType: 'frontend', + }; + + await request + .post('/api/client/metrics/bulk') + .send({ + applications: [unknownFlag], + metrics: [ + { + featureName: 'flag_that_will_be_created', + environment: 'development', + appName: 'demo', + timestamp: startOfHour(new Date()), + yes: 1337, + no: 0, + variants: {}, + }, + { + featureName: 'unknown_flag_2', + environment: 'development', + appName: 'demo', + timestamp: startOfHour(new Date()), + yes: 200, + no: 100, + variants: {}, + }, + ], + }) + .expect(202); + + await services.unknownFlagsService.flush(); + + await request + .post('/api/admin/projects/default/features') + .send({ + name: 'flag_that_will_be_created', + description: '', + type: 'release', + impressionData: false, + }) + .expect(201); + + const res = await request + .get('/api/admin/metrics/unknown-flags') + .expect(200) + .expect('Content-Type', /json/); + + expect(res.body.unknownFlags).toHaveLength(1); + expect(res.body.unknownFlags).toEqual([ + expect.objectContaining({ + name: 'unknown_flag_2', + environment: 'development', + appName: 'demo', + lastEventAt: null, + }), + ]); + }); +});