@shipeasy/sdk 4.1.0 → 4.2.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,14 @@ interface SeeErrorEvent {
37
51
  env?: string;
38
52
  sdk_version: string;
39
53
  ts: number;
54
+ /**
55
+ * The earlier reported problem this occurrence descends from — present when
56
+ * the same error was caught + reported at an inner boundary and then
57
+ * re-thrown (or wrapped via `{ cause }`) and reported again at an outer one.
58
+ * Lets the backend stitch the two issues into a cause chain instead of
59
+ * double-counting them as unrelated.
60
+ */
61
+ caused_by?: SeeCausedBy;
40
62
  }
41
63
  interface SeeExtrasTail {
42
64
  /** Attach debugging metadata. Callable repeatedly — keys merge, later wins. */
@@ -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,14 @@ interface SeeErrorEvent {
37
51
  env?: string;
38
52
  sdk_version: string;
39
53
  ts: number;
54
+ /**
55
+ * The earlier reported problem this occurrence descends from — present when
56
+ * the same error was caught + reported at an inner boundary and then
57
+ * re-thrown (or wrapped via `{ cause }`) and reported again at an outer one.
58
+ * Lets the backend stitch the two issues into a cause chain instead of
59
+ * double-counting them as unrelated.
60
+ */
61
+ caused_by?: SeeCausedBy;
40
62
  }
41
63
  interface SeeExtrasTail {
42
64
  /** Attach debugging metadata. Callable repeatedly — keys merge, later wins. */
@@ -152,6 +152,44 @@ function isExpected(err) {
152
152
  if (typeof err !== "object" || err === null) return false;
153
153
  return err[EXPECTED_SYM] !== void 0;
154
154
  }
155
+ var REPORTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-reported");
156
+ var SEE_MAX_CAUSE_DEPTH = 8;
157
+ function readReportStamp(err) {
158
+ if (typeof err !== "object" || err === null) return void 0;
159
+ const v = err[REPORTED_SYM];
160
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
161
+ }
162
+ function findCausedBy(problem) {
163
+ let cur = problem;
164
+ const seen = /* @__PURE__ */ new Set();
165
+ for (let depth = 0; depth < SEE_MAX_CAUSE_DEPTH; depth++) {
166
+ if (typeof cur !== "object" || cur === null || seen.has(cur)) break;
167
+ seen.add(cur);
168
+ const stamp = readReportStamp(cur);
169
+ if (stamp) return stamp;
170
+ cur = cur.cause;
171
+ }
172
+ return void 0;
173
+ }
174
+ function markReported(problem, ev) {
175
+ if (!(problem instanceof Error)) return;
176
+ const stamp = {
177
+ error_type: ev.error_type,
178
+ message: ev.message,
179
+ subject: ev.subject,
180
+ outcome: ev.outcome
181
+ };
182
+ if (ev.stack !== void 0) stamp.stack = ev.stack;
183
+ try {
184
+ Object.defineProperty(problem, REPORTED_SYM, {
185
+ value: Object.freeze(stamp),
186
+ enumerable: false,
187
+ configurable: true,
188
+ writable: true
189
+ });
190
+ } catch {
191
+ }
192
+ }
155
193
  function truncate(s, max) {
156
194
  return s.length > max ? s.slice(0, max) : s;
157
195
  }
@@ -210,12 +248,15 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
210
248
  ts: Date.now()
211
249
  };
212
250
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
251
+ const causedBy = findCausedBy(problem);
252
+ if (causedBy) ev.caused_by = causedBy;
213
253
  const cleanExtras = sanitizeExtras(extras);
214
254
  if (cleanExtras) ev.extras = cleanExtras;
215
255
  if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
216
256
  if (ctx.userId) ev.user_id = ctx.userId;
217
257
  if (ctx.anonId) ev.anonymous_id = ctx.anonId;
218
258
  if (ctx.env) ev.env = ctx.env;
259
+ markReported(problem, ev);
219
260
  return ev;
220
261
  }
221
262
  function safeString(v) {
@@ -238,7 +279,9 @@ function startSeeChain(getProblem, dispatch) {
238
279
  flushed = true;
239
280
  dispatch(
240
281
  getProblem(),
241
- causesThe(subject ?? "the app").to(outcome ?? "hit an error"),
282
+ // Bare noun phrase titles render as " causes the {subject} …", so a
283
+ // leading article would double up ("causes the the app").
284
+ causesThe(subject ?? "app").to(outcome ?? "hit an error"),
242
285
  collected
243
286
  );
244
287
  });
@@ -449,6 +492,18 @@ var EventBuffer = class {
449
492
  });
450
493
  }
451
494
  };
495
+ function endpointTemplate(rawUrl) {
496
+ 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);
497
+ let u;
498
+ try {
499
+ u = new URL(rawUrl, typeof location !== "undefined" ? location.href : void 0);
500
+ } catch {
501
+ return (rawUrl.split(/[?#]/)[0] ?? "").slice(0, 120);
502
+ }
503
+ 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);
506
+ }
452
507
  function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignoreUrlPrefixes, always = false) {
453
508
  if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
454
509
  const shouldEmit = () => always || buffer.hasExposures();
@@ -497,7 +552,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
497
552
  const problem = err ?? (typeof msg === "string" && msg ? msg : "Unknown error");
498
553
  reportSee(
499
554
  problem,
500
- causesThe("the page").to("hit an unhandled error"),
555
+ causesThe("page").to("hit an unhandled error"),
501
556
  {
502
557
  source: typeof source === "string" ? source : void 0,
503
558
  line: lineno ?? void 0
@@ -513,7 +568,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
513
568
  if (isExpected(reason)) return;
514
569
  reportSee(
515
570
  reason ?? "Unhandled promise rejection",
516
- causesThe("the page").to("hit an unhandled promise rejection"),
571
+ causesThe("page").to("hit an unhandled promise rejection"),
517
572
  void 0,
518
573
  "unhandled_rejection"
519
574
  );
@@ -531,7 +586,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
531
586
  if (!ignored && !isExpected(err)) {
532
587
  reportSee(
533
588
  violation("NetworkError").message(`request to ${bareUrl} failed`),
534
- causesThe("a network request").to("fail without a response"),
589
+ causesThe(`request to ${endpointTemplate(url)}`).to("get no response"),
535
590
  { status: 0, url: url.slice(0, 200) },
536
591
  "network"
537
592
  );
@@ -542,7 +597,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
542
597
  const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
543
598
  reportSee(
544
599
  violation("Http5xx").message(`request to ${bareUrl} returned ${res.status}`),
545
- causesThe("a network request").to(`fail with HTTP ${res.status}`),
600
+ causesThe(`request to ${endpointTemplate(url)}`).to("fail with a server error"),
546
601
  { status: res.status, url: url.slice(0, 200), duration_ms: Math.round(elapsed) },
547
602
  "network"
548
603
  );
@@ -687,6 +742,83 @@ function collectBrowserAttrs() {
687
742
  }
688
743
  return attrs;
689
744
  }
745
+ function collectSeeEnv() {
746
+ const out = {};
747
+ if (typeof navigator === "undefined") return out;
748
+ const nav = navigator;
749
+ const ua = typeof nav.userAgent === "string" ? nav.userAgent : "";
750
+ const browser = parseUaBrowser(ua);
751
+ if (browser) out["env.browser"] = browser;
752
+ const os = parseUaOs(ua) ?? nav.userAgentData?.platform;
753
+ if (os) out["env.os"] = os;
754
+ 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";
755
+ try {
756
+ if (nav.language) out["env.lang"] = nav.language;
757
+ } catch {
758
+ }
759
+ try {
760
+ if (typeof nav.onLine === "boolean") out["env.online"] = nav.onLine;
761
+ } catch {
762
+ }
763
+ try {
764
+ if (typeof nav.hardwareConcurrency === "number") out["env.cores"] = nav.hardwareConcurrency;
765
+ } catch {
766
+ }
767
+ try {
768
+ if (typeof nav.deviceMemory === "number") out["env.memory_gb"] = nav.deviceMemory;
769
+ } catch {
770
+ }
771
+ try {
772
+ const et = nav.connection?.effectiveType;
773
+ if (et) out["env.connection"] = et;
774
+ } catch {
775
+ }
776
+ try {
777
+ if (typeof window !== "undefined" && window.innerWidth && window.innerHeight) {
778
+ out["env.viewport"] = `${window.innerWidth}\xD7${window.innerHeight}`;
779
+ }
780
+ if (typeof window !== "undefined" && typeof window.devicePixelRatio === "number") {
781
+ out["env.dpr"] = window.devicePixelRatio;
782
+ }
783
+ if (typeof screen !== "undefined" && screen.width && screen.height) {
784
+ out["env.screen"] = `${screen.width}\xD7${screen.height}`;
785
+ }
786
+ } catch {
787
+ }
788
+ try {
789
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
790
+ if (tz) out["env.tz"] = tz;
791
+ } catch {
792
+ }
793
+ return out;
794
+ }
795
+ function parseUaBrowser(ua) {
796
+ const tests = [
797
+ [/Edg(?:A|iOS)?\/(\d+)/, "Edge"],
798
+ [/(?:OPR|Opera)\/(\d+)/, "Opera"],
799
+ [/(?:Firefox|FxiOS)\/(\d+)/, "Firefox"],
800
+ [/(?:Chrome|CriOS)\/(\d+)/, "Chrome"],
801
+ [/Version\/(\d+)[.\d]* (?:Mobile.*)?Safari/, "Safari"]
802
+ ];
803
+ for (const [re, name] of tests) {
804
+ const m = re.exec(ua);
805
+ if (m) return `${name} ${m[1]}`;
806
+ }
807
+ return void 0;
808
+ }
809
+ function parseUaOs(ua) {
810
+ if (/Windows NT 10/.test(ua)) return "Windows 10/11";
811
+ if (/Windows NT/.test(ua)) return "Windows";
812
+ let m = /Mac OS X (\d+)[._](\d+)/.exec(ua);
813
+ if (m) return `macOS ${m[1]}.${m[2]}`;
814
+ if (/Macintosh/.test(ua)) return "macOS";
815
+ m = /Android (\d+)/.exec(ua);
816
+ if (m) return `Android ${m[1]}`;
817
+ m = /(?:iPhone|iPad)[^)]* OS (\d+)/.exec(ua);
818
+ if (m) return `iOS ${m[1]}`;
819
+ if (/Linux/.test(ua)) return "Linux";
820
+ return void 0;
821
+ }
690
822
  function readExperimentOverridesFromUrl() {
691
823
  if (typeof window === "undefined") return {};
692
824
  const out = {};
@@ -793,7 +925,8 @@ var FlagsClientBrowser = class {
793
925
  */
794
926
  reportError(problem, consequence, extras, kind) {
795
927
  try {
796
- const ev = buildSeeEvent(problem, consequence, extras, {
928
+ const enriched = { ...collectSeeEnv(), ...extras };
929
+ const ev = buildSeeEvent(problem, consequence, enriched, {
797
930
  side: "client",
798
931
  sdkVersion: version,
799
932
  env: this.env,
@@ -106,6 +106,44 @@ function isExpected(err) {
106
106
  if (typeof err !== "object" || err === null) return false;
107
107
  return err[EXPECTED_SYM] !== void 0;
108
108
  }
109
+ var REPORTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-reported");
110
+ var SEE_MAX_CAUSE_DEPTH = 8;
111
+ function readReportStamp(err) {
112
+ if (typeof err !== "object" || err === null) return void 0;
113
+ const v = err[REPORTED_SYM];
114
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
115
+ }
116
+ function findCausedBy(problem) {
117
+ let cur = problem;
118
+ const seen = /* @__PURE__ */ new Set();
119
+ for (let depth = 0; depth < SEE_MAX_CAUSE_DEPTH; depth++) {
120
+ if (typeof cur !== "object" || cur === null || seen.has(cur)) break;
121
+ seen.add(cur);
122
+ const stamp = readReportStamp(cur);
123
+ if (stamp) return stamp;
124
+ cur = cur.cause;
125
+ }
126
+ return void 0;
127
+ }
128
+ function markReported(problem, ev) {
129
+ if (!(problem instanceof Error)) return;
130
+ const stamp = {
131
+ error_type: ev.error_type,
132
+ message: ev.message,
133
+ subject: ev.subject,
134
+ outcome: ev.outcome
135
+ };
136
+ if (ev.stack !== void 0) stamp.stack = ev.stack;
137
+ try {
138
+ Object.defineProperty(problem, REPORTED_SYM, {
139
+ value: Object.freeze(stamp),
140
+ enumerable: false,
141
+ configurable: true,
142
+ writable: true
143
+ });
144
+ } catch {
145
+ }
146
+ }
109
147
  function truncate(s, max) {
110
148
  return s.length > max ? s.slice(0, max) : s;
111
149
  }
@@ -164,12 +202,15 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
164
202
  ts: Date.now()
165
203
  };
166
204
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
205
+ const causedBy = findCausedBy(problem);
206
+ if (causedBy) ev.caused_by = causedBy;
167
207
  const cleanExtras = sanitizeExtras(extras);
168
208
  if (cleanExtras) ev.extras = cleanExtras;
169
209
  if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
170
210
  if (ctx.userId) ev.user_id = ctx.userId;
171
211
  if (ctx.anonId) ev.anonymous_id = ctx.anonId;
172
212
  if (ctx.env) ev.env = ctx.env;
213
+ markReported(problem, ev);
173
214
  return ev;
174
215
  }
175
216
  function safeString(v) {
@@ -192,7 +233,9 @@ function startSeeChain(getProblem, dispatch) {
192
233
  flushed = true;
193
234
  dispatch(
194
235
  getProblem(),
195
- causesThe(subject ?? "the app").to(outcome ?? "hit an error"),
236
+ // Bare noun phrase titles render as " causes the {subject} …", so a
237
+ // leading article would double up ("causes the the app").
238
+ causesThe(subject ?? "app").to(outcome ?? "hit an error"),
196
239
  collected
197
240
  );
198
241
  });
@@ -403,6 +446,18 @@ var EventBuffer = class {
403
446
  });
404
447
  }
405
448
  };
449
+ function endpointTemplate(rawUrl) {
450
+ 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);
451
+ let u;
452
+ try {
453
+ u = new URL(rawUrl, typeof location !== "undefined" ? location.href : void 0);
454
+ } catch {
455
+ return (rawUrl.split(/[?#]/)[0] ?? "").slice(0, 120);
456
+ }
457
+ 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
+ }
406
461
  function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignoreUrlPrefixes, always = false) {
407
462
  if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
408
463
  const shouldEmit = () => always || buffer.hasExposures();
@@ -451,7 +506,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
451
506
  const problem = err ?? (typeof msg === "string" && msg ? msg : "Unknown error");
452
507
  reportSee(
453
508
  problem,
454
- causesThe("the page").to("hit an unhandled error"),
509
+ causesThe("page").to("hit an unhandled error"),
455
510
  {
456
511
  source: typeof source === "string" ? source : void 0,
457
512
  line: lineno ?? void 0
@@ -467,7 +522,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
467
522
  if (isExpected(reason)) return;
468
523
  reportSee(
469
524
  reason ?? "Unhandled promise rejection",
470
- causesThe("the page").to("hit an unhandled promise rejection"),
525
+ causesThe("page").to("hit an unhandled promise rejection"),
471
526
  void 0,
472
527
  "unhandled_rejection"
473
528
  );
@@ -485,7 +540,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
485
540
  if (!ignored && !isExpected(err)) {
486
541
  reportSee(
487
542
  violation("NetworkError").message(`request to ${bareUrl} failed`),
488
- causesThe("a network request").to("fail without a response"),
543
+ causesThe(`request to ${endpointTemplate(url)}`).to("get no response"),
489
544
  { status: 0, url: url.slice(0, 200) },
490
545
  "network"
491
546
  );
@@ -496,7 +551,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
496
551
  const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
497
552
  reportSee(
498
553
  violation("Http5xx").message(`request to ${bareUrl} returned ${res.status}`),
499
- causesThe("a network request").to(`fail with HTTP ${res.status}`),
554
+ causesThe(`request to ${endpointTemplate(url)}`).to("fail with a server error"),
500
555
  { status: res.status, url: url.slice(0, 200), duration_ms: Math.round(elapsed) },
501
556
  "network"
502
557
  );
@@ -641,6 +696,83 @@ function collectBrowserAttrs() {
641
696
  }
642
697
  return attrs;
643
698
  }
699
+ function collectSeeEnv() {
700
+ const out = {};
701
+ if (typeof navigator === "undefined") return out;
702
+ const nav = navigator;
703
+ const ua = typeof nav.userAgent === "string" ? nav.userAgent : "";
704
+ const browser = parseUaBrowser(ua);
705
+ if (browser) out["env.browser"] = browser;
706
+ const os = parseUaOs(ua) ?? nav.userAgentData?.platform;
707
+ if (os) out["env.os"] = os;
708
+ 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";
709
+ try {
710
+ if (nav.language) out["env.lang"] = nav.language;
711
+ } catch {
712
+ }
713
+ try {
714
+ if (typeof nav.onLine === "boolean") out["env.online"] = nav.onLine;
715
+ } catch {
716
+ }
717
+ try {
718
+ if (typeof nav.hardwareConcurrency === "number") out["env.cores"] = nav.hardwareConcurrency;
719
+ } catch {
720
+ }
721
+ try {
722
+ if (typeof nav.deviceMemory === "number") out["env.memory_gb"] = nav.deviceMemory;
723
+ } catch {
724
+ }
725
+ try {
726
+ const et = nav.connection?.effectiveType;
727
+ if (et) out["env.connection"] = et;
728
+ } catch {
729
+ }
730
+ try {
731
+ if (typeof window !== "undefined" && window.innerWidth && window.innerHeight) {
732
+ out["env.viewport"] = `${window.innerWidth}\xD7${window.innerHeight}`;
733
+ }
734
+ if (typeof window !== "undefined" && typeof window.devicePixelRatio === "number") {
735
+ out["env.dpr"] = window.devicePixelRatio;
736
+ }
737
+ if (typeof screen !== "undefined" && screen.width && screen.height) {
738
+ out["env.screen"] = `${screen.width}\xD7${screen.height}`;
739
+ }
740
+ } catch {
741
+ }
742
+ try {
743
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
744
+ if (tz) out["env.tz"] = tz;
745
+ } catch {
746
+ }
747
+ return out;
748
+ }
749
+ function parseUaBrowser(ua) {
750
+ const tests = [
751
+ [/Edg(?:A|iOS)?\/(\d+)/, "Edge"],
752
+ [/(?:OPR|Opera)\/(\d+)/, "Opera"],
753
+ [/(?:Firefox|FxiOS)\/(\d+)/, "Firefox"],
754
+ [/(?:Chrome|CriOS)\/(\d+)/, "Chrome"],
755
+ [/Version\/(\d+)[.\d]* (?:Mobile.*)?Safari/, "Safari"]
756
+ ];
757
+ for (const [re, name] of tests) {
758
+ const m = re.exec(ua);
759
+ if (m) return `${name} ${m[1]}`;
760
+ }
761
+ return void 0;
762
+ }
763
+ function parseUaOs(ua) {
764
+ if (/Windows NT 10/.test(ua)) return "Windows 10/11";
765
+ if (/Windows NT/.test(ua)) return "Windows";
766
+ let m = /Mac OS X (\d+)[._](\d+)/.exec(ua);
767
+ if (m) return `macOS ${m[1]}.${m[2]}`;
768
+ if (/Macintosh/.test(ua)) return "macOS";
769
+ m = /Android (\d+)/.exec(ua);
770
+ if (m) return `Android ${m[1]}`;
771
+ m = /(?:iPhone|iPad)[^)]* OS (\d+)/.exec(ua);
772
+ if (m) return `iOS ${m[1]}`;
773
+ if (/Linux/.test(ua)) return "Linux";
774
+ return void 0;
775
+ }
644
776
  function readExperimentOverridesFromUrl() {
645
777
  if (typeof window === "undefined") return {};
646
778
  const out = {};
@@ -747,7 +879,8 @@ var FlagsClientBrowser = class {
747
879
  */
748
880
  reportError(problem, consequence, extras, kind) {
749
881
  try {
750
- const ev = buildSeeEvent(problem, consequence, extras, {
882
+ const enriched = { ...collectSeeEnv(), ...extras };
883
+ const ev = buildSeeEvent(problem, consequence, enriched, {
751
884
  side: "client",
752
885
  sdkVersion: version,
753
886
  env: this.env,
@@ -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,14 @@ interface SeeErrorEvent {
37
51
  env?: string;
38
52
  sdk_version: string;
39
53
  ts: number;
54
+ /**
55
+ * The earlier reported problem this occurrence descends from — present when
56
+ * the same error was caught + reported at an inner boundary and then
57
+ * re-thrown (or wrapped via `{ cause }`) and reported again at an outer one.
58
+ * Lets the backend stitch the two issues into a cause chain instead of
59
+ * double-counting them as unrelated.
60
+ */
61
+ caused_by?: SeeCausedBy;
40
62
  }
41
63
  interface SeeExtrasTail {
42
64
  /** Attach debugging metadata. Callable repeatedly — keys merge, later wins. */
@@ -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,14 @@ interface SeeErrorEvent {
37
51
  env?: string;
38
52
  sdk_version: string;
39
53
  ts: number;
54
+ /**
55
+ * The earlier reported problem this occurrence descends from — present when
56
+ * the same error was caught + reported at an inner boundary and then
57
+ * re-thrown (or wrapped via `{ cause }`) and reported again at an outer one.
58
+ * Lets the backend stitch the two issues into a cause chain instead of
59
+ * double-counting them as unrelated.
60
+ */
61
+ caused_by?: SeeCausedBy;
40
62
  }
41
63
  interface SeeExtrasTail {
42
64
  /** Attach debugging metadata. Callable repeatedly — keys merge, later wins. */
@@ -149,6 +149,44 @@ function markExpected(err, because) {
149
149
  } catch {
150
150
  }
151
151
  }
152
+ var REPORTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-reported");
153
+ var SEE_MAX_CAUSE_DEPTH = 8;
154
+ function readReportStamp(err) {
155
+ if (typeof err !== "object" || err === null) return void 0;
156
+ const v = err[REPORTED_SYM];
157
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
158
+ }
159
+ function findCausedBy(problem) {
160
+ let cur = problem;
161
+ const seen = /* @__PURE__ */ new Set();
162
+ for (let depth = 0; depth < SEE_MAX_CAUSE_DEPTH; depth++) {
163
+ if (typeof cur !== "object" || cur === null || seen.has(cur)) break;
164
+ seen.add(cur);
165
+ const stamp = readReportStamp(cur);
166
+ if (stamp) return stamp;
167
+ cur = cur.cause;
168
+ }
169
+ return void 0;
170
+ }
171
+ function markReported(problem, ev) {
172
+ if (!(problem instanceof Error)) return;
173
+ const stamp = {
174
+ error_type: ev.error_type,
175
+ message: ev.message,
176
+ subject: ev.subject,
177
+ outcome: ev.outcome
178
+ };
179
+ if (ev.stack !== void 0) stamp.stack = ev.stack;
180
+ try {
181
+ Object.defineProperty(problem, REPORTED_SYM, {
182
+ value: Object.freeze(stamp),
183
+ enumerable: false,
184
+ configurable: true,
185
+ writable: true
186
+ });
187
+ } catch {
188
+ }
189
+ }
152
190
  function truncate(s, max) {
153
191
  return s.length > max ? s.slice(0, max) : s;
154
192
  }
@@ -207,12 +245,15 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
207
245
  ts: Date.now()
208
246
  };
209
247
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
248
+ const causedBy = findCausedBy(problem);
249
+ if (causedBy) ev.caused_by = causedBy;
210
250
  const cleanExtras = sanitizeExtras(extras);
211
251
  if (cleanExtras) ev.extras = cleanExtras;
212
252
  if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
213
253
  if (ctx.userId) ev.user_id = ctx.userId;
214
254
  if (ctx.anonId) ev.anonymous_id = ctx.anonId;
215
255
  if (ctx.env) ev.env = ctx.env;
256
+ markReported(problem, ev);
216
257
  return ev;
217
258
  }
218
259
  function safeString(v) {
@@ -235,7 +276,9 @@ function startSeeChain(getProblem, dispatch) {
235
276
  flushed = true;
236
277
  dispatch(
237
278
  getProblem(),
238
- causesThe(subject ?? "the app").to(outcome ?? "hit an error"),
279
+ // Bare noun phrase titles render as " causes the {subject} …", so a
280
+ // leading article would double up ("causes the the app").
281
+ causesThe(subject ?? "app").to(outcome ?? "hit an error"),
239
282
  collected
240
283
  );
241
284
  });
@@ -105,6 +105,44 @@ function markExpected(err, because) {
105
105
  } catch {
106
106
  }
107
107
  }
108
+ var REPORTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-reported");
109
+ var SEE_MAX_CAUSE_DEPTH = 8;
110
+ function readReportStamp(err) {
111
+ if (typeof err !== "object" || err === null) return void 0;
112
+ const v = err[REPORTED_SYM];
113
+ return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
114
+ }
115
+ function findCausedBy(problem) {
116
+ let cur = problem;
117
+ const seen = /* @__PURE__ */ new Set();
118
+ for (let depth = 0; depth < SEE_MAX_CAUSE_DEPTH; depth++) {
119
+ if (typeof cur !== "object" || cur === null || seen.has(cur)) break;
120
+ seen.add(cur);
121
+ const stamp = readReportStamp(cur);
122
+ if (stamp) return stamp;
123
+ cur = cur.cause;
124
+ }
125
+ return void 0;
126
+ }
127
+ function markReported(problem, ev) {
128
+ if (!(problem instanceof Error)) return;
129
+ const stamp = {
130
+ error_type: ev.error_type,
131
+ message: ev.message,
132
+ subject: ev.subject,
133
+ outcome: ev.outcome
134
+ };
135
+ if (ev.stack !== void 0) stamp.stack = ev.stack;
136
+ try {
137
+ Object.defineProperty(problem, REPORTED_SYM, {
138
+ value: Object.freeze(stamp),
139
+ enumerable: false,
140
+ configurable: true,
141
+ writable: true
142
+ });
143
+ } catch {
144
+ }
145
+ }
108
146
  function truncate(s, max) {
109
147
  return s.length > max ? s.slice(0, max) : s;
110
148
  }
@@ -163,12 +201,15 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride) {
163
201
  ts: Date.now()
164
202
  };
165
203
  if (stack) ev.stack = truncate(stack, SEE_MAX_STACK);
204
+ const causedBy = findCausedBy(problem);
205
+ if (causedBy) ev.caused_by = causedBy;
166
206
  const cleanExtras = sanitizeExtras(extras);
167
207
  if (cleanExtras) ev.extras = cleanExtras;
168
208
  if (ctx.url) ev.url = truncate(ctx.url, SEE_MAX_SUBJECT);
169
209
  if (ctx.userId) ev.user_id = ctx.userId;
170
210
  if (ctx.anonId) ev.anonymous_id = ctx.anonId;
171
211
  if (ctx.env) ev.env = ctx.env;
212
+ markReported(problem, ev);
172
213
  return ev;
173
214
  }
174
215
  function safeString(v) {
@@ -191,7 +232,9 @@ function startSeeChain(getProblem, dispatch) {
191
232
  flushed = true;
192
233
  dispatch(
193
234
  getProblem(),
194
- causesThe(subject ?? "the app").to(outcome ?? "hit an error"),
235
+ // Bare noun phrase titles render as " causes the {subject} …", so a
236
+ // leading article would double up ("causes the the app").
237
+ causesThe(subject ?? "app").to(outcome ?? "hit an error"),
195
238
  collected
196
239
  );
197
240
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/sdk",
3
- "version": "4.1.0",
3
+ "version": "4.2.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",