Skip to content

feat(replay): capture fetch (Blob/ArrayBuffer) response bodies in Session Replay network details - #6533

Merged
alwx merged 4 commits into
mainfrom
alwx/bugfix/6376
Jul 29, 2026
Merged

feat(replay): capture fetch (Blob/ArrayBuffer) response bodies in Session Replay network details#6533
alwx merged 4 commits into
mainfrom
alwx/bugfix/6376

Conversation

@alwx

@alwx alwx commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

📢 Type of change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring

📜 Description

Carries over #6473 by @Cryptoteep.

From #6473:
React Native's fetch is the whatwg-fetch polyfill built on XMLHttpRequest with responseType = 'blob', so every fetch response body landed in the binary branch of _getResponseBodyString and surfaced as [UNPARSEABLE_BODY_TYPE] in the Replay network tab, even for plain JSON. Text-like binary payloads are now read asynchronously (FileReader for Blob, manual UTF-8 decode for ArrayBuffer) and inlined like text bodies, still gated on networkDetailAllowUrls + networkCaptureBodies and capped at NETWORK_BODY_MAX_SIZE.

Second commit — the review fixes.

Known limitation

The breadcrumb attached to a JS error event still shows [UNPARSEABLE_BODY_TYPE] for these responses — the scope holds a normalized copy that can't be back-patched. The resolved body appears in the Replay network tab. This matches current main exactly, so it is not a regression; noted in the CHANGELOG.

💡 Motivation and Context

Fixes #6376. Supersedes #6473.

fetch is how most RN apps make requests, so before this the network-body half of Session Replay network detail (#6288, #6373) was effectively unusable outside axios/raw XHR.

💚 How did you test it?

Tests were added.

📝 Checklist

  • I added tests to verify changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled. Unchanged opt-in (networkDetailAllowUrls + networkCaptureBodies); auth-like headers still stripped.
  • I updated the docs if needed.
  • I updated the wizard if needed. n/a
  • All tests passing.
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec. No public API change — api-report:check clean.
  • No breaking changes.

@github-actions

github-actions Bot commented Jul 28, 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).


  • feat(replay): capture fetch (Blob/ArrayBuffer) response bodies in Session Replay network details by alwx in #6533
  • chore(deps): update React Native to v0.86.2 by github-actions in #6535
  • feat(tracing): Add check and download timing spans to Expo Updates listener by antonis in #6430
  • fix(tracing): Skip duplicate navigation span from withAnchor POP_TO by antonis in #6439
  • test(e2e): Assert TTID/TTFD spans share their navigation transaction's trace and parent span by antonis in #6530
  • chore(deps): add sharp ^0.35.0 resolution (dev-only advisory) by antonis in #6525
  • chore(deps): add body-parser ^2.3.0 resolution (dev-only advisory) by antonis in #6524
  • chore(deps): add morgan ^1.11.0 resolution (dev-only advisory) by antonis in #6523
  • chore(deps): bump postcss to ^8.5.18 (dev-only advisory) by antonis in #6522
  • chore(deps): bump brace-expansion 5.x to ^5.0.8 (dev-only advisory) by antonis in #6521
  • ci: Disable yarn age gate when bumping first-party @sentry deps by antonis in #6513
  • feat(core): Attach TurboModule breakdown to active spans on spanEnd by alwx in #6478
  • feat(core): Export instrumentStateGraph and deprecate instrumentLangGraph by antonis in #6520
  • chore(deps): bump the codeql-action group across 1 directory with 3 updates by dependabot in #6517
  • chore(deps): bump ruby/setup-ruby from 1.319.0 to 1.321.0 by dependabot in #6518
  • chore(deps): bump actions/checkout from 7.0.0 to 7.0.1 by dependabot in #6519
  • chore(deps): update JavaScript SDK to v10.68.0 by github-actions in #6516
  • chore(deps): bump tar from 7.5.20 to 7.5.21 by dependabot in #6515
  • chore(deps): bump lerna to ^9.0.7 to resolve remaining dev-tooling advisories by antonis in #6499
  • chore(deps): Migrate to @sentry/bundler-plugins by antonis in #6501
  • chore: update changelog warning with fix version by antonis in #6509
  • chore(deps): bump actions/setup-node from 6.4.0 to 7.0.0 by dependabot in #6463
  • chore(deps): bump json from 2.17.1.2 to 2.19.9 in /performance-tests by dependabot in #6512
  • chore(deps): update CLI to v3.6.2 by github-actions in #6511

Plus 2 more


🤖 This preview updates automatically when you update the PR.

…sion Replay network details

React Native's fetch polyfill is built on XMLHttpRequest with responseType
'blob', so every fetch response body previously surfaced as
[UNPARSEABLE_BODY_TYPE] in the Replay network tab even when the payload was
plain JSON or text.

Binary bodies can only be read asynchronously, while xhr breadcrumbs are
forwarded to the native SDKs synchronously. When an allow-listed xhr breadcrumb
carries a text-like (JSON/XML/text/form) Blob or ArrayBuffer response and body
capture is enabled, the breadcrumb is held in beforeBreadcrumb, the body is read
(FileReader for Blob with a 500ms timeout, capped at NETWORK_BODY_MAX_SIZE by
slicing before the read; manual UTF-8 decode for ArrayBuffer since Hermes has no
TextDecoder), and the same breadcrumb is re-added with the resolved body on the
hint. Its original timestamp is preserved. Genuinely binary payloads (images,
octet-stream) keep the UNPARSEABLE_BODY_TYPE marker without being read, and read
failures or timeouts fall back to the same marker.

Squashed from #6473.

Closes #6376
@alwx
alwx force-pushed the alwx/bugfix/6376 branch from e8bc1b2 to b1a1c07 Compare July 28, 2026 13:35
@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 3198355

@alwx
alwx marked this pull request as ready for review July 28, 2026 13:37
…he native sync

Holding the breadcrumb in beforeBreadcrumb (returning null) until the async body
read finished removed it from the scope entirely, so an error captured in that
window lost it from event.breadcrumbs. The common `if (!res.ok) throw` path hits
this reliably: the app never reads the body, so nothing yields long enough for
the FileReader to land before the throw. Breadcrumbs are also filtered by
timestamp into replay segments on Android, so a re-added crumb could be dropped
from the replay when it straddled a segment boundary.

The breadcrumb is now returned as before and lands on the JS scope immediately.
Only the sync to native — which is what feeds the Replay network tab, since the
native converters build the rrweb span from the synced xhr breadcrumb — is
deferred, via deferBreadcrumbNativeSync/syncBreadcrumbToNative in scopeSync, and
performed once with the resolved body. buildResolvedNetworkBreadcrumb builds the
native-bound copy so the breadcrumb already on the scope is never mutated.

Also in this change:

- Retry a truncated blob read with a shorter slice. Slicing at a byte offset can
  cut a multi-byte UTF-8 sequence, and iOS decodes via
  -[NSString initWithData:encoding:], which returns nil for the whole chunk
  rather than substituting U+FFFD — so bodies over the cap silently fell back to
  UNPARSEABLE_BODY_TYPE. readBlobAsText now rejects with a distinguishable
  BLOB_DECODE_FAILED and the read retries with up to 3 fewer bytes, which is
  guaranteed to reach a character boundary. Timeouts and read errors are not
  retried.
- Match javascript/ecmascript content types as text-like.
- Update the networkDetailAllowUrls docs, which still said fetch bodies were
  unsupported.
- Correct the abort() comment: RN's FileReader never enters LOADING, so abort()
  dispatches no events and does not cancel the native read.
- Note in decodeUtf8 that the manual decoder is the path actually taken — neither
  Hermes nor JSC ships TextDecoder, so the TextDecoder branch only runs in tests.

Adds test/replay/networkBodyCapture.test.ts, which exercises the whole flow
against the real @sentry/core pipeline and the real scope sync patch. It guards
the object-identity assumption the deferral relies on: the breadcrumb returned
from beforeBreadcrumb must be the exact object passed to scope.addBreadcrumb.
@alwx
alwx force-pushed the alwx/bugfix/6376 branch from b1a1c07 to fcd8ab3 Compare July 28, 2026 13:39
@antonis antonis added the ready-to-merge Triggers the full CI test suite label Jul 29, 2026
Comment thread CHANGELOG.md
@antonis antonis added ready-to-merge Triggers the full CI test suite and removed ready-to-merge Triggers the full CI test suite labels Jul 29, 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.

Left a comment regarding the changelog. Other than that LGTM once the conflicts are resolved and the full tests suite runs 🎉

alwx and others added 2 commits July 29, 2026 11:55
Co-authored-by: Antonis Lilis <antonis.lilis@sentry.io>

@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 3198355. Configure here.

Comment thread packages/core/src/js/replay/mobilereplay.ts
@sentry

sentry Bot commented Jul 29, 2026

Copy link
Copy Markdown

📲 Install Builds

Android

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

⚙️ sentry-react-native Build Distribution Settings

@github-actions

Copy link
Copy Markdown
Contributor

iOS (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 3835.39 ms 1226.77 ms -2608.62 ms
Size 4.98 MiB 6.56 MiB 1.58 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
7d8c8bd+dirty 3837.24 ms 1215.51 ms -2621.73 ms
7887847+dirty 3849.83 ms 1227.35 ms -2622.48 ms
580fb5c+dirty 3836.13 ms 1218.72 ms -2617.41 ms
e5bb5f6+dirty 3826.14 ms 1212.24 ms -2613.90 ms
f3215d3+dirty 3842.73 ms 1219.33 ms -2623.40 ms
df5d108+dirty 1225.90 ms 1220.14 ms -5.76 ms
5b7e8a7+dirty 3851.22 ms 1223.79 ms -2627.43 ms
d038a14+dirty 3845.71 ms 1228.11 ms -2617.59 ms
68ae91b+dirty 3834.04 ms 1216.60 ms -2617.44 ms
1a2e7e0+dirty 3842.49 ms 1220.04 ms -2622.45 ms

App size

Revision Plain With Sentry Diff
7d8c8bd+dirty 5.15 MiB 6.68 MiB 1.53 MiB
7887847+dirty 4.98 MiB 6.46 MiB 1.48 MiB
580fb5c+dirty 4.98 MiB 6.46 MiB 1.48 MiB
e5bb5f6+dirty 4.98 MiB 6.51 MiB 1.53 MiB
f3215d3+dirty 5.15 MiB 6.67 MiB 1.52 MiB
df5d108+dirty 3.38 MiB 4.73 MiB 1.35 MiB
5b7e8a7+dirty 5.15 MiB 6.70 MiB 1.54 MiB
d038a14+dirty 5.15 MiB 6.67 MiB 1.51 MiB
68ae91b+dirty 4.98 MiB 6.46 MiB 1.48 MiB
1a2e7e0+dirty 4.98 MiB 6.46 MiB 1.49 MiB

@alwx
alwx requested a review from antonis July 29, 2026 10:15
@github-actions

Copy link
Copy Markdown
Contributor

iOS (new) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 3810.60 ms 1210.61 ms -2599.99 ms
Size 4.98 MiB 6.56 MiB 1.58 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
3ce5254+dirty 1217.70 ms 1224.69 ms 6.99 ms
ce7b368+dirty 3851.41 ms 1222.37 ms -2629.04 ms
5125c43+dirty 3827.94 ms 1208.79 ms -2619.15 ms
822d35b+dirty 3841.52 ms 1221.75 ms -2619.77 ms
4e0ba9c+dirty 3856.39 ms 1234.44 ms -2621.95 ms
a858ac3+dirty 3850.72 ms 1226.35 ms -2624.37 ms
bc0d8cf+dirty 3834.64 ms 1223.91 ms -2610.73 ms
40c9884+dirty 3826.11 ms 1217.04 ms -2609.07 ms
a3265b6+dirty 3844.26 ms 1235.60 ms -2608.66 ms
0a9e622+dirty 3825.35 ms 1219.04 ms -2606.31 ms

App size

Revision Plain With Sentry Diff
3ce5254+dirty 3.38 MiB 4.76 MiB 1.38 MiB
ce7b368+dirty 4.98 MiB 6.51 MiB 1.53 MiB
5125c43+dirty 5.15 MiB 6.68 MiB 1.53 MiB
822d35b+dirty 4.98 MiB 6.50 MiB 1.53 MiB
4e0ba9c+dirty 5.15 MiB 6.67 MiB 1.51 MiB
a858ac3+dirty 4.98 MiB 6.51 MiB 1.53 MiB
bc0d8cf+dirty 5.15 MiB 6.67 MiB 1.51 MiB
40c9884+dirty 4.98 MiB 6.51 MiB 1.53 MiB
a3265b6+dirty 5.15 MiB 6.68 MiB 1.53 MiB
0a9e622+dirty 4.98 MiB 6.51 MiB 1.53 MiB

@github-actions

Copy link
Copy Markdown
Contributor

Android (legacy) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 514.83 ms 536.08 ms 21.25 ms
Size 49.74 MiB 55.38 MiB 5.63 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
e763471+dirty 426.57 ms 458.21 ms 31.64 ms
15d4514+dirty 406.77 ms 428.06 ms 21.29 ms
d2eadf8+dirty 414.64 ms 454.56 ms 39.92 ms
64630e5+dirty 419.18 ms 464.58 ms 45.40 ms
882f8ae+dirty 399.98 ms 427.06 ms 27.08 ms
a858ac3+dirty 426.50 ms 484.82 ms 58.32 ms
5748023+dirty 446.69 ms 505.63 ms 58.94 ms
580fb5c+dirty 436.34 ms 471.63 ms 35.28 ms
09a902f+dirty 427.38 ms 473.80 ms 46.42 ms
8448c07+dirty 422.88 ms 471.22 ms 48.34 ms

App size

Revision Plain With Sentry Diff
e763471+dirty 49.74 MiB 54.85 MiB 5.11 MiB
15d4514+dirty 48.30 MiB 53.60 MiB 5.30 MiB
d2eadf8+dirty 48.30 MiB 53.48 MiB 5.18 MiB
64630e5+dirty 49.74 MiB 54.82 MiB 5.07 MiB
882f8ae+dirty 48.30 MiB 53.60 MiB 5.29 MiB
a858ac3+dirty 49.74 MiB 55.08 MiB 5.34 MiB
5748023+dirty 48.30 MiB 53.54 MiB 5.23 MiB
580fb5c+dirty 49.74 MiB 54.79 MiB 5.05 MiB
09a902f+dirty 49.74 MiB 54.81 MiB 5.07 MiB
8448c07+dirty 49.74 MiB 55.37 MiB 5.63 MiB

@github-actions

Copy link
Copy Markdown
Contributor

Android (new) Performance metrics 🚀

  Plain With Sentry Diff
Startup time 436.74 ms 460.38 ms 23.64 ms
Size 49.74 MiB 55.38 MiB 5.63 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
9210ae6+dirty 444.14 ms 459.80 ms 15.66 ms
7a89652+dirty 405.00 ms 437.04 ms 32.04 ms
1e5d96d+dirty 423.33 ms 482.46 ms 59.13 ms
c151573+dirty 485.39 ms 495.18 ms 9.79 ms
e763471+dirty 538.31 ms 574.44 ms 36.13 ms
ae37560+dirty 428.96 ms 456.86 ms 27.90 ms
f170ec3+dirty 505.96 ms 551.88 ms 45.92 ms
ef27341+dirty 519.02 ms 553.42 ms 34.40 ms
a858ac3+dirty 498.50 ms 575.22 ms 76.72 ms
4e0b819+dirty 599.31 ms 654.31 ms 55.00 ms

App size

Revision Plain With Sentry Diff
9210ae6+dirty 48.30 MiB 53.54 MiB 5.23 MiB
7a89652+dirty 48.30 MiB 53.60 MiB 5.30 MiB
1e5d96d+dirty 49.74 MiB 54.81 MiB 5.07 MiB
c151573+dirty 48.30 MiB 53.54 MiB 5.24 MiB
e763471+dirty 49.74 MiB 54.85 MiB 5.11 MiB
ae37560+dirty 48.30 MiB 53.60 MiB 5.29 MiB
f170ec3+dirty 48.30 MiB 53.57 MiB 5.26 MiB
ef27341+dirty 48.30 MiB 53.54 MiB 5.24 MiB
a858ac3+dirty 49.74 MiB 55.08 MiB 5.34 MiB
4e0b819+dirty 49.74 MiB 54.81 MiB 5.07 MiB

@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

@alwx
alwx merged commit 490a436 into main Jul 29, 2026
94 of 95 checks passed
@alwx
alwx deleted the alwx/bugfix/6376 branch July 29, 2026 13:44
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.

Session Replay: capture fetch/binary (Blob, ArrayBuffer) response bodies in network detail

3 participants