11package io.sentry.android.replay
22
33import io.sentry.Breadcrumb
4+ import io.sentry.Hint
45import io.sentry.ReplayBreadcrumbConverter
56import io.sentry.SentryLevel
7+ import io.sentry.SentryOptions
8+ import io.sentry.SentryOptions.BeforeBreadcrumbCallback
69import io.sentry.SpanDataConvention
10+ import io.sentry.TypeCheckHint.SENTRY_REPLAY_NETWORK_DETAILS
711import io.sentry.rrweb.RRWebBreadcrumbEvent
812import io.sentry.rrweb.RRWebEvent
913import io.sentry.rrweb.RRWebSpanEvent
14+ import io.sentry.util.network.NetworkRequestData
15+ import java.util.Collections
1016import kotlin.LazyThreadSafetyMode.NONE
1117
12- public open class DefaultReplayBreadcrumbConverter : ReplayBreadcrumbConverter {
18+ public open class DefaultReplayBreadcrumbConverter () : ReplayBreadcrumbConverter {
19+ private var options: SentryOptions ? = null
20+
21+ public constructor (options: SentryOptions ) : this () {
22+ // We modify options, so keep it around to make that explicit.
23+ this .options = options
24+ this .options?.beforeBreadcrumb = ReplayBeforeBreadcrumbCallback (options.beforeBreadcrumb)
25+ }
26+
1327 internal companion object {
28+ private const val MAX_HTTP_NETWORK_DETAILS = 32
1429 private val snakecasePattern by lazy(NONE ) { " _[a-z]" .toRegex() }
30+
1531 private val supportedNetworkData =
1632 HashSet <String >().apply {
1733 add(" status_code" )
@@ -23,16 +39,68 @@ public open class DefaultReplayBreadcrumbConverter : ReplayBreadcrumbConverter {
2339 }
2440 }
2541
42+ /* *
43+ * Intercept the breadcrumb to process any Network Details data on the hint. Delegate to any
44+ * user-provided callback to provide the actual breadcrumb to process.
45+ */
46+ private inner class ReplayBeforeBreadcrumbCallback (
47+ private val delegate : BeforeBreadcrumbCallback ?
48+ ) : BeforeBreadcrumbCallback {
49+ override fun execute (breadcrumb : Breadcrumb , hint : Hint ): Breadcrumb ? {
50+ val resultBreadcrumb =
51+ if (delegate != null ) {
52+ delegate.execute(breadcrumb, hint)
53+ } else {
54+ breadcrumb
55+ }
56+
57+ resultBreadcrumb?.let { finalBreadcrumb ->
58+ extractNetworkRequestDataFromHint(finalBreadcrumb, hint)?.let { networkData ->
59+ httpNetworkDetails[finalBreadcrumb] = networkData
60+ }
61+ }
62+
63+ return resultBreadcrumb
64+ }
65+
66+ private fun extractNetworkRequestDataFromHint (
67+ breadcrumb : Breadcrumb ,
68+ breadcrumbHint : Hint ,
69+ ): NetworkRequestData ? {
70+ if (breadcrumb.type != " http" && breadcrumb.category != " http" ) {
71+ return null
72+ }
73+
74+ return breadcrumbHint.get(SENTRY_REPLAY_NETWORK_DETAILS ) as ? NetworkRequestData
75+ }
76+ }
77+
2678 private var lastConnectivityState: String? = null
2779
80+ private val httpNetworkDetails =
81+ Collections .synchronizedMap(
82+ object : LinkedHashMap <Breadcrumb , NetworkRequestData >() {
83+ override fun removeEldestEntry (
84+ eldest : MutableMap .MutableEntry <Breadcrumb , NetworkRequestData >?
85+ ): Boolean {
86+ return size > MAX_HTTP_NETWORK_DETAILS
87+ }
88+ }
89+ )
90+
2891 override fun convert (breadcrumb : Breadcrumb ): RRWebEvent ? {
2992 var breadcrumbMessage: String? = null
30- var breadcrumbCategory: String? = null
93+ val breadcrumbCategory: String?
3194 var breadcrumbLevel: SentryLevel ? = null
3295 val breadcrumbData = mutableMapOf<String , Any ?>()
96+
3397 when {
3498 breadcrumb.category == " http" -> {
35- return if (breadcrumb.isValidForRRWebSpan()) breadcrumb.toRRWebSpanEvent() else null
99+ return if (breadcrumb.isValidForRRWebSpan()) {
100+ breadcrumb.toRRWebSpanEvent()
101+ } else {
102+ null
103+ }
36104 }
37105
38106 breadcrumb.type == " navigation" && breadcrumb.category == " app.lifecycle" -> {
@@ -42,6 +110,7 @@ public open class DefaultReplayBreadcrumbConverter : ReplayBreadcrumbConverter {
42110 breadcrumb.type == " navigation" && breadcrumb.category == " device.orientation" -> {
43111 breadcrumbCategory = breadcrumb.category!!
44112 val position = breadcrumb.data[" position" ]
113+
45114 if (position == " landscape" || position == " portrait" ) {
46115 breadcrumbData[" position" ] = position
47116 } else {
@@ -53,8 +122,9 @@ public open class DefaultReplayBreadcrumbConverter : ReplayBreadcrumbConverter {
53122 breadcrumbCategory = " navigation"
54123 breadcrumbData[" to" ] =
55124 when {
56- breadcrumb.data[" state" ] == " resumed" ->
125+ breadcrumb.data[" state" ] == " resumed" -> {
57126 (breadcrumb.data[" screen" ] as ? String )?.substringAfterLast(' .' )
127+ }
58128 " to" in breadcrumb.data -> breadcrumb.data[" to" ] as ? String
59129 else -> null
60130 } ? : return null
@@ -67,6 +137,7 @@ public open class DefaultReplayBreadcrumbConverter : ReplayBreadcrumbConverter {
67137 ? : breadcrumb.data[" view.tag" ]
68138 ? : breadcrumb.data[" view.class" ])
69139 as ? String ? : return null
140+
70141 breadcrumbData.putAll(breadcrumb.data)
71142 }
72143
@@ -75,18 +146,18 @@ public open class DefaultReplayBreadcrumbConverter : ReplayBreadcrumbConverter {
75146 breadcrumbData[" state" ] =
76147 when {
77148 breadcrumb.data[" action" ] == " NETWORK_LOST" -> " offline"
78- " network_type" in breadcrumb.data ->
149+ " network_type" in breadcrumb.data -> {
79150 if (! (breadcrumb.data[" network_type" ] as ? String ).isNullOrEmpty()) {
80151 breadcrumb.data[" network_type" ]
81152 } else {
82153 return null
83154 }
84-
155+ }
85156 else -> return null
86157 }
87158
88159 if (lastConnectivityState == breadcrumbData[" state" ]) {
89- // debounce same state
160+ // Debounce same state
90161 return null
91162 }
92163
@@ -105,6 +176,7 @@ public open class DefaultReplayBreadcrumbConverter : ReplayBreadcrumbConverter {
105176 breadcrumbData.putAll(breadcrumb.data)
106177 }
107178 }
179+
108180 return if (! breadcrumbCategory.isNullOrEmpty()) {
109181 RRWebBreadcrumbEvent ().apply {
110182 timestamp = breadcrumb.timestamp.time
@@ -120,29 +192,34 @@ public open class DefaultReplayBreadcrumbConverter : ReplayBreadcrumbConverter {
120192 }
121193 }
122194
123- private fun Breadcrumb.isValidForRRWebSpan (): Boolean =
124- ! (data[" url" ] as ? String ).isNullOrEmpty() &&
195+ private fun Breadcrumb.isValidForRRWebSpan (): Boolean {
196+ return ! (data[" url" ] as ? String ).isNullOrEmpty() &&
125197 SpanDataConvention .HTTP_START_TIMESTAMP in data &&
126198 SpanDataConvention .HTTP_END_TIMESTAMP in data
199+ }
127200
128- private fun String.snakeToCamelCase (): String =
129- replace(snakecasePattern) { it.value.last().toString().uppercase() }
201+ private fun String.snakeToCamelCase (): String {
202+ return replace(snakecasePattern) { it.value.last().toString().uppercase() }
203+ }
130204
131205 private fun Breadcrumb.toRRWebSpanEvent (): RRWebSpanEvent {
132206 val breadcrumb = this
133207 val httpStartTimestamp = breadcrumb.data[SpanDataConvention .HTTP_START_TIMESTAMP ]
134208 val httpEndTimestamp = breadcrumb.data[SpanDataConvention .HTTP_END_TIMESTAMP ]
209+
135210 return RRWebSpanEvent ().apply {
136211 timestamp = breadcrumb.timestamp.time
137212 op = " resource.http"
138213 description = breadcrumb.data[" url" ] as String
139- // can be double if it was serialized to disk
214+
215+ // Can be double if it was serialized to disk
140216 startTimestamp =
141217 if (httpStartTimestamp is Double ) {
142218 httpStartTimestamp / 1000.0
143219 } else {
144220 (httpStartTimestamp as Long ) / 1000.0
145221 }
222+
146223 endTimestamp =
147224 if (httpEndTimestamp is Double ) {
148225 httpEndTimestamp / 1000.0
@@ -151,13 +228,54 @@ public open class DefaultReplayBreadcrumbConverter : ReplayBreadcrumbConverter {
151228 }
152229
153230 val breadcrumbData = mutableMapOf<String , Any ?>()
231+
232+ val networkDetailData = httpNetworkDetails.remove(breadcrumb)
233+
234+ // Add Network Details data when available
235+ networkDetailData?.let { networkData ->
236+ networkData.method?.let { breadcrumbData[" method" ] = it }
237+ networkData.statusCode?.let { breadcrumbData[" statusCode" ] = it }
238+ networkData.requestBodySize?.let { breadcrumbData[" requestBodySize" ] = it }
239+ networkData.responseBodySize?.let { breadcrumbData[" responseBodySize" ] = it }
240+
241+ networkData.request?.let { request ->
242+ val requestData = mutableMapOf<String , Any ?>()
243+ request.size?.let { requestData[" size" ] = it }
244+ request.body?.let { requestData[" body" ] = it.value }
245+
246+ if (request.headers.isNotEmpty()) {
247+ requestData[" headers" ] = request.headers
248+ }
249+
250+ if (requestData.isNotEmpty()) {
251+ breadcrumbData[" request" ] = requestData
252+ }
253+ }
254+
255+ networkData.response?.let { response ->
256+ val responseData = mutableMapOf<String , Any ?>()
257+ response.size?.let { responseData[" size" ] = it }
258+ response.body?.let { responseData[" body" ] = it.value }
259+
260+ if (response.headers.isNotEmpty()) {
261+ responseData[" headers" ] = response.headers
262+ }
263+
264+ if (responseData.isNotEmpty()) {
265+ breadcrumbData[" response" ] = responseData
266+ }
267+ }
268+ }
269+
270+ // Original breadcrumb http data
154271 for ((key, value) in breadcrumb.data) {
155272 if (key in supportedNetworkData) {
156- breadcrumbData[
157- key.replace(" content_length" , " body_size" ).substringAfter(" ." ).snakeToCamelCase(),
158- ] = value
273+ val formattedKey =
274+ key.replace(" content_length" , " body_size" ).substringAfter(" ." ).snakeToCamelCase()
275+ breadcrumbData[formattedKey ] = value
159276 }
160277 }
278+
161279 data = breadcrumbData
162280 }
163281 }
0 commit comments