diff --git a/src/lib/features/project-status/createProjectStatusService.ts b/src/lib/features/project-status/createProjectStatusService.ts index 8ad9cda800..429fd5a147 100644 --- a/src/lib/features/project-status/createProjectStatusService.ts +++ b/src/lib/features/project-status/createProjectStatusService.ts @@ -8,6 +8,8 @@ import FakeApiTokenStore from '../../../test/fixtures/fake-api-token-store'; import { ApiTokenStore } from '../../db/api-token-store'; 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'; export const createProjectStatusService = ( db: Db, @@ -33,12 +35,15 @@ export const createProjectStatusService = ( config.flagResolver, ); - return new ProjectStatusService({ - eventStore, - projectStore, - apiTokenStore, - segmentStore, - }); + return new ProjectStatusService( + { + eventStore, + projectStore, + apiTokenStore, + segmentStore, + }, + new PersonalDashboardReadModel(db), + ); }; export const createFakeProjectStatusService = () => { @@ -46,12 +51,15 @@ export const createFakeProjectStatusService = () => { const projectStore = new FakeProjectStore(); const apiTokenStore = new FakeApiTokenStore(); const segmentStore = new FakeSegmentStore(); - const projectStatusService = new ProjectStatusService({ - eventStore, - projectStore, - apiTokenStore, - segmentStore, - }); + const projectStatusService = new ProjectStatusService( + { + eventStore, + projectStore, + apiTokenStore, + segmentStore, + }, + new FakePersonalDashboardReadModel(), + ); return { projectStatusService, diff --git a/src/lib/features/project-status/project-status-service.ts b/src/lib/features/project-status/project-status-service.ts index 763c71a558..12bc5a80e3 100644 --- a/src/lib/features/project-status/project-status-service.ts +++ b/src/lib/features/project-status/project-status-service.ts @@ -6,26 +6,32 @@ import type { ISegmentStore, IUnleashStores, } from '../../types'; +import type { IPersonalDashboardReadModel } from '../personal-dashboard/personal-dashboard-read-model-type'; export class ProjectStatusService { private eventStore: IEventStore; private projectStore: IProjectStore; private apiTokenStore: IApiTokenStore; private segmentStore: ISegmentStore; + private personalDashboardReadModel: IPersonalDashboardReadModel; - constructor({ - eventStore, - projectStore, - apiTokenStore, - segmentStore, - }: Pick< - IUnleashStores, - 'eventStore' | 'projectStore' | 'apiTokenStore' | 'segmentStore' - >) { + constructor( + { + eventStore, + projectStore, + apiTokenStore, + segmentStore, + }: Pick< + IUnleashStores, + 'eventStore' | 'projectStore' | 'apiTokenStore' | 'segmentStore' + >, + personalDashboardReadModel: IPersonalDashboardReadModel, + ) { this.eventStore = eventStore; this.projectStore = projectStore; this.apiTokenStore = apiTokenStore; this.segmentStore = segmentStore; + this.personalDashboardReadModel = personalDashboardReadModel; } async getProjectStatus(projectId: string): Promise { @@ -35,14 +41,21 @@ export class ProjectStatusService { apiTokens, segments, activityCountByDate, + healthScores, ] = await Promise.all([ this.projectStore.getConnectedEnvironmentCountForProject(projectId), this.projectStore.getMembersCountByProject(projectId), this.apiTokenStore.countProjectTokens(projectId), this.segmentStore.getProjectSegmentCount(projectId), this.eventStore.getProjectRecentEventActivity(projectId), + this.personalDashboardReadModel.getLatestHealthScores(projectId, 4), ]); + const averageHealth = healthScores.length + ? healthScores.reduce((acc, num) => acc + num, 0) / + healthScores.length + : 0; + return { resources: { connectedEnvironments, @@ -51,6 +64,7 @@ export class ProjectStatusService { segments, }, activityCountByDate, + averageHealth, }; } } 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 857abeb49a..79c1d80304 100644 --- a/src/lib/features/project-status/projects-status.e2e.test.ts +++ b/src/lib/features/project-status/projects-status.e2e.test.ts @@ -23,6 +23,20 @@ let eventService: EventService; const TEST_USER_ID = -9999; const config: IUnleashConfig = createTestConfig(); +const insertHealthScore = (id: string, health: number) => { + const irrelevantFlagTrendDetails = { + total_flags: 10, + stale_flags: 10, + potentially_stale_flags: 10, + }; + return db.rawDatabase('flag_trends').insert({ + ...irrelevantFlagTrendDetails, + id, + project: 'default', + health, + }); +}; + const getCurrentDateStrings = () => { const today = new Date(); const todayString = today.toISOString().split('T')[0]; @@ -226,3 +240,19 @@ test('project resources should contain the right data', async () => { connectedEnvironments: 1, }); }); + +test('project health should be correct average', async () => { + await insertHealthScore('2024-04', 100); + + await insertHealthScore('2024-05', 0); + await insertHealthScore('2024-06', 0); + await insertHealthScore('2024-07', 90); + await insertHealthScore('2024-08', 70); + + const { body } = await app.request + .get('/api/admin/projects/default/status') + .expect('Content-Type', /json/) + .expect(200); + + expect(body.averageHealth).toBe(40); +}); diff --git a/src/lib/openapi/spec/project-status-schema.test.ts b/src/lib/openapi/spec/project-status-schema.test.ts index cdf8db4514..f00b6cea52 100644 --- a/src/lib/openapi/spec/project-status-schema.test.ts +++ b/src/lib/openapi/spec/project-status-schema.test.ts @@ -3,6 +3,7 @@ import type { ProjectStatusSchema } from './project-status-schema'; test('projectStatusSchema', () => { const data: ProjectStatusSchema = { + averageHealth: 50, activityCountByDate: [ { date: '2022-12-14', count: 2 }, { date: '2022-12-15', count: 5 }, diff --git a/src/lib/openapi/spec/project-status-schema.ts b/src/lib/openapi/spec/project-status-schema.ts index 9a4e03a40f..912a970653 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', 'resources'], + required: ['activityCountByDate', 'resources', 'averageHealth'], 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,12 @@ export const projectStatusSchema = { description: 'Array of activity records with date and count, representing the project’s daily activity statistics.', }, + averageHealth: { + type: 'integer', + minimum: 0, + description: + 'The average health score over the last 4 weeks, indicating whether features are stale or active.', + }, resources: { type: 'object', additionalProperties: false,