mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
feat: health score backend (#8687)
Implements the backend part for project health. --------- Co-authored-by: Thomas Heartman <thomas@getunleash.io>
This commit is contained in:
parent
e039cdc85c
commit
d6c03fdbfa
@ -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,
|
||||
|
@ -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<ProjectStatusSchema> {
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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 },
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user