From 1897f8a19d8f01afc3dc05f282d478ddbbe877ea Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 5 Nov 2024 11:12:08 +0100 Subject: [PATCH] chore: add connected environments to project status payload (#8645) This PR adds connected environments to the project status payload. It's done by: - adding a new `getConnectedEnvironmentCountForProject` method to the project store (I opted for this approach instead of creating a new view model because it already has a `getEnvironmentsForProject` method) - adding the project store to the project status service - updating the schema For the schema, I opted for adding a `resources` property, under which I put `connectedEnvironments`. My thinking was that if we want to add the rest of the project resources (that go in the resources widget), it'd make sense to group those together inside an object. However, I'd also be happy to place the property on the top level. If you have opinions one way or the other, let me know. As for the count, we're currently only counting environments that have metrics and that are active for the current project. --- .../createProjectStatusService.ts | 12 ++++- .../project-status/project-status-service.ts | 16 ++++++- .../projects-status.e2e.test.ts | 46 +++++++++++++++++++ .../features/project/project-store-type.ts | 2 + src/lib/features/project/project-store.ts | 16 +++++++ .../spec/project-status-schema.test.ts | 1 + src/lib/openapi/spec/project-status-schema.ts | 15 +++++- src/test/fixtures/fake-project-store.ts | 4 ++ 8 files changed, 108 insertions(+), 4 deletions(-) diff --git a/src/lib/features/project-status/createProjectStatusService.ts b/src/lib/features/project-status/createProjectStatusService.ts index 3448baa31693..6db4fa1a45ea 100644 --- a/src/lib/features/project-status/createProjectStatusService.ts +++ b/src/lib/features/project-status/createProjectStatusService.ts @@ -2,19 +2,29 @@ import type { Db, IUnleashConfig } from '../../server-impl'; import { ProjectStatusService } from './project-status-service'; import EventStore from '../events/event-store'; import FakeEventStore from '../../../test/fixtures/fake-event-store'; +import ProjectStore from '../project/project-store'; +import FakeProjectStore from '../../../test/fixtures/fake-project-store'; export const createProjectStatusService = ( db: Db, config: IUnleashConfig, ): ProjectStatusService => { const eventStore = new EventStore(db, config.getLogger); - return new ProjectStatusService({ eventStore }); + const projectStore = new ProjectStore( + db, + config.eventBus, + config.getLogger, + config.flagResolver, + ); + return new ProjectStatusService({ eventStore, projectStore }); }; export const createFakeProjectStatusService = () => { const eventStore = new FakeEventStore(); + const projectStore = new FakeProjectStore(); const projectStatusService = new ProjectStatusService({ eventStore, + projectStore, }); return { diff --git a/src/lib/features/project-status/project-status-service.ts b/src/lib/features/project-status/project-status-service.ts index b6d905b36c63..47352ca3e031 100644 --- a/src/lib/features/project-status/project-status-service.ts +++ b/src/lib/features/project-status/project-status-service.ts @@ -1,14 +1,26 @@ import type { ProjectStatusSchema } from '../../openapi'; -import type { IEventStore, IUnleashStores } from '../../types'; +import type { IEventStore, IProjectStore, IUnleashStores } from '../../types'; export class ProjectStatusService { private eventStore: IEventStore; - constructor({ eventStore }: Pick) { + private projectStore: IProjectStore; + + constructor({ + eventStore, + projectStore, + }: Pick) { this.eventStore = eventStore; + this.projectStore = projectStore; } async getProjectStatus(projectId: string): Promise { return { + resources: { + connectedEnvironments: + await this.projectStore.getConnectedEnvironmentCountForProject( + projectId, + ), + }, activityCountByDate: await this.eventStore.getProjectEventActivity(projectId), }; diff --git a/src/lib/features/project-status/projects-status.e2e.test.ts b/src/lib/features/project-status/projects-status.e2e.test.ts index dfa9bd93ca31..f19171d2f209 100644 --- a/src/lib/features/project-status/projects-status.e2e.test.ts +++ b/src/lib/features/project-status/projects-status.e2e.test.ts @@ -8,6 +8,7 @@ import { FEATURE_CREATED, type IUnleashConfig } from '../../types'; import type { EventService } from '../../services'; import { createEventsService } from '../events/createEventsService'; import { createTestConfig } from '../../../test/config/test-config'; +import { randomId } from '../../util'; let app: IUnleashTest; let db: ITestDb; @@ -99,3 +100,48 @@ test('project insights should return correct count for each day', async () => { ], }); }); + +test('project status should return environments with connected SDKs', async () => { + const flagName = randomId(); + await app.createFeature(flagName); + + const envs = + await app.services.environmentService.getProjectEnvironments('default'); + expect(envs.some((env) => env.name === 'default')).toBeTruthy(); + + const appName = 'blah'; + const environment = 'default'; + await db.stores.clientMetricsStoreV2.batchInsertMetrics([ + { + featureName: `flag-doesnt-exist`, + appName, + environment, + timestamp: new Date(), + yes: 5, + no: 2, + }, + { + featureName: flagName, + appName: `web2`, + environment, + timestamp: new Date(), + yes: 5, + no: 2, + }, + { + featureName: flagName, + appName, + environment: 'not-a-real-env', + timestamp: new Date(), + yes: 2, + no: 2, + }, + ]); + + const { body } = await app.request + .get('/api/admin/projects/default/status') + .expect('Content-Type', /json/) + .expect(200); + + expect(body.resources.connectedEnvironments).toBe(1); +}); diff --git a/src/lib/features/project/project-store-type.ts b/src/lib/features/project/project-store-type.ts index 9f3a190a27c4..33218f5d9487 100644 --- a/src/lib/features/project/project-store-type.ts +++ b/src/lib/features/project/project-store-type.ts @@ -93,6 +93,8 @@ export interface IProjectStore extends Store { getEnvironmentsForProject(id: string): Promise; + getConnectedEnvironmentCountForProject(id: string): Promise; + getMembersCountByProject(projectId: string): Promise; getMembersCountByProjectAfterDate( diff --git a/src/lib/features/project/project-store.ts b/src/lib/features/project/project-store.ts index 5df93193c63e..6f213782f160 100644 --- a/src/lib/features/project/project-store.ts +++ b/src/lib/features/project/project-store.ts @@ -390,6 +390,22 @@ class ProjectStore implements IProjectStore { return rows.map(this.mapProjectEnvironmentRow); } + async getConnectedEnvironmentCountForProject(id: string): Promise { + const [{ count }] = (await this.db + .countDistinct('cme.environment') + .from('client_metrics_env as cme') + .innerJoin('features', 'cme.feature_name', 'features.name') + .innerJoin('projects', 'features.project', 'projects.id') + .innerJoin( + 'project_environments', + 'cme.environment', + 'project_environments.environment_name', + ) + .where('features.project', id)) as { count: string }[]; + + return Number(count); + } + async getMembersCountByProject(projectId: string): Promise { const members = await this.db .from((db) => { diff --git a/src/lib/openapi/spec/project-status-schema.test.ts b/src/lib/openapi/spec/project-status-schema.test.ts index c3c37a44d13d..23a3ad34ed02 100644 --- a/src/lib/openapi/spec/project-status-schema.test.ts +++ b/src/lib/openapi/spec/project-status-schema.test.ts @@ -7,6 +7,7 @@ test('projectStatusSchema', () => { { date: '2022-12-14', count: 2 }, { date: '2022-12-15', count: 5 }, ], + resources: { connectedEnvironments: 2 }, }; expect( diff --git a/src/lib/openapi/spec/project-status-schema.ts b/src/lib/openapi/spec/project-status-schema.ts index 71085b2da4a3..8683b6ae5e65 100644 --- a/src/lib/openapi/spec/project-status-schema.ts +++ b/src/lib/openapi/spec/project-status-schema.ts @@ -5,7 +5,7 @@ export const projectStatusSchema = { $id: '#/components/schemas/projectStatusSchema', type: 'object', additionalProperties: false, - required: ['activityCountByDate'], + required: ['activityCountByDate', 'resources'], description: 'Schema representing the overall status of a project, including an array of activity records. Each record in the activity array contains a date and a count, providing a snapshot of the project’s activity level over time.', properties: { @@ -14,6 +14,19 @@ export const projectStatusSchema = { description: 'Array of activity records with date and count, representing the project’s daily activity statistics.', }, + resources: { + type: 'object', + additionalProperties: false, + required: ['connectedEnvironments'], + description: 'Key resources within the project', + properties: { + connectedEnvironments: { + type: 'number', + description: + 'The number of environments that have received SDK traffic in this project.', + }, + }, + }, }, components: { schemas: { diff --git a/src/test/fixtures/fake-project-store.ts b/src/test/fixtures/fake-project-store.ts index 7338bc6575bb..438c04ec735d 100644 --- a/src/test/fixtures/fake-project-store.ts +++ b/src/test/fixtures/fake-project-store.ts @@ -214,4 +214,8 @@ export default class FakeProjectStore implements IProjectStore { project.id === id ? { ...project, archivedAt: null } : project, ); } + + async getConnectedEnvironmentCountForProject(): Promise { + return 0; + } }