Skip to content

Commit 7aec744

Browse files
committed
Update okhttp span/breadcrumbs in case interceptors change the request
1 parent 1a52aa2 commit 7aec744

File tree

4 files changed

+112
-10
lines changed

4 files changed

+112
-10
lines changed

sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,28 +28,59 @@ internal class SentryOkHttpEvent(private val scopes: IScopes, private val reques
2828
private var response: Response? = null
2929
private var clientErrorResponse: Response? = null
3030
private val isEventFinished = AtomicBoolean(false)
31-
private val url: String
32-
private val method: String
31+
private var url: String
32+
private var method: String
3333

3434
init {
3535
val urlDetails = UrlUtils.parse(request.url.toString())
3636
url = urlDetails.urlOrFallback
37-
val host: String = request.url.host
38-
val encodedPath: String = request.url.encodedPath
3937
method = request.method
4038

4139
// We start the call span that will contain all the others
4240
val parentSpan = if (Platform.isAndroid()) scopes.transaction else scopes.span
43-
callSpan = parentSpan?.startChild("http.client", "$method $url")
41+
callSpan = parentSpan?.startChild("http.client")
4442
callSpan?.spanContext?.origin = TRACE_ORIGIN
43+
44+
breadcrumb = Breadcrumb().apply {
45+
type = "http"
46+
category = "http"
47+
// needs this as unix timestamp for rrweb
48+
setData(
49+
SpanDataConvention.HTTP_START_TIMESTAMP,
50+
CurrentDateProvider.getInstance().currentTimeMillis
51+
)
52+
}
53+
54+
setRequest(request)
55+
}
56+
57+
/**
58+
* Sets the request.
59+
* This function may be called multiple times in case the request changes e.g. due to interceptors.
60+
*/
61+
fun setRequest(request: Request) {
62+
val urlDetails = UrlUtils.parse(request.url.toString())
63+
url = urlDetails.urlOrFallback
64+
65+
val host: String = request.url.host
66+
val encodedPath: String = request.url.encodedPath
67+
method = request.method
68+
69+
callSpan?.description = "$method $url"
4570
urlDetails.applyToSpan(callSpan)
4671

47-
// We setup a breadcrumb with all meaningful data
48-
breadcrumb = Breadcrumb.http(url, method)
4972
breadcrumb.setData("host", host)
5073
breadcrumb.setData("path", encodedPath)
51-
// needs this as unix timestamp for rrweb
52-
breadcrumb.setData(SpanDataConvention.HTTP_START_TIMESTAMP, CurrentDateProvider.getInstance().currentTimeMillis)
74+
if (urlDetails.url != null) {
75+
breadcrumb.setData("url", urlDetails.url!!)
76+
}
77+
breadcrumb.setData("method", method.uppercase())
78+
if (urlDetails.query != null) {
79+
breadcrumb.setData("http.query", urlDetails.query!!)
80+
}
81+
if (urlDetails.fragment != null) {
82+
breadcrumb.setData("http.fragment", urlDetails.fragment!!)
83+
}
5384

5485
// We add the same data to the call span
5586
callSpan?.setData("url", url)

sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ public open class SentryOkHttpInterceptor(
8181
val parentSpan = if (Platform.isAndroid()) scopes.transaction else scopes.span
8282
span = parentSpan?.startChild("http.client", "$method $url")
8383
}
84+
85+
// interceptors may change the request details, so let's update it here
86+
// this only works correctly if SentryOkHttpInterceptor is the last one in the chain
87+
okHttpEvent?.setRequest(request)
8488
val startTimestamp = CurrentDateProvider.getInstance().currentTimeMillis
8589

8690
span?.spanContext?.origin = TRACE_ORIGIN

sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import io.sentry.exception.SentryHttpClientException
1717
import io.sentry.test.getProperty
1818
import okhttp3.Protocol
1919
import okhttp3.Request
20+
import okhttp3.RequestBody.Companion.toRequestBody
2021
import okhttp3.Response
2122
import okhttp3.mockwebserver.MockWebServer
2223
import org.mockito.kotlin.any
@@ -234,6 +235,35 @@ class SentryOkHttpEventTest {
234235
)
235236
}
236237

238+
@Test
239+
fun `setRequest updates both breadcrumb and span data`() {
240+
val sut = fixture.getSut()
241+
242+
sut.setRequest(
243+
Request.Builder()
244+
.post("".toRequestBody())
245+
.url("https://foo.bar/updated")
246+
.build()
247+
)
248+
sut.finish()
249+
250+
verify(fixture.scopes).addBreadcrumb(
251+
check<Breadcrumb> {
252+
assertEquals("https://foo.bar/updated", it.data["url"])
253+
assertEquals("foo.bar", it.data["host"])
254+
assertEquals("/updated", it.data["path"])
255+
assertEquals("POST", it.data["method"])
256+
},
257+
any()
258+
)
259+
260+
assertNotNull(sut.callSpan)
261+
assertEquals("/updated", sut.callSpan.getData("path"))
262+
assertEquals("POST", sut.callSpan.getData("http.request.method"))
263+
assertEquals("foo.bar", sut.callSpan.getData("host"))
264+
assertEquals("https://foo.bar/updated", sut.callSpan.getData("url"))
265+
}
266+
237267
@Test
238268
fun `when finish multiple times, only one breadcrumb is captured`() {
239269
val sut = fixture.getSut()

sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ import io.sentry.TransactionContext
2121
import io.sentry.TypeCheckHint
2222
import io.sentry.exception.SentryHttpClientException
2323
import io.sentry.mockServerRequestTimeoutMillis
24+
import okhttp3.EventListener
2425
import okhttp3.Interceptor
2526
import okhttp3.MediaType.Companion.toMediaType
2627
import okhttp3.OkHttpClient
2728
import okhttp3.Request
2829
import okhttp3.RequestBody
2930
import okhttp3.RequestBody.Companion.toRequestBody
31+
import okhttp3.Response
3032
import okhttp3.mockwebserver.MockResponse
3133
import okhttp3.mockwebserver.MockWebServer
3234
import okhttp3.mockwebserver.SocketPolicy
@@ -75,6 +77,8 @@ class SentryOkHttpInterceptorTest {
7577
)
7678
),
7779
sendDefaultPii: Boolean = false,
80+
eventListener: EventListener? = null,
81+
additionalInterceptors: List<Interceptor> = emptyList(),
7882
optionsConfiguration: Sentry.OptionsConfiguration<SentryOptions>? = null
7983
): OkHttpClient {
8084
options = SentryOptions().also {
@@ -120,7 +124,15 @@ class SentryOkHttpInterceptorTest {
120124
failedRequestStatusCodes = failedRequestStatusCodes
121125
)
122126
}
123-
return OkHttpClient.Builder().addInterceptor(interceptor).build()
127+
return OkHttpClient.Builder().apply {
128+
if (eventListener != null) {
129+
eventListener(eventListener)
130+
}
131+
for (additionalInterceptor in additionalInterceptors) {
132+
addInterceptor(additionalInterceptor)
133+
}
134+
addInterceptor(interceptor)
135+
}.build()
124136
}
125137
}
126138

@@ -613,4 +625,29 @@ class SentryOkHttpInterceptorTest {
613625
call.execute()
614626
verify(event).finish()
615627
}
628+
629+
@Test
630+
fun `when an interceptor changes the request, the event is updated correctly`() {
631+
val client = fixture.getSut(
632+
eventListener = SentryOkHttpEventListener(fixture.scopes),
633+
additionalInterceptors = listOf(
634+
object : Interceptor {
635+
override fun intercept(chain: Interceptor.Chain): Response {
636+
return chain.proceed(
637+
chain.request().newBuilder()
638+
.url(chain.request().url.newBuilder().addPathSegment("v1").build())
639+
.build()
640+
)
641+
}
642+
}
643+
)
644+
)
645+
646+
val request = getRequest("/hello/")
647+
val call = client.newCall(request)
648+
call.execute()
649+
650+
val okHttpEvent = SentryOkHttpEventListener.eventMap[call]!!
651+
assertEquals(fixture.server.url("/hello/v1").toUrl().toString(), okHttpEvent.callSpan!!.getData("url"))
652+
}
616653
}

0 commit comments

Comments
 (0)