@newtalaria/browser 0.1.15 → 0.1.17

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/README.md CHANGED
@@ -175,7 +175,7 @@ Segment payloads are a **gzip-compressed JSON array** of rrweb events.
175
175
  Custom rrweb events:
176
176
 
177
177
  - `talaria-console` — console level + message/args (truncated)
178
- - `talaria-network` — method, redacted URL, status, duration (no bodies / no auth headers)
178
+ - `talaria-network` — method, sanitized URL (no query by default), status, duration, failure metadata (no bodies / no auth headers)
179
179
 
180
180
  Privacy defaults: `maskAllInputs: true`, password fields masked, `[data-talaria-mask]` blocked.
181
181
 
@@ -191,9 +191,33 @@ By default, completed `fetch` / XHR responses with status **500–599** are sent
191
191
 
192
192
  Transport failures (CORS, offline, DNS — browser `TypeError: Failed to fetch`) are also promoted by default as:
193
193
 
194
- `Network error: GET https://… — TypeError: Failed to fetch`
194
+ `Network error: POST https://www.google-analytics.com/g/collect — TypeError: Failed to fetch`
195
195
 
196
- Replay `talaria-network` breadcrumbs include method, redacted URL, status (when present), duration, and on failures `errorName` / `errorMessage` / `aborted`. Request/response bodies and auth headers are never captured.
196
+ **URL privacy:** network telemetry stores `origin + pathname` only (plus `hostname` / `pathname`). Query strings and fragments are stripped by default — analytics/ads URLs often carry `cid`, `sid`, `dl`, etc. Set `captureRequestQueryParameters: true` to keep query params after sensitive-key redaction (`token`, `api_key`, …).
197
+
198
+ Both **`fetch`** and **`XMLHttpRequest`** are instrumented (`http.transport`: `fetch` | `xhr`). Failure kinds are only claimed when detectable:
199
+
200
+ | `failure.kind` | Meaning |
201
+ | --- | --- |
202
+ | `http` | Completed response with a real status (e.g. 500) |
203
+ | `network` | No usable HTTP response (status 0 / Failed to fetch) — CORS, block, DNS, offline, etc. |
204
+ | `timeout` | `TimeoutError` or XHR `timeout` event |
205
+ | `abort` | `AbortError` / XHR abort — recorded in breadcrumbs, **not** promoted as events |
206
+
207
+ We **do not** classify status 0 as `cors` — browsers do not expose that reliably.
208
+
209
+ Promoted events use structured `extra`:
210
+
211
+ ```json
212
+ {
213
+ "http": { "method": "GET", "url": "https://ct.pinterest.com/user/", "hostname": "ct.pinterest.com", "pathname": "/user/", "transport": "xhr" },
214
+ "failure": { "kind": "network", "name": "NetworkError", "message": "XMLHttpRequest failed (status 0)" },
215
+ "durationMs": 158,
216
+ "aborted": false
217
+ }
218
+ ```
219
+
220
+ Crawler UAs are tagged (`bot=true`, `bot.name=Baiduspider`) without inventing a browser name.
197
221
 
198
222
  ```ts
199
223
  Talaria.init({
@@ -202,8 +226,10 @@ Talaria.init({
202
226
  environment: 'production',
203
227
  // Default true — HTTP status promotion
204
228
  captureFailedRequests: true,
205
- // Default true — CORS/offline/DNS fetch failures
229
+ // Default true — transport failures (fetch + XHR)
206
230
  captureNetworkErrors: true,
231
+ // Default false — do not store ?query on network URLs
232
+ captureRequestQueryParameters: false,
207
233
  // Default [[500, 599]]. CMS admin often wants 4xx too:
208
234
  failedRequestStatusCodes: [[400, 599]],
209
235
  // Extra URL substrings to skip (Talaria /events and /replays are always skipped)
@@ -213,6 +239,15 @@ Talaria.init({
213
239
 
214
240
  `AbortError` is never promoted and is ignored by the global `unhandledrejection` handler. If a fetch failure is left unhandled after network-error promotion, the duplicate bare `TypeError` event is suppressed; if promotion is off, the exception is kept and correlated with the recent request URL/method/duration.
215
241
 
242
+ ### Event timestamps
243
+
244
+ | Field | Meaning |
245
+ | --- | --- |
246
+ | `timestamp` | **Occurrence time** on the client (when capture began — before replay flush) |
247
+ | `createdAt` | **Ingest/storage time** on the server |
248
+
249
+ If replay upload runs before event ingest, `extra.sdk.queuedMs` records how long capture waited (ms). A large `createdAt - timestamp` gap is usually flush/queue delay or clock skew — not a wrong failure classification.
250
+
216
251
  ## Public API
217
252
 
218
253
  | API | Description |
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -98,9 +98,46 @@ function isSdkInternalNoise(opts) {
98
98
  }
99
99
 
100
100
  // src/utils/browser_context.ts
101
+ var BOT_RULES = [
102
+ { name: "Baiduspider", re: /Baiduspider/i },
103
+ { name: "Googlebot", re: /Googlebot/i },
104
+ { name: "Bingbot", re: /bingbot/i },
105
+ { name: "DuckDuckBot", re: /DuckDuckBot/i },
106
+ { name: "YandexBot", re: /Yandex(Bot|Images)/i },
107
+ { name: "Applebot", re: /Applebot/i },
108
+ { name: "facebookexternalhit", re: /facebookexternalhit|Facebot/i },
109
+ { name: "Twitterbot", re: /Twitterbot/i },
110
+ { name: "LinkedInBot", re: /LinkedInBot/i },
111
+ { name: "Slackbot", re: /Slackbot/i },
112
+ { name: "Discordbot", re: /Discordbot/i },
113
+ { name: "Bytespider", re: /Bytespider/i },
114
+ { name: "PetalBot", re: /PetalBot/i },
115
+ { name: "SemrushBot", re: /SemrushBot/i },
116
+ { name: "AhrefsBot", re: /AhrefsBot/i },
117
+ { name: "DotBot", re: /DotBot/i },
118
+ { name: "GPTBot", re: /GPTBot/i },
119
+ { name: "ClaudeBot", re: /ClaudeBot|anthropic-ai/i },
120
+ { name: "Amazonbot", re: /Amazonbot/i },
121
+ { name: "Sogou", re: /Sogou/i },
122
+ // Generic last — still mark as bot without inventing a browser name.
123
+ { name: "bot", re: /\b(?:bot|crawler|spider|slurp)\b/i }
124
+ ];
125
+ function detectBot(ua) {
126
+ if (!ua) return { bot: false };
127
+ for (const rule2 of BOT_RULES) {
128
+ if (rule2.re.test(ua)) {
129
+ return {
130
+ bot: true,
131
+ botName: rule2.name === "bot" ? void 0 : rule2.name
132
+ };
133
+ }
134
+ }
135
+ return { bot: false };
136
+ }
101
137
  function parseBrowserContext(ua = typeof navigator !== "undefined" ? navigator.userAgent : "", language = typeof navigator !== "undefined" ? navigator.language : "") {
102
138
  const userAgent = ua || "";
103
139
  const lang = language || "";
140
+ const botInfo = detectBot(userAgent);
104
141
  let name = "unknown";
105
142
  let version = "";
106
143
  const rules = [
@@ -111,18 +148,20 @@ function parseBrowserContext(ua = typeof navigator !== "undefined" ? navigator.u
111
148
  { name: "Chrome", re: /(?:Chrome|CriOS)\/([\d.]+)/ },
112
149
  { name: "Safari", re: /Version\/([\d.]+).*Safari/ }
113
150
  ];
114
- for (const rule2 of rules) {
115
- const m = userAgent.match(rule2.re);
116
- if (m) {
117
- name = rule2.name;
118
- version = m[1] ?? "";
119
- break;
151
+ if (!botInfo.bot) {
152
+ for (const rule2 of rules) {
153
+ const m = userAgent.match(rule2.re);
154
+ if (m) {
155
+ name = rule2.name;
156
+ version = m[1] ?? "";
157
+ break;
158
+ }
159
+ }
160
+ if (name === "unknown" && /iPhone|iPad|iPod/.test(userAgent) && /AppleWebKit/.test(userAgent)) {
161
+ name = "Safari";
162
+ const m = userAgent.match(/OS ([\d_]+)/);
163
+ if (m?.[1]) version = m[1].replace(/_/g, ".");
120
164
  }
121
- }
122
- if (name === "unknown" && /iPhone|iPad|iPod/.test(userAgent) && /AppleWebKit/.test(userAgent)) {
123
- name = "Safari";
124
- const m = userAgent.match(/OS ([\d_]+)/);
125
- if (m?.[1]) version = m[1].replace(/_/g, ".");
126
165
  }
127
166
  let os = "unknown";
128
167
  let osVersion = "";
@@ -168,7 +207,9 @@ function parseBrowserContext(ua = typeof navigator !== "undefined" ? navigator.u
168
207
  osVersion,
169
208
  device,
170
209
  language: lang,
171
- userAgent
210
+ userAgent,
211
+ bot: botInfo.bot,
212
+ ...botInfo.botName ? { botName: botInfo.botName } : {}
172
213
  };
173
214
  }
174
215
  function browserContextTags(ctx) {
@@ -177,11 +218,16 @@ function browserContextTags(ctx) {
177
218
  "browser.version": ctx.version || "unknown",
178
219
  "os.name": ctx.os,
179
220
  "os.version": ctx.osVersion || "unknown",
180
- device: ctx.device
221
+ device: ctx.device,
222
+ ...ctx.bot ? {
223
+ bot: "true",
224
+ ...ctx.botName ? { "bot.name": ctx.botName } : {}
225
+ } : {}
181
226
  };
182
227
  }
183
228
  async function collectBrowserContext() {
184
229
  const base = parseBrowserContext();
230
+ if (base.bot) return base;
185
231
  const nav = typeof navigator !== "undefined" ? navigator : void 0;
186
232
  const uad = nav?.userAgentData;
187
233
  if (!uad) return base;
@@ -226,7 +272,7 @@ async function collectBrowserContext() {
226
272
 
227
273
  // src/sdk_meta.ts
228
274
  var SDK_NAME = "@newtalaria/browser";
229
- var SDK_VERSION = "0.1.15";
275
+ var SDK_VERSION = "0.1.17";
230
276
 
231
277
  // src/transport/serverpod.ts
232
278
  var ServerpodTransport = class {
@@ -13450,6 +13496,40 @@ function redactUrl(raw) {
13450
13496
  return raw.replace(SENSITIVE_QUERY, (_m, name) => `${name}=[Filtered]`);
13451
13497
  }
13452
13498
  }
13499
+ function sanitizeNetworkUrl(raw, opts) {
13500
+ const baseHref = opts?.baseHref ?? (typeof location !== "undefined" ? location.href : void 0);
13501
+ try {
13502
+ const parsed = new URL(raw, baseHref);
13503
+ const hostname = parsed.hostname;
13504
+ const pathname = parsed.pathname || "/";
13505
+ const originPath = `${parsed.origin}${pathname}`;
13506
+ if (!opts?.includeQuery) {
13507
+ return { url: originPath, hostname, pathname };
13508
+ }
13509
+ const redacted = new URL(redactUrl(parsed.toString()));
13510
+ const search = redacted.search || void 0;
13511
+ return {
13512
+ url: search ? `${originPath}${search}` : originPath,
13513
+ hostname,
13514
+ pathname,
13515
+ ...search ? { search } : {}
13516
+ };
13517
+ } catch {
13518
+ const stripped = (raw.split(/[?#]/)[0] ?? raw).trim() || raw;
13519
+ if (!opts?.includeQuery) {
13520
+ return { url: stripped, hostname: "", pathname: stripped };
13521
+ }
13522
+ const redacted = redactUrl(raw);
13523
+ const qIndex = redacted.indexOf("?");
13524
+ const search = qIndex >= 0 ? redacted.slice(qIndex) : void 0;
13525
+ return {
13526
+ url: redacted.split("#")[0] || redacted,
13527
+ hostname: "",
13528
+ pathname: stripped,
13529
+ ...search ? { search } : {}
13530
+ };
13531
+ }
13532
+ }
13453
13533
  function defaultBlockSelector(extra) {
13454
13534
  const parts = [
13455
13535
  "[data-talaria-mask]",
@@ -13512,7 +13592,7 @@ function isAbortError(error) {
13512
13592
  }
13513
13593
  function isLikelyNetworkFetchError(error) {
13514
13594
  if (!(error instanceof Error)) return false;
13515
- if (error.name === "AbortError") return false;
13595
+ if (error.name === "AbortError" || error.name === "TimeoutError") return false;
13516
13596
  if (error.name === "NetworkError") return true;
13517
13597
  const msg = error.message.toLowerCase().trim();
13518
13598
  return msg === "failed to fetch" || msg === "load failed" || msg === "networkerror when attempting to fetch resource." || msg.startsWith("networkerror") || msg.includes("failed to fetch");
@@ -13522,15 +13602,22 @@ function describeUnknownError(error) {
13522
13602
  return {
13523
13603
  errorName: error.name || "Error",
13524
13604
  errorMessage: (error.message || "").slice(0, 500),
13525
- aborted: error.name === "AbortError"
13605
+ aborted: error.name === "AbortError",
13606
+ timedOut: error.name === "TimeoutError"
13526
13607
  };
13527
13608
  }
13528
13609
  return {
13529
13610
  errorName: "Error",
13530
13611
  errorMessage: String(error).slice(0, 500),
13531
- aborted: false
13612
+ aborted: false,
13613
+ timedOut: false
13532
13614
  };
13533
13615
  }
13616
+ function classifyTransportFailure(opts) {
13617
+ if (opts.aborted || opts.errorName === "AbortError") return "abort";
13618
+ if (opts.timedOut || opts.errorName === "TimeoutError") return "timeout";
13619
+ return "network";
13620
+ }
13534
13621
 
13535
13622
  // src/replay/hooks.ts
13536
13623
  function installVisibilityResumeHook(onResume) {
@@ -13625,7 +13712,6 @@ function emitNetwork(meta) {
13625
13712
  try {
13626
13713
  record.addCustomEvent("talaria-network", {
13627
13714
  ...meta,
13628
- url: redactUrl(meta.url),
13629
13715
  timestamp: Date.now()
13630
13716
  });
13631
13717
  } catch {
@@ -13672,8 +13758,10 @@ function shouldPromoteFailedRequest(meta, opts) {
13672
13758
  }
13673
13759
  function shouldPromoteNetworkError(meta, opts) {
13674
13760
  if (!opts.captureNetworkErrors) return false;
13675
- if (meta.aborted) return false;
13676
- if (meta.failureKind !== "network") return false;
13761
+ if (meta.aborted || meta.failureKind === "abort") return false;
13762
+ if (meta.failureKind !== "network" && meta.failureKind !== "timeout") {
13763
+ return false;
13764
+ }
13677
13765
  const ignore = buildFailedRequestIgnoreUrls(
13678
13766
  opts.failedRequestIgnoreUrls,
13679
13767
  opts.talariaBaseUrl
@@ -13689,14 +13777,28 @@ function enrichNetworkMeta(meta) {
13689
13777
  failureKind: meta.ok === false ? "http" : meta.failureKind
13690
13778
  };
13691
13779
  }
13692
- if (meta.ok === false || meta.errorName || meta.errorMessage) {
13780
+ if (meta.ok === false || meta.errorName || meta.errorMessage || meta.aborted) {
13781
+ const failureKind = meta.failureKind === "abort" || meta.failureKind === "timeout" || meta.failureKind === "network" ? meta.failureKind : classifyTransportFailure({
13782
+ aborted: meta.aborted,
13783
+ errorName: meta.errorName
13784
+ });
13693
13785
  return {
13694
13786
  ...meta,
13695
- failureKind: "network"
13787
+ failureKind,
13788
+ aborted: meta.aborted ?? failureKind === "abort"
13696
13789
  };
13697
13790
  }
13698
13791
  return meta;
13699
13792
  }
13793
+ function networkUrlParts(rawUrl, includeQuery) {
13794
+ const parts = sanitizeNetworkUrl(rawUrl, { includeQuery });
13795
+ return {
13796
+ url: parts.url,
13797
+ hostname: parts.hostname,
13798
+ pathname: parts.pathname,
13799
+ ...parts.search ? { search: parts.search } : {}
13800
+ };
13801
+ }
13700
13802
  function installNetworkHook(options = {}) {
13701
13803
  const originalFetch = typeof fetch === "function" ? fetch.bind(globalThis) : null;
13702
13804
  const XHR = typeof XMLHttpRequest !== "undefined" ? XMLHttpRequest : null;
@@ -13709,8 +13811,13 @@ function installNetworkHook(options = {}) {
13709
13811
  failedRequestIgnoreUrls: options.failedRequestIgnoreUrls ?? [],
13710
13812
  talariaBaseUrl: options.talariaBaseUrl
13711
13813
  };
13814
+ const includeQuery = options.captureRequestQueryParameters ?? options.includeNetworkUrlQuery ?? false;
13712
13815
  const handleMeta = (meta) => {
13713
- const enriched = enrichNetworkMeta(meta);
13816
+ const { rawUrl, ...rest } = meta;
13817
+ const enriched = enrichNetworkMeta({
13818
+ ...rest,
13819
+ ...networkUrlParts(rawUrl, includeQuery)
13820
+ });
13714
13821
  try {
13715
13822
  emitNetwork(enriched);
13716
13823
  options.onNetwork?.(enriched);
@@ -13726,12 +13833,14 @@ function installNetworkHook(options = {}) {
13726
13833
  globalThis.fetch = async (input2, init) => {
13727
13834
  const started = Date.now();
13728
13835
  const method = (init?.method ?? (input2 instanceof Request ? input2.method : "GET")).toUpperCase();
13729
- const url = typeof input2 === "string" ? input2 : input2 instanceof URL ? input2.toString() : input2.url;
13836
+ const rawUrl = typeof input2 === "string" ? input2 : input2 instanceof URL ? input2.toString() : input2.url;
13730
13837
  try {
13731
13838
  const response = await originalFetch(input2, init);
13732
13839
  handleMeta({
13733
13840
  method,
13734
- url,
13841
+ rawUrl,
13842
+ url: rawUrl,
13843
+ transport: "fetch",
13735
13844
  status: response.status,
13736
13845
  durationMs: Date.now() - started,
13737
13846
  ok: response.ok
@@ -13739,15 +13848,18 @@ function installNetworkHook(options = {}) {
13739
13848
  return response;
13740
13849
  } catch (error) {
13741
13850
  const described = describeUnknownError(error);
13851
+ const failureKind = classifyTransportFailure(described);
13742
13852
  handleMeta({
13743
13853
  method,
13744
- url,
13854
+ rawUrl,
13855
+ url: rawUrl,
13856
+ transport: "fetch",
13745
13857
  durationMs: Date.now() - started,
13746
13858
  ok: false,
13747
13859
  errorName: described.errorName,
13748
13860
  errorMessage: described.errorMessage,
13749
13861
  aborted: described.aborted,
13750
- failureKind: "network"
13862
+ failureKind
13751
13863
  });
13752
13864
  throw error;
13753
13865
  }
@@ -13761,19 +13873,44 @@ function installNetworkHook(options = {}) {
13761
13873
  };
13762
13874
  XHR.prototype.send = function(body) {
13763
13875
  this.__talariaStarted = Date.now();
13876
+ this.__talariaTimedOut = false;
13877
+ this.__talariaAborted = false;
13878
+ this.addEventListener(
13879
+ "timeout",
13880
+ () => {
13881
+ this.__talariaTimedOut = true;
13882
+ },
13883
+ { once: true }
13884
+ );
13885
+ this.addEventListener(
13886
+ "abort",
13887
+ () => {
13888
+ this.__talariaAborted = true;
13889
+ },
13890
+ { once: true }
13891
+ );
13764
13892
  const onDone = () => {
13765
13893
  const status = this.status;
13766
- const networkFail = status === 0;
13894
+ const transportFail = status === 0;
13895
+ const rawUrl = this.__talariaUrl ?? "";
13896
+ const aborted = !!this.__talariaAborted;
13897
+ const timedOut = !!this.__talariaTimedOut;
13898
+ const failureKind = transportFail ? classifyTransportFailure({ aborted, timedOut }) : void 0;
13899
+ const errorName = transportFail ? failureKind === "abort" ? "AbortError" : failureKind === "timeout" ? "TimeoutError" : "NetworkError" : void 0;
13900
+ const errorMessage = transportFail ? failureKind === "abort" ? "XMLHttpRequest aborted" : failureKind === "timeout" ? "XMLHttpRequest timed out" : "XMLHttpRequest failed (status 0)" : void 0;
13767
13901
  handleMeta({
13768
13902
  method: this.__talariaMethod ?? "GET",
13769
- url: this.__talariaUrl ?? "",
13903
+ rawUrl,
13904
+ url: rawUrl,
13905
+ transport: "xhr",
13770
13906
  status,
13771
13907
  durationMs: Date.now() - (this.__talariaStarted ?? Date.now()),
13772
13908
  ok: status >= 200 && status < 400,
13773
- ...networkFail ? {
13774
- failureKind: "network",
13775
- errorName: "NetworkError",
13776
- errorMessage: "XMLHttpRequest failed (status 0)"
13909
+ ...transportFail ? {
13910
+ failureKind,
13911
+ errorName,
13912
+ errorMessage,
13913
+ aborted
13777
13914
  } : {}
13778
13915
  });
13779
13916
  };
@@ -13789,28 +13926,49 @@ function installNetworkHook(options = {}) {
13789
13926
  }
13790
13927
 
13791
13928
  // src/client.ts
13929
+ function networkFailureTags(failure) {
13930
+ return {
13931
+ "http.method": failure.method || "GET",
13932
+ "network.failure_kind": failure.failureKind ?? "network",
13933
+ ...failure.transport ? { "network.transport": failure.transport } : {},
13934
+ ...failure.errorName ? { "network.error_name": failure.errorName } : {}
13935
+ };
13936
+ }
13937
+ function networkFailureExtra(failure) {
13938
+ const http = {
13939
+ method: failure.method || "GET",
13940
+ url: failure.url || "(unknown url)"
13941
+ };
13942
+ if (failure.hostname) http.hostname = failure.hostname;
13943
+ if (failure.pathname) http.pathname = failure.pathname;
13944
+ if (failure.search) http.search = failure.search;
13945
+ if (typeof failure.status === "number" && failure.status > 0) {
13946
+ http.status = failure.status;
13947
+ }
13948
+ if (failure.transport) http.transport = failure.transport;
13949
+ const fail = {
13950
+ kind: failure.failureKind ?? "network"
13951
+ };
13952
+ if (failure.errorName) fail.name = failure.errorName;
13953
+ if (failure.errorMessage) fail.message = failure.errorMessage;
13954
+ return {
13955
+ http,
13956
+ failure: fail,
13957
+ durationMs: failure.durationMs,
13958
+ aborted: failure.aborted ?? false,
13959
+ ok: failure.ok ?? false
13960
+ };
13961
+ }
13792
13962
  function mergeNetworkFailureContext(context, failure) {
13793
- const url = redactUrl(failure.url || "(unknown url)");
13794
13963
  return {
13795
13964
  ...context,
13796
13965
  tags: {
13797
13966
  ...context?.tags ?? {},
13798
- "http.method": failure.method || "GET",
13799
- "network.failure_kind": "network",
13800
- ...failure.errorName ? { "network.error_name": failure.errorName } : {}
13967
+ ...networkFailureTags(failure)
13801
13968
  },
13802
13969
  extra: {
13803
13970
  ...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
- }
13971
+ ...networkFailureExtra(failure)
13814
13972
  }
13815
13973
  };
13816
13974
  }
@@ -13841,6 +13999,7 @@ function resolveOptions(options) {
13841
13999
  disableDefaultIntegrations: options.disableDefaultIntegrations ?? false,
13842
14000
  captureFailedRequests: options.captureFailedRequests ?? true,
13843
14001
  captureNetworkErrors: options.captureNetworkErrors ?? true,
14002
+ includeNetworkUrlQuery: options.captureRequestQueryParameters ?? options.includeNetworkUrlQuery ?? false,
13844
14003
  failedRequestStatusCodes: options.failedRequestStatusCodes ?? [[500, 599]],
13845
14004
  failedRequestIgnoreUrls: options.failedRequestIgnoreUrls ?? []
13846
14005
  };
@@ -13969,59 +14128,48 @@ var TalariaClient = class {
13969
14128
  installNetworkHook({
13970
14129
  captureFailedRequests: this.options.captureFailedRequests,
13971
14130
  captureNetworkErrors: this.options.captureNetworkErrors,
14131
+ includeNetworkUrlQuery: this.options.includeNetworkUrlQuery,
13972
14132
  failedRequestStatusCodes: this.options.failedRequestStatusCodes,
13973
14133
  failedRequestIgnoreUrls: this.options.failedRequestIgnoreUrls,
13974
14134
  talariaBaseUrl: this.options.baseUrl,
13975
14135
  onNetwork: (meta) => {
13976
- if (meta.failureKind === "network" && !meta.aborted) {
14136
+ if ((meta.failureKind === "network" || meta.failureKind === "timeout") && !meta.aborted) {
13977
14137
  this.rememberNetworkFailure(meta, { promoted: false });
13978
14138
  }
13979
14139
  },
13980
14140
  onFailedRequest: (meta) => {
13981
14141
  const status = meta.status;
13982
14142
  const method = meta.method || "GET";
13983
- const url = redactUrl(meta.url || "(unknown url)");
14143
+ const url = meta.url || "(unknown url)";
14144
+ const enriched = {
14145
+ ...meta,
14146
+ failureKind: "http",
14147
+ aborted: false
14148
+ };
13984
14149
  void this.captureMessage(
13985
14150
  `HTTP ${status}: ${method} ${url}`,
13986
14151
  status >= 500 ? "error" : "warning",
13987
14152
  {
13988
14153
  tags: {
13989
- "http.status_code": String(status),
13990
- "http.method": method,
13991
- "network.failure_kind": "http"
14154
+ ...networkFailureTags(enriched),
14155
+ "http.status_code": String(status)
13992
14156
  },
13993
- extra: {
13994
- url,
13995
- durationMs: meta.durationMs,
13996
- ok: meta.ok,
13997
- failureKind: "http"
13998
- }
14157
+ extra: networkFailureExtra(enriched)
13999
14158
  }
14000
14159
  );
14001
14160
  },
14002
14161
  onNetworkError: (meta) => {
14003
14162
  this.rememberNetworkFailure(meta, { promoted: true });
14004
14163
  const method = meta.method || "GET";
14005
- const url = redactUrl(meta.url || "(unknown url)");
14164
+ const url = meta.url || "(unknown url)";
14006
14165
  const errLabel = meta.errorName && meta.errorMessage ? `${meta.errorName}: ${meta.errorMessage}` : meta.errorMessage || meta.errorName || "Failed to fetch";
14166
+ const prefix = meta.failureKind === "timeout" ? "Timeout error" : "Network error";
14007
14167
  void this.captureMessage(
14008
- `Network error: ${method} ${url} \u2014 ${errLabel}`,
14168
+ `${prefix}: ${method} ${url} \u2014 ${errLabel}`,
14009
14169
  "error",
14010
14170
  {
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
14024
- }
14171
+ tags: networkFailureTags(meta),
14172
+ extra: networkFailureExtra(meta)
14025
14173
  }
14026
14174
  );
14027
14175
  }
@@ -14200,6 +14348,7 @@ var TalariaClient = class {
14200
14348
  throw new Error("@newtalaria/browser: call Talaria.init() first");
14201
14349
  }
14202
14350
  if (this.ingestDisabled) return;
14351
+ const occurredAt = /* @__PURE__ */ new Date();
14203
14352
  const isErrorLike = args.level === "error" || args.level === "fatal";
14204
14353
  let errorClipOutcome = null;
14205
14354
  let attemptedErrorClip = false;
@@ -14257,6 +14406,7 @@ var TalariaClient = class {
14257
14406
  errorClipOutcome = { status: "failed", reason: "buffer_empty" };
14258
14407
  }
14259
14408
  }
14409
+ const queuedMs = Math.max(0, Date.now() - occurredAt.getTime());
14260
14410
  const tags = applyReplayCaptureTags(
14261
14411
  {
14262
14412
  ...this.browserContext ? browserContextTags(this.browserContext) : {},
@@ -14275,10 +14425,17 @@ var TalariaClient = class {
14275
14425
  osVersion: this.browserContext.osVersion,
14276
14426
  device: this.browserContext.device,
14277
14427
  language: this.browserContext.language,
14278
- userAgent: this.browserContext.userAgent
14428
+ userAgent: this.browserContext.userAgent,
14429
+ bot: this.browserContext.bot,
14430
+ ...this.browserContext.botName ? { botName: this.browserContext.botName } : {}
14279
14431
  }
14280
14432
  } : {},
14281
- sdk: { name: SDK_NAME, version: SDK_VERSION },
14433
+ sdk: {
14434
+ name: SDK_NAME,
14435
+ version: SDK_VERSION,
14436
+ // Time spent in capture() before ingest (mostly replay flush).
14437
+ ...queuedMs >= 50 ? { queuedMs } : {}
14438
+ },
14282
14439
  ...args.context?.extra
14283
14440
  },
14284
14441
  errorClipOutcome
@@ -14298,7 +14455,7 @@ var TalariaClient = class {
14298
14455
  url: typeof location !== "undefined" ? location.href : void 0,
14299
14456
  tags: Object.keys(tags).length ? tags : void 0,
14300
14457
  extraJson: extra ? JSON.stringify(extra) : void 0,
14301
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
14458
+ timestamp: occurredAt.toISOString(),
14302
14459
  keepalive: args.keepalive
14303
14460
  });
14304
14461
  if (replayId && replayId === this.linkableReplayId) {