Record network request/response bodies in Session Replay#6288
Conversation
Semver Impact of This PR⚪ None (no version bump detected) 📋 Changelog PreviewThis is how your changes will appear in the changelog.
🤖 This preview updates automatically when you update the PR. |
|
@cursor review |
`getBodyString` fell through to the `typeof === 'object'` branch for Blob
and ArrayBuffer, producing `"{}"` via `JSON.stringify` with no warning.
Guard both types explicitly and return `UNPARSEABLE_BODY_TYPE`, matching
`getBodySize`'s handling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ings
`_urlMatches` had two latent footguns:
- `RegExp.test()` on a `/g` (or `/y`) pattern advances `lastIndex`, so a
pattern shared across calls would match intermittently. Reset
`lastIndex` before each test.
- `''.indexOf('') !== -1` is true, so an empty string in
`networkDetailAllowUrls` would silently enable body capture for every
request. Treat empty patterns as no-ops.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
652c0ef to
848f040
Compare
…erters The JS-internal `_meta.warnings` field (e.g. `MAX_BODY_SIZE_EXCEEDED`) was being forwarded verbatim onto the rrweb span event. Filter it out in the iOS / Android converters so only documented `body`/`headers` keys reach the native SDK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adding a URL to `networkDetailAllowUrls` now captures headers only by default; users opt in explicitly to body capture with `networkCaptureBodies: true`. This avoids surfacing sensitive request / response payloads in the Replay network tab on the strength of a single allow-list entry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`Uint8Array` and other `ArrayBuffer` views slipped past the `Blob`/`ArrayBuffer` guard and were JSON-stringified into the captured body as misleading data. Extend the check with `ArrayBuffer.isView` so all binary payloads return `UNPARSEABLE_BODY_TYPE` consistently. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
antonis
left a comment
There was a problem hiding this comment.
Overall LGTM! Added a few comments to consider. Let's also update the PR description for future reference.
Two follow-ups from PR review: - Default `captureBodies` to `false` in `DEFAULT_NETWORK_OPTIONS` so the legacy `enrichXhrBreadcrumbsForMobileReplay` path stays consistent with the public `networkCaptureBodies` default in `mobilereplay.ts`. It had no functional effect today (the default `allowUrls: []` short-circuits in `shouldCaptureNetworkDetails`) but was a latent footgun. - The native breadcrumb converters strip the JS-internal `_meta` field before forwarding request/response sides to the native rrweb span, and they drop the whole side when nothing else remains. That meant warnings like `UNPARSEABLE_BODY_TYPE` for binary bodies (Blob, ArrayBuffer, typed arrays) never reached Session Replay. Materialize a placeholder body string from the warnings whenever `_meta` is present but no concrete body was captured, so the signal survives the native strip and surfaces in the Replay UI.
…erters Add basic Android (Kotlin) and iOS (Swift) tests for the recently introduced `sanitizeNetworkSide` logic in `RNSentryReplayBreadcrumbConverter`: - Happy path: `body` and `headers` are forwarded to the native rrweb span and the JS-internal `_meta` field is stripped from both `request` and `response` sides. - Empty-after-strip: a side that contains only `_meta` (or a non-dict value) is omitted from the span entirely. These mirror each other across platforms and only cover the basic scenarios \u2014 just enough to lock down the contract of the native sanitization helper.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0a1e41e. Configure here.
`convertNetworkBreadcrumb` was guarding `start_timestamp` and `end_timestamp` with `instanceof Number` but then casting directly to `Double`. The RN bridge can surface numeric breadcrumb data as `Long` or `Integer` (both pass `instanceof Number`), which would throw a `ClassCastException` at runtime and crash the replay span conversion. Replace the direct cast with `Number.doubleValue()` so any numeric subtype is accepted safely. Added a regression test that exercises `Long` and `Integer` timestamps.
…ping them `getBodyString` previously fell through to the `UNPARSEABLE_BODY_TYPE` branch for `number` and `boolean` values, so when an XHR with `responseType: 'json'` received a JSON primitive (e.g. `42` or `true`) the body was dropped with a warning even though it is trivially serializable. Stringify primitives via `String(body)` so they are recorded as-is.
Follow-up to the timestamp fix: the same `instanceof Double` pattern was used for `status_code`, `request_body_size` and `response_body_size` in `convertNetworkBreadcrumb`. Those don't crash (the cast is guarded), but they silently drop the field when the RN bridge surfaces the value as `Long` or `Integer` instead of `Double`. Switch all three to `instanceof Number` + `Number.intValue()`/ `doubleValue()` for consistency with the timestamp handling, and extend the existing regression test to assert on these fields too.
Add a link to the new `Network Details` section in the Sentry React Native Session Replay docs from the changelog entry for #6288 so users can jump straight to the configuration reference.
antonis
left a comment
There was a problem hiding this comment.
LGTM 🚀
Just added the ready-to-merge label be be on the safe side since this is a big PR :)
📲 Install BuildsAndroid
|
iOS (legacy) Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 5125c43+dirty | 3846.45 ms | 1221.12 ms | -2625.32 ms |
| 8929511+dirty | 1216.42 ms | 1219.02 ms | 2.60 ms |
| a3265b6+dirty | 3826.31 ms | 1207.87 ms | -2618.44 ms |
| c004dae+dirty | 3850.32 ms | 1227.79 ms | -2622.53 ms |
| 9210ae6+dirty | 3815.93 ms | 1214.14 ms | -2601.79 ms |
| 3d377b5+dirty | 1218.48 ms | 1219.51 ms | 1.03 ms |
| 853723c+dirty | 3852.60 ms | 1234.64 ms | -2617.96 ms |
| 04207c4+dirty | 1191.27 ms | 1189.78 ms | -1.48 ms |
| 4953e94+dirty | 1212.06 ms | 1214.83 ms | 2.77 ms |
| 0b5a379+dirty | 3828.91 ms | 1214.12 ms | -2614.79 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 5125c43+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 8929511+dirty | 3.38 MiB | 4.80 MiB | 1.42 MiB |
| a3265b6+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| c004dae+dirty | 5.15 MiB | 6.67 MiB | 1.51 MiB |
| 9210ae6+dirty | 5.15 MiB | 6.68 MiB | 1.53 MiB |
| 3d377b5+dirty | 3.38 MiB | 4.76 MiB | 1.38 MiB |
| 853723c+dirty | 5.15 MiB | 6.69 MiB | 1.53 MiB |
| 04207c4+dirty | 3.38 MiB | 4.76 MiB | 1.38 MiB |
| 4953e94+dirty | 3.38 MiB | 4.73 MiB | 1.35 MiB |
| 0b5a379+dirty | 5.15 MiB | 6.70 MiB | 1.54 MiB |

📢 Type of change
📜 Description
Enrich Mobile Session Replay with HTTP request/response headers and bodies, surfacing them in the Replay network tab (in addition to the URL, method, status code and body sizes already captured).
The feature is opt-in: nothing extra is captured until you list URLs in
networkDetailAllowUrls. Five new options are added onmobileReplayIntegration:networkDetailAllowUrls(string | RegExp)[][]networkCaptureBodiesis on). Strings use substring match; regexes use.test(url).networkDetailDenyUrls(string | RegExp)[][]networkCaptureBodiesbooleanfalsenetworkRequestHeadersstring[][]networkResponseHeadersstring[][]How it works
packages/core/src/js/replay/):xhrUtils.tsregisters abeforeAddBreadcrumbhook that enriches eachxhrbreadcrumb withrequest/responsesides containing headers and optionally a serialized body.networkUtils.tshandles URL allow/deny matching (regexlastIndexis reset on each call so global-flag patterns stay stateless; empty strings in the allow list are ignored), body serialization (strings,URLSearchParams,FormData, plain JSON, and primitives like42/true), and a 150 KB truncation cap that surfaces aMAX_BODY_SIZE_EXCEEDEDwarning. Binary payloads (Blob,ArrayBuffer, typed arrays) are recorded as a placeholder with anUNPARSEABLE_BODY_TYPEwarning rather than serialized to"{}".Content-Type,Content-Length,Acceptby default, plus user-supplied); authorization-like headers (Authorization,Cookie,Set-Cookie,X-API-Key,X-Auth-Token,Proxy-Authorization) are always stripped.RNSentryReplayBreadcrumbConverter):request/responsedicts to the native rrweb span event for Mobile Replay._metakey before forwarding so the native replay SDKs only see fields they understand.start_timestamp,end_timestamp,status_code,request_body_size,response_body_size) accept anyNumbersubtype on Android — the RN bridge can surface them asLong/Integer, which previously crashed (ClassCastException) or were silently dropped.Scope and limitations
axiosand most popular HTTP clients on React Native. Nativefetchbody capture will be added in a follow-up.💡 Motivation and Context
Closes #4106 — long-standing request to inspect HTTP payloads in Mobile Session Replay alongside what the JS browser Replay SDK already exposes for
fetch/XHR on the web.Companion docs PR: getsentry/sentry-docs#18423.
💚 How did you test it?
packages/core/test/replay/xhrUtils.test.ts, 20 cases): allow/deny URL matching (string + regex, empty strings, regexlastIndex), header filtering (defaults + extras, deny-list always stripped), body serialization (string /URLSearchParams/FormData/ object / number / boolean), binary body placeholders (Blob,ArrayBuffer,Uint8Array) producingUNPARSEABLE_BODY_TYPEwith a synthetic placeholder body so warnings survive the native_metastrip, and the 150 KB truncation cap producingMAX_BODY_SIZE_EXCEEDED.RNSentryAndroidTester/.../RNSentryReplayBreadcrumbConverterTest.kt): network breadcrumb forwarding with_metastripped from both sides, empty-after-strip sides dropped, and non-DoubleNumbersubtypes (Long/Integer) accepted for all five numeric fields.RNSentryCocoaTester/.../RNSentryReplayBreadcrumbConverterTests.swift): equivalent coverage for the_metastrip and empty-after-strip cases.Authorization,Cookie) never appear regardless of configuration.📝 Checklist
sendDefaultPIIis enablednetworkCaptureBodies: falseby default); authorization-like headers are always stripped.🔮 Next steps
fetchbody capture (currently XHR only — coversaxiosbut not rawfetch).