From 2442e5c973d0892634b6a07b6c9dfdd808c64ca6 Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Tue, 2 Sep 2025 10:37:34 +0300 Subject: [PATCH] fix: lifecycle metrics will now be posted across all environments (#10586) --- .../feature-lifecycle-service.ts | 165 ++++++++++++++++-- src/lib/types/experimental.ts | 3 +- 2 files changed, 156 insertions(+), 12 deletions(-) diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts index c8b27ea9be..d048c7b25c 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts @@ -8,8 +8,10 @@ import { FeatureCompletedEvent, FeatureUncompletedEvent, type IAuditUser, + type IEnvironment, type IEnvironmentStore, type IEventStore, + type IFeatureEnvironment, type IFeatureEnvironmentStore, type IFlagResolver, type IUnleashConfig, @@ -88,18 +90,25 @@ export class FeatureLifecycleService { CLIENT_METRICS_ADDED, async (events: IClientMetricsEnv[]) => { if (events.length > 0) { - const groupedByEnvironment = groupBy(events, 'environment'); + if (this.flagResolver.isEnabled('optimizeLifecycle')) { + await this.handleBulkMetrics(events); + } else { + const groupedByEnvironment = groupBy( + events, + 'environment', + ); - for (const [environment, metrics] of Object.entries( - groupedByEnvironment, - )) { - const features = metrics.map( - (metric) => metric.featureName, - ); - await this.featuresReceivedMetrics( - features, - environment, - ); + for (const [environment, metrics] of Object.entries( + groupedByEnvironment, + )) { + const features = metrics.map( + (metric) => metric.featureName, + ); + await this.featuresReceivedMetrics( + features, + environment, + ); + } } } }, @@ -169,6 +178,140 @@ export class FeatureLifecycleService { } } + /** + * Optimized bulk processing: reduces DB calls from O(4 * environments) to O(3) by batching all data fetches and processing in-memory + */ + private async handleBulkMetrics(events: IClientMetricsEnv[]) { + try { + const { environments, allFeatures } = + this.extractUniqueEnvironmentsAndFeatures(events); + const envMap = await this.buildEnvironmentMap(); + const featureEnvMap = + await this.buildFeatureEnvironmentMap(allFeatures); + const allStagesToInsert = this.determineLifecycleStages( + events, + environments, + envMap, + featureEnvMap, + ); + + if (allStagesToInsert.length > 0) { + const newlyEnteredStages = + await this.featureLifecycleStore.insert(allStagesToInsert); + this.recordStagesEntered(newlyEnteredStages); + } + } catch (e) { + this.logger.warn( + `Error handling bulk metrics for ${events.length} events`, + e, + ); + } + } + + private extractUniqueEnvironmentsAndFeatures(events: IClientMetricsEnv[]) { + const environments = [...new Set(events.map((e) => e.environment))]; + const allFeatures = [...new Set(events.map((e) => e.featureName))]; + return { environments, allFeatures }; + } + + private async buildEnvironmentMap(): Promise> { + const allEnvs = await this.environmentStore.getAll(); + return new Map(allEnvs.map((env) => [env.name, env])); + } + + private async buildFeatureEnvironmentMap(allFeatures: string[]) { + const allFeatureEnvs = + await this.featureEnvironmentStore.getAllByFeatures(allFeatures); + const featureEnvMap = new Map< + string, + Map + >(); + + allFeatureEnvs.forEach((fe) => { + if (!featureEnvMap.has(fe.environment)) { + featureEnvMap.set(fe.environment, new Map()); + } + const envMap = featureEnvMap.get(fe.environment); + if (envMap) { + envMap.set(fe.featureName, fe); + } + }); + + return featureEnvMap; + } + + private determineLifecycleStages( + events: IClientMetricsEnv[], + environments: string[], + envMap: Map, + featureEnvMap: Map>, + ): Array<{ feature: string; stage: 'pre-live' | 'live' }> { + const allStagesToInsert: Array<{ + feature: string; + stage: 'pre-live' | 'live'; + }> = []; + + for (const environment of environments) { + const env = envMap.get(environment); + if (!env) continue; + + const envFeatures = this.getFeaturesForEnvironment( + events, + environment, + ); + allStagesToInsert.push(...this.createPreLiveStages(envFeatures)); + + if (env.type === 'production') { + const enabledFeatures = this.getEnabledFeaturesForEnvironment( + envFeatures, + environment, + featureEnvMap, + ); + allStagesToInsert.push( + ...this.createLiveStages(enabledFeatures), + ); + } + } + + return allStagesToInsert; + } + + private getFeaturesForEnvironment( + events: IClientMetricsEnv[], + environment: string, + ): string[] { + return events + .filter((e) => e.environment === environment) + .map((e) => e.featureName); + } + + private createPreLiveStages( + features: string[], + ): Array<{ feature: string; stage: 'pre-live' }> { + return features.map((feature) => ({ + feature, + stage: 'pre-live' as const, + })); + } + + private createLiveStages( + features: string[], + ): Array<{ feature: string; stage: 'live' }> { + return features.map((feature) => ({ feature, stage: 'live' as const })); + } + + private getEnabledFeaturesForEnvironment( + features: string[], + environment: string, + featureEnvMap: Map>, + ): string[] { + const envFeatureEnvs = featureEnvMap.get(environment) ?? new Map(); + return features.filter((feature) => { + const fe = envFeatureEnvs.get(feature); + return fe?.enabled; + }); + } + public async featureCompleted( feature: string, projectId: string, diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index 524a5f361b..8efae9bbe7 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -58,7 +58,8 @@ export type IFlagKey = | 'lifecycleGraphs' | 'addConfiguration' | 'etagByEnv' - | 'fetchMode'; + | 'fetchMode' + | 'optimizeLifecycle'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;