mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	chore(1-3133): change avg health to current health in project status (#8803)
This PR updates the project status service (and schemas and UI) to use the project's current health instead of the 4-week average. I nabbed the `calculateHealthRating` from `src/lib/services/project-health-service.ts` instead of relying on the service itself, because that service relies on the project service, which relies on pretty much everything in the entire system. However, I think we can split the health service into a service that *does* need the project service (which is used for 1 of 3 methods) and a service (or read model) that doesn't. We could then rely on the second one for this service without too much overhead. Or we could extract the `calculateHealthRating` into a shared function that takes its stores as arguments. ... but I suggest doing that in a follow-up PR. Because the calculation has been tested other places (especially if we rely on a service / shared function for it), I've simplified the tests to just verify that it's present. I've changed the schema's `averageHealth` into an object in case we want to include average health etc. in the future, but this is up for debate.
This commit is contained in:
		
							parent
							
								
									0f91c6b0c2
								
							
						
					
					
						commit
						04b2b488f6
					
				| @ -90,8 +90,9 @@ const Wrapper = styled(HealthGridTile)(({ theme }) => ({ | ||||
| export const ProjectHealth = () => { | ||||
|     const projectId = useRequiredPathParam('projectId'); | ||||
|     const { | ||||
|         data: { averageHealth, staleFlags }, | ||||
|         data: { health, staleFlags }, | ||||
|     } = useProjectStatus(projectId); | ||||
|     const healthRating = health.current; | ||||
|     const { isOss } = useUiConfig(); | ||||
|     const theme = useTheme(); | ||||
|     const circumference = 2 * Math.PI * ChartRadius; //
 | ||||
| @ -99,12 +100,12 @@ export const ProjectHealth = () => { | ||||
|     const gapLength = 0.3; | ||||
|     const filledLength = 1 - gapLength; | ||||
|     const offset = 0.75 - gapLength / 2; | ||||
|     const healthLength = (averageHealth / 100) * circumference * 0.7; | ||||
|     const healthLength = (healthRating / 100) * circumference * 0.7; | ||||
| 
 | ||||
|     const healthColor = | ||||
|         averageHealth >= 0 && averageHealth <= 24 | ||||
|         healthRating >= 0 && healthRating <= 24 | ||||
|             ? theme.palette.error.main | ||||
|             : averageHealth >= 25 && averageHealth <= 74 | ||||
|             : healthRating >= 25 && healthRating <= 74 | ||||
|               ? theme.palette.warning.border | ||||
|               : theme.palette.success.border; | ||||
| 
 | ||||
| @ -141,14 +142,13 @@ export const ProjectHealth = () => { | ||||
|                             fill={theme.palette.text.primary} | ||||
|                             fontSize={theme.typography.h1.fontSize} | ||||
|                         > | ||||
|                             {averageHealth}% | ||||
|                             {healthRating}% | ||||
|                         </text> | ||||
|                     </StyledSVG> | ||||
|                 </SVGWrapper> | ||||
|                 <TextContainer> | ||||
|                     <Typography> | ||||
|                         On average, your project health has remained at{' '} | ||||
|                         {averageHealth}% the last 4 weeks | ||||
|                         Your current project health rating is {healthRating}% | ||||
|                     </Typography> | ||||
|                     {!isOss() && ( | ||||
|                         <Link to={`/insights?project=IS%3A${projectId}`}> | ||||
|  | ||||
| @ -11,7 +11,9 @@ const placeholderData: ProjectStatusSchema = { | ||||
|         apiTokens: 0, | ||||
|         segments: 0, | ||||
|     }, | ||||
|     averageHealth: 0, | ||||
|     health: { | ||||
|         current: 0, | ||||
|     }, | ||||
|     lifecycleSummary: { | ||||
|         initial: { | ||||
|             currentFlags: 0, | ||||
|  | ||||
| @ -17,7 +17,9 @@ export interface ProjectStatusSchema { | ||||
|      * The average health score over the last 4 weeks, indicating whether features are stale or active. | ||||
|      * @minimum 0 | ||||
|      */ | ||||
|     averageHealth: number; | ||||
|     health: { | ||||
|         current: number; | ||||
|     }; | ||||
|     /** Feature flag lifecycle statistics for this project. */ | ||||
|     lifecycleSummary: ProjectStatusSchemaLifecycleSummary; | ||||
|     /** Key resources within the project */ | ||||
|  | ||||
| @ -8,14 +8,16 @@ 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'; | ||||
| import { | ||||
|     createFakeProjectLifecycleSummaryReadModel, | ||||
|     createProjectLifecycleSummaryReadModel, | ||||
| } from './project-lifecycle-read-model/createProjectLifecycleSummaryReadModel'; | ||||
| import { ProjectStaleFlagsReadModel } from './project-stale-flags-read-model/project-stale-flags-read-model'; | ||||
| import { FakeProjectStaleFlagsReadModel } from './project-stale-flags-read-model/fake-project-stale-flags-read-model'; | ||||
| import FeatureTypeStore from '../../db/feature-type-store'; | ||||
| import FeatureToggleStore from '../feature-toggle/feature-toggle-store'; | ||||
| import FakeFeatureToggleStore from '../feature-toggle/fakes/fake-feature-toggle-store'; | ||||
| import FakeFeatureTypeStore from '../../../test/fixtures/fake-feature-type-store'; | ||||
| 
 | ||||
| export const createProjectStatusService = ( | ||||
|     db: Db, | ||||
| @ -44,14 +46,23 @@ export const createProjectStatusService = ( | ||||
|         createProjectLifecycleSummaryReadModel(db, config); | ||||
|     const projectStaleFlagsReadModel = new ProjectStaleFlagsReadModel(db); | ||||
| 
 | ||||
|     const featureTypeStore = new FeatureTypeStore(db, config.getLogger); | ||||
|     const featureToggleStore = new FeatureToggleStore( | ||||
|         db, | ||||
|         config.eventBus, | ||||
|         config.getLogger, | ||||
|         config.flagResolver, | ||||
|     ); | ||||
| 
 | ||||
|     return new ProjectStatusService( | ||||
|         { | ||||
|             eventStore, | ||||
|             projectStore, | ||||
|             apiTokenStore, | ||||
|             segmentStore, | ||||
|             featureTypeStore, | ||||
|             featureToggleStore, | ||||
|         }, | ||||
|         new PersonalDashboardReadModel(db), | ||||
|         projectLifecycleSummaryReadModel, | ||||
|         projectStaleFlagsReadModel, | ||||
|     ); | ||||
| @ -62,14 +73,17 @@ export const createFakeProjectStatusService = () => { | ||||
|     const projectStore = new FakeProjectStore(); | ||||
|     const apiTokenStore = new FakeApiTokenStore(); | ||||
|     const segmentStore = new FakeSegmentStore(); | ||||
|     const featureTypeStore = new FakeFeatureTypeStore(); | ||||
|     const featureToggleStore = new FakeFeatureToggleStore(); | ||||
|     const projectStatusService = new ProjectStatusService( | ||||
|         { | ||||
|             eventStore, | ||||
|             projectStore, | ||||
|             apiTokenStore, | ||||
|             segmentStore, | ||||
|             featureTypeStore, | ||||
|             featureToggleStore, | ||||
|         }, | ||||
|         new FakePersonalDashboardReadModel(), | ||||
|         createFakeProjectLifecycleSummaryReadModel(), | ||||
|         new FakeProjectStaleFlagsReadModel(), | ||||
|     ); | ||||
|  | ||||
| @ -1,12 +1,14 @@ | ||||
| import { calculateHealthRating } from '../../domain/project-health/project-health'; | ||||
| import type { ProjectStatusSchema } from '../../openapi'; | ||||
| import type { | ||||
|     IApiTokenStore, | ||||
|     IEventStore, | ||||
|     IFeatureToggleStore, | ||||
|     IFeatureTypeStore, | ||||
|     IProjectStore, | ||||
|     ISegmentStore, | ||||
|     IUnleashStores, | ||||
| } from '../../types'; | ||||
| import type { IPersonalDashboardReadModel } from '../personal-dashboard/personal-dashboard-read-model-type'; | ||||
| import type { IProjectLifecycleSummaryReadModel } from './project-lifecycle-read-model/project-lifecycle-read-model-type'; | ||||
| import type { IProjectStaleFlagsReadModel } from './project-stale-flags-read-model/project-stale-flags-read-model-type'; | ||||
| 
 | ||||
| @ -15,9 +17,10 @@ export class ProjectStatusService { | ||||
|     private projectStore: IProjectStore; | ||||
|     private apiTokenStore: IApiTokenStore; | ||||
|     private segmentStore: ISegmentStore; | ||||
|     private personalDashboardReadModel: IPersonalDashboardReadModel; | ||||
|     private projectLifecycleSummaryReadModel: IProjectLifecycleSummaryReadModel; | ||||
|     private projectStaleFlagsReadModel: IProjectStaleFlagsReadModel; | ||||
|     private featureTypeStore: IFeatureTypeStore; | ||||
|     private featureToggleStore: IFeatureToggleStore; | ||||
| 
 | ||||
|     constructor( | ||||
|         { | ||||
| @ -25,11 +28,17 @@ export class ProjectStatusService { | ||||
|             projectStore, | ||||
|             apiTokenStore, | ||||
|             segmentStore, | ||||
|             featureTypeStore, | ||||
|             featureToggleStore, | ||||
|         }: Pick< | ||||
|             IUnleashStores, | ||||
|             'eventStore' | 'projectStore' | 'apiTokenStore' | 'segmentStore' | ||||
|             | 'eventStore' | ||||
|             | 'projectStore' | ||||
|             | 'apiTokenStore' | ||||
|             | 'segmentStore' | ||||
|             | 'featureTypeStore' | ||||
|             | 'featureToggleStore' | ||||
|         >, | ||||
|         personalDashboardReadModel: IPersonalDashboardReadModel, | ||||
|         projectLifecycleReadModel: IProjectLifecycleSummaryReadModel, | ||||
|         projectStaleFlagsReadModel: IProjectStaleFlagsReadModel, | ||||
|     ) { | ||||
| @ -37,9 +46,21 @@ export class ProjectStatusService { | ||||
|         this.projectStore = projectStore; | ||||
|         this.apiTokenStore = apiTokenStore; | ||||
|         this.segmentStore = segmentStore; | ||||
|         this.personalDashboardReadModel = personalDashboardReadModel; | ||||
|         this.projectLifecycleSummaryReadModel = projectLifecycleReadModel; | ||||
|         this.projectStaleFlagsReadModel = projectStaleFlagsReadModel; | ||||
|         this.featureTypeStore = featureTypeStore; | ||||
|         this.featureToggleStore = featureToggleStore; | ||||
|     } | ||||
| 
 | ||||
|     private async calculateHealthRating(projectId: string): Promise<number> { | ||||
|         const featureTypes = await this.featureTypeStore.getAll(); | ||||
| 
 | ||||
|         const toggles = await this.featureToggleStore.getAll({ | ||||
|             project: projectId, | ||||
|             archived: false, | ||||
|         }); | ||||
| 
 | ||||
|         return calculateHealthRating(toggles, featureTypes); | ||||
|     } | ||||
| 
 | ||||
|     async getProjectStatus(projectId: string): Promise<ProjectStatusSchema> { | ||||
| @ -48,7 +69,7 @@ export class ProjectStatusService { | ||||
|             apiTokens, | ||||
|             segments, | ||||
|             activityCountByDate, | ||||
|             healthScores, | ||||
|             currentHealth, | ||||
|             lifecycleSummary, | ||||
|             staleFlagCount, | ||||
|         ] = await Promise.all([ | ||||
| @ -56,7 +77,7 @@ export class ProjectStatusService { | ||||
|             this.apiTokenStore.countProjectTokens(projectId), | ||||
|             this.segmentStore.getProjectSegmentCount(projectId), | ||||
|             this.eventStore.getProjectRecentEventActivity(projectId), | ||||
|             this.personalDashboardReadModel.getLatestHealthScores(projectId, 4), | ||||
|             this.calculateHealthRating(projectId), | ||||
|             this.projectLifecycleSummaryReadModel.getProjectLifecycleSummary( | ||||
|                 projectId, | ||||
|             ), | ||||
| @ -65,11 +86,6 @@ export class ProjectStatusService { | ||||
|             ), | ||||
|         ]); | ||||
| 
 | ||||
|         const averageHealth = healthScores.length | ||||
|             ? healthScores.reduce((acc, num) => acc + num, 0) / | ||||
|               healthScores.length | ||||
|             : 0; | ||||
| 
 | ||||
|         return { | ||||
|             resources: { | ||||
|                 members, | ||||
| @ -77,7 +93,9 @@ export class ProjectStatusService { | ||||
|                 segments, | ||||
|             }, | ||||
|             activityCountByDate, | ||||
|             averageHealth: Math.round(averageHealth), | ||||
|             health: { | ||||
|                 current: currentHealth, | ||||
|             }, | ||||
|             lifecycleSummary, | ||||
|             staleFlags: { | ||||
|                 total: staleFlagCount, | ||||
|  | ||||
| @ -196,33 +196,13 @@ test('project resources should contain the right data', async () => { | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| 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); | ||||
| 
 | ||||
| test('project health contains the current health score', async () => { | ||||
|     const { body } = await app.request | ||||
|         .get('/api/admin/projects/default/status') | ||||
|         .expect('Content-Type', /json/) | ||||
|         .expect(200); | ||||
| 
 | ||||
|     expect(body.averageHealth).toBe(40); | ||||
| }); | ||||
| 
 | ||||
| test('project health stats should round to nearest integer', async () => { | ||||
|     await insertHealthScore('2024-04', 6); | ||||
| 
 | ||||
|     await insertHealthScore('2024-05', 5); | ||||
| 
 | ||||
|     const { body } = await app.request | ||||
|         .get('/api/admin/projects/default/status') | ||||
|         .expect('Content-Type', /json/) | ||||
|         .expect(200); | ||||
| 
 | ||||
|     expect(body.averageHealth).toBe(6); | ||||
|     expect(body.health.current).toBe(100); | ||||
| }); | ||||
| 
 | ||||
| test('project status contains lifecycle data', async () => { | ||||
|  | ||||
| @ -3,7 +3,9 @@ import type { ProjectStatusSchema } from './project-status-schema'; | ||||
| 
 | ||||
| test('projectStatusSchema', () => { | ||||
|     const data: ProjectStatusSchema = { | ||||
|         averageHealth: 50, | ||||
|         health: { | ||||
|             current: 50, | ||||
|         }, | ||||
|         lifecycleSummary: { | ||||
|             initial: { | ||||
|                 currentFlags: 0, | ||||
|  | ||||
| @ -31,7 +31,7 @@ export const projectStatusSchema = { | ||||
|     required: [ | ||||
|         'activityCountByDate', | ||||
|         'resources', | ||||
|         'averageHealth', | ||||
|         'health', | ||||
|         'lifecycleSummary', | ||||
|         'staleFlags', | ||||
|     ], | ||||
| @ -43,11 +43,19 @@ 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.', | ||||
|         health: { | ||||
|             type: 'object', | ||||
|             additionalProperties: false, | ||||
|             required: ['current'], | ||||
|             description: "Information about the project's health rating", | ||||
|             properties: { | ||||
|                 current: { | ||||
|                     type: 'integer', | ||||
|                     minimum: 0, | ||||
|                     description: `The project's current health score, based on the ratio of healthy flags to stale and potentially stale flags.`, | ||||
|                     example: 100, | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|         resources: { | ||||
|             type: 'object', | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user