Skip to content

Commit 7151c08

Browse files
authored
Merge 25e71db into 8c1fb22
2 parents 8c1fb22 + 25e71db commit 7151c08

36 files changed

Lines changed: 892 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Features
6+
7+
- Prevent cross-organization trace continuation ([#5136](https://github.com/getsentry/sentry-java/pull/5136))
8+
- By default, the SDK now extracts the organization ID from the DSN (e.g. `o123.ingest.sentry.io`) and compares it with the `sentry-org_id` value in incoming baggage headers. When the two differ, the SDK starts a fresh trace instead of continuing the foreign one. This guards against accidentally linking traces across organizations.
9+
- New option `enableStrictTraceContinuation` (default `false`): when enabled, both the SDK's org ID **and** the incoming baggage org ID must be present and match for a trace to be continued. Traces with a missing org ID on either side are rejected. Configurable via code (`setStrictTraceContinuation(true)`), `sentry.properties` (`enable-strict-trace-continuation=true`), Android manifest (`io.sentry.strict-trace-continuation.enabled`), or Spring Boot (`sentry.strict-trace-continuation=true`).
10+
- New option `orgId`: allows explicitly setting the organization ID for self-hosted and Relay setups where it cannot be extracted from the DSN. Configurable via code (`setOrgId("123")`), `sentry.properties` (`org-id=123`), Android manifest (`io.sentry.org-id`), or Spring Boot (`sentry.org-id=123`).
11+
312
## 8.37.0
413

514
### Fixes

sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@ final class ManifestMetadataReader {
167167

168168
static final String FEEDBACK_SHOW_BRANDING = "io.sentry.feedback.show-branding";
169169

170+
static final String STRICT_TRACE_CONTINUATION = "io.sentry.strict-trace-continuation.enabled";
171+
static final String ORG_ID = "io.sentry.org-id";
172+
170173
static final String FEEDBACK_USE_SHAKE_GESTURE = "io.sentry.feedback.use-shake-gesture";
171174

172175
static final String SPOTLIGHT_ENABLE = "io.sentry.spotlight.enable";
@@ -667,6 +670,15 @@ static void applyMetadata(
667670
readBool(
668671
metadata, logger, FEEDBACK_USE_SHAKE_GESTURE, feedbackOptions.isUseShakeGesture()));
669672

673+
options.setStrictTraceContinuation(
674+
readBool(
675+
metadata, logger, STRICT_TRACE_CONTINUATION, options.isStrictTraceContinuation()));
676+
677+
final @Nullable String orgId = readString(metadata, logger, ORG_ID, null);
678+
if (orgId != null) {
679+
options.setOrgId(orgId);
680+
}
681+
670682
options.setEnableSpotlight(
671683
readBool(metadata, logger, SPOTLIGHT_ENABLE, options.isEnableSpotlight()));
672684

sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2461,4 +2461,54 @@ class ManifestMetadataReaderTest {
24612461
// maskAllImages should also add WebView
24622462
assertTrue(fixture.options.screenshot.maskViewClasses.contains("android.webkit.WebView"))
24632463
}
2464+
2465+
@Test
2466+
fun `applyMetadata reads strictTraceContinuation and keeps default value if not found`() {
2467+
// Arrange
2468+
val context = fixture.getContext()
2469+
2470+
// Act
2471+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
2472+
2473+
// Assert
2474+
assertFalse(fixture.options.isStrictTraceContinuation)
2475+
}
2476+
2477+
@Test
2478+
fun `applyMetadata reads strictTraceContinuation to options`() {
2479+
// Arrange
2480+
val bundle = bundleOf(ManifestMetadataReader.STRICT_TRACE_CONTINUATION to true)
2481+
val context = fixture.getContext(metaData = bundle)
2482+
2483+
// Act
2484+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
2485+
2486+
// Assert
2487+
assertTrue(fixture.options.isStrictTraceContinuation)
2488+
}
2489+
2490+
@Test
2491+
fun `applyMetadata reads orgId and keeps null if not found`() {
2492+
// Arrange
2493+
val context = fixture.getContext()
2494+
2495+
// Act
2496+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
2497+
2498+
// Assert
2499+
assertNull(fixture.options.orgId)
2500+
}
2501+
2502+
@Test
2503+
fun `applyMetadata reads orgId to options`() {
2504+
// Arrange
2505+
val bundle = bundleOf(ManifestMetadataReader.ORG_ID to "12345")
2506+
val context = fixture.getContext(metaData = bundle)
2507+
2508+
// Act
2509+
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)
2510+
2511+
// Assert
2512+
assertEquals("12345", fixture.options.orgId)
2513+
}
24642514
}

sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ public <C> Context extract(
113113

114114
final @Nullable String baggageString = getter.get(carrier, BaggageHeader.BAGGAGE_HEADER);
115115
final Baggage baggage = Baggage.fromHeader(baggageString);
116+
if (!TracingUtils.shouldContinueTrace(scopes.getOptions(), baggage)) {
117+
scopes
118+
.getOptions()
119+
.getLogger()
120+
.log(
121+
SentryLevel.DEBUG, "Not continuing trace due to strict org ID validation failure.");
122+
return context;
123+
}
116124
final @NotNull TraceState traceState = TraceState.getDefault();
117125

118126
SpanContext otelSpanContext =

sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import io.sentry.SentryEvent;
2323
import io.sentry.SentryLevel;
2424
import io.sentry.SentryLongDate;
25+
import io.sentry.SentryOptions;
2526
import io.sentry.SentryTraceHeader;
2627
import io.sentry.SpanId;
2728
import io.sentry.TracesSamplingDecision;
@@ -94,9 +95,16 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri
9495
}
9596
}
9697

97-
final @NotNull PropagationContext propagationContext =
98-
new PropagationContext(
99-
new SentryId(traceId), sentrySpanId, sentryParentSpanId, baggage, sampled);
98+
final @NotNull SentryOptions sentryOptions = scopes.getOptions();
99+
final @NotNull PropagationContext propagationContext;
100+
if (sentryTraceHeader != null) {
101+
propagationContext =
102+
PropagationContext.fromHeaders(sentryTraceHeader, baggage, sentrySpanId, sentryOptions);
103+
} else {
104+
propagationContext =
105+
new PropagationContext(
106+
new SentryId(traceId), sentrySpanId, sentryParentSpanId, baggage, sampled);
107+
}
100108

101109
baggage = propagationContext.getBaggage();
102110
baggage.setValuesFromSamplingDecision(samplingDecision);

sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryPropagator.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import io.sentry.SentryLevel;
1717
import io.sentry.SentryTraceHeader;
1818
import io.sentry.exception.InvalidSentryTraceHeaderException;
19+
import io.sentry.util.TracingUtils;
1920
import java.util.Arrays;
2021
import java.util.Collection;
2122
import java.util.Collections;
@@ -98,6 +99,17 @@ public <C> Context extract(
9899
try {
99100
SentryTraceHeader sentryTraceHeader = new SentryTraceHeader(sentryTraceString);
100101

102+
final @Nullable String baggageString = getter.get(carrier, BaggageHeader.BAGGAGE_HEADER);
103+
Baggage baggage = Baggage.fromHeader(baggageString);
104+
if (!TracingUtils.shouldContinueTrace(scopes.getOptions(), baggage)) {
105+
scopes
106+
.getOptions()
107+
.getLogger()
108+
.log(
109+
SentryLevel.DEBUG, "Not continuing trace due to strict org ID validation failure.");
110+
return context;
111+
}
112+
101113
SpanContext otelSpanContext =
102114
SpanContext.createFromRemoteParent(
103115
sentryTraceHeader.getTraceId().toString(),
@@ -107,9 +119,6 @@ public <C> Context extract(
107119

108120
@NotNull
109121
Context modifiedContext = context.with(SentryOtelKeys.SENTRY_TRACE_KEY, sentryTraceHeader);
110-
111-
final @Nullable String baggageString = getter.get(carrier, BaggageHeader.BAGGAGE_HEADER);
112-
Baggage baggage = Baggage.fromHeader(baggageString);
113122
modifiedContext = modifiedContext.with(SentryOtelKeys.SENTRY_BAGGAGE_KEY, baggage);
114123

115124
Span wrappedSpan = Span.wrap(otelSpanContext);

sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ public SamplingResult shouldSample(
9191
final @NotNull PropagationContext propagationContext =
9292
sentryTraceHeader == null
9393
? new PropagationContext(new SentryId(traceId), randomSpanId, null, baggage, null)
94-
: PropagationContext.fromHeaders(sentryTraceHeader, baggage, randomSpanId);
94+
: PropagationContext.fromHeaders(
95+
sentryTraceHeader, baggage, randomSpanId, scopes.getOptions());
9596

9697
final @NotNull TransactionContext transactionContext =
9798
TransactionContext.fromPropagationContext(propagationContext);

sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanProcessor.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,10 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri
127127
new SentryId(traceData.getTraceId()), spanId, null, null, null)
128128
: TransactionContext.fromPropagationContext(
129129
PropagationContext.fromHeaders(
130-
traceData.getSentryTraceHeader(), traceData.getBaggage(), spanId));
130+
traceData.getSentryTraceHeader(),
131+
traceData.getBaggage(),
132+
spanId,
133+
scopes.getOptions()));
131134
;
132135
transactionContext.setName(transactionName);
133136
transactionContext.setTransactionNameSource(transactionNameSource);

sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OtelSentryPropagatorTest.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import kotlin.test.AfterTest
1919
import kotlin.test.BeforeTest
2020
import kotlin.test.Test
2121
import kotlin.test.assertEquals
22+
import kotlin.test.assertFalse
2223
import kotlin.test.assertNotNull
2324
import kotlin.test.assertNull
2425
import kotlin.test.assertSame
@@ -69,6 +70,26 @@ class OtelSentryPropagatorTest {
6970
assertSame(scopeInContext, scopes)
7071
}
7172

73+
@Test
74+
fun `ignores incoming headers when strict continuation rejects org id`() {
75+
Sentry.init { options ->
76+
options.dsn = "https://key@o2.ingest.sentry.io/123"
77+
options.isStrictTraceContinuation = true
78+
}
79+
val propagator = OtelSentryPropagator()
80+
val carrier: Map<String, String> =
81+
mapOf(
82+
"sentry-trace" to "f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1",
83+
"baggage" to "sentry-trace_id=f9118105af4a2d42b4124532cd1065ff,sentry-org_id=1",
84+
)
85+
86+
val newContext = propagator.extract(Context.root(), carrier, MapGetter())
87+
88+
assertFalse(Span.fromContext(newContext).spanContext.isValid)
89+
assertNull(newContext.get(SENTRY_TRACE_KEY))
90+
assertNull(newContext.get(SENTRY_BAGGAGE_KEY))
91+
}
92+
7293
@Test
7394
fun `uses incoming headers`() {
7495
val propagator = OtelSentryPropagator()
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.sentry.opentelemetry
2+
3+
import io.opentelemetry.api.trace.Span
4+
import io.opentelemetry.context.Context
5+
import io.sentry.Sentry
6+
import io.sentry.opentelemetry.SentryOtelKeys.SENTRY_BAGGAGE_KEY
7+
import io.sentry.opentelemetry.SentryOtelKeys.SENTRY_TRACE_KEY
8+
import kotlin.test.BeforeTest
9+
import kotlin.test.Test
10+
import kotlin.test.assertFalse
11+
import kotlin.test.assertNull
12+
13+
class SentryPropagatorTest {
14+
15+
@BeforeTest
16+
fun setup() {
17+
Sentry.init("https://key@sentry.io/proj")
18+
}
19+
20+
@Suppress("DEPRECATION")
21+
@Test
22+
fun `ignores incoming headers when strict continuation rejects org id`() {
23+
Sentry.init { options ->
24+
options.dsn = "https://key@o2.ingest.sentry.io/123"
25+
options.isStrictTraceContinuation = true
26+
}
27+
28+
val propagator = SentryPropagator()
29+
val carrier: Map<String, String> =
30+
mapOf(
31+
"sentry-trace" to "f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1",
32+
"baggage" to "sentry-trace_id=f9118105af4a2d42b4124532cd1065ff,sentry-org_id=1",
33+
)
34+
35+
val newContext = propagator.extract(Context.root(), carrier, MapGetter())
36+
37+
assertFalse(Span.fromContext(newContext).spanContext.isValid)
38+
assertNull(newContext.get(SENTRY_TRACE_KEY))
39+
assertNull(newContext.get(SENTRY_BAGGAGE_KEY))
40+
}
41+
}

0 commit comments

Comments
 (0)