mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +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 { ApiTokenStore } from '../../db/api-token-store';
|
||||||
import SegmentStore from '../segment/segment-store';
|
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 { FakePersonalDashboardReadModel } from '../personal-dashboard/fake-personal-dashboard-read-model';
|
||||||
|
|
||||||
export const createProjectStatusService = (
|
export const createProjectStatusService = (
|
||||||
db: Db,
|
db: Db,
|
||||||
@ -33,12 +35,15 @@ export const createProjectStatusService = (
|
|||||||
config.flagResolver,
|
config.flagResolver,
|
||||||
);
|
);
|
||||||
|
|
||||||
return new ProjectStatusService({
|
return new ProjectStatusService(
|
||||||
eventStore,
|
{
|
||||||
projectStore,
|
eventStore,
|
||||||
apiTokenStore,
|
projectStore,
|
||||||
segmentStore,
|
apiTokenStore,
|
||||||
});
|
segmentStore,
|
||||||
|
},
|
||||||
|
new PersonalDashboardReadModel(db),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createFakeProjectStatusService = () => {
|
export const createFakeProjectStatusService = () => {
|
||||||
@ -46,12 +51,15 @@ export const createFakeProjectStatusService = () => {
|
|||||||
const projectStore = new FakeProjectStore();
|
const projectStore = new FakeProjectStore();
|
||||||
const apiTokenStore = new FakeApiTokenStore();
|
const apiTokenStore = new FakeApiTokenStore();
|
||||||
const segmentStore = new FakeSegmentStore();
|
const segmentStore = new FakeSegmentStore();
|
||||||
const projectStatusService = new ProjectStatusService({
|
const projectStatusService = new ProjectStatusService(
|
||||||
eventStore,
|
{
|
||||||
projectStore,
|
eventStore,
|
||||||
apiTokenStore,
|
projectStore,
|
||||||
segmentStore,
|
apiTokenStore,
|
||||||
});
|
segmentStore,
|
||||||
|
},
|
||||||
|
new FakePersonalDashboardReadModel(),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projectStatusService,
|
projectStatusService,
|
||||||
|
@ -6,26 +6,32 @@ import type {
|
|||||||
ISegmentStore,
|
ISegmentStore,
|
||||||
IUnleashStores,
|
IUnleashStores,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
import type { IPersonalDashboardReadModel } from '../personal-dashboard/personal-dashboard-read-model-type';
|
||||||
|
|
||||||
export class ProjectStatusService {
|
export class ProjectStatusService {
|
||||||
private eventStore: IEventStore;
|
private eventStore: IEventStore;
|
||||||
private projectStore: IProjectStore;
|
private projectStore: IProjectStore;
|
||||||
private apiTokenStore: IApiTokenStore;
|
private apiTokenStore: IApiTokenStore;
|
||||||
private segmentStore: ISegmentStore;
|
private segmentStore: ISegmentStore;
|
||||||
|
private personalDashboardReadModel: IPersonalDashboardReadModel;
|
||||||
|
|
||||||
constructor({
|
constructor(
|
||||||
eventStore,
|
{
|
||||||
projectStore,
|
eventStore,
|
||||||
apiTokenStore,
|
projectStore,
|
||||||
segmentStore,
|
apiTokenStore,
|
||||||
}: Pick<
|
segmentStore,
|
||||||
IUnleashStores,
|
}: Pick<
|
||||||
'eventStore' | 'projectStore' | 'apiTokenStore' | 'segmentStore'
|
IUnleashStores,
|
||||||
>) {
|
'eventStore' | 'projectStore' | 'apiTokenStore' | 'segmentStore'
|
||||||
|
>,
|
||||||
|
personalDashboardReadModel: IPersonalDashboardReadModel,
|
||||||
|
) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectStatus(projectId: string): Promise<ProjectStatusSchema> {
|
async getProjectStatus(projectId: string): Promise<ProjectStatusSchema> {
|
||||||
@ -35,14 +41,21 @@ export class ProjectStatusService {
|
|||||||
apiTokens,
|
apiTokens,
|
||||||
segments,
|
segments,
|
||||||
activityCountByDate,
|
activityCountByDate,
|
||||||
|
healthScores,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.projectStore.getConnectedEnvironmentCountForProject(projectId),
|
this.projectStore.getConnectedEnvironmentCountForProject(projectId),
|
||||||
this.projectStore.getMembersCountByProject(projectId),
|
this.projectStore.getMembersCountByProject(projectId),
|
||||||
this.apiTokenStore.countProjectTokens(projectId),
|
this.apiTokenStore.countProjectTokens(projectId),
|
||||||
this.segmentStore.getProjectSegmentCount(projectId),
|
this.segmentStore.getProjectSegmentCount(projectId),
|
||||||
this.eventStore.getProjectRecentEventActivity(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 {
|
return {
|
||||||
resources: {
|
resources: {
|
||||||
connectedEnvironments,
|
connectedEnvironments,
|
||||||
@ -51,6 +64,7 @@ export class ProjectStatusService {
|
|||||||
segments,
|
segments,
|
||||||
},
|
},
|
||||||
activityCountByDate,
|
activityCountByDate,
|
||||||
|
averageHealth,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,20 @@ let eventService: EventService;
|
|||||||
const TEST_USER_ID = -9999;
|
const TEST_USER_ID = -9999;
|
||||||
const config: IUnleashConfig = createTestConfig();
|
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 getCurrentDateStrings = () => {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const todayString = today.toISOString().split('T')[0];
|
const todayString = today.toISOString().split('T')[0];
|
||||||
@ -226,3 +240,19 @@ test('project resources should contain the right data', async () => {
|
|||||||
connectedEnvironments: 1,
|
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', () => {
|
test('projectStatusSchema', () => {
|
||||||
const data: ProjectStatusSchema = {
|
const data: ProjectStatusSchema = {
|
||||||
|
averageHealth: 50,
|
||||||
activityCountByDate: [
|
activityCountByDate: [
|
||||||
{ date: '2022-12-14', count: 2 },
|
{ date: '2022-12-14', count: 2 },
|
||||||
{ date: '2022-12-15', count: 5 },
|
{ date: '2022-12-15', count: 5 },
|
||||||
|
@ -5,7 +5,7 @@ export const projectStatusSchema = {
|
|||||||
$id: '#/components/schemas/projectStatusSchema',
|
$id: '#/components/schemas/projectStatusSchema',
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
required: ['activityCountByDate', 'resources'],
|
required: ['activityCountByDate', 'resources', 'averageHealth'],
|
||||||
description:
|
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.',
|
'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: {
|
properties: {
|
||||||
@ -14,6 +14,12 @@ export const projectStatusSchema = {
|
|||||||
description:
|
description:
|
||||||
'Array of activity records with date and count, representing the project’s daily activity statistics.',
|
'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: {
|
resources: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
|
Loading…
Reference in New Issue
Block a user