Skip to content

Commit a2b6a4a

Browse files
committed
[Android] Add tracing interceptor
1 parent 148517f commit a2b6a4a

File tree

19 files changed

+495
-8
lines changed

19 files changed

+495
-8
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

platform/jvm/capture-plugin/src/main/kotlin/io/bitdrift/capture/instrumentation/okhttp/visitor/OkHttpEventListenerMethodVisitor.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ class OkHttpEventListenerMethodVisitor(
5353
) {
5454
private val captureOkHttpEventListenerFactory =
5555
"io/bitdrift/capture/network/okhttp/CaptureOkHttpEventListenerFactory"
56+
private val captureOkHttpTracingInterceptor =
57+
"io/bitdrift/capture/network/okhttp/CaptureOkHttpTracingInterceptor"
5658

5759
override fun onMethodEnter() {
5860
super.onMethodEnter()
@@ -64,6 +66,8 @@ class OkHttpEventListenerMethodVisitor(
6466
}
6567

6668
private fun addOverwritingEventListener() {
69+
addTracingInterceptor()
70+
6771
// Add the following call at the beginning of the constructor with the Builder parameter:
6872
// builder.eventListenerFactory(new CaptureOkHttpEventListenerFactory());
6973

@@ -95,9 +99,12 @@ class OkHttpEventListenerMethodVisitor(
9599
"(Lokhttp3/EventListener\$Factory;)Lokhttp3/OkHttpClient\$Builder;",
96100
false,
97101
)
102+
visitInsn(Opcodes.POP)
98103
}
99104

100105
private fun addProxyingEventListener() {
106+
addTracingInterceptor()
107+
101108
// Add the following call at the beginning of the constructor with the Builder parameter:
102109
// builder.eventListenerFactory(new CaptureOkHttpEventListenerFactory(builder.eventListenerFactory));
103110

@@ -141,5 +148,35 @@ class OkHttpEventListenerMethodVisitor(
141148
"(Lokhttp3/EventListener\$Factory;)Lokhttp3/OkHttpClient\$Builder;",
142149
false,
143150
)
151+
visitInsn(Opcodes.POP)
152+
}
153+
154+
private fun addTracingInterceptor() {
155+
// Add the following call at the beginning of the constructor with the Builder parameter:
156+
// builder.addInterceptor(new CaptureOkHttpTracingInterceptor());
157+
158+
// OkHttpClient.Builder is the parameter, retrieved here
159+
visitVarInsn(Opcodes.ALOAD, 1)
160+
161+
// Create CaptureOkHttpTracingInterceptor instance
162+
visitTypeInsn(Opcodes.NEW, captureOkHttpTracingInterceptor)
163+
visitInsn(Opcodes.DUP)
164+
visitMethodInsn(
165+
Opcodes.INVOKESPECIAL,
166+
captureOkHttpTracingInterceptor,
167+
"<init>",
168+
"()V",
169+
false,
170+
)
171+
172+
// Call addInterceptor(Interceptor)
173+
visitMethodInsn(
174+
Opcodes.INVOKEVIRTUAL,
175+
"okhttp3/OkHttpClient\$Builder",
176+
"addInterceptor",
177+
"(Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient\$Builder;",
178+
false,
179+
)
180+
visitInsn(Opcodes.POP)
144181
}
145182
}

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/Capture.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,14 @@ object Capture {
225225
val deviceId: String?
226226
get() = logger()?.deviceId
227227

228+
/**
229+
* Whether workflow-controlled tracing is currently active for this session.
230+
* Returns `null` prior to SDK start.
231+
*/
232+
@JvmStatic
233+
val isTracingActive: Boolean?
234+
get() = logger()?.isTracingActive
235+
228236
/**
229237
* Defines the initialization of a new session within the currently running logger
230238
* If no logger is started, this is a no-op.

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/CaptureJniLibrary.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ internal object CaptureJniLibrary : IBridge, IStreamingReportProcessor {
111111
*/
112112
external fun getDeviceId(loggerId: Long): String?
113113

114+
/**
115+
* Returns true when workflow-controlled tracing is active for the current session.
116+
*/
117+
external fun isTracingActive(loggerId: Long): Boolean
118+
114119
/**
115120
* Adds a field that should be attached to all logs emitted by the logger going forward.
116121
* If a field with a given key has already been registered with the logger, its value is

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/ILogger.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ interface ILogger {
3737
*/
3838
val deviceId: String
3939

40+
/**
41+
* Whether workflow-controlled tracing is currently active for this session.
42+
*/
43+
val isTracingActive: Boolean
44+
4045
/**
4146
* Defines the initialization of a new session within the current logger.
4247
*/
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// capture-sdk - bitdrift's client SDK
2+
// Copyright Bitdrift, Inc. All rights reserved.
3+
//
4+
// Use of this source code is governed by a source available license that can be found in the
5+
// LICENSE file or at:
6+
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt
7+
8+
package io.bitdrift.capture
9+
10+
import io.bitdrift.capture.common.RuntimeConfig
11+
import io.bitdrift.capture.common.RuntimeFeature
12+
13+
internal interface IRuntimeProvider {
14+
fun isRuntimeFeatureEnabled(feature: RuntimeFeature): Boolean
15+
16+
fun getRuntimeConfigValue(config: RuntimeConfig): Int
17+
}
18+
19+
internal object CaptureRuntimeProvider : IRuntimeProvider {
20+
override fun isRuntimeFeatureEnabled(feature: RuntimeFeature): Boolean =
21+
getLoggerProvider()?.isRuntimeFeatureEnabled(feature) ?: feature.defaultValue
22+
23+
override fun getRuntimeConfigValue(config: RuntimeConfig): Int =
24+
getLoggerProvider()?.getRuntimeConfigValue(config) ?: config.defaultValue
25+
26+
private fun getLoggerProvider(): IRuntimeProvider? = Capture.logger() as? IRuntimeProvider
27+
}

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/LoggerImpl.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import androidx.lifecycle.ProcessLifecycleOwner
1616
import io.bitdrift.capture.attributes.ClientAttributes
1717
import io.bitdrift.capture.attributes.NetworkAttributes
1818
import io.bitdrift.capture.common.IWindowManager
19+
import io.bitdrift.capture.common.RuntimeConfig
1920
import io.bitdrift.capture.common.RuntimeFeature
2021
import io.bitdrift.capture.common.WindowManager
2122
import io.bitdrift.capture.error.ErrorReporterService
@@ -90,7 +91,8 @@ internal class LoggerImpl(
9091
private val eventListenerDispatcher: CaptureDispatchers.CommonBackground = CaptureDispatchers.CommonBackground,
9192
windowManager: IWindowManager = WindowManager(errorHandler),
9293
) : IInternalLogger,
93-
ICompletedReportsProcessor {
94+
ICompletedReportsProcessor,
95+
IRuntimeProvider {
9496
@OptIn(ExperimentalBitdriftApi::class)
9597
internal val webViewConfiguration: WebViewConfiguration? = configuration.webViewConfiguration
9698

@@ -311,6 +313,9 @@ internal class LoggerImpl(
311313
override val deviceId: String
312314
get() = CaptureJniLibrary.getDeviceId(this.loggerId) ?: "unknown"
313315

316+
override val isTracingActive: Boolean
317+
get() = CaptureJniLibrary.isTracingActive(this.loggerId)
318+
314319
override fun startNewSession() {
315320
CaptureJniLibrary.startNewSession(this.loggerId)
316321
}
@@ -538,6 +543,10 @@ internal class LoggerImpl(
538543
)
539544
}
540545

546+
override fun isRuntimeFeatureEnabled(feature: RuntimeFeature): Boolean = runtime.isEnabled(feature)
547+
548+
override fun getRuntimeConfigValue(config: RuntimeConfig): Int = runtime.getConfigValue(config)
549+
541550
override fun logSessionReplayScreenshot(
542551
fields: Array<Field>,
543552
duration: Duration,

platform/jvm/capture/src/main/kotlin/io/bitdrift/capture/network/okhttp/CaptureOkHttpEventListener.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import io.bitdrift.capture.network.HttpRequestMetrics
1616
import io.bitdrift.capture.network.HttpResponse
1717
import io.bitdrift.capture.network.HttpResponseInfo
1818
import io.bitdrift.capture.network.HttpUrlPath
19+
import io.bitdrift.capture.network.okhttp.CaptureOkHttpTracingInterceptor.Companion.TRACE_ID_HEADER
1920
import okhttp3.Call
2021
import okhttp3.Connection
2122
import okhttp3.EventListener
@@ -380,7 +381,10 @@ internal class CaptureOkHttpEventListener internal constructor(
380381
response = httpResponse,
381382
durationMs = (clock.elapsedRealtime() - callStartTimeMs),
382383
metrics = getMetrics(),
383-
extraFields = responseExtraFieldsProvider.provideExtraFields(response),
384+
extraFields =
385+
responseExtraFieldsProvider
386+
.provideExtraFields(response)
387+
.plus(traceIdFromRequestHeaders(request)),
384388
)
385389

386390
logger?.log(httpResponseInfo)
@@ -421,10 +425,16 @@ internal class CaptureOkHttpEventListener internal constructor(
421425
response = httpResponse,
422426
durationMs = (clock.elapsedRealtime() - callStartTimeMs),
423427
metrics = getMetrics(),
428+
extraFields = traceIdFromRequestHeaders(request),
424429
)
425430
logger?.log(httpResponseInfo)
426431
}
427432

433+
private fun traceIdFromRequestHeaders(request: Request): Map<String, String> {
434+
val traceId = request.header(TRACE_ID_HEADER) ?: return emptyMap()
435+
return mapOf(CaptureTracing.TRACE_ID_FIELD_KEY to traceId)
436+
}
437+
428438
override fun canceled(call: Call) {
429439
runCatching { targetEventListener?.canceled(call) }
430440
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// capture-sdk - bitdrift's client SDK
2+
// Copyright Bitdrift, Inc. All rights reserved.
3+
//
4+
// Use of this source code is governed by a source available license that can be found in the
5+
// LICENSE file or at:
6+
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt
7+
8+
package io.bitdrift.capture.network.okhttp
9+
10+
import androidx.annotation.VisibleForTesting
11+
import io.bitdrift.capture.Capture
12+
import io.bitdrift.capture.CaptureRuntimeProvider
13+
import io.bitdrift.capture.IRuntimeProvider
14+
import io.bitdrift.capture.common.RuntimeConfig
15+
import okhttp3.Interceptor
16+
import okhttp3.Response
17+
18+
/**
19+
* Injects tracing headers into outgoing requests when Capture tracing is active for this session.
20+
*
21+
* The propagation format is resolved from a runtime config flag.
22+
*/
23+
class CaptureOkHttpTracingInterceptor
24+
@VisibleForTesting
25+
internal constructor(
26+
private val runtimeProvider: IRuntimeProvider,
27+
) : Interceptor {
28+
constructor() : this(CaptureRuntimeProvider)
29+
30+
override fun intercept(chain: Interceptor.Chain): Response {
31+
val propagationMode = getPropagationMode()
32+
33+
val request = chain.request()
34+
if (propagationMode == CaptureTracing.TracePropagationMode.OFF || Capture.logger()?.isTracingActive == false) {
35+
return chain.proceed(request)
36+
}
37+
38+
val trace = CaptureTracing.newTraceContext()
39+
val requestBuilder = request.newBuilder()
40+
val propagationValue =
41+
when (propagationMode) {
42+
CaptureTracing.TracePropagationMode.W3C -> trace.traceparent
43+
CaptureTracing.TracePropagationMode.B3 -> trace.b3
44+
CaptureTracing.TracePropagationMode.OFF -> return chain.proceed(request)
45+
}
46+
val headerName = propagationMode.headerName ?: return chain.proceed(request)
47+
requestBuilder.header(headerName, propagationValue)
48+
requestBuilder.header(TRACE_ID_HEADER, trace.traceId)
49+
return chain.proceed(requestBuilder.build())
50+
}
51+
52+
private fun getPropagationMode(): CaptureTracing.TracePropagationMode =
53+
CaptureTracing.TracePropagationMode.fromRuntimeValue(
54+
runtimeProvider.getRuntimeConfigValue(RuntimeConfig.TRACE_PROPAGATION_MODE),
55+
)
56+
57+
internal companion object {
58+
internal const val TRACE_ID_HEADER = "x-capture-span-trace-field-trace_id"
59+
}
60+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// capture-sdk - bitdrift's client SDK
2+
// Copyright Bitdrift, Inc. All rights reserved.
3+
//
4+
// Use of this source code is governed by a source available license that can be found in the
5+
// LICENSE file or at:
6+
// https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt
7+
8+
package io.bitdrift.capture.network.okhttp
9+
10+
import java.security.SecureRandom
11+
12+
internal object CaptureTracing {
13+
internal const val TRACE_ID_FIELD_KEY = "_trace_id"
14+
15+
/** Supported trace propagation modes. */
16+
internal enum class TracePropagationMode(
17+
internal val runtimeValue: Int,
18+
internal val headerName: String?,
19+
) {
20+
/** Disable trace header injection. */
21+
OFF(0, null),
22+
23+
/** W3C Trace Context format via the `traceparent` header. */
24+
W3C(1, "traceparent"),
25+
26+
/** Zipkin B3 single-header format via the `b3` header. */
27+
B3(2, "b3"),
28+
;
29+
30+
internal companion object {
31+
fun fromRuntimeValue(value: Int): TracePropagationMode = entries.firstOrNull { it.runtimeValue == value } ?: OFF
32+
}
33+
}
34+
35+
private val random = SecureRandom()
36+
37+
internal data class TraceContext(
38+
val traceId: String,
39+
val spanId: String,
40+
val traceparent: String, // This is W3C header format
41+
val b3: String, // B3 header format by Zipkin
42+
)
43+
44+
internal fun newTraceContext(): TraceContext {
45+
val traceId = randomHex(16)
46+
val spanId = randomHex(8)
47+
return TraceContext(
48+
traceId = traceId,
49+
spanId = spanId,
50+
traceparent = "00-$traceId-$spanId-01",
51+
b3 = "$traceId-$spanId-1",
52+
)
53+
}
54+
55+
private fun randomHex(byteCount: Int): String {
56+
val bytes = ByteArray(byteCount)
57+
random.nextBytes(bytes)
58+
return bytes.joinToString(separator = "") { "%02x".format(it) }
59+
}
60+
}

0 commit comments

Comments
 (0)