diff --git a/CHANGELOG.md b/CHANGELOG.md index c6d0c4104d..c9db1465cb 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 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 0fd217794e..163571f4c9 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,15 @@ 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 a7bbb6c6cf..3fa3d8ff87 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; @@ -501,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; @@ -528,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 @@ -550,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 @@ -564,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 } @@ -2267,6 +2274,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 +3577,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 +3618,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 +3689,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 +3773,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 +3805,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 diff --git a/sentry/src/main/java/io/sentry/Baggage.java b/sentry/src/main/java/io/sentry/Baggage.java index 5f610a0291..4645df3f3a 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 705d383266..8b039849c0 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,27 @@ 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") + String extractedOrgId = null; + final String host = uri.getHost(); + if (host != null) { + final Matcher matcher = ORG_ID_PATTERN.matcher(host); + if (matcher.find()) { + extractedOrgId = matcher.group(1); + } + } + orgId = extractedOrgId; } catch (Throwable e) { throw new IllegalArgumentException(e); } } + + 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/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index 9eaf26b202..5473876aea 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 e7d39d35fe..0e82c5f1c2 100644 --- a/sentry/src/main/java/io/sentry/PropagationContext.java +++ b/sentry/src/main/java/io/sentry/PropagationContext.java @@ -23,6 +23,20 @@ public static 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); + } + + 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 +44,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 +169,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 e155979e06..82c03feac4 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); + 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 7883ed6b95..6ce07f3ff6 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. * @@ -3525,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/DsnTest.kt b/sentry/src/test/java/io/sentry/DsnTest.kt index 6c454ad5c7..29f7022920 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 8e83dec4de..b27c28b9da 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,163 @@ 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, + listOf(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, + listOf(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, + listOf(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, + listOf(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, + listOf(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, + listOf(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, + listOf(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, + listOf(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, + listOf(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, + listOf(makeBaggage("1")), + options, + ) + assertNotEquals(incomingTraceId, pc.traceId.toString()) + } }