@newtalaria/browser 0.1.13 → 0.1.16

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.
@@ -17,6 +17,43 @@
17
17
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
18
18
  }
19
19
 
20
+ // src/utils/environment.ts
21
+ var WIRE = /* @__PURE__ */ new Set([
22
+ "production",
23
+ "staging",
24
+ "development"
25
+ ]);
26
+ function normalizeEnvironment(raw) {
27
+ const normalized = raw.trim().toLowerCase();
28
+ if (!normalized) {
29
+ throw new Error(
30
+ "@newtalaria/browser: init requires `environment` (production | staging | development)"
31
+ );
32
+ }
33
+ switch (normalized) {
34
+ case "prod":
35
+ case "production":
36
+ case "live":
37
+ return "production";
38
+ case "stage":
39
+ case "staging":
40
+ case "uat":
41
+ case "test":
42
+ return "staging";
43
+ case "dev":
44
+ case "development":
45
+ case "local":
46
+ return "development";
47
+ default:
48
+ if (WIRE.has(normalized)) {
49
+ return normalized;
50
+ }
51
+ throw new Error(
52
+ `@newtalaria/browser: invalid environment '${raw}'. Expected production, staging, or development.`
53
+ );
54
+ }
55
+ }
56
+
20
57
  // src/utils/browser_extension_noise.ts
21
58
  var EXTENSION_URL = /(?:chrome|moz|safari|safari-web|ms-browser)-extension:\/\//i;
22
59
  var WEBKIT_MASKED = /webkit-masked-url:\/\/hidden\//i;
@@ -40,6 +77,28 @@
40
77
  return true;
41
78
  }
42
79
 
80
+ // src/utils/sdk_internal_noise.ts
81
+ var SDK_STACK_FRAME = /@newtalaria\/browser|\/npm\/@newtalaria\/browser(?:@[\w.-]+)?\//i;
82
+ function isSdkInternalNoise(opts) {
83
+ const stack = opts.stack ?? "";
84
+ if (!stack) return false;
85
+ const frameLines = stack.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("at "));
86
+ if (frameLines.length === 0) return false;
87
+ let sdkFrames = 0;
88
+ let otherUrlFrames = 0;
89
+ for (const line of frameLines) {
90
+ const urlMatch = line.match(/\((https?:\/\/[^)]+)\)|(https?:\/\/\S+)/i);
91
+ const url = urlMatch?.[1] ?? urlMatch?.[2];
92
+ if (!url) continue;
93
+ if (SDK_STACK_FRAME.test(url)) {
94
+ sdkFrames += 1;
95
+ } else {
96
+ otherUrlFrames += 1;
97
+ }
98
+ }
99
+ return sdkFrames > 0 && otherUrlFrames === 0;
100
+ }
101
+
43
102
  // src/utils/browser_context.ts
44
103
  function parseBrowserContext(ua = typeof navigator !== "undefined" ? navigator.userAgent : "", language = typeof navigator !== "undefined" ? navigator.language : "") {
45
104
  const userAgent = ua || "";
@@ -169,7 +228,7 @@
169
228
 
170
229
  // src/sdk_meta.ts
171
230
  var SDK_NAME = "@newtalaria/browser";
172
- var SDK_VERSION = "0.1.13";
231
+ var SDK_VERSION = "0.1.16";
173
232
 
174
233
  // src/transport/serverpod.ts
175
234
  var ServerpodTransport = class {
@@ -13393,6 +13452,40 @@
13393
13452
  return raw.replace(SENSITIVE_QUERY, (_m, name) => `${name}=[Filtered]`);
13394
13453
  }
13395
13454
  }
13455
+ function sanitizeNetworkUrl(raw, opts) {
13456
+ const baseHref = opts?.baseHref ?? (typeof location !== "undefined" ? location.href : void 0);
13457
+ try {
13458
+ const parsed = new URL(raw, baseHref);
13459
+ const hostname = parsed.hostname;
13460
+ const pathname = parsed.pathname || "/";
13461
+ const originPath = `${parsed.origin}${pathname}`;
13462
+ if (!opts?.includeQuery) {
13463
+ return { url: originPath, hostname, pathname };
13464
+ }
13465
+ const redacted = new URL(redactUrl(parsed.toString()));
13466
+ const search = redacted.search || void 0;
13467
+ return {
13468
+ url: search ? `${originPath}${search}` : originPath,
13469
+ hostname,
13470
+ pathname,
13471
+ ...search ? { search } : {}
13472
+ };
13473
+ } catch {
13474
+ const stripped = (raw.split(/[?#]/)[0] ?? raw).trim() || raw;
13475
+ if (!opts?.includeQuery) {
13476
+ return { url: stripped, hostname: "", pathname: stripped };
13477
+ }
13478
+ const redacted = redactUrl(raw);
13479
+ const qIndex = redacted.indexOf("?");
13480
+ const search = qIndex >= 0 ? redacted.slice(qIndex) : void 0;
13481
+ return {
13482
+ url: redacted.split("#")[0] || redacted,
13483
+ hostname: "",
13484
+ pathname: stripped,
13485
+ ...search ? { search } : {}
13486
+ };
13487
+ }
13488
+ }
13396
13489
  function defaultBlockSelector(extra) {
13397
13490
  const parts = [
13398
13491
  "[data-talaria-mask]",
@@ -13449,6 +13542,32 @@
13449
13542
  };
13450
13543
  }
13451
13544
 
13545
+ // src/utils/network_error.ts
13546
+ function isAbortError(error) {
13547
+ return error instanceof Error && error.name === "AbortError";
13548
+ }
13549
+ function isLikelyNetworkFetchError(error) {
13550
+ if (!(error instanceof Error)) return false;
13551
+ if (error.name === "AbortError") return false;
13552
+ if (error.name === "NetworkError") return true;
13553
+ const msg = error.message.toLowerCase().trim();
13554
+ return msg === "failed to fetch" || msg === "load failed" || msg === "networkerror when attempting to fetch resource." || msg.startsWith("networkerror") || msg.includes("failed to fetch");
13555
+ }
13556
+ function describeUnknownError(error) {
13557
+ if (error instanceof Error) {
13558
+ return {
13559
+ errorName: error.name || "Error",
13560
+ errorMessage: (error.message || "").slice(0, 500),
13561
+ aborted: error.name === "AbortError"
13562
+ };
13563
+ }
13564
+ return {
13565
+ errorName: "Error",
13566
+ errorMessage: String(error).slice(0, 500),
13567
+ aborted: false
13568
+ };
13569
+ }
13570
+
13452
13571
  // src/replay/hooks.ts
13453
13572
  function installVisibilityResumeHook(onResume) {
13454
13573
  if (typeof document === "undefined") {
@@ -13476,20 +13595,40 @@
13476
13595
  }
13477
13596
  };
13478
13597
  }
13598
+ function serializeConsoleArg(arg) {
13599
+ if (arg instanceof Error) {
13600
+ return { name: arg.name, message: arg.message, stack: arg.stack };
13601
+ }
13602
+ return arg;
13603
+ }
13604
+ function formatConsoleArg(arg) {
13605
+ if (arg == null) return String(arg);
13606
+ if (typeof arg === "string") return arg;
13607
+ if (typeof arg === "number" || typeof arg === "boolean") return String(arg);
13608
+ if (arg instanceof Error) {
13609
+ return arg.stack || `${arg.name}: ${arg.message}`;
13610
+ }
13611
+ if (typeof arg === "object" && arg !== null && "name" in arg && "message" in arg) {
13612
+ const err = arg;
13613
+ if (typeof err.stack === "string" && err.stack) return err.stack;
13614
+ return `${String(err.name)}: ${String(err.message)}`;
13615
+ }
13616
+ try {
13617
+ return JSON.stringify(arg);
13618
+ } catch {
13619
+ return String(arg);
13620
+ }
13621
+ }
13479
13622
  function safeSerialize(args) {
13480
13623
  try {
13481
- return JSON.stringify(
13482
- args.map((a) => {
13483
- if (a instanceof Error) {
13484
- return { name: a.name, message: a.message, stack: a.stack };
13485
- }
13486
- return a;
13487
- })
13488
- );
13624
+ return JSON.stringify(args.map(serializeConsoleArg));
13489
13625
  } catch {
13490
13626
  return "[unserializable]";
13491
13627
  }
13492
13628
  }
13629
+ function consoleMessage(args) {
13630
+ return args.map(formatConsoleArg).join(" ").slice(0, 4e3);
13631
+ }
13493
13632
  function installConsoleHook() {
13494
13633
  const levels = ["log", "info", "warn", "error", "debug"];
13495
13634
  const originals = {};
@@ -13498,9 +13637,12 @@
13498
13637
  originals[level] = original;
13499
13638
  console[level] = (...args) => {
13500
13639
  try {
13640
+ const serializedArgs = safeSerialize(args).slice(0, 4e3);
13501
13641
  record.addCustomEvent("talaria-console", {
13502
13642
  level,
13503
- args: safeSerialize(args).slice(0, 4e3),
13643
+ // `message` is what the replay sidebar displays; `args` kept for tooling.
13644
+ message: consoleMessage(args),
13645
+ args: serializedArgs,
13504
13646
  timestamp: Date.now()
13505
13647
  });
13506
13648
  } catch {
@@ -13519,7 +13661,6 @@
13519
13661
  try {
13520
13662
  record.addCustomEvent("talaria-network", {
13521
13663
  ...meta,
13522
- url: redactUrl(meta.url),
13523
13664
  timestamp: Date.now()
13524
13665
  });
13525
13666
  } catch {
@@ -13536,23 +13677,70 @@
13536
13677
  }
13537
13678
  return false;
13538
13679
  }
13539
- function shouldPromoteFailedRequest(meta, opts) {
13540
- if (!opts.captureFailedRequests) return false;
13541
- if (typeof meta.status !== "number" || Number.isNaN(meta.status)) return false;
13542
- if (!statusMatches(meta.status, opts.failedRequestStatusCodes)) return false;
13543
- const url = meta.url || "";
13544
- const ignore = [...opts.failedRequestIgnoreUrls];
13545
- const base = (opts.talariaBaseUrl ?? "").replace(/\/+$/, "");
13680
+ function buildFailedRequestIgnoreUrls(failedRequestIgnoreUrls, talariaBaseUrl) {
13681
+ const ignore = [...failedRequestIgnoreUrls];
13682
+ const base = (talariaBaseUrl ?? "").replace(/\/+$/, "");
13546
13683
  if (base) {
13547
13684
  ignore.push(`${base}/events/`, `${base}/replays/`);
13548
13685
  }
13549
13686
  ignore.push("/events/ingest", "/events/ingestBatch", "/replays/");
13550
- const lower = url.toLowerCase();
13551
- for (const part of ignore) {
13552
- if (part && lower.includes(part.toLowerCase())) return false;
13687
+ return ignore;
13688
+ }
13689
+ function urlMatchesIgnoreList(url, ignoreUrls) {
13690
+ const lower = (url || "").toLowerCase();
13691
+ for (const part of ignoreUrls) {
13692
+ if (part && lower.includes(part.toLowerCase())) return true;
13553
13693
  }
13694
+ return false;
13695
+ }
13696
+ function shouldPromoteFailedRequest(meta, opts) {
13697
+ if (!opts.captureFailedRequests) return false;
13698
+ if (typeof meta.status !== "number" || Number.isNaN(meta.status)) return false;
13699
+ if (meta.status === 0) return false;
13700
+ if (!statusMatches(meta.status, opts.failedRequestStatusCodes)) return false;
13701
+ const ignore = buildFailedRequestIgnoreUrls(
13702
+ opts.failedRequestIgnoreUrls,
13703
+ opts.talariaBaseUrl
13704
+ );
13705
+ if (urlMatchesIgnoreList(meta.url || "", ignore)) return false;
13554
13706
  return true;
13555
13707
  }
13708
+ function shouldPromoteNetworkError(meta, opts) {
13709
+ if (!opts.captureNetworkErrors) return false;
13710
+ if (meta.aborted) return false;
13711
+ if (meta.failureKind !== "network") return false;
13712
+ const ignore = buildFailedRequestIgnoreUrls(
13713
+ opts.failedRequestIgnoreUrls,
13714
+ opts.talariaBaseUrl
13715
+ );
13716
+ if (urlMatchesIgnoreList(meta.url || "", ignore)) return false;
13717
+ return true;
13718
+ }
13719
+ function enrichNetworkMeta(meta) {
13720
+ const hasHttpStatus = typeof meta.status === "number" && !Number.isNaN(meta.status) && meta.status > 0;
13721
+ if (hasHttpStatus) {
13722
+ return {
13723
+ ...meta,
13724
+ failureKind: meta.ok === false ? "http" : meta.failureKind
13725
+ };
13726
+ }
13727
+ if (meta.ok === false || meta.errorName || meta.errorMessage) {
13728
+ return {
13729
+ ...meta,
13730
+ failureKind: "network"
13731
+ };
13732
+ }
13733
+ return meta;
13734
+ }
13735
+ function networkUrlParts(rawUrl, includeQuery) {
13736
+ const parts = sanitizeNetworkUrl(rawUrl, { includeQuery });
13737
+ return {
13738
+ url: parts.url,
13739
+ hostname: parts.hostname,
13740
+ pathname: parts.pathname,
13741
+ ...parts.search ? { search: parts.search } : {}
13742
+ };
13743
+ }
13556
13744
  function installNetworkHook(options = {}) {
13557
13745
  const originalFetch = typeof fetch === "function" ? fetch.bind(globalThis) : null;
13558
13746
  const XHR = typeof XMLHttpRequest !== "undefined" ? XMLHttpRequest : null;
@@ -13560,38 +13748,57 @@
13560
13748
  const originalSend = XHR?.prototype.send;
13561
13749
  const matchOpts = {
13562
13750
  captureFailedRequests: options.captureFailedRequests ?? true,
13751
+ captureNetworkErrors: options.captureNetworkErrors ?? true,
13563
13752
  failedRequestStatusCodes: options.failedRequestStatusCodes ?? [[500, 599]],
13564
13753
  failedRequestIgnoreUrls: options.failedRequestIgnoreUrls ?? [],
13565
13754
  talariaBaseUrl: options.talariaBaseUrl
13566
13755
  };
13756
+ const includeQuery = options.includeNetworkUrlQuery ?? false;
13567
13757
  const handleMeta = (meta) => {
13568
- emitNetwork(meta);
13569
- options.onNetwork?.(meta);
13570
- if (shouldPromoteFailedRequest(meta, matchOpts)) {
13571
- options.onFailedRequest?.(meta);
13758
+ const { rawUrl, ...rest } = meta;
13759
+ const enriched = enrichNetworkMeta({
13760
+ ...rest,
13761
+ ...networkUrlParts(rawUrl, includeQuery)
13762
+ });
13763
+ try {
13764
+ emitNetwork(enriched);
13765
+ options.onNetwork?.(enriched);
13766
+ if (shouldPromoteFailedRequest(enriched, matchOpts)) {
13767
+ options.onFailedRequest?.(enriched);
13768
+ } else if (shouldPromoteNetworkError(enriched, matchOpts)) {
13769
+ options.onNetworkError?.(enriched);
13770
+ }
13771
+ } catch {
13572
13772
  }
13573
13773
  };
13574
13774
  if (originalFetch) {
13575
13775
  globalThis.fetch = async (input2, init) => {
13576
13776
  const started = Date.now();
13577
13777
  const method = (init?.method ?? (input2 instanceof Request ? input2.method : "GET")).toUpperCase();
13578
- const url = typeof input2 === "string" ? input2 : input2 instanceof URL ? input2.toString() : input2.url;
13778
+ const rawUrl = typeof input2 === "string" ? input2 : input2 instanceof URL ? input2.toString() : input2.url;
13579
13779
  try {
13580
13780
  const response = await originalFetch(input2, init);
13581
13781
  handleMeta({
13582
13782
  method,
13583
- url,
13783
+ rawUrl,
13784
+ url: rawUrl,
13584
13785
  status: response.status,
13585
13786
  durationMs: Date.now() - started,
13586
13787
  ok: response.ok
13587
13788
  });
13588
13789
  return response;
13589
13790
  } catch (error) {
13791
+ const described = describeUnknownError(error);
13590
13792
  handleMeta({
13591
13793
  method,
13592
- url,
13794
+ rawUrl,
13795
+ url: rawUrl,
13593
13796
  durationMs: Date.now() - started,
13594
- ok: false
13797
+ ok: false,
13798
+ errorName: described.errorName,
13799
+ errorMessage: described.errorMessage,
13800
+ aborted: described.aborted,
13801
+ failureKind: "network"
13595
13802
  });
13596
13803
  throw error;
13597
13804
  }
@@ -13606,12 +13813,21 @@
13606
13813
  XHR.prototype.send = function(body) {
13607
13814
  this.__talariaStarted = Date.now();
13608
13815
  const onDone = () => {
13816
+ const status = this.status;
13817
+ const networkFail = status === 0;
13818
+ const rawUrl = this.__talariaUrl ?? "";
13609
13819
  handleMeta({
13610
13820
  method: this.__talariaMethod ?? "GET",
13611
- url: this.__talariaUrl ?? "",
13612
- status: this.status,
13821
+ rawUrl,
13822
+ url: rawUrl,
13823
+ status,
13613
13824
  durationMs: Date.now() - (this.__talariaStarted ?? Date.now()),
13614
- ok: this.status >= 200 && this.status < 400
13825
+ ok: status >= 200 && status < 400,
13826
+ ...networkFail ? {
13827
+ failureKind: "network",
13828
+ errorName: "NetworkError",
13829
+ errorMessage: "XMLHttpRequest failed (status 0)"
13830
+ } : {}
13615
13831
  });
13616
13832
  };
13617
13833
  this.addEventListener("loadend", onDone);
@@ -13626,6 +13842,37 @@
13626
13842
  }
13627
13843
 
13628
13844
  // src/client.ts
13845
+ function networkFailureExtra(failure) {
13846
+ return {
13847
+ method: failure.method || "GET",
13848
+ url: failure.url || "(unknown url)",
13849
+ ...failure.hostname ? { hostname: failure.hostname } : {},
13850
+ ...failure.pathname ? { pathname: failure.pathname } : {},
13851
+ ...failure.search ? { search: failure.search } : {},
13852
+ ...typeof failure.status === "number" ? { status: failure.status } : {},
13853
+ durationMs: failure.durationMs,
13854
+ ok: failure.ok ?? false,
13855
+ failureKind: failure.failureKind ?? "network",
13856
+ aborted: failure.aborted ?? false,
13857
+ ...failure.errorName ? { errorName: failure.errorName } : {},
13858
+ ...failure.errorMessage ? { errorMessage: failure.errorMessage } : {}
13859
+ };
13860
+ }
13861
+ function mergeNetworkFailureContext(context, failure) {
13862
+ return {
13863
+ ...context,
13864
+ tags: {
13865
+ ...context?.tags ?? {},
13866
+ "http.method": failure.method || "GET",
13867
+ "network.failure_kind": "network",
13868
+ ...failure.errorName ? { "network.error_name": failure.errorName } : {}
13869
+ },
13870
+ extra: {
13871
+ ...context?.extra ?? {},
13872
+ network: networkFailureExtra(failure)
13873
+ }
13874
+ };
13875
+ }
13629
13876
  function resolveOptions(options) {
13630
13877
  const baseUrl = (options.baseUrl ?? options.dsn ?? "").trim();
13631
13878
  if (!baseUrl) {
@@ -13640,7 +13887,7 @@
13640
13887
  return {
13641
13888
  baseUrl,
13642
13889
  apiKey: options.apiKey.trim(),
13643
- environment: String(options.environment),
13890
+ environment: normalizeEnvironment(String(options.environment)),
13644
13891
  release: options.release,
13645
13892
  replaysSessionSampleRate: clamp01(options.replaysSessionSampleRate ?? 0),
13646
13893
  replaysOnErrorSampleRate: clamp01(options.replaysOnErrorSampleRate ?? 1),
@@ -13652,10 +13899,13 @@
13652
13899
  tags: options.tags,
13653
13900
  disableDefaultIntegrations: options.disableDefaultIntegrations ?? false,
13654
13901
  captureFailedRequests: options.captureFailedRequests ?? true,
13902
+ captureNetworkErrors: options.captureNetworkErrors ?? true,
13903
+ includeNetworkUrlQuery: options.includeNetworkUrlQuery ?? false,
13655
13904
  failedRequestStatusCodes: options.failedRequestStatusCodes ?? [[500, 599]],
13656
13905
  failedRequestIgnoreUrls: options.failedRequestIgnoreUrls ?? []
13657
13906
  };
13658
13907
  }
13908
+ var RECENT_NETWORK_FAILURE_MS = 5e3;
13659
13909
  function clamp01(n2) {
13660
13910
  if (Number.isNaN(n2)) return 0;
13661
13911
  return Math.min(1, Math.max(0, n2));
@@ -13680,6 +13930,16 @@
13680
13930
  const msg = error instanceof Error ? error.message : String(error);
13681
13931
  return msg.includes("segment exceeds max compressed size") || msg.includes("segment exceeds max uncompressed size") || msg.includes("HTTP 400") && msg.includes("ApiValidationException");
13682
13932
  }
13933
+ function isPermanentIngestError(error) {
13934
+ const msg = error instanceof Error ? error.message : String(error);
13935
+ const match = /Talaria events\/ingest(?:Batch)? failed: HTTP (\d{3})/.exec(
13936
+ msg
13937
+ );
13938
+ if (!match) return false;
13939
+ const status = Number(match[1]);
13940
+ if (status < 400 || status >= 500) return false;
13941
+ return status !== 408 && status !== 429;
13942
+ }
13683
13943
  var TalariaClient = class {
13684
13944
  constructor() {
13685
13945
  this.options = null;
@@ -13714,6 +13974,15 @@
13714
13974
  this.lastReplayCaptureFailure = null;
13715
13975
  /** Cached once per init for event tags/extra. */
13716
13976
  this.browserContext = null;
13977
+ /** Prevent ingest/replay failures from being re-captured via unhandledrejection. */
13978
+ this.capturing = false;
13979
+ /**
13980
+ * Set after a permanent events/ingest 4xx (bad env/auth/wire). Further
13981
+ * captures no-op so we don't spin on retries or burn error-clip replay quota.
13982
+ */
13983
+ this.ingestDisabled = false;
13984
+ /** Recent fetch/XHR transport failures for correlation + dedupe. */
13985
+ this.recentNetworkFailures = [];
13717
13986
  }
13718
13987
  init(options) {
13719
13988
  if (this.options) {
@@ -13728,6 +13997,7 @@
13728
13997
  this.sessionId = createId();
13729
13998
  this.replayId = createId();
13730
13999
  this.closed = false;
14000
+ this.ingestDisabled = false;
13731
14001
  this.finishedOnServer = false;
13732
14002
  this.startedOnServer = false;
13733
14003
  this.segmentIndex = 0;
@@ -13736,6 +14006,7 @@
13736
14006
  this.errorClipDeadlineMs = null;
13737
14007
  this.linkableReplayId = null;
13738
14008
  this.lastReplayCaptureFailure = null;
14009
+ this.recentNetworkFailures = [];
13739
14010
  this.browserContext = parseBrowserContext();
13740
14011
  void collectBrowserContext().then((ctx) => {
13741
14012
  if (this.options) this.browserContext = ctx;
@@ -13757,9 +14028,16 @@
13757
14028
  installConsoleHook(),
13758
14029
  installNetworkHook({
13759
14030
  captureFailedRequests: this.options.captureFailedRequests,
14031
+ captureNetworkErrors: this.options.captureNetworkErrors,
14032
+ includeNetworkUrlQuery: this.options.includeNetworkUrlQuery,
13760
14033
  failedRequestStatusCodes: this.options.failedRequestStatusCodes,
13761
14034
  failedRequestIgnoreUrls: this.options.failedRequestIgnoreUrls,
13762
14035
  talariaBaseUrl: this.options.baseUrl,
14036
+ onNetwork: (meta) => {
14037
+ if (meta.failureKind === "network" && !meta.aborted) {
14038
+ this.rememberNetworkFailure(meta, { promoted: false });
14039
+ }
14040
+ },
13763
14041
  onFailedRequest: (meta) => {
13764
14042
  const status = meta.status;
13765
14043
  const method = meta.method || "GET";
@@ -13770,15 +14048,40 @@
13770
14048
  {
13771
14049
  tags: {
13772
14050
  "http.status_code": String(status),
13773
- "http.method": method
14051
+ "http.method": method,
14052
+ "network.failure_kind": "http"
13774
14053
  },
13775
14054
  extra: {
13776
- url,
13777
- durationMs: meta.durationMs,
13778
- ok: meta.ok
14055
+ ...networkFailureExtra({
14056
+ ...meta,
14057
+ failureKind: "http",
14058
+ aborted: false
14059
+ })
13779
14060
  }
13780
14061
  }
13781
14062
  );
14063
+ },
14064
+ onNetworkError: (meta) => {
14065
+ this.rememberNetworkFailure(meta, { promoted: true });
14066
+ const method = meta.method || "GET";
14067
+ const url = meta.url || "(unknown url)";
14068
+ const errLabel = meta.errorName && meta.errorMessage ? `${meta.errorName}: ${meta.errorMessage}` : meta.errorMessage || meta.errorName || "Failed to fetch";
14069
+ void this.captureMessage(
14070
+ `Network error: ${method} ${url} \u2014 ${errLabel}`,
14071
+ "error",
14072
+ {
14073
+ tags: {
14074
+ "http.method": method,
14075
+ "network.failure_kind": "network",
14076
+ ...meta.errorName ? { "network.error_name": meta.errorName } : {}
14077
+ },
14078
+ extra: networkFailureExtra({
14079
+ ...meta,
14080
+ failureKind: "network",
14081
+ aborted: false
14082
+ })
14083
+ }
14084
+ );
13782
14085
  }
13783
14086
  }),
13784
14087
  installVisibilityResumeHook(() => this.onForegroundResume())
@@ -13812,24 +14115,38 @@
13812
14115
  return this.linkableReplayId;
13813
14116
  }
13814
14117
  async captureException(error, context) {
14118
+ if (this.capturing || this.ingestDisabled) return;
13815
14119
  const err = error instanceof Error ? error : new Error(typeof error === "string" ? error : "Unknown error");
14120
+ if (isAbortError(err)) return;
13816
14121
  const filename = typeof context?.extra?.filename === "string" ? context.extra.filename : void 0;
13817
14122
  if (isBrowserExtensionNoise({
13818
14123
  message: err.message,
13819
14124
  stack: err.stack,
13820
14125
  filename
14126
+ }) || isSdkInternalNoise({
14127
+ message: err.message,
14128
+ stack: err.stack
13821
14129
  })) {
13822
14130
  return;
13823
14131
  }
13824
- await this.capture({
13825
- message: err.message || err.name || "Error",
13826
- level: "error",
13827
- title: err.name || "Error",
13828
- stackTrace: err.stack,
13829
- context
13830
- });
14132
+ const correlated = this.consumeCorrelatedNetworkFailure(err);
14133
+ if (correlated?.promoted) return;
14134
+ const mergedContext = correlated ? mergeNetworkFailureContext(context, correlated) : context;
14135
+ this.capturing = true;
14136
+ try {
14137
+ await this.capture({
14138
+ message: err.message || err.name || "Error",
14139
+ level: "error",
14140
+ title: err.name || "Error",
14141
+ stackTrace: err.stack,
14142
+ context: mergedContext
14143
+ });
14144
+ } finally {
14145
+ this.capturing = false;
14146
+ }
13831
14147
  }
13832
14148
  async captureMessage(message, level = "info", context) {
14149
+ if (this.ingestDisabled) return;
13833
14150
  await this.capture({
13834
14151
  message,
13835
14152
  level,
@@ -13915,6 +14232,7 @@
13915
14232
  this.linkableReplayId = null;
13916
14233
  this.lastReplayCaptureFailure = null;
13917
14234
  this.browserContext = null;
14235
+ this.capturing = false;
13918
14236
  }
13919
14237
  onRrwebEvent(event) {
13920
14238
  if (this.closed || !this.options) return;
@@ -13939,6 +14257,8 @@
13939
14257
  if (!this.options || !this.transport) {
13940
14258
  throw new Error("@newtalaria/browser: call Talaria.init() first");
13941
14259
  }
14260
+ if (this.ingestDisabled) return;
14261
+ const occurredAt = /* @__PURE__ */ new Date();
13942
14262
  const isErrorLike = args.level === "error" || args.level === "fatal";
13943
14263
  let errorClipOutcome = null;
13944
14264
  let attemptedErrorClip = false;
@@ -13996,6 +14316,7 @@
13996
14316
  errorClipOutcome = { status: "failed", reason: "buffer_empty" };
13997
14317
  }
13998
14318
  }
14319
+ const queuedMs = Math.max(0, Date.now() - occurredAt.getTime());
13999
14320
  const tags = applyReplayCaptureTags(
14000
14321
  {
14001
14322
  ...this.browserContext ? browserContextTags(this.browserContext) : {},
@@ -14017,7 +14338,12 @@
14017
14338
  userAgent: this.browserContext.userAgent
14018
14339
  }
14019
14340
  } : {},
14020
- sdk: { name: SDK_NAME, version: SDK_VERSION },
14341
+ sdk: {
14342
+ name: SDK_NAME,
14343
+ version: SDK_VERSION,
14344
+ // Time spent in capture() before ingest (mostly replay flush).
14345
+ ...queuedMs >= 50 ? { queuedMs } : {}
14346
+ },
14021
14347
  ...args.context?.extra
14022
14348
  },
14023
14349
  errorClipOutcome
@@ -14037,7 +14363,7 @@
14037
14363
  url: typeof location !== "undefined" ? location.href : void 0,
14038
14364
  tags: Object.keys(tags).length ? tags : void 0,
14039
14365
  extraJson: extra ? JSON.stringify(extra) : void 0,
14040
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
14366
+ timestamp: occurredAt.toISOString(),
14041
14367
  keepalive: args.keepalive
14042
14368
  });
14043
14369
  if (replayId && replayId === this.linkableReplayId) {
@@ -14045,7 +14371,36 @@
14045
14371
  }
14046
14372
  } catch (error) {
14047
14373
  console.warn("@newtalaria/browser: event ingest failed", error);
14048
- throw error;
14374
+ if (isPermanentIngestError(error)) {
14375
+ this.disableIngestAfterPermanentError(error);
14376
+ }
14377
+ }
14378
+ }
14379
+ /**
14380
+ * Stop further event capture (and error-clip uploads) after a permanent
14381
+ * events/ingest 4xx so misconfig cannot spin forever.
14382
+ */
14383
+ disableIngestAfterPermanentError(error) {
14384
+ if (this.ingestDisabled) return;
14385
+ this.ingestDisabled = true;
14386
+ console.warn(
14387
+ "@newtalaria/browser: event ingest disabled after permanent client error",
14388
+ error
14389
+ );
14390
+ if (!this.sessionSampled && this.uploadEnabled) {
14391
+ this.clearErrorClipTimer();
14392
+ void this.enqueueUpload(async () => {
14393
+ if (this.startedOnServer && !this.finishedOnServer) {
14394
+ await this.finishOnServer({
14395
+ keepalive: false,
14396
+ reason: "ingest_disabled"
14397
+ });
14398
+ }
14399
+ this.resetToBufferMode();
14400
+ });
14401
+ } else if (!this.sessionSampled) {
14402
+ this.clearErrorClipTimer();
14403
+ this.uploadEnabled = false;
14049
14404
  }
14050
14405
  }
14051
14406
  markUploadStarted() {
@@ -14250,6 +14605,57 @@
14250
14605
  this.resetToBufferMode();
14251
14606
  }
14252
14607
  }
14608
+ pruneRecentNetworkFailures(now = Date.now()) {
14609
+ this.recentNetworkFailures = this.recentNetworkFailures.filter(
14610
+ (f) => now - f.at <= RECENT_NETWORK_FAILURE_MS
14611
+ );
14612
+ }
14613
+ rememberNetworkFailure(meta, opts) {
14614
+ const now = Date.now();
14615
+ this.pruneRecentNetworkFailures(now);
14616
+ for (let i = this.recentNetworkFailures.length - 1; i >= 0; i--) {
14617
+ const existing = this.recentNetworkFailures[i];
14618
+ if (existing.method === meta.method && existing.url === meta.url && existing.errorMessage === meta.errorMessage && now - existing.at < 1e3) {
14619
+ existing.promoted = existing.promoted || opts.promoted;
14620
+ existing.durationMs = meta.durationMs ?? existing.durationMs;
14621
+ existing.errorName = meta.errorName ?? existing.errorName;
14622
+ existing.aborted = meta.aborted ?? existing.aborted;
14623
+ existing.failureKind = meta.failureKind ?? existing.failureKind;
14624
+ return;
14625
+ }
14626
+ }
14627
+ this.recentNetworkFailures.push({
14628
+ ...meta,
14629
+ at: now,
14630
+ promoted: opts.promoted
14631
+ });
14632
+ }
14633
+ /**
14634
+ * Find a recent transport failure for a bare fetch TypeError.
14635
+ * Consumes the entry so a later duplicate path cannot reuse it.
14636
+ */
14637
+ consumeCorrelatedNetworkFailure(error) {
14638
+ if (!isLikelyNetworkFetchError(error)) return null;
14639
+ const now = Date.now();
14640
+ this.pruneRecentNetworkFailures(now);
14641
+ const takeAt = (index2) => {
14642
+ const [failure] = this.recentNetworkFailures.splice(index2, 1);
14643
+ return failure;
14644
+ };
14645
+ for (let i = this.recentNetworkFailures.length - 1; i >= 0; i--) {
14646
+ const failure = this.recentNetworkFailures[i];
14647
+ if (failure.aborted || failure.failureKind !== "network") continue;
14648
+ if (failure.errorMessage && failure.errorMessage === error.message) {
14649
+ return takeAt(i);
14650
+ }
14651
+ }
14652
+ for (let i = this.recentNetworkFailures.length - 1; i >= 0; i--) {
14653
+ const failure = this.recentNetworkFailures[i];
14654
+ if (failure.aborted || failure.failureKind !== "network") continue;
14655
+ return takeAt(i);
14656
+ }
14657
+ return null;
14658
+ }
14253
14659
  installGlobalHandlers() {
14254
14660
  if (typeof window === "undefined") return;
14255
14661
  const onError = (event) => {
@@ -14271,7 +14677,21 @@
14271
14677
  });
14272
14678
  };
14273
14679
  const onRejection = (event) => {
14274
- void this.captureException(event.reason);
14680
+ const reason = event.reason;
14681
+ if (isAbortError(reason)) return;
14682
+ const err = reason instanceof Error ? reason : new Error(
14683
+ typeof reason === "string" ? reason : "unhandledrejection"
14684
+ );
14685
+ if (isBrowserExtensionNoise({
14686
+ message: err.message,
14687
+ stack: err.stack
14688
+ }) || isSdkInternalNoise({
14689
+ message: err.message,
14690
+ stack: err.stack
14691
+ })) {
14692
+ return;
14693
+ }
14694
+ void this.captureException(reason);
14275
14695
  };
14276
14696
  window.addEventListener("error", onError);
14277
14697
  window.addEventListener("unhandledrejection", onRejection);