1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00

feat: feature lifecycle usage behind a flag (#6786)

This commit is contained in:
Mateusz Kwasniewski 2024-04-05 13:42:03 +02:00 committed by GitHub
parent e2fabcafd4
commit efda70ac5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 108 additions and 28 deletions

View File

@ -80,6 +80,7 @@ export type UiFlags = {
disableShowContextFieldSelectionValues?: boolean; disableShowContextFieldSelectionValues?: boolean;
variantDependencies?: boolean; variantDependencies?: boolean;
projectOverviewRefactorFeedback?: boolean; projectOverviewRefactorFeedback?: boolean;
featureLifecycle?: boolean;
}; };
export interface IVersionInfo { export interface IVersionInfo {

View File

@ -98,6 +98,7 @@ exports[`should create default config 1`] = `
"executiveDashboard": false, "executiveDashboard": false,
"executiveDashboardUI": false, "executiveDashboardUI": false,
"extendedUsageMetrics": false, "extendedUsageMetrics": false,
"featureLifecycle": false,
"featureSearchFeedback": { "featureSearchFeedback": {
"enabled": false, "enabled": false,
"name": "withText", "name": "withText",

View File

@ -2,16 +2,20 @@ import FakeEventStore from '../../../test/fixtures/fake-event-store';
import { FakeFeatureLifecycleStore } from './fake-feature-lifecycle-store'; import { FakeFeatureLifecycleStore } from './fake-feature-lifecycle-store';
import { FeatureLifecycleService } from './feature-lifecycle-service'; import { FeatureLifecycleService } from './feature-lifecycle-service';
import FakeEnvironmentStore from '../project-environments/fake-environment-store'; import FakeEnvironmentStore from '../project-environments/fake-environment-store';
import type { IUnleashConfig } from '../../types';
export const createFakeFeatureLifecycleService = () => { export const createFakeFeatureLifecycleService = (config: IUnleashConfig) => {
const eventStore = new FakeEventStore(); const eventStore = new FakeEventStore();
const featureLifecycleStore = new FakeFeatureLifecycleStore(); const featureLifecycleStore = new FakeFeatureLifecycleStore();
const environmentStore = new FakeEnvironmentStore(); const environmentStore = new FakeEnvironmentStore();
const featureLifecycleService = new FeatureLifecycleService({ const featureLifecycleService = new FeatureLifecycleService(
{
eventStore, eventStore,
featureLifecycleStore, featureLifecycleStore,
environmentStore, environmentStore,
}); },
config,
);
return { return {
featureLifecycleService, featureLifecycleService,

View File

@ -4,6 +4,7 @@ import {
FEATURE_COMPLETED, FEATURE_COMPLETED,
FEATURE_CREATED, FEATURE_CREATED,
type IEnvironment, type IEnvironment,
type IUnleashConfig,
} from '../../types'; } from '../../types';
import { createFakeFeatureLifecycleService } from './createFeatureLifecycle'; import { createFakeFeatureLifecycleService } from './createFeatureLifecycle';
@ -13,12 +14,16 @@ function ms(timeMs) {
test('can insert and read lifecycle stages', async () => { test('can insert and read lifecycle stages', async () => {
const { featureLifecycleService, eventStore, environmentStore } = const { featureLifecycleService, eventStore, environmentStore } =
createFakeFeatureLifecycleService(); createFakeFeatureLifecycleService({
flagResolver: { isEnabled: () => true },
} as unknown as IUnleashConfig);
const featureName = 'testFeature'; const featureName = 'testFeature';
async function emitMetricsEvent(environment: string) { async function emitMetricsEvent(environment: string) {
await eventStore.emit(CLIENT_METRICS, { featureName, environment }); await eventStore.emit(CLIENT_METRICS, { featureName, environment });
await ms(1); await ms(1);
} }
await environmentStore.create({ await environmentStore.create({
name: 'my-dev-environment', name: 'my-dev-environment',
type: 'development', type: 'development',
@ -61,3 +66,30 @@ test('can insert and read lifecycle stages', async () => {
{ stage: 'archived', enteredStageAt: expect.any(Date) }, { stage: 'archived', enteredStageAt: expect.any(Date) },
]); ]);
}); });
test('ignores lifecycle state updates when flag disabled', async () => {
const { featureLifecycleService, eventStore, environmentStore } =
createFakeFeatureLifecycleService({
flagResolver: { isEnabled: () => false },
} as unknown as IUnleashConfig);
const featureName = 'testFeature';
await environmentStore.create({
name: 'my-dev-environment',
type: 'development',
} as IEnvironment);
featureLifecycleService.listen();
await eventStore.emit(FEATURE_CREATED, { featureName });
await eventStore.emit(FEATURE_COMPLETED, { featureName });
await eventStore.emit(CLIENT_METRICS, {
featureName,
environment: 'development',
});
await eventStore.emit(FEATURE_ARCHIVED, { featureName });
const lifecycle =
await featureLifecycleService.getFeatureLifecycle(featureName);
expect(lifecycle).toEqual([]);
});

View File

@ -5,6 +5,8 @@ import {
FEATURE_CREATED, FEATURE_CREATED,
type IEnvironmentStore, type IEnvironmentStore,
type IEventStore, type IEventStore,
type IFlagResolver,
type IUnleashConfig,
} from '../../types'; } from '../../types';
import type { import type {
FeatureLifecycleView, FeatureLifecycleView,
@ -18,7 +20,10 @@ export class FeatureLifecycleService {
private environmentStore: IEnvironmentStore; private environmentStore: IEnvironmentStore;
constructor({ private flagResolver: IFlagResolver;
constructor(
{
eventStore, eventStore,
featureLifecycleStore, featureLifecycleStore,
environmentStore, environmentStore,
@ -26,27 +31,45 @@ export class FeatureLifecycleService {
eventStore: IEventStore; eventStore: IEventStore;
environmentStore: IEnvironmentStore; environmentStore: IEnvironmentStore;
featureLifecycleStore: IFeatureLifecycleStore; featureLifecycleStore: IFeatureLifecycleStore;
}) { },
{ flagResolver }: Pick<IUnleashConfig, 'flagResolver'>,
) {
this.eventStore = eventStore; this.eventStore = eventStore;
this.featureLifecycleStore = featureLifecycleStore; this.featureLifecycleStore = featureLifecycleStore;
this.environmentStore = environmentStore; this.environmentStore = environmentStore;
this.flagResolver = flagResolver;
}
private async checkEnabled(fn: () => Promise<void>) {
const enabled = this.flagResolver.isEnabled('featureLifecycle');
if (enabled) {
return fn();
}
} }
listen() { listen() {
this.eventStore.on(FEATURE_CREATED, async (event) => { this.eventStore.on(FEATURE_CREATED, async (event) => {
await this.featureInitialized(event.featureName); await this.checkEnabled(() =>
this.featureInitialized(event.featureName),
);
}); });
this.eventStore.on(CLIENT_METRICS, async (event) => { this.eventStore.on(CLIENT_METRICS, async (event) => {
await this.featureReceivedMetrics( await this.checkEnabled(() =>
this.featureReceivedMetrics(
event.featureName, event.featureName,
event.environment, event.environment,
),
); );
}); });
this.eventStore.on(FEATURE_COMPLETED, async (event) => { this.eventStore.on(FEATURE_COMPLETED, async (event) => {
await this.featureCompleted(event.featureName); await this.checkEnabled(() =>
this.featureCompleted(event.featureName),
);
}); });
this.eventStore.on(FEATURE_ARCHIVED, async (event) => { this.eventStore.on(FEATURE_ARCHIVED, async (event) => {
await this.featureArchived(event.featureName); await this.checkEnabled(() =>
this.featureArchived(event.featureName),
);
}); });
} }
@ -54,11 +77,14 @@ export class FeatureLifecycleService {
return this.featureLifecycleStore.get(feature); return this.featureLifecycleStore.get(feature);
} }
async featureInitialized(feature: string) { private async featureInitialized(feature: string) {
await this.featureLifecycleStore.insert({ feature, stage: 'initial' }); await this.featureLifecycleStore.insert({ feature, stage: 'initial' });
} }
async stageReceivedMetrics(feature: string, stage: 'live' | 'pre-live') { private async stageReceivedMetrics(
feature: string,
stage: 'live' | 'pre-live',
) {
const stageExists = await this.featureLifecycleStore.stageExists({ const stageExists = await this.featureLifecycleStore.stageExists({
stage, stage,
feature, feature,
@ -68,7 +94,7 @@ export class FeatureLifecycleService {
} }
} }
async featureReceivedMetrics(feature: string, environment: string) { private async featureReceivedMetrics(feature: string, environment: string) {
const env = await this.environmentStore.get(environment); const env = await this.environmentStore.get(environment);
if (!env) { if (!env) {
return; return;
@ -80,14 +106,14 @@ export class FeatureLifecycleService {
} }
} }
async featureCompleted(feature: string) { private async featureCompleted(feature: string) {
await this.featureLifecycleStore.insert({ await this.featureLifecycleStore.insert({
feature, feature,
stage: 'completed', stage: 'completed',
}); });
} }
async featureArchived(feature: string) { private async featureArchived(feature: string) {
await this.featureLifecycleStore.insert({ feature, stage: 'archived' }); await this.featureLifecycleStore.insert({ feature, stage: 'archived' });
} }
} }

View File

@ -9,3 +9,4 @@ export * from './tag-type/createTagTypeService';
export * from './project-environments/createEnvironmentService'; export * from './project-environments/createEnvironmentService';
export * from './events/createEventsService'; export * from './events/createEventsService';
export * from './instance-stats/createInstanceStatsService'; export * from './instance-stats/createInstanceStatsService';
export * from './feature-lifecycle/createFeatureLifecycle';

View File

@ -124,6 +124,8 @@ import {
createFakeProjectInsightsService, createFakeProjectInsightsService,
createProjectInsightsService, createProjectInsightsService,
} from '../features/project-insights/createProjectInsightsService'; } from '../features/project-insights/createProjectInsightsService';
import { FeatureLifecycleService } from '../features/feature-lifecycle/feature-lifecycle-service';
import { createFakeFeatureLifecycleService } from '../features/feature-lifecycle/createFeatureLifecycle';
export const createServices = ( export const createServices = (
stores: IUnleashStores, stores: IUnleashStores,
@ -347,6 +349,9 @@ export const createServices = (
const inactiveUsersService = new InactiveUsersService(stores, config, { const inactiveUsersService = new InactiveUsersService(stores, config, {
userService, userService,
}); });
const { featureLifecycleService } =
createFakeFeatureLifecycleService(config);
featureLifecycleService.listen();
return { return {
accessService, accessService,
@ -406,6 +411,7 @@ export const createServices = (
featureSearchService, featureSearchService,
inactiveUsersService, inactiveUsersService,
projectInsightsService, projectInsightsService,
featureLifecycleService,
}; };
}; };
@ -453,4 +459,5 @@ export {
ClientFeatureToggleService, ClientFeatureToggleService,
FeatureSearchService, FeatureSearchService,
ProjectInsightsService, ProjectInsightsService,
FeatureLifecycleService,
}; };

View File

@ -57,7 +57,8 @@ export type IFlagKey =
| 'variantDependencies' | 'variantDependencies'
| 'disableShowContextFieldSelectionValues' | 'disableShowContextFieldSelectionValues'
| 'bearerTokenMiddleware' | 'bearerTokenMiddleware'
| 'projectOverviewRefactorFeedback'; | 'projectOverviewRefactorFeedback'
| 'featureLifecycle';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
@ -283,6 +284,10 @@ const flags: IFlags = {
process.env.UNLEASH_EXPERIMENTAL_PROJECT_OVERVIEW_REFACTOR_FEEDBACK, process.env.UNLEASH_EXPERIMENTAL_PROJECT_OVERVIEW_REFACTOR_FEEDBACK,
false, false,
), ),
featureLifecycle: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_FEATURE_LIFECYCLE,
false,
),
}; };
export const defaultExperimentalOptions: IExperimentalOptions = { export const defaultExperimentalOptions: IExperimentalOptions = {

View File

@ -53,6 +53,7 @@ import type { ClientFeatureToggleService } from '../features/client-feature-togg
import type { FeatureSearchService } from '../features/feature-search/feature-search-service'; import type { FeatureSearchService } from '../features/feature-search/feature-search-service';
import type { InactiveUsersService } from '../users/inactive/inactive-users-service'; import type { InactiveUsersService } from '../users/inactive/inactive-users-service';
import type { ProjectInsightsService } from '../features/project-insights/project-insights-service'; import type { ProjectInsightsService } from '../features/project-insights/project-insights-service';
import type { FeatureLifecycleService } from '../features/feature-lifecycle/feature-lifecycle-service';
export interface IUnleashServices { export interface IUnleashServices {
accessService: AccessService; accessService: AccessService;
@ -115,4 +116,5 @@ export interface IUnleashServices {
featureSearchService: FeatureSearchService; featureSearchService: FeatureSearchService;
inactiveUsersService: InactiveUsersService; inactiveUsersService: InactiveUsersService;
projectInsightsService: ProjectInsightsService; projectInsightsService: ProjectInsightsService;
featureLifecycleService: FeatureLifecycleService;
} }

View File

@ -54,6 +54,7 @@ process.nextTick(async () => {
disableShowContextFieldSelectionValues: false, disableShowContextFieldSelectionValues: false,
variantDependencies: true, variantDependencies: true,
projectOverviewRefactorFeedback: true, projectOverviewRefactorFeedback: true,
featureLifecycle: true,
}, },
}, },
authentication: { authentication: {