Skip to content

Commit 8a42315

Browse files
committed
okhttp detection
1 parent ae6ec68 commit 8a42315

File tree

4 files changed

+114
-4
lines changed

4 files changed

+114
-4
lines changed

sentry-ktor-client/api/sentry-ktor-client.api

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ public final class io/sentry/ktorClient/SentryKtorClientPluginConfig {
77
public fun <init> ()V
88
public final fun getBeforeSpan ()Lio/sentry/ktorClient/SentryKtorClientPluginConfig$BeforeSpanCallback;
99
public final fun getCaptureFailedRequests ()Z
10+
public final fun getEnabled ()Z
1011
public final fun getFailedRequestStatusCodes ()Ljava/util/List;
1112
public final fun getFailedRequestTargets ()Ljava/util/List;
1213
public final fun getScopes ()Lio/sentry/IScopes;
1314
public final fun setBeforeSpan (Lio/sentry/ktorClient/SentryKtorClientPluginConfig$BeforeSpanCallback;)V
1415
public final fun setCaptureFailedRequests (Z)V
16+
public final fun setEnabled (Z)V
1517
public final fun setFailedRequestStatusCodes (Ljava/util/List;)V
1618
public final fun setFailedRequestTargets (Ljava/util/List;)V
1719
public final fun setScopes (Lio/sentry/IScopes;)V
@@ -22,7 +24,8 @@ public abstract interface class io/sentry/ktorClient/SentryKtorClientPluginConfi
2224
}
2325

2426
public class io/sentry/ktorClient/SentryKtorClientPluginContextHook : io/ktor/client/plugins/api/ClientHook {
25-
public fun <init> (Lio/sentry/IScopes;)V
27+
public fun <init> (Lio/sentry/IScopes;Z)V
28+
protected final fun getEnabled ()Z
2629
protected final fun getScopes ()Lio/sentry/IScopes;
2730
public synthetic fun install (Lio/ktor/client/HttpClient;Ljava/lang/Object;)V
2831
public fun install (Lio/ktor/client/HttpClient;Lkotlin/jvm/functions/Function2;)V

sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientPlugin.kt

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.sentry.ktorClient
22

33
import io.ktor.client.HttpClient
4+
import io.ktor.client.engine.okhttp.OkHttpConfig
45
import io.ktor.client.plugins.api.*
56
import io.ktor.client.plugins.api.ClientPlugin
67
import io.ktor.client.request.*
@@ -24,7 +25,9 @@ import io.sentry.util.Platform
2425
import io.sentry.util.PropagationTargetsUtils
2526
import io.sentry.util.SpanUtils
2627
import io.sentry.util.TracingUtils
28+
import java.lang.reflect.Field
2729
import kotlinx.coroutines.withContext
30+
import okhttp3.OkHttpClient
2831

2932
/** Configuration for the Sentry Ktor client plugin. */
3033
public class SentryKtorClientPluginConfig {
@@ -62,6 +65,9 @@ public class SentryKtorClientPluginConfig {
6265
public fun execute(span: ISpan, request: HttpRequest): ISpan?
6366
}
6467

68+
/** Whether the plugin is enabled. If disabled, the plugin has no effect. Defaults to true. */
69+
public var enabled: Boolean = true
70+
6571
/**
6672
* Forcefully use the passed in scope instead of relying on the one injected by [SentryContext].
6773
* Used for testing.
@@ -78,6 +84,52 @@ internal const val TRACE_ORIGIN = "auto.http.ktor-client"
7884
*/
7985
public val SentryKtorClientPlugin: ClientPlugin<SentryKtorClientPluginConfig> =
8086
createClientPlugin(SENTRY_KTOR_CLIENT_PLUGIN_KEY, ::SentryKtorClientPluginConfig) {
87+
/**
88+
* Disables the plugin, if necessary.
89+
*
90+
* Currently, the only case in which we want to disable the plugin is when we detect that the
91+
* OkHttp engine is used and SentryOkHttpInterceptor is registered, as otherwise all HTTP
92+
* requests would be doubly instrumented.
93+
*/
94+
fun maybeDisable() {
95+
if (client.engine.config is OkHttpConfig) {
96+
val config = client.engine.config as OkHttpConfig
97+
98+
// Case 1: OkHttp client initialized by Ktor and configured with a `config` block.
99+
//
100+
// The OkHttp client is initialized only upon the first request.
101+
// Attempt to initialize a client to inspect the interceptors that are registered on it.
102+
try {
103+
val configField: Field = OkHttpConfig::class.java.getDeclaredField("config")
104+
configField.isAccessible = true
105+
val configFunction = configField.get(config) as? (OkHttpClient.Builder.() -> Unit)
106+
107+
if (configFunction != null) {
108+
val builder = okhttp3.OkHttpClient.Builder()
109+
configFunction.invoke(builder)
110+
val client = builder.build()
111+
if (client.interceptors.any { it.javaClass.name.contains("SentryOkHttpInterceptor") }) {
112+
pluginConfig.enabled = false
113+
}
114+
}
115+
} catch (_: Throwable) {}
116+
117+
// Case 2: pre-configured OkHttp client passed in.
118+
val client = config.preconfigured
119+
if (client != null) {
120+
if (client.interceptors.any { it.javaClass.name.contains("SentryOkHttpInterceptor") }) {
121+
pluginConfig.enabled = false
122+
}
123+
}
124+
}
125+
}
126+
127+
maybeDisable()
128+
129+
if (!pluginConfig.enabled) {
130+
return@createClientPlugin
131+
}
132+
81133
// Init
82134
SentryIntegrationPackageStorage.getInstance()
83135
.addPackage("maven:io.sentry:sentry-ktor-client", BuildConfig.VERSION_NAME)
@@ -98,6 +150,10 @@ public val SentryKtorClientPlugin: ClientPlugin<SentryKtorClientPluginConfig> =
98150
val requestSpanKey = AttributeKey<ISpan>("SentryRequestSpan")
99151

100152
onRequest { request, _ ->
153+
if (!this@createClientPlugin.pluginConfig.enabled) {
154+
return@onRequest
155+
}
156+
101157
request.attributes.put(
102158
requestStartTimestampKey,
103159
(if (forceScopes) scopes else Sentry.getCurrentScopes()).options.dateProvider.now(),
@@ -141,6 +197,10 @@ public val SentryKtorClientPlugin: ClientPlugin<SentryKtorClientPluginConfig> =
141197
}
142198

143199
client.requestPipeline.intercept(HttpRequestPipeline.Before) {
200+
if (!this@createClientPlugin.pluginConfig.enabled) {
201+
proceed()
202+
}
203+
144204
try {
145205
proceed()
146206
} catch (t: Throwable) {
@@ -154,6 +214,10 @@ public val SentryKtorClientPlugin: ClientPlugin<SentryKtorClientPluginConfig> =
154214
}
155215

156216
onResponse { response ->
217+
if (!this@createClientPlugin.pluginConfig.enabled) {
218+
return@onResponse
219+
}
220+
157221
val request = response.request
158222
val startTimestamp = response.call.attributes.getOrNull(requestStartTimestampKey)
159223
val endTimestamp =
@@ -186,18 +250,24 @@ public val SentryKtorClientPlugin: ClientPlugin<SentryKtorClientPluginConfig> =
186250
}
187251
}
188252

189-
on(SentryKtorClientPluginContextHook(scopes)) { block -> block() }
253+
on(SentryKtorClientPluginContextHook(scopes, pluginConfig.enabled)) { block -> block() }
190254
}
191255

192256
/**
193257
* Context hook to manage scopes during request handling. Forks the current scope and uses
194258
* [SentryContext] to ensure that the whole pipeline runs within the correct scopes.
195259
*/
196-
public open class SentryKtorClientPluginContextHook(protected val scopes: IScopes) :
197-
ClientHook<suspend (suspend () -> Unit) -> Unit> {
260+
public open class SentryKtorClientPluginContextHook(
261+
protected val scopes: IScopes,
262+
protected val enabled: Boolean,
263+
) : ClientHook<suspend (suspend () -> Unit) -> Unit> {
198264
private val phase = PipelinePhase("SentryKtorClientPluginContext")
199265

200266
override fun install(client: HttpClient, handler: suspend (suspend () -> Unit) -> Unit) {
267+
if (!enabled) {
268+
return
269+
}
270+
201271
client.requestPipeline.insertPhaseBefore(HttpRequestPipeline.Before, phase)
202272
client.requestPipeline.intercept(phase) {
203273
val scopes =
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
##---------------Begin: proguard configuration for Ktor Client ----------
2+
3+
-keepclassmembers class io.ktor.client.engine.okhttp.OkHttpConfig {
4+
kotlin.jvm.functions.Function1 config;
5+
}
6+
7+
-keepnames class io.sentry.sentry.okhttp.**
8+
9+
##---------------End: proguard configuration for Ktor Client ----------

sentry-ktor-client/src/test/java/io/sentry/ktorClient/SentryKtorClientPluginTest.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package io.sentry.ktorClient
33
import io.ktor.client.HttpClient
44
import io.ktor.client.engine.HttpClientEngine
55
import io.ktor.client.engine.java.Java
6+
import io.ktor.client.engine.okhttp.OkHttpConfig
7+
import io.ktor.client.engine.okhttp.OkHttpEngine
68
import io.ktor.client.request.get
79
import io.ktor.client.request.post
810
import io.ktor.client.request.setBody
@@ -26,13 +28,16 @@ import io.sentry.SpanStatus
2628
import io.sentry.TransactionContext
2729
import io.sentry.exception.SentryHttpClientException
2830
import io.sentry.mockServerRequestTimeoutMillis
31+
import io.sentry.okhttp.SentryOkHttpInterceptor
2932
import java.util.concurrent.TimeUnit
33+
import kotlin.Unit
3034
import kotlin.test.Test
3135
import kotlin.test.assertEquals
3236
import kotlin.test.assertNotNull
3337
import kotlin.test.assertNull
3438
import kotlin.test.assertTrue
3539
import kotlinx.coroutines.runBlocking
40+
import okhttp3.OkHttpClient
3641
import okhttp3.mockwebserver.MockResponse
3742
import okhttp3.mockwebserver.MockWebServer
3843
import okhttp3.mockwebserver.SocketPolicy
@@ -425,4 +430,27 @@ class SentryKtorClientPluginTest {
425430
assertTrue(baggageHeaderValues[0].contains("sentry-transaction=name"))
426431
assertTrue(baggageHeaderValues[0].contains("sentry-trace_id"))
427432
}
433+
434+
@Test
435+
fun `is disabled when using OkHttp engine with preconfigured client using Sentry interceptor`():
436+
Unit = runBlocking {
437+
val okHttpClient = OkHttpClient.Builder().addInterceptor(SentryOkHttpInterceptor()).build()
438+
val engine = OkHttpEngine(OkHttpConfig().apply { preconfigured = okHttpClient })
439+
val sut = fixture.getSut(httpClientEngine = engine)
440+
441+
sut.get(fixture.server.url("/hello").toString())
442+
443+
assertEquals(0, fixture.sentryTracer.children.size)
444+
}
445+
446+
@Test
447+
fun `is disabled when using OkHttp engine initialized with Sentry interceptor in config block`():
448+
Unit = runBlocking {
449+
val engine = OkHttpEngine(OkHttpConfig().apply { addInterceptor(SentryOkHttpInterceptor()) })
450+
val sut = fixture.getSut(httpClientEngine = engine)
451+
452+
sut.get(fixture.server.url("/hello").toString())
453+
454+
assertEquals(0, fixture.sentryTracer.children.size)
455+
}
428456
}

0 commit comments

Comments
 (0)