@shipeasy/sdk 6.3.1 → 7.0.0

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.
@@ -110,53 +110,6 @@ function send(url) {
110
110
  }
111
111
  }
112
112
 
113
- // src/logger.ts
114
- var LOG_LEVELS = ["silent", "error", "warn", "info", "debug"];
115
- var RANK = {
116
- silent: 0,
117
- error: 1,
118
- warn: 2,
119
- info: 3,
120
- debug: 4
121
- };
122
- var currentLevel = "warn";
123
- function setLogLevel(level) {
124
- if (typeof level === "string" && Object.prototype.hasOwnProperty.call(RANK, level)) {
125
- currentLevel = level;
126
- }
127
- }
128
- function write(method, args) {
129
- try {
130
- const c = globalThis.console;
131
- if (!c) return;
132
- const fn = c[method] ?? c.log;
133
- if (typeof fn === "function") fn.call(c, ...args);
134
- } catch {
135
- }
136
- }
137
- var logger = {
138
- error(...args) {
139
- if (RANK[currentLevel] >= RANK.error) write("error", args);
140
- },
141
- warn(...args) {
142
- if (RANK[currentLevel] >= RANK.warn) write("warn", args);
143
- },
144
- info(...args) {
145
- if (RANK[currentLevel] >= RANK.info) write("info", args);
146
- },
147
- debug(...args) {
148
- if (RANK[currentLevel] >= RANK.debug) write("debug", args);
149
- }
150
- };
151
- function safeRun(label, fallback, fn) {
152
- try {
153
- return fn();
154
- } catch (err) {
155
- logger.error(`[shipeasy] ${label} failed \u2014 returning safe default:`, String(err));
156
- return fallback;
157
- }
158
- }
159
-
160
113
  // src/see/core.ts
161
114
  var SEE_MAX_MESSAGE = 500;
162
115
  var SEE_MAX_STACK = 8e3;
@@ -274,7 +227,7 @@ function captureCallsiteStack() {
274
227
  const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
275
228
  return kept.length ? kept.join("\n") : void 0;
276
229
  }
277
- function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlationId) {
230
+ function buildSeeEvent(problem, consequence, extras, ctx2, kindOverride, correlationId) {
278
231
  let errorType;
279
232
  let message;
280
233
  let stack;
@@ -302,8 +255,8 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
302
255
  message: truncate(message, SEE_MAX_MESSAGE),
303
256
  subject: consequence.subject,
304
257
  outcome: consequence.outcome,
305
- side: ctx.side,
306
- sdk_version: ctx.sdkVersion,
258
+ side: ctx2.side,
259
+ sdk_version: ctx2.sdkVersion,
307
260
  ts: Date.now()
308
261
  };
309
262
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
@@ -312,10 +265,10 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
312
265
  if (causedBy) ev.caused_by = causedBy;
313
266
  const cleanExtras = sanitizeExtras(extras);
314
267
  if (cleanExtras) ev.extras = cleanExtras;
315
- if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
316
- if (ctx.userId) ev.user_id = ctx.userId;
317
- if (ctx.anonId) ev.anonymous_id = ctx.anonId;
318
- if (ctx.env) ev.env = ctx.env;
268
+ if (ctx2.url) ev.url = truncate(ctx2.url, SEE_MAX_SUBJECT);
269
+ if (ctx2.userId) ev.user_id = ctx2.userId;
270
+ if (ctx2.anonId) ev.anonymous_id = ctx2.anonId;
271
+ if (ctx2.env) ev.env = ctx2.env;
319
272
  markReported(problem, ev);
320
273
  return ev;
321
274
  }
@@ -408,6 +361,98 @@ var SeeLimiter = class {
408
361
  }
409
362
  };
410
363
 
364
+ // src/internal-report.ts
365
+ var INGEST_URL = "https://api.shipeasy.ai/collect";
366
+ var PLACEHOLDER_KEY = "sdk_client_REPLACE_WITH_SHIPEASY_INTERNAL_ERROR_KEY";
367
+ var INGEST_KEY = "sdk_client_00bd4608a03e4084922978f9522614d5";
368
+ function keyConfigured() {
369
+ return !!INGEST_KEY && INGEST_KEY !== PLACEHOLDER_KEY;
370
+ }
371
+ var OUTCOME = "returned a safe default";
372
+ var SDK_ID = "ts";
373
+ var ctx = null;
374
+ var limiter = new SeeLimiter();
375
+ function setInternalReportContext(c) {
376
+ ctx = { side: c.side, sdkVersion: c.sdkVersion, enabled: c.enabled !== false };
377
+ }
378
+ function reportInternalError(label, err) {
379
+ try {
380
+ if (!ctx || !ctx.enabled || !keyConfigured()) return;
381
+ const ev = buildSeeEvent(
382
+ err,
383
+ causesThe(label).to(OUTCOME),
384
+ { sdk: SDK_ID },
385
+ { side: ctx.side, sdkVersion: ctx.sdkVersion },
386
+ "caught"
387
+ );
388
+ if (!limiter.shouldSend(ev)) return;
389
+ send2(INGEST_URL, INGEST_KEY, JSON.stringify({ events: [ev] }));
390
+ } catch {
391
+ }
392
+ }
393
+ function send2(url, key, body) {
394
+ try {
395
+ const f = globalThis.fetch;
396
+ if (typeof f !== "function") return;
397
+ void f(url, {
398
+ method: "POST",
399
+ headers: { "X-SDK-Key": key, "Content-Type": "text/plain" },
400
+ body,
401
+ keepalive: true
402
+ }).catch(() => {
403
+ });
404
+ } catch {
405
+ }
406
+ }
407
+
408
+ // src/logger.ts
409
+ var LOG_LEVELS = ["silent", "error", "warn", "info", "debug"];
410
+ var RANK = {
411
+ silent: 0,
412
+ error: 1,
413
+ warn: 2,
414
+ info: 3,
415
+ debug: 4
416
+ };
417
+ var currentLevel = "warn";
418
+ function setLogLevel(level) {
419
+ if (typeof level === "string" && Object.prototype.hasOwnProperty.call(RANK, level)) {
420
+ currentLevel = level;
421
+ }
422
+ }
423
+ function write(method, args) {
424
+ try {
425
+ const c = globalThis.console;
426
+ if (!c) return;
427
+ const fn = c[method] ?? c.log;
428
+ if (typeof fn === "function") fn.call(c, ...args);
429
+ } catch {
430
+ }
431
+ }
432
+ var logger = {
433
+ error(...args) {
434
+ if (RANK[currentLevel] >= RANK.error) write("error", args);
435
+ },
436
+ warn(...args) {
437
+ if (RANK[currentLevel] >= RANK.warn) write("warn", args);
438
+ },
439
+ info(...args) {
440
+ if (RANK[currentLevel] >= RANK.info) write("info", args);
441
+ },
442
+ debug(...args) {
443
+ if (RANK[currentLevel] >= RANK.debug) write("debug", args);
444
+ }
445
+ };
446
+ function safeRun(label, fallback, fn) {
447
+ try {
448
+ return fn();
449
+ } catch (err) {
450
+ logger.error(`[shipeasy] ${label} failed \u2014 returning safe default:`, String(err));
451
+ reportInternalError(label, err);
452
+ return fallback;
453
+ }
454
+ }
455
+
411
456
  // src/client/index.ts
412
457
  var version = "4.0.0";
413
458
  var FLAG_REASONS = [
@@ -418,6 +463,40 @@ var FLAG_REASONS = [
418
463
  "RULE_MATCH",
419
464
  "DEFAULT"
420
465
  ];
466
+ var AssignmentImpl = class {
467
+ constructor(name, group, params) {
468
+ this.name = name;
469
+ this.group = group;
470
+ this.params = params;
471
+ }
472
+ name;
473
+ group;
474
+ params;
475
+ get enrolled() {
476
+ return this.group !== null;
477
+ }
478
+ get(field, fallback) {
479
+ const v = this.params[field];
480
+ return v === void 0 ? fallback : v;
481
+ }
482
+ };
483
+ function hasDomEvents() {
484
+ return typeof window !== "undefined" && typeof window.addEventListener === "function";
485
+ }
486
+ function onWindow(type, handler, opts) {
487
+ try {
488
+ if (hasDomEvents()) window.addEventListener(type, handler, opts);
489
+ } catch {
490
+ }
491
+ }
492
+ function onDocument(type, handler, opts) {
493
+ try {
494
+ if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
495
+ document.addEventListener(type, handler, opts);
496
+ }
497
+ } catch {
498
+ }
499
+ }
421
500
  var FLUSH_INTERVAL_MS = 5e3;
422
501
  var MAX_BUFFER = 100;
423
502
  var ANON_ID_KEY = "__se_anon_id";
@@ -429,8 +508,8 @@ var EventBuffer = class {
429
508
  this.sdkKey = sdkKey;
430
509
  if (typeof window !== "undefined") {
431
510
  this.timer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
432
- window.addEventListener("beforeunload", () => this.flush());
433
- document.addEventListener("visibilitychange", () => {
511
+ onWindow("beforeunload", () => this.flush());
512
+ onDocument("visibilitychange", () => {
434
513
  if (document.visibilityState === "hidden") this.flush(true);
435
514
  });
436
515
  try {
@@ -975,6 +1054,10 @@ var Engine = class _Engine {
975
1054
  env;
976
1055
  evalResult = null;
977
1056
  anonId;
1057
+ // Resolves once a caller-supplied anonymousStore has been consulted (persisted
1058
+ // id adopted, or the minted id written back). identify() awaits it so the
1059
+ // stable id is settled before the first /sdk/evaluate. null when no store.
1060
+ anonReady = null;
978
1061
  userId = "";
979
1062
  buffer;
980
1063
  telemetry;
@@ -1002,9 +1085,14 @@ var Engine = class _Engine {
1002
1085
  constructor(opts) {
1003
1086
  setLogLevel(opts.logLevel);
1004
1087
  this.sdkKey = opts.sdkKey;
1005
- this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
1088
+ this.baseUrl = (opts.baseUrl ?? "https://api.shipeasy.ai").replace(/\/$/, "");
1006
1089
  this.env = opts.env ?? "prod";
1007
1090
  this.testMode = opts.testMode === true;
1091
+ setInternalReportContext({
1092
+ side: "client",
1093
+ sdkVersion: version,
1094
+ enabled: !this.testMode && opts.disableInternalErrorReporting !== true
1095
+ });
1008
1096
  this.autoGuardrails = opts.autoGuardrails !== false;
1009
1097
  this.autoCollectAlways = opts.autoCollectAlways === true;
1010
1098
  this.disableAutoExposure = opts.disableAutoExposure === true;
@@ -1017,6 +1105,9 @@ var Engine = class _Engine {
1017
1105
  engagement: g.engagement ?? this.autoGuardrails
1018
1106
  };
1019
1107
  this.anonId = getOrCreateAnonId();
1108
+ if (opts.anonymousStore && !this.testMode) {
1109
+ this.anonReady = this.hydrateAnonFromStore(opts.anonymousStore);
1110
+ }
1020
1111
  this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
1021
1112
  this.telemetry = new Telemetry({
1022
1113
  endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
@@ -1032,6 +1123,23 @@ var Engine = class _Engine {
1032
1123
  void this.buffer.flushPendingAlias();
1033
1124
  }
1034
1125
  }
1126
+ /**
1127
+ * Consult the caller-supplied {@link AnonymousStore}: adopt a persisted id so
1128
+ * the visitor buckets identically across app launches, or persist the id just
1129
+ * minted by getOrCreateAnonId() on a fresh device. Best-effort — any store
1130
+ * failure leaves the in-memory id in place.
1131
+ */
1132
+ async hydrateAnonFromStore(store) {
1133
+ try {
1134
+ const stored = await store.get(ANON_ID_KEY);
1135
+ if (typeof stored === "string" && stored) {
1136
+ this.anonId = stored;
1137
+ } else {
1138
+ await store.set(ANON_ID_KEY, this.anonId);
1139
+ }
1140
+ } catch {
1141
+ }
1142
+ }
1035
1143
  /**
1036
1144
  * Build a no-network, immediately-usable browser client for tests
1037
1145
  * (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
@@ -1059,6 +1167,10 @@ var Engine = class _Engine {
1059
1167
  this.notify();
1060
1168
  return;
1061
1169
  }
1170
+ if (this.anonReady) {
1171
+ await this.anonReady;
1172
+ this.anonReady = null;
1173
+ }
1062
1174
  const seq = ++this.identifySeq;
1063
1175
  const prevUserId = this.userId;
1064
1176
  if (user.user_id !== void 0) this.userId = user.user_id;
@@ -1216,50 +1328,37 @@ var Engine = class _Engine {
1216
1328
  return void 0;
1217
1329
  }
1218
1330
  }
1219
- getExperiment(name, defaultParams, decodeOrOpts, variantsArg) {
1220
- const opts = typeof decodeOrOpts === "function" ? { decode: decodeOrOpts, variants: variantsArg } : decodeOrOpts ?? {};
1221
- const { decode, variants } = opts;
1331
+ /**
1332
+ * Assign the visitor within a universe: `universe("checkout").assign()`. A
1333
+ * universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
1334
+ * the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
1335
+ * and auto-logs one exposure when enrolled (unless `disableAutoExposure` or
1336
+ * `assign({ logExposure: false })`). An un-enrolled visitor still resolves
1337
+ * `get()` to the universe defaults. This is the sole experiment read path —
1338
+ * there is no `getExperiment` (ask a universe, not an experiment).
1339
+ */
1340
+ universe(name) {
1341
+ return { assign: (opts) => this.assignUniverse(name, opts) };
1342
+ }
1343
+ assignUniverse(name, opts) {
1222
1344
  this.telemetry.emit("experiment", name);
1223
- const notIn = {
1224
- inExperiment: false,
1225
- group: "control",
1226
- params: defaultParams
1227
- };
1228
- const pov = this.experimentOverrides.get(name);
1229
- if (pov) {
1230
- const variantParams = variants?.[pov.group];
1231
- const params = { ...defaultParams, ...pov.params, ...variantParams ?? {} };
1232
- return { inExperiment: true, group: pov.group, params };
1345
+ const defaults = this.evalResult?.universes?.[name]?.defaults ?? {};
1346
+ for (const [expName, pov] of this.experimentOverrides) {
1347
+ if (this.evalResult?.experiments[expName]?.universe !== name) continue;
1348
+ return new AssignmentImpl(expName, pov.group, { ...defaults, ...pov.params });
1233
1349
  }
1234
- const ov = readExpOverride(name);
1235
- if (ov !== null) {
1236
- const variantParams = variants?.[ov];
1237
- const params = variantParams ? { ...defaultParams, ...variantParams } : defaultParams;
1238
- return { inExperiment: true, group: ov, params };
1350
+ for (const [expName, entry] of Object.entries(this.evalResult?.experiments ?? {})) {
1351
+ if (entry.universe !== name) continue;
1352
+ const ov = readExpOverride(expName);
1353
+ if (ov !== null) return new AssignmentImpl(expName, ov, { ...defaults, ...entry.params });
1239
1354
  }
1240
- const entry = this.evalResult?.experiments[name];
1241
- if (!entry || !entry.inExperiment) return notIn;
1242
- const shouldLog = opts.logExposure ?? !this.disableAutoExposure;
1243
- if (shouldLog) this.buffer.pushExposure(name, entry.group, this.userId, this.anonId);
1244
- if (!decode) return { inExperiment: true, group: entry.group, params: entry.params };
1245
- try {
1246
- return { inExperiment: true, group: entry.group, params: decode(entry.params) };
1247
- } catch (err) {
1248
- logger.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
1249
- return notIn;
1355
+ for (const [expName, entry] of Object.entries(this.evalResult?.experiments ?? {})) {
1356
+ if (entry.universe !== name || !entry.inExperiment) continue;
1357
+ const shouldLog = opts?.logExposure ?? !this.disableAutoExposure;
1358
+ if (shouldLog) this.buffer.pushExposure(expName, entry.group, this.userId, this.anonId);
1359
+ return new AssignmentImpl(expName, entry.group, entry.params);
1250
1360
  }
1251
- }
1252
- /**
1253
- * Manually log an exposure for an enrolled experiment (Statsig's
1254
- * `manuallyLogExposure`). Reads the cached eval result; if the visitor is in
1255
- * the experiment, pushes the session-deduped exposure. Pair this with the
1256
- * render of the treatment when reading with `{ logExposure: false }` (or
1257
- * `disableAutoExposure: true`). No-op if the visitor isn't enrolled.
1258
- */
1259
- logExposure(name) {
1260
- const entry = this.evalResult?.experiments[name];
1261
- if (!entry || !entry.inExperiment) return;
1262
- this.buffer.pushExposure(name, entry.group, this.userId, this.anonId);
1361
+ return new AssignmentImpl(null, null, defaults);
1263
1362
  }
1264
1363
  /**
1265
1364
  * Subscribe to state changes — fires after identify() completes and on
@@ -1268,7 +1367,7 @@ var Engine = class _Engine {
1268
1367
  */
1269
1368
  subscribe(listener) {
1270
1369
  this.listeners.add(listener);
1271
- if (!this.overrideListenerInstalled && typeof window !== "undefined") {
1370
+ if (!this.overrideListenerInstalled && hasDomEvents()) {
1272
1371
  this.overrideListenerInstalled = true;
1273
1372
  window.addEventListener("se:override:change", this.onOverrideChange);
1274
1373
  }
@@ -1284,9 +1383,11 @@ var Engine = class _Engine {
1284
1383
  if (typeof window === "undefined") return null;
1285
1384
  const bridge = {
1286
1385
  getFlag: (n) => this.getFlag(n),
1386
+ // Devtools reads assignments by experiment name straight off the eval
1387
+ // result (the public read path is universe-first; this is display-only).
1287
1388
  getExperiment: (n) => {
1288
- const r = this.getExperiment(n, {});
1289
- return { inExperiment: r.inExperiment, group: r.group };
1389
+ const entry = this.evalResult?.experiments[n];
1390
+ return { inExperiment: entry?.inExperiment ?? false, group: entry?.group ?? "control" };
1290
1391
  },
1291
1392
  getConfig: (n) => this.getConfig(n)
1292
1393
  };
@@ -1409,7 +1510,7 @@ function loadDevtools(opts = {}) {
1409
1510
  }
1410
1511
  }
1411
1512
  function attachDevtools(client, opts = {}) {
1412
- if (typeof window === "undefined") return () => {
1513
+ if (!hasDomEvents()) return () => {
1413
1514
  };
1414
1515
  const hotkey = opts.hotkey ?? "Shift+Alt+S";
1415
1516
  const parts = hotkey.split("+");
@@ -1453,9 +1554,11 @@ function shipeasy(opts) {
1453
1554
  autoGuardrailGroups: groups,
1454
1555
  autoCollectAlways: acObj?.always === true,
1455
1556
  disableTelemetry: opts.disableTelemetry,
1557
+ disableInternalErrorReporting: opts.disableInternalErrorReporting,
1456
1558
  disableAutoExposure: opts.disableAutoExposure,
1457
1559
  privateAttributes: opts.privateAttributes,
1458
- stickyBucketing: opts.stickyBucketing
1560
+ stickyBucketing: opts.stickyBucketing,
1561
+ anonymousStore: opts.anonymousStore
1459
1562
  });
1460
1563
  injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
1461
1564
  flags.notifyMounted();
@@ -1537,7 +1640,7 @@ var _mountedAndReady = false;
1537
1640
  var _standaloneListeners = /* @__PURE__ */ new Set();
1538
1641
  var _standaloneOverrideWired = false;
1539
1642
  function wireStandaloneOverride() {
1540
- if (_standaloneOverrideWired || typeof window === "undefined") return;
1643
+ if (_standaloneOverrideWired || !hasDomEvents()) return;
1541
1644
  _standaloneOverrideWired = true;
1542
1645
  window.addEventListener("se:override:change", () => {
1543
1646
  for (const cb of _standaloneListeners) cb();
@@ -1603,21 +1706,22 @@ var flags = {
1603
1706
  }
1604
1707
  });
1605
1708
  },
1606
- getExperiment(name, defaultParams, decodeOrOpts, variants) {
1607
- const fallback = {
1608
- inExperiment: false,
1609
- group: "control",
1610
- params: defaultParams
1709
+ /**
1710
+ * Assign the visitor within a universe: `flags.universe("checkout").assign()`.
1711
+ * A universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
1712
+ * the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
1713
+ * and auto-logs one exposure when enrolled. Before configure() (or on error)
1714
+ * returns a safe not-enrolled handle. Replaces the removed `getExperiment` —
1715
+ * read experiments by universe, never by name.
1716
+ */
1717
+ universe(name) {
1718
+ return {
1719
+ assign: (opts) => safeRun(
1720
+ "flags.universe.assign",
1721
+ new AssignmentImpl(null, null, {}),
1722
+ () => _client ? _client.universe(name).assign(opts) : new AssignmentImpl(null, null, {})
1723
+ )
1611
1724
  };
1612
- return safeRun("flags.getExperiment", fallback, () => {
1613
- if (!_client) return fallback;
1614
- return typeof decodeOrOpts === "function" ? _client.getExperiment(name, defaultParams, decodeOrOpts, variants) : _client.getExperiment(name, defaultParams, decodeOrOpts ?? {});
1615
- });
1616
- },
1617
- /** Manually log an exposure for an enrolled experiment. See
1618
- * {@link Engine.logExposure}. No-op before configure(). */
1619
- logExposure(name) {
1620
- safeRun("flags.logExposure", void 0, () => _client?.logExposure(name));
1621
1725
  },
1622
1726
  track(eventName, props) {
1623
1727
  safeRun("flags.track", void 0, () => _client?.track(eventName, props));
@@ -1723,13 +1827,20 @@ var Client = class {
1723
1827
  () => this.engine.getConfig(name, decodeOrOpts)
1724
1828
  );
1725
1829
  }
1726
- getExperiment(name, defaultParams, decode) {
1727
- const notIn = { inExperiment: false, group: "control", params: defaultParams };
1728
- return safeRun(
1729
- "Client.getExperiment",
1730
- notIn,
1731
- () => this.engine.getExperiment(name, defaultParams, decode)
1732
- );
1830
+ /**
1831
+ * Assign the visitor within a universe: `client.universe("checkout").assign()`.
1832
+ * A universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
1833
+ * returns an {@link Assignment} (`.group` / `.get(field, fallback)`) and
1834
+ * auto-logs one exposure when enrolled. Replaces the removed `getExperiment`.
1835
+ */
1836
+ universe(name) {
1837
+ return {
1838
+ assign: (opts) => safeRun(
1839
+ "Client.universe.assign",
1840
+ new AssignmentImpl(null, null, {}),
1841
+ () => this.engine.universe(name).assign(opts)
1842
+ )
1843
+ };
1733
1844
  }
1734
1845
  /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
1735
1846
  getKillswitch(name, switchKey) {
@@ -1744,15 +1855,6 @@ var Client = class {
1744
1855
  track(eventName, props) {
1745
1856
  safeRun("Client.track", void 0, () => this.engine.track(eventName, props));
1746
1857
  }
1747
- /**
1748
- * Log an exposure for `name` at the treatment's render for the bound user.
1749
- * Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
1750
- * enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
1751
- * `disableAutoExposure` to log exposure exactly when you render.
1752
- */
1753
- logExposure(name) {
1754
- safeRun("Client.logExposure", void 0, () => this.engine.logExposure(name));
1755
- }
1756
1858
  };
1757
1859
  function dispatchSee(problem, consequence, extras, kind) {
1758
1860
  if (!_client) {
@@ -1978,6 +2080,7 @@ var i18n = {
1978
2080
  whenReady() {
1979
2081
  if (typeof window === "undefined") return Promise.resolve();
1980
2082
  if (window.i18n?.locale) return Promise.resolve();
2083
+ if (!hasDomEvents()) return Promise.resolve();
1981
2084
  return new Promise((resolve) => {
1982
2085
  const handler = () => resolve();
1983
2086
  window.addEventListener("se:i18n:ready", handler, { once: true });
@@ -1988,6 +2091,8 @@ var i18n = {
1988
2091
  if (typeof window === "undefined") return () => {
1989
2092
  };
1990
2093
  if (window.i18n) return window.i18n.on("update", cb);
2094
+ if (!hasDomEvents()) return () => {
2095
+ };
1991
2096
  let unsub = () => {
1992
2097
  };
1993
2098
  const handler = () => {