diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cb5f0c695..9d103949ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,17 @@ - Removed `SentrySdk.CaptureUserFeedback` and all associated members. Use the newer `SentrySdk.CaptureFeedback` instead. - Backpressure handling is now enabled by default, meaning that the SDK will monitor system health and reduce the sampling rate of events and transactions when the system is under load. When the system is determined to be healthy again, the sampling rates are returned to their original levels. ([#4615](https://github.com/getsentry/sentry-dotnet/pull/4615)) - ScopeExtensions.Populate is now internal ([#4611](https://github.com/getsentry/sentry-dotnet/pull/4611)) +- Add support for W3C traceparent header for outgoing requests ([#4661](https://github.com/getsentry/sentry-dotnet/pull/4661)) + This feature is disabled by default. When enabled, outgoing requests will include the W3C traceparent header. + ```csharp + SentrySdk.Init(options => + { + // ... + options.PropagateTraceparent = true; + }); + ``` + + See https://develop.sentry.dev/sdk/telemetry/traces/distributed-tracing/#w3c-trace-context-header for more details. ### Fixes diff --git a/src/Sentry/BindableSentryOptions.cs b/src/Sentry/BindableSentryOptions.cs index f9077de9b4..c3314257f8 100644 --- a/src/Sentry/BindableSentryOptions.cs +++ b/src/Sentry/BindableSentryOptions.cs @@ -42,6 +42,7 @@ internal partial class BindableSentryOptions public bool? EnableTracing { get; set; } public double? TracesSampleRate { get; set; } public List? TracePropagationTargets { get; set; } + public bool? PropagateTraceparent { get; set; } public double? ProfilesSampleRate { get; set; } public StackTraceMode? StackTraceMode { get; set; } public long? MaxAttachmentSize { get; set; } @@ -98,6 +99,7 @@ public void ApplyTo(SentryOptions options) options.TracesSampleRate = TracesSampleRate ?? options.TracesSampleRate; options.ProfilesSampleRate = ProfilesSampleRate ?? options.ProfilesSampleRate; options.TracePropagationTargets = TracePropagationTargets?.Select(s => new StringOrRegex(s)).ToList() ?? options.TracePropagationTargets; + options.PropagateTraceparent = PropagateTraceparent ?? options.PropagateTraceparent; options.StackTraceMode = StackTraceMode ?? options.StackTraceMode; options.MaxAttachmentSize = MaxAttachmentSize ?? options.MaxAttachmentSize; options.DetectStartupTime = DetectStartupTime ?? options.DetectStartupTime; diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index 30d2eefffa..9592b23023 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -99,6 +99,11 @@ public void BindException(Exception exception, ISpan span) /// public BaggageHeader? GetBaggage() => null; + /// + /// Returns null. + /// + public W3CTraceparentHeader? GetTraceparentHeader() => null; + /// /// Returns sampled out transaction context. /// diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index 25eb86800c..086a1ad351 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -140,6 +140,13 @@ public void BindException(Exception exception, ISpan span) => public BaggageHeader? GetBaggage() => SentrySdk.GetBaggage(); + /// + /// Forwards the call to . + /// + [DebuggerStepThrough] + public W3CTraceparentHeader? GetTraceparentHeader() + => SentrySdk.GetTraceparentHeader(); + /// /// Forwards the call to . /// diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs index 8c3006c149..1579a7d659 100644 --- a/src/Sentry/IHub.cs +++ b/src/Sentry/IHub.cs @@ -59,6 +59,11 @@ public ITransactionTracer StartTransaction( /// public BaggageHeader? GetBaggage(); + /// + /// Gets the W3C Trace Context traceparent header that allows tracing across services + /// + public W3CTraceparentHeader? GetTraceparentHeader(); + /// /// Continues a trace based on HTTP header values provided as strings. /// diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 2bc02c2185..d4990363b9 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -314,6 +314,18 @@ public BaggageHeader GetBaggage() return propagationContext.GetOrCreateDynamicSamplingContext(_options, _replaySession).ToBaggageHeader(); } + public W3CTraceparentHeader? GetTraceparentHeader() + { + if (GetSpan()?.GetTraceHeader() is { } traceHeader) + { + return new W3CTraceparentHeader(traceHeader.TraceId, traceHeader.SpanId, traceHeader.IsSampled); + } + + // We fall back to the propagation context + var propagationContext = CurrentScope.PropagationContext; + return new W3CTraceparentHeader(propagationContext.TraceId, propagationContext.SpanId, null); + } + public TransactionContext ContinueTrace( string? traceHeader, string? baggageHeader, diff --git a/src/Sentry/SentryMessageHandler.cs b/src/Sentry/SentryMessageHandler.cs index 60c5b05ad5..9c8515e93e 100644 --- a/src/Sentry/SentryMessageHandler.cs +++ b/src/Sentry/SentryMessageHandler.cs @@ -137,6 +137,10 @@ private void PropagateTraceHeaders(HttpRequestMessage request, string url, ISpan { AddSentryTraceHeader(request, parentSpan); AddBaggageHeader(request); + if (_options?.PropagateTraceparent is true) + { + AddTraceparentHeader(request, parentSpan); + } } } @@ -181,4 +185,16 @@ private void AddBaggageHeader(HttpRequestMessage request) // Set the baggage header request.Headers.Add(BaggageHeader.HttpHeaderName, baggage.ToString()); } + + private void AddTraceparentHeader(HttpRequestMessage request, ISpan? parentSpan) + { + // Set W3C traceparent header if it hasn't already been set + if (!request.Headers.Contains(W3CTraceparentHeader.HttpHeaderName) && + // Use the span created by this integration as parent, instead of its own parent + (parentSpan?.GetTraceHeader() ?? _hub.GetTraceHeader()) is { } traceHeader) + { + var traceparentHeader = new W3CTraceparentHeader(traceHeader.TraceId, traceHeader.SpanId, traceHeader.IsSampled); + request.Headers.Add(W3CTraceparentHeader.HttpHeaderName, traceparentHeader.ToString()); + } + } } diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 9b8835a165..1a043100e8 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -995,6 +995,18 @@ public IList TracePropagationTargets set => _tracePropagationTargets = value.WithConfigBinding(); } + /// + /// Whether to send W3C Trace Context traceparent headers in outgoing HTTP requests for distributed tracing. + /// When enabled, the SDK will send the traceparent header in addition to the sentry-trace header + /// for requests matching . + /// + /// + /// The default value is false. Set to true to enable W3C Trace Context propagation + /// for interoperability with services that support OpenTelemetry standards. + /// + /// + public bool PropagateTraceparent { get; set; } + internal ITransactionProfilerFactory? TransactionProfilerFactory { get; set; } private StackTraceMode? _stackTraceMode; diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index 982dbb6c3a..3df33363f6 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -716,6 +716,13 @@ public static void BindException(Exception exception, ISpan span) public static BaggageHeader? GetBaggage() => CurrentHub.GetBaggage(); + /// + /// Gets the W3C Trace Context traceparent header that allows tracing across services + /// + [DebuggerStepThrough] + public static W3CTraceparentHeader? GetTraceparentHeader() + => CurrentHub.GetTraceparentHeader(); + /// /// Continues a trace based on HTTP header values provided as strings. /// diff --git a/src/Sentry/W3CTraceparentHeader.cs b/src/Sentry/W3CTraceparentHeader.cs new file mode 100644 index 0000000000..d31ab4a731 --- /dev/null +++ b/src/Sentry/W3CTraceparentHeader.cs @@ -0,0 +1,39 @@ +namespace Sentry; + +/// +/// W3C Trace Context traceparent header. +/// +public class W3CTraceparentHeader +{ + internal const string HttpHeaderName = "traceparent"; + + /// + /// Trace ID. + /// + public SentryId TraceId { get; } + + /// + /// Span ID. + /// + public SpanId SpanId { get; } + + /// + /// Whether the trace is sampled. + /// + public bool? IsSampled { get; } + + /// + /// Initializes an instance of . + /// + public W3CTraceparentHeader(SentryId traceId, SpanId spanId, bool? isSampled) + { + TraceId = traceId; + SpanId = spanId; + IsSampled = isSampled; + } + + /// + public override string ToString() => IsSampled is { } isSampled + ? $"00-{TraceId}-{SpanId}-{(isSampled ? "01" : "00")}" + : $"00-{TraceId}-{SpanId}-00"; +} diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt index b955fd5d7f..ffe89a225e 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt @@ -198,6 +198,7 @@ namespace Sentry Sentry.BaggageHeader? GetBaggage(); Sentry.ISpan? GetSpan(); Sentry.SentryTraceHeader? GetTraceHeader(); + Sentry.W3CTraceparentHeader? GetTraceparentHeader(); void PauseSession(); void ResumeSession(); void StartSession(); @@ -718,6 +719,7 @@ namespace Sentry public int MaxQueueItems { get; set; } public Sentry.Extensibility.INetworkStatusListener? NetworkStatusListener { get; set; } public double? ProfilesSampleRate { get; set; } + public bool PropagateTraceparent { get; set; } public string? Release { get; set; } public Sentry.ReportAssembliesMode ReportAssembliesMode { get; set; } public bool RequestBodyCompressionBuffered { get; set; } @@ -864,6 +866,7 @@ namespace Sentry public static Sentry.BaggageHeader? GetBaggage() { } public static Sentry.ISpan? GetSpan() { } public static Sentry.SentryTraceHeader? GetTraceHeader() { } + public static Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public static Sentry.ITransactionTracer? GetTransaction() { } public static System.IDisposable Init() { } public static System.IDisposable Init(Sentry.SentryOptions options) { } @@ -1298,6 +1301,14 @@ namespace Sentry protected abstract void WriteAdditionalProperties(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } } + public class W3CTraceparentHeader + { + public W3CTraceparentHeader(Sentry.SentryId traceId, Sentry.SpanId spanId, bool? isSampled) { } + public bool? IsSampled { get; } + public Sentry.SpanId SpanId { get; } + public Sentry.SentryId TraceId { get; } + public override string ToString() { } + } } namespace Sentry.Ben.BlockingDetector { @@ -1384,6 +1395,7 @@ namespace Sentry.Extensibility public Sentry.BaggageHeader? GetBaggage() { } public Sentry.ISpan? GetSpan() { } public Sentry.SentryTraceHeader? GetTraceHeader() { } + public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public void PauseSession() { } public System.IDisposable PushScope() { } public System.IDisposable PushScope(TState state) { } @@ -1433,6 +1445,7 @@ namespace Sentry.Extensibility public Sentry.BaggageHeader? GetBaggage() { } public Sentry.ISpan? GetSpan() { } public Sentry.SentryTraceHeader? GetTraceHeader() { } + public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public void PauseSession() { } public System.IDisposable PushScope() { } public System.IDisposable PushScope(TState state) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index b955fd5d7f..ffe89a225e 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -198,6 +198,7 @@ namespace Sentry Sentry.BaggageHeader? GetBaggage(); Sentry.ISpan? GetSpan(); Sentry.SentryTraceHeader? GetTraceHeader(); + Sentry.W3CTraceparentHeader? GetTraceparentHeader(); void PauseSession(); void ResumeSession(); void StartSession(); @@ -718,6 +719,7 @@ namespace Sentry public int MaxQueueItems { get; set; } public Sentry.Extensibility.INetworkStatusListener? NetworkStatusListener { get; set; } public double? ProfilesSampleRate { get; set; } + public bool PropagateTraceparent { get; set; } public string? Release { get; set; } public Sentry.ReportAssembliesMode ReportAssembliesMode { get; set; } public bool RequestBodyCompressionBuffered { get; set; } @@ -864,6 +866,7 @@ namespace Sentry public static Sentry.BaggageHeader? GetBaggage() { } public static Sentry.ISpan? GetSpan() { } public static Sentry.SentryTraceHeader? GetTraceHeader() { } + public static Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public static Sentry.ITransactionTracer? GetTransaction() { } public static System.IDisposable Init() { } public static System.IDisposable Init(Sentry.SentryOptions options) { } @@ -1298,6 +1301,14 @@ namespace Sentry protected abstract void WriteAdditionalProperties(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } } + public class W3CTraceparentHeader + { + public W3CTraceparentHeader(Sentry.SentryId traceId, Sentry.SpanId spanId, bool? isSampled) { } + public bool? IsSampled { get; } + public Sentry.SpanId SpanId { get; } + public Sentry.SentryId TraceId { get; } + public override string ToString() { } + } } namespace Sentry.Ben.BlockingDetector { @@ -1384,6 +1395,7 @@ namespace Sentry.Extensibility public Sentry.BaggageHeader? GetBaggage() { } public Sentry.ISpan? GetSpan() { } public Sentry.SentryTraceHeader? GetTraceHeader() { } + public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public void PauseSession() { } public System.IDisposable PushScope() { } public System.IDisposable PushScope(TState state) { } @@ -1433,6 +1445,7 @@ namespace Sentry.Extensibility public Sentry.BaggageHeader? GetBaggage() { } public Sentry.ISpan? GetSpan() { } public Sentry.SentryTraceHeader? GetTraceHeader() { } + public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public void PauseSession() { } public System.IDisposable PushScope() { } public System.IDisposable PushScope(TState state) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt index b955fd5d7f..ffe89a225e 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt @@ -198,6 +198,7 @@ namespace Sentry Sentry.BaggageHeader? GetBaggage(); Sentry.ISpan? GetSpan(); Sentry.SentryTraceHeader? GetTraceHeader(); + Sentry.W3CTraceparentHeader? GetTraceparentHeader(); void PauseSession(); void ResumeSession(); void StartSession(); @@ -718,6 +719,7 @@ namespace Sentry public int MaxQueueItems { get; set; } public Sentry.Extensibility.INetworkStatusListener? NetworkStatusListener { get; set; } public double? ProfilesSampleRate { get; set; } + public bool PropagateTraceparent { get; set; } public string? Release { get; set; } public Sentry.ReportAssembliesMode ReportAssembliesMode { get; set; } public bool RequestBodyCompressionBuffered { get; set; } @@ -864,6 +866,7 @@ namespace Sentry public static Sentry.BaggageHeader? GetBaggage() { } public static Sentry.ISpan? GetSpan() { } public static Sentry.SentryTraceHeader? GetTraceHeader() { } + public static Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public static Sentry.ITransactionTracer? GetTransaction() { } public static System.IDisposable Init() { } public static System.IDisposable Init(Sentry.SentryOptions options) { } @@ -1298,6 +1301,14 @@ namespace Sentry protected abstract void WriteAdditionalProperties(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } } + public class W3CTraceparentHeader + { + public W3CTraceparentHeader(Sentry.SentryId traceId, Sentry.SpanId spanId, bool? isSampled) { } + public bool? IsSampled { get; } + public Sentry.SpanId SpanId { get; } + public Sentry.SentryId TraceId { get; } + public override string ToString() { } + } } namespace Sentry.Ben.BlockingDetector { @@ -1384,6 +1395,7 @@ namespace Sentry.Extensibility public Sentry.BaggageHeader? GetBaggage() { } public Sentry.ISpan? GetSpan() { } public Sentry.SentryTraceHeader? GetTraceHeader() { } + public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public void PauseSession() { } public System.IDisposable PushScope() { } public System.IDisposable PushScope(TState state) { } @@ -1433,6 +1445,7 @@ namespace Sentry.Extensibility public Sentry.BaggageHeader? GetBaggage() { } public Sentry.ISpan? GetSpan() { } public Sentry.SentryTraceHeader? GetTraceHeader() { } + public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public void PauseSession() { } public System.IDisposable PushScope() { } public System.IDisposable PushScope(TState state) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index 4acd40d0ff..809051ae57 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -186,6 +186,7 @@ namespace Sentry Sentry.BaggageHeader? GetBaggage(); Sentry.ISpan? GetSpan(); Sentry.SentryTraceHeader? GetTraceHeader(); + Sentry.W3CTraceparentHeader? GetTraceparentHeader(); void PauseSession(); void ResumeSession(); void StartSession(); @@ -700,6 +701,7 @@ namespace Sentry public int MaxQueueItems { get; set; } public Sentry.Extensibility.INetworkStatusListener? NetworkStatusListener { get; set; } public double? ProfilesSampleRate { get; set; } + public bool PropagateTraceparent { get; set; } public string? Release { get; set; } public Sentry.ReportAssembliesMode ReportAssembliesMode { get; set; } public bool RequestBodyCompressionBuffered { get; set; } @@ -840,6 +842,7 @@ namespace Sentry public static Sentry.BaggageHeader? GetBaggage() { } public static Sentry.ISpan? GetSpan() { } public static Sentry.SentryTraceHeader? GetTraceHeader() { } + public static Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public static Sentry.ITransactionTracer? GetTransaction() { } public static System.IDisposable Init() { } public static System.IDisposable Init(Sentry.SentryOptions options) { } @@ -1274,6 +1277,14 @@ namespace Sentry protected abstract void WriteAdditionalProperties(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger); public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } } + public class W3CTraceparentHeader + { + public W3CTraceparentHeader(Sentry.SentryId traceId, Sentry.SpanId spanId, bool? isSampled) { } + public bool? IsSampled { get; } + public Sentry.SpanId SpanId { get; } + public Sentry.SentryId TraceId { get; } + public override string ToString() { } + } } namespace Sentry.Ben.BlockingDetector { @@ -1360,6 +1371,7 @@ namespace Sentry.Extensibility public Sentry.BaggageHeader? GetBaggage() { } public Sentry.ISpan? GetSpan() { } public Sentry.SentryTraceHeader? GetTraceHeader() { } + public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public void PauseSession() { } public System.IDisposable PushScope() { } public System.IDisposable PushScope(TState state) { } @@ -1409,6 +1421,7 @@ namespace Sentry.Extensibility public Sentry.BaggageHeader? GetBaggage() { } public Sentry.ISpan? GetSpan() { } public Sentry.SentryTraceHeader? GetTraceHeader() { } + public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { } public void PauseSession() { } public System.IDisposable PushScope() { } public System.IDisposable PushScope(TState state) { } diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index b1dc382b3e..c04fc23a94 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -1363,6 +1363,47 @@ public void GetBaggage_NoSpanActive_ReturnsBaggageFromPropagationContext() Assert.Contains("sentry-trace_id=43365712692146d08ee11a729dfbcaca", baggage!.ToString()); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GetTraceparentHeader_ReturnsHeaderForActiveSpan(bool isSampled) + { + // Arrange + _fixture.Options.TracesSampleRate = isSampled ? 1 : 0; + var hub = _fixture.GetSut(); + var transaction = hub.StartTransaction("foo", "bar"); + hub.ConfigureScope(scope => scope.Transaction = transaction); + + // Act + var header = hub.GetTraceparentHeader(); + + // Assert + header.Should().NotBeNull(); + header.SpanId.Should().Be(transaction.SpanId); + header.TraceId.Should().Be(transaction.TraceId); + header.IsSampled.Should().Be(transaction.IsSampled); + } + + [Fact] + public void GetTraceparentHeader_NoSpanActive_ReturnsHeaderFromPropagationContext() + { + // Arrange + var hub = _fixture.GetSut(); + var propagationContext = new SentryPropagationContext( + SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"), + SpanId.Parse("2000000000000000")); + hub.ConfigureScope(scope => scope.SetPropagationContext(propagationContext)); + + // Act + var header = hub.GetTraceparentHeader(); + + // Assert + header.Should().NotBeNull(); + header.SpanId.Should().Be(propagationContext.SpanId); + header.TraceId.Should().Be(propagationContext.TraceId); + header.IsSampled.Should().BeNull(); + } + [Fact] public void ContinueTrace_ReceivesHeaders_SetsPropagationContextAndReturnsTransactionContext() { diff --git a/test/Sentry.Tests/Protocol/W3CTraceparentHeaderTests.cs b/test/Sentry.Tests/Protocol/W3CTraceparentHeaderTests.cs new file mode 100644 index 0000000000..96a58d1b80 --- /dev/null +++ b/test/Sentry.Tests/Protocol/W3CTraceparentHeaderTests.cs @@ -0,0 +1,73 @@ +namespace Sentry.Tests.Protocol; + +public class W3CTraceparentHeaderTests +{ + [Fact] + public void ToString_WithSampledTrue_Works() + { + // Arrange + var traceId = SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"); + var spanId = SpanId.Parse("1000000000000000"); + var header = new W3CTraceparentHeader(traceId, spanId, true); + + // Act + var result = header.ToString(); + + // Assert + result.Should().Be("00-75302ac48a024bde9a3b3734a82e36c8-1000000000000000-01"); + } + + [Fact] + public void ToString_WithSampledFalse_Works() + { + // Arrange + var traceId = SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"); + var spanId = SpanId.Parse("1000000000000000"); + var header = new W3CTraceparentHeader(traceId, spanId, false); + + // Act + var result = header.ToString(); + + // Assert + result.Should().Be("00-75302ac48a024bde9a3b3734a82e36c8-1000000000000000-00"); + } + + [Fact] + public void ToString_WithoutSampled_DefaultsToFalse() + { + // Arrange + var traceId = SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"); + var spanId = SpanId.Parse("1000000000000000"); + var header = new W3CTraceparentHeader(traceId, spanId, null); + + // Act + var result = header.ToString(); + + // Assert + result.Should().Be("00-75302ac48a024bde9a3b3734a82e36c8-1000000000000000-00"); + } + + [Fact] + public void Constructor_StoresProperties() + { + // Arrange + var traceId = SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"); + var spanId = SpanId.Parse("1000000000000000"); + var isSampled = true; + + // Act + var header = new W3CTraceparentHeader(traceId, spanId, isSampled); + + // Assert + header.TraceId.Should().Be(traceId); + header.SpanId.Should().Be(spanId); + header.IsSampled.Should().Be(isSampled); + } + + [Fact] + public void HttpHeaderName_IsCorrect() + { + // Act & Assert + W3CTraceparentHeader.HttpHeaderName.Should().Be("traceparent"); + } +} diff --git a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs index a15727e7b4..584fcd43d7 100644 --- a/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs +++ b/test/Sentry.Tests/SentryHttpMessageHandlerTests.cs @@ -155,6 +155,85 @@ public async Task SendAsync_SentryTraceHeaderAlreadySet_NotOverwritten() string.Concat(h.Value) == "foobar"); } + [Fact] + public async Task SendAsync_W3C_TraceParent_NotSet_WhenPropagateTraceparentIsFalse() + { + // Arrange + var hub = Substitute.For(); + + hub.GetTraceHeader().ReturnsForAnyArgs( + SentryTraceHeader.Parse("75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0")); + + var failedRequestHandler = Substitute.For(); + var options = new SentryOptions(); + using var innerHandler = new RecordingHttpMessageHandler(new FakeHttpMessageHandler()); + using var sentryHandler = new SentryHttpMessageHandler(hub, options, innerHandler, failedRequestHandler); + using var client = new HttpClient(sentryHandler); + + // Act + await client.GetAsync("https://localhost/"); + + using var request = innerHandler.GetRequests().Single(); + + // Assert + request.Headers.Should().NotContain(h => h.Key == "traceparent"); + } + + [Fact] + public async Task SendAsync_W3C_TraceParent_Set_WhenPropagateTraceparentIsTrue() + { + // Arrange + var hub = Substitute.For(); + + hub.GetTraceHeader().ReturnsForAnyArgs( + SentryTraceHeader.Parse("75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0")); + + var failedRequestHandler = Substitute.For(); + var options = new SentryOptions(); + options.PropagateTraceparent = true; + using var innerHandler = new RecordingHttpMessageHandler(new FakeHttpMessageHandler()); + using var sentryHandler = new SentryHttpMessageHandler(hub, options, innerHandler, failedRequestHandler); + using var client = new HttpClient(sentryHandler); + + client.DefaultRequestHeaders.Add("sentry-trace", "foobar"); + + // Act + await client.GetAsync("https://localhost/"); + + using var request = innerHandler.GetRequests().Single(); + + // Assert + request.Headers.Should().Contain(h => h.Key == "traceparent" && h.Value.Single() == "00-75302ac48a024bde9a3b3734a82e36c8-1000000000000000-00"); + } + + [Fact] + public async Task SendAsync_W3C_TraceParent_NotSet_WhenPropagateTraceparentAlreadySet() + { + // Arrange + var hub = Substitute.For(); + + hub.GetTraceHeader().ReturnsForAnyArgs( + SentryTraceHeader.Parse("75302ac48a024bde9a3b3734a82e36c8-1000000000000000-0")); + + var failedRequestHandler = Substitute.For(); + var options = new SentryOptions(); + options.PropagateTraceparent = true; + using var innerHandler = new RecordingHttpMessageHandler(new FakeHttpMessageHandler()); + using var sentryHandler = new SentryHttpMessageHandler(hub, options, innerHandler, failedRequestHandler); + using var client = new HttpClient(sentryHandler); + + client.DefaultRequestHeaders.Add("sentry-trace", "foobar"); + client.DefaultRequestHeaders.Add("traceparent", "existing-value"); + + // Act + await client.GetAsync("https://localhost/"); + + using var request = innerHandler.GetRequests().Single(); + + // Assert + request.Headers.Should().Contain(h => h.Key == "traceparent" && h.Value.Single() == "existing-value"); + } + [Fact] public async Task SendAsync_TransactionOnScope_StartsNewSpan() {