@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.
@@ -57,53 +57,6 @@ function send(url) {
57
57
  }
58
58
  }
59
59
 
60
- // src/logger.ts
61
- var LOG_LEVELS = ["silent", "error", "warn", "info", "debug"];
62
- var RANK = {
63
- silent: 0,
64
- error: 1,
65
- warn: 2,
66
- info: 3,
67
- debug: 4
68
- };
69
- var currentLevel = "warn";
70
- function setLogLevel(level) {
71
- if (typeof level === "string" && Object.prototype.hasOwnProperty.call(RANK, level)) {
72
- currentLevel = level;
73
- }
74
- }
75
- function write(method, args) {
76
- try {
77
- const c = globalThis.console;
78
- if (!c) return;
79
- const fn = c[method] ?? c.log;
80
- if (typeof fn === "function") fn.call(c, ...args);
81
- } catch {
82
- }
83
- }
84
- var logger = {
85
- error(...args) {
86
- if (RANK[currentLevel] >= RANK.error) write("error", args);
87
- },
88
- warn(...args) {
89
- if (RANK[currentLevel] >= RANK.warn) write("warn", args);
90
- },
91
- info(...args) {
92
- if (RANK[currentLevel] >= RANK.info) write("info", args);
93
- },
94
- debug(...args) {
95
- if (RANK[currentLevel] >= RANK.debug) write("debug", args);
96
- }
97
- };
98
- function safeRun(label, fallback, fn) {
99
- try {
100
- return fn();
101
- } catch (err) {
102
- logger.error(`[shipeasy] ${label} failed \u2014 returning safe default:`, String(err));
103
- return fallback;
104
- }
105
- }
106
-
107
60
  // src/see/core.ts
108
61
  var SEE_MAX_MESSAGE = 500;
109
62
  var SEE_MAX_STACK = 8e3;
@@ -221,7 +174,7 @@ function captureCallsiteStack() {
221
174
  const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
222
175
  return kept.length ? kept.join("\n") : void 0;
223
176
  }
224
- function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlationId) {
177
+ function buildSeeEvent(problem, consequence, extras, ctx2, kindOverride, correlationId) {
225
178
  let errorType;
226
179
  let message;
227
180
  let stack;
@@ -249,8 +202,8 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
249
202
  message: truncate(message, SEE_MAX_MESSAGE),
250
203
  subject: consequence.subject,
251
204
  outcome: consequence.outcome,
252
- side: ctx.side,
253
- sdk_version: ctx.sdkVersion,
205
+ side: ctx2.side,
206
+ sdk_version: ctx2.sdkVersion,
254
207
  ts: Date.now()
255
208
  };
256
209
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
@@ -259,10 +212,10 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
259
212
  if (causedBy) ev.caused_by = causedBy;
260
213
  const cleanExtras = sanitizeExtras(extras);
261
214
  if (cleanExtras) ev.extras = cleanExtras;
262
- if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
263
- if (ctx.userId) ev.user_id = ctx.userId;
264
- if (ctx.anonId) ev.anonymous_id = ctx.anonId;
265
- if (ctx.env) ev.env = ctx.env;
215
+ if (ctx2.url) ev.url = truncate(ctx2.url, SEE_MAX_SUBJECT);
216
+ if (ctx2.userId) ev.user_id = ctx2.userId;
217
+ if (ctx2.anonId) ev.anonymous_id = ctx2.anonId;
218
+ if (ctx2.env) ev.env = ctx2.env;
266
219
  markReported(problem, ev);
267
220
  return ev;
268
221
  }
@@ -355,6 +308,98 @@ var SeeLimiter = class {
355
308
  }
356
309
  };
357
310
 
311
+ // src/internal-report.ts
312
+ var INGEST_URL = "https://api.shipeasy.ai/collect";
313
+ var PLACEHOLDER_KEY = "sdk_client_REPLACE_WITH_SHIPEASY_INTERNAL_ERROR_KEY";
314
+ var INGEST_KEY = "sdk_client_00bd4608a03e4084922978f9522614d5";
315
+ function keyConfigured() {
316
+ return !!INGEST_KEY && INGEST_KEY !== PLACEHOLDER_KEY;
317
+ }
318
+ var OUTCOME = "returned a safe default";
319
+ var SDK_ID = "ts";
320
+ var ctx = null;
321
+ var limiter = new SeeLimiter();
322
+ function setInternalReportContext(c) {
323
+ ctx = { side: c.side, sdkVersion: c.sdkVersion, enabled: c.enabled !== false };
324
+ }
325
+ function reportInternalError(label, err) {
326
+ try {
327
+ if (!ctx || !ctx.enabled || !keyConfigured()) return;
328
+ const ev = buildSeeEvent(
329
+ err,
330
+ causesThe(label).to(OUTCOME),
331
+ { sdk: SDK_ID },
332
+ { side: ctx.side, sdkVersion: ctx.sdkVersion },
333
+ "caught"
334
+ );
335
+ if (!limiter.shouldSend(ev)) return;
336
+ send2(INGEST_URL, INGEST_KEY, JSON.stringify({ events: [ev] }));
337
+ } catch {
338
+ }
339
+ }
340
+ function send2(url, key, body) {
341
+ try {
342
+ const f = globalThis.fetch;
343
+ if (typeof f !== "function") return;
344
+ void f(url, {
345
+ method: "POST",
346
+ headers: { "X-SDK-Key": key, "Content-Type": "text/plain" },
347
+ body,
348
+ keepalive: true
349
+ }).catch(() => {
350
+ });
351
+ } catch {
352
+ }
353
+ }
354
+
355
+ // src/logger.ts
356
+ var LOG_LEVELS = ["silent", "error", "warn", "info", "debug"];
357
+ var RANK = {
358
+ silent: 0,
359
+ error: 1,
360
+ warn: 2,
361
+ info: 3,
362
+ debug: 4
363
+ };
364
+ var currentLevel = "warn";
365
+ function setLogLevel(level) {
366
+ if (typeof level === "string" && Object.prototype.hasOwnProperty.call(RANK, level)) {
367
+ currentLevel = level;
368
+ }
369
+ }
370
+ function write(method, args) {
371
+ try {
372
+ const c = globalThis.console;
373
+ if (!c) return;
374
+ const fn = c[method] ?? c.log;
375
+ if (typeof fn === "function") fn.call(c, ...args);
376
+ } catch {
377
+ }
378
+ }
379
+ var logger = {
380
+ error(...args) {
381
+ if (RANK[currentLevel] >= RANK.error) write("error", args);
382
+ },
383
+ warn(...args) {
384
+ if (RANK[currentLevel] >= RANK.warn) write("warn", args);
385
+ },
386
+ info(...args) {
387
+ if (RANK[currentLevel] >= RANK.info) write("info", args);
388
+ },
389
+ debug(...args) {
390
+ if (RANK[currentLevel] >= RANK.debug) write("debug", args);
391
+ }
392
+ };
393
+ function safeRun(label, fallback, fn) {
394
+ try {
395
+ return fn();
396
+ } catch (err) {
397
+ logger.error(`[shipeasy] ${label} failed \u2014 returning safe default:`, String(err));
398
+ reportInternalError(label, err);
399
+ return fallback;
400
+ }
401
+ }
402
+
358
403
  // src/client/index.ts
359
404
  var version = "4.0.0";
360
405
  var FLAG_REASONS = [
@@ -365,6 +410,40 @@ var FLAG_REASONS = [
365
410
  "RULE_MATCH",
366
411
  "DEFAULT"
367
412
  ];
413
+ var AssignmentImpl = class {
414
+ constructor(name, group, params) {
415
+ this.name = name;
416
+ this.group = group;
417
+ this.params = params;
418
+ }
419
+ name;
420
+ group;
421
+ params;
422
+ get enrolled() {
423
+ return this.group !== null;
424
+ }
425
+ get(field, fallback) {
426
+ const v = this.params[field];
427
+ return v === void 0 ? fallback : v;
428
+ }
429
+ };
430
+ function hasDomEvents() {
431
+ return typeof window !== "undefined" && typeof window.addEventListener === "function";
432
+ }
433
+ function onWindow(type, handler, opts) {
434
+ try {
435
+ if (hasDomEvents()) window.addEventListener(type, handler, opts);
436
+ } catch {
437
+ }
438
+ }
439
+ function onDocument(type, handler, opts) {
440
+ try {
441
+ if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
442
+ document.addEventListener(type, handler, opts);
443
+ }
444
+ } catch {
445
+ }
446
+ }
368
447
  var FLUSH_INTERVAL_MS = 5e3;
369
448
  var MAX_BUFFER = 100;
370
449
  var ANON_ID_KEY = "__se_anon_id";
@@ -376,8 +455,8 @@ var EventBuffer = class {
376
455
  this.sdkKey = sdkKey;
377
456
  if (typeof window !== "undefined") {
378
457
  this.timer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
379
- window.addEventListener("beforeunload", () => this.flush());
380
- document.addEventListener("visibilitychange", () => {
458
+ onWindow("beforeunload", () => this.flush());
459
+ onDocument("visibilitychange", () => {
381
460
  if (document.visibilityState === "hidden") this.flush(true);
382
461
  });
383
462
  try {
@@ -922,6 +1001,10 @@ var Engine = class _Engine {
922
1001
  env;
923
1002
  evalResult = null;
924
1003
  anonId;
1004
+ // Resolves once a caller-supplied anonymousStore has been consulted (persisted
1005
+ // id adopted, or the minted id written back). identify() awaits it so the
1006
+ // stable id is settled before the first /sdk/evaluate. null when no store.
1007
+ anonReady = null;
925
1008
  userId = "";
926
1009
  buffer;
927
1010
  telemetry;
@@ -949,9 +1032,14 @@ var Engine = class _Engine {
949
1032
  constructor(opts) {
950
1033
  setLogLevel(opts.logLevel);
951
1034
  this.sdkKey = opts.sdkKey;
952
- this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
1035
+ this.baseUrl = (opts.baseUrl ?? "https://api.shipeasy.ai").replace(/\/$/, "");
953
1036
  this.env = opts.env ?? "prod";
954
1037
  this.testMode = opts.testMode === true;
1038
+ setInternalReportContext({
1039
+ side: "client",
1040
+ sdkVersion: version,
1041
+ enabled: !this.testMode && opts.disableInternalErrorReporting !== true
1042
+ });
955
1043
  this.autoGuardrails = opts.autoGuardrails !== false;
956
1044
  this.autoCollectAlways = opts.autoCollectAlways === true;
957
1045
  this.disableAutoExposure = opts.disableAutoExposure === true;
@@ -964,6 +1052,9 @@ var Engine = class _Engine {
964
1052
  engagement: g.engagement ?? this.autoGuardrails
965
1053
  };
966
1054
  this.anonId = getOrCreateAnonId();
1055
+ if (opts.anonymousStore && !this.testMode) {
1056
+ this.anonReady = this.hydrateAnonFromStore(opts.anonymousStore);
1057
+ }
967
1058
  this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
968
1059
  this.telemetry = new Telemetry({
969
1060
  endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
@@ -979,6 +1070,23 @@ var Engine = class _Engine {
979
1070
  void this.buffer.flushPendingAlias();
980
1071
  }
981
1072
  }
1073
+ /**
1074
+ * Consult the caller-supplied {@link AnonymousStore}: adopt a persisted id so
1075
+ * the visitor buckets identically across app launches, or persist the id just
1076
+ * minted by getOrCreateAnonId() on a fresh device. Best-effort — any store
1077
+ * failure leaves the in-memory id in place.
1078
+ */
1079
+ async hydrateAnonFromStore(store) {
1080
+ try {
1081
+ const stored = await store.get(ANON_ID_KEY);
1082
+ if (typeof stored === "string" && stored) {
1083
+ this.anonId = stored;
1084
+ } else {
1085
+ await store.set(ANON_ID_KEY, this.anonId);
1086
+ }
1087
+ } catch {
1088
+ }
1089
+ }
982
1090
  /**
983
1091
  * Build a no-network, immediately-usable browser client for tests
984
1092
  * (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
@@ -1006,6 +1114,10 @@ var Engine = class _Engine {
1006
1114
  this.notify();
1007
1115
  return;
1008
1116
  }
1117
+ if (this.anonReady) {
1118
+ await this.anonReady;
1119
+ this.anonReady = null;
1120
+ }
1009
1121
  const seq = ++this.identifySeq;
1010
1122
  const prevUserId = this.userId;
1011
1123
  if (user.user_id !== void 0) this.userId = user.user_id;
@@ -1163,50 +1275,37 @@ var Engine = class _Engine {
1163
1275
  return void 0;
1164
1276
  }
1165
1277
  }
1166
- getExperiment(name, defaultParams, decodeOrOpts, variantsArg) {
1167
- const opts = typeof decodeOrOpts === "function" ? { decode: decodeOrOpts, variants: variantsArg } : decodeOrOpts ?? {};
1168
- const { decode, variants } = opts;
1278
+ /**
1279
+ * Assign the visitor within a universe: `universe("checkout").assign()`. A
1280
+ * universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
1281
+ * the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
1282
+ * and auto-logs one exposure when enrolled (unless `disableAutoExposure` or
1283
+ * `assign({ logExposure: false })`). An un-enrolled visitor still resolves
1284
+ * `get()` to the universe defaults. This is the sole experiment read path —
1285
+ * there is no `getExperiment` (ask a universe, not an experiment).
1286
+ */
1287
+ universe(name) {
1288
+ return { assign: (opts) => this.assignUniverse(name, opts) };
1289
+ }
1290
+ assignUniverse(name, opts) {
1169
1291
  this.telemetry.emit("experiment", name);
1170
- const notIn = {
1171
- inExperiment: false,
1172
- group: "control",
1173
- params: defaultParams
1174
- };
1175
- const pov = this.experimentOverrides.get(name);
1176
- if (pov) {
1177
- const variantParams = variants?.[pov.group];
1178
- const params = { ...defaultParams, ...pov.params, ...variantParams ?? {} };
1179
- return { inExperiment: true, group: pov.group, params };
1292
+ const defaults = this.evalResult?.universes?.[name]?.defaults ?? {};
1293
+ for (const [expName, pov] of this.experimentOverrides) {
1294
+ if (this.evalResult?.experiments[expName]?.universe !== name) continue;
1295
+ return new AssignmentImpl(expName, pov.group, { ...defaults, ...pov.params });
1180
1296
  }
1181
- const ov = readExpOverride(name);
1182
- if (ov !== null) {
1183
- const variantParams = variants?.[ov];
1184
- const params = variantParams ? { ...defaultParams, ...variantParams } : defaultParams;
1185
- return { inExperiment: true, group: ov, params };
1297
+ for (const [expName, entry] of Object.entries(this.evalResult?.experiments ?? {})) {
1298
+ if (entry.universe !== name) continue;
1299
+ const ov = readExpOverride(expName);
1300
+ if (ov !== null) return new AssignmentImpl(expName, ov, { ...defaults, ...entry.params });
1186
1301
  }
1187
- const entry = this.evalResult?.experiments[name];
1188
- if (!entry || !entry.inExperiment) return notIn;
1189
- const shouldLog = opts.logExposure ?? !this.disableAutoExposure;
1190
- if (shouldLog) this.buffer.pushExposure(name, entry.group, this.userId, this.anonId);
1191
- if (!decode) return { inExperiment: true, group: entry.group, params: entry.params };
1192
- try {
1193
- return { inExperiment: true, group: entry.group, params: decode(entry.params) };
1194
- } catch (err) {
1195
- logger.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
1196
- return notIn;
1302
+ for (const [expName, entry] of Object.entries(this.evalResult?.experiments ?? {})) {
1303
+ if (entry.universe !== name || !entry.inExperiment) continue;
1304
+ const shouldLog = opts?.logExposure ?? !this.disableAutoExposure;
1305
+ if (shouldLog) this.buffer.pushExposure(expName, entry.group, this.userId, this.anonId);
1306
+ return new AssignmentImpl(expName, entry.group, entry.params);
1197
1307
  }
1198
- }
1199
- /**
1200
- * Manually log an exposure for an enrolled experiment (Statsig's
1201
- * `manuallyLogExposure`). Reads the cached eval result; if the visitor is in
1202
- * the experiment, pushes the session-deduped exposure. Pair this with the
1203
- * render of the treatment when reading with `{ logExposure: false }` (or
1204
- * `disableAutoExposure: true`). No-op if the visitor isn't enrolled.
1205
- */
1206
- logExposure(name) {
1207
- const entry = this.evalResult?.experiments[name];
1208
- if (!entry || !entry.inExperiment) return;
1209
- this.buffer.pushExposure(name, entry.group, this.userId, this.anonId);
1308
+ return new AssignmentImpl(null, null, defaults);
1210
1309
  }
1211
1310
  /**
1212
1311
  * Subscribe to state changes — fires after identify() completes and on
@@ -1215,7 +1314,7 @@ var Engine = class _Engine {
1215
1314
  */
1216
1315
  subscribe(listener) {
1217
1316
  this.listeners.add(listener);
1218
- if (!this.overrideListenerInstalled && typeof window !== "undefined") {
1317
+ if (!this.overrideListenerInstalled && hasDomEvents()) {
1219
1318
  this.overrideListenerInstalled = true;
1220
1319
  window.addEventListener("se:override:change", this.onOverrideChange);
1221
1320
  }
@@ -1231,9 +1330,11 @@ var Engine = class _Engine {
1231
1330
  if (typeof window === "undefined") return null;
1232
1331
  const bridge = {
1233
1332
  getFlag: (n) => this.getFlag(n),
1333
+ // Devtools reads assignments by experiment name straight off the eval
1334
+ // result (the public read path is universe-first; this is display-only).
1234
1335
  getExperiment: (n) => {
1235
- const r = this.getExperiment(n, {});
1236
- return { inExperiment: r.inExperiment, group: r.group };
1336
+ const entry = this.evalResult?.experiments[n];
1337
+ return { inExperiment: entry?.inExperiment ?? false, group: entry?.group ?? "control" };
1237
1338
  },
1238
1339
  getConfig: (n) => this.getConfig(n)
1239
1340
  };
@@ -1356,7 +1457,7 @@ function loadDevtools(opts = {}) {
1356
1457
  }
1357
1458
  }
1358
1459
  function attachDevtools(client, opts = {}) {
1359
- if (typeof window === "undefined") return () => {
1460
+ if (!hasDomEvents()) return () => {
1360
1461
  };
1361
1462
  const hotkey = opts.hotkey ?? "Shift+Alt+S";
1362
1463
  const parts = hotkey.split("+");
@@ -1400,9 +1501,11 @@ function shipeasy(opts) {
1400
1501
  autoGuardrailGroups: groups,
1401
1502
  autoCollectAlways: acObj?.always === true,
1402
1503
  disableTelemetry: opts.disableTelemetry,
1504
+ disableInternalErrorReporting: opts.disableInternalErrorReporting,
1403
1505
  disableAutoExposure: opts.disableAutoExposure,
1404
1506
  privateAttributes: opts.privateAttributes,
1405
- stickyBucketing: opts.stickyBucketing
1507
+ stickyBucketing: opts.stickyBucketing,
1508
+ anonymousStore: opts.anonymousStore
1406
1509
  });
1407
1510
  injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
1408
1511
  flags.notifyMounted();
@@ -1484,7 +1587,7 @@ var _mountedAndReady = false;
1484
1587
  var _standaloneListeners = /* @__PURE__ */ new Set();
1485
1588
  var _standaloneOverrideWired = false;
1486
1589
  function wireStandaloneOverride() {
1487
- if (_standaloneOverrideWired || typeof window === "undefined") return;
1590
+ if (_standaloneOverrideWired || !hasDomEvents()) return;
1488
1591
  _standaloneOverrideWired = true;
1489
1592
  window.addEventListener("se:override:change", () => {
1490
1593
  for (const cb of _standaloneListeners) cb();
@@ -1550,21 +1653,22 @@ var flags = {
1550
1653
  }
1551
1654
  });
1552
1655
  },
1553
- getExperiment(name, defaultParams, decodeOrOpts, variants) {
1554
- const fallback = {
1555
- inExperiment: false,
1556
- group: "control",
1557
- params: defaultParams
1656
+ /**
1657
+ * Assign the visitor within a universe: `flags.universe("checkout").assign()`.
1658
+ * A universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
1659
+ * the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
1660
+ * and auto-logs one exposure when enrolled. Before configure() (or on error)
1661
+ * returns a safe not-enrolled handle. Replaces the removed `getExperiment` —
1662
+ * read experiments by universe, never by name.
1663
+ */
1664
+ universe(name) {
1665
+ return {
1666
+ assign: (opts) => safeRun(
1667
+ "flags.universe.assign",
1668
+ new AssignmentImpl(null, null, {}),
1669
+ () => _client ? _client.universe(name).assign(opts) : new AssignmentImpl(null, null, {})
1670
+ )
1558
1671
  };
1559
- return safeRun("flags.getExperiment", fallback, () => {
1560
- if (!_client) return fallback;
1561
- return typeof decodeOrOpts === "function" ? _client.getExperiment(name, defaultParams, decodeOrOpts, variants) : _client.getExperiment(name, defaultParams, decodeOrOpts ?? {});
1562
- });
1563
- },
1564
- /** Manually log an exposure for an enrolled experiment. See
1565
- * {@link Engine.logExposure}. No-op before configure(). */
1566
- logExposure(name) {
1567
- safeRun("flags.logExposure", void 0, () => _client?.logExposure(name));
1568
1672
  },
1569
1673
  track(eventName, props) {
1570
1674
  safeRun("flags.track", void 0, () => _client?.track(eventName, props));
@@ -1670,13 +1774,20 @@ var Client = class {
1670
1774
  () => this.engine.getConfig(name, decodeOrOpts)
1671
1775
  );
1672
1776
  }
1673
- getExperiment(name, defaultParams, decode) {
1674
- const notIn = { inExperiment: false, group: "control", params: defaultParams };
1675
- return safeRun(
1676
- "Client.getExperiment",
1677
- notIn,
1678
- () => this.engine.getExperiment(name, defaultParams, decode)
1679
- );
1777
+ /**
1778
+ * Assign the visitor within a universe: `client.universe("checkout").assign()`.
1779
+ * A universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
1780
+ * returns an {@link Assignment} (`.group` / `.get(field, fallback)`) and
1781
+ * auto-logs one exposure when enrolled. Replaces the removed `getExperiment`.
1782
+ */
1783
+ universe(name) {
1784
+ return {
1785
+ assign: (opts) => safeRun(
1786
+ "Client.universe.assign",
1787
+ new AssignmentImpl(null, null, {}),
1788
+ () => this.engine.universe(name).assign(opts)
1789
+ )
1790
+ };
1680
1791
  }
1681
1792
  /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
1682
1793
  getKillswitch(name, switchKey) {
@@ -1691,15 +1802,6 @@ var Client = class {
1691
1802
  track(eventName, props) {
1692
1803
  safeRun("Client.track", void 0, () => this.engine.track(eventName, props));
1693
1804
  }
1694
- /**
1695
- * Log an exposure for `name` at the treatment's render for the bound user.
1696
- * Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
1697
- * enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
1698
- * `disableAutoExposure` to log exposure exactly when you render.
1699
- */
1700
- logExposure(name) {
1701
- safeRun("Client.logExposure", void 0, () => this.engine.logExposure(name));
1702
- }
1703
1805
  };
1704
1806
  function dispatchSee(problem, consequence, extras, kind) {
1705
1807
  if (!_client) {
@@ -1925,6 +2027,7 @@ var i18n = {
1925
2027
  whenReady() {
1926
2028
  if (typeof window === "undefined") return Promise.resolve();
1927
2029
  if (window.i18n?.locale) return Promise.resolve();
2030
+ if (!hasDomEvents()) return Promise.resolve();
1928
2031
  return new Promise((resolve) => {
1929
2032
  const handler = () => resolve();
1930
2033
  window.addEventListener("se:i18n:ready", handler, { once: true });
@@ -1935,6 +2038,8 @@ var i18n = {
1935
2038
  if (typeof window === "undefined") return () => {
1936
2039
  };
1937
2040
  if (window.i18n) return window.i18n.on("update", cb);
2041
+ if (!hasDomEvents()) return () => {
2042
+ };
1938
2043
  let unsub = () => {
1939
2044
  };
1940
2045
  const handler = () => {