diff --git a/src/lib/db/client-applications-store.ts b/src/lib/db/client-applications-store.ts index e6c7d8b922..9994dd923a 100644 --- a/src/lib/db/client-applications-store.ts +++ b/src/lib/db/client-applications-store.ts @@ -91,7 +91,7 @@ const reduceRows = (rows: any[]): IClientApplication[] => { return Object.values(appsObj); }; -const remapRow = (input) => { +const remapRow = (input: Partial) => { const temp = { app_name: input.appName, updated_at: input.updatedAt || new Date(), @@ -152,10 +152,28 @@ export default class ClientApplicationsStore async bulkUpsert(apps: Partial[]): Promise { const rows = apps.map(remapRow); + const uniqueRows = Object.values( + rows.reduce((acc, row) => { + if (row.app_name) { + acc[row.app_name] = row; + } + return acc; + }, {}), + ); const usageRows = apps.flatMap(this.remapUsageRow); - await this.db(TABLE).insert(rows).onConflict('app_name').merge(); + const uniqueUsageRows = Object.values( + usageRows.reduce((acc, row) => { + if (row.app_name) { + acc[`${row.app_name} ${row.project} ${row.environment}`] = + row; + } + return acc; + }, {}), + ); + + await this.db(TABLE).insert(uniqueRows).onConflict('app_name').merge(); await this.db(TABLE_USAGE) - .insert(usageRows) + .insert(uniqueUsageRows) .onConflict(['app_name', 'project', 'environment']) .merge(); } @@ -444,7 +462,7 @@ export default class ClientApplicationsStore }; } - private remapUsageRow = (input) => { + private remapUsageRow = (input: Partial) => { if (!input.projects || input.projects.length === 0) { return [ { diff --git a/src/lib/features/metrics/instance/instance-service.ts b/src/lib/features/metrics/instance/instance-service.ts index 794019f75f..33682ddff9 100644 --- a/src/lib/features/metrics/instance/instance-service.ts +++ b/src/lib/features/metrics/instance/instance-service.ts @@ -173,8 +173,19 @@ export default class ClientInstanceService { const uniqueRegistrations = Object.values(this.seenClients); const uniqueApps: Partial[] = Object.values( uniqueRegistrations.reduce((soFar, reg) => { - // eslint-disable-next-line no-param-reassign - soFar[reg.appName] = reg; + let existingProjects = []; + if (soFar[`${reg.appName} ${reg.environment}`]) { + existingProjects = + soFar[`${reg.appName} ${reg.environment}`] + .projects || []; + } + soFar[`${reg.appName} ${reg.environment}`] = { + ...reg, + projects: [ + ...existingProjects, + ...(reg.projects || []), + ], + }; return soFar; }, {}), ); diff --git a/src/lib/features/metrics/instance/metrics.test.ts b/src/lib/features/metrics/instance/metrics.test.ts index 07f1c4ae88..8769cd7335 100644 --- a/src/lib/features/metrics/instance/metrics.test.ts +++ b/src/lib/features/metrics/instance/metrics.test.ts @@ -61,6 +61,7 @@ afterAll(async () => { afterEach(async () => { await stores.featureToggleStore.deleteAll(); + await stores.clientApplicationsStore.deleteAll(); }); test('should validate client metrics', () => { @@ -125,7 +126,7 @@ test('should accept client metrics with yes/no with metricsV2', async () => { }) .expect(202); - testRunner.destroy(); + await testRunner.destroy(); }); test('should accept client metrics with variants', () => { @@ -343,6 +344,99 @@ describe('bulk metrics', () => { }); }); + test('should respect project from token', async () => { + const frontendApp: BulkRegistrationSchema = { + appName: 'application-name-token', + instanceId: 'browser', + environment: 'production', + sdkVersion: 'unleash-client-js:1.0.0', + sdkType: 'frontend', + projects: ['project-a', 'project-b'], + }; + const backendApp: BulkRegistrationSchema = { + appName: 'application-name-token', + instanceId: 'instance1234', + environment: 'development', + sdkVersion: 'unleash-client-node', + sdkType: 'backend', + started: '1952-03-11T12:00:00.000Z', + interval: 15000, + projects: ['project-b', 'project-c'], + }; + const defaultApp: BulkRegistrationSchema = { + appName: 'application-name-token', + instanceId: 'instance5678', + environment: 'development', + sdkVersion: 'unleash-client-java', + sdkType: null, + started: '1952-03-11T12:00:00.000Z', + interval: 15000, + projects: ['project-c', 'project-d'], + }; + await request + .post('/api/client/metrics/bulk') + .send({ + applications: [frontendApp, backendApp, defaultApp], + metrics: [], + }) + .expect(202); + + await services.clientInstanceService.bulkAdd(); + const app = await services.clientInstanceService.getApplication( + 'application-name-token', + ); + + expect(app).toMatchObject({ + appName: 'application-name-token', + instances: [ + { + instanceId: 'instance1234', + sdkVersion: 'unleash-client-node', + environment: 'development', + }, + { + instanceId: 'instance5678', + sdkVersion: 'unleash-client-java', + environment: 'development', + }, + { + instanceId: 'browser', + sdkVersion: 'unleash-client-js:1.0.0', + environment: 'production', + }, + ], + }); + + const applications = + await stores.clientApplicationsStore.getApplications({ + limit: 10, + offset: 0, + sortBy: 'name', + sortOrder: 'asc', + }); + expect(applications).toMatchObject({ + applications: [ + { + usage: [ + { + project: 'project-a', + environments: ['production'], + }, + { + project: 'project-b', + environments: ['production', 'development'], + }, + { + project: 'project-c', + environments: ['development'], + }, + { project: 'project-d', environments: ['development'] }, + ], + }, + ], + }); + }); + test('filters out metrics for environments we do not have access for. No auth setup so we can only access default env', async () => { const now = new Date(); diff --git a/src/lib/features/metrics/instance/metrics.ts b/src/lib/features/metrics/instance/metrics.ts index 4d6141f505..183316d72f 100644 --- a/src/lib/features/metrics/instance/metrics.ts +++ b/src/lib/features/metrics/instance/metrics.ts @@ -158,6 +158,7 @@ export default class ClientMetricsController extends Controller { environment: app.environment, sdkType: app.sdkType, sdkVersion: app.sdkVersion, + projects: app.projects, }); } } else { diff --git a/src/lib/openapi/spec/bulk-registration-schema.ts b/src/lib/openapi/spec/bulk-registration-schema.ts index edc1489130..fcce8bac97 100644 --- a/src/lib/openapi/spec/bulk-registration-schema.ts +++ b/src/lib/openapi/spec/bulk-registration-schema.ts @@ -64,6 +64,14 @@ export const bulkRegistrationSchema = { type: 'string', }, }, + projects: { + description: 'The list of projects used in the application', + type: 'array', + example: ['projectA', 'projectB'], + items: { + type: 'string', + }, + }, sdkVersion: { description: 'The version the sdk is running. Typically :', diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index ad5737244e..36600eca56 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -478,6 +478,7 @@ export interface IClientApp { seenToggles?: string[]; metricsCount?: number; strategies?: string[] | Record[]; + projects?: string[]; count?: number; started?: string | number | Date; interval?: number; diff --git a/src/lib/types/stores/client-applications-store.ts b/src/lib/types/stores/client-applications-store.ts index e32bac5699..1ae7430377 100644 --- a/src/lib/types/stores/client-applications-store.ts +++ b/src/lib/types/stores/client-applications-store.ts @@ -19,6 +19,8 @@ export interface IClientApplication { icon: string; strategies: string[]; usage?: IClientApplicationUsage[]; + projects?: string[]; + environment?: string; } export interface IClientApplications {