Skip to content

Commit 17322cb

Browse files
feat: Add propagating of traceparent
Add the option propagateTraceparent, which is disabled by default. When enabled, it adds the W3C Trace Context HTTP header traceparent on outgoing HTTP requests. This is useful when the receiving services only support OTel/W3C propagation. Fixes GH-6017
1 parent 8f2d7b7 commit 17322cb

File tree

11 files changed

+553
-40371
lines changed

11 files changed

+553
-40371
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
### Features
1414

1515
- Add SentryDistribution as Swift Package Manager target (#6149)
16+
- Add option `enablePropagateTraceparent` to support OTel/W3C trace propagation (#6356)
1617

1718
### Fixes
1819

Sources/Sentry/Public/SentryOptions.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,18 @@ typedef void (^SentryProfilingConfigurationBlock)(SentryProfileOptions *_Nonnull
699699
*/
700700
@property (nonatomic, assign) BOOL enableAutoBreadcrumbTracking;
701701

702+
/**
703+
* When enabled, the SDK propagates the W3C Trace Context HTTP header traceparent on outgoing HTTP
704+
* requests.
705+
*
706+
* @discussion This is useful when the receiving services only support OTel/W3C propagation. The
707+
* traceparent header is only sent when this option is @c YES and the request matches @c
708+
* tracePropagationTargets.
709+
*
710+
* @note Default value is @c NO.
711+
*/
712+
@property (nonatomic, assign) BOOL enablePropagateTraceparent;
713+
702714
/**
703715
* An array of hosts or regexes that determines if outgoing HTTP requests will get
704716
* extra @c trace_id and @c baggage headers added.

Sources/Sentry/SentryNetworkTracker.m

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,12 @@ - (void)urlSessionTaskResume:(NSURLSessionTask *)sessionTask
189189
}
190190

191191
SentryBaggage *baggage = [[[SentryTracer getTracer:span] traceContext] toBaggage];
192-
[SentryTracePropagation addBaggageHeader:baggage
193-
traceHeader:[netSpan toTraceHeader]
194-
tracePropagationTargets:SentrySDKInternal.options.tracePropagationTargets
195-
toRequest:sessionTask];
192+
[SentryTracePropagation
193+
addBaggageHeader:baggage
194+
traceHeader:[netSpan toTraceHeader]
195+
propagateTraceparent:SentrySDKInternal.options.enablePropagateTraceparent
196+
tracePropagationTargets:SentrySDKInternal.options.tracePropagationTargets
197+
toRequest:sessionTask];
196198

197199
SENTRY_LOG_DEBUG(
198200
@"SentryNetworkTracker automatically started HTTP span for sessionTask: %@",
@@ -226,6 +228,7 @@ - (void)addTraceWithoutTransactionToTask:(NSURLSessionTask *)sessionTask
226228

227229
[SentryTracePropagation addBaggageHeader:[traceContext toBaggage]
228230
traceHeader:[propagationContext traceHeader]
231+
propagateTraceparent:SentrySDKInternal.options.enablePropagateTraceparent
229232
tracePropagationTargets:SentrySDKInternal.options.tracePropagationTargets
230233
toRequest:sessionTask];
231234
}

Sources/Sentry/SentryOptions.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ - (instancetype)init
121121
self.enableAppHangTracking = YES;
122122
self.appHangTimeoutInterval = 2.0;
123123
self.enableAutoBreadcrumbTracking = YES;
124+
self.enablePropagateTraceparent = NO;
124125
self.enableNetworkTracking = YES;
125126
self.enableFileIOTracing = YES;
126127
self.enableNetworkBreadcrumbs = YES;

Sources/Sentry/SentryTracePropagation.m

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
#import <SentryTraceHeader.h>
55
#import <SentryTracePropagation.h>
66

7+
static NSString *const SENTRY_TRACEPARENT = @"traceparent";
8+
79
@implementation SentryTracePropagation
810

911
+ (void)addBaggageHeader:(SentryBaggage *)baggage
1012
traceHeader:(SentryTraceHeader *)traceHeader
13+
propagateTraceparent:(BOOL)propagateTraceparent
1114
tracePropagationTargets:(NSArray *)tracePropagationTargets
1215
toRequest:(NSURLSessionTask *)sessionTask
1316
{
@@ -33,14 +36,10 @@ + (void)addBaggageHeader:(SentryBaggage *)baggage
3336
// header.
3437
if ([sessionTask.currentRequest isKindOfClass:[NSMutableURLRequest class]]) {
3538
NSMutableURLRequest *currentRequest = (NSMutableURLRequest *)sessionTask.currentRequest;
36-
37-
if ([currentRequest valueForHTTPHeaderField:SENTRY_TRACE_HEADER] == nil) {
38-
[currentRequest setValue:traceHeader.value forHTTPHeaderField:SENTRY_TRACE_HEADER];
39-
}
40-
41-
if (baggageHeader.length > 0) {
42-
[currentRequest setValue:baggageHeader forHTTPHeaderField:SENTRY_BAGGAGE_HEADER];
43-
}
39+
[SentryTracePropagation addHeaderFieldsToRequest:currentRequest
40+
traceHeader:traceHeader
41+
baggageHeader:baggageHeader
42+
propagateTraceparent:propagateTraceparent];
4443
} else {
4544
// Even though NSURLSessionTask doesn't have 'setCurrentRequest', some subclasses
4645
// do. For those subclasses we replace the currentRequest with a mutable one with
@@ -49,14 +48,10 @@ + (void)addBaggageHeader:(SentryBaggage *)baggage
4948
SEL setCurrentRequestSelector = NSSelectorFromString(@"setCurrentRequest:");
5049
if ([sessionTask respondsToSelector:setCurrentRequestSelector]) {
5150
NSMutableURLRequest *newRequest = [sessionTask.currentRequest mutableCopy];
52-
53-
if ([newRequest valueForHTTPHeaderField:SENTRY_TRACE_HEADER] == nil) {
54-
[newRequest setValue:traceHeader.value forHTTPHeaderField:SENTRY_TRACE_HEADER];
55-
}
56-
57-
if (baggageHeader.length > 0) {
58-
[newRequest setValue:baggageHeader forHTTPHeaderField:SENTRY_BAGGAGE_HEADER];
59-
}
51+
[SentryTracePropagation addHeaderFieldsToRequest:newRequest
52+
traceHeader:traceHeader
53+
baggageHeader:baggageHeader
54+
propagateTraceparent:propagateTraceparent];
6055

6156
void (*func)(id, SEL, id param)
6257
= (void *)[sessionTask methodForSelector:setCurrentRequestSelector];
@@ -73,6 +68,29 @@ + (BOOL)sessionTaskRequiresPropagation:(NSURLSessionTask *)sessionTask
7368
withTargets:tracePropagationTargets];
7469
}
7570

71+
+ (void)addHeaderFieldsToRequest:(NSMutableURLRequest *)request
72+
traceHeader:(SentryTraceHeader *)traceHeader
73+
baggageHeader:(NSString *)baggageHeader
74+
propagateTraceparent:(BOOL)propagateTraceparent
75+
{
76+
if ([request valueForHTTPHeaderField:SENTRY_TRACE_HEADER] == nil) {
77+
[request setValue:traceHeader.value forHTTPHeaderField:SENTRY_TRACE_HEADER];
78+
}
79+
80+
if (propagateTraceparent && [request valueForHTTPHeaderField:SENTRY_TRACEPARENT] == nil) {
81+
82+
NSString *traceparent = [NSString stringWithFormat:@"00-%@-%@-%02x",
83+
traceHeader.traceId.sentryIdString, traceHeader.spanId.sentrySpanIdString,
84+
traceHeader.sampled == kSentrySampleDecisionYes ? 1 : 0];
85+
86+
[request setValue:traceparent forHTTPHeaderField:SENTRY_TRACEPARENT];
87+
}
88+
89+
if (baggageHeader.length > 0) {
90+
[request setValue:baggageHeader forHTTPHeaderField:SENTRY_BAGGAGE_HEADER];
91+
}
92+
}
93+
7694
+ (BOOL)isTargetMatch:(NSURL *)URL withTargets:(NSArray *)targets
7795
{
7896
for (id targetCheck in targets) {

Sources/Sentry/SentyOptionsInternal.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,9 @@ + (BOOL)validateOptions:(NSDictionary<NSString *, id> *)options
390390
[self setBool:options[@"enableAutoBreadcrumbTracking"]
391391
block:^(BOOL value) { sentryOptions.enableAutoBreadcrumbTracking = value; }];
392392

393+
[self setBool:options[@"enablePropagateTraceparent"]
394+
block:^(BOOL value) { sentryOptions.enablePropagateTraceparent = value; }];
395+
393396
if ([options[@"tracePropagationTargets"] isKindOfClass:[NSArray class]]) {
394397
sentryOptions.tracePropagationTargets
395398
= SENTRY_UNWRAP_NULLABLE(NSArray, options[@"tracePropagationTargets"]);

Sources/Sentry/include/SentryTracePropagation.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ NS_ASSUME_NONNULL_BEGIN
99

1010
+ (void)addBaggageHeader:(SentryBaggage *)baggage
1111
traceHeader:(SentryTraceHeader *)traceHeader
12+
propagateTraceparent:(BOOL)propagateTraceparent
1213
tracePropagationTargets:(NSArray *)tracePropagationTargets
1314
toRequest:(NSURLSessionTask *)sessionTask;
1415

Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class SentryNetworkTrackerTests: XCTestCase {
3939
init() {
4040
options = Options()
4141
options.dsn = SentryNetworkTrackerTests.dsnAsString
42+
options.enablePropagateTraceparent = true
4243
sentryTask = URLSessionDataTaskMock(request: URLRequest(url: URL(string: options.dsn!)!))
4344
scope = Scope()
4445
client = TestClient(options: options)
@@ -915,6 +916,50 @@ class SentryNetworkTrackerTests: XCTestCase {
915916
XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["sentry-trace"] ?? "", "test")
916917
}
917918

919+
func testPropagateTraceparent() throws {
920+
// Arrange
921+
let sut = fixture.getSut()
922+
let task = createDataTask()
923+
let transaction = try XCTUnwrap(startTransaction() as? SentryTracer)
924+
925+
// Act
926+
sut.urlSessionTaskResume(task)
927+
928+
// Assert
929+
let children = try XCTUnwrap(Dynamic(transaction).children.asArray as? [SentrySpan])
930+
let networkSpan = try XCTUnwrap(children.first)
931+
932+
let traceHeader = transaction.toTraceHeader()
933+
let expectedTraceHeader = "00-\(traceHeader.traceId.sentryIdString)-\(networkSpan.spanId.sentrySpanIdString)-00"
934+
XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["traceparent"] ?? "", expectedTraceHeader)
935+
}
936+
937+
func testPropagateTraceparent_WhenDisabled_NotAdded() throws {
938+
// Arrange
939+
let sut = fixture.getSut()
940+
let task = createDataTask()
941+
_ = try XCTUnwrap(startTransaction() as? SentryTracer)
942+
fixture.options.enablePropagateTraceparent = false
943+
944+
// Act
945+
sut.urlSessionTaskResume(task)
946+
947+
// Assert
948+
XCTAssertNil(task.currentRequest?.allHTTPHeaderFields?["traceparent"])
949+
}
950+
951+
func testDontOverrideTraceparent() {
952+
let sut = fixture.getSut()
953+
let task = createDataTask {
954+
var request = $0
955+
request.setValue("test", forHTTPHeaderField: "traceparent")
956+
return request
957+
}
958+
sut.urlSessionTaskResume(task)
959+
960+
XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["traceparent"] ?? "", "test")
961+
}
962+
918963
@available(*, deprecated)
919964
func testDefaultHeadersWhenDisabled() throws {
920965
let sut = fixture.getSut()

Tests/SentryTests/Integrations/Performance/Network/SentryTracePropagationTests.swift

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,76 @@ import XCTest
22

33
final class SentryTracePropagationTests: XCTestCase {
44

5+
func testAddTraceparent_Sampled() throws {
6+
// Arrange
7+
let defaultRegex = try XCTUnwrap(NSRegularExpression(pattern: ".*"))
8+
let emptyBaggage = Baggage()
9+
let sessionTask = try createSessionTask()
10+
11+
let traceID = SentryId()
12+
let spanID = SpanId()
13+
let traceHeader = TraceHeader(trace: traceID, spanId: spanID, sampled: SentrySampleDecision.yes)
14+
15+
// Act
16+
SentryTracePropagation.addBaggageHeader(emptyBaggage, traceHeader: traceHeader, propagateTraceparent: true, tracePropagationTargets: [defaultRegex], toRequest: sessionTask)
17+
18+
// Assert
19+
let traceParent = try XCTUnwrap(sessionTask.currentRequest?.allHTTPHeaderFields?["traceparent"])
20+
XCTAssertEqual(traceParent, "00-\(traceID.sentryIdString)-\(spanID.sentrySpanIdString)-01")
21+
}
22+
23+
func testAddTraceparent_NotSampled() throws {
24+
// Arrange
25+
let defaultRegex = try XCTUnwrap(NSRegularExpression(pattern: ".*"))
26+
let emptyBaggage = Baggage()
27+
let sessionTask = try createSessionTask()
28+
29+
let traceID = SentryId()
30+
let spanID = SpanId()
31+
let traceHeader = TraceHeader(trace: traceID, spanId: spanID, sampled: SentrySampleDecision.no)
32+
33+
// Act
34+
SentryTracePropagation.addBaggageHeader(emptyBaggage, traceHeader: traceHeader, propagateTraceparent: true, tracePropagationTargets: [defaultRegex], toRequest: sessionTask)
35+
36+
// Assert
37+
let traceParent = try XCTUnwrap(sessionTask.currentRequest?.allHTTPHeaderFields?["traceparent"])
38+
XCTAssertEqual(traceParent, "00-\(traceID.sentryIdString)-\(spanID.sentrySpanIdString)-00")
39+
}
40+
41+
func testAddTraceparent_UndecidedSampled() throws {
42+
// Arrange
43+
let defaultRegex = try XCTUnwrap(NSRegularExpression(pattern: ".*"))
44+
let emptyBaggage = Baggage()
45+
let sessionTask = try createSessionTask()
46+
47+
let traceID = SentryId()
48+
let spanID = SpanId()
49+
let traceHeader = TraceHeader(trace: traceID, spanId: spanID, sampled: SentrySampleDecision.undecided)
50+
51+
// Act
52+
SentryTracePropagation.addBaggageHeader(emptyBaggage, traceHeader: traceHeader, propagateTraceparent: true, tracePropagationTargets: [defaultRegex], toRequest: sessionTask)
53+
54+
// Assert
55+
let traceParent = try XCTUnwrap(sessionTask.currentRequest?.allHTTPHeaderFields?["traceparent"])
56+
XCTAssertEqual(traceParent, "00-\(traceID.sentryIdString)-\(spanID.sentrySpanIdString)-00")
57+
}
58+
59+
func testAddTraceparent_NotAddedWhenTargetDoesntMatch() throws {
60+
// Arrange
61+
let emptyBaggage = Baggage()
62+
let sessionTask = try createSessionTask()
63+
64+
let traceID = SentryId()
65+
let spanID = SpanId()
66+
let traceHeader = TraceHeader(trace: traceID, spanId: spanID, sampled: SentrySampleDecision.no)
67+
68+
// Act
69+
SentryTracePropagation.addBaggageHeader(emptyBaggage, traceHeader: traceHeader, propagateTraceparent: true, tracePropagationTargets: ["localhost"], toRequest: sessionTask)
70+
71+
// Assert
72+
XCTAssertNil(sessionTask.currentRequest?.allHTTPHeaderFields?["traceparent"])
73+
}
74+
575
func testIsTargetMatchWithDefaultRegex_MatchesAllURLs() throws {
676
// Arrange
777
let defaultRegex = try XCTUnwrap(NSRegularExpression(pattern: ".*"))
@@ -77,4 +147,11 @@ final class SentryTracePropagationTests: XCTestCase {
77147
XCTAssertTrue(SentryTracePropagation.isTargetMatch(localhostURL, withTargets: targetsWithInvalidType))
78148
}
79149

150+
private func createSessionTask(method: String = "GET") throws -> URLSessionDownloadTaskMock {
151+
let url = try XCTUnwrap(URL(string: "https://www.domain.com/api?query=value&query2=value2#fragment"))
152+
var request = URLRequest(url: url)
153+
request.httpMethod = method
154+
return URLSessionDownloadTaskMock(request: request)
155+
}
156+
80157
}

Tests/SentryTests/SentryOptionsTest.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,11 @@ - (void)testEnableAutoBreadcrumbTracking
207207
[self testBooleanField:@"enableAutoBreadcrumbTracking"];
208208
}
209209

210+
- (void)testEnablePropagateTraceparent
211+
{
212+
[self testBooleanField:@"enablePropagateTraceparent" defaultValue:NO];
213+
}
214+
210215
- (void)testEnableCoreDataTracking
211216
{
212217
[self testBooleanField:@"enableCoreDataTracing" defaultValue:YES];

0 commit comments

Comments
 (0)