Skip to content

Commit

Permalink
chore(1-3133): change avg health to current health in project status (#…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
thomasheartman authored Nov 20, 2024
1 parent 0f91c6b commit 04b2b48
Show file tree
Hide file tree
Showing 8 changed files with 81 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -90,21 +90,22 @@ 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; //

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;

Expand Down Expand Up @@ -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}`}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ const placeholderData: ProjectStatusSchema = {
apiTokens: 0,
segments: 0,
},
averageHealth: 0,
health: {
current: 0,
},
lifecycleSummary: {
initial: {
currentFlags: 0,
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/openapi/models/projectStatusSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
22 changes: 18 additions & 4 deletions src/lib/features/project-status/createProjectStatusService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
);
Expand All @@ -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(),
);
Expand Down
44 changes: 31 additions & 13 deletions src/lib/features/project-status/project-status-service.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -15,31 +17,50 @@ 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(
{
eventStore,
projectStore,
apiTokenStore,
segmentStore,
featureTypeStore,
featureToggleStore,
}: Pick<
IUnleashStores,
'eventStore' | 'projectStore' | 'apiTokenStore' | 'segmentStore'
| 'eventStore'
| 'projectStore'
| 'apiTokenStore'
| 'segmentStore'
| 'featureTypeStore'
| 'featureToggleStore'
>,
personalDashboardReadModel: IPersonalDashboardReadModel,
projectLifecycleReadModel: IProjectLifecycleSummaryReadModel,
projectStaleFlagsReadModel: IProjectStaleFlagsReadModel,
) {
this.eventStore = eventStore;
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> {
Expand All @@ -48,15 +69,15 @@ export class ProjectStatusService {
apiTokens,
segments,
activityCountByDate,
healthScores,
currentHealth,
lifecycleSummary,
staleFlagCount,
] = await Promise.all([
this.projectStore.getMembersCountByProject(projectId),
this.apiTokenStore.countProjectTokens(projectId),
this.segmentStore.getProjectSegmentCount(projectId),
this.eventStore.getProjectRecentEventActivity(projectId),
this.personalDashboardReadModel.getLatestHealthScores(projectId, 4),
this.calculateHealthRating(projectId),
this.projectLifecycleSummaryReadModel.getProjectLifecycleSummary(
projectId,
),
Expand All @@ -65,19 +86,16 @@ export class ProjectStatusService {
),
]);

const averageHealth = healthScores.length
? healthScores.reduce((acc, num) => acc + num, 0) /
healthScores.length
: 0;

return {
resources: {
members,
apiTokens,
segments,
},
activityCountByDate,
averageHealth: Math.round(averageHealth),
health: {
current: currentHealth,
},
lifecycleSummary,
staleFlags: {
total: staleFlagCount,
Expand Down
24 changes: 2 additions & 22 deletions src/lib/features/project-status/projects-status.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

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);

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(6);
expect(body.health.current).toBe(100);
});

test('project status contains lifecycle data', async () => {
Expand Down
4 changes: 3 additions & 1 deletion src/lib/openapi/spec/project-status-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 14 additions & 6 deletions src/lib/openapi/spec/project-status-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const projectStatusSchema = {
required: [
'activityCountByDate',
'resources',
'averageHealth',
'health',
'lifecycleSummary',
'staleFlags',
],
Expand All @@ -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',
Expand Down

0 comments on commit 04b2b48

Please sign in to comment.