@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.
@@ -174,7 +174,7 @@ function captureCallsiteStack() {
174
174
  const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
175
175
  return kept.length ? kept.join("\n") : void 0;
176
176
  }
177
- function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlationId) {
177
+ function buildSeeEvent(problem, consequence, extras, ctx2, kindOverride, correlationId) {
178
178
  let errorType;
179
179
  let message;
180
180
  let stack;
@@ -202,8 +202,8 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
202
202
  message: truncate(message, SEE_MAX_MESSAGE),
203
203
  subject: consequence.subject,
204
204
  outcome: consequence.outcome,
205
- side: ctx.side,
206
- sdk_version: ctx.sdkVersion,
205
+ side: ctx2.side,
206
+ sdk_version: ctx2.sdkVersion,
207
207
  ts: Date.now()
208
208
  };
209
209
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
@@ -212,10 +212,10 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
212
212
  if (causedBy) ev.caused_by = causedBy;
213
213
  const cleanExtras = sanitizeExtras(extras);
214
214
  if (cleanExtras) ev.extras = cleanExtras;
215
- if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
216
- if (ctx.userId) ev.user_id = ctx.userId;
217
- if (ctx.anonId) ev.anonymous_id = ctx.anonId;
218
- 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;
219
219
  markReported(problem, ev);
220
220
  return ev;
221
221
  }
@@ -308,6 +308,98 @@ var SeeLimiter = class {
308
308
  }
309
309
  };
310
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
+
311
403
  // src/client/index.ts
312
404
  var version = "4.0.0";
313
405
  var FLAG_REASONS = [
@@ -318,6 +410,40 @@ var FLAG_REASONS = [
318
410
  "RULE_MATCH",
319
411
  "DEFAULT"
320
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
+ }
321
447
  var FLUSH_INTERVAL_MS = 5e3;
322
448
  var MAX_BUFFER = 100;
323
449
  var ANON_ID_KEY = "__se_anon_id";
@@ -329,8 +455,8 @@ var EventBuffer = class {
329
455
  this.sdkKey = sdkKey;
330
456
  if (typeof window !== "undefined") {
331
457
  this.timer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
332
- window.addEventListener("beforeunload", () => this.flush());
333
- document.addEventListener("visibilitychange", () => {
458
+ onWindow("beforeunload", () => this.flush());
459
+ onDocument("visibilitychange", () => {
334
460
  if (document.visibilityState === "hidden") this.flush(true);
335
461
  });
336
462
  try {
@@ -875,6 +1001,10 @@ var Engine = class _Engine {
875
1001
  env;
876
1002
  evalResult = null;
877
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;
878
1008
  userId = "";
879
1009
  buffer;
880
1010
  telemetry;
@@ -900,10 +1030,16 @@ var Engine = class _Engine {
900
1030
  this.notify();
901
1031
  };
902
1032
  constructor(opts) {
1033
+ setLogLevel(opts.logLevel);
903
1034
  this.sdkKey = opts.sdkKey;
904
- this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
1035
+ this.baseUrl = (opts.baseUrl ?? "https://api.shipeasy.ai").replace(/\/$/, "");
905
1036
  this.env = opts.env ?? "prod";
906
1037
  this.testMode = opts.testMode === true;
1038
+ setInternalReportContext({
1039
+ side: "client",
1040
+ sdkVersion: version,
1041
+ enabled: !this.testMode && opts.disableInternalErrorReporting !== true
1042
+ });
907
1043
  this.autoGuardrails = opts.autoGuardrails !== false;
908
1044
  this.autoCollectAlways = opts.autoCollectAlways === true;
909
1045
  this.disableAutoExposure = opts.disableAutoExposure === true;
@@ -916,6 +1052,9 @@ var Engine = class _Engine {
916
1052
  engagement: g.engagement ?? this.autoGuardrails
917
1053
  };
918
1054
  this.anonId = getOrCreateAnonId();
1055
+ if (opts.anonymousStore && !this.testMode) {
1056
+ this.anonReady = this.hydrateAnonFromStore(opts.anonymousStore);
1057
+ }
919
1058
  this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
920
1059
  this.telemetry = new Telemetry({
921
1060
  endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
@@ -931,6 +1070,23 @@ var Engine = class _Engine {
931
1070
  void this.buffer.flushPendingAlias();
932
1071
  }
933
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
+ }
934
1090
  /**
935
1091
  * Build a no-network, immediately-usable browser client for tests
936
1092
  * (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
@@ -958,6 +1114,10 @@ var Engine = class _Engine {
958
1114
  this.notify();
959
1115
  return;
960
1116
  }
1117
+ if (this.anonReady) {
1118
+ await this.anonReady;
1119
+ this.anonReady = null;
1120
+ }
961
1121
  const seq = ++this.identifySeq;
962
1122
  const prevUserId = this.userId;
963
1123
  if (user.user_id !== void 0) this.userId = user.user_id;
@@ -1034,7 +1194,7 @@ var Engine = class _Engine {
1034
1194
  try {
1035
1195
  l();
1036
1196
  } catch (err) {
1037
- console.warn("[shipeasy] subscriber threw:", String(err));
1197
+ logger.warn("[shipeasy] subscriber threw:", String(err));
1038
1198
  }
1039
1199
  }
1040
1200
  }
@@ -1111,54 +1271,41 @@ var Engine = class _Engine {
1111
1271
  try {
1112
1272
  return opts.decode(raw);
1113
1273
  } catch (err) {
1114
- console.warn(`[shipeasy] getConfig('${name}') decode failed:`, String(err));
1274
+ logger.warn(`[shipeasy] getConfig('${name}') decode failed:`, String(err));
1115
1275
  return void 0;
1116
1276
  }
1117
1277
  }
1118
- getExperiment(name, defaultParams, decodeOrOpts, variantsArg) {
1119
- const opts = typeof decodeOrOpts === "function" ? { decode: decodeOrOpts, variants: variantsArg } : decodeOrOpts ?? {};
1120
- 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) {
1121
1291
  this.telemetry.emit("experiment", name);
1122
- const notIn = {
1123
- inExperiment: false,
1124
- group: "control",
1125
- params: defaultParams
1126
- };
1127
- const pov = this.experimentOverrides.get(name);
1128
- if (pov) {
1129
- const variantParams = variants?.[pov.group];
1130
- const params = { ...defaultParams, ...pov.params, ...variantParams ?? {} };
1131
- 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 });
1132
1296
  }
1133
- const ov = readExpOverride(name);
1134
- if (ov !== null) {
1135
- const variantParams = variants?.[ov];
1136
- const params = variantParams ? { ...defaultParams, ...variantParams } : defaultParams;
1137
- 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 });
1138
1301
  }
1139
- const entry = this.evalResult?.experiments[name];
1140
- if (!entry || !entry.inExperiment) return notIn;
1141
- const shouldLog = opts.logExposure ?? !this.disableAutoExposure;
1142
- if (shouldLog) this.buffer.pushExposure(name, entry.group, this.userId, this.anonId);
1143
- if (!decode) return { inExperiment: true, group: entry.group, params: entry.params };
1144
- try {
1145
- return { inExperiment: true, group: entry.group, params: decode(entry.params) };
1146
- } catch (err) {
1147
- console.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
1148
- 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);
1149
1307
  }
1150
- }
1151
- /**
1152
- * Manually log an exposure for an enrolled experiment (Statsig's
1153
- * `manuallyLogExposure`). Reads the cached eval result; if the visitor is in
1154
- * the experiment, pushes the session-deduped exposure. Pair this with the
1155
- * render of the treatment when reading with `{ logExposure: false }` (or
1156
- * `disableAutoExposure: true`). No-op if the visitor isn't enrolled.
1157
- */
1158
- logExposure(name) {
1159
- const entry = this.evalResult?.experiments[name];
1160
- if (!entry || !entry.inExperiment) return;
1161
- this.buffer.pushExposure(name, entry.group, this.userId, this.anonId);
1308
+ return new AssignmentImpl(null, null, defaults);
1162
1309
  }
1163
1310
  /**
1164
1311
  * Subscribe to state changes — fires after identify() completes and on
@@ -1167,7 +1314,7 @@ var Engine = class _Engine {
1167
1314
  */
1168
1315
  subscribe(listener) {
1169
1316
  this.listeners.add(listener);
1170
- if (!this.overrideListenerInstalled && typeof window !== "undefined") {
1317
+ if (!this.overrideListenerInstalled && hasDomEvents()) {
1171
1318
  this.overrideListenerInstalled = true;
1172
1319
  window.addEventListener("se:override:change", this.onOverrideChange);
1173
1320
  }
@@ -1183,9 +1330,11 @@ var Engine = class _Engine {
1183
1330
  if (typeof window === "undefined") return null;
1184
1331
  const bridge = {
1185
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).
1186
1335
  getExperiment: (n) => {
1187
- const r = this.getExperiment(n, {});
1188
- return { inExperiment: r.inExperiment, group: r.group };
1336
+ const entry = this.evalResult?.experiments[n];
1337
+ return { inExperiment: entry?.inExperiment ?? false, group: entry?.group ?? "control" };
1189
1338
  },
1190
1339
  getConfig: (n) => this.getConfig(n)
1191
1340
  };
@@ -1308,7 +1457,7 @@ function loadDevtools(opts = {}) {
1308
1457
  }
1309
1458
  }
1310
1459
  function attachDevtools(client, opts = {}) {
1311
- if (typeof window === "undefined") return () => {
1460
+ if (!hasDomEvents()) return () => {
1312
1461
  };
1313
1462
  const hotkey = opts.hotkey ?? "Shift+Alt+S";
1314
1463
  const parts = hotkey.split("+");
@@ -1347,19 +1496,22 @@ function shipeasy(opts) {
1347
1496
  const client = configureShipeasy({
1348
1497
  sdkKey: opts.clientKey,
1349
1498
  baseUrl,
1499
+ logLevel: opts.logLevel,
1350
1500
  autoGuardrails: blanket,
1351
1501
  autoGuardrailGroups: groups,
1352
1502
  autoCollectAlways: acObj?.always === true,
1353
1503
  disableTelemetry: opts.disableTelemetry,
1504
+ disableInternalErrorReporting: opts.disableInternalErrorReporting,
1354
1505
  disableAutoExposure: opts.disableAutoExposure,
1355
1506
  privateAttributes: opts.privateAttributes,
1356
- stickyBucketing: opts.stickyBucketing
1507
+ stickyBucketing: opts.stickyBucketing,
1508
+ anonymousStore: opts.anonymousStore
1357
1509
  });
1358
1510
  injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
1359
1511
  flags.notifyMounted();
1360
1512
  if (opts.autoIdentify !== false) {
1361
1513
  void client.identify({}).catch((err) => {
1362
- console.warn("[shipeasy] auto-identify failed:", String(err));
1514
+ logger.warn("[shipeasy] auto-identify failed:", String(err));
1363
1515
  });
1364
1516
  }
1365
1517
  return attachDevtools(client, { adminUrl: opts.adminUrl });
@@ -1435,7 +1587,7 @@ var _mountedAndReady = false;
1435
1587
  var _standaloneListeners = /* @__PURE__ */ new Set();
1436
1588
  var _standaloneOverrideWired = false;
1437
1589
  function wireStandaloneOverride() {
1438
- if (_standaloneOverrideWired || typeof window === "undefined") return;
1590
+ if (_standaloneOverrideWired || !hasDomEvents()) return;
1439
1591
  _standaloneOverrideWired = true;
1440
1592
  window.addEventListener("se:override:change", () => {
1441
1593
  for (const cb of _standaloneListeners) cb();
@@ -1447,7 +1599,7 @@ var flags = {
1447
1599
  },
1448
1600
  identify(user) {
1449
1601
  if (!_client) {
1450
- console.warn("[shipeasy] flags.identify called before configureShipeasy()");
1602
+ logger.warn("[shipeasy] flags.identify called before configureShipeasy()");
1451
1603
  return Promise.resolve();
1452
1604
  }
1453
1605
  return _client.identify(user);
@@ -1460,57 +1612,66 @@ var flags = {
1460
1612
  * force-static pages where SSR has no flag data.
1461
1613
  */
1462
1614
  get(name, defaultValue = false) {
1463
- const bs = getBootstrap();
1464
- if (bs !== null && name in bs.flags) return bs.flags[name];
1465
- if (!_mountedAndReady) return defaultValue;
1466
- if (_client) return _client.getFlag(name, defaultValue);
1467
- return readGateOverride(name) ?? defaultValue;
1615
+ return safeRun("flags.get", defaultValue, () => {
1616
+ const bs = getBootstrap();
1617
+ if (bs !== null && name in bs.flags) return bs.flags[name];
1618
+ if (!_mountedAndReady) return defaultValue;
1619
+ if (_client) return _client.getFlag(name, defaultValue);
1620
+ return readGateOverride(name) ?? defaultValue;
1621
+ });
1468
1622
  },
1469
1623
  /** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
1470
1624
  getDetail(name) {
1471
- if (_client) return _client.getFlagDetail(name);
1472
- const ov = readGateOverride(name);
1473
- if (ov !== null) return { value: ov, reason: "OVERRIDE" };
1474
- return { value: false, reason: "CLIENT_NOT_READY" };
1625
+ return safeRun("flags.getDetail", { value: false, reason: "CLIENT_NOT_READY" }, () => {
1626
+ if (_client) return _client.getFlagDetail(name);
1627
+ const ov = readGateOverride(name);
1628
+ if (ov !== null) return { value: ov, reason: "OVERRIDE" };
1629
+ return { value: false, reason: "CLIENT_NOT_READY" };
1630
+ });
1475
1631
  },
1476
1632
  getConfig(name, decode) {
1477
- const bs = getBootstrap();
1478
- if (bs !== null && name in bs.configs) {
1479
- const raw = bs.configs[name];
1480
- if (!decode) return raw;
1633
+ return safeRun("flags.getConfig", void 0, () => {
1634
+ const bs = getBootstrap();
1635
+ if (bs !== null && name in bs.configs) {
1636
+ const raw = bs.configs[name];
1637
+ if (!decode) return raw;
1638
+ try {
1639
+ return decode(raw);
1640
+ } catch {
1641
+ return void 0;
1642
+ }
1643
+ }
1644
+ if (!_mountedAndReady) return void 0;
1645
+ if (_client) return _client.getConfig(name, decode);
1646
+ const ov = readConfigOverride(name);
1647
+ if (ov === void 0) return void 0;
1648
+ if (!decode) return ov;
1481
1649
  try {
1482
- return decode(raw);
1650
+ return decode(ov);
1483
1651
  } catch {
1484
1652
  return void 0;
1485
1653
  }
1486
- }
1487
- if (!_mountedAndReady) return void 0;
1488
- if (_client) return _client.getConfig(name, decode);
1489
- const ov = readConfigOverride(name);
1490
- if (ov === void 0) return void 0;
1491
- if (!decode) return ov;
1492
- try {
1493
- return decode(ov);
1494
- } catch {
1495
- return void 0;
1496
- }
1654
+ });
1497
1655
  },
1498
- getExperiment(name, defaultParams, decodeOrOpts, variants) {
1499
- const fallback = {
1500
- inExperiment: false,
1501
- group: "control",
1502
- 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
+ )
1503
1671
  };
1504
- if (!_client) return fallback;
1505
- return typeof decodeOrOpts === "function" ? _client.getExperiment(name, defaultParams, decodeOrOpts, variants) : _client.getExperiment(name, defaultParams, decodeOrOpts ?? {});
1506
- },
1507
- /** Manually log an exposure for an enrolled experiment. See
1508
- * {@link Engine.logExposure}. No-op before configure(). */
1509
- logExposure(name) {
1510
- _client?.logExposure(name);
1511
1672
  },
1512
1673
  track(eventName, props) {
1513
- _client?.track(eventName, props);
1674
+ safeRun("flags.track", void 0, () => _client?.track(eventName, props));
1514
1675
  },
1515
1676
  /**
1516
1677
  * Read a killswitch. Without `switchKey`, returns true when the killswitch is
@@ -1522,15 +1683,17 @@ var flags = {
1522
1683
  * available synchronously on first render.
1523
1684
  */
1524
1685
  ks(name, switchKey) {
1525
- const bs = getBootstrap();
1526
- if (bs !== null && bs.killswitches && name in bs.killswitches) {
1527
- const ks = bs.killswitches[name];
1528
- if (typeof ks === "boolean") return switchKey === void 0 ? ks : false;
1529
- if (switchKey === void 0) return false;
1530
- return ks[switchKey] === true;
1531
- }
1532
- if (!_mountedAndReady) return false;
1533
- return _client?.getKillswitch(name, switchKey) ?? false;
1686
+ return safeRun("flags.ks", false, () => {
1687
+ const bs = getBootstrap();
1688
+ if (bs !== null && bs.killswitches && name in bs.killswitches) {
1689
+ const ks = bs.killswitches[name];
1690
+ if (typeof ks === "boolean") return switchKey === void 0 ? ks : false;
1691
+ if (switchKey === void 0) return false;
1692
+ return ks[switchKey] === true;
1693
+ }
1694
+ if (!_mountedAndReady) return false;
1695
+ return _client?.getKillswitch(name, switchKey) ?? false;
1696
+ });
1534
1697
  },
1535
1698
  flush() {
1536
1699
  return _client?.flush() ?? Promise.resolve();
@@ -1587,7 +1750,7 @@ var Client = class {
1587
1750
  this.engine = engine;
1588
1751
  this.attributes = _attributes(user);
1589
1752
  this._identify = this.engine.identify(this.attributes).catch((err) => {
1590
- console.warn("[shipeasy] Client identify failed:", String(err));
1753
+ logger.warn("[shipeasy] Client identify failed:", String(err));
1591
1754
  });
1592
1755
  }
1593
1756
  /** Resolves once the engine's identify() for this user has completed. */
@@ -1595,20 +1758,40 @@ var Client = class {
1595
1758
  return this._identify;
1596
1759
  }
1597
1760
  getFlag(name, defaultValue = false) {
1598
- return this.engine.getFlag(name, defaultValue);
1761
+ return safeRun("Client.getFlag", defaultValue, () => this.engine.getFlag(name, defaultValue));
1599
1762
  }
1600
1763
  getFlagDetail(name) {
1601
- return this.engine.getFlagDetail(name);
1764
+ return safeRun(
1765
+ "Client.getFlagDetail",
1766
+ { value: false, reason: "CLIENT_NOT_READY" },
1767
+ () => this.engine.getFlagDetail(name)
1768
+ );
1602
1769
  }
1603
1770
  getConfig(name, decodeOrOpts) {
1604
- return this.engine.getConfig(name, decodeOrOpts);
1771
+ return safeRun(
1772
+ "Client.getConfig",
1773
+ void 0,
1774
+ () => this.engine.getConfig(name, decodeOrOpts)
1775
+ );
1605
1776
  }
1606
- getExperiment(name, defaultParams, decode) {
1607
- return this.engine.getExperiment(name, defaultParams, decode);
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
+ };
1608
1791
  }
1609
1792
  /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
1610
1793
  getKillswitch(name, switchKey) {
1611
- return this.engine.getKillswitch(name, switchKey);
1794
+ return safeRun("Client.getKillswitch", false, () => this.engine.getKillswitch(name, switchKey));
1612
1795
  }
1613
1796
  /**
1614
1797
  * Record a conversion/metric event for the bound (identified) user. Delegates
@@ -1617,21 +1800,12 @@ var Client = class {
1617
1800
  * test mode.
1618
1801
  */
1619
1802
  track(eventName, props) {
1620
- this.engine.track(eventName, props);
1621
- }
1622
- /**
1623
- * Log an exposure for `name` at the treatment's render for the bound user.
1624
- * Delegates to {@link Engine.logExposure} (no-op when the visitor isn't
1625
- * enrolled). Pair with `getExperiment(name, …, { logExposure: false })` /
1626
- * `disableAutoExposure` to log exposure exactly when you render.
1627
- */
1628
- logExposure(name) {
1629
- this.engine.logExposure(name);
1803
+ safeRun("Client.track", void 0, () => this.engine.track(eventName, props));
1630
1804
  }
1631
1805
  };
1632
1806
  function dispatchSee(problem, consequence, extras, kind) {
1633
1807
  if (!_client) {
1634
- console.warn("[shipeasy] see() called before shipeasy({ clientKey }) \u2014 error dropped");
1808
+ logger.warn("[shipeasy] see() called before shipeasy({ clientKey }) \u2014 error dropped");
1635
1809
  return;
1636
1810
  }
1637
1811
  _client.reportError(problem, consequence, extras, kind);
@@ -1853,6 +2027,7 @@ var i18n = {
1853
2027
  whenReady() {
1854
2028
  if (typeof window === "undefined") return Promise.resolve();
1855
2029
  if (window.i18n?.locale) return Promise.resolve();
2030
+ if (!hasDomEvents()) return Promise.resolve();
1856
2031
  return new Promise((resolve) => {
1857
2032
  const handler = () => resolve();
1858
2033
  window.addEventListener("se:i18n:ready", handler, { once: true });
@@ -1863,6 +2038,8 @@ var i18n = {
1863
2038
  if (typeof window === "undefined") return () => {
1864
2039
  };
1865
2040
  if (window.i18n) return window.i18n.on("update", cb);
2041
+ if (!hasDomEvents()) return () => {
2042
+ };
1866
2043
  let unsub = () => {
1867
2044
  };
1868
2045
  const handler = () => {
@@ -1883,6 +2060,7 @@ export {
1883
2060
  LABEL_MARKER_RE,
1884
2061
  LABEL_MARKER_SEP,
1885
2062
  LABEL_MARKER_START,
2063
+ LOG_LEVELS,
1886
2064
  _resetConfigureForTests,
1887
2065
  _resetShipeasyForTests,
1888
2066
  attachDevtools,