1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +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:
Thomas Heartman 2024-11-20 11:41:45 +01:00 committed by GitHub
parent 0f91c6b0c2
commit 04b2b488f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 81 additions and 55 deletions

View File

@ -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}`}>

View File

@ -11,7 +11,9 @@ const placeholderData: ProjectStatusSchema = {
apiTokens: 0,
segments: 0,
},
averageHealth: 0,
health: {
current: 0,
},
lifecycleSummary: {
initial: {
currentFlags: 0,

View File

@ -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 */

View File

@ -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(),
);

View File

@ -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,

View File

@ -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 () => {

View File

@ -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,

View File

@ -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 projects 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',