@shipeasy/sdk 4.2.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.
@@ -51,6 +51,16 @@ interface SeeErrorEvent {
51
51
  env?: string;
52
52
  sdk_version: string;
53
53
  ts: number;
54
+ /**
55
+ * Per-request correlation token. The client mints one per same-origin fetch
56
+ * and ships it on both the request header (`X-SE-Correlation`) and any 5xx
57
+ * occurrence it reports; the server safety net reports the matching uncaught
58
+ * error under the same token. The backend joins the two issues by it —
59
+ * populating `caused_by` across the network boundary, where the in-process
60
+ * `.cause`-chain stamp (see `findCausedBy`) cannot reach. Join-only metadata,
61
+ * never persisted as an issue field.
62
+ */
63
+ correlation_id?: string;
54
64
  /**
55
65
  * The earlier reported problem this occurrence descends from — present when
56
66
  * the same error was caught + reported at an inner boundary and then
@@ -120,6 +130,15 @@ interface AutoCollectGroups {
120
130
  errors: boolean;
121
131
  engagement: boolean;
122
132
  }
133
+ /** True when `rawUrl` resolves to the page's own origin (relative URLs included). */
134
+ declare function sameOrigin(rawUrl: string): boolean;
135
+ /**
136
+ * Return a new `fetch` args tuple with `X-SE-Correlation` added, preserving any
137
+ * existing headers across all three arg shapes (string / URL / Request). Never
138
+ * mutates the caller's objects. Best-effort: on any failure the original args
139
+ * pass through unchanged (correlation is optional, never breaks the fetch).
140
+ */
141
+ declare function injectCorrelationHeader(args: Parameters<typeof fetch>, corr: string): Parameters<typeof fetch>;
123
142
  type FlagsClientBrowserEnv = "dev" | "staging" | "prod";
124
143
  interface FlagsClientBrowserOptions {
125
144
  sdkKey: string;
@@ -176,7 +195,7 @@ declare class FlagsClientBrowser {
176
195
  * (beacon-first) — error occurrences are near-real-time, never queued behind
177
196
  * the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
178
197
  */
179
- reportError(problem: unknown, consequence: Consequence, extras?: SeeExtras, kind?: SeeKind): void;
198
+ reportError(problem: unknown, consequence: Consequence, extras?: SeeExtras, kind?: SeeKind, correlationId?: string): void;
180
199
  get ready(): boolean;
181
200
  private notify;
182
201
  initFromBootstrap(data: EvalResponse): void;
@@ -478,4 +497,4 @@ interface I18nFacade {
478
497
  }
479
498
  declare const i18n: I18nFacade;
480
499
 
481
- export { type AutoCollectGroups, type BootstrapPayload, type Consequence, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, see, shipeasy, version };
500
+ export { type AutoCollectGroups, type BootstrapPayload, type Consequence, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
@@ -51,6 +51,16 @@ interface SeeErrorEvent {
51
51
  env?: string;
52
52
  sdk_version: string;
53
53
  ts: number;
54
+ /**
55
+ * Per-request correlation token. The client mints one per same-origin fetch
56
+ * and ships it on both the request header (`X-SE-Correlation`) and any 5xx
57
+ * occurrence it reports; the server safety net reports the matching uncaught
58
+ * error under the same token. The backend joins the two issues by it —
59
+ * populating `caused_by` across the network boundary, where the in-process
60
+ * `.cause`-chain stamp (see `findCausedBy`) cannot reach. Join-only metadata,
61
+ * never persisted as an issue field.
62
+ */
63
+ correlation_id?: string;
54
64
  /**
55
65
  * The earlier reported problem this occurrence descends from — present when
56
66
  * the same error was caught + reported at an inner boundary and then
@@ -120,6 +130,15 @@ interface AutoCollectGroups {
120
130
  errors: boolean;
121
131
  engagement: boolean;
122
132
  }
133
+ /** True when `rawUrl` resolves to the page's own origin (relative URLs included). */
134
+ declare function sameOrigin(rawUrl: string): boolean;
135
+ /**
136
+ * Return a new `fetch` args tuple with `X-SE-Correlation` added, preserving any
137
+ * existing headers across all three arg shapes (string / URL / Request). Never
138
+ * mutates the caller's objects. Best-effort: on any failure the original args
139
+ * pass through unchanged (correlation is optional, never breaks the fetch).
140
+ */
141
+ declare function injectCorrelationHeader(args: Parameters<typeof fetch>, corr: string): Parameters<typeof fetch>;
123
142
  type FlagsClientBrowserEnv = "dev" | "staging" | "prod";
124
143
  interface FlagsClientBrowserOptions {
125
144
  sdkKey: string;
@@ -176,7 +195,7 @@ declare class FlagsClientBrowser {
176
195
  * (beacon-first) — error occurrences are near-real-time, never queued behind
177
196
  * the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
178
197
  */
179
- reportError(problem: unknown, consequence: Consequence, extras?: SeeExtras, kind?: SeeKind): void;
198
+ reportError(problem: unknown, consequence: Consequence, extras?: SeeExtras, kind?: SeeKind, correlationId?: string): void;
180
199
  get ready(): boolean;
181
200
  private notify;
182
201
  initFromBootstrap(data: EvalResponse): void;
@@ -478,4 +497,4 @@ interface I18nFacade {
478
497
  }
479
498
  declare const i18n: I18nFacade;
480
499
 
481
- export { type AutoCollectGroups, type BootstrapPayload, type Consequence, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, see, shipeasy, version };
500
+ export { type AutoCollectGroups, type BootstrapPayload, type Consequence, type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserEnv, type FlagsClientBrowserOptions, type I18nFacade, type I18nKey, type I18nRichComponents, type I18nString, type I18nTagRenderer, type I18nVariables, LABEL_MARKER_END, LABEL_MARKER_RE, LABEL_MARKER_SEP, LABEL_MARKER_START, type LabelAttrs, type SeeApi, type SeeChain, type SeeErrorEvent, type SeeExtras, type SeeKind, type SeeViolationChain, type ShipeasyClientConfig, type ShipeasySdkBridge, type User, type Violation, _resetShipeasyForTests, attachDevtools, configureShipeasy, encodeLabelMarker, flags, getShipeasyClient, i18n, injectCorrelationHeader, isDevtoolsRequested, labelAttrs, loadDevtools, readConfigOverride, readExpOverride, readGateOverride, sameOrigin, see, shipeasy, version };
@@ -32,12 +32,14 @@ __export(client_exports, {
32
32
  flags: () => flags,
33
33
  getShipeasyClient: () => getShipeasyClient,
34
34
  i18n: () => i18n,
35
+ injectCorrelationHeader: () => injectCorrelationHeader,
35
36
  isDevtoolsRequested: () => isDevtoolsRequested,
36
37
  labelAttrs: () => labelAttrs,
37
38
  loadDevtools: () => loadDevtools,
38
39
  readConfigOverride: () => readConfigOverride,
39
40
  readExpOverride: () => readExpOverride,
40
41
  readGateOverride: () => readGateOverride,
42
+ sameOrigin: () => sameOrigin,
41
43
  see: () => see,
42
44
  shipeasy: () => shipeasy,
43
45
  version: () => version
@@ -109,6 +111,7 @@ var SEE_MAX_STACK = 8e3;
109
111
  var SEE_MAX_SUBJECT = 200;
110
112
  var SEE_MAX_EXTRA_VALUE = 200;
111
113
  var SEE_MAX_EXTRA_KEYS = 20;
114
+ var SEE_MAX_CORRELATION = 64;
112
115
  var SEE_DEDUP_WINDOW_MS = 3e4;
113
116
  var SEE_MAX_PER_SESSION = 25;
114
117
  function causesThe(subject) {
@@ -215,7 +218,7 @@ function captureCallsiteStack() {
215
218
  const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
216
219
  return kept.length ? kept.join("\n") : void 0;
217
220
  }
218
- function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
221
+ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlationId) {
219
222
  let errorType;
220
223
  let message;
221
224
  let stack;
@@ -248,6 +251,7 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
248
251
  ts: Date.now()
249
252
  };
250
253
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
254
+ if (correlationId) ev.correlation_id = truncate(String(correlationId), SEE_MAX_CORRELATION);
251
255
  const causedBy = findCausedBy(problem);
252
256
  if (causedBy) ev.caused_by = causedBy;
253
257
  const cleanExtras = sanitizeExtras(extras);
@@ -501,8 +505,33 @@ function endpointTemplate(rawUrl) {
501
505
  return (rawUrl.split(/[?#]/)[0] ?? "").slice(0, 120);
502
506
  }
503
507
  const path = u.pathname.split("/").map((seg) => seg && isIdSegment(seg) ? ":id" : seg).join("/");
504
- const sameOrigin = typeof location !== "undefined" && u.origin === location.origin;
505
- return ((sameOrigin ? "" : u.host) + path).slice(0, 120);
508
+ const sameOrigin2 = typeof location !== "undefined" && u.origin === location.origin;
509
+ return ((sameOrigin2 ? "" : u.host) + path).slice(0, 120);
510
+ }
511
+ function sameOrigin(rawUrl) {
512
+ if (typeof location === "undefined") return false;
513
+ try {
514
+ return new URL(rawUrl, location.href).origin === location.origin;
515
+ } catch {
516
+ return false;
517
+ }
518
+ }
519
+ function injectCorrelationHeader(args, corr) {
520
+ try {
521
+ const input = args[0];
522
+ if (typeof Request !== "undefined" && input instanceof Request) {
523
+ const headers2 = new Headers(input.headers);
524
+ headers2.set("X-SE-Correlation", corr);
525
+ return [new Request(input, { headers: headers2 }), ...args.slice(1)];
526
+ }
527
+ const init = { ...args[1] ?? {} };
528
+ const headers = new Headers(init.headers ?? void 0);
529
+ headers.set("X-SE-Correlation", corr);
530
+ init.headers = headers;
531
+ return [input, init];
532
+ } catch {
533
+ return args;
534
+ }
506
535
  }
507
536
  function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignoreUrlPrefixes, always = false) {
508
537
  if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
@@ -579,6 +608,11 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
579
608
  const url = typeof args[0] === "string" ? args[0] : args[0].toString();
580
609
  const ignored = ignoreUrlPrefixes.some((p) => p && url.startsWith(p));
581
610
  const bareUrl = url.split("?")[0].slice(0, 200);
611
+ let corr;
612
+ if (!ignored && sameOrigin(url) && typeof crypto !== "undefined" && crypto.randomUUID) {
613
+ corr = crypto.randomUUID();
614
+ args = injectCorrelationHeader(args, corr);
615
+ }
582
616
  let res;
583
617
  try {
584
618
  res = await origFetch.apply(this, args);
@@ -599,7 +633,8 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
599
633
  violation("Http5xx").message(`request to ${bareUrl} returned ${res.status}`),
600
634
  causesThe(`request to ${endpointTemplate(url)}`).to("fail with a server error"),
601
635
  { status: res.status, url: url.slice(0, 200), duration_ms: Math.round(elapsed) },
602
- "network"
636
+ "network",
637
+ corr
603
638
  );
604
639
  }
605
640
  return res;
@@ -694,17 +729,40 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
694
729
  });
695
730
  }
696
731
  }
697
- function getOrCreateAnonId() {
732
+ function readAnonCookie() {
698
733
  try {
699
- const stored = localStorage.getItem(ANON_ID_KEY);
700
- if (stored) return stored;
734
+ const m = ("; " + document.cookie).match(/; __se_anon_id=([^;]+)/);
735
+ return m ? decodeURIComponent(m[1]) : null;
701
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)}`;
702
760
  }
703
- const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
704
761
  try {
705
762
  localStorage.setItem(ANON_ID_KEY, id);
706
763
  } catch {
707
764
  }
765
+ writeAnonCookie(id);
708
766
  return id;
709
767
  }
710
768
  function collectBrowserAttrs() {
@@ -911,7 +969,7 @@ var FlagsClientBrowser = class {
911
969
  this.userId,
912
970
  this.anonId,
913
971
  this.autoGuardrailGroups,
914
- (problem, consequence, extras, kind) => this.reportError(problem, consequence, extras, kind),
972
+ (problem, consequence, extras, kind, correlationId) => this.reportError(problem, consequence, extras, kind, correlationId),
915
973
  [`${this.baseUrl}/`, DEFAULT_TELEMETRY_URL],
916
974
  this.autoCollectAlways
917
975
  );
@@ -923,7 +981,7 @@ var FlagsClientBrowser = class {
923
981
  * (beacon-first) — error occurrences are near-real-time, never queued behind
924
982
  * the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
925
983
  */
926
- reportError(problem, consequence, extras, kind) {
984
+ reportError(problem, consequence, extras, kind, correlationId) {
927
985
  try {
928
986
  const enriched = { ...collectSeeEnv(), ...extras };
929
987
  const ev = buildSeeEvent(problem, consequence, enriched, {
@@ -933,7 +991,7 @@ var FlagsClientBrowser = class {
933
991
  url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
934
992
  userId: this.userId || void 0,
935
993
  anonId: this.anonId
936
- }, kind);
994
+ }, kind, correlationId);
937
995
  if (!this.seeLimiter.shouldSend(ev)) return;
938
996
  this.buffer.sendNow([ev]);
939
997
  } catch {
@@ -1600,12 +1658,14 @@ var i18n = {
1600
1658
  flags,
1601
1659
  getShipeasyClient,
1602
1660
  i18n,
1661
+ injectCorrelationHeader,
1603
1662
  isDevtoolsRequested,
1604
1663
  labelAttrs,
1605
1664
  loadDevtools,
1606
1665
  readConfigOverride,
1607
1666
  readExpOverride,
1608
1667
  readGateOverride,
1668
+ sameOrigin,
1609
1669
  see,
1610
1670
  shipeasy,
1611
1671
  version
@@ -63,6 +63,7 @@ var SEE_MAX_STACK = 8e3;
63
63
  var SEE_MAX_SUBJECT = 200;
64
64
  var SEE_MAX_EXTRA_VALUE = 200;
65
65
  var SEE_MAX_EXTRA_KEYS = 20;
66
+ var SEE_MAX_CORRELATION = 64;
66
67
  var SEE_DEDUP_WINDOW_MS = 3e4;
67
68
  var SEE_MAX_PER_SESSION = 25;
68
69
  function causesThe(subject) {
@@ -169,7 +170,7 @@ function captureCallsiteStack() {
169
170
  const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
170
171
  return kept.length ? kept.join("\n") : void 0;
171
172
  }
172
- function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
173
+ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlationId) {
173
174
  let errorType;
174
175
  let message;
175
176
  let stack;
@@ -202,6 +203,7 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
202
203
  ts: Date.now()
203
204
  };
204
205
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
206
+ if (correlationId) ev.correlation_id = truncate(String(correlationId), SEE_MAX_CORRELATION);
205
207
  const causedBy = findCausedBy(problem);
206
208
  if (causedBy) ev.caused_by = causedBy;
207
209
  const cleanExtras = sanitizeExtras(extras);
@@ -455,8 +457,33 @@ function endpointTemplate(rawUrl) {
455
457
  return (rawUrl.split(/[?#]/)[0] ?? "").slice(0, 120);
456
458
  }
457
459
  const path = u.pathname.split("/").map((seg) => seg && isIdSegment(seg) ? ":id" : seg).join("/");
458
- const sameOrigin = typeof location !== "undefined" && u.origin === location.origin;
459
- return ((sameOrigin ? "" : u.host) + path).slice(0, 120);
460
+ const sameOrigin2 = typeof location !== "undefined" && u.origin === location.origin;
461
+ return ((sameOrigin2 ? "" : u.host) + path).slice(0, 120);
462
+ }
463
+ function sameOrigin(rawUrl) {
464
+ if (typeof location === "undefined") return false;
465
+ try {
466
+ return new URL(rawUrl, location.href).origin === location.origin;
467
+ } catch {
468
+ return false;
469
+ }
470
+ }
471
+ function injectCorrelationHeader(args, corr) {
472
+ try {
473
+ const input = args[0];
474
+ if (typeof Request !== "undefined" && input instanceof Request) {
475
+ const headers2 = new Headers(input.headers);
476
+ headers2.set("X-SE-Correlation", corr);
477
+ return [new Request(input, { headers: headers2 }), ...args.slice(1)];
478
+ }
479
+ const init = { ...args[1] ?? {} };
480
+ const headers = new Headers(init.headers ?? void 0);
481
+ headers.set("X-SE-Correlation", corr);
482
+ init.headers = headers;
483
+ return [input, init];
484
+ } catch {
485
+ return args;
486
+ }
460
487
  }
461
488
  function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignoreUrlPrefixes, always = false) {
462
489
  if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
@@ -533,6 +560,11 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
533
560
  const url = typeof args[0] === "string" ? args[0] : args[0].toString();
534
561
  const ignored = ignoreUrlPrefixes.some((p) => p && url.startsWith(p));
535
562
  const bareUrl = url.split("?")[0].slice(0, 200);
563
+ let corr;
564
+ if (!ignored && sameOrigin(url) && typeof crypto !== "undefined" && crypto.randomUUID) {
565
+ corr = crypto.randomUUID();
566
+ args = injectCorrelationHeader(args, corr);
567
+ }
536
568
  let res;
537
569
  try {
538
570
  res = await origFetch.apply(this, args);
@@ -553,7 +585,8 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
553
585
  violation("Http5xx").message(`request to ${bareUrl} returned ${res.status}`),
554
586
  causesThe(`request to ${endpointTemplate(url)}`).to("fail with a server error"),
555
587
  { status: res.status, url: url.slice(0, 200), duration_ms: Math.round(elapsed) },
556
- "network"
588
+ "network",
589
+ corr
557
590
  );
558
591
  }
559
592
  return res;
@@ -648,17 +681,40 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
648
681
  });
649
682
  }
650
683
  }
651
- function getOrCreateAnonId() {
684
+ function readAnonCookie() {
652
685
  try {
653
- const stored = localStorage.getItem(ANON_ID_KEY);
654
- if (stored) return stored;
686
+ const m = ("; " + document.cookie).match(/; __se_anon_id=([^;]+)/);
687
+ return m ? decodeURIComponent(m[1]) : null;
655
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)}`;
656
712
  }
657
- const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
658
713
  try {
659
714
  localStorage.setItem(ANON_ID_KEY, id);
660
715
  } catch {
661
716
  }
717
+ writeAnonCookie(id);
662
718
  return id;
663
719
  }
664
720
  function collectBrowserAttrs() {
@@ -865,7 +921,7 @@ var FlagsClientBrowser = class {
865
921
  this.userId,
866
922
  this.anonId,
867
923
  this.autoGuardrailGroups,
868
- (problem, consequence, extras, kind) => this.reportError(problem, consequence, extras, kind),
924
+ (problem, consequence, extras, kind, correlationId) => this.reportError(problem, consequence, extras, kind, correlationId),
869
925
  [`${this.baseUrl}/`, DEFAULT_TELEMETRY_URL],
870
926
  this.autoCollectAlways
871
927
  );
@@ -877,7 +933,7 @@ var FlagsClientBrowser = class {
877
933
  * (beacon-first) — error occurrences are near-real-time, never queued behind
878
934
  * the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
879
935
  */
880
- reportError(problem, consequence, extras, kind) {
936
+ reportError(problem, consequence, extras, kind, correlationId) {
881
937
  try {
882
938
  const enriched = { ...collectSeeEnv(), ...extras };
883
939
  const ev = buildSeeEvent(problem, consequence, enriched, {
@@ -887,7 +943,7 @@ var FlagsClientBrowser = class {
887
943
  url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
888
944
  userId: this.userId || void 0,
889
945
  anonId: this.anonId
890
- }, kind);
946
+ }, kind, correlationId);
891
947
  if (!this.seeLimiter.shouldSend(ev)) return;
892
948
  this.buffer.sendNow([ev]);
893
949
  } catch {
@@ -1553,12 +1609,14 @@ export {
1553
1609
  flags,
1554
1610
  getShipeasyClient,
1555
1611
  i18n,
1612
+ injectCorrelationHeader,
1556
1613
  isDevtoolsRequested,
1557
1614
  labelAttrs,
1558
1615
  loadDevtools,
1559
1616
  readConfigOverride,
1560
1617
  readExpOverride,
1561
1618
  readGateOverride,
1619
+ sameOrigin,
1562
1620
  see,
1563
1621
  shipeasy,
1564
1622
  version
@@ -1,3 +1,5 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+
1
3
  type SeeExtras = Record<string, string | number | boolean | null | undefined>;
2
4
  type SeeKind = "caught" | "uncaught" | "unhandled_rejection" | "network" | "violation";
3
5
  /** Built by `causesThe(subject).to(outcome)` — never constructed by hand. */
@@ -51,6 +53,16 @@ interface SeeErrorEvent {
51
53
  env?: string;
52
54
  sdk_version: string;
53
55
  ts: number;
56
+ /**
57
+ * Per-request correlation token. The client mints one per same-origin fetch
58
+ * and ships it on both the request header (`X-SE-Correlation`) and any 5xx
59
+ * occurrence it reports; the server safety net reports the matching uncaught
60
+ * error under the same token. The backend joins the two issues by it —
61
+ * populating `caused_by` across the network boundary, where the in-process
62
+ * `.cause`-chain stamp (see `findCausedBy`) cannot reach. Join-only metadata,
63
+ * never persisted as an issue field.
64
+ */
65
+ correlation_id?: string;
54
66
  /**
55
67
  * The earlier reported problem this occurrence descends from — present when
56
68
  * the same error was caught + reported at an inner boundary and then
@@ -60,6 +72,7 @@ interface SeeErrorEvent {
60
72
  */
61
73
  caused_by?: SeeCausedBy;
62
74
  }
75
+ declare function isExpected(err: unknown): boolean;
63
76
  interface SeeExtrasTail {
64
77
  /** Attach debugging metadata. Callable repeatedly — keys merge, later wins. */
65
78
  extras(extras: SeeExtras): SeeExtrasTail;
@@ -121,6 +134,7 @@ interface BootstrapPayload {
121
134
  experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
122
135
  killswitches: Record<string, boolean | Record<string, boolean>>;
123
136
  }
137
+ declare const ANON_ID_COOKIE = "__se_anon_id";
124
138
  type FlagsClientEnv = "dev" | "staging" | "prod";
125
139
  interface FlagsClientOptions {
126
140
  apiKey: string;
@@ -187,6 +201,11 @@ declare class FlagsClient {
187
201
  evaluate(user: User, rawUrl?: string): BootstrapPayload;
188
202
  getKillswitch(name: string, switchKey?: string): boolean;
189
203
  }
204
+ interface SeeCorrelationStore {
205
+ correlationId?: string;
206
+ }
207
+ declare const seeContext: AsyncLocalStorage<SeeCorrelationStore>;
208
+
190
209
  interface I18nForRequest {
191
210
  strings: Record<string, string>;
192
211
  locale: string;
@@ -273,6 +292,14 @@ interface BootstrapHtmlOptions {
273
292
  i18nProfile?: string;
274
293
  /** When true, tEl() embeds label markers so the devtools can highlight them. */
275
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;
276
303
  }
277
304
  /**
278
305
  * Returns a vanilla-JS string for a single inline <script> tag. Handles
@@ -362,4 +389,4 @@ interface SeeApi {
362
389
  */
363
390
  declare const see: SeeApi;
364
391
 
365
- 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, see, 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 };
@@ -1,3 +1,5 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+
1
3
  type SeeExtras = Record<string, string | number | boolean | null | undefined>;
2
4
  type SeeKind = "caught" | "uncaught" | "unhandled_rejection" | "network" | "violation";
3
5
  /** Built by `causesThe(subject).to(outcome)` — never constructed by hand. */
@@ -51,6 +53,16 @@ interface SeeErrorEvent {
51
53
  env?: string;
52
54
  sdk_version: string;
53
55
  ts: number;
56
+ /**
57
+ * Per-request correlation token. The client mints one per same-origin fetch
58
+ * and ships it on both the request header (`X-SE-Correlation`) and any 5xx
59
+ * occurrence it reports; the server safety net reports the matching uncaught
60
+ * error under the same token. The backend joins the two issues by it —
61
+ * populating `caused_by` across the network boundary, where the in-process
62
+ * `.cause`-chain stamp (see `findCausedBy`) cannot reach. Join-only metadata,
63
+ * never persisted as an issue field.
64
+ */
65
+ correlation_id?: string;
54
66
  /**
55
67
  * The earlier reported problem this occurrence descends from — present when
56
68
  * the same error was caught + reported at an inner boundary and then
@@ -60,6 +72,7 @@ interface SeeErrorEvent {
60
72
  */
61
73
  caused_by?: SeeCausedBy;
62
74
  }
75
+ declare function isExpected(err: unknown): boolean;
63
76
  interface SeeExtrasTail {
64
77
  /** Attach debugging metadata. Callable repeatedly — keys merge, later wins. */
65
78
  extras(extras: SeeExtras): SeeExtrasTail;
@@ -121,6 +134,7 @@ interface BootstrapPayload {
121
134
  experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
122
135
  killswitches: Record<string, boolean | Record<string, boolean>>;
123
136
  }
137
+ declare const ANON_ID_COOKIE = "__se_anon_id";
124
138
  type FlagsClientEnv = "dev" | "staging" | "prod";
125
139
  interface FlagsClientOptions {
126
140
  apiKey: string;
@@ -187,6 +201,11 @@ declare class FlagsClient {
187
201
  evaluate(user: User, rawUrl?: string): BootstrapPayload;
188
202
  getKillswitch(name: string, switchKey?: string): boolean;
189
203
  }
204
+ interface SeeCorrelationStore {
205
+ correlationId?: string;
206
+ }
207
+ declare const seeContext: AsyncLocalStorage<SeeCorrelationStore>;
208
+
190
209
  interface I18nForRequest {
191
210
  strings: Record<string, string>;
192
211
  locale: string;
@@ -273,6 +292,14 @@ interface BootstrapHtmlOptions {
273
292
  i18nProfile?: string;
274
293
  /** When true, tEl() embeds label markers so the devtools can highlight them. */
275
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;
276
303
  }
277
304
  /**
278
305
  * Returns a vanilla-JS string for a single inline <script> tag. Handles
@@ -362,4 +389,4 @@ interface SeeApi {
362
389
  */
363
390
  declare const see: SeeApi;
364
391
 
365
- 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, see, 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,
@@ -38,7 +39,9 @@ __export(server_exports, {
38
39
  getBootstrapHtml: () => getBootstrapHtml,
39
40
  getShipeasyServerClient: () => getShipeasyServerClient,
40
41
  i18n: () => i18n,
42
+ isExpected: () => isExpected,
41
43
  see: () => see,
44
+ seeContext: () => seeContext,
42
45
  shipeasy: () => shipeasy,
43
46
  version: () => version
44
47
  });
@@ -110,6 +113,7 @@ var SEE_MAX_STACK = 8e3;
110
113
  var SEE_MAX_SUBJECT = 200;
111
114
  var SEE_MAX_EXTRA_VALUE = 200;
112
115
  var SEE_MAX_EXTRA_KEYS = 20;
116
+ var SEE_MAX_CORRELATION = 64;
113
117
  var SEE_DEDUP_WINDOW_MS = 3e4;
114
118
  var SEE_MAX_PER_SESSION = 25;
115
119
  function causesThe(subject) {
@@ -149,6 +153,10 @@ function markExpected(err, because) {
149
153
  } catch {
150
154
  }
151
155
  }
156
+ function isExpected(err) {
157
+ if (typeof err !== "object" || err === null) return false;
158
+ return err[EXPECTED_SYM] !== void 0;
159
+ }
152
160
  var REPORTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-reported");
153
161
  var SEE_MAX_CAUSE_DEPTH = 8;
154
162
  function readReportStamp(err) {
@@ -212,7 +220,7 @@ function captureCallsiteStack() {
212
220
  const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
213
221
  return kept.length ? kept.join("\n") : void 0;
214
222
  }
215
- function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
223
+ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlationId) {
216
224
  let errorType;
217
225
  let message;
218
226
  let stack;
@@ -245,6 +253,7 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
245
253
  ts: Date.now()
246
254
  };
247
255
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
256
+ if (correlationId) ev.correlation_id = truncate(String(correlationId), SEE_MAX_CORRELATION);
248
257
  const causedBy = findCausedBy(problem);
249
258
  if (causedBy) ev.caused_by = causedBy;
250
259
  const cleanExtras = sanitizeExtras(extras);
@@ -434,6 +443,11 @@ function matchRule(rule, user) {
434
443
  return false;
435
444
  }
436
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
+ }
437
451
  function evalGateInternal(gate, user) {
438
452
  if (isEnabled(gate.killswitch)) return false;
439
453
  if (!isEnabled(gate.enabled)) return false;
@@ -441,7 +455,7 @@ function evalGateInternal(gate, user) {
441
455
  if (!matchRule(rule, user)) return false;
442
456
  }
443
457
  const uid = user.user_id ?? user.anonymous_id;
444
- if (!uid) return false;
458
+ if (!uid) return gate.rolloutPct >= 1e4;
445
459
  return murmur3(`${gate.salt}:${uid}`) % 1e4 < gate.rolloutPct;
446
460
  }
447
461
  var TRUE_RX = /^(true|on|1|yes)$/i;
@@ -654,11 +668,12 @@ var FlagsClient = class {
654
668
  */
655
669
  reportError(problem, consequence, extras, kind) {
656
670
  try {
671
+ const correlationId = seeContext.getStore()?.correlationId;
657
672
  const ev = buildSeeEvent(problem, consequence, extras, {
658
673
  side: "server",
659
674
  sdkVersion: version,
660
675
  env: this.env
661
- }, kind);
676
+ }, kind, correlationId);
662
677
  if (!this.seeLimiter.shouldSend(ev)) return;
663
678
  globalThis.fetch(`${this.baseUrl}/collect`, {
664
679
  method: "POST",
@@ -753,6 +768,8 @@ Object.defineProperty(globalThis, _EDIT_MODE_SSR_SYM, {
753
768
  },
754
769
  configurable: true
755
770
  });
771
+ var _SEE_CORR_ALS_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-correlation-als");
772
+ var seeContext = globalThis[_SEE_CORR_ALS_SYM] ?? (globalThis[_SEE_CORR_ALS_SYM] = new import_node_async_hooks.AsyncLocalStorage());
756
773
  var i18n = {
757
774
  /**
758
775
  * Fetch translation labels for the current request and store them in an
@@ -874,7 +891,23 @@ async function shipeasy(opts) {
874
891
  serverKey ? flags.initOnce() : Promise.resolve(),
875
892
  serverKey ? i18n.init(serverKey, profile) : Promise.resolve()
876
893
  ]);
877
- 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);
878
911
  const i18nData = i18n.getForRequest();
879
912
  return {
880
913
  flags: bootstrap.flags,
@@ -883,7 +916,8 @@ async function shipeasy(opts) {
883
916
  getBootstrapHtml() {
884
917
  return getBootstrapHtml(bootstrap, i18nData, {
885
918
  editLabels,
886
- i18nProfile: profile
919
+ i18nProfile: profile,
920
+ anonId
887
921
  });
888
922
  }
889
923
  };
@@ -903,10 +937,16 @@ function getBootstrapHtml(bootstrap, i18nData, opts) {
903
937
  };
904
938
  if (i18nData) payload.i18n = i18nData;
905
939
  if (opts.editLabels) payload.editLabels = true;
940
+ if (opts.anonId) payload.anonId = opts.anonId;
906
941
  parts.push(
907
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;}});})();`
908
943
  );
909
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
+ }
910
950
  if (i18nData?.strings && Object.keys(i18nData.strings).length > 0) {
911
951
  parts.push(
912
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(){};}};})();`
@@ -992,6 +1032,7 @@ var see = Object.assign(
992
1032
  );
993
1033
  // Annotate the CommonJS export names for ESM import in node:
994
1034
  0 && (module.exports = {
1035
+ ANON_ID_COOKIE,
995
1036
  FlagsClient,
996
1037
  _resetShipeasyServerForTests,
997
1038
  configureShipeasyServer,
@@ -1000,7 +1041,9 @@ var see = Object.assign(
1000
1041
  getBootstrapHtml,
1001
1042
  getShipeasyServerClient,
1002
1043
  i18n,
1044
+ isExpected,
1003
1045
  see,
1046
+ seeContext,
1004
1047
  shipeasy,
1005
1048
  version
1006
1049
  });
@@ -66,6 +66,7 @@ var SEE_MAX_STACK = 8e3;
66
66
  var SEE_MAX_SUBJECT = 200;
67
67
  var SEE_MAX_EXTRA_VALUE = 200;
68
68
  var SEE_MAX_EXTRA_KEYS = 20;
69
+ var SEE_MAX_CORRELATION = 64;
69
70
  var SEE_DEDUP_WINDOW_MS = 3e4;
70
71
  var SEE_MAX_PER_SESSION = 25;
71
72
  function causesThe(subject) {
@@ -105,6 +106,10 @@ function markExpected(err, because) {
105
106
  } catch {
106
107
  }
107
108
  }
109
+ function isExpected(err) {
110
+ if (typeof err !== "object" || err === null) return false;
111
+ return err[EXPECTED_SYM] !== void 0;
112
+ }
108
113
  var REPORTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-reported");
109
114
  var SEE_MAX_CAUSE_DEPTH = 8;
110
115
  function readReportStamp(err) {
@@ -168,7 +173,7 @@ function captureCallsiteStack() {
168
173
  const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
169
174
  return kept.length ? kept.join("\n") : void 0;
170
175
  }
171
- function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
176
+ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlationId) {
172
177
  let errorType;
173
178
  let message;
174
179
  let stack;
@@ -201,6 +206,7 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
201
206
  ts: Date.now()
202
207
  };
203
208
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
209
+ if (correlationId) ev.correlation_id = truncate(String(correlationId), SEE_MAX_CORRELATION);
204
210
  const causedBy = findCausedBy(problem);
205
211
  if (causedBy) ev.caused_by = causedBy;
206
212
  const cleanExtras = sanitizeExtras(extras);
@@ -390,6 +396,11 @@ function matchRule(rule, user) {
390
396
  return false;
391
397
  }
392
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
+ }
393
404
  function evalGateInternal(gate, user) {
394
405
  if (isEnabled(gate.killswitch)) return false;
395
406
  if (!isEnabled(gate.enabled)) return false;
@@ -397,7 +408,7 @@ function evalGateInternal(gate, user) {
397
408
  if (!matchRule(rule, user)) return false;
398
409
  }
399
410
  const uid = user.user_id ?? user.anonymous_id;
400
- if (!uid) return false;
411
+ if (!uid) return gate.rolloutPct >= 1e4;
401
412
  return murmur3(`${gate.salt}:${uid}`) % 1e4 < gate.rolloutPct;
402
413
  }
403
414
  var TRUE_RX = /^(true|on|1|yes)$/i;
@@ -610,11 +621,12 @@ var FlagsClient = class {
610
621
  */
611
622
  reportError(problem, consequence, extras, kind) {
612
623
  try {
624
+ const correlationId = seeContext.getStore()?.correlationId;
613
625
  const ev = buildSeeEvent(problem, consequence, extras, {
614
626
  side: "server",
615
627
  sdkVersion: version,
616
628
  env: this.env
617
- }, kind);
629
+ }, kind, correlationId);
618
630
  if (!this.seeLimiter.shouldSend(ev)) return;
619
631
  globalThis.fetch(`${this.baseUrl}/collect`, {
620
632
  method: "POST",
@@ -709,6 +721,8 @@ Object.defineProperty(globalThis, _EDIT_MODE_SSR_SYM, {
709
721
  },
710
722
  configurable: true
711
723
  });
724
+ var _SEE_CORR_ALS_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-correlation-als");
725
+ var seeContext = globalThis[_SEE_CORR_ALS_SYM] ?? (globalThis[_SEE_CORR_ALS_SYM] = new AsyncLocalStorage());
712
726
  var i18n = {
713
727
  /**
714
728
  * Fetch translation labels for the current request and store them in an
@@ -830,7 +844,23 @@ async function shipeasy(opts) {
830
844
  serverKey ? flags.initOnce() : Promise.resolve(),
831
845
  serverKey ? i18n.init(serverKey, profile) : Promise.resolve()
832
846
  ]);
833
- 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);
834
864
  const i18nData = i18n.getForRequest();
835
865
  return {
836
866
  flags: bootstrap.flags,
@@ -839,7 +869,8 @@ async function shipeasy(opts) {
839
869
  getBootstrapHtml() {
840
870
  return getBootstrapHtml(bootstrap, i18nData, {
841
871
  editLabels,
842
- i18nProfile: profile
872
+ i18nProfile: profile,
873
+ anonId
843
874
  });
844
875
  }
845
876
  };
@@ -859,10 +890,16 @@ function getBootstrapHtml(bootstrap, i18nData, opts) {
859
890
  };
860
891
  if (i18nData) payload.i18n = i18nData;
861
892
  if (opts.editLabels) payload.editLabels = true;
893
+ if (opts.anonId) payload.anonId = opts.anonId;
862
894
  parts.push(
863
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;}});})();`
864
896
  );
865
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
+ }
866
903
  if (i18nData?.strings && Object.keys(i18nData.strings).length > 0) {
867
904
  parts.push(
868
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(){};}};})();`
@@ -947,6 +984,7 @@ var see = Object.assign(
947
984
  }
948
985
  );
949
986
  export {
987
+ ANON_ID_COOKIE,
950
988
  FlagsClient,
951
989
  _resetShipeasyServerForTests,
952
990
  configureShipeasyServer,
@@ -955,7 +993,9 @@ export {
955
993
  getBootstrapHtml,
956
994
  getShipeasyServerClient,
957
995
  i18n,
996
+ isExpected,
958
997
  see,
998
+ seeContext,
959
999
  shipeasy,
960
1000
  version
961
1001
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "4.2.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",