mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	fix: client metrics structure lifecycle (#6924)
This commit is contained in:
		
							parent
							
								
									477da7d514
								
							
						
					
					
						commit
						574eb284b9
					
				| @ -7,7 +7,17 @@ import type { | ||||
| export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore { | ||||
|     private lifecycles: Record<string, FeatureLifecycleView> = {}; | ||||
| 
 | ||||
|     async insert(featureLifecycleStage: FeatureLifecycleStage): Promise<void> { | ||||
|     async insert( | ||||
|         featureLifecycleStages: FeatureLifecycleStage[], | ||||
|     ): Promise<void> { | ||||
|         await Promise.all( | ||||
|             featureLifecycleStages.map((stage) => this.insertOne(stage)), | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private async insertOne( | ||||
|         featureLifecycleStage: FeatureLifecycleStage, | ||||
|     ): Promise<void> { | ||||
|         if (await this.stageExists(featureLifecycleStage)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @ -23,7 +23,10 @@ test('can insert and read lifecycle stages', async () => { | ||||
|     const featureName = 'testFeature'; | ||||
| 
 | ||||
|     function emitMetricsEvent(environment: string) { | ||||
|         eventBus.emit(CLIENT_METRICS, { featureName, environment }); | ||||
|         eventBus.emit(CLIENT_METRICS, { | ||||
|             bucket: { toggles: { [featureName]: 'irrelevant' } }, | ||||
|             environment, | ||||
|         }); | ||||
|     } | ||||
|     function reachedStage(name: StageName) { | ||||
|         return new Promise((resolve) => | ||||
| @ -100,7 +103,7 @@ test('ignores lifecycle state updates when flag disabled', async () => { | ||||
|     await eventStore.emit(FEATURE_CREATED, { featureName }); | ||||
|     await eventStore.emit(FEATURE_COMPLETED, { featureName }); | ||||
|     await eventBus.emit(CLIENT_METRICS, { | ||||
|         featureName, | ||||
|         bucket: { toggles: { [featureName]: 'irrelevant' } }, | ||||
|         environment: 'development', | ||||
|     }); | ||||
|     await eventStore.emit(FEATURE_ARCHIVED, { featureName }); | ||||
|  | ||||
| @ -14,6 +14,7 @@ import type { | ||||
| } from './feature-lifecycle-store-type'; | ||||
| import EventEmitter from 'events'; | ||||
| import type { Logger } from '../../logger'; | ||||
| import type { ValidatedClientMetrics } from '../metrics/shared/schema'; | ||||
| 
 | ||||
| export const STAGE_ENTERED = 'STAGE_ENTERED'; | ||||
| 
 | ||||
| @ -70,15 +71,18 @@ export class FeatureLifecycleService extends EventEmitter { | ||||
|                 this.featureInitialized(event.featureName), | ||||
|             ); | ||||
|         }); | ||||
|         this.eventBus.on(CLIENT_METRICS, async (event) => { | ||||
|             if (!event.featureName || !event.environment) return; | ||||
|         this.eventBus.on( | ||||
|             CLIENT_METRICS, | ||||
|             async (event: ValidatedClientMetrics) => { | ||||
|                 if (event.environment) { | ||||
|                     const features = Object.keys(event.bucket.toggles); | ||||
|                     const environment = event.environment; | ||||
|                     await this.checkEnabled(() => | ||||
|                 this.featureReceivedMetrics( | ||||
|                     event.featureName, | ||||
|                     event.environment, | ||||
|                 ), | ||||
|                         this.featuresReceivedMetrics(features, environment), | ||||
|                     ); | ||||
|                 } | ||||
|             }, | ||||
|         ); | ||||
|         }); | ||||
|         this.eventStore.on(FEATURE_COMPLETED, async (event) => { | ||||
|             await this.checkEnabled(() => | ||||
|                 this.featureCompleted(event.featureName), | ||||
| @ -96,25 +100,26 @@ export class FeatureLifecycleService extends EventEmitter { | ||||
|     } | ||||
| 
 | ||||
|     private async featureInitialized(feature: string) { | ||||
|         await this.featureLifecycleStore.insert({ feature, stage: 'initial' }); | ||||
|         await this.featureLifecycleStore.insert([ | ||||
|             { feature, stage: 'initial' }, | ||||
|         ]); | ||||
|         this.emit(STAGE_ENTERED, { stage: 'initial' }); | ||||
|     } | ||||
| 
 | ||||
|     private async stageReceivedMetrics( | ||||
|         feature: string, | ||||
|         features: string[], | ||||
|         stage: 'live' | 'pre-live', | ||||
|     ) { | ||||
|         const stageExists = await this.featureLifecycleStore.stageExists({ | ||||
|             stage, | ||||
|             feature, | ||||
|         }); | ||||
|         if (!stageExists) { | ||||
|             await this.featureLifecycleStore.insert({ feature, stage }); | ||||
|         await this.featureLifecycleStore.insert( | ||||
|             features.map((feature) => ({ feature, stage })), | ||||
|         ); | ||||
|         this.emit(STAGE_ENTERED, { stage }); | ||||
|     } | ||||
|     } | ||||
| 
 | ||||
|     private async featureReceivedMetrics(feature: string, environment: string) { | ||||
|     private async featuresReceivedMetrics( | ||||
|         features: string[], | ||||
|         environment: string, | ||||
|     ) { | ||||
|         try { | ||||
|             const env = await this.environmentStore.get(environment); | ||||
| 
 | ||||
| @ -122,28 +127,32 @@ export class FeatureLifecycleService extends EventEmitter { | ||||
|                 return; | ||||
|             } | ||||
|             if (env.type === 'production') { | ||||
|                 await this.stageReceivedMetrics(feature, 'live'); | ||||
|                 await this.stageReceivedMetrics(features, 'live'); | ||||
|             } else if (env.type === 'development') { | ||||
|                 await this.stageReceivedMetrics(feature, 'pre-live'); | ||||
|                 await this.stageReceivedMetrics(features, 'pre-live'); | ||||
|             } | ||||
|         } catch (e) { | ||||
|             this.logger.warn( | ||||
|                 `Error handling metrics for ${feature} in ${environment}`, | ||||
|                 `Error handling ${features.length} metrics in ${environment}`, | ||||
|                 e, | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private async featureCompleted(feature: string) { | ||||
|         await this.featureLifecycleStore.insert({ | ||||
|         await this.featureLifecycleStore.insert([ | ||||
|             { | ||||
|                 feature, | ||||
|                 stage: 'completed', | ||||
|         }); | ||||
|             }, | ||||
|         ]); | ||||
|         this.emit(STAGE_ENTERED, { stage: 'completed' }); | ||||
|     } | ||||
| 
 | ||||
|     private async featureArchived(feature: string) { | ||||
|         await this.featureLifecycleStore.insert({ feature, stage: 'archived' }); | ||||
|         await this.featureLifecycleStore.insert([ | ||||
|             { feature, stage: 'archived' }, | ||||
|         ]); | ||||
|         this.emit(STAGE_ENTERED, { stage: 'archived' }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -8,7 +8,7 @@ export type FeatureLifecycleStage = { | ||||
| export type FeatureLifecycleView = IFeatureLifecycleStage[]; | ||||
| 
 | ||||
| export interface IFeatureLifecycleStore { | ||||
|     insert(featureLifecycleStage: FeatureLifecycleStage): Promise<void>; | ||||
|     insert(featureLifecycleStages: FeatureLifecycleStage[]): Promise<void>; | ||||
|     get(feature: string): Promise<FeatureLifecycleView>; | ||||
|     stageExists(stage: FeatureLifecycleStage): Promise<boolean>; | ||||
| } | ||||
|  | ||||
| @ -19,12 +19,16 @@ export class FeatureLifecycleStore implements IFeatureLifecycleStore { | ||||
|         this.db = db; | ||||
|     } | ||||
| 
 | ||||
|     async insert(featureLifecycleStage: FeatureLifecycleStage): Promise<void> { | ||||
|     async insert( | ||||
|         featureLifecycleStages: FeatureLifecycleStage[], | ||||
|     ): Promise<void> { | ||||
|         await this.db('feature_lifecycles') | ||||
|             .insert({ | ||||
|                 feature: featureLifecycleStage.feature, | ||||
|                 stage: featureLifecycleStage.stage, | ||||
|             }) | ||||
|             .insert( | ||||
|                 featureLifecycleStages.map((stage) => ({ | ||||
|                     feature: stage.feature, | ||||
|                     stage: stage.stage, | ||||
|                 })), | ||||
|             ) | ||||
|             .returning('*') | ||||
|             .onConflict(['feature', 'stage']) | ||||
|             .ignore(); | ||||
|  | ||||
| @ -86,16 +86,21 @@ test('should return lifecycle stages', async () => { | ||||
|     await reachedStage('initial'); | ||||
|     await expectFeatureStage('initial'); | ||||
|     eventBus.emit(CLIENT_METRICS, { | ||||
|         featureName: 'my_feature_a', | ||||
|         bucket: { toggles: { my_feature_a: 'irrelevant' } }, | ||||
|         environment: 'default', | ||||
|     }); | ||||
|     // missing feature
 | ||||
|     eventBus.emit(CLIENT_METRICS, { | ||||
|         environment: 'default', | ||||
|         bucket: { toggles: {} }, | ||||
|     }); | ||||
|     // non existent env
 | ||||
|     eventBus.emit(CLIENT_METRICS, { | ||||
|         featureName: 'my_feature_a', | ||||
|         bucket: { | ||||
|             toggles: { | ||||
|                 my_feature_a: 'irrelevant', | ||||
|             }, | ||||
|         }, | ||||
|         environment: 'non-existent', | ||||
|     }); | ||||
|     await reachedStage('live'); | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user