Skip to content
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

Add OkHttp event spans #2659

Merged
merged 14 commits into from
May 26, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

- Add OkHttp event spans ([#2659](https://github.com/getsentry/sentry-java/pull/2659))
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
- Attach Trace Context when an ANR is detected (ANRv1) ([#2583](https://github.com/getsentry/sentry-java/pull/2583))
- Make log4j2 integration compatible with log4j 3.0 ([#2634](https://github.com/getsentry/sentry-java/pull/2634))
- Instead of relying on package scanning, we now use an annotation processor to generate `Log4j2Plugins.dat`
Expand Down
34 changes: 34 additions & 0 deletions sentry-android-okhttp/api/sentry-android-okhttp.api
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,40 @@ public final class io/sentry/android/okhttp/BuildConfig {
public fun <init> ()V
}

public final class io/sentry/android/okhttp/SentryOkHttpEventListener : okhttp3/EventListener {
public static final field Companion Lio/sentry/android/okhttp/SentryOkHttpEventListener$Companion;
public fun <init> ()V
public fun <init> (Lio/sentry/IHub;)V
public synthetic fun <init> (Lio/sentry/IHub;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun callEnd (Lokhttp3/Call;)V
public fun callFailed (Lokhttp3/Call;Ljava/io/IOException;)V
public fun callStart (Lokhttp3/Call;)V
public fun connectEnd (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;)V
public fun connectFailed (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;Lokhttp3/Protocol;Ljava/io/IOException;)V
public fun connectStart (Lokhttp3/Call;Ljava/net/InetSocketAddress;Ljava/net/Proxy;)V
public fun connectionAcquired (Lokhttp3/Call;Lokhttp3/Connection;)V
public fun connectionReleased (Lokhttp3/Call;Lokhttp3/Connection;)V
public fun dnsEnd (Lokhttp3/Call;Ljava/lang/String;Ljava/util/List;)V
public fun dnsStart (Lokhttp3/Call;Ljava/lang/String;)V
public fun proxySelectEnd (Lokhttp3/Call;Lokhttp3/HttpUrl;Ljava/util/List;)V
public fun proxySelectStart (Lokhttp3/Call;Lokhttp3/HttpUrl;)V
public fun requestBodyEnd (Lokhttp3/Call;J)V
public fun requestBodyStart (Lokhttp3/Call;)V
public fun requestFailed (Lokhttp3/Call;Ljava/io/IOException;)V
public fun requestHeadersEnd (Lokhttp3/Call;Lokhttp3/Request;)V
public fun requestHeadersStart (Lokhttp3/Call;)V
public fun responseBodyEnd (Lokhttp3/Call;J)V
public fun responseBodyStart (Lokhttp3/Call;)V
public fun responseFailed (Lokhttp3/Call;Ljava/io/IOException;)V
public fun responseHeadersEnd (Lokhttp3/Call;Lokhttp3/Response;)V
public fun responseHeadersStart (Lokhttp3/Call;)V
public fun secureConnectEnd (Lokhttp3/Call;Lokhttp3/Handshake;)V
public fun secureConnectStart (Lokhttp3/Call;)V
}

public final class io/sentry/android/okhttp/SentryOkHttpEventListener$Companion {
}

public final class io/sentry/android/okhttp/SentryOkHttpInterceptor : io/sentry/IntegrationName, okhttp3/Interceptor {
public fun <init> ()V
public fun <init> (Lio/sentry/IHub;)V
Expand Down
1 change: 1 addition & 0 deletions sentry-android-okhttp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ dependencies {
implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION))

// tests
testImplementation(projects.sentryTestSupport)
testImplementation(Config.Libs.okhttp)
testImplementation(Config.TestLibs.kotlinTestJunit)
testImplementation(Config.TestLibs.androidxJunit)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package io.sentry.android.okhttp

import io.sentry.Breadcrumb
import io.sentry.Hint
import io.sentry.IHub
import io.sentry.ISpan
import io.sentry.SpanStatus
import io.sentry.TypeCheckHint
import io.sentry.util.UrlUtils
import okhttp3.Call
import okhttp3.Response
import java.net.URL

private val uuidRegex by lazy { Regex("[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}") }

internal class SentryOkHttpEvent(private val hub: IHub, private val call: Call) {
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
private val eventSpans: MutableMap<String, ISpan> = HashMap()
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
private val breadcrumb: Breadcrumb
internal val callRootSpan: ISpan?
private var response: Response? = null

init {
val urlDetails = UrlUtils.parse(call.request().url.toString())
val url = urlDetails.urlOrFallback
val trimmedUrl: String = trimUrl(url)
val host: String = call.request().url.host
val encodedPath: String = call.request().url.encodedPath
val method: String = call.request().method

// We start the call span that will contain all the others
callRootSpan = hub.span?.startChild("http.client", "$method $url")

// We setup a breadcrumb with all meaningful data
breadcrumb = Breadcrumb.http(url, method)
breadcrumb.setData("url", url)
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
breadcrumb.setData("filtered_url", trimmedUrl)
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
breadcrumb.setData("host", host)
breadcrumb.setData("path", encodedPath)
breadcrumb.setData("method", method)
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
breadcrumb.setData("success", true)
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved

// We add the same data to the root call span
callRootSpan?.setData("url", url)
callRootSpan?.setData("filtered_url", trimmedUrl)
callRootSpan?.setData("host", host)
callRootSpan?.setData("path", encodedPath)
callRootSpan?.setData("method", method)
callRootSpan?.setData("success", true)
}

private fun trimUrl(url: String): String {
// Remove any uuid from the url and replace it with a "*"
val trimmedUrl = url.replace(uuidRegex, "*")
if (URL(trimmedUrl).query == null) {
return trimmedUrl
}
// Remove any parameter from the url
return trimmedUrl.replace(URL(trimmedUrl).query, "").replace("?", "")
}

/**
* Sets the [Response] that will be sent in the breadcrumb [Hint].
* Also, it sets the protocol and status code in the breadcrumb and the call root span.
*/
fun setResponse(response: Response) {
this.response = response
breadcrumb.setData("protocol", response.protocol.name)
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
breadcrumb.setData("status_code", response.code)
callRootSpan?.setData("protocol", response.protocol.name)
callRootSpan?.setData("status_code", response.code)
stefanosiano marked this conversation as resolved.
Show resolved Hide resolved
callRootSpan?.status = SpanStatus.fromHttpStatusCode(response.code)
}

fun setProtocol(protocolName: String?) {
if (protocolName != null) {
breadcrumb.setData("protocol", protocolName)
callRootSpan?.setData("protocol", protocolName)
}
}

fun setRequestBodySize(byteCount: Long) {
if (byteCount > -1) {
breadcrumb.setData("request_body_size", byteCount)
callRootSpan?.setData("request_body_size", byteCount)
}
}

fun setResponseBodySize(byteCount: Long) {
if (byteCount > -1) {
breadcrumb.setData("response_body_size", byteCount)
callRootSpan?.setData("response_body_size", byteCount)
}
}

/**
* Sets the success flag in the breadcrumb and the call root span to false.
* Also sets the [errorMessage] if not null.
*/
fun setError(errorMessage: String?) {
breadcrumb.setData("success", false)
callRootSpan?.setData("success", false)
if (errorMessage != null) {
breadcrumb.setData("error_message", errorMessage)
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
callRootSpan?.setData("error_message", errorMessage)
}
}

/** Starts a span, if the callRootSpan is not null. */
fun startSpan(event: String) {
val span = callRootSpan?.startChild("http.client", event) ?: return
eventSpans[event] = span
}

/** Finishes a previously started span, and runs [beforeFinish] on it and on the call root span. */
fun finishSpan(event: String, beforeFinish: ((span: ISpan) -> Unit)? = null) {
val span = eventSpans[event] ?: return
beforeFinish?.invoke(span)
callRootSpan?.let { beforeFinish?.invoke(it) }
span.finish()
}

/** Finishes the call root span, and runs [beforeFinish] on it. Then a breadcrumb is sent. */
fun finishEvent(beforeFinish: ((span: ISpan) -> Unit)? = null) {
callRootSpan ?: return

// We forcefully finish all spans, even if they should already have been finished through finishSpan()
eventSpans.values.filter { !it.isFinished }.forEach { it.finish(SpanStatus.DEADLINE_EXCEEDED) }
beforeFinish?.invoke(callRootSpan)
callRootSpan.finish()

// We put data in the hint and send a breadcrumb
val hint = Hint()
hint.set(TypeCheckHint.OKHTTP_REQUEST, call.request())
response?.let { hint.set(TypeCheckHint.OKHTTP_RESPONSE, it) }

hub.addBreadcrumb(breadcrumb, hint)
return
}
}
Loading