Skip to content

Conversation

@43jay
Copy link
Collaborator

@43jay 43jay commented Oct 9, 2025

📜 Description

Initalise DefaultReplayBreadcrumbConverter as the SDK-wide BeforeBreadcrumbCallback. It is responsible for delegating to a user-provided BeforeBreadcrumb.

Introduce new utility/data classes (io.sentry.util.network pkg), following format in the javascript SDK.

In BeforeBreadcrumb, NetworkDetailCaptureUtils extracts 'Network Details' data objects (NetworkRequestData, ReplayNetworkRequestOrResponse) and inserts back into the breadcrumb Hint (only sentry-okhttp for now).

When converting Breadcrumb to RRWebSpanEvent, extract NetworkRequestData data from Hint (name="replay:networkDetails") and insert into the replay performanceSpan when creating the replay segment.

TODOs

Feedback:

  • Are we handling 'enough' body types? Given there are so many diff types of request bodies ->
    Current impl handles JSON, UrlFormEncoded, skips these binary content types, and falls back to raw String as possible

  • Decide on keeping changes to sentry-samples-android

  • Will this work when the gradle plugin is used to add the SentryOkHttpInterceptor? This code implies we only send the breadcrumb w/ network details when that path isn't active.

Implementation:

  • Restrict size of breadcrumbsMap entries after data has been added to replay
  • Respect networkDetail* SDK Options flags
    • Will handle in a Future PR
  • handle case where SentryOkHttpEventListener handles http request instrumentation
    • Will handle in a Future PR
  • Unit tests

Pre-Land:

  • Go through commits and remove any testing-oriented ones
    • Remove FAKE_OPTIONS in SentryOkHttpInterceptor before landing // there for testing
  • Remove remaining unnecessary debug log statements

💡 Motivation and Context

Part of [Mobile Replay] Capture Network request/response bodies

Initially, we were trying to keep SDK changes simple and re-use the existing OKHTTP_REQUEST|RESPONSE hint data.
However, the :sentry-android-replay gradle module doesn't compile against any of the http libs (makes sense).

Because these okhttp3.Request, etc types don't exist in :sentry-android-replay, replay accesses the NetworkDetails data via Hint data ("replay:networkDetails") on the Breadcrumb, when the SDKOptions constraints have been met.

💚 How did you test it?

this replay session is an example recording with no networkDetail* SDK options enabled
(replay sessions below were captured without commit 4bc5b7714).

recording segment data shows sample replay payload .

replay session respects networkDetail[Request|Response]Headers

https://sentry-sdks.sentry.io/explore/replays/f89535ea0e79499ca8d75e34e6270925

image image
replay session captures GET request bodies as null

replay session
image

replay session captures JSON bodies

1. Request body formatted as JSON key/values
ref
image

2. Response body formatted as JSON key/values
ref
image

replay session captures plaintext bodies

replay session
image

replay session captures form bodies (formurlencoded)

replay session
image

replay session captures binary bodies

1. binary body under size limit (valid response size; summarized body content

https://sentry-sdks.sentry.io/explore/replays/baf899dd12c14718bc5d8b877a9fff3d
image

2. binary body over size limit (invalid response size, summarized body content
(https://sentry-sdks.sentry.io/explore/replays/94dc96ae2b8c4f4d8c23e2e0c69e5d1d)
image

replay session truncates plaintext bodies over 150KB (`MAX_NETWORK_BODY_SIZE`)

https://sentry-sdks.sentry.io/explore/replays/e11711a70c164a8fa1a2ed39e8632a05
image

replay sessions properly capture one-shot okhttp request bodies

One-shot HTTP Request Body request
image

Test: read(..) okhttp3.Request.body in an OkHttpInterceptor after cloning body into new request causes the request to succeed ✅ (current impl)

replay session

Valid response body logged to sentry:
image

Sanity Test: read(..) okhttp3.Request.body in an OkHttpInterceptor without cloning body into a new request causes the request to fail ✅

replay session

Invalid response body logged to sentry:
image

@linear
Copy link

linear bot commented Oct 9, 2025

@43jay 43jay marked this pull request as draft October 9, 2025 21:28
cursor[bot]

This comment was marked as outdated.

Comment on lines 404 to 406
options.setBeforeBreadcrumb(
replayBreadcrumbConverter
);
Copy link

Choose a reason for hiding this comment

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

Potential bug: A user-configured BeforeBreadcrumbCallback overwrites the SDK's replay converter during initialization, silently disabling network capture for replays.
  • Description: During SentryAndroid.init(), the DefaultReplayBreadcrumbConverter is set as the BeforeBreadcrumbCallback before the user's configuration callback is executed. If a user provides their own BeforeBreadcrumbCallback in the configuration, it replaces the SDK's converter. This breaks the replay feature's ability to capture network request data, as the logic in DefaultReplayBreadcrumbConverter is never called. The feature fails silently for any user following the common practice of setting a breadcrumb callback.

  • Suggested fix: Modify the initialization logic to chain the callbacks instead of overwriting. The DefaultReplayBreadcrumbConverter should be initialized with the user's callback, which is retrieved after the user's configuration has run. The converter would then execute the user's callback before its own logic.
    severity: 0.7, confidence: 0.99

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Member

Choose a reason for hiding this comment

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

this is legit, so we probably have to move setting the beforeBreadcrumb callback over to initializeIntegrationsAndProcessors which is called after the user config. You can access the converter via options.getReplayController().getBreadcrumbConverter later on

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

nice one 🙈 thanks

Copy link
Collaborator Author

@43jay 43jay Oct 24, 2025

Choose a reason for hiding this comment

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

import io.sentry.util.SpanUtils
import io.sentry.util.TracingUtils
import io.sentry.util.UrlUtils
import io.sentry.util.network.NetworkRequestData
Copy link
Member

Choose a reason for hiding this comment

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

these are missing from the PR, but I assume they are just data holders so nothing fancy there :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed

val networkData = createNetworkRequestData(request, response, requestBodySize, responseBodySize)
it.set("replay:networkDetails", networkData)

// it.set(OKHTTP_REQUEST, request)
Copy link
Member

Choose a reason for hiding this comment

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

we should probably still keep these because some of our customer may rely on them being present in the hint

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done


/**
* Extracts body metadata from OkHttp RequestBody or ResponseBody
* Note: We don't consume the actual body stream to avoid interfering with the request/response
Copy link
Member

@romtsn romtsn Oct 13, 2025

Choose a reason for hiding this comment

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

hm, is it not possible to consume/copy it? I think would be great if we could do that, given that the JS sdk does that too. Otherwise, it's probably not very helpful to just have the headers and some metadata

Copy link
Collaborator Author

@43jay 43jay Oct 24, 2025

Choose a reason for hiding this comment

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

fixed, but need to do some more testing

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed by copying out the body and creating a new request when networkCaptureBodies is true

// First try to get the structured network data from the hint
val networkDetails = breadcrumbHint.get("replay:networkDetails") as? NetworkRequestData
if (networkDetails != null) {
Log.d("SentryNetwork", "SentryNetwork: Found structured NetworkRequestData in hint: $networkDetails")
Copy link
Member

Choose a reason for hiding this comment

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

I guess you're gonna clean these up, but if you wanna keep some logs please use options.logger as it will no-op in production builds

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

cleaned up most, left in the rest to help with debugging. will remove / clean-up before landing (added to TODOs)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed. all unnecessary logs should be gone so lemme know if u see any i missed

43jay added 4 commits October 13, 2025 10:52
added some unit tests:
 ./gradlew :sentry-android-replay:testDebugUnitTest --tests="*DefaultReplayBreadcrumbConverterTest*"
Breadcrumb.java has several timestamp fields:
`timestamp: Date`, `timestampMs: Long`, `nanos: Long`

`hashcode` was relying solely on `timestamp`, which can be null depending on which constructor was used.

=> Change to use getTimestamp as 1. this is what equals does (consistency) 2. getTimestamp initialises timestamp if null.
@43jay 43jay changed the title RFF(replay): Adding OkHttp Request/Response bodies for sentry-java replay(feature): Adding OkHttp Request/Response bodies for sentry-java Oct 24, 2025
@43jay 43jay marked this pull request as ready for review October 24, 2025 15:50
Comment on lines 33 to 42
private val httpNetworkDetails =
Collections.synchronizedMap(
object : LinkedHashMap<Breadcrumb, NetworkRequestData>() {
override fun removeEldestEntry(
eldest: MutableMap.MutableEntry<Breadcrumb, NetworkRequestData>?
): Boolean {
return size > MAX_HTTP_NETWORK_DETAILS
}
}
)

Choose a reason for hiding this comment

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

Bug: Breadcrumb.equals() omits data map, causing HashMap key collisions for concurrent requests, leading to incorrect network data attribution.
Severity: CRITICAL | Confidence: 1.00

🔍 Detailed Analysis

The Breadcrumb.equals() method omits the data map from its comparison. This allows distinct Breadcrumb objects, particularly those generated by HTTP requests occurring within the same millisecond but having different URLs or other data map contents, to be considered equal. When these Breadcrumb objects are used as keys in the httpNetworkDetails LinkedHashMap, one entry can unintentionally overwrite another, causing network details from one request to be incorrectly associated with a different request during session replay processing.

💡 Suggested Fix

Modify Breadcrumb.equals() to include the data map in its comparison, or use a different key for httpNetworkDetails that uniquely identifies each network request, such as a UUID or a composite key including relevant data fields.

🤖 Prompt for AI Agent
Fix this bug. In
sentry-android-replay/src/main/java/io/sentry/android/replay/DefaultReplayBreadcrumbConverter.kt
at lines 33-42: The `Breadcrumb.equals()` method omits the `data` map from its
comparison. This allows distinct `Breadcrumb` objects, particularly those generated by
HTTP requests occurring within the same millisecond but having different URLs or other
`data` map contents, to be considered equal. When these `Breadcrumb` objects are used as
keys in the `httpNetworkDetails` `LinkedHashMap`, one entry can unintentionally
overwrite another, causing network details from one request to be incorrectly associated
with a different request during session replay processing.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@romtsn is there any issue with updating Breadcrumb#equals and Breadcrumb#hashmap to include data in the comparison?

I left it out in my commit cuz assumed we want Breadcrumbs to be equal if they have different data (e.g. it's the same breadcrumb if some more data gets added by a different caller at some point later)

cursor[bot]

This comment was marked as outdated.

Entrypoint is NetworkDetailCaptureUtils (initializeForUrl) called from SentryOkHttpInterceptor
- common logic to handle checking sdk options.
- Accept data from http client via NetworkBodyExtractor, NetworkHeaderExtractor interfaces
that can be reused in future (if needed)

Placeholder impl for req/resp bodies.

From https://docs.sentry.io/platforms/javascript/session-replay/configuration/
- networkDetailAllowUrls, networkDetailDenyUrls,
- networkCaptureBodies
- networkRequestHeaders, networkResponseHeaders

These SDKOptions don't exist yet => impl acts as if they do, but have not been enabled.
43jay added 5 commits October 24, 2025 15:38
…entries

Removes entry when creating RRWebSpanEvent
Uses syncrhonized LinkedHashMap with impl to cap size of map (avoid
memory bloat)
Replaces previous placeholder logic.
Now NetworkBodyParser uses io.sentry.JsonObjectReader
to extract body into JSONObject, JSONArray, with fallback to plain-text String (or nothing)
Linter / formatting / tests / comments
@43jay 43jay marked this pull request as draft October 24, 2025 21:19
@43jay

This comment was marked as resolved.

cursor[bot]

This comment was marked as outdated.

@43jay 43jay marked this pull request as ready for review October 27, 2025 19:33
Comment on lines 67 to 73
private val FAKE_OPTIONS = object {
val networkDetailAllowUrls: Array<String> = arrayOf(".*")
val networkDetailDenyUrls: Array<String>? = null
val networkCaptureBodies: Boolean = true
val networkRequestHeaders: Array<String> = arrayOf("User-Agent", "Accept", "sentry-trace", "Content-Type")
val networkResponseHeaders: Array<String> = arrayOf("User-Agent", "access-control-allow-origin", "x-ratelimit-resource")
}
Copy link

Choose a reason for hiding this comment

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

Bug: The FAKE_OPTIONS object is hardcoded for network detail capture, overriding user configuration.
Severity: CRITICAL | Confidence: 1.00

🔍 Detailed Analysis

The FAKE_OPTIONS object, intended for testing, is hardcoded and used for network detail capture. This causes the system to always use the test configuration, overriding any user-defined networkDetailAllowUrls, networkDetailDenyUrls, networkCaptureBodies, networkRequestHeaders, and networkResponseHeaders settings. Specifically, networkDetailAllowUrls is set to ".*" capturing all URLs, networkCaptureBodies is true always capturing bodies, and specific headers are hardcoded.

💡 Suggested Fix

Remove the FAKE_OPTIONS object and its usage. Implement the actual SentryOptions integration for network detail capture, or disable the feature until proper configuration is available.

🤖 Prompt for AI Agent
Fix this bug. In sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt
at lines 67-73: The `FAKE_OPTIONS` object, intended for testing, is hardcoded and used
for network detail capture. This causes the system to always use the test configuration,
overriding any user-defined `networkDetailAllowUrls`, `networkDetailDenyUrls`,
`networkCaptureBodies`, `networkRequestHeaders`, and `networkResponseHeaders` settings.
Specifically, `networkDetailAllowUrls` is set to `".*"` capturing all URLs,
`networkCaptureBodies` is `true` always capturing bodies, and specific headers are
hardcoded.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

addressed in 4bc5b77

cursor[bot]

This comment was marked as outdated.

43jay added 7 commits October 27, 2025 16:58
 https://github.com/getsentry/sentry-java/pull/4796/files#r2462011291

 Entirely removed any null checks - reading through the code everything is annotated @nonnull, and in the case where a NoOpReplayController could be initialised (believe this is unexpected as ReplayIntegration is always enabled) the NoOpBreadcrumbConverter implements BeforeBreadcrumbCallback and simply no-ops => nothing to crash
Initialize the values in FAKE_OPTIONS so that implementation acts as if dev never turned it on.

Leaves in the FAKE_OPTIONS variable to be easy to update reference to real `options` later on.
@43jay 43jay requested a review from romtsn October 27, 2025 21:26
@romtsn romtsn changed the title replay(feature): Adding OkHttp Request/Response bodies for sentry-java feat(replay): Adding OkHttp Request/Response bodies Oct 29, 2025
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.

3 participants