Skip to content

feat(replay): Add request details to Session Replay on Android - #3875

Open
lucas-zimerman wants to merge 29 commits into
mainfrom
lz/replay/http-data
Open

feat(replay): Add request details to Session Replay on Android #3875
lucas-zimerman wants to merge 29 commits into
mainfrom
lz/replay/http-data

Conversation

@lucas-zimerman

@lucas-zimerman lucas-zimerman commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

📜 Description

Adds opt-in capture of HTTP request/response headers and bodies for Session Replay network breadcrumbs, gated by networkDetailAllowUrls/networkDetailDenyUrls being 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 the RRWebSpanEvent unchanged, and the native SentryReplayOptions are synced 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.

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?

  • Added unit tests for NetworkDetailsCapture and BreadcrumbClient.
  • Manually verified via the example app's "Dart: Web request with network details" button on Android, confirming the captured request/response data appears in the http breadcrumb and is forwarded to the replay.

📝 Checklist

  • I reviewed submitted code
  • I added tests to verify changes
  • No new PII added or SDK only sends newly added PII if sendDefaultPii is enabled
  • I updated the docs if needed
  • All tests passing
  • Public API changes reviewed by another Mobile SDK team member or implemented according to the develop docs spec
  • No breaking changes

🔮 Next steps

  • Extend native forwarding to iOS once the native replay integration supports it.

Changelog Entry

Add HTTP request/response header and body capture for Session Replay network breadcrumbs, gated by networkDetailAllowUrls/networkDetailDenyUrls.

lucas-zimerman and others added 2 commits July 9, 2026 22:28
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>
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

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


Features

Flutter

  • Extend standalone app start by buenaflor in #3918
  • Add standalone app start tracing by buenaflor in #3896

Other

  • Add HTTP request/response header and body capture for Session Replay network breadcrumbs, gated by networkDetailAllowUrls/networkDetailDenyUrls. by lucas-zimerman in #3875

Fixes

  • Read normalized rate limit headers by sentry-junior in #3883

Dependencies

  • chore(deps): update Android SDK to v8.50.1 by github-actions in #3921

Internal Changes

  • (deps) Pin Flutter development dependencies by buenaflor in #3913
  • (grpc) Move MockHub to _sentry_testing package by lucas-zimerman in #3908

🤖 This preview updates automatically when you update the PR.

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.35802% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.95%. Comparing base (f4a14f7) to head (f57a46e).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...lutter/lib/src/replay/network_details_capture.dart 92.42% 5 Missing ⚠️
...es/dart/lib/src/http_client/breadcrumb_client.dart 83.33% 2 Missing ⚠️
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     
Flag Coverage Δ
sentry 87.64% <86.66%> (+0.01%) ⬆️
sentry_dio 97.73% <ø> (ø)
sentry_drift 93.57% <ø> (ø)
sentry_file 65.29% <ø> (ø)
sentry_firebase_remote_config 100.00% <ø> (ø)
sentry_flutter 92.17% <92.42%> (+<0.01%) ⬆️
sentry_grpc 99.09% <ø> (ø)
sentry_hive 77.48% <ø> (ø)
sentry_isar 74.37% <ø> (?)
sentry_link 21.50% <ø> (ø)
sentry_logging 97.01% <ø> (ø)
sentry_sqflite 88.81% <ø> (ø)
sentry_supabase 97.27% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

Comment thread packages/dart/lib/src/http_client/network_details_capture.dart Outdated
Comment thread packages/dart/lib/src/http_client/network_details_capture.dart Outdated
Comment thread packages/dart/lib/src/http_client/network_details_capture.dart Outdated
Comment thread packages/dart/lib/src/http_client/network_details_capture.dart Outdated
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Android Performance metrics 🚀

  Plain With Sentry Diff
Startup time 384.96 ms 402.37 ms 17.41 ms
Size 14.55 MiB 15.87 MiB 1.31 MiB

Baseline results on branch: main

Startup times

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

Comment thread packages/dart/lib/src/sentry_options.dart Outdated
Comment thread packages/dart/lib/src/sentry_options.dart Outdated
Comment thread packages/dart/test/http_client/network_details_capture_test.dart Outdated
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

iOS Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1260.98 ms 1267.02 ms 6.04 ms
Size 5.83 MiB 6.28 MiB 460.13 KiB

Baseline results on branch: main

Startup times

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

lucas-zimerman and others added 2 commits July 10, 2026 19:56
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>
Comment thread packages/dart/lib/src/http_client/sentry_http_client.dart Outdated
Comment thread packages/flutter/lib/src/native/java/sentry_native_java_init.dart Outdated
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>
@lucas-zimerman lucas-zimerman changed the title feat(replay): Add HTTP request/response capture (Android) feat(replay): Add HTTP request/response to Session Replay on Android when enableReplayNetworkDetailsCapturing is set Jul 10, 2026
@lucas-zimerman lucas-zimerman changed the title feat(replay): Add HTTP request/response to Session Replay on Android when enableReplayNetworkDetailsCapturing is set feat(replay): Add request details to Session Replay on Android Jul 10, 2026
Comment thread packages/dart/lib/src/http_client/network_details_capture.dart Outdated
Comment thread packages/dart/lib/src/http_client/network_details_capture.dart Outdated
Comment thread packages/dart/lib/src/http_client/network_details_capture.dart Outdated
Comment thread packages/dart/lib/src/http_client/network_details_capture.dart Outdated
Comment thread packages/dart/lib/src/http_client/network_details_capture.dart Outdated
Comment thread packages/dart/lib/src/http_client/network_details_capture.dart Outdated
Comment thread packages/dart/lib/src/http_client/network_details_capture.dart Outdated
lucas-zimerman and others added 5 commits July 13, 2026 20:45
Reorders 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>
@lucas-zimerman

Copy link
Copy Markdown
Collaborator Author

All CI failures on this PR are pre-existing on main

@lucas-zimerman
lucas-zimerman requested a review from buenaflor July 14, 2026 01:48
Comment thread packages/dart/lib/src/http_client/network_details_capture.dart Outdated
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>
Comment thread packages/dart/lib/src/http_client/network_details_capture.dart Outdated
Comment thread packages/dart/lib/src/utils/stream_utils.dart Outdated
Comment thread packages/dart/lib/src/http_client/breadcrumb_client.dart Outdated
@lucas-zimerman
lucas-zimerman requested a review from buenaflor July 17, 2026 01:10
Comment thread packages/dart/lib/src/sentry_options.dart Outdated
Comment thread packages/dart/lib/src/http_client/network_details_capture.dart Outdated
Comment thread packages/dart/lib/src/sentry_options.dart Outdated
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>
Comment thread packages/dart/lib/src/replay/sentry_replay_options.dart Outdated
Comment thread packages/dart/lib/src/sentry_options.dart Outdated
Comment thread packages/dart/test/http_client/network_details_capture_test.dart Outdated
Comment thread packages/flutter/lib/src/sentry_replay_options.dart Outdated
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>
Comment thread packages/flutter/lib/src/replay/network_details_capture.dart Outdated
Comment thread packages/flutter/lib/src/replay/network_details_capture.dart Outdated
Comment thread packages/flutter/lib/src/replay/network_details_capture.dart Outdated
Comment thread packages/dart/lib/src/http_client/breadcrumb_client.dart
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

Copy link
Copy Markdown
Collaborator Author

Comment thread packages/flutter/lib/src/replay/network_details_capture.dart
Comment thread packages/flutter/lib/src/replay/network_details_capture.dart
@lucas-zimerman
lucas-zimerman requested a review from buenaflor July 24, 2026 02:10
Comment thread packages/flutter/example/lib/main.dart
Comment thread packages/flutter/test/replay/network_details_capture_test.dart Outdated
@buenaflor

Copy link
Copy Markdown
Contributor

@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>
Comment thread packages/dart/lib/src/http_client/breadcrumb_client.dart Outdated
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>
Comment thread packages/dart/lib/src/http_client/breadcrumb_client.dart Outdated
lucas-zimerman and others added 2 commits July 24, 2026 21:43
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>

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

There are 3 total unresolved issues (including 2 from previous reviews).

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 4707b53. Configure here.

Comment thread packages/dart/lib/src/http_client/breadcrumb_client.dart
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>
Comment thread packages/flutter/lib/src/sentry_flutter.dart
Comment thread packages/dart/test/http_client/breadcrumb_client_test.dart Outdated
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 buenaflor 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.

there's one more AI review open

@lucas-zimerman
lucas-zimerman requested a review from buenaflor July 28, 2026 15:05
ffi: ^2.0.0
web: ^1.1.0
jni: 0.14.2
http: '>=0.13.0 <2.0.0'

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.

is including this dependency necessary?

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.

I guess it's added anyway since dart also includes it?

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.

question is if we explicitly need to add it still

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants