1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

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.
This commit is contained in:
Thomas Heartman 2024-11-11 11:22:28 +01:00 committed by GitHub
parent 92f7acbfc4
commit 8493bee272
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 103 additions and 0 deletions

View File

@ -10,6 +10,10 @@ import SegmentStore from '../segment/segment-store';
import FakeSegmentStore from '../../../test/fixtures/fake-segment-store'; import FakeSegmentStore from '../../../test/fixtures/fake-segment-store';
import { PersonalDashboardReadModel } from '../personal-dashboard/personal-dashboard-read-model'; import { PersonalDashboardReadModel } from '../personal-dashboard/personal-dashboard-read-model';
import { FakePersonalDashboardReadModel } from '../personal-dashboard/fake-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 = ( export const createProjectStatusService = (
db: Db, db: Db,
@ -34,6 +38,8 @@ export const createProjectStatusService = (
config.getLogger, config.getLogger,
config.flagResolver, config.flagResolver,
); );
const projectLifecycleSummaryReadModel =
createProjectLifecycleSummaryReadModel(db, config);
return new ProjectStatusService( return new ProjectStatusService(
{ {
@ -43,6 +49,7 @@ export const createProjectStatusService = (
segmentStore, segmentStore,
}, },
new PersonalDashboardReadModel(db), new PersonalDashboardReadModel(db),
projectLifecycleSummaryReadModel,
); );
}; };
@ -59,6 +66,7 @@ export const createFakeProjectStatusService = () => {
segmentStore, segmentStore,
}, },
new FakePersonalDashboardReadModel(), new FakePersonalDashboardReadModel(),
createFakeProjectLifecycleSummaryReadModel(),
); );
return { return {

View File

@ -7,6 +7,7 @@ import type {
IUnleashStores, IUnleashStores,
} from '../../types'; } from '../../types';
import type { IPersonalDashboardReadModel } from '../personal-dashboard/personal-dashboard-read-model-type'; 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 { export class ProjectStatusService {
private eventStore: IEventStore; private eventStore: IEventStore;
@ -14,6 +15,7 @@ export class ProjectStatusService {
private apiTokenStore: IApiTokenStore; private apiTokenStore: IApiTokenStore;
private segmentStore: ISegmentStore; private segmentStore: ISegmentStore;
private personalDashboardReadModel: IPersonalDashboardReadModel; private personalDashboardReadModel: IPersonalDashboardReadModel;
private projectLifecycleSummaryReadModel: IProjectLifecycleSummaryReadModel;
constructor( constructor(
{ {
@ -26,12 +28,14 @@ export class ProjectStatusService {
'eventStore' | 'projectStore' | 'apiTokenStore' | 'segmentStore' 'eventStore' | 'projectStore' | 'apiTokenStore' | 'segmentStore'
>, >,
personalDashboardReadModel: IPersonalDashboardReadModel, personalDashboardReadModel: IPersonalDashboardReadModel,
projectLifecycleReadModel: IProjectLifecycleSummaryReadModel,
) { ) {
this.eventStore = eventStore; this.eventStore = eventStore;
this.projectStore = projectStore; this.projectStore = projectStore;
this.apiTokenStore = apiTokenStore; this.apiTokenStore = apiTokenStore;
this.segmentStore = segmentStore; this.segmentStore = segmentStore;
this.personalDashboardReadModel = personalDashboardReadModel; this.personalDashboardReadModel = personalDashboardReadModel;
this.projectLifecycleSummaryReadModel = projectLifecycleReadModel;
} }
async getProjectStatus(projectId: string): Promise<ProjectStatusSchema> { async getProjectStatus(projectId: string): Promise<ProjectStatusSchema> {
@ -42,6 +46,7 @@ export class ProjectStatusService {
segments, segments,
activityCountByDate, activityCountByDate,
healthScores, healthScores,
lifecycleSummary,
] = await Promise.all([ ] = await Promise.all([
this.projectStore.getConnectedEnvironmentCountForProject(projectId), this.projectStore.getConnectedEnvironmentCountForProject(projectId),
this.projectStore.getMembersCountByProject(projectId), this.projectStore.getMembersCountByProject(projectId),
@ -49,6 +54,9 @@ export class ProjectStatusService {
this.segmentStore.getProjectSegmentCount(projectId), this.segmentStore.getProjectSegmentCount(projectId),
this.eventStore.getProjectRecentEventActivity(projectId), this.eventStore.getProjectRecentEventActivity(projectId),
this.personalDashboardReadModel.getLatestHealthScores(projectId, 4), this.personalDashboardReadModel.getLatestHealthScores(projectId, 4),
this.projectLifecycleSummaryReadModel.getProjectLifecycleSummary(
projectId,
),
]); ]);
const averageHealth = healthScores.length const averageHealth = healthScores.length
@ -65,6 +73,7 @@ export class ProjectStatusService {
}, },
activityCountByDate, activityCountByDate,
averageHealth, averageHealth,
lifecycleSummary,
}; };
} }
} }

View File

@ -256,3 +256,33 @@ test('project health should be correct average', async () => {
expect(body.averageHealth).toBe(40); 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,
},
});
});

View File

@ -1,6 +1,29 @@
import type { FromSchema } from 'json-schema-to-ts'; import type { FromSchema } from 'json-schema-to-ts';
import { projectActivitySchema } from './project-activity-schema'; 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 = { export const projectStatusSchema = {
$id: '#/components/schemas/projectStatusSchema', $id: '#/components/schemas/projectStatusSchema',
type: 'object', 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: { components: {
schemas: { schemas: {