@shipeasy/sdk 6.2.0 → 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.
@@ -27,6 +27,7 @@ __export(client_exports, {
27
27
  LABEL_MARKER_RE: () => LABEL_MARKER_RE,
28
28
  LABEL_MARKER_SEP: () => LABEL_MARKER_SEP,
29
29
  LABEL_MARKER_START: () => LABEL_MARKER_START,
30
+ LOG_LEVELS: () => LOG_LEVELS,
30
31
  _resetConfigureForTests: () => _resetConfigureForTests,
31
32
  _resetShipeasyForTests: () => _resetShipeasyForTests,
32
33
  attachDevtools: () => attachDevtools,
@@ -226,7 +227,7 @@ function captureCallsiteStack() {
226
227
  const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
227
228
  return kept.length ? kept.join("\n") : void 0;
228
229
  }
229
- function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlationId) {
230
+ function buildSeeEvent(problem, consequence, extras, ctx2, kindOverride, correlationId) {
230
231
  let errorType;
231
232
  let message;
232
233
  let stack;
@@ -254,8 +255,8 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
254
255
  message: truncate(message, SEE_MAX_MESSAGE),
255
256
  subject: consequence.subject,
256
257
  outcome: consequence.outcome,
257
- side: ctx.side,
258
- sdk_version: ctx.sdkVersion,
258
+ side: ctx2.side,
259
+ sdk_version: ctx2.sdkVersion,
259
260
  ts: Date.now()
260
261
  };
261
262
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
@@ -264,10 +265,10 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
264
265
  if (causedBy) ev.caused_by = causedBy;
265
266
  const cleanExtras = sanitizeExtras(extras);
266
267
  if (cleanExtras) ev.extras = cleanExtras;
267
- if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
268
- if (ctx.userId) ev.user_id = ctx.userId;
269
- if (ctx.anonId) ev.anonymous_id = ctx.anonId;
270
- 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;
271
272
  markReported(problem, ev);
272
273
  return ev;
273
274
  }
@@ -360,6 +361,98 @@ var SeeLimiter = class {
360
361
  }
361
362
  };
362
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
+
363
456
  // src/client/index.ts
364
457
  var version = "4.0.0";
365
458
  var FLAG_REASONS = [
@@ -370,6 +463,40 @@ var FLAG_REASONS = [
370
463
  "RULE_MATCH",
371
464
  "DEFAULT"
372
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
+ }
373
500
  var FLUSH_INTERVAL_MS = 5e3;
374
501
  var MAX_BUFFER = 100;
375
502
  var ANON_ID_KEY = "__se_anon_id";
@@ -381,8 +508,8 @@ var EventBuffer = class {
381
508
  this.sdkKey = sdkKey;
382
509
  if (typeof window !== "undefined") {
383
510
  this.timer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
384
- window.addEventListener("beforeunload", () => this.flush());
385
- document.addEventListener("visibilitychange", () => {
511
+ onWindow("beforeunload", () => this.flush());
512
+ onDocument("visibilitychange", () => {
386
513
  if (document.visibilityState === "hidden") this.flush(true);
387
514
  });
388
515
  try {
@@ -927,6 +1054,10 @@ var Engine = class _Engine {
927
1054
  env;
928
1055
  evalResult = null;
929
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;
930
1061
  userId = "";
931
1062
  buffer;
932
1063
  telemetry;
@@ -952,10 +1083,16 @@ var Engine = class _Engine {
952
1083
  this.notify();
953
1084
  };
954
1085
  constructor(opts) {
1086
+ setLogLevel(opts.logLevel);
955
1087
  this.sdkKey = opts.sdkKey;
956
- this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
1088
+ this.baseUrl = (opts.baseUrl ?? "https://api.shipeasy.ai").replace(/\/$/, "");
957
1089
  this.env = opts.env ?? "prod";
958
1090
  this.testMode = opts.testMode === true;
1091
+ setInternalReportContext({
1092
+ side: "client",
1093
+ sdkVersion: version,
1094
+ enabled: !this.testMode && opts.disableInternalErrorReporting !== true
1095
+ });
959
1096
  this.autoGuardrails = opts.autoGuardrails !== false;
960
1097
  this.autoCollectAlways = opts.autoCollectAlways === true;
961
1098
  this.disableAutoExposure = opts.disableAutoExposure === true;
@@ -968,6 +1105,9 @@ var Engine = class _Engine {
968
1105
  engagement: g.engagement ?? this.autoGuardrails
969
1106
  };
970
1107
  this.anonId = getOrCreateAnonId();
1108
+ if (opts.anonymousStore && !this.testMode) {
1109
+ this.anonReady = this.hydrateAnonFromStore(opts.anonymousStore);
1110
+ }
971
1111
  this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
972
1112
  this.telemetry = new Telemetry({
973
1113
  endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
@@ -983,6 +1123,23 @@ var Engine = class _Engine {
983
1123
  void this.buffer.flushPendingAlias();
984
1124
  }
985
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
+ }
986
1143
  /**
987
1144
  * Build a no-network, immediately-usable browser client for tests
988
1145
  * (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
@@ -1010,6 +1167,10 @@ var Engine = class _Engine {
1010
1167
  this.notify();
1011
1168
  return;
1012
1169
  }
1170
+ if (this.anonReady) {
1171
+ await this.anonReady;
1172
+ this.anonReady = null;
1173
+ }
1013
1174
  const seq = ++this.identifySeq;
1014
1175
  const prevUserId = this.userId;
1015
1176
  if (user.user_id !== void 0) this.userId = user.user_id;
@@ -1086,7 +1247,7 @@ var Engine = class _Engine {
1086
1247
  try {
1087
1248
  l();
1088
1249
  } catch (err) {
1089
- console.warn("[shipeasy] subscriber threw:", String(err));
1250
+ logger.warn("[shipeasy] subscriber threw:", String(err));
1090
1251
  }
1091
1252
  }
1092
1253
  }
@@ -1163,54 +1324,41 @@ var Engine = class _Engine {
1163
1324
  try {
1164
1325
  return opts.decode(raw);
1165
1326
  } catch (err) {
1166
- console.warn(`[shipeasy] getConfig('${name}') decode failed:`, String(err));
1327
+ logger.warn(`[shipeasy] getConfig('${name}') decode failed:`, String(err));
1167
1328
  return void 0;
1168
1329
  }
1169
1330
  }
1170
- getExperiment(name, defaultParams, decodeOrOpts, variantsArg) {
1171
- const opts = typeof decodeOrOpts === "function" ? { decode: decodeOrOpts, variants: variantsArg } : decodeOrOpts ?? {};
1172
- 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) {
1173
1344
  this.telemetry.emit("experiment", name);
1174
- const notIn = {
1175
- inExperiment: false,
1176
- group: "control",
1177
- params: defaultParams
1178
- };
1179
- const pov = this.experimentOverrides.get(name);
1180
- if (pov) {
1181
- const variantParams = variants?.[pov.group];
1182
- const params = { ...defaultParams, ...pov.params, ...variantParams ?? {} };
1183
- 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 });
1184
1349
  }
1185
- const ov = readExpOverride(name);
1186
- if (ov !== null) {
1187
- const variantParams = variants?.[ov];
1188
- const params = variantParams ? { ...defaultParams, ...variantParams } : defaultParams;
1189
- 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 });
1190
1354
  }
1191
- const entry = this.evalResult?.experiments[name];
1192
- if (!entry || !entry.inExperiment) return notIn;
1193
- const shouldLog = opts.logExposure ?? !this.disableAutoExposure;
1194
- if (shouldLog) this.buffer.pushExposure(name, entry.group, this.userId, this.anonId);
1195
- if (!decode) return { inExperiment: true, group: entry.group, params: entry.params };
1196
- try {
1197
- return { inExperiment: true, group: entry.group, params: decode(entry.params) };
1198
- } catch (err) {
1199
- console.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
1200
- 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);
1201
1360
  }
1202
- }
1203
- /**
1204
- * Manually log an exposure for an enrolled experiment (Statsig's
1205
- * `manuallyLogExposure`). Reads the cached eval result; if the visitor is in
1206
- * the experiment, pushes the session-deduped exposure. Pair this with the
1207
- * render of the treatment when reading with `{ logExposure: false }` (or
1208
- * `disableAutoExposure: true`). No-op if the visitor isn't enrolled.
1209
- */
1210
- logExposure(name) {
1211
- const entry = this.evalResult?.experiments[name];
1212
- if (!entry || !entry.inExperiment) return;
1213
- this.buffer.pushExposure(name, entry.group, this.userId, this.anonId);
1361
+ return new AssignmentImpl(null, null, defaults);
1214
1362
  }
1215
1363
  /**
1216
1364
  * Subscribe to state changes — fires after identify() completes and on
@@ -1219,7 +1367,7 @@ var Engine = class _Engine {
1219
1367
  */
1220
1368
  subscribe(listener) {
1221
1369
  this.listeners.add(listener);
1222
- if (!this.overrideListenerInstalled && typeof window !== "undefined") {
1370
+ if (!this.overrideListenerInstalled && hasDomEvents()) {
1223
1371
  this.overrideListenerInstalled = true;
1224
1372
  window.addEventListener("se:override:change", this.onOverrideChange);
1225
1373
  }
@@ -1235,9 +1383,11 @@ var Engine = class _Engine {
1235
1383
  if (typeof window === "undefined") return null;
1236
1384
  const bridge = {
1237
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).
1238
1388
  getExperiment: (n) => {
1239
- const r = this.getExperiment(n, {});
1240
- return { inExperiment: r.inExperiment, group: r.group };
1389
+ const entry = this.evalResult?.experiments[n];
1390
+ return { inExperiment: entry?.inExperiment ?? false, group: entry?.group ?? "control" };
1241
1391
  },
1242
1392
  getConfig: (n) => this.getConfig(n)
1243
1393
  };
@@ -1360,7 +1510,7 @@ function loadDevtools(opts = {}) {
1360
1510
  }
1361
1511
  }
1362
1512
  function attachDevtools(client, opts = {}) {
1363
- if (typeof window === "undefined") return () => {
1513
+ if (!hasDomEvents()) return () => {
1364
1514
  };
1365
1515
  const hotkey = opts.hotkey ?? "Shift+Alt+S";
1366
1516
  const parts = hotkey.split("+");
@@ -1399,19 +1549,22 @@ function shipeasy(opts) {
1399
1549
  const client = configureShipeasy({
1400
1550
  sdkKey: opts.clientKey,
1401
1551
  baseUrl,
1552
+ logLevel: opts.logLevel,
1402
1553
  autoGuardrails: blanket,
1403
1554
  autoGuardrailGroups: groups,
1404
1555
  autoCollectAlways: acObj?.always === true,
1405
1556
  disableTelemetry: opts.disableTelemetry,
1557
+ disableInternalErrorReporting: opts.disableInternalErrorReporting,
1406
1558
  disableAutoExposure: opts.disableAutoExposure,
1407
1559
  privateAttributes: opts.privateAttributes,
1408
- stickyBucketing: opts.stickyBucketing
1560
+ stickyBucketing: opts.stickyBucketing,
1561
+ anonymousStore: opts.anonymousStore
1409
1562
  });
1410
1563
  injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
1411
1564
  flags.notifyMounted();
1412
1565
  if (opts.autoIdentify !== false) {
1413
1566
  void client.identify({}).catch((err) => {
1414
- console.warn("[shipeasy] auto-identify failed:", String(err));
1567
+ logger.warn("[shipeasy] auto-identify failed:", String(err));
1415
1568
  });
1416
1569
  }
1417
1570
  return attachDevtools(client, { adminUrl: opts.adminUrl });
@@ -1487,7 +1640,7 @@ var _mountedAndReady = false;
1487
1640
  var _standaloneListeners = /* @__PURE__ */ new Set();
1488
1641
  var _standaloneOverrideWired = false;
1489
1642
  function wireStandaloneOverride() {
1490
- if (_standaloneOverrideWired || typeof window === "undefined") return;
1643
+ if (_standaloneOverrideWired || !hasDomEvents()) return;
1491
1644
  _standaloneOverrideWired = true;
1492
1645
  window.addEventListener("se:override:change", () => {
1493
1646
  for (const cb of _standaloneListeners) cb();
@@ -1499,7 +1652,7 @@ var flags = {
1499
1652
  },
1500
1653
  identify(user) {
1501
1654
  if (!_client) {
1502
- console.warn("[shipeasy] flags.identify called before configureShipeasy()");
1655
+ logger.warn("[shipeasy] flags.identify called before configureShipeasy()");
1503
1656
  return Promise.resolve();
1504
1657
  }
1505
1658
  return _client.identify(user);
@@ -1512,57 +1665,66 @@ var flags = {
1512
1665
  * force-static pages where SSR has no flag data.
1513
1666
  */
1514
1667
  get(name, defaultValue = false) {
1515
- const bs = getBootstrap();
1516
- if (bs !== null && name in bs.flags) return bs.flags[name];
1517
- if (!_mountedAndReady) return defaultValue;
1518
- if (_client) return _client.getFlag(name, defaultValue);
1519
- return readGateOverride(name) ?? defaultValue;
1668
+ return safeRun("flags.get", defaultValue, () => {
1669
+ const bs = getBootstrap();
1670
+ if (bs !== null && name in bs.flags) return bs.flags[name];
1671
+ if (!_mountedAndReady) return defaultValue;
1672
+ if (_client) return _client.getFlag(name, defaultValue);
1673
+ return readGateOverride(name) ?? defaultValue;
1674
+ });
1520
1675
  },
1521
1676
  /** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
1522
1677
  getDetail(name) {
1523
- if (_client) return _client.getFlagDetail(name);
1524
- const ov = readGateOverride(name);
1525
- if (ov !== null) return { value: ov, reason: "OVERRIDE" };
1526
- return { value: false, reason: "CLIENT_NOT_READY" };
1678
+ return safeRun("flags.getDetail", { value: false, reason: "CLIENT_NOT_READY" }, () => {
1679
+ if (_client) return _client.getFlagDetail(name);
1680
+ const ov = readGateOverride(name);
1681
+ if (ov !== null) return { value: ov, reason: "OVERRIDE" };
1682
+ return { value: false, reason: "CLIENT_NOT_READY" };
1683
+ });
1527
1684
  },
1528
1685
  getConfig(name, decode) {
1529
- const bs = getBootstrap();
1530
- if (bs !== null && name in bs.configs) {
1531
- const raw = bs.configs[name];
1532
- if (!decode) return raw;
1686
+ return safeRun("flags.getConfig", void 0, () => {
1687
+ const bs = getBootstrap();
1688
+ if (bs !== null && name in bs.configs) {
1689
+ const raw = bs.configs[name];
1690
+ if (!decode) return raw;
1691
+ try {
1692
+ return decode(raw);
1693
+ } catch {
1694
+ return void 0;
1695
+ }
1696
+ }
1697
+ if (!_mountedAndReady) return void 0;
1698
+ if (_client) return _client.getConfig(name, decode);
1699
+ const ov = readConfigOverride(name);
1700
+ if (ov === void 0) return void 0;
1701
+ if (!decode) return ov;
1533
1702
  try {
1534
- return decode(raw);
1703
+ return decode(ov);
1535
1704
  } catch {
1536
1705
  return void 0;
1537
1706
  }
1538
- }
1539
- if (!_mountedAndReady) return void 0;
1540
- if (_client) return _client.getConfig(name, decode);
1541
- const ov = readConfigOverride(name);
1542
- if (ov === void 0) return void 0;
1543
- if (!decode) return ov;
1544
- try {
1545
- return decode(ov);
1546
- } catch {
1547
- return void 0;
1548
- }
1707
+ });
1549
1708
  },
1550
- getExperiment(name, defaultParams, decodeOrOpts, variants) {
1551
- const fallback = {
1552
- inExperiment: false,
1553
- group: "control",
1554
- 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
+ )
1555
1724
  };
1556
- if (!_client) return fallback;
1557
- return typeof decodeOrOpts === "function" ? _client.getExperiment(name, defaultParams, decodeOrOpts, variants) : _client.getExperiment(name, defaultParams, decodeOrOpts ?? {});
1558
- },
1559
- /** Manually log an exposure for an enrolled experiment. See
1560
- * {@link Engine.logExposure}. No-op before configure(). */
1561
- logExposure(name) {
1562
- _client?.logExposure(name);
1563
1725
  },
1564
1726
  track(eventName, props) {
1565
- _client?.track(eventName, props);
1727
+ safeRun("flags.track", void 0, () => _client?.track(eventName, props));
1566
1728
  },
1567
1729
  /**
1568
1730
  * Read a killswitch. Without `switchKey`, returns true when the killswitch is
@@ -1574,15 +1736,17 @@ var flags = {
1574
1736
  * available synchronously on first render.
1575
1737
  */
1576
1738
  ks(name, switchKey) {
1577
- const bs = getBootstrap();
1578
- if (bs !== null && bs.killswitches && name in bs.killswitches) {
1579
- const ks = bs.killswitches[name];
1580
- if (typeof ks === "boolean") return switchKey === void 0 ? ks : false;
1581
- if (switchKey === void 0) return false;
1582
- return ks[switchKey] === true;
1583
- }
1584
- if (!_mountedAndReady) return false;
1585
- return _client?.getKillswitch(name, switchKey) ?? false;
1739
+ return safeRun("flags.ks", false, () => {
1740
+ const bs = getBootstrap();
1741
+ if (bs !== null && bs.killswitches && name in bs.killswitches) {
1742
+ const ks = bs.killswitches[name];
1743
+ if (typeof ks === "boolean") return switchKey === void 0 ? ks : false;
1744
+ if (switchKey === void 0) return false;
1745
+ return ks[switchKey] === true;
1746
+ }
1747
+ if (!_mountedAndReady) return false;
1748
+ return _client?.getKillswitch(name, switchKey) ?? false;
1749
+ });
1586
1750
  },
1587
1751
  flush() {
1588
1752
  return _client?.flush() ?? Promise.resolve();
@@ -1639,7 +1803,7 @@ var Client = class {
1639
1803
  this.engine = engine;
1640
1804
  this.attributes = _attributes(user);
1641
1805
  this._identify = this.engine.identify(this.attributes).catch((err) => {
1642
- console.warn("[shipeasy] Client identify failed:", String(err));
1806
+ logger.warn("[shipeasy] Client identify failed:", String(err));
1643
1807
  });
1644
1808
  }
1645
1809
  /** Resolves once the engine's identify() for this user has completed. */
@@ -1647,20 +1811,40 @@ var Client = class {
1647
1811
  return this._identify;
1648
1812
  }
1649
1813
  getFlag(name, defaultValue = false) {
1650
- return this.engine.getFlag(name, defaultValue);
1814
+ return safeRun("Client.getFlag", defaultValue, () => this.engine.getFlag(name, defaultValue));
1651
1815
  }
1652
1816
  getFlagDetail(name) {
1653
- return this.engine.getFlagDetail(name);
1817
+ return safeRun(
1818
+ "Client.getFlagDetail",
1819
+ { value: false, reason: "CLIENT_NOT_READY" },
1820
+ () => this.engine.getFlagDetail(name)
1821
+ );
1654
1822
  }
1655
1823
  getConfig(name, decodeOrOpts) {
1656
- return this.engine.getConfig(name, decodeOrOpts);
1824
+ return safeRun(
1825
+ "Client.getConfig",
1826
+ void 0,
1827
+ () => this.engine.getConfig(name, decodeOrOpts)
1828
+ );
1657
1829
  }
1658
- getExperiment(name, defaultParams, decode) {
1659
- return this.engine.getExperiment(name, defaultParams, decode);
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
+ };
1660
1844
  }
1661
1845
  /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
1662
1846
  getKillswitch(name, switchKey) {
1663
- return this.engine.getKillswitch(name, switchKey);
1847
+ return safeRun("Client.getKillswitch", false, () => this.engine.getKillswitch(name, switchKey));
1664
1848
  }
1665
1849
  /**
1666
1850
  * Record a conversion/metric event for the bound (identified) user. Delegates
@@ -1669,21 +1853,12 @@ var Client = class {
1669
1853
  * test mode.
1670
1854
  */
1671
1855
  track(eventName, props) {
1672
- this.engine.track(eventName, props);
1673
- }
1674
- /**
1675
- * Log an exposure for `name` at the treatment's render for the bound user.
1676
- * Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
1677
- * enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
1678
- * `disableAutoExposure` to log exposure exactly when you render.
1679
- */
1680
- logExposure(name) {
1681
- this.engine.logExposure(name);
1856
+ safeRun("Client.track", void 0, () => this.engine.track(eventName, props));
1682
1857
  }
1683
1858
  };
1684
1859
  function dispatchSee(problem, consequence, extras, kind) {
1685
1860
  if (!_client) {
1686
- console.warn("[shipeasy] see() called before shipeasy({ clientKey }) \u2014 error dropped");
1861
+ logger.warn("[shipeasy] see() called before shipeasy({ clientKey }) \u2014 error dropped");
1687
1862
  return;
1688
1863
  }
1689
1864
  _client.reportError(problem, consequence, extras, kind);
@@ -1905,6 +2080,7 @@ var i18n = {
1905
2080
  whenReady() {
1906
2081
  if (typeof window === "undefined") return Promise.resolve();
1907
2082
  if (window.i18n?.locale) return Promise.resolve();
2083
+ if (!hasDomEvents()) return Promise.resolve();
1908
2084
  return new Promise((resolve) => {
1909
2085
  const handler = () => resolve();
1910
2086
  window.addEventListener("se:i18n:ready", handler, { once: true });
@@ -1915,6 +2091,8 @@ var i18n = {
1915
2091
  if (typeof window === "undefined") return () => {
1916
2092
  };
1917
2093
  if (window.i18n) return window.i18n.on("update", cb);
2094
+ if (!hasDomEvents()) return () => {
2095
+ };
1918
2096
  let unsub = () => {
1919
2097
  };
1920
2098
  const handler = () => {
@@ -1936,6 +2114,7 @@ var i18n = {
1936
2114
  LABEL_MARKER_RE,
1937
2115
  LABEL_MARKER_SEP,
1938
2116
  LABEL_MARKER_START,
2117
+ LOG_LEVELS,
1939
2118
  _resetConfigureForTests,
1940
2119
  _resetShipeasyForTests,
1941
2120
  attachDevtools,