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