-
-
Notifications
You must be signed in to change notification settings - Fork 458
replay(feature): Adding OkHttp Request/Response bodies for sentry-java #4796
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b8555e2
ca28040
5397e9d
71ed70e
f2ce22e
201102a
724ec42
2d08e7b
ebc5ff3
122a8a6
6964a53
25c42c7
308072b
5836ef1
d9f8254
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ | |
| import io.sentry.NoOpSocketTagger; | ||
| import io.sentry.NoOpTransactionProfiler; | ||
| import io.sentry.NoopVersionDetector; | ||
| import io.sentry.ReplayBreadcrumbConverter; | ||
| import io.sentry.ScopeType; | ||
| import io.sentry.SendFireAndForgetEnvelopeSender; | ||
| import io.sentry.SendFireAndForgetOutboxSender; | ||
|
|
@@ -247,6 +248,14 @@ static void initializeIntegrationsAndProcessors( | |
| options.setCompositePerformanceCollector(new DefaultCompositePerformanceCollector(options)); | ||
| } | ||
|
|
||
| if (options.getReplayController() != null) { // TODO: Triple-check this should be a null check | ||
| ReplayBreadcrumbConverter replayBreadcrumbConverter = options.getReplayController().getBreadcrumbConverter(); | ||
| replayBreadcrumbConverter.setUserBeforeBreadcrumbCallback(options.getBeforeBreadcrumb()); | ||
| options.setBeforeBreadcrumb( | ||
| replayBreadcrumbConverter | ||
| ); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| // Check if the profiler was already instantiated in the app start. | ||
| // We use the Android profiler, that uses a global start/stop api, so we need to preserve the | ||
| // state of the profiler, and it's only possible retaining the instance. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,22 @@ | ||
| package io.sentry.android.replay | ||
|
|
||
| import android.util.Log | ||
| import io.sentry.Breadcrumb | ||
| import io.sentry.Hint | ||
| import io.sentry.ReplayBreadcrumbConverter | ||
| import io.sentry.SentryLevel | ||
| import io.sentry.SentryOptions.BeforeBreadcrumbCallback | ||
| import io.sentry.SpanDataConvention | ||
| import io.sentry.rrweb.RRWebBreadcrumbEvent | ||
| import io.sentry.rrweb.RRWebEvent | ||
| import io.sentry.rrweb.RRWebSpanEvent | ||
| import io.sentry.util.network.NetworkRequestData | ||
| import java.util.Collections | ||
| import kotlin.LazyThreadSafetyMode.NONE | ||
|
|
||
| public open class DefaultReplayBreadcrumbConverter : ReplayBreadcrumbConverter { | ||
| internal companion object { | ||
| private const val MAX_HTTP_NETWORK_DETAILS = 32 | ||
| private val snakecasePattern by lazy(NONE) { "_[a-z]".toRegex() } | ||
| private val supportedNetworkData = | ||
| HashSet<String>().apply { | ||
|
|
@@ -24,10 +30,21 @@ public open class DefaultReplayBreadcrumbConverter : ReplayBreadcrumbConverter { | |
| } | ||
|
|
||
| private var lastConnectivityState: String? = null | ||
| private val httpNetworkDetails = | ||
| Collections.synchronizedMap( | ||
| object : LinkedHashMap<Breadcrumb, NetworkRequestData>() { | ||
| override fun removeEldestEntry( | ||
| eldest: MutableMap.MutableEntry<Breadcrumb, NetworkRequestData>? | ||
| ): Boolean { | ||
| return size > MAX_HTTP_NETWORK_DETAILS | ||
| } | ||
| } | ||
| ) | ||
|
Comment on lines
+33
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: 🔍 Detailed AnalysisThe 💡 Suggested FixModify 🤖 Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 I left it out in my commit cuz assumed we want Breadcrumbs to be equal if they have different |
||
| private var userBeforeBreadcrumbCallback: BeforeBreadcrumbCallback? = null | ||
|
|
||
| override fun convert(breadcrumb: Breadcrumb): RRWebEvent? { | ||
| var breadcrumbMessage: String? = null | ||
| var breadcrumbCategory: String? = null | ||
| val breadcrumbCategory: String? | ||
| var breadcrumbLevel: SentryLevel? = null | ||
| val breadcrumbData = mutableMapOf<String, Any?>() | ||
| when { | ||
|
|
@@ -120,10 +137,76 @@ public open class DefaultReplayBreadcrumbConverter : ReplayBreadcrumbConverter { | |
| } | ||
| } | ||
|
|
||
| private fun Breadcrumb.isValidForRRWebSpan(): Boolean = | ||
| !(data["url"] as? String).isNullOrEmpty() && | ||
| SpanDataConvention.HTTP_START_TIMESTAMP in data && | ||
| SpanDataConvention.HTTP_END_TIMESTAMP in data | ||
| override fun setUserBeforeBreadcrumbCallback(beforeBreadcrumbCallback: BeforeBreadcrumbCallback?) { | ||
| this.userBeforeBreadcrumbCallback = beforeBreadcrumbCallback | ||
| } | ||
|
|
||
| /** Delegate to user-provided callback (if exists) to provide the final breadcrumb to process. */ | ||
| override fun execute(breadcrumb: Breadcrumb, hint: Hint): Breadcrumb? { | ||
| val callback = userBeforeBreadcrumbCallback | ||
| val result = | ||
| if (callback != null) { | ||
| callback.execute(breadcrumb, hint) | ||
| } else { | ||
| breadcrumb | ||
| } | ||
|
|
||
| result?.let { finalBreadcrumb -> | ||
| extractNetworkRequestDataFromHint(finalBreadcrumb, hint)?.let { networkData -> | ||
| httpNetworkDetails[finalBreadcrumb] = networkData | ||
| } | ||
| } | ||
|
|
||
| Log.d( | ||
| "SentryNetwork", | ||
| "SentryNetwork: BeforeBreadcrumbCallback - Hint: $hint, Breadcrumb: $result", | ||
| ) | ||
| return result | ||
| } | ||
|
|
||
| private fun extractNetworkRequestDataFromHint( | ||
| breadcrumb: Breadcrumb, | ||
| breadcrumbHint: Hint, | ||
| ): NetworkRequestData? { | ||
| if (breadcrumb.type != "http" && breadcrumb.category != "http") { | ||
| return null | ||
| } | ||
|
|
||
| val networkDetails = breadcrumbHint.get("replay:networkDetails") as? NetworkRequestData | ||
| if (networkDetails != null) { | ||
| Log.d( | ||
| "SentryNetwork", | ||
| "SentryNetwork: Found structured NetworkRequestData in hint: $networkDetails", | ||
| ) | ||
| return networkDetails | ||
| } | ||
|
|
||
| Log.d("SentryNetwork", "SentryNetwork: No structured NetworkRequestData found on hint") | ||
| return null | ||
| } | ||
|
|
||
| private fun Breadcrumb.isValidForRRWebSpan(): Boolean { | ||
| val url = data["url"] as? String | ||
| val hasStartTimestamp = SpanDataConvention.HTTP_START_TIMESTAMP in data | ||
| val hasEndTimestamp = SpanDataConvention.HTTP_END_TIMESTAMP in data | ||
|
|
||
| val urlValid = !url.isNullOrEmpty() | ||
| val isValid = urlValid && hasStartTimestamp && hasEndTimestamp | ||
|
|
||
| val reasons = mutableListOf<String>() | ||
| if (!urlValid) reasons.add("missing or empty URL") | ||
| if (!hasStartTimestamp) reasons.add("missing start timestamp") | ||
| if (!hasEndTimestamp) reasons.add("missing end timestamp") | ||
|
|
||
| Log.d( | ||
| "SentryReplay", | ||
| "Breadcrumb RRWeb span validation: ${if (isValid) "VALID" else "INVALID"}" + | ||
| if (!isValid) " (${reasons.joinToString(", ")})" | ||
| else "" + " - URL: ${url ?: "null"}, Category: ${category}", | ||
| ) | ||
|
|
||
| return isValid | ||
| } | ||
|
|
||
| private fun String.snakeToCamelCase(): String = | ||
| replace(snakecasePattern) { it.value.last().toString().uppercase() } | ||
|
|
@@ -132,6 +215,16 @@ public open class DefaultReplayBreadcrumbConverter : ReplayBreadcrumbConverter { | |
| val breadcrumb = this | ||
| val httpStartTimestamp = breadcrumb.data[SpanDataConvention.HTTP_START_TIMESTAMP] | ||
| val httpEndTimestamp = breadcrumb.data[SpanDataConvention.HTTP_END_TIMESTAMP] | ||
|
|
||
| // Get the NetworkRequestData if available and remove it from the map | ||
| val networkDetailData = httpNetworkDetails.remove(breadcrumb) | ||
|
|
||
| Log.d( | ||
| "SentryNetwork", | ||
| "SentryNetwork: convert(breadcrumb=${breadcrumb.type}) httpNetworkDetails map size: ${httpNetworkDetails.size}, " + | ||
| "found network data for current breadcrumb: ${networkDetailData != null}", | ||
| ) | ||
|
|
||
| return RRWebSpanEvent().apply { | ||
| timestamp = breadcrumb.timestamp.time | ||
| op = "resource.http" | ||
|
|
@@ -151,13 +244,47 @@ public open class DefaultReplayBreadcrumbConverter : ReplayBreadcrumbConverter { | |
| } | ||
|
|
||
| val breadcrumbData = mutableMapOf<String, Any?>() | ||
|
|
||
| // Add data from NetworkRequestData if available | ||
| networkDetailData?.let { networkData -> | ||
| networkData.method?.let { breadcrumbData["method"] = it } | ||
| networkData.statusCode?.let { breadcrumbData["statusCode"] = it } | ||
| networkData.requestBodySize?.let { breadcrumbData["requestBodySize"] = it } | ||
| networkData.responseBodySize?.let { breadcrumbData["responseBodySize"] = it } | ||
|
|
||
| networkData.request?.let { request -> | ||
| val requestData = mutableMapOf<String, Any?>() | ||
| request.size?.let { requestData["size"] = it } | ||
| request.body?.let { requestData["body"] = it } | ||
| if (request.headers.isNotEmpty()) { | ||
| requestData["headers"] = request.headers | ||
| } | ||
| if (requestData.isNotEmpty()) { | ||
| breadcrumbData["request"] = requestData | ||
| } | ||
| } | ||
|
|
||
| networkData.response?.let { response -> | ||
| val responseData = mutableMapOf<String, Any?>() | ||
| response.size?.let { responseData["size"] = it } | ||
| response.body?.let { responseData["body"] = it } | ||
| if (response.headers.isNotEmpty()) { | ||
| responseData["headers"] = response.headers | ||
| } | ||
| if (responseData.isNotEmpty()) { | ||
| breadcrumbData["response"] = responseData | ||
| } | ||
| } | ||
| } | ||
| // Original breadcrumb http data | ||
| for ((key, value) in breadcrumb.data) { | ||
| if (key in supportedNetworkData) { | ||
| breadcrumbData[ | ||
| key.replace("content_length", "body_size").substringAfter(".").snakeToCamelCase(), | ||
| ] = value | ||
| } | ||
| } | ||
|
|
||
| data = breadcrumbData | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@roman can you be my triple-check? the code above uses
instanceof, but afaict the NoOpReplayController always gets overridden so i put a null check here