@shipeasy/sdk 1.0.0 → 1.2.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.
@@ -123,13 +123,15 @@ var EventBuffer = class {
123
123
  });
124
124
  }
125
125
  };
126
+ var MAX_ERRORS_PER_SESSION = 5;
126
127
  function installAutoGuardrails(buffer, userId, anonId) {
127
128
  if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
128
129
  let lcp = null;
129
130
  let inp = null;
130
131
  let clsBad = false;
131
- let jsError = false;
132
- let netError = false;
132
+ let jsErrorCount = 0;
133
+ let netErrorCount = 0;
134
+ let navTimingFlushed = false;
133
135
  try {
134
136
  const lcpObs = new PerformanceObserver((list) => {
135
137
  const entries = list.getEntries();
@@ -163,30 +165,112 @@ function installAutoGuardrails(buffer, userId, anonId) {
163
165
  } catch {
164
166
  }
165
167
  const origOnError = window.onerror;
166
- window.onerror = (...args) => {
167
- if (!jsError) {
168
- jsError = true;
169
- buffer.pushMetric("__auto_js_error", userId, anonId, { value: 1 });
168
+ window.onerror = (msg, source, lineno, _colno, err) => {
169
+ if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
170
+ jsErrorCount += 1;
171
+ buffer.pushMetric("__auto_js_error", userId, anonId, {
172
+ value: 1,
173
+ kind: "exception",
174
+ message: typeof msg === "string" ? msg.slice(0, 200) : String(err ?? "").slice(0, 200),
175
+ source: typeof source === "string" ? source.slice(0, 200) : "",
176
+ line: lineno ?? 0
177
+ });
170
178
  }
171
- if (typeof origOnError === "function") return origOnError(...args);
179
+ if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
172
180
  return false;
173
181
  };
174
- window.addEventListener("unhandledrejection", () => {
175
- if (!jsError) {
176
- jsError = true;
177
- buffer.pushMetric("__auto_js_error", userId, anonId, { value: 1 });
182
+ window.addEventListener("unhandledrejection", (e) => {
183
+ if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
184
+ jsErrorCount += 1;
185
+ const reason = e.reason;
186
+ const message = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : String(reason);
187
+ buffer.pushMetric("__auto_js_error", userId, anonId, {
188
+ value: 1,
189
+ kind: "unhandled_rejection",
190
+ message: message.slice(0, 200)
191
+ });
178
192
  }
179
193
  });
180
194
  const origFetch = window.fetch;
181
195
  window.fetch = async function(...args) {
182
- const res = await origFetch.apply(this, args);
183
- if (!netError && res.status >= 500) {
184
- netError = true;
185
- buffer.pushMetric("__auto_network_error", userId, anonId, { value: 1 });
196
+ const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
197
+ const url = typeof args[0] === "string" ? args[0] : args[0].toString();
198
+ let res;
199
+ try {
200
+ res = await origFetch.apply(this, args);
201
+ } catch (err) {
202
+ if (netErrorCount < MAX_ERRORS_PER_SESSION) {
203
+ netErrorCount += 1;
204
+ buffer.pushMetric("__auto_network_error", userId, anonId, {
205
+ value: 1,
206
+ kind: "network",
207
+ status: 0,
208
+ url: url.slice(0, 200)
209
+ });
210
+ }
211
+ throw err;
212
+ }
213
+ if (res.status >= 500 && netErrorCount < MAX_ERRORS_PER_SESSION) {
214
+ netErrorCount += 1;
215
+ const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
216
+ buffer.pushMetric("__auto_network_error", userId, anonId, {
217
+ value: 1,
218
+ kind: "5xx",
219
+ status: res.status,
220
+ url: url.slice(0, 200),
221
+ duration_ms: Math.round(elapsed)
222
+ });
186
223
  }
187
224
  return res;
188
225
  };
189
- const flush = () => {
226
+ const flushNavTiming = () => {
227
+ if (navTimingFlushed) return;
228
+ navTimingFlushed = true;
229
+ try {
230
+ const navList = performance.getEntriesByType("navigation");
231
+ const nav = navList[0];
232
+ if (nav) {
233
+ const start = nav.startTime ?? 0;
234
+ if (nav.loadEventEnd > 0) {
235
+ buffer.pushMetric("__auto_page_load", userId, anonId, {
236
+ value: nav.loadEventEnd - start
237
+ });
238
+ }
239
+ if (nav.responseStart > 0) {
240
+ buffer.pushMetric("__auto_ttfb", userId, anonId, {
241
+ value: nav.responseStart - start
242
+ });
243
+ }
244
+ if (nav.domContentLoadedEventEnd > 0) {
245
+ buffer.pushMetric("__auto_dom_ready", userId, anonId, {
246
+ value: nav.domContentLoadedEventEnd - start
247
+ });
248
+ }
249
+ }
250
+ const paints = performance.getEntriesByType("paint");
251
+ for (const p of paints) {
252
+ if (p.name === "first-paint") {
253
+ buffer.pushMetric("__auto_fp", userId, anonId, { value: p.startTime });
254
+ } else if (p.name === "first-contentful-paint") {
255
+ buffer.pushMetric("__auto_fcp", userId, anonId, { value: p.startTime });
256
+ }
257
+ }
258
+ } catch {
259
+ }
260
+ };
261
+ if (document.readyState === "complete") {
262
+ setTimeout(flushNavTiming, 0);
263
+ } else {
264
+ window.addEventListener(
265
+ "load",
266
+ () => {
267
+ setTimeout(flushNavTiming, 0);
268
+ },
269
+ { once: true }
270
+ );
271
+ }
272
+ const flushOnHide = () => {
273
+ flushNavTiming();
190
274
  if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
191
275
  if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
192
276
  if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
@@ -195,7 +279,7 @@ function installAutoGuardrails(buffer, userId, anonId) {
195
279
  buffer.flush(true);
196
280
  };
197
281
  document.addEventListener("visibilitychange", () => {
198
- if (document.visibilityState === "hidden") flush();
282
+ if (document.visibilityState === "hidden") flushOnHide();
199
283
  });
200
284
  }
201
285
  function getOrCreateAnonId() {
@@ -211,18 +295,75 @@ function getOrCreateAnonId() {
211
295
  }
212
296
  return id;
213
297
  }
298
+ function collectBrowserAttrs() {
299
+ if (typeof window === "undefined") return {};
300
+ const attrs = {};
301
+ try {
302
+ if (typeof navigator !== "undefined" && navigator.language) attrs.locale = navigator.language;
303
+ } catch {
304
+ }
305
+ try {
306
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
307
+ if (tz) attrs.timezone = tz;
308
+ } catch {
309
+ }
310
+ try {
311
+ if (document.referrer) attrs.referrer = document.referrer;
312
+ } catch {
313
+ }
314
+ try {
315
+ attrs.path = window.location.pathname;
316
+ } catch {
317
+ }
318
+ try {
319
+ if (window.screen) {
320
+ attrs.screen_width = window.screen.width;
321
+ attrs.screen_height = window.screen.height;
322
+ }
323
+ } catch {
324
+ }
325
+ try {
326
+ if (typeof navigator !== "undefined" && typeof navigator.userAgent === "string") {
327
+ attrs.user_agent = navigator.userAgent;
328
+ }
329
+ } catch {
330
+ }
331
+ return attrs;
332
+ }
333
+ function readExperimentOverridesFromUrl() {
334
+ if (typeof window === "undefined") return {};
335
+ const out = {};
336
+ try {
337
+ const params = new URLSearchParams(window.location.search);
338
+ for (const [k, v] of params) {
339
+ if (!v || v === "default" || v === "none") continue;
340
+ if (k.startsWith("se_exp_")) out[k.slice("se_exp_".length)] = v;
341
+ else if (k.startsWith("se-exp-")) out[k.slice("se-exp-".length)] = v;
342
+ }
343
+ } catch {
344
+ }
345
+ return out;
346
+ }
214
347
  var FlagsClientBrowser = class {
215
348
  sdkKey;
216
349
  baseUrl;
217
350
  autoGuardrails;
351
+ env;
218
352
  evalResult = null;
219
353
  anonId;
220
354
  userId = "";
221
355
  buffer;
222
356
  guardrailsInstalled = false;
357
+ listeners = /* @__PURE__ */ new Set();
358
+ overrideListenerInstalled = false;
359
+ onOverrideChange = () => {
360
+ this.installBridge();
361
+ this.notify();
362
+ };
223
363
  constructor(opts) {
224
364
  this.sdkKey = opts.sdkKey;
225
365
  this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
366
+ this.env = opts.env ?? "prod";
226
367
  this.autoGuardrails = opts.autoGuardrails !== false;
227
368
  this.anonId = getOrCreateAnonId();
228
369
  this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
@@ -234,10 +375,18 @@ var FlagsClientBrowser = class {
234
375
  if (this.anonId && this.userId && this.userId !== prevUserId) {
235
376
  await this.buffer.alias(this.anonId, this.userId);
236
377
  }
237
- const res = await fetch(`${this.baseUrl}/sdk/evaluate`, {
378
+ const userPayload = {
379
+ ...collectBrowserAttrs(),
380
+ anonymous_id: this.anonId,
381
+ ...user
382
+ };
383
+ const res = await fetch(`${this.baseUrl}/sdk/evaluate?env=${this.env}`, {
238
384
  method: "POST",
239
385
  headers: { "X-SDK-Key": this.sdkKey, "Content-Type": "application/json" },
240
- body: JSON.stringify({ user })
386
+ body: JSON.stringify({
387
+ user: userPayload,
388
+ experiment_overrides: readExperimentOverridesFromUrl()
389
+ })
241
390
  });
242
391
  if (!res.ok) throw new Error(`/sdk/evaluate returned ${res.status}`);
243
392
  this.evalResult = await res.json();
@@ -245,26 +394,54 @@ var FlagsClientBrowser = class {
245
394
  this.guardrailsInstalled = true;
246
395
  installAutoGuardrails(this.buffer, this.userId, this.anonId);
247
396
  }
397
+ this.notify();
398
+ }
399
+ get ready() {
400
+ return this.evalResult !== null;
401
+ }
402
+ notify() {
403
+ for (const l of this.listeners) {
404
+ try {
405
+ l();
406
+ } catch (err) {
407
+ console.warn("[shipeasy] subscriber threw:", String(err));
408
+ }
409
+ }
248
410
  }
249
411
  initFromBootstrap(data) {
250
412
  this.evalResult = data;
251
413
  }
252
414
  getFlag(name) {
253
- return this.evalResult?.flags[name]?.enabled ?? false;
415
+ const ov = readGateOverride(name);
416
+ if (ov !== null) return ov;
417
+ return this.evalResult?.flags[name] ?? false;
254
418
  }
255
419
  getConfig(name, decode) {
256
- void name;
257
- void decode;
258
- return void 0;
420
+ const ov = readConfigOverride(name);
421
+ const raw = ov !== void 0 ? ov : this.evalResult?.configs?.[name];
422
+ if (raw === void 0) return void 0;
423
+ if (!decode) return raw;
424
+ try {
425
+ return decode(raw);
426
+ } catch (err) {
427
+ console.warn(`[shipeasy] getConfig('${name}') decode failed:`, String(err));
428
+ return void 0;
429
+ }
259
430
  }
260
- getExperiment(name, defaultParams, decode) {
431
+ getExperiment(name, defaultParams, decode, variants) {
261
432
  const notIn = {
262
433
  inExperiment: false,
263
434
  group: "control",
264
435
  params: defaultParams
265
436
  };
437
+ const ov = readExpOverride(name);
438
+ if (ov !== null) {
439
+ const variantParams = variants?.[ov];
440
+ const params = variantParams ? { ...defaultParams, ...variantParams } : defaultParams;
441
+ return { inExperiment: true, group: ov, params };
442
+ }
266
443
  const entry = this.evalResult?.experiments[name];
267
- if (!entry || !entry.in_experiment) return notIn;
444
+ if (!entry || !entry.inExperiment) return notIn;
268
445
  this.buffer.pushExposure(name, entry.group, this.userId, this.anonId);
269
446
  if (!decode) return { inExperiment: true, group: entry.group, params: entry.params };
270
447
  try {
@@ -274,6 +451,39 @@ var FlagsClientBrowser = class {
274
451
  return notIn;
275
452
  }
276
453
  }
454
+ /**
455
+ * Subscribe to state changes — fires after identify() completes and on
456
+ * `se:override:change` events from the devtools overlay. Returns an
457
+ * unsubscribe function. Used by framework adapters to trigger re-renders.
458
+ */
459
+ subscribe(listener) {
460
+ this.listeners.add(listener);
461
+ if (!this.overrideListenerInstalled && typeof window !== "undefined") {
462
+ this.overrideListenerInstalled = true;
463
+ window.addEventListener("se:override:change", this.onOverrideChange);
464
+ }
465
+ return () => {
466
+ this.listeners.delete(listener);
467
+ };
468
+ }
469
+ /**
470
+ * Publishes the SDK to `window.__shipeasy` so the devtools overlay can read
471
+ * current values. Idempotent. Returns the bridge object for tests.
472
+ */
473
+ installBridge() {
474
+ if (typeof window === "undefined") return null;
475
+ const bridge = {
476
+ getFlag: (n) => this.getFlag(n),
477
+ getExperiment: (n) => {
478
+ const r = this.getExperiment(n, {});
479
+ return { inExperiment: r.inExperiment, group: r.group };
480
+ },
481
+ getConfig: (n) => this.getConfig(n)
482
+ };
483
+ window.__shipeasy = bridge;
484
+ window.dispatchEvent(new CustomEvent("se:state:update"));
485
+ return bridge;
486
+ }
277
487
  track(eventName, props) {
278
488
  this.buffer.pushMetric(eventName, this.userId, this.anonId, props);
279
489
  }
@@ -283,9 +493,266 @@ var FlagsClientBrowser = class {
283
493
  destroy() {
284
494
  this.buffer.flush();
285
495
  this.buffer.destroy();
496
+ this.listeners.clear();
497
+ if (this.overrideListenerInstalled && typeof window !== "undefined") {
498
+ window.removeEventListener("se:override:change", this.onOverrideChange);
499
+ this.overrideListenerInstalled = false;
500
+ }
501
+ }
502
+ };
503
+ var TRUE_RX = /^(true|on|1|yes)$/i;
504
+ var FALSE_RX = /^(false|off|0|no)$/i;
505
+ function parseBool(raw) {
506
+ if (TRUE_RX.test(raw)) return true;
507
+ if (FALSE_RX.test(raw)) return false;
508
+ return null;
509
+ }
510
+ function decodeConfigValue(raw) {
511
+ if (raw.startsWith("b64:")) {
512
+ try {
513
+ const json = atob(raw.slice(4).replace(/-/g, "+").replace(/_/g, "/"));
514
+ return JSON.parse(json);
515
+ } catch {
516
+ return raw;
517
+ }
518
+ }
519
+ try {
520
+ return JSON.parse(raw);
521
+ } catch {
522
+ return raw;
523
+ }
524
+ }
525
+ function readParam(canonical, legacy) {
526
+ if (typeof window === "undefined" || !window.location) return null;
527
+ const params = new URLSearchParams(window.location.search);
528
+ const direct = params.get(canonical);
529
+ if (direct !== null) return direct;
530
+ if (legacy) {
531
+ const legacyVal = params.get(legacy);
532
+ if (legacyVal !== null) return legacyVal;
533
+ }
534
+ return null;
535
+ }
536
+ function readGateOverride(name) {
537
+ const v = readParam(`se_ks_${name}`) ?? readParam(`se_gate_${name}`) ?? readParam(`se-gate-${name}`);
538
+ return v === null ? null : parseBool(v);
539
+ }
540
+ function readConfigOverride(name) {
541
+ const v = readParam(`se_config_${name}`, `se-config-${name}`);
542
+ if (v === null) return void 0;
543
+ return decodeConfigValue(v);
544
+ }
545
+ function readExpOverride(name) {
546
+ const v = readParam(`se_exp_${name}`, `se-exp-${name}`);
547
+ if (v === null || v === "" || v === "default" || v === "none") return null;
548
+ return v;
549
+ }
550
+ function isDevtoolsRequested() {
551
+ if (typeof window === "undefined" || !window.location) return false;
552
+ const p = new URLSearchParams(window.location.search);
553
+ return p.has("se") || p.has("se_devtools") || p.has("se-devtools");
554
+ }
555
+ function loadDevtools(opts = {}) {
556
+ if (typeof window === "undefined") return;
557
+ const wGlobal = window;
558
+ const mod = wGlobal.__shipeasy_devtools_global;
559
+ if (!mod) return;
560
+ mod.init(opts);
561
+ const w = window;
562
+ if (!w.__shipeasy_devtools) {
563
+ let visible = true;
564
+ w.__shipeasy_devtools = {
565
+ toggle() {
566
+ if (visible) {
567
+ mod.destroy();
568
+ visible = false;
569
+ } else {
570
+ mod.init(opts);
571
+ visible = true;
572
+ }
573
+ }
574
+ };
575
+ }
576
+ }
577
+ function attachDevtools(client, opts = {}) {
578
+ if (typeof window === "undefined") return () => {
579
+ };
580
+ const hotkey = opts.hotkey ?? "Shift+Alt+S";
581
+ const parts = hotkey.split("+");
582
+ const key = parts[parts.length - 1];
583
+ const shift = parts.includes("Shift");
584
+ const alt = parts.includes("Alt");
585
+ const ctrl = parts.includes("Ctrl") || parts.includes("Control");
586
+ const meta = parts.includes("Meta") || parts.includes("Cmd");
587
+ client.installBridge();
588
+ if (isDevtoolsRequested()) loadDevtools({ adminUrl: opts.adminUrl, edgeUrl: opts.edgeUrl });
589
+ let loaded = isDevtoolsRequested();
590
+ function onKeyDown(e) {
591
+ if (e.key === key && e.shiftKey === shift && e.altKey === alt && e.ctrlKey === ctrl && e.metaKey === meta) {
592
+ if (!loaded) {
593
+ loaded = true;
594
+ loadDevtools({ adminUrl: opts.adminUrl, edgeUrl: opts.edgeUrl });
595
+ } else {
596
+ window.__shipeasy_devtools?.toggle();
597
+ }
598
+ }
599
+ }
600
+ window.addEventListener("keydown", onKeyDown);
601
+ const unsubBridge = client.subscribe(() => client.installBridge());
602
+ return () => {
603
+ window.removeEventListener("keydown", onKeyDown);
604
+ unsubBridge();
605
+ };
606
+ }
607
+ var _client = null;
608
+ function configureShipeasy(opts) {
609
+ if (_client) return _client;
610
+ _client = new FlagsClientBrowser(opts);
611
+ return _client;
612
+ }
613
+ function getShipeasyClient() {
614
+ return _client;
615
+ }
616
+ function _resetShipeasyForTests() {
617
+ _client?.destroy();
618
+ _client = null;
619
+ }
620
+ var flags = {
621
+ configure(opts) {
622
+ configureShipeasy(opts);
623
+ },
624
+ identify(user) {
625
+ if (!_client) {
626
+ console.warn("[shipeasy] flags.identify called before configureShipeasy()");
627
+ return Promise.resolve();
628
+ }
629
+ return _client.identify(user);
630
+ },
631
+ /** Read a feature gate. Returns false until identify() resolves. */
632
+ get(name) {
633
+ return _client?.getFlag(name) ?? false;
634
+ },
635
+ getConfig(name, decode) {
636
+ return _client?.getConfig(name, decode);
637
+ },
638
+ getExperiment(name, defaultParams, decode, variants) {
639
+ return _client?.getExperiment(name, defaultParams, decode, variants) ?? {
640
+ inExperiment: false,
641
+ group: "control",
642
+ params: defaultParams
643
+ };
644
+ },
645
+ track(eventName, props) {
646
+ _client?.track(eventName, props);
647
+ },
648
+ flush() {
649
+ return _client?.flush() ?? Promise.resolve();
650
+ },
651
+ /** Subscribe for change notifications (identify/override). Used by framework adapters. */
652
+ subscribe(listener) {
653
+ if (!_client) return () => {
654
+ };
655
+ return _client.subscribe(listener);
656
+ },
657
+ /** True once identify() has completed and flags are available. */
658
+ get ready() {
659
+ return _client?.ready ?? false;
660
+ }
661
+ };
662
+ var LABEL_MARKER_START = "\uFFF9";
663
+ var LABEL_MARKER_SEP = "\uFFFA";
664
+ var LABEL_MARKER_END = "\uFFFB";
665
+ var LABEL_MARKER_RE = /([^]+)([^]*)/g;
666
+ function encodeLabelMarker(key, value) {
667
+ return `${LABEL_MARKER_START}${key}${LABEL_MARKER_SEP}${value}${LABEL_MARKER_END}`;
668
+ }
669
+ function labelAttrs(key, variables, desc) {
670
+ const attrs = { "data-label": key };
671
+ if (variables) attrs["data-variables"] = JSON.stringify(variables);
672
+ if (desc) attrs["data-label-desc"] = desc;
673
+ return attrs;
674
+ }
675
+ var _createElement = null;
676
+ var i18n = {
677
+ t(key, variables) {
678
+ if (typeof window !== "undefined" && window.i18n) return window.i18n.t(key, variables);
679
+ return key;
680
+ },
681
+ /**
682
+ * Translate a key and return a framework element (e.g. React <span>)
683
+ * carrying `data-label` / `data-variables` attributes so the ShipEasy
684
+ * devtools "Edit labels" overlay can highlight and edit it in place.
685
+ *
686
+ * Requires a one-time setup call: `i18n.configure({ createElement })`.
687
+ * The returned value is whatever `createElement` returns — pass React's
688
+ * `createElement`, Vue's `h`, Solid's `createSignal`-based factory, etc.
689
+ *
690
+ * Falls back to a plain translated string if `createElement` was not
691
+ * configured (e.g. server-side or in non-JSX contexts).
692
+ */
693
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
694
+ tEl(key, variables, desc) {
695
+ const text = this.t(key, variables);
696
+ if (!_createElement) return text;
697
+ return _createElement("span", labelAttrs(key, variables, desc), text);
698
+ },
699
+ /** Wire up the element creator once at app startup (call before any tEl use). */
700
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
701
+ configure(opts) {
702
+ _createElement = opts.createElement;
703
+ },
704
+ get locale() {
705
+ if (typeof window !== "undefined" && window.i18n) return window.i18n.locale;
706
+ return null;
707
+ },
708
+ get ready() {
709
+ if (typeof window !== "undefined" && window.i18n) return Boolean(window.i18n.locale);
710
+ return false;
711
+ },
712
+ /** Resolves when the loader has installed window.i18n and fetched a profile. */
713
+ whenReady() {
714
+ if (typeof window === "undefined") return Promise.resolve();
715
+ if (window.i18n?.locale) return Promise.resolve();
716
+ return new Promise((resolve) => {
717
+ const handler = () => resolve();
718
+ window.addEventListener("se:i18n:ready", handler, { once: true });
719
+ });
720
+ },
721
+ /** Subscribe to locale/profile updates. Returns an unsubscribe fn. */
722
+ onUpdate(cb) {
723
+ if (typeof window === "undefined") return () => {
724
+ };
725
+ if (window.i18n) return window.i18n.on("update", cb);
726
+ let unsub = () => {
727
+ };
728
+ const handler = () => {
729
+ if (window.i18n) unsub = window.i18n.on("update", cb);
730
+ };
731
+ window.addEventListener("se:i18n:ready", handler, { once: true });
732
+ return () => {
733
+ window.removeEventListener("se:i18n:ready", handler);
734
+ unsub();
735
+ };
286
736
  }
287
737
  };
288
738
  export {
289
739
  FlagsClientBrowser,
740
+ LABEL_MARKER_END,
741
+ LABEL_MARKER_RE,
742
+ LABEL_MARKER_SEP,
743
+ LABEL_MARKER_START,
744
+ _resetShipeasyForTests,
745
+ attachDevtools,
746
+ configureShipeasy,
747
+ encodeLabelMarker,
748
+ flags,
749
+ getShipeasyClient,
750
+ i18n,
751
+ isDevtoolsRequested,
752
+ labelAttrs,
753
+ loadDevtools,
754
+ readConfigOverride,
755
+ readExpOverride,
756
+ readGateOverride,
290
757
  version
291
758
  };
@@ -9,13 +9,17 @@ interface ExperimentResult<P> {
9
9
  group: string;
10
10
  params: P;
11
11
  }
12
+ type FlagsClientEnv = "dev" | "staging" | "prod";
12
13
  interface FlagsClientOptions {
13
14
  apiKey: string;
14
15
  baseUrl?: string;
16
+ /** Which published env to read values from. Defaults to "prod". */
17
+ env?: FlagsClientEnv;
15
18
  }
16
19
  declare class FlagsClient {
17
20
  private readonly apiKey;
18
21
  private readonly baseUrl;
22
+ private readonly env;
19
23
  private flagsBlob;
20
24
  private expsBlob;
21
25
  private flagsEtag;
@@ -36,5 +40,38 @@ declare class FlagsClient {
36
40
  getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
37
41
  track(userId: string, eventName: string, props?: Record<string, unknown>): void;
38
42
  }
43
+ interface LabelFile {
44
+ v: number;
45
+ profile: string;
46
+ chunk: string;
47
+ strings: Record<string, string>;
48
+ }
49
+ interface FetchLabelsOptions {
50
+ key: string;
51
+ profile: string;
52
+ chunk?: string;
53
+ cdnBaseUrl?: string;
54
+ timeoutMs?: number;
55
+ }
56
+ declare function fetchLabelsForSSR(opts: FetchLabelsOptions): Promise<LabelFile | null>;
57
+ declare function configureShipeasyServer(opts: FlagsClientOptions): FlagsClient;
58
+ declare function getShipeasyServerClient(): FlagsClient | null;
59
+ declare function _resetShipeasyServerForTests(): void;
60
+ declare const flags: {
61
+ configure(opts: FlagsClientOptions): void;
62
+ /**
63
+ * Long-running server: starts the background poll. Call once at app boot.
64
+ * Throws if the initial fetch fails (caller decides whether to crash or degrade).
65
+ */
66
+ init(): Promise<void>;
67
+ /** Serverless / edge: fetch rules once, no background timer. */
68
+ initOnce(): Promise<void>;
69
+ /** Stop background timers. Safe to call repeatedly. */
70
+ destroy(): void;
71
+ get(name: string, user: User): boolean;
72
+ getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
73
+ getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
74
+ track(userId: string, eventName: string, props?: Record<string, unknown>): void;
75
+ };
39
76
 
40
- export { type ExperimentResult, FlagsClient, type FlagsClientOptions, type User, version };
77
+ export { type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type LabelFile, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getShipeasyServerClient, version };