Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@

- Outbound HTTP requests now show in the Network tab for Android Session Replays ([#4860](https://github.com/getsentry/sentry-dotnet/pull/4860))

### Features

- Add strict trace continuation support ([#4981](https://github.com/getsentry/sentry-dotnet/pull/4981))

Comment thread
jamescrosswell marked this conversation as resolved.
Outdated
### Fixes

- The SDK now logs a `Warning` instead of an `Error` when being ratelimited ([#4927](https://github.com/getsentry/sentry-dotnet/pull/4927))
Expand Down
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