diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d6a15ac40..f53a8dea7c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### Features
+
+- Add strict trace continuation support ([#4981](https://github.com/getsentry/sentry-dotnet/pull/4981))
+
### Fixes
- The SDK now logs a `Warning` instead of an `Error` when being ratelimited ([#4927](https://github.com/getsentry/sentry-dotnet/pull/4927))
diff --git a/src/Sentry/Dsn.cs b/src/Sentry/Dsn.cs
index 011af39169..6a1d9934c1 100644
--- a/src/Sentry/Dsn.cs
+++ b/src/Sentry/Dsn.cs
@@ -38,13 +38,20 @@ internal sealed class Dsn
///
private Uri ApiBaseUri { get; }
+ ///
+ /// The organization ID parsed from the DSN host (e.g., o1 in o1.ingest.us.sentry.io yields "1").
+ /// Returns null if no org ID is present in the DSN.
+ ///
+ public string? OrgId { get; internal set; }
+
private Dsn(
string source,
string projectId,
string? path,
string? secretKey,
string publicKey,
- Uri apiBaseUri)
+ Uri apiBaseUri,
+ string? orgId = null)
{
Source = source;
ProjectId = projectId;
@@ -52,6 +59,7 @@ private Dsn(
SecretKey = secretKey;
PublicKey = publicKey;
ApiBaseUri = apiBaseUri;
+ OrgId = orgId;
}
public Uri GetStoreEndpointUri() => new(ApiBaseUri, "store/");
@@ -93,6 +101,19 @@ public static Dsn Parse(string dsn)
throw new ArgumentException("Invalid DSN: A Project Id is required.");
}
+ // Parse org ID from host (e.g., "o1.ingest.us.sentry.io" -> "1")
+ string? orgId = null;
+ var hostParts = uri.DnsSafeHost.Split('.');
+ if (hostParts.Length > 0)
+ {
+ var firstPart = hostParts[0];
+ if (firstPart.Length >= 2 && firstPart[0] == 'o' &&
+ ulong.TryParse(firstPart.Substring(1), out _))
+ {
+ orgId = firstPart.Substring(1);
+ }
+ }
+
var apiBaseUri = new UriBuilder
{
Scheme = uri.Scheme,
@@ -107,7 +128,8 @@ public static Dsn Parse(string dsn)
path,
secretKey,
publicKey,
- apiBaseUri);
+ apiBaseUri,
+ orgId);
}
public static Dsn? TryParse(string? dsn)
diff --git a/src/Sentry/DynamicSamplingContext.cs b/src/Sentry/DynamicSamplingContext.cs
index f39d7fa5a7..61f0e6aa5e 100644
--- a/src/Sentry/DynamicSamplingContext.cs
+++ b/src/Sentry/DynamicSamplingContext.cs
@@ -30,7 +30,8 @@ private DynamicSamplingContext(SentryId traceId,
string? release = null,
string? environment = null,
string? transactionName = null,
- IReplaySession? replaySession = null)
+ IReplaySession? replaySession = null,
+ string? orgId = null)
{
// Validate and set required values
if (traceId == SentryId.Empty)
@@ -95,6 +96,11 @@ private DynamicSamplingContext(SentryId traceId,
items.Add("replay_id", replayId.ToString());
}
+ if (!string.IsNullOrWhiteSpace(orgId))
+ {
+ items.Add("org_id", orgId);
+ }
+
_items = items;
}
@@ -200,7 +206,8 @@ public static DynamicSamplingContext CreateFromTransaction(TransactionTracer tra
release,
environment,
transactionName,
- replaySession);
+ replaySession,
+ orgId: options.GetEffectiveOrgId());
}
public static DynamicSamplingContext CreateFromUnsampledTransaction(UnsampledTransaction transaction, SentryOptions options, IReplaySession? replaySession)
@@ -225,7 +232,8 @@ public static DynamicSamplingContext CreateFromUnsampledTransaction(UnsampledTra
release,
environment,
transactionName,
- replaySession);
+ replaySession,
+ orgId: options.GetEffectiveOrgId());
}
public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession)
@@ -241,7 +249,8 @@ public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagat
null,
release: release,
environment: environment,
- replaySession: replaySession
+ replaySession: replaySession,
+ orgId: options.GetEffectiveOrgId()
);
}
}
diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs
index be71b2f1d3..9eb32facda 100644
--- a/src/Sentry/Internal/Hub.cs
+++ b/src/Sentry/Internal/Hub.cs
@@ -355,7 +355,14 @@ public TransactionContext ContinueTrace(
string? name = null,
string? operation = null)
{
- var propagationContext = SentryPropagationContext.CreateFromHeaders(_options.DiagnosticLogger, traceHeader, baggageHeader, _replaySession);
+ if (!ShouldContinueTrace(baggageHeader))
+ {
+ _options.LogDebug("Not continuing trace due to org ID validation. Starting new trace.");
+ traceHeader = null;
+ baggageHeader = null;
+ }
+
+ var propagationContext = SentryPropagationContext.CreateFromHeaders(_options.DiagnosticLogger, traceHeader, baggageHeader, _replaySession, _options.GetEffectiveOrgId());
ConfigureScope(static (scope, propagationContext) => scope.SetPropagationContext(propagationContext), propagationContext);
return new TransactionContext(
@@ -368,6 +375,39 @@ public TransactionContext ContinueTrace(
isParentSampled: traceHeader?.IsSampled);
}
+ internal bool ShouldContinueTrace(BaggageHeader? baggageHeader)
+ {
+ var sdkOrgId = _options.GetEffectiveOrgId();
+
+ string? baggageOrgId = null;
+ if (baggageHeader is not null)
+ {
+ var sentryMembers = baggageHeader.GetSentryMembers();
+ sentryMembers.TryGetValue("org_id", out baggageOrgId);
+ }
+
+ // Mismatched org IDs always cause a new trace, regardless of strict mode
+ if (!string.IsNullOrEmpty(sdkOrgId) && !string.IsNullOrEmpty(baggageOrgId) && sdkOrgId != baggageOrgId)
+ {
+ return false;
+ }
+
+ // In strict mode, both must be present and match
+ if (_options.StrictTraceContinuation)
+ {
+ // If both are missing, continue (nothing to compare)
+ if (string.IsNullOrEmpty(sdkOrgId) && string.IsNullOrEmpty(baggageOrgId))
+ {
+ return true;
+ }
+
+ // Both must be present and equal
+ return sdkOrgId == baggageOrgId;
+ }
+
+ return true;
+ }
+
public void StartSession()
{
// Attempt to recover persisted session left over from previous run
diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs
index 7fc69a600f..7e4f8bdc29 100644
--- a/src/Sentry/SentryOptions.cs
+++ b/src/Sentry/SentryOptions.cs
@@ -448,6 +448,24 @@ public string? Dsn
internal Dsn? _parsedDsn;
internal Dsn ParsedDsn => _parsedDsn ??= Sentry.Dsn.Parse(Dsn!);
+ ///
+ /// Returns the effective org ID, preferring if set, otherwise falling back to the DSN-parsed value.
+ ///
+ internal string? GetEffectiveOrgId()
+ {
+ if (!string.IsNullOrWhiteSpace(OrgId))
+ {
+ return OrgId;
+ }
+
+ if (!string.IsNullOrWhiteSpace(Dsn))
+ {
+ return ParsedDsn.OrgId;
+ }
+
+ return null;
+ }
+
private readonly Lazy _sentryBaseUrl;
internal bool IsSentryRequest(string? requestUri) =>
@@ -1032,6 +1050,28 @@ public IList TracePropagationTargets
///
public bool PropagateTraceparent { get; set; }
+ ///
+ /// Controls trace continuation from third-party services that happen to be instrumented by Sentry.
+ ///
+ ///
+ /// When enabled, the SDK will require org IDs from baggage to match for continuing the trace.
+ /// If the incoming trace does not contain an org ID and this option is true, a new trace will be started.
+ /// When disabled (default), incoming traces without org IDs will be continued as normal,
+ /// but mismatched org IDs will always cause a new trace to be started regardless of this setting.
+ ///
+ public bool StrictTraceContinuation { get; set; }
+
+ ///
+ /// Configures the org ID used for trace propagation and features like .
+ ///
+ ///
+ /// In most cases the org ID is already parsed from the DSN (e.g., o1 in
+ /// https://key@o1.ingest.us.sentry.io/123 yields org ID "1").
+ /// Use this option when non-standard Sentry DSNs are used, such as self-hosted or when using a local Relay.
+ /// When set, this value overrides the org ID parsed from the DSN.
+ ///
+ public string? OrgId { get; set; }
+
internal ITransactionProfilerFactory? TransactionProfilerFactory { get; set; }
private StackTraceMode? _stackTraceMode;
diff --git a/src/Sentry/SentryPropagationContext.cs b/src/Sentry/SentryPropagationContext.cs
index 6183262241..a19d8f7b39 100644
--- a/src/Sentry/SentryPropagationContext.cs
+++ b/src/Sentry/SentryPropagationContext.cs
@@ -48,7 +48,7 @@ public SentryPropagationContext(SentryPropagationContext? other)
_dynamicSamplingContext = other?._dynamicSamplingContext;
}
- public static SentryPropagationContext CreateFromHeaders(IDiagnosticLogger? logger, SentryTraceHeader? traceHeader, BaggageHeader? baggageHeader, IReplaySession replaySession)
+ public static SentryPropagationContext CreateFromHeaders(IDiagnosticLogger? logger, SentryTraceHeader? traceHeader, BaggageHeader? baggageHeader, IReplaySession replaySession, string? sdkOrgId = null)
{
logger?.LogDebug("Creating a propagation context from headers.");
@@ -58,6 +58,19 @@ public static SentryPropagationContext CreateFromHeaders(IDiagnosticLogger? logg
return new SentryPropagationContext();
}
+ // Check for org ID mismatch between SDK configuration and incoming baggage
+ if (!string.IsNullOrEmpty(sdkOrgId) && baggageHeader is not null)
+ {
+ var sentryMembers = baggageHeader.GetSentryMembers();
+ if (sentryMembers.TryGetValue("org_id", out var baggageOrgId)
+ && !string.IsNullOrEmpty(baggageOrgId)
+ && sdkOrgId != baggageOrgId)
+ {
+ logger?.LogInfo("Org ID mismatch (SDK: {0}, baggage: {1}). Starting new trace.", sdkOrgId, baggageOrgId);
+ return new SentryPropagationContext();
+ }
+ }
+
var dynamicSamplingContext = baggageHeader?.CreateDynamicSamplingContext(replaySession);
return new SentryPropagationContext(traceHeader.TraceId, traceHeader.SpanId, dynamicSamplingContext);
}
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt
index 64869d4587..0c8b3542b7 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt
@@ -823,6 +823,7 @@ namespace Sentry
public int MaxCacheItems { get; set; }
public int MaxQueueItems { get; set; }
public Sentry.Extensibility.INetworkStatusListener? NetworkStatusListener { get; set; }
+ public string? OrgId { get; set; }
public double? ProfilesSampleRate { get; set; }
public bool PropagateTraceparent { get; set; }
public string? Release { get; set; }
@@ -838,6 +839,7 @@ namespace Sentry
public System.TimeSpan ShutdownTimeout { get; set; }
public string SpotlightUrl { get; set; }
public Sentry.StackTraceMode StackTraceMode { get; set; }
+ public bool StrictTraceContinuation { get; set; }
public System.Collections.Generic.IList TagFilters { get; set; }
public System.Collections.Generic.IList TracePropagationTargets { get; set; }
public double? TracesSampleRate { get; set; }
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
index 64869d4587..0c8b3542b7 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
@@ -823,6 +823,7 @@ namespace Sentry
public int MaxCacheItems { get; set; }
public int MaxQueueItems { get; set; }
public Sentry.Extensibility.INetworkStatusListener? NetworkStatusListener { get; set; }
+ public string? OrgId { get; set; }
public double? ProfilesSampleRate { get; set; }
public bool PropagateTraceparent { get; set; }
public string? Release { get; set; }
@@ -838,6 +839,7 @@ namespace Sentry
public System.TimeSpan ShutdownTimeout { get; set; }
public string SpotlightUrl { get; set; }
public Sentry.StackTraceMode StackTraceMode { get; set; }
+ public bool StrictTraceContinuation { get; set; }
public System.Collections.Generic.IList TagFilters { get; set; }
public System.Collections.Generic.IList TracePropagationTargets { get; set; }
public double? TracesSampleRate { get; set; }
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
index 64869d4587..0c8b3542b7 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
@@ -823,6 +823,7 @@ namespace Sentry
public int MaxCacheItems { get; set; }
public int MaxQueueItems { get; set; }
public Sentry.Extensibility.INetworkStatusListener? NetworkStatusListener { get; set; }
+ public string? OrgId { get; set; }
public double? ProfilesSampleRate { get; set; }
public bool PropagateTraceparent { get; set; }
public string? Release { get; set; }
@@ -838,6 +839,7 @@ namespace Sentry
public System.TimeSpan ShutdownTimeout { get; set; }
public string SpotlightUrl { get; set; }
public Sentry.StackTraceMode StackTraceMode { get; set; }
+ public bool StrictTraceContinuation { get; set; }
public System.Collections.Generic.IList TagFilters { get; set; }
public System.Collections.Generic.IList TracePropagationTargets { get; set; }
public double? TracesSampleRate { get; set; }
diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
index fbf517dd1e..7bfe25a6a4 100644
--- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
+++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
@@ -809,6 +809,7 @@ namespace Sentry
public int MaxCacheItems { get; set; }
public int MaxQueueItems { get; set; }
public Sentry.Extensibility.INetworkStatusListener? NetworkStatusListener { get; set; }
+ public string? OrgId { get; set; }
public double? ProfilesSampleRate { get; set; }
public bool PropagateTraceparent { get; set; }
public string? Release { get; set; }
@@ -824,6 +825,7 @@ namespace Sentry
public System.TimeSpan ShutdownTimeout { get; set; }
public string SpotlightUrl { get; set; }
public Sentry.StackTraceMode StackTraceMode { get; set; }
+ public bool StrictTraceContinuation { get; set; }
public System.Collections.Generic.IList TagFilters { get; set; }
public System.Collections.Generic.IList TracePropagationTargets { get; set; }
public double? TracesSampleRate { get; set; }
diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs
index c8bb6ea702..a61632ac38 100644
--- a/test/Sentry.Tests/HubTests.cs
+++ b/test/Sentry.Tests/HubTests.cs
@@ -2662,6 +2662,126 @@ public void Dispose_CalledMultipleTimes_CleanupCalledOnlyOnce()
// Assert
integration.Disposed.Should().Be(1);
}
+
+#nullable enable
+ [Theory]
+ // strict=false: matching org IDs -> continue
+ [InlineData(false, "1", "1", true)]
+ // strict=false: no incoming org ID -> continue (permissive)
+ [InlineData(false, "1", null, true)]
+ // strict=false: incoming org ID but no SDK org ID -> continue (permissive)
+ [InlineData(false, null, "1", true)]
+ // strict=false: both missing -> continue
+ [InlineData(false, null, null, true)]
+ // strict=false: mismatched org IDs -> new trace (always)
+ [InlineData(false, "1", "2", false)]
+ // strict=true: matching org IDs -> continue
+ [InlineData(true, "1", "1", true)]
+ // strict=true: no incoming org ID -> new trace (strict requires match)
+ [InlineData(true, "1", null, false)]
+ // strict=true: incoming org ID but no SDK org ID -> new trace (strict requires match)
+ [InlineData(true, null, "1", false)]
+ // strict=true: both missing -> continue (nothing to compare)
+ [InlineData(true, null, null, true)]
+ // strict=true: mismatched org IDs -> new trace
+ [InlineData(true, "1", "2", false)]
+ public void ContinueTrace_StrictTraceContinuation_ValidatesOrgId(
+ bool strict, string? sdkOrgId, string? baggageOrgId, bool expectContinued)
+ {
+ // Arrange
+ var incomingTraceId = SentryId.Parse("bc6d53f15eb88f4320054569b8c553d4");
+
+ _fixture.Options.StrictTraceContinuation = strict;
+ _fixture.Options.OrgId = sdkOrgId;
+
+ var hub = _fixture.GetSut();
+
+ var traceHeader = new SentryTraceHeader(incomingTraceId, SpanId.Parse("b72fa28504b07285"), true);
+
+ var baggageMembers = new List>
+ {
+ { "sentry-trace_id", "bc6d53f15eb88f4320054569b8c553d4" },
+ { "sentry-public_key", "49d0f7386ad645858ae85020e393bef3" },
+ { "sentry-sample_rate", "1.0" }
+ };
+ if (baggageOrgId is not null)
+ {
+ baggageMembers.Add(new KeyValuePair("sentry-org_id", baggageOrgId));
+ }
+ var baggageHeader = BaggageHeader.Create(baggageMembers);
+
+ // Act
+ var transactionContext = hub.ContinueTrace(traceHeader, baggageHeader, "test-name");
+
+ // Assert
+ if (expectContinued)
+ {
+ transactionContext.TraceId.Should().Be(incomingTraceId,
+ "trace should be continued when org IDs match or validation passes");
+ }
+ else
+ {
+ transactionContext.TraceId.Should().NotBe(incomingTraceId,
+ "a new trace should be started when org ID validation fails");
+ }
+ }
+
+ [Fact]
+ public void ContinueTrace_OrgIdFromDsn_IsUsedForValidation()
+ {
+ // Arrange - DSN with org ID "1" in the subdomain
+ _fixture.Options.Dsn = "https://key@o1.ingest.us.sentry.io/123";
+ _fixture.Options.StrictTraceContinuation = true;
+
+ var hub = _fixture.GetSut();
+
+ var incomingTraceId = SentryId.Parse("bc6d53f15eb88f4320054569b8c553d4");
+ var traceHeader = new SentryTraceHeader(incomingTraceId, SpanId.Parse("b72fa28504b07285"), true);
+
+ // Baggage with matching org_id=1
+ var baggageHeader = BaggageHeader.Create(new List>
+ {
+ { "sentry-trace_id", "bc6d53f15eb88f4320054569b8c553d4" },
+ { "sentry-public_key", "49d0f7386ad645858ae85020e393bef3" },
+ { "sentry-sample_rate", "1.0" },
+ { "sentry-org_id", "1" }
+ });
+
+ // Act
+ var transactionContext = hub.ContinueTrace(traceHeader, baggageHeader, "test-name");
+
+ // Assert - should continue because org IDs match
+ transactionContext.TraceId.Should().Be(incomingTraceId);
+ }
+
+ [Fact]
+ public void ContinueTrace_OrgIdOptionOverridesDsn()
+ {
+ // Arrange - DSN has org ID "1", but OrgId option overrides to "2"
+ _fixture.Options.Dsn = "https://key@o1.ingest.us.sentry.io/123";
+ _fixture.Options.OrgId = "2";
+ _fixture.Options.StrictTraceContinuation = false;
+
+ var hub = _fixture.GetSut();
+
+ var incomingTraceId = SentryId.Parse("bc6d53f15eb88f4320054569b8c553d4");
+ var traceHeader = new SentryTraceHeader(incomingTraceId, SpanId.Parse("b72fa28504b07285"), true);
+
+ // Baggage with org_id=1 (matches DSN but not the override)
+ var baggageHeader = BaggageHeader.Create(new List>
+ {
+ { "sentry-trace_id", "bc6d53f15eb88f4320054569b8c553d4" },
+ { "sentry-public_key", "49d0f7386ad645858ae85020e393bef3" },
+ { "sentry-sample_rate", "1.0" },
+ { "sentry-org_id", "1" }
+ });
+
+ // Act
+ var transactionContext = hub.ContinueTrace(traceHeader, baggageHeader, "test-name");
+
+ // Assert - should NOT continue because OrgId override (2) != baggage org_id (1)
+ transactionContext.TraceId.Should().NotBe(incomingTraceId);
+ }
}
#if NET6_0_OR_GREATER
diff --git a/test/Sentry.Tests/Protocol/DsnTests.cs b/test/Sentry.Tests/Protocol/DsnTests.cs
index 41e91b702e..b95b2171ec 100644
--- a/test/Sentry.Tests/Protocol/DsnTests.cs
+++ b/test/Sentry.Tests/Protocol/DsnTests.cs
@@ -277,6 +277,41 @@ public override string ToString()
public static implicit operator Uri(DsnTestCase @case) => new($"{@case.Scheme}://{@case.Host}:{@case.Port}{@case.Path}/api/{@case.ProjectId}/store/");
}
+ [Fact]
+ public void Parse_DsnWithOrgId_ParsesOrgId()
+ {
+ var dsn = Dsn.Parse("https://key@o1.ingest.us.sentry.io/123");
+ Assert.Equal("1", dsn.OrgId);
+ }
+
+ [Fact]
+ public void Parse_DsnWithLargeOrgId_ParsesOrgId()
+ {
+ var dsn = Dsn.Parse("https://key@o123456.ingest.us.sentry.io/123");
+ Assert.Equal("123456", dsn.OrgId);
+ }
+
+ [Fact]
+ public void Parse_DsnWithoutOrgId_OrgIdIsNull()
+ {
+ var dsn = Dsn.Parse("https://key@sentry.io/123");
+ Assert.Null(dsn.OrgId);
+ }
+
+ [Fact]
+ public void Parse_DsnWithNonNumericOrgId_OrgIdIsNull()
+ {
+ var dsn = Dsn.Parse("https://key@oabc.ingest.us.sentry.io/123");
+ Assert.Null(dsn.OrgId);
+ }
+
+ [Fact]
+ public void Parse_StandardValidDsn_NoOrgId()
+ {
+ var dsn = Dsn.Parse(ValidDsn);
+ Assert.Null(dsn.OrgId);
+ }
+
private static void AssertEqual(DsnTestCase @case, Dsn dsn)
{
if (@case == null)
diff --git a/test/Sentry.Tests/SentryPropagationContextTests.cs b/test/Sentry.Tests/SentryPropagationContextTests.cs
index bb90359d0a..6c90e20a48 100644
--- a/test/Sentry.Tests/SentryPropagationContextTests.cs
+++ b/test/Sentry.Tests/SentryPropagationContextTests.cs
@@ -161,4 +161,48 @@ public void CreateFromHeaders_BaggageHeaderNotNull_CreatesPropagationContextWith
Assert.Equal("bfd31b89a59d41c99d96dc2baf840ecd", Assert.Contains("replay_id", dsc.Items));
}
}
+
+ [Fact]
+ public void CreateFromHeaders_WithOrgMismatch_StartsNewTrace()
+ {
+ // Arrange
+ var incomingTraceId = SentryId.Create();
+ var traceHeader = new SentryTraceHeader(incomingTraceId, SpanId.Create(), null);
+ var baggageHeader = BaggageHeader.Create(new List>
+ {
+ { "sentry-trace_id", incomingTraceId.ToString() },
+ { "sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff" },
+ { "sentry-org_id", "2" }
+ });
+
+ // Act - SDK org ID is "1", but baggage has org_id "2"
+ var propagationContext = SentryPropagationContext.CreateFromHeaders(
+ null, traceHeader, baggageHeader, _fixture.InactiveReplaySession, sdkOrgId: "1");
+
+ // Assert - should start a new trace because org IDs don't match
+ Assert.NotEqual(incomingTraceId, propagationContext.TraceId);
+ Assert.Null(propagationContext.ParentSpanId);
+ Assert.Null(propagationContext._dynamicSamplingContext);
+ }
+
+ [Fact]
+ public void CreateFromHeaders_WithOrgMatch_ContinuesTrace()
+ {
+ // Arrange
+ var incomingTraceId = SentryId.Create();
+ var traceHeader = new SentryTraceHeader(incomingTraceId, SpanId.Create(), null);
+ var baggageHeader = BaggageHeader.Create(new List>
+ {
+ { "sentry-trace_id", incomingTraceId.ToString() },
+ { "sentry-public_key", "d4d82fc1c2c4032a83f3a29aa3a3aff" },
+ { "sentry-org_id", "1" }
+ });
+
+ // Act - SDK org ID matches baggage org_id
+ var propagationContext = SentryPropagationContext.CreateFromHeaders(
+ null, traceHeader, baggageHeader, _fixture.InactiveReplaySession, sdkOrgId: "1");
+
+ // Assert - should continue the trace
+ Assert.Equal(incomingTraceId, propagationContext.TraceId);
+ }
}