Skip to content

Commit

Permalink
fix: client metrics structure lifecycle (#6924)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew committed Apr 25, 2024
1 parent 477da7d commit 574eb28
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -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 });
Expand Down
65 changes: 37 additions & 28 deletions src/lib/features/feature-lifecycle/feature-lifecycle-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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),
Expand All @@ -96,54 +100,59 @@ 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);

if (!env) {
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' });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
}
14 changes: 9 additions & 5 deletions src/lib/features/feature-lifecycle/feature-lifecycle-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down

0 comments on commit 574eb28

Please sign in to comment.