@shipeasy/sdk 6.2.0 → 6.3.1

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.
@@ -34,6 +34,7 @@ __export(server_exports, {
34
34
  Client: () => Client,
35
35
  Engine: () => Engine,
36
36
  FLAG_REASONS: () => FLAG_REASONS,
37
+ LOG_LEVELS: () => LOG_LEVELS,
37
38
  _murmur3ForTests: () => _murmur3ForTests,
38
39
  _resetConfigureForTests: () => _resetConfigureForTests,
39
40
  _resetShipeasyServerForTests: () => _resetShipeasyServerForTests,
@@ -121,6 +122,53 @@ function send(url) {
121
122
  }
122
123
  }
123
124
 
125
+ // src/logger.ts
126
+ var LOG_LEVELS = ["silent", "error", "warn", "info", "debug"];
127
+ var RANK = {
128
+ silent: 0,
129
+ error: 1,
130
+ warn: 2,
131
+ info: 3,
132
+ debug: 4
133
+ };
134
+ var currentLevel = "warn";
135
+ function setLogLevel(level) {
136
+ if (typeof level === "string" && Object.prototype.hasOwnProperty.call(RANK, level)) {
137
+ currentLevel = level;
138
+ }
139
+ }
140
+ function write(method, args) {
141
+ try {
142
+ const c = globalThis.console;
143
+ if (!c) return;
144
+ const fn = c[method] ?? c.log;
145
+ if (typeof fn === "function") fn.call(c, ...args);
146
+ } catch {
147
+ }
148
+ }
149
+ var logger = {
150
+ error(...args) {
151
+ if (RANK[currentLevel] >= RANK.error) write("error", args);
152
+ },
153
+ warn(...args) {
154
+ if (RANK[currentLevel] >= RANK.warn) write("warn", args);
155
+ },
156
+ info(...args) {
157
+ if (RANK[currentLevel] >= RANK.info) write("info", args);
158
+ },
159
+ debug(...args) {
160
+ if (RANK[currentLevel] >= RANK.debug) write("debug", args);
161
+ }
162
+ };
163
+ function safeRun(label, fallback, fn) {
164
+ try {
165
+ return fn();
166
+ } catch (err) {
167
+ logger.error(`[shipeasy] ${label} failed \u2014 returning safe default:`, String(err));
168
+ return fallback;
169
+ }
170
+ }
171
+
124
172
  // src/see/core.ts
125
173
  var SEE_MAX_MESSAGE = 500;
126
174
  var SEE_MAX_STACK = 8e3;
@@ -582,6 +630,7 @@ var Engine = class _Engine {
582
630
  // 304). Never fired in testMode/offline (no polling happens there).
583
631
  changeListeners = /* @__PURE__ */ new Set();
584
632
  constructor(opts) {
633
+ setLogLevel(opts.logLevel);
585
634
  this.apiKey = opts.apiKey;
586
635
  this.baseUrl = (opts.baseUrl ?? "https://cdn.shipeasy.ai").replace(/\/$/, "");
587
636
  this.env = opts.env ?? "prod";
@@ -705,14 +754,14 @@ var Engine = class _Engine {
705
754
  try {
706
755
  l();
707
756
  } catch (err) {
708
- console.warn("[shipeasy] onChange listener threw:", String(err));
757
+ logger.warn("[shipeasy] onChange listener threw:", String(err));
709
758
  }
710
759
  }
711
760
  }
712
761
  startPoll() {
713
762
  this.timer = setInterval(() => {
714
763
  this.fetchAll(true).catch(
715
- (err) => console.warn("[shipeasy] background poll failed:", String(err))
764
+ (err) => logger.warn("[shipeasy] background poll failed:", String(err))
716
765
  );
717
766
  }, this.pollInterval * 1e3);
718
767
  }
@@ -791,7 +840,12 @@ var Engine = class _Engine {
791
840
  return "defaultValue" in opts ? opts.defaultValue : void 0;
792
841
  }
793
842
  if (!opts.decode) return raw;
794
- return opts.decode(raw);
843
+ try {
844
+ return opts.decode(raw);
845
+ } catch (err) {
846
+ logger.warn(`[shipeasy] getConfig('${name}') decode failed:`, String(err));
847
+ return "defaultValue" in opts ? opts.defaultValue : void 0;
848
+ }
795
849
  }
796
850
  getExperiment(name, user, defaultParams, decode) {
797
851
  this.telemetry.emit("experiment", name);
@@ -806,7 +860,7 @@ var Engine = class _Engine {
806
860
  try {
807
861
  return { inExperiment: true, group: ov.group, params: decode(ov.params) };
808
862
  } catch (err) {
809
- console.warn(`[shipeasy] getExperiment('${name}') override decode failed:`, String(err));
863
+ logger.warn(`[shipeasy] getExperiment('${name}') override decode failed:`, String(err));
810
864
  return notIn;
811
865
  }
812
866
  }
@@ -831,7 +885,7 @@ var Engine = class _Engine {
831
885
  try {
832
886
  return { inExperiment: true, group: g.name, params: decode(g.params) };
833
887
  } catch (err) {
834
- console.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
888
+ logger.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
835
889
  return notIn;
836
890
  }
837
891
  };
@@ -883,7 +937,7 @@ var Engine = class _Engine {
883
937
  method: "POST",
884
938
  headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
885
939
  body
886
- }).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
940
+ }).catch((err) => logger.warn("[shipeasy] track failed:", String(err)));
887
941
  }
888
942
  /**
889
943
  * Emit an exposure event for an experiment at the server-side decision point
@@ -914,7 +968,7 @@ var Engine = class _Engine {
914
968
  method: "POST",
915
969
  headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
916
970
  body
917
- }).catch((err) => console.warn("[shipeasy] logExposure failed:", String(err)));
971
+ }).catch((err) => logger.warn("[shipeasy] logExposure failed:", String(err)));
918
972
  }
919
973
  /**
920
974
  * Report a structured error into the errors primitive. Fire-and-forget —
@@ -934,7 +988,7 @@ var Engine = class _Engine {
934
988
  method: "POST",
935
989
  headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
936
990
  body: JSON.stringify({ events: [ev] })
937
- }).catch((err) => console.warn("[shipeasy] see() send failed:", String(err)));
991
+ }).catch((err) => logger.warn("[shipeasy] see() send failed:", String(err)));
938
992
  } catch {
939
993
  }
940
994
  }
@@ -1121,7 +1175,7 @@ function _resetShipeasyServerForTests() {
1121
1175
  async function shipeasy(opts) {
1122
1176
  const serverKey = opts.serverKey ?? "";
1123
1177
  if (!serverKey) {
1124
- console.error(
1178
+ logger.error(
1125
1179
  "[shipeasy] No server key \u2014 flags, experiments and SSR i18n skipped. Pass `serverKey` to shipeasy() from @shipeasy/sdk/server with your server key (SHIPEASY_SERVER_KEY). Set it as a Worker secret with `wrangler secret put SHIPEASY_SERVER_KEY` (or add it to .env for local dev). Do not pass a client key here \u2014 the server entrypoint only accepts the server key."
1126
1180
  );
1127
1181
  }
@@ -1129,7 +1183,8 @@ async function shipeasy(opts) {
1129
1183
  flags.configure({
1130
1184
  apiKey: serverKey,
1131
1185
  disableTelemetry: opts.disableTelemetry,
1132
- privateAttributes: opts.privateAttributes
1186
+ privateAttributes: opts.privateAttributes,
1187
+ logLevel: opts.logLevel
1133
1188
  });
1134
1189
  let resolvedUrlOverrides = opts.urlOverrides;
1135
1190
  if (!resolvedUrlOverrides) {
@@ -1255,21 +1310,30 @@ var flags = {
1255
1310
  _server?.destroy();
1256
1311
  },
1257
1312
  get(name, user, defaultValue = false) {
1258
- return _server?.getFlag(name, user, defaultValue) ?? defaultValue;
1313
+ return safeRun("flags.get", defaultValue, () => _server?.getFlag(name, user, defaultValue) ?? defaultValue);
1259
1314
  },
1260
1315
  /** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
1261
1316
  getDetail(name, user) {
1262
- return _server?.getFlagDetail(name, user) ?? { value: false, reason: "CLIENT_NOT_READY" };
1317
+ return safeRun(
1318
+ "flags.getDetail",
1319
+ { value: false, reason: "CLIENT_NOT_READY" },
1320
+ () => _server?.getFlagDetail(name, user) ?? { value: false, reason: "CLIENT_NOT_READY" }
1321
+ );
1263
1322
  },
1264
1323
  getConfig(name, decodeOrOpts) {
1265
- return _server?.getConfig(name, decodeOrOpts);
1324
+ return safeRun(
1325
+ "flags.getConfig",
1326
+ void 0,
1327
+ () => _server?.getConfig(name, decodeOrOpts)
1328
+ );
1266
1329
  },
1267
1330
  getExperiment(name, user, defaultParams, decode) {
1268
- return _server?.getExperiment(name, user, defaultParams, decode) ?? {
1269
- inExperiment: false,
1270
- group: "control",
1271
- params: defaultParams
1272
- };
1331
+ const notIn = { inExperiment: false, group: "control", params: defaultParams };
1332
+ return safeRun(
1333
+ "flags.getExperiment",
1334
+ notIn,
1335
+ () => _server?.getExperiment(name, user, defaultParams, decode) ?? notIn
1336
+ );
1273
1337
  },
1274
1338
  /**
1275
1339
  * Read a killswitch. Without `switchKey`, returns true when the whole
@@ -1277,15 +1341,15 @@ var flags = {
1277
1341
  * switch is on. Unknown killswitches / switches return false.
1278
1342
  */
1279
1343
  ks(name, switchKey) {
1280
- return _server?.getKillswitch(name, switchKey) ?? false;
1344
+ return safeRun("flags.ks", false, () => _server?.getKillswitch(name, switchKey) ?? false);
1281
1345
  },
1282
1346
  track(userId, eventName, props) {
1283
- _server?.track(userId, eventName, props);
1347
+ safeRun("flags.track", void 0, () => _server?.track(userId, eventName, props));
1284
1348
  },
1285
1349
  /** Emit an exposure for an enrolled experiment at the decision point. See
1286
1350
  * {@link Engine.logExposure}. No-op before configure(). */
1287
1351
  logExposure(user, name) {
1288
- _server?.logExposure(user, name);
1352
+ safeRun("flags.logExposure", void 0, () => _server?.logExposure(user, name));
1289
1353
  },
1290
1354
  /**
1291
1355
  * Evaluate all flags / configs / experiments for a user against the locally
@@ -1293,12 +1357,8 @@ var flags = {
1293
1357
  * overrides. Returns an empty payload when the blob hasn't been fetched yet.
1294
1358
  */
1295
1359
  evaluate(user, rawUrl) {
1296
- return _server?.evaluate(user, rawUrl) ?? {
1297
- flags: {},
1298
- configs: {},
1299
- experiments: {},
1300
- killswitches: {}
1301
- };
1360
+ const empty = { flags: {}, configs: {}, experiments: {}, killswitches: {} };
1361
+ return safeRun("flags.evaluate", empty, () => _server?.evaluate(user, rawUrl) ?? empty);
1302
1362
  }
1303
1363
  };
1304
1364
  var _identityAttributes = (user) => user && typeof user === "object" ? user : {};
@@ -1387,20 +1447,37 @@ var Client = class {
1387
1447
  this.attributes = _attributes(user);
1388
1448
  }
1389
1449
  getFlag(name, defaultValue = false) {
1390
- return this.engine.getFlag(name, this.attributes, defaultValue);
1450
+ return safeRun(
1451
+ "Client.getFlag",
1452
+ defaultValue,
1453
+ () => this.engine.getFlag(name, this.attributes, defaultValue)
1454
+ );
1391
1455
  }
1392
1456
  getFlagDetail(name) {
1393
- return this.engine.getFlagDetail(name, this.attributes);
1457
+ return safeRun(
1458
+ "Client.getFlagDetail",
1459
+ { value: false, reason: "CLIENT_NOT_READY" },
1460
+ () => this.engine.getFlagDetail(name, this.attributes)
1461
+ );
1394
1462
  }
1395
1463
  getConfig(name, decodeOrOpts) {
1396
- return this.engine.getConfig(name, decodeOrOpts);
1464
+ return safeRun(
1465
+ "Client.getConfig",
1466
+ void 0,
1467
+ () => this.engine.getConfig(name, decodeOrOpts)
1468
+ );
1397
1469
  }
1398
1470
  getExperiment(name, defaultParams, decode) {
1399
- return this.engine.getExperiment(name, this.attributes, defaultParams, decode);
1471
+ const notIn = { inExperiment: false, group: "control", params: defaultParams };
1472
+ return safeRun(
1473
+ "Client.getExperiment",
1474
+ notIn,
1475
+ () => this.engine.getExperiment(name, this.attributes, defaultParams, decode)
1476
+ );
1400
1477
  }
1401
1478
  /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
1402
1479
  getKillswitch(name, switchKey) {
1403
- return this.engine.getKillswitch(name, switchKey);
1480
+ return safeRun("Client.getKillswitch", false, () => this.engine.getKillswitch(name, switchKey));
1404
1481
  }
1405
1482
  /**
1406
1483
  * Record a conversion/metric event for the bound user. Derives the unit from
@@ -1410,9 +1487,11 @@ var Client = class {
1410
1487
  * mode.
1411
1488
  */
1412
1489
  track(eventName, props) {
1413
- const id = this.attributes.user_id ?? this.attributes.anonymous_id;
1414
- if (id === void 0) return;
1415
- this.engine.track(String(id), eventName, props);
1490
+ safeRun("Client.track", void 0, () => {
1491
+ const id = this.attributes.user_id ?? this.attributes.anonymous_id;
1492
+ if (id === void 0) return;
1493
+ this.engine.track(String(id), eventName, props);
1494
+ });
1416
1495
  }
1417
1496
  /**
1418
1497
  * Emit an exposure event for `name` at this server-side decision point for the
@@ -1421,12 +1500,12 @@ var Client = class {
1421
1500
  * in test mode).
1422
1501
  */
1423
1502
  logExposure(name) {
1424
- this.engine.logExposure(this.attributes, name);
1503
+ safeRun("Client.logExposure", void 0, () => this.engine.logExposure(this.attributes, name));
1425
1504
  }
1426
1505
  };
1427
1506
  function dispatchSee(problem, consequence, extras, kind) {
1428
1507
  if (!_server) {
1429
- console.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
1508
+ logger.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
1430
1509
  return;
1431
1510
  }
1432
1511
  _server.reportError(problem, consequence, extras, kind);
@@ -1444,6 +1523,7 @@ var see = Object.assign(
1444
1523
  Client,
1445
1524
  Engine,
1446
1525
  FLAG_REASONS,
1526
+ LOG_LEVELS,
1447
1527
  _murmur3ForTests,
1448
1528
  _resetConfigureForTests,
1449
1529
  _resetShipeasyServerForTests,
@@ -67,6 +67,53 @@ function send(url) {
67
67
  }
68
68
  }
69
69
 
70
+ // src/logger.ts
71
+ var LOG_LEVELS = ["silent", "error", "warn", "info", "debug"];
72
+ var RANK = {
73
+ silent: 0,
74
+ error: 1,
75
+ warn: 2,
76
+ info: 3,
77
+ debug: 4
78
+ };
79
+ var currentLevel = "warn";
80
+ function setLogLevel(level) {
81
+ if (typeof level === "string" && Object.prototype.hasOwnProperty.call(RANK, level)) {
82
+ currentLevel = level;
83
+ }
84
+ }
85
+ function write(method, args) {
86
+ try {
87
+ const c = globalThis.console;
88
+ if (!c) return;
89
+ const fn = c[method] ?? c.log;
90
+ if (typeof fn === "function") fn.call(c, ...args);
91
+ } catch {
92
+ }
93
+ }
94
+ var logger = {
95
+ error(...args) {
96
+ if (RANK[currentLevel] >= RANK.error) write("error", args);
97
+ },
98
+ warn(...args) {
99
+ if (RANK[currentLevel] >= RANK.warn) write("warn", args);
100
+ },
101
+ info(...args) {
102
+ if (RANK[currentLevel] >= RANK.info) write("info", args);
103
+ },
104
+ debug(...args) {
105
+ if (RANK[currentLevel] >= RANK.debug) write("debug", args);
106
+ }
107
+ };
108
+ function safeRun(label, fallback, fn) {
109
+ try {
110
+ return fn();
111
+ } catch (err) {
112
+ logger.error(`[shipeasy] ${label} failed \u2014 returning safe default:`, String(err));
113
+ return fallback;
114
+ }
115
+ }
116
+
70
117
  // src/see/core.ts
71
118
  var SEE_MAX_MESSAGE = 500;
72
119
  var SEE_MAX_STACK = 8e3;
@@ -528,6 +575,7 @@ var Engine = class _Engine {
528
575
  // 304). Never fired in testMode/offline (no polling happens there).
529
576
  changeListeners = /* @__PURE__ */ new Set();
530
577
  constructor(opts) {
578
+ setLogLevel(opts.logLevel);
531
579
  this.apiKey = opts.apiKey;
532
580
  this.baseUrl = (opts.baseUrl ?? "https://cdn.shipeasy.ai").replace(/\/$/, "");
533
581
  this.env = opts.env ?? "prod";
@@ -651,14 +699,14 @@ var Engine = class _Engine {
651
699
  try {
652
700
  l();
653
701
  } catch (err) {
654
- console.warn("[shipeasy] onChange listener threw:", String(err));
702
+ logger.warn("[shipeasy] onChange listener threw:", String(err));
655
703
  }
656
704
  }
657
705
  }
658
706
  startPoll() {
659
707
  this.timer = setInterval(() => {
660
708
  this.fetchAll(true).catch(
661
- (err) => console.warn("[shipeasy] background poll failed:", String(err))
709
+ (err) => logger.warn("[shipeasy] background poll failed:", String(err))
662
710
  );
663
711
  }, this.pollInterval * 1e3);
664
712
  }
@@ -737,7 +785,12 @@ var Engine = class _Engine {
737
785
  return "defaultValue" in opts ? opts.defaultValue : void 0;
738
786
  }
739
787
  if (!opts.decode) return raw;
740
- return opts.decode(raw);
788
+ try {
789
+ return opts.decode(raw);
790
+ } catch (err) {
791
+ logger.warn(`[shipeasy] getConfig('${name}') decode failed:`, String(err));
792
+ return "defaultValue" in opts ? opts.defaultValue : void 0;
793
+ }
741
794
  }
742
795
  getExperiment(name, user, defaultParams, decode) {
743
796
  this.telemetry.emit("experiment", name);
@@ -752,7 +805,7 @@ var Engine = class _Engine {
752
805
  try {
753
806
  return { inExperiment: true, group: ov.group, params: decode(ov.params) };
754
807
  } catch (err) {
755
- console.warn(`[shipeasy] getExperiment('${name}') override decode failed:`, String(err));
808
+ logger.warn(`[shipeasy] getExperiment('${name}') override decode failed:`, String(err));
756
809
  return notIn;
757
810
  }
758
811
  }
@@ -777,7 +830,7 @@ var Engine = class _Engine {
777
830
  try {
778
831
  return { inExperiment: true, group: g.name, params: decode(g.params) };
779
832
  } catch (err) {
780
- console.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
833
+ logger.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
781
834
  return notIn;
782
835
  }
783
836
  };
@@ -829,7 +882,7 @@ var Engine = class _Engine {
829
882
  method: "POST",
830
883
  headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
831
884
  body
832
- }).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
885
+ }).catch((err) => logger.warn("[shipeasy] track failed:", String(err)));
833
886
  }
834
887
  /**
835
888
  * Emit an exposure event for an experiment at the server-side decision point
@@ -860,7 +913,7 @@ var Engine = class _Engine {
860
913
  method: "POST",
861
914
  headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
862
915
  body
863
- }).catch((err) => console.warn("[shipeasy] logExposure failed:", String(err)));
916
+ }).catch((err) => logger.warn("[shipeasy] logExposure failed:", String(err)));
864
917
  }
865
918
  /**
866
919
  * Report a structured error into the errors primitive. Fire-and-forget —
@@ -880,7 +933,7 @@ var Engine = class _Engine {
880
933
  method: "POST",
881
934
  headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
882
935
  body: JSON.stringify({ events: [ev] })
883
- }).catch((err) => console.warn("[shipeasy] see() send failed:", String(err)));
936
+ }).catch((err) => logger.warn("[shipeasy] see() send failed:", String(err)));
884
937
  } catch {
885
938
  }
886
939
  }
@@ -1067,7 +1120,7 @@ function _resetShipeasyServerForTests() {
1067
1120
  async function shipeasy(opts) {
1068
1121
  const serverKey = opts.serverKey ?? "";
1069
1122
  if (!serverKey) {
1070
- console.error(
1123
+ logger.error(
1071
1124
  "[shipeasy] No server key \u2014 flags, experiments and SSR i18n skipped. Pass `serverKey` to shipeasy() from @shipeasy/sdk/server with your server key (SHIPEASY_SERVER_KEY). Set it as a Worker secret with `wrangler secret put SHIPEASY_SERVER_KEY` (or add it to .env for local dev). Do not pass a client key here \u2014 the server entrypoint only accepts the server key."
1072
1125
  );
1073
1126
  }
@@ -1075,7 +1128,8 @@ async function shipeasy(opts) {
1075
1128
  flags.configure({
1076
1129
  apiKey: serverKey,
1077
1130
  disableTelemetry: opts.disableTelemetry,
1078
- privateAttributes: opts.privateAttributes
1131
+ privateAttributes: opts.privateAttributes,
1132
+ logLevel: opts.logLevel
1079
1133
  });
1080
1134
  let resolvedUrlOverrides = opts.urlOverrides;
1081
1135
  if (!resolvedUrlOverrides) {
@@ -1201,21 +1255,30 @@ var flags = {
1201
1255
  _server?.destroy();
1202
1256
  },
1203
1257
  get(name, user, defaultValue = false) {
1204
- return _server?.getFlag(name, user, defaultValue) ?? defaultValue;
1258
+ return safeRun("flags.get", defaultValue, () => _server?.getFlag(name, user, defaultValue) ?? defaultValue);
1205
1259
  },
1206
1260
  /** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
1207
1261
  getDetail(name, user) {
1208
- return _server?.getFlagDetail(name, user) ?? { value: false, reason: "CLIENT_NOT_READY" };
1262
+ return safeRun(
1263
+ "flags.getDetail",
1264
+ { value: false, reason: "CLIENT_NOT_READY" },
1265
+ () => _server?.getFlagDetail(name, user) ?? { value: false, reason: "CLIENT_NOT_READY" }
1266
+ );
1209
1267
  },
1210
1268
  getConfig(name, decodeOrOpts) {
1211
- return _server?.getConfig(name, decodeOrOpts);
1269
+ return safeRun(
1270
+ "flags.getConfig",
1271
+ void 0,
1272
+ () => _server?.getConfig(name, decodeOrOpts)
1273
+ );
1212
1274
  },
1213
1275
  getExperiment(name, user, defaultParams, decode) {
1214
- return _server?.getExperiment(name, user, defaultParams, decode) ?? {
1215
- inExperiment: false,
1216
- group: "control",
1217
- params: defaultParams
1218
- };
1276
+ const notIn = { inExperiment: false, group: "control", params: defaultParams };
1277
+ return safeRun(
1278
+ "flags.getExperiment",
1279
+ notIn,
1280
+ () => _server?.getExperiment(name, user, defaultParams, decode) ?? notIn
1281
+ );
1219
1282
  },
1220
1283
  /**
1221
1284
  * Read a killswitch. Without `switchKey`, returns true when the whole
@@ -1223,15 +1286,15 @@ var flags = {
1223
1286
  * switch is on. Unknown killswitches / switches return false.
1224
1287
  */
1225
1288
  ks(name, switchKey) {
1226
- return _server?.getKillswitch(name, switchKey) ?? false;
1289
+ return safeRun("flags.ks", false, () => _server?.getKillswitch(name, switchKey) ?? false);
1227
1290
  },
1228
1291
  track(userId, eventName, props) {
1229
- _server?.track(userId, eventName, props);
1292
+ safeRun("flags.track", void 0, () => _server?.track(userId, eventName, props));
1230
1293
  },
1231
1294
  /** Emit an exposure for an enrolled experiment at the decision point. See
1232
1295
  * {@link Engine.logExposure}. No-op before configure(). */
1233
1296
  logExposure(user, name) {
1234
- _server?.logExposure(user, name);
1297
+ safeRun("flags.logExposure", void 0, () => _server?.logExposure(user, name));
1235
1298
  },
1236
1299
  /**
1237
1300
  * Evaluate all flags / configs / experiments for a user against the locally
@@ -1239,12 +1302,8 @@ var flags = {
1239
1302
  * overrides. Returns an empty payload when the blob hasn't been fetched yet.
1240
1303
  */
1241
1304
  evaluate(user, rawUrl) {
1242
- return _server?.evaluate(user, rawUrl) ?? {
1243
- flags: {},
1244
- configs: {},
1245
- experiments: {},
1246
- killswitches: {}
1247
- };
1305
+ const empty = { flags: {}, configs: {}, experiments: {}, killswitches: {} };
1306
+ return safeRun("flags.evaluate", empty, () => _server?.evaluate(user, rawUrl) ?? empty);
1248
1307
  }
1249
1308
  };
1250
1309
  var _identityAttributes = (user) => user && typeof user === "object" ? user : {};
@@ -1333,20 +1392,37 @@ var Client = class {
1333
1392
  this.attributes = _attributes(user);
1334
1393
  }
1335
1394
  getFlag(name, defaultValue = false) {
1336
- return this.engine.getFlag(name, this.attributes, defaultValue);
1395
+ return safeRun(
1396
+ "Client.getFlag",
1397
+ defaultValue,
1398
+ () => this.engine.getFlag(name, this.attributes, defaultValue)
1399
+ );
1337
1400
  }
1338
1401
  getFlagDetail(name) {
1339
- return this.engine.getFlagDetail(name, this.attributes);
1402
+ return safeRun(
1403
+ "Client.getFlagDetail",
1404
+ { value: false, reason: "CLIENT_NOT_READY" },
1405
+ () => this.engine.getFlagDetail(name, this.attributes)
1406
+ );
1340
1407
  }
1341
1408
  getConfig(name, decodeOrOpts) {
1342
- return this.engine.getConfig(name, decodeOrOpts);
1409
+ return safeRun(
1410
+ "Client.getConfig",
1411
+ void 0,
1412
+ () => this.engine.getConfig(name, decodeOrOpts)
1413
+ );
1343
1414
  }
1344
1415
  getExperiment(name, defaultParams, decode) {
1345
- return this.engine.getExperiment(name, this.attributes, defaultParams, decode);
1416
+ const notIn = { inExperiment: false, group: "control", params: defaultParams };
1417
+ return safeRun(
1418
+ "Client.getExperiment",
1419
+ notIn,
1420
+ () => this.engine.getExperiment(name, this.attributes, defaultParams, decode)
1421
+ );
1346
1422
  }
1347
1423
  /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
1348
1424
  getKillswitch(name, switchKey) {
1349
- return this.engine.getKillswitch(name, switchKey);
1425
+ return safeRun("Client.getKillswitch", false, () => this.engine.getKillswitch(name, switchKey));
1350
1426
  }
1351
1427
  /**
1352
1428
  * Record a conversion/metric event for the bound user. Derives the unit from
@@ -1356,9 +1432,11 @@ var Client = class {
1356
1432
  * mode.
1357
1433
  */
1358
1434
  track(eventName, props) {
1359
- const id = this.attributes.user_id ?? this.attributes.anonymous_id;
1360
- if (id === void 0) return;
1361
- this.engine.track(String(id), eventName, props);
1435
+ safeRun("Client.track", void 0, () => {
1436
+ const id = this.attributes.user_id ?? this.attributes.anonymous_id;
1437
+ if (id === void 0) return;
1438
+ this.engine.track(String(id), eventName, props);
1439
+ });
1362
1440
  }
1363
1441
  /**
1364
1442
  * Emit an exposure event for `name` at this server-side decision point for the
@@ -1367,12 +1445,12 @@ var Client = class {
1367
1445
  * in test mode).
1368
1446
  */
1369
1447
  logExposure(name) {
1370
- this.engine.logExposure(this.attributes, name);
1448
+ safeRun("Client.logExposure", void 0, () => this.engine.logExposure(this.attributes, name));
1371
1449
  }
1372
1450
  };
1373
1451
  function dispatchSee(problem, consequence, extras, kind) {
1374
1452
  if (!_server) {
1375
- console.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
1453
+ logger.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
1376
1454
  return;
1377
1455
  }
1378
1456
  _server.reportError(problem, consequence, extras, kind);
@@ -1389,6 +1467,7 @@ export {
1389
1467
  Client,
1390
1468
  Engine,
1391
1469
  FLAG_REASONS,
1470
+ LOG_LEVELS,
1392
1471
  _murmur3ForTests,
1393
1472
  _resetConfigureForTests,
1394
1473
  _resetShipeasyServerForTests,