feat(replay): Add request details to Session Replay on Android - #3875
feat(replay): Add request details to Session Replay on Android #3875lucas-zimerman wants to merge 29 commits into
Conversation
Adds opt-in capture of HTTP request/response headers and bodies for Session Replay network breadcrumbs, gated by enableReplayNetworkDetailsCapturing plus networkDetailAllowUrls/ networkDetailDenyUrls, scoped to Dart-only requests. NetworkDetailsCapture (Dart) attaches the captured data to the http breadcrumb, which the Android/iOS replay breadcrumb converters forward into the RRWebSpanEvent unchanged. Also syncs the network detail options into the native SentryReplayOptions at init so the SDK reports networkDetailHasUrls: true in the replay's options event - the Sentry frontend gates rendering of captured request/response detail on that flag, so without this sync the data would never show up in the dashboard despite being captured correctly. Co-Authored-By: Claude <noreply@anthropic.com>
iOS session replay doesn't yet forward captured request/response detail, so drop the unused iOS converter change and note the Android-only support in the option docs. Co-Authored-By: Claude <noreply@anthropic.com>
Semver Impact of This PR🟡 Minor (new features) 📋 Changelog PreviewThis is how your changes will appear in the changelog. FeaturesFlutter
Other
Fixes
Dependencies
Internal Changes
🤖 This preview updates automatically when you update the PR. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3875 +/- ##
==========================================
- Coverage 88.22% 87.95% -0.28%
==========================================
Files 344 347 +3
Lines 12573 12929 +356
==========================================
+ Hits 11093 11372 +279
- Misses 1480 1557 +77
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
Android Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 4298701 | 524.40 ms | 633.30 ms | 108.90 ms |
| e04b24b | 504.72 ms | 516.43 ms | 11.71 ms |
| 5f4cc67 | 477.80 ms | 484.52 ms | 6.73 ms |
| d5a1225 | 377.37 ms | 366.56 ms | -10.81 ms |
| e200a70 | 433.06 ms | 437.00 ms | 3.94 ms |
| c26ed0a | 465.52 ms | 476.38 ms | 10.86 ms |
| 38a22c2 | 418.68 ms | 436.54 ms | 17.86 ms |
| 027fcfd | 409.63 ms | 402.40 ms | -7.23 ms |
| a5b28db | 383.85 ms | 387.65 ms | 3.80 ms |
| 2d65d04 | 400.19 ms | 410.23 ms | 10.04 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 4298701 | 6.54 MiB | 7.71 MiB | 1.17 MiB |
| e04b24b | 13.93 MiB | 15.00 MiB | 1.06 MiB |
| 5f4cc67 | 14.31 MiB | 15.49 MiB | 1.19 MiB |
| d5a1225 | 14.31 MiB | 15.49 MiB | 1.19 MiB |
| e200a70 | 13.93 MiB | 15.18 MiB | 1.25 MiB |
| c26ed0a | 13.93 MiB | 14.93 MiB | 1.00 MiB |
| 38a22c2 | 14.31 MiB | 15.49 MiB | 1.19 MiB |
| 027fcfd | 14.31 MiB | 15.49 MiB | 1.19 MiB |
| a5b28db | 13.93 MiB | 15.18 MiB | 1.25 MiB |
| 2d65d04 | 14.31 MiB | 15.49 MiB | 1.19 MiB |
Previous results on branch: lz/replay/http-data
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 659ff1a | 356.96 ms | 358.84 ms | 1.88 ms |
| 27b7c5b | 363.30 ms | 375.57 ms | 12.27 ms |
| 945b43c | 341.92 ms | 349.19 ms | 7.27 ms |
| ce004ca | 350.23 ms | 358.30 ms | 8.07 ms |
| cb9bba3 | 409.25 ms | 426.92 ms | 17.67 ms |
| b282d2d | 362.81 ms | 366.86 ms | 4.05 ms |
| 3f8e3bf | 356.68 ms | 350.63 ms | -6.05 ms |
| 2f41f60 | 359.53 ms | 347.43 ms | -12.10 ms |
| a266509 | 375.60 ms | 385.02 ms | 9.42 ms |
| 42bb32f | 358.35 ms | 357.38 ms | -0.97 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 659ff1a | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| 27b7c5b | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| 945b43c | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| ce004ca | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| cb9bba3 | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| b282d2d | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| 3f8e3bf | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| 2f41f60 | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| a266509 | 14.55 MiB | 15.87 MiB | 1.31 MiB |
| 42bb32f | 14.55 MiB | 15.87 MiB | 1.31 MiB |
iOS Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| c1e775e | 1263.08 ms | 1275.32 ms | 12.24 ms |
| 80e0083 | 1248.49 ms | 1254.47 ms | 5.98 ms |
| d789735 | 1240.58 ms | 1246.41 ms | 5.82 ms |
| 9c9cb8e | 1245.59 ms | 1247.22 ms | 1.64 ms |
| f42e9fb | 1247.60 ms | 1255.89 ms | 8.29 ms |
| 192b44c | 1269.08 ms | 1275.52 ms | 6.44 ms |
| c97f488 | 1247.40 ms | 1252.13 ms | 4.73 ms |
| e0c8591 | 1259.85 ms | 1257.31 ms | -2.54 ms |
| 396cb30 | 1265.06 ms | 1265.73 ms | 0.67 ms |
| 73dca78 | 1246.65 ms | 1265.42 ms | 18.76 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| c1e775e | 20.70 MiB | 22.46 MiB | 1.75 MiB |
| 80e0083 | 5.83 MiB | 6.28 MiB | 458.83 KiB |
| d789735 | 5.53 MiB | 5.96 MiB | 443.28 KiB |
| 9c9cb8e | 5.83 MiB | 6.28 MiB | 459.62 KiB |
| f42e9fb | 5.83 MiB | 6.28 MiB | 462.32 KiB |
| 192b44c | 5.53 MiB | 5.96 MiB | 444.33 KiB |
| c97f488 | 5.73 MiB | 6.17 MiB | 455.48 KiB |
| e0c8591 | 5.53 MiB | 5.96 MiB | 444.86 KiB |
| 396cb30 | 5.53 MiB | 6.01 MiB | 488.17 KiB |
| 73dca78 | 7.86 MiB | 9.44 MiB | 1.58 MiB |
Previous results on branch: lz/replay/http-data
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ade5c97 | 1250.85 ms | 1263.50 ms | 12.65 ms |
| 2330d71 | 1253.67 ms | 1258.29 ms | 4.62 ms |
| 4d340e9 | 1250.33 ms | 1251.24 ms | 0.92 ms |
| da583d9 | 1255.56 ms | 1264.43 ms | 8.86 ms |
| 509d5d7 | 1248.93 ms | 1249.53 ms | 0.60 ms |
| 2f41f60 | 1256.82 ms | 1260.91 ms | 4.09 ms |
| ce004ca | 1265.33 ms | 1273.13 ms | 7.81 ms |
| 3f8e3bf | 1244.57 ms | 1265.35 ms | 20.77 ms |
| 945b43c | 1251.87 ms | 1258.43 ms | 6.56 ms |
| 659ff1a | 1253.80 ms | 1251.54 ms | -2.26 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ade5c97 | 5.83 MiB | 6.28 MiB | 460.64 KiB |
| 2330d71 | 5.83 MiB | 6.28 MiB | 459.01 KiB |
| 4d340e9 | 5.83 MiB | 6.28 MiB | 460.55 KiB |
| da583d9 | 5.83 MiB | 6.28 MiB | 460.64 KiB |
| 509d5d7 | 5.83 MiB | 6.28 MiB | 460.64 KiB |
| 2f41f60 | 5.83 MiB | 6.28 MiB | 460.56 KiB |
| ce004ca | 5.83 MiB | 6.28 MiB | 460.86 KiB |
| 3f8e3bf | 5.83 MiB | 6.28 MiB | 459.01 KiB |
| 945b43c | 5.83 MiB | 6.28 MiB | 460.53 KiB |
| 659ff1a | 5.83 MiB | 6.28 MiB | 460.57 KiB |
If decoding the captured response body threw, captureResponse returned the original response whose stream was already drained by toBytes(), silently dropping the body for the caller. Forward the copied bytes regardless of whether the capture decode/truncate step succeeds. Co-Authored-By: Claude <noreply@anthropic.com>
Request/response bodies frequently contain PII, so only collect them when sendDefaultPii is enabled, matching the same gate used by FailedRequestClient. Also register a SentryFeatures constant and track it via options.sdk.addFeature so usage of this option shows up in SDK analytics, per the code guidelines. Co-Authored-By: Claude <noreply@anthropic.com>
Custom header names configured via networkRequestHeaders/ networkResponseHeaders (e.g. Authorization, Cookie) were captured regardless of sendDefaultPii, only the default content-type-like headers should be unconditional. Also mirror the same gate in the Android native sync so setNetworkCaptureBodies and the custom header lists can't bypass the Dart-side privacy check. Co-Authored-By: Claude <noreply@anthropic.com>
enableReplayNetworkDetailsCapturing is set
enableReplayNetworkDetailsCapturing is setReorders members to fields → constructor → public API → private helpers, per review feedback. Co-Authored-By: Claude <noreply@anthropic.com>
Only Cocoa needs a separate enable flag; gate purely on networkDetailAllowUrls being non-empty, matching other SDKs. Co-Authored-By: Claude <noreply@anthropic.com>
Avoids fully utf8-encoding the whole body just to check its size: _truncateBytes slices the Uint8List first, then decodes once. Also reads the request body via bodyBytes instead of the decoded body string. Co-Authored-By: Claude <noreply@anthropic.com>
Checks response.contentLength against maxBodySize upfront, avoiding buffering a body we already know will be discarded past the size we'd truncate it to anyway. Co-Authored-By: Claude <noreply@anthropic.com>
|
All CI failures on this PR are pre-existing on main |
Reading via toBytes() buffered the entire response before truncating, so a large or unbounded chunked body (no Content-Length) was fully materialized in memory even though only maxBodySize is ever captured. Add bufferStreamPrefix, which reads only up to the cap and replays the rest of the stream lazily to the original caller. Co-Authored-By: Claude <noreply@anthropic.com>
Groups networkDetailAllowUrls/DenyUrls, networkCaptureBodies, and networkRequestHeaders/ResponseHeaders under a new SentryReplayOptions in core, with SentryFlutterOptions.replay extending it, instead of sitting flat on SentryOptions. Co-Authored-By: Claude <noreply@anthropic.com>
Network capture for replay is a Flutter-only concern; sentry (dart) now only exposes an internal NetworkDetailsCapture interface plus a nullable hook on SentryOptions, mirroring the Transport seam. Co-Authored-By: Claude <noreply@anthropic.com>
FlutterNetworkDetailsCapture is built in _initDefaultValues, before the user's optionsConfiguration runs, so networkDetailAllowUrls was always empty at construction and the feature flag never got set. Co-Authored-By: Claude <noreply@anthropic.com>
|
@lucas-zimerman pls take a look at the latest AI reviews |
Returning the original response after a failed stream read left callers reading an already-consumed stream, masking the real error behind a confusing "already listened to" failure. Co-Authored-By: Claude <noreply@anthropic.com>
Stopping the stopwatch in `finally` meant response body capture (which can take arbitrarily long) was counted as request duration for allow-listed URLs, while uncaptured requests only measured time-to-headers. Stop it right after headers arrive instead. Co-Authored-By: Claude <noreply@anthropic.com>
Asserting against FlutterNetworkDetailsCapture.maxBodySize makes the test tautological: it can't fail if that constant itself were wrong, since it's the same value the production truncation logic uses. Co-Authored-By: Claude <noreply@anthropic.com>
A failure in captureResponse (e.g. reading the response stream for replay) was caught by the same handler as a failed _client.send, which set requestHadException and forced the http breadcrumb to error level even though the request itself succeeded. Only a transport-level failure should do that; a capture failure after a real response now leaves the breadcrumb level based on status code. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4707b53. Configure here.
Breadcrumb.http derives start_timestamp/end_timestamp from its timestamp param, which defaulted to "now" at breadcrumb construction in the outer finally - after response body capture completes. Once requestDuration stopped covering that capture time, both timestamps drifted by however long capture took instead of anchoring to when headers actually arrived, shifting Session Replay network spans to the end of body download. Co-Authored-By: Claude <noreply@anthropic.com>
getUtcDateTime() reads the system clock directly, unlike the rest of the SDK (e.g. Hub), which goes through SentryOptions.clock so it's fakeable in tests. Switch BreadcrumbClient to the same pattern and rewrite the timestamp regression test to assert call order via an injected clock instead of comparing against real wall-clock time. Co-Authored-By: Claude <noreply@anthropic.com>
buenaflor
left a comment
There was a problem hiding this comment.
there's one more AI review open
| ffi: ^2.0.0 | ||
| web: ^1.1.0 | ||
| jni: 0.14.2 | ||
| http: '>=0.13.0 <2.0.0' |
There was a problem hiding this comment.
is including this dependency necessary?
There was a problem hiding this comment.
I guess it's added anyway since dart also includes it?
There was a problem hiding this comment.
question is if we explicitly need to add it still

📜 Description
Adds opt-in capture of HTTP request/response headers and bodies for Session Replay network breadcrumbs, gated by
networkDetailAllowUrls/networkDetailDenyUrlsbeing non-empty, scoped to Dart-only r8equests.NetworkDetailsCapture(Dart) attaches the captured data to the http breadcrumb. On Android, the replay breadcrumb converter forwards this into theRRWebSpanEventunchanged, and the nativeSentryReplayOptionsare synced at init so the SDK reportsnetworkDetailHasUrls: truein the replay's options event — the Sentry frontend gates rendering of captured request/response detail on that flag.iOS and Web support are not included in this PR — the iOS native replay integration doesn't yet forward this data to the replay, so no platform-specific iOS work was added.
💡 Motivation and Context
Session Replay currently shows network spans without request/response detail. This lets users opt in to seeing headers and bodies alongside those spans for debugging, starting with Android.
💚 How did you test it?
NetworkDetailsCaptureandBreadcrumbClient.📝 Checklist
sendDefaultPiiis enabled🔮 Next steps
Changelog Entry
Add HTTP request/response header and body capture for Session Replay network breadcrumbs, gated by
networkDetailAllowUrls/networkDetailDenyUrls.