UNPKG

@featurevisor/sdk

Version:

Featurevisor SDK for Node.js and the browser

445 lines (374 loc) 11.4 kB
import type { Context, Feature, FeatureKey, StickyFeatures, EvaluatedFeatures, EvaluatedFeature, VariableValue, VariationValue, VariableKey, DatafileContent, } from "@featurevisor/types"; import { createLogger, Logger, LogLevel } from "./logger"; import { HooksManager, Hook } from "./hooks"; import { Emitter, EventCallback, EventName } from "./emitter"; import { DatafileReader } from "./datafileReader"; import { Evaluation, EvaluateDependencies, evaluateWithHooks } from "./evaluate"; import { FeaturevisorChildInstance } from "./child"; import { getParamsForStickySetEvent, getParamsForDatafileSetEvent } from "./events"; import { getValueByType } from "./helpers"; const emptyDatafile: DatafileContent = { schemaVersion: "2", revision: "unknown", segments: {}, features: {}, }; export interface OverrideOptions { sticky?: StickyFeatures; defaultVariationValue?: VariationValue; defaultVariableValue?: VariableValue; } export interface InstanceOptions { datafile?: DatafileContent | string; context?: Context; logLevel?: LogLevel; logger?: Logger; sticky?: StickyFeatures; hooks?: Hook[]; } export class FeaturevisorInstance { // from options private context: Context = {}; private logger: Logger; private sticky?: StickyFeatures; // internally created private datafileReader: DatafileReader; private hooksManager: HooksManager; private emitter: Emitter; constructor(options: InstanceOptions) { // from options this.context = options.context || {}; this.logger = options.logger || createLogger({ level: options.logLevel || Logger.defaultLevel, }); this.hooksManager = new HooksManager({ hooks: options.hooks || [], logger: this.logger, }); this.emitter = new Emitter(); this.sticky = options.sticky; // datafile this.datafileReader = new DatafileReader({ datafile: emptyDatafile, logger: this.logger, }); if (options.datafile) { this.datafileReader = new DatafileReader({ datafile: typeof options.datafile === "string" ? JSON.parse(options.datafile) : options.datafile, logger: this.logger, }); } this.logger.info("Featurevisor SDK initialized"); } setLogLevel(level: LogLevel) { this.logger.setLevel(level); } setDatafile(datafile: DatafileContent | string) { try { const newDatafileReader = new DatafileReader({ datafile: typeof datafile === "string" ? JSON.parse(datafile) : datafile, logger: this.logger, }); const details = getParamsForDatafileSetEvent(this.datafileReader, newDatafileReader); this.datafileReader = newDatafileReader; this.logger.info("datafile set", details); this.emitter.trigger("datafile_set", details); } catch (e) { this.logger.error("could not parse datafile", { error: e }); } } setSticky(sticky: StickyFeatures, replace = false) { const previousStickyFeatures = this.sticky || {}; if (replace) { this.sticky = { ...sticky }; } else { this.sticky = { ...this.sticky, ...sticky, }; } const params = getParamsForStickySetEvent(previousStickyFeatures, this.sticky, replace); this.logger.info("sticky features set", params); this.emitter.trigger("sticky_set", params); } getRevision(): string { return this.datafileReader.getRevision(); } getFeature(featureKey: string): Feature | undefined { return this.datafileReader.getFeature(featureKey); } addHook(hook: Hook) { return this.hooksManager.add(hook); } on(eventName: EventName, callback: EventCallback) { return this.emitter.on(eventName, callback); } close() { this.emitter.clearAll(); } /** * Context */ setContext(context: Context, replace = false) { if (replace) { this.context = context; } else { this.context = { ...this.context, ...context }; } this.emitter.trigger("context_set", { context: this.context, replaced: replace, }); this.logger.debug(replace ? "context replaced" : "context updated", { context: this.context, replaced: replace, }); } getContext(context?: Context): Context { return context ? { ...this.context, ...context, } : this.context; } spawn(context: Context = {}, options: OverrideOptions = {}): FeaturevisorChildInstance { return new FeaturevisorChildInstance({ parent: this, context: this.getContext(context), sticky: options.sticky, }); } /** * Flag */ private getEvaluationDependencies( context: Context, options: OverrideOptions = {}, ): EvaluateDependencies { return { context: this.getContext(context), logger: this.logger, hooksManager: this.hooksManager, datafileReader: this.datafileReader, // OverrideOptions sticky: options.sticky ? { ...this.sticky, ...options.sticky, } : this.sticky, defaultVariationValue: options.defaultVariationValue, defaultVariableValue: options.defaultVariableValue, }; } evaluateFlag( featureKey: FeatureKey, context: Context = {}, options: OverrideOptions = {}, ): Evaluation { return evaluateWithHooks({ ...this.getEvaluationDependencies(context, options), type: "flag", featureKey, }); } isEnabled(featureKey: FeatureKey, context: Context = {}, options: OverrideOptions = {}): boolean { try { const evaluation = this.evaluateFlag(featureKey, context, options); return evaluation.enabled === true; } catch (e) { this.logger.error("isEnabled", { featureKey, error: e }); return false; } } /** * Variation */ evaluateVariation( featureKey: FeatureKey, context: Context = {}, options: OverrideOptions = {}, ): Evaluation { return evaluateWithHooks({ ...this.getEvaluationDependencies(context, options), type: "variation", featureKey, }); } getVariation( featureKey: FeatureKey, context: Context = {}, options: OverrideOptions = {}, ): VariationValue | null { try { const evaluation = this.evaluateVariation(featureKey, context, options); if (typeof evaluation.variationValue !== "undefined") { return evaluation.variationValue; } if (evaluation.variation) { return evaluation.variation.value; } return null; } catch (e) { this.logger.error("getVariation", { featureKey, error: e }); return null; } } /** * Variable */ evaluateVariable( featureKey: FeatureKey, variableKey: VariableKey, context: Context = {}, options: OverrideOptions = {}, ): Evaluation { return evaluateWithHooks({ ...this.getEvaluationDependencies(context, options), type: "variable", featureKey, variableKey, }); } getVariable( featureKey: FeatureKey, variableKey: string, context: Context = {}, options: OverrideOptions = {}, ): VariableValue | null { try { const evaluation = this.evaluateVariable(featureKey, variableKey, context, options); if (typeof evaluation.variableValue !== "undefined") { if ( evaluation.variableSchema && evaluation.variableSchema.type === "json" && typeof evaluation.variableValue === "string" ) { return JSON.parse(evaluation.variableValue); } return evaluation.variableValue; } return null; } catch (e) { this.logger.error("getVariable", { featureKey, variableKey, error: e }); return null; } } getVariableBoolean( featureKey: FeatureKey, variableKey: string, context: Context = {}, options: OverrideOptions = {}, ): boolean | null { const variableValue = this.getVariable(featureKey, variableKey, context, options); return getValueByType(variableValue, "boolean") as boolean | null; } getVariableString( featureKey: FeatureKey, variableKey: string, context: Context = {}, options: OverrideOptions = {}, ): string | null { const variableValue = this.getVariable(featureKey, variableKey, context, options); return getValueByType(variableValue, "string") as string | null; } getVariableInteger( featureKey: FeatureKey, variableKey: string, context: Context = {}, options: OverrideOptions = {}, ): number | null { const variableValue = this.getVariable(featureKey, variableKey, context, options); return getValueByType(variableValue, "integer") as number | null; } getVariableDouble( featureKey: FeatureKey, variableKey: string, context: Context = {}, options: OverrideOptions = {}, ): number | null { const variableValue = this.getVariable(featureKey, variableKey, context, options); return getValueByType(variableValue, "double") as number | null; } getVariableArray( featureKey: FeatureKey, variableKey: string, context: Context = {}, options: OverrideOptions = {}, ): string[] | null { const variableValue = this.getVariable(featureKey, variableKey, context, options); return getValueByType(variableValue, "array") as string[] | null; } getVariableObject<T>( featureKey: FeatureKey, variableKey: string, context: Context = {}, options: OverrideOptions = {}, ): T | null { const variableValue = this.getVariable(featureKey, variableKey, context, options); return getValueByType(variableValue, "object") as T | null; } getVariableJSON<T>( featureKey: FeatureKey, variableKey: string, context: Context = {}, options: OverrideOptions = {}, ): T | null { const variableValue = this.getVariable(featureKey, variableKey, context, options); return getValueByType(variableValue, "json") as T | null; } getAllEvaluations( context: Context = {}, featureKeys: string[] = [], options: OverrideOptions = {}, ): EvaluatedFeatures { const result: EvaluatedFeatures = {}; const keys = featureKeys.length > 0 ? featureKeys : this.datafileReader.getFeatureKeys(); for (const featureKey of keys) { // isEnabled const evaluatedFeature: EvaluatedFeature = { enabled: this.isEnabled(featureKey, context, options), }; // variation if (this.datafileReader.hasVariations(featureKey)) { const variation = this.getVariation(featureKey, context, options); if (variation) { evaluatedFeature.variation = variation; } } // variables const variableKeys = this.datafileReader.getVariableKeys(featureKey); if (variableKeys.length > 0) { evaluatedFeature.variables = {}; for (const variableKey of variableKeys) { evaluatedFeature.variables[variableKey] = this.getVariable( featureKey, variableKey, context, options, ); } } result[featureKey] = evaluatedFeature; } return result; } } export function createInstance(options: InstanceOptions = {}): FeaturevisorInstance { return new FeaturevisorInstance(options); }