diff --git a/src/lib/db/client-applications-store.ts b/src/lib/db/client-applications-store.ts index ea4958d5bc..300ba15449 100644 --- a/src/lib/db/client-applications-store.ts +++ b/src/lib/db/client-applications-store.ts @@ -10,6 +10,7 @@ import { Logger, LogProvider } from '../logger'; import { Db } from './db'; import { IApplicationOverview } from '../features/metrics/instance/models'; import { applySearchFilters } from '../features/feature-search/search-utils'; +import { ApplicationOverviewIssuesSchema } from '../openapi/spec/application-overview-issues-schema'; const COLUMNS = [ 'app_name', @@ -305,7 +306,6 @@ export default class ClientApplicationsStore ); }) .where('a.app_name', appName); - const rows = await query; if (!rows.length) { throw new NotFoundError(`Could not find appName=${appName}`); @@ -316,22 +316,41 @@ export default class ClientApplicationsStore mapApplicationOverviewData(rows: any[]): IApplicationOverview { const featureCount = new Set(rows.map((row) => row.feature_name)).size; + const missingFeatures: Set = new Set(); const environments = rows.reduce((acc, row) => { - const { environment, instance_id, sdk_version, last_seen } = row; + const { + environment, + instance_id, + sdk_version, + last_seen, + project, + feature_name, + } = row; + + if (!environment) return acc; + + if (!project && feature_name) { + missingFeatures.add(feature_name); + } + let env = acc.find((e) => e.name === environment); if (!env) { env = { name: environment, - instanceCount: 1, + instanceCount: instance_id ? 1 : 0, sdks: sdk_version ? [sdk_version] : [], lastSeen: last_seen, - uniqueInstanceIds: new Set([instance_id]), + uniqueInstanceIds: new Set( + instance_id ? [instance_id] : [], + ), }; acc.push(env); } else { - env.uniqueInstanceIds.add(instance_id); - env.instanceCount = env.uniqueInstanceIds.size; + if (instance_id && !env.uniqueInstanceIds.has(instance_id)) { + env.uniqueInstanceIds.add(instance_id); + env.instanceCount = env.uniqueInstanceIds.size; + } if (sdk_version && !env.sdks.includes(sdk_version)) { env.sdks.push(sdk_version); } @@ -342,12 +361,21 @@ export default class ClientApplicationsStore return acc; }, []); - environments.forEach((env) => { delete env.uniqueInstanceIds; env.sdks.sort(); }); + const issues: ApplicationOverviewIssuesSchema[] = + missingFeatures.size > 0 + ? [ + { + type: 'missingFeatures', + items: [...missingFeatures], + }, + ] + : []; + return { projects: [ ...new Set( @@ -358,6 +386,7 @@ export default class ClientApplicationsStore ], featureCount, environments, + issues, }; } } diff --git a/src/lib/openapi/spec/application-overview-issues-schema.ts b/src/lib/openapi/spec/application-overview-issues-schema.ts index 59ed456564..231915f767 100644 --- a/src/lib/openapi/spec/application-overview-issues-schema.ts +++ b/src/lib/openapi/spec/application-overview-issues-schema.ts @@ -9,7 +9,7 @@ export const applicationOverviewIssuesSchema = { properties: { type: { type: 'string', - enum: ['missingFeature', 'missingStrategy'], + enum: ['missingFeatures', 'missingStrategies'], description: 'The name of this action.', }, items: { diff --git a/src/lib/openapi/spec/application-overview-schema.test.ts b/src/lib/openapi/spec/application-overview-schema.test.ts index 7edf749bc8..6e5b959a93 100644 --- a/src/lib/openapi/spec/application-overview-schema.test.ts +++ b/src/lib/openapi/spec/application-overview-schema.test.ts @@ -5,8 +5,8 @@ test('applicationOverviewSchema', () => { projects: ['default', 'dx'], featureCount: 12, issues: [ - { type: 'missingFeature', items: ['feature1'] }, - { type: 'missingStrategy', items: ['strategy1'] }, + { type: 'missingFeatures', items: ['feature1'] }, + { type: 'missingStrategies', items: ['strategy1'] }, ], environments: [ { diff --git a/src/lib/openapi/spec/application-overview-schema.ts b/src/lib/openapi/spec/application-overview-schema.ts index 6c484e7b77..6b11d7679e 100644 --- a/src/lib/openapi/spec/application-overview-schema.ts +++ b/src/lib/openapi/spec/application-overview-schema.ts @@ -8,7 +8,7 @@ export const applicationOverviewSchema = { description: "Data about an application that's connected to Unleash via an SDK.", additionalProperties: false, - required: ['projects', 'featureCount', 'environments'], + required: ['projects', 'featureCount', 'environments', 'issues'], properties: { projects: { description: 'The list of projects the application has been using.', diff --git a/src/test/e2e/api/admin/applications.e2e.test.ts b/src/test/e2e/api/admin/applications.e2e.test.ts index 2304faa043..3e4c3dcd03 100644 --- a/src/test/e2e/api/admin/applications.e2e.test.ts +++ b/src/test/e2e/api/admin/applications.e2e.test.ts @@ -122,6 +122,7 @@ test('should show correct number of total', async () => { const expected = { projects: ['default'], + issues: [], environments: [ { instanceCount: 2, @@ -134,3 +135,49 @@ test('should show correct number of total', async () => { expect(body).toMatchObject(expected); }); + +test('should show missing features', async () => { + await Promise.all([ + app.createFeature('toggle-name-1'), + app.request.post('/api/client/register').send({ + appName: metrics.appName, + instanceId: metrics.instanceId, + strategies: ['default'], + sdkVersion: 'unleash-client-test', + started: Date.now(), + interval: 10, + }), + ]); + await app.services.clientInstanceService.bulkAdd(); + await app.request + .post('/api/client/metrics') + .set('Authorization', defaultToken.secret) + .send(metrics) + .expect(202); + + await app.services.clientMetricsServiceV2.bulkAdd(); + + const { body } = await app.request + .get(`/api/admin/metrics/applications/${metrics.appName}/overview`) + .expect(200); + + const expected = { + projects: ['default'], + issues: [ + { + type: 'missingFeatures', + items: ['toggle-name-2', 'toggle-name-3'], + }, + ], + environments: [ + { + instanceCount: 1, + name: 'default', + sdks: ['unleash-client-test'], + }, + ], + featureCount: 3, + }; + + expect(body).toMatchObject(expected); +});