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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] =?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/13] 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/13] 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/13] 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/13] 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(); }