11package io.sentry.ktorClient
22
33import io.ktor.client.HttpClient
4+ import io.ktor.client.engine.okhttp.OkHttpConfig
45import io.ktor.client.plugins.api.*
56import io.ktor.client.plugins.api.ClientPlugin
67import io.ktor.client.request.*
@@ -24,7 +25,9 @@ import io.sentry.util.Platform
2425import io.sentry.util.PropagationTargetsUtils
2526import io.sentry.util.SpanUtils
2627import io.sentry.util.TracingUtils
28+ import java.lang.reflect.Field
2729import kotlinx.coroutines.withContext
30+ import okhttp3.OkHttpClient
2831
2932/* * Configuration for the Sentry Ktor client plugin. */
3033public 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 */
7985public 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.toString().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.toString().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 =
0 commit comments