From 422aa61debdaa4fc647fd8e4444515c83963724e Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 11 May 2026 15:01:29 +0200 Subject: [PATCH 1/3] fix(core): Keep remapped session trace baggage mutable Copying session trace baggage for a transaction now creates mutable baggage so the transaction can populate DSC fields even if the ambient session baggage was already frozen by an earlier outgoing request. Co-Authored-By: Claude --- sentry/src/main/java/io/sentry/Baggage.java | 4 +- sentry/src/test/java/io/sentry/BaggageTest.kt | 5 +- sentry/src/test/java/io/sentry/ScopesTest.kt | 66 +++++++++++++++++++ 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/sentry/src/main/java/io/sentry/Baggage.java b/sentry/src/main/java/io/sentry/Baggage.java index d651c67122..4d7e76b170 100644 --- a/sentry/src/main/java/io/sentry/Baggage.java +++ b/sentry/src/main/java/io/sentry/Baggage.java @@ -233,8 +233,8 @@ public Baggage(final @NotNull Baggage baggage) { baggage.sampleRate, sampleRand, baggage.thirdPartyHeader, - baggage.mutable, - baggage.shouldFreeze, + true, + false, baggage.logger); } diff --git a/sentry/src/test/java/io/sentry/BaggageTest.kt b/sentry/src/test/java/io/sentry/BaggageTest.kt index 1c1b091394..950577698b 100644 --- a/sentry/src/test/java/io/sentry/BaggageTest.kt +++ b/sentry/src/test/java/io/sentry/BaggageTest.kt @@ -440,7 +440,7 @@ class BaggageTest { } @Test - fun `copy with overrides preserves frozen state`() { + fun `copy with overrides creates mutable baggage`() { val baggage = Baggage.fromHeader( "sentry-sample_rand=0.1,sentry-trace_id=75302ac48a024bde9a3b3734a82e36c8", @@ -450,7 +450,8 @@ class BaggageTest { val copy = Baggage.copyWithOverrides(baggage, SentryId("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0.2) - assertFalse(copy.isMutable) + assertTrue(copy.isMutable) + assertFalse(copy.isShouldFreeze) assertEquals("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", copy.traceId) assertEquals(0.2, copy.sampleRand!!, 0.0001) assertEquals("75302ac48a024bde9a3b3734a82e36c8", baggage.traceId) diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index cfe99d1482..8abef4c84d 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -21,6 +21,7 @@ import io.sentry.test.createTestScopes import io.sentry.test.initForTest import io.sentry.util.HintUtils import io.sentry.util.StringUtils +import io.sentry.util.TracingUtils import java.io.File import java.nio.file.Files import java.util.Queue @@ -1865,6 +1866,47 @@ class ScopesTest { assertNull(transaction.root.spanContext.parentSpanId) } + @Test + fun `session trace transaction baggage is populated after scope baggage is frozen`() { + val scopes = generateScopes { + it.isEnableSessionTraceLifecycle = true + it.release = "1.0.0" + it.environment = "production" + } + + scopes.startSession() + + val sessionTraceId = AtomicReference() + val sessionSampleRand = AtomicReference() + val headersWithoutTransaction = + TracingUtils.traceIfAllowed(scopes, "https://sentry.io/hello", emptyList(), null) + assertNotNull(headersWithoutTransaction) + scopes.configureScope { scope -> + sessionTraceId.set(scope.propagationContext.traceId) + sessionSampleRand.set(scope.propagationContext.sampleRand) + assertFalse(scope.propagationContext.baggage!!.isMutable) + } + + val firstTransaction = + scopes.startTransaction(TransactionContext("first transaction", "ui.load")) + assertSessionTraceBaggage( + firstTransaction, + scopes, + sessionTraceId.get(), + sessionSampleRand.get(), + ) + firstTransaction.finish() + + val secondTransaction = + scopes.startTransaction(TransactionContext("second transaction", "ui.action")) + assertSessionTraceBaggage( + secondTransaction, + scopes, + sessionTraceId.get(), + sessionSampleRand.get(), + ) + } + @Test fun `when startTransaction with bindToScope set to false, transaction is not attached to the scope`() { val scopes = generateScopes() @@ -4408,6 +4450,30 @@ class ScopesTest { return createScopes(options) } + private fun assertSessionTraceBaggage( + transaction: ITransaction, + scopes: IScopes, + sessionTraceId: SentryId, + sessionSampleRand: Double, + ) { + assertTrue(transaction is SentryTracer) + assertEquals(sessionTraceId, transaction.root.spanContext.traceId) + + val tracingHeaders = + TracingUtils.traceIfAllowed(scopes, "https://sentry.io/hello", emptyList(), transaction) + val baggage = + Baggage.fromHeader(tracingHeaders!!.baggageHeader!!.value, NoOpLogger.getInstance()) + + assertEquals(sessionTraceId.toString(), baggage.traceId) + assertEquals(transaction.name, baggage.transaction) + assertEquals("true", baggage.sampled) + assertEquals(1.0, baggage.sampleRate!!, 0.0001) + assertEquals(sessionSampleRand, baggage.sampleRand!!, 0.0001) + assertEquals("key", baggage.publicKey) + assertEquals("1.0.0", baggage.release) + assertEquals("production", baggage.environment) + } + private fun getEnabledScopes( optionsConfiguration: Sentry.OptionsConfiguration? = null ): Triple { From d31251a29750d998a843991fbdb867289292b8cc Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 12 May 2026 13:03:34 +0200 Subject: [PATCH 2/3] fix(core): Require active session for session trace lifecycle Only remap root transactions onto the session propagation context when a session is currently active. This avoids reusing the ambient scope trace when session trace lifecycle is enabled before a session starts. Co-Authored-By: Claude --- sentry/src/main/java/io/sentry/Scopes.java | 7 +++-- sentry/src/test/java/io/sentry/ScopesTest.kt | 32 ++++++++++++++++++-- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index d45c4fee82..5e55fa9877 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -1024,10 +1024,11 @@ && getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE private @NotNull TransactionContext maybeApplySessionTraceLifecycle( final @NotNull TransactionContext transactionContext) { - final @NotNull PropagationContext propagationContext = - getCombinedScopeView().getPropagationContext(); if (getOptions().isEnableSessionTraceLifecycle() - && transactionContext.getParentSpanId() == null) { + && transactionContext.getParentSpanId() == null + && getCombinedScopeView().getSession() != null) { + final @NotNull PropagationContext propagationContext = + getCombinedScopeView().getPropagationContext(); return TransactionContext.fromPropagationContextAsRoot( propagationContext, transactionContext); } diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 8abef4c84d..0467411dd7 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -1823,10 +1823,28 @@ class ScopesTest { } @Test - fun `when session trace lifecycle is enabled, startTransaction uses session propagation context`() { + fun `when session trace lifecycle is enabled without active session, root transaction does not use scope propagation context`() { val scopes = generateScopes { it.isEnableSessionTraceLifecycle = true } var propagationContext: PropagationContext? = null scopes.configureScope { propagationContext = it.propagationContext } + val context = TransactionContext("name", "op") + + val transaction = scopes.startTransaction(context) + + assertTrue(transaction is SentryTracer) + assertEquals(context.traceId, transaction.root.spanContext.traceId) + assertNotEquals(propagationContext!!.traceId, transaction.root.spanContext.traceId) + } + + @Test + fun `when session trace lifecycle is enabled, startTransaction uses session propagation context`() { + val scopes = generateScopes { + it.isEnableSessionTraceLifecycle = true + it.release = "1.0.0" + } + scopes.startSession() + var propagationContext: PropagationContext? = null + scopes.configureScope { propagationContext = it.propagationContext } val transaction = scopes.startTransaction(TransactionContext("name", "op")) @@ -1839,7 +1857,11 @@ class ScopesTest { @Test fun `continued trace with parent span is not remapped to session trace`() { - val scopes = generateScopes { it.isEnableSessionTraceLifecycle = true } + val scopes = generateScopes { + it.isEnableSessionTraceLifecycle = true + it.release = "1.0.0" + } + scopes.startSession() val traceId = "75302ac48a024bde9a3b3734a82e36c8" val parentSpanId = "1000000000000000" val context = scopes.continueTrace("$traceId-$parentSpanId-1", emptyList())!! @@ -1853,7 +1875,11 @@ class ScopesTest { @Test fun `when session trace lifecycle is enabled, root transaction uses current propagation context`() { - val scopes = generateScopes { it.isEnableSessionTraceLifecycle = true } + val scopes = generateScopes { + it.isEnableSessionTraceLifecycle = true + it.release = "1.0.0" + } + scopes.startSession() val traceId = "75302ac48a024bde9a3b3734a82e36c8" val parentSpanId = "1000000000000000" scopes.continueTrace("$traceId-$parentSpanId-1", emptyList()) From 551bdec2940de602dea4a8c864839ee0999cc729 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 12 May 2026 13:07:59 +0200 Subject: [PATCH 3/3] Revert "fix(core): Require active session for session trace lifecycle" This reverts commit d31251a29750d998a843991fbdb867289292b8cc. --- sentry/src/main/java/io/sentry/Scopes.java | 7 ++--- sentry/src/test/java/io/sentry/ScopesTest.kt | 32 ++------------------ 2 files changed, 6 insertions(+), 33 deletions(-) diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index 5e55fa9877..d45c4fee82 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -1024,11 +1024,10 @@ && getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE private @NotNull TransactionContext maybeApplySessionTraceLifecycle( final @NotNull TransactionContext transactionContext) { + final @NotNull PropagationContext propagationContext = + getCombinedScopeView().getPropagationContext(); if (getOptions().isEnableSessionTraceLifecycle() - && transactionContext.getParentSpanId() == null - && getCombinedScopeView().getSession() != null) { - final @NotNull PropagationContext propagationContext = - getCombinedScopeView().getPropagationContext(); + && transactionContext.getParentSpanId() == null) { return TransactionContext.fromPropagationContextAsRoot( propagationContext, transactionContext); } diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 0467411dd7..8abef4c84d 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -1822,27 +1822,9 @@ class ScopesTest { assertEquals(contexts, transaction.root.spanContext) } - @Test - fun `when session trace lifecycle is enabled without active session, root transaction does not use scope propagation context`() { - val scopes = generateScopes { it.isEnableSessionTraceLifecycle = true } - var propagationContext: PropagationContext? = null - scopes.configureScope { propagationContext = it.propagationContext } - val context = TransactionContext("name", "op") - - val transaction = scopes.startTransaction(context) - - assertTrue(transaction is SentryTracer) - assertEquals(context.traceId, transaction.root.spanContext.traceId) - assertNotEquals(propagationContext!!.traceId, transaction.root.spanContext.traceId) - } - @Test fun `when session trace lifecycle is enabled, startTransaction uses session propagation context`() { - val scopes = generateScopes { - it.isEnableSessionTraceLifecycle = true - it.release = "1.0.0" - } - scopes.startSession() + val scopes = generateScopes { it.isEnableSessionTraceLifecycle = true } var propagationContext: PropagationContext? = null scopes.configureScope { propagationContext = it.propagationContext } @@ -1857,11 +1839,7 @@ class ScopesTest { @Test fun `continued trace with parent span is not remapped to session trace`() { - val scopes = generateScopes { - it.isEnableSessionTraceLifecycle = true - it.release = "1.0.0" - } - scopes.startSession() + val scopes = generateScopes { it.isEnableSessionTraceLifecycle = true } val traceId = "75302ac48a024bde9a3b3734a82e36c8" val parentSpanId = "1000000000000000" val context = scopes.continueTrace("$traceId-$parentSpanId-1", emptyList())!! @@ -1875,11 +1853,7 @@ class ScopesTest { @Test fun `when session trace lifecycle is enabled, root transaction uses current propagation context`() { - val scopes = generateScopes { - it.isEnableSessionTraceLifecycle = true - it.release = "1.0.0" - } - scopes.startSession() + val scopes = generateScopes { it.isEnableSessionTraceLifecycle = true } val traceId = "75302ac48a024bde9a3b3734a82e36c8" val parentSpanId = "1000000000000000" scopes.continueTrace("$traceId-$parentSpanId-1", emptyList())