@shipeasy/sdk 6.3.1 → 7.1.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,50 +57,21 @@ 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
- }
60
+ // src/env.ts
61
+ function isProductionEnv(configuredEnv) {
62
+ const native = readNativeEnv();
63
+ if (native !== null) return native === "production" || native === "prod";
64
+ return (configuredEnv ?? "prod").toLowerCase() === "prod";
74
65
  }
75
- function write(method, args) {
66
+ function readNativeEnv() {
76
67
  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);
68
+ const proc = globalThis.process;
69
+ const raw = proc?.env?.SHIPEASY_ENV ?? proc?.env?.NODE_ENV;
70
+ if (typeof raw !== "string") return null;
71
+ const v = raw.trim().toLowerCase();
72
+ return v.length ? v : null;
81
73
  } 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;
74
+ return null;
104
75
  }
105
76
  }
106
77
 
@@ -221,7 +192,7 @@ function captureCallsiteStack() {
221
192
  const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
222
193
  return kept.length ? kept.join("\n") : void 0;
223
194
  }
224
- function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlationId) {
195
+ function buildSeeEvent(problem, consequence, extras, ctx2, kindOverride, correlationId) {
225
196
  let errorType;
226
197
  let message;
227
198
  let stack;
@@ -249,8 +220,8 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
249
220
  message: truncate(message, SEE_MAX_MESSAGE),
250
221
  subject: consequence.subject,
251
222
  outcome: consequence.outcome,
252
- side: ctx.side,
253
- sdk_version: ctx.sdkVersion,
223
+ side: ctx2.side,
224
+ sdk_version: ctx2.sdkVersion,
254
225
  ts: Date.now()
255
226
  };
256
227
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
@@ -259,10 +230,10 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
259
230
  if (causedBy) ev.caused_by = causedBy;
260
231
  const cleanExtras = sanitizeExtras(extras);
261
232
  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;
233
+ if (ctx2.url) ev.url = truncate(ctx2.url, SEE_MAX_SUBJECT);
234
+ if (ctx2.userId) ev.user_id = ctx2.userId;
235
+ if (ctx2.anonId) ev.anonymous_id = ctx2.anonId;
236
+ if (ctx2.env) ev.env = ctx2.env;
266
237
  markReported(problem, ev);
267
238
  return ev;
268
239
  }
@@ -355,6 +326,98 @@ var SeeLimiter = class {
355
326
  }
356
327
  };
357
328
 
329
+ // src/internal-report.ts
330
+ var INGEST_URL = "https://api.shipeasy.ai/collect";
331
+ var PLACEHOLDER_KEY = "sdk_client_REPLACE_WITH_SHIPEASY_INTERNAL_ERROR_KEY";
332
+ var INGEST_KEY = "sdk_client_00bd4608a03e4084922978f9522614d5";
333
+ function keyConfigured() {
334
+ return !!INGEST_KEY && INGEST_KEY !== PLACEHOLDER_KEY;
335
+ }
336
+ var OUTCOME = "returned a safe default";
337
+ var SDK_ID = "ts";
338
+ var ctx = null;
339
+ var limiter = new SeeLimiter();
340
+ function setInternalReportContext(c) {
341
+ ctx = { side: c.side, sdkVersion: c.sdkVersion, enabled: c.enabled !== false };
342
+ }
343
+ function reportInternalError(label, err) {
344
+ try {
345
+ if (!ctx || !ctx.enabled || !keyConfigured()) return;
346
+ const ev = buildSeeEvent(
347
+ err,
348
+ causesThe(label).to(OUTCOME),
349
+ { sdk: SDK_ID },
350
+ { side: ctx.side, sdkVersion: ctx.sdkVersion },
351
+ "caught"
352
+ );
353
+ if (!limiter.shouldSend(ev)) return;
354
+ send2(INGEST_URL, INGEST_KEY, JSON.stringify({ events: [ev] }));
355
+ } catch {
356
+ }
357
+ }
358
+ function send2(url, key, body) {
359
+ try {
360
+ const f = globalThis.fetch;
361
+ if (typeof f !== "function") return;
362
+ void f(url, {
363
+ method: "POST",
364
+ headers: { "X-SDK-Key": key, "Content-Type": "text/plain" },
365
+ body,
366
+ keepalive: true
367
+ }).catch(() => {
368
+ });
369
+ } catch {
370
+ }
371
+ }
372
+
373
+ // src/logger.ts
374
+ var LOG_LEVELS = ["silent", "error", "warn", "info", "debug"];
375
+ var RANK = {
376
+ silent: 0,
377
+ error: 1,
378
+ warn: 2,
379
+ info: 3,
380
+ debug: 4
381
+ };
382
+ var currentLevel = "warn";
383
+ function setLogLevel(level) {
384
+ if (typeof level === "string" && Object.prototype.hasOwnProperty.call(RANK, level)) {
385
+ currentLevel = level;
386
+ }
387
+ }
388
+ function write(method, args) {
389
+ try {
390
+ const c = globalThis.console;
391
+ if (!c) return;
392
+ const fn = c[method] ?? c.log;
393
+ if (typeof fn === "function") fn.call(c, ...args);
394
+ } catch {
395
+ }
396
+ }
397
+ var logger = {
398
+ error(...args) {
399
+ if (RANK[currentLevel] >= RANK.error) write("error", args);
400
+ },
401
+ warn(...args) {
402
+ if (RANK[currentLevel] >= RANK.warn) write("warn", args);
403
+ },
404
+ info(...args) {
405
+ if (RANK[currentLevel] >= RANK.info) write("info", args);
406
+ },
407
+ debug(...args) {
408
+ if (RANK[currentLevel] >= RANK.debug) write("debug", args);
409
+ }
410
+ };
411
+ function safeRun(label, fallback, fn) {
412
+ try {
413
+ return fn();
414
+ } catch (err) {
415
+ logger.error(`[shipeasy] ${label} failed \u2014 returning safe default:`, String(err));
416
+ reportInternalError(label, err);
417
+ return fallback;
418
+ }
419
+ }
420
+
358
421
  // src/client/index.ts
359
422
  var version = "4.0.0";
360
423
  var FLAG_REASONS = [
@@ -365,6 +428,40 @@ var FLAG_REASONS = [
365
428
  "RULE_MATCH",
366
429
  "DEFAULT"
367
430
  ];
431
+ var AssignmentImpl = class {
432
+ constructor(name, group, params) {
433
+ this.name = name;
434
+ this.group = group;
435
+ this.params = params;
436
+ }
437
+ name;
438
+ group;
439
+ params;
440
+ get enrolled() {
441
+ return this.group !== null;
442
+ }
443
+ get(field, fallback) {
444
+ const v = this.params[field];
445
+ return v === void 0 ? fallback : v;
446
+ }
447
+ };
448
+ function hasDomEvents() {
449
+ return typeof window !== "undefined" && typeof window.addEventListener === "function";
450
+ }
451
+ function onWindow(type, handler, opts) {
452
+ try {
453
+ if (hasDomEvents()) window.addEventListener(type, handler, opts);
454
+ } catch {
455
+ }
456
+ }
457
+ function onDocument(type, handler, opts) {
458
+ try {
459
+ if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
460
+ document.addEventListener(type, handler, opts);
461
+ }
462
+ } catch {
463
+ }
464
+ }
368
465
  var FLUSH_INTERVAL_MS = 5e3;
369
466
  var MAX_BUFFER = 100;
370
467
  var ANON_ID_KEY = "__se_anon_id";
@@ -376,8 +473,8 @@ var EventBuffer = class {
376
473
  this.sdkKey = sdkKey;
377
474
  if (typeof window !== "undefined") {
378
475
  this.timer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
379
- window.addEventListener("beforeunload", () => this.flush());
380
- document.addEventListener("visibilitychange", () => {
476
+ onWindow("beforeunload", () => this.flush());
477
+ onDocument("visibilitychange", () => {
381
478
  if (document.visibilityState === "hidden") this.flush(true);
382
479
  });
383
480
  try {
@@ -922,6 +1019,10 @@ var Engine = class _Engine {
922
1019
  env;
923
1020
  evalResult = null;
924
1021
  anonId;
1022
+ // Resolves once a caller-supplied anonymousStore has been consulted (persisted
1023
+ // id adopted, or the minted id written back). identify() awaits it so the
1024
+ // stable id is settled before the first /sdk/evaluate. null when no store.
1025
+ anonReady = null;
925
1026
  userId = "";
926
1027
  buffer;
927
1028
  telemetry;
@@ -936,6 +1037,12 @@ var Engine = class _Engine {
936
1037
  // never fetches, track() is a no-op, telemetry is off, and the client is
937
1038
  // already ready with an empty eval result.
938
1039
  testMode;
1040
+ // Master network gate — false ⇒ the SDK never touches the network. Defaults to
1041
+ // environment-derived (prod-on), forced false by testMode.
1042
+ networkEnabled;
1043
+ // Convenience inverse of networkEnabled — the "no network at all" state every
1044
+ // /sdk/evaluate, /collect and telemetry gate keys on. testMode implies offline.
1045
+ offline;
939
1046
  // Programmatic overrides (Statsig-style). Set on any client via
940
1047
  // overrideFlag/overrideConfig/overrideExperiment; they win over BOTH the URL
941
1048
  // overrides and the fetched eval result. Cleared by clearOverrides().
@@ -949,9 +1056,17 @@ var Engine = class _Engine {
949
1056
  constructor(opts) {
950
1057
  setLogLevel(opts.logLevel);
951
1058
  this.sdkKey = opts.sdkKey;
952
- this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
1059
+ this.baseUrl = (opts.baseUrl ?? "https://api.shipeasy.ai").replace(/\/$/, "");
953
1060
  this.env = opts.env ?? "prod";
954
1061
  this.testMode = opts.testMode === true;
1062
+ const prod = isProductionEnv(this.env);
1063
+ this.networkEnabled = this.testMode ? false : opts.isNetworkEnabled ?? prod;
1064
+ this.offline = !this.networkEnabled;
1065
+ setInternalReportContext({
1066
+ side: "client",
1067
+ sdkVersion: version,
1068
+ enabled: this.networkEnabled && opts.disableInternalErrorReporting !== true
1069
+ });
955
1070
  this.autoGuardrails = opts.autoGuardrails !== false;
956
1071
  this.autoCollectAlways = opts.autoCollectAlways === true;
957
1072
  this.disableAutoExposure = opts.disableAutoExposure === true;
@@ -964,21 +1079,42 @@ var Engine = class _Engine {
964
1079
  engagement: g.engagement ?? this.autoGuardrails
965
1080
  };
966
1081
  this.anonId = getOrCreateAnonId();
1082
+ if (opts.anonymousStore && !this.offline) {
1083
+ this.anonReady = this.hydrateAnonFromStore(opts.anonymousStore);
1084
+ }
967
1085
  this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
968
1086
  this.telemetry = new Telemetry({
969
1087
  endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
970
1088
  sdkKey: this.sdkKey,
971
1089
  side: "client",
972
1090
  env: this.env,
973
- // Test mode never talks to the network telemetry off regardless of opt.
974
- disabled: this.testMode || opts.disableTelemetry
1091
+ // Off when the network is disabled; otherwise honour an explicit
1092
+ // disableTelemetry, else default to prod-on (off outside production).
1093
+ disabled: this.offline || (opts.disableTelemetry ?? !prod)
975
1094
  });
976
- if (this.testMode) {
1095
+ if (this.offline) {
977
1096
  this.evalResult = { flags: {}, configs: {}, experiments: {}, killswitches: {} };
978
1097
  } else {
979
1098
  void this.buffer.flushPendingAlias();
980
1099
  }
981
1100
  }
1101
+ /**
1102
+ * Consult the caller-supplied {@link AnonymousStore}: adopt a persisted id so
1103
+ * the visitor buckets identically across app launches, or persist the id just
1104
+ * minted by getOrCreateAnonId() on a fresh device. Best-effort — any store
1105
+ * failure leaves the in-memory id in place.
1106
+ */
1107
+ async hydrateAnonFromStore(store) {
1108
+ try {
1109
+ const stored = await store.get(ANON_ID_KEY);
1110
+ if (typeof stored === "string" && stored) {
1111
+ this.anonId = stored;
1112
+ } else {
1113
+ await store.set(ANON_ID_KEY, this.anonId);
1114
+ }
1115
+ } catch {
1116
+ }
1117
+ }
982
1118
  /**
983
1119
  * Build a no-network, immediately-usable browser client for tests
984
1120
  * (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
@@ -1001,11 +1137,15 @@ var Engine = class _Engine {
1001
1137
  });
1002
1138
  }
1003
1139
  async identify(user) {
1004
- if (this.testMode) {
1140
+ if (this.offline) {
1005
1141
  if (user.user_id !== void 0) this.userId = user.user_id;
1006
1142
  this.notify();
1007
1143
  return;
1008
1144
  }
1145
+ if (this.anonReady) {
1146
+ await this.anonReady;
1147
+ this.anonReady = null;
1148
+ }
1009
1149
  const seq = ++this.identifySeq;
1010
1150
  const prevUserId = this.userId;
1011
1151
  if (user.user_id !== void 0) this.userId = user.user_id;
@@ -1059,6 +1199,7 @@ var Engine = class _Engine {
1059
1199
  * the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
1060
1200
  */
1061
1201
  reportError(problem, consequence, extras, kind, correlationId) {
1202
+ if (this.offline) return;
1062
1203
  try {
1063
1204
  const enriched = { ...collectSeeEnv(), ...extras };
1064
1205
  const ev = buildSeeEvent(problem, consequence, enriched, {
@@ -1163,50 +1304,37 @@ var Engine = class _Engine {
1163
1304
  return void 0;
1164
1305
  }
1165
1306
  }
1166
- getExperiment(name, defaultParams, decodeOrOpts, variantsArg) {
1167
- const opts = typeof decodeOrOpts === "function" ? { decode: decodeOrOpts, variants: variantsArg } : decodeOrOpts ?? {};
1168
- const { decode, variants } = opts;
1307
+ /**
1308
+ * Assign the visitor within a universe: `universe("checkout").assign()`. A
1309
+ * universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
1310
+ * the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
1311
+ * and auto-logs one exposure when enrolled (unless `disableAutoExposure` or
1312
+ * `assign({ logExposure: false })`). An un-enrolled visitor still resolves
1313
+ * `get()` to the universe defaults. This is the sole experiment read path —
1314
+ * there is no `getExperiment` (ask a universe, not an experiment).
1315
+ */
1316
+ universe(name) {
1317
+ return { assign: (opts) => this.assignUniverse(name, opts) };
1318
+ }
1319
+ assignUniverse(name, opts) {
1169
1320
  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 };
1321
+ const defaults = this.evalResult?.universes?.[name]?.defaults ?? {};
1322
+ for (const [expName, pov] of this.experimentOverrides) {
1323
+ if (this.evalResult?.experiments[expName]?.universe !== name) continue;
1324
+ return new AssignmentImpl(expName, pov.group, { ...defaults, ...pov.params });
1180
1325
  }
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 };
1326
+ for (const [expName, entry] of Object.entries(this.evalResult?.experiments ?? {})) {
1327
+ if (entry.universe !== name) continue;
1328
+ const ov = readExpOverride(expName);
1329
+ if (ov !== null) return new AssignmentImpl(expName, ov, { ...defaults, ...entry.params });
1186
1330
  }
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;
1331
+ for (const [expName, entry] of Object.entries(this.evalResult?.experiments ?? {})) {
1332
+ if (entry.universe !== name || !entry.inExperiment) continue;
1333
+ const shouldLog = opts?.logExposure ?? !this.disableAutoExposure;
1334
+ if (shouldLog) this.buffer.pushExposure(expName, entry.group, this.userId, this.anonId);
1335
+ return new AssignmentImpl(expName, entry.group, entry.params);
1197
1336
  }
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);
1337
+ return new AssignmentImpl(null, null, defaults);
1210
1338
  }
1211
1339
  /**
1212
1340
  * Subscribe to state changes — fires after identify() completes and on
@@ -1215,7 +1343,7 @@ var Engine = class _Engine {
1215
1343
  */
1216
1344
  subscribe(listener) {
1217
1345
  this.listeners.add(listener);
1218
- if (!this.overrideListenerInstalled && typeof window !== "undefined") {
1346
+ if (!this.overrideListenerInstalled && hasDomEvents()) {
1219
1347
  this.overrideListenerInstalled = true;
1220
1348
  window.addEventListener("se:override:change", this.onOverrideChange);
1221
1349
  }
@@ -1231,9 +1359,11 @@ var Engine = class _Engine {
1231
1359
  if (typeof window === "undefined") return null;
1232
1360
  const bridge = {
1233
1361
  getFlag: (n) => this.getFlag(n),
1362
+ // Devtools reads assignments by experiment name straight off the eval
1363
+ // result (the public read path is universe-first; this is display-only).
1234
1364
  getExperiment: (n) => {
1235
- const r = this.getExperiment(n, {});
1236
- return { inExperiment: r.inExperiment, group: r.group };
1365
+ const entry = this.evalResult?.experiments[n];
1366
+ return { inExperiment: entry?.inExperiment ?? false, group: entry?.group ?? "control" };
1237
1367
  },
1238
1368
  getConfig: (n) => this.getConfig(n)
1239
1369
  };
@@ -1242,7 +1372,7 @@ var Engine = class _Engine {
1242
1372
  return bridge;
1243
1373
  }
1244
1374
  track(eventName, props) {
1245
- if (this.testMode) return;
1375
+ if (this.offline) return;
1246
1376
  this.buffer.pushMetric(eventName, this.userId, this.anonId, this.stripPrivate(props));
1247
1377
  }
1248
1378
  /** Drop caller-marked private attributes from an outbound props bag. */
@@ -1356,7 +1486,7 @@ function loadDevtools(opts = {}) {
1356
1486
  }
1357
1487
  }
1358
1488
  function attachDevtools(client, opts = {}) {
1359
- if (typeof window === "undefined") return () => {
1489
+ if (!hasDomEvents()) return () => {
1360
1490
  };
1361
1491
  const hotkey = opts.hotkey ?? "Shift+Alt+S";
1362
1492
  const parts = hotkey.split("+");
@@ -1395,14 +1525,18 @@ function shipeasy(opts) {
1395
1525
  const client = configureShipeasy({
1396
1526
  sdkKey: opts.clientKey,
1397
1527
  baseUrl,
1528
+ env: opts.env,
1398
1529
  logLevel: opts.logLevel,
1399
1530
  autoGuardrails: blanket,
1400
1531
  autoGuardrailGroups: groups,
1401
1532
  autoCollectAlways: acObj?.always === true,
1533
+ isNetworkEnabled: opts.isNetworkEnabled,
1402
1534
  disableTelemetry: opts.disableTelemetry,
1535
+ disableInternalErrorReporting: opts.disableInternalErrorReporting,
1403
1536
  disableAutoExposure: opts.disableAutoExposure,
1404
1537
  privateAttributes: opts.privateAttributes,
1405
- stickyBucketing: opts.stickyBucketing
1538
+ stickyBucketing: opts.stickyBucketing,
1539
+ anonymousStore: opts.anonymousStore
1406
1540
  });
1407
1541
  injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
1408
1542
  flags.notifyMounted();
@@ -1484,7 +1618,7 @@ var _mountedAndReady = false;
1484
1618
  var _standaloneListeners = /* @__PURE__ */ new Set();
1485
1619
  var _standaloneOverrideWired = false;
1486
1620
  function wireStandaloneOverride() {
1487
- if (_standaloneOverrideWired || typeof window === "undefined") return;
1621
+ if (_standaloneOverrideWired || !hasDomEvents()) return;
1488
1622
  _standaloneOverrideWired = true;
1489
1623
  window.addEventListener("se:override:change", () => {
1490
1624
  for (const cb of _standaloneListeners) cb();
@@ -1550,21 +1684,22 @@ var flags = {
1550
1684
  }
1551
1685
  });
1552
1686
  },
1553
- getExperiment(name, defaultParams, decodeOrOpts, variants) {
1554
- const fallback = {
1555
- inExperiment: false,
1556
- group: "control",
1557
- params: defaultParams
1687
+ /**
1688
+ * Assign the visitor within a universe: `flags.universe("checkout").assign()`.
1689
+ * A universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
1690
+ * the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
1691
+ * and auto-logs one exposure when enrolled. Before configure() (or on error)
1692
+ * returns a safe not-enrolled handle. Replaces the removed `getExperiment` —
1693
+ * read experiments by universe, never by name.
1694
+ */
1695
+ universe(name) {
1696
+ return {
1697
+ assign: (opts) => safeRun(
1698
+ "flags.universe.assign",
1699
+ new AssignmentImpl(null, null, {}),
1700
+ () => _client ? _client.universe(name).assign(opts) : new AssignmentImpl(null, null, {})
1701
+ )
1558
1702
  };
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
1703
  },
1569
1704
  track(eventName, props) {
1570
1705
  safeRun("flags.track", void 0, () => _client?.track(eventName, props));
@@ -1670,13 +1805,20 @@ var Client = class {
1670
1805
  () => this.engine.getConfig(name, decodeOrOpts)
1671
1806
  );
1672
1807
  }
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
- );
1808
+ /**
1809
+ * Assign the visitor within a universe: `client.universe("checkout").assign()`.
1810
+ * A universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
1811
+ * returns an {@link Assignment} (`.group` / `.get(field, fallback)`) and
1812
+ * auto-logs one exposure when enrolled. Replaces the removed `getExperiment`.
1813
+ */
1814
+ universe(name) {
1815
+ return {
1816
+ assign: (opts) => safeRun(
1817
+ "Client.universe.assign",
1818
+ new AssignmentImpl(null, null, {}),
1819
+ () => this.engine.universe(name).assign(opts)
1820
+ )
1821
+ };
1680
1822
  }
1681
1823
  /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
1682
1824
  getKillswitch(name, switchKey) {
@@ -1691,15 +1833,6 @@ var Client = class {
1691
1833
  track(eventName, props) {
1692
1834
  safeRun("Client.track", void 0, () => this.engine.track(eventName, props));
1693
1835
  }
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
1836
  };
1704
1837
  function dispatchSee(problem, consequence, extras, kind) {
1705
1838
  if (!_client) {
@@ -1925,6 +2058,7 @@ var i18n = {
1925
2058
  whenReady() {
1926
2059
  if (typeof window === "undefined") return Promise.resolve();
1927
2060
  if (window.i18n?.locale) return Promise.resolve();
2061
+ if (!hasDomEvents()) return Promise.resolve();
1928
2062
  return new Promise((resolve) => {
1929
2063
  const handler = () => resolve();
1930
2064
  window.addEventListener("se:i18n:ready", handler, { once: true });
@@ -1935,6 +2069,8 @@ var i18n = {
1935
2069
  if (typeof window === "undefined") return () => {
1936
2070
  };
1937
2071
  if (window.i18n) return window.i18n.on("update", cb);
2072
+ if (!hasDomEvents()) return () => {
2073
+ };
1938
2074
  let unsub = () => {
1939
2075
  };
1940
2076
  const handler = () => {