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