@shipeasy/sdk 7.0.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -280,10 +280,26 @@ interface EngineOptions {
280
280
  /** Which published env to read values from. Defaults to "prod". */
281
281
  env?: EngineEnv;
282
282
  /**
283
- * Per-evaluation usage telemetry. ON by default each getFlag/getConfig/
283
+ * Master network switch when `false`, the browser SDK makes NO outbound
284
+ * requests at all: identify()/init never call /sdk/evaluate (getters read code
285
+ * defaults / overrides), track() and exposure logging are no-ops, usage
286
+ * telemetry is off, and SDK-internal error self-monitoring is off.
287
+ *
288
+ * DEFAULT is environment-derived (see {@link isProductionEnv}): ON in
289
+ * production, OFF everywhere else. In the browser there is usually no native
290
+ * `NODE_ENV`, so production is taken from the `env` option above (default
291
+ * `"prod"`) — meaning it defaults ON. Pass `false` (or set `env: "dev"`) to
292
+ * keep a local build quiet.
293
+ */
294
+ isNetworkEnabled?: boolean;
295
+ /**
296
+ * Per-evaluation usage telemetry ("tracking") — each getFlag/getConfig/
284
297
  * getExperiment/getKillswitch call fires one fire-and-forget sendBeacon so
285
- * usage is counted by Cloudflare's native per-path analytics. Pass `true` to
286
- * disable entirely.
298
+ * usage is counted by Cloudflare's native per-path analytics.
299
+ *
300
+ * DEFAULT is environment-derived: ON in production, OFF everywhere else (same
301
+ * inference as {@link isNetworkEnabled}). Pass `true` to force it off, `false`
302
+ * to force it on. Forced off whenever `isNetworkEnabled` is false.
287
303
  */
288
304
  disableTelemetry?: boolean;
289
305
  /** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
@@ -365,6 +381,8 @@ declare class Engine {
365
381
  private overrideListenerInstalled;
366
382
  private identifySeq;
367
383
  private readonly testMode;
384
+ private readonly networkEnabled;
385
+ private readonly offline;
368
386
  private readonly flagOverrides;
369
387
  private readonly configOverrides;
370
388
  private readonly experimentOverrides;
@@ -517,6 +535,12 @@ interface ShipeasyClientConfig {
517
535
  clientKey: string;
518
536
  /** Override the ShipEasy CDN/edge base URL. Defaults to https://cdn.shipeasy.ai. */
519
537
  baseUrl?: string;
538
+ /**
539
+ * Which published env to read values from — also the fallback signal for the
540
+ * environment-derived egress defaults (see {@link isNetworkEnabled}) when the
541
+ * browser exposes no native `NODE_ENV`. Defaults to `"prod"`.
542
+ */
543
+ env?: EngineEnv;
520
544
  /** Override the admin URL for the devtools overlay (dev use). */
521
545
  adminUrl?: string;
522
546
  /**
@@ -569,9 +593,18 @@ interface ShipeasyClientConfig {
569
593
  */
570
594
  logLevel?: LogLevel;
571
595
  /**
572
- * Disable per-evaluation usage telemetry. Telemetry is ON by default every
596
+ * Master network switch `false` puts the browser SDK fully offline (no
597
+ * /sdk/evaluate, no /collect, no telemetry, no error self-monitoring).
598
+ * Defaults to environment-derived: ON in production, OFF everywhere else
599
+ * (in the browser, "production" comes from `env`, default `"prod"`). See
600
+ * {@link EngineOptions.isNetworkEnabled}.
601
+ */
602
+ isNetworkEnabled?: boolean;
603
+ /**
604
+ * Disable per-evaluation usage telemetry ("tracking"). Defaults to
605
+ * environment-derived: ON in production, OFF everywhere else. Every
573
606
  * flag/config/experiment/killswitch read fires one fire-and-forget beacon
574
- * counted by Cloudflare's native per-path analytics. Pass `true` to opt out.
607
+ * counted by Cloudflare's native per-path analytics. Pass `true` to force off.
575
608
  */
576
609
  disableTelemetry?: boolean;
577
610
  /**
@@ -280,10 +280,26 @@ interface EngineOptions {
280
280
  /** Which published env to read values from. Defaults to "prod". */
281
281
  env?: EngineEnv;
282
282
  /**
283
- * Per-evaluation usage telemetry. ON by default each getFlag/getConfig/
283
+ * Master network switch when `false`, the browser SDK makes NO outbound
284
+ * requests at all: identify()/init never call /sdk/evaluate (getters read code
285
+ * defaults / overrides), track() and exposure logging are no-ops, usage
286
+ * telemetry is off, and SDK-internal error self-monitoring is off.
287
+ *
288
+ * DEFAULT is environment-derived (see {@link isProductionEnv}): ON in
289
+ * production, OFF everywhere else. In the browser there is usually no native
290
+ * `NODE_ENV`, so production is taken from the `env` option above (default
291
+ * `"prod"`) — meaning it defaults ON. Pass `false` (or set `env: "dev"`) to
292
+ * keep a local build quiet.
293
+ */
294
+ isNetworkEnabled?: boolean;
295
+ /**
296
+ * Per-evaluation usage telemetry ("tracking") — each getFlag/getConfig/
284
297
  * getExperiment/getKillswitch call fires one fire-and-forget sendBeacon so
285
- * usage is counted by Cloudflare's native per-path analytics. Pass `true` to
286
- * disable entirely.
298
+ * usage is counted by Cloudflare's native per-path analytics.
299
+ *
300
+ * DEFAULT is environment-derived: ON in production, OFF everywhere else (same
301
+ * inference as {@link isNetworkEnabled}). Pass `true` to force it off, `false`
302
+ * to force it on. Forced off whenever `isNetworkEnabled` is false.
287
303
  */
288
304
  disableTelemetry?: boolean;
289
305
  /** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
@@ -365,6 +381,8 @@ declare class Engine {
365
381
  private overrideListenerInstalled;
366
382
  private identifySeq;
367
383
  private readonly testMode;
384
+ private readonly networkEnabled;
385
+ private readonly offline;
368
386
  private readonly flagOverrides;
369
387
  private readonly configOverrides;
370
388
  private readonly experimentOverrides;
@@ -517,6 +535,12 @@ interface ShipeasyClientConfig {
517
535
  clientKey: string;
518
536
  /** Override the ShipEasy CDN/edge base URL. Defaults to https://cdn.shipeasy.ai. */
519
537
  baseUrl?: string;
538
+ /**
539
+ * Which published env to read values from — also the fallback signal for the
540
+ * environment-derived egress defaults (see {@link isNetworkEnabled}) when the
541
+ * browser exposes no native `NODE_ENV`. Defaults to `"prod"`.
542
+ */
543
+ env?: EngineEnv;
520
544
  /** Override the admin URL for the devtools overlay (dev use). */
521
545
  adminUrl?: string;
522
546
  /**
@@ -569,9 +593,18 @@ interface ShipeasyClientConfig {
569
593
  */
570
594
  logLevel?: LogLevel;
571
595
  /**
572
- * Disable per-evaluation usage telemetry. Telemetry is ON by default every
596
+ * Master network switch `false` puts the browser SDK fully offline (no
597
+ * /sdk/evaluate, no /collect, no telemetry, no error self-monitoring).
598
+ * Defaults to environment-derived: ON in production, OFF everywhere else
599
+ * (in the browser, "production" comes from `env`, default `"prod"`). See
600
+ * {@link EngineOptions.isNetworkEnabled}.
601
+ */
602
+ isNetworkEnabled?: boolean;
603
+ /**
604
+ * Disable per-evaluation usage telemetry ("tracking"). Defaults to
605
+ * environment-derived: ON in production, OFF everywhere else. Every
573
606
  * flag/config/experiment/killswitch read fires one fire-and-forget beacon
574
- * counted by Cloudflare's native per-path analytics. Pass `true` to opt out.
607
+ * counted by Cloudflare's native per-path analytics. Pass `true` to force off.
575
608
  */
576
609
  disableTelemetry?: boolean;
577
610
  /**
@@ -110,6 +110,24 @@ function send(url) {
110
110
  }
111
111
  }
112
112
 
113
+ // src/env.ts
114
+ function isProductionEnv(configuredEnv) {
115
+ const native = readNativeEnv();
116
+ if (native !== null) return native === "production" || native === "prod";
117
+ return (configuredEnv ?? "prod").toLowerCase() === "prod";
118
+ }
119
+ function readNativeEnv() {
120
+ try {
121
+ const proc = globalThis.process;
122
+ const raw = proc?.env?.SHIPEASY_ENV ?? proc?.env?.NODE_ENV;
123
+ if (typeof raw !== "string") return null;
124
+ const v = raw.trim().toLowerCase();
125
+ return v.length ? v : null;
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+
113
131
  // src/see/core.ts
114
132
  var SEE_MAX_MESSAGE = 500;
115
133
  var SEE_MAX_STACK = 8e3;
@@ -1072,6 +1090,12 @@ var Engine = class _Engine {
1072
1090
  // never fetches, track() is a no-op, telemetry is off, and the client is
1073
1091
  // already ready with an empty eval result.
1074
1092
  testMode;
1093
+ // Master network gate — false ⇒ the SDK never touches the network. Defaults to
1094
+ // environment-derived (prod-on), forced false by testMode.
1095
+ networkEnabled;
1096
+ // Convenience inverse of networkEnabled — the "no network at all" state every
1097
+ // /sdk/evaluate, /collect and telemetry gate keys on. testMode implies offline.
1098
+ offline;
1075
1099
  // Programmatic overrides (Statsig-style). Set on any client via
1076
1100
  // overrideFlag/overrideConfig/overrideExperiment; they win over BOTH the URL
1077
1101
  // overrides and the fetched eval result. Cleared by clearOverrides().
@@ -1088,10 +1112,13 @@ var Engine = class _Engine {
1088
1112
  this.baseUrl = (opts.baseUrl ?? "https://api.shipeasy.ai").replace(/\/$/, "");
1089
1113
  this.env = opts.env ?? "prod";
1090
1114
  this.testMode = opts.testMode === true;
1115
+ const prod = isProductionEnv(this.env);
1116
+ this.networkEnabled = this.testMode ? false : opts.isNetworkEnabled ?? prod;
1117
+ this.offline = !this.networkEnabled;
1091
1118
  setInternalReportContext({
1092
1119
  side: "client",
1093
1120
  sdkVersion: version,
1094
- enabled: !this.testMode && opts.disableInternalErrorReporting !== true
1121
+ enabled: this.networkEnabled && opts.disableInternalErrorReporting !== true
1095
1122
  });
1096
1123
  this.autoGuardrails = opts.autoGuardrails !== false;
1097
1124
  this.autoCollectAlways = opts.autoCollectAlways === true;
@@ -1105,7 +1132,7 @@ var Engine = class _Engine {
1105
1132
  engagement: g.engagement ?? this.autoGuardrails
1106
1133
  };
1107
1134
  this.anonId = getOrCreateAnonId();
1108
- if (opts.anonymousStore && !this.testMode) {
1135
+ if (opts.anonymousStore && !this.offline) {
1109
1136
  this.anonReady = this.hydrateAnonFromStore(opts.anonymousStore);
1110
1137
  }
1111
1138
  this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
@@ -1114,10 +1141,11 @@ var Engine = class _Engine {
1114
1141
  sdkKey: this.sdkKey,
1115
1142
  side: "client",
1116
1143
  env: this.env,
1117
- // Test mode never talks to the network telemetry off regardless of opt.
1118
- disabled: this.testMode || opts.disableTelemetry
1144
+ // Off when the network is disabled; otherwise honour an explicit
1145
+ // disableTelemetry, else default to prod-on (off outside production).
1146
+ disabled: this.offline || (opts.disableTelemetry ?? !prod)
1119
1147
  });
1120
- if (this.testMode) {
1148
+ if (this.offline) {
1121
1149
  this.evalResult = { flags: {}, configs: {}, experiments: {}, killswitches: {} };
1122
1150
  } else {
1123
1151
  void this.buffer.flushPendingAlias();
@@ -1162,7 +1190,7 @@ var Engine = class _Engine {
1162
1190
  });
1163
1191
  }
1164
1192
  async identify(user) {
1165
- if (this.testMode) {
1193
+ if (this.offline) {
1166
1194
  if (user.user_id !== void 0) this.userId = user.user_id;
1167
1195
  this.notify();
1168
1196
  return;
@@ -1224,6 +1252,7 @@ var Engine = class _Engine {
1224
1252
  * the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
1225
1253
  */
1226
1254
  reportError(problem, consequence, extras, kind, correlationId) {
1255
+ if (this.offline) return;
1227
1256
  try {
1228
1257
  const enriched = { ...collectSeeEnv(), ...extras };
1229
1258
  const ev = buildSeeEvent(problem, consequence, enriched, {
@@ -1396,7 +1425,7 @@ var Engine = class _Engine {
1396
1425
  return bridge;
1397
1426
  }
1398
1427
  track(eventName, props) {
1399
- if (this.testMode) return;
1428
+ if (this.offline) return;
1400
1429
  this.buffer.pushMetric(eventName, this.userId, this.anonId, this.stripPrivate(props));
1401
1430
  }
1402
1431
  /** Drop caller-marked private attributes from an outbound props bag. */
@@ -1549,10 +1578,12 @@ function shipeasy(opts) {
1549
1578
  const client = configureShipeasy({
1550
1579
  sdkKey: opts.clientKey,
1551
1580
  baseUrl,
1581
+ env: opts.env,
1552
1582
  logLevel: opts.logLevel,
1553
1583
  autoGuardrails: blanket,
1554
1584
  autoGuardrailGroups: groups,
1555
1585
  autoCollectAlways: acObj?.always === true,
1586
+ isNetworkEnabled: opts.isNetworkEnabled,
1556
1587
  disableTelemetry: opts.disableTelemetry,
1557
1588
  disableInternalErrorReporting: opts.disableInternalErrorReporting,
1558
1589
  disableAutoExposure: opts.disableAutoExposure,
@@ -57,6 +57,24 @@ function send(url) {
57
57
  }
58
58
  }
59
59
 
60
+ // src/env.ts
61
+ function isProductionEnv(configuredEnv) {
62
+ const native = readNativeEnv();
63
+ if (native !== null) return native === "production" || native === "prod";
64
+ return (configuredEnv ?? "prod").toLowerCase() === "prod";
65
+ }
66
+ function readNativeEnv() {
67
+ try {
68
+ const proc = globalThis.process;
69
+ const raw = proc?.env?.SHIPEASY_ENV ?? proc?.env?.NODE_ENV;
70
+ if (typeof raw !== "string") return null;
71
+ const v = raw.trim().toLowerCase();
72
+ return v.length ? v : null;
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
60
78
  // src/see/core.ts
61
79
  var SEE_MAX_MESSAGE = 500;
62
80
  var SEE_MAX_STACK = 8e3;
@@ -1019,6 +1037,12 @@ var Engine = class _Engine {
1019
1037
  // never fetches, track() is a no-op, telemetry is off, and the client is
1020
1038
  // already ready with an empty eval result.
1021
1039
  testMode;
1040
+ // Master network gate — false ⇒ the SDK never touches the network. Defaults to
1041
+ // environment-derived (prod-on), forced false by testMode.
1042
+ networkEnabled;
1043
+ // Convenience inverse of networkEnabled — the "no network at all" state every
1044
+ // /sdk/evaluate, /collect and telemetry gate keys on. testMode implies offline.
1045
+ offline;
1022
1046
  // Programmatic overrides (Statsig-style). Set on any client via
1023
1047
  // overrideFlag/overrideConfig/overrideExperiment; they win over BOTH the URL
1024
1048
  // overrides and the fetched eval result. Cleared by clearOverrides().
@@ -1035,10 +1059,13 @@ var Engine = class _Engine {
1035
1059
  this.baseUrl = (opts.baseUrl ?? "https://api.shipeasy.ai").replace(/\/$/, "");
1036
1060
  this.env = opts.env ?? "prod";
1037
1061
  this.testMode = opts.testMode === true;
1062
+ const prod = isProductionEnv(this.env);
1063
+ this.networkEnabled = this.testMode ? false : opts.isNetworkEnabled ?? prod;
1064
+ this.offline = !this.networkEnabled;
1038
1065
  setInternalReportContext({
1039
1066
  side: "client",
1040
1067
  sdkVersion: version,
1041
- enabled: !this.testMode && opts.disableInternalErrorReporting !== true
1068
+ enabled: this.networkEnabled && opts.disableInternalErrorReporting !== true
1042
1069
  });
1043
1070
  this.autoGuardrails = opts.autoGuardrails !== false;
1044
1071
  this.autoCollectAlways = opts.autoCollectAlways === true;
@@ -1052,7 +1079,7 @@ var Engine = class _Engine {
1052
1079
  engagement: g.engagement ?? this.autoGuardrails
1053
1080
  };
1054
1081
  this.anonId = getOrCreateAnonId();
1055
- if (opts.anonymousStore && !this.testMode) {
1082
+ if (opts.anonymousStore && !this.offline) {
1056
1083
  this.anonReady = this.hydrateAnonFromStore(opts.anonymousStore);
1057
1084
  }
1058
1085
  this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
@@ -1061,10 +1088,11 @@ var Engine = class _Engine {
1061
1088
  sdkKey: this.sdkKey,
1062
1089
  side: "client",
1063
1090
  env: this.env,
1064
- // Test mode never talks to the network telemetry off regardless of opt.
1065
- disabled: this.testMode || opts.disableTelemetry
1091
+ // Off when the network is disabled; otherwise honour an explicit
1092
+ // disableTelemetry, else default to prod-on (off outside production).
1093
+ disabled: this.offline || (opts.disableTelemetry ?? !prod)
1066
1094
  });
1067
- if (this.testMode) {
1095
+ if (this.offline) {
1068
1096
  this.evalResult = { flags: {}, configs: {}, experiments: {}, killswitches: {} };
1069
1097
  } else {
1070
1098
  void this.buffer.flushPendingAlias();
@@ -1109,7 +1137,7 @@ var Engine = class _Engine {
1109
1137
  });
1110
1138
  }
1111
1139
  async identify(user) {
1112
- if (this.testMode) {
1140
+ if (this.offline) {
1113
1141
  if (user.user_id !== void 0) this.userId = user.user_id;
1114
1142
  this.notify();
1115
1143
  return;
@@ -1171,6 +1199,7 @@ var Engine = class _Engine {
1171
1199
  * the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
1172
1200
  */
1173
1201
  reportError(problem, consequence, extras, kind, correlationId) {
1202
+ if (this.offline) return;
1174
1203
  try {
1175
1204
  const enriched = { ...collectSeeEnv(), ...extras };
1176
1205
  const ev = buildSeeEvent(problem, consequence, enriched, {
@@ -1343,7 +1372,7 @@ var Engine = class _Engine {
1343
1372
  return bridge;
1344
1373
  }
1345
1374
  track(eventName, props) {
1346
- if (this.testMode) return;
1375
+ if (this.offline) return;
1347
1376
  this.buffer.pushMetric(eventName, this.userId, this.anonId, this.stripPrivate(props));
1348
1377
  }
1349
1378
  /** Drop caller-marked private attributes from an outbound props bag. */
@@ -1496,10 +1525,12 @@ function shipeasy(opts) {
1496
1525
  const client = configureShipeasy({
1497
1526
  sdkKey: opts.clientKey,
1498
1527
  baseUrl,
1528
+ env: opts.env,
1499
1529
  logLevel: opts.logLevel,
1500
1530
  autoGuardrails: blanket,
1501
1531
  autoGuardrailGroups: groups,
1502
1532
  autoCollectAlways: acObj?.always === true,
1533
+ isNetworkEnabled: opts.isNetworkEnabled,
1503
1534
  disableTelemetry: opts.disableTelemetry,
1504
1535
  disableInternalErrorReporting: opts.disableInternalErrorReporting,
1505
1536
  disableAutoExposure: opts.disableAutoExposure,
@@ -201,12 +201,31 @@ interface EngineOptions {
201
201
  */
202
202
  initialBlob?: FlagsBlob;
203
203
  /**
204
- * Per-evaluation usage telemetry. ON by default each getFlag/getConfig/
205
- * getExperiment/getKillswitch (and the per-key evaluate() loop) fires one
206
- * fire-and-forget beacon counted by Cloudflare's native per-path analytics.
207
- * Pass `true` to disable. NOTE: on Cloudflare Workers each beacon is an
208
- * outbound subrequest (cap 50 free / 1000 paid per invocation), so disable
209
- * this on hot request paths that evaluate many flags per request.
204
+ * Master network switch when `false`, the SDK makes NO outbound requests at
205
+ * all: init()/initOnce() never fetch (getters read code defaults / overrides),
206
+ * track() and exposure logging are no-ops, usage telemetry is off, and
207
+ * SDK-internal error self-monitoring is off. Think of it as a production-safe
208
+ * offline mode.
209
+ *
210
+ * DEFAULT is environment-derived (see {@link isProductionEnv}): ON in
211
+ * production, OFF everywhere else — so a local/dev/CI run never phones home
212
+ * unless you opt in. Production is inferred from `SHIPEASY_ENV`/`NODE_ENV`, or
213
+ * (when neither is set, e.g. on Cloudflare Workers) from the `env` option
214
+ * above, which defaults to `"prod"`. Pass an explicit value to override.
215
+ */
216
+ isNetworkEnabled?: boolean;
217
+ /**
218
+ * Per-evaluation usage telemetry ("tracking"/outside logging). Each
219
+ * getFlag/getConfig/getExperiment/getKillswitch (and the per-key evaluate()
220
+ * loop) fires one fire-and-forget beacon counted by Cloudflare's native
221
+ * per-path analytics.
222
+ *
223
+ * DEFAULT is environment-derived: ON in production, OFF everywhere else (same
224
+ * inference as {@link isNetworkEnabled}). Pass `true` to force it off, `false`
225
+ * to force it on. NOTE: on Cloudflare Workers each beacon is an outbound
226
+ * subrequest (cap 50 free / 1000 paid per invocation), so disable this on hot
227
+ * request paths that evaluate many flags per request. Forced off whenever
228
+ * `isNetworkEnabled` is false.
210
229
  */
211
230
  disableTelemetry?: boolean;
212
231
  /** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
@@ -258,6 +277,8 @@ declare class Engine {
258
277
  private readonly env;
259
278
  private readonly privateAttributes;
260
279
  private readonly stickyStore;
280
+ private readonly networkEnabled;
281
+ private readonly offline;
261
282
  private readonly telemetry;
262
283
  private readonly seeLimiter;
263
284
  private flagsBlob;
@@ -388,8 +409,8 @@ declare class Engine {
388
409
  /**
389
410
  * POST a single exposure for an enrolled `(user, experiment, group)`. Deduped
390
411
  * per process (bounded set) so repeated `assign()` calls in one server don't
391
- * spam `/collect`. Fire-and-forget; no-op in test mode. This is how
392
- * `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
412
+ * spam `/collect`. Fire-and-forget; no-op when the network is disabled. This
413
+ * is how `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
393
414
  */
394
415
  private postExposure;
395
416
  /**
@@ -201,12 +201,31 @@ interface EngineOptions {
201
201
  */
202
202
  initialBlob?: FlagsBlob;
203
203
  /**
204
- * Per-evaluation usage telemetry. ON by default each getFlag/getConfig/
205
- * getExperiment/getKillswitch (and the per-key evaluate() loop) fires one
206
- * fire-and-forget beacon counted by Cloudflare's native per-path analytics.
207
- * Pass `true` to disable. NOTE: on Cloudflare Workers each beacon is an
208
- * outbound subrequest (cap 50 free / 1000 paid per invocation), so disable
209
- * this on hot request paths that evaluate many flags per request.
204
+ * Master network switch when `false`, the SDK makes NO outbound requests at
205
+ * all: init()/initOnce() never fetch (getters read code defaults / overrides),
206
+ * track() and exposure logging are no-ops, usage telemetry is off, and
207
+ * SDK-internal error self-monitoring is off. Think of it as a production-safe
208
+ * offline mode.
209
+ *
210
+ * DEFAULT is environment-derived (see {@link isProductionEnv}): ON in
211
+ * production, OFF everywhere else — so a local/dev/CI run never phones home
212
+ * unless you opt in. Production is inferred from `SHIPEASY_ENV`/`NODE_ENV`, or
213
+ * (when neither is set, e.g. on Cloudflare Workers) from the `env` option
214
+ * above, which defaults to `"prod"`. Pass an explicit value to override.
215
+ */
216
+ isNetworkEnabled?: boolean;
217
+ /**
218
+ * Per-evaluation usage telemetry ("tracking"/outside logging). Each
219
+ * getFlag/getConfig/getExperiment/getKillswitch (and the per-key evaluate()
220
+ * loop) fires one fire-and-forget beacon counted by Cloudflare's native
221
+ * per-path analytics.
222
+ *
223
+ * DEFAULT is environment-derived: ON in production, OFF everywhere else (same
224
+ * inference as {@link isNetworkEnabled}). Pass `true` to force it off, `false`
225
+ * to force it on. NOTE: on Cloudflare Workers each beacon is an outbound
226
+ * subrequest (cap 50 free / 1000 paid per invocation), so disable this on hot
227
+ * request paths that evaluate many flags per request. Forced off whenever
228
+ * `isNetworkEnabled` is false.
210
229
  */
211
230
  disableTelemetry?: boolean;
212
231
  /** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
@@ -258,6 +277,8 @@ declare class Engine {
258
277
  private readonly env;
259
278
  private readonly privateAttributes;
260
279
  private readonly stickyStore;
280
+ private readonly networkEnabled;
281
+ private readonly offline;
261
282
  private readonly telemetry;
262
283
  private readonly seeLimiter;
263
284
  private flagsBlob;
@@ -388,8 +409,8 @@ declare class Engine {
388
409
  /**
389
410
  * POST a single exposure for an enrolled `(user, experiment, group)`. Deduped
390
411
  * per process (bounded set) so repeated `assign()` calls in one server don't
391
- * spam `/collect`. Fire-and-forget; no-op in test mode. This is how
392
- * `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
412
+ * spam `/collect`. Fire-and-forget; no-op when the network is disabled. This
413
+ * is how `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
393
414
  */
394
415
  private postExposure;
395
416
  /**
@@ -158,10 +158,26 @@ interface EngineOptions {
158
158
  /** Which published env to read values from. Defaults to "prod". */
159
159
  env?: EngineEnv;
160
160
  /**
161
- * Per-evaluation usage telemetry. ON by default each getFlag/getConfig/
161
+ * Master network switch when `false`, the browser SDK makes NO outbound
162
+ * requests at all: identify()/init never call /sdk/evaluate (getters read code
163
+ * defaults / overrides), track() and exposure logging are no-ops, usage
164
+ * telemetry is off, and SDK-internal error self-monitoring is off.
165
+ *
166
+ * DEFAULT is environment-derived (see {@link isProductionEnv}): ON in
167
+ * production, OFF everywhere else. In the browser there is usually no native
168
+ * `NODE_ENV`, so production is taken from the `env` option above (default
169
+ * `"prod"`) — meaning it defaults ON. Pass `false` (or set `env: "dev"`) to
170
+ * keep a local build quiet.
171
+ */
172
+ isNetworkEnabled?: boolean;
173
+ /**
174
+ * Per-evaluation usage telemetry ("tracking") — each getFlag/getConfig/
162
175
  * getExperiment/getKillswitch call fires one fire-and-forget sendBeacon so
163
- * usage is counted by Cloudflare's native per-path analytics. Pass `true` to
164
- * disable entirely.
176
+ * usage is counted by Cloudflare's native per-path analytics.
177
+ *
178
+ * DEFAULT is environment-derived: ON in production, OFF everywhere else (same
179
+ * inference as {@link isNetworkEnabled}). Pass `true` to force it off, `false`
180
+ * to force it on. Forced off whenever `isNetworkEnabled` is false.
165
181
  */
166
182
  disableTelemetry?: boolean;
167
183
  /** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
@@ -243,6 +259,8 @@ declare class Engine {
243
259
  private overrideListenerInstalled;
244
260
  private identifySeq;
245
261
  private readonly testMode;
262
+ private readonly networkEnabled;
263
+ private readonly offline;
246
264
  private readonly flagOverrides;
247
265
  private readonly configOverrides;
248
266
  private readonly experimentOverrides;
@@ -158,10 +158,26 @@ interface EngineOptions {
158
158
  /** Which published env to read values from. Defaults to "prod". */
159
159
  env?: EngineEnv;
160
160
  /**
161
- * Per-evaluation usage telemetry. ON by default each getFlag/getConfig/
161
+ * Master network switch when `false`, the browser SDK makes NO outbound
162
+ * requests at all: identify()/init never call /sdk/evaluate (getters read code
163
+ * defaults / overrides), track() and exposure logging are no-ops, usage
164
+ * telemetry is off, and SDK-internal error self-monitoring is off.
165
+ *
166
+ * DEFAULT is environment-derived (see {@link isProductionEnv}): ON in
167
+ * production, OFF everywhere else. In the browser there is usually no native
168
+ * `NODE_ENV`, so production is taken from the `env` option above (default
169
+ * `"prod"`) — meaning it defaults ON. Pass `false` (or set `env: "dev"`) to
170
+ * keep a local build quiet.
171
+ */
172
+ isNetworkEnabled?: boolean;
173
+ /**
174
+ * Per-evaluation usage telemetry ("tracking") — each getFlag/getConfig/
162
175
  * getExperiment/getKillswitch call fires one fire-and-forget sendBeacon so
163
- * usage is counted by Cloudflare's native per-path analytics. Pass `true` to
164
- * disable entirely.
176
+ * usage is counted by Cloudflare's native per-path analytics.
177
+ *
178
+ * DEFAULT is environment-derived: ON in production, OFF everywhere else (same
179
+ * inference as {@link isNetworkEnabled}). Pass `true` to force it off, `false`
180
+ * to force it on. Forced off whenever `isNetworkEnabled` is false.
165
181
  */
166
182
  disableTelemetry?: boolean;
167
183
  /** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
@@ -243,6 +259,8 @@ declare class Engine {
243
259
  private overrideListenerInstalled;
244
260
  private identifySeq;
245
261
  private readonly testMode;
262
+ private readonly networkEnabled;
263
+ private readonly offline;
246
264
  private readonly flagOverrides;
247
265
  private readonly configOverrides;
248
266
  private readonly experimentOverrides;
@@ -309,12 +309,31 @@ interface EngineOptions {
309
309
  */
310
310
  initialBlob?: FlagsBlob;
311
311
  /**
312
- * Per-evaluation usage telemetry. ON by default each getFlag/getConfig/
313
- * getExperiment/getKillswitch (and the per-key evaluate() loop) fires one
314
- * fire-and-forget beacon counted by Cloudflare's native per-path analytics.
315
- * Pass `true` to disable. NOTE: on Cloudflare Workers each beacon is an
316
- * outbound subrequest (cap 50 free / 1000 paid per invocation), so disable
317
- * this on hot request paths that evaluate many flags per request.
312
+ * Master network switch when `false`, the SDK makes NO outbound requests at
313
+ * all: init()/initOnce() never fetch (getters read code defaults / overrides),
314
+ * track() and exposure logging are no-ops, usage telemetry is off, and
315
+ * SDK-internal error self-monitoring is off. Think of it as a production-safe
316
+ * offline mode.
317
+ *
318
+ * DEFAULT is environment-derived (see {@link isProductionEnv}): ON in
319
+ * production, OFF everywhere else — so a local/dev/CI run never phones home
320
+ * unless you opt in. Production is inferred from `SHIPEASY_ENV`/`NODE_ENV`, or
321
+ * (when neither is set, e.g. on Cloudflare Workers) from the `env` option
322
+ * above, which defaults to `"prod"`. Pass an explicit value to override.
323
+ */
324
+ isNetworkEnabled?: boolean;
325
+ /**
326
+ * Per-evaluation usage telemetry ("tracking"/outside logging). Each
327
+ * getFlag/getConfig/getExperiment/getKillswitch (and the per-key evaluate()
328
+ * loop) fires one fire-and-forget beacon counted by Cloudflare's native
329
+ * per-path analytics.
330
+ *
331
+ * DEFAULT is environment-derived: ON in production, OFF everywhere else (same
332
+ * inference as {@link isNetworkEnabled}). Pass `true` to force it off, `false`
333
+ * to force it on. NOTE: on Cloudflare Workers each beacon is an outbound
334
+ * subrequest (cap 50 free / 1000 paid per invocation), so disable this on hot
335
+ * request paths that evaluate many flags per request. Forced off whenever
336
+ * `isNetworkEnabled` is false.
318
337
  */
319
338
  disableTelemetry?: boolean;
320
339
  /** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
@@ -366,6 +385,8 @@ declare class Engine {
366
385
  private readonly env;
367
386
  private readonly privateAttributes;
368
387
  private readonly stickyStore;
388
+ private readonly networkEnabled;
389
+ private readonly offline;
369
390
  private readonly telemetry;
370
391
  private readonly seeLimiter;
371
392
  private flagsBlob;
@@ -496,8 +517,8 @@ declare class Engine {
496
517
  /**
497
518
  * POST a single exposure for an enrolled `(user, experiment, group)`. Deduped
498
519
  * per process (bounded set) so repeated `assign()` calls in one server don't
499
- * spam `/collect`. Fire-and-forget; no-op in test mode. This is how
500
- * `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
520
+ * spam `/collect`. Fire-and-forget; no-op when the network is disabled. This
521
+ * is how `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
501
522
  */
502
523
  private postExposure;
503
524
  /**
@@ -591,7 +612,15 @@ interface ShipeasyServerConfig {
591
612
  */
592
613
  logLevel?: LogLevel;
593
614
  /**
594
- * Disable per-evaluation usage telemetry. ON by default. On Cloudflare
615
+ * Master network switch `false` puts the SDK fully offline (no fetch, no
616
+ * track, no telemetry, no error self-monitoring). Defaults to
617
+ * environment-derived: ON in production, OFF everywhere else. See
618
+ * {@link EngineOptions.isNetworkEnabled}.
619
+ */
620
+ isNetworkEnabled?: boolean;
621
+ /**
622
+ * Disable per-evaluation usage telemetry ("tracking"). Defaults to
623
+ * environment-derived: ON in production, OFF everywhere else. On Cloudflare
595
624
  * Workers each beacon is an outbound subrequest, so disable on hot SSR paths
596
625
  * that evaluate many flags per request. See {@link EngineOptions.disableTelemetry}.
597
626
  */
@@ -309,12 +309,31 @@ interface EngineOptions {
309
309
  */
310
310
  initialBlob?: FlagsBlob;
311
311
  /**
312
- * Per-evaluation usage telemetry. ON by default each getFlag/getConfig/
313
- * getExperiment/getKillswitch (and the per-key evaluate() loop) fires one
314
- * fire-and-forget beacon counted by Cloudflare's native per-path analytics.
315
- * Pass `true` to disable. NOTE: on Cloudflare Workers each beacon is an
316
- * outbound subrequest (cap 50 free / 1000 paid per invocation), so disable
317
- * this on hot request paths that evaluate many flags per request.
312
+ * Master network switch when `false`, the SDK makes NO outbound requests at
313
+ * all: init()/initOnce() never fetch (getters read code defaults / overrides),
314
+ * track() and exposure logging are no-ops, usage telemetry is off, and
315
+ * SDK-internal error self-monitoring is off. Think of it as a production-safe
316
+ * offline mode.
317
+ *
318
+ * DEFAULT is environment-derived (see {@link isProductionEnv}): ON in
319
+ * production, OFF everywhere else — so a local/dev/CI run never phones home
320
+ * unless you opt in. Production is inferred from `SHIPEASY_ENV`/`NODE_ENV`, or
321
+ * (when neither is set, e.g. on Cloudflare Workers) from the `env` option
322
+ * above, which defaults to `"prod"`. Pass an explicit value to override.
323
+ */
324
+ isNetworkEnabled?: boolean;
325
+ /**
326
+ * Per-evaluation usage telemetry ("tracking"/outside logging). Each
327
+ * getFlag/getConfig/getExperiment/getKillswitch (and the per-key evaluate()
328
+ * loop) fires one fire-and-forget beacon counted by Cloudflare's native
329
+ * per-path analytics.
330
+ *
331
+ * DEFAULT is environment-derived: ON in production, OFF everywhere else (same
332
+ * inference as {@link isNetworkEnabled}). Pass `true` to force it off, `false`
333
+ * to force it on. NOTE: on Cloudflare Workers each beacon is an outbound
334
+ * subrequest (cap 50 free / 1000 paid per invocation), so disable this on hot
335
+ * request paths that evaluate many flags per request. Forced off whenever
336
+ * `isNetworkEnabled` is false.
318
337
  */
319
338
  disableTelemetry?: boolean;
320
339
  /** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
@@ -366,6 +385,8 @@ declare class Engine {
366
385
  private readonly env;
367
386
  private readonly privateAttributes;
368
387
  private readonly stickyStore;
388
+ private readonly networkEnabled;
389
+ private readonly offline;
369
390
  private readonly telemetry;
370
391
  private readonly seeLimiter;
371
392
  private flagsBlob;
@@ -496,8 +517,8 @@ declare class Engine {
496
517
  /**
497
518
  * POST a single exposure for an enrolled `(user, experiment, group)`. Deduped
498
519
  * per process (bounded set) so repeated `assign()` calls in one server don't
499
- * spam `/collect`. Fire-and-forget; no-op in test mode. This is how
500
- * `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
520
+ * spam `/collect`. Fire-and-forget; no-op when the network is disabled. This
521
+ * is how `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
501
522
  */
502
523
  private postExposure;
503
524
  /**
@@ -591,7 +612,15 @@ interface ShipeasyServerConfig {
591
612
  */
592
613
  logLevel?: LogLevel;
593
614
  /**
594
- * Disable per-evaluation usage telemetry. ON by default. On Cloudflare
615
+ * Master network switch `false` puts the SDK fully offline (no fetch, no
616
+ * track, no telemetry, no error self-monitoring). Defaults to
617
+ * environment-derived: ON in production, OFF everywhere else. See
618
+ * {@link EngineOptions.isNetworkEnabled}.
619
+ */
620
+ isNetworkEnabled?: boolean;
621
+ /**
622
+ * Disable per-evaluation usage telemetry ("tracking"). Defaults to
623
+ * environment-derived: ON in production, OFF everywhere else. On Cloudflare
595
624
  * Workers each beacon is an outbound subrequest, so disable on hot SSR paths
596
625
  * that evaluate many flags per request. See {@link EngineOptions.disableTelemetry}.
597
626
  */
@@ -122,6 +122,24 @@ function send(url) {
122
122
  }
123
123
  }
124
124
 
125
+ // src/env.ts
126
+ function isProductionEnv(configuredEnv) {
127
+ const native = readNativeEnv();
128
+ if (native !== null) return native === "production" || native === "prod";
129
+ return (configuredEnv ?? "prod").toLowerCase() === "prod";
130
+ }
131
+ function readNativeEnv() {
132
+ try {
133
+ const proc = globalThis.process;
134
+ const raw = proc?.env?.SHIPEASY_ENV ?? proc?.env?.NODE_ENV;
135
+ if (typeof raw !== "string") return null;
136
+ const v = raw.trim().toLowerCase();
137
+ return v.length ? v : null;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
125
143
  // src/see/core.ts
126
144
  var SEE_MAX_MESSAGE = 500;
127
145
  var SEE_MAX_STACK = 8e3;
@@ -724,6 +742,13 @@ var Engine = class _Engine {
724
742
  env;
725
743
  privateAttributes;
726
744
  stickyStore;
745
+ // Master network gate — false ⇒ the SDK never touches the network (no fetch,
746
+ // no track, no telemetry, no internal error reports). Defaults to
747
+ // environment-derived (prod-on), forced false by testMode.
748
+ networkEnabled;
749
+ // Convenience inverse of networkEnabled — the "no network at all" state that
750
+ // every fetch/track/init gate keys on. `testMode` implies offline.
751
+ offline;
727
752
  telemetry;
728
753
  seeLimiter = new SeeLimiter();
729
754
  flagsBlob = null;
@@ -757,20 +782,24 @@ var Engine = class _Engine {
757
782
  this.privateAttributes = opts.privateAttributes ?? [];
758
783
  this.stickyStore = opts.stickyStore;
759
784
  this.testMode = opts.testMode === true;
785
+ const prod = isProductionEnv(this.env);
786
+ this.networkEnabled = this.testMode ? false : opts.isNetworkEnabled ?? prod;
787
+ this.offline = !this.networkEnabled;
760
788
  setInternalReportContext({
761
789
  side: "server",
762
790
  sdkVersion: version,
763
- enabled: !this.testMode && opts.disableInternalErrorReporting !== true
791
+ enabled: this.networkEnabled && opts.disableInternalErrorReporting !== true
764
792
  });
765
793
  this.telemetry = new Telemetry({
766
794
  endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
767
795
  sdkKey: this.apiKey,
768
796
  side: "server",
769
797
  env: this.env,
770
- // Test mode never talks to the network telemetry off regardless of opt.
771
- disabled: this.testMode || opts.disableTelemetry
798
+ // Off when the network is disabled; otherwise honour an explicit
799
+ // disableTelemetry, else default to prod-on (off outside production).
800
+ disabled: this.offline || (opts.disableTelemetry ?? !prod)
772
801
  });
773
- if (opts.initialBlob || this.testMode) {
802
+ if (opts.initialBlob || this.offline) {
774
803
  this.flagsBlob = opts.initialBlob ?? { version: "test", plan: "free", gates: {}, configs: {}, killswitches: {} };
775
804
  this.expsBlob = this.expsBlob ?? { version: "test", universes: {}, experiments: {} };
776
805
  this.initialized = true;
@@ -821,7 +850,7 @@ var Engine = class _Engine {
821
850
  return _Engine.fromSnapshot(snapshot);
822
851
  }
823
852
  async init() {
824
- if (this.testMode) {
853
+ if (this.offline) {
825
854
  this.initialized = true;
826
855
  return;
827
856
  }
@@ -830,7 +859,7 @@ var Engine = class _Engine {
830
859
  this.startPoll();
831
860
  }
832
861
  async initOnce() {
833
- if (this.testMode || this.initialized) return;
862
+ if (this.offline || this.initialized) return;
834
863
  await this.fetchAll();
835
864
  this.initialized = true;
836
865
  }
@@ -1054,7 +1083,7 @@ var Engine = class _Engine {
1054
1083
  return out;
1055
1084
  }
1056
1085
  track(userId, eventName, props) {
1057
- if (this.testMode) return;
1086
+ if (this.offline) return;
1058
1087
  const safeProps = this.stripPrivate(props);
1059
1088
  const body = JSON.stringify({
1060
1089
  events: [
@@ -1076,11 +1105,11 @@ var Engine = class _Engine {
1076
1105
  /**
1077
1106
  * POST a single exposure for an enrolled `(user, experiment, group)`. Deduped
1078
1107
  * per process (bounded set) so repeated `assign()` calls in one server don't
1079
- * spam `/collect`. Fire-and-forget; no-op in test mode. This is how
1080
- * `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
1108
+ * spam `/collect`. Fire-and-forget; no-op when the network is disabled. This
1109
+ * is how `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
1081
1110
  */
1082
1111
  postExposure(user, experiment, group) {
1083
- if (this.testMode) return;
1112
+ if (this.offline) return;
1084
1113
  const uid = user.user_id ?? user.anonymous_id;
1085
1114
  const dedupKey = `${uid ?? ""}:${experiment}:${group}`;
1086
1115
  if (this.exposureSeen.has(dedupKey)) return;
@@ -1110,6 +1139,7 @@ var Engine = class _Engine {
1110
1139
  * dedup window + per-process cap.
1111
1140
  */
1112
1141
  reportError(problem, consequence, extras, kind) {
1142
+ if (this.offline) return;
1113
1143
  try {
1114
1144
  const correlationId = seeContext.getStore()?.correlationId;
1115
1145
  const ev = buildSeeEvent(problem, consequence, extras, {
@@ -1326,6 +1356,7 @@ async function shipeasy(opts) {
1326
1356
  const profile = opts.i18nDefaultProfile ?? "en:prod";
1327
1357
  flags.configure({
1328
1358
  apiKey: serverKey,
1359
+ isNetworkEnabled: opts.isNetworkEnabled,
1329
1360
  disableTelemetry: opts.disableTelemetry,
1330
1361
  disableInternalErrorReporting: opts.disableInternalErrorReporting,
1331
1362
  privateAttributes: opts.privateAttributes,
@@ -67,6 +67,24 @@ function send(url) {
67
67
  }
68
68
  }
69
69
 
70
+ // src/env.ts
71
+ function isProductionEnv(configuredEnv) {
72
+ const native = readNativeEnv();
73
+ if (native !== null) return native === "production" || native === "prod";
74
+ return (configuredEnv ?? "prod").toLowerCase() === "prod";
75
+ }
76
+ function readNativeEnv() {
77
+ try {
78
+ const proc = globalThis.process;
79
+ const raw = proc?.env?.SHIPEASY_ENV ?? proc?.env?.NODE_ENV;
80
+ if (typeof raw !== "string") return null;
81
+ const v = raw.trim().toLowerCase();
82
+ return v.length ? v : null;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
70
88
  // src/see/core.ts
71
89
  var SEE_MAX_MESSAGE = 500;
72
90
  var SEE_MAX_STACK = 8e3;
@@ -669,6 +687,13 @@ var Engine = class _Engine {
669
687
  env;
670
688
  privateAttributes;
671
689
  stickyStore;
690
+ // Master network gate — false ⇒ the SDK never touches the network (no fetch,
691
+ // no track, no telemetry, no internal error reports). Defaults to
692
+ // environment-derived (prod-on), forced false by testMode.
693
+ networkEnabled;
694
+ // Convenience inverse of networkEnabled — the "no network at all" state that
695
+ // every fetch/track/init gate keys on. `testMode` implies offline.
696
+ offline;
672
697
  telemetry;
673
698
  seeLimiter = new SeeLimiter();
674
699
  flagsBlob = null;
@@ -702,20 +727,24 @@ var Engine = class _Engine {
702
727
  this.privateAttributes = opts.privateAttributes ?? [];
703
728
  this.stickyStore = opts.stickyStore;
704
729
  this.testMode = opts.testMode === true;
730
+ const prod = isProductionEnv(this.env);
731
+ this.networkEnabled = this.testMode ? false : opts.isNetworkEnabled ?? prod;
732
+ this.offline = !this.networkEnabled;
705
733
  setInternalReportContext({
706
734
  side: "server",
707
735
  sdkVersion: version,
708
- enabled: !this.testMode && opts.disableInternalErrorReporting !== true
736
+ enabled: this.networkEnabled && opts.disableInternalErrorReporting !== true
709
737
  });
710
738
  this.telemetry = new Telemetry({
711
739
  endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
712
740
  sdkKey: this.apiKey,
713
741
  side: "server",
714
742
  env: this.env,
715
- // Test mode never talks to the network telemetry off regardless of opt.
716
- disabled: this.testMode || opts.disableTelemetry
743
+ // Off when the network is disabled; otherwise honour an explicit
744
+ // disableTelemetry, else default to prod-on (off outside production).
745
+ disabled: this.offline || (opts.disableTelemetry ?? !prod)
717
746
  });
718
- if (opts.initialBlob || this.testMode) {
747
+ if (opts.initialBlob || this.offline) {
719
748
  this.flagsBlob = opts.initialBlob ?? { version: "test", plan: "free", gates: {}, configs: {}, killswitches: {} };
720
749
  this.expsBlob = this.expsBlob ?? { version: "test", universes: {}, experiments: {} };
721
750
  this.initialized = true;
@@ -766,7 +795,7 @@ var Engine = class _Engine {
766
795
  return _Engine.fromSnapshot(snapshot);
767
796
  }
768
797
  async init() {
769
- if (this.testMode) {
798
+ if (this.offline) {
770
799
  this.initialized = true;
771
800
  return;
772
801
  }
@@ -775,7 +804,7 @@ var Engine = class _Engine {
775
804
  this.startPoll();
776
805
  }
777
806
  async initOnce() {
778
- if (this.testMode || this.initialized) return;
807
+ if (this.offline || this.initialized) return;
779
808
  await this.fetchAll();
780
809
  this.initialized = true;
781
810
  }
@@ -999,7 +1028,7 @@ var Engine = class _Engine {
999
1028
  return out;
1000
1029
  }
1001
1030
  track(userId, eventName, props) {
1002
- if (this.testMode) return;
1031
+ if (this.offline) return;
1003
1032
  const safeProps = this.stripPrivate(props);
1004
1033
  const body = JSON.stringify({
1005
1034
  events: [
@@ -1021,11 +1050,11 @@ var Engine = class _Engine {
1021
1050
  /**
1022
1051
  * POST a single exposure for an enrolled `(user, experiment, group)`. Deduped
1023
1052
  * per process (bounded set) so repeated `assign()` calls in one server don't
1024
- * spam `/collect`. Fire-and-forget; no-op in test mode. This is how
1025
- * `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
1053
+ * spam `/collect`. Fire-and-forget; no-op when the network is disabled. This
1054
+ * is how `assignUniverse` auto-logs — the browser's auto-exposure parity for SSR.
1026
1055
  */
1027
1056
  postExposure(user, experiment, group) {
1028
- if (this.testMode) return;
1057
+ if (this.offline) return;
1029
1058
  const uid = user.user_id ?? user.anonymous_id;
1030
1059
  const dedupKey = `${uid ?? ""}:${experiment}:${group}`;
1031
1060
  if (this.exposureSeen.has(dedupKey)) return;
@@ -1055,6 +1084,7 @@ var Engine = class _Engine {
1055
1084
  * dedup window + per-process cap.
1056
1085
  */
1057
1086
  reportError(problem, consequence, extras, kind) {
1087
+ if (this.offline) return;
1058
1088
  try {
1059
1089
  const correlationId = seeContext.getStore()?.correlationId;
1060
1090
  const ev = buildSeeEvent(problem, consequence, extras, {
@@ -1271,6 +1301,7 @@ async function shipeasy(opts) {
1271
1301
  const profile = opts.i18nDefaultProfile ?? "en:prod";
1272
1302
  flags.configure({
1273
1303
  apiKey: serverKey,
1304
+ isNetworkEnabled: opts.isNetworkEnabled,
1274
1305
  disableTelemetry: opts.disableTelemetry,
1275
1306
  disableInternalErrorReporting: opts.disableInternalErrorReporting,
1276
1307
  privateAttributes: opts.privateAttributes,
@@ -42,6 +42,12 @@ flags.getFlagDetail("new_checkout"); // { value, reason }
42
42
  `configure()` is first-config-wins and owns the fetch lifecycle (one-shot by
43
43
  default; `poll: true` for a background refresh). Construct `new Client(user)` once
44
44
  per user/request — it binds the user, so no method takes a user argument.
45
+
46
+ Egress is **environment-derived**: network + usage telemetry default ON in
47
+ production and OFF otherwise (prod is read from `SHIPEASY_ENV`/`NODE_ENV`, else
48
+ the `env` option, default `"prod"`), so the SDK never phones home from dev/CI.
49
+ Override with `isNetworkEnabled: false` (fully offline) or `disableTelemetry: true`
50
+ (keep fetching, no usage beacons).
45
51
  Full reference: <https://shipeasy-ai.github.io/sdk-ts/pages/configuration.md> ·
46
52
  <https://shipeasy-ai.github.io/sdk-ts/pages/flags.md> ·
47
53
  snippets <https://shipeasy-ai.github.io/sdk-ts/snippets/release/flags.md> ·
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "7.0.0",
3
+ "version": "7.1.0",
4
4
  "description": "Shipeasy SDK — feature gates, runtime configs, experiments, and metrics for the Shipeasy hosted service.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://shipeasy.ai",