@newtalaria/browser 0.1.13 → 0.1.15

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.15";
171
230
 
172
231
  // src/transport/serverpod.ts
173
232
  var ServerpodTransport = class {
@@ -13447,6 +13506,32 @@ function startRecorder(options) {
13447
13506
  };
13448
13507
  }
13449
13508
 
13509
+ // src/utils/network_error.ts
13510
+ function isAbortError(error) {
13511
+ return error instanceof Error && error.name === "AbortError";
13512
+ }
13513
+ function isLikelyNetworkFetchError(error) {
13514
+ if (!(error instanceof Error)) return false;
13515
+ if (error.name === "AbortError") return false;
13516
+ if (error.name === "NetworkError") return true;
13517
+ const msg = error.message.toLowerCase().trim();
13518
+ return msg === "failed to fetch" || msg === "load failed" || msg === "networkerror when attempting to fetch resource." || msg.startsWith("networkerror") || msg.includes("failed to fetch");
13519
+ }
13520
+ function describeUnknownError(error) {
13521
+ if (error instanceof Error) {
13522
+ return {
13523
+ errorName: error.name || "Error",
13524
+ errorMessage: (error.message || "").slice(0, 500),
13525
+ aborted: error.name === "AbortError"
13526
+ };
13527
+ }
13528
+ return {
13529
+ errorName: "Error",
13530
+ errorMessage: String(error).slice(0, 500),
13531
+ aborted: false
13532
+ };
13533
+ }
13534
+
13450
13535
  // src/replay/hooks.ts
13451
13536
  function installVisibilityResumeHook(onResume) {
13452
13537
  if (typeof document === "undefined") {
@@ -13474,20 +13559,40 @@ function installVisibilityResumeHook(onResume) {
13474
13559
  }
13475
13560
  };
13476
13561
  }
13562
+ function serializeConsoleArg(arg) {
13563
+ if (arg instanceof Error) {
13564
+ return { name: arg.name, message: arg.message, stack: arg.stack };
13565
+ }
13566
+ return arg;
13567
+ }
13568
+ function formatConsoleArg(arg) {
13569
+ if (arg == null) return String(arg);
13570
+ if (typeof arg === "string") return arg;
13571
+ if (typeof arg === "number" || typeof arg === "boolean") return String(arg);
13572
+ if (arg instanceof Error) {
13573
+ return arg.stack || `${arg.name}: ${arg.message}`;
13574
+ }
13575
+ if (typeof arg === "object" && arg !== null && "name" in arg && "message" in arg) {
13576
+ const err = arg;
13577
+ if (typeof err.stack === "string" && err.stack) return err.stack;
13578
+ return `${String(err.name)}: ${String(err.message)}`;
13579
+ }
13580
+ try {
13581
+ return JSON.stringify(arg);
13582
+ } catch {
13583
+ return String(arg);
13584
+ }
13585
+ }
13477
13586
  function safeSerialize(args) {
13478
13587
  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
- );
13588
+ return JSON.stringify(args.map(serializeConsoleArg));
13487
13589
  } catch {
13488
13590
  return "[unserializable]";
13489
13591
  }
13490
13592
  }
13593
+ function consoleMessage(args) {
13594
+ return args.map(formatConsoleArg).join(" ").slice(0, 4e3);
13595
+ }
13491
13596
  function installConsoleHook() {
13492
13597
  const levels = ["log", "info", "warn", "error", "debug"];
13493
13598
  const originals = {};
@@ -13496,9 +13601,12 @@ function installConsoleHook() {
13496
13601
  originals[level] = original;
13497
13602
  console[level] = (...args) => {
13498
13603
  try {
13604
+ const serializedArgs = safeSerialize(args).slice(0, 4e3);
13499
13605
  record.addCustomEvent("talaria-console", {
13500
13606
  level,
13501
- args: safeSerialize(args).slice(0, 4e3),
13607
+ // `message` is what the replay sidebar displays; `args` kept for tooling.
13608
+ message: consoleMessage(args),
13609
+ args: serializedArgs,
13502
13610
  timestamp: Date.now()
13503
13611
  });
13504
13612
  } catch {
@@ -13534,23 +13642,61 @@ function statusMatches(status, codes) {
13534
13642
  }
13535
13643
  return false;
13536
13644
  }
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(/\/+$/, "");
13645
+ function buildFailedRequestIgnoreUrls(failedRequestIgnoreUrls, talariaBaseUrl) {
13646
+ const ignore = [...failedRequestIgnoreUrls];
13647
+ const base = (talariaBaseUrl ?? "").replace(/\/+$/, "");
13544
13648
  if (base) {
13545
13649
  ignore.push(`${base}/events/`, `${base}/replays/`);
13546
13650
  }
13547
13651
  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;
13652
+ return ignore;
13653
+ }
13654
+ function urlMatchesIgnoreList(url, ignoreUrls) {
13655
+ const lower = (url || "").toLowerCase();
13656
+ for (const part of ignoreUrls) {
13657
+ if (part && lower.includes(part.toLowerCase())) return true;
13551
13658
  }
13659
+ return false;
13660
+ }
13661
+ function shouldPromoteFailedRequest(meta, opts) {
13662
+ if (!opts.captureFailedRequests) return false;
13663
+ if (typeof meta.status !== "number" || Number.isNaN(meta.status)) return false;
13664
+ if (meta.status === 0) return false;
13665
+ if (!statusMatches(meta.status, opts.failedRequestStatusCodes)) return false;
13666
+ const ignore = buildFailedRequestIgnoreUrls(
13667
+ opts.failedRequestIgnoreUrls,
13668
+ opts.talariaBaseUrl
13669
+ );
13670
+ if (urlMatchesIgnoreList(meta.url || "", ignore)) return false;
13671
+ return true;
13672
+ }
13673
+ function shouldPromoteNetworkError(meta, opts) {
13674
+ if (!opts.captureNetworkErrors) return false;
13675
+ if (meta.aborted) return false;
13676
+ if (meta.failureKind !== "network") return false;
13677
+ const ignore = buildFailedRequestIgnoreUrls(
13678
+ opts.failedRequestIgnoreUrls,
13679
+ opts.talariaBaseUrl
13680
+ );
13681
+ if (urlMatchesIgnoreList(meta.url || "", ignore)) return false;
13552
13682
  return true;
13553
13683
  }
13684
+ function enrichNetworkMeta(meta) {
13685
+ const hasHttpStatus = typeof meta.status === "number" && !Number.isNaN(meta.status) && meta.status > 0;
13686
+ if (hasHttpStatus) {
13687
+ return {
13688
+ ...meta,
13689
+ failureKind: meta.ok === false ? "http" : meta.failureKind
13690
+ };
13691
+ }
13692
+ if (meta.ok === false || meta.errorName || meta.errorMessage) {
13693
+ return {
13694
+ ...meta,
13695
+ failureKind: "network"
13696
+ };
13697
+ }
13698
+ return meta;
13699
+ }
13554
13700
  function installNetworkHook(options = {}) {
13555
13701
  const originalFetch = typeof fetch === "function" ? fetch.bind(globalThis) : null;
13556
13702
  const XHR = typeof XMLHttpRequest !== "undefined" ? XMLHttpRequest : null;
@@ -13558,15 +13704,22 @@ function installNetworkHook(options = {}) {
13558
13704
  const originalSend = XHR?.prototype.send;
13559
13705
  const matchOpts = {
13560
13706
  captureFailedRequests: options.captureFailedRequests ?? true,
13707
+ captureNetworkErrors: options.captureNetworkErrors ?? true,
13561
13708
  failedRequestStatusCodes: options.failedRequestStatusCodes ?? [[500, 599]],
13562
13709
  failedRequestIgnoreUrls: options.failedRequestIgnoreUrls ?? [],
13563
13710
  talariaBaseUrl: options.talariaBaseUrl
13564
13711
  };
13565
13712
  const handleMeta = (meta) => {
13566
- emitNetwork(meta);
13567
- options.onNetwork?.(meta);
13568
- if (shouldPromoteFailedRequest(meta, matchOpts)) {
13569
- options.onFailedRequest?.(meta);
13713
+ const enriched = enrichNetworkMeta(meta);
13714
+ try {
13715
+ emitNetwork(enriched);
13716
+ options.onNetwork?.(enriched);
13717
+ if (shouldPromoteFailedRequest(enriched, matchOpts)) {
13718
+ options.onFailedRequest?.(enriched);
13719
+ } else if (shouldPromoteNetworkError(enriched, matchOpts)) {
13720
+ options.onNetworkError?.(enriched);
13721
+ }
13722
+ } catch {
13570
13723
  }
13571
13724
  };
13572
13725
  if (originalFetch) {
@@ -13585,11 +13738,16 @@ function installNetworkHook(options = {}) {
13585
13738
  });
13586
13739
  return response;
13587
13740
  } catch (error) {
13741
+ const described = describeUnknownError(error);
13588
13742
  handleMeta({
13589
13743
  method,
13590
13744
  url,
13591
13745
  durationMs: Date.now() - started,
13592
- ok: false
13746
+ ok: false,
13747
+ errorName: described.errorName,
13748
+ errorMessage: described.errorMessage,
13749
+ aborted: described.aborted,
13750
+ failureKind: "network"
13593
13751
  });
13594
13752
  throw error;
13595
13753
  }
@@ -13604,12 +13762,19 @@ function installNetworkHook(options = {}) {
13604
13762
  XHR.prototype.send = function(body) {
13605
13763
  this.__talariaStarted = Date.now();
13606
13764
  const onDone = () => {
13765
+ const status = this.status;
13766
+ const networkFail = status === 0;
13607
13767
  handleMeta({
13608
13768
  method: this.__talariaMethod ?? "GET",
13609
13769
  url: this.__talariaUrl ?? "",
13610
- status: this.status,
13770
+ status,
13611
13771
  durationMs: Date.now() - (this.__talariaStarted ?? Date.now()),
13612
- ok: this.status >= 200 && this.status < 400
13772
+ ok: status >= 200 && status < 400,
13773
+ ...networkFail ? {
13774
+ failureKind: "network",
13775
+ errorName: "NetworkError",
13776
+ errorMessage: "XMLHttpRequest failed (status 0)"
13777
+ } : {}
13613
13778
  });
13614
13779
  };
13615
13780
  this.addEventListener("loadend", onDone);
@@ -13624,6 +13789,31 @@ function installNetworkHook(options = {}) {
13624
13789
  }
13625
13790
 
13626
13791
  // src/client.ts
13792
+ function mergeNetworkFailureContext(context, failure) {
13793
+ const url = redactUrl(failure.url || "(unknown url)");
13794
+ return {
13795
+ ...context,
13796
+ tags: {
13797
+ ...context?.tags ?? {},
13798
+ "http.method": failure.method || "GET",
13799
+ "network.failure_kind": "network",
13800
+ ...failure.errorName ? { "network.error_name": failure.errorName } : {}
13801
+ },
13802
+ extra: {
13803
+ ...context?.extra ?? {},
13804
+ network: {
13805
+ method: failure.method,
13806
+ url,
13807
+ durationMs: failure.durationMs,
13808
+ ok: false,
13809
+ failureKind: "network",
13810
+ errorName: failure.errorName,
13811
+ errorMessage: failure.errorMessage,
13812
+ aborted: failure.aborted ?? false
13813
+ }
13814
+ }
13815
+ };
13816
+ }
13627
13817
  function resolveOptions(options) {
13628
13818
  const baseUrl = (options.baseUrl ?? options.dsn ?? "").trim();
13629
13819
  if (!baseUrl) {
@@ -13638,7 +13828,7 @@ function resolveOptions(options) {
13638
13828
  return {
13639
13829
  baseUrl,
13640
13830
  apiKey: options.apiKey.trim(),
13641
- environment: String(options.environment),
13831
+ environment: normalizeEnvironment(String(options.environment)),
13642
13832
  release: options.release,
13643
13833
  replaysSessionSampleRate: clamp01(options.replaysSessionSampleRate ?? 0),
13644
13834
  replaysOnErrorSampleRate: clamp01(options.replaysOnErrorSampleRate ?? 1),
@@ -13650,10 +13840,12 @@ function resolveOptions(options) {
13650
13840
  tags: options.tags,
13651
13841
  disableDefaultIntegrations: options.disableDefaultIntegrations ?? false,
13652
13842
  captureFailedRequests: options.captureFailedRequests ?? true,
13843
+ captureNetworkErrors: options.captureNetworkErrors ?? true,
13653
13844
  failedRequestStatusCodes: options.failedRequestStatusCodes ?? [[500, 599]],
13654
13845
  failedRequestIgnoreUrls: options.failedRequestIgnoreUrls ?? []
13655
13846
  };
13656
13847
  }
13848
+ var RECENT_NETWORK_FAILURE_MS = 5e3;
13657
13849
  function clamp01(n2) {
13658
13850
  if (Number.isNaN(n2)) return 0;
13659
13851
  return Math.min(1, Math.max(0, n2));
@@ -13678,6 +13870,16 @@ function isOversizedSegmentError(error) {
13678
13870
  const msg = error instanceof Error ? error.message : String(error);
13679
13871
  return msg.includes("segment exceeds max compressed size") || msg.includes("segment exceeds max uncompressed size") || msg.includes("HTTP 400") && msg.includes("ApiValidationException");
13680
13872
  }
13873
+ function isPermanentIngestError(error) {
13874
+ const msg = error instanceof Error ? error.message : String(error);
13875
+ const match = /Talaria events\/ingest(?:Batch)? failed: HTTP (\d{3})/.exec(
13876
+ msg
13877
+ );
13878
+ if (!match) return false;
13879
+ const status = Number(match[1]);
13880
+ if (status < 400 || status >= 500) return false;
13881
+ return status !== 408 && status !== 429;
13882
+ }
13681
13883
  var TalariaClient = class {
13682
13884
  constructor() {
13683
13885
  this.options = null;
@@ -13712,6 +13914,15 @@ var TalariaClient = class {
13712
13914
  this.lastReplayCaptureFailure = null;
13713
13915
  /** Cached once per init for event tags/extra. */
13714
13916
  this.browserContext = null;
13917
+ /** Prevent ingest/replay failures from being re-captured via unhandledrejection. */
13918
+ this.capturing = false;
13919
+ /**
13920
+ * Set after a permanent events/ingest 4xx (bad env/auth/wire). Further
13921
+ * captures no-op so we don't spin on retries or burn error-clip replay quota.
13922
+ */
13923
+ this.ingestDisabled = false;
13924
+ /** Recent fetch/XHR transport failures for correlation + dedupe. */
13925
+ this.recentNetworkFailures = [];
13715
13926
  }
13716
13927
  init(options) {
13717
13928
  if (this.options) {
@@ -13726,6 +13937,7 @@ var TalariaClient = class {
13726
13937
  this.sessionId = createId();
13727
13938
  this.replayId = createId();
13728
13939
  this.closed = false;
13940
+ this.ingestDisabled = false;
13729
13941
  this.finishedOnServer = false;
13730
13942
  this.startedOnServer = false;
13731
13943
  this.segmentIndex = 0;
@@ -13734,6 +13946,7 @@ var TalariaClient = class {
13734
13946
  this.errorClipDeadlineMs = null;
13735
13947
  this.linkableReplayId = null;
13736
13948
  this.lastReplayCaptureFailure = null;
13949
+ this.recentNetworkFailures = [];
13737
13950
  this.browserContext = parseBrowserContext();
13738
13951
  void collectBrowserContext().then((ctx) => {
13739
13952
  if (this.options) this.browserContext = ctx;
@@ -13755,25 +13968,59 @@ var TalariaClient = class {
13755
13968
  installConsoleHook(),
13756
13969
  installNetworkHook({
13757
13970
  captureFailedRequests: this.options.captureFailedRequests,
13971
+ captureNetworkErrors: this.options.captureNetworkErrors,
13758
13972
  failedRequestStatusCodes: this.options.failedRequestStatusCodes,
13759
13973
  failedRequestIgnoreUrls: this.options.failedRequestIgnoreUrls,
13760
13974
  talariaBaseUrl: this.options.baseUrl,
13975
+ onNetwork: (meta) => {
13976
+ if (meta.failureKind === "network" && !meta.aborted) {
13977
+ this.rememberNetworkFailure(meta, { promoted: false });
13978
+ }
13979
+ },
13761
13980
  onFailedRequest: (meta) => {
13762
13981
  const status = meta.status;
13763
13982
  const method = meta.method || "GET";
13764
- const url = meta.url || "(unknown url)";
13983
+ const url = redactUrl(meta.url || "(unknown url)");
13765
13984
  void this.captureMessage(
13766
13985
  `HTTP ${status}: ${method} ${url}`,
13767
13986
  status >= 500 ? "error" : "warning",
13768
13987
  {
13769
13988
  tags: {
13770
13989
  "http.status_code": String(status),
13771
- "http.method": method
13990
+ "http.method": method,
13991
+ "network.failure_kind": "http"
13772
13992
  },
13773
13993
  extra: {
13774
13994
  url,
13775
13995
  durationMs: meta.durationMs,
13776
- ok: meta.ok
13996
+ ok: meta.ok,
13997
+ failureKind: "http"
13998
+ }
13999
+ }
14000
+ );
14001
+ },
14002
+ onNetworkError: (meta) => {
14003
+ this.rememberNetworkFailure(meta, { promoted: true });
14004
+ const method = meta.method || "GET";
14005
+ const url = redactUrl(meta.url || "(unknown url)");
14006
+ const errLabel = meta.errorName && meta.errorMessage ? `${meta.errorName}: ${meta.errorMessage}` : meta.errorMessage || meta.errorName || "Failed to fetch";
14007
+ void this.captureMessage(
14008
+ `Network error: ${method} ${url} \u2014 ${errLabel}`,
14009
+ "error",
14010
+ {
14011
+ tags: {
14012
+ "http.method": method,
14013
+ "network.failure_kind": "network",
14014
+ ...meta.errorName ? { "network.error_name": meta.errorName } : {}
14015
+ },
14016
+ extra: {
14017
+ url,
14018
+ durationMs: meta.durationMs,
14019
+ ok: false,
14020
+ failureKind: "network",
14021
+ errorName: meta.errorName,
14022
+ errorMessage: meta.errorMessage,
14023
+ aborted: false
13777
14024
  }
13778
14025
  }
13779
14026
  );
@@ -13810,24 +14057,38 @@ var TalariaClient = class {
13810
14057
  return this.linkableReplayId;
13811
14058
  }
13812
14059
  async captureException(error, context) {
14060
+ if (this.capturing || this.ingestDisabled) return;
13813
14061
  const err = error instanceof Error ? error : new Error(typeof error === "string" ? error : "Unknown error");
14062
+ if (isAbortError(err)) return;
13814
14063
  const filename = typeof context?.extra?.filename === "string" ? context.extra.filename : void 0;
13815
14064
  if (isBrowserExtensionNoise({
13816
14065
  message: err.message,
13817
14066
  stack: err.stack,
13818
14067
  filename
14068
+ }) || isSdkInternalNoise({
14069
+ message: err.message,
14070
+ stack: err.stack
13819
14071
  })) {
13820
14072
  return;
13821
14073
  }
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
- });
14074
+ const correlated = this.consumeCorrelatedNetworkFailure(err);
14075
+ if (correlated?.promoted) return;
14076
+ const mergedContext = correlated ? mergeNetworkFailureContext(context, correlated) : context;
14077
+ this.capturing = true;
14078
+ try {
14079
+ await this.capture({
14080
+ message: err.message || err.name || "Error",
14081
+ level: "error",
14082
+ title: err.name || "Error",
14083
+ stackTrace: err.stack,
14084
+ context: mergedContext
14085
+ });
14086
+ } finally {
14087
+ this.capturing = false;
14088
+ }
13829
14089
  }
13830
14090
  async captureMessage(message, level = "info", context) {
14091
+ if (this.ingestDisabled) return;
13831
14092
  await this.capture({
13832
14093
  message,
13833
14094
  level,
@@ -13913,6 +14174,7 @@ var TalariaClient = class {
13913
14174
  this.linkableReplayId = null;
13914
14175
  this.lastReplayCaptureFailure = null;
13915
14176
  this.browserContext = null;
14177
+ this.capturing = false;
13916
14178
  }
13917
14179
  onRrwebEvent(event) {
13918
14180
  if (this.closed || !this.options) return;
@@ -13937,6 +14199,7 @@ var TalariaClient = class {
13937
14199
  if (!this.options || !this.transport) {
13938
14200
  throw new Error("@newtalaria/browser: call Talaria.init() first");
13939
14201
  }
14202
+ if (this.ingestDisabled) return;
13940
14203
  const isErrorLike = args.level === "error" || args.level === "fatal";
13941
14204
  let errorClipOutcome = null;
13942
14205
  let attemptedErrorClip = false;
@@ -14043,7 +14306,36 @@ var TalariaClient = class {
14043
14306
  }
14044
14307
  } catch (error) {
14045
14308
  console.warn("@newtalaria/browser: event ingest failed", error);
14046
- throw error;
14309
+ if (isPermanentIngestError(error)) {
14310
+ this.disableIngestAfterPermanentError(error);
14311
+ }
14312
+ }
14313
+ }
14314
+ /**
14315
+ * Stop further event capture (and error-clip uploads) after a permanent
14316
+ * events/ingest 4xx so misconfig cannot spin forever.
14317
+ */
14318
+ disableIngestAfterPermanentError(error) {
14319
+ if (this.ingestDisabled) return;
14320
+ this.ingestDisabled = true;
14321
+ console.warn(
14322
+ "@newtalaria/browser: event ingest disabled after permanent client error",
14323
+ error
14324
+ );
14325
+ if (!this.sessionSampled && this.uploadEnabled) {
14326
+ this.clearErrorClipTimer();
14327
+ void this.enqueueUpload(async () => {
14328
+ if (this.startedOnServer && !this.finishedOnServer) {
14329
+ await this.finishOnServer({
14330
+ keepalive: false,
14331
+ reason: "ingest_disabled"
14332
+ });
14333
+ }
14334
+ this.resetToBufferMode();
14335
+ });
14336
+ } else if (!this.sessionSampled) {
14337
+ this.clearErrorClipTimer();
14338
+ this.uploadEnabled = false;
14047
14339
  }
14048
14340
  }
14049
14341
  markUploadStarted() {
@@ -14248,6 +14540,57 @@ var TalariaClient = class {
14248
14540
  this.resetToBufferMode();
14249
14541
  }
14250
14542
  }
14543
+ pruneRecentNetworkFailures(now = Date.now()) {
14544
+ this.recentNetworkFailures = this.recentNetworkFailures.filter(
14545
+ (f) => now - f.at <= RECENT_NETWORK_FAILURE_MS
14546
+ );
14547
+ }
14548
+ rememberNetworkFailure(meta, opts) {
14549
+ const now = Date.now();
14550
+ this.pruneRecentNetworkFailures(now);
14551
+ for (let i = this.recentNetworkFailures.length - 1; i >= 0; i--) {
14552
+ const existing = this.recentNetworkFailures[i];
14553
+ if (existing.method === meta.method && existing.url === meta.url && existing.errorMessage === meta.errorMessage && now - existing.at < 1e3) {
14554
+ existing.promoted = existing.promoted || opts.promoted;
14555
+ existing.durationMs = meta.durationMs ?? existing.durationMs;
14556
+ existing.errorName = meta.errorName ?? existing.errorName;
14557
+ existing.aborted = meta.aborted ?? existing.aborted;
14558
+ existing.failureKind = meta.failureKind ?? existing.failureKind;
14559
+ return;
14560
+ }
14561
+ }
14562
+ this.recentNetworkFailures.push({
14563
+ ...meta,
14564
+ at: now,
14565
+ promoted: opts.promoted
14566
+ });
14567
+ }
14568
+ /**
14569
+ * Find a recent transport failure for a bare fetch TypeError.
14570
+ * Consumes the entry so a later duplicate path cannot reuse it.
14571
+ */
14572
+ consumeCorrelatedNetworkFailure(error) {
14573
+ if (!isLikelyNetworkFetchError(error)) return null;
14574
+ const now = Date.now();
14575
+ this.pruneRecentNetworkFailures(now);
14576
+ const takeAt = (index2) => {
14577
+ const [failure] = this.recentNetworkFailures.splice(index2, 1);
14578
+ return failure;
14579
+ };
14580
+ for (let i = this.recentNetworkFailures.length - 1; i >= 0; i--) {
14581
+ const failure = this.recentNetworkFailures[i];
14582
+ if (failure.aborted || failure.failureKind !== "network") continue;
14583
+ if (failure.errorMessage && failure.errorMessage === error.message) {
14584
+ return takeAt(i);
14585
+ }
14586
+ }
14587
+ for (let i = this.recentNetworkFailures.length - 1; i >= 0; i--) {
14588
+ const failure = this.recentNetworkFailures[i];
14589
+ if (failure.aborted || failure.failureKind !== "network") continue;
14590
+ return takeAt(i);
14591
+ }
14592
+ return null;
14593
+ }
14251
14594
  installGlobalHandlers() {
14252
14595
  if (typeof window === "undefined") return;
14253
14596
  const onError = (event) => {
@@ -14269,7 +14612,21 @@ var TalariaClient = class {
14269
14612
  });
14270
14613
  };
14271
14614
  const onRejection = (event) => {
14272
- void this.captureException(event.reason);
14615
+ const reason = event.reason;
14616
+ if (isAbortError(reason)) return;
14617
+ const err = reason instanceof Error ? reason : new Error(
14618
+ typeof reason === "string" ? reason : "unhandledrejection"
14619
+ );
14620
+ if (isBrowserExtensionNoise({
14621
+ message: err.message,
14622
+ stack: err.stack
14623
+ }) || isSdkInternalNoise({
14624
+ message: err.message,
14625
+ stack: err.stack
14626
+ })) {
14627
+ return;
14628
+ }
14629
+ void this.captureException(reason);
14273
14630
  };
14274
14631
  window.addEventListener("error", onError);
14275
14632
  window.addEventListener("unhandledrejection", onRejection);