@shipeasy/sdk 4.1.0 → 4.3.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.
@@ -18,6 +18,20 @@ interface Violation {
18
18
  /** Attach free-form detail. Variable data goes HERE (or in extras), never in the name. */
19
19
  message(msg: string): Violation;
20
20
  }
21
+ /**
22
+ * Identity of a problem that see() already reported, carried on the wire as
23
+ * the `caused_by` of a later occurrence. Holds exactly the fields the worker's
24
+ * fingerprint function consumes (raw `message`/`stack` — the server normalizes
25
+ * them) so the backend can recompute the prior issue's fingerprint and link
26
+ * the two issues. See `findCausedBy` for how the link is discovered.
27
+ */
28
+ interface SeeCausedBy {
29
+ error_type: string;
30
+ message: string;
31
+ stack?: string;
32
+ subject: string;
33
+ outcome: string;
34
+ }
21
35
  /** Wire shape — the `type:"error"` RawEvent variant accepted by POST /collect. */
22
36
  interface SeeErrorEvent {
23
37
  type: "error";
@@ -37,6 +51,24 @@ interface SeeErrorEvent {
37
51
  env?: string;
38
52
  sdk_version: string;
39
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;
64
+ /**
65
+ * The earlier reported problem this occurrence descends from — present when
66
+ * the same error was caught + reported at an inner boundary and then
67
+ * re-thrown (or wrapped via `{ cause }`) and reported again at an outer one.
68
+ * Lets the backend stitch the two issues into a cause chain instead of
69
+ * double-counting them as unrelated.
70
+ */
71
+ caused_by?: SeeCausedBy;
40
72
  }
41
73
  interface SeeExtrasTail {
42
74
  /** Attach debugging metadata. Callable repeatedly — keys merge, later wins. */
@@ -98,6 +130,15 @@ interface AutoCollectGroups {
98
130
  errors: boolean;
99
131
  engagement: boolean;
100
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>;
101
142
  type FlagsClientBrowserEnv = "dev" | "staging" | "prod";
102
143
  interface FlagsClientBrowserOptions {
103
144
  sdkKey: string;
@@ -154,7 +195,7 @@ declare class FlagsClientBrowser {
154
195
  * (beacon-first) — error occurrences are near-real-time, never queued behind
155
196
  * the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
156
197
  */
157
- reportError(problem: unknown, consequence: Consequence, extras?: SeeExtras, kind?: SeeKind): void;
198
+ reportError(problem: unknown, consequence: Consequence, extras?: SeeExtras, kind?: SeeKind, correlationId?: string): void;
158
199
  get ready(): boolean;
159
200
  private notify;
160
201
  initFromBootstrap(data: EvalResponse): void;
@@ -456,4 +497,4 @@ interface I18nFacade {
456
497
  }
457
498
  declare const i18n: I18nFacade;
458
499
 
459
- 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 };
@@ -18,6 +18,20 @@ interface Violation {
18
18
  /** Attach free-form detail. Variable data goes HERE (or in extras), never in the name. */
19
19
  message(msg: string): Violation;
20
20
  }
21
+ /**
22
+ * Identity of a problem that see() already reported, carried on the wire as
23
+ * the `caused_by` of a later occurrence. Holds exactly the fields the worker's
24
+ * fingerprint function consumes (raw `message`/`stack` — the server normalizes
25
+ * them) so the backend can recompute the prior issue's fingerprint and link
26
+ * the two issues. See `findCausedBy` for how the link is discovered.
27
+ */
28
+ interface SeeCausedBy {
29
+ error_type: string;
30
+ message: string;
31
+ stack?: string;
32
+ subject: string;
33
+ outcome: string;
34
+ }
21
35
  /** Wire shape — the `type:"error"` RawEvent variant accepted by POST /collect. */
22
36
  interface SeeErrorEvent {
23
37
  type: "error";
@@ -37,6 +51,24 @@ interface SeeErrorEvent {
37
51
  env?: string;
38
52
  sdk_version: string;
39
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;
64
+ /**
65
+ * The earlier reported problem this occurrence descends from — present when
66
+ * the same error was caught + reported at an inner boundary and then
67
+ * re-thrown (or wrapped via `{ cause }`) and reported again at an outer one.
68
+ * Lets the backend stitch the two issues into a cause chain instead of
69
+ * double-counting them as unrelated.
70
+ */
71
+ caused_by?: SeeCausedBy;
40
72
  }
41
73
  interface SeeExtrasTail {
42
74
  /** Attach debugging metadata. Callable repeatedly — keys merge, later wins. */
@@ -98,6 +130,15 @@ interface AutoCollectGroups {
98
130
  errors: boolean;
99
131
  engagement: boolean;
100
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>;
101
142
  type FlagsClientBrowserEnv = "dev" | "staging" | "prod";
102
143
  interface FlagsClientBrowserOptions {
103
144
  sdkKey: string;
@@ -154,7 +195,7 @@ declare class FlagsClientBrowser {
154
195
  * (beacon-first) — error occurrences are near-real-time, never queued behind
155
196
  * the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
156
197
  */
157
- reportError(problem: unknown, consequence: Consequence, extras?: SeeExtras, kind?: SeeKind): void;
198
+ reportError(problem: unknown, consequence: Consequence, extras?: SeeExtras, kind?: SeeKind, correlationId?: string): void;
158
199
  get ready(): boolean;
159
200
  private notify;
160
201
  initFromBootstrap(data: EvalResponse): void;
@@ -456,4 +497,4 @@ interface I18nFacade {
456
497
  }
457
498
  declare const i18n: I18nFacade;
458
499
 
459
- 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) {
@@ -152,6 +155,44 @@ function isExpected(err) {
152
155
  if (typeof err !== "object" || err === null) return false;
153
156
  return err[EXPECTED_SYM] !== void 0;
154
157
  }
158
+ var REPORTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-reported");
159
+ var SEE_MAX_CAUSE_DEPTH = 8;
160
+ function readReportStamp(err) {
161
+ if (typeof err !== "object" || err === null) return void 0;
162
+ const v = err[REPORTED_SYM];
163
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
164
+ }
165
+ function findCausedBy(problem) {
166
+ let cur = problem;
167
+ const seen = /* @__PURE__ */ new Set();
168
+ for (let depth = 0; depth < SEE_MAX_CAUSE_DEPTH; depth++) {
169
+ if (typeof cur !== "object" || cur === null || seen.has(cur)) break;
170
+ seen.add(cur);
171
+ const stamp = readReportStamp(cur);
172
+ if (stamp) return stamp;
173
+ cur = cur.cause;
174
+ }
175
+ return void 0;
176
+ }
177
+ function markReported(problem, ev) {
178
+ if (!(problem instanceof Error)) return;
179
+ const stamp = {
180
+ error_type: ev.error_type,
181
+ message: ev.message,
182
+ subject: ev.subject,
183
+ outcome: ev.outcome
184
+ };
185
+ if (ev.stack !== void 0) stamp.stack = ev.stack;
186
+ try {
187
+ Object.defineProperty(problem, REPORTED_SYM, {
188
+ value: Object.freeze(stamp),
189
+ enumerable: false,
190
+ configurable: true,
191
+ writable: true
192
+ });
193
+ } catch {
194
+ }
195
+ }
155
196
  function truncate(s, max) {
156
197
  return s.length > max ? s.slice(0, max) : s;
157
198
  }
@@ -177,7 +218,7 @@ function captureCallsiteStack() {
177
218
  const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
178
219
  return kept.length ? kept.join("\n") : void 0;
179
220
  }
180
- function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
221
+ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlationId) {
181
222
  let errorType;
182
223
  let message;
183
224
  let stack;
@@ -210,12 +251,16 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
210
251
  ts: Date.now()
211
252
  };
212
253
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
254
+ if (correlationId) ev.correlation_id = truncate(String(correlationId), SEE_MAX_CORRELATION);
255
+ const causedBy = findCausedBy(problem);
256
+ if (causedBy) ev.caused_by = causedBy;
213
257
  const cleanExtras = sanitizeExtras(extras);
214
258
  if (cleanExtras) ev.extras = cleanExtras;
215
259
  if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
216
260
  if (ctx.userId) ev.user_id = ctx.userId;
217
261
  if (ctx.anonId) ev.anonymous_id = ctx.anonId;
218
262
  if (ctx.env) ev.env = ctx.env;
263
+ markReported(problem, ev);
219
264
  return ev;
220
265
  }
221
266
  function safeString(v) {
@@ -238,7 +283,9 @@ function startSeeChain(getProblem, dispatch) {
238
283
  flushed = true;
239
284
  dispatch(
240
285
  getProblem(),
241
- causesThe(subject ?? "the app").to(outcome ?? "hit an error"),
286
+ // Bare noun phrase titles render as " causes the {subject} …", so a
287
+ // leading article would double up ("causes the the app").
288
+ causesThe(subject ?? "app").to(outcome ?? "hit an error"),
242
289
  collected
243
290
  );
244
291
  });
@@ -449,6 +496,43 @@ var EventBuffer = class {
449
496
  });
450
497
  }
451
498
  };
499
+ function endpointTemplate(rawUrl) {
500
+ const isIdSegment = (seg) => /^\d+$/.test(seg) || /^0x[0-9a-f]+$/i.test(seg) || /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(seg) || /^[0-9a-f]{8,}$/i.test(seg) || seg.length >= 12 && /\d/.test(seg) && /[a-z]/i.test(seg);
501
+ let u;
502
+ try {
503
+ u = new URL(rawUrl, typeof location !== "undefined" ? location.href : void 0);
504
+ } catch {
505
+ return (rawUrl.split(/[?#]/)[0] ?? "").slice(0, 120);
506
+ }
507
+ const path = u.pathname.split("/").map((seg) => seg && isIdSegment(seg) ? ":id" : seg).join("/");
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
+ }
535
+ }
452
536
  function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignoreUrlPrefixes, always = false) {
453
537
  if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
454
538
  const shouldEmit = () => always || buffer.hasExposures();
@@ -497,7 +581,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
497
581
  const problem = err ?? (typeof msg === "string" && msg ? msg : "Unknown error");
498
582
  reportSee(
499
583
  problem,
500
- causesThe("the page").to("hit an unhandled error"),
584
+ causesThe("page").to("hit an unhandled error"),
501
585
  {
502
586
  source: typeof source === "string" ? source : void 0,
503
587
  line: lineno ?? void 0
@@ -513,7 +597,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
513
597
  if (isExpected(reason)) return;
514
598
  reportSee(
515
599
  reason ?? "Unhandled promise rejection",
516
- causesThe("the page").to("hit an unhandled promise rejection"),
600
+ causesThe("page").to("hit an unhandled promise rejection"),
517
601
  void 0,
518
602
  "unhandled_rejection"
519
603
  );
@@ -524,6 +608,11 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
524
608
  const url = typeof args[0] === "string" ? args[0] : args[0].toString();
525
609
  const ignored = ignoreUrlPrefixes.some((p) => p && url.startsWith(p));
526
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
+ }
527
616
  let res;
528
617
  try {
529
618
  res = await origFetch.apply(this, args);
@@ -531,7 +620,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
531
620
  if (!ignored && !isExpected(err)) {
532
621
  reportSee(
533
622
  violation("NetworkError").message(`request to ${bareUrl} failed`),
534
- causesThe("a network request").to("fail without a response"),
623
+ causesThe(`request to ${endpointTemplate(url)}`).to("get no response"),
535
624
  { status: 0, url: url.slice(0, 200) },
536
625
  "network"
537
626
  );
@@ -542,9 +631,10 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
542
631
  const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
543
632
  reportSee(
544
633
  violation("Http5xx").message(`request to ${bareUrl} returned ${res.status}`),
545
- causesThe("a network request").to(`fail with HTTP ${res.status}`),
634
+ causesThe(`request to ${endpointTemplate(url)}`).to("fail with a server error"),
546
635
  { status: res.status, url: url.slice(0, 200), duration_ms: Math.round(elapsed) },
547
- "network"
636
+ "network",
637
+ corr
548
638
  );
549
639
  }
550
640
  return res;
@@ -687,6 +777,83 @@ function collectBrowserAttrs() {
687
777
  }
688
778
  return attrs;
689
779
  }
780
+ function collectSeeEnv() {
781
+ const out = {};
782
+ if (typeof navigator === "undefined") return out;
783
+ const nav = navigator;
784
+ const ua = typeof nav.userAgent === "string" ? nav.userAgent : "";
785
+ const browser = parseUaBrowser(ua);
786
+ if (browser) out["env.browser"] = browser;
787
+ const os = parseUaOs(ua) ?? nav.userAgentData?.platform;
788
+ if (os) out["env.os"] = os;
789
+ out["env.device"] = typeof nav.userAgentData?.mobile === "boolean" ? nav.userAgentData.mobile ? "mobile" : "desktop" : /iPad|Tablet/.test(ua) ? "tablet" : /Mobi|iPhone|Android.*Mobile/.test(ua) ? "mobile" : "desktop";
790
+ try {
791
+ if (nav.language) out["env.lang"] = nav.language;
792
+ } catch {
793
+ }
794
+ try {
795
+ if (typeof nav.onLine === "boolean") out["env.online"] = nav.onLine;
796
+ } catch {
797
+ }
798
+ try {
799
+ if (typeof nav.hardwareConcurrency === "number") out["env.cores"] = nav.hardwareConcurrency;
800
+ } catch {
801
+ }
802
+ try {
803
+ if (typeof nav.deviceMemory === "number") out["env.memory_gb"] = nav.deviceMemory;
804
+ } catch {
805
+ }
806
+ try {
807
+ const et = nav.connection?.effectiveType;
808
+ if (et) out["env.connection"] = et;
809
+ } catch {
810
+ }
811
+ try {
812
+ if (typeof window !== "undefined" && window.innerWidth && window.innerHeight) {
813
+ out["env.viewport"] = `${window.innerWidth}\xD7${window.innerHeight}`;
814
+ }
815
+ if (typeof window !== "undefined" && typeof window.devicePixelRatio === "number") {
816
+ out["env.dpr"] = window.devicePixelRatio;
817
+ }
818
+ if (typeof screen !== "undefined" && screen.width && screen.height) {
819
+ out["env.screen"] = `${screen.width}\xD7${screen.height}`;
820
+ }
821
+ } catch {
822
+ }
823
+ try {
824
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
825
+ if (tz) out["env.tz"] = tz;
826
+ } catch {
827
+ }
828
+ return out;
829
+ }
830
+ function parseUaBrowser(ua) {
831
+ const tests = [
832
+ [/Edg(?:A|iOS)?\/(\d+)/, "Edge"],
833
+ [/(?:OPR|Opera)\/(\d+)/, "Opera"],
834
+ [/(?:Firefox|FxiOS)\/(\d+)/, "Firefox"],
835
+ [/(?:Chrome|CriOS)\/(\d+)/, "Chrome"],
836
+ [/Version\/(\d+)[.\d]* (?:Mobile.*)?Safari/, "Safari"]
837
+ ];
838
+ for (const [re, name] of tests) {
839
+ const m = re.exec(ua);
840
+ if (m) return `${name} ${m[1]}`;
841
+ }
842
+ return void 0;
843
+ }
844
+ function parseUaOs(ua) {
845
+ if (/Windows NT 10/.test(ua)) return "Windows 10/11";
846
+ if (/Windows NT/.test(ua)) return "Windows";
847
+ let m = /Mac OS X (\d+)[._](\d+)/.exec(ua);
848
+ if (m) return `macOS ${m[1]}.${m[2]}`;
849
+ if (/Macintosh/.test(ua)) return "macOS";
850
+ m = /Android (\d+)/.exec(ua);
851
+ if (m) return `Android ${m[1]}`;
852
+ m = /(?:iPhone|iPad)[^)]* OS (\d+)/.exec(ua);
853
+ if (m) return `iOS ${m[1]}`;
854
+ if (/Linux/.test(ua)) return "Linux";
855
+ return void 0;
856
+ }
690
857
  function readExperimentOverridesFromUrl() {
691
858
  if (typeof window === "undefined") return {};
692
859
  const out = {};
@@ -779,7 +946,7 @@ var FlagsClientBrowser = class {
779
946
  this.userId,
780
947
  this.anonId,
781
948
  this.autoGuardrailGroups,
782
- (problem, consequence, extras, kind) => this.reportError(problem, consequence, extras, kind),
949
+ (problem, consequence, extras, kind, correlationId) => this.reportError(problem, consequence, extras, kind, correlationId),
783
950
  [`${this.baseUrl}/`, DEFAULT_TELEMETRY_URL],
784
951
  this.autoCollectAlways
785
952
  );
@@ -791,16 +958,17 @@ var FlagsClientBrowser = class {
791
958
  * (beacon-first) — error occurrences are near-real-time, never queued behind
792
959
  * the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
793
960
  */
794
- reportError(problem, consequence, extras, kind) {
961
+ reportError(problem, consequence, extras, kind, correlationId) {
795
962
  try {
796
- const ev = buildSeeEvent(problem, consequence, extras, {
963
+ const enriched = { ...collectSeeEnv(), ...extras };
964
+ const ev = buildSeeEvent(problem, consequence, enriched, {
797
965
  side: "client",
798
966
  sdkVersion: version,
799
967
  env: this.env,
800
968
  url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
801
969
  userId: this.userId || void 0,
802
970
  anonId: this.anonId
803
- }, kind);
971
+ }, kind, correlationId);
804
972
  if (!this.seeLimiter.shouldSend(ev)) return;
805
973
  this.buffer.sendNow([ev]);
806
974
  } catch {
@@ -1467,12 +1635,14 @@ var i18n = {
1467
1635
  flags,
1468
1636
  getShipeasyClient,
1469
1637
  i18n,
1638
+ injectCorrelationHeader,
1470
1639
  isDevtoolsRequested,
1471
1640
  labelAttrs,
1472
1641
  loadDevtools,
1473
1642
  readConfigOverride,
1474
1643
  readExpOverride,
1475
1644
  readGateOverride,
1645
+ sameOrigin,
1476
1646
  see,
1477
1647
  shipeasy,
1478
1648
  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) {
@@ -106,6 +107,44 @@ function isExpected(err) {
106
107
  if (typeof err !== "object" || err === null) return false;
107
108
  return err[EXPECTED_SYM] !== void 0;
108
109
  }
110
+ var REPORTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-reported");
111
+ var SEE_MAX_CAUSE_DEPTH = 8;
112
+ function readReportStamp(err) {
113
+ if (typeof err !== "object" || err === null) return void 0;
114
+ const v = err[REPORTED_SYM];
115
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
116
+ }
117
+ function findCausedBy(problem) {
118
+ let cur = problem;
119
+ const seen = /* @__PURE__ */ new Set();
120
+ for (let depth = 0; depth < SEE_MAX_CAUSE_DEPTH; depth++) {
121
+ if (typeof cur !== "object" || cur === null || seen.has(cur)) break;
122
+ seen.add(cur);
123
+ const stamp = readReportStamp(cur);
124
+ if (stamp) return stamp;
125
+ cur = cur.cause;
126
+ }
127
+ return void 0;
128
+ }
129
+ function markReported(problem, ev) {
130
+ if (!(problem instanceof Error)) return;
131
+ const stamp = {
132
+ error_type: ev.error_type,
133
+ message: ev.message,
134
+ subject: ev.subject,
135
+ outcome: ev.outcome
136
+ };
137
+ if (ev.stack !== void 0) stamp.stack = ev.stack;
138
+ try {
139
+ Object.defineProperty(problem, REPORTED_SYM, {
140
+ value: Object.freeze(stamp),
141
+ enumerable: false,
142
+ configurable: true,
143
+ writable: true
144
+ });
145
+ } catch {
146
+ }
147
+ }
109
148
  function truncate(s, max) {
110
149
  return s.length > max ? s.slice(0, max) : s;
111
150
  }
@@ -131,7 +170,7 @@ function captureCallsiteStack() {
131
170
  const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
132
171
  return kept.length ? kept.join("\n") : void 0;
133
172
  }
134
- function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
173
+ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlationId) {
135
174
  let errorType;
136
175
  let message;
137
176
  let stack;
@@ -164,12 +203,16 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
164
203
  ts: Date.now()
165
204
  };
166
205
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
206
+ if (correlationId) ev.correlation_id = truncate(String(correlationId), SEE_MAX_CORRELATION);
207
+ const causedBy = findCausedBy(problem);
208
+ if (causedBy) ev.caused_by = causedBy;
167
209
  const cleanExtras = sanitizeExtras(extras);
168
210
  if (cleanExtras) ev.extras = cleanExtras;
169
211
  if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
170
212
  if (ctx.userId) ev.user_id = ctx.userId;
171
213
  if (ctx.anonId) ev.anonymous_id = ctx.anonId;
172
214
  if (ctx.env) ev.env = ctx.env;
215
+ markReported(problem, ev);
173
216
  return ev;
174
217
  }
175
218
  function safeString(v) {
@@ -192,7 +235,9 @@ function startSeeChain(getProblem, dispatch) {
192
235
  flushed = true;
193
236
  dispatch(
194
237
  getProblem(),
195
- causesThe(subject ?? "the app").to(outcome ?? "hit an error"),
238
+ // Bare noun phrase titles render as " causes the {subject} …", so a
239
+ // leading article would double up ("causes the the app").
240
+ causesThe(subject ?? "app").to(outcome ?? "hit an error"),
196
241
  collected
197
242
  );
198
243
  });
@@ -403,6 +448,43 @@ var EventBuffer = class {
403
448
  });
404
449
  }
405
450
  };
451
+ function endpointTemplate(rawUrl) {
452
+ const isIdSegment = (seg) => /^\d+$/.test(seg) || /^0x[0-9a-f]+$/i.test(seg) || /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(seg) || /^[0-9a-f]{8,}$/i.test(seg) || seg.length >= 12 && /\d/.test(seg) && /[a-z]/i.test(seg);
453
+ let u;
454
+ try {
455
+ u = new URL(rawUrl, typeof location !== "undefined" ? location.href : void 0);
456
+ } catch {
457
+ return (rawUrl.split(/[?#]/)[0] ?? "").slice(0, 120);
458
+ }
459
+ const path = u.pathname.split("/").map((seg) => seg && isIdSegment(seg) ? ":id" : seg).join("/");
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
+ }
487
+ }
406
488
  function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignoreUrlPrefixes, always = false) {
407
489
  if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
408
490
  const shouldEmit = () => always || buffer.hasExposures();
@@ -451,7 +533,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
451
533
  const problem = err ?? (typeof msg === "string" && msg ? msg : "Unknown error");
452
534
  reportSee(
453
535
  problem,
454
- causesThe("the page").to("hit an unhandled error"),
536
+ causesThe("page").to("hit an unhandled error"),
455
537
  {
456
538
  source: typeof source === "string" ? source : void 0,
457
539
  line: lineno ?? void 0
@@ -467,7 +549,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
467
549
  if (isExpected(reason)) return;
468
550
  reportSee(
469
551
  reason ?? "Unhandled promise rejection",
470
- causesThe("the page").to("hit an unhandled promise rejection"),
552
+ causesThe("page").to("hit an unhandled promise rejection"),
471
553
  void 0,
472
554
  "unhandled_rejection"
473
555
  );
@@ -478,6 +560,11 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
478
560
  const url = typeof args[0] === "string" ? args[0] : args[0].toString();
479
561
  const ignored = ignoreUrlPrefixes.some((p) => p && url.startsWith(p));
480
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
+ }
481
568
  let res;
482
569
  try {
483
570
  res = await origFetch.apply(this, args);
@@ -485,7 +572,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
485
572
  if (!ignored && !isExpected(err)) {
486
573
  reportSee(
487
574
  violation("NetworkError").message(`request to ${bareUrl} failed`),
488
- causesThe("a network request").to("fail without a response"),
575
+ causesThe(`request to ${endpointTemplate(url)}`).to("get no response"),
489
576
  { status: 0, url: url.slice(0, 200) },
490
577
  "network"
491
578
  );
@@ -496,9 +583,10 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
496
583
  const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
497
584
  reportSee(
498
585
  violation("Http5xx").message(`request to ${bareUrl} returned ${res.status}`),
499
- causesThe("a network request").to(`fail with HTTP ${res.status}`),
586
+ causesThe(`request to ${endpointTemplate(url)}`).to("fail with a server error"),
500
587
  { status: res.status, url: url.slice(0, 200), duration_ms: Math.round(elapsed) },
501
- "network"
588
+ "network",
589
+ corr
502
590
  );
503
591
  }
504
592
  return res;
@@ -641,6 +729,83 @@ function collectBrowserAttrs() {
641
729
  }
642
730
  return attrs;
643
731
  }
732
+ function collectSeeEnv() {
733
+ const out = {};
734
+ if (typeof navigator === "undefined") return out;
735
+ const nav = navigator;
736
+ const ua = typeof nav.userAgent === "string" ? nav.userAgent : "";
737
+ const browser = parseUaBrowser(ua);
738
+ if (browser) out["env.browser"] = browser;
739
+ const os = parseUaOs(ua) ?? nav.userAgentData?.platform;
740
+ if (os) out["env.os"] = os;
741
+ out["env.device"] = typeof nav.userAgentData?.mobile === "boolean" ? nav.userAgentData.mobile ? "mobile" : "desktop" : /iPad|Tablet/.test(ua) ? "tablet" : /Mobi|iPhone|Android.*Mobile/.test(ua) ? "mobile" : "desktop";
742
+ try {
743
+ if (nav.language) out["env.lang"] = nav.language;
744
+ } catch {
745
+ }
746
+ try {
747
+ if (typeof nav.onLine === "boolean") out["env.online"] = nav.onLine;
748
+ } catch {
749
+ }
750
+ try {
751
+ if (typeof nav.hardwareConcurrency === "number") out["env.cores"] = nav.hardwareConcurrency;
752
+ } catch {
753
+ }
754
+ try {
755
+ if (typeof nav.deviceMemory === "number") out["env.memory_gb"] = nav.deviceMemory;
756
+ } catch {
757
+ }
758
+ try {
759
+ const et = nav.connection?.effectiveType;
760
+ if (et) out["env.connection"] = et;
761
+ } catch {
762
+ }
763
+ try {
764
+ if (typeof window !== "undefined" && window.innerWidth && window.innerHeight) {
765
+ out["env.viewport"] = `${window.innerWidth}\xD7${window.innerHeight}`;
766
+ }
767
+ if (typeof window !== "undefined" && typeof window.devicePixelRatio === "number") {
768
+ out["env.dpr"] = window.devicePixelRatio;
769
+ }
770
+ if (typeof screen !== "undefined" && screen.width && screen.height) {
771
+ out["env.screen"] = `${screen.width}\xD7${screen.height}`;
772
+ }
773
+ } catch {
774
+ }
775
+ try {
776
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
777
+ if (tz) out["env.tz"] = tz;
778
+ } catch {
779
+ }
780
+ return out;
781
+ }
782
+ function parseUaBrowser(ua) {
783
+ const tests = [
784
+ [/Edg(?:A|iOS)?\/(\d+)/, "Edge"],
785
+ [/(?:OPR|Opera)\/(\d+)/, "Opera"],
786
+ [/(?:Firefox|FxiOS)\/(\d+)/, "Firefox"],
787
+ [/(?:Chrome|CriOS)\/(\d+)/, "Chrome"],
788
+ [/Version\/(\d+)[.\d]* (?:Mobile.*)?Safari/, "Safari"]
789
+ ];
790
+ for (const [re, name] of tests) {
791
+ const m = re.exec(ua);
792
+ if (m) return `${name} ${m[1]}`;
793
+ }
794
+ return void 0;
795
+ }
796
+ function parseUaOs(ua) {
797
+ if (/Windows NT 10/.test(ua)) return "Windows 10/11";
798
+ if (/Windows NT/.test(ua)) return "Windows";
799
+ let m = /Mac OS X (\d+)[._](\d+)/.exec(ua);
800
+ if (m) return `macOS ${m[1]}.${m[2]}`;
801
+ if (/Macintosh/.test(ua)) return "macOS";
802
+ m = /Android (\d+)/.exec(ua);
803
+ if (m) return `Android ${m[1]}`;
804
+ m = /(?:iPhone|iPad)[^)]* OS (\d+)/.exec(ua);
805
+ if (m) return `iOS ${m[1]}`;
806
+ if (/Linux/.test(ua)) return "Linux";
807
+ return void 0;
808
+ }
644
809
  function readExperimentOverridesFromUrl() {
645
810
  if (typeof window === "undefined") return {};
646
811
  const out = {};
@@ -733,7 +898,7 @@ var FlagsClientBrowser = class {
733
898
  this.userId,
734
899
  this.anonId,
735
900
  this.autoGuardrailGroups,
736
- (problem, consequence, extras, kind) => this.reportError(problem, consequence, extras, kind),
901
+ (problem, consequence, extras, kind, correlationId) => this.reportError(problem, consequence, extras, kind, correlationId),
737
902
  [`${this.baseUrl}/`, DEFAULT_TELEMETRY_URL],
738
903
  this.autoCollectAlways
739
904
  );
@@ -745,16 +910,17 @@ var FlagsClientBrowser = class {
745
910
  * (beacon-first) — error occurrences are near-real-time, never queued behind
746
911
  * the 5s metric batch. Spam-guarded by a 30s dedup window + per-session cap.
747
912
  */
748
- reportError(problem, consequence, extras, kind) {
913
+ reportError(problem, consequence, extras, kind, correlationId) {
749
914
  try {
750
- const ev = buildSeeEvent(problem, consequence, extras, {
915
+ const enriched = { ...collectSeeEnv(), ...extras };
916
+ const ev = buildSeeEvent(problem, consequence, enriched, {
751
917
  side: "client",
752
918
  sdkVersion: version,
753
919
  env: this.env,
754
920
  url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
755
921
  userId: this.userId || void 0,
756
922
  anonId: this.anonId
757
- }, kind);
923
+ }, kind, correlationId);
758
924
  if (!this.seeLimiter.shouldSend(ev)) return;
759
925
  this.buffer.sendNow([ev]);
760
926
  } catch {
@@ -1420,12 +1586,14 @@ export {
1420
1586
  flags,
1421
1587
  getShipeasyClient,
1422
1588
  i18n,
1589
+ injectCorrelationHeader,
1423
1590
  isDevtoolsRequested,
1424
1591
  labelAttrs,
1425
1592
  loadDevtools,
1426
1593
  readConfigOverride,
1427
1594
  readExpOverride,
1428
1595
  readGateOverride,
1596
+ sameOrigin,
1429
1597
  see,
1430
1598
  shipeasy,
1431
1599
  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. */
@@ -18,6 +20,20 @@ interface Violation {
18
20
  /** Attach free-form detail. Variable data goes HERE (or in extras), never in the name. */
19
21
  message(msg: string): Violation;
20
22
  }
23
+ /**
24
+ * Identity of a problem that see() already reported, carried on the wire as
25
+ * the `caused_by` of a later occurrence. Holds exactly the fields the worker's
26
+ * fingerprint function consumes (raw `message`/`stack` — the server normalizes
27
+ * them) so the backend can recompute the prior issue's fingerprint and link
28
+ * the two issues. See `findCausedBy` for how the link is discovered.
29
+ */
30
+ interface SeeCausedBy {
31
+ error_type: string;
32
+ message: string;
33
+ stack?: string;
34
+ subject: string;
35
+ outcome: string;
36
+ }
21
37
  /** Wire shape — the `type:"error"` RawEvent variant accepted by POST /collect. */
22
38
  interface SeeErrorEvent {
23
39
  type: "error";
@@ -37,7 +53,26 @@ interface SeeErrorEvent {
37
53
  env?: string;
38
54
  sdk_version: string;
39
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;
66
+ /**
67
+ * The earlier reported problem this occurrence descends from — present when
68
+ * the same error was caught + reported at an inner boundary and then
69
+ * re-thrown (or wrapped via `{ cause }`) and reported again at an outer one.
70
+ * Lets the backend stitch the two issues into a cause chain instead of
71
+ * double-counting them as unrelated.
72
+ */
73
+ caused_by?: SeeCausedBy;
40
74
  }
75
+ declare function isExpected(err: unknown): boolean;
41
76
  interface SeeExtrasTail {
42
77
  /** Attach debugging metadata. Callable repeatedly — keys merge, later wins. */
43
78
  extras(extras: SeeExtras): SeeExtrasTail;
@@ -165,6 +200,11 @@ declare class FlagsClient {
165
200
  evaluate(user: User, rawUrl?: string): BootstrapPayload;
166
201
  getKillswitch(name: string, switchKey?: string): boolean;
167
202
  }
203
+ interface SeeCorrelationStore {
204
+ correlationId?: string;
205
+ }
206
+ declare const seeContext: AsyncLocalStorage<SeeCorrelationStore>;
207
+
168
208
  interface I18nForRequest {
169
209
  strings: Record<string, string>;
170
210
  locale: string;
@@ -340,4 +380,4 @@ interface SeeApi {
340
380
  */
341
381
  declare const see: SeeApi;
342
382
 
343
- 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 };
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 };
@@ -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. */
@@ -18,6 +20,20 @@ interface Violation {
18
20
  /** Attach free-form detail. Variable data goes HERE (or in extras), never in the name. */
19
21
  message(msg: string): Violation;
20
22
  }
23
+ /**
24
+ * Identity of a problem that see() already reported, carried on the wire as
25
+ * the `caused_by` of a later occurrence. Holds exactly the fields the worker's
26
+ * fingerprint function consumes (raw `message`/`stack` — the server normalizes
27
+ * them) so the backend can recompute the prior issue's fingerprint and link
28
+ * the two issues. See `findCausedBy` for how the link is discovered.
29
+ */
30
+ interface SeeCausedBy {
31
+ error_type: string;
32
+ message: string;
33
+ stack?: string;
34
+ subject: string;
35
+ outcome: string;
36
+ }
21
37
  /** Wire shape — the `type:"error"` RawEvent variant accepted by POST /collect. */
22
38
  interface SeeErrorEvent {
23
39
  type: "error";
@@ -37,7 +53,26 @@ interface SeeErrorEvent {
37
53
  env?: string;
38
54
  sdk_version: string;
39
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;
66
+ /**
67
+ * The earlier reported problem this occurrence descends from — present when
68
+ * the same error was caught + reported at an inner boundary and then
69
+ * re-thrown (or wrapped via `{ cause }`) and reported again at an outer one.
70
+ * Lets the backend stitch the two issues into a cause chain instead of
71
+ * double-counting them as unrelated.
72
+ */
73
+ caused_by?: SeeCausedBy;
40
74
  }
75
+ declare function isExpected(err: unknown): boolean;
41
76
  interface SeeExtrasTail {
42
77
  /** Attach debugging metadata. Callable repeatedly — keys merge, later wins. */
43
78
  extras(extras: SeeExtras): SeeExtrasTail;
@@ -165,6 +200,11 @@ declare class FlagsClient {
165
200
  evaluate(user: User, rawUrl?: string): BootstrapPayload;
166
201
  getKillswitch(name: string, switchKey?: string): boolean;
167
202
  }
203
+ interface SeeCorrelationStore {
204
+ correlationId?: string;
205
+ }
206
+ declare const seeContext: AsyncLocalStorage<SeeCorrelationStore>;
207
+
168
208
  interface I18nForRequest {
169
209
  strings: Record<string, string>;
170
210
  locale: string;
@@ -340,4 +380,4 @@ interface SeeApi {
340
380
  */
341
381
  declare const see: SeeApi;
342
382
 
343
- 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 };
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 };
@@ -38,7 +38,9 @@ __export(server_exports, {
38
38
  getBootstrapHtml: () => getBootstrapHtml,
39
39
  getShipeasyServerClient: () => getShipeasyServerClient,
40
40
  i18n: () => i18n,
41
+ isExpected: () => isExpected,
41
42
  see: () => see,
43
+ seeContext: () => seeContext,
42
44
  shipeasy: () => shipeasy,
43
45
  version: () => version
44
46
  });
@@ -110,6 +112,7 @@ var SEE_MAX_STACK = 8e3;
110
112
  var SEE_MAX_SUBJECT = 200;
111
113
  var SEE_MAX_EXTRA_VALUE = 200;
112
114
  var SEE_MAX_EXTRA_KEYS = 20;
115
+ var SEE_MAX_CORRELATION = 64;
113
116
  var SEE_DEDUP_WINDOW_MS = 3e4;
114
117
  var SEE_MAX_PER_SESSION = 25;
115
118
  function causesThe(subject) {
@@ -149,6 +152,48 @@ function markExpected(err, because) {
149
152
  } catch {
150
153
  }
151
154
  }
155
+ function isExpected(err) {
156
+ if (typeof err !== "object" || err === null) return false;
157
+ return err[EXPECTED_SYM] !== void 0;
158
+ }
159
+ var REPORTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-reported");
160
+ var SEE_MAX_CAUSE_DEPTH = 8;
161
+ function readReportStamp(err) {
162
+ if (typeof err !== "object" || err === null) return void 0;
163
+ const v = err[REPORTED_SYM];
164
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
165
+ }
166
+ function findCausedBy(problem) {
167
+ let cur = problem;
168
+ const seen = /* @__PURE__ */ new Set();
169
+ for (let depth = 0; depth < SEE_MAX_CAUSE_DEPTH; depth++) {
170
+ if (typeof cur !== "object" || cur === null || seen.has(cur)) break;
171
+ seen.add(cur);
172
+ const stamp = readReportStamp(cur);
173
+ if (stamp) return stamp;
174
+ cur = cur.cause;
175
+ }
176
+ return void 0;
177
+ }
178
+ function markReported(problem, ev) {
179
+ if (!(problem instanceof Error)) return;
180
+ const stamp = {
181
+ error_type: ev.error_type,
182
+ message: ev.message,
183
+ subject: ev.subject,
184
+ outcome: ev.outcome
185
+ };
186
+ if (ev.stack !== void 0) stamp.stack = ev.stack;
187
+ try {
188
+ Object.defineProperty(problem, REPORTED_SYM, {
189
+ value: Object.freeze(stamp),
190
+ enumerable: false,
191
+ configurable: true,
192
+ writable: true
193
+ });
194
+ } catch {
195
+ }
196
+ }
152
197
  function truncate(s, max) {
153
198
  return s.length > max ? s.slice(0, max) : s;
154
199
  }
@@ -174,7 +219,7 @@ function captureCallsiteStack() {
174
219
  const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
175
220
  return kept.length ? kept.join("\n") : void 0;
176
221
  }
177
- function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
222
+ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlationId) {
178
223
  let errorType;
179
224
  let message;
180
225
  let stack;
@@ -207,12 +252,16 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
207
252
  ts: Date.now()
208
253
  };
209
254
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
255
+ if (correlationId) ev.correlation_id = truncate(String(correlationId), SEE_MAX_CORRELATION);
256
+ const causedBy = findCausedBy(problem);
257
+ if (causedBy) ev.caused_by = causedBy;
210
258
  const cleanExtras = sanitizeExtras(extras);
211
259
  if (cleanExtras) ev.extras = cleanExtras;
212
260
  if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
213
261
  if (ctx.userId) ev.user_id = ctx.userId;
214
262
  if (ctx.anonId) ev.anonymous_id = ctx.anonId;
215
263
  if (ctx.env) ev.env = ctx.env;
264
+ markReported(problem, ev);
216
265
  return ev;
217
266
  }
218
267
  function safeString(v) {
@@ -235,7 +284,9 @@ function startSeeChain(getProblem, dispatch) {
235
284
  flushed = true;
236
285
  dispatch(
237
286
  getProblem(),
238
- causesThe(subject ?? "the app").to(outcome ?? "hit an error"),
287
+ // Bare noun phrase titles render as " causes the {subject} …", so a
288
+ // leading article would double up ("causes the the app").
289
+ causesThe(subject ?? "app").to(outcome ?? "hit an error"),
239
290
  collected
240
291
  );
241
292
  });
@@ -611,11 +662,12 @@ var FlagsClient = class {
611
662
  */
612
663
  reportError(problem, consequence, extras, kind) {
613
664
  try {
665
+ const correlationId = seeContext.getStore()?.correlationId;
614
666
  const ev = buildSeeEvent(problem, consequence, extras, {
615
667
  side: "server",
616
668
  sdkVersion: version,
617
669
  env: this.env
618
- }, kind);
670
+ }, kind, correlationId);
619
671
  if (!this.seeLimiter.shouldSend(ev)) return;
620
672
  globalThis.fetch(`${this.baseUrl}/collect`, {
621
673
  method: "POST",
@@ -710,6 +762,8 @@ Object.defineProperty(globalThis, _EDIT_MODE_SSR_SYM, {
710
762
  },
711
763
  configurable: true
712
764
  });
765
+ var _SEE_CORR_ALS_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-correlation-als");
766
+ var seeContext = globalThis[_SEE_CORR_ALS_SYM] ?? (globalThis[_SEE_CORR_ALS_SYM] = new import_node_async_hooks.AsyncLocalStorage());
713
767
  var i18n = {
714
768
  /**
715
769
  * Fetch translation labels for the current request and store them in an
@@ -957,7 +1011,9 @@ var see = Object.assign(
957
1011
  getBootstrapHtml,
958
1012
  getShipeasyServerClient,
959
1013
  i18n,
1014
+ isExpected,
960
1015
  see,
1016
+ seeContext,
961
1017
  shipeasy,
962
1018
  version
963
1019
  });
@@ -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,48 @@ 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
+ }
113
+ var REPORTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-reported");
114
+ var SEE_MAX_CAUSE_DEPTH = 8;
115
+ function readReportStamp(err) {
116
+ if (typeof err !== "object" || err === null) return void 0;
117
+ const v = err[REPORTED_SYM];
118
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
119
+ }
120
+ function findCausedBy(problem) {
121
+ let cur = problem;
122
+ const seen = /* @__PURE__ */ new Set();
123
+ for (let depth = 0; depth < SEE_MAX_CAUSE_DEPTH; depth++) {
124
+ if (typeof cur !== "object" || cur === null || seen.has(cur)) break;
125
+ seen.add(cur);
126
+ const stamp = readReportStamp(cur);
127
+ if (stamp) return stamp;
128
+ cur = cur.cause;
129
+ }
130
+ return void 0;
131
+ }
132
+ function markReported(problem, ev) {
133
+ if (!(problem instanceof Error)) return;
134
+ const stamp = {
135
+ error_type: ev.error_type,
136
+ message: ev.message,
137
+ subject: ev.subject,
138
+ outcome: ev.outcome
139
+ };
140
+ if (ev.stack !== void 0) stamp.stack = ev.stack;
141
+ try {
142
+ Object.defineProperty(problem, REPORTED_SYM, {
143
+ value: Object.freeze(stamp),
144
+ enumerable: false,
145
+ configurable: true,
146
+ writable: true
147
+ });
148
+ } catch {
149
+ }
150
+ }
108
151
  function truncate(s, max) {
109
152
  return s.length > max ? s.slice(0, max) : s;
110
153
  }
@@ -130,7 +173,7 @@ function captureCallsiteStack() {
130
173
  const kept = lines.slice(1).filter((l) => !/@shipeasy[\\/]sdk|see[\\/]core|captureCallsiteStack|\bsee\b\s*\(/.test(l));
131
174
  return kept.length ? kept.join("\n") : void 0;
132
175
  }
133
- function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
176
+ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlationId) {
134
177
  let errorType;
135
178
  let message;
136
179
  let stack;
@@ -163,12 +206,16 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
163
206
  ts: Date.now()
164
207
  };
165
208
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
209
+ if (correlationId) ev.correlation_id = truncate(String(correlationId), SEE_MAX_CORRELATION);
210
+ const causedBy = findCausedBy(problem);
211
+ if (causedBy) ev.caused_by = causedBy;
166
212
  const cleanExtras = sanitizeExtras(extras);
167
213
  if (cleanExtras) ev.extras = cleanExtras;
168
214
  if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
169
215
  if (ctx.userId) ev.user_id = ctx.userId;
170
216
  if (ctx.anonId) ev.anonymous_id = ctx.anonId;
171
217
  if (ctx.env) ev.env = ctx.env;
218
+ markReported(problem, ev);
172
219
  return ev;
173
220
  }
174
221
  function safeString(v) {
@@ -191,7 +238,9 @@ function startSeeChain(getProblem, dispatch) {
191
238
  flushed = true;
192
239
  dispatch(
193
240
  getProblem(),
194
- causesThe(subject ?? "the app").to(outcome ?? "hit an error"),
241
+ // Bare noun phrase titles render as " causes the {subject} …", so a
242
+ // leading article would double up ("causes the the app").
243
+ causesThe(subject ?? "app").to(outcome ?? "hit an error"),
195
244
  collected
196
245
  );
197
246
  });
@@ -567,11 +616,12 @@ var FlagsClient = class {
567
616
  */
568
617
  reportError(problem, consequence, extras, kind) {
569
618
  try {
619
+ const correlationId = seeContext.getStore()?.correlationId;
570
620
  const ev = buildSeeEvent(problem, consequence, extras, {
571
621
  side: "server",
572
622
  sdkVersion: version,
573
623
  env: this.env
574
- }, kind);
624
+ }, kind, correlationId);
575
625
  if (!this.seeLimiter.shouldSend(ev)) return;
576
626
  globalThis.fetch(`${this.baseUrl}/collect`, {
577
627
  method: "POST",
@@ -666,6 +716,8 @@ Object.defineProperty(globalThis, _EDIT_MODE_SSR_SYM, {
666
716
  },
667
717
  configurable: true
668
718
  });
719
+ var _SEE_CORR_ALS_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-correlation-als");
720
+ var seeContext = globalThis[_SEE_CORR_ALS_SYM] ?? (globalThis[_SEE_CORR_ALS_SYM] = new AsyncLocalStorage());
669
721
  var i18n = {
670
722
  /**
671
723
  * Fetch translation labels for the current request and store them in an
@@ -912,7 +964,9 @@ export {
912
964
  getBootstrapHtml,
913
965
  getShipeasyServerClient,
914
966
  i18n,
967
+ isExpected,
915
968
  see,
969
+ seeContext,
916
970
  shipeasy,
917
971
  version
918
972
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "4.1.0",
3
+ "version": "4.3.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",