Skip to content

Record network request/response bodies in Session Replay#6288

Merged
alwx merged 15 commits into
mainfrom
alwx/improvement/response-body-session-replay
Jun 16, 2026
Merged

Record network request/response bodies in Session Replay#6288
alwx merged 15 commits into
mainfrom
alwx/improvement/response-body-session-replay

Conversation

@alwx

@alwx alwx commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 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 on mobileReplayIntegration:

Option Type Default Purpose
networkDetailAllowUrls (string | RegExp)[] [] URLs to enrich with headers (and bodies, when networkCaptureBodies is on). Strings use substring match; regexes use .test(url).
networkDetailDenyUrls (string | RegExp)[] [] URLs to never enrich, even if they match the allow list.
networkCaptureBodies boolean false Opt in to capturing request/response bodies for allow-listed URLs.
networkRequestHeaders string[] [] Extra request headers to capture, in addition to the defaults.
networkResponseHeaders string[] [] Extra response headers to capture, in addition to the defaults.
Sentry.init({
  dsn: "___PUBLIC_DSN___",
  integrations: [
    Sentry.mobileReplayIntegration({
      networkDetailAllowUrls: ["api.example.com", /^https:\/\/cdn\./],
      networkDetailDenyUrls: [/\/auth\//],
      networkCaptureBodies: true,
      networkRequestHeaders: ["X-My-Header"],
      networkResponseHeaders: ["X-Response-Header"],
    }),
  ],
});

How it works

  • JS layer (packages/core/src/js/replay/):
    • xhrUtils.ts registers a beforeAddBreadcrumb hook that enriches each xhr breadcrumb with request / response sides containing headers and optionally a serialized body.
    • networkUtils.ts handles URL allow/deny matching (regex lastIndex is 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 like 42 / true), and a 150 KB truncation cap that surfaces a MAX_BODY_SIZE_EXCEEDED warning. Binary payloads (Blob, ArrayBuffer, typed arrays) are recorded as a placeholder with an UNPARSEABLE_BODY_TYPE warning rather than serialized to "{}".
    • Headers are filtered against an allow list (Content-Type, Content-Length, Accept by default, plus user-supplied); authorization-like headers (Authorization, Cookie, Set-Cookie, X-API-Key, X-Auth-Token, Proxy-Authorization) are always stripped.
  • Native layer (Android Java + iOS ObjC RNSentryReplayBreadcrumbConverter):
    • Forward the enriched request / response dicts to the native rrweb span event for Mobile Replay.
    • Strip the JS-internal _meta key before forwarding so the native replay SDKs only see fields they understand.
    • Numeric fields (start_timestamp, end_timestamp, status_code, request_body_size, response_body_size) accept any Number subtype on Android — the RN bridge can surface them as Long/Integer, which previously crashed (ClassCastException) or were silently dropped.

Scope and limitations

  • XHR only for now. This covers axios and most popular HTTP clients on React Native. Native fetch body capture will be added in a follow-up.
  • Bodies and headers are PII-scrubbed server-side; users should still be deliberate about which URLs they allow-list.

💡 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?

  • JS unit tests (packages/core/test/replay/xhrUtils.test.ts, 20 cases): allow/deny URL matching (string + regex, empty strings, regex lastIndex), header filtering (defaults + extras, deny-list always stripped), body serialization (string / URLSearchParams / FormData / object / number / boolean), binary body placeholders (Blob, ArrayBuffer, Uint8Array) producing UNPARSEABLE_BODY_TYPE with a synthetic placeholder body so warnings survive the native _meta strip, and the 150 KB truncation cap producing MAX_BODY_SIZE_EXCEEDED.
  • Android native tests (RNSentryAndroidTester/.../RNSentryReplayBreadcrumbConverterTest.kt): network breadcrumb forwarding with _meta stripped from both sides, empty-after-strip sides dropped, and non-Double Number subtypes (Long/Integer) accepted for all five numeric fields.
  • iOS native tests (RNSentryCocoaTester/.../RNSentryReplayBreadcrumbConverterTests.swift): equivalent coverage for the _meta strip and empty-after-strip cases.
  • Manually verified end-to-end on a sample app: bodies + headers visible in the Replay network tab; binary bodies show the placeholder; sensitive headers (Authorization, Cookie) never appear regardless of configuration.

📝 Checklist

  • I added tests to verify changes
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled
    • Body / header capture is fully opt-in (empty default allow list + networkCaptureBodies: false by default); authorization-like headers are always stripped.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • All tests passing
  • No breaking changes

🔮 Next steps

  • Add native fetch body capture (currently XHR only — covers axios but not raw fetch).

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Semver Impact of This PR

None (no version bump detected)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


  • Record network request/response bodies in Session Replay by alwx in #6288
  • chore(deps): bump tar from 7.5.11 to 7.5.16 by dependabot in #6293
  • fix(ci): Update renamed @sentry-internal/* packages in JS updater script by antonis in #6294
  • chore(deps): bump launch-editor from 2.11.1 to 2.14.1 by dependabot in #6291
  • chore(deps-dev): bump @babel/core from 7.26.7 to 7.29.6 by dependabot in #6292
  • fix(deps): Resolve shell-quote to >=1.8.4 (Dependabot RNSentryModule.captureEvent is ignoring environment #547) by antonis in #6286
  • fix(ci): Support version catalog in android SDK version check by antonis in #6280
  • test(e2e): Bump E2E tests to React Native 0.86.0 by antonis in #6268
  • feat(android): Add nativeStackAndroid support to NativeLinkedErrors by lucas-zimerman in #6278
  • chore(deps): bump ruby/setup-ruby from 1.310.0 to 1.313.0 by dependabot in #6282
  • chore(deps): update Maestro to v2.6.1 by github-actions in #6277
  • chore(deps): bump gradle/actions from 6.1.0 to 6.2.0 by dependabot in #6284
  • chore(deps): bump getsentry/craft from 2.26.8 to 2.26.10 by dependabot in #6283
  • chore(deps): bump getsentry/craft/.github/workflows/changelog-preview.yml from 2.26.8 to 2.26.10 by dependabot in #6281
  • chore(deps): update Sentry Android Gradle Plugin to v6.11.0 by github-actions in #6275
  • chore(deps): update Android SDK to v8.43.2 by github-actions in #6273
  • chore(deps): bump joi from 17.13.3 to 17.13.4 by dependabot in #6279
  • chore(deps): update Cocoa SDK to v9.17.1 by github-actions in #6272
  • docs(replay): clarify fast renderer option docs by leohara in #6276
  • feat(core): Warn when multiple versions of Sentry JS SDK are detected by antonis in #6269

🤖 This preview updates automatically when you update the PR.

sentry-warden[bot]

This comment was marked as resolved.

@alwx

alwx commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

@cursor review

Comment thread packages/core/src/js/replay/networkUtils.ts
Comment thread packages/core/src/js/replay/networkUtils.ts
alwx and others added 5 commits June 15, 2026 14:06
`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>
@alwx alwx force-pushed the alwx/improvement/response-body-session-replay branch from 652c0ef to 848f040 Compare June 15, 2026 12:08
@alwx alwx marked this pull request as ready for review June 15, 2026 12:08
Comment thread packages/core/src/js/replay/mobilereplay.ts
…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>
Comment thread packages/core/src/js/replay/networkUtils.ts
alwx and others added 2 commits June 15, 2026 14:48
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>
Comment thread packages/core/src/js/replay/xhrUtils.ts Outdated
Comment thread packages/core/ios/RNSentryReplayBreadcrumbConverter.m

@antonis antonis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM! Added a few comments to consider. Let's also update the PR description for future reference.

alwx added 2 commits June 16, 2026 10:35
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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread packages/core/src/js/replay/networkUtils.ts
alwx added 2 commits June 16, 2026 11:25
`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.
@alwx alwx requested a review from antonis June 16, 2026 09:42
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 antonis added the ready-to-merge Triggers the full CI test suite label Jun 16, 2026

@antonis antonis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🚀
Just added the ready-to-merge label be be on the safe side since this is a big PR :)

@sentry

sentry Bot commented Jun 16, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
Sentry RN io.sentry.reactnative.sample 8.14.0 (91) Release

⚙️ sentry-react-native Build Distribution Settings

@github-actions

Copy link
Copy Markdown
Contributor

iOS (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 3837.72 ms 1214.34 ms -2623.38 ms
Size 5.15 MiB 6.69 MiB 1.54 MiB

Baseline results on branch: main

Startup times

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

@alwx alwx merged commit 071eab4 into main Jun 16, 2026
101 of 118 checks passed
@alwx alwx deleted the alwx/improvement/response-body-session-replay branch June 16, 2026 11:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-merge Triggers the full CI test suite

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Record network request/response bodies in Session Replay

2 participants