@xyd-js/host 0.1.0-build.158

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3574 @@
1
+ var growthbook = (function (exports) {
2
+ 'use strict';
3
+
4
+ const polyfills$1 = {
5
+ fetch: globalThis.fetch ? globalThis.fetch.bind(globalThis) : undefined,
6
+ SubtleCrypto: globalThis.crypto ? globalThis.crypto.subtle : undefined,
7
+ EventSource: globalThis.EventSource
8
+ };
9
+ function getPolyfills() {
10
+ return polyfills$1;
11
+ }
12
+ function hashFnv32a(str) {
13
+ let hval = 0x811c9dc5;
14
+ const l = str.length;
15
+ for (let i = 0; i < l; i++) {
16
+ hval ^= str.charCodeAt(i);
17
+ hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
18
+ }
19
+ return hval >>> 0;
20
+ }
21
+ function hash(seed, value, version) {
22
+ // New unbiased hashing algorithm
23
+ if (version === 2) {
24
+ return hashFnv32a(hashFnv32a(seed + value) + "") % 10000 / 10000;
25
+ }
26
+ // Original biased hashing algorithm (keep for backwards compatibility)
27
+ if (version === 1) {
28
+ return hashFnv32a(value + seed) % 1000 / 1000;
29
+ }
30
+
31
+ // Unknown hash version
32
+ return null;
33
+ }
34
+ function getEqualWeights(n) {
35
+ if (n <= 0) return [];
36
+ return new Array(n).fill(1 / n);
37
+ }
38
+ function inRange(n, range) {
39
+ return n >= range[0] && n < range[1];
40
+ }
41
+ function inNamespace(hashValue, namespace) {
42
+ const n = hash("__" + namespace[0], hashValue, 1);
43
+ if (n === null) return false;
44
+ return n >= namespace[1] && n < namespace[2];
45
+ }
46
+ function chooseVariation(n, ranges) {
47
+ for (let i = 0; i < ranges.length; i++) {
48
+ if (inRange(n, ranges[i])) {
49
+ return i;
50
+ }
51
+ }
52
+ return -1;
53
+ }
54
+ function getUrlRegExp(regexString) {
55
+ try {
56
+ const escaped = regexString.replace(/([^\\])\//g, "$1\\/");
57
+ return new RegExp(escaped);
58
+ } catch (e) {
59
+ console.error(e);
60
+ return undefined;
61
+ }
62
+ }
63
+ function isURLTargeted(url, targets) {
64
+ if (!targets.length) return false;
65
+ let hasIncludeRules = false;
66
+ let isIncluded = false;
67
+ for (let i = 0; i < targets.length; i++) {
68
+ const match = _evalURLTarget(url, targets[i].type, targets[i].pattern);
69
+ if (targets[i].include === false) {
70
+ if (match) return false;
71
+ } else {
72
+ hasIncludeRules = true;
73
+ if (match) isIncluded = true;
74
+ }
75
+ }
76
+ return isIncluded || !hasIncludeRules;
77
+ }
78
+ function _evalSimpleUrlPart(actual, pattern, isPath) {
79
+ try {
80
+ // Escape special regex characters and change wildcard `_____` to `.*`
81
+ let escaped = pattern.replace(/[*.+?^${}()|[\]\\]/g, "\\$&").replace(/_____/g, ".*");
82
+ if (isPath) {
83
+ // When matching pathname, make leading/trailing slashes optional
84
+ escaped = "\\/?" + escaped.replace(/(^\/|\/$)/g, "") + "\\/?";
85
+ }
86
+ const regex = new RegExp("^" + escaped + "$", "i");
87
+ return regex.test(actual);
88
+ } catch (e) {
89
+ return false;
90
+ }
91
+ }
92
+ function _evalSimpleUrlTarget(actual, pattern) {
93
+ try {
94
+ // If a protocol is missing, but a host is specified, add `https://` to the front
95
+ // Use "_____" as the wildcard since `*` is not a valid hostname in some browsers
96
+ const expected = new URL(pattern.replace(/^([^:/?]*)\./i, "https://$1.").replace(/\*/g, "_____"), "https://_____");
97
+
98
+ // Compare each part of the URL separately
99
+ const comps = [[actual.host, expected.host, false], [actual.pathname, expected.pathname, true]];
100
+ // We only want to compare hashes if it's explicitly being targeted
101
+ if (expected.hash) {
102
+ comps.push([actual.hash, expected.hash, false]);
103
+ }
104
+ expected.searchParams.forEach((v, k) => {
105
+ comps.push([actual.searchParams.get(k) || "", v, false]);
106
+ });
107
+
108
+ // If any comparisons fail, the whole thing fails
109
+ return !comps.some(data => !_evalSimpleUrlPart(data[0], data[1], data[2]));
110
+ } catch (e) {
111
+ return false;
112
+ }
113
+ }
114
+ function _evalURLTarget(url, type, pattern) {
115
+ try {
116
+ const parsed = new URL(url, "https://_");
117
+ if (type === "regex") {
118
+ const regex = getUrlRegExp(pattern);
119
+ if (!regex) return false;
120
+ return regex.test(parsed.href) || regex.test(parsed.href.substring(parsed.origin.length));
121
+ } else if (type === "simple") {
122
+ return _evalSimpleUrlTarget(parsed, pattern);
123
+ }
124
+ return false;
125
+ } catch (e) {
126
+ return false;
127
+ }
128
+ }
129
+ function getBucketRanges(numVariations, coverage, weights) {
130
+ coverage = coverage === undefined ? 1 : coverage;
131
+
132
+ // Make sure coverage is within bounds
133
+ if (coverage < 0) {
134
+ coverage = 0;
135
+ } else if (coverage > 1) {
136
+ coverage = 1;
137
+ }
138
+
139
+ // Default to equal weights if missing or invalid
140
+ const equal = getEqualWeights(numVariations);
141
+ weights = weights || equal;
142
+ if (weights.length !== numVariations) {
143
+ weights = equal;
144
+ }
145
+
146
+ // If weights don't add up to 1 (or close to it), default to equal weights
147
+ const totalWeight = weights.reduce((w, sum) => sum + w, 0);
148
+ if (totalWeight < 0.99 || totalWeight > 1.01) {
149
+ weights = equal;
150
+ }
151
+
152
+ // Covert weights to ranges
153
+ let cumulative = 0;
154
+ return weights.map(w => {
155
+ const start = cumulative;
156
+ cumulative += w;
157
+ return [start, start + coverage * w];
158
+ });
159
+ }
160
+ function getQueryStringOverride(id, url, numVariations) {
161
+ if (!url) {
162
+ return null;
163
+ }
164
+ const search = url.split("?")[1];
165
+ if (!search) {
166
+ return null;
167
+ }
168
+ const match = search.replace(/#.*/, "") // Get rid of anchor
169
+ .split("&") // Split into key/value pairs
170
+ .map(kv => kv.split("=", 2)).filter(_ref => {
171
+ let [k] = _ref;
172
+ return k === id;
173
+ }) // Look for key that matches the experiment id
174
+ .map(_ref2 => {
175
+ let [, v] = _ref2;
176
+ return parseInt(v);
177
+ }); // Parse the value into an integer
178
+
179
+ if (match.length > 0 && match[0] >= 0 && match[0] < numVariations) return match[0];
180
+ return null;
181
+ }
182
+ function isIncluded(include) {
183
+ try {
184
+ return include();
185
+ } catch (e) {
186
+ console.error(e);
187
+ return false;
188
+ }
189
+ }
190
+ const base64ToBuf = b => Uint8Array.from(atob(b), c => c.charCodeAt(0));
191
+ async function decrypt(encryptedString, decryptionKey, subtle) {
192
+ decryptionKey = decryptionKey || "";
193
+ subtle = subtle || globalThis.crypto && globalThis.crypto.subtle || polyfills$1.SubtleCrypto;
194
+ if (!subtle) {
195
+ throw new Error("No SubtleCrypto implementation found");
196
+ }
197
+ try {
198
+ const key = await subtle.importKey("raw", base64ToBuf(decryptionKey), {
199
+ name: "AES-CBC",
200
+ length: 128
201
+ }, true, ["encrypt", "decrypt"]);
202
+ const [iv, cipherText] = encryptedString.split(".");
203
+ const plainTextBuffer = await subtle.decrypt({
204
+ name: "AES-CBC",
205
+ iv: base64ToBuf(iv)
206
+ }, key, base64ToBuf(cipherText));
207
+ return new TextDecoder().decode(plainTextBuffer);
208
+ } catch (e) {
209
+ throw new Error("Failed to decrypt");
210
+ }
211
+ }
212
+
213
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
214
+ function toString(input) {
215
+ if (typeof input === "string") return input;
216
+ return JSON.stringify(input);
217
+ }
218
+
219
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
220
+ function paddedVersionString(input) {
221
+ if (typeof input === "number") {
222
+ input = input + "";
223
+ }
224
+ if (!input || typeof input !== "string") {
225
+ input = "0";
226
+ }
227
+ // Remove build info and leading `v` if any
228
+ // Split version into parts (both core version numbers and pre-release tags)
229
+ // "v1.2.3-rc.1+build123" -> ["1","2","3","rc","1"]
230
+ const parts = input.replace(/(^v|\+.*$)/g, "").split(/[-.]/);
231
+
232
+ // If it's SemVer without a pre-release, add `~` to the end
233
+ // ["1","0","0"] -> ["1","0","0","~"]
234
+ // "~" is the largest ASCII character, so this will make "1.0.0" greater than "1.0.0-beta" for example
235
+ if (parts.length === 3) {
236
+ parts.push("~");
237
+ }
238
+
239
+ // Left pad each numeric part with spaces so string comparisons will work ("9">"10", but " 9"<"10")
240
+ // Then, join back together into a single string
241
+ return parts.map(v => v.match(/^[0-9]+$/) ? v.padStart(5, " ") : v).join("-");
242
+ }
243
+ function loadSDKVersion() {
244
+ let version;
245
+ try {
246
+ // @ts-expect-error right-hand value to be replaced by build with string literal
247
+ version = "1.5.1";
248
+ } catch (e) {
249
+ version = "";
250
+ }
251
+ return version;
252
+ }
253
+ function mergeQueryStrings(oldUrl, newUrl) {
254
+ let currUrl;
255
+ let redirectUrl;
256
+ try {
257
+ currUrl = new URL(oldUrl);
258
+ redirectUrl = new URL(newUrl);
259
+ } catch (e) {
260
+ console.error(`Unable to merge query strings: ${e}`);
261
+ return newUrl;
262
+ }
263
+ currUrl.searchParams.forEach((value, key) => {
264
+ // skip if search param already exists in redirectUrl
265
+ if (redirectUrl.searchParams.has(key)) {
266
+ return;
267
+ }
268
+ redirectUrl.searchParams.set(key, value);
269
+ });
270
+ return redirectUrl.toString();
271
+ }
272
+ function isObj(x) {
273
+ return typeof x === "object" && x !== null;
274
+ }
275
+ function getAutoExperimentChangeType(exp) {
276
+ if (exp.urlPatterns && exp.variations.some(variation => isObj(variation) && "urlRedirect" in variation)) {
277
+ return "redirect";
278
+ } else if (exp.variations.some(variation => isObj(variation) && (variation.domMutations || "js" in variation || "css" in variation))) {
279
+ return "visual";
280
+ }
281
+ return "unknown";
282
+ }
283
+
284
+ // Guarantee the promise always resolves within {timeout} ms
285
+ // Resolved value will be `null` when there's an error or it takes too long
286
+ // Note: The promise will continue running in the background, even if the timeout is hit
287
+ async function promiseTimeout(promise, timeout) {
288
+ return new Promise(resolve => {
289
+ let resolved = false;
290
+ let timer;
291
+ const finish = data => {
292
+ if (resolved) return;
293
+ resolved = true;
294
+ timer && clearTimeout(timer);
295
+ resolve(data || null);
296
+ };
297
+ if (timeout) {
298
+ timer = setTimeout(() => finish(), timeout);
299
+ }
300
+ promise.then(data => finish(data)).catch(() => finish());
301
+ });
302
+ }
303
+
304
+ // Config settings
305
+ const cacheSettings = {
306
+ // Consider a fetch stale after 1 minute
307
+ staleTTL: 1000 * 60,
308
+ // Max time to keep a fetch in cache (4 hours default)
309
+ maxAge: 1000 * 60 * 60 * 4,
310
+ cacheKey: "gbFeaturesCache",
311
+ backgroundSync: true,
312
+ maxEntries: 10,
313
+ disableIdleStreams: false,
314
+ idleStreamInterval: 20000,
315
+ disableCache: false
316
+ };
317
+ const polyfills = getPolyfills();
318
+ const helpers = {
319
+ fetchFeaturesCall: _ref => {
320
+ let {
321
+ host,
322
+ clientKey,
323
+ headers
324
+ } = _ref;
325
+ return polyfills.fetch(`${host}/api/features/${clientKey}`, {
326
+ headers
327
+ });
328
+ },
329
+ fetchRemoteEvalCall: _ref2 => {
330
+ let {
331
+ host,
332
+ clientKey,
333
+ payload,
334
+ headers
335
+ } = _ref2;
336
+ const options = {
337
+ method: "POST",
338
+ headers: {
339
+ "Content-Type": "application/json",
340
+ ...headers
341
+ },
342
+ body: JSON.stringify(payload)
343
+ };
344
+ return polyfills.fetch(`${host}/api/eval/${clientKey}`, options);
345
+ },
346
+ eventSourceCall: _ref3 => {
347
+ let {
348
+ host,
349
+ clientKey,
350
+ headers
351
+ } = _ref3;
352
+ if (headers) {
353
+ return new polyfills.EventSource(`${host}/sub/${clientKey}`, {
354
+ headers
355
+ });
356
+ }
357
+ return new polyfills.EventSource(`${host}/sub/${clientKey}`);
358
+ },
359
+ startIdleListener: () => {
360
+ let idleTimeout;
361
+ const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
362
+ if (!isBrowser) return;
363
+ const onVisibilityChange = () => {
364
+ if (document.visibilityState === "visible") {
365
+ window.clearTimeout(idleTimeout);
366
+ onVisible();
367
+ } else if (document.visibilityState === "hidden") {
368
+ idleTimeout = window.setTimeout(onHidden, cacheSettings.idleStreamInterval);
369
+ }
370
+ };
371
+ document.addEventListener("visibilitychange", onVisibilityChange);
372
+ return () => document.removeEventListener("visibilitychange", onVisibilityChange);
373
+ },
374
+ stopIdleListener: () => {
375
+ // No-op, replaced by startIdleListener
376
+ }
377
+ };
378
+ try {
379
+ if (globalThis.localStorage) {
380
+ polyfills.localStorage = globalThis.localStorage;
381
+ }
382
+ } catch (e) {
383
+ // Ignore localStorage errors
384
+ }
385
+
386
+ // Global state
387
+ const subscribedInstances = new Map();
388
+ let cacheInitialized = false;
389
+ const cache = new Map();
390
+ const activeFetches = new Map();
391
+ const streams = new Map();
392
+ const supportsSSE = new Set();
393
+
394
+ // Public functions
395
+ function setPolyfills(overrides) {
396
+ Object.assign(polyfills, overrides);
397
+ }
398
+ function configureCache(overrides) {
399
+ Object.assign(cacheSettings, overrides);
400
+ if (!cacheSettings.backgroundSync) {
401
+ clearAutoRefresh();
402
+ }
403
+ }
404
+ async function clearCache() {
405
+ cache.clear();
406
+ activeFetches.clear();
407
+ clearAutoRefresh();
408
+ cacheInitialized = false;
409
+ await updatePersistentCache();
410
+ }
411
+
412
+ // Get or fetch features and refresh the SDK instance
413
+ async function refreshFeatures(_ref4) {
414
+ let {
415
+ instance,
416
+ timeout,
417
+ skipCache,
418
+ allowStale,
419
+ backgroundSync
420
+ } = _ref4;
421
+ if (!backgroundSync) {
422
+ cacheSettings.backgroundSync = false;
423
+ }
424
+ return fetchFeaturesWithCache({
425
+ instance,
426
+ allowStale,
427
+ timeout,
428
+ skipCache
429
+ });
430
+ }
431
+
432
+ // Subscribe a GrowthBook instance to feature changes
433
+ function subscribe(instance) {
434
+ const key = getKey(instance);
435
+ const subs = subscribedInstances.get(key) || new Set();
436
+ subs.add(instance);
437
+ subscribedInstances.set(key, subs);
438
+ }
439
+ function unsubscribe(instance) {
440
+ subscribedInstances.forEach(s => s.delete(instance));
441
+ }
442
+ function onHidden() {
443
+ streams.forEach(channel => {
444
+ if (!channel) return;
445
+ channel.state = "idle";
446
+ disableChannel(channel);
447
+ });
448
+ }
449
+ function onVisible() {
450
+ streams.forEach(channel => {
451
+ if (!channel) return;
452
+ if (channel.state !== "idle") return;
453
+ enableChannel(channel);
454
+ });
455
+ }
456
+
457
+ // Private functions
458
+
459
+ async function updatePersistentCache() {
460
+ try {
461
+ if (!polyfills.localStorage) return;
462
+ await polyfills.localStorage.setItem(cacheSettings.cacheKey, JSON.stringify(Array.from(cache.entries())));
463
+ } catch (e) {
464
+ // Ignore localStorage errors
465
+ }
466
+ }
467
+
468
+ // SWR wrapper for fetching features. May indirectly or directly start SSE streaming.
469
+ async function fetchFeaturesWithCache(_ref5) {
470
+ let {
471
+ instance,
472
+ allowStale,
473
+ timeout,
474
+ skipCache
475
+ } = _ref5;
476
+ const key = getKey(instance);
477
+ const cacheKey = getCacheKey(instance);
478
+ const now = new Date();
479
+ const minStaleAt = new Date(now.getTime() - cacheSettings.maxAge + cacheSettings.staleTTL);
480
+ await initializeCache();
481
+ const existing = !cacheSettings.disableCache && !skipCache ? cache.get(cacheKey) : undefined;
482
+ if (existing && (allowStale || existing.staleAt > now) && existing.staleAt > minStaleAt) {
483
+ // Restore from cache whether SSE is supported
484
+ if (existing.sse) supportsSSE.add(key);
485
+
486
+ // Reload features in the background if stale
487
+ if (existing.staleAt < now) {
488
+ fetchFeatures(instance);
489
+ }
490
+ // Otherwise, if we don't need to refresh now, start a background sync
491
+ else {
492
+ startAutoRefresh(instance);
493
+ }
494
+ return {
495
+ data: existing.data,
496
+ success: true,
497
+ source: "cache"
498
+ };
499
+ } else {
500
+ const res = await promiseTimeout(fetchFeatures(instance), timeout);
501
+ return res || {
502
+ data: null,
503
+ success: false,
504
+ source: "timeout",
505
+ error: new Error("Timeout")
506
+ };
507
+ }
508
+ }
509
+ function getKey(instance) {
510
+ const [apiHost, clientKey] = instance.getApiInfo();
511
+ return `${apiHost}||${clientKey}`;
512
+ }
513
+ function getCacheKey(instance) {
514
+ const baseKey = getKey(instance);
515
+ if (!("isRemoteEval" in instance) || !instance.isRemoteEval()) return baseKey;
516
+ const attributes = instance.getAttributes();
517
+ const cacheKeyAttributes = instance.getCacheKeyAttributes() || Object.keys(instance.getAttributes());
518
+ const ca = {};
519
+ cacheKeyAttributes.forEach(key => {
520
+ ca[key] = attributes[key];
521
+ });
522
+ const fv = instance.getForcedVariations();
523
+ const url = instance.getUrl();
524
+ return `${baseKey}||${JSON.stringify({
525
+ ca,
526
+ fv,
527
+ url
528
+ })}`;
529
+ }
530
+
531
+ // Populate cache from localStorage (if available)
532
+ async function initializeCache() {
533
+ if (cacheInitialized) return;
534
+ cacheInitialized = true;
535
+ try {
536
+ if (polyfills.localStorage) {
537
+ const value = await polyfills.localStorage.getItem(cacheSettings.cacheKey);
538
+ if (!cacheSettings.disableCache && value) {
539
+ const parsed = JSON.parse(value);
540
+ if (parsed && Array.isArray(parsed)) {
541
+ parsed.forEach(_ref6 => {
542
+ let [key, data] = _ref6;
543
+ cache.set(key, {
544
+ ...data,
545
+ staleAt: new Date(data.staleAt)
546
+ });
547
+ });
548
+ }
549
+ cleanupCache();
550
+ }
551
+ }
552
+ } catch (e) {
553
+ // Ignore localStorage errors
554
+ }
555
+ if (!cacheSettings.disableIdleStreams) {
556
+ const cleanupFn = helpers.startIdleListener();
557
+ if (cleanupFn) {
558
+ helpers.stopIdleListener = cleanupFn;
559
+ }
560
+ }
561
+ }
562
+
563
+ // Enforce the maxEntries limit
564
+ function cleanupCache() {
565
+ const entriesWithTimestamps = Array.from(cache.entries()).map(_ref7 => {
566
+ let [key, value] = _ref7;
567
+ return {
568
+ key,
569
+ staleAt: value.staleAt.getTime()
570
+ };
571
+ }).sort((a, b) => a.staleAt - b.staleAt);
572
+ const entriesToRemoveCount = Math.min(Math.max(0, cache.size - cacheSettings.maxEntries), cache.size);
573
+ for (let i = 0; i < entriesToRemoveCount; i++) {
574
+ cache.delete(entriesWithTimestamps[i].key);
575
+ }
576
+ }
577
+
578
+ // Called whenever new features are fetched from the API
579
+ function onNewFeatureData(key, cacheKey, data) {
580
+ // If contents haven't changed, ignore the update, extend the stale TTL
581
+ const version = data.dateUpdated || "";
582
+ const staleAt = new Date(Date.now() + cacheSettings.staleTTL);
583
+ const existing = !cacheSettings.disableCache ? cache.get(cacheKey) : undefined;
584
+ if (existing && version && existing.version === version) {
585
+ existing.staleAt = staleAt;
586
+ updatePersistentCache();
587
+ return;
588
+ }
589
+ if (!cacheSettings.disableCache) {
590
+ // Update in-memory cache
591
+ cache.set(cacheKey, {
592
+ data,
593
+ version,
594
+ staleAt,
595
+ sse: supportsSSE.has(key)
596
+ });
597
+ cleanupCache();
598
+ }
599
+ // Update local storage (don't await this, just update asynchronously)
600
+ updatePersistentCache();
601
+
602
+ // Update features for all subscribed GrowthBook instances
603
+ const instances = subscribedInstances.get(key);
604
+ instances && instances.forEach(instance => refreshInstance(instance, data));
605
+ }
606
+ async function refreshInstance(instance, data) {
607
+ await instance.setPayload(data || instance.getPayload());
608
+ }
609
+
610
+ // Fetch the features payload from helper function or from in-mem injected payload
611
+ async function fetchFeatures(instance) {
612
+ const {
613
+ apiHost,
614
+ apiRequestHeaders
615
+ } = instance.getApiHosts();
616
+ const clientKey = instance.getClientKey();
617
+ const remoteEval = "isRemoteEval" in instance && instance.isRemoteEval();
618
+ const key = getKey(instance);
619
+ const cacheKey = getCacheKey(instance);
620
+ let promise = activeFetches.get(cacheKey);
621
+ if (!promise) {
622
+ const fetcher = remoteEval ? helpers.fetchRemoteEvalCall({
623
+ host: apiHost,
624
+ clientKey,
625
+ payload: {
626
+ attributes: instance.getAttributes(),
627
+ forcedVariations: instance.getForcedVariations(),
628
+ forcedFeatures: Array.from(instance.getForcedFeatures().entries()),
629
+ url: instance.getUrl()
630
+ },
631
+ headers: apiRequestHeaders
632
+ }) : helpers.fetchFeaturesCall({
633
+ host: apiHost,
634
+ clientKey,
635
+ headers: apiRequestHeaders
636
+ });
637
+
638
+ // TODO: auto-retry if status code indicates a temporary error
639
+ promise = fetcher.then(res => {
640
+ if (!res.ok) {
641
+ throw new Error(`HTTP error: ${res.status}`);
642
+ }
643
+ if (res.headers.get("x-sse-support") === "enabled") {
644
+ supportsSSE.add(key);
645
+ }
646
+ return res.json();
647
+ }).then(data => {
648
+ onNewFeatureData(key, cacheKey, data);
649
+ startAutoRefresh(instance);
650
+ activeFetches.delete(cacheKey);
651
+ return {
652
+ data,
653
+ success: true,
654
+ source: "network"
655
+ };
656
+ }).catch(e => {
657
+ activeFetches.delete(cacheKey);
658
+ return {
659
+ data: null,
660
+ source: "error",
661
+ success: false,
662
+ error: e
663
+ };
664
+ });
665
+ activeFetches.set(cacheKey, promise);
666
+ }
667
+ return promise;
668
+ }
669
+
670
+ // Start SSE streaming, listens to feature payload changes and triggers a refresh or re-fetch
671
+ function startAutoRefresh(instance) {
672
+ let forceSSE = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
673
+ const key = getKey(instance);
674
+ const cacheKey = getCacheKey(instance);
675
+ const {
676
+ streamingHost,
677
+ streamingHostRequestHeaders
678
+ } = instance.getApiHosts();
679
+ const clientKey = instance.getClientKey();
680
+ if (forceSSE) {
681
+ supportsSSE.add(key);
682
+ }
683
+ if (cacheSettings.backgroundSync && supportsSSE.has(key) && polyfills.EventSource) {
684
+ if (streams.has(key)) return;
685
+ const channel = {
686
+ src: null,
687
+ host: streamingHost,
688
+ clientKey,
689
+ headers: streamingHostRequestHeaders,
690
+ cb: event => {
691
+ try {
692
+ if (event.type === "features-updated") {
693
+ const instances = subscribedInstances.get(key);
694
+ instances && instances.forEach(instance => {
695
+ fetchFeatures(instance);
696
+ });
697
+ } else if (event.type === "features") {
698
+ const json = JSON.parse(event.data);
699
+ onNewFeatureData(key, cacheKey, json);
700
+ }
701
+ // Reset error count on success
702
+ channel.errors = 0;
703
+ } catch (e) {
704
+ onSSEError(channel);
705
+ }
706
+ },
707
+ errors: 0,
708
+ state: "active"
709
+ };
710
+ streams.set(key, channel);
711
+ enableChannel(channel);
712
+ }
713
+ }
714
+ function onSSEError(channel) {
715
+ if (channel.state === "idle") return;
716
+ channel.errors++;
717
+ if (channel.errors > 3 || channel.src && channel.src.readyState === 2) {
718
+ // exponential backoff after 4 errors, with jitter
719
+ const delay = Math.pow(3, channel.errors - 3) * (1000 + Math.random() * 1000);
720
+ disableChannel(channel);
721
+ setTimeout(() => {
722
+ if (["idle", "active"].includes(channel.state)) return;
723
+ enableChannel(channel);
724
+ }, Math.min(delay, 300000)); // 5 minutes max
725
+ }
726
+ }
727
+
728
+ function disableChannel(channel) {
729
+ if (!channel.src) return;
730
+ channel.src.onopen = null;
731
+ channel.src.onerror = null;
732
+ channel.src.close();
733
+ channel.src = null;
734
+ if (channel.state === "active") {
735
+ channel.state = "disabled";
736
+ }
737
+ }
738
+ function enableChannel(channel) {
739
+ channel.src = helpers.eventSourceCall({
740
+ host: channel.host,
741
+ clientKey: channel.clientKey,
742
+ headers: channel.headers
743
+ });
744
+ channel.state = "active";
745
+ channel.src.addEventListener("features", channel.cb);
746
+ channel.src.addEventListener("features-updated", channel.cb);
747
+ channel.src.onerror = () => onSSEError(channel);
748
+ channel.src.onopen = () => {
749
+ channel.errors = 0;
750
+ };
751
+ }
752
+ function destroyChannel(channel, key) {
753
+ disableChannel(channel);
754
+ streams.delete(key);
755
+ }
756
+ function clearAutoRefresh() {
757
+ // Clear list of which keys are auto-updated
758
+ supportsSSE.clear();
759
+
760
+ // Stop listening for any SSE events
761
+ streams.forEach(destroyChannel);
762
+
763
+ // Remove all references to GrowthBook instances
764
+ subscribedInstances.clear();
765
+
766
+ // Run the idle stream cleanup function
767
+ helpers.stopIdleListener();
768
+ }
769
+ function startStreaming(instance, options) {
770
+ if (options.streaming) {
771
+ if (!instance.getClientKey()) {
772
+ throw new Error("Must specify clientKey to enable streaming");
773
+ }
774
+ if (options.payload) {
775
+ startAutoRefresh(instance, true);
776
+ }
777
+ subscribe(instance);
778
+ }
779
+ }
780
+
781
+ var validAttributeName = /^[a-zA-Z:_][a-zA-Z0-9:_.-]*$/;
782
+ var nullController = {
783
+ revert: function revert() {}
784
+ };
785
+ var elements = /*#__PURE__*/new Map();
786
+ var mutations = /*#__PURE__*/new Set();
787
+ function getObserverInit(attr) {
788
+ return attr === 'html' ? {
789
+ childList: true,
790
+ subtree: true,
791
+ attributes: true,
792
+ characterData: true
793
+ } : {
794
+ childList: false,
795
+ subtree: false,
796
+ attributes: true,
797
+ attributeFilter: [attr]
798
+ };
799
+ }
800
+ function getElementRecord(element) {
801
+ var record = elements.get(element);
802
+ if (!record) {
803
+ record = {
804
+ element: element,
805
+ attributes: {}
806
+ };
807
+ elements.set(element, record);
808
+ }
809
+ return record;
810
+ }
811
+ function createElementPropertyRecord(el, attr, getCurrentValue, setValue, mutationRunner) {
812
+ var currentValue = getCurrentValue(el);
813
+ var record = {
814
+ isDirty: false,
815
+ originalValue: currentValue,
816
+ virtualValue: currentValue,
817
+ mutations: [],
818
+ el: el,
819
+ _positionTimeout: null,
820
+ observer: new MutationObserver(function () {
821
+ // enact a 1 second timeout that blocks subsequent firing of the
822
+ // observer until the timeout is complete. This will prevent multiple
823
+ // mutations from firing in quick succession, which can cause the
824
+ // mutation to be reverted before the DOM has a chance to update.
825
+ if (attr === 'position' && record._positionTimeout) return;else if (attr === 'position') record._positionTimeout = setTimeout(function () {
826
+ record._positionTimeout = null;
827
+ }, 1000);
828
+ var currentValue = getCurrentValue(el);
829
+ if (attr === 'position' && currentValue.parentNode === record.virtualValue.parentNode && currentValue.insertBeforeNode === record.virtualValue.insertBeforeNode) return;
830
+ if (currentValue === record.virtualValue) return;
831
+ record.originalValue = currentValue;
832
+ mutationRunner(record);
833
+ }),
834
+ mutationRunner: mutationRunner,
835
+ setValue: setValue,
836
+ getCurrentValue: getCurrentValue
837
+ };
838
+ if (attr === 'position' && el.parentNode) {
839
+ record.observer.observe(el.parentNode, {
840
+ childList: true,
841
+ subtree: true,
842
+ attributes: false,
843
+ characterData: false
844
+ });
845
+ } else {
846
+ record.observer.observe(el, getObserverInit(attr));
847
+ }
848
+ return record;
849
+ }
850
+ function queueIfNeeded(val, record) {
851
+ var currentVal = record.getCurrentValue(record.el);
852
+ record.virtualValue = val;
853
+ if (val && typeof val !== 'string') {
854
+ if (!currentVal || val.parentNode !== currentVal.parentNode || val.insertBeforeNode !== currentVal.insertBeforeNode) {
855
+ record.isDirty = true;
856
+ runDOMUpdates();
857
+ }
858
+ } else if (val !== currentVal) {
859
+ record.isDirty = true;
860
+ runDOMUpdates();
861
+ }
862
+ }
863
+ function htmlMutationRunner(record) {
864
+ var val = record.originalValue;
865
+ record.mutations.forEach(function (m) {
866
+ return val = m.mutate(val);
867
+ });
868
+ queueIfNeeded(getTransformedHTML(val), record);
869
+ }
870
+ function classMutationRunner(record) {
871
+ var val = new Set(record.originalValue.split(/\s+/).filter(Boolean));
872
+ record.mutations.forEach(function (m) {
873
+ return m.mutate(val);
874
+ });
875
+ queueIfNeeded(Array.from(val).filter(Boolean).join(' '), record);
876
+ }
877
+ function attrMutationRunner(record) {
878
+ var val = record.originalValue;
879
+ record.mutations.forEach(function (m) {
880
+ return val = m.mutate(val);
881
+ });
882
+ queueIfNeeded(val, record);
883
+ }
884
+ function _loadDOMNodes(_ref) {
885
+ var parentSelector = _ref.parentSelector,
886
+ insertBeforeSelector = _ref.insertBeforeSelector;
887
+ var parentNode = document.querySelector(parentSelector);
888
+ if (!parentNode) return null;
889
+ var insertBeforeNode = insertBeforeSelector ? document.querySelector(insertBeforeSelector) : null;
890
+ if (insertBeforeSelector && !insertBeforeNode) return null;
891
+ return {
892
+ parentNode: parentNode,
893
+ insertBeforeNode: insertBeforeNode
894
+ };
895
+ }
896
+ function positionMutationRunner(record) {
897
+ var val = record.originalValue;
898
+ record.mutations.forEach(function (m) {
899
+ var selectors = m.mutate();
900
+ var newNodes = _loadDOMNodes(selectors);
901
+ val = newNodes || val;
902
+ });
903
+ queueIfNeeded(val, record);
904
+ }
905
+ var getHTMLValue = function getHTMLValue(el) {
906
+ return el.innerHTML;
907
+ };
908
+ var setHTMLValue = function setHTMLValue(el, value) {
909
+ return el.innerHTML = value;
910
+ };
911
+ function getElementHTMLRecord(element) {
912
+ var elementRecord = getElementRecord(element);
913
+ if (!elementRecord.html) {
914
+ elementRecord.html = createElementPropertyRecord(element, 'html', getHTMLValue, setHTMLValue, htmlMutationRunner);
915
+ }
916
+ return elementRecord.html;
917
+ }
918
+ var getElementPosition = function getElementPosition(el) {
919
+ return {
920
+ parentNode: el.parentElement,
921
+ insertBeforeNode: el.nextElementSibling
922
+ };
923
+ };
924
+ var setElementPosition = function setElementPosition(el, value) {
925
+ if (value.insertBeforeNode && !value.parentNode.contains(value.insertBeforeNode)) {
926
+ // skip position mutation - insertBeforeNode not a child of parent. happens
927
+ // when mutation observer for indvidual element fires out of order
928
+ return;
929
+ }
930
+ value.parentNode.insertBefore(el, value.insertBeforeNode);
931
+ };
932
+ function getElementPositionRecord(element) {
933
+ var elementRecord = getElementRecord(element);
934
+ if (!elementRecord.position) {
935
+ elementRecord.position = createElementPropertyRecord(element, 'position', getElementPosition, setElementPosition, positionMutationRunner);
936
+ }
937
+ return elementRecord.position;
938
+ }
939
+ var setClassValue = function setClassValue(el, val) {
940
+ return val ? el.className = val : el.removeAttribute('class');
941
+ };
942
+ var getClassValue = function getClassValue(el) {
943
+ return el.className;
944
+ };
945
+ function getElementClassRecord(el) {
946
+ var elementRecord = getElementRecord(el);
947
+ if (!elementRecord.classes) {
948
+ elementRecord.classes = createElementPropertyRecord(el, 'class', getClassValue, setClassValue, classMutationRunner);
949
+ }
950
+ return elementRecord.classes;
951
+ }
952
+ var getAttrValue = function getAttrValue(attrName) {
953
+ return function (el) {
954
+ var _el$getAttribute;
955
+ return (_el$getAttribute = el.getAttribute(attrName)) != null ? _el$getAttribute : null;
956
+ };
957
+ };
958
+ var setAttrValue = function setAttrValue(attrName) {
959
+ return function (el, val) {
960
+ return val !== null ? el.setAttribute(attrName, val) : el.removeAttribute(attrName);
961
+ };
962
+ };
963
+ function getElementAttributeRecord(el, attr) {
964
+ var elementRecord = getElementRecord(el);
965
+ if (!elementRecord.attributes[attr]) {
966
+ elementRecord.attributes[attr] = createElementPropertyRecord(el, attr, getAttrValue(attr), setAttrValue(attr), attrMutationRunner);
967
+ }
968
+ return elementRecord.attributes[attr];
969
+ }
970
+ function deleteElementPropertyRecord(el, attr) {
971
+ var element = elements.get(el);
972
+ if (!element) return;
973
+ if (attr === 'html') {
974
+ var _element$html, _element$html$observe;
975
+ (_element$html = element.html) == null ? void 0 : (_element$html$observe = _element$html.observer) == null ? void 0 : _element$html$observe.disconnect();
976
+ delete element.html;
977
+ } else if (attr === 'class') {
978
+ var _element$classes, _element$classes$obse;
979
+ (_element$classes = element.classes) == null ? void 0 : (_element$classes$obse = _element$classes.observer) == null ? void 0 : _element$classes$obse.disconnect();
980
+ delete element.classes;
981
+ } else if (attr === 'position') {
982
+ var _element$position, _element$position$obs;
983
+ (_element$position = element.position) == null ? void 0 : (_element$position$obs = _element$position.observer) == null ? void 0 : _element$position$obs.disconnect();
984
+ delete element.position;
985
+ } else {
986
+ var _element$attributes, _element$attributes$a, _element$attributes$a2;
987
+ (_element$attributes = element.attributes) == null ? void 0 : (_element$attributes$a = _element$attributes[attr]) == null ? void 0 : (_element$attributes$a2 = _element$attributes$a.observer) == null ? void 0 : _element$attributes$a2.disconnect();
988
+ delete element.attributes[attr];
989
+ }
990
+ }
991
+ var transformContainer;
992
+ function getTransformedHTML(html) {
993
+ if (!transformContainer) {
994
+ transformContainer = document.createElement('div');
995
+ }
996
+ transformContainer.innerHTML = html;
997
+ return transformContainer.innerHTML;
998
+ }
999
+ function setPropertyValue(el, attr, m) {
1000
+ if (!m.isDirty) return;
1001
+ m.isDirty = false;
1002
+ var val = m.virtualValue;
1003
+ if (!m.mutations.length) {
1004
+ deleteElementPropertyRecord(el, attr);
1005
+ }
1006
+ m.setValue(el, val);
1007
+ }
1008
+ function setValue(m, el) {
1009
+ m.html && setPropertyValue(el, 'html', m.html);
1010
+ m.classes && setPropertyValue(el, 'class', m.classes);
1011
+ m.position && setPropertyValue(el, 'position', m.position);
1012
+ Object.keys(m.attributes).forEach(function (attr) {
1013
+ setPropertyValue(el, attr, m.attributes[attr]);
1014
+ });
1015
+ }
1016
+ function runDOMUpdates() {
1017
+ elements.forEach(setValue);
1018
+ } // find or create ElementPropertyRecord, add mutation to it, then run
1019
+
1020
+ function startMutating(mutation, element) {
1021
+ var record = null;
1022
+ if (mutation.kind === 'html') {
1023
+ record = getElementHTMLRecord(element);
1024
+ } else if (mutation.kind === 'class') {
1025
+ record = getElementClassRecord(element);
1026
+ } else if (mutation.kind === 'attribute') {
1027
+ record = getElementAttributeRecord(element, mutation.attribute);
1028
+ } else if (mutation.kind === 'position') {
1029
+ record = getElementPositionRecord(element);
1030
+ }
1031
+ if (!record) return;
1032
+ record.mutations.push(mutation);
1033
+ record.mutationRunner(record);
1034
+ } // get (existing) ElementPropertyRecord, remove mutation from it, then run
1035
+
1036
+ function stopMutating(mutation, el) {
1037
+ var record = null;
1038
+ if (mutation.kind === 'html') {
1039
+ record = getElementHTMLRecord(el);
1040
+ } else if (mutation.kind === 'class') {
1041
+ record = getElementClassRecord(el);
1042
+ } else if (mutation.kind === 'attribute') {
1043
+ record = getElementAttributeRecord(el, mutation.attribute);
1044
+ } else if (mutation.kind === 'position') {
1045
+ record = getElementPositionRecord(el);
1046
+ }
1047
+ if (!record) return;
1048
+ var index = record.mutations.indexOf(mutation);
1049
+ if (index !== -1) record.mutations.splice(index, 1);
1050
+ record.mutationRunner(record);
1051
+ } // maintain list of elements associated with mutation
1052
+
1053
+ function refreshElementsSet(mutation) {
1054
+ // if a position mutation has already found an element to move, don't move
1055
+ // any more elements
1056
+ if (mutation.kind === 'position' && mutation.elements.size === 1) return;
1057
+ var existingElements = new Set(mutation.elements);
1058
+ var matchingElements = document.querySelectorAll(mutation.selector);
1059
+ matchingElements.forEach(function (el) {
1060
+ if (!existingElements.has(el)) {
1061
+ mutation.elements.add(el);
1062
+ startMutating(mutation, el);
1063
+ }
1064
+ });
1065
+ }
1066
+ function revertMutation(mutation) {
1067
+ mutation.elements.forEach(function (el) {
1068
+ return stopMutating(mutation, el);
1069
+ });
1070
+ mutation.elements.clear();
1071
+ mutations["delete"](mutation);
1072
+ }
1073
+ function refreshAllElementSets() {
1074
+ mutations.forEach(refreshElementsSet);
1075
+ } // Observer for elements that don't exist in the DOM yet
1076
+
1077
+ var observer;
1078
+ function connectGlobalObserver() {
1079
+ if (typeof document === 'undefined') return;
1080
+ if (!observer) {
1081
+ observer = new MutationObserver(function () {
1082
+ refreshAllElementSets();
1083
+ });
1084
+ }
1085
+ refreshAllElementSets();
1086
+ observer.observe(document.documentElement, {
1087
+ childList: true,
1088
+ subtree: true,
1089
+ attributes: false,
1090
+ characterData: false
1091
+ });
1092
+ } // run on init
1093
+
1094
+ connectGlobalObserver();
1095
+ function newMutation(m) {
1096
+ // Not in a browser
1097
+ if (typeof document === 'undefined') return nullController; // add to global index of mutations
1098
+
1099
+ mutations.add(m); // run refresh on init to establish list of elements associated w/ mutation
1100
+
1101
+ refreshElementsSet(m);
1102
+ return {
1103
+ revert: function revert() {
1104
+ revertMutation(m);
1105
+ }
1106
+ };
1107
+ }
1108
+ function html(selector, mutate) {
1109
+ return newMutation({
1110
+ kind: 'html',
1111
+ elements: new Set(),
1112
+ mutate: mutate,
1113
+ selector: selector
1114
+ });
1115
+ }
1116
+ function position(selector, mutate) {
1117
+ return newMutation({
1118
+ kind: 'position',
1119
+ elements: new Set(),
1120
+ mutate: mutate,
1121
+ selector: selector
1122
+ });
1123
+ }
1124
+ function classes(selector, mutate) {
1125
+ return newMutation({
1126
+ kind: 'class',
1127
+ elements: new Set(),
1128
+ mutate: mutate,
1129
+ selector: selector
1130
+ });
1131
+ }
1132
+ function attribute(selector, attribute, mutate) {
1133
+ if (!validAttributeName.test(attribute)) return nullController;
1134
+ if (attribute === 'class' || attribute === 'className') {
1135
+ return classes(selector, function (classnames) {
1136
+ var mutatedClassnames = mutate(Array.from(classnames).join(' '));
1137
+ classnames.clear();
1138
+ if (!mutatedClassnames) return;
1139
+ mutatedClassnames.split(/\s+/g).filter(Boolean).forEach(function (c) {
1140
+ return classnames.add(c);
1141
+ });
1142
+ });
1143
+ }
1144
+ return newMutation({
1145
+ kind: 'attribute',
1146
+ attribute: attribute,
1147
+ elements: new Set(),
1148
+ mutate: mutate,
1149
+ selector: selector
1150
+ });
1151
+ }
1152
+ function declarative(_ref2) {
1153
+ var selector = _ref2.selector,
1154
+ action = _ref2.action,
1155
+ value = _ref2.value,
1156
+ attr = _ref2.attribute,
1157
+ parentSelector = _ref2.parentSelector,
1158
+ insertBeforeSelector = _ref2.insertBeforeSelector;
1159
+ if (attr === 'html') {
1160
+ if (action === 'append') {
1161
+ return html(selector, function (val) {
1162
+ return val + (value != null ? value : '');
1163
+ });
1164
+ } else if (action === 'set') {
1165
+ return html(selector, function () {
1166
+ return value != null ? value : '';
1167
+ });
1168
+ }
1169
+ } else if (attr === 'class') {
1170
+ if (action === 'append') {
1171
+ return classes(selector, function (val) {
1172
+ if (value) val.add(value);
1173
+ });
1174
+ } else if (action === 'remove') {
1175
+ return classes(selector, function (val) {
1176
+ if (value) val["delete"](value);
1177
+ });
1178
+ } else if (action === 'set') {
1179
+ return classes(selector, function (val) {
1180
+ val.clear();
1181
+ if (value) val.add(value);
1182
+ });
1183
+ }
1184
+ } else if (attr === 'position') {
1185
+ if (action === 'set' && parentSelector) {
1186
+ return position(selector, function () {
1187
+ return {
1188
+ insertBeforeSelector: insertBeforeSelector,
1189
+ parentSelector: parentSelector
1190
+ };
1191
+ });
1192
+ }
1193
+ } else {
1194
+ if (action === 'append') {
1195
+ return attribute(selector, attr, function (val) {
1196
+ return val !== null ? val + (value != null ? value : '') : value != null ? value : '';
1197
+ });
1198
+ } else if (action === 'set') {
1199
+ return attribute(selector, attr, function () {
1200
+ return value != null ? value : '';
1201
+ });
1202
+ } else if (action === 'remove') {
1203
+ return attribute(selector, attr, function () {
1204
+ return null;
1205
+ });
1206
+ }
1207
+ }
1208
+ return nullController;
1209
+ }
1210
+ var index = {
1211
+ html: html,
1212
+ classes: classes,
1213
+ attribute: attribute,
1214
+ position: position,
1215
+ declarative: declarative
1216
+ };
1217
+
1218
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1219
+ const _regexCache = {};
1220
+
1221
+ // The top-level condition evaluation function
1222
+ function evalCondition(obj, condition,
1223
+ // Must be included for `condition` to correctly evaluate group Operators
1224
+ savedGroups) {
1225
+ savedGroups = savedGroups || {};
1226
+ // Condition is an object, keys are either specific operators or object paths
1227
+ // values are either arguments for operators or conditions for paths
1228
+ for (const [k, v] of Object.entries(condition)) {
1229
+ switch (k) {
1230
+ case "$or":
1231
+ if (!evalOr(obj, v, savedGroups)) return false;
1232
+ break;
1233
+ case "$nor":
1234
+ if (evalOr(obj, v, savedGroups)) return false;
1235
+ break;
1236
+ case "$and":
1237
+ if (!evalAnd(obj, v, savedGroups)) return false;
1238
+ break;
1239
+ case "$not":
1240
+ if (evalCondition(obj, v, savedGroups)) return false;
1241
+ break;
1242
+ default:
1243
+ if (!evalConditionValue(v, getPath(obj, k), savedGroups)) return false;
1244
+ }
1245
+ }
1246
+ return true;
1247
+ }
1248
+
1249
+ // Return value at dot-separated path of an object
1250
+ function getPath(obj, path) {
1251
+ const parts = path.split(".");
1252
+ let current = obj;
1253
+ for (let i = 0; i < parts.length; i++) {
1254
+ if (current && typeof current === "object" && parts[i] in current) {
1255
+ current = current[parts[i]];
1256
+ } else {
1257
+ return null;
1258
+ }
1259
+ }
1260
+ return current;
1261
+ }
1262
+
1263
+ // Transform a regex string into a real RegExp object
1264
+ function getRegex(regex) {
1265
+ if (!_regexCache[regex]) {
1266
+ _regexCache[regex] = new RegExp(regex.replace(/([^\\])\//g, "$1\\/"));
1267
+ }
1268
+ return _regexCache[regex];
1269
+ }
1270
+
1271
+ // Evaluate a single value against a condition
1272
+ function evalConditionValue(condition, value, savedGroups) {
1273
+ // Simple equality comparisons
1274
+ if (typeof condition === "string") {
1275
+ return value + "" === condition;
1276
+ }
1277
+ if (typeof condition === "number") {
1278
+ return value * 1 === condition;
1279
+ }
1280
+ if (typeof condition === "boolean") {
1281
+ return value !== null && !!value === condition;
1282
+ }
1283
+ if (condition === null) {
1284
+ return value === null;
1285
+ }
1286
+ if (Array.isArray(condition) || !isOperatorObject(condition)) {
1287
+ return JSON.stringify(value) === JSON.stringify(condition);
1288
+ }
1289
+
1290
+ // This is a special operator condition and we should evaluate each one separately
1291
+ for (const op in condition) {
1292
+ if (!evalOperatorCondition(op, value, condition[op], savedGroups)) {
1293
+ return false;
1294
+ }
1295
+ }
1296
+ return true;
1297
+ }
1298
+
1299
+ // If the object has only keys that start with '$'
1300
+ function isOperatorObject(obj) {
1301
+ const keys = Object.keys(obj);
1302
+ return keys.length > 0 && keys.filter(k => k[0] === "$").length === keys.length;
1303
+ }
1304
+
1305
+ // Return the data type of a value
1306
+ function getType(v) {
1307
+ if (v === null) return "null";
1308
+ if (Array.isArray(v)) return "array";
1309
+ const t = typeof v;
1310
+ if (["string", "number", "boolean", "object", "undefined"].includes(t)) {
1311
+ return t;
1312
+ }
1313
+ return "unknown";
1314
+ }
1315
+
1316
+ // At least one element of actual must match the expected condition/value
1317
+ function elemMatch(actual, expected, savedGroups) {
1318
+ if (!Array.isArray(actual)) return false;
1319
+ const check = isOperatorObject(expected) ? v => evalConditionValue(expected, v, savedGroups) : v => evalCondition(v, expected, savedGroups);
1320
+ for (let i = 0; i < actual.length; i++) {
1321
+ if (actual[i] && check(actual[i])) {
1322
+ return true;
1323
+ }
1324
+ }
1325
+ return false;
1326
+ }
1327
+ function isIn(actual, expected) {
1328
+ // Do an intersection if attribute is an array
1329
+ if (Array.isArray(actual)) {
1330
+ return actual.some(el => expected.includes(el));
1331
+ }
1332
+ return expected.includes(actual);
1333
+ }
1334
+
1335
+ // Evaluate a single operator condition
1336
+ function evalOperatorCondition(operator, actual, expected, savedGroups) {
1337
+ switch (operator) {
1338
+ case "$veq":
1339
+ return paddedVersionString(actual) === paddedVersionString(expected);
1340
+ case "$vne":
1341
+ return paddedVersionString(actual) !== paddedVersionString(expected);
1342
+ case "$vgt":
1343
+ return paddedVersionString(actual) > paddedVersionString(expected);
1344
+ case "$vgte":
1345
+ return paddedVersionString(actual) >= paddedVersionString(expected);
1346
+ case "$vlt":
1347
+ return paddedVersionString(actual) < paddedVersionString(expected);
1348
+ case "$vlte":
1349
+ return paddedVersionString(actual) <= paddedVersionString(expected);
1350
+ case "$eq":
1351
+ return actual === expected;
1352
+ case "$ne":
1353
+ return actual !== expected;
1354
+ case "$lt":
1355
+ return actual < expected;
1356
+ case "$lte":
1357
+ return actual <= expected;
1358
+ case "$gt":
1359
+ return actual > expected;
1360
+ case "$gte":
1361
+ return actual >= expected;
1362
+ case "$exists":
1363
+ // Using `!=` and `==` instead of strict checks so it also matches for undefined
1364
+ return expected ? actual != null : actual == null;
1365
+ case "$in":
1366
+ if (!Array.isArray(expected)) return false;
1367
+ return isIn(actual, expected);
1368
+ case "$inGroup":
1369
+ return isIn(actual, savedGroups[expected] || []);
1370
+ case "$notInGroup":
1371
+ return !isIn(actual, savedGroups[expected] || []);
1372
+ case "$nin":
1373
+ if (!Array.isArray(expected)) return false;
1374
+ return !isIn(actual, expected);
1375
+ case "$not":
1376
+ return !evalConditionValue(expected, actual, savedGroups);
1377
+ case "$size":
1378
+ if (!Array.isArray(actual)) return false;
1379
+ return evalConditionValue(expected, actual.length, savedGroups);
1380
+ case "$elemMatch":
1381
+ return elemMatch(actual, expected, savedGroups);
1382
+ case "$all":
1383
+ if (!Array.isArray(actual)) return false;
1384
+ for (let i = 0; i < expected.length; i++) {
1385
+ let passed = false;
1386
+ for (let j = 0; j < actual.length; j++) {
1387
+ if (evalConditionValue(expected[i], actual[j], savedGroups)) {
1388
+ passed = true;
1389
+ break;
1390
+ }
1391
+ }
1392
+ if (!passed) return false;
1393
+ }
1394
+ return true;
1395
+ case "$regex":
1396
+ try {
1397
+ return getRegex(expected).test(actual);
1398
+ } catch (e) {
1399
+ return false;
1400
+ }
1401
+ case "$type":
1402
+ return getType(actual) === expected;
1403
+ default:
1404
+ console.error("Unknown operator: " + operator);
1405
+ return false;
1406
+ }
1407
+ }
1408
+
1409
+ // Recursive $or rule
1410
+ function evalOr(obj, conditions, savedGroups) {
1411
+ if (!conditions.length) return true;
1412
+ for (let i = 0; i < conditions.length; i++) {
1413
+ if (evalCondition(obj, conditions[i], savedGroups)) {
1414
+ return true;
1415
+ }
1416
+ }
1417
+ return false;
1418
+ }
1419
+
1420
+ // Recursive $and rule
1421
+ function evalAnd(obj, conditions, savedGroups) {
1422
+ for (let i = 0; i < conditions.length; i++) {
1423
+ if (!evalCondition(obj, conditions[i], savedGroups)) {
1424
+ return false;
1425
+ }
1426
+ }
1427
+ return true;
1428
+ }
1429
+
1430
+ const EVENT_FEATURE_EVALUATED = "Feature Evaluated";
1431
+ const EVENT_EXPERIMENT_VIEWED = "Experiment Viewed";
1432
+ function getForcedFeatureValues(ctx) {
1433
+ // Merge user and global values
1434
+ const ret = new Map();
1435
+ if (ctx.global.forcedFeatureValues) {
1436
+ ctx.global.forcedFeatureValues.forEach((v, k) => ret.set(k, v));
1437
+ }
1438
+ if (ctx.user.forcedFeatureValues) {
1439
+ ctx.user.forcedFeatureValues.forEach((v, k) => ret.set(k, v));
1440
+ }
1441
+ return ret;
1442
+ }
1443
+ function getForcedVariations(ctx) {
1444
+ // Merge user and global values
1445
+ if (ctx.global.forcedVariations && ctx.user.forcedVariations) {
1446
+ return {
1447
+ ...ctx.global.forcedVariations,
1448
+ ...ctx.user.forcedVariations
1449
+ };
1450
+ } else if (ctx.global.forcedVariations) {
1451
+ return ctx.global.forcedVariations;
1452
+ } else if (ctx.user.forcedVariations) {
1453
+ return ctx.user.forcedVariations;
1454
+ } else {
1455
+ return {};
1456
+ }
1457
+ }
1458
+ async function safeCall(fn) {
1459
+ try {
1460
+ await fn();
1461
+ } catch (e) {
1462
+ // Do nothing
1463
+ }
1464
+ }
1465
+ function onExperimentViewed(ctx, experiment, result) {
1466
+ // Make sure a tracking callback is only fired once per unique experiment
1467
+ if (ctx.user.trackedExperiments) {
1468
+ const k = getExperimentDedupeKey(experiment, result);
1469
+ if (ctx.user.trackedExperiments.has(k)) {
1470
+ return [];
1471
+ }
1472
+ ctx.user.trackedExperiments.add(k);
1473
+ }
1474
+ if (ctx.user.enableDevMode && ctx.user.devLogs) {
1475
+ ctx.user.devLogs.push({
1476
+ experiment,
1477
+ result,
1478
+ timestamp: Date.now().toString(),
1479
+ logType: "experiment"
1480
+ });
1481
+ }
1482
+ const calls = [];
1483
+ if (ctx.global.trackingCallback) {
1484
+ const cb = ctx.global.trackingCallback;
1485
+ calls.push(safeCall(() => cb(experiment, result, ctx.user)));
1486
+ }
1487
+ if (ctx.user.trackingCallback) {
1488
+ const cb = ctx.user.trackingCallback;
1489
+ calls.push(safeCall(() => cb(experiment, result)));
1490
+ }
1491
+ if (ctx.global.eventLogger) {
1492
+ const cb = ctx.global.eventLogger;
1493
+ calls.push(safeCall(() => cb(EVENT_EXPERIMENT_VIEWED, {
1494
+ experimentId: experiment.key,
1495
+ variationId: result.key,
1496
+ hashAttribute: result.hashAttribute,
1497
+ hashValue: result.hashValue
1498
+ }, ctx.user)));
1499
+ }
1500
+ return calls;
1501
+ }
1502
+ function onFeatureUsage(ctx, key, ret) {
1503
+ // Only track a feature once, unless the assigned value changed
1504
+ if (ctx.user.trackedFeatureUsage) {
1505
+ const stringifiedValue = JSON.stringify(ret.value);
1506
+ if (ctx.user.trackedFeatureUsage[key] === stringifiedValue) return;
1507
+ ctx.user.trackedFeatureUsage[key] = stringifiedValue;
1508
+ if (ctx.user.enableDevMode && ctx.user.devLogs) {
1509
+ ctx.user.devLogs.push({
1510
+ featureKey: key,
1511
+ result: ret,
1512
+ timestamp: Date.now().toString(),
1513
+ logType: "feature"
1514
+ });
1515
+ }
1516
+ }
1517
+ if (ctx.global.onFeatureUsage) {
1518
+ const cb = ctx.global.onFeatureUsage;
1519
+ safeCall(() => cb(key, ret, ctx.user));
1520
+ }
1521
+ if (ctx.user.onFeatureUsage) {
1522
+ const cb = ctx.user.onFeatureUsage;
1523
+ safeCall(() => cb(key, ret));
1524
+ }
1525
+ if (ctx.global.eventLogger) {
1526
+ const cb = ctx.global.eventLogger;
1527
+ safeCall(() => cb(EVENT_FEATURE_EVALUATED, {
1528
+ feature: key,
1529
+ source: ret.source,
1530
+ value: ret.value,
1531
+ ruleId: ret.source === "defaultValue" ? "$default" : ret.ruleId || "",
1532
+ variationId: ret.experimentResult ? ret.experimentResult.key : ""
1533
+ }, ctx.user));
1534
+ }
1535
+ }
1536
+ function evalFeature(id, ctx) {
1537
+ if (ctx.stack.evaluatedFeatures.has(id)) {
1538
+ return getFeatureResult(ctx, id, null, "cyclicPrerequisite");
1539
+ }
1540
+ ctx.stack.evaluatedFeatures.add(id);
1541
+ ctx.stack.id = id;
1542
+
1543
+ // Global override
1544
+ const forcedValues = getForcedFeatureValues(ctx);
1545
+ if (forcedValues.has(id)) {
1546
+ return getFeatureResult(ctx, id, forcedValues.get(id), "override");
1547
+ }
1548
+
1549
+ // Unknown feature id
1550
+ if (!ctx.global.features || !ctx.global.features[id]) {
1551
+ return getFeatureResult(ctx, id, null, "unknownFeature");
1552
+ }
1553
+
1554
+ // Get the feature
1555
+ const feature = ctx.global.features[id];
1556
+
1557
+ // Loop through the rules
1558
+ if (feature.rules) {
1559
+ const evaluatedFeatures = new Set(ctx.stack.evaluatedFeatures);
1560
+ rules: for (const rule of feature.rules) {
1561
+ // If there are prerequisite flag(s), evaluate them
1562
+ if (rule.parentConditions) {
1563
+ for (const parentCondition of rule.parentConditions) {
1564
+ ctx.stack.evaluatedFeatures = new Set(evaluatedFeatures);
1565
+ const parentResult = evalFeature(parentCondition.id, ctx);
1566
+ // break out for cyclic prerequisites
1567
+ if (parentResult.source === "cyclicPrerequisite") {
1568
+ return getFeatureResult(ctx, id, null, "cyclicPrerequisite");
1569
+ }
1570
+ const evalObj = {
1571
+ value: parentResult.value
1572
+ };
1573
+ const evaled = evalCondition(evalObj, parentCondition.condition || {});
1574
+ if (!evaled) {
1575
+ // blocking prerequisite eval failed: feature evaluation fails
1576
+ if (parentCondition.gate) {
1577
+ return getFeatureResult(ctx, id, null, "prerequisite");
1578
+ }
1579
+ continue rules;
1580
+ }
1581
+ }
1582
+ }
1583
+
1584
+ // If there are filters for who is included (e.g. namespaces)
1585
+ if (rule.filters && isFilteredOut(rule.filters, ctx)) {
1586
+ continue;
1587
+ }
1588
+
1589
+ // Feature value is being forced
1590
+ if ("force" in rule) {
1591
+ // If it's a conditional rule, skip if the condition doesn't pass
1592
+ if (rule.condition && !conditionPasses(rule.condition, ctx)) {
1593
+ continue;
1594
+ }
1595
+
1596
+ // If this is a percentage rollout, skip if not included
1597
+ if (!isIncludedInRollout(ctx, rule.seed || id, rule.hashAttribute, ctx.user.saveStickyBucketAssignmentDoc && !rule.disableStickyBucketing ? rule.fallbackAttribute : undefined, rule.range, rule.coverage, rule.hashVersion)) {
1598
+ continue;
1599
+ }
1600
+
1601
+ // If this was a remotely evaluated experiment, fire the tracking callbacks
1602
+ if (rule.tracks) {
1603
+ rule.tracks.forEach(t => {
1604
+ const calls = onExperimentViewed(ctx, t.experiment, t.result);
1605
+ if (!calls.length && ctx.global.saveDeferredTrack) {
1606
+ ctx.global.saveDeferredTrack({
1607
+ experiment: t.experiment,
1608
+ result: t.result
1609
+ });
1610
+ }
1611
+ });
1612
+ }
1613
+ return getFeatureResult(ctx, id, rule.force, "force", rule.id);
1614
+ }
1615
+ if (!rule.variations) {
1616
+ continue;
1617
+ }
1618
+
1619
+ // For experiment rules, run an experiment
1620
+ const exp = {
1621
+ variations: rule.variations,
1622
+ key: rule.key || id
1623
+ };
1624
+ if ("coverage" in rule) exp.coverage = rule.coverage;
1625
+ if (rule.weights) exp.weights = rule.weights;
1626
+ if (rule.hashAttribute) exp.hashAttribute = rule.hashAttribute;
1627
+ if (rule.fallbackAttribute) exp.fallbackAttribute = rule.fallbackAttribute;
1628
+ if (rule.disableStickyBucketing) exp.disableStickyBucketing = rule.disableStickyBucketing;
1629
+ if (rule.bucketVersion !== undefined) exp.bucketVersion = rule.bucketVersion;
1630
+ if (rule.minBucketVersion !== undefined) exp.minBucketVersion = rule.minBucketVersion;
1631
+ if (rule.namespace) exp.namespace = rule.namespace;
1632
+ if (rule.meta) exp.meta = rule.meta;
1633
+ if (rule.ranges) exp.ranges = rule.ranges;
1634
+ if (rule.name) exp.name = rule.name;
1635
+ if (rule.phase) exp.phase = rule.phase;
1636
+ if (rule.seed) exp.seed = rule.seed;
1637
+ if (rule.hashVersion) exp.hashVersion = rule.hashVersion;
1638
+ if (rule.filters) exp.filters = rule.filters;
1639
+ if (rule.condition) exp.condition = rule.condition;
1640
+
1641
+ // Only return a value if the user is part of the experiment
1642
+ const {
1643
+ result
1644
+ } = runExperiment(exp, id, ctx);
1645
+ ctx.global.onExperimentEval && ctx.global.onExperimentEval(exp, result);
1646
+ if (result.inExperiment && !result.passthrough) {
1647
+ return getFeatureResult(ctx, id, result.value, "experiment", rule.id, exp, result);
1648
+ }
1649
+ }
1650
+ }
1651
+
1652
+ // Fall back to using the default value
1653
+ return getFeatureResult(ctx, id, feature.defaultValue === undefined ? null : feature.defaultValue, "defaultValue");
1654
+ }
1655
+ function runExperiment(experiment, featureId, ctx) {
1656
+ const key = experiment.key;
1657
+ const numVariations = experiment.variations.length;
1658
+
1659
+ // 1. If experiment has less than 2 variations, return immediately
1660
+ if (numVariations < 2) {
1661
+ return {
1662
+ result: getExperimentResult(ctx, experiment, -1, false, featureId)
1663
+ };
1664
+ }
1665
+
1666
+ // 2. If the context is disabled, return immediately
1667
+ if (ctx.global.enabled === false || ctx.user.enabled === false) {
1668
+ return {
1669
+ result: getExperimentResult(ctx, experiment, -1, false, featureId)
1670
+ };
1671
+ }
1672
+
1673
+ // 2.5. Merge in experiment overrides from the context
1674
+ experiment = mergeOverrides(experiment, ctx);
1675
+
1676
+ // 2.6 New, more powerful URL targeting
1677
+ if (experiment.urlPatterns && !isURLTargeted(ctx.user.url || "", experiment.urlPatterns)) {
1678
+ return {
1679
+ result: getExperimentResult(ctx, experiment, -1, false, featureId)
1680
+ };
1681
+ }
1682
+
1683
+ // 3. If a variation is forced from a querystring, return the forced variation
1684
+ const qsOverride = getQueryStringOverride(key, ctx.user.url || "", numVariations);
1685
+ if (qsOverride !== null) {
1686
+ return {
1687
+ result: getExperimentResult(ctx, experiment, qsOverride, false, featureId)
1688
+ };
1689
+ }
1690
+
1691
+ // 4. If a variation is forced in the context, return the forced variation
1692
+ const forcedVariations = getForcedVariations(ctx);
1693
+ if (key in forcedVariations) {
1694
+ const variation = forcedVariations[key];
1695
+ return {
1696
+ result: getExperimentResult(ctx, experiment, variation, false, featureId)
1697
+ };
1698
+ }
1699
+
1700
+ // 5. Exclude if a draft experiment or not active
1701
+ if (experiment.status === "draft" || experiment.active === false) {
1702
+ return {
1703
+ result: getExperimentResult(ctx, experiment, -1, false, featureId)
1704
+ };
1705
+ }
1706
+
1707
+ // 6. Get the hash attribute and return if empty
1708
+ const {
1709
+ hashAttribute,
1710
+ hashValue
1711
+ } = getHashAttribute(ctx, experiment.hashAttribute, ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing ? experiment.fallbackAttribute : undefined);
1712
+ if (!hashValue) {
1713
+ return {
1714
+ result: getExperimentResult(ctx, experiment, -1, false, featureId)
1715
+ };
1716
+ }
1717
+ let assigned = -1;
1718
+ let foundStickyBucket = false;
1719
+ let stickyBucketVersionIsBlocked = false;
1720
+ if (ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing) {
1721
+ const {
1722
+ variation,
1723
+ versionIsBlocked
1724
+ } = getStickyBucketVariation({
1725
+ ctx,
1726
+ expKey: experiment.key,
1727
+ expBucketVersion: experiment.bucketVersion,
1728
+ expHashAttribute: experiment.hashAttribute,
1729
+ expFallbackAttribute: experiment.fallbackAttribute,
1730
+ expMinBucketVersion: experiment.minBucketVersion,
1731
+ expMeta: experiment.meta
1732
+ });
1733
+ foundStickyBucket = variation >= 0;
1734
+ assigned = variation;
1735
+ stickyBucketVersionIsBlocked = !!versionIsBlocked;
1736
+ }
1737
+
1738
+ // Some checks are not needed if we already have a sticky bucket
1739
+ if (!foundStickyBucket) {
1740
+ // 7. Exclude if user is filtered out (used to be called "namespace")
1741
+ if (experiment.filters) {
1742
+ if (isFilteredOut(experiment.filters, ctx)) {
1743
+ return {
1744
+ result: getExperimentResult(ctx, experiment, -1, false, featureId)
1745
+ };
1746
+ }
1747
+ } else if (experiment.namespace && !inNamespace(hashValue, experiment.namespace)) {
1748
+ return {
1749
+ result: getExperimentResult(ctx, experiment, -1, false, featureId)
1750
+ };
1751
+ }
1752
+
1753
+ // 7.5. Exclude if experiment.include returns false or throws
1754
+ if (experiment.include && !isIncluded(experiment.include)) {
1755
+ return {
1756
+ result: getExperimentResult(ctx, experiment, -1, false, featureId)
1757
+ };
1758
+ }
1759
+
1760
+ // 8. Exclude if condition is false
1761
+ if (experiment.condition && !conditionPasses(experiment.condition, ctx)) {
1762
+ return {
1763
+ result: getExperimentResult(ctx, experiment, -1, false, featureId)
1764
+ };
1765
+ }
1766
+
1767
+ // 8.05. Exclude if prerequisites are not met
1768
+ if (experiment.parentConditions) {
1769
+ const evaluatedFeatures = new Set(ctx.stack.evaluatedFeatures);
1770
+ for (const parentCondition of experiment.parentConditions) {
1771
+ ctx.stack.evaluatedFeatures = new Set(evaluatedFeatures);
1772
+ const parentResult = evalFeature(parentCondition.id, ctx);
1773
+ // break out for cyclic prerequisites
1774
+ if (parentResult.source === "cyclicPrerequisite") {
1775
+ return {
1776
+ result: getExperimentResult(ctx, experiment, -1, false, featureId)
1777
+ };
1778
+ }
1779
+ const evalObj = {
1780
+ value: parentResult.value
1781
+ };
1782
+ if (!evalCondition(evalObj, parentCondition.condition || {})) {
1783
+ return {
1784
+ result: getExperimentResult(ctx, experiment, -1, false, featureId)
1785
+ };
1786
+ }
1787
+ }
1788
+ }
1789
+
1790
+ // 8.1. Exclude if user is not in a required group
1791
+ if (experiment.groups && !hasGroupOverlap(experiment.groups, ctx)) {
1792
+ return {
1793
+ result: getExperimentResult(ctx, experiment, -1, false, featureId)
1794
+ };
1795
+ }
1796
+ }
1797
+
1798
+ // 8.2. Old style URL targeting
1799
+ if (experiment.url && !urlIsValid(experiment.url, ctx)) {
1800
+ return {
1801
+ result: getExperimentResult(ctx, experiment, -1, false, featureId)
1802
+ };
1803
+ }
1804
+
1805
+ // 9. Get the variation from the sticky bucket or get bucket ranges and choose variation
1806
+ const n = hash(experiment.seed || key, hashValue, experiment.hashVersion || 1);
1807
+ if (n === null) {
1808
+ return {
1809
+ result: getExperimentResult(ctx, experiment, -1, false, featureId)
1810
+ };
1811
+ }
1812
+ if (!foundStickyBucket) {
1813
+ const ranges = experiment.ranges || getBucketRanges(numVariations, experiment.coverage === undefined ? 1 : experiment.coverage, experiment.weights);
1814
+ assigned = chooseVariation(n, ranges);
1815
+ }
1816
+
1817
+ // 9.5 Unenroll if any prior sticky buckets are blocked by version
1818
+ if (stickyBucketVersionIsBlocked) {
1819
+ return {
1820
+ result: getExperimentResult(ctx, experiment, -1, false, featureId, undefined, true)
1821
+ };
1822
+ }
1823
+
1824
+ // 10. Return if not in experiment
1825
+ if (assigned < 0) {
1826
+ return {
1827
+ result: getExperimentResult(ctx, experiment, -1, false, featureId)
1828
+ };
1829
+ }
1830
+
1831
+ // 11. Experiment has a forced variation
1832
+ if ("force" in experiment) {
1833
+ return {
1834
+ result: getExperimentResult(ctx, experiment, experiment.force === undefined ? -1 : experiment.force, false, featureId)
1835
+ };
1836
+ }
1837
+
1838
+ // 12. Exclude if in QA mode
1839
+ if (ctx.global.qaMode || ctx.user.qaMode) {
1840
+ return {
1841
+ result: getExperimentResult(ctx, experiment, -1, false, featureId)
1842
+ };
1843
+ }
1844
+
1845
+ // 12.5. Exclude if experiment is stopped
1846
+ if (experiment.status === "stopped") {
1847
+ return {
1848
+ result: getExperimentResult(ctx, experiment, -1, false, featureId)
1849
+ };
1850
+ }
1851
+
1852
+ // 13. Build the result object
1853
+ const result = getExperimentResult(ctx, experiment, assigned, true, featureId, n, foundStickyBucket);
1854
+
1855
+ // 13.5. Persist sticky bucket
1856
+ if (ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing) {
1857
+ const {
1858
+ changed,
1859
+ key: attrKey,
1860
+ doc
1861
+ } = generateStickyBucketAssignmentDoc(ctx, hashAttribute, toString(hashValue), {
1862
+ [getStickyBucketExperimentKey(experiment.key, experiment.bucketVersion)]: result.key
1863
+ });
1864
+ if (changed) {
1865
+ // update local docs
1866
+ ctx.user.stickyBucketAssignmentDocs = ctx.user.stickyBucketAssignmentDocs || {};
1867
+ ctx.user.stickyBucketAssignmentDocs[attrKey] = doc;
1868
+ // save doc
1869
+ ctx.user.saveStickyBucketAssignmentDoc(doc);
1870
+ }
1871
+ }
1872
+
1873
+ // 14. Fire the tracking callback(s)
1874
+ // Store the promise in case we're awaiting it (ex: browser url redirects)
1875
+ const trackingCalls = onExperimentViewed(ctx, experiment, result);
1876
+ if (trackingCalls.length === 0 && ctx.global.saveDeferredTrack) {
1877
+ ctx.global.saveDeferredTrack({
1878
+ experiment,
1879
+ result
1880
+ });
1881
+ }
1882
+ const trackingCall = !trackingCalls.length ? undefined : trackingCalls.length === 1 ? trackingCalls[0] : Promise.all(trackingCalls).then(() => {});
1883
+
1884
+ // 14.1 Keep track of completed changeIds
1885
+ "changeId" in experiment && experiment.changeId && ctx.global.recordChangeId && ctx.global.recordChangeId(experiment.changeId);
1886
+ return {
1887
+ result,
1888
+ trackingCall
1889
+ };
1890
+ }
1891
+ function getFeatureResult(ctx, key, value, source, ruleId, experiment, result) {
1892
+ const ret = {
1893
+ value,
1894
+ on: !!value,
1895
+ off: !value,
1896
+ source,
1897
+ ruleId: ruleId || ""
1898
+ };
1899
+ if (experiment) ret.experiment = experiment;
1900
+ if (result) ret.experimentResult = result;
1901
+
1902
+ // Track the usage of this feature in real-time
1903
+ if (source !== "override") {
1904
+ onFeatureUsage(ctx, key, ret);
1905
+ }
1906
+ return ret;
1907
+ }
1908
+ function getAttributes(ctx) {
1909
+ return {
1910
+ ...ctx.user.attributes,
1911
+ ...ctx.user.attributeOverrides
1912
+ };
1913
+ }
1914
+ function conditionPasses(condition, ctx) {
1915
+ return evalCondition(getAttributes(ctx), condition, ctx.global.savedGroups || {});
1916
+ }
1917
+ function isFilteredOut(filters, ctx) {
1918
+ return filters.some(filter => {
1919
+ const {
1920
+ hashValue
1921
+ } = getHashAttribute(ctx, filter.attribute);
1922
+ if (!hashValue) return true;
1923
+ const n = hash(filter.seed, hashValue, filter.hashVersion || 2);
1924
+ if (n === null) return true;
1925
+ return !filter.ranges.some(r => inRange(n, r));
1926
+ });
1927
+ }
1928
+ function isIncludedInRollout(ctx, seed, hashAttribute, fallbackAttribute, range, coverage, hashVersion) {
1929
+ if (!range && coverage === undefined) return true;
1930
+ if (!range && coverage === 0) return false;
1931
+ const {
1932
+ hashValue
1933
+ } = getHashAttribute(ctx, hashAttribute, fallbackAttribute);
1934
+ if (!hashValue) {
1935
+ return false;
1936
+ }
1937
+ const n = hash(seed, hashValue, hashVersion || 1);
1938
+ if (n === null) return false;
1939
+ return range ? inRange(n, range) : coverage !== undefined ? n <= coverage : true;
1940
+ }
1941
+ function getExperimentResult(ctx, experiment, variationIndex, hashUsed, featureId, bucket, stickyBucketUsed) {
1942
+ let inExperiment = true;
1943
+ // If assigned variation is not valid, use the baseline and mark the user as not in the experiment
1944
+ if (variationIndex < 0 || variationIndex >= experiment.variations.length) {
1945
+ variationIndex = 0;
1946
+ inExperiment = false;
1947
+ }
1948
+ const {
1949
+ hashAttribute,
1950
+ hashValue
1951
+ } = getHashAttribute(ctx, experiment.hashAttribute, ctx.user.saveStickyBucketAssignmentDoc && !experiment.disableStickyBucketing ? experiment.fallbackAttribute : undefined);
1952
+ const meta = experiment.meta ? experiment.meta[variationIndex] : {};
1953
+ const res = {
1954
+ key: meta.key || "" + variationIndex,
1955
+ featureId,
1956
+ inExperiment,
1957
+ hashUsed,
1958
+ variationId: variationIndex,
1959
+ value: experiment.variations[variationIndex],
1960
+ hashAttribute,
1961
+ hashValue,
1962
+ stickyBucketUsed: !!stickyBucketUsed
1963
+ };
1964
+ if (meta.name) res.name = meta.name;
1965
+ if (bucket !== undefined) res.bucket = bucket;
1966
+ if (meta.passthrough) res.passthrough = meta.passthrough;
1967
+ return res;
1968
+ }
1969
+ function mergeOverrides(experiment, ctx) {
1970
+ const key = experiment.key;
1971
+ const o = ctx.global.overrides;
1972
+ if (o && o[key]) {
1973
+ experiment = Object.assign({}, experiment, o[key]);
1974
+ if (typeof experiment.url === "string") {
1975
+ experiment.url = getUrlRegExp(
1976
+ // eslint-disable-next-line
1977
+ experiment.url);
1978
+ }
1979
+ }
1980
+ return experiment;
1981
+ }
1982
+ function getHashAttribute(ctx, attr, fallback) {
1983
+ let hashAttribute = attr || "id";
1984
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1985
+ let hashValue = "";
1986
+ const attributes = getAttributes(ctx);
1987
+ if (attributes[hashAttribute]) {
1988
+ hashValue = attributes[hashAttribute];
1989
+ }
1990
+
1991
+ // if no match, try fallback
1992
+ if (!hashValue && fallback) {
1993
+ if (attributes[fallback]) {
1994
+ hashValue = attributes[fallback];
1995
+ }
1996
+ if (hashValue) {
1997
+ hashAttribute = fallback;
1998
+ }
1999
+ }
2000
+ return {
2001
+ hashAttribute,
2002
+ hashValue
2003
+ };
2004
+ }
2005
+ function urlIsValid(urlRegex, ctx) {
2006
+ const url = ctx.user.url;
2007
+ if (!url) return false;
2008
+ const pathOnly = url.replace(/^https?:\/\//, "").replace(/^[^/]*\//, "/");
2009
+ if (urlRegex.test(url)) return true;
2010
+ if (urlRegex.test(pathOnly)) return true;
2011
+ return false;
2012
+ }
2013
+ function hasGroupOverlap(expGroups, ctx) {
2014
+ const groups = ctx.global.groups || {};
2015
+ for (let i = 0; i < expGroups.length; i++) {
2016
+ if (groups[expGroups[i]]) return true;
2017
+ }
2018
+ return false;
2019
+ }
2020
+ function getStickyBucketVariation(_ref) {
2021
+ let {
2022
+ ctx,
2023
+ expKey,
2024
+ expBucketVersion,
2025
+ expHashAttribute,
2026
+ expFallbackAttribute,
2027
+ expMinBucketVersion,
2028
+ expMeta
2029
+ } = _ref;
2030
+ expBucketVersion = expBucketVersion || 0;
2031
+ expMinBucketVersion = expMinBucketVersion || 0;
2032
+ expHashAttribute = expHashAttribute || "id";
2033
+ expMeta = expMeta || [];
2034
+ const id = getStickyBucketExperimentKey(expKey, expBucketVersion);
2035
+ const assignments = getStickyBucketAssignments(ctx, expHashAttribute, expFallbackAttribute);
2036
+
2037
+ // users with any blocked bucket version (0 to minExperimentBucketVersion) are excluded from the test
2038
+ if (expMinBucketVersion > 0) {
2039
+ for (let i = 0; i <= expMinBucketVersion; i++) {
2040
+ const blockedKey = getStickyBucketExperimentKey(expKey, i);
2041
+ if (assignments[blockedKey] !== undefined) {
2042
+ return {
2043
+ variation: -1,
2044
+ versionIsBlocked: true
2045
+ };
2046
+ }
2047
+ }
2048
+ }
2049
+ const variationKey = assignments[id];
2050
+ if (variationKey === undefined)
2051
+ // no assignment found
2052
+ return {
2053
+ variation: -1
2054
+ };
2055
+ const variation = expMeta.findIndex(m => m.key === variationKey);
2056
+ if (variation < 0)
2057
+ // invalid assignment, treat as "no assignment found"
2058
+ return {
2059
+ variation: -1
2060
+ };
2061
+ return {
2062
+ variation
2063
+ };
2064
+ }
2065
+ function getStickyBucketExperimentKey(experimentKey, experimentBucketVersion) {
2066
+ experimentBucketVersion = experimentBucketVersion || 0;
2067
+ return `${experimentKey}__${experimentBucketVersion}`;
2068
+ }
2069
+ function getStickyBucketAttributeKey(attributeName, attributeValue) {
2070
+ return `${attributeName}||${attributeValue}`;
2071
+ }
2072
+ function getStickyBucketAssignments(ctx, expHashAttribute, expFallbackAttribute) {
2073
+ if (!ctx.user.stickyBucketAssignmentDocs) return {};
2074
+ const {
2075
+ hashAttribute,
2076
+ hashValue
2077
+ } = getHashAttribute(ctx, expHashAttribute);
2078
+ const hashKey = getStickyBucketAttributeKey(hashAttribute, toString(hashValue));
2079
+ const {
2080
+ hashAttribute: fallbackAttribute,
2081
+ hashValue: fallbackValue
2082
+ } = getHashAttribute(ctx, expFallbackAttribute);
2083
+ const fallbackKey = fallbackValue ? getStickyBucketAttributeKey(fallbackAttribute, toString(fallbackValue)) : null;
2084
+ const assignments = {};
2085
+ if (fallbackKey && ctx.user.stickyBucketAssignmentDocs[fallbackKey]) {
2086
+ Object.assign(assignments, ctx.user.stickyBucketAssignmentDocs[fallbackKey].assignments || {});
2087
+ }
2088
+ if (ctx.user.stickyBucketAssignmentDocs[hashKey]) {
2089
+ Object.assign(assignments, ctx.user.stickyBucketAssignmentDocs[hashKey].assignments || {});
2090
+ }
2091
+ return assignments;
2092
+ }
2093
+ function generateStickyBucketAssignmentDoc(ctx, attributeName, attributeValue, assignments) {
2094
+ const key = getStickyBucketAttributeKey(attributeName, attributeValue);
2095
+ const existingAssignments = ctx.user.stickyBucketAssignmentDocs && ctx.user.stickyBucketAssignmentDocs[key] ? ctx.user.stickyBucketAssignmentDocs[key].assignments || {} : {};
2096
+ const newAssignments = {
2097
+ ...existingAssignments,
2098
+ ...assignments
2099
+ };
2100
+ const changed = JSON.stringify(existingAssignments) !== JSON.stringify(newAssignments);
2101
+ return {
2102
+ key,
2103
+ doc: {
2104
+ attributeName,
2105
+ attributeValue,
2106
+ assignments: newAssignments
2107
+ },
2108
+ changed
2109
+ };
2110
+ }
2111
+ function deriveStickyBucketIdentifierAttributes(ctx, data) {
2112
+ const attributes = new Set();
2113
+ const features = data && data.features ? data.features : ctx.global.features || {};
2114
+ const experiments = data && data.experiments ? data.experiments : ctx.global.experiments || [];
2115
+ Object.keys(features).forEach(id => {
2116
+ const feature = features[id];
2117
+ if (feature.rules) {
2118
+ for (const rule of feature.rules) {
2119
+ if (rule.variations) {
2120
+ attributes.add(rule.hashAttribute || "id");
2121
+ if (rule.fallbackAttribute) {
2122
+ attributes.add(rule.fallbackAttribute);
2123
+ }
2124
+ }
2125
+ }
2126
+ }
2127
+ });
2128
+ experiments.map(experiment => {
2129
+ attributes.add(experiment.hashAttribute || "id");
2130
+ if (experiment.fallbackAttribute) {
2131
+ attributes.add(experiment.fallbackAttribute);
2132
+ }
2133
+ });
2134
+ return Array.from(attributes);
2135
+ }
2136
+ async function getAllStickyBucketAssignmentDocs(ctx, stickyBucketService, data) {
2137
+ const attributes = getStickyBucketAttributes(ctx, data);
2138
+ return stickyBucketService.getAllAssignments(attributes);
2139
+ }
2140
+ function getStickyBucketAttributes(ctx, data) {
2141
+ const attributes = {};
2142
+ const stickyBucketIdentifierAttributes = deriveStickyBucketIdentifierAttributes(ctx, data);
2143
+ stickyBucketIdentifierAttributes.forEach(attr => {
2144
+ const {
2145
+ hashValue
2146
+ } = getHashAttribute(ctx, attr);
2147
+ attributes[attr] = toString(hashValue);
2148
+ });
2149
+ return attributes;
2150
+ }
2151
+ async function decryptPayload(data, decryptionKey, subtle) {
2152
+ data = {
2153
+ ...data
2154
+ };
2155
+ if (data.encryptedFeatures) {
2156
+ try {
2157
+ data.features = JSON.parse(await decrypt(data.encryptedFeatures, decryptionKey, subtle));
2158
+ } catch (e) {
2159
+ console.error(e);
2160
+ }
2161
+ delete data.encryptedFeatures;
2162
+ }
2163
+ if (data.encryptedExperiments) {
2164
+ try {
2165
+ data.experiments = JSON.parse(await decrypt(data.encryptedExperiments, decryptionKey, subtle));
2166
+ } catch (e) {
2167
+ console.error(e);
2168
+ }
2169
+ delete data.encryptedExperiments;
2170
+ }
2171
+ if (data.encryptedSavedGroups) {
2172
+ try {
2173
+ data.savedGroups = JSON.parse(await decrypt(data.encryptedSavedGroups, decryptionKey, subtle));
2174
+ } catch (e) {
2175
+ console.error(e);
2176
+ }
2177
+ delete data.encryptedSavedGroups;
2178
+ }
2179
+ return data;
2180
+ }
2181
+ function getApiHosts(options) {
2182
+ const defaultHost = options.apiHost || "https://cdn.growthbook.io";
2183
+ return {
2184
+ apiHost: defaultHost.replace(/\/*$/, ""),
2185
+ streamingHost: (options.streamingHost || defaultHost).replace(/\/*$/, ""),
2186
+ apiRequestHeaders: options.apiHostRequestHeaders,
2187
+ streamingHostRequestHeaders: options.streamingHostRequestHeaders
2188
+ };
2189
+ }
2190
+ function getExperimentDedupeKey(experiment, result) {
2191
+ return result.hashAttribute + result.hashValue + experiment.key + result.variationId;
2192
+ }
2193
+
2194
+ const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
2195
+ const SDK_VERSION$1 = loadSDKVersion();
2196
+ class GrowthBook {
2197
+ // context is technically private, but some tools depend on it so we can't mangle the name
2198
+
2199
+ // Properties and methods that start with "_" are mangled by Terser (saves ~150 bytes)
2200
+
2201
+ constructor(options) {
2202
+ options = options || {};
2203
+ // These properties are all initialized in the constructor instead of above
2204
+ // This saves ~80 bytes in the final output
2205
+ this.version = SDK_VERSION$1;
2206
+ this._options = this.context = options;
2207
+ this._renderer = options.renderer || null;
2208
+ this._trackedExperiments = new Set();
2209
+ this._completedChangeIds = new Set();
2210
+ this._trackedFeatures = {};
2211
+ this.debug = !!options.debug;
2212
+ this._subscriptions = new Set();
2213
+ this.ready = false;
2214
+ this._assigned = new Map();
2215
+ this._activeAutoExperiments = new Map();
2216
+ this._triggeredExpKeys = new Set();
2217
+ this._initialized = false;
2218
+ this._redirectedUrl = "";
2219
+ this._deferredTrackingCalls = new Map();
2220
+ this._autoExperimentsAllowed = !options.disableExperimentsOnLoad;
2221
+ this._destroyCallbacks = [];
2222
+ this.logs = [];
2223
+ this.log = this.log.bind(this);
2224
+ this._saveDeferredTrack = this._saveDeferredTrack.bind(this);
2225
+ this._fireSubscriptions = this._fireSubscriptions.bind(this);
2226
+ this._recordChangedId = this._recordChangedId.bind(this);
2227
+ if (options.remoteEval) {
2228
+ if (options.decryptionKey) {
2229
+ throw new Error("Encryption is not available for remoteEval");
2230
+ }
2231
+ if (!options.clientKey) {
2232
+ throw new Error("Missing clientKey");
2233
+ }
2234
+ let isGbHost = false;
2235
+ try {
2236
+ isGbHost = !!new URL(options.apiHost || "").hostname.match(/growthbook\.io$/i);
2237
+ } catch (e) {
2238
+ // ignore invalid URLs
2239
+ }
2240
+ if (isGbHost) {
2241
+ throw new Error("Cannot use remoteEval on GrowthBook Cloud");
2242
+ }
2243
+ } else {
2244
+ if (options.cacheKeyAttributes) {
2245
+ throw new Error("cacheKeyAttributes are only used for remoteEval");
2246
+ }
2247
+ }
2248
+ if (options.stickyBucketService) {
2249
+ const s = options.stickyBucketService;
2250
+ this._saveStickyBucketAssignmentDoc = doc => {
2251
+ return s.saveAssignments(doc);
2252
+ };
2253
+ }
2254
+ if (options.plugins) {
2255
+ for (const plugin of options.plugins) {
2256
+ plugin(this);
2257
+ }
2258
+ }
2259
+ if (options.features) {
2260
+ this.ready = true;
2261
+ }
2262
+ if (isBrowser && options.enableDevMode) {
2263
+ window._growthbook = this;
2264
+ document.dispatchEvent(new Event("gbloaded"));
2265
+ }
2266
+ if (options.experiments) {
2267
+ this.ready = true;
2268
+ this._updateAllAutoExperiments();
2269
+ }
2270
+
2271
+ // Hydrate sticky bucket service
2272
+ if (this._options.stickyBucketService && this._options.stickyBucketAssignmentDocs) {
2273
+ for (const key in this._options.stickyBucketAssignmentDocs) {
2274
+ const doc = this._options.stickyBucketAssignmentDocs[key];
2275
+ if (doc) {
2276
+ this._options.stickyBucketService.saveAssignments(doc).catch(() => {
2277
+ // Ignore hydration errors
2278
+ });
2279
+ }
2280
+ }
2281
+ }
2282
+
2283
+ // Legacy - passing in features/experiments into the constructor instead of using init
2284
+ if (this.ready) {
2285
+ this.refreshStickyBuckets(this.getPayload());
2286
+ }
2287
+ }
2288
+ async setPayload(payload) {
2289
+ this._payload = payload;
2290
+ const data = await decryptPayload(payload, this._options.decryptionKey);
2291
+ this._decryptedPayload = data;
2292
+ await this.refreshStickyBuckets(data);
2293
+ if (data.features) {
2294
+ this._options.features = data.features;
2295
+ }
2296
+ if (data.savedGroups) {
2297
+ this._options.savedGroups = data.savedGroups;
2298
+ }
2299
+ if (data.experiments) {
2300
+ this._options.experiments = data.experiments;
2301
+ this._updateAllAutoExperiments();
2302
+ }
2303
+ this.ready = true;
2304
+ this._render();
2305
+ }
2306
+ initSync(options) {
2307
+ this._initialized = true;
2308
+ const payload = options.payload;
2309
+ if (payload.encryptedExperiments || payload.encryptedFeatures) {
2310
+ throw new Error("initSync does not support encrypted payloads");
2311
+ }
2312
+ if (this._options.stickyBucketService && !this._options.stickyBucketAssignmentDocs) {
2313
+ this._options.stickyBucketAssignmentDocs = this.generateStickyBucketAssignmentDocsSync(this._options.stickyBucketService, payload);
2314
+ }
2315
+ this._payload = payload;
2316
+ this._decryptedPayload = payload;
2317
+ if (payload.features) {
2318
+ this._options.features = payload.features;
2319
+ }
2320
+ if (payload.experiments) {
2321
+ this._options.experiments = payload.experiments;
2322
+ this._updateAllAutoExperiments();
2323
+ }
2324
+ this.ready = true;
2325
+ startStreaming(this, options);
2326
+ return this;
2327
+ }
2328
+ async init(options) {
2329
+ this._initialized = true;
2330
+ options = options || {};
2331
+ if (options.cacheSettings) {
2332
+ configureCache(options.cacheSettings);
2333
+ }
2334
+ if (options.payload) {
2335
+ await this.setPayload(options.payload);
2336
+ startStreaming(this, options);
2337
+ return {
2338
+ success: true,
2339
+ source: "init"
2340
+ };
2341
+ } else {
2342
+ const {
2343
+ data,
2344
+ ...res
2345
+ } = await this._refresh({
2346
+ ...options,
2347
+ allowStale: true
2348
+ });
2349
+ startStreaming(this, options);
2350
+ await this.setPayload(data || {});
2351
+ return res;
2352
+ }
2353
+ }
2354
+
2355
+ /** @deprecated Use {@link init} */
2356
+ async loadFeatures(options) {
2357
+ options = options || {};
2358
+ await this.init({
2359
+ skipCache: options.skipCache,
2360
+ timeout: options.timeout,
2361
+ streaming: (this._options.backgroundSync ?? true) && (options.autoRefresh || this._options.subscribeToChanges)
2362
+ });
2363
+ }
2364
+ async refreshFeatures(options) {
2365
+ const res = await this._refresh({
2366
+ ...(options || {}),
2367
+ allowStale: false
2368
+ });
2369
+ if (res.data) {
2370
+ await this.setPayload(res.data);
2371
+ }
2372
+ }
2373
+ getApiInfo() {
2374
+ return [this.getApiHosts().apiHost, this.getClientKey()];
2375
+ }
2376
+ getApiHosts() {
2377
+ return getApiHosts(this._options);
2378
+ }
2379
+ getClientKey() {
2380
+ return this._options.clientKey || "";
2381
+ }
2382
+ getPayload() {
2383
+ return this._payload || {
2384
+ features: this.getFeatures(),
2385
+ experiments: this.getExperiments()
2386
+ };
2387
+ }
2388
+ getDecryptedPayload() {
2389
+ return this._decryptedPayload || this.getPayload();
2390
+ }
2391
+ isRemoteEval() {
2392
+ return this._options.remoteEval || false;
2393
+ }
2394
+ getCacheKeyAttributes() {
2395
+ return this._options.cacheKeyAttributes;
2396
+ }
2397
+ async _refresh(_ref) {
2398
+ let {
2399
+ timeout,
2400
+ skipCache,
2401
+ allowStale,
2402
+ streaming
2403
+ } = _ref;
2404
+ if (!this._options.clientKey) {
2405
+ throw new Error("Missing clientKey");
2406
+ }
2407
+ // Trigger refresh in feature repository
2408
+ return refreshFeatures({
2409
+ instance: this,
2410
+ timeout,
2411
+ skipCache: skipCache || this._options.disableCache,
2412
+ allowStale,
2413
+ backgroundSync: streaming ?? this._options.backgroundSync ?? true
2414
+ });
2415
+ }
2416
+ _render() {
2417
+ if (this._renderer) {
2418
+ try {
2419
+ this._renderer();
2420
+ } catch (e) {
2421
+ console.error("Failed to render", e);
2422
+ }
2423
+ }
2424
+ }
2425
+
2426
+ /** @deprecated Use {@link setPayload} */
2427
+ setFeatures(features) {
2428
+ this._options.features = features;
2429
+ this.ready = true;
2430
+ this._render();
2431
+ }
2432
+
2433
+ /** @deprecated Use {@link setPayload} */
2434
+ async setEncryptedFeatures(encryptedString, decryptionKey, subtle) {
2435
+ const featuresJSON = await decrypt(encryptedString, decryptionKey || this._options.decryptionKey, subtle);
2436
+ this.setFeatures(JSON.parse(featuresJSON));
2437
+ }
2438
+
2439
+ /** @deprecated Use {@link setPayload} */
2440
+ setExperiments(experiments) {
2441
+ this._options.experiments = experiments;
2442
+ this.ready = true;
2443
+ this._updateAllAutoExperiments();
2444
+ }
2445
+
2446
+ /** @deprecated Use {@link setPayload} */
2447
+ async setEncryptedExperiments(encryptedString, decryptionKey, subtle) {
2448
+ const experimentsJSON = await decrypt(encryptedString, decryptionKey || this._options.decryptionKey, subtle);
2449
+ this.setExperiments(JSON.parse(experimentsJSON));
2450
+ }
2451
+ async setAttributes(attributes) {
2452
+ this._options.attributes = attributes;
2453
+ if (this._options.stickyBucketService) {
2454
+ await this.refreshStickyBuckets();
2455
+ }
2456
+ if (this._options.remoteEval) {
2457
+ await this._refreshForRemoteEval();
2458
+ return;
2459
+ }
2460
+ this._render();
2461
+ this._updateAllAutoExperiments();
2462
+ }
2463
+ async updateAttributes(attributes) {
2464
+ return this.setAttributes({
2465
+ ...this._options.attributes,
2466
+ ...attributes
2467
+ });
2468
+ }
2469
+ async setAttributeOverrides(overrides) {
2470
+ this._options.attributeOverrides = overrides;
2471
+ if (this._options.stickyBucketService) {
2472
+ await this.refreshStickyBuckets();
2473
+ }
2474
+ if (this._options.remoteEval) {
2475
+ await this._refreshForRemoteEval();
2476
+ return;
2477
+ }
2478
+ this._render();
2479
+ this._updateAllAutoExperiments();
2480
+ }
2481
+ async setForcedVariations(vars) {
2482
+ this._options.forcedVariations = vars || {};
2483
+ if (this._options.remoteEval) {
2484
+ await this._refreshForRemoteEval();
2485
+ return;
2486
+ }
2487
+ this._render();
2488
+ this._updateAllAutoExperiments();
2489
+ }
2490
+
2491
+ // eslint-disable-next-line
2492
+ setForcedFeatures(map) {
2493
+ this._options.forcedFeatureValues = map;
2494
+ this._render();
2495
+ }
2496
+ async setURL(url) {
2497
+ if (url === this._options.url) return;
2498
+ this._options.url = url;
2499
+ this._redirectedUrl = "";
2500
+ if (this._options.remoteEval) {
2501
+ await this._refreshForRemoteEval();
2502
+ this._updateAllAutoExperiments(true);
2503
+ return;
2504
+ }
2505
+ this._updateAllAutoExperiments(true);
2506
+ }
2507
+ getAttributes() {
2508
+ return {
2509
+ ...this._options.attributes,
2510
+ ...this._options.attributeOverrides
2511
+ };
2512
+ }
2513
+ getForcedVariations() {
2514
+ return this._options.forcedVariations || {};
2515
+ }
2516
+ getForcedFeatures() {
2517
+ // eslint-disable-next-line
2518
+ return this._options.forcedFeatureValues || new Map();
2519
+ }
2520
+ getStickyBucketAssignmentDocs() {
2521
+ return this._options.stickyBucketAssignmentDocs || {};
2522
+ }
2523
+ getUrl() {
2524
+ return this._options.url || "";
2525
+ }
2526
+ getFeatures() {
2527
+ return this._options.features || {};
2528
+ }
2529
+ getExperiments() {
2530
+ return this._options.experiments || [];
2531
+ }
2532
+ getCompletedChangeIds() {
2533
+ return Array.from(this._completedChangeIds);
2534
+ }
2535
+ subscribe(cb) {
2536
+ this._subscriptions.add(cb);
2537
+ return () => {
2538
+ this._subscriptions.delete(cb);
2539
+ };
2540
+ }
2541
+ async _refreshForRemoteEval() {
2542
+ if (!this._options.remoteEval) return;
2543
+ if (!this._initialized) return;
2544
+ const res = await this._refresh({
2545
+ allowStale: false
2546
+ });
2547
+ if (res.data) {
2548
+ await this.setPayload(res.data);
2549
+ }
2550
+ }
2551
+ getAllResults() {
2552
+ return new Map(this._assigned);
2553
+ }
2554
+ onDestroy(cb) {
2555
+ this._destroyCallbacks.push(cb);
2556
+ }
2557
+ isDestroyed() {
2558
+ return !!this._destroyed;
2559
+ }
2560
+ destroy() {
2561
+ this._destroyed = true;
2562
+
2563
+ // Custom callbacks
2564
+ // Do this first in case it needs access to the below data that is cleared
2565
+ this._destroyCallbacks.forEach(cb => {
2566
+ try {
2567
+ cb();
2568
+ } catch (e) {
2569
+ console.error(e);
2570
+ }
2571
+ });
2572
+
2573
+ // Release references to save memory
2574
+ this._subscriptions.clear();
2575
+ this._assigned.clear();
2576
+ this._trackedExperiments.clear();
2577
+ this._completedChangeIds.clear();
2578
+ this._deferredTrackingCalls.clear();
2579
+ this._trackedFeatures = {};
2580
+ this._destroyCallbacks = [];
2581
+ this._payload = undefined;
2582
+ this._saveStickyBucketAssignmentDoc = undefined;
2583
+ unsubscribe(this);
2584
+ this.logs = [];
2585
+ if (isBrowser && window._growthbook === this) {
2586
+ delete window._growthbook;
2587
+ }
2588
+
2589
+ // Undo any active auto experiments
2590
+ this._activeAutoExperiments.forEach(exp => {
2591
+ exp.undo();
2592
+ });
2593
+ this._activeAutoExperiments.clear();
2594
+ this._triggeredExpKeys.clear();
2595
+ }
2596
+ setRenderer(renderer) {
2597
+ this._renderer = renderer;
2598
+ }
2599
+ forceVariation(key, variation) {
2600
+ this._options.forcedVariations = this._options.forcedVariations || {};
2601
+ this._options.forcedVariations[key] = variation;
2602
+ if (this._options.remoteEval) {
2603
+ this._refreshForRemoteEval();
2604
+ return;
2605
+ }
2606
+ this._updateAllAutoExperiments();
2607
+ this._render();
2608
+ }
2609
+ run(experiment) {
2610
+ const {
2611
+ result
2612
+ } = runExperiment(experiment, null, this._getEvalContext());
2613
+ this._fireSubscriptions(experiment, result);
2614
+ return result;
2615
+ }
2616
+ triggerExperiment(key) {
2617
+ this._triggeredExpKeys.add(key);
2618
+ if (!this._options.experiments) return null;
2619
+ const experiments = this._options.experiments.filter(exp => exp.key === key);
2620
+ return experiments.map(exp => {
2621
+ return this._runAutoExperiment(exp);
2622
+ }).filter(res => res !== null);
2623
+ }
2624
+ triggerAutoExperiments() {
2625
+ this._autoExperimentsAllowed = true;
2626
+ this._updateAllAutoExperiments(true);
2627
+ }
2628
+ _getEvalContext() {
2629
+ return {
2630
+ user: this._getUserContext(),
2631
+ global: this._getGlobalContext(),
2632
+ stack: {
2633
+ evaluatedFeatures: new Set()
2634
+ }
2635
+ };
2636
+ }
2637
+ _getUserContext() {
2638
+ return {
2639
+ attributes: this._options.user ? {
2640
+ ...this._options.user,
2641
+ ...this._options.attributes
2642
+ } : this._options.attributes,
2643
+ enableDevMode: this._options.enableDevMode,
2644
+ blockedChangeIds: this._options.blockedChangeIds,
2645
+ stickyBucketAssignmentDocs: this._options.stickyBucketAssignmentDocs,
2646
+ url: this._getContextUrl(),
2647
+ forcedVariations: this._options.forcedVariations,
2648
+ forcedFeatureValues: this._options.forcedFeatureValues,
2649
+ attributeOverrides: this._options.attributeOverrides,
2650
+ saveStickyBucketAssignmentDoc: this._saveStickyBucketAssignmentDoc,
2651
+ trackingCallback: this._options.trackingCallback,
2652
+ onFeatureUsage: this._options.onFeatureUsage,
2653
+ devLogs: this.logs,
2654
+ trackedExperiments: this._trackedExperiments,
2655
+ trackedFeatureUsage: this._trackedFeatures
2656
+ };
2657
+ }
2658
+ _getGlobalContext() {
2659
+ return {
2660
+ features: this._options.features,
2661
+ experiments: this._options.experiments,
2662
+ log: this.log,
2663
+ enabled: this._options.enabled,
2664
+ qaMode: this._options.qaMode,
2665
+ savedGroups: this._options.savedGroups,
2666
+ groups: this._options.groups,
2667
+ overrides: this._options.overrides,
2668
+ onExperimentEval: this._subscriptions.size > 0 ? this._fireSubscriptions : undefined,
2669
+ recordChangeId: this._recordChangedId,
2670
+ saveDeferredTrack: this._saveDeferredTrack,
2671
+ eventLogger: this._options.eventLogger
2672
+ };
2673
+ }
2674
+ _runAutoExperiment(experiment, forceRerun) {
2675
+ const existing = this._activeAutoExperiments.get(experiment);
2676
+
2677
+ // If this is a manual experiment and it's not already running, skip
2678
+ if (experiment.manual && !this._triggeredExpKeys.has(experiment.key) && !existing) return null;
2679
+
2680
+ // Check if this particular experiment is blocked by options settings
2681
+ // For example, if all visualEditor experiments are disabled
2682
+ const isBlocked = this._isAutoExperimentBlockedByContext(experiment);
2683
+ let result;
2684
+ let trackingCall;
2685
+ // Run the experiment (if blocked exclude)
2686
+ if (isBlocked) {
2687
+ result = getExperimentResult(this._getEvalContext(), experiment, -1, false, "");
2688
+ } else {
2689
+ ({
2690
+ result,
2691
+ trackingCall
2692
+ } = runExperiment(experiment, null, this._getEvalContext()));
2693
+ this._fireSubscriptions(experiment, result);
2694
+ }
2695
+
2696
+ // A hash to quickly tell if the assigned value changed
2697
+ const valueHash = JSON.stringify(result.value);
2698
+
2699
+ // If the changes are already active, no need to re-apply them
2700
+ if (!forceRerun && result.inExperiment && existing && existing.valueHash === valueHash) {
2701
+ return result;
2702
+ }
2703
+
2704
+ // Undo any existing changes
2705
+ if (existing) this._undoActiveAutoExperiment(experiment);
2706
+
2707
+ // Apply new changes
2708
+ if (result.inExperiment) {
2709
+ const changeType = getAutoExperimentChangeType(experiment);
2710
+ if (changeType === "redirect" && result.value.urlRedirect && experiment.urlPatterns) {
2711
+ const url = experiment.persistQueryString ? mergeQueryStrings(this._getContextUrl(), result.value.urlRedirect) : result.value.urlRedirect;
2712
+ if (isURLTargeted(url, experiment.urlPatterns)) {
2713
+ this.log("Skipping redirect because original URL matches redirect URL", {
2714
+ id: experiment.key
2715
+ });
2716
+ return result;
2717
+ }
2718
+ this._redirectedUrl = url;
2719
+ const {
2720
+ navigate,
2721
+ delay
2722
+ } = this._getNavigateFunction();
2723
+ if (navigate) {
2724
+ if (isBrowser) {
2725
+ // Wait for the possibly-async tracking callback, bound by min and max delays
2726
+ Promise.all([...(trackingCall ? [promiseTimeout(trackingCall, this._options.maxNavigateDelay ?? 1000)] : []), new Promise(resolve => window.setTimeout(resolve, this._options.navigateDelay ?? delay))]).then(() => {
2727
+ try {
2728
+ navigate(url);
2729
+ } catch (e) {
2730
+ console.error(e);
2731
+ }
2732
+ });
2733
+ } else {
2734
+ try {
2735
+ navigate(url);
2736
+ } catch (e) {
2737
+ console.error(e);
2738
+ }
2739
+ }
2740
+ }
2741
+ } else if (changeType === "visual") {
2742
+ const undo = this._options.applyDomChangesCallback ? this._options.applyDomChangesCallback(result.value) : this._applyDOMChanges(result.value);
2743
+ if (undo) {
2744
+ this._activeAutoExperiments.set(experiment, {
2745
+ undo,
2746
+ valueHash
2747
+ });
2748
+ }
2749
+ }
2750
+ }
2751
+ return result;
2752
+ }
2753
+ _undoActiveAutoExperiment(exp) {
2754
+ const data = this._activeAutoExperiments.get(exp);
2755
+ if (data) {
2756
+ data.undo();
2757
+ this._activeAutoExperiments.delete(exp);
2758
+ }
2759
+ }
2760
+ _updateAllAutoExperiments(forceRerun) {
2761
+ if (!this._autoExperimentsAllowed) return;
2762
+ const experiments = this._options.experiments || [];
2763
+
2764
+ // Stop any experiments that are no longer defined
2765
+ const keys = new Set(experiments);
2766
+ this._activeAutoExperiments.forEach((v, k) => {
2767
+ if (!keys.has(k)) {
2768
+ v.undo();
2769
+ this._activeAutoExperiments.delete(k);
2770
+ }
2771
+ });
2772
+
2773
+ // Re-run all new/updated experiments
2774
+ for (const exp of experiments) {
2775
+ const result = this._runAutoExperiment(exp, forceRerun);
2776
+
2777
+ // Once you're in a redirect experiment, break out of the loop and don't run any further experiments
2778
+ if (result !== null && result !== void 0 && result.inExperiment && getAutoExperimentChangeType(exp) === "redirect") {
2779
+ break;
2780
+ }
2781
+ }
2782
+ }
2783
+ _fireSubscriptions(experiment, result) {
2784
+ const key = experiment.key;
2785
+
2786
+ // If assigned variation has changed, fire subscriptions
2787
+ const prev = this._assigned.get(key);
2788
+ // TODO: what if the experiment definition has changed?
2789
+ if (!prev || prev.result.inExperiment !== result.inExperiment || prev.result.variationId !== result.variationId) {
2790
+ this._assigned.set(key, {
2791
+ experiment,
2792
+ result
2793
+ });
2794
+ this._subscriptions.forEach(cb => {
2795
+ try {
2796
+ cb(experiment, result);
2797
+ } catch (e) {
2798
+ console.error(e);
2799
+ }
2800
+ });
2801
+ }
2802
+ }
2803
+ _recordChangedId(id) {
2804
+ this._completedChangeIds.add(id);
2805
+ }
2806
+ isOn(key) {
2807
+ return this.evalFeature(key).on;
2808
+ }
2809
+ isOff(key) {
2810
+ return this.evalFeature(key).off;
2811
+ }
2812
+ getFeatureValue(key, defaultValue) {
2813
+ const value = this.evalFeature(key).value;
2814
+ return value === null ? defaultValue : value;
2815
+ }
2816
+
2817
+ /**
2818
+ * @deprecated Use {@link evalFeature}
2819
+ * @param id
2820
+ */
2821
+ // eslint-disable-next-line
2822
+ feature(id) {
2823
+ return this.evalFeature(id);
2824
+ }
2825
+ evalFeature(id) {
2826
+ return evalFeature(id, this._getEvalContext());
2827
+ }
2828
+ log(msg, ctx) {
2829
+ if (!this.debug) return;
2830
+ if (this._options.log) this._options.log(msg, ctx);else console.log(msg, ctx);
2831
+ }
2832
+ getDeferredTrackingCalls() {
2833
+ return Array.from(this._deferredTrackingCalls.values());
2834
+ }
2835
+ setDeferredTrackingCalls(calls) {
2836
+ this._deferredTrackingCalls = new Map(calls.filter(c => c && c.experiment && c.result).map(c => {
2837
+ return [getExperimentDedupeKey(c.experiment, c.result), c];
2838
+ }));
2839
+ }
2840
+ async fireDeferredTrackingCalls() {
2841
+ if (!this._options.trackingCallback) return;
2842
+ const promises = [];
2843
+ this._deferredTrackingCalls.forEach(call => {
2844
+ if (!call || !call.experiment || !call.result) {
2845
+ console.error("Invalid deferred tracking call", {
2846
+ call: call
2847
+ });
2848
+ } else {
2849
+ promises.push(this._options.trackingCallback(call.experiment, call.result));
2850
+ }
2851
+ });
2852
+ this._deferredTrackingCalls.clear();
2853
+ await Promise.all(promises);
2854
+ }
2855
+ setTrackingCallback(callback) {
2856
+ this._options.trackingCallback = callback;
2857
+ this.fireDeferredTrackingCalls();
2858
+ }
2859
+ setEventLogger(logger) {
2860
+ this._options.eventLogger = logger;
2861
+ }
2862
+ async logEvent(eventName, properties) {
2863
+ if (this._destroyed) {
2864
+ console.error("Cannot log event to destroyed GrowthBook instance");
2865
+ return;
2866
+ }
2867
+ if (this._options.enableDevMode) {
2868
+ this.logs.push({
2869
+ eventName,
2870
+ properties,
2871
+ timestamp: Date.now().toString(),
2872
+ logType: "event"
2873
+ });
2874
+ }
2875
+ if (this._options.eventLogger) {
2876
+ try {
2877
+ await this._options.eventLogger(eventName, properties || {}, this._getUserContext());
2878
+ } catch (e) {
2879
+ console.error(e);
2880
+ }
2881
+ } else {
2882
+ console.error("No event logger configured");
2883
+ }
2884
+ }
2885
+ _saveDeferredTrack(data) {
2886
+ this._deferredTrackingCalls.set(getExperimentDedupeKey(data.experiment, data.result), data);
2887
+ }
2888
+ _getContextUrl() {
2889
+ return this._options.url || (isBrowser ? window.location.href : "");
2890
+ }
2891
+ _isAutoExperimentBlockedByContext(experiment) {
2892
+ const changeType = getAutoExperimentChangeType(experiment);
2893
+ if (changeType === "visual") {
2894
+ if (this._options.disableVisualExperiments) return true;
2895
+ if (this._options.disableJsInjection) {
2896
+ if (experiment.variations.some(v => v.js)) {
2897
+ return true;
2898
+ }
2899
+ }
2900
+ } else if (changeType === "redirect") {
2901
+ if (this._options.disableUrlRedirectExperiments) return true;
2902
+
2903
+ // Validate URLs
2904
+ try {
2905
+ const current = new URL(this._getContextUrl());
2906
+ for (const v of experiment.variations) {
2907
+ if (!v || !v.urlRedirect) continue;
2908
+ const url = new URL(v.urlRedirect);
2909
+
2910
+ // If we're blocking cross origin redirects, block if the protocol or host is different
2911
+ if (this._options.disableCrossOriginUrlRedirectExperiments) {
2912
+ if (url.protocol !== current.protocol) return true;
2913
+ if (url.host !== current.host) return true;
2914
+ }
2915
+ }
2916
+ } catch (e) {
2917
+ // Problem parsing one of the URLs
2918
+ this.log("Error parsing current or redirect URL", {
2919
+ id: experiment.key,
2920
+ error: e
2921
+ });
2922
+ return true;
2923
+ }
2924
+ } else {
2925
+ // Block any unknown changeTypes
2926
+ return true;
2927
+ }
2928
+ if (experiment.changeId && (this._options.blockedChangeIds || []).includes(experiment.changeId)) {
2929
+ return true;
2930
+ }
2931
+ return false;
2932
+ }
2933
+ getRedirectUrl() {
2934
+ return this._redirectedUrl;
2935
+ }
2936
+ _getNavigateFunction() {
2937
+ if (this._options.navigate) {
2938
+ return {
2939
+ navigate: this._options.navigate,
2940
+ delay: 0
2941
+ };
2942
+ } else if (isBrowser) {
2943
+ return {
2944
+ navigate: url => {
2945
+ window.location.replace(url);
2946
+ },
2947
+ delay: 100
2948
+ };
2949
+ }
2950
+ return {
2951
+ navigate: null,
2952
+ delay: 0
2953
+ };
2954
+ }
2955
+ _applyDOMChanges(changes) {
2956
+ if (!isBrowser) return;
2957
+ const undo = [];
2958
+ if (changes.css) {
2959
+ const s = document.createElement("style");
2960
+ s.innerHTML = changes.css;
2961
+ document.head.appendChild(s);
2962
+ undo.push(() => s.remove());
2963
+ }
2964
+ if (changes.js) {
2965
+ const script = document.createElement("script");
2966
+ script.innerHTML = changes.js;
2967
+ if (this._options.jsInjectionNonce) {
2968
+ script.nonce = this._options.jsInjectionNonce;
2969
+ }
2970
+ document.head.appendChild(script);
2971
+ undo.push(() => script.remove());
2972
+ }
2973
+ if (changes.domMutations) {
2974
+ changes.domMutations.forEach(mutation => {
2975
+ undo.push(index.declarative(mutation).revert);
2976
+ });
2977
+ }
2978
+ return () => {
2979
+ undo.forEach(fn => fn());
2980
+ };
2981
+ }
2982
+ async refreshStickyBuckets(data) {
2983
+ if (this._options.stickyBucketService) {
2984
+ const ctx = this._getEvalContext();
2985
+ const docs = await getAllStickyBucketAssignmentDocs(ctx, this._options.stickyBucketService, data);
2986
+ this._options.stickyBucketAssignmentDocs = docs;
2987
+ }
2988
+ }
2989
+ generateStickyBucketAssignmentDocsSync(stickyBucketService, payload) {
2990
+ if (!("getAllAssignmentsSync" in stickyBucketService)) {
2991
+ console.error("generating StickyBucketAssignmentDocs docs requires StickyBucketServiceSync");
2992
+ return;
2993
+ }
2994
+ const ctx = this._getEvalContext();
2995
+ const attributes = getStickyBucketAttributes(ctx, payload);
2996
+ return stickyBucketService.getAllAssignmentsSync(attributes);
2997
+ }
2998
+ inDevMode() {
2999
+ return !!this._options.enableDevMode;
3000
+ }
3001
+ }
3002
+ async function prefetchPayload(options) {
3003
+ // Create a temporary instance, just to fetch the payload
3004
+ const instance = new GrowthBook(options);
3005
+ await refreshFeatures({
3006
+ instance,
3007
+ skipCache: options.skipCache,
3008
+ allowStale: false,
3009
+ backgroundSync: options.streaming
3010
+ });
3011
+ instance.destroy();
3012
+ }
3013
+
3014
+ const SDK_VERSION = loadSDKVersion();
3015
+ class GrowthBookClient {
3016
+ // Properties and methods that start with "_" are mangled by Terser (saves ~150 bytes)
3017
+
3018
+ constructor(options) {
3019
+ options = options || {};
3020
+ // These properties are all initialized in the constructor instead of above
3021
+ // This saves ~80 bytes in the final output
3022
+ this.version = SDK_VERSION;
3023
+ this._options = options;
3024
+ this.debug = !!options.debug;
3025
+ this.ready = false;
3026
+ this._features = {};
3027
+ this._experiments = [];
3028
+ this.log = this.log.bind(this);
3029
+ if (options.plugins) {
3030
+ for (const plugin of options.plugins) {
3031
+ plugin(this);
3032
+ }
3033
+ }
3034
+ }
3035
+ async setPayload(payload) {
3036
+ this._payload = payload;
3037
+ const data = await decryptPayload(payload, this._options.decryptionKey);
3038
+ this._decryptedPayload = data;
3039
+ if (data.features) {
3040
+ this._features = data.features;
3041
+ }
3042
+ if (data.experiments) {
3043
+ this._experiments = data.experiments;
3044
+ }
3045
+ if (data.savedGroups) {
3046
+ this._options.savedGroups = data.savedGroups;
3047
+ }
3048
+ this.ready = true;
3049
+ }
3050
+ initSync(options) {
3051
+ const payload = options.payload;
3052
+ if (payload.encryptedExperiments || payload.encryptedFeatures) {
3053
+ throw new Error("initSync does not support encrypted payloads");
3054
+ }
3055
+ this._payload = payload;
3056
+ this._decryptedPayload = payload;
3057
+ if (payload.features) {
3058
+ this._features = payload.features;
3059
+ }
3060
+ if (payload.experiments) {
3061
+ this._experiments = payload.experiments;
3062
+ }
3063
+ this.ready = true;
3064
+ startStreaming(this, options);
3065
+ return this;
3066
+ }
3067
+ async init(options) {
3068
+ options = options || {};
3069
+ if (options.cacheSettings) {
3070
+ configureCache(options.cacheSettings);
3071
+ }
3072
+ if (options.payload) {
3073
+ await this.setPayload(options.payload);
3074
+ startStreaming(this, options);
3075
+ return {
3076
+ success: true,
3077
+ source: "init"
3078
+ };
3079
+ } else {
3080
+ const {
3081
+ data,
3082
+ ...res
3083
+ } = await this._refresh({
3084
+ ...options,
3085
+ allowStale: true
3086
+ });
3087
+ startStreaming(this, options);
3088
+ await this.setPayload(data || {});
3089
+ return res;
3090
+ }
3091
+ }
3092
+ async refreshFeatures(options) {
3093
+ const res = await this._refresh({
3094
+ ...(options || {}),
3095
+ allowStale: false
3096
+ });
3097
+ if (res.data) {
3098
+ await this.setPayload(res.data);
3099
+ }
3100
+ }
3101
+ getApiInfo() {
3102
+ return [this.getApiHosts().apiHost, this.getClientKey()];
3103
+ }
3104
+ getApiHosts() {
3105
+ return getApiHosts(this._options);
3106
+ }
3107
+ getClientKey() {
3108
+ return this._options.clientKey || "";
3109
+ }
3110
+ getPayload() {
3111
+ return this._payload || {
3112
+ features: this.getFeatures(),
3113
+ experiments: this._experiments || []
3114
+ };
3115
+ }
3116
+ getDecryptedPayload() {
3117
+ return this._decryptedPayload || this.getPayload();
3118
+ }
3119
+ async _refresh(_ref) {
3120
+ let {
3121
+ timeout,
3122
+ skipCache,
3123
+ allowStale,
3124
+ streaming
3125
+ } = _ref;
3126
+ if (!this._options.clientKey) {
3127
+ throw new Error("Missing clientKey");
3128
+ }
3129
+ // Trigger refresh in feature repository
3130
+ return refreshFeatures({
3131
+ instance: this,
3132
+ timeout,
3133
+ skipCache: skipCache || this._options.disableCache,
3134
+ allowStale,
3135
+ backgroundSync: streaming ?? true
3136
+ });
3137
+ }
3138
+ getFeatures() {
3139
+ return this._features || {};
3140
+ }
3141
+ getGlobalAttributes() {
3142
+ return this._options.globalAttributes || {};
3143
+ }
3144
+ setGlobalAttributes(attributes) {
3145
+ this._options.globalAttributes = attributes;
3146
+ }
3147
+ destroy() {
3148
+ this._destroyed = true;
3149
+ unsubscribe(this);
3150
+
3151
+ // Release references to save memory
3152
+ this._features = {};
3153
+ this._experiments = [];
3154
+ this._decryptedPayload = undefined;
3155
+ this._payload = undefined;
3156
+ this._options = {};
3157
+ }
3158
+ isDestroyed() {
3159
+ return !!this._destroyed;
3160
+ }
3161
+ setEventLogger(logger) {
3162
+ this._options.eventLogger = logger;
3163
+ }
3164
+ logEvent(eventName, properties, userContext) {
3165
+ if (this._options.eventLogger) {
3166
+ const ctx = this._getEvalContext(userContext);
3167
+ this._options.eventLogger(eventName, properties, ctx.user);
3168
+ }
3169
+ }
3170
+ runInlineExperiment(experiment, userContext) {
3171
+ const {
3172
+ result
3173
+ } = runExperiment(experiment, null, this._getEvalContext(userContext));
3174
+ return result;
3175
+ }
3176
+ _getEvalContext(userContext) {
3177
+ if (this._options.globalAttributes) {
3178
+ userContext = {
3179
+ ...userContext,
3180
+ attributes: {
3181
+ ...this._options.globalAttributes,
3182
+ ...userContext.attributes
3183
+ }
3184
+ };
3185
+ }
3186
+ return {
3187
+ user: userContext,
3188
+ global: this._getGlobalContext(),
3189
+ stack: {
3190
+ evaluatedFeatures: new Set()
3191
+ }
3192
+ };
3193
+ }
3194
+ _getGlobalContext() {
3195
+ return {
3196
+ features: this._features,
3197
+ experiments: this._experiments,
3198
+ log: this.log,
3199
+ enabled: this._options.enabled,
3200
+ qaMode: this._options.qaMode,
3201
+ savedGroups: this._options.savedGroups,
3202
+ forcedFeatureValues: this._options.forcedFeatureValues,
3203
+ forcedVariations: this._options.forcedVariations,
3204
+ trackingCallback: this._options.trackingCallback,
3205
+ onFeatureUsage: this._options.onFeatureUsage
3206
+ };
3207
+ }
3208
+ isOn(key, userContext) {
3209
+ return this.evalFeature(key, userContext).on;
3210
+ }
3211
+ isOff(key, userContext) {
3212
+ return this.evalFeature(key, userContext).off;
3213
+ }
3214
+ getFeatureValue(key, defaultValue, userContext) {
3215
+ const value = this.evalFeature(key, userContext).value;
3216
+ return value === null ? defaultValue : value;
3217
+ }
3218
+ evalFeature(id, userContext) {
3219
+ return evalFeature(id, this._getEvalContext(userContext));
3220
+ }
3221
+ log(msg, ctx) {
3222
+ if (!this.debug) return;
3223
+ if (this._options.log) this._options.log(msg, ctx);else console.log(msg, ctx);
3224
+ }
3225
+ setTrackingCallback(callback) {
3226
+ this._options.trackingCallback = callback;
3227
+ }
3228
+ async applyStickyBuckets(partialContext, stickyBucketService) {
3229
+ const ctx = this._getEvalContext(partialContext);
3230
+ const stickyBucketAssignmentDocs = await getAllStickyBucketAssignmentDocs(ctx, stickyBucketService);
3231
+ return {
3232
+ ...partialContext,
3233
+ stickyBucketAssignmentDocs,
3234
+ saveStickyBucketAssignmentDoc: doc => stickyBucketService.saveAssignments(doc)
3235
+ };
3236
+ }
3237
+ createScopedInstance(userContext, userPlugins) {
3238
+ return new UserScopedGrowthBook(this, userContext, [...(this._options.plugins || []), ...(userPlugins || [])]);
3239
+ }
3240
+ }
3241
+ class UserScopedGrowthBook {
3242
+ constructor(gb, userContext, plugins) {
3243
+ this._gb = gb;
3244
+ this._userContext = userContext;
3245
+ this.logs = [];
3246
+ this._userContext.trackedExperiments = this._userContext.trackedExperiments || new Set();
3247
+ this._userContext.trackedFeatureUsage = this._userContext.trackedFeatureUsage || {};
3248
+ this._userContext.devLogs = this.logs;
3249
+ if (plugins) {
3250
+ for (const plugin of plugins) {
3251
+ plugin(this);
3252
+ }
3253
+ }
3254
+ }
3255
+ runInlineExperiment(experiment) {
3256
+ return this._gb.runInlineExperiment(experiment, this._userContext);
3257
+ }
3258
+ isOn(key) {
3259
+ return this._gb.isOn(key, this._userContext);
3260
+ }
3261
+ isOff(key) {
3262
+ return this._gb.isOff(key, this._userContext);
3263
+ }
3264
+ getFeatureValue(key, defaultValue) {
3265
+ return this._gb.getFeatureValue(key, defaultValue, this._userContext);
3266
+ }
3267
+ evalFeature(id) {
3268
+ return this._gb.evalFeature(id, this._userContext);
3269
+ }
3270
+ logEvent(eventName, properties) {
3271
+ if (this._userContext.enableDevMode) {
3272
+ this.logs.push({
3273
+ eventName,
3274
+ properties,
3275
+ timestamp: Date.now().toString(),
3276
+ logType: "event"
3277
+ });
3278
+ }
3279
+ this._gb.logEvent(eventName, properties || {}, this._userContext);
3280
+ }
3281
+ setTrackingCallback(cb) {
3282
+ this._userContext.trackingCallback = cb;
3283
+ }
3284
+ getApiInfo() {
3285
+ return this._gb.getApiInfo();
3286
+ }
3287
+ getClientKey() {
3288
+ return this._gb.getClientKey();
3289
+ }
3290
+ setURL(url) {
3291
+ this._userContext.url = url;
3292
+ }
3293
+ updateAttributes(attributes) {
3294
+ this._userContext.attributes = {
3295
+ ...this._userContext.attributes,
3296
+ ...attributes
3297
+ };
3298
+ }
3299
+ setAttributeOverrides(overrides) {
3300
+ this._userContext.attributeOverrides = overrides;
3301
+ }
3302
+ async setForcedVariations(vars) {
3303
+ this._userContext.forcedVariations = vars || {};
3304
+ }
3305
+ // eslint-disable-next-line
3306
+ setForcedFeatures(map) {
3307
+ this._userContext.forcedFeatureValues = map;
3308
+ }
3309
+ getUserContext() {
3310
+ return this._userContext;
3311
+ }
3312
+ getVersion() {
3313
+ return SDK_VERSION;
3314
+ }
3315
+ getDecryptedPayload() {
3316
+ return this._gb.getDecryptedPayload();
3317
+ }
3318
+ inDevMode() {
3319
+ return !!this._userContext.enableDevMode;
3320
+ }
3321
+ }
3322
+
3323
+ /**
3324
+ * Responsible for reading and writing documents which describe sticky bucket assignments.
3325
+ */
3326
+ class StickyBucketService {
3327
+ constructor(opts) {
3328
+ opts = opts || {};
3329
+ this.prefix = opts.prefix || "";
3330
+ }
3331
+ /**
3332
+ * The SDK calls getAllAssignments to populate sticky buckets. This in turn will
3333
+ * typically loop through individual getAssignments calls. However, some StickyBucketService
3334
+ * instances (i.e. Redis) will instead perform a multi-query inside getAllAssignments instead.
3335
+ */
3336
+ async getAllAssignments(attributes) {
3337
+ const docs = {};
3338
+ (await Promise.all(Object.entries(attributes).map(_ref => {
3339
+ let [attributeName, attributeValue] = _ref;
3340
+ return this.getAssignments(attributeName, attributeValue);
3341
+ }))).forEach(doc => {
3342
+ if (doc) {
3343
+ const key = getStickyBucketAttributeKey(doc.attributeName, doc.attributeValue);
3344
+ docs[key] = doc;
3345
+ }
3346
+ });
3347
+ return docs;
3348
+ }
3349
+ getKey(attributeName, attributeValue) {
3350
+ return `${this.prefix}${attributeName}||${attributeValue}`;
3351
+ }
3352
+ }
3353
+ class StickyBucketServiceSync extends StickyBucketService {
3354
+ async getAssignments(attributeName, attributeValue) {
3355
+ return this.getAssignmentsSync(attributeName, attributeValue);
3356
+ }
3357
+ async saveAssignments(doc) {
3358
+ this.saveAssignmentsSync(doc);
3359
+ }
3360
+ getAllAssignmentsSync(attributes) {
3361
+ const docs = {};
3362
+ Object.entries(attributes).map(_ref2 => {
3363
+ let [attributeName, attributeValue] = _ref2;
3364
+ return this.getAssignmentsSync(attributeName, attributeValue);
3365
+ }).forEach(doc => {
3366
+ if (doc) {
3367
+ const key = getStickyBucketAttributeKey(doc.attributeName, doc.attributeValue);
3368
+ docs[key] = doc;
3369
+ }
3370
+ });
3371
+ return docs;
3372
+ }
3373
+ }
3374
+ class LocalStorageStickyBucketService extends StickyBucketService {
3375
+ constructor(opts) {
3376
+ opts = opts || {};
3377
+ super();
3378
+ this.prefix = opts.prefix || "gbStickyBuckets__";
3379
+ try {
3380
+ this.localStorage = opts.localStorage || globalThis.localStorage;
3381
+ } catch (e) {
3382
+ // Ignore localStorage errors
3383
+ }
3384
+ }
3385
+ async getAssignments(attributeName, attributeValue) {
3386
+ const key = this.getKey(attributeName, attributeValue);
3387
+ let doc = null;
3388
+ if (!this.localStorage) return doc;
3389
+ try {
3390
+ const raw = (await this.localStorage.getItem(key)) || "{}";
3391
+ const data = JSON.parse(raw);
3392
+ if (data.attributeName && data.attributeValue && data.assignments) {
3393
+ doc = data;
3394
+ }
3395
+ } catch (e) {
3396
+ // Ignore localStorage errors
3397
+ }
3398
+ return doc;
3399
+ }
3400
+ async saveAssignments(doc) {
3401
+ const key = this.getKey(doc.attributeName, doc.attributeValue);
3402
+ if (!this.localStorage) return;
3403
+ try {
3404
+ await this.localStorage.setItem(key, JSON.stringify(doc));
3405
+ } catch (e) {
3406
+ // Ignore localStorage errors
3407
+ }
3408
+ }
3409
+ }
3410
+ class ExpressCookieStickyBucketService extends StickyBucketServiceSync {
3411
+ /**
3412
+ * Intended to be used with cookieParser() middleware from npm: 'cookie-parser'.
3413
+ * Assumes:
3414
+ * - reading a cookie is automatically decoded via decodeURIComponent() or similar
3415
+ * - writing a cookie name & value must be manually encoded via encodeURIComponent() or similar
3416
+ * - all cookie bodies are JSON encoded strings and are manually encoded/decoded
3417
+ */
3418
+
3419
+ constructor(_ref3) {
3420
+ let {
3421
+ prefix = "gbStickyBuckets__",
3422
+ req,
3423
+ res,
3424
+ cookieAttributes = {
3425
+ maxAge: 180 * 24 * 3600 * 1000
3426
+ } // 180 days
3427
+ } = _ref3;
3428
+ super();
3429
+ this.prefix = prefix;
3430
+ this.req = req;
3431
+ this.res = res;
3432
+ this.cookieAttributes = cookieAttributes;
3433
+ }
3434
+ getAssignmentsSync(attributeName, attributeValue) {
3435
+ const key = this.getKey(attributeName, attributeValue);
3436
+ let doc = null;
3437
+ if (!this.req) return doc;
3438
+ try {
3439
+ const raw = this.req.cookies[key] || "{}";
3440
+ const data = JSON.parse(raw);
3441
+ if (data.attributeName && data.attributeValue && data.assignments) {
3442
+ doc = data;
3443
+ }
3444
+ } catch (e) {
3445
+ // Ignore cookie errors
3446
+ }
3447
+ return doc;
3448
+ }
3449
+ saveAssignmentsSync(doc) {
3450
+ const key = this.getKey(doc.attributeName, doc.attributeValue);
3451
+ if (!this.res) return;
3452
+ const str = JSON.stringify(doc);
3453
+ this.res.cookie(encodeURIComponent(key), encodeURIComponent(str), this.cookieAttributes);
3454
+ }
3455
+ }
3456
+ class BrowserCookieStickyBucketService extends StickyBucketServiceSync {
3457
+ /**
3458
+ * Intended to be used with npm: 'js-cookie'.
3459
+ * Assumes:
3460
+ * - reading a cookie is automatically decoded via decodeURIComponent() or similar
3461
+ * - writing a cookie name & value is automatically encoded via encodeURIComponent() or similar
3462
+ * - all cookie bodies are JSON encoded strings and are manually encoded/decoded
3463
+ */
3464
+
3465
+ constructor(_ref4) {
3466
+ let {
3467
+ prefix = "gbStickyBuckets__",
3468
+ jsCookie,
3469
+ cookieAttributes = {
3470
+ expires: 180
3471
+ } // 180 days
3472
+ } = _ref4;
3473
+ super();
3474
+ this.prefix = prefix;
3475
+ this.jsCookie = jsCookie;
3476
+ this.cookieAttributes = cookieAttributes;
3477
+ }
3478
+ getAssignmentsSync(attributeName, attributeValue) {
3479
+ const key = this.getKey(attributeName, attributeValue);
3480
+ let doc = null;
3481
+ if (!this.jsCookie) return doc;
3482
+ try {
3483
+ const raw = this.jsCookie.get(key);
3484
+ const data = JSON.parse(raw || "{}");
3485
+ if (data.attributeName && data.attributeValue && data.assignments) {
3486
+ doc = data;
3487
+ }
3488
+ } catch (e) {
3489
+ // Ignore cookie errors
3490
+ }
3491
+ return doc;
3492
+ }
3493
+ async saveAssignmentsSync(doc) {
3494
+ const key = this.getKey(doc.attributeName, doc.attributeValue);
3495
+ if (!this.jsCookie) return;
3496
+ const str = JSON.stringify(doc);
3497
+ this.jsCookie.set(key, str, this.cookieAttributes);
3498
+ }
3499
+ }
3500
+ class RedisStickyBucketService extends StickyBucketService {
3501
+ /** Intended to be used with npm: 'ioredis'. **/
3502
+
3503
+ constructor(_ref5) {
3504
+ let {
3505
+ redis
3506
+ } = _ref5;
3507
+ super();
3508
+ this.redis = redis;
3509
+ }
3510
+ async getAllAssignments(attributes) {
3511
+ const docs = {};
3512
+ const keys = Object.entries(attributes).map(_ref6 => {
3513
+ let [attributeName, attributeValue] = _ref6;
3514
+ return getStickyBucketAttributeKey(attributeName, attributeValue);
3515
+ });
3516
+ if (!this.redis) return docs;
3517
+ await this.redis.mget(...keys).then(values => {
3518
+ values.forEach(raw => {
3519
+ try {
3520
+ const data = JSON.parse(raw || "{}");
3521
+ if (data.attributeName && "attributeValue" in data && data.assignments) {
3522
+ const key = getStickyBucketAttributeKey(data.attributeName, toString(data.attributeValue));
3523
+ docs[key] = data;
3524
+ }
3525
+ } catch (e) {
3526
+ // ignore redis doc parse errors
3527
+ }
3528
+ });
3529
+ });
3530
+ return docs;
3531
+ }
3532
+ async getAssignments(_attributeName, _attributeValue) {
3533
+ // not implemented
3534
+ return null;
3535
+ }
3536
+ async saveAssignments(doc) {
3537
+ const key = this.getKey(doc.attributeName, doc.attributeValue);
3538
+ if (!this.redis) return;
3539
+ await this.redis.set(key, JSON.stringify(doc));
3540
+ }
3541
+ }
3542
+
3543
+ exports.BrowserCookieStickyBucketService = BrowserCookieStickyBucketService;
3544
+ exports.EVENT_EXPERIMENT_VIEWED = EVENT_EXPERIMENT_VIEWED;
3545
+ exports.EVENT_FEATURE_EVALUATED = EVENT_FEATURE_EVALUATED;
3546
+ exports.ExpressCookieStickyBucketService = ExpressCookieStickyBucketService;
3547
+ exports.GrowthBook = GrowthBook;
3548
+ exports.GrowthBookClient = GrowthBookClient;
3549
+ exports.GrowthBookMultiUser = GrowthBookClient;
3550
+ exports.LocalStorageStickyBucketService = LocalStorageStickyBucketService;
3551
+ exports.RedisStickyBucketService = RedisStickyBucketService;
3552
+ exports.StickyBucketService = StickyBucketService;
3553
+ exports.StickyBucketServiceSync = StickyBucketServiceSync;
3554
+ exports.UserScopedGrowthBook = UserScopedGrowthBook;
3555
+ exports.clearCache = clearCache;
3556
+ exports.configureCache = configureCache;
3557
+ exports.evalCondition = evalCondition;
3558
+ exports.getAutoExperimentChangeType = getAutoExperimentChangeType;
3559
+ exports.getPolyfills = getPolyfills;
3560
+ exports.helpers = helpers;
3561
+ exports.isURLTargeted = isURLTargeted;
3562
+ exports.onHidden = onHidden;
3563
+ exports.onVisible = onVisible;
3564
+ exports.paddedVersionString = paddedVersionString;
3565
+ exports.prefetchPayload = prefetchPayload;
3566
+ exports.setPolyfills = setPolyfills;
3567
+
3568
+ Object.defineProperty(exports, '__esModule', { value: true });
3569
+
3570
+ return exports;
3571
+
3572
+ })({});
3573
+ //# sourceMappingURL=index.js.map
3574
+