UNPKG

@unleash/proxy

Version:
222 lines (189 loc) 6.66 kB
import EventEmitter from 'events'; import { type Context, type Unleash, UnleashEvents, type Variant, } from 'unleash-client'; import type { FeatureInterface } from 'unleash-client/lib/feature'; import type Metrics from 'unleash-client/lib/metrics'; import { getDefaultVariant } from 'unleash-client/lib/variant'; import type { IProxyConfig } from './config'; import type { Logger } from './logger'; import { lastMetricsFetch, lastMetricsUpdate } from './prom-metrics'; export type FeatureToggleStatus = { name: string; enabled: boolean; impressionData?: boolean; variant?: Variant; }; interface VariantBucket { [s: string]: number; } interface Bucket { toggles: { [s: string]: { yes?: number; no?: number; variants?: VariantBucket; }; }; } export interface IMetrics { bucket: Bucket; } export interface IClient extends EventEmitter { setUnleashApiToken: (unleashApiToken: string) => void; getEnabledToggles: (context: Context) => FeatureToggleStatus[]; getDefinedToggles: ( toggleNames: string[], context: Context, ) => FeatureToggleStatus[]; getAllToggles: (context: Context) => FeatureToggleStatus[]; getFeatureToggleDefinitions(): FeatureInterface[]; registerMetrics(metrics: IMetrics): void; isReady(): boolean; } class Client extends EventEmitter implements IClient { unleash: Unleash; private unleashApiToken: string; private environment?: string; private metrics: Metrics; private logger: Logger; private ready: boolean = false; constructor(config: IProxyConfig, unleash: Unleash, metrics: Metrics) { super(); this.unleashApiToken = config.unleashApiToken; this.environment = config.environment; this.logger = config.logger; this.unleash = unleash; this.metrics = metrics; this.metrics.on('error', (msg) => this.logger.error(`metrics: ${msg}`)); this.unleash.on('error', (msg) => this.logger.error(msg)); this.unleash.on('ready', () => { this.emit('ready'); this.ready = true; this.metrics.start(); }); this.unleash.on(UnleashEvents.Unchanged, () => { lastMetricsFetch.set(new Date().getTime()); }); this.unleash.on(UnleashEvents.Changed, () => { const updatedAt = new Date().getTime(); lastMetricsFetch.set(updatedAt); lastMetricsUpdate.set(updatedAt); }); } setUnleashApiToken(unleashApiToken: string): void { this.unleashApiToken = unleashApiToken; } fixContext(context: Context): Context { const { environment } = this; if (environment) { return { ...context, environment }; } return context; } getAllToggles(inContext: Context): FeatureToggleStatus[] { this.logger.debug( 'Get all feature toggles for provided context', inContext, ); const context = this.fixContext(inContext); const sessionId = context.sessionId || String(Math.random()); const definitions = this.unleash.getFeatureToggleDefinitions() || []; return definitions.map((d) => { const enabled = this.unleash.isEnabled(d.name, { ...context, sessionId, }); const variant = enabled ? this.unleash.getVariant(d.name, { ...context, sessionId }) : getDefaultVariant(); return { name: d.name, enabled: enabled, variant: variant, impressionData: d.impressionData, }; }); } getEnabledToggles(inContext: Context): FeatureToggleStatus[] { this.logger.debug( 'Get enabled feature toggles for provided context', inContext, ); const context = this.fixContext(inContext); const sessionId = context.sessionId || String(Math.random()); const definitions = this.unleash.getFeatureToggleDefinitions() || []; return definitions .filter((d) => this.unleash.isEnabled(d.name, { ...context, sessionId }), ) .map((d) => ({ name: d.name, enabled: true, variant: this.unleash.getVariant(d.name, { ...context, sessionId, }), impressionData: d.impressionData, })); } getDefinedToggles( toggleNames: string[], inContext: Context, ): FeatureToggleStatus[] { const context = this.fixContext(inContext); return toggleNames.map((name) => { const definition = this.unleash.getFeatureToggleDefinition(name); const enabled = this.unleash.isEnabled(name, context); this.metrics.count(name, enabled); return { name, enabled, variant: this.unleash.getVariant(name, context), impressionData: definition?.impressionData ?? false, }; }); } getFeatureToggleDefinitions(): FeatureInterface[] { return this.unleash.getFeatureToggleDefinitions(); } /* * A very simplistic implementation which support counts. * In future we must consider to look at start/stop times * and adjust counting thereafter. */ registerMetrics(metrics: IMetrics): void { const { toggles } = metrics.bucket; Object.keys(toggles).forEach((toggleName) => { const toggle = toggles[toggleName]; const yesCount: number = toggle.yes ?? 0; const noCount: number = toggle.no ?? 0; [...Array(yesCount)].forEach(() => this.metrics.count(toggleName, true), ); [...Array(noCount)].forEach(() => this.metrics.count(toggleName, false), ); const variants = toggle.variants; if (variants) { Object.entries(variants).forEach( ([variantName, variantCount]) => { [...Array(variantCount)].forEach(() => this.metrics.countVariant(toggleName, variantName), ); }, ); } }); } destroy(): void { this.unleash.destroy(); } isReady(): boolean { return this.ready; } } export default Client;