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 { | export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore { | ||||||
|     private lifecycles: Record<string, FeatureLifecycleView> = {}; |     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)) { |         if (await this.stageExists(featureLifecycleStage)) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -23,7 +23,10 @@ test('can insert and read lifecycle stages', async () => { | |||||||
|     const featureName = 'testFeature'; |     const featureName = 'testFeature'; | ||||||
| 
 | 
 | ||||||
|     function emitMetricsEvent(environment: string) { |     function emitMetricsEvent(environment: string) { | ||||||
|         eventBus.emit(CLIENT_METRICS, { featureName, environment }); |         eventBus.emit(CLIENT_METRICS, { | ||||||
|  |             bucket: { toggles: { [featureName]: 'irrelevant' } }, | ||||||
|  |             environment, | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
|     function reachedStage(name: StageName) { |     function reachedStage(name: StageName) { | ||||||
|         return new Promise((resolve) => |         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_CREATED, { featureName }); | ||||||
|     await eventStore.emit(FEATURE_COMPLETED, { featureName }); |     await eventStore.emit(FEATURE_COMPLETED, { featureName }); | ||||||
|     await eventBus.emit(CLIENT_METRICS, { |     await eventBus.emit(CLIENT_METRICS, { | ||||||
|         featureName, |         bucket: { toggles: { [featureName]: 'irrelevant' } }, | ||||||
|         environment: 'development', |         environment: 'development', | ||||||
|     }); |     }); | ||||||
|     await eventStore.emit(FEATURE_ARCHIVED, { featureName }); |     await eventStore.emit(FEATURE_ARCHIVED, { featureName }); | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ import type { | |||||||
| } from './feature-lifecycle-store-type'; | } from './feature-lifecycle-store-type'; | ||||||
| import EventEmitter from 'events'; | import EventEmitter from 'events'; | ||||||
| import type { Logger } from '../../logger'; | import type { Logger } from '../../logger'; | ||||||
|  | import type { ValidatedClientMetrics } from '../metrics/shared/schema'; | ||||||
| 
 | 
 | ||||||
| export const STAGE_ENTERED = 'STAGE_ENTERED'; | export const STAGE_ENTERED = 'STAGE_ENTERED'; | ||||||
| 
 | 
 | ||||||
| @ -70,15 +71,18 @@ export class FeatureLifecycleService extends EventEmitter { | |||||||
|                 this.featureInitialized(event.featureName), |                 this.featureInitialized(event.featureName), | ||||||
|             ); |             ); | ||||||
|         }); |         }); | ||||||
|         this.eventBus.on(CLIENT_METRICS, async (event) => { |         this.eventBus.on( | ||||||
|             if (!event.featureName || !event.environment) return; |             CLIENT_METRICS, | ||||||
|  |             async (event: ValidatedClientMetrics) => { | ||||||
|  |                 if (event.environment) { | ||||||
|  |                     const features = Object.keys(event.bucket.toggles); | ||||||
|  |                     const environment = event.environment; | ||||||
|                     await this.checkEnabled(() => |                     await this.checkEnabled(() => | ||||||
|                 this.featureReceivedMetrics( |                         this.featuresReceivedMetrics(features, environment), | ||||||
|                     event.featureName, |                     ); | ||||||
|                     event.environment, |                 } | ||||||
|                 ), |             }, | ||||||
|         ); |         ); | ||||||
|         }); |  | ||||||
|         this.eventStore.on(FEATURE_COMPLETED, async (event) => { |         this.eventStore.on(FEATURE_COMPLETED, async (event) => { | ||||||
|             await this.checkEnabled(() => |             await this.checkEnabled(() => | ||||||
|                 this.featureCompleted(event.featureName), |                 this.featureCompleted(event.featureName), | ||||||
| @ -96,25 +100,26 @@ export class FeatureLifecycleService extends EventEmitter { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private async featureInitialized(feature: string) { |     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' }); |         this.emit(STAGE_ENTERED, { stage: 'initial' }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private async stageReceivedMetrics( |     private async stageReceivedMetrics( | ||||||
|         feature: string, |         features: string[], | ||||||
|         stage: 'live' | 'pre-live', |         stage: 'live' | 'pre-live', | ||||||
|     ) { |     ) { | ||||||
|         const stageExists = await this.featureLifecycleStore.stageExists({ |         await this.featureLifecycleStore.insert( | ||||||
|             stage, |             features.map((feature) => ({ feature, stage })), | ||||||
|             feature, |         ); | ||||||
|         }); |  | ||||||
|         if (!stageExists) { |  | ||||||
|             await this.featureLifecycleStore.insert({ feature, stage }); |  | ||||||
|         this.emit(STAGE_ENTERED, { stage }); |         this.emit(STAGE_ENTERED, { stage }); | ||||||
|     } |     } | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     private async featureReceivedMetrics(feature: string, environment: string) { |     private async featuresReceivedMetrics( | ||||||
|  |         features: string[], | ||||||
|  |         environment: string, | ||||||
|  |     ) { | ||||||
|         try { |         try { | ||||||
|             const env = await this.environmentStore.get(environment); |             const env = await this.environmentStore.get(environment); | ||||||
| 
 | 
 | ||||||
| @ -122,28 +127,32 @@ export class FeatureLifecycleService extends EventEmitter { | |||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             if (env.type === 'production') { |             if (env.type === 'production') { | ||||||
|                 await this.stageReceivedMetrics(feature, 'live'); |                 await this.stageReceivedMetrics(features, 'live'); | ||||||
|             } else if (env.type === 'development') { |             } else if (env.type === 'development') { | ||||||
|                 await this.stageReceivedMetrics(feature, 'pre-live'); |                 await this.stageReceivedMetrics(features, 'pre-live'); | ||||||
|             } |             } | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             this.logger.warn( |             this.logger.warn( | ||||||
|                 `Error handling metrics for ${feature} in ${environment}`, |                 `Error handling ${features.length} metrics in ${environment}`, | ||||||
|                 e, |                 e, | ||||||
|             ); |             ); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private async featureCompleted(feature: string) { |     private async featureCompleted(feature: string) { | ||||||
|         await this.featureLifecycleStore.insert({ |         await this.featureLifecycleStore.insert([ | ||||||
|  |             { | ||||||
|                 feature, |                 feature, | ||||||
|                 stage: 'completed', |                 stage: 'completed', | ||||||
|         }); |             }, | ||||||
|  |         ]); | ||||||
|         this.emit(STAGE_ENTERED, { stage: 'completed' }); |         this.emit(STAGE_ENTERED, { stage: 'completed' }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private async featureArchived(feature: string) { |     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' }); |         this.emit(STAGE_ENTERED, { stage: 'archived' }); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ export type FeatureLifecycleStage = { | |||||||
| export type FeatureLifecycleView = IFeatureLifecycleStage[]; | export type FeatureLifecycleView = IFeatureLifecycleStage[]; | ||||||
| 
 | 
 | ||||||
| export interface IFeatureLifecycleStore { | export interface IFeatureLifecycleStore { | ||||||
|     insert(featureLifecycleStage: FeatureLifecycleStage): Promise<void>; |     insert(featureLifecycleStages: FeatureLifecycleStage[]): Promise<void>; | ||||||
|     get(feature: string): Promise<FeatureLifecycleView>; |     get(feature: string): Promise<FeatureLifecycleView>; | ||||||
|     stageExists(stage: FeatureLifecycleStage): Promise<boolean>; |     stageExists(stage: FeatureLifecycleStage): Promise<boolean>; | ||||||
| } | } | ||||||
|  | |||||||
| @ -19,12 +19,16 @@ export class FeatureLifecycleStore implements IFeatureLifecycleStore { | |||||||
|         this.db = db; |         this.db = db; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async insert(featureLifecycleStage: FeatureLifecycleStage): Promise<void> { |     async insert( | ||||||
|  |         featureLifecycleStages: FeatureLifecycleStage[], | ||||||
|  |     ): Promise<void> { | ||||||
|         await this.db('feature_lifecycles') |         await this.db('feature_lifecycles') | ||||||
|             .insert({ |             .insert( | ||||||
|                 feature: featureLifecycleStage.feature, |                 featureLifecycleStages.map((stage) => ({ | ||||||
|                 stage: featureLifecycleStage.stage, |                     feature: stage.feature, | ||||||
|             }) |                     stage: stage.stage, | ||||||
|  |                 })), | ||||||
|  |             ) | ||||||
|             .returning('*') |             .returning('*') | ||||||
|             .onConflict(['feature', 'stage']) |             .onConflict(['feature', 'stage']) | ||||||
|             .ignore(); |             .ignore(); | ||||||
|  | |||||||
| @ -86,16 +86,21 @@ test('should return lifecycle stages', async () => { | |||||||
|     await reachedStage('initial'); |     await reachedStage('initial'); | ||||||
|     await expectFeatureStage('initial'); |     await expectFeatureStage('initial'); | ||||||
|     eventBus.emit(CLIENT_METRICS, { |     eventBus.emit(CLIENT_METRICS, { | ||||||
|         featureName: 'my_feature_a', |         bucket: { toggles: { my_feature_a: 'irrelevant' } }, | ||||||
|         environment: 'default', |         environment: 'default', | ||||||
|     }); |     }); | ||||||
|     // missing feature
 |     // missing feature
 | ||||||
|     eventBus.emit(CLIENT_METRICS, { |     eventBus.emit(CLIENT_METRICS, { | ||||||
|         environment: 'default', |         environment: 'default', | ||||||
|  |         bucket: { toggles: {} }, | ||||||
|     }); |     }); | ||||||
|     // non existent env
 |     // non existent env
 | ||||||
|     eventBus.emit(CLIENT_METRICS, { |     eventBus.emit(CLIENT_METRICS, { | ||||||
|         featureName: 'my_feature_a', |         bucket: { | ||||||
|  |             toggles: { | ||||||
|  |                 my_feature_a: 'irrelevant', | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|         environment: 'non-existent', |         environment: 'non-existent', | ||||||
|     }); |     }); | ||||||
|     await reachedStage('live'); |     await reachedStage('live'); | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user