@shipeasy/sdk 6.0.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,10 +34,14 @@ __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,
41
+ clearOverrides: () => clearOverrides,
40
42
  configure: () => configure,
43
+ configureForOffline: () => configureForOffline,
44
+ configureForTesting: () => configureForTesting,
41
45
  configureShipeasyServer: () => configureShipeasyServer,
42
46
  createInMemoryStickyStore: () => createInMemoryStickyStore,
43
47
  fetchLabelsForSSR: () => fetchLabelsForSSR,
@@ -47,6 +51,10 @@ __export(server_exports, {
47
51
  getShipeasyServerClient: () => getShipeasyServerClient,
48
52
  i18n: () => i18n,
49
53
  isExpected: () => isExpected,
54
+ onChange: () => onChange,
55
+ overrideConfig: () => overrideConfig,
56
+ overrideExperiment: () => overrideExperiment,
57
+ overrideFlag: () => overrideFlag,
50
58
  see: () => see,
51
59
  seeContext: () => seeContext,
52
60
  shipeasy: () => shipeasy,
@@ -114,6 +122,53 @@ function send(url) {
114
122
  }
115
123
  }
116
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
+
117
172
  // src/see/core.ts
118
173
  var SEE_MAX_MESSAGE = 500;
119
174
  var SEE_MAX_STACK = 8e3;
@@ -575,6 +630,7 @@ var Engine = class _Engine {
575
630
  // 304). Never fired in testMode/offline (no polling happens there).
576
631
  changeListeners = /* @__PURE__ */ new Set();
577
632
  constructor(opts) {
633
+ setLogLevel(opts.logLevel);
578
634
  this.apiKey = opts.apiKey;
579
635
  this.baseUrl = (opts.baseUrl ?? "https://cdn.shipeasy.ai").replace(/\/$/, "");
580
636
  this.env = opts.env ?? "prod";
@@ -698,14 +754,14 @@ var Engine = class _Engine {
698
754
  try {
699
755
  l();
700
756
  } catch (err) {
701
- console.warn("[shipeasy] onChange listener threw:", String(err));
757
+ logger.warn("[shipeasy] onChange listener threw:", String(err));
702
758
  }
703
759
  }
704
760
  }
705
761
  startPoll() {
706
762
  this.timer = setInterval(() => {
707
763
  this.fetchAll(true).catch(
708
- (err) => console.warn("[shipeasy] background poll failed:", String(err))
764
+ (err) => logger.warn("[shipeasy] background poll failed:", String(err))
709
765
  );
710
766
  }, this.pollInterval * 1e3);
711
767
  }
@@ -784,7 +840,12 @@ var Engine = class _Engine {
784
840
  return "defaultValue" in opts ? opts.defaultValue : void 0;
785
841
  }
786
842
  if (!opts.decode) return raw;
787
- 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
+ }
788
849
  }
789
850
  getExperiment(name, user, defaultParams, decode) {
790
851
  this.telemetry.emit("experiment", name);
@@ -799,7 +860,7 @@ var Engine = class _Engine {
799
860
  try {
800
861
  return { inExperiment: true, group: ov.group, params: decode(ov.params) };
801
862
  } catch (err) {
802
- console.warn(`[shipeasy] getExperiment('${name}') override decode failed:`, String(err));
863
+ logger.warn(`[shipeasy] getExperiment('${name}') override decode failed:`, String(err));
803
864
  return notIn;
804
865
  }
805
866
  }
@@ -824,7 +885,7 @@ var Engine = class _Engine {
824
885
  try {
825
886
  return { inExperiment: true, group: g.name, params: decode(g.params) };
826
887
  } catch (err) {
827
- console.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
888
+ logger.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
828
889
  return notIn;
829
890
  }
830
891
  };
@@ -876,7 +937,7 @@ var Engine = class _Engine {
876
937
  method: "POST",
877
938
  headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
878
939
  body
879
- }).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
940
+ }).catch((err) => logger.warn("[shipeasy] track failed:", String(err)));
880
941
  }
881
942
  /**
882
943
  * Emit an exposure event for an experiment at the server-side decision point
@@ -907,7 +968,7 @@ var Engine = class _Engine {
907
968
  method: "POST",
908
969
  headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
909
970
  body
910
- }).catch((err) => console.warn("[shipeasy] logExposure failed:", String(err)));
971
+ }).catch((err) => logger.warn("[shipeasy] logExposure failed:", String(err)));
911
972
  }
912
973
  /**
913
974
  * Report a structured error into the errors primitive. Fire-and-forget —
@@ -927,7 +988,7 @@ var Engine = class _Engine {
927
988
  method: "POST",
928
989
  headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
929
990
  body: JSON.stringify({ events: [ev] })
930
- }).catch((err) => console.warn("[shipeasy] see() send failed:", String(err)));
991
+ }).catch((err) => logger.warn("[shipeasy] see() send failed:", String(err)));
931
992
  } catch {
932
993
  }
933
994
  }
@@ -981,7 +1042,11 @@ var Engine = class _Engine {
981
1042
  const ks = this.flagsBlob?.killswitches?.[name];
982
1043
  if (!ks) return false;
983
1044
  if (switchKey === void 0) return isEnabled(ks.killed);
984
- return isEnabled(ks.switches?.[switchKey]);
1045
+ const switches = ks.switches ?? {};
1046
+ if (Object.prototype.hasOwnProperty.call(switches, switchKey)) {
1047
+ return isEnabled(switches[switchKey]);
1048
+ }
1049
+ return isEnabled(ks.killed);
985
1050
  }
986
1051
  };
987
1052
  var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
@@ -1110,7 +1175,7 @@ function _resetShipeasyServerForTests() {
1110
1175
  async function shipeasy(opts) {
1111
1176
  const serverKey = opts.serverKey ?? "";
1112
1177
  if (!serverKey) {
1113
- console.error(
1178
+ logger.error(
1114
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."
1115
1180
  );
1116
1181
  }
@@ -1118,7 +1183,8 @@ async function shipeasy(opts) {
1118
1183
  flags.configure({
1119
1184
  apiKey: serverKey,
1120
1185
  disableTelemetry: opts.disableTelemetry,
1121
- privateAttributes: opts.privateAttributes
1186
+ privateAttributes: opts.privateAttributes,
1187
+ logLevel: opts.logLevel
1122
1188
  });
1123
1189
  let resolvedUrlOverrides = opts.urlOverrides;
1124
1190
  if (!resolvedUrlOverrides) {
@@ -1244,21 +1310,30 @@ var flags = {
1244
1310
  _server?.destroy();
1245
1311
  },
1246
1312
  get(name, user, defaultValue = false) {
1247
- return _server?.getFlag(name, user, defaultValue) ?? defaultValue;
1313
+ return safeRun("flags.get", defaultValue, () => _server?.getFlag(name, user, defaultValue) ?? defaultValue);
1248
1314
  },
1249
1315
  /** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
1250
1316
  getDetail(name, user) {
1251
- 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
+ );
1252
1322
  },
1253
1323
  getConfig(name, decodeOrOpts) {
1254
- return _server?.getConfig(name, decodeOrOpts);
1324
+ return safeRun(
1325
+ "flags.getConfig",
1326
+ void 0,
1327
+ () => _server?.getConfig(name, decodeOrOpts)
1328
+ );
1255
1329
  },
1256
1330
  getExperiment(name, user, defaultParams, decode) {
1257
- return _server?.getExperiment(name, user, defaultParams, decode) ?? {
1258
- inExperiment: false,
1259
- group: "control",
1260
- params: defaultParams
1261
- };
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
+ );
1262
1337
  },
1263
1338
  /**
1264
1339
  * Read a killswitch. Without `switchKey`, returns true when the whole
@@ -1266,15 +1341,15 @@ var flags = {
1266
1341
  * switch is on. Unknown killswitches / switches return false.
1267
1342
  */
1268
1343
  ks(name, switchKey) {
1269
- return _server?.getKillswitch(name, switchKey) ?? false;
1344
+ return safeRun("flags.ks", false, () => _server?.getKillswitch(name, switchKey) ?? false);
1270
1345
  },
1271
1346
  track(userId, eventName, props) {
1272
- _server?.track(userId, eventName, props);
1347
+ safeRun("flags.track", void 0, () => _server?.track(userId, eventName, props));
1273
1348
  },
1274
1349
  /** Emit an exposure for an enrolled experiment at the decision point. See
1275
1350
  * {@link Engine.logExposure}. No-op before configure(). */
1276
1351
  logExposure(user, name) {
1277
- _server?.logExposure(user, name);
1352
+ safeRun("flags.logExposure", void 0, () => _server?.logExposure(user, name));
1278
1353
  },
1279
1354
  /**
1280
1355
  * Evaluate all flags / configs / experiments for a user against the locally
@@ -1282,27 +1357,81 @@ var flags = {
1282
1357
  * overrides. Returns an empty payload when the blob hasn't been fetched yet.
1283
1358
  */
1284
1359
  evaluate(user, rawUrl) {
1285
- return _server?.evaluate(user, rawUrl) ?? {
1286
- flags: {},
1287
- configs: {},
1288
- experiments: {},
1289
- killswitches: {}
1290
- };
1360
+ const empty = { flags: {}, configs: {}, experiments: {}, killswitches: {} };
1361
+ return safeRun("flags.evaluate", empty, () => _server?.evaluate(user, rawUrl) ?? empty);
1291
1362
  }
1292
1363
  };
1293
1364
  var _identityAttributes = (user) => user && typeof user === "object" ? user : {};
1294
1365
  var _attributes = _identityAttributes;
1295
1366
  function configure(opts) {
1296
- const { attributes, ...engineOpts } = opts;
1367
+ const { attributes, poll = false, init = true, ...engineOpts } = opts;
1297
1368
  _attributes = attributes ?? _identityAttributes;
1298
1369
  const engine = configureShipeasyServer(engineOpts);
1299
- void engine.initOnce().catch(() => {
1300
- });
1370
+ if (poll) {
1371
+ void engine.init().catch(() => {
1372
+ });
1373
+ } else if (init) {
1374
+ void engine.initOnce().catch(() => {
1375
+ });
1376
+ }
1301
1377
  return engine;
1302
1378
  }
1303
1379
  function _resetConfigureForTests() {
1304
1380
  _attributes = _identityAttributes;
1305
1381
  }
1382
+ function _installGlobalEngine(engine, attributes) {
1383
+ _server?.destroy();
1384
+ _server = engine;
1385
+ _attributes = attributes ?? _identityAttributes;
1386
+ return engine;
1387
+ }
1388
+ function _applyOverrides(engine, flags2, configs, experiments) {
1389
+ for (const [name, value] of Object.entries(flags2 ?? {})) engine.overrideFlag(name, value);
1390
+ for (const [name, value] of Object.entries(configs ?? {})) engine.overrideConfig(name, value);
1391
+ for (const [name, [group, params]] of Object.entries(experiments ?? {}))
1392
+ engine.overrideExperiment(name, group, params);
1393
+ }
1394
+ function configureForTesting(opts = {}) {
1395
+ const engine = Engine.forTesting();
1396
+ _applyOverrides(engine, opts.flags, opts.configs, opts.experiments);
1397
+ return _installGlobalEngine(engine, opts.attributes);
1398
+ }
1399
+ function configureForOffline(opts) {
1400
+ let engine;
1401
+ if (opts.path !== void 0) {
1402
+ engine = Engine.fromFile(opts.path);
1403
+ } else if (opts.snapshot !== void 0) {
1404
+ engine = Engine.fromSnapshot(opts.snapshot);
1405
+ } else {
1406
+ throw new Error("[shipeasy] configureForOffline requires either { snapshot } or { path }");
1407
+ }
1408
+ _applyOverrides(engine, opts.flags, opts.configs, opts.experiments);
1409
+ return _installGlobalEngine(engine, opts.attributes);
1410
+ }
1411
+ function _requireGlobal(fn) {
1412
+ const engine = getShipeasyServerClient();
1413
+ if (!engine) {
1414
+ throw new Error(
1415
+ `[shipeasy] ${fn}(...) called before configure({ apiKey }) (or a configureFor* sibling).`
1416
+ );
1417
+ }
1418
+ return engine;
1419
+ }
1420
+ function overrideFlag(name, value) {
1421
+ _requireGlobal("overrideFlag").overrideFlag(name, value);
1422
+ }
1423
+ function overrideConfig(name, value) {
1424
+ _requireGlobal("overrideConfig").overrideConfig(name, value);
1425
+ }
1426
+ function overrideExperiment(name, group, params) {
1427
+ _requireGlobal("overrideExperiment").overrideExperiment(name, group, params);
1428
+ }
1429
+ function clearOverrides() {
1430
+ _requireGlobal("clearOverrides").clearOverrides();
1431
+ }
1432
+ function onChange(listener) {
1433
+ return _requireGlobal("onChange").onChange(listener);
1434
+ }
1306
1435
  var Client = class {
1307
1436
  engine;
1308
1437
  /** The resolved attribute bag this handle evaluates against. */
@@ -1318,25 +1447,65 @@ var Client = class {
1318
1447
  this.attributes = _attributes(user);
1319
1448
  }
1320
1449
  getFlag(name, defaultValue = false) {
1321
- 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
+ );
1322
1455
  }
1323
1456
  getFlagDetail(name) {
1324
- 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
+ );
1325
1462
  }
1326
1463
  getConfig(name, decodeOrOpts) {
1327
- return this.engine.getConfig(name, decodeOrOpts);
1464
+ return safeRun(
1465
+ "Client.getConfig",
1466
+ void 0,
1467
+ () => this.engine.getConfig(name, decodeOrOpts)
1468
+ );
1328
1469
  }
1329
1470
  getExperiment(name, defaultParams, decode) {
1330
- 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
+ );
1331
1477
  }
1332
1478
  /** Read a killswitch (not user-bound; mirrors {@link Engine.getKillswitch}). */
1333
1479
  getKillswitch(name, switchKey) {
1334
- return this.engine.getKillswitch(name, switchKey);
1480
+ return safeRun("Client.getKillswitch", false, () => this.engine.getKillswitch(name, switchKey));
1481
+ }
1482
+ /**
1483
+ * Record a conversion/metric event for the bound user. Derives the unit from
1484
+ * the resolved attribute bag (`user_id`, else `anonymous_id`) and delegates to
1485
+ * {@link Engine.track} — so an experiment is end-to-end Client-only (no need to
1486
+ * drop down to the Engine to log a conversion). Fire-and-forget; no-op in test
1487
+ * mode.
1488
+ */
1489
+ track(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
+ });
1495
+ }
1496
+ /**
1497
+ * Emit an exposure event for `name` at this server-side decision point for the
1498
+ * bound user. Delegates to {@link Engine.logExposure} with the resolved
1499
+ * attribute bag (re-evaluates enrolment; no-op when the user isn't enrolled or
1500
+ * in test mode).
1501
+ */
1502
+ logExposure(name) {
1503
+ safeRun("Client.logExposure", void 0, () => this.engine.logExposure(this.attributes, name));
1335
1504
  }
1336
1505
  };
1337
1506
  function dispatchSee(problem, consequence, extras, kind) {
1338
1507
  if (!_server) {
1339
- console.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
1508
+ logger.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
1340
1509
  return;
1341
1510
  }
1342
1511
  _server.reportError(problem, consequence, extras, kind);
@@ -1354,10 +1523,14 @@ var see = Object.assign(
1354
1523
  Client,
1355
1524
  Engine,
1356
1525
  FLAG_REASONS,
1526
+ LOG_LEVELS,
1357
1527
  _murmur3ForTests,
1358
1528
  _resetConfigureForTests,
1359
1529
  _resetShipeasyServerForTests,
1530
+ clearOverrides,
1360
1531
  configure,
1532
+ configureForOffline,
1533
+ configureForTesting,
1361
1534
  configureShipeasyServer,
1362
1535
  createInMemoryStickyStore,
1363
1536
  fetchLabelsForSSR,
@@ -1367,6 +1540,10 @@ var see = Object.assign(
1367
1540
  getShipeasyServerClient,
1368
1541
  i18n,
1369
1542
  isExpected,
1543
+ onChange,
1544
+ overrideConfig,
1545
+ overrideExperiment,
1546
+ overrideFlag,
1370
1547
  see,
1371
1548
  seeContext,
1372
1549
  shipeasy,