@newtalaria/browser 0.1.12 → 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.12";
229
+ var SDK_VERSION = "0.1.15";
171
230
 
172
231
  // src/transport/serverpod.ts
173
232
  var ServerpodTransport = class {
@@ -284,6 +343,7 @@ var MAX_ERROR_CLIP_COMPRESSED_BYTES = 2 * 1024 * 1024;
284
343
  var ERROR_CLIP_FLUSH_SLACK_MS = 5e3;
285
344
  var SEGMENT_FLUSH_MS = 5e3;
286
345
  var RING_BUFFER_MS = 6e4;
346
+ var RING_BUFFER_CHECKOUT_MS = RING_BUFFER_MS;
287
347
  var RING_BUFFER_MAX_BYTES = 15e5;
288
348
  var ERROR_REPLAY_AFTER_MS = 15e3;
289
349
  var MAX_REPLAY_DURATION_MS = 5 * 60 * 1e3;
@@ -13426,6 +13486,7 @@ function startRecorder(options) {
13426
13486
  inlineStylesheet: options.inlineStylesheet,
13427
13487
  // Shrink DOM snapshots on heavy sites (docs / marketing).
13428
13488
  slimDOMOptions: "all",
13489
+ ...options.checkoutEveryNms != null && options.checkoutEveryNms > 0 ? { checkoutEveryNms: options.checkoutEveryNms } : {},
13429
13490
  sampling: {
13430
13491
  mousemove: 150,
13431
13492
  scroll: 200,
@@ -13445,21 +13506,93 @@ function startRecorder(options) {
13445
13506
  };
13446
13507
  }
13447
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
+
13448
13535
  // src/replay/hooks.ts
13536
+ function installVisibilityResumeHook(onResume) {
13537
+ if (typeof document === "undefined") {
13538
+ return () => {
13539
+ };
13540
+ }
13541
+ const onVisibility = () => {
13542
+ if (document.visibilityState === "visible") {
13543
+ onResume();
13544
+ }
13545
+ };
13546
+ const onPageShow = (event) => {
13547
+ if (event.persisted) {
13548
+ onResume();
13549
+ }
13550
+ };
13551
+ document.addEventListener("visibilitychange", onVisibility);
13552
+ if (typeof window !== "undefined") {
13553
+ window.addEventListener("pageshow", onPageShow);
13554
+ }
13555
+ return () => {
13556
+ document.removeEventListener("visibilitychange", onVisibility);
13557
+ if (typeof window !== "undefined") {
13558
+ window.removeEventListener("pageshow", onPageShow);
13559
+ }
13560
+ };
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
+ }
13449
13586
  function safeSerialize(args) {
13450
13587
  try {
13451
- return JSON.stringify(
13452
- args.map((a) => {
13453
- if (a instanceof Error) {
13454
- return { name: a.name, message: a.message, stack: a.stack };
13455
- }
13456
- return a;
13457
- })
13458
- );
13588
+ return JSON.stringify(args.map(serializeConsoleArg));
13459
13589
  } catch {
13460
13590
  return "[unserializable]";
13461
13591
  }
13462
13592
  }
13593
+ function consoleMessage(args) {
13594
+ return args.map(formatConsoleArg).join(" ").slice(0, 4e3);
13595
+ }
13463
13596
  function installConsoleHook() {
13464
13597
  const levels = ["log", "info", "warn", "error", "debug"];
13465
13598
  const originals = {};
@@ -13468,9 +13601,12 @@ function installConsoleHook() {
13468
13601
  originals[level] = original;
13469
13602
  console[level] = (...args) => {
13470
13603
  try {
13604
+ const serializedArgs = safeSerialize(args).slice(0, 4e3);
13471
13605
  record.addCustomEvent("talaria-console", {
13472
13606
  level,
13473
- 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,
13474
13610
  timestamp: Date.now()
13475
13611
  });
13476
13612
  } catch {
@@ -13506,23 +13642,61 @@ function statusMatches(status, codes) {
13506
13642
  }
13507
13643
  return false;
13508
13644
  }
13509
- function shouldPromoteFailedRequest(meta, opts) {
13510
- if (!opts.captureFailedRequests) return false;
13511
- if (typeof meta.status !== "number" || Number.isNaN(meta.status)) return false;
13512
- if (!statusMatches(meta.status, opts.failedRequestStatusCodes)) return false;
13513
- const url = meta.url || "";
13514
- const ignore = [...opts.failedRequestIgnoreUrls];
13515
- const base = (opts.talariaBaseUrl ?? "").replace(/\/+$/, "");
13645
+ function buildFailedRequestIgnoreUrls(failedRequestIgnoreUrls, talariaBaseUrl) {
13646
+ const ignore = [...failedRequestIgnoreUrls];
13647
+ const base = (talariaBaseUrl ?? "").replace(/\/+$/, "");
13516
13648
  if (base) {
13517
13649
  ignore.push(`${base}/events/`, `${base}/replays/`);
13518
13650
  }
13519
13651
  ignore.push("/events/ingest", "/events/ingestBatch", "/replays/");
13520
- const lower = url.toLowerCase();
13521
- for (const part of ignore) {
13522
- 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;
13523
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;
13524
13682
  return true;
13525
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
+ }
13526
13700
  function installNetworkHook(options = {}) {
13527
13701
  const originalFetch = typeof fetch === "function" ? fetch.bind(globalThis) : null;
13528
13702
  const XHR = typeof XMLHttpRequest !== "undefined" ? XMLHttpRequest : null;
@@ -13530,15 +13704,22 @@ function installNetworkHook(options = {}) {
13530
13704
  const originalSend = XHR?.prototype.send;
13531
13705
  const matchOpts = {
13532
13706
  captureFailedRequests: options.captureFailedRequests ?? true,
13707
+ captureNetworkErrors: options.captureNetworkErrors ?? true,
13533
13708
  failedRequestStatusCodes: options.failedRequestStatusCodes ?? [[500, 599]],
13534
13709
  failedRequestIgnoreUrls: options.failedRequestIgnoreUrls ?? [],
13535
13710
  talariaBaseUrl: options.talariaBaseUrl
13536
13711
  };
13537
13712
  const handleMeta = (meta) => {
13538
- emitNetwork(meta);
13539
- options.onNetwork?.(meta);
13540
- if (shouldPromoteFailedRequest(meta, matchOpts)) {
13541
- 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 {
13542
13723
  }
13543
13724
  };
13544
13725
  if (originalFetch) {
@@ -13557,11 +13738,16 @@ function installNetworkHook(options = {}) {
13557
13738
  });
13558
13739
  return response;
13559
13740
  } catch (error) {
13741
+ const described = describeUnknownError(error);
13560
13742
  handleMeta({
13561
13743
  method,
13562
13744
  url,
13563
13745
  durationMs: Date.now() - started,
13564
- ok: false
13746
+ ok: false,
13747
+ errorName: described.errorName,
13748
+ errorMessage: described.errorMessage,
13749
+ aborted: described.aborted,
13750
+ failureKind: "network"
13565
13751
  });
13566
13752
  throw error;
13567
13753
  }
@@ -13576,12 +13762,19 @@ function installNetworkHook(options = {}) {
13576
13762
  XHR.prototype.send = function(body) {
13577
13763
  this.__talariaStarted = Date.now();
13578
13764
  const onDone = () => {
13765
+ const status = this.status;
13766
+ const networkFail = status === 0;
13579
13767
  handleMeta({
13580
13768
  method: this.__talariaMethod ?? "GET",
13581
13769
  url: this.__talariaUrl ?? "",
13582
- status: this.status,
13770
+ status,
13583
13771
  durationMs: Date.now() - (this.__talariaStarted ?? Date.now()),
13584
- 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
+ } : {}
13585
13778
  });
13586
13779
  };
13587
13780
  this.addEventListener("loadend", onDone);
@@ -13596,6 +13789,31 @@ function installNetworkHook(options = {}) {
13596
13789
  }
13597
13790
 
13598
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
+ }
13599
13817
  function resolveOptions(options) {
13600
13818
  const baseUrl = (options.baseUrl ?? options.dsn ?? "").trim();
13601
13819
  if (!baseUrl) {
@@ -13610,7 +13828,7 @@ function resolveOptions(options) {
13610
13828
  return {
13611
13829
  baseUrl,
13612
13830
  apiKey: options.apiKey.trim(),
13613
- environment: String(options.environment),
13831
+ environment: normalizeEnvironment(String(options.environment)),
13614
13832
  release: options.release,
13615
13833
  replaysSessionSampleRate: clamp01(options.replaysSessionSampleRate ?? 0),
13616
13834
  replaysOnErrorSampleRate: clamp01(options.replaysOnErrorSampleRate ?? 1),
@@ -13622,10 +13840,12 @@ function resolveOptions(options) {
13622
13840
  tags: options.tags,
13623
13841
  disableDefaultIntegrations: options.disableDefaultIntegrations ?? false,
13624
13842
  captureFailedRequests: options.captureFailedRequests ?? true,
13843
+ captureNetworkErrors: options.captureNetworkErrors ?? true,
13625
13844
  failedRequestStatusCodes: options.failedRequestStatusCodes ?? [[500, 599]],
13626
13845
  failedRequestIgnoreUrls: options.failedRequestIgnoreUrls ?? []
13627
13846
  };
13628
13847
  }
13848
+ var RECENT_NETWORK_FAILURE_MS = 5e3;
13629
13849
  function clamp01(n2) {
13630
13850
  if (Number.isNaN(n2)) return 0;
13631
13851
  return Math.min(1, Math.max(0, n2));
@@ -13650,6 +13870,16 @@ function isOversizedSegmentError(error) {
13650
13870
  const msg = error instanceof Error ? error.message : String(error);
13651
13871
  return msg.includes("segment exceeds max compressed size") || msg.includes("segment exceeds max uncompressed size") || msg.includes("HTTP 400") && msg.includes("ApiValidationException");
13652
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
+ }
13653
13883
  var TalariaClient = class {
13654
13884
  constructor() {
13655
13885
  this.options = null;
@@ -13684,6 +13914,15 @@ var TalariaClient = class {
13684
13914
  this.lastReplayCaptureFailure = null;
13685
13915
  /** Cached once per init for event tags/extra. */
13686
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 = [];
13687
13926
  }
13688
13927
  init(options) {
13689
13928
  if (this.options) {
@@ -13698,6 +13937,7 @@ var TalariaClient = class {
13698
13937
  this.sessionId = createId();
13699
13938
  this.replayId = createId();
13700
13939
  this.closed = false;
13940
+ this.ingestDisabled = false;
13701
13941
  this.finishedOnServer = false;
13702
13942
  this.startedOnServer = false;
13703
13943
  this.segmentIndex = 0;
@@ -13706,6 +13946,7 @@ var TalariaClient = class {
13706
13946
  this.errorClipDeadlineMs = null;
13707
13947
  this.linkableReplayId = null;
13708
13948
  this.lastReplayCaptureFailure = null;
13949
+ this.recentNetworkFailures = [];
13709
13950
  this.browserContext = parseBrowserContext();
13710
13951
  void collectBrowserContext().then((ctx) => {
13711
13952
  if (this.options) this.browserContext = ctx;
@@ -13718,36 +13959,74 @@ var TalariaClient = class {
13718
13959
  maskAllInputs: this.options.maskAllInputs,
13719
13960
  inlineStylesheet: this.options.inlineStylesheet,
13720
13961
  blockSelector: this.options.blockSelector,
13962
+ // Buffer/error path: keep a FullSnapshot inside the ~60s ring. Session
13963
+ // sample uploads continuously so periodic checkouts are skipped (size).
13964
+ checkoutEveryNms: this.sessionSampled ? void 0 : RING_BUFFER_CHECKOUT_MS,
13721
13965
  onEvent: (event) => this.onRrwebEvent(event)
13722
13966
  });
13723
13967
  this.teardowns.push(
13724
13968
  installConsoleHook(),
13725
13969
  installNetworkHook({
13726
13970
  captureFailedRequests: this.options.captureFailedRequests,
13971
+ captureNetworkErrors: this.options.captureNetworkErrors,
13727
13972
  failedRequestStatusCodes: this.options.failedRequestStatusCodes,
13728
13973
  failedRequestIgnoreUrls: this.options.failedRequestIgnoreUrls,
13729
13974
  talariaBaseUrl: this.options.baseUrl,
13975
+ onNetwork: (meta) => {
13976
+ if (meta.failureKind === "network" && !meta.aborted) {
13977
+ this.rememberNetworkFailure(meta, { promoted: false });
13978
+ }
13979
+ },
13730
13980
  onFailedRequest: (meta) => {
13731
13981
  const status = meta.status;
13732
13982
  const method = meta.method || "GET";
13733
- const url = meta.url || "(unknown url)";
13983
+ const url = redactUrl(meta.url || "(unknown url)");
13734
13984
  void this.captureMessage(
13735
13985
  `HTTP ${status}: ${method} ${url}`,
13736
13986
  status >= 500 ? "error" : "warning",
13737
13987
  {
13738
13988
  tags: {
13739
13989
  "http.status_code": String(status),
13740
- "http.method": method
13990
+ "http.method": method,
13991
+ "network.failure_kind": "http"
13741
13992
  },
13742
13993
  extra: {
13743
13994
  url,
13744
13995
  durationMs: meta.durationMs,
13745
- 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
13746
14024
  }
13747
14025
  }
13748
14026
  );
13749
14027
  }
13750
- })
14028
+ }),
14029
+ installVisibilityResumeHook(() => this.onForegroundResume())
13751
14030
  );
13752
14031
  if (!this.options.disableDefaultIntegrations) {
13753
14032
  this.installGlobalHandlers();
@@ -13778,24 +14057,38 @@ var TalariaClient = class {
13778
14057
  return this.linkableReplayId;
13779
14058
  }
13780
14059
  async captureException(error, context) {
14060
+ if (this.capturing || this.ingestDisabled) return;
13781
14061
  const err = error instanceof Error ? error : new Error(typeof error === "string" ? error : "Unknown error");
14062
+ if (isAbortError(err)) return;
13782
14063
  const filename = typeof context?.extra?.filename === "string" ? context.extra.filename : void 0;
13783
14064
  if (isBrowserExtensionNoise({
13784
14065
  message: err.message,
13785
14066
  stack: err.stack,
13786
14067
  filename
14068
+ }) || isSdkInternalNoise({
14069
+ message: err.message,
14070
+ stack: err.stack
13787
14071
  })) {
13788
14072
  return;
13789
14073
  }
13790
- await this.capture({
13791
- message: err.message || err.name || "Error",
13792
- level: "error",
13793
- title: err.name || "Error",
13794
- stackTrace: err.stack,
13795
- context
13796
- });
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
+ }
13797
14089
  }
13798
14090
  async captureMessage(message, level = "info", context) {
14091
+ if (this.ingestDisabled) return;
13799
14092
  await this.capture({
13800
14093
  message,
13801
14094
  level,
@@ -13881,6 +14174,7 @@ var TalariaClient = class {
13881
14174
  this.linkableReplayId = null;
13882
14175
  this.lastReplayCaptureFailure = null;
13883
14176
  this.browserContext = null;
14177
+ this.capturing = false;
13884
14178
  }
13885
14179
  onRrwebEvent(event) {
13886
14180
  if (this.closed || !this.options) return;
@@ -13905,6 +14199,7 @@ var TalariaClient = class {
13905
14199
  if (!this.options || !this.transport) {
13906
14200
  throw new Error("@newtalaria/browser: call Talaria.init() first");
13907
14201
  }
14202
+ if (this.ingestDisabled) return;
13908
14203
  const isErrorLike = args.level === "error" || args.level === "fatal";
13909
14204
  let errorClipOutcome = null;
13910
14205
  let attemptedErrorClip = false;
@@ -14011,7 +14306,36 @@ var TalariaClient = class {
14011
14306
  }
14012
14307
  } catch (error) {
14013
14308
  console.warn("@newtalaria/browser: event ingest failed", error);
14014
- 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;
14015
14339
  }
14016
14340
  }
14017
14341
  markUploadStarted() {
@@ -14141,6 +14465,18 @@ var TalariaClient = class {
14141
14465
  checkoutFullSnapshot() {
14142
14466
  this.recorder?.takeFullSnapshot();
14143
14467
  }
14468
+ /**
14469
+ * Tab focus / bfcache restore: background timers are throttled, so the
14470
+ * buffer-mode checkout interval may not have fired. Refresh the paint base
14471
+ * so an error clip still has ~60s of usable lead-up after the user returns.
14472
+ */
14473
+ onForegroundResume() {
14474
+ if (this.closed || !this.options || !this.recorder) return;
14475
+ if (!this.uploadEnabled) {
14476
+ this.buffer.trimRing();
14477
+ }
14478
+ this.checkoutFullSnapshot();
14479
+ }
14144
14480
  /**
14145
14481
  * Ensure segment 0 begins at Meta+FullSnapshot. Orphan increments alone
14146
14482
  * produce a blank rrweb player.
@@ -14204,6 +14540,57 @@ var TalariaClient = class {
14204
14540
  this.resetToBufferMode();
14205
14541
  }
14206
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
+ }
14207
14594
  installGlobalHandlers() {
14208
14595
  if (typeof window === "undefined") return;
14209
14596
  const onError = (event) => {
@@ -14225,7 +14612,21 @@ var TalariaClient = class {
14225
14612
  });
14226
14613
  };
14227
14614
  const onRejection = (event) => {
14228
- 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);
14229
14630
  };
14230
14631
  window.addEventListener("error", onError);
14231
14632
  window.addEventListener("unhandledrejection", onRejection);