diff --git a/frontend/src/hooks/api/getters/useProjectStatus/useProjectStatus.ts b/frontend/src/hooks/api/getters/useProjectStatus/useProjectStatus.ts index 8a29fbe01932..9668611b80b7 100644 --- a/frontend/src/hooks/api/getters/useProjectStatus/useProjectStatus.ts +++ b/frontend/src/hooks/api/getters/useProjectStatus/useProjectStatus.ts @@ -35,6 +35,9 @@ const placeholderData: ProjectStatusSchema = { last30Days: 0, }, }, + staleFlags: { + total: 0, + }, }; export const useProjectStatus = (projectId: string) => { diff --git a/frontend/src/openapi/models/projectStatusSchema.ts b/frontend/src/openapi/models/projectStatusSchema.ts index 87dda22ef496..f33b5f9dd258 100644 --- a/frontend/src/openapi/models/projectStatusSchema.ts +++ b/frontend/src/openapi/models/projectStatusSchema.ts @@ -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; + }; } diff --git a/src/lib/features/project-status/createProjectStatusService.ts b/src/lib/features/project-status/createProjectStatusService.ts index 9b85c3196577..0c5c02463c8b 100644 --- a/src/lib/features/project-status/createProjectStatusService.ts +++ b/src/lib/features/project-status/createProjectStatusService.ts @@ -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, @@ -40,6 +42,7 @@ export const createProjectStatusService = ( ); const projectLifecycleSummaryReadModel = createProjectLifecycleSummaryReadModel(db, config); + const projectStaleFlagsReadModel = new ProjectStaleFlagsReadModel(db); return new ProjectStatusService( { @@ -50,6 +53,7 @@ export const createProjectStatusService = ( }, new PersonalDashboardReadModel(db), projectLifecycleSummaryReadModel, + projectStaleFlagsReadModel, ); }; @@ -67,6 +71,7 @@ export const createFakeProjectStatusService = () => { }, new FakePersonalDashboardReadModel(), createFakeProjectLifecycleSummaryReadModel(), + new FakeProjectStaleFlagsReadModel(), ); return { diff --git a/src/lib/features/project-status/project-stale-flags-read-model/fake-project-stale-flags-read-model.ts b/src/lib/features/project-status/project-stale-flags-read-model/fake-project-stale-flags-read-model.ts new file mode 100644 index 000000000000..74bce1c16667 --- /dev/null +++ b/src/lib/features/project-status/project-stale-flags-read-model/fake-project-stale-flags-read-model.ts @@ -0,0 +1,9 @@ +import type { IProjectStaleFlagsReadModel } from './project-stale-flags-read-model-type'; + +export class FakeProjectStaleFlagsReadModel + implements IProjectStaleFlagsReadModel +{ + async getStaleFlagCountForProject(): Promise { + return 0; + } +} diff --git a/src/lib/features/project-status/project-stale-flags-read-model/project-stale-flags-read-model-type.ts b/src/lib/features/project-status/project-stale-flags-read-model/project-stale-flags-read-model-type.ts new file mode 100644 index 000000000000..e8e68af9a259 --- /dev/null +++ b/src/lib/features/project-status/project-stale-flags-read-model/project-stale-flags-read-model-type.ts @@ -0,0 +1,3 @@ +export interface IProjectStaleFlagsReadModel { + getStaleFlagCountForProject: (projectId: string) => Promise; +} diff --git a/src/lib/features/project-status/project-stale-flags-read-model/project-stale-flags-read-model.ts b/src/lib/features/project-status/project-stale-flags-read-model/project-stale-flags-read-model.ts new file mode 100644 index 000000000000..bc0a4c56123a --- /dev/null +++ b/src/lib/features/project-status/project-stale-flags-read-model/project-stale-flags-read-model.ts @@ -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 { + 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); + } +} diff --git a/src/lib/features/project-status/project-status-service.ts b/src/lib/features/project-status/project-status-service.ts index 2900915654e4..aae6c02fc36c 100644 --- a/src/lib/features/project-status/project-status-service.ts +++ b/src/lib/features/project-status/project-status-service.ts @@ -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; @@ -16,6 +17,7 @@ export class ProjectStatusService { private segmentStore: ISegmentStore; private personalDashboardReadModel: IPersonalDashboardReadModel; private projectLifecycleSummaryReadModel: IProjectLifecycleSummaryReadModel; + private projectStaleFlagsReadModel: IProjectStaleFlagsReadModel; constructor( { @@ -29,6 +31,7 @@ export class ProjectStatusService { >, personalDashboardReadModel: IPersonalDashboardReadModel, projectLifecycleReadModel: IProjectLifecycleSummaryReadModel, + projectStaleFlagsReadModel: IProjectStaleFlagsReadModel, ) { this.eventStore = eventStore; this.projectStore = projectStore; @@ -36,6 +39,7 @@ export class ProjectStatusService { this.segmentStore = segmentStore; this.personalDashboardReadModel = personalDashboardReadModel; this.projectLifecycleSummaryReadModel = projectLifecycleReadModel; + this.projectStaleFlagsReadModel = projectStaleFlagsReadModel; } async getProjectStatus(projectId: string): Promise { @@ -47,6 +51,7 @@ export class ProjectStatusService { activityCountByDate, healthScores, lifecycleSummary, + staleFlagCount, ] = await Promise.all([ this.projectStore.getConnectedEnvironmentCountForProject(projectId), this.projectStore.getMembersCountByProject(projectId), @@ -57,6 +62,9 @@ export class ProjectStatusService { this.projectLifecycleSummaryReadModel.getProjectLifecycleSummary( projectId, ), + this.projectStaleFlagsReadModel.getStaleFlagCountForProject( + projectId, + ), ]); const averageHealth = healthScores.length @@ -74,6 +82,9 @@ export class ProjectStatusService { activityCountByDate, averageHealth: Math.round(averageHealth), lifecycleSummary, + staleFlags: { + total: staleFlagCount, + }, }; } } diff --git a/src/lib/features/project-status/projects-status.e2e.test.ts b/src/lib/features/project-status/projects-status.e2e.test.ts index 85ea0faffa8e..e3b1f2697bde 100644 --- a/src/lib/features/project-status/projects-status.e2e.test.ts +++ b/src/lib/features/project-status/projects-status.e2e.test.ts @@ -6,6 +6,7 @@ import { import getLogger from '../../../test/fixtures/no-logger'; import { FEATURE_CREATED, + type IUser, RoleName, type IAuditUser, type IUnleashConfig, @@ -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, + }); +}); diff --git a/src/lib/openapi/spec/project-status-schema.test.ts b/src/lib/openapi/spec/project-status-schema.test.ts index bad54698db0f..b5db36a885da 100644 --- a/src/lib/openapi/spec/project-status-schema.test.ts +++ b/src/lib/openapi/spec/project-status-schema.test.ts @@ -36,6 +36,9 @@ test('projectStatusSchema', () => { members: 1, segments: 0, }, + staleFlags: { + total: 0, + }, }; expect( diff --git a/src/lib/openapi/spec/project-status-schema.ts b/src/lib/openapi/spec/project-status-schema.ts index 5b59f6cdb9c2..21b3ae026731 100644 --- a/src/lib/openapi/spec/project-status-schema.ts +++ b/src/lib/openapi/spec/project-status-schema.ts @@ -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.', @@ -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, diff --git a/website/prepare-generated-docs.mjs b/website/prepare-generated-docs.mjs index 20a46ffc31ac..c7e461b895a4 100644 --- a/website/prepare-generated-docs.mjs +++ b/website/prepare-generated-docs.mjs @@ -29,24 +29,20 @@ if (!response.ok) { const data = await response.json(); -data.servers = [{ - url: '', -}]; +data.servers = [ + { + 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}`); - -