@shipeasy/sdk 2.1.11 → 2.1.13

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.
@@ -48,6 +48,7 @@ declare class FlagsClientBrowser {
48
48
  private guardrailsInstalled;
49
49
  private listeners;
50
50
  private overrideListenerInstalled;
51
+ private identifySeq;
51
52
  private onOverrideChange;
52
53
  constructor(opts: FlagsClientBrowserOptions);
53
54
  identify(user: User): Promise<void>;
@@ -117,11 +118,22 @@ interface ShipeasyClientConfig {
117
118
  baseUrl?: string;
118
119
  /** Override the admin URL for the devtools overlay (dev use). */
119
120
  adminUrl?: string;
121
+ /**
122
+ * Skip the lazy auto-identify({}) at boot. Defaults to true (auto-identify on).
123
+ * Turn off when the host has its own identify orchestration and wants to
124
+ * avoid the initial anon /sdk/evaluate round-trip.
125
+ */
126
+ autoIdentify?: boolean;
120
127
  }
121
128
  /**
122
129
  * Initialise the ShipEasy client SDK and wire up lazy devtools.
123
130
  * Call this once at app startup (e.g. in a useEffect in your root layout).
124
131
  * Returns a cleanup function — call it on unmount to remove event listeners.
132
+ *
133
+ * Lazy-identifies the visitor under the hood with a stable anonId + auto-collected
134
+ * browser attrs (locale, timezone, path, screen, referrer, user_agent), so flags
135
+ * and experiments are warm without callers having to wire identify() manually.
136
+ * A later flags.identify({ user_id }) overrides this in place; anonId stays stable.
125
137
  */
126
138
  declare function shipeasy(opts: ShipeasyClientConfig): () => void;
127
139
  declare function configureShipeasy(opts: FlagsClientBrowserOptions): FlagsClientBrowser;
@@ -48,6 +48,7 @@ declare class FlagsClientBrowser {
48
48
  private guardrailsInstalled;
49
49
  private listeners;
50
50
  private overrideListenerInstalled;
51
+ private identifySeq;
51
52
  private onOverrideChange;
52
53
  constructor(opts: FlagsClientBrowserOptions);
53
54
  identify(user: User): Promise<void>;
@@ -117,11 +118,22 @@ interface ShipeasyClientConfig {
117
118
  baseUrl?: string;
118
119
  /** Override the admin URL for the devtools overlay (dev use). */
119
120
  adminUrl?: string;
121
+ /**
122
+ * Skip the lazy auto-identify({}) at boot. Defaults to true (auto-identify on).
123
+ * Turn off when the host has its own identify orchestration and wants to
124
+ * avoid the initial anon /sdk/evaluate round-trip.
125
+ */
126
+ autoIdentify?: boolean;
120
127
  }
121
128
  /**
122
129
  * Initialise the ShipEasy client SDK and wire up lazy devtools.
123
130
  * Call this once at app startup (e.g. in a useEffect in your root layout).
124
131
  * Returns a cleanup function — call it on unmount to remove event listeners.
132
+ *
133
+ * Lazy-identifies the visitor under the hood with a stable anonId + auto-collected
134
+ * browser attrs (locale, timezone, path, screen, referrer, user_agent), so flags
135
+ * and experiments are warm without callers having to wire identify() manually.
136
+ * A later flags.identify({ user_id }) overrides this in place; anonId stays stable.
125
137
  */
126
138
  declare function shipeasy(opts: ShipeasyClientConfig): () => void;
127
139
  declare function configureShipeasy(opts: FlagsClientBrowserOptions): FlagsClientBrowser;
@@ -399,6 +399,9 @@ var FlagsClientBrowser = class {
399
399
  guardrailsInstalled = false;
400
400
  listeners = /* @__PURE__ */ new Set();
401
401
  overrideListenerInstalled = false;
402
+ // Monotonic counter so a later identify() always wins even if its /sdk/evaluate
403
+ // response races and lands before an earlier in-flight call's response.
404
+ identifySeq = 0;
402
405
  onOverrideChange = () => {
403
406
  this.installBridge();
404
407
  this.notify();
@@ -413,8 +416,9 @@ var FlagsClientBrowser = class {
413
416
  void this.buffer.flushPendingAlias();
414
417
  }
415
418
  async identify(user) {
419
+ const seq = ++this.identifySeq;
416
420
  const prevUserId = this.userId;
417
- this.userId = user.user_id ?? "";
421
+ if (user.user_id !== void 0) this.userId = user.user_id;
418
422
  if (this.anonId && this.userId && this.userId !== prevUserId) {
419
423
  await this.buffer.alias(this.anonId, this.userId);
420
424
  }
@@ -432,7 +436,9 @@ var FlagsClientBrowser = class {
432
436
  })
433
437
  });
434
438
  if (!res.ok) throw new Error(`/sdk/evaluate returned ${res.status}`);
435
- this.evalResult = await res.json();
439
+ const data = await res.json();
440
+ if (seq !== this.identifySeq) return;
441
+ this.evalResult = data;
436
442
  if (this.autoGuardrails && !this.guardrailsInstalled) {
437
443
  this.guardrailsInstalled = true;
438
444
  installAutoGuardrails(this.buffer, this.userId, this.anonId);
@@ -656,6 +662,11 @@ function shipeasy(opts) {
656
662
  baseUrl: opts.baseUrl ?? "https://cdn.shipeasy.ai"
657
663
  });
658
664
  flags.notifyMounted();
665
+ if (opts.autoIdentify !== false) {
666
+ void client.identify({}).catch((err) => {
667
+ console.warn("[shipeasy] auto-identify failed:", String(err));
668
+ });
669
+ }
659
670
  return attachDevtools(client, { adminUrl: opts.adminUrl });
660
671
  }
661
672
  function configureShipeasy(opts) {
@@ -791,12 +802,16 @@ var _EDIT_MODE_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode
791
802
  function getSSRI18nStore() {
792
803
  return globalThis[_I18N_SSR_SYM]?.() ?? null;
793
804
  }
805
+ var _EDIT_MODE_FALLBACK_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-fallback");
794
806
  function isEditLabelsMode() {
795
807
  if (typeof window !== "undefined") {
796
808
  return !!window.__SE_BOOTSTRAP?.editLabels || new URLSearchParams(location.search).has("se_edit_labels");
797
809
  }
798
810
  const val = globalThis[_EDIT_MODE_SSR_SYM];
799
- return typeof val === "boolean" ? val : typeof val === "function" ? val() : false;
811
+ if (typeof val === "boolean") return val;
812
+ if (typeof val === "function") return val();
813
+ const fb = globalThis[_EDIT_MODE_FALLBACK_SYM];
814
+ return typeof fb === "boolean" ? fb : false;
800
815
  }
801
816
  function interpolate(raw, variables) {
802
817
  if (!variables) return raw;
@@ -356,6 +356,9 @@ var FlagsClientBrowser = class {
356
356
  guardrailsInstalled = false;
357
357
  listeners = /* @__PURE__ */ new Set();
358
358
  overrideListenerInstalled = false;
359
+ // Monotonic counter so a later identify() always wins even if its /sdk/evaluate
360
+ // response races and lands before an earlier in-flight call's response.
361
+ identifySeq = 0;
359
362
  onOverrideChange = () => {
360
363
  this.installBridge();
361
364
  this.notify();
@@ -370,8 +373,9 @@ var FlagsClientBrowser = class {
370
373
  void this.buffer.flushPendingAlias();
371
374
  }
372
375
  async identify(user) {
376
+ const seq = ++this.identifySeq;
373
377
  const prevUserId = this.userId;
374
- this.userId = user.user_id ?? "";
378
+ if (user.user_id !== void 0) this.userId = user.user_id;
375
379
  if (this.anonId && this.userId && this.userId !== prevUserId) {
376
380
  await this.buffer.alias(this.anonId, this.userId);
377
381
  }
@@ -389,7 +393,9 @@ var FlagsClientBrowser = class {
389
393
  })
390
394
  });
391
395
  if (!res.ok) throw new Error(`/sdk/evaluate returned ${res.status}`);
392
- this.evalResult = await res.json();
396
+ const data = await res.json();
397
+ if (seq !== this.identifySeq) return;
398
+ this.evalResult = data;
393
399
  if (this.autoGuardrails && !this.guardrailsInstalled) {
394
400
  this.guardrailsInstalled = true;
395
401
  installAutoGuardrails(this.buffer, this.userId, this.anonId);
@@ -613,6 +619,11 @@ function shipeasy(opts) {
613
619
  baseUrl: opts.baseUrl ?? "https://cdn.shipeasy.ai"
614
620
  });
615
621
  flags.notifyMounted();
622
+ if (opts.autoIdentify !== false) {
623
+ void client.identify({}).catch((err) => {
624
+ console.warn("[shipeasy] auto-identify failed:", String(err));
625
+ });
626
+ }
616
627
  return attachDevtools(client, { adminUrl: opts.adminUrl });
617
628
  }
618
629
  function configureShipeasy(opts) {
@@ -748,12 +759,16 @@ var _EDIT_MODE_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode
748
759
  function getSSRI18nStore() {
749
760
  return globalThis[_I18N_SSR_SYM]?.() ?? null;
750
761
  }
762
+ var _EDIT_MODE_FALLBACK_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-fallback");
751
763
  function isEditLabelsMode() {
752
764
  if (typeof window !== "undefined") {
753
765
  return !!window.__SE_BOOTSTRAP?.editLabels || new URLSearchParams(location.search).has("se_edit_labels");
754
766
  }
755
767
  const val = globalThis[_EDIT_MODE_SSR_SYM];
756
- return typeof val === "boolean" ? val : typeof val === "function" ? val() : false;
768
+ if (typeof val === "boolean") return val;
769
+ if (typeof val === "function") return val();
770
+ const fb = globalThis[_EDIT_MODE_FALLBACK_SYM];
771
+ return typeof fb === "boolean" ? fb : false;
757
772
  }
758
773
  function interpolate(raw, variables) {
759
774
  if (!variables) return raw;
@@ -493,10 +493,17 @@ async function shipeasy(opts) {
493
493
  let resolvedUrlOverrides = opts.urlOverrides;
494
494
  if (!resolvedUrlOverrides) {
495
495
  try {
496
- const { headers } = await import("next/headers");
496
+ const { headers, cookies } = await import("next/headers");
497
497
  const h = await Promise.resolve(headers());
498
498
  const search = h.get("x-se-search") ?? "";
499
- if (search) resolvedUrlOverrides = search;
499
+ if (search) {
500
+ resolvedUrlOverrides = search;
501
+ } else {
502
+ const c = await Promise.resolve(cookies());
503
+ if (c.get?.("se_edit_labels")?.value === "1") {
504
+ resolvedUrlOverrides = "se_edit_labels=1";
505
+ }
506
+ }
500
507
  } catch {
501
508
  }
502
509
  }
@@ -532,7 +539,7 @@ function getBootstrapHtml(bootstrap, i18nData, opts) {
532
539
  if (i18nData) payload.i18n = i18nData;
533
540
  if (opts.editLabels) payload.editLabels = true;
534
541
  parts.push(
535
- `(function(){if(!new URLSearchParams(location.search).has('se_edit_labels'))return;var R;function P(v){if(!v||typeof v.t!=='function'||v.__sePatched)return;var O=v.t.bind(v);v.__sePatched=true;window._sei18n_t=O;v.t=function(k,vars){var r=O(k,vars);if(r===k)return k;var V='';try{if(vars&&typeof vars==='object'){var hasKey=false;for(var _k in vars){hasKey=true;break;}if(hasKey)V=JSON.stringify(vars);}}catch(_){V='';}return '\\uFFF9'+k+'\\uFFFA'+V+'\\uFFFA'+r+'\\uFFFB';};}Object.defineProperty(window,'i18n',{configurable:true,get:function(){return R;},set:function(v){P(v);R=v;}});})();`
542
+ `(function(){if(!new URLSearchParams(location.search).has('se_edit_labels'))return;try{document.cookie='se_edit_labels=1;path=/;max-age=86400;samesite=lax';}catch(_){}var R;function P(v){if(!v||typeof v.t!=='function'||v.__sePatched)return;var O=v.t.bind(v);v.__sePatched=true;window._sei18n_t=O;v.t=function(k,vars){var r=O(k,vars);if(r===k)return k;var V='';try{if(vars&&typeof vars==='object'){var hasKey=false;for(var _k in vars){hasKey=true;break;}if(hasKey)V=JSON.stringify(vars);}}catch(_){V='';}return '\\uFFF9'+k+'\\uFFFA'+V+'\\uFFFA'+r+'\\uFFFB';};}Object.defineProperty(window,'i18n',{configurable:true,get:function(){return R;},set:function(v){P(v);R=v;}});})();`
536
543
  );
537
544
  parts.push(`window.__SE_BOOTSTRAP=${JSON.stringify(payload)};`);
538
545
  if (i18nData?.strings && Object.keys(i18nData.strings).length > 0) {
@@ -450,10 +450,17 @@ async function shipeasy(opts) {
450
450
  let resolvedUrlOverrides = opts.urlOverrides;
451
451
  if (!resolvedUrlOverrides) {
452
452
  try {
453
- const { headers } = await import("next/headers");
453
+ const { headers, cookies } = await import("next/headers");
454
454
  const h = await Promise.resolve(headers());
455
455
  const search = h.get("x-se-search") ?? "";
456
- if (search) resolvedUrlOverrides = search;
456
+ if (search) {
457
+ resolvedUrlOverrides = search;
458
+ } else {
459
+ const c = await Promise.resolve(cookies());
460
+ if (c.get?.("se_edit_labels")?.value === "1") {
461
+ resolvedUrlOverrides = "se_edit_labels=1";
462
+ }
463
+ }
457
464
  } catch {
458
465
  }
459
466
  }
@@ -489,7 +496,7 @@ function getBootstrapHtml(bootstrap, i18nData, opts) {
489
496
  if (i18nData) payload.i18n = i18nData;
490
497
  if (opts.editLabels) payload.editLabels = true;
491
498
  parts.push(
492
- `(function(){if(!new URLSearchParams(location.search).has('se_edit_labels'))return;var R;function P(v){if(!v||typeof v.t!=='function'||v.__sePatched)return;var O=v.t.bind(v);v.__sePatched=true;window._sei18n_t=O;v.t=function(k,vars){var r=O(k,vars);if(r===k)return k;var V='';try{if(vars&&typeof vars==='object'){var hasKey=false;for(var _k in vars){hasKey=true;break;}if(hasKey)V=JSON.stringify(vars);}}catch(_){V='';}return '\\uFFF9'+k+'\\uFFFA'+V+'\\uFFFA'+r+'\\uFFFB';};}Object.defineProperty(window,'i18n',{configurable:true,get:function(){return R;},set:function(v){P(v);R=v;}});})();`
499
+ `(function(){if(!new URLSearchParams(location.search).has('se_edit_labels'))return;try{document.cookie='se_edit_labels=1;path=/;max-age=86400;samesite=lax';}catch(_){}var R;function P(v){if(!v||typeof v.t!=='function'||v.__sePatched)return;var O=v.t.bind(v);v.__sePatched=true;window._sei18n_t=O;v.t=function(k,vars){var r=O(k,vars);if(r===k)return k;var V='';try{if(vars&&typeof vars==='object'){var hasKey=false;for(var _k in vars){hasKey=true;break;}if(hasKey)V=JSON.stringify(vars);}}catch(_){V='';}return '\\uFFF9'+k+'\\uFFFA'+V+'\\uFFFA'+r+'\\uFFFB';};}Object.defineProperty(window,'i18n',{configurable:true,get:function(){return R;},set:function(v){P(v);R=v;}});})();`
493
500
  );
494
501
  parts.push(`window.__SE_BOOTSTRAP=${JSON.stringify(payload)};`);
495
502
  if (i18nData?.strings && Object.keys(i18nData.strings).length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "2.1.11",
3
+ "version": "2.1.13",
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",