diff --git a/CHANGELOG.md b/CHANGELOG.md index 33e972d35ee..40cf29bba00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## Unreleased + +### Features + +- Add support for w3c traceparent header ([#4671](https://github.com/getsentry/sentry-java/pull/4671)) + - This feature is disabled by default. If enabled, outgoing requests will include the w3c `traceparent` header. + - See https://develop.sentry.dev/sdk/telemetry/traces/distributed-tracing/#w3c-trace-context-header for more details. + ```kotlin + Sentry(Android).init(context) { options -> + // ... + options.isPropagateTraceparent = true + } + ``` + ## 8.21.1 ### Fixes diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt index 9b3baca9a38..8337eeb7b15 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt @@ -132,6 +132,9 @@ constructor( .toMutableList() .apply { add(HttpHeader(baggageHeader.name, baggageHeader.value)) } } + it.w3cTraceparentHeader?.let { w3cHeader -> + cleanedHeaders.add(HttpHeader(w3cHeader.name, w3cHeader.value)) + } } } diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt index 375479f4d98..8316f6c0f33 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt @@ -23,6 +23,7 @@ import io.sentry.SpanStatus import io.sentry.TraceContext import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext +import io.sentry.W3CTraceparentHeader import io.sentry.apollo3.SentryApollo3HttpInterceptor.BeforeSpanCallback import io.sentry.mockServerRequestTimeoutMillis import io.sentry.protocol.SdkVersion @@ -351,6 +352,26 @@ class SentryApollo3InterceptorTest { verify(fixture.scopes).span } + @Test + fun `adds W3C traceparent header when propagateTraceparent is enabled`() { + fixture.options.isPropagateTraceparent = true + executeQuery() + val recorderRequest = + fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recorderRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } + + @Test + fun `does not add W3C traceparent header when propagateTraceparent is disabled`() { + fixture.options.isPropagateTraceparent = false + executeQuery() + val recorderRequest = + fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recorderRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } + private fun assertTransactionDetails( it: SentryTransaction, httpStatusCode: Int? = 200, diff --git a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt index 0b9a29f1e33..fcf50564e5a 100644 --- a/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt +++ b/sentry-apollo-4/src/main/java/io/sentry/apollo4/SentryApollo4HttpInterceptor.kt @@ -132,6 +132,9 @@ constructor( .toMutableList() .apply { add(HttpHeader(baggageHeader.name, baggageHeader.value)) } } + it.w3cTraceparentHeader?.let { w3cHeader -> + cleanedHeaders.add(HttpHeader(w3cHeader.name, w3cHeader.value)) + } } } diff --git a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt index a0bb7a2e154..d92cefe9772 100644 --- a/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt +++ b/sentry-apollo-4/src/test/java/io/sentry/apollo4/SentryApollo4HttpInterceptorTest.kt @@ -26,6 +26,7 @@ import io.sentry.SpanStatus import io.sentry.TraceContext import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext +import io.sentry.W3CTraceparentHeader import io.sentry.apollo4.SentryApollo4HttpInterceptor.BeforeSpanCallback import io.sentry.apollo4.generated.LaunchDetailsQuery import io.sentry.mockServerRequestTimeoutMillis @@ -363,6 +364,26 @@ abstract class SentryApollo4HttpInterceptorTest( verify(fixture.scopes).span } + @Test + fun `adds W3C traceparent header when propagateTraceparent is enabled`() { + fixture.options.isPropagateTraceparent = true + executeQuery() + val recorderRequest = + fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recorderRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } + + @Test + fun `does not add W3C traceparent header when propagateTraceparent is disabled`() { + fixture.options.isPropagateTraceparent = false + executeQuery() + val recorderRequest = + fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recorderRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } + private fun assertTransactionDetails( it: SentryTransaction, httpStatusCode: Int? = 200, diff --git a/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientPlugin.kt b/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientPlugin.kt index a653304e992..2f559f804fc 100644 --- a/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientPlugin.kt +++ b/sentry-ktor-client/src/main/java/io/sentry/ktorClient/SentryKtorClientPlugin.kt @@ -137,6 +137,7 @@ public val SentryKtorClientPlugin: ClientPlugin = request.headers.remove(BaggageHeader.BAGGAGE_HEADER) request.headers[it.name] = it.value } + tracingHeaders.w3cTraceparentHeader?.let { request.headers[it.name] = it.value } } } } diff --git a/sentry-ktor-client/src/test/java/io/sentry/ktorClient/SentryKtorClientPluginTest.kt b/sentry-ktor-client/src/test/java/io/sentry/ktorClient/SentryKtorClientPluginTest.kt index b15ae23d03d..976d3200e11 100644 --- a/sentry-ktor-client/src/test/java/io/sentry/ktorClient/SentryKtorClientPluginTest.kt +++ b/sentry-ktor-client/src/test/java/io/sentry/ktorClient/SentryKtorClientPluginTest.kt @@ -24,6 +24,7 @@ import io.sentry.SentryTracer import io.sentry.SpanDataConvention import io.sentry.SpanStatus import io.sentry.TransactionContext +import io.sentry.W3CTraceparentHeader import io.sentry.exception.SentryHttpClientException import io.sentry.mockServerRequestTimeoutMillis import java.util.concurrent.TimeUnit @@ -426,4 +427,29 @@ class SentryKtorClientPluginTest { assertTrue(baggageHeaderValues[0].contains("sentry-transaction=name")) assertTrue(baggageHeaderValues[0].contains("sentry-trace_id")) } + + @Test + fun `adds W3C traceparent header when propagateTraceparent is enabled`(): Unit = runBlocking { + val sut = + fixture.getSut(optionsConfiguration = { options -> options.isPropagateTraceparent = true }) + sut.get(fixture.server.url("/hello").toString()) + + val recordedRequest = + fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recordedRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } + + @Test + fun `does not add W3C traceparent header when propagateTraceparent is disabled`(): Unit = + runBlocking { + val sut = + fixture.getSut(optionsConfiguration = { options -> options.isPropagateTraceparent = false }) + sut.get(fixture.server.url("/hello").toString()) + + val recordedRequest = + fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recordedRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } } diff --git a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt index e36d3772bf3..be1ee1caf37 100644 --- a/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt +++ b/sentry-okhttp/src/main/java/io/sentry/okhttp/SentryOkHttpInterceptor.kt @@ -116,6 +116,7 @@ public open class SentryOkHttpInterceptor( requestBuilder.removeHeader(BaggageHeader.BAGGAGE_HEADER) requestBuilder.addHeader(it.name, it.value) } + tracingHeaders.w3cTraceparentHeader?.let { requestBuilder.addHeader(it.name, it.value) } } } diff --git a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt index 5f3198c097c..bbdb3a86516 100644 --- a/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt +++ b/sentry-okhttp/src/test/java/io/sentry/okhttp/SentryOkHttpInterceptorTest.kt @@ -19,6 +19,7 @@ import io.sentry.SpanDataConvention import io.sentry.SpanStatus import io.sentry.TransactionContext import io.sentry.TypeCheckHint +import io.sentry.W3CTraceparentHeader import io.sentry.exception.SentryHttpClientException import io.sentry.mockServerRequestTimeoutMillis import java.io.IOException @@ -645,4 +646,38 @@ class SentryOkHttpInterceptorTest { val okHttpEvent = SentryOkHttpEventListener.eventMap[call]!! assertTrue(okHttpEvent.isEventFinished.get()) } + + @Test + fun `adds W3C traceparent header when propagateTraceparent is enabled`() { + val client = + fixture.getSut( + optionsConfiguration = Sentry.OptionsConfiguration { it.isPropagateTraceparent = true } + ) + + fixture.server.enqueue(MockResponse().setResponseCode(200)) + + val request = getRequest("/test") + client.newCall(request).execute() + + val recordedRequest = fixture.server.takeRequest() + assertNotNull(recordedRequest.getHeader(SentryTraceHeader.SENTRY_TRACE_HEADER)) + assertNotNull(recordedRequest.getHeader(W3CTraceparentHeader.TRACEPARENT_HEADER)) + } + + @Test + fun `does not add W3C traceparent header when propagateTraceparent is disabled`() { + val client = + fixture.getSut( + optionsConfiguration = Sentry.OptionsConfiguration { it.isPropagateTraceparent = false } + ) + + fixture.server.enqueue(MockResponse().setResponseCode(200)) + + val request = getRequest("/test") + client.newCall(request).execute() + + val recordedRequest = fixture.server.takeRequest() + assertNotNull(recordedRequest.getHeader(SentryTraceHeader.SENTRY_TRACE_HEADER)) + assertNull(recordedRequest.getHeader(W3CTraceparentHeader.TRACEPARENT_HEADER)) + } } diff --git a/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java b/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java index 35b2b5c8fdb..acd73bbec7c 100644 --- a/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java +++ b/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java @@ -17,6 +17,7 @@ import io.sentry.SpanDataConvention; import io.sentry.SpanOptions; import io.sentry.SpanStatus; +import io.sentry.W3CTraceparentHeader; import io.sentry.util.Objects; import io.sentry.util.SpanUtils; import io.sentry.util.TracingUtils; @@ -137,6 +138,12 @@ public Response execute(final @NotNull Request request, final @NotNull Request.O requestWrapper.removeHeader(BaggageHeader.BAGGAGE_HEADER); requestWrapper.header(baggageHeader.getName(), baggageHeader.getValue()); } + + final @Nullable W3CTraceparentHeader w3cTraceparentHeader = + tracingHeaders.getW3cTraceparentHeader(); + if (w3cTraceparentHeader != null) { + requestWrapper.header(w3cTraceparentHeader.getName(), w3cTraceparentHeader.getValue()); + } } return requestWrapper.build(); diff --git a/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt b/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt index 66fbd86a91a..571a2339326 100644 --- a/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt +++ b/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt @@ -16,6 +16,7 @@ import io.sentry.SentryTracer import io.sentry.SpanDataConvention import io.sentry.SpanStatus import io.sentry.TransactionContext +import io.sentry.W3CTraceparentHeader import io.sentry.mockServerRequestTimeoutMillis import java.util.concurrent.TimeUnit import kotlin.test.BeforeTest @@ -316,6 +317,32 @@ class SentryFeignClientTest { assertNotNull(httpClientSpan.spanContext.sampled) { assertFalse(it) } } + @Test + fun `adds W3C traceparent header when propagateTraceparent is enabled`() { + fixture.sentryOptions.isTraceSampling = true + fixture.sentryOptions.isPropagateTraceparent = true + fixture.sentryOptions.dsn = "https://key@sentry.io/proj" + val sut = fixture.getSut() + sut.getOk() + val recorderRequest = + fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recorderRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } + + @Test + fun `does not add W3C traceparent header when propagateTraceparent is disabled`() { + fixture.sentryOptions.isTraceSampling = true + fixture.sentryOptions.isPropagateTraceparent = false + fixture.sentryOptions.dsn = "https://key@sentry.io/proj" + val sut = fixture.getSut() + sut.getOk() + val recorderRequest = + fixture.server.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recorderRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } + interface MockApi { @RequestLine("GET /status/200") fun getOk(): String diff --git a/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptor.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptor.java index 901321f0213..50a8d0539b0 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptor.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptor.java @@ -13,6 +13,7 @@ import io.sentry.SpanDataConvention; import io.sentry.SpanOptions; import io.sentry.SpanStatus; +import io.sentry.W3CTraceparentHeader; import io.sentry.util.Objects; import io.sentry.util.SpanUtils; import io.sentry.util.TracingUtils; @@ -113,6 +114,12 @@ private void maybeAddTracingHeaders( if (baggageHeader != null) { request.getHeaders().set(baggageHeader.getName(), baggageHeader.getValue()); } + + final @Nullable W3CTraceparentHeader w3cTraceparentHeader = + tracingHeaders.getW3cTraceparentHeader(); + if (w3cTraceparentHeader != null) { + request.getHeaders().add(w3cTraceparentHeader.getName(), w3cTraceparentHeader.getValue()); + } } } diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptorTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptorTest.kt new file mode 100644 index 00000000000..85d138604dd --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptorTest.kt @@ -0,0 +1,78 @@ +package io.sentry.spring7.tracing + +import io.sentry.IScopes +import io.sentry.Scope +import io.sentry.ScopeCallback +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.SentryTraceHeader +import io.sentry.W3CTraceparentHeader +import java.net.URI +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.springframework.http.HttpMethod +import org.springframework.http.client.ClientHttpRequestExecution +import org.springframework.http.client.ClientHttpResponse +import org.springframework.mock.http.client.MockClientHttpRequest +import org.springframework.test.context.junit4.SpringRunner + +@RunWith(SpringRunner::class) +class SentrySpanClientHttpRequestInterceptorTest { + + class Fixture { + val request = MockClientHttpRequest(HttpMethod.GET, URI.create("https://example.com/users/123")) + + val options = + SentryOptions().apply { + dsn = "https://key@sentry.io/proj" + tracesSampleRate = 1.0 + } + val scope = Scope(options) + + val scopes = mock() + val requestExecution = mock() + val body = "data".toByteArray() + + init { + whenever(scopes.options).thenReturn(options) + doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(any()) + + whenever(requestExecution.execute(any(), any())).thenReturn(mock()) + } + + fun create( + config: Sentry.OptionsConfiguration + ): SentrySpanClientHttpRequestInterceptor { + config.configure(options) + return SentrySpanClientHttpRequestInterceptor(scopes) + } + } + + val fixture = Fixture() + + @Test + fun `attaches w3c trace parent header when enabled`() { + val sut = fixture.create { options -> options.isPropagateTraceparent = true } + sut.intercept(fixture.request, fixture.body, fixture.requestExecution) + + assertNotNull(fixture.request.headers.get(SentryTraceHeader.SENTRY_TRACE_HEADER)) + assertNotNull(fixture.request.headers.get(W3CTraceparentHeader.TRACEPARENT_HEADER)) + } + + @Test + fun `does not attach w3c trace parent header when disabled`() { + val sut = fixture.create { options -> options.isPropagateTraceparent = false } + sut.intercept(fixture.request, fixture.body, fixture.requestExecution) + + assertNotNull(fixture.request.headers.get(SentryTraceHeader.SENTRY_TRACE_HEADER)) + assertNull(fixture.request.headers.get(W3CTraceparentHeader.TRACEPARENT_HEADER)) + } +} diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestClientCustomizerTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestClientCustomizerTest.kt index fb803a3100f..8c19c7c7066 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestClientCustomizerTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestClientCustomizerTest.kt @@ -11,6 +11,7 @@ import io.sentry.SentryTracer import io.sentry.SpanStatus import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext +import io.sentry.W3CTraceparentHeader import io.sentry.mockServerRequestTimeoutMillis import java.time.Duration import java.util.concurrent.TimeUnit @@ -383,4 +384,36 @@ class SentrySpanRestClientCustomizerTest { anyOrNull(), ) } + + @Test + fun `adds W3C traceparent header when propagateTraceparent is enabled`() { + fixture.sentryOptions.isPropagateTraceparent = true + fixture + .getSut(isTransactionActive = true, includeMockServerInTracingOrigins = true) + .build() + .get() + .uri(fixture.url) + .retrieve() + .toEntity(String::class.java) + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recordedRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } + + @Test + fun `does not add W3C traceparent header when propagateTraceparent is disabled`() { + fixture.sentryOptions.isPropagateTraceparent = false + fixture + .getSut(isTransactionActive = true, includeMockServerInTracingOrigins = true) + .build() + .get() + .uri(fixture.url) + .retrieve() + .toEntity(String::class.java) + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recordedRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } } diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestTemplateCustomizerTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestTemplateCustomizerTest.kt index c6bd6d2ccaf..988824465bb 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestTemplateCustomizerTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestTemplateCustomizerTest.kt @@ -11,6 +11,7 @@ import io.sentry.SentryTracer import io.sentry.SpanStatus import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext +import io.sentry.W3CTraceparentHeader import io.sentry.mockServerRequestTimeoutMillis import java.time.Duration import java.util.concurrent.TimeUnit @@ -328,4 +329,28 @@ class SentrySpanRestTemplateCustomizerTest { anyOrNull(), ) } + + @Test + fun `adds W3C traceparent header when propagateTraceparent is enabled`() { + fixture.sentryOptions.isPropagateTraceparent = true + fixture + .getSut(isTransactionActive = true, includeMockServerInTracingOrigins = true) + .getForObject(fixture.url, String::class.java) + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recordedRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } + + @Test + fun `does not add W3C traceparent header when propagateTraceparent is disabled`() { + fixture.sentryOptions.isPropagateTraceparent = false + fixture + .getSut(isTransactionActive = true, includeMockServerInTracingOrigins = true) + .getForObject(fixture.url, String::class.java) + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recordedRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } } diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizerTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizerTest.kt index a0ce38e7c3d..7a2e6a0ff4f 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizerTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizerTest.kt @@ -11,6 +11,7 @@ import io.sentry.SentryTracer import io.sentry.SpanStatus import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext +import io.sentry.W3CTraceparentHeader import io.sentry.mockServerRequestTimeoutMillis import java.time.Duration import java.util.concurrent.TimeUnit @@ -383,4 +384,36 @@ class SentrySpanRestClientCustomizerTest { anyOrNull(), ) } + + @Test + fun `adds W3C traceparent header when propagateTraceparent is enabled`() { + fixture.sentryOptions.isPropagateTraceparent = true + fixture + .getSut(isTransactionActive = true, includeMockServerInTracingOrigins = true) + .build() + .get() + .uri(fixture.url) + .retrieve() + .toEntity(String::class.java) + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recordedRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } + + @Test + fun `does not add W3C traceparent header when propagateTraceparent is disabled`() { + fixture.sentryOptions.isPropagateTraceparent = false + fixture + .getSut(isTransactionActive = true, includeMockServerInTracingOrigins = true) + .build() + .get() + .uri(fixture.url) + .retrieve() + .toEntity(String::class.java) + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recordedRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } } diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizerTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizerTest.kt index f7a7f55c5db..c6c7707aab9 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizerTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizerTest.kt @@ -11,6 +11,7 @@ import io.sentry.SentryTracer import io.sentry.SpanStatus import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext +import io.sentry.W3CTraceparentHeader import io.sentry.mockServerRequestTimeoutMillis import java.time.Duration import java.util.concurrent.TimeUnit @@ -328,4 +329,28 @@ class SentrySpanRestTemplateCustomizerTest { anyOrNull(), ) } + + @Test + fun `adds W3C traceparent header when propagateTraceparent is enabled`() { + fixture.sentryOptions.isPropagateTraceparent = true + fixture + .getSut(isTransactionActive = true, includeMockServerInTracingOrigins = true) + .getForObject(fixture.url, String::class.java) + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recordedRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } + + @Test + fun `does not add W3C traceparent header when propagateTraceparent is disabled`() { + fixture.sentryOptions.isPropagateTraceparent = false + fixture + .getSut(isTransactionActive = true, includeMockServerInTracingOrigins = true) + .getForObject(fixture.url, String::class.java) + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recordedRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } } diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentrySpanRestTemplateCustomizerTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentrySpanRestTemplateCustomizerTest.kt index 6bc1aade4e9..29699f61022 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentrySpanRestTemplateCustomizerTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentrySpanRestTemplateCustomizerTest.kt @@ -11,6 +11,7 @@ import io.sentry.SentryTracer import io.sentry.SpanStatus import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext +import io.sentry.W3CTraceparentHeader import io.sentry.mockServerRequestTimeoutMillis import java.time.Duration import java.util.concurrent.TimeUnit @@ -328,4 +329,28 @@ class SentrySpanRestTemplateCustomizerTest { anyOrNull(), ) } + + @Test + fun `adds W3C traceparent header when propagateTraceparent is enabled`() { + fixture.sentryOptions.isPropagateTraceparent = true + fixture + .getSut(isTransactionActive = true, includeMockServerInTracingOrigins = true) + .getForObject(fixture.url, String::class.java) + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recordedRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } + + @Test + fun `does not add W3C traceparent header when propagateTraceparent is disabled`() { + fixture.sentryOptions.isPropagateTraceparent = false + fixture + .getSut(isTransactionActive = true, includeMockServerInTracingOrigins = true) + .getForObject(fixture.url, String::class.java) + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recordedRequest.headers[W3CTraceparentHeader.TRACEPARENT_HEADER]) + } } diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java index f50c93976e5..e305816bb05 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java @@ -13,6 +13,7 @@ import io.sentry.SpanDataConvention; import io.sentry.SpanOptions; import io.sentry.SpanStatus; +import io.sentry.W3CTraceparentHeader; import io.sentry.util.Objects; import io.sentry.util.SpanUtils; import io.sentry.util.TracingUtils; @@ -113,6 +114,12 @@ private void maybeAddTracingHeaders( if (baggageHeader != null) { request.getHeaders().set(baggageHeader.getName(), baggageHeader.getValue()); } + + final @Nullable W3CTraceparentHeader w3cTraceparentHeader = + tracingHeaders.getW3cTraceparentHeader(); + if (w3cTraceparentHeader != null) { + request.getHeaders().add(w3cTraceparentHeader.getName(), w3cTraceparentHeader.getValue()); + } } } diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptorTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptorTest.kt new file mode 100644 index 00000000000..e3f9ad2dfa9 --- /dev/null +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptorTest.kt @@ -0,0 +1,78 @@ +package io.sentry.spring.jakarta.tracing + +import io.sentry.IScopes +import io.sentry.Scope +import io.sentry.ScopeCallback +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.SentryTraceHeader +import io.sentry.W3CTraceparentHeader +import java.net.URI +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.springframework.http.HttpMethod +import org.springframework.http.client.ClientHttpRequestExecution +import org.springframework.http.client.ClientHttpResponse +import org.springframework.mock.http.client.MockClientHttpRequest +import org.springframework.test.context.junit4.SpringRunner + +@RunWith(SpringRunner::class) +class SentrySpanClientHttpRequestInterceptorTest { + + class Fixture { + val request = MockClientHttpRequest(HttpMethod.GET, URI.create("https://example.com/users/123")) + + val options = + SentryOptions().apply { + dsn = "https://key@sentry.io/proj" + tracesSampleRate = 1.0 + } + val scope = Scope(options) + + val scopes = mock() + val requestExecution = mock() + val body = "data".toByteArray() + + init { + whenever(scopes.options).thenReturn(options) + doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(any()) + + whenever(requestExecution.execute(any(), any())).thenReturn(mock()) + } + + fun create( + config: Sentry.OptionsConfiguration + ): SentrySpanClientHttpRequestInterceptor { + config.configure(options) + return SentrySpanClientHttpRequestInterceptor(scopes) + } + } + + val fixture = Fixture() + + @Test + fun `attaches w3c trace parent header when enabled`() { + val sut = fixture.create { options -> options.isPropagateTraceparent = true } + sut.intercept(fixture.request, fixture.body, fixture.requestExecution) + + assertNotNull(fixture.request.headers.get(SentryTraceHeader.SENTRY_TRACE_HEADER)) + assertNotNull(fixture.request.headers.get(W3CTraceparentHeader.TRACEPARENT_HEADER)) + } + + @Test + fun `does not attach w3c trace parent header when disabled`() { + val sut = fixture.create { options -> options.isPropagateTraceparent = false } + sut.intercept(fixture.request, fixture.body, fixture.requestExecution) + + assertNotNull(fixture.request.headers.get(SentryTraceHeader.SENTRY_TRACE_HEADER)) + assertNull(fixture.request.headers.get(W3CTraceparentHeader.TRACEPARENT_HEADER)) + } +} diff --git a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptor.java b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptor.java index 17be912a2c6..ed63c5ea080 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptor.java +++ b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptor.java @@ -13,6 +13,7 @@ import io.sentry.SpanDataConvention; import io.sentry.SpanOptions; import io.sentry.SpanStatus; +import io.sentry.W3CTraceparentHeader; import io.sentry.util.Objects; import io.sentry.util.SpanUtils; import io.sentry.util.TracingUtils; @@ -105,6 +106,12 @@ private void maybeAddTracingHeaders( if (baggageHeader != null) { request.getHeaders().set(baggageHeader.getName(), baggageHeader.getValue()); } + + final @Nullable W3CTraceparentHeader w3cTraceparentHeader = + tracingHeaders.getW3cTraceparentHeader(); + if (w3cTraceparentHeader != null) { + request.getHeaders().add(w3cTraceparentHeader.getName(), w3cTraceparentHeader.getValue()); + } } } diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptorTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptorTest.kt new file mode 100644 index 00000000000..a850480099a --- /dev/null +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptorTest.kt @@ -0,0 +1,78 @@ +package io.sentry.spring.tracing + +import io.sentry.IScopes +import io.sentry.Scope +import io.sentry.ScopeCallback +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.SentryTraceHeader +import io.sentry.W3CTraceparentHeader +import java.net.URI +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.springframework.http.HttpMethod +import org.springframework.http.client.ClientHttpRequestExecution +import org.springframework.http.client.ClientHttpResponse +import org.springframework.mock.http.client.MockClientHttpRequest +import org.springframework.test.context.junit4.SpringRunner + +@RunWith(SpringRunner::class) +class SentrySpanClientHttpRequestInterceptorTest { + + class Fixture { + val request = MockClientHttpRequest(HttpMethod.GET, URI.create("https://example.com/users/123")) + + val options = + SentryOptions().apply { + dsn = "https://key@sentry.io/proj" + tracesSampleRate = 1.0 + } + val scope = Scope(options) + + val scopes = mock() + val requestExecution = mock() + val body = "data".toByteArray() + + init { + whenever(scopes.options).thenReturn(options) + doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(any()) + + whenever(requestExecution.execute(any(), any())).thenReturn(mock()) + } + + fun create( + config: Sentry.OptionsConfiguration + ): SentrySpanClientHttpRequestInterceptor { + config.configure(options) + return SentrySpanClientHttpRequestInterceptor(scopes) + } + } + + val fixture = Fixture() + + @Test + fun `attaches w3c trace parent header when enabled`() { + val sut = fixture.create { options -> options.isPropagateTraceparent = true } + sut.intercept(fixture.request, fixture.body, fixture.requestExecution) + + assertNotNull(fixture.request.headers.get(SentryTraceHeader.SENTRY_TRACE_HEADER)) + assertNotNull(fixture.request.headers.get(W3CTraceparentHeader.TRACEPARENT_HEADER)) + } + + @Test + fun `does not attach w3c trace parent header when disabled`() { + val sut = fixture.create { options -> options.isPropagateTraceparent = false } + sut.intercept(fixture.request, fixture.body, fixture.requestExecution) + + assertNotNull(fixture.request.headers.get(SentryTraceHeader.SENTRY_TRACE_HEADER)) + assertNull(fixture.request.headers.get(W3CTraceparentHeader.TRACEPARENT_HEADER)) + } +} diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 3b85705208e..17ccb16829c 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3405,6 +3405,7 @@ public class io/sentry/SentryOptions { public fun isGlobalHubMode ()Ljava/lang/Boolean; public fun isPrintUncaughtStackTrace ()Z public fun isProfilingEnabled ()Z + public fun isPropagateTraceparent ()Z public fun isSendClientReports ()Z public fun isSendDefaultPii ()Z public fun isSendModules ()Z @@ -3492,6 +3493,7 @@ public class io/sentry/SentryOptions { public fun setProfilesSampler (Lio/sentry/SentryOptions$ProfilesSamplerCallback;)V public fun setProfilingTracesHz (I)V public fun setProguardUuid (Ljava/lang/String;)V + public fun setPropagateTraceparent (Z)V public fun setProxy (Lio/sentry/SentryOptions$Proxy;)V public fun setReadTimeoutMillis (I)V public fun setRelease (Ljava/lang/String;)V @@ -4322,6 +4324,13 @@ public final class io/sentry/UserFeedback$JsonKeys { public fun ()V } +public final class io/sentry/W3CTraceparentHeader { + public static final field TRACEPARENT_HEADER Ljava/lang/String; + public fun (Lio/sentry/protocol/SentryId;Lio/sentry/SpanId;Ljava/lang/Boolean;)V + public fun getName ()Ljava/lang/String; + public fun getValue ()Ljava/lang/String; +} + public final class io/sentry/backpressure/BackpressureMonitor : io/sentry/backpressure/IBackpressureMonitor, java/lang/Runnable { public fun (Lio/sentry/SentryOptions;Lio/sentry/IScopes;)V public fun close ()V @@ -7062,8 +7071,10 @@ public final class io/sentry/util/TracingUtils { public final class io/sentry/util/TracingUtils$TracingHeaders { public fun (Lio/sentry/SentryTraceHeader;Lio/sentry/BaggageHeader;)V + public fun (Lio/sentry/SentryTraceHeader;Lio/sentry/BaggageHeader;Lio/sentry/W3CTraceparentHeader;)V public fun getBaggageHeader ()Lio/sentry/BaggageHeader; public fun getSentryTraceHeader ()Lio/sentry/SentryTraceHeader; + public fun getW3cTraceparentHeader ()Lio/sentry/W3CTraceparentHeader; } public final class io/sentry/util/UUIDGenerator { diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index b51691f4abd..73e4266c141 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -393,6 +393,9 @@ public class SentryOptions { private final @NotNull List defaultTracePropagationTargets = Collections.singletonList(DEFAULT_PROPAGATION_TARGETS); + /** Whether to propagate W3C traceparent HTTP header. */ + private boolean propagateTraceparent = false; + /** Proguard UUID. */ private @Nullable String proguardUuid; @@ -2110,6 +2113,24 @@ public void setTracePropagationTargets(final @Nullable List tracePropaga } } + /** + * Returns whether W3C traceparent HTTP header propagation is enabled. + * + * @return true if enabled false otherwise + */ + public boolean isPropagateTraceparent() { + return propagateTraceparent; + } + + /** + * Enables or disables W3C traceparent HTTP header propagation. + * + * @param propagateTraceparent true if enabled false otherwise + */ + public void setPropagateTraceparent(final boolean propagateTraceparent) { + this.propagateTraceparent = propagateTraceparent; + } + /** * Returns a Proguard UUID. * diff --git a/sentry/src/main/java/io/sentry/W3CTraceparentHeader.java b/sentry/src/main/java/io/sentry/W3CTraceparentHeader.java new file mode 100644 index 00000000000..1eadc9881b6 --- /dev/null +++ b/sentry/src/main/java/io/sentry/W3CTraceparentHeader.java @@ -0,0 +1,32 @@ +package io.sentry; + +import io.sentry.protocol.SentryId; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Represents W3C traceparent HTTP header. */ +public final class W3CTraceparentHeader { + public static final String TRACEPARENT_HEADER = "traceparent"; + + private final @NotNull SentryId traceId; + private final @NotNull SpanId spanId; + private final @Nullable Boolean sampled; + + public W3CTraceparentHeader( + final @NotNull SentryId traceId, + final @NotNull SpanId spanId, + final @Nullable Boolean sampled) { + this.traceId = traceId; + this.spanId = spanId; + this.sampled = sampled; + } + + public @NotNull String getName() { + return TRACEPARENT_HEADER; + } + + public @NotNull String getValue() { + final String sampledFlag = sampled != null && sampled ? "01" : "00"; + return String.format("00-%s-%s-%s", traceId, spanId, sampledFlag); + } +} diff --git a/sentry/src/main/java/io/sentry/util/TracingUtils.java b/sentry/src/main/java/io/sentry/util/TracingUtils.java index 4af89a79035..673980e7359 100644 --- a/sentry/src/main/java/io/sentry/util/TracingUtils.java +++ b/sentry/src/main/java/io/sentry/util/TracingUtils.java @@ -10,7 +10,9 @@ import io.sentry.PropagationContext; import io.sentry.SentryOptions; import io.sentry.SentryTraceHeader; +import io.sentry.SpanContext; import io.sentry.TracesSamplingDecision; +import io.sentry.W3CTraceparentHeader; import java.util.List; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -59,8 +61,18 @@ public static void setTrace( final @NotNull SentryOptions sentryOptions = scopes.getOptions(); if (span != null && !span.isNoOp()) { - return new TracingHeaders( - span.toSentryTrace(), span.toBaggageHeader(thirdPartyBaggageHeaders)); + final @NotNull SentryTraceHeader sentryTraceHeader = span.toSentryTrace(); + final @Nullable BaggageHeader baggageHeader = span.toBaggageHeader(thirdPartyBaggageHeaders); + @Nullable W3CTraceparentHeader w3cTraceparentHeader = null; + + if (sentryOptions.isPropagateTraceparent()) { + final @NotNull SpanContext spanContext = span.getSpanContext(); + w3cTraceparentHeader = + new W3CTraceparentHeader( + spanContext.getTraceId(), spanContext.getSpanId(), sentryTraceHeader.isSampled()); + } + + return new TracingHeaders(sentryTraceHeader, baggageHeader, w3cTraceparentHeader); } else { final @NotNull PropagationContextHolder returnValue = new PropagationContextHolder(); scopes.configureScope( @@ -74,12 +86,22 @@ public static void setTrace( final @NotNull BaggageHeader baggageHeader = BaggageHeader.fromBaggageAndOutgoingHeader(baggage, thirdPartyBaggageHeaders); - return new TracingHeaders( + final @NotNull SentryTraceHeader sentryTraceHeader = new SentryTraceHeader( propagationContext.getTraceId(), propagationContext.getSpanId(), - propagationContext.isSampled()), - baggageHeader); + propagationContext.isSampled()); + + @Nullable W3CTraceparentHeader w3cTraceparentHeader = null; + if (sentryOptions.isPropagateTraceparent()) { + w3cTraceparentHeader = + new W3CTraceparentHeader( + propagationContext.getTraceId(), + propagationContext.getSpanId(), + propagationContext.isSampled()); + } + + return new TracingHeaders(sentryTraceHeader, baggageHeader, w3cTraceparentHeader); } return null; @@ -110,12 +132,23 @@ private static final class PropagationContextHolder { public static final class TracingHeaders { private final @NotNull SentryTraceHeader sentryTraceHeader; private final @Nullable BaggageHeader baggageHeader; + private final @Nullable W3CTraceparentHeader w3cTraceparentHeader; public TracingHeaders( final @NotNull SentryTraceHeader sentryTraceHeader, final @Nullable BaggageHeader baggageHeader) { this.sentryTraceHeader = sentryTraceHeader; this.baggageHeader = baggageHeader; + this.w3cTraceparentHeader = null; + } + + public TracingHeaders( + final @NotNull SentryTraceHeader sentryTraceHeader, + final @Nullable BaggageHeader baggageHeader, + final @Nullable W3CTraceparentHeader w3cTraceparentHeader) { + this.sentryTraceHeader = sentryTraceHeader; + this.baggageHeader = baggageHeader; + this.w3cTraceparentHeader = w3cTraceparentHeader; } public @NotNull SentryTraceHeader getSentryTraceHeader() { @@ -125,6 +158,10 @@ public TracingHeaders( public @Nullable BaggageHeader getBaggageHeader() { return baggageHeader; } + + public @Nullable W3CTraceparentHeader getW3cTraceparentHeader() { + return w3cTraceparentHeader; + } } /** Checks if a transaction is to be ignored. */ diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 469b2f4b160..d7fef7a5798 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -858,4 +858,17 @@ class SentryOptionsTest { options.deadlineTimeout = -1L assertEquals(-1L, options.deadlineTimeout) } + + @Test + fun `propagateTraceparent option defaults to false`() { + val options = SentryOptions() + assertFalse(options.isPropagateTraceparent) + } + + @Test + fun `propagateTraceparent option can be changed`() { + val options = SentryOptions() + options.isPropagateTraceparent = true + assertTrue(options.isPropagateTraceparent) + } } diff --git a/sentry/src/test/java/io/sentry/W3CTraceparentHeaderTest.kt b/sentry/src/test/java/io/sentry/W3CTraceparentHeaderTest.kt new file mode 100644 index 00000000000..f9052779029 --- /dev/null +++ b/sentry/src/test/java/io/sentry/W3CTraceparentHeaderTest.kt @@ -0,0 +1,54 @@ +package io.sentry + +import io.sentry.protocol.SentryId +import kotlin.test.Test +import kotlin.test.assertEquals + +class W3CTraceparentHeaderTest { + + @Test + fun `creates header with sampled true`() { + val traceId = SentryId("12345678123456781234567812345678") + val spanId = SpanId("1234567812345678") + val header = W3CTraceparentHeader(traceId, spanId, true) + + assertEquals("traceparent", header.name) + assertEquals("00-12345678123456781234567812345678-1234567812345678-01", header.value) + } + + @Test + fun `creates header with sampled false`() { + val traceId = SentryId("12345678123456781234567812345678") + val spanId = SpanId("1234567812345678") + val header = W3CTraceparentHeader(traceId, spanId, false) + + assertEquals("traceparent", header.name) + assertEquals("00-12345678123456781234567812345678-1234567812345678-00", header.value) + } + + @Test + fun `creates header with sampled null`() { + val traceId = SentryId("12345678123456781234567812345678") + val spanId = SpanId("1234567812345678") + val header = W3CTraceparentHeader(traceId, spanId, null) + + assertEquals("traceparent", header.name) + assertEquals("00-12345678123456781234567812345678-1234567812345678-00", header.value) + } + + @Test + fun `value follows W3C format`() { + val traceId = SentryId("abcdefabcdefabcdabcdefabcdefabcd") + val spanId = SpanId("abcdefabcdefabcd") + val header = W3CTraceparentHeader(traceId, spanId, true) + + val value = header.value + val parts = value.split("-") + + assertEquals(4, parts.size) + assertEquals("00", parts[0]) // Version + assertEquals("abcdefabcdefabcdabcdefabcdefabcd", parts[1]) // Trace ID (32 hex chars) + assertEquals("abcdefabcdefabcd", parts[2]) // Span ID (16 hex chars) + assertEquals("01", parts[3]) // Sampled flag + } +} diff --git a/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt b/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt index e97540ce142..9c906712261 100644 --- a/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt @@ -14,6 +14,7 @@ import io.sentry.SpanId import io.sentry.SpanOptions import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext +import io.sentry.W3CTraceparentHeader import io.sentry.protocol.SentryId import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -443,4 +444,70 @@ class TracingUtilsTest { assertTrue(sampleRand < 1.0) assertTrue(sampleRand >= 0.9999) } + + @Test + fun `trace does not return w3c traceparent header when propagateTraceparent is disabled`() { + val fixture = Fixture() + fixture.setup() + fixture.options.isPropagateTraceparent = false + + val tracingHeaders = TracingUtils.trace(fixture.scopes, null, fixture.span) + + assertNotNull(tracingHeaders) + assertNotNull(tracingHeaders.sentryTraceHeader) + assertNull(tracingHeaders.w3cTraceparentHeader) + } + + @Test + fun `trace returns w3c traceparent header when propagateTraceparent is enabled`() { + val fixture = Fixture() + fixture.setup() + fixture.options.isPropagateTraceparent = true + + val tracingHeaders = TracingUtils.trace(fixture.scopes, null, fixture.span) + + assertNotNull(tracingHeaders) + assertNotNull(tracingHeaders.sentryTraceHeader) + assertNotNull(tracingHeaders.w3cTraceparentHeader) + assertEquals( + W3CTraceparentHeader.TRACEPARENT_HEADER, + tracingHeaders.w3cTraceparentHeader!!.name, + ) + + val headerValue = tracingHeaders.w3cTraceparentHeader!!.value + assertTrue(headerValue.startsWith("00-")) + + val parts = headerValue.split("-") + assertEquals(4, parts.size) + assertEquals("00", parts[0]) + assertEquals(fixture.span.spanContext.traceId.toString(), parts[1]) + assertEquals(fixture.span.spanContext.spanId.toString(), parts[2]) + assertEquals("01", parts[3]) + } + + @Test + fun `trace returns w3c traceparent header when no span provided and propagateTraceparent is enabled`() { + val fixture = Fixture() + fixture.options.isPropagateTraceparent = true + fixture.setup() + + val tracingHeaders = TracingUtils.trace(fixture.scopes, null, null) + + assertNotNull(tracingHeaders) + assertNotNull(tracingHeaders.sentryTraceHeader) + assertNotNull(tracingHeaders.w3cTraceparentHeader) + + val w3cTrace = tracingHeaders.w3cTraceparentHeader!! + assertEquals(W3CTraceparentHeader.TRACEPARENT_HEADER, w3cTrace.name) + + val headerValue = w3cTrace.value + assertTrue(headerValue.startsWith("00-")) + + val parts = headerValue.split("-") + assertEquals(4, parts.size) + assertEquals("00", parts[0]) + assertEquals(fixture.scope.propagationContext.traceId.toString(), parts[1]) + assertEquals(fixture.scope.propagationContext.spanId.toString(), parts[2]) + assertEquals("00", parts[3]) + } }