1
1
package io.sentry.ktorClient
2
2
3
3
import io.ktor.client.HttpClient
4
+ import io.ktor.client.engine.okhttp.OkHttpConfig
4
5
import io.ktor.client.plugins.api.*
5
6
import io.ktor.client.plugins.api.ClientPlugin
6
7
import io.ktor.client.request.*
@@ -24,7 +25,9 @@ import io.sentry.util.Platform
24
25
import io.sentry.util.PropagationTargetsUtils
25
26
import io.sentry.util.SpanUtils
26
27
import io.sentry.util.TracingUtils
28
+ import java.lang.reflect.Field
27
29
import kotlinx.coroutines.withContext
30
+ import okhttp3.OkHttpClient
28
31
29
32
/* * Configuration for the Sentry Ktor client plugin. */
30
33
public class SentryKtorClientPluginConfig {
@@ -62,6 +65,9 @@ public class SentryKtorClientPluginConfig {
62
65
public fun execute (span : ISpan , request : HttpRequest ): ISpan ?
63
66
}
64
67
68
+ /* * Whether the plugin is enabled. If disabled, the plugin has no effect. Defaults to true. */
69
+ public var enabled: Boolean = true
70
+
65
71
/* *
66
72
* Forcefully use the passed in scope instead of relying on the one injected by [SentryContext].
67
73
* Used for testing.
@@ -78,6 +84,52 @@ internal const val TRACE_ORIGIN = "auto.http.ktor-client"
78
84
*/
79
85
public val SentryKtorClientPlugin : ClientPlugin <SentryKtorClientPluginConfig > =
80
86
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
+
81
133
// Init
82
134
SentryIntegrationPackageStorage .getInstance()
83
135
.addPackage(" maven:io.sentry:sentry-ktor-client" , BuildConfig .VERSION_NAME )
@@ -98,6 +150,10 @@ public val SentryKtorClientPlugin: ClientPlugin<SentryKtorClientPluginConfig> =
98
150
val requestSpanKey = AttributeKey <ISpan >(" SentryRequestSpan" )
99
151
100
152
onRequest { request, _ ->
153
+ if (! this @createClientPlugin.pluginConfig.enabled) {
154
+ return @onRequest
155
+ }
156
+
101
157
request.attributes.put(
102
158
requestStartTimestampKey,
103
159
(if (forceScopes) scopes else Sentry .getCurrentScopes()).options.dateProvider.now(),
@@ -141,6 +197,10 @@ public val SentryKtorClientPlugin: ClientPlugin<SentryKtorClientPluginConfig> =
141
197
}
142
198
143
199
client.requestPipeline.intercept(HttpRequestPipeline .Before ) {
200
+ if (! this @createClientPlugin.pluginConfig.enabled) {
201
+ proceed()
202
+ }
203
+
144
204
try {
145
205
proceed()
146
206
} catch (t: Throwable ) {
@@ -154,6 +214,10 @@ public val SentryKtorClientPlugin: ClientPlugin<SentryKtorClientPluginConfig> =
154
214
}
155
215
156
216
onResponse { response ->
217
+ if (! this @createClientPlugin.pluginConfig.enabled) {
218
+ return @onResponse
219
+ }
220
+
157
221
val request = response.request
158
222
val startTimestamp = response.call.attributes.getOrNull(requestStartTimestampKey)
159
223
val endTimestamp =
@@ -186,18 +250,24 @@ public val SentryKtorClientPlugin: ClientPlugin<SentryKtorClientPluginConfig> =
186
250
}
187
251
}
188
252
189
- on(SentryKtorClientPluginContextHook (scopes)) { block -> block() }
253
+ on(SentryKtorClientPluginContextHook (scopes, pluginConfig.enabled )) { block -> block() }
190
254
}
191
255
192
256
/* *
193
257
* Context hook to manage scopes during request handling. Forks the current scope and uses
194
258
* [SentryContext] to ensure that the whole pipeline runs within the correct scopes.
195
259
*/
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> {
198
264
private val phase = PipelinePhase (" SentryKtorClientPluginContext" )
199
265
200
266
override fun install (client : HttpClient , handler : suspend (suspend () -> Unit ) -> Unit ) {
267
+ if (! enabled) {
268
+ return
269
+ }
270
+
201
271
client.requestPipeline.insertPhaseBefore(HttpRequestPipeline .Before , phase)
202
272
client.requestPipeline.intercept(phase) {
203
273
val scopes =
0 commit comments