import type { ApiHost, Attributes, AutoExperiment, ClientKey, ClientOptions, DestroyOptions, EvalContext, EventLogger, EventProperties, Experiment, FeatureApiResponse, FeatureDefinitions, FeatureResult, FeatureUsageCallback, GlobalContext, InitOptions, InitResponse, InitSyncOptions, LogUnion, Plugin, RefreshFeaturesOptions, Result, TrackingCallback, TrackingCallbackWithUser, UserContext, WidenPrimitives, } from "./types/growthbook"; import { loadSDKVersion } from "./util"; import { clearAutoRefresh, configureCache, refreshFeatures, startStreaming, unsubscribe, } from "./feature-repository"; import { decryptPayload, evalFeature as _evalFeature, getAllStickyBucketAssignmentDocs, getApiHosts, getTrackingUserContext, runExperiment, } from "./core"; import { StickyBucketService } from "./sticky-bucket-service"; const SDK_VERSION = loadSDKVersion(); export class GrowthBookClient< // eslint-disable-next-line @typescript-eslint/no-explicit-any AppFeatures extends Record = Record, > { public debug: boolean; public ready: boolean; public version: string; // Properties and methods that start with "_" are mangled by Terser (saves ~150 bytes) private _options: ClientOptions; private _features: FeatureDefinitions; private _experiments: AutoExperiment[]; private _payload: FeatureApiResponse | undefined; private _decryptedPayload: FeatureApiResponse | undefined; private _destroyed?: boolean; constructor(options?: ClientOptions) { options = options || {}; // These properties are all initialized in the constructor instead of above // This saves ~80 bytes in the final output this.version = SDK_VERSION; this._options = options; this.debug = !!options.debug; this.ready = false; this._features = {}; this._experiments = []; this.log = this.log.bind(this); if (options.plugins) { for (const plugin of options.plugins) { plugin(this); } } } public async setPayload(payload: FeatureApiResponse): Promise { this._payload = payload; const data = await decryptPayload(payload, this._options.decryptionKey); this._decryptedPayload = data; if (data.features) { this._features = data.features; } if (data.experiments) { this._experiments = data.experiments; } if (data.savedGroups) { this._options.savedGroups = data.savedGroups; } if (data.contextualBandits) { this._options.contextualBandits = data.contextualBandits; } this.ready = true; } public initSync(options: InitSyncOptions): GrowthBookClient { const payload = options.payload; if (payload.encryptedExperiments || payload.encryptedFeatures) { throw new Error("initSync does not support encrypted payloads"); } this._payload = payload; this._decryptedPayload = payload; if (payload.features) { this._features = payload.features; } if (payload.savedGroups) { this._options.savedGroups = payload.savedGroups; } if (payload.contextualBandits) { this._options.contextualBandits = payload.contextualBandits; } if (payload.experiments) { this._experiments = payload.experiments; } this.ready = true; startStreaming(this, options); return this; } public async init(options?: InitOptions): Promise { options = options || {}; if (options.cacheSettings) { configureCache(options.cacheSettings); } if (options.payload) { await this.setPayload(options.payload); startStreaming(this, options); return { success: true, source: "init", }; } else { const { data, ...res } = await this._refresh({ ...options, allowStale: true, }); startStreaming(this, options); await this.setPayload(data || {}); return res; } } public async refreshFeatures( options?: RefreshFeaturesOptions, ): Promise { const res = await this._refresh({ ...(options || {}), allowStale: false, }); if (res.data) { await this.setPayload(res.data); } } public getApiInfo(): [ApiHost, ClientKey] { return [this.getApiHosts().apiHost, this.getClientKey()]; } public getApiHosts() { return getApiHosts(this._options); } public getClientKey(): string { return this._options.clientKey || ""; } public getPayload(): FeatureApiResponse { return ( this._payload || { features: this.getFeatures(), experiments: this._experiments || [], } ); } public getDecryptedPayload(): FeatureApiResponse { return this._decryptedPayload || this.getPayload(); } private async _refresh({ timeout, skipCache, allowStale, streaming, }: RefreshFeaturesOptions & { allowStale?: boolean; streaming?: boolean; }) { if (!this._options.clientKey) { throw new Error("Missing clientKey"); } // Trigger refresh in feature repository return refreshFeatures({ instance: this, timeout, skipCache: skipCache || this._options.disableCache, allowStale, backgroundSync: streaming ?? true, }); } public getFeatures() { return this._features || {}; } public getGlobalAttributes(): Attributes { return this._options.globalAttributes || {}; } public setGlobalAttributes(attributes: Attributes) { this._options.globalAttributes = attributes; } public destroy(options?: DestroyOptions) { options = options || {}; this._destroyed = true; unsubscribe(this); if (options.destroyAllStreams) { clearAutoRefresh(); } // Release references to save memory this._features = {}; this._experiments = []; this._decryptedPayload = undefined; this._payload = undefined; this._options = {}; } public isDestroyed() { return !!this._destroyed; } public setEventLogger(logger: EventLogger) { this._options.eventLogger = logger; } public logEvent( eventName: string, properties: EventProperties, userContext: UserContext, ) { if (this._options.eventLogger) { const ctx = this._getEvalContext(userContext); this._options.eventLogger( eventName, properties, getTrackingUserContext(ctx.user), ); } } public runInlineExperiment( experiment: Experiment, userContext: UserContext, ): Result { const { result } = runExperiment( experiment, null, this._getEvalContext(userContext), ); return result; } private _getEvalContext(userContext: UserContext): EvalContext { if (this._options.globalAttributes) { userContext = { ...userContext, attributes: { ...this._options.globalAttributes, ...userContext.attributes, }, }; } return { user: userContext, global: this._getGlobalContext(), stack: { evaluatedFeatures: new Set(), }, }; } private _getGlobalContext(): GlobalContext { return { features: this._features, experiments: this._experiments, log: this.log, enabled: this._options.enabled, qaMode: this._options.qaMode, savedGroups: this._options.savedGroups, contextualBandits: this._options.contextualBandits, forcedFeatureValues: this._options.forcedFeatureValues, forcedVariations: this._options.forcedVariations, trackingCallback: this._options.trackingCallback, onFeatureUsage: this._options.onFeatureUsage, eventLogger: this._options.eventLogger, }; } public isOn( key: K, userContext: UserContext, ): boolean { return this.evalFeature(key, userContext).on; } public isOff( key: K, userContext: UserContext, ): boolean { return this.evalFeature(key, userContext).off; } public getFeatureValue< V extends AppFeatures[K], K extends string & keyof AppFeatures = string, >(key: K, defaultValue: V, userContext: UserContext): WidenPrimitives { const value = this.evalFeature, K>( key, userContext, ).value; return value === null ? (defaultValue as WidenPrimitives) : value; } public evalFeature< V extends AppFeatures[K], K extends string & keyof AppFeatures = string, >(id: K, userContext: UserContext): FeatureResult { return _evalFeature(id, this._getEvalContext(userContext)); } log(msg: string, ctx: Record) { if (!this.debug) return; if (this._options.log) this._options.log(msg, ctx); else console.log(msg, ctx); } public setTrackingCallback(callback: TrackingCallbackWithUser) { this._options.trackingCallback = callback; } public setFeatureUsageCallback(callback: FeatureUsageCallback) { this._options.onFeatureUsage = callback; } public async applyStickyBuckets( partialContext: Omit< UserContext, "stickyBucketService" | "stickyBucketAssignmentDocs" >, stickyBucketService: StickyBucketService, ): Promise { const ctx = this._getEvalContext(partialContext); const stickyBucketAssignmentDocs = await getAllStickyBucketAssignmentDocs( ctx, stickyBucketService, ); return { ...partialContext, stickyBucketAssignmentDocs, saveStickyBucketAssignmentDoc: (doc) => stickyBucketService.saveAssignments(doc), }; } public createScopedInstance( userContext: UserContext, userPlugins?: Plugin[], ) { return new UserScopedGrowthBook(this, userContext, [ ...(this._options.plugins || []), ...(userPlugins || []), ]); } } export class UserScopedGrowthBook< // eslint-disable-next-line @typescript-eslint/no-explicit-any AppFeatures extends Record = Record, > { private _gb: GrowthBookClient; private _userContext: UserContext; public logs: Array; constructor( gb: GrowthBookClient, userContext: UserContext, plugins?: Plugin[], ) { this._gb = gb; this._userContext = userContext; this.logs = []; this._userContext.trackedExperiments = this._userContext.trackedExperiments || new Set(); this._userContext.trackedFeatureUsage = this._userContext.trackedFeatureUsage || {}; this._userContext.devLogs = this.logs; if (plugins) { for (const plugin of plugins) { plugin(this); } } } public runInlineExperiment(experiment: Experiment): Result { return this._gb.runInlineExperiment(experiment, this._userContext); } public isOn(key: K): boolean { return this._gb.isOn(key, this._userContext); } public isOff(key: K): boolean { return this._gb.isOff(key, this._userContext); } public getFeatureValue< V extends AppFeatures[K], K extends string & keyof AppFeatures = string, >(key: K, defaultValue: V): WidenPrimitives { return this._gb.getFeatureValue(key, defaultValue, this._userContext); } public evalFeature< V extends AppFeatures[K], K extends string & keyof AppFeatures = string, >(id: K): FeatureResult { return this._gb.evalFeature(id, this._userContext); } public logEvent(eventName: string, properties?: EventProperties) { if (this._userContext.enableDevMode) { this.logs.push({ eventName, properties, timestamp: Date.now().toString(), logType: "event", }); } this._gb.logEvent(eventName, properties || {}, this._userContext); } public setTrackingCallback(cb: TrackingCallback) { this._userContext.trackingCallback = cb; } public setFeatureUsageCallback(cb: FeatureUsageCallback) { this._userContext.onFeatureUsage = cb; } public getApiInfo(): [ApiHost, ClientKey] { return this._gb.getApiInfo(); } public getClientKey() { return this._gb.getClientKey(); } public setURL(url: string) { this._userContext.url = url; } public updateAttributes(attributes: Attributes) { this._userContext.attributes = { ...this._userContext.attributes, ...attributes, }; } public setAttributeOverrides(overrides: Attributes) { this._userContext.attributeOverrides = overrides; } public async setForcedVariations(vars: Record) { this._userContext.forcedVariations = vars || {}; } // eslint-disable-next-line public setForcedFeatures(map: Map) { this._userContext.forcedFeatureValues = map; } public getUserContext() { return this._userContext; } public getVersion() { return SDK_VERSION; } public getDecryptedPayload() { return this._gb.getDecryptedPayload(); } public inDevMode(): boolean { return !!this._userContext.enableDevMode; } }