Skip to content

Commit

Permalink
feat: health score backend (#8687)
Browse files Browse the repository at this point in the history
Implements the backend part for project health.

---------

Co-authored-by: Thomas Heartman <[email protected]>
  • Loading branch information
sjaanus and thomasheartman authored Nov 7, 2024
1 parent e039cdc commit d6c03fd
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 22 deletions.
32 changes: 20 additions & 12 deletions src/lib/features/project-status/createProjectStatusService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -33,25 +35,31 @@ 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 = () => {
const eventStore = new FakeEventStore();
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,
Expand Down
32 changes: 23 additions & 9 deletions src/lib/features/project-status/project-status-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand All @@ -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,
Expand All @@ -51,6 +64,7 @@ export class ProjectStatusService {
segments,
},
activityCountByDate,
averageHealth,
};
}
}
30 changes: 30 additions & 0 deletions src/lib/features/project-status/projects-status.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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);
});
1 change: 1 addition & 0 deletions src/lib/openapi/spec/project-status-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
8 changes: 7 additions & 1 deletion src/lib/openapi/spec/project-status-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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,
Expand Down

0 comments on commit d6c03fd

Please sign in to comment.