Skip to content

Commit 8800ba1

Browse files
feat: Add propagating of traceparent (#6356)
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 535ebd9 commit 8800ba1

File tree

11 files changed

+269
-20
lines changed

11 files changed

+269
-20
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
### Features
1919

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

2223
### Fixes
2324

Sources/Sentry/Public/SentryOptions.h

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

698+
/**
699+
* When enabled, the SDK propagates the W3C Trace Context HTTP header traceparent on outgoing HTTP
700+
* requests.
701+
*
702+
* @discussion This is useful when the receiving services only support OTel/W3C propagation. The
703+
* traceparent header is only sent when this option is @c YES and the request matches @c
704+
* tracePropagationTargets.
705+
*
706+
* @note Default value is @c NO.
707+
*/
708+
@property (nonatomic, assign) BOOL enablePropagateTraceparent;
709+
698710
/**
699711
* An array of hosts or regexes that determines if outgoing HTTP requests will get
700712
* 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
@@ -114,6 +114,7 @@ - (instancetype)init
114114
self.enableAppHangTracking = YES;
115115
self.appHangTimeoutInterval = 2.0;
116116
self.enableAutoBreadcrumbTracking = YES;
117+
self.enablePropagateTraceparent = NO;
117118
self.enableNetworkTracking = YES;
118119
self.enableFileIOTracing = YES;
119120
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
@@ -386,6 +386,9 @@ + (BOOL)validateOptions:(NSDictionary<NSString *, id> *)options
386386
[self setBool:options[@"enableAutoBreadcrumbTracking"]
387387
block:^(BOOL value) { sentryOptions.enableAutoBreadcrumbTracking = value; }];
388388

389+
[self setBool:options[@"enablePropagateTraceparent"]
390+
block:^(BOOL value) { sentryOptions.enablePropagateTraceparent = value; }];
391+
389392
if ([options[@"tracePropagationTargets"] isKindOfClass:[NSArray class]]) {
390393
sentryOptions.tracePropagationTargets
391394
= 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
@@ -212,6 +212,11 @@ - (void)testEnableAutoBreadcrumbTracking
212212
[self testBooleanField:@"enableAutoBreadcrumbTracking"];
213213
}
214214

215+
- (void)testEnablePropagateTraceparent
216+
{
217+
[self testBooleanField:@"enablePropagateTraceparent" defaultValue:NO];
218+
}
219+
215220
- (void)testEnableCoreDataTracking
216221
{
217222
[self testBooleanField:@"enableCoreDataTracing" defaultValue:YES];

0 commit comments

Comments
 (0)