1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-05 17:53:12 +02:00

fix: lifecycle metrics will now be posted across all environments (#10586)

This commit is contained in:
Jaanus Sellin 2025-09-02 10:37:34 +03:00 committed by GitHub
parent 547f7ac14e
commit 2442e5c973
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 156 additions and 12 deletions

View File

@ -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<Map<string, IEnvironment>> {
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<string, IFeatureEnvironment>
>();
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<string, IEnvironment>,
featureEnvMap: Map<string, Map<string, IFeatureEnvironment>>,
): 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, Map<string, IFeatureEnvironment>>,
): 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,

View File

@ -58,7 +58,8 @@ export type IFlagKey =
| 'lifecycleGraphs'
| 'addConfiguration'
| 'etagByEnv'
| 'fetchMode';
| 'fetchMode'
| 'optimizeLifecycle';
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;