@shipeasy/sdk 4.3.0 → 4.4.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.
@@ -729,17 +729,40 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
729
729
  });
730
730
  }
731
731
  }
732
- function getOrCreateAnonId() {
732
+ function readAnonCookie() {
733
733
  try {
734
- const stored = localStorage.getItem(ANON_ID_KEY);
735
- if (stored) return stored;
734
+ const m = ("; " + document.cookie).match(/; __se_anon_id=([^;]+)/);
735
+ return m ? decodeURIComponent(m[1]) : null;
736
736
  } catch {
737
+ return null;
738
+ }
739
+ }
740
+ function writeAnonCookie(id) {
741
+ try {
742
+ const secure = location.protocol === "https:" ? ";secure" : "";
743
+ document.cookie = `${ANON_ID_KEY}=${id};path=/;max-age=31536000;samesite=lax${secure}`;
744
+ } catch {
745
+ }
746
+ }
747
+ function getOrCreateAnonId() {
748
+ let id = readAnonCookie();
749
+ if (!id && typeof window !== "undefined") {
750
+ id = window.__SE_BOOTSTRAP?.anonId ?? null;
751
+ }
752
+ if (!id) {
753
+ try {
754
+ id = localStorage.getItem(ANON_ID_KEY);
755
+ } catch {
756
+ }
757
+ }
758
+ if (!id) {
759
+ id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
737
760
  }
738
- const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
739
761
  try {
740
762
  localStorage.setItem(ANON_ID_KEY, id);
741
763
  } catch {
742
764
  }
765
+ writeAnonCookie(id);
743
766
  return id;
744
767
  }
745
768
  function collectBrowserAttrs() {
@@ -681,17 +681,40 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
681
681
  });
682
682
  }
683
683
  }
684
- function getOrCreateAnonId() {
684
+ function readAnonCookie() {
685
685
  try {
686
- const stored = localStorage.getItem(ANON_ID_KEY);
687
- if (stored) return stored;
686
+ const m = ("; " + document.cookie).match(/; __se_anon_id=([^;]+)/);
687
+ return m ? decodeURIComponent(m[1]) : null;
688
688
  } catch {
689
+ return null;
690
+ }
691
+ }
692
+ function writeAnonCookie(id) {
693
+ try {
694
+ const secure = location.protocol === "https:" ? ";secure" : "";
695
+ document.cookie = `${ANON_ID_KEY}=${id};path=/;max-age=31536000;samesite=lax${secure}`;
696
+ } catch {
697
+ }
698
+ }
699
+ function getOrCreateAnonId() {
700
+ let id = readAnonCookie();
701
+ if (!id && typeof window !== "undefined") {
702
+ id = window.__SE_BOOTSTRAP?.anonId ?? null;
703
+ }
704
+ if (!id) {
705
+ try {
706
+ id = localStorage.getItem(ANON_ID_KEY);
707
+ } catch {
708
+ }
709
+ }
710
+ if (!id) {
711
+ id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
689
712
  }
690
- const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
691
713
  try {
692
714
  localStorage.setItem(ANON_ID_KEY, id);
693
715
  } catch {
694
716
  }
717
+ writeAnonCookie(id);
695
718
  return id;
696
719
  }
697
720
  function collectBrowserAttrs() {
@@ -134,6 +134,7 @@ interface BootstrapPayload {
134
134
  experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
135
135
  killswitches: Record<string, boolean | Record<string, boolean>>;
136
136
  }
137
+ declare const ANON_ID_COOKIE = "__se_anon_id";
137
138
  type FlagsClientEnv = "dev" | "staging" | "prod";
138
139
  interface FlagsClientOptions {
139
140
  apiKey: string;
@@ -291,6 +292,14 @@ interface BootstrapHtmlOptions {
291
292
  i18nProfile?: string;
292
293
  /** When true, tEl() embeds label markers so the devtools can highlight them. */
293
294
  editLabels?: boolean;
295
+ /**
296
+ * Stable anonymous bucketing id the server evaluated against. Emitted into
297
+ * window.__SE_BOOTSTRAP and persisted (pre-paint) to the first-party
298
+ * `__se_anon_id` cookie, so the browser SDK buckets identically to SSR.
299
+ * Normally minted by edge middleware; this write is the fallback for routes
300
+ * middleware doesn't cover. See experiment-platform/18-identity-bucketing.md.
301
+ */
302
+ anonId?: string;
294
303
  }
295
304
  /**
296
305
  * Returns a vanilla-JS string for a single inline <script> tag. Handles
@@ -380,4 +389,4 @@ interface SeeApi {
380
389
  */
381
390
  declare const see: SeeApi;
382
391
 
383
- export { type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
392
+ export { ANON_ID_COOKIE, type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
@@ -134,6 +134,7 @@ interface BootstrapPayload {
134
134
  experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
135
135
  killswitches: Record<string, boolean | Record<string, boolean>>;
136
136
  }
137
+ declare const ANON_ID_COOKIE = "__se_anon_id";
137
138
  type FlagsClientEnv = "dev" | "staging" | "prod";
138
139
  interface FlagsClientOptions {
139
140
  apiKey: string;
@@ -291,6 +292,14 @@ interface BootstrapHtmlOptions {
291
292
  i18nProfile?: string;
292
293
  /** When true, tEl() embeds label markers so the devtools can highlight them. */
293
294
  editLabels?: boolean;
295
+ /**
296
+ * Stable anonymous bucketing id the server evaluated against. Emitted into
297
+ * window.__SE_BOOTSTRAP and persisted (pre-paint) to the first-party
298
+ * `__se_anon_id` cookie, so the browser SDK buckets identically to SSR.
299
+ * Normally minted by edge middleware; this write is the fallback for routes
300
+ * middleware doesn't cover. See experiment-platform/18-identity-bucketing.md.
301
+ */
302
+ anonId?: string;
294
303
  }
295
304
  /**
296
305
  * Returns a vanilla-JS string for a single inline <script> tag. Handles
@@ -380,4 +389,4 @@ interface SeeApi {
380
389
  */
381
390
  declare const see: SeeApi;
382
391
 
383
- export { type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
392
+ export { ANON_ID_COOKIE, type BootstrapHtmlOptions, type BootstrapPayload, type Consequence, type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type I18nForRequest, type LabelFile, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyServerConfig, type ShipeasyServerHandle, type User, type Violation, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getBootstrapHtml, getShipeasyServerClient, i18n, isExpected, see, seeContext, shipeasy, version };
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/server/index.ts
31
31
  var server_exports = {};
32
32
  __export(server_exports, {
33
+ ANON_ID_COOKIE: () => ANON_ID_COOKIE,
33
34
  FlagsClient: () => FlagsClient,
34
35
  _resetShipeasyServerForTests: () => _resetShipeasyServerForTests,
35
36
  configureShipeasyServer: () => configureShipeasyServer,
@@ -442,6 +443,11 @@ function matchRule(rule, user) {
442
443
  return false;
443
444
  }
444
445
  }
446
+ var ANON_ID_COOKIE = "__se_anon_id";
447
+ var ANON_ID_RX = /^[A-Za-z0-9_-]{1,64}$/;
448
+ function mintAnonId() {
449
+ return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
450
+ }
445
451
  function evalGateInternal(gate, user) {
446
452
  if (isEnabled(gate.killswitch)) return false;
447
453
  if (!isEnabled(gate.enabled)) return false;
@@ -449,7 +455,7 @@ function evalGateInternal(gate, user) {
449
455
  if (!matchRule(rule, user)) return false;
450
456
  }
451
457
  const uid = user.user_id ?? user.anonymous_id;
452
- if (!uid) return false;
458
+ if (!uid) return gate.rolloutPct >= 1e4;
453
459
  return murmur3(`${gate.salt}:${uid}`) % 1e4 < gate.rolloutPct;
454
460
  }
455
461
  var TRUE_RX = /^(true|on|1|yes)$/i;
@@ -885,7 +891,23 @@ async function shipeasy(opts) {
885
891
  serverKey ? flags.initOnce() : Promise.resolve(),
886
892
  serverKey ? i18n.init(serverKey, profile) : Promise.resolve()
887
893
  ]);
888
- const bootstrap = flags.evaluate(opts.user ?? {}, resolvedUrlOverrides);
894
+ let anonId;
895
+ if (!opts.user?.user_id) {
896
+ if (opts.user?.anonymous_id) {
897
+ anonId = opts.user.anonymous_id;
898
+ } else {
899
+ try {
900
+ const { cookies } = await import("next/headers");
901
+ const c = await Promise.resolve(cookies());
902
+ const raw = c.get?.(ANON_ID_COOKIE)?.value;
903
+ if (raw && ANON_ID_RX.test(raw)) anonId = raw;
904
+ } catch {
905
+ }
906
+ if (!anonId) anonId = mintAnonId();
907
+ }
908
+ }
909
+ const effectiveUser = anonId ? { anonymous_id: anonId, ...opts.user } : { ...opts.user };
910
+ const bootstrap = flags.evaluate(effectiveUser, resolvedUrlOverrides);
889
911
  const i18nData = i18n.getForRequest();
890
912
  return {
891
913
  flags: bootstrap.flags,
@@ -894,7 +916,8 @@ async function shipeasy(opts) {
894
916
  getBootstrapHtml() {
895
917
  return getBootstrapHtml(bootstrap, i18nData, {
896
918
  editLabels,
897
- i18nProfile: profile
919
+ i18nProfile: profile,
920
+ anonId
898
921
  });
899
922
  }
900
923
  };
@@ -914,10 +937,16 @@ function getBootstrapHtml(bootstrap, i18nData, opts) {
914
937
  };
915
938
  if (i18nData) payload.i18n = i18nData;
916
939
  if (opts.editLabels) payload.editLabels = true;
940
+ if (opts.anonId) payload.anonId = opts.anonId;
917
941
  parts.push(
918
942
  `(function(){var Q=new URLSearchParams(location.search).has('se_edit_labels');var C=/(?:^|;\\s*)se_edit_labels=1(?:;|$)/.test(document.cookie);if(!Q&&!C)return;if(Q){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;}});})();`
919
943
  );
920
944
  parts.push(`window.__SE_BOOTSTRAP=${JSON.stringify(payload)};`);
945
+ if (opts.anonId) {
946
+ parts.push(
947
+ `(function(){try{var k=${JSON.stringify(ANON_ID_COOKIE)},v=${JSON.stringify(opts.anonId)};if(('; '+document.cookie).indexOf('; '+k+'=')===-1){document.cookie=k+'='+v+';path=/;max-age=31536000;samesite=lax'+(location.protocol==='https:'?';secure':'');}}catch(_){}})();`
948
+ );
949
+ }
921
950
  if (i18nData?.strings && Object.keys(i18nData.strings).length > 0) {
922
951
  parts.push(
923
952
  `(function(){var d=window.__SE_BOOTSTRAP.i18n;if(!d)return;window.i18n={locale:d.locale,t:function(k,v){var r=d.strings[k];if(!r)return k;return v?r.replace(/\\{\\{(\\w+)\\}\\}/g,function(_,p){return v[p]!==undefined?String(v[p]):'{{'+p+'}}'}):r;},on:function(){return function(){};}};})();`
@@ -1003,6 +1032,7 @@ var see = Object.assign(
1003
1032
  );
1004
1033
  // Annotate the CommonJS export names for ESM import in node:
1005
1034
  0 && (module.exports = {
1035
+ ANON_ID_COOKIE,
1006
1036
  FlagsClient,
1007
1037
  _resetShipeasyServerForTests,
1008
1038
  configureShipeasyServer,
@@ -396,6 +396,11 @@ function matchRule(rule, user) {
396
396
  return false;
397
397
  }
398
398
  }
399
+ var ANON_ID_COOKIE = "__se_anon_id";
400
+ var ANON_ID_RX = /^[A-Za-z0-9_-]{1,64}$/;
401
+ function mintAnonId() {
402
+ return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
403
+ }
399
404
  function evalGateInternal(gate, user) {
400
405
  if (isEnabled(gate.killswitch)) return false;
401
406
  if (!isEnabled(gate.enabled)) return false;
@@ -403,7 +408,7 @@ function evalGateInternal(gate, user) {
403
408
  if (!matchRule(rule, user)) return false;
404
409
  }
405
410
  const uid = user.user_id ?? user.anonymous_id;
406
- if (!uid) return false;
411
+ if (!uid) return gate.rolloutPct >= 1e4;
407
412
  return murmur3(`${gate.salt}:${uid}`) % 1e4 < gate.rolloutPct;
408
413
  }
409
414
  var TRUE_RX = /^(true|on|1|yes)$/i;
@@ -839,7 +844,23 @@ async function shipeasy(opts) {
839
844
  serverKey ? flags.initOnce() : Promise.resolve(),
840
845
  serverKey ? i18n.init(serverKey, profile) : Promise.resolve()
841
846
  ]);
842
- const bootstrap = flags.evaluate(opts.user ?? {}, resolvedUrlOverrides);
847
+ let anonId;
848
+ if (!opts.user?.user_id) {
849
+ if (opts.user?.anonymous_id) {
850
+ anonId = opts.user.anonymous_id;
851
+ } else {
852
+ try {
853
+ const { cookies } = await import("next/headers");
854
+ const c = await Promise.resolve(cookies());
855
+ const raw = c.get?.(ANON_ID_COOKIE)?.value;
856
+ if (raw && ANON_ID_RX.test(raw)) anonId = raw;
857
+ } catch {
858
+ }
859
+ if (!anonId) anonId = mintAnonId();
860
+ }
861
+ }
862
+ const effectiveUser = anonId ? { anonymous_id: anonId, ...opts.user } : { ...opts.user };
863
+ const bootstrap = flags.evaluate(effectiveUser, resolvedUrlOverrides);
843
864
  const i18nData = i18n.getForRequest();
844
865
  return {
845
866
  flags: bootstrap.flags,
@@ -848,7 +869,8 @@ async function shipeasy(opts) {
848
869
  getBootstrapHtml() {
849
870
  return getBootstrapHtml(bootstrap, i18nData, {
850
871
  editLabels,
851
- i18nProfile: profile
872
+ i18nProfile: profile,
873
+ anonId
852
874
  });
853
875
  }
854
876
  };
@@ -868,10 +890,16 @@ function getBootstrapHtml(bootstrap, i18nData, opts) {
868
890
  };
869
891
  if (i18nData) payload.i18n = i18nData;
870
892
  if (opts.editLabels) payload.editLabels = true;
893
+ if (opts.anonId) payload.anonId = opts.anonId;
871
894
  parts.push(
872
895
  `(function(){var Q=new URLSearchParams(location.search).has('se_edit_labels');var C=/(?:^|;\\s*)se_edit_labels=1(?:;|$)/.test(document.cookie);if(!Q&&!C)return;if(Q){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;}});})();`
873
896
  );
874
897
  parts.push(`window.__SE_BOOTSTRAP=${JSON.stringify(payload)};`);
898
+ if (opts.anonId) {
899
+ parts.push(
900
+ `(function(){try{var k=${JSON.stringify(ANON_ID_COOKIE)},v=${JSON.stringify(opts.anonId)};if(('; '+document.cookie).indexOf('; '+k+'=')===-1){document.cookie=k+'='+v+';path=/;max-age=31536000;samesite=lax'+(location.protocol==='https:'?';secure':'');}}catch(_){}})();`
901
+ );
902
+ }
875
903
  if (i18nData?.strings && Object.keys(i18nData.strings).length > 0) {
876
904
  parts.push(
877
905
  `(function(){var d=window.__SE_BOOTSTRAP.i18n;if(!d)return;window.i18n={locale:d.locale,t:function(k,v){var r=d.strings[k];if(!r)return k;return v?r.replace(/\\{\\{(\\w+)\\}\\}/g,function(_,p){return v[p]!==undefined?String(v[p]):'{{'+p+'}}'}):r;},on:function(){return function(){};}};})();`
@@ -956,6 +984,7 @@ var see = Object.assign(
956
984
  }
957
985
  );
958
986
  export {
987
+ ANON_ID_COOKIE,
959
988
  FlagsClient,
960
989
  _resetShipeasyServerForTests,
961
990
  configureShipeasyServer,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "4.3.0",
3
+ "version": "4.4.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",