Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 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
1d8e301
fix(tracing): Fix empty orgId bypassing DSN fallback
adinauer Mar 26, 2026
25e71db
Merge branch 'main' into feat/strict-trace-continuation
adinauer Mar 26, 2026
a3c86e7
Format code
getsentry-bot Mar 26, 2026
3d1d119
ref: Remove redundant trim of already-trimmed effective org ID
adinauer Mar 27, 2026
bad469f
Merge branch 'main' into feat/strict-trace-continuation
adinauer Mar 30, 2026
cf7d9fe
ref(tracing): Revert shouldContinueTrace check in OtelSentrySpanProce…
adinauer Mar 30, 2026
435d2e7
fix(test): Clean up global Sentry state in SentryPropagatorTest
adinauer Mar 30, 2026
63222e5
fix(test): Use mock scopes in propagator strict continuation tests
adinauer Mar 30, 2026
a4cd81a
fix(test): Reset OTel context in propagator test teardown
adinauer Mar 30, 2026
f029100
Merge branch 'main' into feat/strict-trace-continuation
adinauer Mar 30, 2026
070ee32
fix(test): Add back test for inject with invalid span
adinauer Mar 30, 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
Prev Previous commit
Next Next commit
fix: Address review comments — pass options to PropagationContext, fi…
…x OTel overload, add option tests, make Dsn.orgId final

- Remove PropagationContext.fromHeaders overload without SentryOptions; all
  callers now pass options (or null) explicitly instead of relying on
  Sentry.getCurrentScopes()
- Add SentryOptions parameter to the OTel-facing fromHeaders(SentryTraceHeader,
  Baggage, SpanId) overload so OpenTelemetry integrations also check orgId
- Make Dsn.orgId final and remove the setter — orgId is only set during
  DSN parsing in the constructor
- Add tests for strictTraceContinuation and orgId options in
  ExternalOptionsTest, SentryOptionsTest, and ManifestMetadataReaderTest
- Improve CHANGELOG entry with customer-facing description
- Update API declarations (apiDump)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
  • Loading branch information
giortzisg and claude committed Mar 9, 2026
commit 519f6a664962fad0d461d534fe33d07954eebf11
9 changes: 4 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@

### Features

- Add strict trace continuation support ([#5136](https://github.com/getsentry/sentry-java/pull/5136))
- The SDK now extracts `org_id` from the DSN host and propagates it via `sentry-org_id` in the baggage header.
- When an incoming trace has a mismatched `org_id`, the SDK starts a new trace instead of continuing the foreign one.
- New option `strictTraceContinuation` (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.
- New option `orgId`: allows explicitly setting the organization ID for self-hosted and Relay setups where it cannot be extracted from the DSN.
- 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 `strictTraceContinuation` (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.
- 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, `sentry.properties` (`org-id`), or Android manifest (`io.sentry.org-id`).

## 8.34.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2386,4 +2386,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 @@ -91,7 +91,7 @@ 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,7 @@ 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()));
Comment thread
adinauer marked this conversation as resolved.
Outdated
;
transactionContext.setName(transactionName);
transactionContext.setTransactionNameSource(transactionNameSource);
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 @@ -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 @@ -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
5 changes: 2 additions & 3 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -2272,10 +2272,9 @@ public final class io/sentry/PropagationContext {
public fun <init> (Lio/sentry/PropagationContext;)V
public fun <init> (Lio/sentry/protocol/SentryId;Lio/sentry/SpanId;Lio/sentry/SpanId;Lio/sentry/Baggage;Ljava/lang/Boolean;)V
public static fun fromExistingTrace (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;Ljava/lang/Double;)Lio/sentry/PropagationContext;
public static fun fromHeaders (Lio/sentry/ILogger;Ljava/lang/String;Ljava/lang/String;)Lio/sentry/PropagationContext;
public static fun fromHeaders (Lio/sentry/ILogger;Ljava/lang/String;Ljava/util/List;)Lio/sentry/PropagationContext;
public static fun fromHeaders (Lio/sentry/ILogger;Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryOptions;)Lio/sentry/PropagationContext;
public static fun fromHeaders (Lio/sentry/ILogger;Ljava/lang/String;Ljava/util/List;Lio/sentry/SentryOptions;)Lio/sentry/PropagationContext;
public static fun fromHeaders (Lio/sentry/SentryTraceHeader;Lio/sentry/Baggage;Lio/sentry/SpanId;)Lio/sentry/PropagationContext;
public static fun fromHeaders (Lio/sentry/SentryTraceHeader;Lio/sentry/Baggage;Lio/sentry/SpanId;Lio/sentry/SentryOptions;)Lio/sentry/PropagationContext;
public fun getBaggage ()Lio/sentry/Baggage;
public fun getParentSpanId ()Lio/sentry/SpanId;
public fun getSampleRand ()Ljava/lang/Double;
Expand Down
5 changes: 1 addition & 4 deletions sentry/src/main/java/io/sentry/Dsn.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ final class Dsn {
private final @Nullable String secretKey;
private final @NotNull String publicKey;
private final @NotNull URI sentryUri;
private @Nullable String orgId;
private final @Nullable String orgId;

/*
/ The project ID which the authenticated user is bound to.
Expand Down Expand Up @@ -112,7 +112,4 @@ URI getSentryUri() {
return orgId;
}

public void setOrgId(final @Nullable String orgId) {
this.orgId = orgId;
}
}
27 changes: 10 additions & 17 deletions sentry/src/main/java/io/sentry/PropagationContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,9 @@ public final class PropagationContext {
public static PropagationContext fromHeaders(
final @NotNull ILogger logger,
final @Nullable String sentryTraceHeader,
final @Nullable String baggageHeader) {
return fromHeaders(logger, sentryTraceHeader, Arrays.asList(baggageHeader));
}

public static @NotNull PropagationContext fromHeaders(
final @NotNull ILogger logger,
final @Nullable String sentryTraceHeaderString,
final @Nullable List<String> baggageHeaderStrings) {
@Nullable SentryOptions options = null;
try {
options = Sentry.getCurrentScopes().getOptions();
} catch (Throwable ignored) {
// options may not be available if Sentry is not initialized
}
return fromHeaders(logger, sentryTraceHeaderString, baggageHeaderStrings, options);
final @Nullable String baggageHeader,
final @Nullable SentryOptions options) {
return fromHeaders(logger, sentryTraceHeader, Arrays.asList(baggageHeader), options);
}

public static @NotNull PropagationContext fromHeaders(
Expand All @@ -50,7 +38,7 @@ public static PropagationContext fromHeaders(
return new PropagationContext();
}

return fromHeaders(traceHeader, baggage, null);
return fromHeaders(traceHeader, baggage, null, null);
} catch (InvalidSentryTraceHeaderException e) {
logger.log(SentryLevel.DEBUG, e, "Failed to parse Sentry trace header: %s", e.getMessage());
return new PropagationContext();
Expand All @@ -60,7 +48,12 @@ public static PropagationContext fromHeaders(
public static @NotNull PropagationContext fromHeaders(
Comment thread
adinauer marked this conversation as resolved.
final @NotNull SentryTraceHeader sentryTraceHeader,
final @Nullable Baggage baggage,
final @Nullable SpanId spanId) {
final @Nullable SpanId spanId,
final @Nullable SentryOptions options) {
if (options != null && !shouldContinueTrace(options, baggage)) {
return new PropagationContext();
Comment thread
adinauer marked this conversation as resolved.
}
Comment thread
adinauer marked this conversation as resolved.

final @NotNull SpanId spanIdToUse = spanId == null ? new SpanId() : spanId;

return new PropagationContext(
Expand Down
7 changes: 0 additions & 7 deletions sentry/src/test/java/io/sentry/DsnTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,4 @@ class DsnTest {
assertNull(dsn.orgId)
}

@Test
fun `org id can be overridden via setter`() {
val dsn = Dsn("https://key@o123.ingest.sentry.io/456")
assertEquals("123", dsn.orgId)
dsn.setOrgId("999")
assertEquals("999", dsn.orgId)
}
}
31 changes: 31 additions & 0 deletions sentry/src/test/java/io/sentry/ExternalOptionsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,37 @@ class ExternalOptionsTest {
}
}

@Test
fun `creates options with strictTraceContinuation set to true`() {
withPropertiesFile("strict-trace-continuation=true") { options ->
assertTrue(options.isStrictTraceContinuation == true)
}
}

@Test
fun `creates options with strictTraceContinuation set to false`() {
withPropertiesFile("strict-trace-continuation=false") { options ->
assertTrue(options.isStrictTraceContinuation == false)
}
}

@Test
fun `creates options with strictTraceContinuation set to null when not set`() {
withPropertiesFile { assertNull(it.isStrictTraceContinuation) }
}

@Test
fun `creates options with orgId using external properties`() {
withPropertiesFile("org-id=12345") { options ->
assertEquals("12345", options.orgId)
}
}

@Test
fun `creates options with orgId set to null when not set`() {
withPropertiesFile { assertNull(it.orgId) }
}

private fun withPropertiesFile(
textLines: List<String> = emptyList(),
logger: ILogger = mock(),
Expand Down
3 changes: 3 additions & 0 deletions sentry/src/test/java/io/sentry/PropagationContextTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class PropagationContextTest {
NoOpLogger.getInstance(),
"2722d9f6ec019ade60c776169d9a8904-cedf5b7571cb4972-1",
"sentry-trace_id=a,sentry-transaction=sentryTransaction",
null,
)
assertFalse(propagationContext.baggage.isMutable)
assertTrue(propagationContext.baggage.isShouldFreeze)
Expand All @@ -27,6 +28,7 @@ class PropagationContextTest {
NoOpLogger.getInstance(),
"2722d9f6ec019ade60c776169d9a8904-cedf5b7571cb4972-1",
"a=b",
null,
)
assertTrue(propagationContext.baggage.isMutable)
assertFalse(propagationContext.baggage.isShouldFreeze)
Expand All @@ -39,6 +41,7 @@ class PropagationContextTest {
NoOpLogger.getInstance(),
"2722d9f6ec019ade60c776169d9a8904-cedf5b7571cb4972-1",
null as? String?,
null,
)
assertNotNull(propagationContext.baggage)
assertTrue(propagationContext.baggage.isMutable)
Expand Down
68 changes: 68 additions & 0 deletions sentry/src/test/java/io/sentry/SentryOptionsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -960,4 +960,72 @@ class SentryOptionsTest {
options.logs.loggerBatchProcessorFactory = mock
assertSame(mock, options.logs.loggerBatchProcessorFactory)
}

@Test
fun `when options is initialized, strictTraceContinuation is false`() {
assertFalse(SentryOptions().isStrictTraceContinuation)
}

@Test
fun `when options is initialized, orgId is null`() {
assertNull(SentryOptions().orgId)
}

@Test
fun `merging options applies strictTraceContinuation`() {
val externalOptions = ExternalOptions()
externalOptions.setStrictTraceContinuation(true)
val options = SentryOptions()
options.merge(externalOptions)
assertTrue(options.isStrictTraceContinuation)
}

@Test
fun `merging options when strictTraceContinuation is not set preserves the previous value`() {
val externalOptions = ExternalOptions()
val options = SentryOptions()
options.isStrictTraceContinuation = true
options.merge(externalOptions)
assertTrue(options.isStrictTraceContinuation)
}

@Test
fun `merging options applies orgId`() {
val externalOptions = ExternalOptions()
externalOptions.setOrgId("12345")
val options = SentryOptions()
options.merge(externalOptions)
assertEquals("12345", options.orgId)
}

@Test
fun `merging options when orgId is not set preserves the previous value`() {
val externalOptions = ExternalOptions()
val options = SentryOptions()
options.orgId = "original"
options.merge(externalOptions)
assertEquals("original", options.orgId)
}

@Test
fun `getEffectiveOrgId prefers explicit orgId over DSN`() {
val options = SentryOptions()
options.dsn = "https://key@o123.ingest.sentry.io/456"
options.orgId = "999"
assertEquals("999", options.effectiveOrgId)
}

@Test
fun `getEffectiveOrgId falls back to DSN org id`() {
val options = SentryOptions()
options.dsn = "https://key@o123.ingest.sentry.io/456"
assertEquals("123", options.effectiveOrgId)
}

@Test
fun `getEffectiveOrgId returns null when no orgId configured`() {
val options = SentryOptions()
options.dsn = "https://key@sentry.io/456"
assertNull(options.effectiveOrgId)
}
}
4 changes: 4 additions & 0 deletions sentry/src/test/java/io/sentry/TransactionContextTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class TransactionContextTest {
logger,
SentryTraceHeader(SentryId(), SpanId(), false).value,
"sentry-trace_id=a,sentry-transaction=sentryTransaction,sentry-sample_rate=0.3",
null,
)
val context = TransactionContext.fromPropagationContext(propagationContext)
assertNull(context.sampled)
Expand All @@ -48,6 +49,7 @@ class TransactionContextTest {
logger,
SentryTraceHeader(SentryId(), SpanId(), false).value,
"sentry-trace_id=a,sentry-transaction=sentryTransaction",
null,
)
val context = TransactionContext.fromPropagationContext(propagationContext)
assertNull(context.sampled)
Expand All @@ -65,6 +67,7 @@ class TransactionContextTest {
logger,
SentryTraceHeader(SentryId(), SpanId(), true).value,
"sentry-trace_id=a,sentry-transaction=sentryTransaction,sentry-sample_rate=0.3",
null,
)
val context = TransactionContext.fromPropagationContext(propagationContext)
assertNull(context.sampled)
Expand All @@ -82,6 +85,7 @@ class TransactionContextTest {
logger,
SentryTraceHeader(SentryId(), SpanId(), true).value,
"sentry-trace_id=a,sentry-transaction=sentryTransaction",
null,
)
val context = TransactionContext.fromPropagationContext(propagationContext)
assertNull(context.sampled)
Expand Down