mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +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; | ||||
|     variantDependencies?: boolean; | ||||
|     projectOverviewRefactorFeedback?: boolean; | ||||
|     featureLifecycle?: boolean; | ||||
| }; | ||||
| 
 | ||||
| export interface IVersionInfo { | ||||
|  | ||||
| @ -98,6 +98,7 @@ exports[`should create default config 1`] = ` | ||||
|       "executiveDashboard": false, | ||||
|       "executiveDashboardUI": false, | ||||
|       "extendedUsageMetrics": false, | ||||
|       "featureLifecycle": false, | ||||
|       "featureSearchFeedback": { | ||||
|         "enabled": false, | ||||
|         "name": "withText", | ||||
|  | ||||
| @ -2,16 +2,20 @@ import FakeEventStore from '../../../test/fixtures/fake-event-store'; | ||||
| import { FakeFeatureLifecycleStore } from './fake-feature-lifecycle-store'; | ||||
| import { FeatureLifecycleService } from './feature-lifecycle-service'; | ||||
| 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 featureLifecycleStore = new FakeFeatureLifecycleStore(); | ||||
|     const environmentStore = new FakeEnvironmentStore(); | ||||
|     const featureLifecycleService = new FeatureLifecycleService({ | ||||
|         eventStore, | ||||
|         featureLifecycleStore, | ||||
|         environmentStore, | ||||
|     }); | ||||
|     const featureLifecycleService = new FeatureLifecycleService( | ||||
|         { | ||||
|             eventStore, | ||||
|             featureLifecycleStore, | ||||
|             environmentStore, | ||||
|         }, | ||||
|         config, | ||||
|     ); | ||||
| 
 | ||||
|     return { | ||||
|         featureLifecycleService, | ||||
|  | ||||
| @ -4,6 +4,7 @@ import { | ||||
|     FEATURE_COMPLETED, | ||||
|     FEATURE_CREATED, | ||||
|     type IEnvironment, | ||||
|     type IUnleashConfig, | ||||
| } from '../../types'; | ||||
| import { createFakeFeatureLifecycleService } from './createFeatureLifecycle'; | ||||
| 
 | ||||
| @ -13,12 +14,16 @@ function ms(timeMs) { | ||||
| 
 | ||||
| test('can insert and read lifecycle stages', async () => { | ||||
|     const { featureLifecycleService, eventStore, environmentStore } = | ||||
|         createFakeFeatureLifecycleService(); | ||||
|         createFakeFeatureLifecycleService({ | ||||
|             flagResolver: { isEnabled: () => true }, | ||||
|         } as unknown as IUnleashConfig); | ||||
|     const featureName = 'testFeature'; | ||||
| 
 | ||||
|     async function emitMetricsEvent(environment: string) { | ||||
|         await eventStore.emit(CLIENT_METRICS, { featureName, environment }); | ||||
|         await ms(1); | ||||
|     } | ||||
| 
 | ||||
|     await environmentStore.create({ | ||||
|         name: 'my-dev-environment', | ||||
|         type: 'development', | ||||
| @ -61,3 +66,30 @@ test('can insert and read lifecycle stages', async () => { | ||||
|         { 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, | ||||
|     type IEnvironmentStore, | ||||
|     type IEventStore, | ||||
|     type IFlagResolver, | ||||
|     type IUnleashConfig, | ||||
| } from '../../types'; | ||||
| import type { | ||||
|     FeatureLifecycleView, | ||||
| @ -18,35 +20,56 @@ export class FeatureLifecycleService { | ||||
| 
 | ||||
|     private environmentStore: IEnvironmentStore; | ||||
| 
 | ||||
|     constructor({ | ||||
|         eventStore, | ||||
|         featureLifecycleStore, | ||||
|         environmentStore, | ||||
|     }: { | ||||
|         eventStore: IEventStore; | ||||
|         environmentStore: IEnvironmentStore; | ||||
|         featureLifecycleStore: IFeatureLifecycleStore; | ||||
|     }) { | ||||
|     private flagResolver: IFlagResolver; | ||||
| 
 | ||||
|     constructor( | ||||
|         { | ||||
|             eventStore, | ||||
|             featureLifecycleStore, | ||||
|             environmentStore, | ||||
|         }: { | ||||
|             eventStore: IEventStore; | ||||
|             environmentStore: IEnvironmentStore; | ||||
|             featureLifecycleStore: IFeatureLifecycleStore; | ||||
|         }, | ||||
|         { flagResolver }: Pick<IUnleashConfig, 'flagResolver'>, | ||||
|     ) { | ||||
|         this.eventStore = eventStore; | ||||
|         this.featureLifecycleStore = featureLifecycleStore; | ||||
|         this.environmentStore = environmentStore; | ||||
|         this.flagResolver = flagResolver; | ||||
|     } | ||||
| 
 | ||||
|     private async checkEnabled(fn: () => Promise<void>) { | ||||
|         const enabled = this.flagResolver.isEnabled('featureLifecycle'); | ||||
|         if (enabled) { | ||||
|             return fn(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     listen() { | ||||
|         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) => { | ||||
|             await this.featureReceivedMetrics( | ||||
|                 event.featureName, | ||||
|                 event.environment, | ||||
|             await this.checkEnabled(() => | ||||
|                 this.featureReceivedMetrics( | ||||
|                     event.featureName, | ||||
|                     event.environment, | ||||
|                 ), | ||||
|             ); | ||||
|         }); | ||||
|         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) => { | ||||
|             await this.featureArchived(event.featureName); | ||||
|             await this.checkEnabled(() => | ||||
|                 this.featureArchived(event.featureName), | ||||
|             ); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
| @ -54,11 +77,14 @@ export class FeatureLifecycleService { | ||||
|         return this.featureLifecycleStore.get(feature); | ||||
|     } | ||||
| 
 | ||||
|     async featureInitialized(feature: string) { | ||||
|     private async featureInitialized(feature: string) { | ||||
|         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({ | ||||
|             stage, | ||||
|             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); | ||||
|         if (!env) { | ||||
|             return; | ||||
| @ -80,14 +106,14 @@ export class FeatureLifecycleService { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async featureCompleted(feature: string) { | ||||
|     private async featureCompleted(feature: string) { | ||||
|         await this.featureLifecycleStore.insert({ | ||||
|             feature, | ||||
|             stage: 'completed', | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     async featureArchived(feature: string) { | ||||
|     private async featureArchived(feature: string) { | ||||
|         await this.featureLifecycleStore.insert({ feature, stage: 'archived' }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -9,3 +9,4 @@ export * from './tag-type/createTagTypeService'; | ||||
| export * from './project-environments/createEnvironmentService'; | ||||
| export * from './events/createEventsService'; | ||||
| export * from './instance-stats/createInstanceStatsService'; | ||||
| export * from './feature-lifecycle/createFeatureLifecycle'; | ||||
|  | ||||
| @ -124,6 +124,8 @@ import { | ||||
|     createFakeProjectInsightsService, | ||||
|     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 = ( | ||||
|     stores: IUnleashStores, | ||||
| @ -347,6 +349,9 @@ export const createServices = ( | ||||
|     const inactiveUsersService = new InactiveUsersService(stores, config, { | ||||
|         userService, | ||||
|     }); | ||||
|     const { featureLifecycleService } = | ||||
|         createFakeFeatureLifecycleService(config); | ||||
|     featureLifecycleService.listen(); | ||||
| 
 | ||||
|     return { | ||||
|         accessService, | ||||
| @ -406,6 +411,7 @@ export const createServices = ( | ||||
|         featureSearchService, | ||||
|         inactiveUsersService, | ||||
|         projectInsightsService, | ||||
|         featureLifecycleService, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| @ -453,4 +459,5 @@ export { | ||||
|     ClientFeatureToggleService, | ||||
|     FeatureSearchService, | ||||
|     ProjectInsightsService, | ||||
|     FeatureLifecycleService, | ||||
| }; | ||||
|  | ||||
| @ -57,7 +57,8 @@ export type IFlagKey = | ||||
|     | 'variantDependencies' | ||||
|     | 'disableShowContextFieldSelectionValues' | ||||
|     | 'bearerTokenMiddleware' | ||||
|     | 'projectOverviewRefactorFeedback'; | ||||
|     | 'projectOverviewRefactorFeedback' | ||||
|     | 'featureLifecycle'; | ||||
| 
 | ||||
| export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; | ||||
| 
 | ||||
| @ -283,6 +284,10 @@ const flags: IFlags = { | ||||
|         process.env.UNLEASH_EXPERIMENTAL_PROJECT_OVERVIEW_REFACTOR_FEEDBACK, | ||||
|         false, | ||||
|     ), | ||||
|     featureLifecycle: parseEnvVarBoolean( | ||||
|         process.env.UNLEASH_EXPERIMENTAL_FEATURE_LIFECYCLE, | ||||
|         false, | ||||
|     ), | ||||
| }; | ||||
| 
 | ||||
| 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 { InactiveUsersService } from '../users/inactive/inactive-users-service'; | ||||
| import type { ProjectInsightsService } from '../features/project-insights/project-insights-service'; | ||||
| import type { FeatureLifecycleService } from '../features/feature-lifecycle/feature-lifecycle-service'; | ||||
| 
 | ||||
| export interface IUnleashServices { | ||||
|     accessService: AccessService; | ||||
| @ -115,4 +116,5 @@ export interface IUnleashServices { | ||||
|     featureSearchService: FeatureSearchService; | ||||
|     inactiveUsersService: InactiveUsersService; | ||||
|     projectInsightsService: ProjectInsightsService; | ||||
|     featureLifecycleService: FeatureLifecycleService; | ||||
| } | ||||
|  | ||||
| @ -54,6 +54,7 @@ process.nextTick(async () => { | ||||
|                         disableShowContextFieldSelectionValues: false, | ||||
|                         variantDependencies: true, | ||||
|                         projectOverviewRefactorFeedback: true, | ||||
|                         featureLifecycle: true, | ||||
|                     }, | ||||
|                 }, | ||||
|                 authentication: { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user