Skip to content

Commit aaaf007

Browse files
markushibitsandfoxes
authored andcommitted
feat: Add support for w3c trace parent headers for outgoing requests
1 parent e7d6b84 commit aaaf007

File tree

11 files changed

+230
-0
lines changed

11 files changed

+230
-0
lines changed

src/Sentry/Extensibility/DisabledHub.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ public void BindException(Exception exception, ISpan span)
9999
/// </summary>
100100
public BaggageHeader? GetBaggage() => null;
101101

102+
/// <summary>
103+
/// Returns null.
104+
/// </summary>
105+
public W3CTraceparentHeader? GetTraceparentHeader() => null;
106+
102107
/// <summary>
103108
/// Returns sampled out transaction context.
104109
/// </summary>

src/Sentry/Extensibility/HubAdapter.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,13 @@ public void BindException(Exception exception, ISpan span) =>
140140
public BaggageHeader? GetBaggage()
141141
=> SentrySdk.GetBaggage();
142142

143+
/// <summary>
144+
/// Forwards the call to <see cref="SentrySdk"/>.
145+
/// </summary>
146+
[DebuggerStepThrough]
147+
public W3CTraceparentHeader? GetTraceparentHeader()
148+
=> SentrySdk.GetTraceparentHeader();
149+
143150
/// <summary>
144151
/// Forwards the call to <see cref="SentrySdk"/>.
145152
/// </summary>

src/Sentry/IHub.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ public ITransactionTracer StartTransaction(
5959
/// </summary>
6060
public BaggageHeader? GetBaggage();
6161

62+
/// <summary>
63+
/// Gets the W3C Trace Context traceparent header that allows tracing across services
64+
/// </summary>
65+
public W3CTraceparentHeader? GetTraceparentHeader();
66+
6267
/// <summary>
6368
/// Continues a trace based on HTTP header values provided as strings.
6469
/// </summary>

src/Sentry/Internal/Hub.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,18 @@ public BaggageHeader GetBaggage()
314314
return propagationContext.GetOrCreateDynamicSamplingContext(_options, _replaySession).ToBaggageHeader();
315315
}
316316

317+
public W3CTraceparentHeader? GetTraceparentHeader()
318+
{
319+
if (GetSpan()?.GetTraceHeader() is { } traceHeader)
320+
{
321+
return new W3CTraceparentHeader(traceHeader.TraceId, traceHeader.SpanId, traceHeader.IsSampled);
322+
}
323+
324+
// We fall back to the propagation context
325+
var propagationContext = CurrentScope.PropagationContext;
326+
return new W3CTraceparentHeader(propagationContext.TraceId, propagationContext.SpanId, null);
327+
}
328+
317329
public TransactionContext ContinueTrace(
318330
string? traceHeader,
319331
string? baggageHeader,

src/Sentry/SentryMessageHandler.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ private void PropagateTraceHeaders(HttpRequestMessage request, string url, ISpan
137137
{
138138
AddSentryTraceHeader(request, parentSpan);
139139
AddBaggageHeader(request);
140+
if (_options?.PropagateTraceparent is true)
141+
{
142+
AddTraceparentHeader(request, parentSpan);
143+
}
140144
}
141145
}
142146

@@ -181,4 +185,16 @@ private void AddBaggageHeader(HttpRequestMessage request)
181185
// Set the baggage header
182186
request.Headers.Add(BaggageHeader.HttpHeaderName, baggage.ToString());
183187
}
188+
189+
private void AddTraceparentHeader(HttpRequestMessage request, ISpan? parentSpan)
190+
{
191+
// Set W3C traceparent header if it hasn't already been set
192+
if (!request.Headers.Contains(W3CTraceparentHeader.HttpHeaderName) &&
193+
// Use the span created by this integration as parent, instead of its own parent
194+
(parentSpan?.GetTraceHeader() ?? _hub.GetTraceHeader()) is { } traceHeader)
195+
{
196+
var traceparentHeader = new W3CTraceparentHeader(traceHeader.TraceId, traceHeader.SpanId, traceHeader.IsSampled);
197+
request.Headers.Add(W3CTraceparentHeader.HttpHeaderName, traceparentHeader.ToString());
198+
}
199+
}
184200
}

src/Sentry/SentryOptions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,6 +995,18 @@ public IList<StringOrRegex> TracePropagationTargets
995995
set => _tracePropagationTargets = value.WithConfigBinding();
996996
}
997997

998+
/// <summary>
999+
/// Whether to send W3C Trace Context traceparent headers in outgoing HTTP requests for distributed tracing.
1000+
/// When enabled, the SDK will send the <c>traceparent</c> header in addition to the <c>sentry-trace</c> header
1001+
/// for requests matching <see cref="TracePropagationTargets"/>.
1002+
/// </summary>
1003+
/// <remarks>
1004+
/// The default value is <c>false</c>. Set to <c>true</c> to enable W3C Trace Context propagation
1005+
/// for interoperability with services that support OpenTelemetry standards.
1006+
/// </remarks>
1007+
/// <seealso href="https://develop.sentry.dev/sdk/telemetry/traces/#propagatetraceparent"/>
1008+
public bool PropagateTraceparent { get; set; } = false;
1009+
9981010
internal ITransactionProfilerFactory? TransactionProfilerFactory { get; set; }
9991011

10001012
private StackTraceMode? _stackTraceMode;

src/Sentry/SentrySdk.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,13 @@ public static void BindException(Exception exception, ISpan span)
716716
public static BaggageHeader? GetBaggage()
717717
=> CurrentHub.GetBaggage();
718718

719+
/// <summary>
720+
/// Gets the W3C Trace Context traceparent header that allows tracing across services
721+
/// </summary>
722+
[DebuggerStepThrough]
723+
public static W3CTraceparentHeader? GetTraceparentHeader()
724+
=> CurrentHub.GetTraceparentHeader();
725+
719726
/// <summary>
720727
/// Continues a trace based on HTTP header values provided as strings.
721728
/// </summary>

src/Sentry/W3CTraceparentHeader.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
namespace Sentry;
2+
3+
/// <summary>
4+
/// W3C Trace Context traceparent header.
5+
/// </summary>
6+
public class W3CTraceparentHeader
7+
{
8+
internal const string HttpHeaderName = "traceparent";
9+
10+
/// <summary>
11+
/// Trace ID.
12+
/// </summary>
13+
public SentryId TraceId { get; }
14+
15+
/// <summary>
16+
/// Span ID.
17+
/// </summary>
18+
public SpanId SpanId { get; }
19+
20+
/// <summary>
21+
/// Whether the trace is sampled.
22+
/// </summary>
23+
public bool? IsSampled { get; }
24+
25+
/// <summary>
26+
/// Initializes an instance of <see cref="W3CTraceparentHeader"/>.
27+
/// </summary>
28+
public W3CTraceparentHeader(SentryId traceId, SpanId spanId, bool? isSampled)
29+
{
30+
TraceId = traceId;
31+
SpanId = spanId;
32+
IsSampled = isSampled;
33+
}
34+
35+
/// <inheritdoc />
36+
public override string ToString() => IsSampled is { } isSampled
37+
? $"00-{TraceId}-{SpanId}-{(isSampled ? "01" : "00")}"
38+
: $"00-{TraceId}-{SpanId}-00";
39+
}

test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ namespace Sentry
198198
Sentry.BaggageHeader? GetBaggage();
199199
Sentry.ISpan? GetSpan();
200200
Sentry.SentryTraceHeader? GetTraceHeader();
201+
Sentry.W3CTraceparentHeader? GetTraceparentHeader();
201202
void PauseSession();
202203
void ResumeSession();
203204
void StartSession();
@@ -718,6 +719,7 @@ namespace Sentry
718719
public int MaxQueueItems { get; set; }
719720
public Sentry.Extensibility.INetworkStatusListener? NetworkStatusListener { get; set; }
720721
public double? ProfilesSampleRate { get; set; }
722+
public bool PropagateTraceparent { get; set; }
721723
public string? Release { get; set; }
722724
public Sentry.ReportAssembliesMode ReportAssembliesMode { get; set; }
723725
public bool RequestBodyCompressionBuffered { get; set; }
@@ -864,6 +866,7 @@ namespace Sentry
864866
public static Sentry.BaggageHeader? GetBaggage() { }
865867
public static Sentry.ISpan? GetSpan() { }
866868
public static Sentry.SentryTraceHeader? GetTraceHeader() { }
869+
public static Sentry.W3CTraceparentHeader? GetTraceparentHeader() { }
867870
public static Sentry.ITransactionTracer? GetTransaction() { }
868871
public static System.IDisposable Init() { }
869872
public static System.IDisposable Init(Sentry.SentryOptions options) { }
@@ -1298,6 +1301,14 @@ namespace Sentry
12981301
protected abstract void WriteAdditionalProperties(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger);
12991302
public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { }
13001303
}
1304+
public class W3CTraceparentHeader
1305+
{
1306+
public W3CTraceparentHeader(Sentry.SentryId traceId, Sentry.SpanId spanId, bool? isSampled) { }
1307+
public bool? IsSampled { get; }
1308+
public Sentry.SpanId SpanId { get; }
1309+
public Sentry.SentryId TraceId { get; }
1310+
public override string ToString() { }
1311+
}
13011312
}
13021313
namespace Sentry.Ben.BlockingDetector
13031314
{
@@ -1384,6 +1395,7 @@ namespace Sentry.Extensibility
13841395
public Sentry.BaggageHeader? GetBaggage() { }
13851396
public Sentry.ISpan? GetSpan() { }
13861397
public Sentry.SentryTraceHeader? GetTraceHeader() { }
1398+
public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { }
13871399
public void PauseSession() { }
13881400
public System.IDisposable PushScope() { }
13891401
public System.IDisposable PushScope<TState>(TState state) { }
@@ -1433,6 +1445,7 @@ namespace Sentry.Extensibility
14331445
public Sentry.BaggageHeader? GetBaggage() { }
14341446
public Sentry.ISpan? GetSpan() { }
14351447
public Sentry.SentryTraceHeader? GetTraceHeader() { }
1448+
public Sentry.W3CTraceparentHeader? GetTraceparentHeader() { }
14361449
public void PauseSession() { }
14371450
public System.IDisposable PushScope() { }
14381451
public System.IDisposable PushScope<TState>(TState state) { }

test/Sentry.Tests/HubTests.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,6 +1363,47 @@ public void GetBaggage_NoSpanActive_ReturnsBaggageFromPropagationContext()
13631363
Assert.Contains("sentry-trace_id=43365712692146d08ee11a729dfbcaca", baggage!.ToString());
13641364
}
13651365

1366+
[Theory]
1367+
[InlineData(true)]
1368+
[InlineData(false)]
1369+
public void GetTraceparentHeader_ReturnsHeaderForActiveSpan(bool isSampled)
1370+
{
1371+
// Arrange
1372+
_fixture.Options.TracesSampleRate = isSampled ? 1 : 0;
1373+
var hub = _fixture.GetSut();
1374+
var transaction = hub.StartTransaction("foo", "bar");
1375+
hub.ConfigureScope(scope => scope.Transaction = transaction);
1376+
1377+
// Act
1378+
var header = hub.GetTraceparentHeader();
1379+
1380+
// Assert
1381+
header.Should().NotBeNull();
1382+
header.SpanId.Should().Be(transaction.SpanId);
1383+
header.TraceId.Should().Be(transaction.TraceId);
1384+
header.IsSampled.Should().Be(transaction.IsSampled);
1385+
}
1386+
1387+
[Fact]
1388+
public void GetTraceparentHeader_NoSpanActive_ReturnsHeaderFromPropagationContext()
1389+
{
1390+
// Arrange
1391+
var hub = _fixture.GetSut();
1392+
var propagationContext = new SentryPropagationContext(
1393+
SentryId.Parse("75302ac48a024bde9a3b3734a82e36c8"),
1394+
SpanId.Parse("2000000000000000"));
1395+
hub.ConfigureScope(scope => scope.SetPropagationContext(propagationContext));
1396+
1397+
// Act
1398+
var header = hub.GetTraceparentHeader();
1399+
1400+
// Assert
1401+
header.Should().NotBeNull();
1402+
header.SpanId.Should().Be(propagationContext.SpanId);
1403+
header.TraceId.Should().Be(propagationContext.TraceId);
1404+
header.IsSampled.Should().BeNull();
1405+
}
1406+
13661407
[Fact]
13671408
public void ContinueTrace_ReceivesHeaders_SetsPropagationContextAndReturnsTransactionContext()
13681409
{

0 commit comments

Comments
 (0)