From 31f48eb906a6281492530d5058bafbae8ab4e9f9 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 12 May 2026 13:03:34 +0200 Subject: [PATCH 1/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 bb4cac3672e8b9901e009188fd8a625ef584ba15 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 13 May 2026 13:51:19 +0200 Subject: [PATCH 2/3] fix(core): Stop unfreezing Baggage --- sentry/src/main/java/io/sentry/Baggage.java | 16 +++++++++------- .../main/java/io/sentry/TransactionContext.java | 2 +- sentry/src/test/java/io/sentry/BaggageTest.kt | 16 +++++++++++++--- .../java/io/sentry/TransactionContextTest.kt | 3 +++ 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/sentry/src/main/java/io/sentry/Baggage.java b/sentry/src/main/java/io/sentry/Baggage.java index 4d7e76b170..6ede0a88df 100644 --- a/sentry/src/main/java/io/sentry/Baggage.java +++ b/sentry/src/main/java/io/sentry/Baggage.java @@ -221,21 +221,23 @@ public Baggage(final @NotNull Baggage baggage) { @ApiStatus.Internal static @NotNull Baggage copyWithOverrides( - final @NotNull Baggage baggage, + final @Nullable Baggage baggage, final @NotNull SentryId traceId, final @Nullable Double sampleRand) { + final @NotNull Baggage source = + baggage == null ? new Baggage(NoOpLogger.getInstance()) : baggage; final @NotNull ConcurrentHashMap keyValues = - new ConcurrentHashMap<>(baggage.keyValues); + new ConcurrentHashMap<>(source.keyValues); keyValues.put(DSCKeys.TRACE_ID, traceId.toString()); return new Baggage( keyValues, - baggage.sampleRate, + source.sampleRate, sampleRand, - baggage.thirdPartyHeader, - true, - false, - baggage.logger); + source.thirdPartyHeader, + source.mutable, + source.shouldFreeze, + source.logger); } @ApiStatus.Internal diff --git a/sentry/src/main/java/io/sentry/TransactionContext.java b/sentry/src/main/java/io/sentry/TransactionContext.java index 470e8f1975..c424750e69 100644 --- a/sentry/src/main/java/io/sentry/TransactionContext.java +++ b/sentry/src/main/java/io/sentry/TransactionContext.java @@ -44,7 +44,7 @@ public static TransactionContext fromPropagationContext( final @NotNull TransactionContext transactionContext) { final @NotNull Baggage baggage = Baggage.copyWithOverrides( - propagationContext.getBaggage(), + transactionContext.getBaggage(), propagationContext.getTraceId(), propagationContext.getSampleRand()); diff --git a/sentry/src/test/java/io/sentry/BaggageTest.kt b/sentry/src/test/java/io/sentry/BaggageTest.kt index 950577698b..eb7e052a04 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 creates mutable baggage`() { + fun `copy with overrides preserves frozen state`() { val baggage = Baggage.fromHeader( "sentry-sample_rand=0.1,sentry-trace_id=75302ac48a024bde9a3b3734a82e36c8", @@ -450,14 +450,24 @@ class BaggageTest { val copy = Baggage.copyWithOverrides(baggage, SentryId("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0.2) - assertTrue(copy.isMutable) - assertFalse(copy.isShouldFreeze) + assertFalse(copy.isMutable) + assertTrue(copy.isShouldFreeze) assertEquals("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", copy.traceId) assertEquals(0.2, copy.sampleRand!!, 0.0001) assertEquals("75302ac48a024bde9a3b3734a82e36c8", baggage.traceId) assertEquals(0.1, baggage.sampleRand!!, 0.0001) } + @Test + fun `copy with overrides creates baggage when source is null`() { + val copy = Baggage.copyWithOverrides(null, SentryId("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0.2) + + assertTrue(copy.isMutable) + assertFalse(copy.isShouldFreeze) + assertEquals("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", copy.traceId) + assertEquals(0.2, copy.sampleRand!!, 0.0001) + } + @Test fun `if header contains sentry values baggage is marked as shouldFreeze`() { val baggage = diff --git a/sentry/src/test/java/io/sentry/TransactionContextTest.kt b/sentry/src/test/java/io/sentry/TransactionContextTest.kt index bba16a581c..bef2f491e6 100644 --- a/sentry/src/test/java/io/sentry/TransactionContextTest.kt +++ b/sentry/src/test/java/io/sentry/TransactionContextTest.kt @@ -99,6 +99,7 @@ class TransactionContextTest { fun `fromPropagationContextAsRoot copies non trace state`() { val propagationBaggage = Baggage(NoOpLogger.getInstance()) propagationBaggage.sampleRand = 0.42 + propagationBaggage.publicKey = "propagation-public-key" val propagationContext = PropagationContext( SentryId("75302ac48a024bde9a3b3734a82e36c8"), @@ -109,6 +110,7 @@ class TransactionContextTest { ) val samplingDecision = TracesSamplingDecision(true, 0.3, true, 0.4) val transactionContext = TransactionContext("name", "op", samplingDecision) + transactionContext.baggage!!.publicKey = "transaction-public-key" transactionContext.transactionNameSource = TransactionNameSource.ROUTE transactionContext.description = "description" transactionContext.status = SpanStatus.OK @@ -145,6 +147,7 @@ class TransactionContextTest { assertEquals(0.4, context.samplingDecision!!.profileSampleRate) assertEquals(0.42, context.baggage!!.sampleRand) assertEquals(propagationContext.traceId.toString(), context.baggage!!.traceId) + assertEquals("transaction-public-key", context.baggage!!.publicKey) assertNull(context.featureFlagBuffer.featureFlags) } From cd713a47d570978f788a526ff2f2f73bdb2d486a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 13 May 2026 13:55:19 +0200 Subject: [PATCH 3/3] Revert "fix(core): Stop unfreezing Baggage" This reverts commit bb4cac3672e8b9901e009188fd8a625ef584ba15. --- sentry/src/main/java/io/sentry/Baggage.java | 16 +++++++--------- .../main/java/io/sentry/TransactionContext.java | 2 +- sentry/src/test/java/io/sentry/BaggageTest.kt | 16 +++------------- .../java/io/sentry/TransactionContextTest.kt | 3 --- 4 files changed, 11 insertions(+), 26 deletions(-) diff --git a/sentry/src/main/java/io/sentry/Baggage.java b/sentry/src/main/java/io/sentry/Baggage.java index 6ede0a88df..4d7e76b170 100644 --- a/sentry/src/main/java/io/sentry/Baggage.java +++ b/sentry/src/main/java/io/sentry/Baggage.java @@ -221,23 +221,21 @@ public Baggage(final @NotNull Baggage baggage) { @ApiStatus.Internal static @NotNull Baggage copyWithOverrides( - final @Nullable Baggage baggage, + final @NotNull Baggage baggage, final @NotNull SentryId traceId, final @Nullable Double sampleRand) { - final @NotNull Baggage source = - baggage == null ? new Baggage(NoOpLogger.getInstance()) : baggage; final @NotNull ConcurrentHashMap keyValues = - new ConcurrentHashMap<>(source.keyValues); + new ConcurrentHashMap<>(baggage.keyValues); keyValues.put(DSCKeys.TRACE_ID, traceId.toString()); return new Baggage( keyValues, - source.sampleRate, + baggage.sampleRate, sampleRand, - source.thirdPartyHeader, - source.mutable, - source.shouldFreeze, - source.logger); + baggage.thirdPartyHeader, + true, + false, + baggage.logger); } @ApiStatus.Internal diff --git a/sentry/src/main/java/io/sentry/TransactionContext.java b/sentry/src/main/java/io/sentry/TransactionContext.java index c424750e69..470e8f1975 100644 --- a/sentry/src/main/java/io/sentry/TransactionContext.java +++ b/sentry/src/main/java/io/sentry/TransactionContext.java @@ -44,7 +44,7 @@ public static TransactionContext fromPropagationContext( final @NotNull TransactionContext transactionContext) { final @NotNull Baggage baggage = Baggage.copyWithOverrides( - transactionContext.getBaggage(), + propagationContext.getBaggage(), propagationContext.getTraceId(), propagationContext.getSampleRand()); diff --git a/sentry/src/test/java/io/sentry/BaggageTest.kt b/sentry/src/test/java/io/sentry/BaggageTest.kt index eb7e052a04..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,22 +450,12 @@ class BaggageTest { val copy = Baggage.copyWithOverrides(baggage, SentryId("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0.2) - assertFalse(copy.isMutable) - assertTrue(copy.isShouldFreeze) - assertEquals("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", copy.traceId) - assertEquals(0.2, copy.sampleRand!!, 0.0001) - assertEquals("75302ac48a024bde9a3b3734a82e36c8", baggage.traceId) - assertEquals(0.1, baggage.sampleRand!!, 0.0001) - } - - @Test - fun `copy with overrides creates baggage when source is null`() { - val copy = Baggage.copyWithOverrides(null, SentryId("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), 0.2) - assertTrue(copy.isMutable) assertFalse(copy.isShouldFreeze) assertEquals("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", copy.traceId) assertEquals(0.2, copy.sampleRand!!, 0.0001) + assertEquals("75302ac48a024bde9a3b3734a82e36c8", baggage.traceId) + assertEquals(0.1, baggage.sampleRand!!, 0.0001) } @Test diff --git a/sentry/src/test/java/io/sentry/TransactionContextTest.kt b/sentry/src/test/java/io/sentry/TransactionContextTest.kt index bef2f491e6..bba16a581c 100644 --- a/sentry/src/test/java/io/sentry/TransactionContextTest.kt +++ b/sentry/src/test/java/io/sentry/TransactionContextTest.kt @@ -99,7 +99,6 @@ class TransactionContextTest { fun `fromPropagationContextAsRoot copies non trace state`() { val propagationBaggage = Baggage(NoOpLogger.getInstance()) propagationBaggage.sampleRand = 0.42 - propagationBaggage.publicKey = "propagation-public-key" val propagationContext = PropagationContext( SentryId("75302ac48a024bde9a3b3734a82e36c8"), @@ -110,7 +109,6 @@ class TransactionContextTest { ) val samplingDecision = TracesSamplingDecision(true, 0.3, true, 0.4) val transactionContext = TransactionContext("name", "op", samplingDecision) - transactionContext.baggage!!.publicKey = "transaction-public-key" transactionContext.transactionNameSource = TransactionNameSource.ROUTE transactionContext.description = "description" transactionContext.status = SpanStatus.OK @@ -147,7 +145,6 @@ class TransactionContextTest { assertEquals(0.4, context.samplingDecision!!.profileSampleRate) assertEquals(0.42, context.baggage!!.sampleRand) assertEquals(propagationContext.traceId.toString(), context.baggage!!.traceId) - assertEquals("transaction-public-key", context.baggage!!.publicKey) assertNull(context.featureFlagBuffer.featureFlags) }