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