Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(1-3133): change avg health to current health in project status #8803

Merged
merged 4 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading