Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Sentry/BindableSentryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ internal partial class BindableSentryOptions
public double? TracesSampleRate { get; set; }
public List<string>? TracePropagationTargets { get; set; }
public bool? PropagateTraceparent { get; set; }
public bool? StrictTraceContinuation { get; set; }
public string? OrgId { get; set; }
public double? ProfilesSampleRate { get; set; }
public StackTraceMode? StackTraceMode { get; set; }
public long? MaxAttachmentSize { get; set; }
Expand Down Expand Up @@ -99,6 +101,8 @@ public void ApplyTo(SentryOptions options)
options.ProfilesSampleRate = ProfilesSampleRate ?? options.ProfilesSampleRate;
options.TracePropagationTargets = TracePropagationTargets?.Select(s => new StringOrRegex(s)).ToList() ?? options.TracePropagationTargets;
options.PropagateTraceparent = PropagateTraceparent ?? options.PropagateTraceparent;
options.StrictTraceContinuation = StrictTraceContinuation ?? options.StrictTraceContinuation;
options.OrgId = OrgId ?? options.OrgId;
options.StackTraceMode = StackTraceMode ?? options.StackTraceMode;
options.MaxAttachmentSize = MaxAttachmentSize ?? options.MaxAttachmentSize;
options.DetectStartupTime = DetectStartupTime ?? options.DetectStartupTime;
Expand Down
26 changes: 24 additions & 2 deletions src/Sentry/Dsn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,28 @@ internal sealed class Dsn
/// </summary>
private Uri ApiBaseUri { get; }

/// <summary>
/// The organization ID parsed from the DSN host (e.g., <c>o1</c> in <c>o1.ingest.us.sentry.io</c> yields <c>"1"</c>).
/// Returns <c>null</c> if no org ID is present in the DSN.
/// </summary>
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;
Path = path;
SecretKey = secretKey;
PublicKey = publicKey;
ApiBaseUri = apiBaseUri;
OrgId = orgId;
}

public Uri GetStoreEndpointUri() => new(ApiBaseUri, "store/");
Expand Down Expand Up @@ -95,6 +103,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,
Expand All @@ -109,7 +130,8 @@ public static Dsn Parse(string dsn)
path,
secretKey,
publicKey,
apiBaseUri);
apiBaseUri,
orgId);
}

public static Dsn? TryParse(string? dsn)
Expand Down
20 changes: 15 additions & 5 deletions src/Sentry/DynamicSamplingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,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)
Expand Down Expand Up @@ -94,6 +95,11 @@ private DynamicSamplingContext(SentryId traceId,
items.Add("replay_id", replayId.ToString());
}

if (!string.IsNullOrWhiteSpace(orgId))
{
items.Add("org_id", orgId);
}

_items = items;
}

Expand Down Expand Up @@ -199,7 +205,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)
Expand All @@ -224,7 +231,8 @@ public static DynamicSamplingContext CreateFromUnsampledTransaction(UnsampledTra
release,
environment,
transactionName,
replaySession);
replaySession,
orgId: options.GetEffectiveOrgId());
}

public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagationContext propagationContext, SentryOptions options, IReplaySession? replaySession)
Expand All @@ -240,7 +248,8 @@ public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagat
null,
release: release,
environment: environment,
replaySession: replaySession
replaySession: replaySession,
orgId: options.GetEffectiveOrgId()
Comment thread
cursor[bot] marked this conversation as resolved.
);
}

Comment thread
sentry[bot] marked this conversation as resolved.
Expand All @@ -263,7 +272,8 @@ public static DynamicSamplingContext CreateFromPropagationContext(SentryPropagat
propagationContext.SampleRand,
release: release,
environment: environment,
replaySession: replaySession
replaySession: replaySession,
orgId: options.GetEffectiveOrgId()
);
}
}
Expand Down
40 changes: 40 additions & 0 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,13 @@ public TransactionContext ContinueTrace(
string? name = null,
string? operation = null)
{
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);
ConfigureScope(static (scope, propagationContext) => scope.SetPropagationContext(propagationContext), propagationContext);

Expand All @@ -391,6 +398,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
Expand Down
40 changes: 40 additions & 0 deletions src/Sentry/SentryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,24 @@ public string? Dsn
internal Dsn? _parsedDsn;
internal Dsn ParsedDsn => _parsedDsn ??= Sentry.Dsn.Parse(Dsn!);

/// <summary>
/// Returns the effective org ID, preferring <see cref="OrgId"/> if set, otherwise falling back to the DSN-parsed value.
/// </summary>
internal string? GetEffectiveOrgId()
{
if (!string.IsNullOrWhiteSpace(OrgId))
{
return OrgId;
}

if (!string.IsNullOrWhiteSpace(Dsn))
{
return ParsedDsn.OrgId;
}

return null;
}

private readonly Lazy<string> _sentryBaseUrl;

internal bool IsSentryRequest(string? requestUri) =>
Expand Down Expand Up @@ -1068,6 +1086,28 @@ public IList<StringOrRegex> TracePropagationTargets
/// <seealso href="https://develop.sentry.dev/sdk/telemetry/traces/#propagatetraceparent"/>
public bool PropagateTraceparent { get; set; }

/// <summary>
/// Controls trace continuation from third-party services that happen to be instrumented by Sentry.
/// </summary>
/// <remarks>
/// 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 <c>true</c>, 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.
/// </remarks>
public bool StrictTraceContinuation { get; set; }

/// <summary>
/// Configures the org ID used for trace propagation and features like <see cref="StrictTraceContinuation"/>.
/// </summary>
/// <remarks>
/// In most cases the org ID is already parsed from the DSN (e.g., <c>o1</c> in
/// <c>https://key@o1.ingest.us.sentry.io/123</c> yields org ID <c>"1"</c>).
/// 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.
/// </remarks>
public string? OrgId { get; set; }

internal ITransactionProfilerFactory? TransactionProfilerFactory { get; set; }

private StackTraceMode? _stackTraceMode;
Expand Down
5 changes: 5 additions & 0 deletions test/Sentry.Testing/DsnSamples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ public static class DsnSamples
/// </summary>
public const string ValidDsn = "https://d4d82fc1c2c4032a83f3a29aa3a3aff@fake-sentry.io:65535/2147483647";

/// <summary>
/// A DSN whose host has an org-ID prefix (e.g. o123.ingest.us.sentry.io), causing OrgId to be parsed as "123".
/// </summary>
public const string ValidDsnWithOrgId = "https://d4d82fc1c2c4032a83f3a29aa3a3aff@o123.ingest.us.sentry.io/456";

/// <summary>
/// Missing ProjectId
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,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; }
Expand All @@ -840,6 +841,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<Sentry.StringOrRegex> TagFilters { get; set; }
public System.Collections.Generic.IList<Sentry.HttpStatusCodeRange> TraceIgnoreStatusCodes { get; set; }
public System.Collections.Generic.IList<Sentry.StringOrRegex> TracePropagationTargets { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,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; }
Expand All @@ -840,6 +841,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<Sentry.StringOrRegex> TagFilters { get; set; }
public System.Collections.Generic.IList<Sentry.HttpStatusCodeRange> TraceIgnoreStatusCodes { get; set; }
public System.Collections.Generic.IList<Sentry.StringOrRegex> TracePropagationTargets { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,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; }
Expand All @@ -840,6 +841,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<Sentry.StringOrRegex> TagFilters { get; set; }
public System.Collections.Generic.IList<Sentry.HttpStatusCodeRange> TraceIgnoreStatusCodes { get; set; }
public System.Collections.Generic.IList<Sentry.StringOrRegex> TracePropagationTargets { get; set; }
Expand Down
2 changes: 2 additions & 0 deletions test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,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; }
Expand All @@ -827,6 +828,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<Sentry.StringOrRegex> TagFilters { get; set; }
public System.Collections.Generic.IList<Sentry.HttpStatusCodeRange> TraceIgnoreStatusCodes { get; set; }
public System.Collections.Generic.IList<Sentry.StringOrRegex> TracePropagationTargets { get; set; }
Expand Down
Loading
Loading