Skip to content

Commit cc97e32

Browse files
adinauerclaude
andcommitted
fix(core): Preserve context state for session trace roots
Reuse SpanContext copy logic when remapping root transactions onto the session propagation context. Preserve transaction identity while replacing the trace id and clearing the parent span id. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5aa927f commit cc97e32

3 files changed

Lines changed: 82 additions & 33 deletions

File tree

sentry/src/main/java/io/sentry/SpanContext.java

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -122,24 +122,35 @@ public SpanContext(final @NotNull SpanContext spanContext) {
122122
this.traceId = spanContext.traceId;
123123
this.spanId = spanContext.spanId;
124124
this.parentSpanId = spanContext.parentSpanId;
125-
setSamplingDecision(spanContext.samplingDecision);
126125
this.op = spanContext.op;
127-
this.description = spanContext.description;
128-
this.status = spanContext.status;
129-
final Map<String, String> copiedTags = CollectionUtils.newConcurrentHashMap(spanContext.tags);
126+
copyNonTraceState(spanContext, this, spanContext.baggage);
127+
}
128+
129+
@ApiStatus.Internal
130+
static void copyNonTraceState(
131+
final @NotNull SpanContext source,
132+
final @NotNull SpanContext target,
133+
final @Nullable Baggage baggage) {
134+
target.op = source.op;
135+
target.description = source.description;
136+
target.status = source.status;
137+
target.origin = source.origin;
138+
target.instrumenter = source.instrumenter;
139+
target.baggage = baggage;
140+
target.setSamplingDecision(source.samplingDecision);
141+
final Map<String, String> copiedTags = CollectionUtils.newConcurrentHashMap(source.tags);
130142
if (copiedTags != null) {
131-
this.tags = copiedTags;
143+
target.tags = copiedTags;
132144
}
133-
final Map<String, Object> copiedUnknown =
134-
CollectionUtils.newConcurrentHashMap(spanContext.unknown);
145+
final Map<String, Object> copiedUnknown = CollectionUtils.newConcurrentHashMap(source.unknown);
135146
if (copiedUnknown != null) {
136-
this.unknown = copiedUnknown;
147+
target.unknown = copiedUnknown;
137148
}
138-
this.baggage = spanContext.baggage;
139-
final Map<String, Object> copiedData = CollectionUtils.newConcurrentHashMap(spanContext.data);
149+
final Map<String, Object> copiedData = CollectionUtils.newConcurrentHashMap(source.data);
140150
if (copiedData != null) {
141-
this.data = copiedData;
151+
target.data = copiedData;
142152
}
153+
target.profilerId = source.profilerId;
143154
}
144155

145156
public void setOperation(final @NotNull String operation) {

sentry/src/main/java/io/sentry/TransactionContext.java

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import io.sentry.protocol.SentryId;
44
import io.sentry.protocol.TransactionNameSource;
5-
import io.sentry.util.CollectionUtils;
65
import io.sentry.util.Objects;
76
import io.sentry.util.TracingUtils;
87
import org.jetbrains.annotations.ApiStatus;
@@ -51,27 +50,13 @@ public static TransactionContext fromPropagationContext(
5150
propagationContext.getSampleRand());
5251

5352
final @NotNull TransactionContext sessionContext =
54-
new TransactionContext(propagationContext.getTraceId(), new SpanId(), null, null, baggage);
55-
sessionContext.setName(transactionContext.getName());
56-
sessionContext.setTransactionNameSource(transactionContext.getTransactionNameSource());
57-
sessionContext.setOperation(transactionContext.getOperation());
58-
sessionContext.setDescription(transactionContext.getDescription());
59-
sessionContext.setStatus(transactionContext.getStatus());
60-
sessionContext.setOrigin(transactionContext.getOrigin());
61-
sessionContext.setInstrumenter(transactionContext.getInstrumenter());
62-
sessionContext.setSamplingDecision(transactionContext.getSamplingDecision());
63-
sessionContext.setForNextAppStart(transactionContext.isForNextAppStart());
64-
sessionContext.setProfilerId(transactionContext.getProfilerId());
65-
final @Nullable java.util.Map<String, @NotNull String> copiedTags =
66-
CollectionUtils.newConcurrentHashMap(transactionContext.tags);
67-
if (copiedTags != null) {
68-
sessionContext.tags = copiedTags;
69-
}
70-
final @Nullable java.util.Map<String, @NotNull Object> copiedData =
71-
CollectionUtils.newConcurrentHashMap(transactionContext.data);
72-
if (copiedData != null) {
73-
sessionContext.data = copiedData;
74-
}
53+
new TransactionContext(
54+
propagationContext.getTraceId(), transactionContext.getSpanId(), null, null, baggage);
55+
copyNonTraceState(transactionContext, sessionContext, baggage);
56+
sessionContext.name = transactionContext.name;
57+
sessionContext.transactionNameSource = transactionContext.transactionNameSource;
58+
sessionContext.parentSamplingDecision = transactionContext.parentSamplingDecision;
59+
sessionContext.isForNextAppStart = transactionContext.isForNextAppStart;
7560
sessionContext.forceNewTrace = transactionContext.forceNewTrace;
7661
return sessionContext;
7762
}

sentry/src/test/java/io/sentry/TransactionContextTest.kt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,59 @@ class TransactionContextTest {
9595
assertFalse(context.isForNextAppStart)
9696
}
9797

98+
@Test
99+
fun `fromPropagationContextAsRoot copies non trace state`() {
100+
val propagationBaggage = Baggage(NoOpLogger.getInstance())
101+
propagationBaggage.sampleRand = 0.42
102+
val propagationContext =
103+
PropagationContext(
104+
SentryId("75302ac48a024bde9a3b3734a82e36c8"),
105+
SpanId("2000000000000000"),
106+
SpanId("1000000000000000"),
107+
propagationBaggage,
108+
true,
109+
)
110+
val samplingDecision = TracesSamplingDecision(true, 0.3, true, 0.4)
111+
val transactionContext = TransactionContext("name", "op", samplingDecision)
112+
transactionContext.transactionNameSource = TransactionNameSource.ROUTE
113+
transactionContext.description = "description"
114+
transactionContext.status = SpanStatus.OK
115+
transactionContext.origin = "auto.test"
116+
transactionContext.instrumenter = Instrumenter.OTEL
117+
transactionContext.isForNextAppStart = true
118+
transactionContext.profilerId = SentryId("12345678123456781234567812345678")
119+
transactionContext.setTag("tag-key", "tag-value")
120+
transactionContext.setData("data-key", "data-value")
121+
transactionContext.unknown = mapOf("unknown-key" to "unknown-value")
122+
transactionContext.addFeatureFlag("feature-flag", true)
123+
124+
val context =
125+
TransactionContext.fromPropagationContextAsRoot(propagationContext, transactionContext)
126+
127+
assertEquals(propagationContext.traceId, context.traceId)
128+
assertEquals(transactionContext.spanId, context.spanId)
129+
assertNull(context.parentSpanId)
130+
assertEquals("name", context.name)
131+
assertEquals(TransactionNameSource.ROUTE, context.transactionNameSource)
132+
assertEquals("op", context.operation)
133+
assertEquals("description", context.description)
134+
assertEquals(SpanStatus.OK, context.status)
135+
assertEquals("auto.test", context.origin)
136+
assertEquals(Instrumenter.OTEL, context.instrumenter)
137+
assertTrue(context.isForNextAppStart)
138+
assertEquals(SentryId("12345678123456781234567812345678"), context.profilerId)
139+
assertEquals("tag-value", context.tags["tag-key"])
140+
assertEquals("data-value", context.data["data-key"])
141+
assertEquals("unknown-value", context.unknown!!["unknown-key"])
142+
assertEquals(true, context.sampled)
143+
assertEquals(0.3, context.samplingDecision!!.sampleRate)
144+
assertEquals(true, context.profileSampled)
145+
assertEquals(0.4, context.samplingDecision!!.profileSampleRate)
146+
assertEquals(0.42, context.baggage!!.sampleRand)
147+
assertEquals(propagationContext.traceId.toString(), context.baggage!!.traceId)
148+
assertNull(context.featureFlagBuffer.featureFlags)
149+
}
150+
98151
@Test
99152
fun `setForNextAppStart sets the isForNextAppStart flag`() {
100153
val context = TransactionContext("name", "op")

0 commit comments

Comments
 (0)