diff --git a/.craft.yml b/.craft.yml index f3f0f52c9a..28c7191380 100644 --- a/.craft.yml +++ b/.craft.yml @@ -49,6 +49,7 @@ targets: maven:io.sentry:sentry-jdbc: maven:io.sentry:sentry-graphql: maven:io.sentry:sentry-quartz: + # maven:io.sentry:sentry-okhttp: maven:io.sentry:sentry-android-navigation: maven:io.sentry:sentry-compose: maven:io.sentry:sentry-compose-android: diff --git a/.github/ISSUE_TEMPLATE/bug_report_java.yml b/.github/ISSUE_TEMPLATE/bug_report_java.yml index 429fbdee73..f802c3a0cc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_java.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_java.yml @@ -30,6 +30,7 @@ body: - sentry-quartz - sentry-openfeign - sentry-apache-http-client-5 + - sentry-okhttp - other validations: required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c570959a5..00d3494b15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +### Features + +- Add `sentry-okhttp` module to support instrumenting OkHttp in non-Android projects ([#3005](https://github.com/getsentry/sentry-java/pull/3005)) + - This deprecates `sentry-android-okhttp` classes. Make sure to replace `io.sentry.android.okhttp` package name with `io.sentry.okhttp` before the next major, where the classes will be removed + - `SentryOkHttpUtils` was removed from public API as it's been exposed by mistake + ## 7.0.0-rc.1 ### Features diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index e2cbf460af..6c81e13a20 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -236,6 +236,7 @@ object Config { val SENTRY_SERVLET_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet" val SENTRY_SERVLET_JAKARTA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet.jakarta" val SENTRY_COMPOSE_HELPER_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.compose.helper" + val SENTRY_OKHTTP_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.okhttp" val group = "io.sentry" val description = "SDK for sentry.io" val versionNameProp = "versionName" diff --git a/sentry-android-okhttp/api/sentry-android-okhttp.api b/sentry-android-okhttp/api/sentry-android-okhttp.api index 099534ea61..a1ad9114a2 100644 --- a/sentry-android-okhttp/api/sentry-android-okhttp.api +++ b/sentry-android-okhttp/api/sentry-android-okhttp.api @@ -7,7 +7,6 @@ public final class io/sentry/android/okhttp/BuildConfig { } public final class io/sentry/android/okhttp/SentryOkHttpEventListener : okhttp3/EventListener { - public static final field Companion Lio/sentry/android/okhttp/SentryOkHttpEventListener$Companion; public fun ()V public fun (Lio/sentry/IHub;Lkotlin/jvm/functions/Function1;)V public synthetic fun (Lio/sentry/IHub;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -48,9 +47,6 @@ public final class io/sentry/android/okhttp/SentryOkHttpEventListener : okhttp3/ public fun secureConnectStart (Lokhttp3/Call;)V } -public final class io/sentry/android/okhttp/SentryOkHttpEventListener$Companion { -} - public final class io/sentry/android/okhttp/SentryOkHttpInterceptor : okhttp3/Interceptor { public fun ()V public fun (Lio/sentry/IHub;)V @@ -64,8 +60,3 @@ public abstract interface class io/sentry/android/okhttp/SentryOkHttpInterceptor public abstract fun execute (Lio/sentry/ISpan;Lokhttp3/Request;Lokhttp3/Response;)Lio/sentry/ISpan; } -public final class io/sentry/android/okhttp/SentryOkHttpUtils { - public static final field INSTANCE Lio/sentry/android/okhttp/SentryOkHttpUtils; - public final fun captureClientError (Lio/sentry/IHub;Lokhttp3/Request;Lokhttp3/Response;)V -} - diff --git a/sentry-android-okhttp/build.gradle.kts b/sentry-android-okhttp/build.gradle.kts index 0f98011fac..f3eaa59303 100644 --- a/sentry-android-okhttp/build.gradle.kts +++ b/sentry-android-okhttp/build.gradle.kts @@ -24,9 +24,7 @@ android { buildTypes { getByName("debug") - getByName("release") { - consumerProguardFiles("proguard-rules.pro") - } + getByName("release") } kotlinOptions { @@ -63,6 +61,7 @@ kotlin { dependencies { api(projects.sentry) + api(projects.sentryOkhttp) compileOnly(Config.Libs.okhttp) diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEventListener.kt b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEventListener.kt index da13ee110a..7ca5313d8f 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEventListener.kt +++ b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEventListener.kt @@ -2,8 +2,6 @@ package io.sentry.android.okhttp import io.sentry.HubAdapter import io.sentry.IHub -import io.sentry.SpanDataConvention -import io.sentry.SpanStatus import okhttp3.Call import okhttp3.Connection import okhttp3.EventListener @@ -16,12 +14,11 @@ import java.io.IOException import java.net.InetAddress import java.net.InetSocketAddress import java.net.Proxy -import java.util.concurrent.ConcurrentHashMap /** * Logs network performance event metrics to Sentry * - * Usage - add instance of [SentryOkHttpEventListener] in [OkHttpClient.eventListener] + * Usage - add instance of [SentryOkHttpEventListener] in [okhttp3.OkHttpClient.Builder.eventListener] * * ``` * val client = OkHttpClient.Builder() @@ -30,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap * .build() * ``` * - * If you already use a [OkHttpClient.eventListener], you can pass it in the constructor. + * If you already use a [okhttp3.EventListener], you can pass it in the constructor. * * ``` * val client = OkHttpClient.Builder() @@ -39,28 +36,15 @@ import java.util.concurrent.ConcurrentHashMap * .build() * ``` */ +@Deprecated( + "Use SentryOkHttpEventListener from sentry-okhttp instead", + ReplaceWith("SentryOkHttpEventListener", "io.sentry.okhttp.SentryOkHttpEventListener") +) @Suppress("TooManyFunctions") class SentryOkHttpEventListener( - private val hub: IHub = HubAdapter.getInstance(), - private val originalEventListenerCreator: ((call: Call) -> EventListener)? = null + hub: IHub = HubAdapter.getInstance(), + originalEventListenerCreator: ((call: Call) -> EventListener)? = null ) : EventListener() { - - private var originalEventListener: EventListener? = null - - companion object { - internal const val PROXY_SELECT_EVENT = "proxy_select" - internal const val DNS_EVENT = "dns" - internal const val SECURE_CONNECT_EVENT = "secure_connect" - internal const val CONNECT_EVENT = "connect" - internal const val CONNECTION_EVENT = "connection" - internal const val REQUEST_HEADERS_EVENT = "request_headers" - internal const val REQUEST_BODY_EVENT = "request_body" - internal const val RESPONSE_HEADERS_EVENT = "response_headers" - internal const val RESPONSE_BODY_EVENT = "response_body" - - internal val eventMap: MutableMap = ConcurrentHashMap() - } - constructor() : this( HubAdapter.getInstance(), originalEventListenerCreator = null @@ -86,98 +70,34 @@ class SentryOkHttpEventListener( originalEventListenerCreator = { originalEventListenerFactory.create(it) } ) - override fun callStart(call: Call) { - originalEventListener = originalEventListenerCreator?.invoke(call) - originalEventListener?.callStart(call) - // If the wrapped EventListener is ours, we can just delegate the calls, - // without creating other events that would create duplicates - if (canCreateEventSpan()) { - eventMap[call] = SentryOkHttpEvent(hub, call.request()) - } - } + private val delegate = io.sentry.okhttp.SentryOkHttpEventListener(hub, originalEventListenerCreator) - override fun proxySelectStart(call: Call, url: HttpUrl) { - originalEventListener?.proxySelectStart(call, url) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(PROXY_SELECT_EVENT) + override fun cacheConditionalHit(call: Call, cachedResponse: Response) { + delegate.cacheConditionalHit(call, cachedResponse) } - override fun proxySelectEnd( - call: Call, - url: HttpUrl, - proxies: List - ) { - originalEventListener?.proxySelectEnd(call, url, proxies) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(PROXY_SELECT_EVENT) { - if (proxies.isNotEmpty()) { - it.setData("proxies", proxies.joinToString { proxy -> proxy.toString() }) - } - } + override fun cacheHit(call: Call, response: Response) { + delegate.cacheHit(call, response) } - override fun dnsStart(call: Call, domainName: String) { - originalEventListener?.dnsStart(call, domainName) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(DNS_EVENT) + override fun cacheMiss(call: Call) { + delegate.cacheMiss(call) } - override fun dnsEnd( - call: Call, - domainName: String, - inetAddressList: List - ) { - originalEventListener?.dnsEnd(call, domainName, inetAddressList) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(DNS_EVENT) { - it.setData("domain_name", domainName) - if (inetAddressList.isNotEmpty()) { - it.setData("dns_addresses", inetAddressList.joinToString { address -> address.toString() }) - } - } + override fun callEnd(call: Call) { + delegate.callEnd(call) } - override fun connectStart( - call: Call, - inetSocketAddress: InetSocketAddress, - proxy: Proxy - ) { - originalEventListener?.connectStart(call, inetSocketAddress, proxy) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(CONNECT_EVENT) + override fun callFailed(call: Call, ioe: IOException) { + delegate.callFailed(call, ioe) } - override fun secureConnectStart(call: Call) { - originalEventListener?.secureConnectStart(call) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(SECURE_CONNECT_EVENT) + override fun callStart(call: Call) { + delegate.callStart(call) } - override fun secureConnectEnd(call: Call, handshake: Handshake?) { - originalEventListener?.secureConnectEnd(call, handshake) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(SECURE_CONNECT_EVENT) + override fun canceled(call: Call) { + delegate.canceled(call) } override fun connectEnd( @@ -186,13 +106,7 @@ class SentryOkHttpEventListener( proxy: Proxy, protocol: Protocol? ) { - originalEventListener?.connectEnd(call, inetSocketAddress, proxy, protocol) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.setProtocol(protocol?.name) - okHttpEvent.finishSpan(CONNECT_EVENT) + delegate.connectEnd(call, inetSocketAddress, proxy, protocol) } override fun connectFailed( @@ -202,210 +116,86 @@ class SentryOkHttpEventListener( protocol: Protocol?, ioe: IOException ) { - originalEventListener?.connectFailed(call, inetSocketAddress, proxy, protocol, ioe) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.setProtocol(protocol?.name) - okHttpEvent.setError(ioe.message) - okHttpEvent.finishSpan(CONNECT_EVENT) { - it.throwable = ioe - it.status = SpanStatus.INTERNAL_ERROR - } + delegate.connectFailed(call, inetSocketAddress, proxy, protocol, ioe) + } + + override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) { + delegate.connectStart(call, inetSocketAddress, proxy) } override fun connectionAcquired(call: Call, connection: Connection) { - originalEventListener?.connectionAcquired(call, connection) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(CONNECTION_EVENT) + delegate.connectionAcquired(call, connection) } override fun connectionReleased(call: Call, connection: Connection) { - originalEventListener?.connectionReleased(call, connection) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(CONNECTION_EVENT) + delegate.connectionReleased(call, connection) } - override fun requestHeadersStart(call: Call) { - originalEventListener?.requestHeadersStart(call) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(REQUEST_HEADERS_EVENT) + override fun dnsEnd(call: Call, domainName: String, inetAddressList: List) { + delegate.dnsEnd(call, domainName, inetAddressList) } - override fun requestHeadersEnd(call: Call, request: Request) { - originalEventListener?.requestHeadersEnd(call, request) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(REQUEST_HEADERS_EVENT) + override fun dnsStart(call: Call, domainName: String) { + delegate.dnsStart(call, domainName) } - override fun requestBodyStart(call: Call) { - originalEventListener?.requestBodyStart(call) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(REQUEST_BODY_EVENT) + override fun proxySelectEnd(call: Call, url: HttpUrl, proxies: List) { + delegate.proxySelectEnd(call, url, proxies) + } + + override fun proxySelectStart(call: Call, url: HttpUrl) { + delegate.proxySelectStart(call, url) } override fun requestBodyEnd(call: Call, byteCount: Long) { - originalEventListener?.requestBodyEnd(call, byteCount) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.finishSpan(REQUEST_BODY_EVENT) { - if (byteCount > 0) { - it.setData("http.request_content_length", byteCount) - } - } - okHttpEvent.setRequestBodySize(byteCount) + delegate.requestBodyEnd(call, byteCount) } - override fun requestFailed(call: Call, ioe: IOException) { - originalEventListener?.requestFailed(call, ioe) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.setError(ioe.message) - // requestFailed can happen after requestHeaders or requestBody. - // If requestHeaders already finished, we don't change its status. - okHttpEvent.finishSpan(REQUEST_HEADERS_EVENT) { - if (!it.isFinished) { - it.status = SpanStatus.INTERNAL_ERROR - it.throwable = ioe - } - } - okHttpEvent.finishSpan(REQUEST_BODY_EVENT) { - it.status = SpanStatus.INTERNAL_ERROR - it.throwable = ioe - } + override fun requestBodyStart(call: Call) { + delegate.requestBodyStart(call) } - override fun responseHeadersStart(call: Call) { - originalEventListener?.responseHeadersStart(call) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(RESPONSE_HEADERS_EVENT) + override fun requestFailed(call: Call, ioe: IOException) { + delegate.requestFailed(call, ioe) } - override fun responseHeadersEnd(call: Call, response: Response) { - originalEventListener?.responseHeadersEnd(call, response) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.setResponse(response) - val responseHeadersSpan = okHttpEvent.finishSpan(RESPONSE_HEADERS_EVENT) { - it.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, response.code) - // Let's not override the status of a span that was set - if (it.status == null) { - it.status = SpanStatus.fromHttpStatusCode(response.code) - } - } - okHttpEvent.scheduleFinish(responseHeadersSpan?.finishDate ?: hub.options.dateProvider.now()) + override fun requestHeadersEnd(call: Call, request: Request) { + delegate.requestHeadersEnd(call, request) } - override fun responseBodyStart(call: Call) { - originalEventListener?.responseBodyStart(call) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.startSpan(RESPONSE_BODY_EVENT) + override fun requestHeadersStart(call: Call) { + delegate.requestHeadersStart(call) } override fun responseBodyEnd(call: Call, byteCount: Long) { - originalEventListener?.responseBodyEnd(call, byteCount) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.setResponseBodySize(byteCount) - okHttpEvent.finishSpan(RESPONSE_BODY_EVENT) { - if (byteCount > 0) { - it.setData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY, byteCount) - } - } + delegate.responseBodyEnd(call, byteCount) } - override fun responseFailed(call: Call, ioe: IOException) { - originalEventListener?.responseFailed(call, ioe) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return - okHttpEvent.setError(ioe.message) - // responseFailed can happen after responseHeaders or responseBody. - // If responseHeaders already finished, we don't change its status. - okHttpEvent.finishSpan(RESPONSE_HEADERS_EVENT) { - if (!it.isFinished) { - it.status = SpanStatus.INTERNAL_ERROR - it.throwable = ioe - } - } - okHttpEvent.finishSpan(RESPONSE_BODY_EVENT) { - it.status = SpanStatus.INTERNAL_ERROR - it.throwable = ioe - } + override fun responseBodyStart(call: Call) { + delegate.responseBodyStart(call) } - override fun callEnd(call: Call) { - originalEventListener?.callEnd(call) - val okHttpEvent: SentryOkHttpEvent = eventMap.remove(call) ?: return - okHttpEvent.finishEvent() + override fun responseFailed(call: Call, ioe: IOException) { + delegate.responseFailed(call, ioe) } - override fun callFailed(call: Call, ioe: IOException) { - originalEventListener?.callFailed(call, ioe) - if (!canCreateEventSpan()) { - return - } - val okHttpEvent: SentryOkHttpEvent = eventMap.remove(call) ?: return - okHttpEvent.setError(ioe.message) - okHttpEvent.finishEvent { - it.status = SpanStatus.INTERNAL_ERROR - it.throwable = ioe - } + override fun responseHeadersEnd(call: Call, response: Response) { + delegate.responseHeadersEnd(call, response) } - override fun canceled(call: Call) { - originalEventListener?.canceled(call) + override fun responseHeadersStart(call: Call) { + delegate.responseHeadersStart(call) } override fun satisfactionFailure(call: Call, response: Response) { - originalEventListener?.satisfactionFailure(call, response) - } - - override fun cacheHit(call: Call, response: Response) { - originalEventListener?.cacheHit(call, response) - } - - override fun cacheMiss(call: Call) { - originalEventListener?.cacheMiss(call) + delegate.satisfactionFailure(call, response) } - override fun cacheConditionalHit(call: Call, cachedResponse: Response) { - originalEventListener?.cacheConditionalHit(call, cachedResponse) + override fun secureConnectEnd(call: Call, handshake: Handshake?) { + delegate.secureConnectEnd(call, handshake) } - private fun canCreateEventSpan(): Boolean { - // If the wrapped EventListener is ours, we shouldn't create spans, as the originalEventListener already did it - return originalEventListener !is SentryOkHttpEventListener + override fun secureConnectStart(call: Call) { + delegate.secureConnectStart(call) } } diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt index ddefaabbde..678434f1db 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt +++ b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpInterceptor.kt @@ -1,27 +1,16 @@ package io.sentry.android.okhttp -import io.sentry.BaggageHeader -import io.sentry.Breadcrumb -import io.sentry.Hint import io.sentry.HttpStatusCodeRange import io.sentry.HubAdapter import io.sentry.IHub import io.sentry.ISpan import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS -import io.sentry.SpanDataConvention -import io.sentry.SpanStatus -import io.sentry.TypeCheckHint.OKHTTP_REQUEST -import io.sentry.TypeCheckHint.OKHTTP_RESPONSE +import io.sentry.android.okhttp.SentryOkHttpInterceptor.BeforeSpanCallback import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion -import io.sentry.util.Platform -import io.sentry.util.PropagationTargetsUtils -import io.sentry.util.TracingUtils -import io.sentry.util.UrlUtils import okhttp3.Interceptor import okhttp3.Request import okhttp3.Response -import java.io.IOException /** * The Sentry's [SentryOkHttpInterceptor], it will automatically add a breadcrumb and start a span @@ -37,6 +26,10 @@ import java.io.IOException * @param failedRequestTargets The SDK will only capture HTTP Client errors if the HTTP Request URL * is a match for any of the defined targets. */ +@Deprecated( + "Use SentryOkHttpInterceptor from sentry-okhttp instead", + ReplaceWith("SentryOkHttpInterceptor", "io.sentry.okhttp.SentryOkHttpInterceptor") +) class SentryOkHttpInterceptor( private val hub: IHub = HubAdapter.getInstance(), private val beforeSpan: BeforeSpanCallback? = null, @@ -45,7 +38,15 @@ class SentryOkHttpInterceptor( HttpStatusCodeRange(HttpStatusCodeRange.DEFAULT_MIN, HttpStatusCodeRange.DEFAULT_MAX) ), private val failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS) -) : Interceptor { +) : Interceptor by io.sentry.okhttp.SentryOkHttpInterceptor( + hub, + { span, request, response -> + beforeSpan?.execute(span, request, response) + }, + captureFailedRequests, + failedRequestStatusCodes, + failedRequestTargets +) { constructor() : this(HubAdapter.getInstance()) constructor(hub: IHub) : this(hub, null) @@ -57,163 +58,13 @@ class SentryOkHttpInterceptor( .addPackage("maven:io.sentry:sentry-android-okhttp", BuildConfig.VERSION_NAME) } - @Suppress("LongMethod") - override fun intercept(chain: Interceptor.Chain): Response { - var request = chain.request() - - val urlDetails = UrlUtils.parse(request.url.toString()) - val url = urlDetails.urlOrFallback - val method = request.method - - val span: ISpan? - val okHttpEvent: SentryOkHttpEvent? - - if (SentryOkHttpEventListener.eventMap.containsKey(chain.call())) { - // read the span from the event listener - okHttpEvent = SentryOkHttpEventListener.eventMap[chain.call()] - span = okHttpEvent?.callRootSpan - } else { - // read the span from the bound scope - okHttpEvent = null - val parentSpan = if (Platform.isAndroid()) hub.transaction else hub.span - span = parentSpan?.startChild("http.client", "$method $url") - } - - span?.spanContext?.origin = TRACE_ORIGIN - - urlDetails.applyToSpan(span) - - val isFromEventListener = okHttpEvent != null - var response: Response? = null - var code: Int? = null - - try { - val requestBuilder = request.newBuilder() - - TracingUtils.traceIfAllowed( - hub, - request.url.toString(), - request.headers(BaggageHeader.BAGGAGE_HEADER), - span - )?.let { tracingHeaders -> - requestBuilder.addHeader(tracingHeaders.sentryTraceHeader.name, tracingHeaders.sentryTraceHeader.value) - tracingHeaders.baggageHeader?.let { - requestBuilder.removeHeader(BaggageHeader.BAGGAGE_HEADER) - requestBuilder.addHeader(it.name, it.value) - } - } - - request = requestBuilder.build() - response = chain.proceed(request) - code = response.code - span?.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, code) - span?.status = SpanStatus.fromHttpStatusCode(code) - - // OkHttp errors (4xx, 5xx) don't throw, so it's safe to call within this block. - // breadcrumbs are added on the finally block because we'd like to know if the device - // had an unstable connection or something similar - if (shouldCaptureClientError(request, response)) { - // If we capture the client error directly, it could be associated with the - // currently running span by the backend. In case the listener is in use, that is - // an inner span. So, if the listener is in use, we let it capture the client - // error, to shown it in the http root call span in the dashboard. - if (isFromEventListener && okHttpEvent != null) { - okHttpEvent.setClientErrorResponse(response) - } else { - SentryOkHttpUtils.captureClientError(hub, request, response) - } - } - - return response - } catch (e: IOException) { - span?.apply { - this.throwable = e - this.status = SpanStatus.INTERNAL_ERROR - } - throw e - } finally { - finishSpan(span, request, response, isFromEventListener) - - // The SentryOkHttpEventListener will send the breadcrumb itself if used for this call - if (!isFromEventListener) { - sendBreadcrumb(request, code, response) - } - } - } - - private fun sendBreadcrumb(request: Request, code: Int?, response: Response?) { - val breadcrumb = Breadcrumb.http(request.url.toString(), request.method, code) - request.body?.contentLength().ifHasValidLength { - breadcrumb.setData("http.request_content_length", it) - } - - val hint = Hint().also { it.set(OKHTTP_REQUEST, request) } - response?.let { - it.body?.contentLength().ifHasValidLength { responseBodySize -> - breadcrumb.setData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY, responseBodySize) - } - - hint[OKHTTP_RESPONSE] = it - } - - hub.addBreadcrumb(breadcrumb, hint) - } - - private fun finishSpan(span: ISpan?, request: Request, response: Response?, isFromEventListener: Boolean) { - if (span == null) { - return - } - if (beforeSpan != null) { - val result = beforeSpan.execute(span, request, response) - if (result == null) { - // span is dropped - span.spanContext.sampled = false - } else { - // The SentryOkHttpEventListener will finish the span itself if used for this call - if (!isFromEventListener) { - span.finish() - } - } - } else { - // The SentryOkHttpEventListener will finish the span itself if used for this call - if (!isFromEventListener) { - span.finish() - } - } - } - - private fun Long?.ifHasValidLength(fn: (Long) -> Unit) { - if (this != null && this != -1L) { - fn.invoke(this) - } - } - - private fun shouldCaptureClientError(request: Request, response: Response): Boolean { - // return if the feature is disabled or its not within the range - if (!captureFailedRequests || !containsStatusCode(response.code)) { - return false - } - - // return if its not a target match - if (!PropagationTargetsUtils.contain(failedRequestTargets, request.url.toString())) { - return false - } - - return true - } - - private fun containsStatusCode(statusCode: Int): Boolean { - for (item in failedRequestStatusCodes) { - if (item.isInRange(statusCode)) { - return true - } - } - return false - } - /** * The BeforeSpan callback */ + @Deprecated( + "Use BeforeSpanCallback from sentry-okhttp instead", + ReplaceWith("BeforeSpanCallback", "io.sentry.okhttp.SentryOkHttpInterceptor.BeforeSpanCallback") + ) fun interface BeforeSpanCallback { /** * Mutates or drops span before being added diff --git a/sentry-okhttp/api/sentry-okhttp.api b/sentry-okhttp/api/sentry-okhttp.api new file mode 100644 index 0000000000..3095659c88 --- /dev/null +++ b/sentry-okhttp/api/sentry-okhttp.api @@ -0,0 +1,63 @@ +public final class io/sentry/okhttp/BuildConfig { + public static final field SENTRY_OKHTTP_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public class io/sentry/okhttp/SentryOkHttpEventListener : okhttp3/EventListener { + public static final field Companion Lio/sentry/okhttp/SentryOkHttpEventListener$Companion; + public fun ()V + public fun (Lio/sentry/IHub;Lkotlin/jvm/functions/Function1;)V + public synthetic fun (Lio/sentry/IHub;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/IHub;Lokhttp3/EventListener$Factory;)V + public synthetic fun (Lio/sentry/IHub;Lokhttp3/EventListener$Factory;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/IHub;Lokhttp3/EventListener;)V + public synthetic fun (Lio/sentry/IHub;Lokhttp3/EventListener;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lokhttp3/EventListener$Factory;)V + public fun (Lokhttp3/EventListener;)V + public fun cacheConditionalHit (Lokhttp3/Call;Lokhttp3/Response;)V + public fun cacheHit (Lokhttp3/Call;Lokhttp3/Response;)V + public fun cacheMiss (Lokhttp3/Call;)V + public fun callEnd (Lokhttp3/Call;)V + public fun callFailed (Lokhttp3/Call;Ljava/io/IOException;)V + public fun callStart (Lokhttp3/Call;)V + public fun canceled (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 satisfactionFailure (Lokhttp3/Call;Lokhttp3/Response;)V + public fun secureConnectEnd (Lokhttp3/Call;Lokhttp3/Handshake;)V + public fun secureConnectStart (Lokhttp3/Call;)V +} + +public final class io/sentry/okhttp/SentryOkHttpEventListener$Companion { +} + +public class io/sentry/okhttp/SentryOkHttpInterceptor : okhttp3/Interceptor { + public fun ()V + public fun (Lio/sentry/IHub;)V + public fun (Lio/sentry/IHub;Lio/sentry/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;ZLjava/util/List;Ljava/util/List;)V + public synthetic fun (Lio/sentry/IHub;Lio/sentry/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;ZLjava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Lio/sentry/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback;)V + public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; +} + +public abstract interface class io/sentry/okhttp/SentryOkHttpInterceptor$BeforeSpanCallback { + public abstract fun execute (Lio/sentry/ISpan;Lokhttp3/Request;Lokhttp3/Response;)Lio/sentry/ISpan; +} + diff --git a/sentry-okhttp/build.gradle.kts b/sentry-okhttp/build.gradle.kts new file mode 100644 index 0000000000..a30e2d0594 --- /dev/null +++ b/sentry-okhttp/build.gradle.kts @@ -0,0 +1,92 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.config.KotlinCompilerVersion +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + kotlin("jvm") + jacoco + id(Config.QualityPlugins.errorProne) + id(Config.QualityPlugins.gradleVersions) + id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion +} + +configure { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() +} + +kotlin { + explicitApi() +} + +dependencies { + api(projects.sentry) + + compileOnly(Config.Libs.okhttp) + + implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) + + compileOnly(Config.CompileOnly.nopen) + errorprone(Config.CompileOnly.nopenChecker) + errorprone(Config.CompileOnly.errorprone) + errorprone(Config.CompileOnly.errorProneNullAway) + compileOnly(Config.CompileOnly.jetbrainsAnnotations) + + // tests + testImplementation(projects.sentryTestSupport) + testImplementation(Config.Libs.okhttp) + testImplementation(Config.TestLibs.kotlinTestJunit) + testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.mockitoInline) + testImplementation(Config.TestLibs.mockWebserver) +} + +configure { + test { + java.srcDir("src/test/java") + } +} + +jacoco { + toolVersion = Config.QualityPlugins.Jacoco.version +} + +tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(false) + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { + rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } + } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +buildConfig { + useJavaOutput() + packageName("io.sentry.okhttp") + buildConfigField("String", "SENTRY_OKHTTP_SDK_NAME", "\"${Config.Sentry.SENTRY_OKHTTP_SDK_NAME}\"") + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") +} + +val generateBuildConfig by tasks +tasks.withType().configureEach { + dependsOn(generateBuildConfig) + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt similarity index 92% rename from sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt rename to sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt index e38445afac..4870c8bb64 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEvent.kt @@ -1,4 +1,4 @@ -package io.sentry.android.okhttp +package io.sentry.okhttp import io.sentry.Breadcrumb import io.sentry.Hint @@ -9,13 +9,13 @@ import io.sentry.SentryLevel import io.sentry.SpanDataConvention import io.sentry.SpanStatus import io.sentry.TypeCheckHint -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.CONNECTION_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.CONNECT_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.REQUEST_BODY_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.REQUEST_HEADERS_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_BODY_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_HEADERS_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.SECURE_CONNECT_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.CONNECTION_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.CONNECT_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.REQUEST_BODY_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.REQUEST_HEADERS_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_BODY_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_HEADERS_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.SECURE_CONNECT_EVENT import io.sentry.util.Platform import io.sentry.util.UrlUtils import okhttp3.Request diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEventListener.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEventListener.kt new file mode 100644 index 0000000000..67a8cd8b56 --- /dev/null +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpEventListener.kt @@ -0,0 +1,411 @@ +package io.sentry.okhttp + +import io.sentry.HubAdapter +import io.sentry.IHub +import io.sentry.SpanDataConvention +import io.sentry.SpanStatus +import okhttp3.Call +import okhttp3.Connection +import okhttp3.EventListener +import okhttp3.Handshake +import okhttp3.HttpUrl +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response +import java.io.IOException +import java.net.InetAddress +import java.net.InetSocketAddress +import java.net.Proxy +import java.util.concurrent.ConcurrentHashMap + +/** + * Logs network performance event metrics to Sentry + * + * Usage - add instance of [SentryOkHttpEventListener] in [okhttp3.OkHttpClient.Builder.eventListener] + * + * ``` + * val client = OkHttpClient.Builder() + * .eventListener(SentryOkHttpEventListener()) + * .addInterceptor(SentryOkHttpInterceptor()) + * .build() + * ``` + * + * If you already use a [okhttp3.EventListener], you can pass it in the constructor. + * + * ``` + * val client = OkHttpClient.Builder() + * .eventListener(SentryOkHttpEventListener(myEventListener)) + * .addInterceptor(SentryOkHttpInterceptor()) + * .build() + * ``` + */ +@Suppress("TooManyFunctions") +public open class SentryOkHttpEventListener( + private val hub: IHub = HubAdapter.getInstance(), + private val originalEventListenerCreator: ((call: Call) -> EventListener)? = null +) : EventListener() { + + private var originalEventListener: EventListener? = null + + public companion object { + internal const val PROXY_SELECT_EVENT = "proxy_select" + internal const val DNS_EVENT = "dns" + internal const val SECURE_CONNECT_EVENT = "secure_connect" + internal const val CONNECT_EVENT = "connect" + internal const val CONNECTION_EVENT = "connection" + internal const val REQUEST_HEADERS_EVENT = "request_headers" + internal const val REQUEST_BODY_EVENT = "request_body" + internal const val RESPONSE_HEADERS_EVENT = "response_headers" + internal const val RESPONSE_BODY_EVENT = "response_body" + + internal val eventMap: MutableMap = ConcurrentHashMap() + } + + public constructor() : this( + HubAdapter.getInstance(), + originalEventListenerCreator = null + ) + + public constructor(originalEventListener: EventListener) : this( + HubAdapter.getInstance(), + originalEventListenerCreator = { originalEventListener } + ) + + public constructor(originalEventListenerFactory: Factory) : this( + HubAdapter.getInstance(), + originalEventListenerCreator = { originalEventListenerFactory.create(it) } + ) + + public constructor(hub: IHub = HubAdapter.getInstance(), originalEventListener: EventListener) : this( + hub, + originalEventListenerCreator = { originalEventListener } + ) + + public constructor(hub: IHub = HubAdapter.getInstance(), originalEventListenerFactory: Factory) : this( + hub, + originalEventListenerCreator = { originalEventListenerFactory.create(it) } + ) + + override fun callStart(call: Call) { + originalEventListener = originalEventListenerCreator?.invoke(call) + originalEventListener?.callStart(call) + // If the wrapped EventListener is ours, we can just delegate the calls, + // without creating other events that would create duplicates + if (canCreateEventSpan()) { + eventMap[call] = SentryOkHttpEvent(hub, call.request()) + } + } + + override fun proxySelectStart(call: Call, url: HttpUrl) { + originalEventListener?.proxySelectStart(call, url) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(PROXY_SELECT_EVENT) + } + + override fun proxySelectEnd( + call: Call, + url: HttpUrl, + proxies: List + ) { + originalEventListener?.proxySelectEnd(call, url, proxies) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.finishSpan(PROXY_SELECT_EVENT) { + if (proxies.isNotEmpty()) { + it.setData("proxies", proxies.joinToString { proxy -> proxy.toString() }) + } + } + } + + override fun dnsStart(call: Call, domainName: String) { + originalEventListener?.dnsStart(call, domainName) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(DNS_EVENT) + } + + override fun dnsEnd( + call: Call, + domainName: String, + inetAddressList: List + ) { + originalEventListener?.dnsEnd(call, domainName, inetAddressList) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.finishSpan(DNS_EVENT) { + it.setData("domain_name", domainName) + if (inetAddressList.isNotEmpty()) { + it.setData("dns_addresses", inetAddressList.joinToString { address -> address.toString() }) + } + } + } + + override fun connectStart( + call: Call, + inetSocketAddress: InetSocketAddress, + proxy: Proxy + ) { + originalEventListener?.connectStart(call, inetSocketAddress, proxy) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(CONNECT_EVENT) + } + + override fun secureConnectStart(call: Call) { + originalEventListener?.secureConnectStart(call) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(SECURE_CONNECT_EVENT) + } + + override fun secureConnectEnd(call: Call, handshake: Handshake?) { + originalEventListener?.secureConnectEnd(call, handshake) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.finishSpan(SECURE_CONNECT_EVENT) + } + + override fun connectEnd( + call: Call, + inetSocketAddress: InetSocketAddress, + proxy: Proxy, + protocol: Protocol? + ) { + originalEventListener?.connectEnd(call, inetSocketAddress, proxy, protocol) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.setProtocol(protocol?.name) + okHttpEvent.finishSpan(CONNECT_EVENT) + } + + override fun connectFailed( + call: Call, + inetSocketAddress: InetSocketAddress, + proxy: Proxy, + protocol: Protocol?, + ioe: IOException + ) { + originalEventListener?.connectFailed(call, inetSocketAddress, proxy, protocol, ioe) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.setProtocol(protocol?.name) + okHttpEvent.setError(ioe.message) + okHttpEvent.finishSpan(CONNECT_EVENT) { + it.throwable = ioe + it.status = SpanStatus.INTERNAL_ERROR + } + } + + override fun connectionAcquired(call: Call, connection: Connection) { + originalEventListener?.connectionAcquired(call, connection) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(CONNECTION_EVENT) + } + + override fun connectionReleased(call: Call, connection: Connection) { + originalEventListener?.connectionReleased(call, connection) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.finishSpan(CONNECTION_EVENT) + } + + override fun requestHeadersStart(call: Call) { + originalEventListener?.requestHeadersStart(call) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(REQUEST_HEADERS_EVENT) + } + + override fun requestHeadersEnd(call: Call, request: Request) { + originalEventListener?.requestHeadersEnd(call, request) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.finishSpan(REQUEST_HEADERS_EVENT) + } + + override fun requestBodyStart(call: Call) { + originalEventListener?.requestBodyStart(call) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(REQUEST_BODY_EVENT) + } + + override fun requestBodyEnd(call: Call, byteCount: Long) { + originalEventListener?.requestBodyEnd(call, byteCount) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.finishSpan(REQUEST_BODY_EVENT) { + if (byteCount > 0) { + it.setData("http.request_content_length", byteCount) + } + } + okHttpEvent.setRequestBodySize(byteCount) + } + + override fun requestFailed(call: Call, ioe: IOException) { + originalEventListener?.requestFailed(call, ioe) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.setError(ioe.message) + // requestFailed can happen after requestHeaders or requestBody. + // If requestHeaders already finished, we don't change its status. + okHttpEvent.finishSpan(REQUEST_HEADERS_EVENT) { + if (!it.isFinished) { + it.status = SpanStatus.INTERNAL_ERROR + it.throwable = ioe + } + } + okHttpEvent.finishSpan(REQUEST_BODY_EVENT) { + it.status = SpanStatus.INTERNAL_ERROR + it.throwable = ioe + } + } + + override fun responseHeadersStart(call: Call) { + originalEventListener?.responseHeadersStart(call) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(RESPONSE_HEADERS_EVENT) + } + + override fun responseHeadersEnd(call: Call, response: Response) { + originalEventListener?.responseHeadersEnd(call, response) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.setResponse(response) + val responseHeadersSpan = okHttpEvent.finishSpan(RESPONSE_HEADERS_EVENT) { + it.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, response.code) + // Let's not override the status of a span that was set + if (it.status == null) { + it.status = SpanStatus.fromHttpStatusCode(response.code) + } + } + okHttpEvent.scheduleFinish(responseHeadersSpan?.finishDate ?: hub.options.dateProvider.now()) + } + + override fun responseBodyStart(call: Call) { + originalEventListener?.responseBodyStart(call) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.startSpan(RESPONSE_BODY_EVENT) + } + + override fun responseBodyEnd(call: Call, byteCount: Long) { + originalEventListener?.responseBodyEnd(call, byteCount) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.setResponseBodySize(byteCount) + okHttpEvent.finishSpan(RESPONSE_BODY_EVENT) { + if (byteCount > 0) { + it.setData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY, byteCount) + } + } + } + + override fun responseFailed(call: Call, ioe: IOException) { + originalEventListener?.responseFailed(call, ioe) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap[call] ?: return + okHttpEvent.setError(ioe.message) + // responseFailed can happen after responseHeaders or responseBody. + // If responseHeaders already finished, we don't change its status. + okHttpEvent.finishSpan(RESPONSE_HEADERS_EVENT) { + if (!it.isFinished) { + it.status = SpanStatus.INTERNAL_ERROR + it.throwable = ioe + } + } + okHttpEvent.finishSpan(RESPONSE_BODY_EVENT) { + it.status = SpanStatus.INTERNAL_ERROR + it.throwable = ioe + } + } + + override fun callEnd(call: Call) { + originalEventListener?.callEnd(call) + val okHttpEvent: SentryOkHttpEvent = eventMap.remove(call) ?: return + okHttpEvent.finishEvent() + } + + override fun callFailed(call: Call, ioe: IOException) { + originalEventListener?.callFailed(call, ioe) + if (!canCreateEventSpan()) { + return + } + val okHttpEvent: SentryOkHttpEvent = eventMap.remove(call) ?: return + okHttpEvent.setError(ioe.message) + okHttpEvent.finishEvent { + it.status = SpanStatus.INTERNAL_ERROR + it.throwable = ioe + } + } + + override fun canceled(call: Call) { + originalEventListener?.canceled(call) + } + + override fun satisfactionFailure(call: Call, response: Response) { + originalEventListener?.satisfactionFailure(call, response) + } + + override fun cacheHit(call: Call, response: Response) { + originalEventListener?.cacheHit(call, response) + } + + override fun cacheMiss(call: Call) { + originalEventListener?.cacheMiss(call) + } + + override fun cacheConditionalHit(call: Call, cachedResponse: Response) { + originalEventListener?.cacheConditionalHit(call, cachedResponse) + } + + private fun canCreateEventSpan(): Boolean { + // If the wrapped EventListener is ours, we shouldn't create spans, as the originalEventListener already did it + return originalEventListener !is SentryOkHttpEventListener + } +} diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt new file mode 100644 index 0000000000..1f1aaf8c4e --- /dev/null +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt @@ -0,0 +1,228 @@ +package io.sentry.okhttp + +import io.sentry.BaggageHeader +import io.sentry.Breadcrumb +import io.sentry.Hint +import io.sentry.HttpStatusCodeRange +import io.sentry.HubAdapter +import io.sentry.IHub +import io.sentry.ISpan +import io.sentry.SentryIntegrationPackageStorage +import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS +import io.sentry.SpanDataConvention +import io.sentry.SpanStatus +import io.sentry.TypeCheckHint.OKHTTP_REQUEST +import io.sentry.TypeCheckHint.OKHTTP_RESPONSE +import io.sentry.okhttp.SentryOkHttpInterceptor.BeforeSpanCallback +import io.sentry.util.IntegrationUtils.addIntegrationToSdkVersion +import io.sentry.util.Platform +import io.sentry.util.PropagationTargetsUtils +import io.sentry.util.TracingUtils +import io.sentry.util.UrlUtils +import okhttp3.Interceptor +import okhttp3.Request +import okhttp3.Response +import java.io.IOException + +/** + * The Sentry's [SentryOkHttpInterceptor], it will automatically add a breadcrumb and start a span + * out of the active span bound to the scope for each HTTP Request. + * If [captureFailedRequests] is enabled, the SDK will capture HTTP Client errors as well. + * + * @param hub The [IHub], internal and only used for testing. + * @param beforeSpan The [ISpan] can be customized or dropped with the [BeforeSpanCallback]. + * @param captureFailedRequests The SDK will only capture HTTP Client errors if it is enabled, + * Defaults to false. + * @param failedRequestStatusCodes The SDK will only capture HTTP Client errors if the HTTP Response + * status code is within the defined ranges. + * @param failedRequestTargets The SDK will only capture HTTP Client errors if the HTTP Request URL + * is a match for any of the defined targets. + */ +public open class SentryOkHttpInterceptor( + private val hub: IHub = HubAdapter.getInstance(), + private val beforeSpan: BeforeSpanCallback? = null, + private val captureFailedRequests: Boolean = true, + private val failedRequestStatusCodes: List = listOf( + HttpStatusCodeRange(HttpStatusCodeRange.DEFAULT_MIN, HttpStatusCodeRange.DEFAULT_MAX) + ), + private val failedRequestTargets: List = listOf(DEFAULT_PROPAGATION_TARGETS) +) : Interceptor { + + public constructor() : this(HubAdapter.getInstance()) + public constructor(hub: IHub) : this(hub, null) + public constructor(beforeSpan: BeforeSpanCallback) : this(HubAdapter.getInstance(), beforeSpan) + + init { + addIntegrationToSdkVersion(javaClass) + SentryIntegrationPackageStorage.getInstance() + .addPackage("maven:io.sentry:sentry-okhttp", BuildConfig.VERSION_NAME) + } + + @Suppress("LongMethod") + override fun intercept(chain: Interceptor.Chain): Response { + var request = chain.request() + + val urlDetails = UrlUtils.parse(request.url.toString()) + val url = urlDetails.urlOrFallback + val method = request.method + + val span: ISpan? + val okHttpEvent: SentryOkHttpEvent? + + if (SentryOkHttpEventListener.eventMap.containsKey(chain.call())) { + // read the span from the event listener + okHttpEvent = SentryOkHttpEventListener.eventMap[chain.call()] + span = okHttpEvent?.callRootSpan + } else { + // read the span from the bound scope + okHttpEvent = null + val parentSpan = if (Platform.isAndroid()) hub.transaction else hub.span + span = parentSpan?.startChild("http.client", "$method $url") + } + + span?.spanContext?.origin = TRACE_ORIGIN + + urlDetails.applyToSpan(span) + + val isFromEventListener = okHttpEvent != null + var response: Response? = null + var code: Int? = null + + try { + val requestBuilder = request.newBuilder() + + TracingUtils.traceIfAllowed( + hub, + request.url.toString(), + request.headers(BaggageHeader.BAGGAGE_HEADER), + span + )?.let { tracingHeaders -> + requestBuilder.addHeader(tracingHeaders.sentryTraceHeader.name, tracingHeaders.sentryTraceHeader.value) + tracingHeaders.baggageHeader?.let { + requestBuilder.removeHeader(BaggageHeader.BAGGAGE_HEADER) + requestBuilder.addHeader(it.name, it.value) + } + } + + request = requestBuilder.build() + response = chain.proceed(request) + code = response.code + span?.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, code) + span?.status = SpanStatus.fromHttpStatusCode(code) + + // OkHttp errors (4xx, 5xx) don't throw, so it's safe to call within this block. + // breadcrumbs are added on the finally block because we'd like to know if the device + // had an unstable connection or something similar + if (shouldCaptureClientError(request, response)) { + // If we capture the client error directly, it could be associated with the + // currently running span by the backend. In case the listener is in use, that is + // an inner span. So, if the listener is in use, we let it capture the client + // error, to shown it in the http root call span in the dashboard. + if (isFromEventListener && okHttpEvent != null) { + okHttpEvent.setClientErrorResponse(response) + } else { + SentryOkHttpUtils.captureClientError(hub, request, response) + } + } + + return response + } catch (e: IOException) { + span?.apply { + this.throwable = e + this.status = SpanStatus.INTERNAL_ERROR + } + throw e + } finally { + finishSpan(span, request, response, isFromEventListener) + + // The SentryOkHttpEventListener will send the breadcrumb itself if used for this call + if (!isFromEventListener) { + sendBreadcrumb(request, code, response) + } + } + } + + private fun sendBreadcrumb(request: Request, code: Int?, response: Response?) { + val breadcrumb = Breadcrumb.http(request.url.toString(), request.method, code) + request.body?.contentLength().ifHasValidLength { + breadcrumb.setData("http.request_content_length", it) + } + + val hint = Hint().also { it.set(OKHTTP_REQUEST, request) } + response?.let { + it.body?.contentLength().ifHasValidLength { responseBodySize -> + breadcrumb.setData(SpanDataConvention.HTTP_RESPONSE_CONTENT_LENGTH_KEY, responseBodySize) + } + + hint[OKHTTP_RESPONSE] = it + } + + hub.addBreadcrumb(breadcrumb, hint) + } + + private fun finishSpan(span: ISpan?, request: Request, response: Response?, isFromEventListener: Boolean) { + if (span == null) { + return + } + if (beforeSpan != null) { + val result = beforeSpan.execute(span, request, response) + if (result == null) { + // span is dropped + span.spanContext.sampled = false + } else { + // The SentryOkHttpEventListener will finish the span itself if used for this call + if (!isFromEventListener) { + span.finish() + } + } + } else { + // The SentryOkHttpEventListener will finish the span itself if used for this call + if (!isFromEventListener) { + span.finish() + } + } + } + + private fun Long?.ifHasValidLength(fn: (Long) -> Unit) { + if (this != null && this != -1L) { + fn.invoke(this) + } + } + + private fun shouldCaptureClientError(request: Request, response: Response): Boolean { + // return if the feature is disabled or its not within the range + if (!captureFailedRequests || !containsStatusCode(response.code)) { + return false + } + + // return if its not a target match + if (!PropagationTargetsUtils.contain(failedRequestTargets, request.url.toString())) { + return false + } + + return true + } + + private fun containsStatusCode(statusCode: Int): Boolean { + for (item in failedRequestStatusCodes) { + if (item.isInRange(statusCode)) { + return true + } + } + return false + } + + /** + * The BeforeSpan callback + */ + public fun interface BeforeSpanCallback { + /** + * Mutates or drops span before being added + * + * @param span the span to mutate or drop + * @param request the HTTP request executed by okHttp + * @param response the HTTP response received by okHttp + */ + public fun execute(span: ISpan, request: Request, response: Response?): ISpan? + } +} diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpUtils.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpUtils.kt similarity index 95% rename from sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpUtils.kt rename to sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpUtils.kt index 8d9a24edbc..0cfc1c5a75 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpUtils.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpUtils.kt @@ -1,4 +1,4 @@ -package io.sentry.android.okhttp +package io.sentry.okhttp import io.sentry.Hint import io.sentry.IHub @@ -13,9 +13,9 @@ import okhttp3.Headers import okhttp3.Request import okhttp3.Response -object SentryOkHttpUtils { +internal object SentryOkHttpUtils { - fun captureClientError(hub: IHub, request: Request, response: Response) { + internal fun captureClientError(hub: IHub, request: Request, response: Response) { // not possible to get a parameterized url, but we remove at least the // query string and the fragment. // url example: https://api.github.com/users/getsentry/repos/#fragment?query=query diff --git a/sentry-okhttp/src/main/resources/META-INF/proguard/sentry-okhttp.pro b/sentry-okhttp/src/main/resources/META-INF/proguard/sentry-okhttp.pro new file mode 100644 index 0000000000..3f9ea4feb2 --- /dev/null +++ b/sentry-okhttp/src/main/resources/META-INF/proguard/sentry-okhttp.pro @@ -0,0 +1,13 @@ +##---------------Begin: proguard configuration for OkHttp ---------- + +# To ensure that stack traces is unambiguous +# https://developer.android.com/studio/build/shrink-code#decode-stack-trace +-keepattributes LineNumberTable,SourceFile + +# https://square.github.io/okhttp/features/r8_proguard/ +# If you use OkHttp as a dependency in an Android project which uses R8 as a default compiler you +# don’t have to do anything. The specific rules are already bundled into the JAR which can +# be interpreted by R8 automatically. +# https://raw.githubusercontent.com/square/okhttp/master/okhttp/src/jvmMain/resources/META-INF/proguard/okhttp3.pro + +##---------------End: proguard configuration for OkHttp ---------- diff --git a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpEventListenerTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt similarity index 99% rename from sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpEventListenerTest.kt rename to sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt index a1cea5e3d7..c6d10fce10 100644 --- a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpEventListenerTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventListenerTest.kt @@ -1,4 +1,4 @@ -package io.sentry.android.okhttp +package io.sentry.okhttp import io.sentry.BaggageHeader import io.sentry.IHub diff --git a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpEventTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt similarity index 97% rename from sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpEventTest.kt rename to sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt index 9fab3b475c..1363c23785 100644 --- a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpEventTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpEventTest.kt @@ -1,4 +1,4 @@ -package io.sentry.android.okhttp +package io.sentry.okhttp import io.sentry.Breadcrumb import io.sentry.Hint @@ -15,14 +15,14 @@ import io.sentry.SpanStatus import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext import io.sentry.TypeCheckHint -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.CONNECTION_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.CONNECT_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.REQUEST_BODY_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.REQUEST_HEADERS_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_BODY_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_HEADERS_EVENT -import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.SECURE_CONNECT_EVENT import io.sentry.exception.SentryHttpClientException +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.CONNECTION_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.CONNECT_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.REQUEST_BODY_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.REQUEST_HEADERS_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_BODY_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_HEADERS_EVENT +import io.sentry.okhttp.SentryOkHttpEventListener.Companion.SECURE_CONNECT_EVENT import io.sentry.test.getProperty import okhttp3.Protocol import okhttp3.Request diff --git a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt similarity index 99% rename from sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt rename to sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt index b01ab8edd2..b856d93fb1 100644 --- a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpInterceptorTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt @@ -1,6 +1,6 @@ @file:Suppress("MaxLineLength") -package io.sentry.android.okhttp +package io.sentry.okhttp import io.sentry.BaggageHeader import io.sentry.Breadcrumb diff --git a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpUtilsTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpUtilsTest.kt similarity index 99% rename from sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpUtilsTest.kt rename to sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpUtilsTest.kt index 6f9ea500a7..ec19454327 100644 --- a/sentry-android-okhttp/src/test/java/io/sentry/android/okhttp/SentryOkHttpUtilsTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpUtilsTest.kt @@ -1,4 +1,4 @@ -package io.sentry.android.okhttp +package io.sentry.okhttp import io.sentry.Hint import io.sentry.IHub diff --git a/sentry-android-okhttp/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/sentry-okhttp/src/test/resources/mockito-extensions/org.mockito.plugin.MockMaker similarity index 100% rename from sentry-android-okhttp/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker rename to sentry-okhttp/src/test/resources/mockito-extensions/org.mockito.plugin.MockMaker diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GithubAPI.kt b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GithubAPI.kt index 09ad9a2d07..66e455a190 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GithubAPI.kt +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/GithubAPI.kt @@ -1,8 +1,8 @@ package io.sentry.samples.android import io.sentry.HttpStatusCodeRange -import io.sentry.android.okhttp.SentryOkHttpEventListener -import io.sentry.android.okhttp.SentryOkHttpInterceptor +import io.sentry.okhttp.SentryOkHttpEventListener +import io.sentry.okhttp.SentryOkHttpInterceptor import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory diff --git a/settings.gradle.kts b/settings.gradle.kts index f87b9e0126..cdec3d7181 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -46,6 +46,7 @@ include( "sentry-opentelemetry:sentry-opentelemetry-agentcustomization", "sentry-opentelemetry:sentry-opentelemetry-agent", "sentry-quartz", + "sentry-okhttp", "sentry-samples:sentry-samples-android", "sentry-samples:sentry-samples-console", "sentry-samples:sentry-samples-jul",