Skip to content

Commit fe90ed9

Browse files
authored
Add OkHttp event spans (#2659)
* made SentryOkHttpInterceptor work nicely with SentryOkHttpEventListener * added SentryOkHttpEventListener, with an optional instance of EventListener to propagate calls to * added SentryOkHttpEvent * updated span data to align with https://develop.sentry.dev/sdk/performance/span-data-conventions/
1 parent 37cd75a commit fe90ed9

File tree

10 files changed

+1447
-32
lines changed

10 files changed

+1447
-32
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@
2424

2525
### Features
2626

27+
- More granular http requests instrumentation with a new SentryOkHttpEventListener ([#2659](https://github.com/getsentry/sentry-java/pull/2659))
28+
- Create spans for time spent on:
29+
- Proxy selection
30+
- DNS resolution
31+
- HTTPS setup
32+
- Connection
33+
- Requesting headers
34+
- Receiving response
35+
- You can attach the event listener to your OkHttpClient through `client.eventListener(new SentryOkHttpEventListener()).addInterceptor(new SentryOkHttpInterceptor()).build();`
36+
- In case you already have an event listener you can use the SentryOkHttpEventListener as well through `client.eventListener(new SentryOkHttpEventListener(myListener)).addInterceptor(new SentryOkHttpInterceptor()).build();`
2737
- Add Screenshot and ViewHierarchy to integrations list ([#2698](https://github.com/getsentry/sentry-java/pull/2698))
2838
- New ANR detection based on [ApplicationExitInfo API](https://developer.android.com/reference/android/app/ApplicationExitInfo) ([#2697](https://github.com/getsentry/sentry-java/pull/2697))
2939
- This implementation completely replaces the old one (based on a watchdog) on devices running Android 11 and above:

sentry-android-okhttp/api/sentry-android-okhttp.api

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,46 @@ public final class io/sentry/android/okhttp/BuildConfig {
66
public fun <init> ()V
77
}
88

9+
public final class io/sentry/android/okhttp/SentryOkHttpEventListener : okhttp3/EventListener {
10+
public static final field Companion Lio/sentry/android/okhttp/SentryOkHttpEventListener$Companion;
11+
public fun <init> ()V
12+
public fun <init> (Lio/sentry/IHub;Lkotlin/jvm/functions/Function1;)V
13+
public synthetic fun <init> (Lio/sentry/IHub;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
14+
public fun <init> (Lio/sentry/IHub;Lokhttp3/EventListener$Factory;)V
15+
public synthetic fun <init> (Lio/sentry/IHub;Lokhttp3/EventListener$Factory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
16+
public fun <init> (Lio/sentry/IHub;Lokhttp3/EventListener;)V
17+
public synthetic fun <init> (Lio/sentry/IHub;Lokhttp3/EventListener;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
18+
public fun <init> (Lokhttp3/EventListener$Factory;)V
19+
public fun <init> (Lokhttp3/EventListener;)V
20+
public fun callEnd (Lokhttp3/Call;)V
21+
public fun callFailed (Lokhttp3/Call;Ljava/io/IOException;)V
22+
public fun callStart (Lokhttp3/Call;)V
23+
public fun connectEnd (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;)V
24+
public fun connectFailed (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;Ljava/io/IOException;)V
25+
public fun connectStart (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;)V
26+
public fun connectionAcquired (Lokhttp3/Call;Lokhttp3/Connection;)V
27+
public fun connectionReleased (Lokhttp3/Call;Lokhttp3/Connection;)V
28+
public fun dnsEnd (Lokhttp3/Call;Ljava/lang/String;Ljava/util/List;)V
29+
public fun dnsStart (Lokhttp3/Call;Ljava/lang/String;)V
30+
public fun proxySelectEnd (Lokhttp3/Call;Lokhttp3/HttpUrl;Ljava/util/List;)V
31+
public fun proxySelectStart (Lokhttp3/Call;Lokhttp3/HttpUrl;)V
32+
public fun requestBodyEnd (Lokhttp3/Call;J)V
33+
public fun requestBodyStart (Lokhttp3/Call;)V
34+
public fun requestFailed (Lokhttp3/Call;Ljava/io/IOException;)V
35+
public fun requestHeadersEnd (Lokhttp3/Call;Lokhttp3/Request;)V
36+
public fun requestHeadersStart (Lokhttp3/Call;)V
37+
public fun responseBodyEnd (Lokhttp3/Call;J)V
38+
public fun responseBodyStart (Lokhttp3/Call;)V
39+
public fun responseFailed (Lokhttp3/Call;Ljava/io/IOException;)V
40+
public fun responseHeadersEnd (Lokhttp3/Call;Lokhttp3/Response;)V
41+
public fun responseHeadersStart (Lokhttp3/Call;)V
42+
public fun secureConnectEnd (Lokhttp3/Call;Lokhttp3/Handshake;)V
43+
public fun secureConnectStart (Lokhttp3/Call;)V
44+
}
45+
46+
public final class io/sentry/android/okhttp/SentryOkHttpEventListener$Companion {
47+
}
48+
949
public final class io/sentry/android/okhttp/SentryOkHttpInterceptor : io/sentry/IntegrationName, okhttp3/Interceptor {
1050
public fun <init> ()V
1151
public fun <init> (Lio/sentry/IHub;)V

sentry-android-okhttp/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ dependencies {
7474
implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION))
7575

7676
// tests
77+
testImplementation(projects.sentryTestSupport)
7778
testImplementation(Config.Libs.okhttp)
7879
testImplementation(Config.TestLibs.kotlinTestJunit)
7980
testImplementation(Config.TestLibs.androidxJunit)
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package io.sentry.android.okhttp
2+
3+
import io.sentry.Breadcrumb
4+
import io.sentry.Hint
5+
import io.sentry.IHub
6+
import io.sentry.ISpan
7+
import io.sentry.SpanStatus
8+
import io.sentry.TypeCheckHint
9+
import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.CONNECTION_EVENT
10+
import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.CONNECT_EVENT
11+
import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.REQUEST_BODY_EVENT
12+
import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.REQUEST_HEADERS_EVENT
13+
import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_BODY_EVENT
14+
import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_HEADERS_EVENT
15+
import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.SECURE_CONNECT_EVENT
16+
import io.sentry.util.UrlUtils
17+
import okhttp3.Request
18+
import okhttp3.Response
19+
import java.util.concurrent.ConcurrentHashMap
20+
21+
private const val PROTOCOL_KEY = "protocol"
22+
private const val ERROR_MESSAGE_KEY = "error_message"
23+
24+
internal class SentryOkHttpEvent(private val hub: IHub, private val request: Request) {
25+
private val eventSpans: MutableMap<String, ISpan> = ConcurrentHashMap()
26+
private val breadcrumb: Breadcrumb
27+
internal val callRootSpan: ISpan?
28+
private var response: Response? = null
29+
30+
init {
31+
val urlDetails = UrlUtils.parse(request.url.toString())
32+
val url = urlDetails.urlOrFallback
33+
val host: String = request.url.host
34+
val encodedPath: String = request.url.encodedPath
35+
val method: String = request.method
36+
37+
// We start the call span that will contain all the others
38+
callRootSpan = hub.span?.startChild("http.client", "$method $url")
39+
40+
urlDetails.applyToSpan(callRootSpan)
41+
42+
// We setup a breadcrumb with all meaningful data
43+
breadcrumb = Breadcrumb.http(url, method)
44+
breadcrumb.setData("host", host)
45+
breadcrumb.setData("path", encodedPath)
46+
47+
// We add the same data to the root call span
48+
callRootSpan?.setData("url", url)
49+
callRootSpan?.setData("host", host)
50+
callRootSpan?.setData("path", encodedPath)
51+
callRootSpan?.setData("http.method", method)
52+
}
53+
54+
/**
55+
* Sets the [Response] that will be sent in the breadcrumb [Hint].
56+
* Also, it sets the protocol and status code in the breadcrumb and the call root span.
57+
*/
58+
fun setResponse(response: Response) {
59+
this.response = response
60+
breadcrumb.setData(PROTOCOL_KEY, response.protocol.name)
61+
breadcrumb.setData("status_code", response.code)
62+
callRootSpan?.setData(PROTOCOL_KEY, response.protocol.name)
63+
callRootSpan?.setData("http.status_code", response.code)
64+
callRootSpan?.status = SpanStatus.fromHttpStatusCode(response.code)
65+
}
66+
67+
fun setProtocol(protocolName: String?) {
68+
if (protocolName != null) {
69+
breadcrumb.setData(PROTOCOL_KEY, protocolName)
70+
callRootSpan?.setData(PROTOCOL_KEY, protocolName)
71+
}
72+
}
73+
74+
fun setRequestBodySize(byteCount: Long) {
75+
if (byteCount > -1) {
76+
breadcrumb.setData("request_content_length", byteCount)
77+
callRootSpan?.setData("http.request_content_length", byteCount)
78+
}
79+
}
80+
81+
fun setResponseBodySize(byteCount: Long) {
82+
if (byteCount > -1) {
83+
breadcrumb.setData("response_content_length", byteCount)
84+
callRootSpan?.setData("http.response_content_length", byteCount)
85+
}
86+
}
87+
88+
/** Sets the [errorMessage] if not null. */
89+
fun setError(errorMessage: String?) {
90+
if (errorMessage != null) {
91+
breadcrumb.setData(ERROR_MESSAGE_KEY, errorMessage)
92+
callRootSpan?.setData(ERROR_MESSAGE_KEY, errorMessage)
93+
}
94+
}
95+
96+
/** Starts a span, if the callRootSpan is not null. */
97+
fun startSpan(event: String) {
98+
// Find the parent of the span being created. E.g. secureConnect is child of connect
99+
val parentSpan = when (event) {
100+
// PROXY_SELECT, DNS, CONNECT and CONNECTION are not children of one another
101+
SECURE_CONNECT_EVENT -> eventSpans[CONNECT_EVENT]
102+
REQUEST_HEADERS_EVENT -> eventSpans[CONNECTION_EVENT]
103+
REQUEST_BODY_EVENT -> eventSpans[CONNECTION_EVENT]
104+
RESPONSE_HEADERS_EVENT -> eventSpans[CONNECTION_EVENT]
105+
RESPONSE_BODY_EVENT -> eventSpans[CONNECTION_EVENT]
106+
else -> callRootSpan
107+
} ?: callRootSpan
108+
val span = parentSpan?.startChild("http.client.$event") ?: return
109+
eventSpans[event] = span
110+
}
111+
112+
/** Finishes a previously started span, and runs [beforeFinish] on it and on the call root span. */
113+
fun finishSpan(event: String, beforeFinish: ((span: ISpan) -> Unit)? = null) {
114+
val span = eventSpans[event] ?: return
115+
beforeFinish?.invoke(span)
116+
callRootSpan?.let { beforeFinish?.invoke(it) }
117+
span.finish()
118+
}
119+
120+
/** Finishes the call root span, and runs [beforeFinish] on it. Then a breadcrumb is sent. */
121+
fun finishEvent(beforeFinish: ((span: ISpan) -> Unit)? = null) {
122+
callRootSpan ?: return
123+
124+
// We forcefully finish all spans, even if they should already have been finished through finishSpan()
125+
eventSpans.values.filter { !it.isFinished }.forEach { it.finish(SpanStatus.DEADLINE_EXCEEDED) }
126+
beforeFinish?.invoke(callRootSpan)
127+
callRootSpan.finish()
128+
129+
// We put data in the hint and send a breadcrumb
130+
val hint = Hint()
131+
hint.set(TypeCheckHint.OKHTTP_REQUEST, request)
132+
response?.let { hint.set(TypeCheckHint.OKHTTP_RESPONSE, it) }
133+
134+
hub.addBreadcrumb(breadcrumb, hint)
135+
return
136+
}
137+
}

0 commit comments

Comments
 (0)