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:
parent
e2fabcafd4
commit
efda70ac5d
@ -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 {
|
||||||
|
@ -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",
|
||||||
|
@ -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,
|
||||||
|
@ -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([]);
|
||||||
|
});
|
||||||
|
@ -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' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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';
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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 = {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,7 @@ process.nextTick(async () => {
|
|||||||
disableShowContextFieldSelectionValues: false,
|
disableShowContextFieldSelectionValues: false,
|
||||||
variantDependencies: true,
|
variantDependencies: true,
|
||||||
projectOverviewRefactorFeedback: true,
|
projectOverviewRefactorFeedback: true,
|
||||||
|
featureLifecycle: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
|
Loading…
Reference in New Issue
Block a user