mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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,
 | 
					            eventStore,
 | 
				
			||||||
            projectStore,
 | 
					            projectStore,
 | 
				
			||||||
            apiTokenStore,
 | 
					            apiTokenStore,
 | 
				
			||||||
            segmentStore,
 | 
					            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,
 | 
					            eventStore,
 | 
				
			||||||
            projectStore,
 | 
					            projectStore,
 | 
				
			||||||
            apiTokenStore,
 | 
					            apiTokenStore,
 | 
				
			||||||
            segmentStore,
 | 
					            segmentStore,
 | 
				
			||||||
    });
 | 
					        },
 | 
				
			||||||
 | 
					        new FakePersonalDashboardReadModel(),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
        projectStatusService,
 | 
					        projectStatusService,
 | 
				
			||||||
 | 
				
			|||||||
@ -6,14 +6,17 @@ 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,
 | 
					            eventStore,
 | 
				
			||||||
            projectStore,
 | 
					            projectStore,
 | 
				
			||||||
            apiTokenStore,
 | 
					            apiTokenStore,
 | 
				
			||||||
@ -21,11 +24,14 @@ export class ProjectStatusService {
 | 
				
			|||||||
        }: Pick<
 | 
					        }: Pick<
 | 
				
			||||||
            IUnleashStores,
 | 
					            IUnleashStores,
 | 
				
			||||||
            'eventStore' | 'projectStore' | 'apiTokenStore' | 'segmentStore'
 | 
					            '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