Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
dc8d319
feat: Add strict trace continuation support
giortzisg Mar 2, 2026
e550ae3
Format code
getsentry-bot Mar 2, 2026
c272ee4
Add changelog entry
giortzisg Mar 2, 2026
1cfc2ea
Update API surface file for strict trace continuation
giortzisg Mar 3, 2026
108eb2d
Address review comments for strict trace continuation
giortzisg Mar 4, 2026
e30064c
Fix compilation errors after rebase on main
giortzisg Mar 4, 2026
4545735
fix: Move changelog entry to Unreleased section
giortzisg Mar 4, 2026
61ada6a
Format code
getsentry-bot Mar 4, 2026
519f6a6
fix: Address review comments — pass options to PropagationContext, fi…
giortzisg Mar 9, 2026
360fc94
fix: Add missing 8.34.1 changelog section
giortzisg Mar 9, 2026
ea9f456
merge: Resolve changelog conflict with main
giortzisg Mar 9, 2026
58bfc8e
Format code
getsentry-bot Mar 9, 2026
ebe8c4c
fix: Address PR review comments for strict trace continuation
giortzisg Mar 11, 2026
824b30b
Format code
getsentry-bot Mar 11, 2026
e2fdc46
fix(tracing): Clarify strict org validation debug log
adinauer Mar 20, 2026
d3b073d
fix(android): Use enabled suffix for strict trace manifest key
adinauer Mar 20, 2026
f7fef22
fix(api): Mark effective org ID helper as internal
adinauer Mar 20, 2026
71562fa
ref(tracing): Extract trace continuation decision into TracingUtils
adinauer Mar 23, 2026
327d897
fix(opentelemetry): Enforce strict continuation in propagators
adinauer Mar 23, 2026
f1edc98
Merge branch 'main' into feat/strict-trace-continuation
adinauer Mar 23, 2026
863f05b
Format code
getsentry-bot Mar 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

### Features

- Prevent cross-organization trace continuation ([#5136](https://github.com/getsentry/sentry-java/pull/5136))
- 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.
- 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`).
- 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`).
- Android: Add `beforeErrorSampling` callback to Session Replay ([#5214](https://github.com/getsentry/sentry-java/pull/5214))
- Allows filtering which errors trigger replay capture before the `onErrorSampleRate` is checked
- Returning `false` skips replay capture entirely for that error; returning `true` proceeds with the normal sample rate check
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ final class ManifestMetadataReader {

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

static final String STRICT_TRACE_CONTINUATION = "io.sentry.strict-trace-continuation.enabled";
static final String ORG_ID = "io.sentry.org-id";

static final String FEEDBACK_USE_SHAKE_GESTURE = "io.sentry.feedback.use-shake-gesture";

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

options.setStrictTraceContinuation(
readBool(
metadata, logger, STRICT_TRACE_CONTINUATION, options.isStrictTraceContinuation()));

final @Nullable String orgId = readString(metadata, logger, ORG_ID, null);
if (orgId != null) {
options.setOrgId(orgId);
}

options.setEnableSpotlight(
readBool(metadata, logger, SPOTLIGHT_ENABLE, options.isEnableSpotlight()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2461,4 +2461,54 @@ class ManifestMetadataReaderTest {
// maskAllImages should also add WebView
assertTrue(fixture.options.screenshot.maskViewClasses.contains("android.webkit.WebView"))
}

@Test
fun `applyMetadata reads strictTraceContinuation and keeps default value if not found`() {
// Arrange
val context = fixture.getContext()

// Act
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

// Assert
assertFalse(fixture.options.isStrictTraceContinuation)
}

@Test
fun `applyMetadata reads strictTraceContinuation to options`() {
// Arrange
val bundle = bundleOf(ManifestMetadataReader.STRICT_TRACE_CONTINUATION to true)
val context = fixture.getContext(metaData = bundle)

// Act
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

// Assert
assertTrue(fixture.options.isStrictTraceContinuation)
}

@Test
fun `applyMetadata reads orgId and keeps null if not found`() {
// Arrange
val context = fixture.getContext()

// Act
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

// Assert
assertNull(fixture.options.orgId)
}

@Test
fun `applyMetadata reads orgId to options`() {
// Arrange
val bundle = bundleOf(ManifestMetadataReader.ORG_ID to "12345")
val context = fixture.getContext(metaData = bundle)

// Act
ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider)

// Assert
assertEquals("12345", fixture.options.orgId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ public <C> Context extract(

final @Nullable String baggageString = getter.get(carrier, BaggageHeader.BAGGAGE_HEADER);
final Baggage baggage = Baggage.fromHeader(baggageString);
if (!TracingUtils.shouldContinueTrace(scopes.getOptions(), baggage)) {
scopes
.getOptions()
.getLogger()
.log(
SentryLevel.DEBUG, "Not continuing trace due to strict org ID validation failure.");
return context;
}
final @NotNull TraceState traceState = TraceState.getDefault();

SpanContext otelSpanContext =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.SentryLongDate;
import io.sentry.SentryOptions;
import io.sentry.SentryTraceHeader;
import io.sentry.SpanId;
import io.sentry.TracesSamplingDecision;
Expand Down Expand Up @@ -94,9 +95,16 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri
}
}

final @NotNull PropagationContext propagationContext =
new PropagationContext(
new SentryId(traceId), sentrySpanId, sentryParentSpanId, baggage, sampled);
final @NotNull SentryOptions sentryOptions = scopes.getOptions();
final @NotNull PropagationContext propagationContext;
if (sentryTraceHeader != null) {
propagationContext =
PropagationContext.fromHeaders(sentryTraceHeader, baggage, sentrySpanId, sentryOptions);
} else {
propagationContext =
new PropagationContext(
new SentryId(traceId), sentrySpanId, sentryParentSpanId, baggage, sampled);
}

baggage = propagationContext.getBaggage();
baggage.setValuesFromSamplingDecision(samplingDecision);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import io.sentry.SentryLevel;
import io.sentry.SentryTraceHeader;
import io.sentry.exception.InvalidSentryTraceHeaderException;
import io.sentry.util.TracingUtils;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
Expand Down Expand Up @@ -98,6 +99,17 @@ public <C> Context extract(
try {
SentryTraceHeader sentryTraceHeader = new SentryTraceHeader(sentryTraceString);

final @Nullable String baggageString = getter.get(carrier, BaggageHeader.BAGGAGE_HEADER);
Baggage baggage = Baggage.fromHeader(baggageString);
if (!TracingUtils.shouldContinueTrace(scopes.getOptions(), baggage)) {
scopes
.getOptions()
.getLogger()
.log(
SentryLevel.DEBUG, "Not continuing trace due to strict org ID validation failure.");
return context;
}

SpanContext otelSpanContext =
SpanContext.createFromRemoteParent(
sentryTraceHeader.getTraceId().toString(),
Expand All @@ -107,9 +119,6 @@ public <C> Context extract(

@NotNull
Context modifiedContext = context.with(SentryOtelKeys.SENTRY_TRACE_KEY, sentryTraceHeader);

final @Nullable String baggageString = getter.get(carrier, BaggageHeader.BAGGAGE_HEADER);
Baggage baggage = Baggage.fromHeader(baggageString);
modifiedContext = modifiedContext.with(SentryOtelKeys.SENTRY_BAGGAGE_KEY, baggage);

Span wrappedSpan = Span.wrap(otelSpanContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ public SamplingResult shouldSample(
final @NotNull PropagationContext propagationContext =
sentryTraceHeader == null
? new PropagationContext(new SentryId(traceId), randomSpanId, null, baggage, null)
: PropagationContext.fromHeaders(sentryTraceHeader, baggage, randomSpanId);
: PropagationContext.fromHeaders(
sentryTraceHeader, baggage, randomSpanId, scopes.getOptions());

final @NotNull TransactionContext transactionContext =
TransactionContext.fromPropagationContext(propagationContext);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,10 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri
new SentryId(traceData.getTraceId()), spanId, null, null, null)
: TransactionContext.fromPropagationContext(
PropagationContext.fromHeaders(
traceData.getSentryTraceHeader(), traceData.getBaggage(), spanId));
traceData.getSentryTraceHeader(),
traceData.getBaggage(),
spanId,
scopes.getOptions()));
;
transactionContext.setName(transactionName);
transactionContext.setTransactionNameSource(transactionNameSource);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertSame
Expand Down Expand Up @@ -69,6 +70,26 @@ class OtelSentryPropagatorTest {
assertSame(scopeInContext, scopes)
}

@Test
fun `ignores incoming headers when strict continuation rejects org id`() {
Sentry.init { options ->
options.dsn = "https://key@o2.ingest.sentry.io/123"
options.isStrictTraceContinuation = true
}
val propagator = OtelSentryPropagator()
val carrier: Map<String, String> =
mapOf(
"sentry-trace" to "f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1",
"baggage" to "sentry-trace_id=f9118105af4a2d42b4124532cd1065ff,sentry-org_id=1",
)

val newContext = propagator.extract(Context.root(), carrier, MapGetter())

assertFalse(Span.fromContext(newContext).spanContext.isValid)
assertNull(newContext.get(SENTRY_TRACE_KEY))
assertNull(newContext.get(SENTRY_BAGGAGE_KEY))
}

@Test
fun `uses incoming headers`() {
val propagator = OtelSentryPropagator()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.sentry.opentelemetry

import io.opentelemetry.api.trace.Span
import io.opentelemetry.context.Context
import io.sentry.Sentry
import io.sentry.opentelemetry.SentryOtelKeys.SENTRY_BAGGAGE_KEY
import io.sentry.opentelemetry.SentryOtelKeys.SENTRY_TRACE_KEY
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertNull

class SentryPropagatorTest {

@BeforeTest
fun setup() {
Sentry.init("https://key@sentry.io/proj")
}

@Suppress("DEPRECATION")
@Test
fun `ignores incoming headers when strict continuation rejects org id`() {
Sentry.init { options ->
options.dsn = "https://key@o2.ingest.sentry.io/123"
options.isStrictTraceContinuation = true
}

val propagator = SentryPropagator()
val carrier: Map<String, String> =
mapOf(
"sentry-trace" to "f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1",
"baggage" to "sentry-trace_id=f9118105af4a2d42b4124532cd1065ff,sentry-org_id=1",
)

val newContext = propagator.extract(Context.root(), carrier, MapGetter())

assertFalse(Span.fromContext(newContext).spanContext.isValid)
assertNull(newContext.get(SENTRY_TRACE_KEY))
assertNull(newContext.get(SENTRY_BAGGAGE_KEY))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.sentry.SentryLevel;
import io.sentry.SentryTraceHeader;
import io.sentry.exception.InvalidSentryTraceHeaderException;
import io.sentry.util.TracingUtils;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
Expand Down Expand Up @@ -87,6 +88,16 @@ public <C> Context extract(
SentryTraceHeader sentryTraceHeader = new SentryTraceHeader(sentryTraceString);

final @Nullable String baggageString = getter.get(carrier, BaggageHeader.BAGGAGE_HEADER);
final @Nullable Baggage baggage =
baggageString == null ? null : Baggage.fromHeader(baggageString);
if (!TracingUtils.shouldContinueTrace(scopes.getOptions(), baggage)) {
scopes
.getOptions()
.getLogger()
.log(
SentryLevel.DEBUG, "Not continuing trace due to strict org ID validation failure.");
return context;
}
final @NotNull TraceState traceState = TraceState.getDefault();

final @NotNull TraceFlags traceFlags =
Expand All @@ -104,9 +115,8 @@ public <C> Context extract(
Span wrappedSpan = Span.wrap(otelSpanContext);

@NotNull Context modifiedContext = context.with(wrappedSpan);
if (baggageString != null) {
modifiedContext =
modifiedContext.with(SENTRY_BAGGAGE_KEY, Baggage.fromHeader(baggageString));
if (baggage != null) {
modifiedContext = modifiedContext.with(SENTRY_BAGGAGE_KEY, baggage);
}

scopes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ class OpenTelemetryOtlpPropagatorTest {
assertNull(baggage)
}

@Test
fun `ignores incoming headers when strict continuation rejects org id`() {
Sentry.init { options ->
options.dsn = "https://key@o2.ingest.sentry.io/123"
options.isStrictTraceContinuation = true
}
val propagator = OpenTelemetryOtlpPropagator()
val carrier: Map<String, String> =
mapOf(
"sentry-trace" to "f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1",
"baggage" to "sentry-trace_id=f9118105af4a2d42b4124532cd1065ff,sentry-org_id=1",
)

val newContext = propagator.extract(Context.root(), carrier, MapGetter())

assertFalse(Span.fromContext(newContext).spanContext.isValid)
assertNull(newContext.get(OpenTelemetryOtlpPropagator.SENTRY_BAGGAGE_KEY))
}

@Test
fun `uses incoming headers`() {
val propagator = OpenTelemetryOtlpPropagator()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ class SentryTracingFilterTest {
logger,
it.arguments[0] as String?,
it.arguments[1] as List<String>?,
null,
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ class SentryWebFluxTracingFilterTest {
logger,
it.arguments[0] as String?,
it.arguments[1] as List<String>?,
null,
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ class SentryAutoConfigurationTest {
"sentry.cron.default-failure-issue-threshold=40",
"sentry.cron.default-recovery-threshold=50",
"sentry.logs.enabled=true",
"sentry.strict-trace-continuation=true",
"sentry.org-id=12345",
)
.run {
val options = it.getBean(SentryProperties::class.java)
Expand Down Expand Up @@ -296,6 +298,8 @@ class SentryAutoConfigurationTest {
assertThat(options.cron!!.defaultFailureIssueThreshold).isEqualTo(40L)
assertThat(options.cron!!.defaultRecoveryThreshold).isEqualTo(50L)
assertThat(options.logs.isEnabled).isEqualTo(true)
assertThat(options.isStrictTraceContinuation).isEqualTo(true)
assertThat(options.orgId).isEqualTo("12345")
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ class SentryAutoConfigurationTest {
"sentry.profile-session-sample-rate=1.0",
"sentry.profiling-traces-dir-path=tmp/sentry/profiling-traces",
"sentry.profile-lifecycle=TRACE",
"sentry.strict-trace-continuation=true",
"sentry.org-id=12345",
)
.run {
val options = it.getBean(SentryProperties::class.java)
Expand Down Expand Up @@ -307,6 +309,8 @@ class SentryAutoConfigurationTest {
assertThat(options.profilingTracesDirPath)
.startsWith(File("tmp/sentry/profiling-traces").absolutePath)
assertThat(options.profileLifecycle).isEqualTo(ProfileLifecycle.TRACE)
assertThat(options.isStrictTraceContinuation).isEqualTo(true)
assertThat(options.orgId).isEqualTo("12345")
}
}

Expand Down
Loading