mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: personal dashboard project avg health scores (#8328)
This commit is contained in:
		
							parent
							
								
									a874ac085d
								
							
						
					
					
						commit
						f47ae12263
					
				| @ -8,6 +8,13 @@ import type { | |||||||
| export class FakePersonalDashboardReadModel | export class FakePersonalDashboardReadModel | ||||||
|     implements IPersonalDashboardReadModel |     implements IPersonalDashboardReadModel | ||||||
| { | { | ||||||
|  |     async getLatestHealthScores( | ||||||
|  |         project: string, | ||||||
|  |         count: number, | ||||||
|  |     ): Promise<number[]> { | ||||||
|  |         return []; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     async getPersonalFeatures(userId: number): Promise<PersonalFeature[]> { |     async getPersonalFeatures(userId: number): Promise<PersonalFeature[]> { | ||||||
|         return []; |         return []; | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -227,6 +227,10 @@ test('should return personal dashboard project details', async () => { | |||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     expect(body).toMatchObject({ |     expect(body).toMatchObject({ | ||||||
|  |         insights: { | ||||||
|  |             avgHealthCurrentWindow: null, | ||||||
|  |             avgHealthPastWindow: null, | ||||||
|  |         }, | ||||||
|         owners: [{}], |         owners: [{}], | ||||||
|         roles: [{}], |         roles: [{}], | ||||||
|         onboardingStatus: { |         onboardingStatus: { | ||||||
| @ -260,6 +264,41 @@ test('should return personal dashboard project details', async () => { | |||||||
|             }, |             }, | ||||||
|         ], |         ], | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|  |     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: project.id, | ||||||
|  |             health, | ||||||
|  |         }); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     await insertHealthScore('2024-01', 80); | ||||||
|  |     await insertHealthScore('2024-02', 80); | ||||||
|  |     await insertHealthScore('2024-03', 80); | ||||||
|  |     await insertHealthScore('2024-04', 81); | ||||||
|  | 
 | ||||||
|  |     await insertHealthScore('2024-05', 90); | ||||||
|  |     await insertHealthScore('2024-06', 91); | ||||||
|  |     await insertHealthScore('2024-07', 91); | ||||||
|  |     await insertHealthScore('2024-08', 91); | ||||||
|  | 
 | ||||||
|  |     const { body: bodyWithHealthScores } = await app.request.get( | ||||||
|  |         `/api/admin/personal-dashboard/${project.id}`, | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     expect(bodyWithHealthScores).toMatchObject({ | ||||||
|  |         insights: { | ||||||
|  |             avgHealthPastWindow: 80, | ||||||
|  |             avgHealthCurrentWindow: 91, | ||||||
|  |         }, | ||||||
|  |     }); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('should return Unleash admins', async () => { | test('should return Unleash admins', async () => { | ||||||
|  | |||||||
| @ -21,4 +21,5 @@ export type PersonalProject = BasePersonalProject & { | |||||||
| export interface IPersonalDashboardReadModel { | export interface IPersonalDashboardReadModel { | ||||||
|     getPersonalFeatures(userId: number): Promise<PersonalFeature[]>; |     getPersonalFeatures(userId: number): Promise<PersonalFeature[]>; | ||||||
|     getPersonalProjects(userId: number): Promise<BasePersonalProject[]>; |     getPersonalProjects(userId: number): Promise<BasePersonalProject[]>; | ||||||
|  |     getLatestHealthScores(project: string, count: number): Promise<number[]>; | ||||||
| } | } | ||||||
|  | |||||||
| @ -19,6 +19,19 @@ export class PersonalDashboardReadModel implements IPersonalDashboardReadModel { | |||||||
|         this.db = db; |         this.db = db; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     async getLatestHealthScores( | ||||||
|  |         project: string, | ||||||
|  |         count: number, | ||||||
|  |     ): Promise<number[]> { | ||||||
|  |         const results = await this.db<{ health: number }>('flag_trends') | ||||||
|  |             .select('health') | ||||||
|  |             .orderBy('created_at', 'desc') | ||||||
|  |             .where('project', project) | ||||||
|  |             .limit(count); | ||||||
|  | 
 | ||||||
|  |         return results.map((row) => Number(row.health)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     async getPersonalProjects(userId: number): Promise<BasePersonalProject[]> { |     async getPersonalProjects(userId: number): Promise<BasePersonalProject[]> { | ||||||
|         const result = await this.db<{ |         const result = await this.db<{ | ||||||
|             name: string; |             name: string; | ||||||
|  | |||||||
| @ -136,11 +136,39 @@ export class PersonalDashboardService { | |||||||
|                 type: role.type as PersonalDashboardProjectDetailsSchema['roles'][number]['type'], |                 type: role.type as PersonalDashboardProjectDetailsSchema['roles'][number]['type'], | ||||||
|             })); |             })); | ||||||
| 
 | 
 | ||||||
|  |         const healthScores = | ||||||
|  |             await this.personalDashboardReadModel.getLatestHealthScores( | ||||||
|  |                 projectId, | ||||||
|  |                 8, | ||||||
|  |             ); | ||||||
|  |         let avgHealthCurrentWindow: number | null = null; | ||||||
|  |         let avgHealthPastWindow: number | null = null; | ||||||
|  | 
 | ||||||
|  |         if (healthScores.length >= 4) { | ||||||
|  |             avgHealthCurrentWindow = Math.round( | ||||||
|  |                 healthScores | ||||||
|  |                     .slice(0, 4) | ||||||
|  |                     .reduce((acc, score) => acc + score, 0) / 4, | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (healthScores.length >= 8) { | ||||||
|  |             avgHealthPastWindow = Math.round( | ||||||
|  |                 healthScores | ||||||
|  |                     .slice(4, 8) | ||||||
|  |                     .reduce((acc, score) => acc + score, 0) / 4, | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         return { |         return { | ||||||
|             latestEvents: formattedEvents, |             latestEvents: formattedEvents, | ||||||
|             onboardingStatus, |             onboardingStatus, | ||||||
|             owners, |             owners, | ||||||
|             roles: projectRoles, |             roles: projectRoles, | ||||||
|  |             insights: { | ||||||
|  |                 avgHealthCurrentWindow, | ||||||
|  |                 avgHealthPastWindow, | ||||||
|  |             }, | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -7,8 +7,36 @@ export const personalDashboardProjectDetailsSchema = { | |||||||
|     type: 'object', |     type: 'object', | ||||||
|     description: 'Project details in personal dashboard', |     description: 'Project details in personal dashboard', | ||||||
|     additionalProperties: false, |     additionalProperties: false, | ||||||
|     required: ['owners', 'roles', 'latestEvents', 'onboardingStatus'], |     required: [ | ||||||
|  |         'owners', | ||||||
|  |         'roles', | ||||||
|  |         'latestEvents', | ||||||
|  |         'onboardingStatus', | ||||||
|  |         'insights', | ||||||
|  |     ], | ||||||
|     properties: { |     properties: { | ||||||
|  |         insights: { | ||||||
|  |             type: 'object', | ||||||
|  |             description: 'Insights for the project', | ||||||
|  |             additionalProperties: false, | ||||||
|  |             required: ['avgHealthCurrentWindow', 'avgHealthPastWindow'], | ||||||
|  |             properties: { | ||||||
|  |                 avgHealthCurrentWindow: { | ||||||
|  |                     type: 'number', | ||||||
|  |                     description: | ||||||
|  |                         'The average health score in the current window of the last 4 weeks', | ||||||
|  |                     example: 80, | ||||||
|  |                     nullable: true, | ||||||
|  |                 }, | ||||||
|  |                 avgHealthPastWindow: { | ||||||
|  |                     type: 'number', | ||||||
|  |                     description: | ||||||
|  |                         'The average health score in the previous 4 weeks before the current window', | ||||||
|  |                     example: 70, | ||||||
|  |                     nullable: true, | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|         onboardingStatus: projectOverviewSchema.properties.onboardingStatus, |         onboardingStatus: projectOverviewSchema.properties.onboardingStatus, | ||||||
|         latestEvents: { |         latestEvents: { | ||||||
|             type: 'array', |             type: 'array', | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user