@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.
@@ -110,50 +110,21 @@ 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
- }
113
+ // src/env.ts
114
+ function isProductionEnv(configuredEnv) {
115
+ const native = readNativeEnv();
116
+ if (native !== null) return native === "production" || native === "prod";
117
+ return (configuredEnv ?? "prod").toLowerCase() === "prod";
127
118
  }
128
- function write(method, args) {
119
+ function readNativeEnv() {
129
120
  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);
121
+ const proc = globalThis.process;
122
+ const raw = proc?.env?.SHIPEASY_ENV ?? proc?.env?.NODE_ENV;
123
+ if (typeof raw !== "string") return null;
124
+ const v = raw.trim().toLowerCase();
125
+ return v.length ? v : null;
134
126
  } 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;
127
+ return null;
157
128
  }
158
129
  }
159
130
 
@@ -274,7 +245,7 @@ function captureCallsiteStack() {
274
245
  const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
275
246
  return kept.length ? kept.join("\n") : void 0;
276
247
  }
277
- function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlationId) {
248
+ function buildSeeEvent(problem, consequence, extras, ctx2, kindOverride, correlationId) {
278
249
  let errorType;
279
250
  let message;
280
251
  let stack;
@@ -302,8 +273,8 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
302
273
  message: truncate(message, SEE_MAX_MESSAGE),
303
274
  subject: consequence.subject,
304
275
  outcome: consequence.outcome,
305
- side: ctx.side,
306
- sdk_version: ctx.sdkVersion,
276
+ side: ctx2.side,
277
+ sdk_version: ctx2.sdkVersion,
307
278
  ts: Date.now()
308
279
  };
309
280
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
@@ -312,10 +283,10 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
312
283
  if (causedBy) ev.caused_by = causedBy;
313
284
  const cleanExtras = sanitizeExtras(extras);
314
285
  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;
286
+ if (ctx2.url) ev.url = truncate(ctx2.url, SEE_MAX_SUBJECT);
287
+ if (ctx2.userId) ev.user_id = ctx2.userId;
288
+ if (ctx2.anonId) ev.anonymous_id = ctx2.anonId;
289
+ if (ctx2.env) ev.env = ctx2.env;
319
290
  markReported(problem, ev);
320
291
  return ev;
321
292
  }
@@ -408,6 +379,98 @@ var SeeLimiter = class {
408
379
  }
409
380
  };
410
381
 
382
+ // src/internal-report.ts
383
+ var INGEST_URL = "https://api.shipeasy.ai/collect";
384
+ var PLACEHOLDER_KEY = "sdk_client_REPLACE_WITH_SHIPEASY_INTERNAL_ERROR_KEY";
385
+ var INGEST_KEY = "sdk_client_00bd4608a03e4084922978f9522614d5";
386
+ function keyConfigured() {
387
+ return !!INGEST_KEY && INGEST_KEY !== PLACEHOLDER_KEY;
388
+ }
389
+ var OUTCOME = "returned a safe default";
390
+ var SDK_ID = "ts";
391
+ var ctx = null;
392
+ var limiter = new SeeLimiter();
393
+ function setInternalReportContext(c) {
394
+ ctx = { side: c.side, sdkVersion: c.sdkVersion, enabled: c.enabled !== false };
395
+ }
396
+ function reportInternalError(label, err) {
397
+ try {
398
+ if (!ctx || !ctx.enabled || !keyConfigured()) return;
399
+ const ev = buildSeeEvent(
400
+ err,
401
+ causesThe(label).to(OUTCOME),
402
+ { sdk: SDK_ID },
403
+ { side: ctx.side, sdkVersion: ctx.sdkVersion },
404
+ "caught"
405
+ );
406
+ if (!limiter.shouldSend(ev)) return;
407
+ send2(INGEST_URL, INGEST_KEY, JSON.stringify({ events: [ev] }));
408
+ } catch {
409
+ }
410
+ }
411
+ function send2(url, key, body) {
412
+ try {
413
+ const f = globalThis.fetch;
414
+ if (typeof f !== "function") return;
415
+ void f(url, {
416
+ method: "POST",
417
+ headers: { "X-SDK-Key": key, "Content-Type": "text/plain" },
418
+ body,
419
+ keepalive: true
420
+ }).catch(() => {
421
+ });
422
+ } catch {
423
+ }
424
+ }
425
+
426
+ // src/logger.ts
427
+ var LOG_LEVELS = ["silent", "error", "warn", "info", "debug"];
428
+ var RANK = {
429
+ silent: 0,
430
+ error: 1,
431
+ warn: 2,
432
+ info: 3,
433
+ debug: 4
434
+ };
435
+ var currentLevel = "warn";
436
+ function setLogLevel(level) {
437
+ if (typeof level === "string" && Object.prototype.hasOwnProperty.call(RANK, level)) {
438
+ currentLevel = level;
439
+ }
440
+ }
441
+ function write(method, args) {
442
+ try {
443
+ const c = globalThis.console;
444
+ if (!c) return;
445
+ const fn = c[method] ?? c.log;
446
+ if (typeof fn === "function") fn.call(c, ...args);
447
+ } catch {
448
+ }
449
+ }
450
+ var logger = {
451
+ error(...args) {
452
+ if (RANK[currentLevel] >= RANK.error) write("error", args);
453
+ },
454
+ warn(...args) {
455
+ if (RANK[currentLevel] >= RANK.warn) write("warn", args);
456
+ },
457
+ info(...args) {
458
+ if (RANK[currentLevel] >= RANK.info) write("info", args);
459
+ },
460
+ debug(...args) {
461
+ if (RANK[currentLevel] >= RANK.debug) write("debug", args);
462
+ }
463
+ };
464
+ function safeRun(label, fallback, fn) {
465
+ try {
466
+ return fn();
467
+ } catch (err) {
468
+ logger.error(`[shipeasy] ${label} failed \u2014 returning safe default:`, String(err));
469
+ reportInternalError(label, err);
470
+ return fallback;
471
+ }
472
+ }
473
+
411
474
  // src/client/index.ts
412
475
  var version = "4.0.0";
413
476
  var FLAG_REASONS = [
@@ -418,6 +481,40 @@ var FLAG_REASONS = [
418
481
  "RULE_MATCH",
419
482
  "DEFAULT"
420
483
  ];
484
+ var AssignmentImpl = class {
485
+ constructor(name, group, params) {
486
+ this.name = name;
487
+ this.group = group;
488
+ this.params = params;
489
+ }
490
+ name;
491
+ group;
492
+ params;
493
+ get enrolled() {
494
+ return this.group !== null;
495
+ }
496
+ get(field, fallback) {
497
+ const v = this.params[field];
498
+ return v === void 0 ? fallback : v;
499
+ }
500
+ };
501
+ function hasDomEvents() {
502
+ return typeof window !== "undefined" && typeof window.addEventListener === "function";
503
+ }
504
+ function onWindow(type, handler, opts) {
505
+ try {
506
+ if (hasDomEvents()) window.addEventListener(type, handler, opts);
507
+ } catch {
508
+ }
509
+ }
510
+ function onDocument(type, handler, opts) {
511
+ try {
512
+ if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
513
+ document.addEventListener(type, handler, opts);
514
+ }
515
+ } catch {
516
+ }
517
+ }
421
518
  var FLUSH_INTERVAL_MS = 5e3;
422
519
  var MAX_BUFFER = 100;
423
520
  var ANON_ID_KEY = "__se_anon_id";
@@ -429,8 +526,8 @@ var EventBuffer = class {
429
526
  this.sdkKey = sdkKey;
430
527
  if (typeof window !== "undefined") {
431
528
  this.timer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
432
- window.addEventListener("beforeunload", () => this.flush());
433
- document.addEventListener("visibilitychange", () => {
529
+ onWindow("beforeunload", () => this.flush());
530
+ onDocument("visibilitychange", () => {
434
531
  if (document.visibilityState === "hidden") this.flush(true);
435
532
  });
436
533
  try {
@@ -975,6 +1072,10 @@ var Engine = class _Engine {
975
1072
  env;
976
1073
  evalResult = null;
977
1074
  anonId;
1075
+ // Resolves once a caller-supplied anonymousStore has been consulted (persisted
1076
+ // id adopted, or the minted id written back). identify() awaits it so the
1077
+ // stable id is settled before the first /sdk/evaluate. null when no store.
1078
+ anonReady = null;
978
1079
  userId = "";
979
1080
  buffer;
980
1081
  telemetry;
@@ -989,6 +1090,12 @@ var Engine = class _Engine {
989
1090
  // never fetches, track() is a no-op, telemetry is off, and the client is
990
1091
  // already ready with an empty eval result.
991
1092
  testMode;
1093
+ // Master network gate — false ⇒ the SDK never touches the network. Defaults to
1094
+ // environment-derived (prod-on), forced false by testMode.
1095
+ networkEnabled;
1096
+ // Convenience inverse of networkEnabled — the "no network at all" state every
1097
+ // /sdk/evaluate, /collect and telemetry gate keys on. testMode implies offline.
1098
+ offline;
992
1099
  // Programmatic overrides (Statsig-style). Set on any client via
993
1100
  // overrideFlag/overrideConfig/overrideExperiment; they win over BOTH the URL
994
1101
  // overrides and the fetched eval result. Cleared by clearOverrides().
@@ -1002,9 +1109,17 @@ var Engine = class _Engine {
1002
1109
  constructor(opts) {
1003
1110
  setLogLevel(opts.logLevel);
1004
1111
  this.sdkKey = opts.sdkKey;
1005
- this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
1112
+ this.baseUrl = (opts.baseUrl ?? "https://api.shipeasy.ai").replace(/\/$/, "");
1006
1113
  this.env = opts.env ?? "prod";
1007
1114
  this.testMode = opts.testMode === true;
1115
+ const prod = isProductionEnv(this.env);
1116
+ this.networkEnabled = this.testMode ? false : opts.isNetworkEnabled ?? prod;
1117
+ this.offline = !this.networkEnabled;
1118
+ setInternalReportContext({
1119
+ side: "client",
1120
+ sdkVersion: version,
1121
+ enabled: this.networkEnabled && opts.disableInternalErrorReporting !== true
1122
+ });
1008
1123
  this.autoGuardrails = opts.autoGuardrails !== false;
1009
1124
  this.autoCollectAlways = opts.autoCollectAlways === true;
1010
1125
  this.disableAutoExposure = opts.disableAutoExposure === true;
@@ -1017,21 +1132,42 @@ var Engine = class _Engine {
1017
1132
  engagement: g.engagement ?? this.autoGuardrails
1018
1133
  };
1019
1134
  this.anonId = getOrCreateAnonId();
1135
+ if (opts.anonymousStore && !this.offline) {
1136
+ this.anonReady = this.hydrateAnonFromStore(opts.anonymousStore);
1137
+ }
1020
1138
  this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
1021
1139
  this.telemetry = new Telemetry({
1022
1140
  endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
1023
1141
  sdkKey: this.sdkKey,
1024
1142
  side: "client",
1025
1143
  env: this.env,
1026
- // Test mode never talks to the network telemetry off regardless of opt.
1027
- disabled: this.testMode || opts.disableTelemetry
1144
+ // Off when the network is disabled; otherwise honour an explicit
1145
+ // disableTelemetry, else default to prod-on (off outside production).
1146
+ disabled: this.offline || (opts.disableTelemetry ?? !prod)
1028
1147
  });
1029
- if (this.testMode) {
1148
+ if (this.offline) {
1030
1149
  this.evalResult = { flags: {}, configs: {}, experiments: {}, killswitches: {} };
1031
1150
  } else {
1032
1151
  void this.buffer.flushPendingAlias();
1033
1152
  }
1034
1153
  }
1154
+ /**
1155
+ * Consult the caller-supplied {@link AnonymousStore}: adopt a persisted id so
1156
+ * the visitor buckets identically across app launches, or persist the id just
1157
+ * minted by getOrCreateAnonId() on a fresh device. Best-effort — any store
1158
+ * failure leaves the in-memory id in place.
1159
+ */
1160
+ async hydrateAnonFromStore(store) {
1161
+ try {
1162
+ const stored = await store.get(ANON_ID_KEY);
1163
+ if (typeof stored === "string" && stored) {
1164
+ this.anonId = stored;
1165
+ } else {
1166
+ await store.set(ANON_ID_KEY, this.anonId);
1167
+ }
1168
+ } catch {
1169
+ }
1170
+ }
1035
1171
  /**
1036
1172
  * Build a no-network, immediately-usable browser client for tests
1037
1173
  * (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
@@ -1054,11 +1190,15 @@ var Engine = class _Engine {
1054
1190
  });
1055
1191
  }
1056
1192
  async identify(user) {
1057
- if (this.testMode) {
1193
+ if (this.offline) {
1058
1194
  if (user.user_id !== void 0) this.userId = user.user_id;
1059
1195
  this.notify();
1060
1196
  return;
1061
1197
  }
1198
+ if (this.anonReady) {
1199
+ await this.anonReady;
1200
+ this.anonReady = null;
1201
+ }
1062
1202
  const seq = ++this.identifySeq;
1063
1203
  const prevUserId = this.userId;
1064
1204
  if (user.user_id !== void 0) this.userId = user.user_id;
@@ -1112,6 +1252,7 @@ var Engine = class _Engine {
1112
1252
  * the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
1113
1253
  */
1114
1254
  reportError(problem, consequence, extras, kind, correlationId) {
1255
+ if (this.offline) return;
1115
1256
  try {
1116
1257
  const enriched = { ...collectSeeEnv(), ...extras };
1117
1258
  const ev = buildSeeEvent(problem, consequence, enriched, {
@@ -1216,50 +1357,37 @@ var Engine = class _Engine {
1216
1357
  return void 0;
1217
1358
  }
1218
1359
  }
1219
- getExperiment(name, defaultParams, decodeOrOpts, variantsArg) {
1220
- const opts = typeof decodeOrOpts === "function" ? { decode: decodeOrOpts, variants: variantsArg } : decodeOrOpts ?? {};
1221
- const { decode, variants } = opts;
1360
+ /**
1361
+ * Assign the visitor within a universe: `universe("checkout").assign()`. A
1362
+ * universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
1363
+ * the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
1364
+ * and auto-logs one exposure when enrolled (unless `disableAutoExposure` or
1365
+ * `assign({ logExposure: false })`). An un-enrolled visitor still resolves
1366
+ * `get()` to the universe defaults. This is the sole experiment read path —
1367
+ * there is no `getExperiment` (ask a universe, not an experiment).
1368
+ */
1369
+ universe(name) {
1370
+ return { assign: (opts) => this.assignUniverse(name, opts) };
1371
+ }
1372
+ assignUniverse(name, opts) {
1222
1373
  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 };
1374
+ const defaults = this.evalResult?.universes?.[name]?.defaults ?? {};
1375
+ for (const [expName, pov] of this.experimentOverrides) {
1376
+ if (this.evalResult?.experiments[expName]?.universe !== name) continue;
1377
+ return new AssignmentImpl(expName, pov.group, { ...defaults, ...pov.params });
1233
1378
  }
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 };
1379
+ for (const [expName, entry] of Object.entries(this.evalResult?.experiments ?? {})) {
1380
+ if (entry.universe !== name) continue;
1381
+ const ov = readExpOverride(expName);
1382
+ if (ov !== null) return new AssignmentImpl(expName, ov, { ...defaults, ...entry.params });
1239
1383
  }
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;
1384
+ for (const [expName, entry] of Object.entries(this.evalResult?.experiments ?? {})) {
1385
+ if (entry.universe !== name || !entry.inExperiment) continue;
1386
+ const shouldLog = opts?.logExposure ?? !this.disableAutoExposure;
1387
+ if (shouldLog) this.buffer.pushExposure(expName, entry.group, this.userId, this.anonId);
1388
+ return new AssignmentImpl(expName, entry.group, entry.params);
1250
1389
  }
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);
1390
+ return new AssignmentImpl(null, null, defaults);
1263
1391
  }
1264
1392
  /**
1265
1393
  * Subscribe to state changes — fires after identify() completes and on
@@ -1268,7 +1396,7 @@ var Engine = class _Engine {
1268
1396
  */
1269
1397
  subscribe(listener) {
1270
1398
  this.listeners.add(listener);
1271
- if (!this.overrideListenerInstalled && typeof window !== "undefined") {
1399
+ if (!this.overrideListenerInstalled && hasDomEvents()) {
1272
1400
  this.overrideListenerInstalled = true;
1273
1401
  window.addEventListener("se:override:change", this.onOverrideChange);
1274
1402
  }
@@ -1284,9 +1412,11 @@ var Engine = class _Engine {
1284
1412
  if (typeof window === "undefined") return null;
1285
1413
  const bridge = {
1286
1414
  getFlag: (n) => this.getFlag(n),
1415
+ // Devtools reads assignments by experiment name straight off the eval
1416
+ // result (the public read path is universe-first; this is display-only).
1287
1417
  getExperiment: (n) => {
1288
- const r = this.getExperiment(n, {});
1289
- return { inExperiment: r.inExperiment, group: r.group };
1418
+ const entry = this.evalResult?.experiments[n];
1419
+ return { inExperiment: entry?.inExperiment ?? false, group: entry?.group ?? "control" };
1290
1420
  },
1291
1421
  getConfig: (n) => this.getConfig(n)
1292
1422
  };
@@ -1295,7 +1425,7 @@ var Engine = class _Engine {
1295
1425
  return bridge;
1296
1426
  }
1297
1427
  track(eventName, props) {
1298
- if (this.testMode) return;
1428
+ if (this.offline) return;
1299
1429
  this.buffer.pushMetric(eventName, this.userId, this.anonId, this.stripPrivate(props));
1300
1430
  }
1301
1431
  /** Drop caller-marked private attributes from an outbound props bag. */
@@ -1409,7 +1539,7 @@ function loadDevtools(opts = {}) {
1409
1539
  }
1410
1540
  }
1411
1541
  function attachDevtools(client, opts = {}) {
1412
- if (typeof window === "undefined") return () => {
1542
+ if (!hasDomEvents()) return () => {
1413
1543
  };
1414
1544
  const hotkey = opts.hotkey ?? "Shift+Alt+S";
1415
1545
  const parts = hotkey.split("+");
@@ -1448,14 +1578,18 @@ function shipeasy(opts) {
1448
1578
  const client = configureShipeasy({
1449
1579
  sdkKey: opts.clientKey,
1450
1580
  baseUrl,
1581
+ env: opts.env,
1451
1582
  logLevel: opts.logLevel,
1452
1583
  autoGuardrails: blanket,
1453
1584
  autoGuardrailGroups: groups,
1454
1585
  autoCollectAlways: acObj?.always === true,
1586
+ isNetworkEnabled: opts.isNetworkEnabled,
1455
1587
  disableTelemetry: opts.disableTelemetry,
1588
+ disableInternalErrorReporting: opts.disableInternalErrorReporting,
1456
1589
  disableAutoExposure: opts.disableAutoExposure,
1457
1590
  privateAttributes: opts.privateAttributes,
1458
- stickyBucketing: opts.stickyBucketing
1591
+ stickyBucketing: opts.stickyBucketing,
1592
+ anonymousStore: opts.anonymousStore
1459
1593
  });
1460
1594
  injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
1461
1595
  flags.notifyMounted();
@@ -1537,7 +1671,7 @@ var _mountedAndReady = false;
1537
1671
  var _standaloneListeners = /* @__PURE__ */ new Set();
1538
1672
  var _standaloneOverrideWired = false;
1539
1673
  function wireStandaloneOverride() {
1540
- if (_standaloneOverrideWired || typeof window === "undefined") return;
1674
+ if (_standaloneOverrideWired || !hasDomEvents()) return;
1541
1675
  _standaloneOverrideWired = true;
1542
1676
  window.addEventListener("se:override:change", () => {
1543
1677
  for (const cb of _standaloneListeners) cb();
@@ -1603,21 +1737,22 @@ var flags = {
1603
1737
  }
1604
1738
  });
1605
1739
  },
1606
- getExperiment(name, defaultParams, decodeOrOpts, variants) {
1607
- const fallback = {
1608
- inExperiment: false,
1609
- group: "control",
1610
- params: defaultParams
1740
+ /**
1741
+ * Assign the visitor within a universe: `flags.universe("checkout").assign()`.
1742
+ * A universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
1743
+ * the returned {@link Assignment} exposes `.group` / `.get(field, fallback)`
1744
+ * and auto-logs one exposure when enrolled. Before configure() (or on error)
1745
+ * returns a safe not-enrolled handle. Replaces the removed `getExperiment` —
1746
+ * read experiments by universe, never by name.
1747
+ */
1748
+ universe(name) {
1749
+ return {
1750
+ assign: (opts) => safeRun(
1751
+ "flags.universe.assign",
1752
+ new AssignmentImpl(null, null, {}),
1753
+ () => _client ? _client.universe(name).assign(opts) : new AssignmentImpl(null, null, {})
1754
+ )
1611
1755
  };
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
1756
  },
1622
1757
  track(eventName, props) {
1623
1758
  safeRun("flags.track", void 0, () => _client?.track(eventName, props));
@@ -1723,13 +1858,20 @@ var Client = class {
1723
1858
  () => this.engine.getConfig(name, decodeOrOpts)
1724
1859
  );
1725
1860
  }
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
- );
1861
+ /**
1862
+ * Assign the visitor within a universe: `client.universe("checkout").assign()`.
1863
+ * A universe is a mutual-exclusion pool, so the visitor lands in ≤1 experiment;
1864
+ * returns an {@link Assignment} (`.group` / `.get(field, fallback)`) and
1865
+ * auto-logs one exposure when enrolled. Replaces the removed `getExperiment`.
1866
+ */
1867
+ universe(name) {
1868
+ return {
1869
+ assign: (opts) => safeRun(
1870
+ "Client.universe.assign",
1871
+ new AssignmentImpl(null, null, {}),
1872
+ () => this.engine.universe(name).assign(opts)
1873
+ )
1874
+ };
1733
1875
  }
1734
1876
  /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
1735
1877
  getKillswitch(name, switchKey) {
@@ -1744,15 +1886,6 @@ var Client = class {
1744
1886
  track(eventName, props) {
1745
1887
  safeRun("Client.track", void 0, () => this.engine.track(eventName, props));
1746
1888
  }
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
1889
  };
1757
1890
  function dispatchSee(problem, consequence, extras, kind) {
1758
1891
  if (!_client) {
@@ -1978,6 +2111,7 @@ var i18n = {
1978
2111
  whenReady() {
1979
2112
  if (typeof window === "undefined") return Promise.resolve();
1980
2113
  if (window.i18n?.locale) return Promise.resolve();
2114
+ if (!hasDomEvents()) return Promise.resolve();
1981
2115
  return new Promise((resolve) => {
1982
2116
  const handler = () => resolve();
1983
2117
  window.addEventListener("se:i18n:ready", handler, { once: true });
@@ -1988,6 +2122,8 @@ var i18n = {
1988
2122
  if (typeof window === "undefined") return () => {
1989
2123
  };
1990
2124
  if (window.i18n) return window.i18n.on("update", cb);
2125
+ if (!hasDomEvents()) return () => {
2126
+ };
1991
2127
  let unsub = () => {
1992
2128
  };
1993
2129
  const handler = () => {