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:
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,
|
||||||
await this.checkEnabled(() =>
|
async (event: ValidatedClientMetrics) => {
|
||||||
this.featureReceivedMetrics(
|
if (event.environment) {
|
||||||
event.featureName,
|
const features = Object.keys(event.bucket.toggles);
|
||||||
event.environment,
|
const environment = event.environment;
|
||||||
),
|
await this.checkEnabled(() =>
|
||||||
);
|
this.featuresReceivedMetrics(features, 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,
|
);
|
||||||
});
|
this.emit(STAGE_ENTERED, { stage });
|
||||||
if (!stageExists) {
|
|
||||||
await this.featureLifecycleStore.insert({ feature, 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,
|
{
|
||||||
stage: 'completed',
|
feature,
|
||||||
});
|
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