Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
feat: Add support for w3c trace parent headers for outgoing requests
  • Loading branch information
markushi authored and bitsandfoxes committed Oct 21, 2025
commit aaaf007e38242daa30e90708431b7fd67197b872
5 changes: 5 additions & 0 deletions src/Sentry/Extensibility/DisabledHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ public void BindException(Exception exception, ISpan span)
/// </summary>
public BaggageHeader? GetBaggage() => null;

/// <summary>
/// Returns null.
/// </summary>
public W3CTraceparentHeader? GetTraceparentHeader() => null;

/// <summary>
/// Returns sampled out transaction context.
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions src/Sentry/Extensibility/HubAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ public void BindException(Exception exception, ISpan span) =>
public BaggageHeader? GetBaggage()
=> SentrySdk.GetBaggage();

/// <summary>
/// Forwards the call to <see cref="SentrySdk"/>.
/// </summary>
[DebuggerStepThrough]
public W3CTraceparentHeader? GetTraceparentHeader()
=> SentrySdk.GetTraceparentHeader();

/// <summary>
/// Forwards the call to <see cref="SentrySdk"/>.
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Sentry/IHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public ITransactionTracer StartTransaction(
/// </summary>
public BaggageHeader? GetBaggage();

/// <summary>
/// Gets the W3C Trace Context traceparent header that allows tracing across services
/// </summary>
public W3CTraceparentHeader? GetTraceparentHeader();

/// <summary>
/// Continues a trace based on HTTP header values provided as strings.
/// </summary>
Expand Down
12 changes: 12 additions & 0 deletions src/Sentry/Internal/Hub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions src/Sentry/SentryMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}

Expand Down Expand Up @@ -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());
}
}
}
12 changes: 12 additions & 0 deletions src/Sentry/SentryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,18 @@ public IList<StringOrRegex> TracePropagationTargets
set => _tracePropagationTargets = value.WithConfigBinding();
}

/// <summary>
/// Whether to send W3C Trace Context traceparent headers in outgoing HTTP requests for distributed tracing.
/// When enabled, the SDK will send the <c>traceparent</c> header in addition to the <c>sentry-trace</c> header
/// for requests matching <see cref="TracePropagationTargets"/>.
/// </summary>
/// <remarks>
/// The default value is <c>false</c>. Set to <c>true</c> to enable W3C Trace Context propagation
/// for interoperability with services that support OpenTelemetry standards.
/// </remarks>
/// <seealso href="https://develop.sentry.dev/sdk/telemetry/traces/#propagatetraceparent"/>
Copy link
Collaborator

@jamescrosswell jamescrosswell Oct 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably document this in a more "SDK User" friendly format... I added #4663 as a follow up for this.

public bool PropagateTraceparent { get; set; } = false;

internal ITransactionProfilerFactory? TransactionProfilerFactory { get; set; }

private StackTraceMode? _stackTraceMode;
Expand Down
7 changes: 7 additions & 0 deletions src/Sentry/SentrySdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,13 @@ public static void BindException(Exception exception, ISpan span)
public static BaggageHeader? GetBaggage()
=> CurrentHub.GetBaggage();

/// <summary>
/// Gets the W3C Trace Context traceparent header that allows tracing across services
/// </summary>
[DebuggerStepThrough]
public static W3CTraceparentHeader? GetTraceparentHeader()
=> CurrentHub.GetTraceparentHeader();

/// <summary>
/// Continues a trace based on HTTP header values provided as strings.
/// </summary>
Expand Down
39 changes: 39 additions & 0 deletions src/Sentry/W3CTraceparentHeader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace Sentry;

/// <summary>
/// W3C Trace Context traceparent header.
/// </summary>
public class W3CTraceparentHeader
{
internal const string HttpHeaderName = "traceparent";

/// <summary>
/// Trace ID.
/// </summary>
public SentryId TraceId { get; }

/// <summary>
/// Span ID.
/// </summary>
public SpanId SpanId { get; }

/// <summary>
/// Whether the trace is sampled.
/// </summary>
public bool? IsSampled { get; }

/// <summary>
/// Initializes an instance of <see cref="W3CTraceparentHeader"/>.
/// </summary>
public W3CTraceparentHeader(SentryId traceId, SpanId spanId, bool? isSampled)
{
TraceId = traceId;
SpanId = spanId;
IsSampled = isSampled;
}

/// <inheritdoc />
public override string ToString() => IsSampled is { } isSampled
? $"00-{TraceId}-{SpanId}-{(isSampled ? "01" : "00")}"
: $"00-{TraceId}-{SpanId}-00";
}
13 changes: 13 additions & 0 deletions test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ namespace Sentry
Sentry.BaggageHeader? GetBaggage();
Sentry.ISpan? GetSpan();
Sentry.SentryTraceHeader? GetTraceHeader();
Sentry.W3CTraceparentHeader? GetTraceparentHeader();
void PauseSession();
void ResumeSession();
void StartSession();
Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -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) { }
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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>(TState state) { }
Expand Down Expand Up @@ -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>(TState state) { }
Expand Down
41 changes: 41 additions & 0 deletions test/Sentry.Tests/HubTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
73 changes: 73 additions & 0 deletions test/Sentry.Tests/Protocol/W3CTraceparentHeaderTests.cs
Original file line number Diff line number Diff line change
@@ -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");
}
}