From dc8d31969b658d75e4978f27737770c01a310af5 Mon Sep 17 00:00:00 2001 From: Giannis Gkiortzis <58184179+giortzisg@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:10:30 +0100 Subject: [PATCH 01/21] feat: Add strict trace continuation support Extract org ID from DSN host, add strictTraceContinuation and orgId options, propagate sentry-org_id in baggage, and validate incoming traces per the decision matrix. Closes #5128 --- sentry/src/main/java/io/sentry/Baggage.java | 17 +++- sentry/src/main/java/io/sentry/Dsn.java | 22 +++++ .../java/io/sentry/PropagationContext.java | 35 +++++++ sentry/src/main/java/io/sentry/Scopes.java | 2 +- .../main/java/io/sentry/SentryOptions.java | 46 +++++++++ sentry/src/test/java/io/sentry/DsnTest.kt | 32 ++++++ .../java/io/sentry/PropagationContextTest.kt | 97 +++++++++++++++++++ 7 files changed, 249 insertions(+), 2 deletions(-) diff --git a/sentry/src/main/java/io/sentry/Baggage.java b/sentry/src/main/java/io/sentry/Baggage.java index 5f610a02918..4645df3f3a4 100644 --- a/sentry/src/main/java/io/sentry/Baggage.java +++ b/sentry/src/main/java/io/sentry/Baggage.java @@ -186,6 +186,7 @@ public static Baggage fromEvent( baggage.setPublicKey(options.retrieveParsedDsn().getPublicKey()); baggage.setRelease(event.getRelease()); baggage.setEnvironment(event.getEnvironment()); + baggage.setOrgId(options.getEffectiveOrgId()); baggage.setTransaction(transaction); // we don't persist sample rate baggage.setSampleRate(null); @@ -450,6 +451,16 @@ public void setReplayId(final @Nullable String replayId) { set(DSCKeys.REPLAY_ID, replayId); } + @ApiStatus.Internal + public @Nullable String getOrgId() { + return get(DSCKeys.ORG_ID); + } + + @ApiStatus.Internal + public void setOrgId(final @Nullable String orgId) { + set(DSCKeys.ORG_ID, orgId); + } + /** * Sets / updates a value, but only if the baggage is still mutable. * @@ -501,6 +512,7 @@ public void setValuesFromTransaction( if (replayId != null && !SentryId.EMPTY_ID.equals(replayId)) { setReplayId(replayId.toString()); } + setOrgId(sentryOptions.getEffectiveOrgId()); setSampleRate(sampleRate(samplingDecision)); setSampled(StringUtils.toString(sampled(samplingDecision))); setSampleRand(sampleRand(samplingDecision)); @@ -536,6 +548,7 @@ public void setValuesFromScope( if (!SentryId.EMPTY_ID.equals(replayId)) { setReplayId(replayId.toString()); } + setOrgId(options.getEffectiveOrgId()); setTransaction(null); setSampleRate(null); setSampled(null); @@ -632,6 +645,7 @@ public static final class DSCKeys { public static final String SAMPLE_RAND = "sentry-sample_rand"; public static final String SAMPLED = "sentry-sampled"; public static final String REPLAY_ID = "sentry-replay_id"; + public static final String ORG_ID = "sentry-org_id"; public static final List ALL = Arrays.asList( @@ -644,6 +658,7 @@ public static final class DSCKeys { SAMPLE_RATE, SAMPLE_RAND, SAMPLED, - REPLAY_ID); + REPLAY_ID, + ORG_ID); } } diff --git a/sentry/src/main/java/io/sentry/Dsn.java b/sentry/src/main/java/io/sentry/Dsn.java index 705d383266e..e28e831848e 100644 --- a/sentry/src/main/java/io/sentry/Dsn.java +++ b/sentry/src/main/java/io/sentry/Dsn.java @@ -2,15 +2,20 @@ import io.sentry.util.Objects; import java.net.URI; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; final class Dsn { + private static final @NotNull Pattern ORG_ID_PATTERN = Pattern.compile("^o(\\d+)\\."); + private final @NotNull String projectId; private final @Nullable String path; private final @Nullable String secretKey; private final @NotNull String publicKey; private final @NotNull URI sentryUri; + private @Nullable String orgId; /* / The project ID which the authenticated user is bound to. @@ -87,8 +92,25 @@ URI getSentryUri() { sentryUri = new URI( scheme, null, uri.getHost(), uri.getPort(), path + "api/" + projectId, null, null); + + // Extract org ID from host (e.g., "o123.ingest.sentry.io" -> "123") + final String host = uri.getHost(); + if (host != null) { + final Matcher matcher = ORG_ID_PATTERN.matcher(host); + if (matcher.find()) { + orgId = matcher.group(1); + } + } } catch (Throwable e) { throw new IllegalArgumentException(e); } } + + public @Nullable String getOrgId() { + return orgId; + } + + void setOrgId(final @Nullable String orgId) { + this.orgId = orgId; + } } diff --git a/sentry/src/main/java/io/sentry/PropagationContext.java b/sentry/src/main/java/io/sentry/PropagationContext.java index e7d39d35fe5..772013ec567 100644 --- a/sentry/src/main/java/io/sentry/PropagationContext.java +++ b/sentry/src/main/java/io/sentry/PropagationContext.java @@ -23,6 +23,14 @@ public static PropagationContext fromHeaders( final @NotNull ILogger logger, final @Nullable String sentryTraceHeaderString, final @Nullable List baggageHeaderStrings) { + return fromHeaders(logger, sentryTraceHeaderString, baggageHeaderStrings, null); + } + + public static @NotNull PropagationContext fromHeaders( + final @NotNull ILogger logger, + final @Nullable String sentryTraceHeaderString, + final @Nullable List baggageHeaderStrings, + final @Nullable SentryOptions options) { if (sentryTraceHeaderString == null) { return new PropagationContext(); } @@ -30,6 +38,12 @@ public static PropagationContext fromHeaders( try { final @NotNull SentryTraceHeader traceHeader = new SentryTraceHeader(sentryTraceHeaderString); final @NotNull Baggage baggage = Baggage.fromHeader(baggageHeaderStrings, logger); + + if (options != null && !shouldContinueTrace(options, baggage)) { + logger.log(SentryLevel.DEBUG, "Not continuing trace due to org ID mismatch."); + return new PropagationContext(); + } + return fromHeaders(traceHeader, baggage, null); } catch (InvalidSentryTraceHeaderException e) { logger.log(SentryLevel.DEBUG, e, "Failed to parse Sentry trace header: %s", e.getMessage()); @@ -149,4 +163,25 @@ public void setSampled(final @Nullable Boolean sampled) { // should never be null since we ensure it in ctor return sampleRand == null ? 0.0 : sampleRand; } + + static boolean shouldContinueTrace( + final @NotNull SentryOptions options, final @Nullable Baggage baggage) { + final @Nullable String sdkOrgId = options.getEffectiveOrgId(); + final @Nullable String baggageOrgId = baggage != null ? baggage.getOrgId() : null; + + // Mismatched org IDs always reject regardless of strict mode + if (sdkOrgId != null && baggageOrgId != null && !sdkOrgId.equals(baggageOrgId)) { + return false; + } + + // In strict mode, both must be present and match (unless both are missing) + if (options.isStrictTraceContinuation()) { + if (sdkOrgId == null && baggageOrgId == null) { + return true; + } + return sdkOrgId != null && sdkOrgId.equals(baggageOrgId); + } + + return true; + } } diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index e155979e064..4fc1e61c271 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -1135,7 +1135,7 @@ public void reportFullyDisplayed() { final @Nullable String sentryTrace, final @Nullable List baggageHeaders) { @NotNull PropagationContext propagationContext = - PropagationContext.fromHeaders(getOptions().getLogger(), sentryTrace, baggageHeaders); + PropagationContext.fromHeaders(getOptions().getLogger(), sentryTrace, baggageHeaders, getOptions()); configureScope( (scope) -> { scope.withPropagationContext( diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 7883ed6b95b..9457a369713 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -432,6 +432,21 @@ public class SentryOptions { /** Whether to propagate W3C traceparent HTTP header. */ private boolean propagateTraceparent = false; + /** + * Controls whether the SDK requires matching org IDs from incoming baggage to continue a trace. + * When true, both the SDK's org ID and the incoming baggage org ID must be present and match. + * When false, a mismatch between present org IDs will still start a new trace, but missing org + * IDs on either side are tolerated. + */ + private boolean strictTraceContinuation = false; + + /** + * An optional organization ID. The SDK will try to extract it from the DSN in most cases but you + * can provide it explicitly for self-hosted and Relay setups. This value is used for trace + * propagation and for features like {@link #strictTraceContinuation}. + */ + private @Nullable String orgId; + /** Proguard UUID. */ private @Nullable String proguardUuid; @@ -2301,6 +2316,37 @@ public void setPropagateTraceparent(final boolean propagateTraceparent) { this.propagateTraceparent = propagateTraceparent; } + public boolean isStrictTraceContinuation() { + return strictTraceContinuation; + } + + public void setStrictTraceContinuation(final boolean strictTraceContinuation) { + this.strictTraceContinuation = strictTraceContinuation; + } + + public @Nullable String getOrgId() { + return orgId; + } + + public void setOrgId(final @Nullable String orgId) { + this.orgId = orgId; + } + + /** + * Returns the effective org ID, preferring the explicit config option over the DSN-parsed value. + */ + public @Nullable String getEffectiveOrgId() { + if (orgId != null) { + return orgId; + } + try { + final @Nullable String dsnOrgId = retrieveParsedDsn().getOrgId(); + return dsnOrgId; + } catch (Throwable e) { + return null; + } + } + /** * Returns a Proguard UUID. * diff --git a/sentry/src/test/java/io/sentry/DsnTest.kt b/sentry/src/test/java/io/sentry/DsnTest.kt index 6c454ad5c75..29f70229202 100644 --- a/sentry/src/test/java/io/sentry/DsnTest.kt +++ b/sentry/src/test/java/io/sentry/DsnTest.kt @@ -121,4 +121,36 @@ class DsnTest { Dsn("HTTP://publicKey:secretKey@host/path/id") Dsn("HTTPS://publicKey:secretKey@host/path/id") } + + @Test + fun `extracts org id from host`() { + val dsn = Dsn("https://key@o123.ingest.sentry.io/456") + assertEquals("123", dsn.orgId) + } + + @Test + fun `extracts single digit org id from host`() { + val dsn = Dsn("https://key@o1.ingest.us.sentry.io/456") + assertEquals("1", dsn.orgId) + } + + @Test + fun `returns null org id when host has no org prefix`() { + val dsn = Dsn("https://key@sentry.io/456") + assertNull(dsn.orgId) + } + + @Test + fun `returns null org id for non-standard host`() { + val dsn = Dsn("http://key@localhost:9000/456") + 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) + } } diff --git a/sentry/src/test/java/io/sentry/PropagationContextTest.kt b/sentry/src/test/java/io/sentry/PropagationContextTest.kt index 8e83dec4deb..3431a88f37a 100644 --- a/sentry/src/test/java/io/sentry/PropagationContextTest.kt +++ b/sentry/src/test/java/io/sentry/PropagationContextTest.kt @@ -1,7 +1,9 @@ package io.sentry import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -42,4 +44,99 @@ class PropagationContextTest { assertTrue(propagationContext.baggage.isMutable) assertFalse(propagationContext.baggage.isShouldFreeze) } + + // Decision matrix tests for shouldContinueTrace + + private val incomingTraceId = "bc6d53f15eb88f4320054569b8c553d4" + private val sentryTrace = "bc6d53f15eb88f4320054569b8c553d4-b72fa28504b07285-1" + + private fun makeOptions(dsnOrgId: String?, explicitOrgId: String? = null, strict: Boolean = false): SentryOptions { + val options = SentryOptions() + if (dsnOrgId != null) { + options.dsn = "https://key@o$dsnOrgId.ingest.sentry.io/123" + } else { + options.dsn = "https://key@sentry.io/123" + } + options.orgId = explicitOrgId + options.isStrictTraceContinuation = strict + return options + } + + private fun makeBaggage(orgId: String?): String { + val parts = mutableListOf("sentry-trace_id=$incomingTraceId") + if (orgId != null) { + parts.add("sentry-org_id=$orgId") + } + return parts.joinToString(",") + } + + @Test + fun `strict=false, matching orgs - continues trace`() { + val options = makeOptions(dsnOrgId = "1", strict = false) + val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage("1"), options) + assertEquals(incomingTraceId, pc.traceId.toString()) + } + + @Test + fun `strict=false, baggage missing org - continues trace`() { + val options = makeOptions(dsnOrgId = "1", strict = false) + val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage(null), options) + assertEquals(incomingTraceId, pc.traceId.toString()) + } + + @Test + fun `strict=false, sdk missing org - continues trace`() { + val options = makeOptions(dsnOrgId = null, strict = false) + val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage("1"), options) + assertEquals(incomingTraceId, pc.traceId.toString()) + } + + @Test + fun `strict=false, both missing org - continues trace`() { + val options = makeOptions(dsnOrgId = null, strict = false) + val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage(null), options) + assertEquals(incomingTraceId, pc.traceId.toString()) + } + + @Test + fun `strict=false, mismatched orgs - starts new trace`() { + val options = makeOptions(dsnOrgId = "2", strict = false) + val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage("1"), options) + assertNotEquals(incomingTraceId, pc.traceId.toString()) + } + + @Test + fun `strict=true, matching orgs - continues trace`() { + val options = makeOptions(dsnOrgId = "1", strict = true) + val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage("1"), options) + assertEquals(incomingTraceId, pc.traceId.toString()) + } + + @Test + fun `strict=true, baggage missing org - starts new trace`() { + val options = makeOptions(dsnOrgId = "1", strict = true) + val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage(null), options) + assertNotEquals(incomingTraceId, pc.traceId.toString()) + } + + @Test + fun `strict=true, sdk missing org - starts new trace`() { + val options = makeOptions(dsnOrgId = null, strict = true) + val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage("1"), options) + assertNotEquals(incomingTraceId, pc.traceId.toString()) + } + + @Test + fun `strict=true, both missing org - continues trace`() { + val options = makeOptions(dsnOrgId = null, strict = true) + val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage(null), options) + assertEquals(incomingTraceId, pc.traceId.toString()) + } + + @Test + fun `strict=true, mismatched orgs - starts new trace`() { + val options = makeOptions(dsnOrgId = "2", strict = true) + val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage("1"), options) + assertNotEquals(incomingTraceId, pc.traceId.toString()) + } } From e550ae3e13a1b70a3332b8f616dcab13fb136678 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 2 Mar 2026 15:17:51 +0000 Subject: [PATCH 02/21] Format code --- sentry/src/main/java/io/sentry/Scopes.java | 3 +- .../java/io/sentry/PropagationContextTest.kt | 86 ++++++++++++++++--- 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index 4fc1e61c271..82c03feac4b 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -1135,7 +1135,8 @@ public void reportFullyDisplayed() { final @Nullable String sentryTrace, final @Nullable List baggageHeaders) { @NotNull PropagationContext propagationContext = - PropagationContext.fromHeaders(getOptions().getLogger(), sentryTrace, baggageHeaders, getOptions()); + PropagationContext.fromHeaders( + getOptions().getLogger(), sentryTrace, baggageHeaders, getOptions()); configureScope( (scope) -> { scope.withPropagationContext( diff --git a/sentry/src/test/java/io/sentry/PropagationContextTest.kt b/sentry/src/test/java/io/sentry/PropagationContextTest.kt index 3431a88f37a..75068c55255 100644 --- a/sentry/src/test/java/io/sentry/PropagationContextTest.kt +++ b/sentry/src/test/java/io/sentry/PropagationContextTest.kt @@ -50,7 +50,11 @@ class PropagationContextTest { private val incomingTraceId = "bc6d53f15eb88f4320054569b8c553d4" private val sentryTrace = "bc6d53f15eb88f4320054569b8c553d4-b72fa28504b07285-1" - private fun makeOptions(dsnOrgId: String?, explicitOrgId: String? = null, strict: Boolean = false): SentryOptions { + private fun makeOptions( + dsnOrgId: String?, + explicitOrgId: String? = null, + strict: Boolean = false, + ): SentryOptions { val options = SentryOptions() if (dsnOrgId != null) { options.dsn = "https://key@o$dsnOrgId.ingest.sentry.io/123" @@ -73,70 +77,130 @@ class PropagationContextTest { @Test fun `strict=false, matching orgs - continues trace`() { val options = makeOptions(dsnOrgId = "1", strict = false) - val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage("1"), options) + val pc = + PropagationContext.fromHeaders( + NoOpLogger.getInstance(), + sentryTrace, + makeBaggage("1"), + options, + ) assertEquals(incomingTraceId, pc.traceId.toString()) } @Test fun `strict=false, baggage missing org - continues trace`() { val options = makeOptions(dsnOrgId = "1", strict = false) - val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage(null), options) + val pc = + PropagationContext.fromHeaders( + NoOpLogger.getInstance(), + sentryTrace, + makeBaggage(null), + options, + ) assertEquals(incomingTraceId, pc.traceId.toString()) } @Test fun `strict=false, sdk missing org - continues trace`() { val options = makeOptions(dsnOrgId = null, strict = false) - val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage("1"), options) + val pc = + PropagationContext.fromHeaders( + NoOpLogger.getInstance(), + sentryTrace, + makeBaggage("1"), + options, + ) assertEquals(incomingTraceId, pc.traceId.toString()) } @Test fun `strict=false, both missing org - continues trace`() { val options = makeOptions(dsnOrgId = null, strict = false) - val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage(null), options) + val pc = + PropagationContext.fromHeaders( + NoOpLogger.getInstance(), + sentryTrace, + makeBaggage(null), + options, + ) assertEquals(incomingTraceId, pc.traceId.toString()) } @Test fun `strict=false, mismatched orgs - starts new trace`() { val options = makeOptions(dsnOrgId = "2", strict = false) - val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage("1"), options) + val pc = + PropagationContext.fromHeaders( + NoOpLogger.getInstance(), + sentryTrace, + makeBaggage("1"), + options, + ) assertNotEquals(incomingTraceId, pc.traceId.toString()) } @Test fun `strict=true, matching orgs - continues trace`() { val options = makeOptions(dsnOrgId = "1", strict = true) - val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage("1"), options) + val pc = + PropagationContext.fromHeaders( + NoOpLogger.getInstance(), + sentryTrace, + makeBaggage("1"), + options, + ) assertEquals(incomingTraceId, pc.traceId.toString()) } @Test fun `strict=true, baggage missing org - starts new trace`() { val options = makeOptions(dsnOrgId = "1", strict = true) - val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage(null), options) + val pc = + PropagationContext.fromHeaders( + NoOpLogger.getInstance(), + sentryTrace, + makeBaggage(null), + options, + ) assertNotEquals(incomingTraceId, pc.traceId.toString()) } @Test fun `strict=true, sdk missing org - starts new trace`() { val options = makeOptions(dsnOrgId = null, strict = true) - val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage("1"), options) + val pc = + PropagationContext.fromHeaders( + NoOpLogger.getInstance(), + sentryTrace, + makeBaggage("1"), + options, + ) assertNotEquals(incomingTraceId, pc.traceId.toString()) } @Test fun `strict=true, both missing org - continues trace`() { val options = makeOptions(dsnOrgId = null, strict = true) - val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage(null), options) + val pc = + PropagationContext.fromHeaders( + NoOpLogger.getInstance(), + sentryTrace, + makeBaggage(null), + options, + ) assertEquals(incomingTraceId, pc.traceId.toString()) } @Test fun `strict=true, mismatched orgs - starts new trace`() { val options = makeOptions(dsnOrgId = "2", strict = true) - val pc = PropagationContext.fromHeaders(NoOpLogger.getInstance(), sentryTrace, makeBaggage("1"), options) + val pc = + PropagationContext.fromHeaders( + NoOpLogger.getInstance(), + sentryTrace, + makeBaggage("1"), + options, + ) assertNotEquals(incomingTraceId, pc.traceId.toString()) } } From c272ee4093b077ba12502299424084cbe07719e8 Mon Sep 17 00:00:00 2001 From: Giannis Gkiortzis <58184179+giortzisg@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:01:47 +0100 Subject: [PATCH 03/21] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6d0c4104dd..f18223b6926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - New APIs are `Sentry.setAttribute`, `Sentry.setAttributes`, `Sentry.removeAttribute` - Support collections and arrays in attribute type inference ([#5124](https://github.com/getsentry/sentry-java/pull/5124)) - Add support for `SENTRY_SAMPLE_RATE` environment variable / `sample-rate` property ([#5112](https://github.com/getsentry/sentry-java/pull/5112)) +- Add strict trace continuation support ([#5136](https://github.com/getsentry/sentry-java/pull/5136)) - Create `sentry-opentelemetry-otlp` and `sentry-opentelemetry-otlp-spring` modules for combining OpenTelemetry SDK OTLP export with Sentry SDK ([#5100](https://github.com/getsentry/sentry-java/pull/5100)) - OpenTelemetry is configured to send spans to Sentry directly using an OTLP endpoint. - Sentry only uses trace and span ID from OpenTelemetry (via `OpenTelemetryOtlpEventProcessor`) but will not send spans through OpenTelemetry nor use OpenTelemetry `Context` for `Scopes` propagation. From 1cfc2eaf4e3d10d3fc03a87022a2b1aba6e72c9a Mon Sep 17 00:00:00 2001 From: Giannis Gkiortzis <58184179+giortzisg@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:12:09 +0100 Subject: [PATCH 04/21] Update API surface file for strict trace continuation Add public API declarations for new org ID and strict trace continuation methods on Baggage, PropagationContext, and SentryOptions. Co-Authored-By: Claude Opus 4.6 --- sentry/api/sentry.api | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index a7bbb6c6cfa..55302361099 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -47,6 +47,7 @@ public final class io/sentry/Baggage { public static fun fromHeader (Ljava/util/List;ZLio/sentry/ILogger;)Lio/sentry/Baggage; public fun get (Ljava/lang/String;)Ljava/lang/String; public fun getEnvironment ()Ljava/lang/String; + public fun getOrgId ()Ljava/lang/String; public fun getPublicKey ()Ljava/lang/String; public fun getRelease ()Ljava/lang/String; public fun getReplayId ()Ljava/lang/String; @@ -62,6 +63,7 @@ public final class io/sentry/Baggage { public fun isShouldFreeze ()Z public fun set (Ljava/lang/String;Ljava/lang/String;)V public fun setEnvironment (Ljava/lang/String;)V + public fun setOrgId (Ljava/lang/String;)V public fun setPublicKey (Ljava/lang/String;)V public fun setRelease (Ljava/lang/String;)V public fun setReplayId (Ljava/lang/String;)V @@ -81,6 +83,7 @@ public final class io/sentry/Baggage { public final class io/sentry/Baggage$DSCKeys { public static final field ALL Ljava/util/List; public static final field ENVIRONMENT Ljava/lang/String; + public static final field ORG_ID Ljava/lang/String; public static final field PUBLIC_KEY Ljava/lang/String; public static final field RELEASE Ljava/lang/String; public static final field REPLAY_ID Ljava/lang/String; @@ -2267,6 +2270,7 @@ public final class io/sentry/PropagationContext { 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/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 fun getBaggage ()Lio/sentry/Baggage; public fun getParentSpanId ()Lio/sentry/SpanId; @@ -3569,6 +3573,7 @@ public class io/sentry/SentryOptions { public fun getDistribution ()Lio/sentry/SentryOptions$DistributionOptions; public fun getDistributionController ()Lio/sentry/IDistributionApi; public fun getDsn ()Ljava/lang/String; + public fun getEffectiveOrgId ()Ljava/lang/String; public fun getEnvelopeDiskCache ()Lio/sentry/cache/IEnvelopeCache; public fun getEnvelopeReader ()Lio/sentry/IEnvelopeReader; public fun getEnvironment ()Ljava/lang/String; @@ -3609,6 +3614,7 @@ public class io/sentry/SentryOptions { public fun getOnOversizedEvent ()Lio/sentry/SentryOptions$OnOversizedEventCallback; public fun getOpenTelemetryMode ()Lio/sentry/SentryOpenTelemetryMode; public fun getOptionsObservers ()Ljava/util/List; + public fun getOrgId ()Ljava/lang/String; public fun getOutboxPath ()Ljava/lang/String; public fun getPerformanceCollectors ()Ljava/util/List; public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle; @@ -3679,6 +3685,7 @@ public class io/sentry/SentryOptions { public fun isSendDefaultPii ()Z public fun isSendModules ()Z public fun isStartProfilerOnAppStart ()Z + public fun isStrictTraceContinuation ()Z public fun isTraceOptionsRequests ()Z public fun isTraceSampling ()Z public fun isTracingEnabled ()Z @@ -3762,6 +3769,7 @@ public class io/sentry/SentryOptions { public fun setOnDiscard (Lio/sentry/SentryOptions$OnDiscardCallback;)V public fun setOnOversizedEvent (Lio/sentry/SentryOptions$OnOversizedEventCallback;)V public fun setOpenTelemetryMode (Lio/sentry/SentryOpenTelemetryMode;)V + public fun setOrgId (Ljava/lang/String;)V public fun setPrintUncaughtStackTrace (Z)V public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V public fun setProfileSessionSampleRate (Ljava/lang/Double;)V @@ -3793,6 +3801,7 @@ public class io/sentry/SentryOptions { public fun setSpotlightConnectionUrl (Ljava/lang/String;)V public fun setSslSocketFactory (Ljavax/net/ssl/SSLSocketFactory;)V public fun setStartProfilerOnAppStart (Z)V + public fun setStrictTraceContinuation (Z)V public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setThreadChecker (Lio/sentry/util/thread/IThreadChecker;)V public fun setTraceOptionsRequests (Z)V From 108eb2d068c6afddccc5b20a2178267bfa664f31 Mon Sep 17 00:00:00 2001 From: Giannis Gkiortzis <58184179+giortzisg@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:11:51 +0100 Subject: [PATCH 05/21] Address review comments for strict trace continuation - Make Dsn.orgId final, remove unnecessary setter - Fix test signatures to use List for baggage headers - Add strictTraceContinuation and orgId to ExternalOptions and merge() - Add options to ManifestMetadataReader for Android manifest config - Use Sentry.getCurrentScopes().getOptions() in legacy fromHeaders overload - Improve CHANGELOG description with details about new options - Update API surface file for ExternalOptions changes Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 4 ++++ .../android/core/ManifestMetadataReader.java | 15 ++++++++++++ sentry/api/sentry.api | 4 ++++ sentry/src/main/java/io/sentry/Dsn.java | 10 ++++---- .../main/java/io/sentry/ExternalOptions.java | 23 +++++++++++++++++++ .../java/io/sentry/PropagationContext.java | 7 +++++- .../main/java/io/sentry/SentryOptions.java | 6 +++++ .../java/io/sentry/PropagationContextTest.kt | 20 ++++++++-------- 8 files changed, 72 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f18223b6926..716795b08cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ - Support collections and arrays in attribute type inference ([#5124](https://github.com/getsentry/sentry-java/pull/5124)) - Add support for `SENTRY_SAMPLE_RATE` environment variable / `sample-rate` property ([#5112](https://github.com/getsentry/sentry-java/pull/5112)) - 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. - Create `sentry-opentelemetry-otlp` and `sentry-opentelemetry-otlp-spring` modules for combining OpenTelemetry SDK OTLP export with Sentry SDK ([#5100](https://github.com/getsentry/sentry-java/pull/5100)) - OpenTelemetry is configured to send spans to Sentry directly using an OTLP endpoint. - Sentry only uses trace and span ID from OpenTelemetry (via `OpenTelemetryOtlpEventProcessor`) but will not send spans through OpenTelemetry nor use OpenTelemetry `Context` for `Scopes` propagation. diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 0fd217794e2..2f51f873c37 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -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"; + static final String ORG_ID = "io.sentry.org-id"; + static final String SPOTLIGHT_ENABLE = "io.sentry.spotlight.enable"; static final String SPOTLIGHT_CONNECTION_URL = "io.sentry.spotlight.url"; @@ -658,6 +661,18 @@ static void applyMetadata( feedbackOptions.setShowBranding( readBool(metadata, logger, FEEDBACK_SHOW_BRANDING, feedbackOptions.isShowBranding())); + 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())); diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 55302361099..3fa3d8ff870 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -504,6 +504,7 @@ public final class io/sentry/ExternalOptions { public fun getInAppExcludes ()Ljava/util/List; public fun getInAppIncludes ()Ljava/util/List; public fun getMaxRequestBodySize ()Lio/sentry/SentryOptions$RequestSize; + public fun getOrgId ()Ljava/lang/String; public fun getPrintUncaughtStackTrace ()Ljava/lang/Boolean; public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle; public fun getProfileSessionSampleRate ()Ljava/lang/Double; @@ -531,6 +532,7 @@ public final class io/sentry/ExternalOptions { public fun isGlobalHubMode ()Ljava/lang/Boolean; public fun isSendDefaultPii ()Ljava/lang/Boolean; public fun isSendModules ()Ljava/lang/Boolean; + public fun isStrictTraceContinuation ()Ljava/lang/Boolean; public fun setCaptureOpenTelemetryEvents (Ljava/lang/Boolean;)V public fun setCron (Lio/sentry/SentryOptions$Cron;)V public fun setDebug (Ljava/lang/Boolean;)V @@ -553,6 +555,7 @@ public final class io/sentry/ExternalOptions { public fun setIgnoredErrors (Ljava/util/List;)V public fun setIgnoredTransactions (Ljava/util/List;)V public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V + public fun setOrgId (Ljava/lang/String;)V public fun setPrintUncaughtStackTrace (Ljava/lang/Boolean;)V public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V public fun setProfileSessionSampleRate (Ljava/lang/Double;)V @@ -567,6 +570,7 @@ public final class io/sentry/ExternalOptions { public fun setSendModules (Ljava/lang/Boolean;)V public fun setServerName (Ljava/lang/String;)V public fun setSpotlightConnectionUrl (Ljava/lang/String;)V + public fun setStrictTraceContinuation (Ljava/lang/Boolean;)V public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTracesSampleRate (Ljava/lang/Double;)V } diff --git a/sentry/src/main/java/io/sentry/Dsn.java b/sentry/src/main/java/io/sentry/Dsn.java index e28e831848e..0d21499b5fc 100644 --- a/sentry/src/main/java/io/sentry/Dsn.java +++ b/sentry/src/main/java/io/sentry/Dsn.java @@ -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. @@ -94,13 +94,15 @@ URI getSentryUri() { scheme, null, uri.getHost(), uri.getPort(), path + "api/" + projectId, null, null); // Extract org ID from host (e.g., "o123.ingest.sentry.io" -> "123") + String extractedOrgId = null; final String host = uri.getHost(); if (host != null) { final Matcher matcher = ORG_ID_PATTERN.matcher(host); if (matcher.find()) { - orgId = matcher.group(1); + extractedOrgId = matcher.group(1); } } + orgId = extractedOrgId; } catch (Throwable e) { throw new IllegalArgumentException(e); } @@ -109,8 +111,4 @@ URI getSentryUri() { public @Nullable String getOrgId() { return orgId; } - - void setOrgId(final @Nullable String orgId) { - this.orgId = orgId; - } } diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index 9eaf26b202f..5473876aeaf 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -63,6 +63,9 @@ public final class ExternalOptions { private @Nullable String profilingTracesDirPath; private @Nullable ProfileLifecycle profileLifecycle; + private @Nullable Boolean strictTraceContinuation; + private @Nullable String orgId; + private @Nullable SentryOptions.Cron cron; @SuppressWarnings("unchecked") @@ -213,6 +216,10 @@ public final class ExternalOptions { options.setCron(cron); } + options.setStrictTraceContinuation( + propertiesProvider.getBooleanProperty("strict-trace-continuation")); + options.setOrgId(propertiesProvider.getProperty("org-id")); + options.setEnableSpotlight(propertiesProvider.getBooleanProperty("enable-spotlight")); options.setSpotlightConnectionUrl(propertiesProvider.getProperty("spotlight-connection-url")); options.setProfileSessionSampleRate( @@ -589,6 +596,22 @@ public void setProfilingTracesDirPath(@Nullable String profilingTracesDirPath) { this.profilingTracesDirPath = profilingTracesDirPath; } + public @Nullable Boolean isStrictTraceContinuation() { + return strictTraceContinuation; + } + + public void setStrictTraceContinuation(final @Nullable Boolean strictTraceContinuation) { + this.strictTraceContinuation = strictTraceContinuation; + } + + public @Nullable String getOrgId() { + return orgId; + } + + public void setOrgId(final @Nullable String orgId) { + this.orgId = orgId; + } + public @Nullable ProfileLifecycle getProfileLifecycle() { return profileLifecycle; } diff --git a/sentry/src/main/java/io/sentry/PropagationContext.java b/sentry/src/main/java/io/sentry/PropagationContext.java index 772013ec567..5accabd5b12 100644 --- a/sentry/src/main/java/io/sentry/PropagationContext.java +++ b/sentry/src/main/java/io/sentry/PropagationContext.java @@ -23,7 +23,12 @@ public static PropagationContext fromHeaders( final @NotNull ILogger logger, final @Nullable String sentryTraceHeaderString, final @Nullable List baggageHeaderStrings) { - return fromHeaders(logger, sentryTraceHeaderString, baggageHeaderStrings, null); + @Nullable SentryOptions options = null; + try { + options = Sentry.getCurrentScopes().getOptions(); + } catch (Throwable ignored) { + } + return fromHeaders(logger, sentryTraceHeaderString, baggageHeaderStrings, options); } public static @NotNull PropagationContext fromHeaders( diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 9457a369713..6ce07f3ff67 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -3571,6 +3571,12 @@ public void merge(final @NotNull ExternalOptions options) { if (options.getProfileLifecycle() != null) { setProfileLifecycle(options.getProfileLifecycle()); } + if (options.isStrictTraceContinuation() != null) { + setStrictTraceContinuation(options.isStrictTraceContinuation()); + } + if (options.getOrgId() != null) { + setOrgId(options.getOrgId()); + } } private @NotNull SdkVersion createSdkVersion() { diff --git a/sentry/src/test/java/io/sentry/PropagationContextTest.kt b/sentry/src/test/java/io/sentry/PropagationContextTest.kt index 75068c55255..b27c28b9da6 100644 --- a/sentry/src/test/java/io/sentry/PropagationContextTest.kt +++ b/sentry/src/test/java/io/sentry/PropagationContextTest.kt @@ -81,7 +81,7 @@ class PropagationContextTest { PropagationContext.fromHeaders( NoOpLogger.getInstance(), sentryTrace, - makeBaggage("1"), + listOf(makeBaggage("1")), options, ) assertEquals(incomingTraceId, pc.traceId.toString()) @@ -94,7 +94,7 @@ class PropagationContextTest { PropagationContext.fromHeaders( NoOpLogger.getInstance(), sentryTrace, - makeBaggage(null), + listOf(makeBaggage(null)), options, ) assertEquals(incomingTraceId, pc.traceId.toString()) @@ -107,7 +107,7 @@ class PropagationContextTest { PropagationContext.fromHeaders( NoOpLogger.getInstance(), sentryTrace, - makeBaggage("1"), + listOf(makeBaggage("1")), options, ) assertEquals(incomingTraceId, pc.traceId.toString()) @@ -120,7 +120,7 @@ class PropagationContextTest { PropagationContext.fromHeaders( NoOpLogger.getInstance(), sentryTrace, - makeBaggage(null), + listOf(makeBaggage(null)), options, ) assertEquals(incomingTraceId, pc.traceId.toString()) @@ -133,7 +133,7 @@ class PropagationContextTest { PropagationContext.fromHeaders( NoOpLogger.getInstance(), sentryTrace, - makeBaggage("1"), + listOf(makeBaggage("1")), options, ) assertNotEquals(incomingTraceId, pc.traceId.toString()) @@ -146,7 +146,7 @@ class PropagationContextTest { PropagationContext.fromHeaders( NoOpLogger.getInstance(), sentryTrace, - makeBaggage("1"), + listOf(makeBaggage("1")), options, ) assertEquals(incomingTraceId, pc.traceId.toString()) @@ -159,7 +159,7 @@ class PropagationContextTest { PropagationContext.fromHeaders( NoOpLogger.getInstance(), sentryTrace, - makeBaggage(null), + listOf(makeBaggage(null)), options, ) assertNotEquals(incomingTraceId, pc.traceId.toString()) @@ -172,7 +172,7 @@ class PropagationContextTest { PropagationContext.fromHeaders( NoOpLogger.getInstance(), sentryTrace, - makeBaggage("1"), + listOf(makeBaggage("1")), options, ) assertNotEquals(incomingTraceId, pc.traceId.toString()) @@ -185,7 +185,7 @@ class PropagationContextTest { PropagationContext.fromHeaders( NoOpLogger.getInstance(), sentryTrace, - makeBaggage(null), + listOf(makeBaggage(null)), options, ) assertEquals(incomingTraceId, pc.traceId.toString()) @@ -198,7 +198,7 @@ class PropagationContextTest { PropagationContext.fromHeaders( NoOpLogger.getInstance(), sentryTrace, - makeBaggage("1"), + listOf(makeBaggage("1")), options, ) assertNotEquals(incomingTraceId, pc.traceId.toString()) From e30064c93043b0a8bc9added2934675bbd849450 Mon Sep 17 00:00:00 2001 From: Giannis Gkiortzis <58184179+giortzisg@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:13:45 +0100 Subject: [PATCH 06/21] Fix compilation errors after rebase on main - Add comment to empty catch block in PropagationContext to satisfy -Werror - Add setOrgId setter to Dsn class (remove final modifier on orgId field) to support the existing test for org ID override Co-Authored-By: Claude Opus 4.6 --- sentry/src/main/java/io/sentry/Dsn.java | 6 +++++- sentry/src/main/java/io/sentry/PropagationContext.java | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/Dsn.java b/sentry/src/main/java/io/sentry/Dsn.java index 0d21499b5fc..8b039849c01 100644 --- a/sentry/src/main/java/io/sentry/Dsn.java +++ b/sentry/src/main/java/io/sentry/Dsn.java @@ -15,7 +15,7 @@ final class Dsn { private final @Nullable String secretKey; private final @NotNull String publicKey; private final @NotNull URI sentryUri; - private final @Nullable String orgId; + private @Nullable String orgId; /* / The project ID which the authenticated user is bound to. @@ -111,4 +111,8 @@ URI getSentryUri() { public @Nullable String getOrgId() { return orgId; } + + public void setOrgId(final @Nullable String orgId) { + this.orgId = orgId; + } } diff --git a/sentry/src/main/java/io/sentry/PropagationContext.java b/sentry/src/main/java/io/sentry/PropagationContext.java index 5accabd5b12..0e82c5f1c27 100644 --- a/sentry/src/main/java/io/sentry/PropagationContext.java +++ b/sentry/src/main/java/io/sentry/PropagationContext.java @@ -27,6 +27,7 @@ public static PropagationContext fromHeaders( try { options = Sentry.getCurrentScopes().getOptions(); } catch (Throwable ignored) { + // options may not be available if Sentry is not initialized } return fromHeaders(logger, sentryTraceHeaderString, baggageHeaderStrings, options); } From 45457351697a30abfe9489b414fbb07347e8a7f4 Mon Sep 17 00:00:00 2001 From: Giannis Gkiortzis <58184179+giortzisg@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:20:11 +0100 Subject: [PATCH 07/21] fix: Move changelog entry to Unreleased section --- CHANGELOG.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 716795b08cb..c9db1465cb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Unreleased + +### 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. + ## 8.34.0 ### Features @@ -9,11 +19,6 @@ - New APIs are `Sentry.setAttribute`, `Sentry.setAttributes`, `Sentry.removeAttribute` - Support collections and arrays in attribute type inference ([#5124](https://github.com/getsentry/sentry-java/pull/5124)) - Add support for `SENTRY_SAMPLE_RATE` environment variable / `sample-rate` property ([#5112](https://github.com/getsentry/sentry-java/pull/5112)) -- 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. - Create `sentry-opentelemetry-otlp` and `sentry-opentelemetry-otlp-spring` modules for combining OpenTelemetry SDK OTLP export with Sentry SDK ([#5100](https://github.com/getsentry/sentry-java/pull/5100)) - OpenTelemetry is configured to send spans to Sentry directly using an OTLP endpoint. - Sentry only uses trace and span ID from OpenTelemetry (via `OpenTelemetryOtlpEventProcessor`) but will not send spans through OpenTelemetry nor use OpenTelemetry `Context` for `Scopes` propagation. From 61ada6a18d6576c81723616f3b05c13a64ea5e4a Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Wed, 4 Mar 2026 13:20:36 +0000 Subject: [PATCH 08/21] Format code --- .../java/io/sentry/android/core/ManifestMetadataReader.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 2f51f873c37..163571f4c99 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -663,10 +663,7 @@ static void applyMetadata( options.setStrictTraceContinuation( readBool( - metadata, - logger, - STRICT_TRACE_CONTINUATION, - options.isStrictTraceContinuation())); + metadata, logger, STRICT_TRACE_CONTINUATION, options.isStrictTraceContinuation())); final @Nullable String orgId = readString(metadata, logger, ORG_ID, null); if (orgId != null) { From 519f6a664962fad0d461d534fe33d07954eebf11 Mon Sep 17 00:00:00 2001 From: Giannis Gkiortzis <58184179+giortzisg@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:26:37 +0100 Subject: [PATCH 09/21] =?UTF-8?q?fix:=20Address=20review=20comments=20?= =?UTF-8?q?=E2=80=94=20pass=20options=20to=20PropagationContext,=20fix=20O?= =?UTF-8?q?Tel=20overload,=20add=20option=20tests,=20make=20Dsn.orgId=20fi?= =?UTF-8?q?nal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CHANGELOG.md | 9 ++- .../core/ManifestMetadataReaderTest.kt | 50 ++++++++++++++ .../sentry/opentelemetry/SentrySampler.java | 2 +- .../opentelemetry/SentrySpanProcessor.java | 2 +- .../tracing/SentryTracingFilterTest.kt | 1 + .../webflux/SentryWebFluxTracingFilterTest.kt | 1 + .../tracing/SentryTracingFilterTest.kt | 1 + .../webflux/SentryWebFluxTracingFilterTest.kt | 1 + .../spring/tracing/SentryTracingFilterTest.kt | 1 + .../webflux/SentryWebFluxTracingFilterTest.kt | 1 + sentry/api/sentry.api | 5 +- sentry/src/main/java/io/sentry/Dsn.java | 5 +- .../java/io/sentry/PropagationContext.java | 27 +++----- sentry/src/test/java/io/sentry/DsnTest.kt | 7 -- .../java/io/sentry/ExternalOptionsTest.kt | 31 +++++++++ .../java/io/sentry/PropagationContextTest.kt | 3 + .../test/java/io/sentry/SentryOptionsTest.kt | 68 +++++++++++++++++++ .../java/io/sentry/TransactionContextTest.kt | 4 ++ 18 files changed, 181 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9db1465cb7..15425cccf91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index fd5c9cffc89..a67b945ec99 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -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) + } } diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java index 5493ba033cb..53a8624f286 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java @@ -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); diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanProcessor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanProcessor.java index 2b650ef9dd2..9588c3d4d23 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanProcessor.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanProcessor.java @@ -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())); ; transactionContext.setName(transactionName); transactionContext.setTransactionNameSource(transactionNameSource); diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentryTracingFilterTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentryTracingFilterTest.kt index f63b5d9631c..9778a6d0154 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentryTracingFilterTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentryTracingFilterTest.kt @@ -96,6 +96,7 @@ class SentryTracingFilterTest { logger, it.arguments[0] as String?, it.arguments[1] as List?, + null, ) ) } diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryWebFluxTracingFilterTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryWebFluxTracingFilterTest.kt index 495f45ac650..bb14538d921 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryWebFluxTracingFilterTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryWebFluxTracingFilterTest.kt @@ -98,6 +98,7 @@ class SentryWebFluxTracingFilterTest { logger, it.arguments[0] as String?, it.arguments[1] as List?, + null, ) ) } diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTracingFilterTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTracingFilterTest.kt index dfb8376286a..ffdd2b9ad75 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTracingFilterTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTracingFilterTest.kt @@ -96,6 +96,7 @@ class SentryTracingFilterTest { logger, it.arguments[0] as String?, it.arguments[1] as List?, + null, ) ) } diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt index b14f1b5910c..f0b8d62e025 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt @@ -98,6 +98,7 @@ class SentryWebFluxTracingFilterTest { logger, it.arguments[0] as String?, it.arguments[1] as List?, + null, ) ) } diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentryTracingFilterTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentryTracingFilterTest.kt index f12517ee19e..f942da342c5 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentryTracingFilterTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentryTracingFilterTest.kt @@ -96,6 +96,7 @@ class SentryTracingFilterTest { logger, it.arguments[0] as String?, it.arguments[1] as List?, + null, ) ) } diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebFluxTracingFilterTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebFluxTracingFilterTest.kt index b27c0856106..5d91ec58486 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebFluxTracingFilterTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebFluxTracingFilterTest.kt @@ -98,6 +98,7 @@ class SentryWebFluxTracingFilterTest { logger, it.arguments[0] as String?, it.arguments[1] as List?, + null, ) ) } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 3fa3d8ff870..023f2410e81 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2272,10 +2272,9 @@ public final class io/sentry/PropagationContext { public fun (Lio/sentry/PropagationContext;)V public fun (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; diff --git a/sentry/src/main/java/io/sentry/Dsn.java b/sentry/src/main/java/io/sentry/Dsn.java index 8b039849c01..1b114071a08 100644 --- a/sentry/src/main/java/io/sentry/Dsn.java +++ b/sentry/src/main/java/io/sentry/Dsn.java @@ -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. @@ -112,7 +112,4 @@ URI getSentryUri() { return orgId; } - public void setOrgId(final @Nullable String orgId) { - this.orgId = orgId; - } } diff --git a/sentry/src/main/java/io/sentry/PropagationContext.java b/sentry/src/main/java/io/sentry/PropagationContext.java index 0e82c5f1c27..da45620b88e 100644 --- a/sentry/src/main/java/io/sentry/PropagationContext.java +++ b/sentry/src/main/java/io/sentry/PropagationContext.java @@ -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 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( @@ -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(); @@ -60,7 +48,12 @@ public static PropagationContext fromHeaders( public static @NotNull PropagationContext fromHeaders( 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(); + } + final @NotNull SpanId spanIdToUse = spanId == null ? new SpanId() : spanId; return new PropagationContext( diff --git a/sentry/src/test/java/io/sentry/DsnTest.kt b/sentry/src/test/java/io/sentry/DsnTest.kt index 29f70229202..548ecfd0c49 100644 --- a/sentry/src/test/java/io/sentry/DsnTest.kt +++ b/sentry/src/test/java/io/sentry/DsnTest.kt @@ -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) - } } diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index 5a8bb1c7872..d77833b0c0c 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -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 = emptyList(), logger: ILogger = mock(), diff --git a/sentry/src/test/java/io/sentry/PropagationContextTest.kt b/sentry/src/test/java/io/sentry/PropagationContextTest.kt index b27c28b9da6..5e38846519d 100644 --- a/sentry/src/test/java/io/sentry/PropagationContextTest.kt +++ b/sentry/src/test/java/io/sentry/PropagationContextTest.kt @@ -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) @@ -27,6 +28,7 @@ class PropagationContextTest { NoOpLogger.getInstance(), "2722d9f6ec019ade60c776169d9a8904-cedf5b7571cb4972-1", "a=b", + null, ) assertTrue(propagationContext.baggage.isMutable) assertFalse(propagationContext.baggage.isShouldFreeze) @@ -39,6 +41,7 @@ class PropagationContextTest { NoOpLogger.getInstance(), "2722d9f6ec019ade60c776169d9a8904-cedf5b7571cb4972-1", null as? String?, + null, ) assertNotNull(propagationContext.baggage) assertTrue(propagationContext.baggage.isMutable) diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 2f5b3579cb3..fbdf531530f 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -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) + } } diff --git a/sentry/src/test/java/io/sentry/TransactionContextTest.kt b/sentry/src/test/java/io/sentry/TransactionContextTest.kt index a27a600e96c..55603853a66 100644 --- a/sentry/src/test/java/io/sentry/TransactionContextTest.kt +++ b/sentry/src/test/java/io/sentry/TransactionContextTest.kt @@ -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) @@ -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) @@ -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) @@ -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) From 360fc94aea66a0ac9013de5b6ee1bf5e9e45e8d9 Mon Sep 17 00:00:00 2001 From: Giannis Gkiortzis <58184179+giortzisg@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:29:42 +0100 Subject: [PATCH 10/21] fix: Add missing 8.34.1 changelog section Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15425cccf91..113c7bd95f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,15 @@ - 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.1 + +### Fixes + +- Common: Finalize previous session even when auto session tracking is disabled ([#5154](https://github.com/getsentry/sentry-java/pull/5154)) +- Android: Add `filterTouchesWhenObscured` to prevent Tapjacking on user feedback dialog ([#5155](https://github.com/getsentry/sentry-java/pull/5155)) +- Android: Add proguard rules to prevent error about missing Replay classes ([#5153](https://github.com/getsentry/sentry-java/pull/5153)) +- Android: Remove the dependency on protobuf-lite for tombstones ([#5157](https://github.com/getsentry/sentry-java/pull/5157)) + ## 8.34.0 ### Features From 58bfc8e10ef7a3284675f4be7a343eb98e1294ec Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 9 Mar 2026 13:36:13 +0000 Subject: [PATCH 11/21] Format code --- .../src/main/java/io/sentry/opentelemetry/SentrySampler.java | 3 ++- .../java/io/sentry/opentelemetry/SentrySpanProcessor.java | 5 ++++- sentry/src/main/java/io/sentry/Dsn.java | 1 - sentry/src/test/java/io/sentry/DsnTest.kt | 1 - sentry/src/test/java/io/sentry/ExternalOptionsTest.kt | 4 +--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java index 53a8624f286..1a9e8724ca6 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java @@ -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, scopes.getOptions()); + : PropagationContext.fromHeaders( + sentryTraceHeader, baggage, randomSpanId, scopes.getOptions()); final @NotNull TransactionContext transactionContext = TransactionContext.fromPropagationContext(propagationContext); diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanProcessor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanProcessor.java index 9588c3d4d23..9c6a51f17c3 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanProcessor.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanProcessor.java @@ -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, scopes.getOptions())); + traceData.getSentryTraceHeader(), + traceData.getBaggage(), + spanId, + scopes.getOptions())); ; transactionContext.setName(transactionName); transactionContext.setTransactionNameSource(transactionNameSource); diff --git a/sentry/src/main/java/io/sentry/Dsn.java b/sentry/src/main/java/io/sentry/Dsn.java index 1b114071a08..0d21499b5fc 100644 --- a/sentry/src/main/java/io/sentry/Dsn.java +++ b/sentry/src/main/java/io/sentry/Dsn.java @@ -111,5 +111,4 @@ URI getSentryUri() { public @Nullable String getOrgId() { return orgId; } - } diff --git a/sentry/src/test/java/io/sentry/DsnTest.kt b/sentry/src/test/java/io/sentry/DsnTest.kt index 548ecfd0c49..7e2982073f1 100644 --- a/sentry/src/test/java/io/sentry/DsnTest.kt +++ b/sentry/src/test/java/io/sentry/DsnTest.kt @@ -145,5 +145,4 @@ class DsnTest { val dsn = Dsn("http://key@localhost:9000/456") assertNull(dsn.orgId) } - } diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index 61df5abb847..8818f7daeda 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -470,9 +470,7 @@ class ExternalOptionsTest { @Test fun `creates options with orgId using external properties`() { - withPropertiesFile("org-id=12345") { options -> - assertEquals("12345", options.orgId) - } + withPropertiesFile("org-id=12345") { options -> assertEquals("12345", options.orgId) } } @Test From ebe8c4cd77a9d0c7bffc6c30a2a1d57579e30341 Mon Sep 17 00:00:00 2001 From: Giannis Gkiortzis <58184179+giortzisg@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:52:38 +0100 Subject: [PATCH 12/21] fix: Address PR review comments for strict trace continuation - Remove duplicated org ID check in PropagationContext.fromHeaders, pass options through to the single-check overload instead - Add debug log when trace is not continued in the SentryTraceHeader overload - Handle empty/blank org ID strings in shouldContinueTrace to avoid silently breaking traces - Update OtelSentrySpanProcessor to use PropagationContext.fromHeaders with options for org_id validation - Rename ExternalOptions property key to enable-strict-trace-continuation (matching the enable- prefix convention for newer options) - Update ExternalOptionsTest to use the new property key - Add strict-trace-continuation and org-id properties to all 3 Spring Boot SentryAutoConfigurationTest modules - Improve CHANGELOG entry with detailed customer-facing descriptions and configuration examples for all options Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 4 ++-- .../OtelSentrySpanProcessor.java | 14 ++++++++++--- .../boot4/SentryAutoConfigurationTest.kt | 4 ++++ .../jakarta/SentryAutoConfigurationTest.kt | 4 ++++ .../boot/SentryAutoConfigurationTest.kt | 4 ++++ .../main/java/io/sentry/ExternalOptions.java | 2 +- .../java/io/sentry/PropagationContext.java | 20 +++++++++++-------- .../java/io/sentry/ExternalOptionsTest.kt | 4 ++-- 8 files changed, 40 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e017f34014..bf0dc0065f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,8 @@ - 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`). + - 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`), 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`). ## 8.34.1 diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java index 1cf6fa5d833..bf577d7f5e1 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java @@ -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; @@ -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); diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt index a5566ef2f30..25a2bdd4b29 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt @@ -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) @@ -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") } } diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt index f37122812b1..91677d16b4e 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt @@ -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) @@ -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") } } diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt index 4ce0bf61208..d9e598d0473 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt @@ -247,6 +247,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) @@ -305,6 +307,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") } } diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index 33884015236..d0236bec8de 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -222,7 +222,7 @@ public final class ExternalOptions { } options.setStrictTraceContinuation( - propertiesProvider.getBooleanProperty("strict-trace-continuation")); + propertiesProvider.getBooleanProperty("enable-strict-trace-continuation")); options.setOrgId(propertiesProvider.getProperty("org-id")); options.setEnableSpotlight(propertiesProvider.getBooleanProperty("enable-spotlight")); diff --git a/sentry/src/main/java/io/sentry/PropagationContext.java b/sentry/src/main/java/io/sentry/PropagationContext.java index da45620b88e..50d1446c58a 100644 --- a/sentry/src/main/java/io/sentry/PropagationContext.java +++ b/sentry/src/main/java/io/sentry/PropagationContext.java @@ -33,12 +33,7 @@ public static PropagationContext fromHeaders( final @NotNull SentryTraceHeader traceHeader = new SentryTraceHeader(sentryTraceHeaderString); final @NotNull Baggage baggage = Baggage.fromHeader(baggageHeaderStrings, logger); - if (options != null && !shouldContinueTrace(options, baggage)) { - logger.log(SentryLevel.DEBUG, "Not continuing trace due to org ID mismatch."); - return new PropagationContext(); - } - - return fromHeaders(traceHeader, baggage, null, null); + return fromHeaders(traceHeader, baggage, null, options); } catch (InvalidSentryTraceHeaderException e) { logger.log(SentryLevel.DEBUG, e, "Failed to parse Sentry trace header: %s", e.getMessage()); return new PropagationContext(); @@ -51,6 +46,9 @@ public static PropagationContext fromHeaders( final @Nullable SpanId spanId, final @Nullable SentryOptions options) { if (options != null && !shouldContinueTrace(options, baggage)) { + options + .getLogger() + .log(SentryLevel.DEBUG, "Not continuing trace due to org ID mismatch."); return new PropagationContext(); } @@ -165,8 +163,14 @@ public void setSampled(final @Nullable Boolean sampled) { static boolean shouldContinueTrace( final @NotNull SentryOptions options, final @Nullable Baggage baggage) { - final @Nullable String sdkOrgId = options.getEffectiveOrgId(); - final @Nullable String baggageOrgId = baggage != null ? baggage.getOrgId() : null; + final @Nullable String rawSdkOrgId = options.getEffectiveOrgId(); + final @Nullable String sdkOrgId = + (rawSdkOrgId != null && !rawSdkOrgId.trim().isEmpty()) ? rawSdkOrgId.trim() : null; + final @Nullable String rawBaggageOrgId = baggage != null ? baggage.getOrgId() : null; + final @Nullable String baggageOrgId = + (rawBaggageOrgId != null && !rawBaggageOrgId.trim().isEmpty()) + ? rawBaggageOrgId.trim() + : null; // Mismatched org IDs always reject regardless of strict mode if (sdkOrgId != null && baggageOrgId != null && !sdkOrgId.equals(baggageOrgId)) { diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index 8818f7daeda..fed88dd384e 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -451,14 +451,14 @@ class ExternalOptionsTest { @Test fun `creates options with strictTraceContinuation set to true`() { - withPropertiesFile("strict-trace-continuation=true") { options -> + withPropertiesFile("enable-strict-trace-continuation=true") { options -> assertTrue(options.isStrictTraceContinuation == true) } } @Test fun `creates options with strictTraceContinuation set to false`() { - withPropertiesFile("strict-trace-continuation=false") { options -> + withPropertiesFile("enable-strict-trace-continuation=false") { options -> assertTrue(options.isStrictTraceContinuation == false) } } From 824b30bdfd434e13797b4cb1d15a5d029dca5e44 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Wed, 11 Mar 2026 10:56:51 +0000 Subject: [PATCH 13/21] Format code --- sentry/src/main/java/io/sentry/PropagationContext.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sentry/src/main/java/io/sentry/PropagationContext.java b/sentry/src/main/java/io/sentry/PropagationContext.java index 50d1446c58a..af3de8e6c5c 100644 --- a/sentry/src/main/java/io/sentry/PropagationContext.java +++ b/sentry/src/main/java/io/sentry/PropagationContext.java @@ -46,9 +46,7 @@ public static PropagationContext fromHeaders( final @Nullable SpanId spanId, final @Nullable SentryOptions options) { if (options != null && !shouldContinueTrace(options, baggage)) { - options - .getLogger() - .log(SentryLevel.DEBUG, "Not continuing trace due to org ID mismatch."); + options.getLogger().log(SentryLevel.DEBUG, "Not continuing trace due to org ID mismatch."); return new PropagationContext(); } From e2fdc461cd8621ad07cc78aeaf0acc922634d9a8 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 20 Mar 2026 12:06:45 +0100 Subject: [PATCH 14/21] fix(tracing): Clarify strict org validation debug log Update the trace-continuation rejection log message to cover all strict org ID validation failures, including missing org IDs, not just mismatches. Co-Authored-By: Claude --- sentry/src/main/java/io/sentry/PropagationContext.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/PropagationContext.java b/sentry/src/main/java/io/sentry/PropagationContext.java index af3de8e6c5c..9fdb6ad9a3e 100644 --- a/sentry/src/main/java/io/sentry/PropagationContext.java +++ b/sentry/src/main/java/io/sentry/PropagationContext.java @@ -46,7 +46,11 @@ public static PropagationContext fromHeaders( final @Nullable SpanId spanId, final @Nullable SentryOptions options) { if (options != null && !shouldContinueTrace(options, baggage)) { - options.getLogger().log(SentryLevel.DEBUG, "Not continuing trace due to org ID mismatch."); + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Not continuing trace due to strict org ID validation failure."); return new PropagationContext(); } From d3b073dee9b1f9bbf5d8b1c3d1cecf2d088ba564 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 20 Mar 2026 12:12:58 +0100 Subject: [PATCH 15/21] fix(android): Use enabled suffix for strict trace manifest key Rename the Android manifest option to io.sentry.strict-trace-continuation.enabled to align with existing enabled-style manifest flags. Update changelog documentation to match the new Android key. Co-Authored-By: Claude --- CHANGELOG.md | 2 +- .../java/io/sentry/android/core/ManifestMetadataReader.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf0dc0065f2..f6658a4f4f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - 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`), or Spring Boot (`sentry.strict-trace-continuation=true`). + - 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`). ## 8.34.1 diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 21a89c06166..1d8712f9316 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -167,7 +167,7 @@ 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"; + static final String STRICT_TRACE_CONTINUATION = "io.sentry.strict-trace-continuation.enabled"; static final String ORG_ID = "io.sentry.org-id"; static final String SPOTLIGHT_ENABLE = "io.sentry.spotlight.enable"; From f7fef224c2e140e991bd3088ef7537673d47eff5 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 20 Mar 2026 12:16:03 +0100 Subject: [PATCH 16/21] fix(api): Mark effective org ID helper as internal Annotate SentryOptions.getEffectiveOrgId with ApiStatus.Internal since it is used as an internal helper for trace propagation org ID resolution. Co-Authored-By: Claude --- sentry/src/main/java/io/sentry/SentryOptions.java | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 7ae93ca4e4d..937bdc1fdcf 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -2335,6 +2335,7 @@ public void setOrgId(final @Nullable String orgId) { /** * Returns the effective org ID, preferring the explicit config option over the DSN-parsed value. */ + @ApiStatus.Internal public @Nullable String getEffectiveOrgId() { if (orgId != null) { return orgId; From 71562fa460e34394c1deb26ec29efa362e56bdd2 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 23 Mar 2026 06:27:54 +0100 Subject: [PATCH 17/21] ref(tracing): Extract trace continuation decision into TracingUtils Move strict trace continuation org-id validation logic to TracingUtils so it can be reused by tracing entry points. Update PropagationContext to call the shared helper and add dedicated TracingUtils tests for strict/non-strict org-id continuation outcomes. Co-Authored-By: Claude --- .../java/io/sentry/PropagationContext.java | 28 +--------- .../java/io/sentry/util/TracingUtils.java | 28 ++++++++++ .../java/io/sentry/util/TracingUtilsTest.kt | 56 +++++++++++++++++++ 3 files changed, 85 insertions(+), 27 deletions(-) diff --git a/sentry/src/main/java/io/sentry/PropagationContext.java b/sentry/src/main/java/io/sentry/PropagationContext.java index 9fdb6ad9a3e..618a76b2c87 100644 --- a/sentry/src/main/java/io/sentry/PropagationContext.java +++ b/sentry/src/main/java/io/sentry/PropagationContext.java @@ -45,7 +45,7 @@ public static PropagationContext fromHeaders( final @Nullable Baggage baggage, final @Nullable SpanId spanId, final @Nullable SentryOptions options) { - if (options != null && !shouldContinueTrace(options, baggage)) { + if (options != null && !TracingUtils.shouldContinueTrace(options, baggage)) { options .getLogger() .log( @@ -163,30 +163,4 @@ public void setSampled(final @Nullable Boolean sampled) { return sampleRand == null ? 0.0 : sampleRand; } - static boolean shouldContinueTrace( - final @NotNull SentryOptions options, final @Nullable Baggage baggage) { - final @Nullable String rawSdkOrgId = options.getEffectiveOrgId(); - final @Nullable String sdkOrgId = - (rawSdkOrgId != null && !rawSdkOrgId.trim().isEmpty()) ? rawSdkOrgId.trim() : null; - final @Nullable String rawBaggageOrgId = baggage != null ? baggage.getOrgId() : null; - final @Nullable String baggageOrgId = - (rawBaggageOrgId != null && !rawBaggageOrgId.trim().isEmpty()) - ? rawBaggageOrgId.trim() - : null; - - // Mismatched org IDs always reject regardless of strict mode - if (sdkOrgId != null && baggageOrgId != null && !sdkOrgId.equals(baggageOrgId)) { - return false; - } - - // In strict mode, both must be present and match (unless both are missing) - if (options.isStrictTraceContinuation()) { - if (sdkOrgId == null && baggageOrgId == null) { - return true; - } - return sdkOrgId != null && sdkOrgId.equals(baggageOrgId); - } - - return true; - } } diff --git a/sentry/src/main/java/io/sentry/util/TracingUtils.java b/sentry/src/main/java/io/sentry/util/TracingUtils.java index 673980e7359..7f81f6fda03 100644 --- a/sentry/src/main/java/io/sentry/util/TracingUtils.java +++ b/sentry/src/main/java/io/sentry/util/TracingUtils.java @@ -196,6 +196,34 @@ public static boolean isIgnored( return false; } + @ApiStatus.Internal + public static boolean shouldContinueTrace( + final @NotNull SentryOptions options, final @Nullable Baggage baggage) { + final @Nullable String rawSdkOrgId = options.getEffectiveOrgId(); + final @Nullable String sdkOrgId = + (rawSdkOrgId != null && !rawSdkOrgId.trim().isEmpty()) ? rawSdkOrgId.trim() : null; + final @Nullable String rawBaggageOrgId = baggage != null ? baggage.getOrgId() : null; + final @Nullable String baggageOrgId = + (rawBaggageOrgId != null && !rawBaggageOrgId.trim().isEmpty()) + ? rawBaggageOrgId.trim() + : null; + + // Mismatched org IDs always reject regardless of strict mode + if (sdkOrgId != null && baggageOrgId != null && !sdkOrgId.equals(baggageOrgId)) { + return false; + } + + // In strict mode, both must be present and match (unless both are missing) + if (options.isStrictTraceContinuation()) { + if (sdkOrgId == null && baggageOrgId == null) { + return true; + } + return sdkOrgId != null && sdkOrgId.equals(baggageOrgId); + } + + return true; + } + /** * Ensures a non null baggage instance is present by creating a new Baggage instance if null is * passed in. diff --git a/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt b/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt index 9c906712261..fdba2551664 100644 --- a/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt @@ -510,4 +510,60 @@ class TracingUtilsTest { assertEquals(fixture.scope.propagationContext.spanId.toString(), parts[2]) assertEquals("00", parts[3]) } + + private fun makeOptions( + dsnOrgId: String?, + explicitOrgId: String? = null, + strict: Boolean = false, + ): SentryOptions { + val options = SentryOptions() + if (dsnOrgId != null) { + options.dsn = "https://key@o$dsnOrgId.ingest.sentry.io/123" + } else { + options.dsn = "https://key@sentry.io/123" + } + options.orgId = explicitOrgId + options.isStrictTraceContinuation = strict + return options + } + + private fun makeBaggage(orgId: String?): Baggage { + val raw = + if (orgId != null) { + "sentry-trace_id=bc6d53f15eb88f4320054569b8c553d4,sentry-org_id=$orgId" + } else { + "sentry-trace_id=bc6d53f15eb88f4320054569b8c553d4" + } + return Baggage.fromHeader(raw, NoOpLogger.getInstance()) + } + + @Test + fun `shouldContinueTrace strict=false matching org ids returns true`() { + val options = makeOptions(dsnOrgId = "1", strict = false) + assertTrue(TracingUtils.shouldContinueTrace(options, makeBaggage("1"))) + } + + @Test + fun `shouldContinueTrace strict=false mismatched org ids returns false`() { + val options = makeOptions(dsnOrgId = "2", strict = false) + assertFalse(TracingUtils.shouldContinueTrace(options, makeBaggage("1"))) + } + + @Test + fun `shouldContinueTrace strict=true matching org ids returns true`() { + val options = makeOptions(dsnOrgId = "1", strict = true) + assertTrue(TracingUtils.shouldContinueTrace(options, makeBaggage("1"))) + } + + @Test + fun `shouldContinueTrace strict=true missing baggage org id returns false`() { + val options = makeOptions(dsnOrgId = "1", strict = true) + assertFalse(TracingUtils.shouldContinueTrace(options, makeBaggage(null))) + } + + @Test + fun `shouldContinueTrace strict=true both missing org ids returns true`() { + val options = makeOptions(dsnOrgId = null, strict = true) + assertTrue(TracingUtils.shouldContinueTrace(options, makeBaggage(null))) + } } From 327d897514a88251775984c07f52a1d20a996fc1 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 23 Mar 2026 06:45:21 +0100 Subject: [PATCH 18/21] fix(opentelemetry): Enforce strict continuation in propagators Apply strict trace continuation checks in all OpenTelemetry propagator extract paths before creating remote parent span context. When org-id validation fails, return the original context and ignore incoming sentry-trace and baggage to keep propagation behavior aligned with strict continuation requirements. Add rejection tests for OtelSentryPropagator, deprecated SentryPropagator, and OpenTelemetryOtlpPropagator. Co-Authored-By: Claude --- .../opentelemetry/OtelSentryPropagator.java | 8 ++++ .../opentelemetry/SentryPropagator.java | 15 +++++-- .../test/kotlin/OtelSentryPropagatorTest.kt | 21 ++++++++++ .../src/test/kotlin/SentryPropagatorTest.kt | 41 +++++++++++++++++++ .../otlp/OpenTelemetryOtlpPropagator.java | 16 ++++++-- .../test/kotlin/OtelSentryPropagatorTest.kt | 19 +++++++++ sentry/api/sentry.api | 1 + .../java/io/sentry/PropagationContext.java | 5 +-- 8 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryPropagatorTest.kt diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java index 56e87c67896..e87af070748 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentryPropagator.java @@ -113,6 +113,14 @@ public 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 = diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryPropagator.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryPropagator.java index ffcab9ed541..4016debf2bb 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryPropagator.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentryPropagator.java @@ -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; @@ -98,6 +99,17 @@ public 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(), @@ -107,9 +119,6 @@ public 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); diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OtelSentryPropagatorTest.kt b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OtelSentryPropagatorTest.kt index 2315412fd46..7aa531a0f7c 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OtelSentryPropagatorTest.kt +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OtelSentryPropagatorTest.kt @@ -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 @@ -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 = + 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() diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryPropagatorTest.kt b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryPropagatorTest.kt new file mode 100644 index 00000000000..c08ed3dd66a --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryPropagatorTest.kt @@ -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 = + 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)) + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-otlp/src/main/java/io/sentry/opentelemetry/otlp/OpenTelemetryOtlpPropagator.java b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/main/java/io/sentry/opentelemetry/otlp/OpenTelemetryOtlpPropagator.java index e6bc31ca827..a4249b27ec0 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-otlp/src/main/java/io/sentry/opentelemetry/otlp/OpenTelemetryOtlpPropagator.java +++ b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/main/java/io/sentry/opentelemetry/otlp/OpenTelemetryOtlpPropagator.java @@ -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; @@ -87,6 +88,16 @@ public 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 = @@ -104,9 +115,8 @@ public 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 diff --git a/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtelSentryPropagatorTest.kt b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtelSentryPropagatorTest.kt index e9bfe26c11d..4e17fd1b648 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtelSentryPropagatorTest.kt +++ b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtelSentryPropagatorTest.kt @@ -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 = + 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() diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index ff6e7088288..0670102c577 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -7786,6 +7786,7 @@ public final class io/sentry/util/TracingUtils { public static fun isIgnored (Ljava/util/List;Ljava/lang/String;)Z public static fun maybeUpdateBaggage (Lio/sentry/IScope;Lio/sentry/SentryOptions;)Lio/sentry/PropagationContext; public static fun setTrace (Lio/sentry/IScopes;Lio/sentry/PropagationContext;)V + public static fun shouldContinueTrace (Lio/sentry/SentryOptions;Lio/sentry/Baggage;)Z public static fun startNewTrace (Lio/sentry/IScopes;)V public static fun trace (Lio/sentry/IScopes;Ljava/util/List;Lio/sentry/ISpan;)Lio/sentry/util/TracingUtils$TracingHeaders; public static fun traceIfAllowed (Lio/sentry/IScopes;Ljava/lang/String;Ljava/util/List;Lio/sentry/ISpan;)Lio/sentry/util/TracingUtils$TracingHeaders; diff --git a/sentry/src/main/java/io/sentry/PropagationContext.java b/sentry/src/main/java/io/sentry/PropagationContext.java index 618a76b2c87..a6779805276 100644 --- a/sentry/src/main/java/io/sentry/PropagationContext.java +++ b/sentry/src/main/java/io/sentry/PropagationContext.java @@ -48,9 +48,7 @@ public static PropagationContext fromHeaders( if (options != null && !TracingUtils.shouldContinueTrace(options, baggage)) { options .getLogger() - .log( - SentryLevel.DEBUG, - "Not continuing trace due to strict org ID validation failure."); + .log(SentryLevel.DEBUG, "Not continuing trace due to strict org ID validation failure."); return new PropagationContext(); } @@ -162,5 +160,4 @@ public void setSampled(final @Nullable Boolean sampled) { // should never be null since we ensure it in ctor return sampleRand == null ? 0.0 : sampleRand; } - } From 863f05b76a7b12c4af6d7a77bc10e680e0bad8d8 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 23 Mar 2026 16:02:23 +0000 Subject: [PATCH 19/21] Format code --- .../java/io/sentry/android/core/ManifestMetadataReader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index f4c0ff1835b..6d90bb5ca8e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -169,7 +169,7 @@ final class ManifestMetadataReader { 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"; From 1d8e3016d9c4587d4e2a8ca604822d5359db2a39 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 26 Mar 2026 06:15:22 +0100 Subject: [PATCH 20/21] fix(tracing): Fix empty orgId bypassing DSN fallback The getEffectiveOrgId() method only checked orgId != null, allowing empty strings and whitespace-only values to bypass the DSN fallback mechanism. This caused empty org IDs to propagate to outgoing baggage headers as sentry-org_id=, silently breaking trace continuation in strict mode. Update getEffectiveOrgId() to trim the orgId value and check if it's empty after trimming. Empty or blank values now correctly fall back to the DSN org ID instead of propagating as empty strings. Add comprehensive test coverage for all edge cases including empty strings, whitespace-only values, and their impact on baggage propagation and strict trace continuation. Co-Authored-By: Claude --- .../main/java/io/sentry/SentryOptions.java | 6 +- sentry/src/test/java/io/sentry/BaggageTest.kt | 85 +++++++++++++++++++ .../test/java/io/sentry/SentryOptionsTest.kt | 40 +++++++++ .../java/io/sentry/util/TracingUtilsTest.kt | 24 ++++++ 4 files changed, 154 insertions(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 937bdc1fdcf..7f67ca3a9cc 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -2334,11 +2334,15 @@ public void setOrgId(final @Nullable String orgId) { /** * Returns the effective org ID, preferring the explicit config option over the DSN-parsed value. + * Empty or whitespace-only explicit org IDs are treated as unset and fall back to the DSN. */ @ApiStatus.Internal public @Nullable String getEffectiveOrgId() { if (orgId != null) { - return orgId; + final @NotNull String trimmed = orgId.trim(); + if (!trimmed.isEmpty()) { + return trimmed; + } } try { final @Nullable String dsnOrgId = retrieveParsedDsn().getOrgId(); diff --git a/sentry/src/test/java/io/sentry/BaggageTest.kt b/sentry/src/test/java/io/sentry/BaggageTest.kt index fd187c61982..e177645734d 100644 --- a/sentry/src/test/java/io/sentry/BaggageTest.kt +++ b/sentry/src/test/java/io/sentry/BaggageTest.kt @@ -4,6 +4,7 @@ import com.github.javafaker.Faker import io.sentry.Baggage.MAX_BAGGAGE_LIST_MEMBER_COUNT import io.sentry.Baggage.MAX_BAGGAGE_STRING_LENGTH import io.sentry.protocol.SentryId +import io.sentry.protocol.TransactionNameSource import java.util.UUID import kotlin.test.BeforeTest import kotlin.test.Test @@ -736,6 +737,90 @@ class BaggageTest { assertNull(baggage.sampleRate) } + @Test + fun `setValuesFromScope falls back to DSN org id when explicit orgId is empty`() { + val options = + SentryOptions().apply { + dsn = "https://key@o123.ingest.sentry.io/456" + orgId = "" + } + val scope = Scope(options) + val baggage = Baggage(logger) + + baggage.setValuesFromScope(scope, options) + + assertEquals("123", baggage.orgId) + } + + @Test + fun `setValuesFromScope falls back to DSN org id when explicit orgId is whitespace`() { + val options = + SentryOptions().apply { + dsn = "https://key@o123.ingest.sentry.io/456" + orgId = " " + } + val scope = Scope(options) + val baggage = Baggage(logger) + + baggage.setValuesFromScope(scope, options) + + assertEquals("123", baggage.orgId) + } + + @Test + fun `setValuesFromTransaction falls back to DSN org id when explicit orgId is empty`() { + val options = + SentryOptions().apply { + dsn = "https://key@o123.ingest.sentry.io/456" + orgId = "" + } + val baggage = Baggage(logger) + + baggage.setValuesFromTransaction( + SentryId(), + SentryId(), + options, + TracesSamplingDecision(true, 1.0), + "test-transaction", + TransactionNameSource.CUSTOM, + ) + + assertEquals("123", baggage.orgId) + } + + @Test + fun `fromEvent falls back to DSN org id when explicit orgId is empty`() { + val options = + SentryOptions().apply { + dsn = "https://key@o123.ingest.sentry.io/456" + orgId = "" + } + val event = SentryEvent() + event.contexts.setTrace(SpanContext("test-op")) + + val baggage = Baggage.fromEvent(event, "test-transaction", options) + + assertEquals("123", baggage.orgId) + } + + @Test + fun `baggage header does not include org id when both explicit and DSN org ids are empty`() { + val options = + SentryOptions().apply { + dsn = "https://key@sentry.io/456" + orgId = "" + release = "1.0.0" + } + val scope = Scope(options) + val baggage = Baggage(logger) + + baggage.setValuesFromScope(scope, options) + val headerString = baggage.toHeaderString(null) + + // Should not contain sentry-org_id if both explicit and DSN org ids are null/empty + assertFalse(headerString.contains("sentry-org_id")) + } + /** * token = 1*tchar tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / * "`" / "|" / "~" / DIGIT / ALPHA ; any VCHAR, except delimiters diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 3b1f559bd0a..2d33f9b499e 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -1032,4 +1032,44 @@ class SentryOptionsTest { options.dsn = "https://key@sentry.io/456" assertNull(options.effectiveOrgId) } + + @Test + fun `getEffectiveOrgId falls back to DSN when explicit orgId is empty string`() { + val options = SentryOptions() + options.dsn = "https://key@o123.ingest.sentry.io/456" + options.orgId = "" + assertEquals("123", options.effectiveOrgId) + } + + @Test + fun `getEffectiveOrgId falls back to DSN when explicit orgId is whitespace only`() { + val options = SentryOptions() + options.dsn = "https://key@o123.ingest.sentry.io/456" + options.orgId = " " + assertEquals("123", options.effectiveOrgId) + } + + @Test + fun `getEffectiveOrgId falls back to DSN when explicit orgId is tab and newline`() { + val options = SentryOptions() + options.dsn = "https://key@o123.ingest.sentry.io/456" + options.orgId = "\t\n" + assertEquals("123", options.effectiveOrgId) + } + + @Test + fun `getEffectiveOrgId returns null when explicit orgId is empty and no DSN orgId`() { + val options = SentryOptions() + options.dsn = "https://key@sentry.io/456" + options.orgId = "" + assertNull(options.effectiveOrgId) + } + + @Test + fun `getEffectiveOrgId trims whitespace from explicit orgId`() { + val options = SentryOptions() + options.dsn = "https://key@o123.ingest.sentry.io/456" + options.orgId = " 999 " + assertEquals("999", options.effectiveOrgId) + } } diff --git a/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt b/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt index fdba2551664..edfcc361b08 100644 --- a/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/TracingUtilsTest.kt @@ -566,4 +566,28 @@ class TracingUtilsTest { val options = makeOptions(dsnOrgId = null, strict = true) assertTrue(TracingUtils.shouldContinueTrace(options, makeBaggage(null))) } + + @Test + fun `shouldContinueTrace uses DSN fallback when explicit orgId is empty`() { + val options = makeOptions(dsnOrgId = "123", explicitOrgId = "", strict = true) + assertTrue(TracingUtils.shouldContinueTrace(options, makeBaggage("123"))) + } + + @Test + fun `shouldContinueTrace uses DSN fallback when explicit orgId is whitespace`() { + val options = makeOptions(dsnOrgId = "123", explicitOrgId = " ", strict = true) + assertTrue(TracingUtils.shouldContinueTrace(options, makeBaggage("123"))) + } + + @Test + fun `shouldContinueTrace rejects mismatch after empty explicit orgId falls back to DSN`() { + val options = makeOptions(dsnOrgId = "123", explicitOrgId = "", strict = true) + assertFalse(TracingUtils.shouldContinueTrace(options, makeBaggage("999"))) + } + + @Test + fun `shouldContinueTrace strict=false with empty explicit orgId uses DSN fallback`() { + val options = makeOptions(dsnOrgId = "123", explicitOrgId = "", strict = false) + assertTrue(TracingUtils.shouldContinueTrace(options, makeBaggage("123"))) + } } From a3c86e7b96deade4bb2da246209875a0254956cc Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 26 Mar 2026 05:30:56 +0000 Subject: [PATCH 21/21] Format code --- sentry/src/test/java/io/sentry/SentryOptionsTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 2915f617d7f..da014b30f74 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -1079,7 +1079,7 @@ class SentryOptionsTest { options.orgId = " 999 " assertEquals("999", options.effectiveOrgId) } - + @Test fun `scopesStorageFactory is null by default`() { val options = SentryOptions()