1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

fix: client metrics structure lifecycle (#6924)

This commit is contained in:
Mateusz Kwasniewski 2024-04-25 09:27:20 +02:00 committed by GitHub
parent 477da7d514
commit 574eb284b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 70 additions and 39 deletions

View File

@ -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;
}

View File

@ -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 });

View File

@ -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;
await this.checkEnabled(() =>
this.featureReceivedMetrics(
event.featureName,
event.environment,
),
);
});
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.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 });
this.emit(STAGE_ENTERED, { 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({
feature,
stage: 'completed',
});
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' });
}
}

View File

@ -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>;
}

View File

@ -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();

View File

@ -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');