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