From 8493bee27250f2c52ac1324b324f1c8a02cd047d Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Mon, 11 Nov 2024 11:22:28 +0100 Subject: [PATCH] feat(1-3085): hook up lifecycle read model data to endpoint (#8709) This PR hooks up the project lifecycle summary read model to the service and exposes the lifecycle summary data in the controller. --- .../createProjectStatusService.ts | 8 +++ .../project-status/project-status-service.ts | 9 +++ .../projects-status.e2e.test.ts | 30 ++++++++++ src/lib/openapi/spec/project-status-schema.ts | 56 +++++++++++++++++++ 4 files changed, 103 insertions(+) diff --git a/src/lib/features/project-status/createProjectStatusService.ts b/src/lib/features/project-status/createProjectStatusService.ts index 429fd5a147ac..9b85c3196577 100644 --- a/src/lib/features/project-status/createProjectStatusService.ts +++ b/src/lib/features/project-status/createProjectStatusService.ts @@ -10,6 +10,10 @@ import SegmentStore from '../segment/segment-store'; import FakeSegmentStore from '../../../test/fixtures/fake-segment-store'; import { PersonalDashboardReadModel } from '../personal-dashboard/personal-dashboard-read-model'; import { FakePersonalDashboardReadModel } from '../personal-dashboard/fake-personal-dashboard-read-model'; +import { + createFakeProjectLifecycleSummaryReadModel, + createProjectLifecycleSummaryReadModel, +} from './project-lifecycle-read-model/createProjectLifecycleSummaryReadModel'; export const createProjectStatusService = ( db: Db, @@ -34,6 +38,8 @@ export const createProjectStatusService = ( config.getLogger, config.flagResolver, ); + const projectLifecycleSummaryReadModel = + createProjectLifecycleSummaryReadModel(db, config); return new ProjectStatusService( { @@ -43,6 +49,7 @@ export const createProjectStatusService = ( segmentStore, }, new PersonalDashboardReadModel(db), + projectLifecycleSummaryReadModel, ); }; @@ -59,6 +66,7 @@ export const createFakeProjectStatusService = () => { segmentStore, }, new FakePersonalDashboardReadModel(), + createFakeProjectLifecycleSummaryReadModel(), ); return { diff --git a/src/lib/features/project-status/project-status-service.ts b/src/lib/features/project-status/project-status-service.ts index 12bc5a80e3d0..6f7606db05ae 100644 --- a/src/lib/features/project-status/project-status-service.ts +++ b/src/lib/features/project-status/project-status-service.ts @@ -7,6 +7,7 @@ import type { IUnleashStores, } from '../../types'; import type { IPersonalDashboardReadModel } from '../personal-dashboard/personal-dashboard-read-model-type'; +import type { IProjectLifecycleSummaryReadModel } from './project-lifecycle-read-model/project-lifecycle-read-model-type'; export class ProjectStatusService { private eventStore: IEventStore; @@ -14,6 +15,7 @@ export class ProjectStatusService { private apiTokenStore: IApiTokenStore; private segmentStore: ISegmentStore; private personalDashboardReadModel: IPersonalDashboardReadModel; + private projectLifecycleSummaryReadModel: IProjectLifecycleSummaryReadModel; constructor( { @@ -26,12 +28,14 @@ export class ProjectStatusService { 'eventStore' | 'projectStore' | 'apiTokenStore' | 'segmentStore' >, personalDashboardReadModel: IPersonalDashboardReadModel, + projectLifecycleReadModel: IProjectLifecycleSummaryReadModel, ) { this.eventStore = eventStore; this.projectStore = projectStore; this.apiTokenStore = apiTokenStore; this.segmentStore = segmentStore; this.personalDashboardReadModel = personalDashboardReadModel; + this.projectLifecycleSummaryReadModel = projectLifecycleReadModel; } async getProjectStatus(projectId: string): Promise { @@ -42,6 +46,7 @@ export class ProjectStatusService { segments, activityCountByDate, healthScores, + lifecycleSummary, ] = await Promise.all([ this.projectStore.getConnectedEnvironmentCountForProject(projectId), this.projectStore.getMembersCountByProject(projectId), @@ -49,6 +54,9 @@ export class ProjectStatusService { this.segmentStore.getProjectSegmentCount(projectId), this.eventStore.getProjectRecentEventActivity(projectId), this.personalDashboardReadModel.getLatestHealthScores(projectId, 4), + this.projectLifecycleSummaryReadModel.getProjectLifecycleSummary( + projectId, + ), ]); const averageHealth = healthScores.length @@ -65,6 +73,7 @@ export class ProjectStatusService { }, activityCountByDate, averageHealth, + lifecycleSummary, }; } } 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 79c1d803040a..16c3397dec4f 100644 --- a/src/lib/features/project-status/projects-status.e2e.test.ts +++ b/src/lib/features/project-status/projects-status.e2e.test.ts @@ -256,3 +256,33 @@ test('project health should be correct average', async () => { expect(body.averageHealth).toBe(40); }); + +test('project status contains lifecycle data', async () => { + const { body } = await app.request + .get('/api/admin/projects/default/status') + .expect('Content-Type', /json/) + .expect(200); + + expect(body.lifecycleSummary).toMatchObject({ + initial: { + averageDays: null, + currentFlags: 0, + }, + preLive: { + averageDays: null, + currentFlags: 0, + }, + live: { + averageDays: null, + currentFlags: 0, + }, + completed: { + averageDays: null, + currentFlags: 0, + }, + archived: { + currentFlags: 0, + last30Days: 0, + }, + }); +}); diff --git a/src/lib/openapi/spec/project-status-schema.ts b/src/lib/openapi/spec/project-status-schema.ts index 1efdd3625984..808ba4f0eabd 100644 --- a/src/lib/openapi/spec/project-status-schema.ts +++ b/src/lib/openapi/spec/project-status-schema.ts @@ -1,6 +1,29 @@ import type { FromSchema } from 'json-schema-to-ts'; import { projectActivitySchema } from './project-activity-schema'; +const stageDataWithAverageDaysSchema = { + type: 'object', + additionalProperties: false, + description: + 'Statistics on feature flags in a given stage in this project.', + required: ['averageDays', 'currentFlags'], + properties: { + averageDays: { + type: 'number', + nullable: true, + description: + "The average number of days a feature flag remains in a stage in this project. Will be null if Unleash doesn't have any data for this stage yet.", + example: 5, + }, + currentFlags: { + type: 'integer', + description: + 'The number of feature flags currently in a stage in this project.', + example: 10, + }, + }, +} as const; + export const projectStatusSchema = { $id: '#/components/schemas/projectStatusSchema', type: 'object', @@ -57,6 +80,39 @@ export const projectStatusSchema = { }, }, }, + lifecycleSummary: { + type: 'object', + additionalProperties: false, + description: 'Feature flag lifecycle statistics for this project.', + required: ['initial', 'preLive', 'live', 'completed', 'archived'], + properties: { + initial: stageDataWithAverageDaysSchema, + preLive: stageDataWithAverageDaysSchema, + live: stageDataWithAverageDaysSchema, + completed: stageDataWithAverageDaysSchema, + archived: { + type: 'object', + additionalProperties: false, + required: ['currentFlags', 'last30Days'], + description: + 'Information on archived flags in this project.', + properties: { + currentFlags: { + type: 'integer', + description: + 'The number of archived feature flags in this project. If a flag is deleted permanently, it will no longer be counted as part of this statistic.', + example: 10, + }, + last30Days: { + type: 'integer', + description: + 'The number of flags in this project that have been changed over the last 30 days.', + example: 5, + }, + }, + }, + }, + }, }, components: { schemas: {