@fabricorg/experiments-web 0.0.2

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.
package/dist/index.js ADDED
@@ -0,0 +1,653 @@
1
+ // src/hash.ts
2
+ function murmur3(key) {
3
+ const data = new TextEncoder().encode(key);
4
+ const len = data.length;
5
+ const nblocks = len >>> 2;
6
+ let h1 = 0;
7
+ const c1 = 3432918353;
8
+ const c2 = 461845907;
9
+ for (let i = 0; i < nblocks; i++) {
10
+ const o = i * 4;
11
+ let k12 = data[o] & 255 | (data[o + 1] & 255) << 8 | (data[o + 2] & 255) << 16 | (data[o + 3] & 255) << 24;
12
+ k12 = mul(k12, c1);
13
+ k12 = rot(k12, 15);
14
+ k12 = mul(k12, c2);
15
+ h1 ^= k12;
16
+ h1 = rot(h1, 13);
17
+ h1 = mul(h1, 5) + 3864292196 >>> 0;
18
+ }
19
+ let k1 = 0;
20
+ const tail = len & 3;
21
+ const ts = nblocks * 4;
22
+ if (tail >= 3) k1 ^= (data[ts + 2] & 255) << 16;
23
+ if (tail >= 2) k1 ^= (data[ts + 1] & 255) << 8;
24
+ if (tail >= 1) {
25
+ k1 ^= data[ts] & 255;
26
+ k1 = mul(k1, c1);
27
+ k1 = rot(k1, 15);
28
+ k1 = mul(k1, c2);
29
+ h1 ^= k1;
30
+ }
31
+ h1 ^= len;
32
+ h1 ^= h1 >>> 16;
33
+ h1 = mul(h1, 2246822507);
34
+ h1 ^= h1 >>> 13;
35
+ h1 = mul(h1, 3266489909);
36
+ h1 ^= h1 >>> 16;
37
+ return h1 >>> 0;
38
+ }
39
+ function rot(x, r) {
40
+ return (x << r | x >>> 32 - r) >>> 0;
41
+ }
42
+ function mul(a, b) {
43
+ const aH = a >>> 16 & 65535;
44
+ const aL = a & 65535;
45
+ const bH = b >>> 16 & 65535;
46
+ const bL = b & 65535;
47
+ return aL * bL + (aH * bL + aL * bH << 16 >>> 0 | 0) >>> 0;
48
+ }
49
+ function bucket(experimentId, salt, subjectId) {
50
+ return murmur3(`${experimentId}:${salt}:${subjectId}`) % 1e4;
51
+ }
52
+
53
+ // src/dom-ops.ts
54
+ var DEFAULT_WAIT_MS = 2e3;
55
+ var POLL_INTERVAL_MS = 50;
56
+ function waitFor(selector, timeoutMs) {
57
+ return new Promise((resolve) => {
58
+ if (typeof document === "undefined") return resolve(null);
59
+ const initial = document.querySelector(selector);
60
+ if (initial) return resolve(initial);
61
+ if (timeoutMs <= 0) return resolve(null);
62
+ const deadline = Date.now() + timeoutMs;
63
+ const tick = () => {
64
+ const el = document.querySelector(selector);
65
+ if (el) return resolve(el);
66
+ if (Date.now() >= deadline) return resolve(null);
67
+ setTimeout(tick, POLL_INTERVAL_MS);
68
+ };
69
+ setTimeout(tick, POLL_INTERVAL_MS);
70
+ });
71
+ }
72
+ function injectStyle(experimentId, css) {
73
+ if (typeof document === "undefined") return;
74
+ const style = document.createElement("style");
75
+ style.dataset.fxExperiment = experimentId;
76
+ style.dataset.fxOp = "injectCSS";
77
+ style.textContent = css;
78
+ document.head.appendChild(style);
79
+ }
80
+ async function applyOne(experimentId, op) {
81
+ var _a;
82
+ if (typeof document === "undefined") return false;
83
+ if (op.op === "injectCSS") {
84
+ injectStyle(experimentId, op.value);
85
+ return true;
86
+ }
87
+ const wait = "waitForMs" in op && typeof op.waitForMs === "number" ? op.waitForMs : DEFAULT_WAIT_MS;
88
+ const el = await waitFor(op.selector, wait);
89
+ if (!el) return false;
90
+ switch (op.op) {
91
+ case "replaceText":
92
+ el.textContent = op.value;
93
+ return true;
94
+ case "replaceHTML":
95
+ el.innerHTML = op.value;
96
+ return true;
97
+ case "setAttr":
98
+ el.setAttribute(op.name, op.value);
99
+ return true;
100
+ case "addClass":
101
+ el.classList.add(...op.value.split(/\s+/).filter(Boolean));
102
+ return true;
103
+ case "removeClass":
104
+ el.classList.remove(...op.value.split(/\s+/).filter(Boolean));
105
+ return true;
106
+ case "setStyle":
107
+ el.style.setProperty(op.name, op.value);
108
+ return true;
109
+ case "remove":
110
+ (_a = el.parentNode) == null ? void 0 : _a.removeChild(el);
111
+ return true;
112
+ case "injectHTML":
113
+ el.insertAdjacentHTML(op.position, op.value);
114
+ return true;
115
+ }
116
+ }
117
+ async function applyDomOps(experimentId, ops) {
118
+ for (const op of ops) {
119
+ try {
120
+ await applyOne(experimentId, op);
121
+ } catch (e) {
122
+ }
123
+ }
124
+ }
125
+
126
+ // src/preview.ts
127
+ function parsePreviewParams(search) {
128
+ if (!search) return null;
129
+ const q = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
130
+ const fxp = q.get("fxpreview");
131
+ const tok = q.get("fxtoken");
132
+ if (!fxp || !tok) return null;
133
+ const [expId, variantKey] = fxp.split(":");
134
+ if (!expId || !variantKey) return null;
135
+ return { expId, variantKey, token: tok };
136
+ }
137
+ function b64urlToBytes(s) {
138
+ const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
139
+ const b64 = (s + pad).replace(/-/g, "+").replace(/_/g, "/");
140
+ const bin = typeof atob === "function" ? atob(b64) : Buffer.from(b64, "base64").toString("binary");
141
+ const out = new Uint8Array(bin.length);
142
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
143
+ return out;
144
+ }
145
+ function bytesToString(bytes) {
146
+ return typeof TextDecoder !== "undefined" ? new TextDecoder().decode(bytes) : Buffer.from(bytes).toString("utf8");
147
+ }
148
+ function constantTimeEqual(a, b) {
149
+ if (a.length !== b.length) return false;
150
+ let r = 0;
151
+ for (let i = 0; i < a.length; i++) r |= a[i] ^ b[i];
152
+ return r === 0;
153
+ }
154
+ async function verifyPreviewToken(token, secret, manifest, now = Date.now()) {
155
+ var _a, _b;
156
+ const parts = token.split(".");
157
+ if (parts.length !== 3) return null;
158
+ const [headerB64, payloadB64, sigB64] = parts;
159
+ let header;
160
+ let payload;
161
+ try {
162
+ header = JSON.parse(bytesToString(b64urlToBytes(headerB64)));
163
+ payload = JSON.parse(bytesToString(b64urlToBytes(payloadB64)));
164
+ } catch (e) {
165
+ return null;
166
+ }
167
+ if (header.alg !== "HS256") return null;
168
+ const subtle = (_b = (_a = globalThis.crypto) == null ? void 0 : _a.subtle) != null ? _b : null;
169
+ if (!subtle) return null;
170
+ const enc = new TextEncoder();
171
+ const key = await subtle.importKey(
172
+ "raw",
173
+ enc.encode(secret),
174
+ { name: "HMAC", hash: "SHA-256" },
175
+ false,
176
+ ["sign"]
177
+ );
178
+ const signed = enc.encode(`${headerB64}.${payloadB64}`);
179
+ const expectedBuf = await subtle.sign("HMAC", key, signed);
180
+ const expected = new Uint8Array(expectedBuf);
181
+ const provided = b64urlToBytes(sigB64);
182
+ if (!constantTimeEqual(expected, provided)) return null;
183
+ if (typeof payload.exp !== "number" || payload.exp * 1e3 < now || typeof payload.experimentId !== "string" || typeof payload.variantKey !== "string" || typeof payload.tenantId !== "string") {
184
+ return null;
185
+ }
186
+ if (payload.tenantId !== manifest.tenantId) return null;
187
+ return payload;
188
+ }
189
+
190
+ // src/storage.ts
191
+ var DEFAULT_PREFIX = "fx.";
192
+ function escape(s) {
193
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
194
+ }
195
+ function cookieStorage(options = {}) {
196
+ var _a, _b;
197
+ const prefix = (_a = options.cookiePrefix) != null ? _a : DEFAULT_PREFIX;
198
+ const maxAge = (_b = options.cookieMaxAgeSeconds) != null ? _b : 365 * 24 * 60 * 60;
199
+ return {
200
+ read(experimentId) {
201
+ var _a2;
202
+ const k = prefix + experimentId;
203
+ if (typeof document !== "undefined" && document.cookie) {
204
+ const m = document.cookie.match(new RegExp(`(?:^|; )${escape(k)}=([^;]*)`));
205
+ if (m) return decodeURIComponent(m[1]);
206
+ }
207
+ try {
208
+ const v = (_a2 = globalThis.localStorage) == null ? void 0 : _a2.getItem(k);
209
+ if (v != null) return v;
210
+ } catch (e) {
211
+ }
212
+ return null;
213
+ },
214
+ write(experimentId, variantKey) {
215
+ var _a2;
216
+ const k = prefix + experimentId;
217
+ try {
218
+ if (typeof document !== "undefined") {
219
+ document.cookie = `${k}=${encodeURIComponent(variantKey)};max-age=${maxAge};path=/;samesite=lax`;
220
+ }
221
+ } catch (e) {
222
+ }
223
+ try {
224
+ (_a2 = globalThis.localStorage) == null ? void 0 : _a2.setItem(k, variantKey);
225
+ } catch (e) {
226
+ }
227
+ }
228
+ };
229
+ }
230
+ function localStorageOnly(options = {}) {
231
+ var _a;
232
+ const prefix = (_a = options.cookiePrefix) != null ? _a : DEFAULT_PREFIX;
233
+ return {
234
+ read(experimentId) {
235
+ var _a2, _b;
236
+ try {
237
+ return (_b = (_a2 = globalThis.localStorage) == null ? void 0 : _a2.getItem(prefix + experimentId)) != null ? _b : null;
238
+ } catch (e) {
239
+ return null;
240
+ }
241
+ },
242
+ write(experimentId, variantKey) {
243
+ var _a2;
244
+ try {
245
+ (_a2 = globalThis.localStorage) == null ? void 0 : _a2.setItem(prefix + experimentId, variantKey);
246
+ } catch (e) {
247
+ }
248
+ }
249
+ };
250
+ }
251
+ function sessionStorageOnly(options = {}) {
252
+ var _a;
253
+ const prefix = (_a = options.cookiePrefix) != null ? _a : DEFAULT_PREFIX;
254
+ return {
255
+ read(experimentId) {
256
+ var _a2, _b;
257
+ try {
258
+ return (_b = (_a2 = globalThis.sessionStorage) == null ? void 0 : _a2.getItem(prefix + experimentId)) != null ? _b : null;
259
+ } catch (e) {
260
+ return null;
261
+ }
262
+ },
263
+ write(experimentId, variantKey) {
264
+ var _a2;
265
+ try {
266
+ (_a2 = globalThis.sessionStorage) == null ? void 0 : _a2.setItem(prefix + experimentId, variantKey);
267
+ } catch (e) {
268
+ }
269
+ }
270
+ };
271
+ }
272
+ function inMemoryStorage() {
273
+ const map = /* @__PURE__ */ new Map();
274
+ return {
275
+ read: (experimentId) => {
276
+ var _a;
277
+ return (_a = map.get(experimentId)) != null ? _a : null;
278
+ },
279
+ write: (experimentId, variantKey) => {
280
+ map.set(experimentId, variantKey);
281
+ }
282
+ };
283
+ }
284
+ function defaultStorage(options = {}) {
285
+ return cookieStorage(options);
286
+ }
287
+ var ANON_DEFAULT_COOKIE = "_fx_sid";
288
+ var ANON_DEFAULT_TTL_DAYS = 365;
289
+ function readCookie(name) {
290
+ if (typeof document === "undefined" || !document.cookie) return null;
291
+ const m = document.cookie.match(new RegExp(`(?:^|; )${escape(name)}=([^;]*)`));
292
+ return m ? decodeURIComponent(m[1]) : null;
293
+ }
294
+ function randomId() {
295
+ const c = globalThis.crypto;
296
+ if (c == null ? void 0 : c.randomUUID) return c.randomUUID();
297
+ if (c == null ? void 0 : c.getRandomValues) {
298
+ const a = new Uint8Array(16);
299
+ c.getRandomValues(a);
300
+ return Array.from(a, (b) => b.toString(16).padStart(2, "0")).join("");
301
+ }
302
+ return Date.now().toString(36) + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
303
+ }
304
+ function getOrCreateAnonymousId(options = {}) {
305
+ var _a, _b, _c;
306
+ const cookieName = (_a = options.cookieName) != null ? _a : ANON_DEFAULT_COOKIE;
307
+ const ttlDays = (_b = options.ttlDays) != null ? _b : ANON_DEFAULT_TTL_DAYS;
308
+ const existing = readCookie(cookieName);
309
+ if (existing) return existing;
310
+ const fromStorage = (() => {
311
+ var _a2, _b2;
312
+ try {
313
+ return (_b2 = (_a2 = globalThis.localStorage) == null ? void 0 : _a2.getItem(cookieName)) != null ? _b2 : null;
314
+ } catch (e) {
315
+ return null;
316
+ }
317
+ })();
318
+ const id = fromStorage != null ? fromStorage : randomId();
319
+ try {
320
+ if (typeof document !== "undefined") {
321
+ const maxAge = ttlDays * 24 * 60 * 60;
322
+ document.cookie = `${cookieName}=${encodeURIComponent(id)};max-age=${maxAge};path=/;samesite=lax`;
323
+ }
324
+ } catch (e) {
325
+ }
326
+ try {
327
+ (_c = globalThis.localStorage) == null ? void 0 : _c.setItem(cookieName, id);
328
+ } catch (e) {
329
+ }
330
+ return id;
331
+ }
332
+
333
+ // src/trigger.ts
334
+ function awaitTrigger(trigger) {
335
+ var _a;
336
+ if (!trigger || trigger.kind === "auto") return Promise.resolve(true);
337
+ if (trigger.kind === "urlMatch") {
338
+ if (typeof location === "undefined") return Promise.resolve(false);
339
+ const href = location.href;
340
+ if (trigger.regex) {
341
+ try {
342
+ return Promise.resolve(new RegExp(trigger.pattern).test(href));
343
+ } catch (e) {
344
+ return Promise.resolve(false);
345
+ }
346
+ }
347
+ return Promise.resolve(href.includes(trigger.pattern));
348
+ }
349
+ if (trigger.kind === "waitForSelector") {
350
+ if (typeof document === "undefined") return Promise.resolve(false);
351
+ const timeoutMs = (_a = trigger.timeoutMs) != null ? _a : 2e3;
352
+ return new Promise((resolve) => {
353
+ const found = document.querySelector(trigger.selector);
354
+ if (found) return resolve(true);
355
+ if (timeoutMs <= 0) return resolve(false);
356
+ const deadline = Date.now() + timeoutMs;
357
+ const tick = () => {
358
+ if (document.querySelector(trigger.selector)) return resolve(true);
359
+ if (Date.now() >= deadline) return resolve(false);
360
+ setTimeout(tick, 50);
361
+ };
362
+ setTimeout(tick, 50);
363
+ });
364
+ }
365
+ if (typeof addEventListener === "undefined") return Promise.resolve(false);
366
+ return new Promise((resolve) => {
367
+ const handler = () => {
368
+ removeEventListener(trigger.name, handler);
369
+ resolve(true);
370
+ };
371
+ addEventListener(trigger.name, handler);
372
+ });
373
+ }
374
+
375
+ // src/index.ts
376
+ async function init(options) {
377
+ var _a;
378
+ const f = (_a = options.fetchImpl) != null ? _a : globalThis.fetch;
379
+ if (!f) throw new Error("init: no fetch available");
380
+ const res = await f(options.manifestUrl);
381
+ if (!res.ok) throw new Error(`init: manifest fetch ${res.status}`);
382
+ const manifest = await res.json();
383
+ return createClient(manifest, options);
384
+ }
385
+ function createClient(manifest, options) {
386
+ var _a, _b, _c;
387
+ const exposed = /* @__PURE__ */ new Set();
388
+ const pending = [];
389
+ const pendingExposure = /* @__PURE__ */ new Map();
390
+ const forcedPreviews = /* @__PURE__ */ new Map();
391
+ const sharedApplied = /* @__PURE__ */ new Set();
392
+ const isExcluded = (() => {
393
+ if (options.excluded === false) return false;
394
+ if (options.excluded === true) return true;
395
+ if (typeof options.excluded === "function") {
396
+ try {
397
+ return Boolean(options.excluded());
398
+ } catch (e) {
399
+ return true;
400
+ }
401
+ }
402
+ return typeof navigator !== "undefined" && navigator.cookieEnabled === false;
403
+ })();
404
+ const storage = (_a = options.storage) != null ? _a : options.cookiePrefix ? cookieStorage({ cookiePrefix: options.cookiePrefix }) : defaultStorage();
405
+ const stackLimit = (_b = options.maxErrorStackLength) != null ? _b : 1e3;
406
+ const decisionAdapter = options.decisionAdapter;
407
+ const search = (_c = options.locationSearch) != null ? _c : typeof location !== "undefined" ? location.search : void 0;
408
+ const previewParams = parsePreviewParams(search);
409
+ if (previewParams && options.previewSecret) {
410
+ void verifyPreviewToken(previewParams.token, options.previewSecret, manifest).then((claim) => {
411
+ if (claim && claim.experimentId === previewParams.expId && claim.variantKey === previewParams.variantKey) {
412
+ forcedPreviews.set(claim.experimentId, claim.variantKey);
413
+ }
414
+ });
415
+ }
416
+ if (options.beaconEndpoint && typeof addEventListener !== "undefined") {
417
+ const flush = () => {
418
+ if (pending.length === 0) return;
419
+ const body = JSON.stringify({ exposures: pending.splice(0, pending.length) });
420
+ try {
421
+ const sb = navigator.sendBeacon;
422
+ if (sb == null ? void 0 : sb.call(navigator, options.beaconEndpoint, body)) return;
423
+ } catch (e) {
424
+ }
425
+ try {
426
+ fetch(options.beaconEndpoint, {
427
+ method: "POST",
428
+ headers: { "content-type": "application/json" },
429
+ body,
430
+ keepalive: true
431
+ }).catch(() => void 0);
432
+ } catch (e) {
433
+ }
434
+ };
435
+ addEventListener("pagehide", flush);
436
+ addEventListener("visibilitychange", () => {
437
+ if (document.visibilityState === "hidden") flush();
438
+ });
439
+ }
440
+ function decide(experimentId) {
441
+ var _a2;
442
+ const exp = manifest.experiments.find((e) => e.id === experimentId);
443
+ if (!exp) return { exp: null, variant: null, variantKey: null, preview: false };
444
+ const forced = forcedPreviews.get(experimentId);
445
+ if (forced) {
446
+ const v = exp.variants.find((vv) => vv.key === forced);
447
+ if (v) return { exp, variant: v, variantKey: forced, preview: true };
448
+ }
449
+ if (exp.state !== "running") {
450
+ return { exp, variant: null, variantKey: null, preview: false };
451
+ }
452
+ if (exp.divertTo) {
453
+ const v = exp.variants.find((vv) => vv.key === exp.divertTo);
454
+ if (v) return { exp, variant: v, variantKey: v.key, preview: false };
455
+ }
456
+ const sticky = storage.read(experimentId);
457
+ if (sticky && exp.variants.some((v) => v.key === sticky)) {
458
+ return {
459
+ exp,
460
+ variant: exp.variants.find((v) => v.key === sticky),
461
+ variantKey: sticky,
462
+ preview: false
463
+ };
464
+ }
465
+ if (exp.sampleRate < 1) {
466
+ const sb = bucket(`${exp.id}:sample`, exp.salt, options.subjectId) / 1e4;
467
+ if (sb >= exp.sampleRate)
468
+ return { exp, variant: null, variantKey: null, preview: false };
469
+ }
470
+ let chosenKey = null;
471
+ if (decisionAdapter) {
472
+ try {
473
+ const forced2 = decisionAdapter({
474
+ experimentId: exp.id,
475
+ salt: exp.salt,
476
+ subjectId: options.subjectId,
477
+ variants: exp.variants.map((v) => ({ key: v.key, weight: v.weight }))
478
+ });
479
+ if (forced2 && exp.variants.some((v) => v.key === forced2)) {
480
+ chosenKey = forced2;
481
+ }
482
+ } catch (e) {
483
+ }
484
+ }
485
+ const b = bucket(exp.id, exp.salt, options.subjectId);
486
+ let key;
487
+ if (chosenKey !== null) {
488
+ key = chosenKey;
489
+ } else {
490
+ const total = exp.variants.reduce((s, v) => {
491
+ var _a3;
492
+ return s + ((_a3 = v.weight) != null ? _a3 : 0);
493
+ }, 0);
494
+ if (total > 0) {
495
+ let cum = 0;
496
+ key = exp.variants[exp.variants.length - 1].key;
497
+ for (const v of exp.variants) {
498
+ cum += (_a2 = v.weight) != null ? _a2 : 0;
499
+ if (b < cum) {
500
+ key = v.key;
501
+ break;
502
+ }
503
+ }
504
+ } else {
505
+ const idx = Math.min(Math.floor(b / 1e4 * exp.variants.length), exp.variants.length - 1);
506
+ key = exp.variants[idx].key;
507
+ }
508
+ }
509
+ const chosen = exp.variants.find((v) => v.key === key);
510
+ if (typeof chosen.recipeSampleRate === "number" && chosen.recipeSampleRate < 1) {
511
+ const rs = bucket(`${exp.id}:${key}:rs`, exp.salt, options.subjectId) / 1e4;
512
+ if (rs >= chosen.recipeSampleRate) {
513
+ return { exp, variant: null, variantKey: null, preview: false };
514
+ }
515
+ }
516
+ return { exp, variant: chosen, variantKey: key, preview: false };
517
+ }
518
+ function truncStack(stack) {
519
+ if (!stack || stackLimit <= 0) return stack;
520
+ return stack.length > stackLimit ? stack.slice(0, stackLimit) : stack;
521
+ }
522
+ function expose(experimentId, variantKey, failure, preview) {
523
+ var _a2, _b2;
524
+ const sig = `${experimentId}:${variantKey}:${failure ? "F" : "O"}`;
525
+ if (exposed.has(sig)) return;
526
+ exposed.add(sig);
527
+ if (preview) return;
528
+ const record = {
529
+ experimentId,
530
+ subjectId: options.subjectId,
531
+ variantKey,
532
+ manifestVersion: manifest.manifestVersion,
533
+ at: (/* @__PURE__ */ new Date()).toISOString(),
534
+ failure: failure ? { message: failure.message, stack: truncStack(failure.stack) } : null
535
+ };
536
+ (_a2 = options.onExposure) == null ? void 0 : _a2.call(options, record);
537
+ if (failure) (_b2 = options.onRecipeFailure) == null ? void 0 : _b2.call(options, record);
538
+ if (options.beaconEndpoint) pending.push(record);
539
+ }
540
+ function applyShared(exp) {
541
+ if (typeof document === "undefined") return;
542
+ if (sharedApplied.has(exp.id)) return;
543
+ sharedApplied.add(exp.id);
544
+ if (exp.sharedCss) {
545
+ const style = document.createElement("style");
546
+ style.dataset.fxExperiment = exp.id;
547
+ style.dataset.fxScope = "shared";
548
+ style.textContent = exp.sharedCss;
549
+ document.head.appendChild(style);
550
+ }
551
+ if (exp.sharedJs) {
552
+ try {
553
+ new Function("experiment", exp.sharedJs)(exp);
554
+ } catch (err) {
555
+ const e = err;
556
+ expose(exp.id, "shared", { message: e.message, stack: e.stack }, false);
557
+ }
558
+ }
559
+ }
560
+ function applyVariant(exp, variant) {
561
+ if (typeof document === "undefined") return true;
562
+ applyShared(exp);
563
+ if (variant.css) {
564
+ const style = document.createElement("style");
565
+ style.dataset.fxExperiment = exp.id;
566
+ style.textContent = variant.css;
567
+ document.head.appendChild(style);
568
+ }
569
+ if (variant.domOps && variant.domOps.length > 0) {
570
+ void applyDomOps(exp.id, variant.domOps);
571
+ }
572
+ if (variant.js) {
573
+ try {
574
+ new Function("experiment", "variant", variant.js)(exp, variant);
575
+ } catch (err) {
576
+ const e = err;
577
+ expose(exp.id, variant.key, { message: e.message, stack: e.stack }, false);
578
+ return false;
579
+ }
580
+ }
581
+ return true;
582
+ }
583
+ function activate(experimentId) {
584
+ var _a2;
585
+ if (isExcluded) return null;
586
+ const { exp, variant, variantKey, preview } = decide(experimentId);
587
+ if (!exp || !variant || !variantKey) return null;
588
+ if (!preview) storage.write(experimentId, variantKey);
589
+ const hostTrigger = (_a2 = options.triggers) == null ? void 0 : _a2[experimentId];
590
+ const manifestTrigger = exp.trigger;
591
+ const needsAsyncTrigger = !!hostTrigger || manifestTrigger !== void 0 && manifestTrigger.kind !== "auto";
592
+ const finish = () => {
593
+ const ok = exp.applyVariants ? applyVariant(exp, variant) : true;
594
+ if (!ok) return;
595
+ if (exp.manualExposure) {
596
+ pendingExposure.set(experimentId, { variant, preview });
597
+ } else {
598
+ expose(experimentId, variantKey, null, preview);
599
+ }
600
+ };
601
+ if (!needsAsyncTrigger) {
602
+ finish();
603
+ } else {
604
+ void (async () => {
605
+ if (hostTrigger) {
606
+ try {
607
+ const r = await hostTrigger();
608
+ if (r === false) return;
609
+ } catch (e) {
610
+ return;
611
+ }
612
+ }
613
+ if (manifestTrigger) {
614
+ const ok = await awaitTrigger(manifestTrigger);
615
+ if (!ok) return;
616
+ }
617
+ finish();
618
+ })();
619
+ }
620
+ return { variant, key: variantKey, preview };
621
+ }
622
+ return {
623
+ treatment(experimentId) {
624
+ const a = activate(experimentId);
625
+ return a ? a.key : null;
626
+ },
627
+ payload(experimentId) {
628
+ if (isExcluded) return void 0;
629
+ const { variant } = decide(experimentId);
630
+ return variant == null ? void 0 : variant.payload;
631
+ },
632
+ metadata(experimentId) {
633
+ const exp = manifest.experiments.find((e) => e.id === experimentId);
634
+ return exp == null ? void 0 : exp.metadata;
635
+ },
636
+ expose(experimentId) {
637
+ if (isExcluded) return;
638
+ const p = pendingExposure.get(experimentId);
639
+ if (!p) return;
640
+ pendingExposure.delete(experimentId);
641
+ expose(experimentId, p.variant.key, null, p.preview);
642
+ },
643
+ track(eventName, props) {
644
+ var _a2;
645
+ if (isExcluded) return;
646
+ (_a2 = options.onTrack) == null ? void 0 : _a2.call(options, eventName, props);
647
+ }
648
+ };
649
+ }
650
+
651
+ export { bucket as _bucket, murmur3 as _murmur3, applyDomOps, awaitTrigger, cookieStorage, createClient, defaultStorage, getOrCreateAnonymousId, inMemoryStorage, init, localStorageOnly, parsePreviewParams, sessionStorageOnly, verifyPreviewToken };
652
+ //# sourceMappingURL=index.js.map
653
+ //# sourceMappingURL=index.js.map