Skip to content

Commit

Permalink
feat: add stale flag count to project status payload (#8751)
Browse files Browse the repository at this point in the history
This PR adds stale flag count to the project status payload. This is
useful for the project status page to show the number of stale flags in
the project.
  • Loading branch information
thomasheartman authored Nov 14, 2024
1 parent b4e2d5d commit 5d32d14
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ const placeholderData: ProjectStatusSchema = {
last30Days: 0,
},
},
staleFlags: {
total: 0,
},
};

export const useProjectStatus = (projectId: string) => {
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/openapi/models/projectStatusSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,9 @@ export interface ProjectStatusSchema {
lifecycleSummary: ProjectStatusSchemaLifecycleSummary;
/** Key resources within the project */
resources: ProjectStatusSchemaResources;
/** Information on stale and potentially stale flags in this project. */
staleFlags: {
/** The total number of flags in this project that are stale or potentially stale. */
total: number;
};
}
5 changes: 5 additions & 0 deletions src/lib/features/project-status/createProjectStatusService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ 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';

export const createProjectStatusService = (
db: Db,
Expand All @@ -40,6 +42,7 @@ export const createProjectStatusService = (
);
const projectLifecycleSummaryReadModel =
createProjectLifecycleSummaryReadModel(db, config);
const projectStaleFlagsReadModel = new ProjectStaleFlagsReadModel(db);

return new ProjectStatusService(
{
Expand All @@ -50,6 +53,7 @@ export const createProjectStatusService = (
},
new PersonalDashboardReadModel(db),
projectLifecycleSummaryReadModel,
projectStaleFlagsReadModel,
);
};

Expand All @@ -67,6 +71,7 @@ export const createFakeProjectStatusService = () => {
},
new FakePersonalDashboardReadModel(),
createFakeProjectLifecycleSummaryReadModel(),
new FakeProjectStaleFlagsReadModel(),
);

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { IProjectStaleFlagsReadModel } from './project-stale-flags-read-model-type';

export class FakeProjectStaleFlagsReadModel
implements IProjectStaleFlagsReadModel
{
async getStaleFlagCountForProject(): Promise<number> {
return 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface IProjectStaleFlagsReadModel {
getStaleFlagCountForProject: (projectId: string) => Promise<number>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Db } from '../../../server-impl';
import type { IProjectStaleFlagsReadModel } from './project-stale-flags-read-model-type';

export class ProjectStaleFlagsReadModel implements IProjectStaleFlagsReadModel {
constructor(private db: Db) {}

async getStaleFlagCountForProject(projectId: string): Promise<number> {
const result = await this.db('features')
.count()
.where({ project: projectId, archived: false })
.where((builder) =>
builder
.orWhere({ stale: true })
.orWhere({ potentially_stale: true }),
);

return Number(result[0].count);
}
}
11 changes: 11 additions & 0 deletions src/lib/features/project-status/project-status-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} 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';

export class ProjectStatusService {
private eventStore: IEventStore;
Expand All @@ -16,6 +17,7 @@ export class ProjectStatusService {
private segmentStore: ISegmentStore;
private personalDashboardReadModel: IPersonalDashboardReadModel;
private projectLifecycleSummaryReadModel: IProjectLifecycleSummaryReadModel;
private projectStaleFlagsReadModel: IProjectStaleFlagsReadModel;

constructor(
{
Expand All @@ -29,13 +31,15 @@ export class ProjectStatusService {
>,
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;
}

async getProjectStatus(projectId: string): Promise<ProjectStatusSchema> {
Expand All @@ -47,6 +51,7 @@ export class ProjectStatusService {
activityCountByDate,
healthScores,
lifecycleSummary,
staleFlagCount,
] = await Promise.all([
this.projectStore.getConnectedEnvironmentCountForProject(projectId),
this.projectStore.getMembersCountByProject(projectId),
Expand All @@ -57,6 +62,9 @@ export class ProjectStatusService {
this.projectLifecycleSummaryReadModel.getProjectLifecycleSummary(
projectId,
),
this.projectStaleFlagsReadModel.getStaleFlagCountForProject(
projectId,
),
]);

const averageHealth = healthScores.length
Expand All @@ -74,6 +82,9 @@ export class ProjectStatusService {
activityCountByDate,
averageHealth: Math.round(averageHealth),
lifecycleSummary,
staleFlags: {
total: staleFlagCount,
},
};
}
}
50 changes: 50 additions & 0 deletions src/lib/features/project-status/projects-status.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import getLogger from '../../../test/fixtures/no-logger';
import {
FEATURE_CREATED,
type IUser,
RoleName,
type IAuditUser,
type IUnleashConfig,
Expand Down Expand Up @@ -300,3 +301,52 @@ test('project status contains lifecycle data', async () => {
},
});
});

test('project status includes stale flags', async () => {
const otherProject = await app.services.projectService.createProject(
{
name: 'otherProject',
id: randomId(),
},
{} as IUser,
{} as IAuditUser,
);
const createFlagInState = async (
name: string,
state?: Object,
projectId?: string,
) => {
await app.createFeature(name, projectId);
if (state) {
await db.rawDatabase('features').update(state).where({ name });
}
};

await createFlagInState('stale-flag', { stale: true });
await createFlagInState('potentially-stale-flag', {
potentially_stale: true,
});
await createFlagInState('potentially-stale-and-stale-flag', {
potentially_stale: true,
stale: true,
});
await createFlagInState('non-stale-flag');
await createFlagInState('archived-stale-flag', {
archived: true,
stale: true,
});
await createFlagInState(
'stale-other-project',
{ stale: true },
otherProject.id,
);

const { body } = await app.request
.get('/api/admin/projects/default/status')
.expect('Content-Type', /json/)
.expect(200);

expect(body.staleFlags).toMatchObject({
total: 3,
});
});
3 changes: 3 additions & 0 deletions src/lib/openapi/spec/project-status-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ test('projectStatusSchema', () => {
members: 1,
segments: 0,
},
staleFlags: {
total: 0,
},
};

expect(
Expand Down
16 changes: 16 additions & 0 deletions src/lib/openapi/spec/project-status-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const projectStatusSchema = {
'resources',
'averageHealth',
'lifecycleSummary',
'staleFlags',
],
description:
'Schema representing the overall status of a project, including an array of activity records. Each record in the activity array contains a date and a count, providing a snapshot of the project’s activity level over time.',
Expand Down Expand Up @@ -85,6 +86,21 @@ export const projectStatusSchema = {
},
},
},
staleFlags: {
type: 'object',
additionalProperties: false,
description:
'Information on stale and potentially stale flags in this project.',
required: ['total'],
properties: {
total: {
type: 'integer',
minimum: 0,
description:
'The total number of flags in this project that are stale or potentially stale.',
},
},
},
lifecycleSummary: {
type: 'object',
additionalProperties: false,
Expand Down
20 changes: 8 additions & 12 deletions website/prepare-generated-docs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,20 @@ if (!response.ok) {

const data = await response.json();

data.servers = [{
url: '<your-unleash-url>',
}];
data.servers = [
{
url: '<your-unleash-url>',
},
];

const outputDir = './docs/generated/'
const outputDir = './docs/generated/';

// Write the JSON to file
const outputPath = path.join(outputDir, 'openapi.json')
const outputPath = path.join(outputDir, 'openapi.json');

// Ensure directory exists
await fs.mkdir(outputDir, { recursive: true });

await fs.writeFile(
outputPath,
JSON.stringify(data, null, 2),
'utf8'
);
await fs.writeFile(outputPath, JSON.stringify(data, null, 2), 'utf8');

console.log(`OpenAPI spec saved to ${outputPath}`);


0 comments on commit 5d32d14

Please sign in to comment.