1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

feat: lifecycle count query (#9824)

This commit is contained in:
Mateusz Kwasniewski 2025-04-24 09:36:06 +02:00 committed by GitHub
parent 26f582db21
commit 9911fe89be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 153 additions and 33 deletions

View File

@ -181,10 +181,7 @@ export const createStores = (
featureStrategiesReadModel: new FeatureStrategiesReadModel(db),
onboardingReadModel: createOnboardingReadModel(db),
onboardingStore: new OnboardingStore(db),
featureLifecycleReadModel: new FeatureLifecycleReadModel(
db,
config.flagResolver,
),
featureLifecycleReadModel: new FeatureLifecycleReadModel(db),
largestResourcesReadModel: new LargestResourcesReadModel(db),
integrationEventsStore: new IntegrationEventsStore(db, { eventBus }),
featureCollaboratorsReadModel: new FeatureCollaboratorsReadModel(db),

View File

@ -0,0 +1,80 @@
import {
type IFeatureLifecycleReadModel,
type IUnleashConfig,
type IUnleashServices,
NONE,
} from '../../types';
import type { OpenApiService } from '../../services';
import { createResponseSchema, getStandardResponses } from '../../openapi';
import Controller from '../../routes/controller';
import type { Request, Response } from 'express';
import {
type FeatureLifecycleCountSchema,
featureLifecycleCountSchema,
} from '../../openapi/spec/feature-lifecycle-count-schema';
export default class FeatureLifecycleCountController extends Controller {
private featureLifecycleReadModel: IFeatureLifecycleReadModel;
private openApiService: OpenApiService;
constructor(
config: IUnleashConfig,
{
openApiService,
featureLifecycleReadModel,
}: Pick<
IUnleashServices,
'openApiService' | 'featureLifecycleReadModel'
>,
) {
super(config);
this.featureLifecycleReadModel = featureLifecycleReadModel;
this.openApiService = openApiService;
this.route({
method: 'get',
path: '/count',
handler: this.getStageCount,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['Unstable'],
summary: 'Get all features lifecycle stage count',
description:
'Information about the number of features in each lifecycle stage.',
operationId: 'getFeatureLifecycleStageCount',
responses: {
200: createResponseSchema(
'featureLifecycleCountSchema',
),
...getStandardResponses(401, 403, 404),
},
}),
],
});
}
async getStageCount(
_: Request<any, any, any, any>,
res: Response<FeatureLifecycleCountSchema>,
): Promise<void> {
const stageCounts =
await this.featureLifecycleReadModel.getStageCount();
const result: Record<string, number> = stageCounts.reduce(
(acc, { stage, count }) => {
acc[stage === 'pre-live' ? 'preLive' : stage] = count;
return acc;
},
{ initial: 0, preLive: 0, live: 0, completed: 0, archived: 0 },
);
this.openApiService.respondWithValidation(
200,
res,
featureLifecycleCountSchema.$id,
result,
);
}
}

View File

@ -4,25 +4,15 @@ import { FeatureLifecycleReadModel } from './feature-lifecycle-read-model';
import type { IFeatureLifecycleStore } from './feature-lifecycle-store-type';
import type { IFeatureLifecycleReadModel } from './feature-lifecycle-read-model-type';
import type { IFeatureToggleStore } from '../feature-toggle/types/feature-toggle-store-type';
import type { IFlagResolver } from '../../types';
let db: ITestDb;
let featureLifecycleReadModel: IFeatureLifecycleReadModel;
let featureLifecycleStore: IFeatureLifecycleStore;
let featureToggleStore: IFeatureToggleStore;
const alwaysOnFlagResolver = {
isEnabled() {
return true;
},
} as unknown as IFlagResolver;
beforeAll(async () => {
db = await dbInit('feature_lifecycle_read_model', getLogger);
featureLifecycleReadModel = new FeatureLifecycleReadModel(
db.rawDatabase,
alwaysOnFlagResolver,
);
featureLifecycleReadModel = new FeatureLifecycleReadModel(db.rawDatabase);
featureLifecycleStore = db.stores.featureLifecycleStore;
featureToggleStore = db.stores.featureToggleStore;
});

View File

@ -7,7 +7,6 @@ import type {
import { getCurrentStage } from './get-current-stage';
import type {
IFeatureLifecycleStage,
IFlagResolver,
IProjectLifecycleStageDuration,
StageName,
} from '../../types';
@ -28,11 +27,8 @@ type DBProjectType = DBType & {
export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel {
private db: Db;
private flagResolver: IFlagResolver;
constructor(db: Db, flagResolver: IFlagResolver) {
constructor(db: Db) {
this.db = db;
this.flagResolver = flagResolver;
}
async getStageCount(): Promise<StageCount[]> {

View File

@ -41,10 +41,7 @@ beforeAll(async () => {
);
eventStore = db.stores.eventStore;
eventBus = app.config.eventBus;
featureLifecycleReadModel = new FeatureLifecycleReadModel(
db.rawDatabase,
app.config.flagResolver,
);
featureLifecycleReadModel = new FeatureLifecycleReadModel(db.rawDatabase);
featureLifecycleStore = db.stores.featureLifecycleStore;
await app.request
@ -115,6 +112,10 @@ const expectFeatureStage = async (featureName: string, stage: StageName) => {
});
};
const getFeaturesLifecycleCount = async () => {
return app.request.get(`/api/admin/lifecycle/count`).expect(200);
};
test('should return lifecycle stages', async () => {
await app.createFeature('my_feature_a');
await app.enableFeature('my_feature_a', 'default');
@ -173,6 +174,15 @@ test('should return lifecycle stages', async () => {
eventStore.emit(FEATURE_REVIVED, { featureName: 'my_feature_a' });
await reachedStage('my_feature_a', 'initial');
const { body: lifecycleCount } = await getFeaturesLifecycleCount();
expect(lifecycleCount).toEqual({
initial: 1,
preLive: 0,
live: 0,
completed: 0,
archived: 0,
});
});
test('should be able to toggle between completed/uncompleted', async () => {

View File

@ -122,10 +122,7 @@ export const createFeatureToggleService = (
const dependentFeaturesReadModel = new DependentFeaturesReadModel(db);
const featureLifecycleReadModel = new FeatureLifecycleReadModel(
db,
config.flagResolver,
);
const featureLifecycleReadModel = new FeatureLifecycleReadModel(db);
const dependentFeaturesService = createDependentFeaturesService(config)(db);

View File

@ -67,10 +67,7 @@ beforeAll(async () => {
const versionService = new VersionService(stores, config);
db = await dbInit('metrics_test', getLogger);
featureLifeCycleReadModel = new FeatureLifecycleReadModel(
db.rawDatabase,
config.flagResolver,
);
featureLifeCycleReadModel = new FeatureLifecycleReadModel(db.rawDatabase);
stores.featureLifecycleReadModel = featureLifeCycleReadModel;
featureLifeCycleStore = new FeatureLifecycleStore(db.rawDatabase);
stores.featureLifecycleStore = featureLifeCycleStore;

View File

@ -0,0 +1,43 @@
import type { FromSchema } from 'json-schema-to-ts';
export const featureLifecycleCountSchema = {
$id: '#/components/schemas/featureLifecycleCountSchema',
type: 'object',
description: 'A number features in each of the lifecycle stages',
required: ['initial', 'preLive', 'live', 'completed', 'archived'],
additionalProperties: false,
properties: {
initial: {
type: 'number',
example: 1,
description: 'Number of features in the initial stage',
},
preLive: {
type: 'number',
example: 1,
description: 'Number of features in the pre-live stage',
},
live: {
type: 'number',
example: 1,
description: 'Number of features in the live stage',
},
completed: {
type: 'number',
example: 1,
description: 'Number of features in the completed stage',
},
archived: {
type: 'number',
example: 1,
description: 'Number of features in the archived stage',
},
},
components: {
schemas: {},
},
} as const;
export type FeatureLifecycleCountSchema = FromSchema<
typeof featureLifecycleCountSchema
>;

View File

@ -83,6 +83,7 @@ export * from './feature-environment-metrics-schema';
export * from './feature-environment-schema';
export * from './feature-events-schema';
export * from './feature-lifecycle-completed-schema';
export * from './feature-lifecycle-count-schema';
export * from './feature-lifecycle-schema';
export * from './feature-metrics-schema';
export * from './feature-schema';

View File

@ -36,6 +36,7 @@ import { InactiveUsersController } from '../../users/inactive/inactive-users-con
import { UiObservabilityController } from '../../features/ui-observability-controller/ui-observability-controller';
import { SearchApi } from './search';
import PersonalDashboardController from '../../features/personal-dashboard/personal-dashboard-controller';
import FeatureLifecycleCountController from '../../features/feature-lifecycle/feature-lifecycle-count-controller';
export class AdminApi extends Controller {
constructor(config: IUnleashConfig, services: IUnleashServices, db: Db) {
@ -121,6 +122,10 @@ export class AdminApi extends Controller {
'/projects',
new ProjectController(config, services, db).router,
);
this.app.use(
'/lifecycle',
new FeatureLifecycleCountController(config, services).router,
);
this.app.use(
'/personal-dashboard',
new PersonalDashboardController(config, services).router,

View File

@ -198,7 +198,7 @@ export const createServices = (
? new DependentFeaturesReadModel(db)
: new FakeDependentFeaturesReadModel();
const featureLifecycleReadModel = db
? new FeatureLifecycleReadModel(db, config.flagResolver)
? new FeatureLifecycleReadModel(db)
: new FakeFeatureLifecycleReadModel();
const transactionalContextService = db
@ -490,6 +490,7 @@ export const createServices = (
projectStatusService,
transactionalUserSubscriptionsService,
uniqueConnectionService,
featureLifecycleReadModel,
};
};
@ -544,4 +545,5 @@ export {
ProjectStatusService,
UserSubscriptionsService,
UniqueConnectionService,
FeatureLifecycleReadModel,
};

View File

@ -60,6 +60,7 @@ import type { PersonalDashboardService } from '../features/personal-dashboard/pe
import type { ProjectStatusService } from '../features/project-status/project-status-service';
import type { UserSubscriptionsService } from '../features/user-subscriptions/user-subscriptions-service';
import type { UniqueConnectionService } from '../features/unique-connection/unique-connection-service';
import type { IFeatureLifecycleReadModel } from '../features/feature-lifecycle/feature-lifecycle-read-model-type';
export interface IUnleashServices {
transactionalAccessService: WithTransactional<AccessService>;
@ -131,4 +132,5 @@ export interface IUnleashServices {
projectStatusService: ProjectStatusService;
transactionalUserSubscriptionsService: WithTransactional<UserSubscriptionsService>;
uniqueConnectionService: UniqueConnectionService;
featureLifecycleReadModel: IFeatureLifecycleReadModel;
}