feat: Complete zCode CLI X with Telegram bot integration

- Add full Telegram bot functionality with Z.AI API integration
- Implement 4 tools: Bash, FileEdit, WebSearch, Git
- Add 3 agents: Code Reviewer, Architect, DevOps Engineer
- Add 6 skills for common coding tasks
- Add systemd service file for 24/7 operation
- Add nginx configuration for HTTPS webhook
- Add comprehensive documentation
- Implement WebSocket server for real-time updates
- Add logging system with Winston
- Add environment validation

🤖 zCode CLI X - Agentic coder with Z.AI + Telegram integration
This commit is contained in:
admin
2026-05-05 09:01:26 +00:00
Unverified
parent 4a7035dd92
commit 875c7f9b91
24688 changed files with 3224957 additions and 221 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,123 @@
import type { ApiHost, Attributes, AutoExperiment, AutoExperimentVariation, ClientKey, Options, Experiment, FeatureApiResponse, FeatureDefinition, FeatureResult, FeatureUsageCallback, LoadFeaturesOptions, RefreshFeaturesOptions, RenderFunction, Result, SubscriptionFunction, TrackingCallback, TrackingData, WidenPrimitives, InitOptions, InitResponse, InitSyncOptions, PrefetchOptions, StickyAssignmentsDocument, EventLogger, LogUnion, DestroyOptions } from "./types/growthbook";
import { StickyBucketServiceSync } from "./sticky-bucket-service";
export declare class GrowthBook<AppFeatures extends Record<string, any> = Record<string, any>> {
private context;
debug: boolean;
ready: boolean;
version: string;
logs: Array<LogUnion>;
private _options;
private _renderer;
private _redirectedUrl;
private _trackedExperiments;
private _completedChangeIds;
private _trackedFeatures;
private _subscriptions;
private _assigned;
private _activeAutoExperiments;
private _triggeredExpKeys;
private _initialized;
private _deferredTrackingCalls;
private _saveStickyBucketAssignmentDoc;
private _payload;
private _decryptedPayload;
private _destroyCallbacks;
private _autoExperimentsAllowed;
private _destroyed?;
constructor(options?: Options);
setPayload(payload: FeatureApiResponse): Promise<void>;
initSync(options: InitSyncOptions): GrowthBook;
init(options?: InitOptions): Promise<InitResponse>;
/** @deprecated Use {@link init} */
loadFeatures(options?: LoadFeaturesOptions): Promise<void>;
refreshFeatures(options?: RefreshFeaturesOptions): Promise<void>;
getApiInfo(): [ApiHost, ClientKey];
getApiHosts(): {
apiHost: string;
streamingHost: string;
apiRequestHeaders?: Record<string, string>;
streamingHostRequestHeaders?: Record<string, string>;
};
getClientKey(): string;
getPayload(): FeatureApiResponse;
getDecryptedPayload(): FeatureApiResponse;
isRemoteEval(): boolean;
getCacheKeyAttributes(): (keyof Attributes)[] | undefined;
private _refresh;
private _render;
/** @deprecated Use {@link setPayload} */
setFeatures(features: Record<string, FeatureDefinition>): void;
/** @deprecated Use {@link setPayload} */
setEncryptedFeatures(encryptedString: string, decryptionKey?: string, subtle?: SubtleCrypto): Promise<void>;
/** @deprecated Use {@link setPayload} */
setExperiments(experiments: AutoExperiment[]): void;
/** @deprecated Use {@link setPayload} */
setEncryptedExperiments(encryptedString: string, decryptionKey?: string, subtle?: SubtleCrypto): Promise<void>;
setAttributes(attributes: Attributes): Promise<void>;
updateAttributes(attributes: Attributes): Promise<void>;
setAttributeOverrides(overrides: Attributes): Promise<void>;
setForcedVariations(vars: Record<string, number>): Promise<void>;
setForcedFeatures(map: Map<string, any>): void;
setURL(url: string): Promise<void>;
getAttributes(): {
[x: string]: any;
};
getForcedVariations(): Record<string, number>;
getForcedFeatures(): Map<string, any>;
getStickyBucketAssignmentDocs(): Record<string, StickyAssignmentsDocument>;
getUrl(): string;
getFeatures(): Record<string, FeatureDefinition<any>>;
getExperiments(): AutoExperiment<AutoExperimentVariation>[];
getCompletedChangeIds(): string[];
subscribe(cb: SubscriptionFunction): () => void;
private _refreshForRemoteEval;
getAllResults(): Map<string, {
experiment: Experiment<any>;
result: Result<any>;
}>;
onDestroy(cb: () => void): void;
isDestroyed(): boolean;
destroy(options?: DestroyOptions): void;
setRenderer(renderer: null | RenderFunction): void;
forceVariation(key: string, variation: number): void;
run<T>(experiment: Experiment<T>): Result<T>;
triggerExperiment(key: string): Result<AutoExperimentVariation>[] | null;
triggerAutoExperiments(): void;
private _getEvalContext;
private _getUserContext;
private _getGlobalContext;
private _runAutoExperiment;
private _undoActiveAutoExperiment;
private _updateAllAutoExperiments;
private _onExperimentEval;
private _fireSubscriptions;
private _recordChangedId;
isOn<K extends string & keyof AppFeatures = string>(key: K): boolean;
isOff<K extends string & keyof AppFeatures = string>(key: K): boolean;
getFeatureValue<V extends AppFeatures[K], K extends string & keyof AppFeatures = string>(key: K, defaultValue: V): WidenPrimitives<V>;
/**
* @deprecated Use {@link evalFeature}
* @param id
*/
feature<V extends AppFeatures[K], K extends string & keyof AppFeatures = string>(id: K): FeatureResult<V | null>;
evalFeature<V extends AppFeatures[K], K extends string & keyof AppFeatures = string>(id: K): FeatureResult<V | null>;
log(msg: string, ctx: Record<string, unknown>): void;
getDeferredTrackingCalls(): TrackingData[];
setDeferredTrackingCalls(calls: TrackingData[]): void;
fireDeferredTrackingCalls(): Promise<void>;
setTrackingCallback(callback: TrackingCallback): void;
setFeatureUsageCallback(callback: FeatureUsageCallback): void;
setEventLogger(logger: EventLogger): void;
logEvent(eventName: string, properties?: Record<string, unknown>): Promise<void>;
private _saveDeferredTrack;
private _getContextUrl;
private _isAutoExperimentBlockedByContext;
getRedirectUrl(): string;
private _getNavigateFunction;
private _applyDOMChanges;
refreshStickyBuckets(data?: FeatureApiResponse): Promise<void>;
generateStickyBucketAssignmentDocsSync(stickyBucketService: StickyBucketServiceSync, payload: FeatureApiResponse): Record<string, StickyAssignmentsDocument> | undefined;
inDevMode(): boolean;
}
export declare function prefetchPayload(options: PrefetchOptions): Promise<void>;
//# sourceMappingURL=GrowthBook.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"GrowthBook.d.ts","sourceRoot":"","sources":["../src/GrowthBook.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,OAAO,EACP,UAAU,EACV,cAAc,EACd,uBAAuB,EACvB,SAAS,EACT,OAAO,EACP,UAAU,EACV,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,oBAAoB,EACpB,mBAAmB,EACnB,sBAAsB,EACtB,cAAc,EACd,MAAM,EACN,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACZ,eAAe,EAEf,WAAW,EACX,YAAY,EACZ,eAAe,EACf,eAAe,EAGf,yBAAyB,EACzB,WAAW,EACX,QAAQ,EACR,cAAc,EACf,MAAM,oBAAoB,CAAC;AA0B5B,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAOlE,qBAAa,UAAU,CAErB,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAG7D,OAAO,CAAC,OAAO,CAAU;IAClB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;IAG7B,OAAO,CAAC,QAAQ,CAAU;IAC1B,OAAO,CAAC,SAAS,CAAwB;IACzC,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,mBAAmB,CAAc;IACzC,OAAO,CAAC,mBAAmB,CAAc;IACzC,OAAO,CAAC,gBAAgB,CAAyB;IACjD,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,SAAS,CAQf;IACF,OAAO,CAAC,sBAAsB,CAG5B;IACF,OAAO,CAAC,iBAAiB,CAAc;IACvC,OAAO,CAAC,YAAY,CAAU;IAC9B,OAAO,CAAC,sBAAsB,CAA4B;IAC1D,OAAO,CAAC,8BAA8B,CAEqB;IAE3D,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,iBAAiB,CAAiC;IAC1D,OAAO,CAAC,iBAAiB,CAAiB;IAE1C,OAAO,CAAC,uBAAuB,CAAU;IACzC,OAAO,CAAC,UAAU,CAAC,CAAU;gBAEjB,OAAO,CAAC,EAAE,OAAO;IAqGhB,UAAU,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB5D,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,UAAU;IAqCxC,IAAI,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IA0B/D,mCAAmC;IACtB,YAAY,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAW1D,eAAe,CAC1B,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,IAAI,CAAC;IAUT,UAAU,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC;IAGlC,WAAW;;;;;;IAGX,YAAY,IAAI,MAAM;IAGtB,UAAU,IAAI,kBAAkB;IAQhC,mBAAmB,IAAI,kBAAkB;IAIzC,YAAY,IAAI,OAAO;IAIvB,qBAAqB,IAAI,CAAC,MAAM,UAAU,CAAC,EAAE,GAAG,SAAS;YAIlD,QAAQ;IAsBtB,OAAO,CAAC,OAAO;IAUf,yCAAyC;IAClC,WAAW,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC;IAM9D,yCAAyC;IAC5B,oBAAoB,CAC/B,eAAe,EAAE,MAAM,EACvB,aAAa,CAAC,EAAE,MAAM,EACtB,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,IAAI,CAAC;IAWhB,yCAAyC;IAClC,cAAc,CAAC,WAAW,EAAE,cAAc,EAAE,GAAG,IAAI;IAM1D,yCAAyC;IAC5B,uBAAuB,CAClC,eAAe,EAAE,MAAM,EACvB,aAAa,CAAC,EAAE,MAAM,EACtB,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,IAAI,CAAC;IASH,aAAa,CAAC,UAAU,EAAE,UAAU;IAapC,gBAAgB,CAAC,UAAU,EAAE,UAAU;IAIvC,qBAAqB,CAAC,SAAS,EAAE,UAAU;IAa3C,mBAAmB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAWtD,iBAAiB,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC;IAKjC,MAAM,CAAC,GAAG,EAAE,MAAM;IAYxB,aAAa;;;IAIb,mBAAmB;IAInB,iBAAiB;IAKjB,6BAA6B;IAI7B,MAAM;IAIN,WAAW;IAIX,cAAc;IAId,qBAAqB,IAAI,MAAM,EAAE;IAIjC,SAAS,CAAC,EAAE,EAAE,oBAAoB,GAAG,MAAM,IAAI;YAOxC,qBAAqB;IAW5B,aAAa;oBA5bJ,UAAU,CAAC,GAAG,CAAC;gBAEnB,MAAM,CAAC,GAAG,CAAC;;IA8bhB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI;IAIxB,WAAW;IAIX,OAAO,CAAC,OAAO,CAAC,EAAE,cAAc;IA0ChC,WAAW,CAAC,QAAQ,EAAE,IAAI,GAAG,cAAc;IAI3C,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAW7C,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;IAM5C,iBAAiB,CAAC,GAAG,EAAE,MAAM;IAa7B,sBAAsB;IAK7B,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,eAAe;IAuBvB,OAAO,CAAC,iBAAiB;IAiBzB,OAAO,CAAC,kBAAkB;IAgI1B,OAAO,CAAC,yBAAyB;IAQjC,OAAO,CAAC,yBAAyB;IA6BjC,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,kBAAkB;IAuB1B,OAAO,CAAC,gBAAgB;IAIjB,IAAI,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,WAAW,GAAG,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO;IAIpE,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,WAAW,GAAG,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO;IAIrE,eAAe,CACpB,CAAC,SAAS,WAAW,CAAC,CAAC,CAAC,EACxB,CAAC,SAAS,MAAM,GAAG,MAAM,WAAW,GAAG,MAAM,EAC7C,GAAG,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC;IAK9C;;;OAGG;IAEI,OAAO,CACZ,CAAC,SAAS,WAAW,CAAC,CAAC,CAAC,EACxB,CAAC,SAAS,MAAM,GAAG,MAAM,WAAW,GAAG,MAAM,EAC7C,EAAE,EAAE,CAAC,GAAG,aAAa,CAAC,CAAC,GAAG,IAAI,CAAC;IAI1B,WAAW,CAChB,CAAC,SAAS,WAAW,CAAC,CAAC,CAAC,EACxB,CAAC,SAAS,MAAM,GAAG,MAAM,WAAW,GAAG,MAAM,EAC7C,EAAE,EAAE,CAAC,GAAG,aAAa,CAAC,CAAC,GAAG,IAAI,CAAC;IAIjC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAMtC,wBAAwB,IAAI,YAAY,EAAE;IAI1C,wBAAwB,CAAC,KAAK,EAAE,YAAY,EAAE;IAUxC,yBAAyB;IAoB/B,mBAAmB,CAAC,QAAQ,EAAE,gBAAgB;IAK9C,uBAAuB,CAAC,QAAQ,EAAE,oBAAoB;IAItD,cAAc,CAAC,MAAM,EAAE,WAAW;IAI5B,QAAQ,CACnB,SAAS,EAAE,MAAM,EACjB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IA6BtC,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,iCAAiC;IAmDlC,cAAc,IAAI,MAAM;IAI/B,OAAO,CAAC,oBAAoB;IAuB5B,OAAO,CAAC,gBAAgB;IA4BX,oBAAoB,CAAC,IAAI,CAAC,EAAE,kBAAkB;IAYpD,sCAAsC,CAC3C,mBAAmB,EAAE,uBAAuB,EAC5C,OAAO,EAAE,kBAAkB;IAatB,SAAS,IAAI,OAAO;CAG5B;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,eAAe,iBAY7D"}

View File

@@ -0,0 +1,73 @@
import type { ApiHost, Attributes, ClientKey, ClientOptions, DestroyOptions, EventLogger, EventProperties, Experiment, FeatureApiResponse, FeatureDefinitions, FeatureResult, FeatureUsageCallback, InitOptions, InitResponse, InitSyncOptions, LogUnion, Plugin, RefreshFeaturesOptions, Result, TrackingCallback, TrackingCallbackWithUser, UserContext, WidenPrimitives } from "./types/growthbook";
import { StickyBucketService } from "./sticky-bucket-service";
export declare class GrowthBookClient<AppFeatures extends Record<string, any> = Record<string, any>> {
debug: boolean;
ready: boolean;
version: string;
private _options;
private _features;
private _experiments;
private _payload;
private _decryptedPayload;
private _destroyed?;
constructor(options?: ClientOptions);
setPayload(payload: FeatureApiResponse): Promise<void>;
initSync(options: InitSyncOptions): GrowthBookClient<AppFeatures>;
init(options?: InitOptions): Promise<InitResponse>;
refreshFeatures(options?: RefreshFeaturesOptions): Promise<void>;
getApiInfo(): [ApiHost, ClientKey];
getApiHosts(): {
apiHost: string;
streamingHost: string;
apiRequestHeaders?: Record<string, string>;
streamingHostRequestHeaders?: Record<string, string>;
};
getClientKey(): string;
getPayload(): FeatureApiResponse;
getDecryptedPayload(): FeatureApiResponse;
private _refresh;
getFeatures(): FeatureDefinitions;
getGlobalAttributes(): Attributes;
setGlobalAttributes(attributes: Attributes): void;
destroy(options?: DestroyOptions): void;
isDestroyed(): boolean;
setEventLogger(logger: EventLogger): void;
logEvent(eventName: string, properties: EventProperties, userContext: UserContext): void;
runInlineExperiment<T>(experiment: Experiment<T>, userContext: UserContext): Result<T>;
private _getEvalContext;
private _getGlobalContext;
isOn<K extends string & keyof AppFeatures = string>(key: K, userContext: UserContext): boolean;
isOff<K extends string & keyof AppFeatures = string>(key: K, userContext: UserContext): boolean;
getFeatureValue<V extends AppFeatures[K], K extends string & keyof AppFeatures = string>(key: K, defaultValue: V, userContext: UserContext): WidenPrimitives<V>;
evalFeature<V extends AppFeatures[K], K extends string & keyof AppFeatures = string>(id: K, userContext: UserContext): FeatureResult<V | null>;
log(msg: string, ctx: Record<string, unknown>): void;
setTrackingCallback(callback: TrackingCallbackWithUser): void;
setFeatureUsageCallback(callback: FeatureUsageCallback): void;
applyStickyBuckets(partialContext: Omit<UserContext, "stickyBucketService" | "stickyBucketAssignmentDocs">, stickyBucketService: StickyBucketService): Promise<UserContext>;
createScopedInstance(userContext: UserContext, userPlugins?: Plugin[]): UserScopedGrowthBook<AppFeatures>;
}
export declare class UserScopedGrowthBook<AppFeatures extends Record<string, any> = Record<string, any>> {
private _gb;
private _userContext;
logs: Array<LogUnion>;
constructor(gb: GrowthBookClient<AppFeatures>, userContext: UserContext, plugins?: Plugin[]);
runInlineExperiment<T>(experiment: Experiment<T>): Result<T>;
isOn<K extends string & keyof AppFeatures = string>(key: K): boolean;
isOff<K extends string & keyof AppFeatures = string>(key: K): boolean;
getFeatureValue<V extends AppFeatures[K], K extends string & keyof AppFeatures = string>(key: K, defaultValue: V): WidenPrimitives<V>;
evalFeature<V extends AppFeatures[K], K extends string & keyof AppFeatures = string>(id: K): FeatureResult<V | null>;
logEvent(eventName: string, properties?: EventProperties): void;
setTrackingCallback(cb: TrackingCallback): void;
getApiInfo(): [ApiHost, ClientKey];
getClientKey(): string;
setURL(url: string): void;
updateAttributes(attributes: Attributes): void;
setAttributeOverrides(overrides: Attributes): void;
setForcedVariations(vars: Record<string, number>): Promise<void>;
setForcedFeatures(map: Map<string, any>): void;
getUserContext(): UserContext;
getVersion(): string;
getDecryptedPayload(): FeatureApiResponse;
inDevMode(): boolean;
}
//# sourceMappingURL=GrowthBookClient.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"GrowthBookClient.d.ts","sourceRoot":"","sources":["../src/GrowthBookClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EACP,UAAU,EAEV,SAAS,EACT,aAAa,EACb,cAAc,EAEd,WAAW,EACX,eAAe,EACf,UAAU,EACV,kBAAkB,EAClB,kBAAkB,EAClB,aAAa,EACb,oBAAoB,EAEpB,WAAW,EACX,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,MAAM,EACN,sBAAsB,EACtB,MAAM,EACN,gBAAgB,EAChB,wBAAwB,EACxB,WAAW,EACX,eAAe,EAChB,MAAM,oBAAoB,CAAC;AAgB5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAI9D,qBAAa,gBAAgB,CAE3B,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAEtD,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAGvB,OAAO,CAAC,QAAQ,CAAgB;IAEhC,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,iBAAiB,CAAiC;IAC1D,OAAO,CAAC,UAAU,CAAC,CAAU;gBAEjB,OAAO,CAAC,EAAE,aAAa;IAoBtB,UAAU,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB5D,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,gBAAgB,CAAC,WAAW,CAAC;IAuB3D,IAAI,CAAC,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;IAyBlD,eAAe,CAC1B,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,IAAI,CAAC;IAUT,UAAU,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC;IAGlC,WAAW;;;;;;IAGX,YAAY,IAAI,MAAM;IAGtB,UAAU,IAAI,kBAAkB;IAQhC,mBAAmB,IAAI,kBAAkB;YAIlC,QAAQ;IAsBf,WAAW;IAIX,mBAAmB,IAAI,UAAU;IAGjC,mBAAmB,CAAC,UAAU,EAAE,UAAU;IAI1C,OAAO,CAAC,OAAO,CAAC,EAAE,cAAc;IAgBhC,WAAW;IAIX,cAAc,CAAC,MAAM,EAAE,WAAW;IAIlC,QAAQ,CACb,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,eAAe,EAC3B,WAAW,EAAE,WAAW;IAQnB,mBAAmB,CAAC,CAAC,EAC1B,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,EACzB,WAAW,EAAE,WAAW,GACvB,MAAM,CAAC,CAAC,CAAC;IASZ,OAAO,CAAC,eAAe;IAoBvB,OAAO,CAAC,iBAAiB;IAelB,IAAI,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,WAAW,GAAG,MAAM,EACvD,GAAG,EAAE,CAAC,EACN,WAAW,EAAE,WAAW,GACvB,OAAO;IAIH,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,WAAW,GAAG,MAAM,EACxD,GAAG,EAAE,CAAC,EACN,WAAW,EAAE,WAAW,GACvB,OAAO;IAIH,eAAe,CACpB,CAAC,SAAS,WAAW,CAAC,CAAC,CAAC,EACxB,CAAC,SAAS,MAAM,GAAG,MAAM,WAAW,GAAG,MAAM,EAC7C,GAAG,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,WAAW,EAAE,WAAW,GAAG,eAAe,CAAC,CAAC,CAAC;IAQjE,WAAW,CAChB,CAAC,SAAS,WAAW,CAAC,CAAC,CAAC,EACxB,CAAC,SAAS,MAAM,GAAG,MAAM,WAAW,GAAG,MAAM,EAC7C,EAAE,EAAE,CAAC,EAAE,WAAW,EAAE,WAAW,GAAG,aAAa,CAAC,CAAC,GAAG,IAAI,CAAC;IAI3D,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAMtC,mBAAmB,CAAC,QAAQ,EAAE,wBAAwB;IAItD,uBAAuB,CAAC,QAAQ,EAAE,oBAAoB;IAIhD,kBAAkB,CAC7B,cAAc,EAAE,IAAI,CAClB,WAAW,EACX,qBAAqB,GAAG,4BAA4B,CACrD,EACD,mBAAmB,EAAE,mBAAmB,GACvC,OAAO,CAAC,WAAW,CAAC;IAgBhB,oBAAoB,CACzB,WAAW,EAAE,WAAW,EACxB,WAAW,CAAC,EAAE,MAAM,EAAE;CAOzB;AAED,qBAAa,oBAAoB,CAE/B,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAE7D,OAAO,CAAC,GAAG,CAAmB;IAC9B,OAAO,CAAC,YAAY,CAAc;IAC3B,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;gBAG3B,EAAE,EAAE,gBAAgB,CAAC,WAAW,CAAC,EACjC,WAAW,EAAE,WAAW,EACxB,OAAO,CAAC,EAAE,MAAM,EAAE;IAmBb,mBAAmB,CAAC,CAAC,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC;IAI5D,IAAI,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,WAAW,GAAG,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO;IAIpE,KAAK,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,WAAW,GAAG,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO;IAIrE,eAAe,CACpB,CAAC,SAAS,WAAW,CAAC,CAAC,CAAC,EACxB,CAAC,SAAS,MAAM,GAAG,MAAM,WAAW,GAAG,MAAM,EAC7C,GAAG,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,GAAG,eAAe,CAAC,CAAC,CAAC;IAIvC,WAAW,CAChB,CAAC,SAAS,WAAW,CAAC,CAAC,CAAC,EACxB,CAAC,SAAS,MAAM,GAAG,MAAM,WAAW,GAAG,MAAM,EAC7C,EAAE,EAAE,CAAC,GAAG,aAAa,CAAC,CAAC,GAAG,IAAI,CAAC;IAI1B,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,eAAe;IAYxD,mBAAmB,CAAC,EAAE,EAAE,gBAAgB;IAGxC,UAAU,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC;IAGlC,YAAY;IAGZ,MAAM,CAAC,GAAG,EAAE,MAAM;IAGlB,gBAAgB,CAAC,UAAU,EAAE,UAAU;IAMvC,qBAAqB,CAAC,SAAS,EAAE,UAAU;IAGrC,mBAAmB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAItD,iBAAiB,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC;IAGvC,cAAc;IAGd,UAAU;IAGV,mBAAmB;IAGnB,SAAS,IAAI,OAAO;CAG5B"}

View File

@@ -0,0 +1,33 @@
import { CacheSettings, Options as Context, FeatureApiResponse, TrackingCallback } from "./types/growthbook";
import { GrowthBook } from "./GrowthBook";
type WindowContext = Context & {
uuidCookieName?: string;
uuidKey?: string;
uuid?: string;
persistUuidOnLoad?: boolean;
noStreaming?: boolean;
useStickyBucketService?: "cookie" | "localStorage";
stickyBucketPrefix?: string;
payload?: FeatureApiResponse;
cacheSettings?: CacheSettings;
antiFlicker?: boolean;
antiFlickerTimeout?: number;
additionalTrackingCallback?: TrackingCallback;
};
declare global {
interface Window {
_growthbook?: GrowthBook;
growthbook_queue?: Array<(gb: GrowthBook) => void> | {
push: (cb: (gb: GrowthBook) => void) => void;
};
growthbook_config?: WindowContext;
dataLayer?: unknown[];
analytics?: {
track?: (name: string, props?: Record<string, unknown>) => void;
};
gtag?: (...args: unknown[]) => void;
}
}
declare const gb: GrowthBook<Record<string, any>>;
export default gb;
//# sourceMappingURL=auto-wrapper.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"auto-wrapper.d.ts","sourceRoot":"","sources":["../src/auto-wrapper.ts"],"names":[],"mappings":"AACA,OAAO,EACL,aAAa,EACb,OAAO,IAAI,OAAO,EAClB,kBAAkB,EAElB,gBAAgB,EACjB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAa1C,KAAK,aAAa,GAAG,OAAO,GAAG;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,sBAAsB,CAAC,EAAE,QAAQ,GAAG,cAAc,CAAC;IACnD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,0BAA0B,CAAC,EAAE,gBAAgB,CAAC;CAC/C,CAAC;AACF,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,WAAW,CAAC,EAAE,UAAU,CAAC;QACzB,gBAAgB,CAAC,EACb,KAAK,CAAC,CAAC,EAAE,EAAE,UAAU,KAAK,IAAI,CAAC,GAC/B;YAAE,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,UAAU,KAAK,IAAI,KAAK,IAAI,CAAA;SAAE,CAAC;QACrD,iBAAiB,CAAC,EAAE,aAAa,CAAC;QAClC,SAAS,CAAC,EAAE,OAAO,EAAE,CAAC;QACtB,SAAS,CAAC,EAAE;YACV,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;SACjE,CAAC;QACF,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;KACrC;CACF;AAmHD,QAAA,MAAM,EAAE,iCAON,CAAC;AAkDH,eAAe,EAAE,CAAC"}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,850 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.GrowthBook = void 0;
exports.prefetchPayload = prefetchPayload;
var _domMutator = _interopRequireDefault(require("dom-mutator"));
var _util = require("./util");
var _featureRepository = require("./feature-repository");
var _core = require("./core");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
const SDK_VERSION = (0, _util.loadSDKVersion)();
class GrowthBook {
// context is technically private, but some tools depend on it so we can't mangle the name
// Properties and methods that start with "_" are mangled by Terser (saves ~150 bytes)
constructor(options) {
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 = this.context = options;
this._renderer = options.renderer || null;
this._trackedExperiments = new Set();
this._completedChangeIds = new Set();
this._trackedFeatures = {};
this.debug = !!options.debug;
this._subscriptions = new Set();
this.ready = false;
this._assigned = new Map();
this._activeAutoExperiments = new Map();
this._triggeredExpKeys = new Set();
this._initialized = false;
this._redirectedUrl = "";
this._deferredTrackingCalls = new Map();
this._autoExperimentsAllowed = !options.disableExperimentsOnLoad;
this._destroyCallbacks = [];
this.logs = [];
this.log = this.log.bind(this);
this._saveDeferredTrack = this._saveDeferredTrack.bind(this);
this._onExperimentEval = this._onExperimentEval.bind(this);
this._fireSubscriptions = this._fireSubscriptions.bind(this);
this._recordChangedId = this._recordChangedId.bind(this);
if (options.remoteEval) {
if (options.decryptionKey) {
throw new Error("Encryption is not available for remoteEval");
}
if (!options.clientKey) {
throw new Error("Missing clientKey");
}
let isGbHost = false;
try {
isGbHost = !!new URL(options.apiHost || "").hostname.match(/growthbook\.io$/i);
} catch (e) {
// ignore invalid URLs
}
if (isGbHost) {
throw new Error("Cannot use remoteEval on GrowthBook Cloud");
}
} else {
if (options.cacheKeyAttributes) {
throw new Error("cacheKeyAttributes are only used for remoteEval");
}
}
if (options.stickyBucketService) {
const s = options.stickyBucketService;
this._saveStickyBucketAssignmentDoc = doc => {
return s.saveAssignments(doc);
};
}
if (options.plugins) {
for (const plugin of options.plugins) {
plugin(this);
}
}
if (options.features) {
this.ready = true;
}
if (isBrowser && options.enableDevMode) {
window._growthbook = this;
document.dispatchEvent(new Event("gbloaded"));
}
if (options.experiments) {
this.ready = true;
this._updateAllAutoExperiments();
}
// Hydrate sticky bucket service
if (this._options.stickyBucketService && this._options.stickyBucketAssignmentDocs) {
for (const key in this._options.stickyBucketAssignmentDocs) {
const doc = this._options.stickyBucketAssignmentDocs[key];
if (doc) {
this._options.stickyBucketService.saveAssignments(doc).catch(() => {
// Ignore hydration errors
});
}
}
}
// Legacy - passing in features/experiments into the constructor instead of using init
if (this.ready) {
this.refreshStickyBuckets(this.getPayload());
}
}
async setPayload(payload) {
this._payload = payload;
const data = await (0, _core.decryptPayload)(payload, this._options.decryptionKey);
this._decryptedPayload = data;
await this.refreshStickyBuckets(data);
if (data.features) {
this._options.features = data.features;
}
if (data.savedGroups) {
this._options.savedGroups = data.savedGroups;
}
if (data.experiments) {
this._options.experiments = data.experiments;
this._updateAllAutoExperiments();
}
this.ready = true;
this._render();
}
initSync(options) {
this._initialized = true;
const payload = options.payload;
if (payload.encryptedExperiments || payload.encryptedFeatures) {
throw new Error("initSync does not support encrypted payloads");
}
if (this._options.stickyBucketService && !this._options.stickyBucketAssignmentDocs) {
this._options.stickyBucketAssignmentDocs = this.generateStickyBucketAssignmentDocsSync(this._options.stickyBucketService, payload);
}
this._payload = payload;
this._decryptedPayload = payload;
if (payload.features) {
this._options.features = payload.features;
}
if (payload.experiments) {
this._options.experiments = payload.experiments;
this._updateAllAutoExperiments();
}
this.ready = true;
(0, _featureRepository.startStreaming)(this, options);
return this;
}
async init(options) {
this._initialized = true;
options = options || {};
if (options.cacheSettings) {
(0, _featureRepository.configureCache)(options.cacheSettings);
}
if (options.payload) {
await this.setPayload(options.payload);
(0, _featureRepository.startStreaming)(this, options);
return {
success: true,
source: "init"
};
} else {
const {
data,
...res
} = await this._refresh({
...options,
allowStale: true
});
(0, _featureRepository.startStreaming)(this, options);
await this.setPayload(data || {});
return res;
}
}
/** @deprecated Use {@link init} */
async loadFeatures(options) {
options = options || {};
await this.init({
skipCache: options.skipCache,
timeout: options.timeout,
streaming: (this._options.backgroundSync ?? true) && (options.autoRefresh || this._options.subscribeToChanges)
});
}
async refreshFeatures(options) {
const res = await this._refresh({
...(options || {}),
allowStale: false
});
if (res.data) {
await this.setPayload(res.data);
}
}
getApiInfo() {
return [this.getApiHosts().apiHost, this.getClientKey()];
}
getApiHosts() {
return (0, _core.getApiHosts)(this._options);
}
getClientKey() {
return this._options.clientKey || "";
}
getPayload() {
return this._payload || {
features: this.getFeatures(),
experiments: this.getExperiments()
};
}
getDecryptedPayload() {
return this._decryptedPayload || this.getPayload();
}
isRemoteEval() {
return this._options.remoteEval || false;
}
getCacheKeyAttributes() {
return this._options.cacheKeyAttributes;
}
async _refresh({
timeout,
skipCache,
allowStale,
streaming
}) {
if (!this._options.clientKey) {
throw new Error("Missing clientKey");
}
// Trigger refresh in feature repository
return (0, _featureRepository.refreshFeatures)({
instance: this,
timeout,
skipCache: skipCache || this._options.disableCache,
allowStale,
backgroundSync: streaming ?? this._options.backgroundSync ?? true
});
}
_render() {
if (this._renderer) {
try {
this._renderer();
} catch (e) {
console.error("Failed to render", e);
}
}
}
/** @deprecated Use {@link setPayload} */
setFeatures(features) {
this._options.features = features;
this.ready = true;
this._render();
}
/** @deprecated Use {@link setPayload} */
async setEncryptedFeatures(encryptedString, decryptionKey, subtle) {
const featuresJSON = await (0, _util.decrypt)(encryptedString, decryptionKey || this._options.decryptionKey, subtle);
this.setFeatures(JSON.parse(featuresJSON));
}
/** @deprecated Use {@link setPayload} */
setExperiments(experiments) {
this._options.experiments = experiments;
this.ready = true;
this._updateAllAutoExperiments();
}
/** @deprecated Use {@link setPayload} */
async setEncryptedExperiments(encryptedString, decryptionKey, subtle) {
const experimentsJSON = await (0, _util.decrypt)(encryptedString, decryptionKey || this._options.decryptionKey, subtle);
this.setExperiments(JSON.parse(experimentsJSON));
}
async setAttributes(attributes) {
this._options.attributes = attributes;
if (this._options.stickyBucketService) {
await this.refreshStickyBuckets();
}
if (this._options.remoteEval) {
await this._refreshForRemoteEval();
return;
}
this._render();
this._updateAllAutoExperiments();
}
async updateAttributes(attributes) {
return this.setAttributes({
...this._options.attributes,
...attributes
});
}
async setAttributeOverrides(overrides) {
this._options.attributeOverrides = overrides;
if (this._options.stickyBucketService) {
await this.refreshStickyBuckets();
}
if (this._options.remoteEval) {
await this._refreshForRemoteEval();
return;
}
this._render();
this._updateAllAutoExperiments();
}
async setForcedVariations(vars) {
this._options.forcedVariations = vars || {};
if (this._options.remoteEval) {
await this._refreshForRemoteEval();
return;
}
this._render();
this._updateAllAutoExperiments();
}
// eslint-disable-next-line
setForcedFeatures(map) {
this._options.forcedFeatureValues = map;
this._render();
}
async setURL(url) {
if (url === this._options.url) return;
this._options.url = url;
this._redirectedUrl = "";
if (this._options.remoteEval) {
await this._refreshForRemoteEval();
this._updateAllAutoExperiments(true);
return;
}
this._updateAllAutoExperiments(true);
}
getAttributes() {
return {
...this._options.attributes,
...this._options.attributeOverrides
};
}
getForcedVariations() {
return this._options.forcedVariations || {};
}
getForcedFeatures() {
// eslint-disable-next-line
return this._options.forcedFeatureValues || new Map();
}
getStickyBucketAssignmentDocs() {
return this._options.stickyBucketAssignmentDocs || {};
}
getUrl() {
return this._options.url || "";
}
getFeatures() {
return this._options.features || {};
}
getExperiments() {
return this._options.experiments || [];
}
getCompletedChangeIds() {
return Array.from(this._completedChangeIds);
}
subscribe(cb) {
this._subscriptions.add(cb);
return () => {
this._subscriptions.delete(cb);
};
}
async _refreshForRemoteEval() {
if (!this._options.remoteEval) return;
if (!this._initialized) return;
const res = await this._refresh({
allowStale: false
});
if (res.data) {
await this.setPayload(res.data);
}
}
getAllResults() {
return new Map(this._assigned);
}
onDestroy(cb) {
this._destroyCallbacks.push(cb);
}
isDestroyed() {
return !!this._destroyed;
}
destroy(options) {
options = options || {};
this._destroyed = true;
// Custom callbacks
// Do this first in case it needs access to the below data that is cleared
this._destroyCallbacks.forEach(cb => {
try {
cb();
} catch (e) {
console.error(e);
}
});
// Release references to save memory
this._subscriptions.clear();
this._assigned.clear();
this._trackedExperiments.clear();
this._completedChangeIds.clear();
this._deferredTrackingCalls.clear();
this._trackedFeatures = {};
this._destroyCallbacks = [];
this._payload = undefined;
this._saveStickyBucketAssignmentDoc = undefined;
(0, _featureRepository.unsubscribe)(this);
if (options.destroyAllStreams) {
(0, _featureRepository.clearAutoRefresh)();
}
this.logs = [];
if (isBrowser && window._growthbook === this) {
delete window._growthbook;
}
// Undo any active auto experiments
this._activeAutoExperiments.forEach(exp => {
exp.undo();
});
this._activeAutoExperiments.clear();
this._triggeredExpKeys.clear();
}
setRenderer(renderer) {
this._renderer = renderer;
}
forceVariation(key, variation) {
this._options.forcedVariations = this._options.forcedVariations || {};
this._options.forcedVariations[key] = variation;
if (this._options.remoteEval) {
this._refreshForRemoteEval();
return;
}
this._updateAllAutoExperiments();
this._render();
}
run(experiment) {
const {
result
} = (0, _core.runExperiment)(experiment, null, this._getEvalContext());
this._onExperimentEval(experiment, result);
return result;
}
triggerExperiment(key) {
this._triggeredExpKeys.add(key);
if (!this._options.experiments) return null;
const experiments = this._options.experiments.filter(exp => exp.key === key);
return experiments.map(exp => {
return this._runAutoExperiment(exp);
}).filter(res => res !== null);
}
triggerAutoExperiments() {
this._autoExperimentsAllowed = true;
this._updateAllAutoExperiments(true);
}
_getEvalContext() {
return {
user: this._getUserContext(),
global: this._getGlobalContext(),
stack: {
evaluatedFeatures: new Set()
}
};
}
_getUserContext() {
return {
attributes: this._options.user ? {
...this._options.user,
...this._options.attributes
} : this._options.attributes,
enableDevMode: this._options.enableDevMode,
blockedChangeIds: this._options.blockedChangeIds,
stickyBucketAssignmentDocs: this._options.stickyBucketAssignmentDocs,
url: this._getContextUrl(),
forcedVariations: this._options.forcedVariations,
forcedFeatureValues: this._options.forcedFeatureValues,
attributeOverrides: this._options.attributeOverrides,
saveStickyBucketAssignmentDoc: this._saveStickyBucketAssignmentDoc,
trackingCallback: this._options.trackingCallback,
onFeatureUsage: this._options.onFeatureUsage,
devLogs: this.logs,
trackedExperiments: this._trackedExperiments,
trackedFeatureUsage: this._trackedFeatures
};
}
_getGlobalContext() {
return {
features: this._options.features,
experiments: this._options.experiments,
log: this.log,
enabled: this._options.enabled,
qaMode: this._options.qaMode,
savedGroups: this._options.savedGroups,
groups: this._options.groups,
overrides: this._options.overrides,
onExperimentEval: this._onExperimentEval,
recordChangeId: this._recordChangedId,
saveDeferredTrack: this._saveDeferredTrack,
eventLogger: this._options.eventLogger
};
}
_runAutoExperiment(experiment, forceRerun) {
const existing = this._activeAutoExperiments.get(experiment);
// If this is a manual experiment and it's not already running, skip
if (experiment.manual && !this._triggeredExpKeys.has(experiment.key) && !existing) return null;
// Check if this particular experiment is blocked by options settings
// For example, if all visualEditor experiments are disabled
const isBlocked = this._isAutoExperimentBlockedByContext(experiment);
if (isBlocked) {
process.env.NODE_ENV !== "production" && this.log("Auto experiment blocked", {
id: experiment.key
});
}
let result;
let trackingCall;
// Run the experiment (if blocked exclude)
if (isBlocked) {
result = (0, _core.getExperimentResult)(this._getEvalContext(), experiment, -1, false, "");
} else {
({
result,
trackingCall
} = (0, _core.runExperiment)(experiment, null, this._getEvalContext()));
this._onExperimentEval(experiment, result);
}
// A hash to quickly tell if the assigned value changed
const valueHash = JSON.stringify(result.value);
// If the changes are already active, no need to re-apply them
if (!forceRerun && result.inExperiment && existing && existing.valueHash === valueHash) {
return result;
}
// Undo any existing changes
if (existing) this._undoActiveAutoExperiment(experiment);
// Apply new changes
if (result.inExperiment) {
const changeType = (0, _util.getAutoExperimentChangeType)(experiment);
if (changeType === "redirect" && result.value.urlRedirect && experiment.urlPatterns) {
const url = experiment.persistQueryString ? (0, _util.mergeQueryStrings)(this._getContextUrl(), result.value.urlRedirect) : result.value.urlRedirect;
if ((0, _util.isURLTargeted)(url, experiment.urlPatterns)) {
this.log("Skipping redirect because original URL matches redirect URL", {
id: experiment.key
});
return result;
}
this._redirectedUrl = url;
const {
navigate,
delay
} = this._getNavigateFunction();
if (navigate) {
if (isBrowser) {
// Wait for the possibly-async tracking callback, bound by min and max delays
Promise.all([...(trackingCall ? [(0, _util.promiseTimeout)(trackingCall, this._options.maxNavigateDelay ?? 1000)] : []), new Promise(resolve => window.setTimeout(resolve, this._options.navigateDelay ?? delay))]).then(() => {
try {
navigate(url);
} catch (e) {
console.error(e);
}
});
} else {
try {
navigate(url);
} catch (e) {
console.error(e);
}
}
}
} else if (changeType === "visual") {
const undo = this._options.applyDomChangesCallback ? this._options.applyDomChangesCallback(result.value) : this._applyDOMChanges(result.value);
if (undo) {
this._activeAutoExperiments.set(experiment, {
undo,
valueHash
});
}
}
}
return result;
}
_undoActiveAutoExperiment(exp) {
const data = this._activeAutoExperiments.get(exp);
if (data) {
data.undo();
this._activeAutoExperiments.delete(exp);
}
}
_updateAllAutoExperiments(forceRerun) {
if (!this._autoExperimentsAllowed) return;
const experiments = this._options.experiments || [];
// Stop any experiments that are no longer defined
const keys = new Set(experiments);
this._activeAutoExperiments.forEach((v, k) => {
if (!keys.has(k)) {
v.undo();
this._activeAutoExperiments.delete(k);
}
});
// Re-run all new/updated experiments
for (const exp of experiments) {
const result = this._runAutoExperiment(exp, forceRerun);
// Once you're in a redirect experiment, break out of the loop and don't run any further experiments
if (result && result.inExperiment && (0, _util.getAutoExperimentChangeType)(exp) === "redirect") {
break;
}
}
}
_onExperimentEval(experiment, result) {
const prev = this._assigned.get(experiment.key);
this._assigned.set(experiment.key, {
experiment,
result
});
if (this._subscriptions.size > 0) {
this._fireSubscriptions(experiment, result, prev);
}
}
_fireSubscriptions(experiment, result,
// eslint-disable-next-line
prev) {
// If assigned variation has changed, fire subscriptions
// TODO: what if the experiment definition has changed?
if (!prev || prev.result.inExperiment !== result.inExperiment || prev.result.variationId !== result.variationId) {
this._subscriptions.forEach(cb => {
try {
cb(experiment, result);
} catch (e) {
console.error(e);
}
});
}
}
_recordChangedId(id) {
this._completedChangeIds.add(id);
}
isOn(key) {
return this.evalFeature(key).on;
}
isOff(key) {
return this.evalFeature(key).off;
}
getFeatureValue(key, defaultValue) {
const value = this.evalFeature(key).value;
return value === null ? defaultValue : value;
}
/**
* @deprecated Use {@link evalFeature}
* @param id
*/
feature(id) {
return this.evalFeature(id);
}
evalFeature(id) {
return (0, _core.evalFeature)(id, this._getEvalContext());
}
log(msg, ctx) {
if (!this.debug) return;
if (this._options.log) this._options.log(msg, ctx);else console.log(msg, ctx);
}
getDeferredTrackingCalls() {
return Array.from(this._deferredTrackingCalls.values());
}
setDeferredTrackingCalls(calls) {
this._deferredTrackingCalls = new Map(calls.filter(c => c && c.experiment && c.result).map(c => {
return [(0, _core.getExperimentDedupeKey)(c.experiment, c.result), c];
}));
}
async fireDeferredTrackingCalls() {
if (!this._options.trackingCallback) return;
const promises = [];
this._deferredTrackingCalls.forEach(call => {
if (!call || !call.experiment || !call.result) {
console.error("Invalid deferred tracking call", {
call: call
});
} else {
promises.push(this._options.trackingCallback(call.experiment, call.result));
}
});
this._deferredTrackingCalls.clear();
await Promise.all(promises);
}
setTrackingCallback(callback) {
this._options.trackingCallback = callback;
this.fireDeferredTrackingCalls();
}
setFeatureUsageCallback(callback) {
this._options.onFeatureUsage = callback;
}
setEventLogger(logger) {
this._options.eventLogger = logger;
}
async logEvent(eventName, properties) {
if (this._destroyed) {
console.error("Cannot log event to destroyed GrowthBook instance");
return;
}
if (this._options.enableDevMode) {
this.logs.push({
eventName,
properties,
timestamp: Date.now().toString(),
logType: "event"
});
}
if (this._options.eventLogger) {
try {
await this._options.eventLogger(eventName, properties || {}, this._getUserContext());
} catch (e) {
console.error(e);
}
} else {
console.error("No event logger configured");
}
}
_saveDeferredTrack(data) {
this._deferredTrackingCalls.set((0, _core.getExperimentDedupeKey)(data.experiment, data.result), data);
}
_getContextUrl() {
return this._options.url || (isBrowser ? window.location.href : "");
}
_isAutoExperimentBlockedByContext(experiment) {
const changeType = (0, _util.getAutoExperimentChangeType)(experiment);
if (changeType === "visual") {
if (this._options.disableVisualExperiments) return true;
if (this._options.disableJsInjection) {
if (experiment.variations.some(v => v.js)) {
return true;
}
}
} else if (changeType === "redirect") {
if (this._options.disableUrlRedirectExperiments) return true;
// Validate URLs
try {
const current = new URL(this._getContextUrl());
for (const v of experiment.variations) {
if (!v || !v.urlRedirect) continue;
const url = new URL(v.urlRedirect);
// If we're blocking cross origin redirects, block if the protocol or host is different
if (this._options.disableCrossOriginUrlRedirectExperiments) {
if (url.protocol !== current.protocol) return true;
if (url.host !== current.host) return true;
}
}
} catch (e) {
// Problem parsing one of the URLs
this.log("Error parsing current or redirect URL", {
id: experiment.key,
error: e
});
return true;
}
} else {
// Block any unknown changeTypes
return true;
}
if (experiment.changeId && (this._options.blockedChangeIds || []).includes(experiment.changeId)) {
return true;
}
return false;
}
getRedirectUrl() {
return this._redirectedUrl;
}
_getNavigateFunction() {
if (this._options.navigate) {
return {
navigate: this._options.navigate,
delay: 0
};
} else if (isBrowser) {
return {
navigate: url => {
window.location.replace(url);
},
delay: 100
};
}
return {
navigate: null,
delay: 0
};
}
_applyDOMChanges(changes) {
if (!isBrowser) return;
const undo = [];
if (changes.css) {
const s = document.createElement("style");
s.innerHTML = changes.css;
document.head.appendChild(s);
undo.push(() => s.remove());
}
if (changes.js) {
const script = document.createElement("script");
script.innerHTML = changes.js;
if (this._options.jsInjectionNonce) {
script.nonce = this._options.jsInjectionNonce;
}
document.head.appendChild(script);
undo.push(() => script.remove());
}
if (changes.domMutations) {
changes.domMutations.forEach(mutation => {
undo.push(_domMutator.default.declarative(mutation).revert);
});
}
return () => {
undo.forEach(fn => fn());
};
}
async refreshStickyBuckets(data) {
if (this._options.stickyBucketService) {
const ctx = this._getEvalContext();
const docs = await (0, _core.getAllStickyBucketAssignmentDocs)(ctx, this._options.stickyBucketService, data);
this._options.stickyBucketAssignmentDocs = docs;
}
}
generateStickyBucketAssignmentDocsSync(stickyBucketService, payload) {
if (!("getAllAssignmentsSync" in stickyBucketService)) {
console.error("generating StickyBucketAssignmentDocs docs requires StickyBucketServiceSync");
return;
}
const ctx = this._getEvalContext();
const attributes = (0, _core.getStickyBucketAttributes)(ctx, payload);
return stickyBucketService.getAllAssignmentsSync(attributes);
}
inDevMode() {
return !!this._options.enableDevMode;
}
}
exports.GrowthBook = GrowthBook;
async function prefetchPayload(options) {
// Create a temporary instance, just to fetch the payload
const instance = new GrowthBook(options);
await (0, _featureRepository.refreshFeatures)({
instance,
skipCache: options.skipCache,
allowStale: false,
backgroundSync: options.streaming
});
instance.destroy();
}
//# sourceMappingURL=GrowthBook.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,326 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.UserScopedGrowthBook = exports.GrowthBookClient = void 0;
var _util = require("./util");
var _featureRepository = require("./feature-repository");
var _core = require("./core");
const SDK_VERSION = (0, _util.loadSDKVersion)();
class GrowthBookClient {
// Properties and methods that start with "_" are mangled by Terser (saves ~150 bytes)
constructor(options) {
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);
}
}
}
async setPayload(payload) {
this._payload = payload;
const data = await (0, _core.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;
}
this.ready = true;
}
initSync(options) {
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.experiments) {
this._experiments = payload.experiments;
}
this.ready = true;
(0, _featureRepository.startStreaming)(this, options);
return this;
}
async init(options) {
options = options || {};
if (options.cacheSettings) {
(0, _featureRepository.configureCache)(options.cacheSettings);
}
if (options.payload) {
await this.setPayload(options.payload);
(0, _featureRepository.startStreaming)(this, options);
return {
success: true,
source: "init"
};
} else {
const {
data,
...res
} = await this._refresh({
...options,
allowStale: true
});
(0, _featureRepository.startStreaming)(this, options);
await this.setPayload(data || {});
return res;
}
}
async refreshFeatures(options) {
const res = await this._refresh({
...(options || {}),
allowStale: false
});
if (res.data) {
await this.setPayload(res.data);
}
}
getApiInfo() {
return [this.getApiHosts().apiHost, this.getClientKey()];
}
getApiHosts() {
return (0, _core.getApiHosts)(this._options);
}
getClientKey() {
return this._options.clientKey || "";
}
getPayload() {
return this._payload || {
features: this.getFeatures(),
experiments: this._experiments || []
};
}
getDecryptedPayload() {
return this._decryptedPayload || this.getPayload();
}
async _refresh({
timeout,
skipCache,
allowStale,
streaming
}) {
if (!this._options.clientKey) {
throw new Error("Missing clientKey");
}
// Trigger refresh in feature repository
return (0, _featureRepository.refreshFeatures)({
instance: this,
timeout,
skipCache: skipCache || this._options.disableCache,
allowStale,
backgroundSync: streaming ?? true
});
}
getFeatures() {
return this._features || {};
}
getGlobalAttributes() {
return this._options.globalAttributes || {};
}
setGlobalAttributes(attributes) {
this._options.globalAttributes = attributes;
}
destroy(options) {
options = options || {};
this._destroyed = true;
(0, _featureRepository.unsubscribe)(this);
if (options.destroyAllStreams) {
(0, _featureRepository.clearAutoRefresh)();
}
// Release references to save memory
this._features = {};
this._experiments = [];
this._decryptedPayload = undefined;
this._payload = undefined;
this._options = {};
}
isDestroyed() {
return !!this._destroyed;
}
setEventLogger(logger) {
this._options.eventLogger = logger;
}
logEvent(eventName, properties, userContext) {
if (this._options.eventLogger) {
const ctx = this._getEvalContext(userContext);
this._options.eventLogger(eventName, properties, ctx.user);
}
}
runInlineExperiment(experiment, userContext) {
const {
result
} = (0, _core.runExperiment)(experiment, null, this._getEvalContext(userContext));
return result;
}
_getEvalContext(userContext) {
if (this._options.globalAttributes) {
userContext = {
...userContext,
attributes: {
...this._options.globalAttributes,
...userContext.attributes
}
};
}
return {
user: userContext,
global: this._getGlobalContext(),
stack: {
evaluatedFeatures: new Set()
}
};
}
_getGlobalContext() {
return {
features: this._features,
experiments: this._experiments,
log: this.log,
enabled: this._options.enabled,
qaMode: this._options.qaMode,
savedGroups: this._options.savedGroups,
forcedFeatureValues: this._options.forcedFeatureValues,
forcedVariations: this._options.forcedVariations,
trackingCallback: this._options.trackingCallback,
onFeatureUsage: this._options.onFeatureUsage
};
}
isOn(key, userContext) {
return this.evalFeature(key, userContext).on;
}
isOff(key, userContext) {
return this.evalFeature(key, userContext).off;
}
getFeatureValue(key, defaultValue, userContext) {
const value = this.evalFeature(key, userContext).value;
return value === null ? defaultValue : value;
}
evalFeature(id, userContext) {
return (0, _core.evalFeature)(id, this._getEvalContext(userContext));
}
log(msg, ctx) {
if (!this.debug) return;
if (this._options.log) this._options.log(msg, ctx);else console.log(msg, ctx);
}
setTrackingCallback(callback) {
this._options.trackingCallback = callback;
}
setFeatureUsageCallback(callback) {
this._options.onFeatureUsage = callback;
}
async applyStickyBuckets(partialContext, stickyBucketService) {
const ctx = this._getEvalContext(partialContext);
const stickyBucketAssignmentDocs = await (0, _core.getAllStickyBucketAssignmentDocs)(ctx, stickyBucketService);
return {
...partialContext,
stickyBucketAssignmentDocs,
saveStickyBucketAssignmentDoc: doc => stickyBucketService.saveAssignments(doc)
};
}
createScopedInstance(userContext, userPlugins) {
return new UserScopedGrowthBook(this, userContext, [...(this._options.plugins || []), ...(userPlugins || [])]);
}
}
exports.GrowthBookClient = GrowthBookClient;
class UserScopedGrowthBook {
constructor(gb, userContext, plugins) {
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);
}
}
}
runInlineExperiment(experiment) {
return this._gb.runInlineExperiment(experiment, this._userContext);
}
isOn(key) {
return this._gb.isOn(key, this._userContext);
}
isOff(key) {
return this._gb.isOff(key, this._userContext);
}
getFeatureValue(key, defaultValue) {
return this._gb.getFeatureValue(key, defaultValue, this._userContext);
}
evalFeature(id) {
return this._gb.evalFeature(id, this._userContext);
}
logEvent(eventName, properties) {
if (this._userContext.enableDevMode) {
this.logs.push({
eventName,
properties,
timestamp: Date.now().toString(),
logType: "event"
});
}
this._gb.logEvent(eventName, properties || {}, this._userContext);
}
setTrackingCallback(cb) {
this._userContext.trackingCallback = cb;
}
getApiInfo() {
return this._gb.getApiInfo();
}
getClientKey() {
return this._gb.getClientKey();
}
setURL(url) {
this._userContext.url = url;
}
updateAttributes(attributes) {
this._userContext.attributes = {
...this._userContext.attributes,
...attributes
};
}
setAttributeOverrides(overrides) {
this._userContext.attributeOverrides = overrides;
}
async setForcedVariations(vars) {
this._userContext.forcedVariations = vars || {};
}
// eslint-disable-next-line
setForcedFeatures(map) {
this._userContext.forcedFeatureValues = map;
}
getUserContext() {
return this._userContext;
}
getVersion() {
return SDK_VERSION;
}
getDecryptedPayload() {
return this._gb.getDecryptedPayload();
}
inDevMode() {
return !!this._userContext.enableDevMode;
}
}
exports.UserScopedGrowthBook = UserScopedGrowthBook;
//# sourceMappingURL=GrowthBookClient.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,894 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.EVENT_FEATURE_EVALUATED = exports.EVENT_EXPERIMENT_VIEWED = void 0;
exports.decryptPayload = decryptPayload;
exports.evalFeature = evalFeature;
exports.getAllStickyBucketAssignmentDocs = getAllStickyBucketAssignmentDocs;
exports.getApiHosts = getApiHosts;
exports.getExperimentDedupeKey = getExperimentDedupeKey;
exports.getExperimentResult = getExperimentResult;
exports.getHashAttribute = getHashAttribute;
exports.getStickyBucketAttributeKey = getStickyBucketAttributeKey;
exports.getStickyBucketAttributes = getStickyBucketAttributes;
exports.runExperiment = runExperiment;
var _mongrule = require("./mongrule");
var _util = require("./util");
const EVENT_FEATURE_EVALUATED = exports.EVENT_FEATURE_EVALUATED = "Feature Evaluated";
const EVENT_EXPERIMENT_VIEWED = exports.EVENT_EXPERIMENT_VIEWED = "Experiment Viewed";
function getForcedFeatureValues(ctx) {
// Merge user and global values
const ret = new Map();
if (ctx.global.forcedFeatureValues) {
ctx.global.forcedFeatureValues.forEach((v, k) => ret.set(k, v));
}
if (ctx.user.forcedFeatureValues) {
ctx.user.forcedFeatureValues.forEach((v, k) => ret.set(k, v));
}
return ret;
}
function getForcedVariations(ctx) {
// Merge user and global values
if (ctx.global.forcedVariations && ctx.user.forcedVariations) {
return {
...ctx.global.forcedVariations,
...ctx.user.forcedVariations
};
} else if (ctx.global.forcedVariations) {
return ctx.global.forcedVariations;
} else if (ctx.user.forcedVariations) {
return ctx.user.forcedVariations;
} else {
return {};
}
}
async function safeCall(fn) {
try {
await fn();
} catch (e) {
// Do nothing
}
}
function onExperimentViewed(ctx, experiment, result) {
// Make sure a tracking callback is only fired once per unique experiment
if (ctx.user.trackedExperiments) {
const k = getExperimentDedupeKey(experiment, result);
if (ctx.user.trackedExperiments.has(k)) {
return [];
}
ctx.user.trackedExperiments.add(k);
}
if (ctx.user.enableDevMode && ctx.user.devLogs) {
ctx.user.devLogs.push({
experiment,
result,
timestamp: Date.now().toString(),
logType: "experiment"
});
}
const calls = [];
if (ctx.global.trackingCallback) {
const cb = ctx.global.trackingCallback;
calls.push(safeCall(() => cb(experiment, result, ctx.user)));
}
if (ctx.user.trackingCallback) {
const cb = ctx.user.trackingCallback;
calls.push(safeCall(() => cb(experiment, result)));
}
if (ctx.global.eventLogger) {
const cb = ctx.global.eventLogger;
calls.push(safeCall(() => cb(EVENT_EXPERIMENT_VIEWED, {
experimentId: experiment.key,
variationId: result.key,
hashAttribute: result.hashAttribute,
hashValue: result.hashValue
}, ctx.user)));
}
return calls;
}
function onFeatureUsage(ctx, key, ret) {
// Only track a feature once, unless the assigned value changed
if (ctx.user.trackedFeatureUsage) {
const stringifiedValue = JSON.stringify(ret.value);
if (ctx.user.trackedFeatureUsage[key] === stringifiedValue) return;
ctx.user.trackedFeatureUsage[key] = stringifiedValue;
if (ctx.user.enableDevMode && ctx.user.devLogs) {
ctx.user.devLogs.push({
featureKey: key,
result: ret,
timestamp: Date.now().toString(),
logType: "feature"
});
}
}
if (ctx.global.onFeatureUsage) {
const cb = ctx.global.onFeatureUsage;
safeCall(() => cb(key, ret, ctx.user));
}
if (ctx.user.onFeatureUsage) {
const cb = ctx.user.onFeatureUsage;
safeCall(() => cb(key, ret));
}
if (ctx.global.eventLogger) {
const cb = ctx.global.eventLogger;
safeCall(() => cb(EVENT_FEATURE_EVALUATED, {
feature: key,
source: ret.source,
value: ret.value,
ruleId: ret.source === "defaultValue" ? "$default" : ret.ruleId || "",
variationId: ret.experimentResult ? ret.experimentResult.key : ""
}, ctx.user));
}
}
function evalFeature(id, ctx) {
if (ctx.stack.evaluatedFeatures.has(id)) {
process.env.NODE_ENV !== "production" && ctx.global.log(`evalFeature: circular dependency detected: ${ctx.stack.id} -> ${id}`, {
from: ctx.stack.id,
to: id
});
return getFeatureResult(ctx, id, null, "cyclicPrerequisite");
}
ctx.stack.evaluatedFeatures.add(id);
ctx.stack.id = id;
// Global override
const forcedValues = getForcedFeatureValues(ctx);
if (forcedValues.has(id)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Global override", {
id,
value: forcedValues.get(id)
});
return getFeatureResult(ctx, id, forcedValues.get(id), "override");
}
// Unknown feature id
if (!ctx.global.features || !ctx.global.features[id]) {
process.env.NODE_ENV !== "production" && ctx.global.log("Unknown feature", {
id
});
return getFeatureResult(ctx, id, null, "unknownFeature");
}
// Get the feature
const feature = ctx.global.features[id];
// Loop through the rules
if (feature.rules) {
const evaluatedFeatures = new Set(ctx.stack.evaluatedFeatures);
rules: for (const rule of feature.rules) {
// If there are prerequisite flag(s), evaluate them
if (rule.parentConditions) {
for (const parentCondition of rule.parentConditions) {
ctx.stack.evaluatedFeatures = new Set(evaluatedFeatures);
const parentResult = evalFeature(parentCondition.id, ctx);
// break out for cyclic prerequisites
if (parentResult.source === "cyclicPrerequisite") {
return getFeatureResult(ctx, id, null, "cyclicPrerequisite");
}
const evalObj = {
value: parentResult.value
};
const evaled = (0, _mongrule.evalCondition)(evalObj, parentCondition.condition || {});
if (!evaled) {
// blocking prerequisite eval failed: feature evaluation fails
if (parentCondition.gate) {
process.env.NODE_ENV !== "production" && ctx.global.log("Feature blocked by prerequisite", {
id,
rule
});
return getFeatureResult(ctx, id, null, "prerequisite");
}
// non-blocking prerequisite eval failed: break out of parentConditions loop, jump to the next rule
process.env.NODE_ENV !== "production" && ctx.global.log("Skip rule because prerequisite evaluation fails", {
id,
rule
});
continue rules;
}
}
}
// If there are filters for who is included (e.g. namespaces)
if (rule.filters && isFilteredOut(rule.filters, ctx)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip rule because of filters", {
id,
rule
});
continue;
}
// Feature value is being forced
if ("force" in rule) {
// If it's a conditional rule, skip if the condition doesn't pass
if (rule.condition && !conditionPasses(rule.condition, ctx)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip rule because of condition ff", {
id,
rule
});
continue;
}
// If this is a percentage rollout, skip if not included
if (!isIncludedInRollout(ctx, rule.seed || id, rule.hashAttribute, ctx.user.saveStickyBucketAssignmentDoc && !rule.disableStickyBucketing ? rule.fallbackAttribute : undefined, rule.range, rule.coverage, rule.hashVersion)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip rule because user not included in rollout", {
id,
rule
});
continue;
}
process.env.NODE_ENV !== "production" && ctx.global.log("Force value from rule", {
id,
rule
});
// If this was a remotely evaluated experiment, fire the tracking callbacks
if (rule.tracks) {
rule.tracks.forEach(t => {
const calls = onExperimentViewed(ctx, t.experiment, t.result);
if (!calls.length && ctx.global.saveDeferredTrack) {
ctx.global.saveDeferredTrack({
experiment: t.experiment,
result: t.result
});
}
});
}
return getFeatureResult(ctx, id, rule.force, "force", rule.id);
}
if (!rule.variations) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip invalid rule", {
id,
rule
});
continue;
}
// For experiment rules, run an experiment
const exp = {
variations: rule.variations,
key: rule.key || id
};
if ("coverage" in rule) exp.coverage = rule.coverage;
if (rule.weights) exp.weights = rule.weights;
if (rule.hashAttribute) exp.hashAttribute = rule.hashAttribute;
if (rule.fallbackAttribute) exp.fallbackAttribute = rule.fallbackAttribute;
if (rule.disableStickyBucketing) exp.disableStickyBucketing = rule.disableStickyBucketing;
if (rule.bucketVersion !== undefined) exp.bucketVersion = rule.bucketVersion;
if (rule.minBucketVersion !== undefined) exp.minBucketVersion = rule.minBucketVersion;
if (rule.namespace) exp.namespace = rule.namespace;
if (rule.meta) exp.meta = rule.meta;
if (rule.ranges) exp.ranges = rule.ranges;
if (rule.name) exp.name = rule.name;
if (rule.phase) exp.phase = rule.phase;
if (rule.seed) exp.seed = rule.seed;
if (rule.hashVersion) exp.hashVersion = rule.hashVersion;
if (rule.filters) exp.filters = rule.filters;
if (rule.condition) exp.condition = rule.condition;
// Only return a value if the user is part of the experiment
const {
result
} = runExperiment(exp, id, ctx);
ctx.global.onExperimentEval && ctx.global.onExperimentEval(exp, result);
if (result.inExperiment && !result.passthrough) {
return getFeatureResult(ctx, id, result.value, "experiment", rule.id, exp, result);
}
}
}
process.env.NODE_ENV !== "production" && ctx.global.log("Use default value", {
id,
value: feature.defaultValue
});
// Fall back to using the default value
return getFeatureResult(ctx, id, feature.defaultValue === undefined ? null : feature.defaultValue, "defaultValue");
}
function runExperiment(experiment, featureId, ctx) {
const key = experiment.key;
const numVariations = experiment.variations.length;
// 1. If experiment has less than 2 variations, return immediately
if (numVariations < 2) {
process.env.NODE_ENV !== "production" && ctx.global.log("Invalid experiment", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 2. If the context is disabled, return immediately
if (ctx.global.enabled === false || ctx.user.enabled === false) {
process.env.NODE_ENV !== "production" && ctx.global.log("Context disabled", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 2.5. Merge in experiment overrides from the context
experiment = mergeOverrides(experiment, ctx);
// 2.6 New, more powerful URL targeting
if (experiment.urlPatterns && !(0, _util.isURLTargeted)(ctx.user.url || "", experiment.urlPatterns)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of url targeting", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 3. If a variation is forced from a querystring, return the forced variation
const qsOverride = (0, _util.getQueryStringOverride)(key, ctx.user.url || "", numVariations);
if (qsOverride !== null) {
process.env.NODE_ENV !== "production" && ctx.global.log("Force via querystring", {
id: key,
variation: qsOverride
});
return {
result: getExperimentResult(ctx, experiment, qsOverride, false, featureId)
};
}
// 4. If a variation is forced in the context, return the forced variation
const forcedVariations = getForcedVariations(ctx);
if (key in forcedVariations) {
const variation = forcedVariations[key];
process.env.NODE_ENV !== "production" && ctx.global.log("Force via dev tools", {
id: key,
variation
});
return {
result: getExperimentResult(ctx, experiment, variation, false, featureId)
};
}
// 5. Exclude if a draft experiment or not active
if (experiment.status === "draft" || experiment.active === false) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because inactive", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 6. Get the hash attribute and return if empty
const {
hashAttribute,
hashValue
} = getHashAttribute(ctx, experiment.hashAttribute, ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing ? experiment.fallbackAttribute : undefined);
if (!hashValue) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because missing hashAttribute", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
let assigned = -1;
let foundStickyBucket = false;
let stickyBucketVersionIsBlocked = false;
if (ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing) {
const {
variation,
versionIsBlocked
} = getStickyBucketVariation({
ctx,
expKey: experiment.key,
expBucketVersion: experiment.bucketVersion,
expHashAttribute: experiment.hashAttribute,
expFallbackAttribute: experiment.fallbackAttribute,
expMinBucketVersion: experiment.minBucketVersion,
expMeta: experiment.meta
});
foundStickyBucket = variation >= 0;
assigned = variation;
stickyBucketVersionIsBlocked = !!versionIsBlocked;
}
// Some checks are not needed if we already have a sticky bucket
if (!foundStickyBucket) {
// 7. Exclude if user is filtered out (used to be called "namespace")
if (experiment.filters) {
if (isFilteredOut(experiment.filters, ctx)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of filters", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
} else if (experiment.namespace && !(0, _util.inNamespace)(hashValue, experiment.namespace)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of namespace", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 7.5. Exclude if experiment.include returns false or throws
if (experiment.include && !(0, _util.isIncluded)(experiment.include)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of include function", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 8. Exclude if condition is false
if (experiment.condition && !conditionPasses(experiment.condition, ctx)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of condition exp", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 8.05. Exclude if prerequisites are not met
if (experiment.parentConditions) {
const evaluatedFeatures = new Set(ctx.stack.evaluatedFeatures);
for (const parentCondition of experiment.parentConditions) {
ctx.stack.evaluatedFeatures = new Set(evaluatedFeatures);
const parentResult = evalFeature(parentCondition.id, ctx);
// break out for cyclic prerequisites
if (parentResult.source === "cyclicPrerequisite") {
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
const evalObj = {
value: parentResult.value
};
if (!(0, _mongrule.evalCondition)(evalObj, parentCondition.condition || {})) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because prerequisite evaluation fails", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
}
}
// 8.1. Exclude if user is not in a required group
if (experiment.groups && !hasGroupOverlap(experiment.groups, ctx)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of groups", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
}
// 8.2. Old style URL targeting
if (experiment.url && !urlIsValid(experiment.url, ctx)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of url", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 9. Get the variation from the sticky bucket or get bucket ranges and choose variation
const n = (0, _util.hash)(experiment.seed || key, hashValue, experiment.hashVersion || 1);
if (n === null) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of invalid hash version", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
if (!foundStickyBucket) {
const ranges = experiment.ranges || (0, _util.getBucketRanges)(numVariations, experiment.coverage === undefined ? 1 : experiment.coverage, experiment.weights);
assigned = (0, _util.chooseVariation)(n, ranges);
}
// 9.5 Unenroll if any prior sticky buckets are blocked by version
if (stickyBucketVersionIsBlocked) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because sticky bucket version is blocked", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId, undefined, true)
};
}
// 10. Return if not in experiment
if (assigned < 0) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of coverage", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 11. Experiment has a forced variation
if ("force" in experiment) {
process.env.NODE_ENV !== "production" && ctx.global.log("Force variation", {
id: key,
variation: experiment.force
});
return {
result: getExperimentResult(ctx, experiment, experiment.force === undefined ? -1 : experiment.force, false, featureId)
};
}
// 12. Exclude if in QA mode
if (ctx.global.qaMode || ctx.user.qaMode) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because QA mode", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 12.5. Exclude if experiment is stopped
if (experiment.status === "stopped") {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because stopped", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 13. Build the result object
const result = getExperimentResult(ctx, experiment, assigned, true, featureId, n, foundStickyBucket);
// 13.5. Persist sticky bucket
if (ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing) {
const {
changed,
key: attrKey,
doc
} = generateStickyBucketAssignmentDoc(ctx, hashAttribute, (0, _util.toString)(hashValue), {
[getStickyBucketExperimentKey(experiment.key, experiment.bucketVersion)]: result.key
});
if (changed) {
// update local docs
ctx.user.stickyBucketAssignmentDocs = ctx.user.stickyBucketAssignmentDocs || {};
ctx.user.stickyBucketAssignmentDocs[attrKey] = doc;
// save doc
ctx.user.saveStickyBucketAssignmentDoc(doc);
}
}
// 14. Fire the tracking callback(s)
// Store the promise in case we're awaiting it (ex: browser url redirects)
const trackingCalls = onExperimentViewed(ctx, experiment, result);
if (trackingCalls.length === 0 && ctx.global.saveDeferredTrack) {
ctx.global.saveDeferredTrack({
experiment,
result
});
}
const trackingCall = !trackingCalls.length ? undefined : trackingCalls.length === 1 ? trackingCalls[0] : Promise.all(trackingCalls).then(() => {});
// 14.1 Keep track of completed changeIds
"changeId" in experiment && experiment.changeId && ctx.global.recordChangeId && ctx.global.recordChangeId(experiment.changeId);
// 15. Return the result
process.env.NODE_ENV !== "production" && ctx.global.log("In experiment", {
id: key,
variation: result.variationId
});
return {
result,
trackingCall
};
}
function getFeatureResult(ctx, key, value, source, ruleId, experiment, result) {
const ret = {
value,
on: !!value,
off: !value,
source,
ruleId: ruleId || ""
};
if (experiment) ret.experiment = experiment;
if (result) ret.experimentResult = result;
// Track the usage of this feature in real-time
if (source !== "override") {
onFeatureUsage(ctx, key, ret);
}
return ret;
}
function getAttributes(ctx) {
return {
...ctx.user.attributes,
...ctx.user.attributeOverrides
};
}
function conditionPasses(condition, ctx) {
return (0, _mongrule.evalCondition)(getAttributes(ctx), condition, ctx.global.savedGroups || {});
}
function isFilteredOut(filters, ctx) {
return filters.some(filter => {
const {
hashValue
} = getHashAttribute(ctx, filter.attribute);
if (!hashValue) return true;
const n = (0, _util.hash)(filter.seed, hashValue, filter.hashVersion || 2);
if (n === null) return true;
return !filter.ranges.some(r => (0, _util.inRange)(n, r));
});
}
function isIncludedInRollout(ctx, seed, hashAttribute, fallbackAttribute, range, coverage, hashVersion) {
if (!range && coverage === undefined) return true;
if (!range && coverage === 0) return false;
const {
hashValue
} = getHashAttribute(ctx, hashAttribute, fallbackAttribute);
if (!hashValue) {
return false;
}
const n = (0, _util.hash)(seed, hashValue, hashVersion || 1);
if (n === null) return false;
return range ? (0, _util.inRange)(n, range) : coverage !== undefined ? n <= coverage : true;
}
function getExperimentResult(ctx, experiment, variationIndex, hashUsed, featureId, bucket, stickyBucketUsed) {
let inExperiment = true;
// If assigned variation is not valid, use the baseline and mark the user as not in the experiment
if (variationIndex < 0 || variationIndex >= experiment.variations.length) {
variationIndex = 0;
inExperiment = false;
}
const {
hashAttribute,
hashValue
} = getHashAttribute(ctx, experiment.hashAttribute, ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing ? experiment.fallbackAttribute : undefined);
const meta = experiment.meta ? experiment.meta[variationIndex] : {};
const res = {
key: meta.key || "" + variationIndex,
featureId,
inExperiment,
hashUsed,
variationId: variationIndex,
value: experiment.variations[variationIndex],
hashAttribute,
hashValue,
stickyBucketUsed: !!stickyBucketUsed
};
if (meta.name) res.name = meta.name;
if (bucket !== undefined) res.bucket = bucket;
if (meta.passthrough) res.passthrough = meta.passthrough;
return res;
}
function mergeOverrides(experiment, ctx) {
const key = experiment.key;
const o = ctx.global.overrides;
if (o && o[key]) {
experiment = Object.assign({}, experiment, o[key]);
if (typeof experiment.url === "string") {
experiment.url = (0, _util.getUrlRegExp)(
// eslint-disable-next-line
experiment.url);
}
}
return experiment;
}
function getHashAttribute(ctx, attr, fallback) {
let hashAttribute = attr || "id";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let hashValue = "";
const attributes = getAttributes(ctx);
if (attributes[hashAttribute]) {
hashValue = attributes[hashAttribute];
}
// if no match, try fallback
if (!hashValue && fallback) {
if (attributes[fallback]) {
hashValue = attributes[fallback];
}
if (hashValue) {
hashAttribute = fallback;
}
}
return {
hashAttribute,
hashValue
};
}
function urlIsValid(urlRegex, ctx) {
const url = ctx.user.url;
if (!url) return false;
const pathOnly = url.replace(/^https?:\/\//, "").replace(/^[^/]*\//, "/");
if (urlRegex.test(url)) return true;
if (urlRegex.test(pathOnly)) return true;
return false;
}
function hasGroupOverlap(expGroups, ctx) {
const groups = ctx.global.groups || {};
for (let i = 0; i < expGroups.length; i++) {
if (groups[expGroups[i]]) return true;
}
return false;
}
function getStickyBucketVariation({
ctx,
expKey,
expBucketVersion,
expHashAttribute,
expFallbackAttribute,
expMinBucketVersion,
expMeta
}) {
expBucketVersion = expBucketVersion || 0;
expMinBucketVersion = expMinBucketVersion || 0;
expHashAttribute = expHashAttribute || "id";
expMeta = expMeta || [];
const id = getStickyBucketExperimentKey(expKey, expBucketVersion);
const assignments = getStickyBucketAssignments(ctx, expHashAttribute, expFallbackAttribute);
// users with any blocked bucket version (0 to minExperimentBucketVersion - 1) are excluded from the test
if (expMinBucketVersion > 0) {
for (let i = 0; i < expMinBucketVersion; i++) {
const blockedKey = getStickyBucketExperimentKey(expKey, i);
if (assignments[blockedKey] !== undefined) {
return {
variation: -1,
versionIsBlocked: true
};
}
}
}
const variationKey = assignments[id];
if (variationKey === undefined)
// no assignment found
return {
variation: -1
};
const variation = expMeta.findIndex(m => m.key === variationKey);
if (variation < 0)
// invalid assignment, treat as "no assignment found"
return {
variation: -1
};
return {
variation
};
}
function getStickyBucketExperimentKey(experimentKey, experimentBucketVersion) {
experimentBucketVersion = experimentBucketVersion || 0;
return `${experimentKey}__${experimentBucketVersion}`;
}
function getStickyBucketAttributeKey(attributeName, attributeValue) {
return `${attributeName}||${attributeValue}`;
}
function getStickyBucketAssignments(ctx, expHashAttribute, expFallbackAttribute) {
if (!ctx.user.stickyBucketAssignmentDocs) return {};
const {
hashAttribute,
hashValue
} = getHashAttribute(ctx, expHashAttribute);
const hashKey = getStickyBucketAttributeKey(hashAttribute, (0, _util.toString)(hashValue));
const {
hashAttribute: fallbackAttribute,
hashValue: fallbackValue
} = getHashAttribute(ctx, expFallbackAttribute);
const fallbackKey = fallbackValue ? getStickyBucketAttributeKey(fallbackAttribute, (0, _util.toString)(fallbackValue)) : null;
const assignments = {};
if (fallbackKey && ctx.user.stickyBucketAssignmentDocs[fallbackKey]) {
Object.assign(assignments, ctx.user.stickyBucketAssignmentDocs[fallbackKey].assignments || {});
}
if (ctx.user.stickyBucketAssignmentDocs[hashKey]) {
Object.assign(assignments, ctx.user.stickyBucketAssignmentDocs[hashKey].assignments || {});
}
return assignments;
}
function generateStickyBucketAssignmentDoc(ctx, attributeName, attributeValue, assignments) {
const key = getStickyBucketAttributeKey(attributeName, attributeValue);
const existingAssignments = ctx.user.stickyBucketAssignmentDocs && ctx.user.stickyBucketAssignmentDocs[key] ? ctx.user.stickyBucketAssignmentDocs[key].assignments || {} : {};
const newAssignments = {
...existingAssignments,
...assignments
};
const changed = JSON.stringify(existingAssignments) !== JSON.stringify(newAssignments);
return {
key,
doc: {
attributeName,
attributeValue,
assignments: newAssignments
},
changed
};
}
function deriveStickyBucketIdentifierAttributes(ctx, data) {
const attributes = new Set();
const features = data && data.features ? data.features : ctx.global.features || {};
const experiments = data && data.experiments ? data.experiments : ctx.global.experiments || [];
Object.keys(features).forEach(id => {
const feature = features[id];
if (feature.rules) {
for (const rule of feature.rules) {
if (rule.variations) {
attributes.add(rule.hashAttribute || "id");
if (rule.fallbackAttribute) {
attributes.add(rule.fallbackAttribute);
}
}
}
}
});
experiments.map(experiment => {
attributes.add(experiment.hashAttribute || "id");
if (experiment.fallbackAttribute) {
attributes.add(experiment.fallbackAttribute);
}
});
return Array.from(attributes);
}
async function getAllStickyBucketAssignmentDocs(ctx, stickyBucketService, data) {
const attributes = getStickyBucketAttributes(ctx, data);
return stickyBucketService.getAllAssignments(attributes);
}
function getStickyBucketAttributes(ctx, data) {
const attributes = {};
const stickyBucketIdentifierAttributes = deriveStickyBucketIdentifierAttributes(ctx, data);
stickyBucketIdentifierAttributes.forEach(attr => {
const {
hashValue
} = getHashAttribute(ctx, attr);
attributes[attr] = (0, _util.toString)(hashValue);
});
return attributes;
}
async function decryptPayload(data, decryptionKey, subtle) {
data = {
...data
};
if (data.encryptedFeatures) {
try {
data.features = JSON.parse(await (0, _util.decrypt)(data.encryptedFeatures, decryptionKey, subtle));
} catch (e) {
console.error(e);
}
delete data.encryptedFeatures;
}
if (data.encryptedExperiments) {
try {
data.experiments = JSON.parse(await (0, _util.decrypt)(data.encryptedExperiments, decryptionKey, subtle));
} catch (e) {
console.error(e);
}
delete data.encryptedExperiments;
}
if (data.encryptedSavedGroups) {
try {
data.savedGroups = JSON.parse(await (0, _util.decrypt)(data.encryptedSavedGroups, decryptionKey, subtle));
} catch (e) {
console.error(e);
}
delete data.encryptedSavedGroups;
}
return data;
}
function getApiHosts(options) {
const defaultHost = options.apiHost || "https://cdn.growthbook.io";
return {
apiHost: defaultHost.replace(/\/*$/, ""),
streamingHost: (options.streamingHost || defaultHost).replace(/\/*$/, ""),
apiRequestHeaders: options.apiHostRequestHeaders,
streamingHostRequestHeaders: options.streamingHostRequestHeaders
};
}
function getExperimentDedupeKey(experiment, result) {
return result.hashAttribute + result.hashValue + experiment.key + result.variationId;
}
//# sourceMappingURL=core.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,492 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.clearAutoRefresh = clearAutoRefresh;
exports.clearCache = clearCache;
exports.configureCache = configureCache;
exports.helpers = void 0;
exports.onHidden = onHidden;
exports.onVisible = onVisible;
exports.refreshFeatures = refreshFeatures;
exports.setPolyfills = setPolyfills;
exports.startStreaming = startStreaming;
exports.unsubscribe = unsubscribe;
var _util = require("./util");
// Config settings
const cacheSettings = {
// Consider a fetch stale after 1 minute
staleTTL: 1000 * 60,
// Max time to keep a fetch in cache (4 hours default)
maxAge: 1000 * 60 * 60 * 4,
cacheKey: "gbFeaturesCache",
backgroundSync: true,
maxEntries: 10,
disableIdleStreams: false,
idleStreamInterval: 20000,
disableCache: false
};
const polyfills = (0, _util.getPolyfills)();
const helpers = exports.helpers = {
fetchFeaturesCall: ({
host,
clientKey,
headers
}) => {
return polyfills.fetch(`${host}/api/features/${clientKey}`, {
headers
});
},
fetchRemoteEvalCall: ({
host,
clientKey,
payload,
headers
}) => {
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
...headers
},
body: JSON.stringify(payload)
};
return polyfills.fetch(`${host}/api/eval/${clientKey}`, options);
},
eventSourceCall: ({
host,
clientKey,
headers
}) => {
if (headers) {
return new polyfills.EventSource(`${host}/sub/${clientKey}`, {
headers
});
}
return new polyfills.EventSource(`${host}/sub/${clientKey}`);
},
startIdleListener: () => {
let idleTimeout;
const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
if (!isBrowser) return;
const onVisibilityChange = () => {
if (document.visibilityState === "visible") {
window.clearTimeout(idleTimeout);
onVisible();
} else if (document.visibilityState === "hidden") {
idleTimeout = window.setTimeout(onHidden, cacheSettings.idleStreamInterval);
}
};
document.addEventListener("visibilitychange", onVisibilityChange);
return () => document.removeEventListener("visibilitychange", onVisibilityChange);
},
stopIdleListener: () => {
// No-op, replaced by startIdleListener
}
};
try {
if (globalThis.localStorage) {
polyfills.localStorage = globalThis.localStorage;
}
} catch (e) {
// Ignore localStorage errors
}
// Global state
const subscribedInstances = new Map();
let cacheInitialized = false;
const cache = new Map();
const activeFetches = new Map();
const streams = new Map();
const supportsSSE = new Set();
// Public functions
function setPolyfills(overrides) {
Object.assign(polyfills, overrides);
}
function configureCache(overrides) {
Object.assign(cacheSettings, overrides);
if (!cacheSettings.backgroundSync) {
clearAutoRefresh();
}
}
async function clearCache() {
cache.clear();
activeFetches.clear();
clearAutoRefresh();
cacheInitialized = false;
await updatePersistentCache();
}
// Get or fetch features and refresh the SDK instance
async function refreshFeatures({
instance,
timeout,
skipCache,
allowStale,
backgroundSync
}) {
if (!backgroundSync) {
cacheSettings.backgroundSync = false;
}
return fetchFeaturesWithCache({
instance,
allowStale,
timeout,
skipCache
});
}
// Subscribe a GrowthBook instance to feature changes
function subscribe(instance) {
const key = getKey(instance);
const subs = subscribedInstances.get(key) || new Set();
subs.add(instance);
subscribedInstances.set(key, subs);
}
function unsubscribe(instance) {
subscribedInstances.forEach(s => s.delete(instance));
}
function onHidden() {
streams.forEach(channel => {
if (!channel) return;
channel.state = "idle";
disableChannel(channel);
});
}
function onVisible() {
streams.forEach(channel => {
if (!channel) return;
if (channel.state !== "idle") return;
enableChannel(channel);
});
}
// Private functions
async function updatePersistentCache() {
try {
if (!polyfills.localStorage) return;
await polyfills.localStorage.setItem(cacheSettings.cacheKey, JSON.stringify(Array.from(cache.entries())));
} catch (e) {
// Ignore localStorage errors
}
}
// SWR wrapper for fetching features. May indirectly or directly start SSE streaming.
async function fetchFeaturesWithCache({
instance,
allowStale,
timeout,
skipCache
}) {
const key = getKey(instance);
const cacheKey = getCacheKey(instance);
const now = new Date();
const minStaleAt = new Date(now.getTime() - cacheSettings.maxAge + cacheSettings.staleTTL);
await initializeCache();
const existing = !cacheSettings.disableCache && !skipCache ? cache.get(cacheKey) : undefined;
if (existing && (allowStale || existing.staleAt > now) && existing.staleAt > minStaleAt) {
// Restore from cache whether SSE is supported
if (existing.sse) supportsSSE.add(key);
// Reload features in the background if stale
if (existing.staleAt < now) {
fetchFeatures(instance);
}
// Otherwise, if we don't need to refresh now, start a background sync
else {
startAutoRefresh(instance);
}
return {
data: existing.data,
success: true,
source: "cache"
};
} else {
const res = await (0, _util.promiseTimeout)(fetchFeatures(instance), timeout);
return res || {
data: null,
success: false,
source: "timeout",
error: new Error("Timeout")
};
}
}
function getKey(instance) {
const [apiHost, clientKey] = instance.getApiInfo();
return `${apiHost}||${clientKey}`;
}
function getCacheKey(instance) {
const baseKey = getKey(instance);
if (!("isRemoteEval" in instance) || !instance.isRemoteEval()) return baseKey;
const attributes = instance.getAttributes();
const cacheKeyAttributes = instance.getCacheKeyAttributes() || Object.keys(instance.getAttributes());
const ca = {};
cacheKeyAttributes.forEach(key => {
ca[key] = attributes[key];
});
const fv = instance.getForcedVariations();
const url = instance.getUrl();
return `${baseKey}||${JSON.stringify({
ca,
fv,
url
})}`;
}
// Populate cache from localStorage (if available)
async function initializeCache() {
if (cacheInitialized) return;
cacheInitialized = true;
try {
if (polyfills.localStorage) {
const value = await polyfills.localStorage.getItem(cacheSettings.cacheKey);
if (!cacheSettings.disableCache && value) {
const parsed = JSON.parse(value);
if (parsed && Array.isArray(parsed)) {
parsed.forEach(([key, data]) => {
cache.set(key, {
...data,
staleAt: new Date(data.staleAt)
});
});
}
cleanupCache();
}
}
} catch (e) {
// Ignore localStorage errors
}
if (!cacheSettings.disableIdleStreams) {
const cleanupFn = helpers.startIdleListener();
if (cleanupFn) {
helpers.stopIdleListener = cleanupFn;
}
}
}
// Enforce the maxEntries limit
function cleanupCache() {
const entriesWithTimestamps = Array.from(cache.entries()).map(([key, value]) => ({
key,
staleAt: value.staleAt.getTime()
})).sort((a, b) => a.staleAt - b.staleAt);
const entriesToRemoveCount = Math.min(Math.max(0, cache.size - cacheSettings.maxEntries), cache.size);
for (let i = 0; i < entriesToRemoveCount; i++) {
cache.delete(entriesWithTimestamps[i].key);
}
}
// Called whenever new features are fetched from the API
function onNewFeatureData(key, cacheKey, data) {
// If contents haven't changed, ignore the update, extend the stale TTL
const version = data.dateUpdated || "";
const staleAt = new Date(Date.now() + cacheSettings.staleTTL);
const existing = !cacheSettings.disableCache ? cache.get(cacheKey) : undefined;
if (existing && version && existing.version === version) {
existing.staleAt = staleAt;
updatePersistentCache();
return;
}
if (!cacheSettings.disableCache) {
// Update in-memory cache
cache.set(cacheKey, {
data,
version,
staleAt,
sse: supportsSSE.has(key)
});
cleanupCache();
}
// Update local storage (don't await this, just update asynchronously)
updatePersistentCache();
// Update features for all subscribed GrowthBook instances
const instances = subscribedInstances.get(key);
instances && instances.forEach(instance => refreshInstance(instance, data));
}
async function refreshInstance(instance, data) {
await instance.setPayload(data || instance.getPayload());
}
// Fetch the features payload from helper function or from in-mem injected payload
async function fetchFeatures(instance) {
const {
apiHost,
apiRequestHeaders
} = instance.getApiHosts();
const clientKey = instance.getClientKey();
const remoteEval = "isRemoteEval" in instance && instance.isRemoteEval();
const key = getKey(instance);
const cacheKey = getCacheKey(instance);
let promise = activeFetches.get(cacheKey);
if (!promise) {
const fetcher = remoteEval ? helpers.fetchRemoteEvalCall({
host: apiHost,
clientKey,
payload: {
attributes: instance.getAttributes(),
forcedVariations: instance.getForcedVariations(),
forcedFeatures: Array.from(instance.getForcedFeatures().entries()),
url: instance.getUrl()
},
headers: apiRequestHeaders
}) : helpers.fetchFeaturesCall({
host: apiHost,
clientKey,
headers: apiRequestHeaders
});
// TODO: auto-retry if status code indicates a temporary error
promise = fetcher.then(res => {
if (!res.ok) {
throw new Error(`HTTP error: ${res.status}`);
}
if (res.headers.get("x-sse-support") === "enabled") {
supportsSSE.add(key);
}
return res.json();
}).then(data => {
onNewFeatureData(key, cacheKey, data);
startAutoRefresh(instance);
activeFetches.delete(cacheKey);
return {
data,
success: true,
source: "network"
};
}).catch(e => {
process.env.NODE_ENV !== "production" && instance.log("Error fetching features", {
apiHost,
clientKey,
error: e ? e.message : null
});
activeFetches.delete(cacheKey);
return {
data: null,
source: "error",
success: false,
error: e
};
});
activeFetches.set(cacheKey, promise);
}
return promise;
}
// Start SSE streaming, listens to feature payload changes and triggers a refresh or re-fetch
function startAutoRefresh(instance, forceSSE = false) {
const key = getKey(instance);
const cacheKey = getCacheKey(instance);
const {
streamingHost,
streamingHostRequestHeaders
} = instance.getApiHosts();
const clientKey = instance.getClientKey();
if (forceSSE) {
supportsSSE.add(key);
}
if (cacheSettings.backgroundSync && supportsSSE.has(key) && polyfills.EventSource) {
if (streams.has(key)) return;
const channel = {
src: null,
host: streamingHost,
clientKey,
headers: streamingHostRequestHeaders,
cb: event => {
try {
if (event.type === "features-updated") {
const instances = subscribedInstances.get(key);
instances && instances.forEach(instance => {
fetchFeatures(instance);
});
} else if (event.type === "features") {
const json = JSON.parse(event.data);
onNewFeatureData(key, cacheKey, json);
}
// Reset error count on success
channel.errors = 0;
} catch (e) {
process.env.NODE_ENV !== "production" && instance.log("SSE Error", {
streamingHost,
clientKey,
error: e ? e.message : null
});
onSSEError(channel);
}
},
errors: 0,
state: "active"
};
streams.set(key, channel);
enableChannel(channel);
}
}
function onSSEError(channel) {
if (channel.state === "idle") return;
channel.errors++;
if (channel.errors > 3 || channel.src && channel.src.readyState === 2) {
// exponential backoff after 4 errors, with jitter
const delay = Math.pow(3, channel.errors - 3) * (1000 + Math.random() * 1000);
disableChannel(channel);
setTimeout(() => {
if (["idle", "active"].includes(channel.state)) return;
enableChannel(channel);
}, Math.min(delay, 300000)); // 5 minutes max
}
}
function disableChannel(channel) {
if (!channel.src) return;
channel.src.onopen = null;
channel.src.onerror = null;
channel.src.close();
channel.src = null;
if (channel.state === "active") {
channel.state = "disabled";
}
}
function enableChannel(channel) {
channel.src = helpers.eventSourceCall({
host: channel.host,
clientKey: channel.clientKey,
headers: channel.headers
});
channel.state = "active";
channel.src.addEventListener("features", channel.cb);
channel.src.addEventListener("features-updated", channel.cb);
channel.src.onerror = () => onSSEError(channel);
channel.src.onopen = () => {
channel.errors = 0;
};
}
function destroyChannel(channel, key) {
disableChannel(channel);
streams.delete(key);
}
function clearAutoRefresh() {
// Clear list of which keys are auto-updated
supportsSSE.clear();
// Stop listening for any SSE events
streams.forEach(destroyChannel);
// Remove all references to GrowthBook instances
subscribedInstances.clear();
// Run the idle stream cleanup function
helpers.stopIdleListener();
}
function startStreaming(instance, options) {
if (options.streaming) {
if (!instance.getClientKey()) {
throw new Error("Must specify clientKey to enable streaming");
}
if (options.payload) {
startAutoRefresh(instance, true);
}
subscribe(instance);
}
}
//# sourceMappingURL=feature-repository.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,157 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "BrowserCookieStickyBucketService", {
enumerable: true,
get: function () {
return _stickyBucketService.BrowserCookieStickyBucketService;
}
});
Object.defineProperty(exports, "EVENT_EXPERIMENT_VIEWED", {
enumerable: true,
get: function () {
return _core.EVENT_EXPERIMENT_VIEWED;
}
});
Object.defineProperty(exports, "EVENT_FEATURE_EVALUATED", {
enumerable: true,
get: function () {
return _core.EVENT_FEATURE_EVALUATED;
}
});
Object.defineProperty(exports, "ExpressCookieStickyBucketService", {
enumerable: true,
get: function () {
return _stickyBucketService.ExpressCookieStickyBucketService;
}
});
Object.defineProperty(exports, "GrowthBook", {
enumerable: true,
get: function () {
return _GrowthBook.GrowthBook;
}
});
Object.defineProperty(exports, "GrowthBookClient", {
enumerable: true,
get: function () {
return _GrowthBookClient.GrowthBookClient;
}
});
Object.defineProperty(exports, "GrowthBookMultiUser", {
enumerable: true,
get: function () {
return _GrowthBookClient.GrowthBookClient;
}
});
Object.defineProperty(exports, "LocalStorageStickyBucketService", {
enumerable: true,
get: function () {
return _stickyBucketService.LocalStorageStickyBucketService;
}
});
Object.defineProperty(exports, "RedisStickyBucketService", {
enumerable: true,
get: function () {
return _stickyBucketService.RedisStickyBucketService;
}
});
Object.defineProperty(exports, "StickyBucketService", {
enumerable: true,
get: function () {
return _stickyBucketService.StickyBucketService;
}
});
Object.defineProperty(exports, "StickyBucketServiceSync", {
enumerable: true,
get: function () {
return _stickyBucketService.StickyBucketServiceSync;
}
});
Object.defineProperty(exports, "UserScopedGrowthBook", {
enumerable: true,
get: function () {
return _GrowthBookClient.UserScopedGrowthBook;
}
});
Object.defineProperty(exports, "clearCache", {
enumerable: true,
get: function () {
return _featureRepository.clearCache;
}
});
Object.defineProperty(exports, "configureCache", {
enumerable: true,
get: function () {
return _featureRepository.configureCache;
}
});
Object.defineProperty(exports, "evalCondition", {
enumerable: true,
get: function () {
return _mongrule.evalCondition;
}
});
Object.defineProperty(exports, "getAutoExperimentChangeType", {
enumerable: true,
get: function () {
return _util.getAutoExperimentChangeType;
}
});
Object.defineProperty(exports, "getPolyfills", {
enumerable: true,
get: function () {
return _util.getPolyfills;
}
});
Object.defineProperty(exports, "helpers", {
enumerable: true,
get: function () {
return _featureRepository.helpers;
}
});
Object.defineProperty(exports, "isURLTargeted", {
enumerable: true,
get: function () {
return _util.isURLTargeted;
}
});
Object.defineProperty(exports, "onHidden", {
enumerable: true,
get: function () {
return _featureRepository.onHidden;
}
});
Object.defineProperty(exports, "onVisible", {
enumerable: true,
get: function () {
return _featureRepository.onVisible;
}
});
Object.defineProperty(exports, "paddedVersionString", {
enumerable: true,
get: function () {
return _util.paddedVersionString;
}
});
Object.defineProperty(exports, "prefetchPayload", {
enumerable: true,
get: function () {
return _GrowthBook.prefetchPayload;
}
});
Object.defineProperty(exports, "setPolyfills", {
enumerable: true,
get: function () {
return _featureRepository.setPolyfills;
}
});
var _featureRepository = require("./feature-repository");
var _GrowthBook = require("./GrowthBook");
var _GrowthBookClient = require("./GrowthBookClient");
var _stickyBucketService = require("./sticky-bucket-service");
var _mongrule = require("./mongrule");
var _util = require("./util");
var _core = require("./core");
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","names":["_featureRepository","require","_GrowthBook","_GrowthBookClient","_stickyBucketService","_mongrule","_util","_core"],"sources":["../../src/index.ts"],"sourcesContent":["export type {\n Options as Context,\n Options,\n ClientOptions as MultiUserOptions,\n ClientOptions,\n TrackingCallbackWithUser,\n TrackingDataWithUser,\n FeatureUsageCallback,\n FeatureUsageCallbackWithUser,\n UserContext,\n Attributes,\n Polyfills,\n CacheSettings,\n FeatureApiResponse,\n LoadFeaturesOptions,\n RefreshFeaturesOptions,\n DestroyOptions,\n FeatureDefinitions,\n FeatureDefinition,\n FeatureRule,\n FeatureResult,\n FeatureResultSource,\n Experiment,\n Result,\n ExperimentOverride,\n ExperimentStatus,\n JSONValue,\n SubscriptionFunction,\n LocalStorageCompat,\n WidenPrimitives,\n VariationMeta,\n Filter,\n VariationRange,\n UrlTarget,\n AutoExperiment,\n AutoExperimentVariation,\n AutoExperimentChangeType,\n DOMMutation,\n UrlTargetType,\n RenderFunction,\n StickyAttributeKey,\n StickyExperimentKey,\n StickyAssignments,\n StickyAssignmentsDocument,\n TrackingData,\n TrackingCallback,\n NavigateCallback,\n ApplyDomChangesCallback,\n InitOptions,\n PrefetchOptions,\n InitResponse,\n InitSyncOptions,\n Helpers,\n GrowthBookPayload,\n SavedGroupsValues,\n EventLogger,\n EventProperties,\n Plugin,\n LogUnion,\n} from \"./types/growthbook\";\n\nexport type {\n ConditionInterface,\n ParentConditionInterface,\n} from \"./types/mongrule\";\n\nexport {\n setPolyfills,\n clearCache,\n configureCache,\n helpers,\n onVisible,\n onHidden,\n} from \"./feature-repository\";\n\nexport { GrowthBook, prefetchPayload } from \"./GrowthBook\";\n\nexport {\n GrowthBookClient as GrowthBookMultiUser,\n GrowthBookClient,\n UserScopedGrowthBook,\n} from \"./GrowthBookClient\";\n\nexport {\n StickyBucketService,\n StickyBucketServiceSync,\n LocalStorageStickyBucketService,\n ExpressCookieStickyBucketService,\n BrowserCookieStickyBucketService,\n RedisStickyBucketService,\n} from \"./sticky-bucket-service\";\n\nexport { evalCondition } from \"./mongrule\";\n\nexport {\n isURLTargeted,\n getPolyfills,\n getAutoExperimentChangeType,\n paddedVersionString,\n} from \"./util\";\n\nexport { EVENT_EXPERIMENT_VIEWED, EVENT_FEATURE_EVALUATED } from \"./core\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkEA,IAAAA,kBAAA,GAAAC,OAAA;AASA,IAAAC,WAAA,GAAAD,OAAA;AAEA,IAAAE,iBAAA,GAAAF,OAAA;AAMA,IAAAG,oBAAA,GAAAH,OAAA;AASA,IAAAI,SAAA,GAAAJ,OAAA;AAEA,IAAAK,KAAA,GAAAL,OAAA;AAOA,IAAAM,KAAA,GAAAN,OAAA","ignoreList":[]}

View File

@@ -0,0 +1,251 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.evalCondition = evalCondition;
var _util = require("./util");
/* eslint-disable @typescript-eslint/no-explicit-any */
const _regexCache = {};
// The top-level condition evaluation function
function evalCondition(obj, condition,
// Must be included for `condition` to correctly evaluate group Operators
savedGroups) {
savedGroups = savedGroups || {};
// Condition is an object, keys are either specific operators or object paths
// values are either arguments for operators or conditions for paths
for (const [k, v] of Object.entries(condition)) {
switch (k) {
case "$or":
if (!evalOr(obj, v, savedGroups)) return false;
break;
case "$nor":
if (evalOr(obj, v, savedGroups)) return false;
break;
case "$and":
if (!evalAnd(obj, v, savedGroups)) return false;
break;
case "$not":
if (evalCondition(obj, v, savedGroups)) return false;
break;
default:
if (!evalConditionValue(v, getPath(obj, k), savedGroups)) return false;
}
}
return true;
}
// Return value at dot-separated path of an object
function getPath(obj, path) {
const parts = path.split(".");
let current = obj;
for (let i = 0; i < parts.length; i++) {
if (current && typeof current === "object" && parts[i] in current) {
current = current[parts[i]];
} else {
return null;
}
}
return current;
}
// Transform a regex string into a real RegExp object
function getRegex(regex, insensitive = false) {
const cacheKey = `${regex}${insensitive ? "/i" : ""}`;
if (!_regexCache[cacheKey]) {
_regexCache[cacheKey] = new RegExp(regex.replace(/([^\\])\//g, "$1\\/"), insensitive ? "i" : undefined);
}
return _regexCache[cacheKey];
}
// Evaluate a single value against a condition
function evalConditionValue(condition, value, savedGroups, insensitive = false) {
// Simple equality comparisons
if (typeof condition === "string") {
if (insensitive) {
return String(value).toLowerCase() === condition.toLowerCase();
}
return value + "" === condition;
}
if (typeof condition === "number") {
return value * 1 === condition;
}
if (typeof condition === "boolean") {
return value !== null && !!value === condition;
}
if (condition === null) {
return value === null;
}
if (Array.isArray(condition) || !isOperatorObject(condition)) {
return JSON.stringify(value) === JSON.stringify(condition);
}
// This is a special operator condition and we should evaluate each one separately
for (const op in condition) {
if (!evalOperatorCondition(op, value, condition[op], savedGroups)) {
return false;
}
}
return true;
}
// If the object has only keys that start with '$'
function isOperatorObject(obj) {
const keys = Object.keys(obj);
return keys.length > 0 && keys.filter(k => k[0] === "$").length === keys.length;
}
// Return the data type of a value
function getType(v) {
if (v === null) return "null";
if (Array.isArray(v)) return "array";
const t = typeof v;
if (["string", "number", "boolean", "object", "undefined"].includes(t)) {
return t;
}
return "unknown";
}
// At least one element of actual must match the expected condition/value
function elemMatch(actual, expected, savedGroups) {
if (!Array.isArray(actual)) return false;
const check = isOperatorObject(expected) ? v => evalConditionValue(expected, v, savedGroups) : v => evalCondition(v, expected, savedGroups);
for (let i = 0; i < actual.length; i++) {
if (actual[i] && check(actual[i])) {
return true;
}
}
return false;
}
function isIn(actual, expected, insensitive = false) {
if (insensitive) {
const caseFold = val => typeof val === "string" ? val.toLowerCase() : val;
// Do an intersection if attribute is an array (insensitive)
if (Array.isArray(actual)) {
return actual.some(el => expected.some(exp => caseFold(el) === caseFold(exp)));
}
return expected.some(exp => caseFold(actual) === caseFold(exp));
}
// Do an intersection if attribute is an array
if (Array.isArray(actual)) {
return actual.some(el => expected.includes(el));
}
return expected.includes(actual);
}
function isInAll(actual, expected, savedGroups, insensitive = false) {
if (!Array.isArray(actual)) return false;
for (let i = 0; i < expected.length; i++) {
let passed = false;
for (let j = 0; j < actual.length; j++) {
if (evalConditionValue(expected[i], actual[j], savedGroups, insensitive)) {
passed = true;
break;
}
}
if (!passed) return false;
}
return true;
}
// Evaluate a single operator condition
function evalOperatorCondition(operator, actual, expected, savedGroups) {
switch (operator) {
case "$veq":
return (0, _util.paddedVersionString)(actual) === (0, _util.paddedVersionString)(expected);
case "$vne":
return (0, _util.paddedVersionString)(actual) !== (0, _util.paddedVersionString)(expected);
case "$vgt":
return (0, _util.paddedVersionString)(actual) > (0, _util.paddedVersionString)(expected);
case "$vgte":
return (0, _util.paddedVersionString)(actual) >= (0, _util.paddedVersionString)(expected);
case "$vlt":
return (0, _util.paddedVersionString)(actual) < (0, _util.paddedVersionString)(expected);
case "$vlte":
return (0, _util.paddedVersionString)(actual) <= (0, _util.paddedVersionString)(expected);
case "$eq":
return actual === expected;
case "$ne":
return actual !== expected;
case "$lt":
return actual < expected;
case "$lte":
return actual <= expected;
case "$gt":
return actual > expected;
case "$gte":
return actual >= expected;
case "$exists":
// Using `!=` and `==` instead of strict checks so it also matches for undefined
return expected ? actual != null : actual == null;
case "$in":
if (!Array.isArray(expected)) return false;
return isIn(actual, expected);
case "$ini":
if (!Array.isArray(expected)) return false;
return isIn(actual, expected, true);
case "$inGroup":
return isIn(actual, savedGroups[expected] || []);
case "$notInGroup":
return !isIn(actual, savedGroups[expected] || []);
case "$nin":
if (!Array.isArray(expected)) return false;
return !isIn(actual, expected);
case "$nini":
if (!Array.isArray(expected)) return false;
return !isIn(actual, expected, true);
case "$not":
return !evalConditionValue(expected, actual, savedGroups);
case "$size":
if (!Array.isArray(actual)) return false;
return evalConditionValue(expected, actual.length, savedGroups);
case "$elemMatch":
return elemMatch(actual, expected, savedGroups);
case "$all":
if (!Array.isArray(expected)) return false;
return isInAll(actual, expected, savedGroups);
case "$alli":
if (!Array.isArray(expected)) return false;
return isInAll(actual, expected, savedGroups, true);
case "$regex":
try {
return getRegex(expected).test(actual);
} catch (e) {
return false;
}
case "$regexi":
try {
return getRegex(expected, true).test(actual);
} catch (e) {
return false;
}
case "$type":
return getType(actual) === expected;
default:
console.error("Unknown operator: " + operator);
return false;
}
}
// Recursive $or rule
function evalOr(obj, conditions, savedGroups) {
if (!conditions.length) return true;
for (let i = 0; i < conditions.length; i++) {
if (evalCondition(obj, conditions[i], savedGroups)) {
return true;
}
}
return false;
}
// Recursive $and rule
function evalAnd(obj, conditions, savedGroups) {
for (let i = 0; i < conditions.length; i++) {
if (!evalCondition(obj, conditions[i], savedGroups)) {
return false;
}
}
return true;
}
//# sourceMappingURL=mongrule.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,191 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.autoAttributesPlugin = autoAttributesPlugin;
function getBrowserDevice(ua) {
const browser = ua.match(/Edg/) ? "edge" : ua.match(/Chrome/) ? "chrome" : ua.match(/Firefox/) ? "firefox" : ua.match(/Safari/) ? "safari" : "unknown";
const deviceType = ua.match(/Mobi/) ? "mobile" : "desktop";
return {
browser,
deviceType
};
}
function getURLAttributes(url) {
if (!url) return {};
return {
url: url.href,
path: url.pathname,
host: url.host,
query: url.search
};
}
function autoAttributesPlugin(settings = {}) {
// Browser only
if (typeof window === "undefined") {
throw new Error("autoAttributesPlugin only works in the browser");
}
const COOKIE_NAME = settings.uuidCookieName || "gbuuid";
const uuidKey = settings.uuidKey || "id";
let uuid = settings.uuid || "";
function persistUUID() {
setCookie(COOKIE_NAME, uuid);
}
function getUUID() {
// Already stored in memory, return
if (uuid) return uuid;
// If cookie is already set, return
uuid = getCookie(COOKIE_NAME);
if (uuid) return uuid;
// Generate a new UUID
uuid = genUUID(window.crypto);
return uuid;
}
// Listen for a custom event to persist the UUID cookie
document.addEventListener("growthbookpersist", () => {
persistUUID();
});
function getAutoAttributes(settings) {
const ua = navigator.userAgent;
const _uuid = getUUID();
// If a uuid is provided, default persist to false, otherwise default to true
if (settings.uuidAutoPersist ?? !settings.uuid) {
persistUUID();
}
const url = location;
return {
...getDataLayerVariables(),
[uuidKey]: _uuid,
...getURLAttributes(url),
pageTitle: document.title,
...getBrowserDevice(ua),
...getUtmAttributes(url)
};
}
return gb => {
// Only works for instances with user attributes
if ("createScopedInstance" in gb) {
return;
}
// Set initial attributes
const attributes = getAutoAttributes(settings);
attributes.url && gb.setURL(attributes.url);
gb.updateAttributes(attributes);
// Poll for URL changes and update GrowthBook
let currentUrl = attributes.url;
const intervalTimer = setInterval(() => {
if (location.href !== currentUrl) {
currentUrl = location.href;
gb.setURL(currentUrl);
gb.updateAttributes(getAutoAttributes(settings));
}
}, 500);
// Listen for a custom event to update URL and attributes
const refreshListener = () => {
if (location.href !== currentUrl) {
currentUrl = location.href;
gb.setURL(currentUrl);
}
gb.updateAttributes(getAutoAttributes(settings));
};
document.addEventListener("growthbookrefresh", refreshListener);
if ("onDestroy" in gb) {
gb.onDestroy(() => {
clearInterval(intervalTimer);
document.removeEventListener("growthbookrefresh", refreshListener);
});
}
};
}
function setCookie(name, value) {
const d = new Date();
const COOKIE_DAYS = 400; // 400 days is the max cookie duration for chrome
d.setTime(d.getTime() + 24 * 60 * 60 * 1000 * COOKIE_DAYS);
document.cookie = name + "=" + value + ";path=/;expires=" + d.toUTCString();
}
function getCookie(name) {
const value = "; " + document.cookie;
const parts = value.split(`; ${name}=`);
return parts.length === 2 ? parts[1].split(";")[0] : "";
}
// Use the browsers crypto.randomUUID if set to generate a UUID
function genUUID(crypto) {
if (crypto && crypto.randomUUID) return crypto.randomUUID();
return ("" + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => {
const n = crypto && crypto.getRandomValues ? crypto.getRandomValues(new Uint8Array(1))[0] : Math.floor(Math.random() * 256);
return (c ^ n & 15 >> c / 4).toString(16);
});
}
function getUtmAttributes(url) {
// Store utm- params in sessionStorage for future page loads
let utms = {};
try {
const existing = sessionStorage.getItem("utm_params");
if (existing) {
utms = JSON.parse(existing);
}
} catch (e) {
// Do nothing if sessionStorage is disabled (e.g. incognito window)
}
// Add utm params from querystring
if (url && url.search) {
const params = new URLSearchParams(url.search);
let hasChanges = false;
["source", "medium", "campaign", "term", "content"].forEach(k => {
// Querystring is in snake_case
const param = `utm_${k}`;
// Attribute keys are camelCase
const attr = `utm` + k[0].toUpperCase() + k.slice(1);
if (params.has(param)) {
utms[attr] = params.get(param) || "";
hasChanges = true;
}
});
// Write back to sessionStorage
if (hasChanges) {
try {
sessionStorage.setItem("utm_params", JSON.stringify(utms));
} catch (e) {
// Do nothing if sessionStorage is disabled (e.g. incognito window)
}
}
}
return utms;
}
function getDataLayerVariables() {
if (typeof window === "undefined" || !window.dataLayer || !window.dataLayer.forEach) {
return {};
}
const obj = {};
window.dataLayer.forEach(item => {
// Skip empty and non-object entries
if (!item || typeof item !== "object" || "length" in item) return;
// Skip events
if ("event" in item) return;
Object.keys(item).forEach(k => {
// Filter out known properties that aren't useful
if (typeof k !== "string" || k.match(/^(gtm)/)) return;
const val = item[k];
// Only add primitive variable values
const valueType = typeof val;
if (["string", "number", "boolean"].includes(valueType)) {
obj[k] = val;
}
});
});
return obj;
}
//# sourceMappingURL=auto-attributes.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,173 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.devtoolsExpressPlugin = devtoolsExpressPlugin;
exports.devtoolsNextjsPlugin = devtoolsNextjsPlugin;
exports.devtoolsPlugin = devtoolsPlugin;
exports.getDebugEvent = getDebugEvent;
exports.getDebugScriptContents = getDebugScriptContents;
var _GrowthBook = require("../GrowthBook");
var _GrowthBookClient = require("../GrowthBookClient");
/* eslint-disable @typescript-eslint/no-explicit-any */
function applyDevtoolsState(devtoolsState, gb) {
// Only enable in dev mode
if (!gb.inDevMode()) {
return;
}
if (devtoolsState.attributes && typeof devtoolsState.attributes === "object") {
gb.setAttributeOverrides(devtoolsState.attributes);
}
if (devtoolsState.features && typeof devtoolsState.features === "object") {
const map = new Map(Object.entries(devtoolsState.features));
gb.setForcedFeatures(map);
}
if (devtoolsState.experiments && typeof devtoolsState.experiments === "object") {
gb.setForcedVariations(devtoolsState.experiments);
}
}
function devtoolsPlugin(devtoolsState) {
return gb => {
// Only works for user-scoped GrowthBook instances
if ("createScopedInstance" in gb) {
throw new Error("devtoolsPlugin can only be set on a user-scoped instance");
}
if (devtoolsState) {
applyDevtoolsState(devtoolsState, gb);
}
};
}
/**
* For NextJS environments.
* When using server components, use the `searchParams` and `requestCookies` fields.
* - Note: In NextJS 15+, you should await these values before passing them to the plugin
* When using middleware / api routes, provide the `request` field instead.
*/
function devtoolsNextjsPlugin({
searchParams,
requestCookies,
request
}) {
function extractGbDebugPayload({
searchParams,
requestCookies
}) {
if (searchParams) {
if ("_gbdebug" in searchParams) {
return searchParams._gbdebug;
}
if (searchParams instanceof URLSearchParams) {
return searchParams.get("_gbdebug") ?? undefined;
}
}
return requestCookies?.get("_gbdebug")?.value;
}
return gb => {
let payload = extractGbDebugPayload({
searchParams,
requestCookies
});
if (!payload && request) {
payload = extractGbDebugPayload({
searchParams: request.nextUrl.searchParams,
requestCookies: request.cookies
});
}
let state = {};
if (payload) {
try {
state = JSON.parse(payload);
} catch (e) {
console.error("cannot parse devtools payload", e);
}
}
devtoolsPlugin(state)(gb);
};
}
/**
* Intended to be used with cookieParser() middleware from npm: 'cookie-parser'.
*/
function devtoolsExpressPlugin({
request
}) {
return gb => {
let payload = typeof request?.query?.["_gbdebug"] === "string" ? request.query["_gbdebug"] : undefined;
if (!payload) {
payload = typeof request?.cookies?.["_gbdebug"] === "string" ? request.cookies["_gbdebug"] : undefined;
}
let state = {};
if (payload) {
try {
state = JSON.parse(payload);
} catch (e) {
console.error("cannot parse devtools payload", e);
}
}
devtoolsPlugin(state)(gb);
};
}
/**
* Helper method to get debug script contents for DevTools
* @param gb - GrowthBook instance. DevMode must be enabled to view log events.
* @param {string} [source] - Label these events for ease of reading in DevTools
* @example
* A React logger component (implement yourself):
```
return (
<script dangerouslySetInnerHTML={{
__html: getDebugScriptContents(gb, "nextjs")
}} />
);
```
*/
function getDebugScriptContents(gb, source) {
const event = getDebugEvent(gb, source);
if (!event) return "";
return `(window._gbdebugEvents = (window._gbdebugEvents || [])).push(${JSON.stringify(event)});`;
}
function getDebugEvent(gb, source) {
if (!("logs" in gb)) return null;
// Only enable in dev mode
if (!gb.inDevMode()) {
return null;
}
if (gb instanceof _GrowthBook.GrowthBook) {
// GrowthBook SDK
const [apiHost, clientKey] = gb.getApiInfo();
return {
logs: gb.logs,
sdkInfo: {
apiHost,
clientKey,
source,
version: gb.version,
payload: gb.getDecryptedPayload(),
attributes: gb.getAttributes()
}
};
} else if (gb instanceof _GrowthBookClient.UserScopedGrowthBook) {
// UserScopedGrowthBook SDK
const userContext = gb.getUserContext();
const [apiHost, clientKey] = gb.getApiInfo();
return {
logs: gb.logs,
sdkInfo: {
apiHost,
clientKey,
source,
version: gb.getVersion(),
payload: gb.getDecryptedPayload(),
attributes: {
...userContext.attributes,
...userContext.attributeOverrides
}
}
};
}
return null;
}
//# sourceMappingURL=devtools.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,215 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.growthbookTrackingPlugin = growthbookTrackingPlugin;
var _util = require("../util");
var _core = require("../core");
const SDK_VERSION = (0, _util.loadSDKVersion)();
function parseString(value) {
return typeof value === "string" ? value : null;
}
function parseAttributes(attributes) {
const {
user_id,
device_id,
anonymous_id,
id,
page_id,
session_id,
utmCampaign,
utmContent,
utmMedium,
utmSource,
utmTerm,
pageTitle,
...nested
} = attributes;
return {
nested,
topLevel: {
user_id: parseString(user_id),
device_id: parseString(device_id || anonymous_id || id),
page_id: parseString(page_id),
session_id: parseString(session_id),
utm_campaign: parseString(utmCampaign) || undefined,
utm_content: parseString(utmContent) || undefined,
utm_medium: parseString(utmMedium) || undefined,
utm_source: parseString(utmSource) || undefined,
utm_term: parseString(utmTerm) || undefined,
page_title: parseString(pageTitle) || undefined
}
};
}
function getEventPayload({
eventName,
properties,
attributes,
url
}) {
const {
nested,
topLevel
} = parseAttributes(attributes || {});
return {
event_name: eventName,
properties_json: properties || {},
...topLevel,
sdk_language: "js",
sdk_version: SDK_VERSION,
url: url,
context_json: nested
};
}
async function track({
clientKey,
ingestorHost,
events
}) {
if (!events.length) return;
const endpoint = `${ingestorHost || "https://us1.gb-ingest.com"}/track?client_key=${clientKey}`;
const body = JSON.stringify(events);
try {
await fetch(endpoint, {
method: "POST",
body,
headers: {
Accept: "application/json",
"Content-Type": "text/plain"
},
credentials: "omit"
});
} catch (e) {
console.error("Failed to track event", e);
}
}
function growthbookTrackingPlugin({
queueFlushInterval = 100,
ingestorHost,
enable = true,
debug,
dedupeCacheSize = 1000,
dedupeKeyAttributes = [],
eventFilter
} = {}) {
return gb => {
const clientKey = gb.getClientKey();
if (!clientKey) {
throw new Error("clientKey must be specified to use event logging");
}
// LRU cache for events to avoid duplicates
const eventCache = new Set();
if ("setEventLogger" in gb) {
let _q = [];
let timer = null;
const flush = async () => {
const events = _q;
_q = [];
timer && clearTimeout(timer);
timer = null;
events.length && (await track({
clientKey,
events,
ingestorHost
}));
};
let promise = null;
gb.setEventLogger(async (eventName, properties, userContext) => {
const data = {
eventName,
properties,
attributes: userContext.attributes || {},
url: userContext.url || ""
};
// Skip logging if the event is being filtered
if (eventFilter && !eventFilter(data)) {
return;
}
// De-dupe Feature Evaluated and Experiment Viewed events
if (eventName === _core.EVENT_FEATURE_EVALUATED || eventName === _core.EVENT_EXPERIMENT_VIEWED) {
// Build the key for de-duping
const dedupeKeyData = {
eventName,
properties
};
for (const key of dedupeKeyAttributes) {
dedupeKeyData["attr:" + key] = data.attributes[key];
}
const k = JSON.stringify(dedupeKeyData);
// Duplicate event fired recently, move to end of LRU cache and skip
if (eventCache.has(k)) {
eventCache.delete(k);
eventCache.add(k);
return;
}
eventCache.add(k);
// If the cache is too big, remove the oldest item
if (eventCache.size > dedupeCacheSize) {
const oldest = eventCache.values().next().value;
oldest && eventCache.delete(oldest);
}
}
const payload = getEventPayload(data);
debug && console.log("Logging event to GrowthBook", JSON.parse(JSON.stringify(payload)));
if (!enable) return;
_q.push(payload);
// Only one in-progress promise at a time
if (!promise) {
promise = new Promise((resolve, reject) => {
// Flush the queue after a delay
timer = setTimeout(() => {
flush().then(resolve).catch(reject);
promise = null;
}, queueFlushInterval);
});
}
await promise;
});
// Flush the queue on page unload
if (typeof document !== "undefined" && document.visibilityState) {
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
flush().catch(console.error);
}
});
}
// Flush the queue when the growthbook instance is destroyed
"onDestroy" in gb && gb.onDestroy(() => {
flush().catch(console.error);
});
}
// Listen on window.gbEvents.push if in a browser
// This makes it easier to integrate with Segment, GTM, etc.
if (typeof window !== "undefined" && !("createScopedInstance" in gb)) {
const prevEvents = Array.isArray(window.gbEvents) ? window.gbEvents : [];
window.gbEvents = {
push: event => {
if ("isDestroyed" in gb && gb.isDestroyed()) {
// If trying to log and the instance has been destroyed, switch back to just an array
// This will let the next GrowthBook instance pick it up
window.gbEvents = [event];
return;
}
if (typeof event === "string") {
gb.logEvent(event);
} else if (event) {
gb.logEvent(event.eventName, event.properties);
}
}
};
for (const event of prevEvents) {
window.gbEvents.push(event);
}
}
};
}
//# sourceMappingURL=growthbook-tracking.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,58 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "autoAttributesPlugin", {
enumerable: true,
get: function () {
return _autoAttributes.autoAttributesPlugin;
}
});
Object.defineProperty(exports, "devtoolsExpressPlugin", {
enumerable: true,
get: function () {
return _devtools.devtoolsExpressPlugin;
}
});
Object.defineProperty(exports, "devtoolsNextjsPlugin", {
enumerable: true,
get: function () {
return _devtools.devtoolsNextjsPlugin;
}
});
Object.defineProperty(exports, "devtoolsPlugin", {
enumerable: true,
get: function () {
return _devtools.devtoolsPlugin;
}
});
Object.defineProperty(exports, "getDebugEvent", {
enumerable: true,
get: function () {
return _devtools.getDebugEvent;
}
});
Object.defineProperty(exports, "getDebugScriptContents", {
enumerable: true,
get: function () {
return _devtools.getDebugScriptContents;
}
});
Object.defineProperty(exports, "growthbookTrackingPlugin", {
enumerable: true,
get: function () {
return _growthbookTracking.growthbookTrackingPlugin;
}
});
Object.defineProperty(exports, "thirdPartyTrackingPlugin", {
enumerable: true,
get: function () {
return _thirdPartyTracking.thirdPartyTrackingPlugin;
}
});
var _autoAttributes = require("./auto-attributes");
var _growthbookTracking = require("./growthbook-tracking");
var _thirdPartyTracking = require("./third-party-tracking");
var _devtools = require("./devtools");
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","names":["_autoAttributes","require","_growthbookTracking","_thirdPartyTracking","_devtools"],"sources":["../../../src/plugins/index.ts"],"sourcesContent":["export { autoAttributesPlugin } from \"./auto-attributes\";\nexport { growthbookTrackingPlugin } from \"./growthbook-tracking\";\nexport { thirdPartyTrackingPlugin } from \"./third-party-tracking\";\nexport {\n devtoolsPlugin,\n devtoolsNextjsPlugin,\n devtoolsExpressPlugin,\n getDebugScriptContents,\n getDebugEvent,\n} from \"./devtools\";\n\n// Types must be exported separately, otherwise rollup includes them in the javascript output which breaks things\nexport type {\n DevtoolsState,\n ExpressRequestCompat,\n NextjsReadonlyRequestCookiesCompat,\n NextjsRequestCompat,\n LogEvent,\n SdkInfo,\n} from \"./devtools\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,IAAAA,eAAA,GAAAC,OAAA;AACA,IAAAC,mBAAA,GAAAD,OAAA;AACA,IAAAE,mBAAA,GAAAF,OAAA;AACA,IAAAG,SAAA,GAAAH,OAAA","ignoreList":[]}

View File

@@ -0,0 +1,63 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.thirdPartyTrackingPlugin = thirdPartyTrackingPlugin;
function thirdPartyTrackingPlugin({
additionalCallback,
trackers = ["gtag", "gtm", "segment"]
} = {}) {
// Browser only
if (typeof window === "undefined") {
throw new Error("thirdPartyTrackingPlugin only works in the browser");
}
return gb => {
gb.setTrackingCallback(async (e, r) => {
const promises = [];
const eventParams = {
experiment_id: e.key,
variation_id: r.key
};
if (additionalCallback) {
promises.push(Promise.resolve(additionalCallback(e, r)));
}
// GA4 - gtag
if (trackers.includes("gtag") && window.gtag) {
let gtagResolve;
const gtagPromise = new Promise(resolve => {
gtagResolve = resolve;
});
promises.push(gtagPromise);
window.gtag("event", "experiment_viewed", {
...eventParams,
event_callback: gtagResolve
});
}
// GTM - dataLayer
if (trackers.includes("gtm") && window.dataLayer) {
let datalayerResolve;
const datalayerPromise = new Promise(resolve => {
datalayerResolve = resolve;
});
promises.push(datalayerPromise);
window.dataLayer.push({
event: "experiment_viewed",
...eventParams,
eventCallback: datalayerResolve
});
}
// Segment - analytics.js
if (trackers.includes("segment") && window.analytics && window.analytics.track) {
window.analytics.track("Experiment Viewed", eventParams);
const segmentPromise = new Promise(resolve => window.setTimeout(resolve, 300));
promises.push(segmentPromise);
}
await Promise.all(promises);
});
};
}
//# sourceMappingURL=third-party-tracking.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"third-party-tracking.js","names":["thirdPartyTrackingPlugin","additionalCallback","trackers","window","Error","gb","setTrackingCallback","e","r","promises","eventParams","experiment_id","key","variation_id","push","Promise","resolve","includes","gtag","gtagResolve","gtagPromise","event_callback","dataLayer","datalayerResolve","datalayerPromise","event","eventCallback","analytics","track","segmentPromise","setTimeout","all"],"sources":["../../../src/plugins/third-party-tracking.ts"],"sourcesContent":["import type { TrackingCallback } from \"../types/growthbook\";\nimport type { GrowthBook } from \"../GrowthBook\";\nimport type {\n GrowthBookClient,\n UserScopedGrowthBook,\n} from \"../GrowthBookClient\";\n\nexport type Trackers = \"gtag\" | \"gtm\" | \"segment\";\n\nexport function thirdPartyTrackingPlugin({\n additionalCallback,\n trackers = [\"gtag\", \"gtm\", \"segment\"],\n}: {\n additionalCallback?: TrackingCallback;\n trackers?: Trackers[];\n} = {}) {\n // Browser only\n if (typeof window === \"undefined\") {\n throw new Error(\"thirdPartyTrackingPlugin only works in the browser\");\n }\n\n return (gb: GrowthBook | UserScopedGrowthBook | GrowthBookClient) => {\n gb.setTrackingCallback(async (e, r) => {\n const promises: Promise<unknown>[] = [];\n const eventParams = { experiment_id: e.key, variation_id: r.key };\n\n if (additionalCallback) {\n promises.push(Promise.resolve(additionalCallback(e, r)));\n }\n\n // GA4 - gtag\n if (trackers.includes(\"gtag\") && window.gtag) {\n let gtagResolve;\n const gtagPromise = new Promise((resolve) => {\n gtagResolve = resolve;\n });\n promises.push(gtagPromise);\n window.gtag(\"event\", \"experiment_viewed\", {\n ...eventParams,\n event_callback: gtagResolve,\n });\n }\n\n // GTM - dataLayer\n if (trackers.includes(\"gtm\") && window.dataLayer) {\n let datalayerResolve;\n const datalayerPromise = new Promise((resolve) => {\n datalayerResolve = resolve;\n });\n promises.push(datalayerPromise);\n window.dataLayer.push({\n event: \"experiment_viewed\",\n ...eventParams,\n eventCallback: datalayerResolve,\n });\n }\n\n // Segment - analytics.js\n if (\n trackers.includes(\"segment\") &&\n window.analytics &&\n window.analytics.track\n ) {\n window.analytics.track(\"Experiment Viewed\", eventParams);\n const segmentPromise = new Promise((resolve) =>\n window.setTimeout(resolve, 300),\n );\n promises.push(segmentPromise);\n }\n\n await Promise.all(promises);\n });\n };\n}\n"],"mappings":";;;;;;AASO,SAASA,wBAAwBA,CAAC;EACvCC,kBAAkB;EAClBC,QAAQ,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,SAAS;AAItC,CAAC,GAAG,CAAC,CAAC,EAAE;EACN;EACA,IAAI,OAAOC,MAAM,KAAK,WAAW,EAAE;IACjC,MAAM,IAAIC,KAAK,CAAC,oDAAoD,CAAC;EACvE;EAEA,OAAQC,EAAwD,IAAK;IACnEA,EAAE,CAACC,mBAAmB,CAAC,OAAOC,CAAC,EAAEC,CAAC,KAAK;MACrC,MAAMC,QAA4B,GAAG,EAAE;MACvC,MAAMC,WAAW,GAAG;QAAEC,aAAa,EAAEJ,CAAC,CAACK,GAAG;QAAEC,YAAY,EAAEL,CAAC,CAACI;MAAI,CAAC;MAEjE,IAAIX,kBAAkB,EAAE;QACtBQ,QAAQ,CAACK,IAAI,CAACC,OAAO,CAACC,OAAO,CAACf,kBAAkB,CAACM,CAAC,EAAEC,CAAC,CAAC,CAAC,CAAC;MAC1D;;MAEA;MACA,IAAIN,QAAQ,CAACe,QAAQ,CAAC,MAAM,CAAC,IAAId,MAAM,CAACe,IAAI,EAAE;QAC5C,IAAIC,WAAW;QACf,MAAMC,WAAW,GAAG,IAAIL,OAAO,CAAEC,OAAO,IAAK;UAC3CG,WAAW,GAAGH,OAAO;QACvB,CAAC,CAAC;QACFP,QAAQ,CAACK,IAAI,CAACM,WAAW,CAAC;QAC1BjB,MAAM,CAACe,IAAI,CAAC,OAAO,EAAE,mBAAmB,EAAE;UACxC,GAAGR,WAAW;UACdW,cAAc,EAAEF;QAClB,CAAC,CAAC;MACJ;;MAEA;MACA,IAAIjB,QAAQ,CAACe,QAAQ,CAAC,KAAK,CAAC,IAAId,MAAM,CAACmB,SAAS,EAAE;QAChD,IAAIC,gBAAgB;QACpB,MAAMC,gBAAgB,GAAG,IAAIT,OAAO,CAAEC,OAAO,IAAK;UAChDO,gBAAgB,GAAGP,OAAO;QAC5B,CAAC,CAAC;QACFP,QAAQ,CAACK,IAAI,CAACU,gBAAgB,CAAC;QAC/BrB,MAAM,CAACmB,SAAS,CAACR,IAAI,CAAC;UACpBW,KAAK,EAAE,mBAAmB;UAC1B,GAAGf,WAAW;UACdgB,aAAa,EAAEH;QACjB,CAAC,CAAC;MACJ;;MAEA;MACA,IACErB,QAAQ,CAACe,QAAQ,CAAC,SAAS,CAAC,IAC5Bd,MAAM,CAACwB,SAAS,IAChBxB,MAAM,CAACwB,SAAS,CAACC,KAAK,EACtB;QACAzB,MAAM,CAACwB,SAAS,CAACC,KAAK,CAAC,mBAAmB,EAAElB,WAAW,CAAC;QACxD,MAAMmB,cAAc,GAAG,IAAId,OAAO,CAAEC,OAAO,IACzCb,MAAM,CAAC2B,UAAU,CAACd,OAAO,EAAE,GAAG,CAChC,CAAC;QACDP,QAAQ,CAACK,IAAI,CAACe,cAAc,CAAC;MAC/B;MAEA,MAAMd,OAAO,CAACgB,GAAG,CAACtB,QAAQ,CAAC;IAC7B,CAAC,CAAC;EACJ,CAAC;AACH","ignoreList":[]}

View File

@@ -0,0 +1,222 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.StickyBucketServiceSync = exports.StickyBucketService = exports.RedisStickyBucketService = exports.LocalStorageStickyBucketService = exports.ExpressCookieStickyBucketService = exports.BrowserCookieStickyBucketService = void 0;
var _util = require("./util");
var _core = require("./core");
/**
* Responsible for reading and writing documents which describe sticky bucket assignments.
*/
class StickyBucketService {
constructor(opts) {
opts = opts || {};
this.prefix = opts.prefix || "";
}
/**
* The SDK calls getAllAssignments to populate sticky buckets. This in turn will
* typically loop through individual getAssignments calls. However, some StickyBucketService
* instances (i.e. Redis) will instead perform a multi-query inside getAllAssignments instead.
*/
async getAllAssignments(attributes) {
const docs = {};
(await Promise.all(Object.entries(attributes).map(([attributeName, attributeValue]) => this.getAssignments(attributeName, attributeValue)))).forEach(doc => {
if (doc) {
const key = (0, _core.getStickyBucketAttributeKey)(doc.attributeName, doc.attributeValue);
docs[key] = doc;
}
});
return docs;
}
getKey(attributeName, attributeValue) {
return `${this.prefix}${attributeName}||${attributeValue}`;
}
}
exports.StickyBucketService = StickyBucketService;
class StickyBucketServiceSync extends StickyBucketService {
async getAssignments(attributeName, attributeValue) {
return this.getAssignmentsSync(attributeName, attributeValue);
}
async saveAssignments(doc) {
this.saveAssignmentsSync(doc);
}
getAllAssignmentsSync(attributes) {
const docs = {};
Object.entries(attributes).map(([attributeName, attributeValue]) => this.getAssignmentsSync(attributeName, attributeValue)).forEach(doc => {
if (doc) {
const key = (0, _core.getStickyBucketAttributeKey)(doc.attributeName, doc.attributeValue);
docs[key] = doc;
}
});
return docs;
}
}
exports.StickyBucketServiceSync = StickyBucketServiceSync;
class LocalStorageStickyBucketService extends StickyBucketService {
constructor(opts) {
opts = opts || {};
super();
this.prefix = opts.prefix || "gbStickyBuckets__";
try {
this.localStorage = opts.localStorage || globalThis.localStorage;
} catch (e) {
// Ignore localStorage errors
}
}
async getAssignments(attributeName, attributeValue) {
const key = this.getKey(attributeName, attributeValue);
let doc = null;
if (!this.localStorage) return doc;
try {
const raw = (await this.localStorage.getItem(key)) || "{}";
const data = JSON.parse(raw);
if (data.attributeName && data.attributeValue && data.assignments) {
doc = data;
}
} catch (e) {
// Ignore localStorage errors
}
return doc;
}
async saveAssignments(doc) {
const key = this.getKey(doc.attributeName, doc.attributeValue);
if (!this.localStorage) return;
try {
await this.localStorage.setItem(key, JSON.stringify(doc));
} catch (e) {
// Ignore localStorage errors
}
}
}
exports.LocalStorageStickyBucketService = LocalStorageStickyBucketService;
class ExpressCookieStickyBucketService extends StickyBucketServiceSync {
/**
* Intended to be used with cookieParser() middleware from npm: 'cookie-parser'.
* Assumes:
* - reading a cookie is automatically decoded via decodeURIComponent() or similar
* - writing a cookie name & value must be manually encoded via encodeURIComponent() or similar
* - all cookie bodies are JSON encoded strings and are manually encoded/decoded
*/
constructor({
prefix = "gbStickyBuckets__",
req,
res,
cookieAttributes = {
maxAge: 180 * 24 * 3600 * 1000
} // 180 days
}) {
super();
this.prefix = prefix;
this.req = req;
this.res = res;
this.cookieAttributes = cookieAttributes;
}
getAssignmentsSync(attributeName, attributeValue) {
const key = this.getKey(attributeName, attributeValue);
let doc = null;
if (!this.req) return doc;
try {
const raw = this.req.cookies[key] || "{}";
const data = JSON.parse(raw);
if (data.attributeName && data.attributeValue && data.assignments) {
doc = data;
}
} catch (e) {
// Ignore cookie errors
}
return doc;
}
saveAssignmentsSync(doc) {
const key = this.getKey(doc.attributeName, doc.attributeValue);
if (!this.res) return;
const str = JSON.stringify(doc);
this.res.cookie(encodeURIComponent(key), encodeURIComponent(str), this.cookieAttributes);
}
}
exports.ExpressCookieStickyBucketService = ExpressCookieStickyBucketService;
class BrowserCookieStickyBucketService extends StickyBucketServiceSync {
/**
* Intended to be used with npm: 'js-cookie'.
* Assumes:
* - reading a cookie is automatically decoded via decodeURIComponent() or similar
* - writing a cookie name & value is automatically encoded via encodeURIComponent() or similar
* - all cookie bodies are JSON encoded strings and are manually encoded/decoded
*/
constructor({
prefix = "gbStickyBuckets__",
jsCookie,
cookieAttributes = {
expires: 180
} // 180 days
}) {
super();
this.prefix = prefix;
this.jsCookie = jsCookie;
this.cookieAttributes = cookieAttributes;
}
getAssignmentsSync(attributeName, attributeValue) {
const key = this.getKey(attributeName, attributeValue);
let doc = null;
if (!this.jsCookie) return doc;
try {
const raw = this.jsCookie.get(key);
const data = JSON.parse(raw || "{}");
if (data.attributeName && data.attributeValue && data.assignments) {
doc = data;
}
} catch (e) {
// Ignore cookie errors
}
return doc;
}
async saveAssignmentsSync(doc) {
const key = this.getKey(doc.attributeName, doc.attributeValue);
if (!this.jsCookie) return;
const str = JSON.stringify(doc);
this.jsCookie.set(key, str, this.cookieAttributes);
}
}
exports.BrowserCookieStickyBucketService = BrowserCookieStickyBucketService;
class RedisStickyBucketService extends StickyBucketService {
/** Intended to be used with npm: 'ioredis'. **/
constructor({
redis
}) {
super();
this.redis = redis;
}
async getAllAssignments(attributes) {
const docs = {};
const keys = Object.entries(attributes).map(([attributeName, attributeValue]) => (0, _core.getStickyBucketAttributeKey)(attributeName, attributeValue));
if (!this.redis) return docs;
await this.redis.mget(...keys).then(values => {
values.forEach(raw => {
try {
const data = JSON.parse(raw || "{}");
if (data.attributeName && "attributeValue" in data && data.assignments) {
const key = (0, _core.getStickyBucketAttributeKey)(data.attributeName, (0, _util.toString)(data.attributeValue));
docs[key] = data;
}
} catch (e) {
// ignore redis doc parse errors
}
});
});
return docs;
}
async getAssignments(_attributeName, _attributeValue) {
// not implemented
return null;
}
async saveAssignments(doc) {
const key = this.getKey(doc.attributeName, doc.attributeValue);
if (!this.redis) return;
await this.redis.set(key, JSON.stringify(doc));
}
}
exports.RedisStickyBucketService = RedisStickyBucketService;
//# sourceMappingURL=sticky-bucket-service.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
//# sourceMappingURL=growthbook.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
//# sourceMappingURL=mongrule.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"mongrule.js","names":[],"sources":["../../../src/types/mongrule.ts"],"sourcesContent":["type OrCondition = {\n $or: ConditionInterface[];\n};\ntype NorCondition = {\n $nor: ConditionInterface[];\n};\ntype AndCondition = {\n $and: ConditionInterface[];\n};\ntype NotCondition = {\n $not: ConditionInterface;\n};\nexport type Operator =\n | \"$in\"\n | \"$ini\"\n | \"$inGroup\"\n | \"$nin\"\n | \"$nini\"\n | \"$notInGroup\"\n | \"$gt\"\n | \"$gte\"\n | \"$lt\"\n | \"$lte\"\n | \"$regex\"\n | \"$regexi\"\n | \"$ne\"\n | \"$eq\"\n | \"$size\"\n | \"$elemMatch\"\n | \"$all\"\n | \"$alli\"\n | \"$not\"\n | \"$type\"\n | \"$exists\"\n | \"$vgt\"\n | \"$vgte\"\n | \"$vlt\"\n | \"$vlte\"\n | \"$vne\"\n | \"$veq\";\nexport type VarType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"array\"\n | \"object\"\n | \"null\"\n | \"undefined\";\nexport type OperatorConditionValue = {\n $in?: (string | number)[];\n $ini?: (string | number)[];\n $inGroup?: string;\n $nin?: (string | number)[];\n $nini?: (string | number)[];\n $notInGroup?: string;\n $gt?: number | string;\n $gte?: number | string;\n $lt?: number | string;\n $lte?: number | string;\n $regex?: string;\n $regexi?: string;\n $ne?: number | string;\n $eq?: number | string;\n $exists?: boolean;\n $all?: ConditionValue[];\n $alli?: ConditionValue[];\n $size?: number | ConditionValue;\n $type?: VarType;\n $elemMatch?: ConditionInterface | OperatorConditionValue;\n $not?: ConditionValue;\n};\n\nexport type ConditionValue =\n | OperatorConditionValue\n | string\n | number\n | boolean\n // eslint-disable-next-line\n | Array<any>\n // eslint-disable-next-line\n | Record<string, any>\n | null;\n\nexport type OperatorCondition = {\n [key: string]: ConditionValue;\n};\n\nexport type ConditionInterface =\n | OrCondition\n | NorCondition\n | AndCondition\n | NotCondition\n | OperatorCondition;\n\nexport type ParentConditionInterface = {\n id: string;\n condition: ConditionInterface;\n gate?: boolean;\n};\n\n// eslint-disable-next-line\nexport type TestedObj = Record<string, any>;\n"],"mappings":"","ignoreList":[]}

View File

@@ -0,0 +1,329 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.chooseVariation = chooseVariation;
exports.decrypt = decrypt;
exports.getAutoExperimentChangeType = getAutoExperimentChangeType;
exports.getBucketRanges = getBucketRanges;
exports.getEqualWeights = getEqualWeights;
exports.getPolyfills = getPolyfills;
exports.getQueryStringOverride = getQueryStringOverride;
exports.getUrlRegExp = getUrlRegExp;
exports.hash = hash;
exports.inNamespace = inNamespace;
exports.inRange = inRange;
exports.isIncluded = isIncluded;
exports.isURLTargeted = isURLTargeted;
exports.loadSDKVersion = loadSDKVersion;
exports.mergeQueryStrings = mergeQueryStrings;
exports.paddedVersionString = paddedVersionString;
exports.promiseTimeout = promiseTimeout;
exports.toString = toString;
const polyfills = {
fetch: globalThis.fetch ? globalThis.fetch.bind(globalThis) : undefined,
SubtleCrypto: globalThis.crypto ? globalThis.crypto.subtle : undefined,
EventSource: globalThis.EventSource
};
function getPolyfills() {
return polyfills;
}
function hashFnv32a(str) {
let hval = 0x811c9dc5;
const l = str.length;
for (let i = 0; i < l; i++) {
hval ^= str.charCodeAt(i);
hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
}
return hval >>> 0;
}
function hash(seed, value, version) {
// New unbiased hashing algorithm
if (version === 2) {
return hashFnv32a(hashFnv32a(seed + value) + "") % 10000 / 10000;
}
// Original biased hashing algorithm (keep for backwards compatibility)
if (version === 1) {
return hashFnv32a(value + seed) % 1000 / 1000;
}
// Unknown hash version
return null;
}
function getEqualWeights(n) {
if (n <= 0) return [];
return new Array(n).fill(1 / n);
}
function inRange(n, range) {
return n >= range[0] && n < range[1];
}
function inNamespace(hashValue, namespace) {
const n = hash("__" + namespace[0], hashValue, 1);
if (n === null) return false;
return n >= namespace[1] && n < namespace[2];
}
function chooseVariation(n, ranges) {
for (let i = 0; i < ranges.length; i++) {
if (inRange(n, ranges[i])) {
return i;
}
}
return -1;
}
function getUrlRegExp(regexString) {
try {
const escaped = regexString.replace(/([^\\])\//g, "$1\\/");
return new RegExp(escaped);
} catch (e) {
console.error(e);
return undefined;
}
}
function isURLTargeted(url, targets) {
if (!targets.length) return false;
let hasIncludeRules = false;
let isIncluded = false;
for (let i = 0; i < targets.length; i++) {
const match = _evalURLTarget(url, targets[i].type, targets[i].pattern);
if (targets[i].include === false) {
if (match) return false;
} else {
hasIncludeRules = true;
if (match) isIncluded = true;
}
}
return isIncluded || !hasIncludeRules;
}
function _evalSimpleUrlPart(actual, pattern, isPath) {
try {
// Escape special regex characters and change wildcard `_____` to `.*`
let escaped = pattern.replace(/[*.+?^${}()|[\]\\]/g, "\\$&").replace(/_____/g, ".*");
if (isPath) {
// When matching pathname, make leading/trailing slashes optional
escaped = "\\/?" + escaped.replace(/(^\/|\/$)/g, "") + "\\/?";
}
const regex = new RegExp("^" + escaped + "$", "i");
return regex.test(actual);
} catch (e) {
return false;
}
}
function _evalSimpleUrlTarget(actual, pattern) {
try {
// If a protocol is missing, but a host is specified, add `https://` to the front
// Use "_____" as the wildcard since `*` is not a valid hostname in some browsers
const expected = new URL(pattern.replace(/^([^:/?]*)\./i, "https://$1.").replace(/\*/g, "_____"), "https://_____");
// Compare each part of the URL separately
const comps = [[actual.host, expected.host, false], [actual.pathname, expected.pathname, true]];
// We only want to compare hashes if it's explicitly being targeted
if (expected.hash) {
comps.push([actual.hash, expected.hash, false]);
}
expected.searchParams.forEach((v, k) => {
comps.push([actual.searchParams.get(k) || "", v, false]);
});
// If any comparisons fail, the whole thing fails
return !comps.some(data => !_evalSimpleUrlPart(data[0], data[1], data[2]));
} catch (e) {
return false;
}
}
function _evalURLTarget(url, type, pattern) {
try {
const parsed = new URL(url, "https://_");
if (type === "regex") {
const regex = getUrlRegExp(pattern);
if (!regex) return false;
return regex.test(parsed.href) || regex.test(parsed.href.substring(parsed.origin.length));
} else if (type === "simple") {
return _evalSimpleUrlTarget(parsed, pattern);
}
return false;
} catch (e) {
return false;
}
}
function getBucketRanges(numVariations, coverage, weights) {
coverage = coverage === undefined ? 1 : coverage;
// Make sure coverage is within bounds
if (coverage < 0) {
if (process.env.NODE_ENV !== "production") {
console.error("Experiment.coverage must be greater than or equal to 0");
}
coverage = 0;
} else if (coverage > 1) {
if (process.env.NODE_ENV !== "production") {
console.error("Experiment.coverage must be less than or equal to 1");
}
coverage = 1;
}
// Default to equal weights if missing or invalid
const equal = getEqualWeights(numVariations);
weights = weights || equal;
if (weights.length !== numVariations) {
if (process.env.NODE_ENV !== "production") {
console.error("Experiment.weights array must be the same length as Experiment.variations");
}
weights = equal;
}
// If weights don't add up to 1 (or close to it), default to equal weights
const totalWeight = weights.reduce((w, sum) => sum + w, 0);
if (totalWeight < 0.99 || totalWeight > 1.01) {
if (process.env.NODE_ENV !== "production") {
console.error("Experiment.weights must add up to 1");
}
weights = equal;
}
// Covert weights to ranges
let cumulative = 0;
return weights.map(w => {
const start = cumulative;
cumulative += w;
return [start, start + coverage * w];
});
}
function getQueryStringOverride(id, url, numVariations) {
if (!url) {
return null;
}
const search = url.split("?")[1];
if (!search) {
return null;
}
const match = search.replace(/#.*/, "") // Get rid of anchor
.split("&") // Split into key/value pairs
.map(kv => kv.split("=", 2)).filter(([k]) => k === id) // Look for key that matches the experiment id
.map(([, v]) => parseInt(v)); // Parse the value into an integer
if (match.length > 0 && match[0] >= 0 && match[0] < numVariations) return match[0];
return null;
}
function isIncluded(include) {
try {
return include();
} catch (e) {
console.error(e);
return false;
}
}
const base64ToBuf = b => Uint8Array.from(atob(b), c => c.charCodeAt(0));
async function decrypt(encryptedString, decryptionKey, subtle) {
decryptionKey = decryptionKey || "";
subtle = subtle || globalThis.crypto && globalThis.crypto.subtle || polyfills.SubtleCrypto;
if (!subtle) {
throw new Error("No SubtleCrypto implementation found");
}
try {
const key = await subtle.importKey("raw", base64ToBuf(decryptionKey), {
name: "AES-CBC",
length: 128
}, true, ["encrypt", "decrypt"]);
const [iv, cipherText] = encryptedString.split(".");
const plainTextBuffer = await subtle.decrypt({
name: "AES-CBC",
iv: base64ToBuf(iv)
}, key, base64ToBuf(cipherText));
return new TextDecoder().decode(plainTextBuffer);
} catch (e) {
throw new Error("Failed to decrypt");
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function toString(input) {
if (typeof input === "string") return input;
return JSON.stringify(input);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function paddedVersionString(input) {
if (typeof input === "number") {
input = input + "";
}
if (!input || typeof input !== "string") {
input = "0";
}
// Remove build info and leading `v` if any
// Split version into parts (both core version numbers and pre-release tags)
// "v1.2.3-rc.1+build123" -> ["1","2","3","rc","1"]
const parts = input.replace(/(^v|\+.*$)/g, "").split(/[-.]/);
// If it's SemVer without a pre-release, add `~` to the end
// ["1","0","0"] -> ["1","0","0","~"]
// "~" is the largest ASCII character, so this will make "1.0.0" greater than "1.0.0-beta" for example
if (parts.length === 3) {
parts.push("~");
}
// Left pad each numeric part with spaces so string comparisons will work ("9">"10", but " 9"<"10")
// Then, join back together into a single string
return parts.map(v => v.match(/^[0-9]+$/) ? v.padStart(5, " ") : v).join("-");
}
function loadSDKVersion() {
let version;
try {
// @ts-expect-error right-hand value to be replaced by build with string literal
version = "1.6.5";
} catch (e) {
version = "";
}
return version;
}
function mergeQueryStrings(oldUrl, newUrl) {
let currUrl;
let redirectUrl;
try {
currUrl = new URL(oldUrl);
redirectUrl = new URL(newUrl);
} catch (e) {
console.error(`Unable to merge query strings: ${e}`);
return newUrl;
}
currUrl.searchParams.forEach((value, key) => {
// skip if search param already exists in redirectUrl
if (redirectUrl.searchParams.has(key)) {
return;
}
redirectUrl.searchParams.set(key, value);
});
return redirectUrl.toString();
}
function isObj(x) {
return typeof x === "object" && x !== null;
}
function getAutoExperimentChangeType(exp) {
if (exp.urlPatterns && exp.variations.some(variation => isObj(variation) && "urlRedirect" in variation)) {
return "redirect";
} else if (exp.variations.some(variation => isObj(variation) && (variation.domMutations || "js" in variation || "css" in variation))) {
return "visual";
}
return "unknown";
}
// Guarantee the promise always resolves within {timeout} ms
// Resolved value will be `null` when there's an error or it takes too long
// Note: The promise will continue running in the background, even if the timeout is hit
async function promiseTimeout(promise, timeout) {
return new Promise(resolve => {
let resolved = false;
let timer;
const finish = data => {
if (resolved) return;
resolved = true;
timer && clearTimeout(timer);
resolve(data || null);
};
if (timeout) {
timer = setTimeout(() => finish(), timeout);
}
promise.then(data => finish(data)).catch(() => finish());
});
}
//# sourceMappingURL=util.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,26 @@
import { EvalContext, FeatureResult, Experiment, Result, StickyAttributeKey, StickyAssignmentsDocument, FeatureApiResponse, Options, ClientOptions } from "./types/growthbook";
import { StickyBucketService } from "./sticky-bucket-service";
export declare const EVENT_FEATURE_EVALUATED = "Feature Evaluated";
export declare const EVENT_EXPERIMENT_VIEWED = "Experiment Viewed";
export declare function evalFeature<V = unknown>(id: string, ctx: EvalContext): FeatureResult<V | null>;
export declare function runExperiment<T>(experiment: Experiment<T>, featureId: string | null, ctx: EvalContext): {
result: Result<T>;
trackingCall?: Promise<void>;
};
export declare function getExperimentResult<T>(ctx: EvalContext, experiment: Experiment<T>, variationIndex: number, hashUsed: boolean, featureId: string | null, bucket?: number, stickyBucketUsed?: boolean): Result<T>;
export declare function getHashAttribute(ctx: EvalContext, attr?: string, fallback?: string): {
hashAttribute: string;
hashValue: any;
};
export declare function getStickyBucketAttributeKey(attributeName: string, attributeValue: string): StickyAttributeKey;
export declare function getAllStickyBucketAssignmentDocs(ctx: EvalContext, stickyBucketService: StickyBucketService, data?: FeatureApiResponse): Promise<Record<string, StickyAssignmentsDocument>>;
export declare function getStickyBucketAttributes(ctx: EvalContext, data?: FeatureApiResponse): Record<string, string>;
export declare function decryptPayload(data: FeatureApiResponse, decryptionKey: string | undefined, subtle?: SubtleCrypto): Promise<FeatureApiResponse>;
export declare function getApiHosts(options: Options | ClientOptions): {
apiHost: string;
streamingHost: string;
apiRequestHeaders?: Record<string, string>;
streamingHostRequestHeaders?: Record<string, string>;
};
export declare function getExperimentDedupeKey(experiment: Experiment<unknown>, result: Result<unknown>): string;
//# sourceMappingURL=core.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EAEX,aAAa,EACb,UAAU,EAEV,MAAM,EAMN,kBAAkB,EAClB,yBAAyB,EACzB,kBAAkB,EAClB,OAAO,EACP,aAAa,EACd,MAAM,oBAAoB,CAAC;AAgB5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAE9D,eAAO,MAAM,uBAAuB,sBAAsB,CAAC;AAC3D,eAAO,MAAM,uBAAuB,sBAAsB,CAAC;AAuI3D,wBAAgB,WAAW,CAAC,CAAC,GAAG,OAAO,EACrC,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,WAAW,GACf,aAAa,CAAC,CAAC,GAAG,IAAI,CAAC,CAgNzB;AAED,wBAAgB,aAAa,CAAC,CAAC,EAC7B,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,EACzB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,GAAG,EAAE,WAAW,GACf;IACD,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IAClB,YAAY,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAyYA;AAqFD,wBAAgB,mBAAmB,CAAC,CAAC,EACnC,GAAG,EAAE,WAAW,EAChB,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,EACzB,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,OAAO,EACjB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,MAAM,CAAC,EAAE,MAAM,EACf,gBAAgB,CAAC,EAAE,OAAO,GACzB,MAAM,CAAC,CAAC,CAAC,CAqCX;AAqBD,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,IAAI,CAAC,EAAE,MAAM,EACb,QAAQ,CAAC,EAAE,MAAM;;;EAuBlB;AAoFD,wBAAgB,2BAA2B,CACzC,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,GACrB,kBAAkB,CAEpB;AAkGD,wBAAsB,gCAAgC,CACpD,GAAG,EAAE,WAAW,EAChB,mBAAmB,EAAE,mBAAmB,EACxC,IAAI,CAAC,EAAE,kBAAkB,sDAI1B;AAED,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,WAAW,EAChB,IAAI,CAAC,EAAE,kBAAkB,GACxB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASxB;AAED,wBAAsB,cAAc,CAClC,IAAI,EAAE,kBAAkB,EACxB,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,MAAM,CAAC,EAAE,YAAY,GACpB,OAAO,CAAC,kBAAkB,CAAC,CAiC7B;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,GAAG;IAC7D,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C,2BAA2B,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtD,CAQA;AAED,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,UAAU,CAAC,OAAO,CAAC,EAC/B,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,UAQxB"}

View File

@@ -0,0 +1,841 @@
import mutate from "dom-mutator";
import { decrypt, getAutoExperimentChangeType, isURLTargeted, loadSDKVersion, mergeQueryStrings, promiseTimeout } from "./util.mjs";
import { clearAutoRefresh, configureCache, refreshFeatures, startStreaming, unsubscribe } from "./feature-repository.mjs";
import { runExperiment, evalFeature as _evalFeature, getExperimentResult, getAllStickyBucketAssignmentDocs, decryptPayload, getApiHosts, getExperimentDedupeKey, getStickyBucketAttributes } from "./core.mjs";
const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
const SDK_VERSION = loadSDKVersion();
export class GrowthBook {
// context is technically private, but some tools depend on it so we can't mangle the name
// Properties and methods that start with "_" are mangled by Terser (saves ~150 bytes)
constructor(options) {
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 = this.context = options;
this._renderer = options.renderer || null;
this._trackedExperiments = new Set();
this._completedChangeIds = new Set();
this._trackedFeatures = {};
this.debug = !!options.debug;
this._subscriptions = new Set();
this.ready = false;
this._assigned = new Map();
this._activeAutoExperiments = new Map();
this._triggeredExpKeys = new Set();
this._initialized = false;
this._redirectedUrl = "";
this._deferredTrackingCalls = new Map();
this._autoExperimentsAllowed = !options.disableExperimentsOnLoad;
this._destroyCallbacks = [];
this.logs = [];
this.log = this.log.bind(this);
this._saveDeferredTrack = this._saveDeferredTrack.bind(this);
this._onExperimentEval = this._onExperimentEval.bind(this);
this._fireSubscriptions = this._fireSubscriptions.bind(this);
this._recordChangedId = this._recordChangedId.bind(this);
if (options.remoteEval) {
if (options.decryptionKey) {
throw new Error("Encryption is not available for remoteEval");
}
if (!options.clientKey) {
throw new Error("Missing clientKey");
}
let isGbHost = false;
try {
isGbHost = !!new URL(options.apiHost || "").hostname.match(/growthbook\.io$/i);
} catch (e) {
// ignore invalid URLs
}
if (isGbHost) {
throw new Error("Cannot use remoteEval on GrowthBook Cloud");
}
} else {
if (options.cacheKeyAttributes) {
throw new Error("cacheKeyAttributes are only used for remoteEval");
}
}
if (options.stickyBucketService) {
const s = options.stickyBucketService;
this._saveStickyBucketAssignmentDoc = doc => {
return s.saveAssignments(doc);
};
}
if (options.plugins) {
for (const plugin of options.plugins) {
plugin(this);
}
}
if (options.features) {
this.ready = true;
}
if (isBrowser && options.enableDevMode) {
window._growthbook = this;
document.dispatchEvent(new Event("gbloaded"));
}
if (options.experiments) {
this.ready = true;
this._updateAllAutoExperiments();
}
// Hydrate sticky bucket service
if (this._options.stickyBucketService && this._options.stickyBucketAssignmentDocs) {
for (const key in this._options.stickyBucketAssignmentDocs) {
const doc = this._options.stickyBucketAssignmentDocs[key];
if (doc) {
this._options.stickyBucketService.saveAssignments(doc).catch(() => {
// Ignore hydration errors
});
}
}
}
// Legacy - passing in features/experiments into the constructor instead of using init
if (this.ready) {
this.refreshStickyBuckets(this.getPayload());
}
}
async setPayload(payload) {
this._payload = payload;
const data = await decryptPayload(payload, this._options.decryptionKey);
this._decryptedPayload = data;
await this.refreshStickyBuckets(data);
if (data.features) {
this._options.features = data.features;
}
if (data.savedGroups) {
this._options.savedGroups = data.savedGroups;
}
if (data.experiments) {
this._options.experiments = data.experiments;
this._updateAllAutoExperiments();
}
this.ready = true;
this._render();
}
initSync(options) {
this._initialized = true;
const payload = options.payload;
if (payload.encryptedExperiments || payload.encryptedFeatures) {
throw new Error("initSync does not support encrypted payloads");
}
if (this._options.stickyBucketService && !this._options.stickyBucketAssignmentDocs) {
this._options.stickyBucketAssignmentDocs = this.generateStickyBucketAssignmentDocsSync(this._options.stickyBucketService, payload);
}
this._payload = payload;
this._decryptedPayload = payload;
if (payload.features) {
this._options.features = payload.features;
}
if (payload.experiments) {
this._options.experiments = payload.experiments;
this._updateAllAutoExperiments();
}
this.ready = true;
startStreaming(this, options);
return this;
}
async init(options) {
this._initialized = true;
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;
}
}
/** @deprecated Use {@link init} */
async loadFeatures(options) {
options = options || {};
await this.init({
skipCache: options.skipCache,
timeout: options.timeout,
streaming: (this._options.backgroundSync ?? true) && (options.autoRefresh || this._options.subscribeToChanges)
});
}
async refreshFeatures(options) {
const res = await this._refresh({
...(options || {}),
allowStale: false
});
if (res.data) {
await this.setPayload(res.data);
}
}
getApiInfo() {
return [this.getApiHosts().apiHost, this.getClientKey()];
}
getApiHosts() {
return getApiHosts(this._options);
}
getClientKey() {
return this._options.clientKey || "";
}
getPayload() {
return this._payload || {
features: this.getFeatures(),
experiments: this.getExperiments()
};
}
getDecryptedPayload() {
return this._decryptedPayload || this.getPayload();
}
isRemoteEval() {
return this._options.remoteEval || false;
}
getCacheKeyAttributes() {
return this._options.cacheKeyAttributes;
}
async _refresh({
timeout,
skipCache,
allowStale,
streaming
}) {
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 ?? this._options.backgroundSync ?? true
});
}
_render() {
if (this._renderer) {
try {
this._renderer();
} catch (e) {
console.error("Failed to render", e);
}
}
}
/** @deprecated Use {@link setPayload} */
setFeatures(features) {
this._options.features = features;
this.ready = true;
this._render();
}
/** @deprecated Use {@link setPayload} */
async setEncryptedFeatures(encryptedString, decryptionKey, subtle) {
const featuresJSON = await decrypt(encryptedString, decryptionKey || this._options.decryptionKey, subtle);
this.setFeatures(JSON.parse(featuresJSON));
}
/** @deprecated Use {@link setPayload} */
setExperiments(experiments) {
this._options.experiments = experiments;
this.ready = true;
this._updateAllAutoExperiments();
}
/** @deprecated Use {@link setPayload} */
async setEncryptedExperiments(encryptedString, decryptionKey, subtle) {
const experimentsJSON = await decrypt(encryptedString, decryptionKey || this._options.decryptionKey, subtle);
this.setExperiments(JSON.parse(experimentsJSON));
}
async setAttributes(attributes) {
this._options.attributes = attributes;
if (this._options.stickyBucketService) {
await this.refreshStickyBuckets();
}
if (this._options.remoteEval) {
await this._refreshForRemoteEval();
return;
}
this._render();
this._updateAllAutoExperiments();
}
async updateAttributes(attributes) {
return this.setAttributes({
...this._options.attributes,
...attributes
});
}
async setAttributeOverrides(overrides) {
this._options.attributeOverrides = overrides;
if (this._options.stickyBucketService) {
await this.refreshStickyBuckets();
}
if (this._options.remoteEval) {
await this._refreshForRemoteEval();
return;
}
this._render();
this._updateAllAutoExperiments();
}
async setForcedVariations(vars) {
this._options.forcedVariations = vars || {};
if (this._options.remoteEval) {
await this._refreshForRemoteEval();
return;
}
this._render();
this._updateAllAutoExperiments();
}
// eslint-disable-next-line
setForcedFeatures(map) {
this._options.forcedFeatureValues = map;
this._render();
}
async setURL(url) {
if (url === this._options.url) return;
this._options.url = url;
this._redirectedUrl = "";
if (this._options.remoteEval) {
await this._refreshForRemoteEval();
this._updateAllAutoExperiments(true);
return;
}
this._updateAllAutoExperiments(true);
}
getAttributes() {
return {
...this._options.attributes,
...this._options.attributeOverrides
};
}
getForcedVariations() {
return this._options.forcedVariations || {};
}
getForcedFeatures() {
// eslint-disable-next-line
return this._options.forcedFeatureValues || new Map();
}
getStickyBucketAssignmentDocs() {
return this._options.stickyBucketAssignmentDocs || {};
}
getUrl() {
return this._options.url || "";
}
getFeatures() {
return this._options.features || {};
}
getExperiments() {
return this._options.experiments || [];
}
getCompletedChangeIds() {
return Array.from(this._completedChangeIds);
}
subscribe(cb) {
this._subscriptions.add(cb);
return () => {
this._subscriptions.delete(cb);
};
}
async _refreshForRemoteEval() {
if (!this._options.remoteEval) return;
if (!this._initialized) return;
const res = await this._refresh({
allowStale: false
});
if (res.data) {
await this.setPayload(res.data);
}
}
getAllResults() {
return new Map(this._assigned);
}
onDestroy(cb) {
this._destroyCallbacks.push(cb);
}
isDestroyed() {
return !!this._destroyed;
}
destroy(options) {
options = options || {};
this._destroyed = true;
// Custom callbacks
// Do this first in case it needs access to the below data that is cleared
this._destroyCallbacks.forEach(cb => {
try {
cb();
} catch (e) {
console.error(e);
}
});
// Release references to save memory
this._subscriptions.clear();
this._assigned.clear();
this._trackedExperiments.clear();
this._completedChangeIds.clear();
this._deferredTrackingCalls.clear();
this._trackedFeatures = {};
this._destroyCallbacks = [];
this._payload = undefined;
this._saveStickyBucketAssignmentDoc = undefined;
unsubscribe(this);
if (options.destroyAllStreams) {
clearAutoRefresh();
}
this.logs = [];
if (isBrowser && window._growthbook === this) {
delete window._growthbook;
}
// Undo any active auto experiments
this._activeAutoExperiments.forEach(exp => {
exp.undo();
});
this._activeAutoExperiments.clear();
this._triggeredExpKeys.clear();
}
setRenderer(renderer) {
this._renderer = renderer;
}
forceVariation(key, variation) {
this._options.forcedVariations = this._options.forcedVariations || {};
this._options.forcedVariations[key] = variation;
if (this._options.remoteEval) {
this._refreshForRemoteEval();
return;
}
this._updateAllAutoExperiments();
this._render();
}
run(experiment) {
const {
result
} = runExperiment(experiment, null, this._getEvalContext());
this._onExperimentEval(experiment, result);
return result;
}
triggerExperiment(key) {
this._triggeredExpKeys.add(key);
if (!this._options.experiments) return null;
const experiments = this._options.experiments.filter(exp => exp.key === key);
return experiments.map(exp => {
return this._runAutoExperiment(exp);
}).filter(res => res !== null);
}
triggerAutoExperiments() {
this._autoExperimentsAllowed = true;
this._updateAllAutoExperiments(true);
}
_getEvalContext() {
return {
user: this._getUserContext(),
global: this._getGlobalContext(),
stack: {
evaluatedFeatures: new Set()
}
};
}
_getUserContext() {
return {
attributes: this._options.user ? {
...this._options.user,
...this._options.attributes
} : this._options.attributes,
enableDevMode: this._options.enableDevMode,
blockedChangeIds: this._options.blockedChangeIds,
stickyBucketAssignmentDocs: this._options.stickyBucketAssignmentDocs,
url: this._getContextUrl(),
forcedVariations: this._options.forcedVariations,
forcedFeatureValues: this._options.forcedFeatureValues,
attributeOverrides: this._options.attributeOverrides,
saveStickyBucketAssignmentDoc: this._saveStickyBucketAssignmentDoc,
trackingCallback: this._options.trackingCallback,
onFeatureUsage: this._options.onFeatureUsage,
devLogs: this.logs,
trackedExperiments: this._trackedExperiments,
trackedFeatureUsage: this._trackedFeatures
};
}
_getGlobalContext() {
return {
features: this._options.features,
experiments: this._options.experiments,
log: this.log,
enabled: this._options.enabled,
qaMode: this._options.qaMode,
savedGroups: this._options.savedGroups,
groups: this._options.groups,
overrides: this._options.overrides,
onExperimentEval: this._onExperimentEval,
recordChangeId: this._recordChangedId,
saveDeferredTrack: this._saveDeferredTrack,
eventLogger: this._options.eventLogger
};
}
_runAutoExperiment(experiment, forceRerun) {
const existing = this._activeAutoExperiments.get(experiment);
// If this is a manual experiment and it's not already running, skip
if (experiment.manual && !this._triggeredExpKeys.has(experiment.key) && !existing) return null;
// Check if this particular experiment is blocked by options settings
// For example, if all visualEditor experiments are disabled
const isBlocked = this._isAutoExperimentBlockedByContext(experiment);
if (isBlocked) {
process.env.NODE_ENV !== "production" && this.log("Auto experiment blocked", {
id: experiment.key
});
}
let result;
let trackingCall;
// Run the experiment (if blocked exclude)
if (isBlocked) {
result = getExperimentResult(this._getEvalContext(), experiment, -1, false, "");
} else {
({
result,
trackingCall
} = runExperiment(experiment, null, this._getEvalContext()));
this._onExperimentEval(experiment, result);
}
// A hash to quickly tell if the assigned value changed
const valueHash = JSON.stringify(result.value);
// If the changes are already active, no need to re-apply them
if (!forceRerun && result.inExperiment && existing && existing.valueHash === valueHash) {
return result;
}
// Undo any existing changes
if (existing) this._undoActiveAutoExperiment(experiment);
// Apply new changes
if (result.inExperiment) {
const changeType = getAutoExperimentChangeType(experiment);
if (changeType === "redirect" && result.value.urlRedirect && experiment.urlPatterns) {
const url = experiment.persistQueryString ? mergeQueryStrings(this._getContextUrl(), result.value.urlRedirect) : result.value.urlRedirect;
if (isURLTargeted(url, experiment.urlPatterns)) {
this.log("Skipping redirect because original URL matches redirect URL", {
id: experiment.key
});
return result;
}
this._redirectedUrl = url;
const {
navigate,
delay
} = this._getNavigateFunction();
if (navigate) {
if (isBrowser) {
// Wait for the possibly-async tracking callback, bound by min and max delays
Promise.all([...(trackingCall ? [promiseTimeout(trackingCall, this._options.maxNavigateDelay ?? 1000)] : []), new Promise(resolve => window.setTimeout(resolve, this._options.navigateDelay ?? delay))]).then(() => {
try {
navigate(url);
} catch (e) {
console.error(e);
}
});
} else {
try {
navigate(url);
} catch (e) {
console.error(e);
}
}
}
} else if (changeType === "visual") {
const undo = this._options.applyDomChangesCallback ? this._options.applyDomChangesCallback(result.value) : this._applyDOMChanges(result.value);
if (undo) {
this._activeAutoExperiments.set(experiment, {
undo,
valueHash
});
}
}
}
return result;
}
_undoActiveAutoExperiment(exp) {
const data = this._activeAutoExperiments.get(exp);
if (data) {
data.undo();
this._activeAutoExperiments.delete(exp);
}
}
_updateAllAutoExperiments(forceRerun) {
if (!this._autoExperimentsAllowed) return;
const experiments = this._options.experiments || [];
// Stop any experiments that are no longer defined
const keys = new Set(experiments);
this._activeAutoExperiments.forEach((v, k) => {
if (!keys.has(k)) {
v.undo();
this._activeAutoExperiments.delete(k);
}
});
// Re-run all new/updated experiments
for (const exp of experiments) {
const result = this._runAutoExperiment(exp, forceRerun);
// Once you're in a redirect experiment, break out of the loop and don't run any further experiments
if (result && result.inExperiment && getAutoExperimentChangeType(exp) === "redirect") {
break;
}
}
}
_onExperimentEval(experiment, result) {
const prev = this._assigned.get(experiment.key);
this._assigned.set(experiment.key, {
experiment,
result
});
if (this._subscriptions.size > 0) {
this._fireSubscriptions(experiment, result, prev);
}
}
_fireSubscriptions(experiment, result,
// eslint-disable-next-line
prev) {
// If assigned variation has changed, fire subscriptions
// TODO: what if the experiment definition has changed?
if (!prev || prev.result.inExperiment !== result.inExperiment || prev.result.variationId !== result.variationId) {
this._subscriptions.forEach(cb => {
try {
cb(experiment, result);
} catch (e) {
console.error(e);
}
});
}
}
_recordChangedId(id) {
this._completedChangeIds.add(id);
}
isOn(key) {
return this.evalFeature(key).on;
}
isOff(key) {
return this.evalFeature(key).off;
}
getFeatureValue(key, defaultValue) {
const value = this.evalFeature(key).value;
return value === null ? defaultValue : value;
}
/**
* @deprecated Use {@link evalFeature}
* @param id
*/
feature(id) {
return this.evalFeature(id);
}
evalFeature(id) {
return _evalFeature(id, this._getEvalContext());
}
log(msg, ctx) {
if (!this.debug) return;
if (this._options.log) this._options.log(msg, ctx);else console.log(msg, ctx);
}
getDeferredTrackingCalls() {
return Array.from(this._deferredTrackingCalls.values());
}
setDeferredTrackingCalls(calls) {
this._deferredTrackingCalls = new Map(calls.filter(c => c && c.experiment && c.result).map(c => {
return [getExperimentDedupeKey(c.experiment, c.result), c];
}));
}
async fireDeferredTrackingCalls() {
if (!this._options.trackingCallback) return;
const promises = [];
this._deferredTrackingCalls.forEach(call => {
if (!call || !call.experiment || !call.result) {
console.error("Invalid deferred tracking call", {
call: call
});
} else {
promises.push(this._options.trackingCallback(call.experiment, call.result));
}
});
this._deferredTrackingCalls.clear();
await Promise.all(promises);
}
setTrackingCallback(callback) {
this._options.trackingCallback = callback;
this.fireDeferredTrackingCalls();
}
setFeatureUsageCallback(callback) {
this._options.onFeatureUsage = callback;
}
setEventLogger(logger) {
this._options.eventLogger = logger;
}
async logEvent(eventName, properties) {
if (this._destroyed) {
console.error("Cannot log event to destroyed GrowthBook instance");
return;
}
if (this._options.enableDevMode) {
this.logs.push({
eventName,
properties,
timestamp: Date.now().toString(),
logType: "event"
});
}
if (this._options.eventLogger) {
try {
await this._options.eventLogger(eventName, properties || {}, this._getUserContext());
} catch (e) {
console.error(e);
}
} else {
console.error("No event logger configured");
}
}
_saveDeferredTrack(data) {
this._deferredTrackingCalls.set(getExperimentDedupeKey(data.experiment, data.result), data);
}
_getContextUrl() {
return this._options.url || (isBrowser ? window.location.href : "");
}
_isAutoExperimentBlockedByContext(experiment) {
const changeType = getAutoExperimentChangeType(experiment);
if (changeType === "visual") {
if (this._options.disableVisualExperiments) return true;
if (this._options.disableJsInjection) {
if (experiment.variations.some(v => v.js)) {
return true;
}
}
} else if (changeType === "redirect") {
if (this._options.disableUrlRedirectExperiments) return true;
// Validate URLs
try {
const current = new URL(this._getContextUrl());
for (const v of experiment.variations) {
if (!v || !v.urlRedirect) continue;
const url = new URL(v.urlRedirect);
// If we're blocking cross origin redirects, block if the protocol or host is different
if (this._options.disableCrossOriginUrlRedirectExperiments) {
if (url.protocol !== current.protocol) return true;
if (url.host !== current.host) return true;
}
}
} catch (e) {
// Problem parsing one of the URLs
this.log("Error parsing current or redirect URL", {
id: experiment.key,
error: e
});
return true;
}
} else {
// Block any unknown changeTypes
return true;
}
if (experiment.changeId && (this._options.blockedChangeIds || []).includes(experiment.changeId)) {
return true;
}
return false;
}
getRedirectUrl() {
return this._redirectedUrl;
}
_getNavigateFunction() {
if (this._options.navigate) {
return {
navigate: this._options.navigate,
delay: 0
};
} else if (isBrowser) {
return {
navigate: url => {
window.location.replace(url);
},
delay: 100
};
}
return {
navigate: null,
delay: 0
};
}
_applyDOMChanges(changes) {
if (!isBrowser) return;
const undo = [];
if (changes.css) {
const s = document.createElement("style");
s.innerHTML = changes.css;
document.head.appendChild(s);
undo.push(() => s.remove());
}
if (changes.js) {
const script = document.createElement("script");
script.innerHTML = changes.js;
if (this._options.jsInjectionNonce) {
script.nonce = this._options.jsInjectionNonce;
}
document.head.appendChild(script);
undo.push(() => script.remove());
}
if (changes.domMutations) {
changes.domMutations.forEach(mutation => {
undo.push(mutate.declarative(mutation).revert);
});
}
return () => {
undo.forEach(fn => fn());
};
}
async refreshStickyBuckets(data) {
if (this._options.stickyBucketService) {
const ctx = this._getEvalContext();
const docs = await getAllStickyBucketAssignmentDocs(ctx, this._options.stickyBucketService, data);
this._options.stickyBucketAssignmentDocs = docs;
}
}
generateStickyBucketAssignmentDocsSync(stickyBucketService, payload) {
if (!("getAllAssignmentsSync" in stickyBucketService)) {
console.error("generating StickyBucketAssignmentDocs docs requires StickyBucketServiceSync");
return;
}
const ctx = this._getEvalContext();
const attributes = getStickyBucketAttributes(ctx, payload);
return stickyBucketService.getAllAssignmentsSync(attributes);
}
inDevMode() {
return !!this._options.enableDevMode;
}
}
export async function prefetchPayload(options) {
// Create a temporary instance, just to fetch the payload
const instance = new GrowthBook(options);
await refreshFeatures({
instance,
skipCache: options.skipCache,
allowStale: false,
backgroundSync: options.streaming
});
instance.destroy();
}
//# sourceMappingURL=GrowthBook.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,318 @@
import { loadSDKVersion } from "./util.mjs";
import { clearAutoRefresh, configureCache, refreshFeatures, startStreaming, unsubscribe } from "./feature-repository.mjs";
import { decryptPayload, evalFeature as _evalFeature, getAllStickyBucketAssignmentDocs, getApiHosts, runExperiment } from "./core.mjs";
const SDK_VERSION = loadSDKVersion();
export class GrowthBookClient {
// Properties and methods that start with "_" are mangled by Terser (saves ~150 bytes)
constructor(options) {
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);
}
}
}
async setPayload(payload) {
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;
}
this.ready = true;
}
initSync(options) {
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.experiments) {
this._experiments = payload.experiments;
}
this.ready = true;
startStreaming(this, options);
return this;
}
async init(options) {
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;
}
}
async refreshFeatures(options) {
const res = await this._refresh({
...(options || {}),
allowStale: false
});
if (res.data) {
await this.setPayload(res.data);
}
}
getApiInfo() {
return [this.getApiHosts().apiHost, this.getClientKey()];
}
getApiHosts() {
return getApiHosts(this._options);
}
getClientKey() {
return this._options.clientKey || "";
}
getPayload() {
return this._payload || {
features: this.getFeatures(),
experiments: this._experiments || []
};
}
getDecryptedPayload() {
return this._decryptedPayload || this.getPayload();
}
async _refresh({
timeout,
skipCache,
allowStale,
streaming
}) {
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
});
}
getFeatures() {
return this._features || {};
}
getGlobalAttributes() {
return this._options.globalAttributes || {};
}
setGlobalAttributes(attributes) {
this._options.globalAttributes = attributes;
}
destroy(options) {
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 = {};
}
isDestroyed() {
return !!this._destroyed;
}
setEventLogger(logger) {
this._options.eventLogger = logger;
}
logEvent(eventName, properties, userContext) {
if (this._options.eventLogger) {
const ctx = this._getEvalContext(userContext);
this._options.eventLogger(eventName, properties, ctx.user);
}
}
runInlineExperiment(experiment, userContext) {
const {
result
} = runExperiment(experiment, null, this._getEvalContext(userContext));
return result;
}
_getEvalContext(userContext) {
if (this._options.globalAttributes) {
userContext = {
...userContext,
attributes: {
...this._options.globalAttributes,
...userContext.attributes
}
};
}
return {
user: userContext,
global: this._getGlobalContext(),
stack: {
evaluatedFeatures: new Set()
}
};
}
_getGlobalContext() {
return {
features: this._features,
experiments: this._experiments,
log: this.log,
enabled: this._options.enabled,
qaMode: this._options.qaMode,
savedGroups: this._options.savedGroups,
forcedFeatureValues: this._options.forcedFeatureValues,
forcedVariations: this._options.forcedVariations,
trackingCallback: this._options.trackingCallback,
onFeatureUsage: this._options.onFeatureUsage
};
}
isOn(key, userContext) {
return this.evalFeature(key, userContext).on;
}
isOff(key, userContext) {
return this.evalFeature(key, userContext).off;
}
getFeatureValue(key, defaultValue, userContext) {
const value = this.evalFeature(key, userContext).value;
return value === null ? defaultValue : value;
}
evalFeature(id, userContext) {
return _evalFeature(id, this._getEvalContext(userContext));
}
log(msg, ctx) {
if (!this.debug) return;
if (this._options.log) this._options.log(msg, ctx);else console.log(msg, ctx);
}
setTrackingCallback(callback) {
this._options.trackingCallback = callback;
}
setFeatureUsageCallback(callback) {
this._options.onFeatureUsage = callback;
}
async applyStickyBuckets(partialContext, stickyBucketService) {
const ctx = this._getEvalContext(partialContext);
const stickyBucketAssignmentDocs = await getAllStickyBucketAssignmentDocs(ctx, stickyBucketService);
return {
...partialContext,
stickyBucketAssignmentDocs,
saveStickyBucketAssignmentDoc: doc => stickyBucketService.saveAssignments(doc)
};
}
createScopedInstance(userContext, userPlugins) {
return new UserScopedGrowthBook(this, userContext, [...(this._options.plugins || []), ...(userPlugins || [])]);
}
}
export class UserScopedGrowthBook {
constructor(gb, userContext, plugins) {
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);
}
}
}
runInlineExperiment(experiment) {
return this._gb.runInlineExperiment(experiment, this._userContext);
}
isOn(key) {
return this._gb.isOn(key, this._userContext);
}
isOff(key) {
return this._gb.isOff(key, this._userContext);
}
getFeatureValue(key, defaultValue) {
return this._gb.getFeatureValue(key, defaultValue, this._userContext);
}
evalFeature(id) {
return this._gb.evalFeature(id, this._userContext);
}
logEvent(eventName, properties) {
if (this._userContext.enableDevMode) {
this.logs.push({
eventName,
properties,
timestamp: Date.now().toString(),
logType: "event"
});
}
this._gb.logEvent(eventName, properties || {}, this._userContext);
}
setTrackingCallback(cb) {
this._userContext.trackingCallback = cb;
}
getApiInfo() {
return this._gb.getApiInfo();
}
getClientKey() {
return this._gb.getClientKey();
}
setURL(url) {
this._userContext.url = url;
}
updateAttributes(attributes) {
this._userContext.attributes = {
...this._userContext.attributes,
...attributes
};
}
setAttributeOverrides(overrides) {
this._userContext.attributeOverrides = overrides;
}
async setForcedVariations(vars) {
this._userContext.forcedVariations = vars || {};
}
// eslint-disable-next-line
setForcedFeatures(map) {
this._userContext.forcedFeatureValues = map;
}
getUserContext() {
return this._userContext;
}
getVersion() {
return SDK_VERSION;
}
getDecryptedPayload() {
return this._gb.getDecryptedPayload();
}
inDevMode() {
return !!this._userContext.enableDevMode;
}
}
//# sourceMappingURL=GrowthBookClient.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,878 @@
import { evalCondition } from "./mongrule.mjs";
import { chooseVariation, decrypt, getBucketRanges, getQueryStringOverride, getUrlRegExp, hash, inNamespace, inRange, isIncluded, isURLTargeted, toString } from "./util.mjs";
export const EVENT_FEATURE_EVALUATED = "Feature Evaluated";
export const EVENT_EXPERIMENT_VIEWED = "Experiment Viewed";
function getForcedFeatureValues(ctx) {
// Merge user and global values
const ret = new Map();
if (ctx.global.forcedFeatureValues) {
ctx.global.forcedFeatureValues.forEach((v, k) => ret.set(k, v));
}
if (ctx.user.forcedFeatureValues) {
ctx.user.forcedFeatureValues.forEach((v, k) => ret.set(k, v));
}
return ret;
}
function getForcedVariations(ctx) {
// Merge user and global values
if (ctx.global.forcedVariations && ctx.user.forcedVariations) {
return {
...ctx.global.forcedVariations,
...ctx.user.forcedVariations
};
} else if (ctx.global.forcedVariations) {
return ctx.global.forcedVariations;
} else if (ctx.user.forcedVariations) {
return ctx.user.forcedVariations;
} else {
return {};
}
}
async function safeCall(fn) {
try {
await fn();
} catch (e) {
// Do nothing
}
}
function onExperimentViewed(ctx, experiment, result) {
// Make sure a tracking callback is only fired once per unique experiment
if (ctx.user.trackedExperiments) {
const k = getExperimentDedupeKey(experiment, result);
if (ctx.user.trackedExperiments.has(k)) {
return [];
}
ctx.user.trackedExperiments.add(k);
}
if (ctx.user.enableDevMode && ctx.user.devLogs) {
ctx.user.devLogs.push({
experiment,
result,
timestamp: Date.now().toString(),
logType: "experiment"
});
}
const calls = [];
if (ctx.global.trackingCallback) {
const cb = ctx.global.trackingCallback;
calls.push(safeCall(() => cb(experiment, result, ctx.user)));
}
if (ctx.user.trackingCallback) {
const cb = ctx.user.trackingCallback;
calls.push(safeCall(() => cb(experiment, result)));
}
if (ctx.global.eventLogger) {
const cb = ctx.global.eventLogger;
calls.push(safeCall(() => cb(EVENT_EXPERIMENT_VIEWED, {
experimentId: experiment.key,
variationId: result.key,
hashAttribute: result.hashAttribute,
hashValue: result.hashValue
}, ctx.user)));
}
return calls;
}
function onFeatureUsage(ctx, key, ret) {
// Only track a feature once, unless the assigned value changed
if (ctx.user.trackedFeatureUsage) {
const stringifiedValue = JSON.stringify(ret.value);
if (ctx.user.trackedFeatureUsage[key] === stringifiedValue) return;
ctx.user.trackedFeatureUsage[key] = stringifiedValue;
if (ctx.user.enableDevMode && ctx.user.devLogs) {
ctx.user.devLogs.push({
featureKey: key,
result: ret,
timestamp: Date.now().toString(),
logType: "feature"
});
}
}
if (ctx.global.onFeatureUsage) {
const cb = ctx.global.onFeatureUsage;
safeCall(() => cb(key, ret, ctx.user));
}
if (ctx.user.onFeatureUsage) {
const cb = ctx.user.onFeatureUsage;
safeCall(() => cb(key, ret));
}
if (ctx.global.eventLogger) {
const cb = ctx.global.eventLogger;
safeCall(() => cb(EVENT_FEATURE_EVALUATED, {
feature: key,
source: ret.source,
value: ret.value,
ruleId: ret.source === "defaultValue" ? "$default" : ret.ruleId || "",
variationId: ret.experimentResult ? ret.experimentResult.key : ""
}, ctx.user));
}
}
export function evalFeature(id, ctx) {
if (ctx.stack.evaluatedFeatures.has(id)) {
process.env.NODE_ENV !== "production" && ctx.global.log(`evalFeature: circular dependency detected: ${ctx.stack.id} -> ${id}`, {
from: ctx.stack.id,
to: id
});
return getFeatureResult(ctx, id, null, "cyclicPrerequisite");
}
ctx.stack.evaluatedFeatures.add(id);
ctx.stack.id = id;
// Global override
const forcedValues = getForcedFeatureValues(ctx);
if (forcedValues.has(id)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Global override", {
id,
value: forcedValues.get(id)
});
return getFeatureResult(ctx, id, forcedValues.get(id), "override");
}
// Unknown feature id
if (!ctx.global.features || !ctx.global.features[id]) {
process.env.NODE_ENV !== "production" && ctx.global.log("Unknown feature", {
id
});
return getFeatureResult(ctx, id, null, "unknownFeature");
}
// Get the feature
const feature = ctx.global.features[id];
// Loop through the rules
if (feature.rules) {
const evaluatedFeatures = new Set(ctx.stack.evaluatedFeatures);
rules: for (const rule of feature.rules) {
// If there are prerequisite flag(s), evaluate them
if (rule.parentConditions) {
for (const parentCondition of rule.parentConditions) {
ctx.stack.evaluatedFeatures = new Set(evaluatedFeatures);
const parentResult = evalFeature(parentCondition.id, ctx);
// break out for cyclic prerequisites
if (parentResult.source === "cyclicPrerequisite") {
return getFeatureResult(ctx, id, null, "cyclicPrerequisite");
}
const evalObj = {
value: parentResult.value
};
const evaled = evalCondition(evalObj, parentCondition.condition || {});
if (!evaled) {
// blocking prerequisite eval failed: feature evaluation fails
if (parentCondition.gate) {
process.env.NODE_ENV !== "production" && ctx.global.log("Feature blocked by prerequisite", {
id,
rule
});
return getFeatureResult(ctx, id, null, "prerequisite");
}
// non-blocking prerequisite eval failed: break out of parentConditions loop, jump to the next rule
process.env.NODE_ENV !== "production" && ctx.global.log("Skip rule because prerequisite evaluation fails", {
id,
rule
});
continue rules;
}
}
}
// If there are filters for who is included (e.g. namespaces)
if (rule.filters && isFilteredOut(rule.filters, ctx)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip rule because of filters", {
id,
rule
});
continue;
}
// Feature value is being forced
if ("force" in rule) {
// If it's a conditional rule, skip if the condition doesn't pass
if (rule.condition && !conditionPasses(rule.condition, ctx)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip rule because of condition ff", {
id,
rule
});
continue;
}
// If this is a percentage rollout, skip if not included
if (!isIncludedInRollout(ctx, rule.seed || id, rule.hashAttribute, ctx.user.saveStickyBucketAssignmentDoc && !rule.disableStickyBucketing ? rule.fallbackAttribute : undefined, rule.range, rule.coverage, rule.hashVersion)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip rule because user not included in rollout", {
id,
rule
});
continue;
}
process.env.NODE_ENV !== "production" && ctx.global.log("Force value from rule", {
id,
rule
});
// If this was a remotely evaluated experiment, fire the tracking callbacks
if (rule.tracks) {
rule.tracks.forEach(t => {
const calls = onExperimentViewed(ctx, t.experiment, t.result);
if (!calls.length && ctx.global.saveDeferredTrack) {
ctx.global.saveDeferredTrack({
experiment: t.experiment,
result: t.result
});
}
});
}
return getFeatureResult(ctx, id, rule.force, "force", rule.id);
}
if (!rule.variations) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip invalid rule", {
id,
rule
});
continue;
}
// For experiment rules, run an experiment
const exp = {
variations: rule.variations,
key: rule.key || id
};
if ("coverage" in rule) exp.coverage = rule.coverage;
if (rule.weights) exp.weights = rule.weights;
if (rule.hashAttribute) exp.hashAttribute = rule.hashAttribute;
if (rule.fallbackAttribute) exp.fallbackAttribute = rule.fallbackAttribute;
if (rule.disableStickyBucketing) exp.disableStickyBucketing = rule.disableStickyBucketing;
if (rule.bucketVersion !== undefined) exp.bucketVersion = rule.bucketVersion;
if (rule.minBucketVersion !== undefined) exp.minBucketVersion = rule.minBucketVersion;
if (rule.namespace) exp.namespace = rule.namespace;
if (rule.meta) exp.meta = rule.meta;
if (rule.ranges) exp.ranges = rule.ranges;
if (rule.name) exp.name = rule.name;
if (rule.phase) exp.phase = rule.phase;
if (rule.seed) exp.seed = rule.seed;
if (rule.hashVersion) exp.hashVersion = rule.hashVersion;
if (rule.filters) exp.filters = rule.filters;
if (rule.condition) exp.condition = rule.condition;
// Only return a value if the user is part of the experiment
const {
result
} = runExperiment(exp, id, ctx);
ctx.global.onExperimentEval && ctx.global.onExperimentEval(exp, result);
if (result.inExperiment && !result.passthrough) {
return getFeatureResult(ctx, id, result.value, "experiment", rule.id, exp, result);
}
}
}
process.env.NODE_ENV !== "production" && ctx.global.log("Use default value", {
id,
value: feature.defaultValue
});
// Fall back to using the default value
return getFeatureResult(ctx, id, feature.defaultValue === undefined ? null : feature.defaultValue, "defaultValue");
}
export function runExperiment(experiment, featureId, ctx) {
const key = experiment.key;
const numVariations = experiment.variations.length;
// 1. If experiment has less than 2 variations, return immediately
if (numVariations < 2) {
process.env.NODE_ENV !== "production" && ctx.global.log("Invalid experiment", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 2. If the context is disabled, return immediately
if (ctx.global.enabled === false || ctx.user.enabled === false) {
process.env.NODE_ENV !== "production" && ctx.global.log("Context disabled", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 2.5. Merge in experiment overrides from the context
experiment = mergeOverrides(experiment, ctx);
// 2.6 New, more powerful URL targeting
if (experiment.urlPatterns && !isURLTargeted(ctx.user.url || "", experiment.urlPatterns)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of url targeting", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 3. If a variation is forced from a querystring, return the forced variation
const qsOverride = getQueryStringOverride(key, ctx.user.url || "", numVariations);
if (qsOverride !== null) {
process.env.NODE_ENV !== "production" && ctx.global.log("Force via querystring", {
id: key,
variation: qsOverride
});
return {
result: getExperimentResult(ctx, experiment, qsOverride, false, featureId)
};
}
// 4. If a variation is forced in the context, return the forced variation
const forcedVariations = getForcedVariations(ctx);
if (key in forcedVariations) {
const variation = forcedVariations[key];
process.env.NODE_ENV !== "production" && ctx.global.log("Force via dev tools", {
id: key,
variation
});
return {
result: getExperimentResult(ctx, experiment, variation, false, featureId)
};
}
// 5. Exclude if a draft experiment or not active
if (experiment.status === "draft" || experiment.active === false) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because inactive", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 6. Get the hash attribute and return if empty
const {
hashAttribute,
hashValue
} = getHashAttribute(ctx, experiment.hashAttribute, ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing ? experiment.fallbackAttribute : undefined);
if (!hashValue) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because missing hashAttribute", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
let assigned = -1;
let foundStickyBucket = false;
let stickyBucketVersionIsBlocked = false;
if (ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing) {
const {
variation,
versionIsBlocked
} = getStickyBucketVariation({
ctx,
expKey: experiment.key,
expBucketVersion: experiment.bucketVersion,
expHashAttribute: experiment.hashAttribute,
expFallbackAttribute: experiment.fallbackAttribute,
expMinBucketVersion: experiment.minBucketVersion,
expMeta: experiment.meta
});
foundStickyBucket = variation >= 0;
assigned = variation;
stickyBucketVersionIsBlocked = !!versionIsBlocked;
}
// Some checks are not needed if we already have a sticky bucket
if (!foundStickyBucket) {
// 7. Exclude if user is filtered out (used to be called "namespace")
if (experiment.filters) {
if (isFilteredOut(experiment.filters, ctx)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of filters", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
} else if (experiment.namespace && !inNamespace(hashValue, experiment.namespace)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of namespace", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 7.5. Exclude if experiment.include returns false or throws
if (experiment.include && !isIncluded(experiment.include)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of include function", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 8. Exclude if condition is false
if (experiment.condition && !conditionPasses(experiment.condition, ctx)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of condition exp", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 8.05. Exclude if prerequisites are not met
if (experiment.parentConditions) {
const evaluatedFeatures = new Set(ctx.stack.evaluatedFeatures);
for (const parentCondition of experiment.parentConditions) {
ctx.stack.evaluatedFeatures = new Set(evaluatedFeatures);
const parentResult = evalFeature(parentCondition.id, ctx);
// break out for cyclic prerequisites
if (parentResult.source === "cyclicPrerequisite") {
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
const evalObj = {
value: parentResult.value
};
if (!evalCondition(evalObj, parentCondition.condition || {})) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because prerequisite evaluation fails", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
}
}
// 8.1. Exclude if user is not in a required group
if (experiment.groups && !hasGroupOverlap(experiment.groups, ctx)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of groups", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
}
// 8.2. Old style URL targeting
if (experiment.url && !urlIsValid(experiment.url, ctx)) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of url", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 9. Get the variation from the sticky bucket or get bucket ranges and choose variation
const n = hash(experiment.seed || key, hashValue, experiment.hashVersion || 1);
if (n === null) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of invalid hash version", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
if (!foundStickyBucket) {
const ranges = experiment.ranges || getBucketRanges(numVariations, experiment.coverage === undefined ? 1 : experiment.coverage, experiment.weights);
assigned = chooseVariation(n, ranges);
}
// 9.5 Unenroll if any prior sticky buckets are blocked by version
if (stickyBucketVersionIsBlocked) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because sticky bucket version is blocked", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId, undefined, true)
};
}
// 10. Return if not in experiment
if (assigned < 0) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because of coverage", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 11. Experiment has a forced variation
if ("force" in experiment) {
process.env.NODE_ENV !== "production" && ctx.global.log("Force variation", {
id: key,
variation: experiment.force
});
return {
result: getExperimentResult(ctx, experiment, experiment.force === undefined ? -1 : experiment.force, false, featureId)
};
}
// 12. Exclude if in QA mode
if (ctx.global.qaMode || ctx.user.qaMode) {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because QA mode", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 12.5. Exclude if experiment is stopped
if (experiment.status === "stopped") {
process.env.NODE_ENV !== "production" && ctx.global.log("Skip because stopped", {
id: key
});
return {
result: getExperimentResult(ctx, experiment, -1, false, featureId)
};
}
// 13. Build the result object
const result = getExperimentResult(ctx, experiment, assigned, true, featureId, n, foundStickyBucket);
// 13.5. Persist sticky bucket
if (ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing) {
const {
changed,
key: attrKey,
doc
} = generateStickyBucketAssignmentDoc(ctx, hashAttribute, toString(hashValue), {
[getStickyBucketExperimentKey(experiment.key, experiment.bucketVersion)]: result.key
});
if (changed) {
// update local docs
ctx.user.stickyBucketAssignmentDocs = ctx.user.stickyBucketAssignmentDocs || {};
ctx.user.stickyBucketAssignmentDocs[attrKey] = doc;
// save doc
ctx.user.saveStickyBucketAssignmentDoc(doc);
}
}
// 14. Fire the tracking callback(s)
// Store the promise in case we're awaiting it (ex: browser url redirects)
const trackingCalls = onExperimentViewed(ctx, experiment, result);
if (trackingCalls.length === 0 && ctx.global.saveDeferredTrack) {
ctx.global.saveDeferredTrack({
experiment,
result
});
}
const trackingCall = !trackingCalls.length ? undefined : trackingCalls.length === 1 ? trackingCalls[0] : Promise.all(trackingCalls).then(() => {});
// 14.1 Keep track of completed changeIds
"changeId" in experiment && experiment.changeId && ctx.global.recordChangeId && ctx.global.recordChangeId(experiment.changeId);
// 15. Return the result
process.env.NODE_ENV !== "production" && ctx.global.log("In experiment", {
id: key,
variation: result.variationId
});
return {
result,
trackingCall
};
}
function getFeatureResult(ctx, key, value, source, ruleId, experiment, result) {
const ret = {
value,
on: !!value,
off: !value,
source,
ruleId: ruleId || ""
};
if (experiment) ret.experiment = experiment;
if (result) ret.experimentResult = result;
// Track the usage of this feature in real-time
if (source !== "override") {
onFeatureUsage(ctx, key, ret);
}
return ret;
}
function getAttributes(ctx) {
return {
...ctx.user.attributes,
...ctx.user.attributeOverrides
};
}
function conditionPasses(condition, ctx) {
return evalCondition(getAttributes(ctx), condition, ctx.global.savedGroups || {});
}
function isFilteredOut(filters, ctx) {
return filters.some(filter => {
const {
hashValue
} = getHashAttribute(ctx, filter.attribute);
if (!hashValue) return true;
const n = hash(filter.seed, hashValue, filter.hashVersion || 2);
if (n === null) return true;
return !filter.ranges.some(r => inRange(n, r));
});
}
function isIncludedInRollout(ctx, seed, hashAttribute, fallbackAttribute, range, coverage, hashVersion) {
if (!range && coverage === undefined) return true;
if (!range && coverage === 0) return false;
const {
hashValue
} = getHashAttribute(ctx, hashAttribute, fallbackAttribute);
if (!hashValue) {
return false;
}
const n = hash(seed, hashValue, hashVersion || 1);
if (n === null) return false;
return range ? inRange(n, range) : coverage !== undefined ? n <= coverage : true;
}
export function getExperimentResult(ctx, experiment, variationIndex, hashUsed, featureId, bucket, stickyBucketUsed) {
let inExperiment = true;
// If assigned variation is not valid, use the baseline and mark the user as not in the experiment
if (variationIndex < 0 || variationIndex >= experiment.variations.length) {
variationIndex = 0;
inExperiment = false;
}
const {
hashAttribute,
hashValue
} = getHashAttribute(ctx, experiment.hashAttribute, ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing ? experiment.fallbackAttribute : undefined);
const meta = experiment.meta ? experiment.meta[variationIndex] : {};
const res = {
key: meta.key || "" + variationIndex,
featureId,
inExperiment,
hashUsed,
variationId: variationIndex,
value: experiment.variations[variationIndex],
hashAttribute,
hashValue,
stickyBucketUsed: !!stickyBucketUsed
};
if (meta.name) res.name = meta.name;
if (bucket !== undefined) res.bucket = bucket;
if (meta.passthrough) res.passthrough = meta.passthrough;
return res;
}
function mergeOverrides(experiment, ctx) {
const key = experiment.key;
const o = ctx.global.overrides;
if (o && o[key]) {
experiment = Object.assign({}, experiment, o[key]);
if (typeof experiment.url === "string") {
experiment.url = getUrlRegExp(
// eslint-disable-next-line
experiment.url);
}
}
return experiment;
}
export function getHashAttribute(ctx, attr, fallback) {
let hashAttribute = attr || "id";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let hashValue = "";
const attributes = getAttributes(ctx);
if (attributes[hashAttribute]) {
hashValue = attributes[hashAttribute];
}
// if no match, try fallback
if (!hashValue && fallback) {
if (attributes[fallback]) {
hashValue = attributes[fallback];
}
if (hashValue) {
hashAttribute = fallback;
}
}
return {
hashAttribute,
hashValue
};
}
function urlIsValid(urlRegex, ctx) {
const url = ctx.user.url;
if (!url) return false;
const pathOnly = url.replace(/^https?:\/\//, "").replace(/^[^/]*\//, "/");
if (urlRegex.test(url)) return true;
if (urlRegex.test(pathOnly)) return true;
return false;
}
function hasGroupOverlap(expGroups, ctx) {
const groups = ctx.global.groups || {};
for (let i = 0; i < expGroups.length; i++) {
if (groups[expGroups[i]]) return true;
}
return false;
}
function getStickyBucketVariation({
ctx,
expKey,
expBucketVersion,
expHashAttribute,
expFallbackAttribute,
expMinBucketVersion,
expMeta
}) {
expBucketVersion = expBucketVersion || 0;
expMinBucketVersion = expMinBucketVersion || 0;
expHashAttribute = expHashAttribute || "id";
expMeta = expMeta || [];
const id = getStickyBucketExperimentKey(expKey, expBucketVersion);
const assignments = getStickyBucketAssignments(ctx, expHashAttribute, expFallbackAttribute);
// users with any blocked bucket version (0 to minExperimentBucketVersion - 1) are excluded from the test
if (expMinBucketVersion > 0) {
for (let i = 0; i < expMinBucketVersion; i++) {
const blockedKey = getStickyBucketExperimentKey(expKey, i);
if (assignments[blockedKey] !== undefined) {
return {
variation: -1,
versionIsBlocked: true
};
}
}
}
const variationKey = assignments[id];
if (variationKey === undefined)
// no assignment found
return {
variation: -1
};
const variation = expMeta.findIndex(m => m.key === variationKey);
if (variation < 0)
// invalid assignment, treat as "no assignment found"
return {
variation: -1
};
return {
variation
};
}
function getStickyBucketExperimentKey(experimentKey, experimentBucketVersion) {
experimentBucketVersion = experimentBucketVersion || 0;
return `${experimentKey}__${experimentBucketVersion}`;
}
export function getStickyBucketAttributeKey(attributeName, attributeValue) {
return `${attributeName}||${attributeValue}`;
}
function getStickyBucketAssignments(ctx, expHashAttribute, expFallbackAttribute) {
if (!ctx.user.stickyBucketAssignmentDocs) return {};
const {
hashAttribute,
hashValue
} = getHashAttribute(ctx, expHashAttribute);
const hashKey = getStickyBucketAttributeKey(hashAttribute, toString(hashValue));
const {
hashAttribute: fallbackAttribute,
hashValue: fallbackValue
} = getHashAttribute(ctx, expFallbackAttribute);
const fallbackKey = fallbackValue ? getStickyBucketAttributeKey(fallbackAttribute, toString(fallbackValue)) : null;
const assignments = {};
if (fallbackKey && ctx.user.stickyBucketAssignmentDocs[fallbackKey]) {
Object.assign(assignments, ctx.user.stickyBucketAssignmentDocs[fallbackKey].assignments || {});
}
if (ctx.user.stickyBucketAssignmentDocs[hashKey]) {
Object.assign(assignments, ctx.user.stickyBucketAssignmentDocs[hashKey].assignments || {});
}
return assignments;
}
function generateStickyBucketAssignmentDoc(ctx, attributeName, attributeValue, assignments) {
const key = getStickyBucketAttributeKey(attributeName, attributeValue);
const existingAssignments = ctx.user.stickyBucketAssignmentDocs && ctx.user.stickyBucketAssignmentDocs[key] ? ctx.user.stickyBucketAssignmentDocs[key].assignments || {} : {};
const newAssignments = {
...existingAssignments,
...assignments
};
const changed = JSON.stringify(existingAssignments) !== JSON.stringify(newAssignments);
return {
key,
doc: {
attributeName,
attributeValue,
assignments: newAssignments
},
changed
};
}
function deriveStickyBucketIdentifierAttributes(ctx, data) {
const attributes = new Set();
const features = data && data.features ? data.features : ctx.global.features || {};
const experiments = data && data.experiments ? data.experiments : ctx.global.experiments || [];
Object.keys(features).forEach(id => {
const feature = features[id];
if (feature.rules) {
for (const rule of feature.rules) {
if (rule.variations) {
attributes.add(rule.hashAttribute || "id");
if (rule.fallbackAttribute) {
attributes.add(rule.fallbackAttribute);
}
}
}
}
});
experiments.map(experiment => {
attributes.add(experiment.hashAttribute || "id");
if (experiment.fallbackAttribute) {
attributes.add(experiment.fallbackAttribute);
}
});
return Array.from(attributes);
}
export async function getAllStickyBucketAssignmentDocs(ctx, stickyBucketService, data) {
const attributes = getStickyBucketAttributes(ctx, data);
return stickyBucketService.getAllAssignments(attributes);
}
export function getStickyBucketAttributes(ctx, data) {
const attributes = {};
const stickyBucketIdentifierAttributes = deriveStickyBucketIdentifierAttributes(ctx, data);
stickyBucketIdentifierAttributes.forEach(attr => {
const {
hashValue
} = getHashAttribute(ctx, attr);
attributes[attr] = toString(hashValue);
});
return attributes;
}
export async function decryptPayload(data, decryptionKey, subtle) {
data = {
...data
};
if (data.encryptedFeatures) {
try {
data.features = JSON.parse(await decrypt(data.encryptedFeatures, decryptionKey, subtle));
} catch (e) {
console.error(e);
}
delete data.encryptedFeatures;
}
if (data.encryptedExperiments) {
try {
data.experiments = JSON.parse(await decrypt(data.encryptedExperiments, decryptionKey, subtle));
} catch (e) {
console.error(e);
}
delete data.encryptedExperiments;
}
if (data.encryptedSavedGroups) {
try {
data.savedGroups = JSON.parse(await decrypt(data.encryptedSavedGroups, decryptionKey, subtle));
} catch (e) {
console.error(e);
}
delete data.encryptedSavedGroups;
}
return data;
}
export function getApiHosts(options) {
const defaultHost = options.apiHost || "https://cdn.growthbook.io";
return {
apiHost: defaultHost.replace(/\/*$/, ""),
streamingHost: (options.streamingHost || defaultHost).replace(/\/*$/, ""),
apiRequestHeaders: options.apiHostRequestHeaders,
streamingHostRequestHeaders: options.streamingHostRequestHeaders
};
}
export function getExperimentDedupeKey(experiment, result) {
return result.hashAttribute + result.hashValue + experiment.key + result.variationId;
}
//# sourceMappingURL=core.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,477 @@
import { getPolyfills, promiseTimeout } from "./util.mjs";
// Config settings
const cacheSettings = {
// Consider a fetch stale after 1 minute
staleTTL: 1000 * 60,
// Max time to keep a fetch in cache (4 hours default)
maxAge: 1000 * 60 * 60 * 4,
cacheKey: "gbFeaturesCache",
backgroundSync: true,
maxEntries: 10,
disableIdleStreams: false,
idleStreamInterval: 20000,
disableCache: false
};
const polyfills = getPolyfills();
export const helpers = {
fetchFeaturesCall: ({
host,
clientKey,
headers
}) => {
return polyfills.fetch(`${host}/api/features/${clientKey}`, {
headers
});
},
fetchRemoteEvalCall: ({
host,
clientKey,
payload,
headers
}) => {
const options = {
method: "POST",
headers: {
"Content-Type": "application/json",
...headers
},
body: JSON.stringify(payload)
};
return polyfills.fetch(`${host}/api/eval/${clientKey}`, options);
},
eventSourceCall: ({
host,
clientKey,
headers
}) => {
if (headers) {
return new polyfills.EventSource(`${host}/sub/${clientKey}`, {
headers
});
}
return new polyfills.EventSource(`${host}/sub/${clientKey}`);
},
startIdleListener: () => {
let idleTimeout;
const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
if (!isBrowser) return;
const onVisibilityChange = () => {
if (document.visibilityState === "visible") {
window.clearTimeout(idleTimeout);
onVisible();
} else if (document.visibilityState === "hidden") {
idleTimeout = window.setTimeout(onHidden, cacheSettings.idleStreamInterval);
}
};
document.addEventListener("visibilitychange", onVisibilityChange);
return () => document.removeEventListener("visibilitychange", onVisibilityChange);
},
stopIdleListener: () => {
// No-op, replaced by startIdleListener
}
};
try {
if (globalThis.localStorage) {
polyfills.localStorage = globalThis.localStorage;
}
} catch (e) {
// Ignore localStorage errors
}
// Global state
const subscribedInstances = new Map();
let cacheInitialized = false;
const cache = new Map();
const activeFetches = new Map();
const streams = new Map();
const supportsSSE = new Set();
// Public functions
export function setPolyfills(overrides) {
Object.assign(polyfills, overrides);
}
export function configureCache(overrides) {
Object.assign(cacheSettings, overrides);
if (!cacheSettings.backgroundSync) {
clearAutoRefresh();
}
}
export async function clearCache() {
cache.clear();
activeFetches.clear();
clearAutoRefresh();
cacheInitialized = false;
await updatePersistentCache();
}
// Get or fetch features and refresh the SDK instance
export async function refreshFeatures({
instance,
timeout,
skipCache,
allowStale,
backgroundSync
}) {
if (!backgroundSync) {
cacheSettings.backgroundSync = false;
}
return fetchFeaturesWithCache({
instance,
allowStale,
timeout,
skipCache
});
}
// Subscribe a GrowthBook instance to feature changes
function subscribe(instance) {
const key = getKey(instance);
const subs = subscribedInstances.get(key) || new Set();
subs.add(instance);
subscribedInstances.set(key, subs);
}
export function unsubscribe(instance) {
subscribedInstances.forEach(s => s.delete(instance));
}
export function onHidden() {
streams.forEach(channel => {
if (!channel) return;
channel.state = "idle";
disableChannel(channel);
});
}
export function onVisible() {
streams.forEach(channel => {
if (!channel) return;
if (channel.state !== "idle") return;
enableChannel(channel);
});
}
// Private functions
async function updatePersistentCache() {
try {
if (!polyfills.localStorage) return;
await polyfills.localStorage.setItem(cacheSettings.cacheKey, JSON.stringify(Array.from(cache.entries())));
} catch (e) {
// Ignore localStorage errors
}
}
// SWR wrapper for fetching features. May indirectly or directly start SSE streaming.
async function fetchFeaturesWithCache({
instance,
allowStale,
timeout,
skipCache
}) {
const key = getKey(instance);
const cacheKey = getCacheKey(instance);
const now = new Date();
const minStaleAt = new Date(now.getTime() - cacheSettings.maxAge + cacheSettings.staleTTL);
await initializeCache();
const existing = !cacheSettings.disableCache && !skipCache ? cache.get(cacheKey) : undefined;
if (existing && (allowStale || existing.staleAt > now) && existing.staleAt > minStaleAt) {
// Restore from cache whether SSE is supported
if (existing.sse) supportsSSE.add(key);
// Reload features in the background if stale
if (existing.staleAt < now) {
fetchFeatures(instance);
}
// Otherwise, if we don't need to refresh now, start a background sync
else {
startAutoRefresh(instance);
}
return {
data: existing.data,
success: true,
source: "cache"
};
} else {
const res = await promiseTimeout(fetchFeatures(instance), timeout);
return res || {
data: null,
success: false,
source: "timeout",
error: new Error("Timeout")
};
}
}
function getKey(instance) {
const [apiHost, clientKey] = instance.getApiInfo();
return `${apiHost}||${clientKey}`;
}
function getCacheKey(instance) {
const baseKey = getKey(instance);
if (!("isRemoteEval" in instance) || !instance.isRemoteEval()) return baseKey;
const attributes = instance.getAttributes();
const cacheKeyAttributes = instance.getCacheKeyAttributes() || Object.keys(instance.getAttributes());
const ca = {};
cacheKeyAttributes.forEach(key => {
ca[key] = attributes[key];
});
const fv = instance.getForcedVariations();
const url = instance.getUrl();
return `${baseKey}||${JSON.stringify({
ca,
fv,
url
})}`;
}
// Populate cache from localStorage (if available)
async function initializeCache() {
if (cacheInitialized) return;
cacheInitialized = true;
try {
if (polyfills.localStorage) {
const value = await polyfills.localStorage.getItem(cacheSettings.cacheKey);
if (!cacheSettings.disableCache && value) {
const parsed = JSON.parse(value);
if (parsed && Array.isArray(parsed)) {
parsed.forEach(([key, data]) => {
cache.set(key, {
...data,
staleAt: new Date(data.staleAt)
});
});
}
cleanupCache();
}
}
} catch (e) {
// Ignore localStorage errors
}
if (!cacheSettings.disableIdleStreams) {
const cleanupFn = helpers.startIdleListener();
if (cleanupFn) {
helpers.stopIdleListener = cleanupFn;
}
}
}
// Enforce the maxEntries limit
function cleanupCache() {
const entriesWithTimestamps = Array.from(cache.entries()).map(([key, value]) => ({
key,
staleAt: value.staleAt.getTime()
})).sort((a, b) => a.staleAt - b.staleAt);
const entriesToRemoveCount = Math.min(Math.max(0, cache.size - cacheSettings.maxEntries), cache.size);
for (let i = 0; i < entriesToRemoveCount; i++) {
cache.delete(entriesWithTimestamps[i].key);
}
}
// Called whenever new features are fetched from the API
function onNewFeatureData(key, cacheKey, data) {
// If contents haven't changed, ignore the update, extend the stale TTL
const version = data.dateUpdated || "";
const staleAt = new Date(Date.now() + cacheSettings.staleTTL);
const existing = !cacheSettings.disableCache ? cache.get(cacheKey) : undefined;
if (existing && version && existing.version === version) {
existing.staleAt = staleAt;
updatePersistentCache();
return;
}
if (!cacheSettings.disableCache) {
// Update in-memory cache
cache.set(cacheKey, {
data,
version,
staleAt,
sse: supportsSSE.has(key)
});
cleanupCache();
}
// Update local storage (don't await this, just update asynchronously)
updatePersistentCache();
// Update features for all subscribed GrowthBook instances
const instances = subscribedInstances.get(key);
instances && instances.forEach(instance => refreshInstance(instance, data));
}
async function refreshInstance(instance, data) {
await instance.setPayload(data || instance.getPayload());
}
// Fetch the features payload from helper function or from in-mem injected payload
async function fetchFeatures(instance) {
const {
apiHost,
apiRequestHeaders
} = instance.getApiHosts();
const clientKey = instance.getClientKey();
const remoteEval = "isRemoteEval" in instance && instance.isRemoteEval();
const key = getKey(instance);
const cacheKey = getCacheKey(instance);
let promise = activeFetches.get(cacheKey);
if (!promise) {
const fetcher = remoteEval ? helpers.fetchRemoteEvalCall({
host: apiHost,
clientKey,
payload: {
attributes: instance.getAttributes(),
forcedVariations: instance.getForcedVariations(),
forcedFeatures: Array.from(instance.getForcedFeatures().entries()),
url: instance.getUrl()
},
headers: apiRequestHeaders
}) : helpers.fetchFeaturesCall({
host: apiHost,
clientKey,
headers: apiRequestHeaders
});
// TODO: auto-retry if status code indicates a temporary error
promise = fetcher.then(res => {
if (!res.ok) {
throw new Error(`HTTP error: ${res.status}`);
}
if (res.headers.get("x-sse-support") === "enabled") {
supportsSSE.add(key);
}
return res.json();
}).then(data => {
onNewFeatureData(key, cacheKey, data);
startAutoRefresh(instance);
activeFetches.delete(cacheKey);
return {
data,
success: true,
source: "network"
};
}).catch(e => {
process.env.NODE_ENV !== "production" && instance.log("Error fetching features", {
apiHost,
clientKey,
error: e ? e.message : null
});
activeFetches.delete(cacheKey);
return {
data: null,
source: "error",
success: false,
error: e
};
});
activeFetches.set(cacheKey, promise);
}
return promise;
}
// Start SSE streaming, listens to feature payload changes and triggers a refresh or re-fetch
function startAutoRefresh(instance, forceSSE = false) {
const key = getKey(instance);
const cacheKey = getCacheKey(instance);
const {
streamingHost,
streamingHostRequestHeaders
} = instance.getApiHosts();
const clientKey = instance.getClientKey();
if (forceSSE) {
supportsSSE.add(key);
}
if (cacheSettings.backgroundSync && supportsSSE.has(key) && polyfills.EventSource) {
if (streams.has(key)) return;
const channel = {
src: null,
host: streamingHost,
clientKey,
headers: streamingHostRequestHeaders,
cb: event => {
try {
if (event.type === "features-updated") {
const instances = subscribedInstances.get(key);
instances && instances.forEach(instance => {
fetchFeatures(instance);
});
} else if (event.type === "features") {
const json = JSON.parse(event.data);
onNewFeatureData(key, cacheKey, json);
}
// Reset error count on success
channel.errors = 0;
} catch (e) {
process.env.NODE_ENV !== "production" && instance.log("SSE Error", {
streamingHost,
clientKey,
error: e ? e.message : null
});
onSSEError(channel);
}
},
errors: 0,
state: "active"
};
streams.set(key, channel);
enableChannel(channel);
}
}
function onSSEError(channel) {
if (channel.state === "idle") return;
channel.errors++;
if (channel.errors > 3 || channel.src && channel.src.readyState === 2) {
// exponential backoff after 4 errors, with jitter
const delay = Math.pow(3, channel.errors - 3) * (1000 + Math.random() * 1000);
disableChannel(channel);
setTimeout(() => {
if (["idle", "active"].includes(channel.state)) return;
enableChannel(channel);
}, Math.min(delay, 300000)); // 5 minutes max
}
}
function disableChannel(channel) {
if (!channel.src) return;
channel.src.onopen = null;
channel.src.onerror = null;
channel.src.close();
channel.src = null;
if (channel.state === "active") {
channel.state = "disabled";
}
}
function enableChannel(channel) {
channel.src = helpers.eventSourceCall({
host: channel.host,
clientKey: channel.clientKey,
headers: channel.headers
});
channel.state = "active";
channel.src.addEventListener("features", channel.cb);
channel.src.addEventListener("features-updated", channel.cb);
channel.src.onerror = () => onSSEError(channel);
channel.src.onopen = () => {
channel.errors = 0;
};
}
function destroyChannel(channel, key) {
disableChannel(channel);
streams.delete(key);
}
export function clearAutoRefresh() {
// Clear list of which keys are auto-updated
supportsSSE.clear();
// Stop listening for any SSE events
streams.forEach(destroyChannel);
// Remove all references to GrowthBook instances
subscribedInstances.clear();
// Run the idle stream cleanup function
helpers.stopIdleListener();
}
export function startStreaming(instance, options) {
if (options.streaming) {
if (!instance.getClientKey()) {
throw new Error("Must specify clientKey to enable streaming");
}
if (options.payload) {
startAutoRefresh(instance, true);
}
subscribe(instance);
}
}
//# sourceMappingURL=feature-repository.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
export { setPolyfills, clearCache, configureCache, helpers, onVisible, onHidden } from "./feature-repository.mjs";
export { GrowthBook, prefetchPayload } from "./GrowthBook.mjs";
export { GrowthBookClient as GrowthBookMultiUser, GrowthBookClient, UserScopedGrowthBook } from "./GrowthBookClient.mjs";
export { StickyBucketService, StickyBucketServiceSync, LocalStorageStickyBucketService, ExpressCookieStickyBucketService, BrowserCookieStickyBucketService, RedisStickyBucketService } from "./sticky-bucket-service.mjs";
export { evalCondition } from "./mongrule.mjs";
export { isURLTargeted, getPolyfills, getAutoExperimentChangeType, paddedVersionString } from "./util.mjs";
export { EVENT_EXPERIMENT_VIEWED, EVENT_FEATURE_EVALUATED } from "./core.mjs";
//# sourceMappingURL=index.mjs.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.mjs","names":["setPolyfills","clearCache","configureCache","helpers","onVisible","onHidden","GrowthBook","prefetchPayload","GrowthBookClient","GrowthBookMultiUser","UserScopedGrowthBook","StickyBucketService","StickyBucketServiceSync","LocalStorageStickyBucketService","ExpressCookieStickyBucketService","BrowserCookieStickyBucketService","RedisStickyBucketService","evalCondition","isURLTargeted","getPolyfills","getAutoExperimentChangeType","paddedVersionString","EVENT_EXPERIMENT_VIEWED","EVENT_FEATURE_EVALUATED"],"sources":["../../src/index.ts"],"sourcesContent":["export type {\n Options as Context,\n Options,\n ClientOptions as MultiUserOptions,\n ClientOptions,\n TrackingCallbackWithUser,\n TrackingDataWithUser,\n FeatureUsageCallback,\n FeatureUsageCallbackWithUser,\n UserContext,\n Attributes,\n Polyfills,\n CacheSettings,\n FeatureApiResponse,\n LoadFeaturesOptions,\n RefreshFeaturesOptions,\n DestroyOptions,\n FeatureDefinitions,\n FeatureDefinition,\n FeatureRule,\n FeatureResult,\n FeatureResultSource,\n Experiment,\n Result,\n ExperimentOverride,\n ExperimentStatus,\n JSONValue,\n SubscriptionFunction,\n LocalStorageCompat,\n WidenPrimitives,\n VariationMeta,\n Filter,\n VariationRange,\n UrlTarget,\n AutoExperiment,\n AutoExperimentVariation,\n AutoExperimentChangeType,\n DOMMutation,\n UrlTargetType,\n RenderFunction,\n StickyAttributeKey,\n StickyExperimentKey,\n StickyAssignments,\n StickyAssignmentsDocument,\n TrackingData,\n TrackingCallback,\n NavigateCallback,\n ApplyDomChangesCallback,\n InitOptions,\n PrefetchOptions,\n InitResponse,\n InitSyncOptions,\n Helpers,\n GrowthBookPayload,\n SavedGroupsValues,\n EventLogger,\n EventProperties,\n Plugin,\n LogUnion,\n} from \"./types/growthbook\";\n\nexport type {\n ConditionInterface,\n ParentConditionInterface,\n} from \"./types/mongrule\";\n\nexport {\n setPolyfills,\n clearCache,\n configureCache,\n helpers,\n onVisible,\n onHidden,\n} from \"./feature-repository\";\n\nexport { GrowthBook, prefetchPayload } from \"./GrowthBook\";\n\nexport {\n GrowthBookClient as GrowthBookMultiUser,\n GrowthBookClient,\n UserScopedGrowthBook,\n} from \"./GrowthBookClient\";\n\nexport {\n StickyBucketService,\n StickyBucketServiceSync,\n LocalStorageStickyBucketService,\n ExpressCookieStickyBucketService,\n BrowserCookieStickyBucketService,\n RedisStickyBucketService,\n} from \"./sticky-bucket-service\";\n\nexport { evalCondition } from \"./mongrule\";\n\nexport {\n isURLTargeted,\n getPolyfills,\n getAutoExperimentChangeType,\n paddedVersionString,\n} from \"./util\";\n\nexport { EVENT_EXPERIMENT_VIEWED, EVENT_FEATURE_EVALUATED } from \"./core\";\n"],"mappings":"AAkEA,SACEA,YAAY,EACZC,UAAU,EACVC,cAAc,EACdC,OAAO,EACPC,SAAS,EACTC,QAAQ,QACH,0BAAsB;AAE7B,SAASC,UAAU,EAAEC,eAAe,QAAQ,kBAAc;AAE1D,SACEC,gBAAgB,IAAIC,mBAAmB,EACvCD,gBAAgB,EAChBE,oBAAoB,QACf,wBAAoB;AAE3B,SACEC,mBAAmB,EACnBC,uBAAuB,EACvBC,+BAA+B,EAC/BC,gCAAgC,EAChCC,gCAAgC,EAChCC,wBAAwB,QACnB,6BAAyB;AAEhC,SAASC,aAAa,QAAQ,gBAAY;AAE1C,SACEC,aAAa,EACbC,YAAY,EACZC,2BAA2B,EAC3BC,mBAAmB,QACd,YAAQ;AAEf,SAASC,uBAAuB,EAAEC,uBAAuB,QAAQ,YAAQ","ignoreList":[]}

View File

@@ -0,0 +1,245 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { paddedVersionString } from "./util.mjs";
const _regexCache = {};
// The top-level condition evaluation function
export function evalCondition(obj, condition,
// Must be included for `condition` to correctly evaluate group Operators
savedGroups) {
savedGroups = savedGroups || {};
// Condition is an object, keys are either specific operators or object paths
// values are either arguments for operators or conditions for paths
for (const [k, v] of Object.entries(condition)) {
switch (k) {
case "$or":
if (!evalOr(obj, v, savedGroups)) return false;
break;
case "$nor":
if (evalOr(obj, v, savedGroups)) return false;
break;
case "$and":
if (!evalAnd(obj, v, savedGroups)) return false;
break;
case "$not":
if (evalCondition(obj, v, savedGroups)) return false;
break;
default:
if (!evalConditionValue(v, getPath(obj, k), savedGroups)) return false;
}
}
return true;
}
// Return value at dot-separated path of an object
function getPath(obj, path) {
const parts = path.split(".");
let current = obj;
for (let i = 0; i < parts.length; i++) {
if (current && typeof current === "object" && parts[i] in current) {
current = current[parts[i]];
} else {
return null;
}
}
return current;
}
// Transform a regex string into a real RegExp object
function getRegex(regex, insensitive = false) {
const cacheKey = `${regex}${insensitive ? "/i" : ""}`;
if (!_regexCache[cacheKey]) {
_regexCache[cacheKey] = new RegExp(regex.replace(/([^\\])\//g, "$1\\/"), insensitive ? "i" : undefined);
}
return _regexCache[cacheKey];
}
// Evaluate a single value against a condition
function evalConditionValue(condition, value, savedGroups, insensitive = false) {
// Simple equality comparisons
if (typeof condition === "string") {
if (insensitive) {
return String(value).toLowerCase() === condition.toLowerCase();
}
return value + "" === condition;
}
if (typeof condition === "number") {
return value * 1 === condition;
}
if (typeof condition === "boolean") {
return value !== null && !!value === condition;
}
if (condition === null) {
return value === null;
}
if (Array.isArray(condition) || !isOperatorObject(condition)) {
return JSON.stringify(value) === JSON.stringify(condition);
}
// This is a special operator condition and we should evaluate each one separately
for (const op in condition) {
if (!evalOperatorCondition(op, value, condition[op], savedGroups)) {
return false;
}
}
return true;
}
// If the object has only keys that start with '$'
function isOperatorObject(obj) {
const keys = Object.keys(obj);
return keys.length > 0 && keys.filter(k => k[0] === "$").length === keys.length;
}
// Return the data type of a value
function getType(v) {
if (v === null) return "null";
if (Array.isArray(v)) return "array";
const t = typeof v;
if (["string", "number", "boolean", "object", "undefined"].includes(t)) {
return t;
}
return "unknown";
}
// At least one element of actual must match the expected condition/value
function elemMatch(actual, expected, savedGroups) {
if (!Array.isArray(actual)) return false;
const check = isOperatorObject(expected) ? v => evalConditionValue(expected, v, savedGroups) : v => evalCondition(v, expected, savedGroups);
for (let i = 0; i < actual.length; i++) {
if (actual[i] && check(actual[i])) {
return true;
}
}
return false;
}
function isIn(actual, expected, insensitive = false) {
if (insensitive) {
const caseFold = val => typeof val === "string" ? val.toLowerCase() : val;
// Do an intersection if attribute is an array (insensitive)
if (Array.isArray(actual)) {
return actual.some(el => expected.some(exp => caseFold(el) === caseFold(exp)));
}
return expected.some(exp => caseFold(actual) === caseFold(exp));
}
// Do an intersection if attribute is an array
if (Array.isArray(actual)) {
return actual.some(el => expected.includes(el));
}
return expected.includes(actual);
}
function isInAll(actual, expected, savedGroups, insensitive = false) {
if (!Array.isArray(actual)) return false;
for (let i = 0; i < expected.length; i++) {
let passed = false;
for (let j = 0; j < actual.length; j++) {
if (evalConditionValue(expected[i], actual[j], savedGroups, insensitive)) {
passed = true;
break;
}
}
if (!passed) return false;
}
return true;
}
// Evaluate a single operator condition
function evalOperatorCondition(operator, actual, expected, savedGroups) {
switch (operator) {
case "$veq":
return paddedVersionString(actual) === paddedVersionString(expected);
case "$vne":
return paddedVersionString(actual) !== paddedVersionString(expected);
case "$vgt":
return paddedVersionString(actual) > paddedVersionString(expected);
case "$vgte":
return paddedVersionString(actual) >= paddedVersionString(expected);
case "$vlt":
return paddedVersionString(actual) < paddedVersionString(expected);
case "$vlte":
return paddedVersionString(actual) <= paddedVersionString(expected);
case "$eq":
return actual === expected;
case "$ne":
return actual !== expected;
case "$lt":
return actual < expected;
case "$lte":
return actual <= expected;
case "$gt":
return actual > expected;
case "$gte":
return actual >= expected;
case "$exists":
// Using `!=` and `==` instead of strict checks so it also matches for undefined
return expected ? actual != null : actual == null;
case "$in":
if (!Array.isArray(expected)) return false;
return isIn(actual, expected);
case "$ini":
if (!Array.isArray(expected)) return false;
return isIn(actual, expected, true);
case "$inGroup":
return isIn(actual, savedGroups[expected] || []);
case "$notInGroup":
return !isIn(actual, savedGroups[expected] || []);
case "$nin":
if (!Array.isArray(expected)) return false;
return !isIn(actual, expected);
case "$nini":
if (!Array.isArray(expected)) return false;
return !isIn(actual, expected, true);
case "$not":
return !evalConditionValue(expected, actual, savedGroups);
case "$size":
if (!Array.isArray(actual)) return false;
return evalConditionValue(expected, actual.length, savedGroups);
case "$elemMatch":
return elemMatch(actual, expected, savedGroups);
case "$all":
if (!Array.isArray(expected)) return false;
return isInAll(actual, expected, savedGroups);
case "$alli":
if (!Array.isArray(expected)) return false;
return isInAll(actual, expected, savedGroups, true);
case "$regex":
try {
return getRegex(expected).test(actual);
} catch (e) {
return false;
}
case "$regexi":
try {
return getRegex(expected, true).test(actual);
} catch (e) {
return false;
}
case "$type":
return getType(actual) === expected;
default:
console.error("Unknown operator: " + operator);
return false;
}
}
// Recursive $or rule
function evalOr(obj, conditions, savedGroups) {
if (!conditions.length) return true;
for (let i = 0; i < conditions.length; i++) {
if (evalCondition(obj, conditions[i], savedGroups)) {
return true;
}
}
return false;
}
// Recursive $and rule
function evalAnd(obj, conditions, savedGroups) {
for (let i = 0; i < conditions.length; i++) {
if (!evalCondition(obj, conditions[i], savedGroups)) {
return false;
}
}
return true;
}
//# sourceMappingURL=mongrule.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,185 @@
function getBrowserDevice(ua) {
const browser = ua.match(/Edg/) ? "edge" : ua.match(/Chrome/) ? "chrome" : ua.match(/Firefox/) ? "firefox" : ua.match(/Safari/) ? "safari" : "unknown";
const deviceType = ua.match(/Mobi/) ? "mobile" : "desktop";
return {
browser,
deviceType
};
}
function getURLAttributes(url) {
if (!url) return {};
return {
url: url.href,
path: url.pathname,
host: url.host,
query: url.search
};
}
export function autoAttributesPlugin(settings = {}) {
// Browser only
if (typeof window === "undefined") {
throw new Error("autoAttributesPlugin only works in the browser");
}
const COOKIE_NAME = settings.uuidCookieName || "gbuuid";
const uuidKey = settings.uuidKey || "id";
let uuid = settings.uuid || "";
function persistUUID() {
setCookie(COOKIE_NAME, uuid);
}
function getUUID() {
// Already stored in memory, return
if (uuid) return uuid;
// If cookie is already set, return
uuid = getCookie(COOKIE_NAME);
if (uuid) return uuid;
// Generate a new UUID
uuid = genUUID(window.crypto);
return uuid;
}
// Listen for a custom event to persist the UUID cookie
document.addEventListener("growthbookpersist", () => {
persistUUID();
});
function getAutoAttributes(settings) {
const ua = navigator.userAgent;
const _uuid = getUUID();
// If a uuid is provided, default persist to false, otherwise default to true
if (settings.uuidAutoPersist ?? !settings.uuid) {
persistUUID();
}
const url = location;
return {
...getDataLayerVariables(),
[uuidKey]: _uuid,
...getURLAttributes(url),
pageTitle: document.title,
...getBrowserDevice(ua),
...getUtmAttributes(url)
};
}
return gb => {
// Only works for instances with user attributes
if ("createScopedInstance" in gb) {
return;
}
// Set initial attributes
const attributes = getAutoAttributes(settings);
attributes.url && gb.setURL(attributes.url);
gb.updateAttributes(attributes);
// Poll for URL changes and update GrowthBook
let currentUrl = attributes.url;
const intervalTimer = setInterval(() => {
if (location.href !== currentUrl) {
currentUrl = location.href;
gb.setURL(currentUrl);
gb.updateAttributes(getAutoAttributes(settings));
}
}, 500);
// Listen for a custom event to update URL and attributes
const refreshListener = () => {
if (location.href !== currentUrl) {
currentUrl = location.href;
gb.setURL(currentUrl);
}
gb.updateAttributes(getAutoAttributes(settings));
};
document.addEventListener("growthbookrefresh", refreshListener);
if ("onDestroy" in gb) {
gb.onDestroy(() => {
clearInterval(intervalTimer);
document.removeEventListener("growthbookrefresh", refreshListener);
});
}
};
}
function setCookie(name, value) {
const d = new Date();
const COOKIE_DAYS = 400; // 400 days is the max cookie duration for chrome
d.setTime(d.getTime() + 24 * 60 * 60 * 1000 * COOKIE_DAYS);
document.cookie = name + "=" + value + ";path=/;expires=" + d.toUTCString();
}
function getCookie(name) {
const value = "; " + document.cookie;
const parts = value.split(`; ${name}=`);
return parts.length === 2 ? parts[1].split(";")[0] : "";
}
// Use the browsers crypto.randomUUID if set to generate a UUID
function genUUID(crypto) {
if (crypto && crypto.randomUUID) return crypto.randomUUID();
return ("" + 1e7 + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => {
const n = crypto && crypto.getRandomValues ? crypto.getRandomValues(new Uint8Array(1))[0] : Math.floor(Math.random() * 256);
return (c ^ n & 15 >> c / 4).toString(16);
});
}
function getUtmAttributes(url) {
// Store utm- params in sessionStorage for future page loads
let utms = {};
try {
const existing = sessionStorage.getItem("utm_params");
if (existing) {
utms = JSON.parse(existing);
}
} catch (e) {
// Do nothing if sessionStorage is disabled (e.g. incognito window)
}
// Add utm params from querystring
if (url && url.search) {
const params = new URLSearchParams(url.search);
let hasChanges = false;
["source", "medium", "campaign", "term", "content"].forEach(k => {
// Querystring is in snake_case
const param = `utm_${k}`;
// Attribute keys are camelCase
const attr = `utm` + k[0].toUpperCase() + k.slice(1);
if (params.has(param)) {
utms[attr] = params.get(param) || "";
hasChanges = true;
}
});
// Write back to sessionStorage
if (hasChanges) {
try {
sessionStorage.setItem("utm_params", JSON.stringify(utms));
} catch (e) {
// Do nothing if sessionStorage is disabled (e.g. incognito window)
}
}
}
return utms;
}
function getDataLayerVariables() {
if (typeof window === "undefined" || !window.dataLayer || !window.dataLayer.forEach) {
return {};
}
const obj = {};
window.dataLayer.forEach(item => {
// Skip empty and non-object entries
if (!item || typeof item !== "object" || "length" in item) return;
// Skip events
if ("event" in item) return;
Object.keys(item).forEach(k => {
// Filter out known properties that aren't useful
if (typeof k !== "string" || k.match(/^(gtm)/)) return;
const val = item[k];
// Only add primitive variable values
const valueType = typeof val;
if (["string", "number", "boolean"].includes(valueType)) {
obj[k] = val;
}
});
});
return obj;
}
//# sourceMappingURL=auto-attributes.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,162 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { GrowthBook } from "../GrowthBook.mjs";
import { UserScopedGrowthBook } from "../GrowthBookClient.mjs";
function applyDevtoolsState(devtoolsState, gb) {
// Only enable in dev mode
if (!gb.inDevMode()) {
return;
}
if (devtoolsState.attributes && typeof devtoolsState.attributes === "object") {
gb.setAttributeOverrides(devtoolsState.attributes);
}
if (devtoolsState.features && typeof devtoolsState.features === "object") {
const map = new Map(Object.entries(devtoolsState.features));
gb.setForcedFeatures(map);
}
if (devtoolsState.experiments && typeof devtoolsState.experiments === "object") {
gb.setForcedVariations(devtoolsState.experiments);
}
}
export function devtoolsPlugin(devtoolsState) {
return gb => {
// Only works for user-scoped GrowthBook instances
if ("createScopedInstance" in gb) {
throw new Error("devtoolsPlugin can only be set on a user-scoped instance");
}
if (devtoolsState) {
applyDevtoolsState(devtoolsState, gb);
}
};
}
/**
* For NextJS environments.
* When using server components, use the `searchParams` and `requestCookies` fields.
* - Note: In NextJS 15+, you should await these values before passing them to the plugin
* When using middleware / api routes, provide the `request` field instead.
*/
export function devtoolsNextjsPlugin({
searchParams,
requestCookies,
request
}) {
function extractGbDebugPayload({
searchParams,
requestCookies
}) {
if (searchParams) {
if ("_gbdebug" in searchParams) {
return searchParams._gbdebug;
}
if (searchParams instanceof URLSearchParams) {
return searchParams.get("_gbdebug") ?? undefined;
}
}
return requestCookies?.get("_gbdebug")?.value;
}
return gb => {
let payload = extractGbDebugPayload({
searchParams,
requestCookies
});
if (!payload && request) {
payload = extractGbDebugPayload({
searchParams: request.nextUrl.searchParams,
requestCookies: request.cookies
});
}
let state = {};
if (payload) {
try {
state = JSON.parse(payload);
} catch (e) {
console.error("cannot parse devtools payload", e);
}
}
devtoolsPlugin(state)(gb);
};
}
/**
* Intended to be used with cookieParser() middleware from npm: 'cookie-parser'.
*/
export function devtoolsExpressPlugin({
request
}) {
return gb => {
let payload = typeof request?.query?.["_gbdebug"] === "string" ? request.query["_gbdebug"] : undefined;
if (!payload) {
payload = typeof request?.cookies?.["_gbdebug"] === "string" ? request.cookies["_gbdebug"] : undefined;
}
let state = {};
if (payload) {
try {
state = JSON.parse(payload);
} catch (e) {
console.error("cannot parse devtools payload", e);
}
}
devtoolsPlugin(state)(gb);
};
}
/**
* Helper method to get debug script contents for DevTools
* @param gb - GrowthBook instance. DevMode must be enabled to view log events.
* @param {string} [source] - Label these events for ease of reading in DevTools
* @example
* A React logger component (implement yourself):
```
return (
<script dangerouslySetInnerHTML={{
__html: getDebugScriptContents(gb, "nextjs")
}} />
);
```
*/
export function getDebugScriptContents(gb, source) {
const event = getDebugEvent(gb, source);
if (!event) return "";
return `(window._gbdebugEvents = (window._gbdebugEvents || [])).push(${JSON.stringify(event)});`;
}
export function getDebugEvent(gb, source) {
if (!("logs" in gb)) return null;
// Only enable in dev mode
if (!gb.inDevMode()) {
return null;
}
if (gb instanceof GrowthBook) {
// GrowthBook SDK
const [apiHost, clientKey] = gb.getApiInfo();
return {
logs: gb.logs,
sdkInfo: {
apiHost,
clientKey,
source,
version: gb.version,
payload: gb.getDecryptedPayload(),
attributes: gb.getAttributes()
}
};
} else if (gb instanceof UserScopedGrowthBook) {
// UserScopedGrowthBook SDK
const userContext = gb.getUserContext();
const [apiHost, clientKey] = gb.getApiInfo();
return {
logs: gb.logs,
sdkInfo: {
apiHost,
clientKey,
source,
version: gb.getVersion(),
payload: gb.getDecryptedPayload(),
attributes: {
...userContext.attributes,
...userContext.attributeOverrides
}
}
};
}
return null;
}
//# sourceMappingURL=devtools.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,209 @@
import { loadSDKVersion } from "../util.mjs";
import { EVENT_EXPERIMENT_VIEWED, EVENT_FEATURE_EVALUATED } from "../core.mjs";
const SDK_VERSION = loadSDKVersion();
function parseString(value) {
return typeof value === "string" ? value : null;
}
function parseAttributes(attributes) {
const {
user_id,
device_id,
anonymous_id,
id,
page_id,
session_id,
utmCampaign,
utmContent,
utmMedium,
utmSource,
utmTerm,
pageTitle,
...nested
} = attributes;
return {
nested,
topLevel: {
user_id: parseString(user_id),
device_id: parseString(device_id || anonymous_id || id),
page_id: parseString(page_id),
session_id: parseString(session_id),
utm_campaign: parseString(utmCampaign) || undefined,
utm_content: parseString(utmContent) || undefined,
utm_medium: parseString(utmMedium) || undefined,
utm_source: parseString(utmSource) || undefined,
utm_term: parseString(utmTerm) || undefined,
page_title: parseString(pageTitle) || undefined
}
};
}
function getEventPayload({
eventName,
properties,
attributes,
url
}) {
const {
nested,
topLevel
} = parseAttributes(attributes || {});
return {
event_name: eventName,
properties_json: properties || {},
...topLevel,
sdk_language: "js",
sdk_version: SDK_VERSION,
url: url,
context_json: nested
};
}
async function track({
clientKey,
ingestorHost,
events
}) {
if (!events.length) return;
const endpoint = `${ingestorHost || "https://us1.gb-ingest.com"}/track?client_key=${clientKey}`;
const body = JSON.stringify(events);
try {
await fetch(endpoint, {
method: "POST",
body,
headers: {
Accept: "application/json",
"Content-Type": "text/plain"
},
credentials: "omit"
});
} catch (e) {
console.error("Failed to track event", e);
}
}
export function growthbookTrackingPlugin({
queueFlushInterval = 100,
ingestorHost,
enable = true,
debug,
dedupeCacheSize = 1000,
dedupeKeyAttributes = [],
eventFilter
} = {}) {
return gb => {
const clientKey = gb.getClientKey();
if (!clientKey) {
throw new Error("clientKey must be specified to use event logging");
}
// LRU cache for events to avoid duplicates
const eventCache = new Set();
if ("setEventLogger" in gb) {
let _q = [];
let timer = null;
const flush = async () => {
const events = _q;
_q = [];
timer && clearTimeout(timer);
timer = null;
events.length && (await track({
clientKey,
events,
ingestorHost
}));
};
let promise = null;
gb.setEventLogger(async (eventName, properties, userContext) => {
const data = {
eventName,
properties,
attributes: userContext.attributes || {},
url: userContext.url || ""
};
// Skip logging if the event is being filtered
if (eventFilter && !eventFilter(data)) {
return;
}
// De-dupe Feature Evaluated and Experiment Viewed events
if (eventName === EVENT_FEATURE_EVALUATED || eventName === EVENT_EXPERIMENT_VIEWED) {
// Build the key for de-duping
const dedupeKeyData = {
eventName,
properties
};
for (const key of dedupeKeyAttributes) {
dedupeKeyData["attr:" + key] = data.attributes[key];
}
const k = JSON.stringify(dedupeKeyData);
// Duplicate event fired recently, move to end of LRU cache and skip
if (eventCache.has(k)) {
eventCache.delete(k);
eventCache.add(k);
return;
}
eventCache.add(k);
// If the cache is too big, remove the oldest item
if (eventCache.size > dedupeCacheSize) {
const oldest = eventCache.values().next().value;
oldest && eventCache.delete(oldest);
}
}
const payload = getEventPayload(data);
debug && console.log("Logging event to GrowthBook", JSON.parse(JSON.stringify(payload)));
if (!enable) return;
_q.push(payload);
// Only one in-progress promise at a time
if (!promise) {
promise = new Promise((resolve, reject) => {
// Flush the queue after a delay
timer = setTimeout(() => {
flush().then(resolve).catch(reject);
promise = null;
}, queueFlushInterval);
});
}
await promise;
});
// Flush the queue on page unload
if (typeof document !== "undefined" && document.visibilityState) {
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
flush().catch(console.error);
}
});
}
// Flush the queue when the growthbook instance is destroyed
"onDestroy" in gb && gb.onDestroy(() => {
flush().catch(console.error);
});
}
// Listen on window.gbEvents.push if in a browser
// This makes it easier to integrate with Segment, GTM, etc.
if (typeof window !== "undefined" && !("createScopedInstance" in gb)) {
const prevEvents = Array.isArray(window.gbEvents) ? window.gbEvents : [];
window.gbEvents = {
push: event => {
if ("isDestroyed" in gb && gb.isDestroyed()) {
// If trying to log and the instance has been destroyed, switch back to just an array
// This will let the next GrowthBook instance pick it up
window.gbEvents = [event];
return;
}
if (typeof event === "string") {
gb.logEvent(event);
} else if (event) {
gb.logEvent(event.eventName, event.properties);
}
}
};
for (const event of prevEvents) {
window.gbEvents.push(event);
}
}
};
}
//# sourceMappingURL=growthbook-tracking.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
export { autoAttributesPlugin } from "./auto-attributes.mjs";
export { growthbookTrackingPlugin } from "./growthbook-tracking.mjs";
export { thirdPartyTrackingPlugin } from "./third-party-tracking.mjs";
export { devtoolsPlugin, devtoolsNextjsPlugin, devtoolsExpressPlugin, getDebugScriptContents, getDebugEvent } from "./devtools.mjs";
// Types must be exported separately, otherwise rollup includes them in the javascript output which breaks things
//# sourceMappingURL=index.mjs.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.mjs","names":["autoAttributesPlugin","growthbookTrackingPlugin","thirdPartyTrackingPlugin","devtoolsPlugin","devtoolsNextjsPlugin","devtoolsExpressPlugin","getDebugScriptContents","getDebugEvent"],"sources":["../../../src/plugins/index.ts"],"sourcesContent":["export { autoAttributesPlugin } from \"./auto-attributes\";\nexport { growthbookTrackingPlugin } from \"./growthbook-tracking\";\nexport { thirdPartyTrackingPlugin } from \"./third-party-tracking\";\nexport {\n devtoolsPlugin,\n devtoolsNextjsPlugin,\n devtoolsExpressPlugin,\n getDebugScriptContents,\n getDebugEvent,\n} from \"./devtools\";\n\n// Types must be exported separately, otherwise rollup includes them in the javascript output which breaks things\nexport type {\n DevtoolsState,\n ExpressRequestCompat,\n NextjsReadonlyRequestCookiesCompat,\n NextjsRequestCompat,\n LogEvent,\n SdkInfo,\n} from \"./devtools\";\n"],"mappings":"AAAA,SAASA,oBAAoB,QAAQ,uBAAmB;AACxD,SAASC,wBAAwB,QAAQ,2BAAuB;AAChE,SAASC,wBAAwB,QAAQ,4BAAwB;AACjE,SACEC,cAAc,EACdC,oBAAoB,EACpBC,qBAAqB,EACrBC,sBAAsB,EACtBC,aAAa,QACR,gBAAY;;AAEnB","ignoreList":[]}

View File

@@ -0,0 +1,57 @@
export function thirdPartyTrackingPlugin({
additionalCallback,
trackers = ["gtag", "gtm", "segment"]
} = {}) {
// Browser only
if (typeof window === "undefined") {
throw new Error("thirdPartyTrackingPlugin only works in the browser");
}
return gb => {
gb.setTrackingCallback(async (e, r) => {
const promises = [];
const eventParams = {
experiment_id: e.key,
variation_id: r.key
};
if (additionalCallback) {
promises.push(Promise.resolve(additionalCallback(e, r)));
}
// GA4 - gtag
if (trackers.includes("gtag") && window.gtag) {
let gtagResolve;
const gtagPromise = new Promise(resolve => {
gtagResolve = resolve;
});
promises.push(gtagPromise);
window.gtag("event", "experiment_viewed", {
...eventParams,
event_callback: gtagResolve
});
}
// GTM - dataLayer
if (trackers.includes("gtm") && window.dataLayer) {
let datalayerResolve;
const datalayerPromise = new Promise(resolve => {
datalayerResolve = resolve;
});
promises.push(datalayerPromise);
window.dataLayer.push({
event: "experiment_viewed",
...eventParams,
eventCallback: datalayerResolve
});
}
// Segment - analytics.js
if (trackers.includes("segment") && window.analytics && window.analytics.track) {
window.analytics.track("Experiment Viewed", eventParams);
const segmentPromise = new Promise(resolve => window.setTimeout(resolve, 300));
promises.push(segmentPromise);
}
await Promise.all(promises);
});
};
}
//# sourceMappingURL=third-party-tracking.mjs.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"third-party-tracking.mjs","names":["thirdPartyTrackingPlugin","additionalCallback","trackers","window","Error","gb","setTrackingCallback","e","r","promises","eventParams","experiment_id","key","variation_id","push","Promise","resolve","includes","gtag","gtagResolve","gtagPromise","event_callback","dataLayer","datalayerResolve","datalayerPromise","event","eventCallback","analytics","track","segmentPromise","setTimeout","all"],"sources":["../../../src/plugins/third-party-tracking.ts"],"sourcesContent":["import type { TrackingCallback } from \"../types/growthbook\";\nimport type { GrowthBook } from \"../GrowthBook\";\nimport type {\n GrowthBookClient,\n UserScopedGrowthBook,\n} from \"../GrowthBookClient\";\n\nexport type Trackers = \"gtag\" | \"gtm\" | \"segment\";\n\nexport function thirdPartyTrackingPlugin({\n additionalCallback,\n trackers = [\"gtag\", \"gtm\", \"segment\"],\n}: {\n additionalCallback?: TrackingCallback;\n trackers?: Trackers[];\n} = {}) {\n // Browser only\n if (typeof window === \"undefined\") {\n throw new Error(\"thirdPartyTrackingPlugin only works in the browser\");\n }\n\n return (gb: GrowthBook | UserScopedGrowthBook | GrowthBookClient) => {\n gb.setTrackingCallback(async (e, r) => {\n const promises: Promise<unknown>[] = [];\n const eventParams = { experiment_id: e.key, variation_id: r.key };\n\n if (additionalCallback) {\n promises.push(Promise.resolve(additionalCallback(e, r)));\n }\n\n // GA4 - gtag\n if (trackers.includes(\"gtag\") && window.gtag) {\n let gtagResolve;\n const gtagPromise = new Promise((resolve) => {\n gtagResolve = resolve;\n });\n promises.push(gtagPromise);\n window.gtag(\"event\", \"experiment_viewed\", {\n ...eventParams,\n event_callback: gtagResolve,\n });\n }\n\n // GTM - dataLayer\n if (trackers.includes(\"gtm\") && window.dataLayer) {\n let datalayerResolve;\n const datalayerPromise = new Promise((resolve) => {\n datalayerResolve = resolve;\n });\n promises.push(datalayerPromise);\n window.dataLayer.push({\n event: \"experiment_viewed\",\n ...eventParams,\n eventCallback: datalayerResolve,\n });\n }\n\n // Segment - analytics.js\n if (\n trackers.includes(\"segment\") &&\n window.analytics &&\n window.analytics.track\n ) {\n window.analytics.track(\"Experiment Viewed\", eventParams);\n const segmentPromise = new Promise((resolve) =>\n window.setTimeout(resolve, 300),\n );\n promises.push(segmentPromise);\n }\n\n await Promise.all(promises);\n });\n };\n}\n"],"mappings":"AASA,OAAO,SAASA,wBAAwBA,CAAC;EACvCC,kBAAkB;EAClBC,QAAQ,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,SAAS;AAItC,CAAC,GAAG,CAAC,CAAC,EAAE;EACN;EACA,IAAI,OAAOC,MAAM,KAAK,WAAW,EAAE;IACjC,MAAM,IAAIC,KAAK,CAAC,oDAAoD,CAAC;EACvE;EAEA,OAAQC,EAAwD,IAAK;IACnEA,EAAE,CAACC,mBAAmB,CAAC,OAAOC,CAAC,EAAEC,CAAC,KAAK;MACrC,MAAMC,QAA4B,GAAG,EAAE;MACvC,MAAMC,WAAW,GAAG;QAAEC,aAAa,EAAEJ,CAAC,CAACK,GAAG;QAAEC,YAAY,EAAEL,CAAC,CAACI;MAAI,CAAC;MAEjE,IAAIX,kBAAkB,EAAE;QACtBQ,QAAQ,CAACK,IAAI,CAACC,OAAO,CAACC,OAAO,CAACf,kBAAkB,CAACM,CAAC,EAAEC,CAAC,CAAC,CAAC,CAAC;MAC1D;;MAEA;MACA,IAAIN,QAAQ,CAACe,QAAQ,CAAC,MAAM,CAAC,IAAId,MAAM,CAACe,IAAI,EAAE;QAC5C,IAAIC,WAAW;QACf,MAAMC,WAAW,GAAG,IAAIL,OAAO,CAAEC,OAAO,IAAK;UAC3CG,WAAW,GAAGH,OAAO;QACvB,CAAC,CAAC;QACFP,QAAQ,CAACK,IAAI,CAACM,WAAW,CAAC;QAC1BjB,MAAM,CAACe,IAAI,CAAC,OAAO,EAAE,mBAAmB,EAAE;UACxC,GAAGR,WAAW;UACdW,cAAc,EAAEF;QAClB,CAAC,CAAC;MACJ;;MAEA;MACA,IAAIjB,QAAQ,CAACe,QAAQ,CAAC,KAAK,CAAC,IAAId,MAAM,CAACmB,SAAS,EAAE;QAChD,IAAIC,gBAAgB;QACpB,MAAMC,gBAAgB,GAAG,IAAIT,OAAO,CAAEC,OAAO,IAAK;UAChDO,gBAAgB,GAAGP,OAAO;QAC5B,CAAC,CAAC;QACFP,QAAQ,CAACK,IAAI,CAACU,gBAAgB,CAAC;QAC/BrB,MAAM,CAACmB,SAAS,CAACR,IAAI,CAAC;UACpBW,KAAK,EAAE,mBAAmB;UAC1B,GAAGf,WAAW;UACdgB,aAAa,EAAEH;QACjB,CAAC,CAAC;MACJ;;MAEA;MACA,IACErB,QAAQ,CAACe,QAAQ,CAAC,SAAS,CAAC,IAC5Bd,MAAM,CAACwB,SAAS,IAChBxB,MAAM,CAACwB,SAAS,CAACC,KAAK,EACtB;QACAzB,MAAM,CAACwB,SAAS,CAACC,KAAK,CAAC,mBAAmB,EAAElB,WAAW,CAAC;QACxD,MAAMmB,cAAc,GAAG,IAAId,OAAO,CAAEC,OAAO,IACzCb,MAAM,CAAC2B,UAAU,CAACd,OAAO,EAAE,GAAG,CAChC,CAAC;QACDP,QAAQ,CAACK,IAAI,CAACe,cAAc,CAAC;MAC/B;MAEA,MAAMd,OAAO,CAACgB,GAAG,CAACtB,QAAQ,CAAC;IAC7B,CAAC,CAAC;EACJ,CAAC;AACH","ignoreList":[]}

View File

@@ -0,0 +1,210 @@
import { toString } from "./util.mjs";
import { getStickyBucketAttributeKey } from "./core.mjs";
/**
* Responsible for reading and writing documents which describe sticky bucket assignments.
*/
export class StickyBucketService {
constructor(opts) {
opts = opts || {};
this.prefix = opts.prefix || "";
}
/**
* The SDK calls getAllAssignments to populate sticky buckets. This in turn will
* typically loop through individual getAssignments calls. However, some StickyBucketService
* instances (i.e. Redis) will instead perform a multi-query inside getAllAssignments instead.
*/
async getAllAssignments(attributes) {
const docs = {};
(await Promise.all(Object.entries(attributes).map(([attributeName, attributeValue]) => this.getAssignments(attributeName, attributeValue)))).forEach(doc => {
if (doc) {
const key = getStickyBucketAttributeKey(doc.attributeName, doc.attributeValue);
docs[key] = doc;
}
});
return docs;
}
getKey(attributeName, attributeValue) {
return `${this.prefix}${attributeName}||${attributeValue}`;
}
}
export class StickyBucketServiceSync extends StickyBucketService {
async getAssignments(attributeName, attributeValue) {
return this.getAssignmentsSync(attributeName, attributeValue);
}
async saveAssignments(doc) {
this.saveAssignmentsSync(doc);
}
getAllAssignmentsSync(attributes) {
const docs = {};
Object.entries(attributes).map(([attributeName, attributeValue]) => this.getAssignmentsSync(attributeName, attributeValue)).forEach(doc => {
if (doc) {
const key = getStickyBucketAttributeKey(doc.attributeName, doc.attributeValue);
docs[key] = doc;
}
});
return docs;
}
}
export class LocalStorageStickyBucketService extends StickyBucketService {
constructor(opts) {
opts = opts || {};
super();
this.prefix = opts.prefix || "gbStickyBuckets__";
try {
this.localStorage = opts.localStorage || globalThis.localStorage;
} catch (e) {
// Ignore localStorage errors
}
}
async getAssignments(attributeName, attributeValue) {
const key = this.getKey(attributeName, attributeValue);
let doc = null;
if (!this.localStorage) return doc;
try {
const raw = (await this.localStorage.getItem(key)) || "{}";
const data = JSON.parse(raw);
if (data.attributeName && data.attributeValue && data.assignments) {
doc = data;
}
} catch (e) {
// Ignore localStorage errors
}
return doc;
}
async saveAssignments(doc) {
const key = this.getKey(doc.attributeName, doc.attributeValue);
if (!this.localStorage) return;
try {
await this.localStorage.setItem(key, JSON.stringify(doc));
} catch (e) {
// Ignore localStorage errors
}
}
}
export class ExpressCookieStickyBucketService extends StickyBucketServiceSync {
/**
* Intended to be used with cookieParser() middleware from npm: 'cookie-parser'.
* Assumes:
* - reading a cookie is automatically decoded via decodeURIComponent() or similar
* - writing a cookie name & value must be manually encoded via encodeURIComponent() or similar
* - all cookie bodies are JSON encoded strings and are manually encoded/decoded
*/
constructor({
prefix = "gbStickyBuckets__",
req,
res,
cookieAttributes = {
maxAge: 180 * 24 * 3600 * 1000
} // 180 days
}) {
super();
this.prefix = prefix;
this.req = req;
this.res = res;
this.cookieAttributes = cookieAttributes;
}
getAssignmentsSync(attributeName, attributeValue) {
const key = this.getKey(attributeName, attributeValue);
let doc = null;
if (!this.req) return doc;
try {
const raw = this.req.cookies[key] || "{}";
const data = JSON.parse(raw);
if (data.attributeName && data.attributeValue && data.assignments) {
doc = data;
}
} catch (e) {
// Ignore cookie errors
}
return doc;
}
saveAssignmentsSync(doc) {
const key = this.getKey(doc.attributeName, doc.attributeValue);
if (!this.res) return;
const str = JSON.stringify(doc);
this.res.cookie(encodeURIComponent(key), encodeURIComponent(str), this.cookieAttributes);
}
}
export class BrowserCookieStickyBucketService extends StickyBucketServiceSync {
/**
* Intended to be used with npm: 'js-cookie'.
* Assumes:
* - reading a cookie is automatically decoded via decodeURIComponent() or similar
* - writing a cookie name & value is automatically encoded via encodeURIComponent() or similar
* - all cookie bodies are JSON encoded strings and are manually encoded/decoded
*/
constructor({
prefix = "gbStickyBuckets__",
jsCookie,
cookieAttributes = {
expires: 180
} // 180 days
}) {
super();
this.prefix = prefix;
this.jsCookie = jsCookie;
this.cookieAttributes = cookieAttributes;
}
getAssignmentsSync(attributeName, attributeValue) {
const key = this.getKey(attributeName, attributeValue);
let doc = null;
if (!this.jsCookie) return doc;
try {
const raw = this.jsCookie.get(key);
const data = JSON.parse(raw || "{}");
if (data.attributeName && data.attributeValue && data.assignments) {
doc = data;
}
} catch (e) {
// Ignore cookie errors
}
return doc;
}
async saveAssignmentsSync(doc) {
const key = this.getKey(doc.attributeName, doc.attributeValue);
if (!this.jsCookie) return;
const str = JSON.stringify(doc);
this.jsCookie.set(key, str, this.cookieAttributes);
}
}
export class RedisStickyBucketService extends StickyBucketService {
/** Intended to be used with npm: 'ioredis'. **/
constructor({
redis
}) {
super();
this.redis = redis;
}
async getAllAssignments(attributes) {
const docs = {};
const keys = Object.entries(attributes).map(([attributeName, attributeValue]) => getStickyBucketAttributeKey(attributeName, attributeValue));
if (!this.redis) return docs;
await this.redis.mget(...keys).then(values => {
values.forEach(raw => {
try {
const data = JSON.parse(raw || "{}");
if (data.attributeName && "attributeValue" in data && data.assignments) {
const key = getStickyBucketAttributeKey(data.attributeName, toString(data.attributeValue));
docs[key] = data;
}
} catch (e) {
// ignore redis doc parse errors
}
});
});
return docs;
}
async getAssignments(_attributeName, _attributeValue) {
// not implemented
return null;
}
async saveAssignments(doc) {
const key = this.getKey(doc.attributeName, doc.attributeValue);
if (!this.redis) return;
await this.redis.set(key, JSON.stringify(doc));
}
}
//# sourceMappingURL=sticky-bucket-service.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=growthbook.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=mongrule.mjs.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"mongrule.mjs","names":[],"sources":["../../../src/types/mongrule.ts"],"sourcesContent":["type OrCondition = {\n $or: ConditionInterface[];\n};\ntype NorCondition = {\n $nor: ConditionInterface[];\n};\ntype AndCondition = {\n $and: ConditionInterface[];\n};\ntype NotCondition = {\n $not: ConditionInterface;\n};\nexport type Operator =\n | \"$in\"\n | \"$ini\"\n | \"$inGroup\"\n | \"$nin\"\n | \"$nini\"\n | \"$notInGroup\"\n | \"$gt\"\n | \"$gte\"\n | \"$lt\"\n | \"$lte\"\n | \"$regex\"\n | \"$regexi\"\n | \"$ne\"\n | \"$eq\"\n | \"$size\"\n | \"$elemMatch\"\n | \"$all\"\n | \"$alli\"\n | \"$not\"\n | \"$type\"\n | \"$exists\"\n | \"$vgt\"\n | \"$vgte\"\n | \"$vlt\"\n | \"$vlte\"\n | \"$vne\"\n | \"$veq\";\nexport type VarType =\n | \"string\"\n | \"number\"\n | \"boolean\"\n | \"array\"\n | \"object\"\n | \"null\"\n | \"undefined\";\nexport type OperatorConditionValue = {\n $in?: (string | number)[];\n $ini?: (string | number)[];\n $inGroup?: string;\n $nin?: (string | number)[];\n $nini?: (string | number)[];\n $notInGroup?: string;\n $gt?: number | string;\n $gte?: number | string;\n $lt?: number | string;\n $lte?: number | string;\n $regex?: string;\n $regexi?: string;\n $ne?: number | string;\n $eq?: number | string;\n $exists?: boolean;\n $all?: ConditionValue[];\n $alli?: ConditionValue[];\n $size?: number | ConditionValue;\n $type?: VarType;\n $elemMatch?: ConditionInterface | OperatorConditionValue;\n $not?: ConditionValue;\n};\n\nexport type ConditionValue =\n | OperatorConditionValue\n | string\n | number\n | boolean\n // eslint-disable-next-line\n | Array<any>\n // eslint-disable-next-line\n | Record<string, any>\n | null;\n\nexport type OperatorCondition = {\n [key: string]: ConditionValue;\n};\n\nexport type ConditionInterface =\n | OrCondition\n | NorCondition\n | AndCondition\n | NotCondition\n | OperatorCondition;\n\nexport type ParentConditionInterface = {\n id: string;\n condition: ConditionInterface;\n gate?: boolean;\n};\n\n// eslint-disable-next-line\nexport type TestedObj = Record<string, any>;\n"],"mappings":"","ignoreList":[]}

View File

@@ -0,0 +1,306 @@
const polyfills = {
fetch: globalThis.fetch ? globalThis.fetch.bind(globalThis) : undefined,
SubtleCrypto: globalThis.crypto ? globalThis.crypto.subtle : undefined,
EventSource: globalThis.EventSource
};
export function getPolyfills() {
return polyfills;
}
function hashFnv32a(str) {
let hval = 0x811c9dc5;
const l = str.length;
for (let i = 0; i < l; i++) {
hval ^= str.charCodeAt(i);
hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
}
return hval >>> 0;
}
export function hash(seed, value, version) {
// New unbiased hashing algorithm
if (version === 2) {
return hashFnv32a(hashFnv32a(seed + value) + "") % 10000 / 10000;
}
// Original biased hashing algorithm (keep for backwards compatibility)
if (version === 1) {
return hashFnv32a(value + seed) % 1000 / 1000;
}
// Unknown hash version
return null;
}
export function getEqualWeights(n) {
if (n <= 0) return [];
return new Array(n).fill(1 / n);
}
export function inRange(n, range) {
return n >= range[0] && n < range[1];
}
export function inNamespace(hashValue, namespace) {
const n = hash("__" + namespace[0], hashValue, 1);
if (n === null) return false;
return n >= namespace[1] && n < namespace[2];
}
export function chooseVariation(n, ranges) {
for (let i = 0; i < ranges.length; i++) {
if (inRange(n, ranges[i])) {
return i;
}
}
return -1;
}
export function getUrlRegExp(regexString) {
try {
const escaped = regexString.replace(/([^\\])\//g, "$1\\/");
return new RegExp(escaped);
} catch (e) {
console.error(e);
return undefined;
}
}
export function isURLTargeted(url, targets) {
if (!targets.length) return false;
let hasIncludeRules = false;
let isIncluded = false;
for (let i = 0; i < targets.length; i++) {
const match = _evalURLTarget(url, targets[i].type, targets[i].pattern);
if (targets[i].include === false) {
if (match) return false;
} else {
hasIncludeRules = true;
if (match) isIncluded = true;
}
}
return isIncluded || !hasIncludeRules;
}
function _evalSimpleUrlPart(actual, pattern, isPath) {
try {
// Escape special regex characters and change wildcard `_____` to `.*`
let escaped = pattern.replace(/[*.+?^${}()|[\]\\]/g, "\\$&").replace(/_____/g, ".*");
if (isPath) {
// When matching pathname, make leading/trailing slashes optional
escaped = "\\/?" + escaped.replace(/(^\/|\/$)/g, "") + "\\/?";
}
const regex = new RegExp("^" + escaped + "$", "i");
return regex.test(actual);
} catch (e) {
return false;
}
}
function _evalSimpleUrlTarget(actual, pattern) {
try {
// If a protocol is missing, but a host is specified, add `https://` to the front
// Use "_____" as the wildcard since `*` is not a valid hostname in some browsers
const expected = new URL(pattern.replace(/^([^:/?]*)\./i, "https://$1.").replace(/\*/g, "_____"), "https://_____");
// Compare each part of the URL separately
const comps = [[actual.host, expected.host, false], [actual.pathname, expected.pathname, true]];
// We only want to compare hashes if it's explicitly being targeted
if (expected.hash) {
comps.push([actual.hash, expected.hash, false]);
}
expected.searchParams.forEach((v, k) => {
comps.push([actual.searchParams.get(k) || "", v, false]);
});
// If any comparisons fail, the whole thing fails
return !comps.some(data => !_evalSimpleUrlPart(data[0], data[1], data[2]));
} catch (e) {
return false;
}
}
function _evalURLTarget(url, type, pattern) {
try {
const parsed = new URL(url, "https://_");
if (type === "regex") {
const regex = getUrlRegExp(pattern);
if (!regex) return false;
return regex.test(parsed.href) || regex.test(parsed.href.substring(parsed.origin.length));
} else if (type === "simple") {
return _evalSimpleUrlTarget(parsed, pattern);
}
return false;
} catch (e) {
return false;
}
}
export function getBucketRanges(numVariations, coverage, weights) {
coverage = coverage === undefined ? 1 : coverage;
// Make sure coverage is within bounds
if (coverage < 0) {
if (process.env.NODE_ENV !== "production") {
console.error("Experiment.coverage must be greater than or equal to 0");
}
coverage = 0;
} else if (coverage > 1) {
if (process.env.NODE_ENV !== "production") {
console.error("Experiment.coverage must be less than or equal to 1");
}
coverage = 1;
}
// Default to equal weights if missing or invalid
const equal = getEqualWeights(numVariations);
weights = weights || equal;
if (weights.length !== numVariations) {
if (process.env.NODE_ENV !== "production") {
console.error("Experiment.weights array must be the same length as Experiment.variations");
}
weights = equal;
}
// If weights don't add up to 1 (or close to it), default to equal weights
const totalWeight = weights.reduce((w, sum) => sum + w, 0);
if (totalWeight < 0.99 || totalWeight > 1.01) {
if (process.env.NODE_ENV !== "production") {
console.error("Experiment.weights must add up to 1");
}
weights = equal;
}
// Covert weights to ranges
let cumulative = 0;
return weights.map(w => {
const start = cumulative;
cumulative += w;
return [start, start + coverage * w];
});
}
export function getQueryStringOverride(id, url, numVariations) {
if (!url) {
return null;
}
const search = url.split("?")[1];
if (!search) {
return null;
}
const match = search.replace(/#.*/, "") // Get rid of anchor
.split("&") // Split into key/value pairs
.map(kv => kv.split("=", 2)).filter(([k]) => k === id) // Look for key that matches the experiment id
.map(([, v]) => parseInt(v)); // Parse the value into an integer
if (match.length > 0 && match[0] >= 0 && match[0] < numVariations) return match[0];
return null;
}
export function isIncluded(include) {
try {
return include();
} catch (e) {
console.error(e);
return false;
}
}
const base64ToBuf = b => Uint8Array.from(atob(b), c => c.charCodeAt(0));
export async function decrypt(encryptedString, decryptionKey, subtle) {
decryptionKey = decryptionKey || "";
subtle = subtle || globalThis.crypto && globalThis.crypto.subtle || polyfills.SubtleCrypto;
if (!subtle) {
throw new Error("No SubtleCrypto implementation found");
}
try {
const key = await subtle.importKey("raw", base64ToBuf(decryptionKey), {
name: "AES-CBC",
length: 128
}, true, ["encrypt", "decrypt"]);
const [iv, cipherText] = encryptedString.split(".");
const plainTextBuffer = await subtle.decrypt({
name: "AES-CBC",
iv: base64ToBuf(iv)
}, key, base64ToBuf(cipherText));
return new TextDecoder().decode(plainTextBuffer);
} catch (e) {
throw new Error("Failed to decrypt");
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function toString(input) {
if (typeof input === "string") return input;
return JSON.stringify(input);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function paddedVersionString(input) {
if (typeof input === "number") {
input = input + "";
}
if (!input || typeof input !== "string") {
input = "0";
}
// Remove build info and leading `v` if any
// Split version into parts (both core version numbers and pre-release tags)
// "v1.2.3-rc.1+build123" -> ["1","2","3","rc","1"]
const parts = input.replace(/(^v|\+.*$)/g, "").split(/[-.]/);
// If it's SemVer without a pre-release, add `~` to the end
// ["1","0","0"] -> ["1","0","0","~"]
// "~" is the largest ASCII character, so this will make "1.0.0" greater than "1.0.0-beta" for example
if (parts.length === 3) {
parts.push("~");
}
// Left pad each numeric part with spaces so string comparisons will work ("9">"10", but " 9"<"10")
// Then, join back together into a single string
return parts.map(v => v.match(/^[0-9]+$/) ? v.padStart(5, " ") : v).join("-");
}
export function loadSDKVersion() {
let version;
try {
// @ts-expect-error right-hand value to be replaced by build with string literal
version = "1.6.5";
} catch (e) {
version = "";
}
return version;
}
export function mergeQueryStrings(oldUrl, newUrl) {
let currUrl;
let redirectUrl;
try {
currUrl = new URL(oldUrl);
redirectUrl = new URL(newUrl);
} catch (e) {
console.error(`Unable to merge query strings: ${e}`);
return newUrl;
}
currUrl.searchParams.forEach((value, key) => {
// skip if search param already exists in redirectUrl
if (redirectUrl.searchParams.has(key)) {
return;
}
redirectUrl.searchParams.set(key, value);
});
return redirectUrl.toString();
}
function isObj(x) {
return typeof x === "object" && x !== null;
}
export function getAutoExperimentChangeType(exp) {
if (exp.urlPatterns && exp.variations.some(variation => isObj(variation) && "urlRedirect" in variation)) {
return "redirect";
} else if (exp.variations.some(variation => isObj(variation) && (variation.domMutations || "js" in variation || "css" in variation))) {
return "visual";
}
return "unknown";
}
// Guarantee the promise always resolves within {timeout} ms
// Resolved value will be `null` when there's an error or it takes too long
// Note: The promise will continue running in the background, even if the timeout is hit
export async function promiseTimeout(promise, timeout) {
return new Promise(resolve => {
let resolved = false;
let timer;
const finish = data => {
if (resolved) return;
resolved = true;
timer && clearTimeout(timer);
resolve(data || null);
};
if (timeout) {
timer = setTimeout(() => finish(), timeout);
}
promise.then(data => finish(data)).catch(() => finish());
});
}
//# sourceMappingURL=util.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,19 @@
import { CacheSettings, FetchResponse, Helpers, Polyfills } from "./types/growthbook";
import type { GrowthBook, InitOptions, InitSyncOptions, GrowthBookClient } from ".";
export declare const helpers: Helpers;
export declare function setPolyfills(overrides: Partial<Polyfills>): void;
export declare function configureCache(overrides: Partial<CacheSettings>): void;
export declare function clearCache(): Promise<void>;
export declare function refreshFeatures({ instance, timeout, skipCache, allowStale, backgroundSync, }: {
instance: GrowthBook | GrowthBookClient;
timeout?: number;
skipCache?: boolean;
allowStale?: boolean;
backgroundSync?: boolean;
}): Promise<FetchResponse>;
export declare function unsubscribe(instance: GrowthBook | GrowthBookClient): void;
export declare function onHidden(): void;
export declare function onVisible(): void;
export declare function clearAutoRefresh(): void;
export declare function startStreaming(instance: GrowthBook | GrowthBookClient, options: InitOptions | InitSyncOptions): void;
//# sourceMappingURL=feature-repository.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"feature-repository.d.ts","sourceRoot":"","sources":["../src/feature-repository.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,aAAa,EAEb,aAAa,EACb,OAAO,EACP,SAAS,EACV,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EACV,UAAU,EACV,WAAW,EACX,eAAe,EACf,gBAAgB,EACjB,MAAM,GAAG,CAAC;AAkCX,eAAO,MAAM,OAAO,EAAE,OAiDrB,CAAC;AAsBF,wBAAgB,YAAY,CAAC,SAAS,EAAE,OAAO,CAAC,SAAS,CAAC,GAAG,IAAI,CAEhE;AACD,wBAAgB,cAAc,CAAC,SAAS,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI,CAKtE;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAMhD;AAGD,wBAAsB,eAAe,CAAC,EACpC,QAAQ,EACR,OAAO,EACP,SAAS,EACT,UAAU,EACV,cAAc,GACf,EAAE;IACD,QAAQ,EAAE,UAAU,GAAG,gBAAgB,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,GAAG,OAAO,CAAC,aAAa,CAAC,CAWzB;AASD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,UAAU,GAAG,gBAAgB,GAAG,IAAI,CAEzE;AAED,wBAAgB,QAAQ,SAMvB;AAED,wBAAgB,SAAS,SAMxB;AA6WD,wBAAgB,gBAAgB,SAY/B;AAED,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,UAAU,GAAG,gBAAgB,EACvC,OAAO,EAAE,WAAW,GAAG,eAAe,QAWvC"}

View File

@@ -0,0 +1,10 @@
export type { Options as Context, Options, ClientOptions as MultiUserOptions, ClientOptions, TrackingCallbackWithUser, TrackingDataWithUser, FeatureUsageCallback, FeatureUsageCallbackWithUser, UserContext, Attributes, Polyfills, CacheSettings, FeatureApiResponse, LoadFeaturesOptions, RefreshFeaturesOptions, DestroyOptions, FeatureDefinitions, FeatureDefinition, FeatureRule, FeatureResult, FeatureResultSource, Experiment, Result, ExperimentOverride, ExperimentStatus, JSONValue, SubscriptionFunction, LocalStorageCompat, WidenPrimitives, VariationMeta, Filter, VariationRange, UrlTarget, AutoExperiment, AutoExperimentVariation, AutoExperimentChangeType, DOMMutation, UrlTargetType, RenderFunction, StickyAttributeKey, StickyExperimentKey, StickyAssignments, StickyAssignmentsDocument, TrackingData, TrackingCallback, NavigateCallback, ApplyDomChangesCallback, InitOptions, PrefetchOptions, InitResponse, InitSyncOptions, Helpers, GrowthBookPayload, SavedGroupsValues, EventLogger, EventProperties, Plugin, LogUnion, } from "./types/growthbook";
export type { ConditionInterface, ParentConditionInterface, } from "./types/mongrule";
export { setPolyfills, clearCache, configureCache, helpers, onVisible, onHidden, } from "./feature-repository";
export { GrowthBook, prefetchPayload } from "./GrowthBook";
export { GrowthBookClient as GrowthBookMultiUser, GrowthBookClient, UserScopedGrowthBook, } from "./GrowthBookClient";
export { StickyBucketService, StickyBucketServiceSync, LocalStorageStickyBucketService, ExpressCookieStickyBucketService, BrowserCookieStickyBucketService, RedisStickyBucketService, } from "./sticky-bucket-service";
export { evalCondition } from "./mongrule";
export { isURLTargeted, getPolyfills, getAutoExperimentChangeType, paddedVersionString, } from "./util";
export { EVENT_EXPERIMENT_VIEWED, EVENT_FEATURE_EVALUATED } from "./core";
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,OAAO,IAAI,OAAO,EAClB,OAAO,EACP,aAAa,IAAI,gBAAgB,EACjC,aAAa,EACb,wBAAwB,EACxB,oBAAoB,EACpB,oBAAoB,EACpB,4BAA4B,EAC5B,WAAW,EACX,UAAU,EACV,SAAS,EACT,aAAa,EACb,kBAAkB,EAClB,mBAAmB,EACnB,sBAAsB,EACtB,cAAc,EACd,kBAAkB,EAClB,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,mBAAmB,EACnB,UAAU,EACV,MAAM,EACN,kBAAkB,EAClB,gBAAgB,EAChB,SAAS,EACT,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,aAAa,EACb,MAAM,EACN,cAAc,EACd,SAAS,EACT,cAAc,EACd,uBAAuB,EACvB,wBAAwB,EACxB,WAAW,EACX,aAAa,EACb,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,yBAAyB,EACzB,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,uBAAuB,EACvB,WAAW,EACX,eAAe,EACf,YAAY,EACZ,eAAe,EACf,OAAO,EACP,iBAAiB,EACjB,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,MAAM,EACN,QAAQ,GACT,MAAM,oBAAoB,CAAC;AAE5B,YAAY,EACV,kBAAkB,EAClB,wBAAwB,GACzB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,YAAY,EACZ,UAAU,EACV,cAAc,EACd,OAAO,EACP,SAAS,EACT,QAAQ,GACT,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE3D,OAAO,EACL,gBAAgB,IAAI,mBAAmB,EACvC,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EACvB,+BAA+B,EAC/B,gCAAgC,EAChC,gCAAgC,EAChC,wBAAwB,GACzB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,OAAO,EACL,aAAa,EACb,YAAY,EACZ,2BAA2B,EAC3B,mBAAmB,GACpB,MAAM,QAAQ,CAAC;AAEhB,OAAO,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,QAAQ,CAAC"}

View File

@@ -0,0 +1,4 @@
import { SavedGroupsValues } from "./types/growthbook";
import { ConditionInterface, TestedObj } from "./types/mongrule";
export declare function evalCondition(obj: TestedObj, condition: ConditionInterface, savedGroups?: SavedGroupsValues): boolean;
//# sourceMappingURL=mongrule.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"mongrule.d.ts","sourceRoot":"","sources":["../src/mongrule.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EACL,kBAAkB,EAClB,SAAS,EAKV,MAAM,kBAAkB,CAAC;AAM1B,wBAAgB,aAAa,CAC3B,GAAG,EAAE,SAAS,EACd,SAAS,EAAE,kBAAkB,EAE7B,WAAW,CAAC,EAAE,iBAAiB,GAC9B,OAAO,CAwBT"}

View File

@@ -0,0 +1,10 @@
import type { GrowthBook } from "../GrowthBook";
import type { UserScopedGrowthBook, GrowthBookClient } from "../GrowthBookClient";
export type AutoAttributeSettings = {
uuidCookieName?: string;
uuidKey?: string;
uuid?: string;
uuidAutoPersist?: boolean;
};
export declare function autoAttributesPlugin(settings?: AutoAttributeSettings): (gb: GrowthBook | UserScopedGrowthBook | GrowthBookClient) => void;
//# sourceMappingURL=auto-attributes.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"auto-attributes.d.ts","sourceRoot":"","sources":["../../src/plugins/auto-attributes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EACV,oBAAoB,EACpB,gBAAgB,EACjB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,qBAAqB,GAAG;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AA4BF,wBAAgB,oBAAoB,CAAC,QAAQ,GAAE,qBAA0B,QAoD3D,UAAU,GAAG,oBAAoB,GAAG,gBAAgB,UAsCjE"}

View File

@@ -0,0 +1,79 @@
import { GrowthBook } from "../GrowthBook";
import { Attributes, FeatureApiResponse, LogUnion, Plugin } from "../types/growthbook";
import { UserScopedGrowthBook } from "../GrowthBookClient";
export type DevtoolsState = {
attributes?: Record<string, any>;
features?: Record<string, any>;
experiments?: Record<string, number>;
};
export interface NextjsReadonlyRequestCookiesCompat {
get: (name: string) => {
name: string;
value: string;
} | undefined;
}
export interface NextjsRequestCompat {
nextUrl: {
searchParams: URLSearchParams;
};
cookies: {
get: (name: string) => {
name: string;
value: string;
} | undefined;
};
}
export interface ExpressRequestCompat {
cookies: Record<string, string | string[]>;
query: Record<string, string>;
[key: string]: unknown;
}
export declare function devtoolsPlugin(devtoolsState?: DevtoolsState): Plugin;
/**
* For NextJS environments.
* When using server components, use the `searchParams` and `requestCookies` fields.
* - Note: In NextJS 15+, you should await these values before passing them to the plugin
* When using middleware / api routes, provide the `request` field instead.
*/
export declare function devtoolsNextjsPlugin({ searchParams, requestCookies, request, }: {
searchParams?: {
_gbdebug?: string;
};
requestCookies?: NextjsReadonlyRequestCookiesCompat;
request?: NextjsRequestCompat;
}): Plugin;
/**
* Intended to be used with cookieParser() middleware from npm: 'cookie-parser'.
*/
export declare function devtoolsExpressPlugin({ request, }: {
request?: ExpressRequestCompat;
}): Plugin;
export type SdkInfo = {
apiHost: string;
clientKey: string;
source?: string;
version?: string;
payload?: FeatureApiResponse;
attributes?: Attributes;
};
export type LogEvent = {
logs: LogUnion[];
sdkInfo?: SdkInfo;
};
/**
* Helper method to get debug script contents for DevTools
* @param gb - GrowthBook instance. DevMode must be enabled to view log events.
* @param {string} [source] - Label these events for ease of reading in DevTools
* @example
* A React logger component (implement yourself):
```
return (
<script dangerouslySetInnerHTML={{
__html: getDebugScriptContents(gb, "nextjs")
}} />
);
```
*/
export declare function getDebugScriptContents(gb: GrowthBook, source?: string): string;
export declare function getDebugEvent(gb: GrowthBook | UserScopedGrowthBook, source?: string): LogEvent | null;
//# sourceMappingURL=devtools.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"devtools.d.ts","sourceRoot":"","sources":["../../src/plugins/devtools.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,QAAQ,EACR,MAAM,EACP,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAoB,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAE7E,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC,CAAC;AAEF,MAAM,WAAW,kCAAkC;IACjD,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC;CACpE;AACD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE;QACP,YAAY,EAAE,eAAe,CAAC;KAC/B,CAAC;IACF,OAAO,EAAE;QACP,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,GAAG,SAAS,CAAC;KACpE,CAAC;CACH;AACD,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC3C,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AA6BD,wBAAgB,cAAc,CAAC,aAAa,CAAC,EAAE,aAAa,GAAG,MAAM,CAYpE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,YAAY,EACZ,cAAc,EACd,OAAO,GACR,EAAE;IACD,YAAY,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACrC,cAAc,CAAC,EAAE,kCAAkC,CAAC;IACpD,OAAO,CAAC,EAAE,mBAAmB,CAAC;CAC/B,GAAG,MAAM,CAwCT;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,OAAO,GACR,EAAE;IACD,OAAO,CAAC,EAAE,oBAAoB,CAAC;CAChC,GAAG,MAAM,CAwBT;AAED,MAAM,MAAM,OAAO,GAAG;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,CAAC;AACF,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AACF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,UAAU,EACd,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,CAMR;AAED,wBAAgB,aAAa,CAC3B,EAAE,EAAE,UAAU,GAAG,oBAAoB,EACrC,MAAM,CAAC,EAAE,MAAM,GACd,QAAQ,GAAG,IAAI,CAwCjB"}

View File

@@ -0,0 +1,31 @@
import type { Attributes, EventProperties } from "../types/growthbook";
import type { GrowthBook } from "../GrowthBook";
import type { GrowthBookClient, UserScopedGrowthBook } from "../GrowthBookClient";
type GlobalTrackedEvent = {
eventName: string;
properties: Record<string, unknown>;
};
declare global {
interface Window {
gbEvents?: (GlobalTrackedEvent | string)[] | {
push: (event: GlobalTrackedEvent | string) => void;
};
}
}
type EventData = {
eventName: string;
properties: EventProperties;
attributes: Attributes;
url: string;
};
export declare function growthbookTrackingPlugin({ queueFlushInterval, ingestorHost, enable, debug, dedupeCacheSize, dedupeKeyAttributes, eventFilter, }?: {
queueFlushInterval?: number;
ingestorHost?: string;
enable?: boolean;
debug?: boolean;
dedupeCacheSize?: number;
dedupeKeyAttributes?: string[];
eventFilter?: (event: EventData) => boolean;
}): (gb: GrowthBook | UserScopedGrowthBook | GrowthBookClient) => void;
export {};
//# sourceMappingURL=growthbook-tracking.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"growthbook-tracking.d.ts","sourceRoot":"","sources":["../../src/plugins/growthbook-tracking.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EACV,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,qBAAqB,CAAC;AAK7B,KAAK,kBAAkB,GAAG;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC,CAAC;AACF,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,QAAQ,CAAC,EACL,CAAC,kBAAkB,GAAG,MAAM,CAAC,EAAE,GAC/B;YACE,IAAI,EAAE,CAAC,KAAK,EAAE,kBAAkB,GAAG,MAAM,KAAK,IAAI,CAAC;SACpD,CAAC;KACP;CACF;AAyED,KAAK,SAAS,GAAG;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,eAAe,CAAC;IAC5B,UAAU,EAAE,UAAU,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAoDF,wBAAgB,wBAAwB,CAAC,EACvC,kBAAwB,EACxB,YAAY,EACZ,MAAa,EACb,KAAK,EACL,eAAsB,EACtB,mBAAwB,EACxB,WAAW,GACZ,GAAE;IAED,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,OAAO,CAAC;CACxC,QACQ,UAAU,GAAG,oBAAoB,GAAG,gBAAgB,UAiIjE"}

View File

@@ -0,0 +1,6 @@
export { autoAttributesPlugin } from "./auto-attributes";
export { growthbookTrackingPlugin } from "./growthbook-tracking";
export { thirdPartyTrackingPlugin } from "./third-party-tracking";
export { devtoolsPlugin, devtoolsNextjsPlugin, devtoolsExpressPlugin, getDebugScriptContents, getDebugEvent, } from "./devtools";
export type { DevtoolsState, ExpressRequestCompat, NextjsReadonlyRequestCookiesCompat, NextjsRequestCompat, LogEvent, SdkInfo, } from "./devtools";
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EACL,cAAc,EACd,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,EACtB,aAAa,GACd,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,kCAAkC,EAClC,mBAAmB,EACnB,QAAQ,EACR,OAAO,GACR,MAAM,YAAY,CAAC"}

View File

@@ -0,0 +1,9 @@
import type { TrackingCallback } from "../types/growthbook";
import type { GrowthBook } from "../GrowthBook";
import type { GrowthBookClient, UserScopedGrowthBook } from "../GrowthBookClient";
export type Trackers = "gtag" | "gtm" | "segment";
export declare function thirdPartyTrackingPlugin({ additionalCallback, trackers, }?: {
additionalCallback?: TrackingCallback;
trackers?: Trackers[];
}): (gb: GrowthBook | UserScopedGrowthBook | GrowthBookClient) => void;
//# sourceMappingURL=third-party-tracking.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"third-party-tracking.d.ts","sourceRoot":"","sources":["../../src/plugins/third-party-tracking.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EACV,gBAAgB,EAChB,oBAAoB,EACrB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;AAElD,wBAAgB,wBAAwB,CAAC,EACvC,kBAAkB,EAClB,QAAqC,GACtC,GAAE;IACD,kBAAkB,CAAC,EAAE,gBAAgB,CAAC;IACtC,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC;CAClB,QAMQ,UAAU,GAAG,oBAAoB,GAAG,gBAAgB,UAoDjE"}

View File

@@ -0,0 +1,112 @@
import { LocalStorageCompat, StickyAssignmentsDocument, StickyAttributeKey } from "./types/growthbook";
export interface CookieAttributes {
expires?: number | Date | undefined;
path?: string | undefined;
domain?: string | undefined;
secure?: boolean | undefined;
sameSite?: "strict" | "Strict" | "lax" | "Lax" | "none" | "None" | undefined;
[property: string]: any;
}
export interface JsCookiesCompat<T = string> {
set(name: string, value: string | T, options?: CookieAttributes): string | undefined;
get(name: string): string | T | undefined;
get(): {
[key: string]: string;
};
remove(name: string, options?: CookieAttributes): void;
}
export interface IORedisCompat {
mget(...keys: string[]): Promise<string[]>;
set(key: string, value: string): Promise<string>;
}
export interface RequestCompat {
cookies: Record<string, string>;
[key: string]: unknown;
}
export interface ResponseCompat {
cookie(name: string, value: string, options?: CookieAttributes): ResponseCompat;
[key: string]: unknown;
}
/**
* Responsible for reading and writing documents which describe sticky bucket assignments.
*/
export declare abstract class StickyBucketService {
protected prefix: string;
constructor(opts?: {
prefix?: string;
});
abstract getAssignments(attributeName: string, attributeValue: string): Promise<StickyAssignmentsDocument | null>;
abstract saveAssignments(doc: StickyAssignmentsDocument): Promise<unknown>;
/**
* The SDK calls getAllAssignments to populate sticky buckets. This in turn will
* typically loop through individual getAssignments calls. However, some StickyBucketService
* instances (i.e. Redis) will instead perform a multi-query inside getAllAssignments instead.
*/
getAllAssignments(attributes: Record<string, string>): Promise<Record<StickyAttributeKey, StickyAssignmentsDocument>>;
getKey(attributeName: string, attributeValue: string): string;
}
export declare abstract class StickyBucketServiceSync extends StickyBucketService {
abstract getAssignmentsSync(attributeName: string, attributeValue: string): StickyAssignmentsDocument | null;
abstract saveAssignmentsSync(doc: StickyAssignmentsDocument): void;
getAssignments(attributeName: string, attributeValue: string): Promise<StickyAssignmentsDocument | null>;
saveAssignments(doc: StickyAssignmentsDocument): Promise<void>;
getAllAssignmentsSync(attributes: Record<string, string>): Record<StickyAttributeKey, StickyAssignmentsDocument>;
}
export declare class LocalStorageStickyBucketService extends StickyBucketService {
private localStorage;
constructor(opts?: {
prefix?: string;
localStorage?: LocalStorageCompat;
});
getAssignments(attributeName: string, attributeValue: string): Promise<StickyAssignmentsDocument | null>;
saveAssignments(doc: StickyAssignmentsDocument): Promise<void>;
}
export declare class ExpressCookieStickyBucketService extends StickyBucketServiceSync {
/**
* Intended to be used with cookieParser() middleware from npm: 'cookie-parser'.
* Assumes:
* - reading a cookie is automatically decoded via decodeURIComponent() or similar
* - writing a cookie name & value must be manually encoded via encodeURIComponent() or similar
* - all cookie bodies are JSON encoded strings and are manually encoded/decoded
*/
private req;
private res;
private cookieAttributes;
constructor({ prefix, req, res, cookieAttributes, }: {
prefix?: string;
req: RequestCompat;
res: ResponseCompat;
cookieAttributes?: CookieAttributes;
});
getAssignmentsSync(attributeName: string, attributeValue: string): StickyAssignmentsDocument | null;
saveAssignmentsSync(doc: StickyAssignmentsDocument): void;
}
export declare class BrowserCookieStickyBucketService extends StickyBucketServiceSync {
/**
* Intended to be used with npm: 'js-cookie'.
* Assumes:
* - reading a cookie is automatically decoded via decodeURIComponent() or similar
* - writing a cookie name & value is automatically encoded via encodeURIComponent() or similar
* - all cookie bodies are JSON encoded strings and are manually encoded/decoded
*/
private jsCookie;
private cookieAttributes;
constructor({ prefix, jsCookie, cookieAttributes, }: {
prefix?: string;
jsCookie: JsCookiesCompat;
cookieAttributes?: CookieAttributes;
});
getAssignmentsSync(attributeName: string, attributeValue: string): StickyAssignmentsDocument | null;
saveAssignmentsSync(doc: StickyAssignmentsDocument): Promise<void>;
}
export declare class RedisStickyBucketService extends StickyBucketService {
/** Intended to be used with npm: 'ioredis'. **/
private redis;
constructor({ redis }: {
redis: IORedisCompat;
});
getAllAssignments(attributes: Record<string, string>): Promise<Record<StickyAttributeKey, StickyAssignmentsDocument>>;
getAssignments(_attributeName: string, _attributeValue: string): Promise<null>;
saveAssignments(doc: StickyAssignmentsDocument): Promise<void>;
}
//# sourceMappingURL=sticky-bucket-service.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"sticky-bucket-service.d.ts","sourceRoot":"","sources":["../src/sticky-bucket-service.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,kBAAkB,EACnB,MAAM,oBAAoB,CAAC;AAI5B,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACpC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,QAAQ,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAE7E,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC;CACzB;AACD,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,MAAM;IACzC,GAAG,CACD,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,CAAC,EACjB,OAAO,CAAC,EAAE,gBAAgB,GACzB,MAAM,GAAG,SAAS,CAAC;IACtB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;IAC1C,GAAG,IAAI;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACjC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;CACxD;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AACD,MAAM,WAAW,cAAc;IAC7B,MAAM,CACJ,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,gBAAgB,GACzB,cAAc,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,8BAAsB,mBAAmB;IACvC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC;gBAEb,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;IAKtC,QAAQ,CAAC,cAAc,CACrB,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,yBAAyB,GAAG,IAAI,CAAC;IAE5C,QAAQ,CAAC,eAAe,CAAC,GAAG,EAAE,yBAAyB,GAAG,OAAO,CAAC,OAAO,CAAC;IAE1E;;;;OAIG;IACG,iBAAiB,CACrB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,OAAO,CAAC,MAAM,CAAC,kBAAkB,EAAE,yBAAyB,CAAC,CAAC;IAoBjE,MAAM,CAAC,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM;CAG9D;AAED,8BAAsB,uBAAwB,SAAQ,mBAAmB;IACvE,QAAQ,CAAC,kBAAkB,CACzB,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,GACrB,yBAAyB,GAAG,IAAI;IAEnC,QAAQ,CAAC,mBAAmB,CAAC,GAAG,EAAE,yBAAyB,GAAG,IAAI;IAE5D,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM;IAI5D,eAAe,CAAC,GAAG,EAAE,yBAAyB;IAIpD,qBAAqB,CACnB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,MAAM,CAAC,kBAAkB,EAAE,yBAAyB,CAAC;CAiBzD;AAED,qBAAa,+BAAgC,SAAQ,mBAAmB;IACtE,OAAO,CAAC,YAAY,CAAiC;gBACzC,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,kBAAkB,CAAA;KAAE;IAUnE,cAAc,CAAC,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM;IAe5D,eAAe,CAAC,GAAG,EAAE,yBAAyB;CASrD;AAED,qBAAa,gCAAiC,SAAQ,uBAAuB;IAC3E;;;;;;OAMG;IACH,OAAO,CAAC,GAAG,CAAgB;IAC3B,OAAO,CAAC,GAAG,CAAiB;IAC5B,OAAO,CAAC,gBAAgB,CAAmB;gBAC/B,EACV,MAA4B,EAC5B,GAAG,EACH,GAAG,EACH,gBAAqD,GACtD,EAAE;QACD,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,GAAG,EAAE,aAAa,CAAC;QACnB,GAAG,EAAE,cAAc,CAAC;QACpB,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;KACrC;IAOD,kBAAkB,CAAC,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM;IAehE,mBAAmB,CAAC,GAAG,EAAE,yBAAyB;CAUnD;AAED,qBAAa,gCAAiC,SAAQ,uBAAuB;IAC3E;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAkB;IAClC,OAAO,CAAC,gBAAgB,CAAmB;gBAC/B,EACV,MAA4B,EAC5B,QAAQ,EACR,gBAAmC,GACpC,EAAE;QACD,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,eAAe,CAAC;QAC1B,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;KACrC;IAMD,kBAAkB,CAAC,aAAa,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM;IAe1D,mBAAmB,CAAC,GAAG,EAAE,yBAAyB;CAMzD;AAED,qBAAa,wBAAyB,SAAQ,mBAAmB;IAC/D,gDAAgD;IAChD,OAAO,CAAC,KAAK,CAA4B;gBAC7B,EAAE,KAAK,EAAE,EAAE;QAAE,KAAK,EAAE,aAAa,CAAA;KAAE;IAIzC,iBAAiB,CACrB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,OAAO,CAAC,MAAM,CAAC,kBAAkB,EAAE,yBAAyB,CAAC,CAAC;IA6B3D,cAAc,CAAC,cAAc,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM;IAI9D,eAAe,CAAC,GAAG,EAAE,yBAAyB;CAKrD"}

View File

@@ -0,0 +1,430 @@
import type { GrowthBook, GrowthBookClient, StickyBucketService, UserScopedGrowthBook } from "..";
import { ConditionInterface, ParentConditionInterface } from "./mongrule";
declare global {
interface Window {
_growthbook?: GrowthBook;
}
}
export type VariationMeta = {
passthrough?: boolean;
key?: string;
name?: string;
};
export type FeatureRule<T = any> = {
id?: string;
condition?: ConditionInterface;
parentConditions?: ParentConditionInterface[];
force?: T;
variations?: T[];
weights?: number[];
key?: string;
hashAttribute?: string;
fallbackAttribute?: string;
hashVersion?: number;
disableStickyBucketing?: boolean;
bucketVersion?: number;
minBucketVersion?: number;
range?: VariationRange;
coverage?: number;
/** @deprecated */
namespace?: [string, number, number];
ranges?: VariationRange[];
meta?: VariationMeta[];
filters?: Filter[];
seed?: string;
name?: string;
phase?: string;
tracks?: Array<{
experiment: Experiment<T>;
result: Result<T>;
}>;
};
export interface FeatureDefinition<T = any> {
defaultValue?: T;
rules?: FeatureRule<T>[];
}
export type FeatureResultSource = "unknownFeature" | "defaultValue" | "force" | "override" | "experiment" | "prerequisite" | "cyclicPrerequisite";
export interface FeatureResult<T = any> {
value: T | null;
source: FeatureResultSource;
on: boolean;
off: boolean;
ruleId: string;
experiment?: Experiment<T>;
experimentResult?: Result<T>;
}
/** @deprecated */
export type ExperimentStatus = "draft" | "running" | "stopped";
export type UrlTargetType = "regex" | "simple";
export type UrlTarget = {
include: boolean;
type: UrlTargetType;
pattern: string;
};
export type Experiment<T> = {
key: string;
variations: [T, T, ...T[]];
ranges?: VariationRange[];
meta?: VariationMeta[];
filters?: Filter[];
seed?: string;
name?: string;
phase?: string;
urlPatterns?: UrlTarget[];
weights?: number[];
condition?: ConditionInterface;
parentConditions?: ParentConditionInterface[];
coverage?: number;
include?: () => boolean;
/** @deprecated */
namespace?: [string, number, number];
force?: number;
hashAttribute?: string;
fallbackAttribute?: string;
hashVersion?: number;
disableStickyBucketing?: boolean;
bucketVersion?: number;
minBucketVersion?: number;
active?: boolean;
persistQueryString?: boolean;
/** @deprecated */
status?: ExperimentStatus;
/** @deprecated */
url?: RegExp;
/** @deprecated */
groups?: string[];
};
export type AutoExperimentChangeType = "redirect" | "visual" | "unknown";
export type AutoExperiment<T = AutoExperimentVariation> = Experiment<T> & {
changeId?: string;
manual?: boolean;
};
export type ExperimentOverride = {
condition?: ConditionInterface;
weights?: number[];
active?: boolean;
status?: ExperimentStatus;
force?: number;
coverage?: number;
groups?: string[];
namespace?: [string, number, number];
url?: RegExp | string;
};
export interface Result<T> {
value: T;
variationId: number;
key: string;
name?: string;
bucket?: number;
passthrough?: boolean;
inExperiment: boolean;
hashUsed?: boolean;
hashAttribute: string;
hashValue: string;
featureId: string | null;
stickyBucketUsed?: boolean;
}
export type Attributes = Record<string, any>;
export interface TrackingData {
experiment: Experiment<any>;
result: Result<any>;
}
export interface TrackingDataWithUser {
experiment: Experiment<any>;
result: Result<any>;
user: UserContext;
}
export type TrackingCallback = (experiment: Experiment<any>, result: Result<any>) => Promise<void> | void;
export type TrackingCallbackWithUser = (experiment: Experiment<any>, result: Result<any>, user: UserContext) => Promise<void> | void;
export type FeatureUsageCallback = (key: string, result: FeatureResult<any>) => void;
export type FeatureUsageCallbackWithUser = (key: string, result: FeatureResult<any>, user: UserContext) => void;
export type Plugin = (gb: GrowthBook | UserScopedGrowthBook | GrowthBookClient) => void;
export type EventProperties = Record<string, unknown>;
export type EventLogger = (eventName: string, properties: EventProperties, userContext: UserContext) => void | Promise<void>;
export type NavigateCallback = (url: string) => void | Promise<void>;
export type ApplyDomChangesCallback = (changes: AutoExperimentVariation) => () => void;
export type RenderFunction = () => void;
export type Options = {
enabled?: boolean;
attributes?: Attributes;
url?: string;
features?: Record<string, FeatureDefinition>;
experiments?: AutoExperiment[];
forcedVariations?: Record<string, number>;
forcedFeatureValues?: Map<string, any>;
attributeOverrides?: Attributes;
blockedChangeIds?: string[];
disableVisualExperiments?: boolean;
disableJsInjection?: boolean;
jsInjectionNonce?: string;
disableUrlRedirectExperiments?: boolean;
disableCrossOriginUrlRedirectExperiments?: boolean;
disableExperimentsOnLoad?: boolean;
stickyBucketAssignmentDocs?: Record<StickyAttributeKey, StickyAssignmentsDocument>;
stickyBucketService?: StickyBucketService;
debug?: boolean;
log?: (msg: string, ctx: any) => void;
qaMode?: boolean;
/** @deprecated */
backgroundSync?: boolean;
/** @deprecated */
subscribeToChanges?: boolean;
enableDevMode?: boolean;
disableCache?: boolean;
/** @deprecated */
disableDevTools?: boolean;
trackingCallback?: TrackingCallback;
onFeatureUsage?: FeatureUsageCallback;
eventLogger?: EventLogger;
cacheKeyAttributes?: (keyof Attributes)[];
/** @deprecated */
user?: {
id?: string;
anonId?: string;
[key: string]: string | undefined;
};
/** @deprecated */
overrides?: Record<string, ExperimentOverride>;
/** @deprecated */
groups?: Record<string, boolean>;
apiHost?: string;
streamingHost?: string;
apiHostRequestHeaders?: Record<string, string>;
streamingHostRequestHeaders?: Record<string, string>;
clientKey?: string;
renderer?: null | RenderFunction;
decryptionKey?: string;
remoteEval?: boolean;
navigate?: NavigateCallback;
navigateDelay?: number;
maxNavigateDelay?: number;
/** @deprecated */
antiFlicker?: boolean;
/** @deprecated */
antiFlickerTimeout?: number;
applyDomChangesCallback?: ApplyDomChangesCallback;
savedGroups?: SavedGroupsValues;
plugins?: Plugin[];
};
export type ClientOptions = {
enabled?: boolean;
debug?: boolean;
globalAttributes?: Attributes;
forcedVariations?: Record<string, number>;
forcedFeatureValues?: Map<string, any>;
log?: (msg: string, ctx: any) => void;
qaMode?: boolean;
disableCache?: boolean;
trackingCallback?: TrackingCallbackWithUser;
onFeatureUsage?: (key: string, result: FeatureResult<any>, user: UserContext) => void;
eventLogger?: EventLogger;
apiHost?: string;
streamingHost?: string;
apiHostRequestHeaders?: Record<string, string>;
streamingHostRequestHeaders?: Record<string, string>;
clientKey?: string;
decryptionKey?: string;
savedGroups?: SavedGroupsValues;
plugins?: Plugin[];
};
export type GlobalContext = {
log: (msg: string, ctx: any) => void;
features?: FeatureDefinitions;
experiments?: AutoExperiment[];
enabled?: boolean;
qaMode?: boolean;
savedGroups?: SavedGroupsValues;
forcedVariations?: Record<string, number>;
forcedFeatureValues?: Map<string, any>;
trackingCallback?: TrackingCallbackWithUser;
onFeatureUsage?: FeatureUsageCallbackWithUser;
onExperimentEval?: (experiment: Experiment<any>, result: Result<any>) => void;
saveDeferredTrack?: (data: TrackingData) => void;
recordChangeId?: (changeId: string) => void;
eventLogger?: EventLogger;
/** @deprecated */
overrides?: Record<string, ExperimentOverride>;
/** @deprecated */
groups?: Record<string, boolean>;
/** @deprecated */
user?: {
id?: string;
anonId?: string;
[key: string]: string | undefined;
};
};
export type UserContext = {
enabled?: boolean;
qaMode?: boolean;
enableDevMode?: boolean;
attributes?: Attributes;
url?: string;
blockedChangeIds?: string[];
stickyBucketAssignmentDocs?: Record<StickyAttributeKey, StickyAssignmentsDocument>;
saveStickyBucketAssignmentDoc?: (doc: StickyAssignmentsDocument) => Promise<unknown>;
forcedVariations?: Record<string, number>;
forcedFeatureValues?: Map<string, any>;
attributeOverrides?: Attributes;
trackingCallback?: TrackingCallback;
onFeatureUsage?: FeatureUsageCallback;
trackedExperiments?: Set<string>;
trackedFeatureUsage?: Record<string, string>;
devLogs?: LogUnion[];
};
export type StackContext = {
id?: string;
evaluatedFeatures: Set<string>;
};
export type EvalContext = {
global: GlobalContext;
user: UserContext;
stack: StackContext;
};
export type PrefetchOptions = Pick<Options, "decryptionKey" | "apiHost" | "apiHostRequestHeaders" | "streamingHost" | "streamingHostRequestHeaders"> & {
clientKey: string;
streaming?: boolean;
skipCache?: boolean;
};
export type SubscriptionFunction = (experiment: Experiment<any>, result: Result<any>) => void;
export type VariationRange = [number, number];
export interface InitResponse {
success: boolean;
source: "init" | "cache" | "network" | "error" | "timeout";
error?: Error;
}
export interface FetchResponse {
data: FeatureApiResponse | null;
success: boolean;
source: "cache" | "network" | "error" | "timeout";
error?: Error;
}
export type JSONValue = null | number | string | boolean | Array<JSONValue> | Record<string, unknown> | {
[key: string]: JSONValue;
};
export type WidenPrimitives<T> = T extends string ? string : T extends number ? number : T extends boolean ? boolean : T;
export type DOMMutation = {
selector: string;
action: string;
attribute: string;
value?: string;
parentSelector?: string;
insertBeforeSelector?: string;
};
export type AutoExperimentVariation = {
domMutations?: DOMMutation[];
css?: string;
js?: string;
urlRedirect?: string;
};
export type FeatureDefinitions = Record<string, FeatureDefinition>;
export type FeatureApiResponse = {
features?: FeatureDefinitions;
dateUpdated?: string;
encryptedFeatures?: string;
experiments?: AutoExperiment[];
encryptedExperiments?: string;
savedGroups?: SavedGroupsValues;
encryptedSavedGroups?: string;
};
export type GrowthBookPayload = FeatureApiResponse;
export type Polyfills = {
fetch: any;
SubtleCrypto: any;
EventSource: any;
localStorage?: LocalStorageCompat;
};
export type Helpers = {
fetchFeaturesCall: ({ host, clientKey, headers, }: {
host: string;
clientKey: string;
headers?: Record<string, string>;
}) => Promise<Response>;
fetchRemoteEvalCall: ({ host, clientKey, payload, headers, }: {
host: string;
clientKey: string;
payload: any;
headers?: Record<string, string>;
}) => Promise<Response>;
eventSourceCall: ({ host, clientKey, headers, }: {
host: string;
clientKey: string;
headers?: Record<string, string>;
}) => EventSource;
startIdleListener: () => (() => void) | void;
stopIdleListener: () => void;
};
export interface LocalStorageCompat {
getItem(key: string): string | null | Promise<string | null>;
setItem(key: string, value: string): void | Promise<void>;
}
export type CacheSettings = {
backgroundSync: boolean;
cacheKey: string;
staleTTL: number;
maxAge: number;
maxEntries: number;
disableIdleStreams: boolean;
idleStreamInterval: number;
disableCache: boolean;
};
export type ApiHost = string;
export type ClientKey = string;
export type InitOptions = {
timeout?: number;
skipCache?: boolean;
payload?: FeatureApiResponse;
streaming?: boolean;
cacheSettings?: CacheSettings;
};
export type InitSyncOptions = {
payload: FeatureApiResponse;
streaming?: boolean;
};
export type LoadFeaturesOptions = {
/** @deprecated */
autoRefresh?: boolean;
timeout?: number;
skipCache?: boolean;
};
export type RefreshFeaturesOptions = {
timeout?: number;
skipCache?: boolean;
};
export type DestroyOptions = {
destroyAllStreams?: boolean;
};
export interface Filter {
attribute?: string;
seed: string;
hashVersion: number;
ranges: VariationRange[];
}
export type StickyAttributeKey = string;
export type StickyExperimentKey = string;
export type StickyAssignments = Record<StickyExperimentKey, string>;
export interface StickyAssignmentsDocument {
attributeName: string;
attributeValue: string;
assignments: StickyAssignments;
}
export type SavedGroupsValues = Record<string, (string | number)[]>;
export type BaseLog = {
timestamp: string;
};
export type EventLog = BaseLog & {
logType: "event";
eventName: string;
properties?: Record<string, unknown>;
};
export type ExperimentLog<T> = BaseLog & {
logType: "experiment";
experiment: Experiment<T>;
result: Result<T>;
};
export type FeatureLog = BaseLog & {
logType: "feature";
featureKey: string;
result: FeatureResult;
};
export type LogUnion = EventLog | ExperimentLog<any> | FeatureLog;
//# sourceMappingURL=growthbook.d.ts.map

Some files were not shown because too many files have changed in this diff Show More