@newtalaria/browser 0.1.17 → 0.1.18

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
@@ -2,6 +2,8 @@
2
2
 
3
3
  Browser SDK for [Talaria](https://github.com/) — error capture and **session replay** (rrweb event streams, not video).
4
4
 
5
+ Network calls (`fetch` / XHR) are instrumented as replay breadcrumbs; **only first-party (same-origin) or allowlisted origins** promote failed requests to error events — see [Failed HTTP / network requests](#failed-http--network-requests--events).
6
+
5
7
  ## Install
6
8
 
7
9
  ```bash
@@ -187,15 +189,24 @@ Set `inlineStylesheet: true` so same-origin stylesheet rules are embedded while
187
189
 
188
190
  ### Failed HTTP / network requests → events
189
191
 
190
- By default, completed `fetch` / XHR responses with status **500–599** are sent as Talaria events (message like `HTTP 500: GET /api/...`).
192
+ All instrumented `fetch` / XHR calls are recorded as replay breadcrumbs (`talaria-network`). **Error events are promoted only for first-party (same-origin) or explicitly allowlisted origins.**
191
193
 
192
- Transport failures (CORS, offline, DNS browser `TypeError: Failed to fetch`) are also promoted by default as:
194
+ | Request | HTTP 5xx | Transport failure / timeout | Abort |
195
+ | --- | --- | --- | --- |
196
+ | Same-origin | Error event | Error event | Breadcrumb only |
197
+ | Allowlisted third-party (`networkErrorOrigins`) | Error event | Error event | Breadcrumb only |
198
+ | Other third-party (analytics, ads, widgets, …) | Breadcrumb only | Breadcrumb only | Breadcrumb only |
193
199
 
194
- `Network error: POST https://www.google-analytics.com/g/collectTypeError: Failed to fetch`
200
+ This avoids Issue spam from Google Analytics, ad pixels, privacy blockers, and similar third-party noisewithout a hardcoded domain denylist. Add origins you intentionally depend on (payments, CMS APIs, widgets):
195
201
 
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`, …).
202
+ ```ts
203
+ networkErrorOrigins: ['https://api.stripe.com', 'https://widget.yonder.example'],
204
+ // Escape hatch (not recommended): networkErrorOrigins: ['*']
205
+ ```
197
206
 
198
- Both **`fetch`** and **`XMLHttpRequest`** are instrumented (`http.transport`: `fetch` | `xhr`). Failure kinds are only claimed when detectable:
207
+ **URL privacy:** network telemetry stores `origin + pathname` only (plus `hostname` / `pathname`). Query strings and fragments are stripped by default. Set `captureRequestQueryParameters: true` to keep query params after redaction of secrets **and** common tracking keys (`token`, `api_key`, `gclid`, `fbclid`, `_ga`, `cid`, `sid`, …). Bodies and auth headers are never captured.
208
+
209
+ Both **`fetch`** and **`XMLHttpRequest`** are instrumented (`network.transport`: `fetch` | `xhr`). Failure kinds are only claimed when detectable:
199
210
 
200
211
  | `failure.kind` | Meaning |
201
212
  | --- | --- |
@@ -204,16 +215,24 @@ Both **`fetch`** and **`XMLHttpRequest`** are instrumented (`http.transport`: `f
204
215
  | `timeout` | `TimeoutError` or XHR `timeout` event |
205
216
  | `abort` | `AbortError` / XHR abort — recorded in breadcrumbs, **not** promoted as events |
206
217
 
207
- We **do not** classify status 0 as `cors` — browsers do not expose that reliably.
218
+ We **do not** classify status 0 as `cors` — browsers do not expose that reliably. Tags include `network.party` (`first_party` \| `third_party`).
208
219
 
209
- Promoted events use structured `extra`:
220
+ Promoted events use structured `extra` (server groups on method + host + path + kind + status):
210
221
 
211
222
  ```json
212
223
  {
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
224
+ "http": {
225
+ "method": "POST",
226
+ "url": "https://app.example.com/api/orders",
227
+ "origin": "https://app.example.com",
228
+ "hostname": "app.example.com",
229
+ "pathname": "/api/orders",
230
+ "transport": "fetch"
231
+ },
232
+ "failure": { "kind": "network", "name": "TypeError", "message": "Failed to fetch" },
233
+ "network": { "party": "first_party", "durationMs": 842, "aborted": false, "ok": false },
234
+ "exception_class": "NetworkError",
235
+ "status_code": null
217
236
  }
218
237
  ```
219
238
 
@@ -224,10 +243,12 @@ Talaria.init({
224
243
  dsn: 'https://api.example.com',
225
244
  apiKey: 'tal_live_…',
226
245
  environment: 'production',
227
- // Default true — HTTP status promotion
246
+ // Default true — HTTP status promotion (first-party / allowlisted only)
228
247
  captureFailedRequests: true,
229
- // Default true — transport failures (fetch + XHR)
248
+ // Default true — transport failures (first-party / allowlisted only)
230
249
  captureNetworkErrors: true,
250
+ // Extra origins to treat like first-party for promotion
251
+ networkErrorOrigins: ['https://api.stripe.com'],
231
252
  // Default false — do not store ?query on network URLs
232
253
  captureRequestQueryParameters: false,
233
254
  // Default [[500, 599]]. CMS admin often wants 4xx too:
@@ -237,7 +258,24 @@ Talaria.init({
237
258
  });
238
259
  ```
239
260
 
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.
261
+ `AbortError` is never promoted and is ignored by the global `unhandledrejection` handler. Bare `TypeError: Failed to fetch` / `TimeoutError` rejections are correlated with recent network breadcrumbs: promoted failures and third-party noise are suppressed; first-party failures with promotion off keep the exception and merge request context.
262
+
263
+ #### Network event tags (low cardinality)
264
+
265
+ | Tag | Values | Notes |
266
+ | --- | --- | --- |
267
+ | `http.method` | `GET`, `POST`, … | |
268
+ | `http.status_code` | e.g. `500` | Present on HTTP promotions only |
269
+ | `network.failure_kind` | `http` \| `network` \| `timeout` | |
270
+ | `network.transport` | `fetch` \| `xhr` | |
271
+ | `network.party` | `first_party` \| `third_party` | Same-origin vs cross-origin (allowlist does not change this tag) |
272
+ | `network.error_name` | e.g. `TypeError`, `TimeoutError` | When available |
273
+
274
+ Hostname, pathname, full sanitized URL, and duration live in `extra` — not tags — to avoid high-cardinality facets.
275
+
276
+ #### Issue grouping (server)
277
+
278
+ Promoted network events set `extra.exception_class` (`HttpError` \| `NetworkError` \| `TimeoutError`) and `extra.status_code`. The server fingerprints them on **method + hostname + pathname + failure kind + status** (not the human message), so query strings and volatile error text do not fragment Issues. See [`planning/fingerprints.md`](../../../planning/fingerprints.md) §6.4.
241
279
 
242
280
  ### Event timestamps
243
281
 
@@ -248,11 +286,35 @@ Talaria.init({
248
286
 
249
287
  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
288
 
289
+ ## Init options reference
290
+
291
+ | Option | Default | Description |
292
+ | --- | --- | --- |
293
+ | `dsn` / `baseUrl` | *(required)* | Serverpod host, e.g. `https://api.example.com` |
294
+ | `apiKey` | *(required)* | Project API key (`tal_live_…`) |
295
+ | `environment` | *(required)* | `production` \| `staging` \| `development` (aliases accepted) |
296
+ | `release` | — | Optional release string on every event |
297
+ | `userId` | — | Optional app user id |
298
+ | `tags` | — | Tags merged into every event |
299
+ | `replaysSessionSampleRate` | `0` | Fraction of sessions that upload continuously |
300
+ | `replaysOnErrorSampleRate` | `1` | Fraction of errors that promote the ring buffer |
301
+ | `replaysErrorAfterMs` | `15000` | Post-error upload window; `0` = continue to 5 min cap |
302
+ | `maskAllInputs` | `true` | rrweb input masking |
303
+ | `inlineStylesheet` | `false` | Embed same-origin CSS (CMS/admin) |
304
+ | `blockSelector` | — | Extra CSS selectors blocked from the DOM snapshot |
305
+ | `disableDefaultIntegrations` | `false` | Skip `window.onerror` / `unhandledrejection` |
306
+ | `captureFailedRequests` | `true` | Promote HTTP status failures (**first-party / allowlisted only**) |
307
+ | `captureNetworkErrors` | `true` | Promote transport/timeout failures (**first-party / allowlisted only**) |
308
+ | `networkErrorOrigins` | `[]` | Extra origins eligible for promotion; same-origin always eligible; `['*']` = all (not recommended) |
309
+ | `failedRequestStatusCodes` | `[[500, 599]]` | Status codes/ranges to promote when origin is eligible |
310
+ | `failedRequestIgnoreUrls` | `[]` | URL substrings never promoted (Talaria `/events` + `/replays` always ignored) |
311
+ | `captureRequestQueryParameters` | `false` | Keep `?query` on network URLs after redaction (`includeNetworkUrlQuery` alias) |
312
+
251
313
  ## Public API
252
314
 
253
315
  | API | Description |
254
316
  | --- | --- |
255
- | `Talaria.init(options)` | Configure + start recording |
317
+ | `Talaria.init(options)` | Configure + start recording + install network/console hooks |
256
318
  | `Talaria.captureException(error)` | Ingest error (+ replay link when sampled) |
257
319
  | `Talaria.captureMessage(message, level?)` | Ingest message |
258
320
  | `Talaria.getReplayId()` | Active upload replay id, or `null` |
package/dist/client.d.ts CHANGED
@@ -107,7 +107,7 @@ export declare class TalariaClient {
107
107
  private pruneRecentNetworkFailures;
108
108
  private rememberNetworkFailure;
109
109
  /**
110
- * Find a recent transport failure for a bare fetch TypeError.
110
+ * Find a recent transport failure for a bare fetch TypeError / TimeoutError.
111
111
  * Consumes the entry so a later duplicate path cannot reuse it.
112
112
  */
113
113
  private consumeCorrelatedNetworkFailure;
@@ -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
@@ -272,7 +272,7 @@ async function collectBrowserContext() {
272
272
 
273
273
  // src/sdk_meta.ts
274
274
  var SDK_NAME = "@newtalaria/browser";
275
- var SDK_VERSION = "0.1.17";
275
+ var SDK_VERSION = "0.1.18";
276
276
 
277
277
  // src/transport/serverpod.ts
278
278
  var ServerpodTransport = class {
@@ -13482,18 +13482,22 @@ var { freezePage } = record;
13482
13482
  var { takeFullSnapshot } = record;
13483
13483
 
13484
13484
  // src/replay/privacy.ts
13485
- var SENSITIVE_QUERY = /(?:^|[?&])(token|secret|password|auth|api[_-]?key|access[_-]?token)=([^&#]*)/gi;
13485
+ var SENSITIVE_QUERY_KEY = /^(?:token|secret|password|auth|api[_-]?key|access[_-]?token|gclid|fbclid|gcl_au|msclkid|_ga(?:_.*)?|cid|sid)$/i;
13486
+ var SENSITIVE_QUERY_LEGACY = /(?:^|[?&])(token|secret|password|auth|api[_-]?key|access[_-]?token|gclid|fbclid|gcl_au|msclkid|_ga(?:_[^=&#]*)?|cid|sid)=([^&#]*)/gi;
13487
+ function isSensitiveQueryKey(key) {
13488
+ return SENSITIVE_QUERY_KEY.test(key);
13489
+ }
13486
13490
  function redactUrl(raw) {
13487
13491
  try {
13488
13492
  const url = new URL(raw, typeof location !== "undefined" ? location.href : void 0);
13489
13493
  for (const key of [...url.searchParams.keys()]) {
13490
- if (/token|secret|password|auth|api[_-]?key|access[_-]?token/i.test(key)) {
13494
+ if (isSensitiveQueryKey(key)) {
13491
13495
  url.searchParams.set(key, "[Filtered]");
13492
13496
  }
13493
13497
  }
13494
13498
  return url.toString();
13495
13499
  } catch {
13496
- return raw.replace(SENSITIVE_QUERY, (_m, name) => `${name}=[Filtered]`);
13500
+ return raw.replace(SENSITIVE_QUERY_LEGACY, (_m, name) => `${name}=[Filtered]`);
13497
13501
  }
13498
13502
  }
13499
13503
  function sanitizeNetworkUrl(raw, opts) {
@@ -13590,6 +13594,9 @@ function startRecorder(options) {
13590
13594
  function isAbortError(error) {
13591
13595
  return error instanceof Error && error.name === "AbortError";
13592
13596
  }
13597
+ function isTimeoutError(error) {
13598
+ return error instanceof Error && error.name === "TimeoutError";
13599
+ }
13593
13600
  function isLikelyNetworkFetchError(error) {
13594
13601
  if (!(error instanceof Error)) return false;
13595
13602
  if (error.name === "AbortError" || error.name === "TimeoutError") return false;
@@ -13597,6 +13604,9 @@ function isLikelyNetworkFetchError(error) {
13597
13604
  const msg = error.message.toLowerCase().trim();
13598
13605
  return msg === "failed to fetch" || msg === "load failed" || msg === "networkerror when attempting to fetch resource." || msg.startsWith("networkerror") || msg.includes("failed to fetch");
13599
13606
  }
13607
+ function isCorrelatableTransportError(error) {
13608
+ return isLikelyNetworkFetchError(error) || isTimeoutError(error);
13609
+ }
13600
13610
  function describeUnknownError(error) {
13601
13611
  if (error instanceof Error) {
13602
13612
  return {
@@ -13744,6 +13754,56 @@ function urlMatchesIgnoreList(url, ignoreUrls) {
13744
13754
  }
13745
13755
  return false;
13746
13756
  }
13757
+ function normalizeNetworkOrigin(raw) {
13758
+ const trimmed = (raw || "").trim();
13759
+ if (!trimmed || trimmed === "*") return trimmed;
13760
+ try {
13761
+ return new URL(trimmed).origin;
13762
+ } catch {
13763
+ try {
13764
+ return new URL(`https://${trimmed}`).origin;
13765
+ } catch {
13766
+ return trimmed.replace(/\/+$/, "");
13767
+ }
13768
+ }
13769
+ }
13770
+ function resolvePageOrigin(pageOrigin) {
13771
+ if (pageOrigin) {
13772
+ try {
13773
+ return new URL(pageOrigin).origin;
13774
+ } catch {
13775
+ return pageOrigin.replace(/\/+$/, "");
13776
+ }
13777
+ }
13778
+ if (typeof location !== "undefined" && location.origin) {
13779
+ return location.origin;
13780
+ }
13781
+ return "";
13782
+ }
13783
+ function resolveRequestOrigin(rawUrl, pageOrigin) {
13784
+ const base = pageOrigin || (typeof location !== "undefined" ? location.href : void 0);
13785
+ try {
13786
+ return new URL(rawUrl, base).origin;
13787
+ } catch {
13788
+ return "";
13789
+ }
13790
+ }
13791
+ function isAllowedNetworkOrigin(requestOrigin, opts) {
13792
+ if (!requestOrigin) return false;
13793
+ const allow = opts.networkErrorOrigins.map(normalizeNetworkOrigin);
13794
+ if (allow.includes("*")) return true;
13795
+ const page = resolvePageOrigin(opts.pageOrigin);
13796
+ if (page && requestOrigin === page) return true;
13797
+ return allow.includes(requestOrigin);
13798
+ }
13799
+ function classifyNetworkParty(requestOrigin, pageOrigin) {
13800
+ const page = resolvePageOrigin(pageOrigin);
13801
+ if (page && requestOrigin && requestOrigin === page) return "first_party";
13802
+ return "third_party";
13803
+ }
13804
+ function isHttpOkStatus(status) {
13805
+ return status >= 200 && status < 300;
13806
+ }
13747
13807
  function shouldPromoteFailedRequest(meta, opts) {
13748
13808
  if (!opts.captureFailedRequests) return false;
13749
13809
  if (typeof meta.status !== "number" || Number.isNaN(meta.status)) return false;
@@ -13754,6 +13814,13 @@ function shouldPromoteFailedRequest(meta, opts) {
13754
13814
  opts.talariaBaseUrl
13755
13815
  );
13756
13816
  if (urlMatchesIgnoreList(meta.url || "", ignore)) return false;
13817
+ const requestOrigin = meta.origin || resolveRequestOrigin(meta.url || "", opts.pageOrigin);
13818
+ if (!isAllowedNetworkOrigin(requestOrigin, {
13819
+ networkErrorOrigins: opts.networkErrorOrigins ?? [],
13820
+ pageOrigin: opts.pageOrigin
13821
+ })) {
13822
+ return false;
13823
+ }
13757
13824
  return true;
13758
13825
  }
13759
13826
  function shouldPromoteNetworkError(meta, opts) {
@@ -13767,6 +13834,13 @@ function shouldPromoteNetworkError(meta, opts) {
13767
13834
  opts.talariaBaseUrl
13768
13835
  );
13769
13836
  if (urlMatchesIgnoreList(meta.url || "", ignore)) return false;
13837
+ const requestOrigin = meta.origin || resolveRequestOrigin(meta.url || "", opts.pageOrigin);
13838
+ if (!isAllowedNetworkOrigin(requestOrigin, {
13839
+ networkErrorOrigins: opts.networkErrorOrigins ?? [],
13840
+ pageOrigin: opts.pageOrigin
13841
+ })) {
13842
+ return false;
13843
+ }
13770
13844
  return true;
13771
13845
  }
13772
13846
  function enrichNetworkMeta(meta) {
@@ -13777,7 +13851,8 @@ function enrichNetworkMeta(meta) {
13777
13851
  failureKind: meta.ok === false ? "http" : meta.failureKind
13778
13852
  };
13779
13853
  }
13780
- if (meta.ok === false || meta.errorName || meta.errorMessage || meta.aborted) {
13854
+ const hasTransportSignal = !!meta.errorName || !!meta.errorMessage || !!meta.aborted || meta.failureKind === "abort" || meta.failureKind === "timeout" || meta.failureKind === "network";
13855
+ if (hasTransportSignal) {
13781
13856
  const failureKind = meta.failureKind === "abort" || meta.failureKind === "timeout" || meta.failureKind === "network" ? meta.failureKind : classifyTransportFailure({
13782
13857
  aborted: meta.aborted,
13783
13858
  errorName: meta.errorName
@@ -13790,10 +13865,13 @@ function enrichNetworkMeta(meta) {
13790
13865
  }
13791
13866
  return meta;
13792
13867
  }
13793
- function networkUrlParts(rawUrl, includeQuery) {
13794
- const parts = sanitizeNetworkUrl(rawUrl, { includeQuery });
13868
+ function networkUrlParts(rawUrl, includeQuery, pageOrigin) {
13869
+ const baseHref = pageOrigin || (typeof location !== "undefined" ? location.href : void 0);
13870
+ const parts = sanitizeNetworkUrl(rawUrl, { includeQuery, baseHref });
13871
+ const origin = resolveRequestOrigin(rawUrl, pageOrigin);
13795
13872
  return {
13796
13873
  url: parts.url,
13874
+ origin: origin || void 0,
13797
13875
  hostname: parts.hostname,
13798
13876
  pathname: parts.pathname,
13799
13877
  ...parts.search ? { search: parts.search } : {}
@@ -13809,14 +13887,22 @@ function installNetworkHook(options = {}) {
13809
13887
  captureNetworkErrors: options.captureNetworkErrors ?? true,
13810
13888
  failedRequestStatusCodes: options.failedRequestStatusCodes ?? [[500, 599]],
13811
13889
  failedRequestIgnoreUrls: options.failedRequestIgnoreUrls ?? [],
13890
+ networkErrorOrigins: options.networkErrorOrigins ?? [],
13891
+ pageOrigin: options.pageOrigin,
13812
13892
  talariaBaseUrl: options.talariaBaseUrl
13813
13893
  };
13814
13894
  const includeQuery = options.captureRequestQueryParameters ?? options.includeNetworkUrlQuery ?? false;
13815
13895
  const handleMeta = (meta) => {
13816
13896
  const { rawUrl, ...rest } = meta;
13897
+ const parts = networkUrlParts(rawUrl, includeQuery, matchOpts.pageOrigin);
13898
+ const party = classifyNetworkParty(
13899
+ parts.origin || "",
13900
+ matchOpts.pageOrigin
13901
+ );
13817
13902
  const enriched = enrichNetworkMeta({
13818
13903
  ...rest,
13819
- ...networkUrlParts(rawUrl, includeQuery)
13904
+ ...parts,
13905
+ party
13820
13906
  });
13821
13907
  try {
13822
13908
  emitNetwork(enriched);
@@ -13836,6 +13922,7 @@ function installNetworkHook(options = {}) {
13836
13922
  const rawUrl = typeof input2 === "string" ? input2 : input2 instanceof URL ? input2.toString() : input2.url;
13837
13923
  try {
13838
13924
  const response = await originalFetch(input2, init);
13925
+ const opaque = response.type === "opaque" || response.type === "opaqueredirect";
13839
13926
  handleMeta({
13840
13927
  method,
13841
13928
  rawUrl,
@@ -13843,7 +13930,8 @@ function installNetworkHook(options = {}) {
13843
13930
  transport: "fetch",
13844
13931
  status: response.status,
13845
13932
  durationMs: Date.now() - started,
13846
- ok: response.ok
13933
+ // Opaque responses resolve successfully with status 0 — not a failure.
13934
+ ok: opaque ? true : response.ok
13847
13935
  });
13848
13936
  return response;
13849
13937
  } catch (error) {
@@ -13905,7 +13993,7 @@ function installNetworkHook(options = {}) {
13905
13993
  transport: "xhr",
13906
13994
  status,
13907
13995
  durationMs: Date.now() - (this.__talariaStarted ?? Date.now()),
13908
- ok: status >= 200 && status < 400,
13996
+ ok: isHttpOkStatus(status),
13909
13997
  ...transportFail ? {
13910
13998
  failureKind,
13911
13999
  errorName,
@@ -13914,7 +14002,7 @@ function installNetworkHook(options = {}) {
13914
14002
  } : {}
13915
14003
  });
13916
14004
  };
13917
- this.addEventListener("loadend", onDone);
14005
+ this.addEventListener("loadend", onDone, { once: true });
13918
14006
  return originalSend.call(this, body);
13919
14007
  };
13920
14008
  }
@@ -13926,10 +14014,16 @@ function installNetworkHook(options = {}) {
13926
14014
  }
13927
14015
 
13928
14016
  // src/client.ts
14017
+ function networkExceptionClass(failure) {
14018
+ if (failure.failureKind === "http") return "HttpError";
14019
+ if (failure.failureKind === "timeout") return "TimeoutError";
14020
+ return "NetworkError";
14021
+ }
13929
14022
  function networkFailureTags(failure) {
13930
14023
  return {
13931
14024
  "http.method": failure.method || "GET",
13932
14025
  "network.failure_kind": failure.failureKind ?? "network",
14026
+ ...failure.party ? { "network.party": failure.party } : {},
13933
14027
  ...failure.transport ? { "network.transport": failure.transport } : {},
13934
14028
  ...failure.errorName ? { "network.error_name": failure.errorName } : {}
13935
14029
  };
@@ -13939,6 +14033,7 @@ function networkFailureExtra(failure) {
13939
14033
  method: failure.method || "GET",
13940
14034
  url: failure.url || "(unknown url)"
13941
14035
  };
14036
+ if (failure.origin) http.origin = failure.origin;
13942
14037
  if (failure.hostname) http.hostname = failure.hostname;
13943
14038
  if (failure.pathname) http.pathname = failure.pathname;
13944
14039
  if (failure.search) http.search = failure.search;
@@ -13951,9 +14046,19 @@ function networkFailureExtra(failure) {
13951
14046
  };
13952
14047
  if (failure.errorName) fail.name = failure.errorName;
13953
14048
  if (failure.errorMessage) fail.message = failure.errorMessage;
14049
+ const statusCode = typeof failure.status === "number" && failure.status > 0 ? failure.status : null;
13954
14050
  return {
13955
14051
  http,
13956
14052
  failure: fail,
14053
+ network: {
14054
+ party: failure.party ?? "third_party",
14055
+ durationMs: failure.durationMs,
14056
+ aborted: failure.aborted ?? false,
14057
+ ok: failure.ok ?? false
14058
+ },
14059
+ // Top-level for server fingerprint compatibility.
14060
+ status_code: statusCode,
14061
+ exception_class: networkExceptionClass(failure),
13957
14062
  durationMs: failure.durationMs,
13958
14063
  aborted: failure.aborted ?? false,
13959
14064
  ok: failure.ok ?? false
@@ -13999,6 +14104,7 @@ function resolveOptions(options) {
13999
14104
  disableDefaultIntegrations: options.disableDefaultIntegrations ?? false,
14000
14105
  captureFailedRequests: options.captureFailedRequests ?? true,
14001
14106
  captureNetworkErrors: options.captureNetworkErrors ?? true,
14107
+ networkErrorOrigins: options.networkErrorOrigins ?? [],
14002
14108
  includeNetworkUrlQuery: options.captureRequestQueryParameters ?? options.includeNetworkUrlQuery ?? false,
14003
14109
  failedRequestStatusCodes: options.failedRequestStatusCodes ?? [[500, 599]],
14004
14110
  failedRequestIgnoreUrls: options.failedRequestIgnoreUrls ?? []
@@ -14128,6 +14234,8 @@ var TalariaClient = class {
14128
14234
  installNetworkHook({
14129
14235
  captureFailedRequests: this.options.captureFailedRequests,
14130
14236
  captureNetworkErrors: this.options.captureNetworkErrors,
14237
+ networkErrorOrigins: this.options.networkErrorOrigins,
14238
+ pageOrigin: typeof location !== "undefined" ? location.origin : void 0,
14131
14239
  includeNetworkUrlQuery: this.options.includeNetworkUrlQuery,
14132
14240
  failedRequestStatusCodes: this.options.failedRequestStatusCodes,
14133
14241
  failedRequestIgnoreUrls: this.options.failedRequestIgnoreUrls,
@@ -14221,6 +14329,7 @@ var TalariaClient = class {
14221
14329
  }
14222
14330
  const correlated = this.consumeCorrelatedNetworkFailure(err);
14223
14331
  if (correlated?.promoted) return;
14332
+ if (correlated && correlated.party === "third_party") return;
14224
14333
  const mergedContext = correlated ? mergeNetworkFailureContext(context, correlated) : context;
14225
14334
  this.capturing = true;
14226
14335
  try {
@@ -14703,6 +14812,13 @@ var TalariaClient = class {
14703
14812
  );
14704
14813
  }
14705
14814
  rememberNetworkFailure(meta, opts) {
14815
+ if (this.options) {
14816
+ const ignore = buildFailedRequestIgnoreUrls(
14817
+ this.options.failedRequestIgnoreUrls,
14818
+ this.options.baseUrl
14819
+ );
14820
+ if (urlMatchesIgnoreList(meta.url || "", ignore)) return;
14821
+ }
14706
14822
  const now = Date.now();
14707
14823
  this.pruneRecentNetworkFailures(now);
14708
14824
  for (let i = this.recentNetworkFailures.length - 1; i >= 0; i--) {
@@ -14713,6 +14829,8 @@ var TalariaClient = class {
14713
14829
  existing.errorName = meta.errorName ?? existing.errorName;
14714
14830
  existing.aborted = meta.aborted ?? existing.aborted;
14715
14831
  existing.failureKind = meta.failureKind ?? existing.failureKind;
14832
+ existing.party = meta.party ?? existing.party;
14833
+ existing.origin = meta.origin ?? existing.origin;
14716
14834
  return;
14717
14835
  }
14718
14836
  }
@@ -14723,11 +14841,12 @@ var TalariaClient = class {
14723
14841
  });
14724
14842
  }
14725
14843
  /**
14726
- * Find a recent transport failure for a bare fetch TypeError.
14844
+ * Find a recent transport failure for a bare fetch TypeError / TimeoutError.
14727
14845
  * Consumes the entry so a later duplicate path cannot reuse it.
14728
14846
  */
14729
14847
  consumeCorrelatedNetworkFailure(error) {
14730
- if (!isLikelyNetworkFetchError(error)) return null;
14848
+ if (!isCorrelatableTransportError(error)) return null;
14849
+ const wantKind = isTimeoutError(error) ? "timeout" : "network";
14731
14850
  const now = Date.now();
14732
14851
  this.pruneRecentNetworkFailures(now);
14733
14852
  const takeAt = (index2) => {
@@ -14736,14 +14855,14 @@ var TalariaClient = class {
14736
14855
  };
14737
14856
  for (let i = this.recentNetworkFailures.length - 1; i >= 0; i--) {
14738
14857
  const failure = this.recentNetworkFailures[i];
14739
- if (failure.aborted || failure.failureKind !== "network") continue;
14858
+ if (failure.aborted || failure.failureKind !== wantKind) continue;
14740
14859
  if (failure.errorMessage && failure.errorMessage === error.message) {
14741
14860
  return takeAt(i);
14742
14861
  }
14743
14862
  }
14744
14863
  for (let i = this.recentNetworkFailures.length - 1; i >= 0; i--) {
14745
14864
  const failure = this.recentNetworkFailures[i];
14746
- if (failure.aborted || failure.failureKind !== "network") continue;
14865
+ if (failure.aborted || failure.failureKind !== wantKind) continue;
14747
14866
  return takeAt(i);
14748
14867
  }
14749
14868
  return null;