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