feat(replay): capture fetch (Blob/ArrayBuffer) response bodies in Session Replay network details#6473
Conversation
…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 now 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. Closes getsentry#6376 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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. |
…f-8 decoder dropping bytes after an interrupted sequence - resolveXhrResponseBody now snapshots request headers, raw response headers and the response body size synchronously, and the re-added breadcrumb is enriched only from that snapshot — the live xhr may have been reused or cleared during the async gap (Bugbot: stale XHR data). - the manual UTF-8 fallback decoder no longer discards bytes following a truncated/interrupted multi-byte sequence; the consumed prefix decodes to a single U+FFFD (maximal subpart), matching TextDecoder. - drop non-null assertions flagged by oxlint no-unnecessary-type-assertion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lid code point A structurally complete sequence that decodes to a surrogate or a code point above U+10FFFF previously advanced by one byte, so its continuation bytes were re-decoded as stray bytes and produced extra U+FFFDs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 e479547. Configure here.
| .then(undefined, (error: unknown) => { | ||
| debug.error(`[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} Failed to re-add network breadcrumb`, error); | ||
| }); | ||
| return null; |
There was a problem hiding this comment.
Held fetch breadcrumbs miss error events
High Severity
Returning null from beforeBreadcrumb while awaiting FileReader drops allow-listed fetch xhr breadcrumbs from the scope until the read finishes. RN fetch always uses responseType: 'blob', and FileReader completion runs after promise microtasks, so errors thrown in fetch response handlers (and hard crashes in that window) no longer include that network breadcrumb — a regression versus the previous synchronous UNPARSEABLE_BODY_TYPE path. Flagged under the PR review correctness guidance for instrumentation that must not lose telemetry on common error paths.
Additional Locations (2)
Triggered by project rule: PR Review Guidelines for Cursor Bot
Reviewed by Cursor Bugbot for commit e479547. Configure here.


📜 Description
Closes #6376
In React Native,
fetchis the whatwg-fetch polyfill built onXMLHttpRequestwithresponseType = 'blob', so everyfetchresponse body landed in the binary branch of_getResponseBodyStringand surfaced as[UNPARSEABLE_BODY_TYPE]in the Replay network tab — even when the payload was plain JSON or text. This PR inlines parseable Blob/ArrayBuffer response bodies like text bodies, following the approach sketched in the issue.The async capture path
Binary bodies can only be read asynchronously (
FileReader), but xhr breadcrumbs are forwarded to the native SDKs synchronously (thebeforeAddBreadcrumbenrichment hook runs right beforescope.addBreadcrumb, which syncs to native). So the enrichment itself can't wait for the read.Instead, when body capture is enabled and an allow-listed xhr breadcrumb carries a text-like binary response, the wrapped
beforeBreadcrumb(same wrapping pattern the integration already uses forbeforeSend):null),FileReaderforblobwith a 500 ms timeout, slicing toNETWORK_BODY_MAX_SIZEbefore the read so a huge payload is never fully read into memory; manual UTF-8 decode forarraybuffer(Hermes has noTextDecoder; uses it when available),addBreadcrumbwith the resolved body carried on the hint. Itstimestampwas set on the first pass and is preserved; the user'sbeforeBreadcrumbruns only on the first pass; the resolved-body hint key makes the second pass skip the hold branch, and the existingbeforeAddBreadcrumbenrichment uses the resolved body instead of readingxhr.response.Guardrails, per the issue's considerations:
networkDetailAllowUrls(minusnetworkDetailDenyUrls) withnetworkCaptureBodiesenabled;beforeBreadcrumbisn't wrapped at all otherwise.text/*, form-urlencoded) qualify; everything else keeps theUNPARSEABLE_BODY_TYPEmarker as before.UNPARSEABLE_BODY_TYPE— never worse data than today.NETWORK_BODY_MAX_SIZEwith the existingMAX_BODY_SIZE_EXCEEDEDwarning.Trade-off to be aware of: a held breadcrumb reaches the scope (and native) a few ms later than its neighbors, so strict insertion order among breadcrumbs can shift; the breadcrumb's own timestamp stays correct. Request bodies for
fetch(Request/RequestInit.bodyBlobs) are intentionally left for a follow-up, as noted in the issue.💚 How did you test it?
networkUtils,xhrUtilsandmobilereplaycovering: content-type classification, the manual UTF-8 decoder (multi-byte, invalid sequences, no-TextDecoder path), FileReader success/error/timeout, blob slicing + truncation warning, the hold/re-add flow, single execution of the user'sbeforeBreadcrumb, user drops staying dropped, and no wrapping when capture is off.test/replaysuite green (115 tests);tsc -p tsconfig.build.json,oxlintandoxfmtclean.📝 Checklist
networkDetailAllowUrls+networkCaptureBodies, auth-like headers still stripped).🤖 Generated with Claude Code