Skip to content

Commit 957c698

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 3f3c1fe commit 957c698

File tree

11 files changed

+165
-20
lines changed

11 files changed

+165
-20
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Removes `enablePerformanceV2` option and makes this the default. The app start d
1010
### Features
1111

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

1415
### Fixes
1516

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 propagateTraceparent is @c NO and the request matches @c
708+
* tracePropagationTargets.
709+
*
710+
* @note Default value is @c YES.
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/Public/SentryTraceHeader.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
NS_ASSUME_NONNULL_BEGIN
1414

1515
static NSString *const SENTRY_TRACE_HEADER = @"sentry-trace";
16+
static NSString *const SENTRY_TRACEPARENT = @"traceparent";
1617

1718
NS_SWIFT_NAME(TraceHeader)
1819
@interface SentryTraceHeader : NSObject
@@ -46,6 +47,8 @@ SENTRY_NO_INIT
4647
*/
4748
- (NSString *)value;
4849

50+
- (NSString *)traceParentValue;
51+
4952
@end
5053

5154
NS_ASSUME_NONNULL_END

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/SentryTraceHeader.m

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ - (NSString *)value
3030
: [NSString stringWithFormat:@"%@-%@", _traceId.sentryIdString, _spanId.sentrySpanIdString];
3131
}
3232

33+
- (NSString *)traceParentValue
34+
{
35+
return [NSString stringWithFormat:@"00-%@-%@-0%i", _traceId.sentryIdString,
36+
_spanId.sentrySpanIdString, _sampled == kSentrySampleDecisionYes ? 1 : 0];
37+
}
38+
3339
@end
3440

3541
NS_ASSUME_NONNULL_END

Sources/Sentry/SentryTracePropagation.m

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ @implementation SentryTracePropagation
88

99
+ (void)addBaggageHeader:(SentryBaggage *)baggage
1010
traceHeader:(SentryTraceHeader *)traceHeader
11+
propagateTraceparent:(BOOL)propagateTraceparent
1112
tracePropagationTargets:(NSArray *)tracePropagationTargets
1213
toRequest:(NSURLSessionTask *)sessionTask
1314
{
@@ -33,14 +34,10 @@ + (void)addBaggageHeader:(SentryBaggage *)baggage
3334
// header.
3435
if ([sessionTask.currentRequest isKindOfClass:[NSMutableURLRequest class]]) {
3536
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-
}
37+
[SentryTracePropagation addHeaderFieldsToRequest:currentRequest
38+
traceHeader:traceHeader
39+
baggageHeader:baggageHeader
40+
propagateTraceparent:propagateTraceparent];
4441
} else {
4542
// Even though NSURLSessionTask doesn't have 'setCurrentRequest', some subclasses
4643
// do. For those subclasses we replace the currentRequest with a mutable one with
@@ -49,14 +46,10 @@ + (void)addBaggageHeader:(SentryBaggage *)baggage
4946
SEL setCurrentRequestSelector = NSSelectorFromString(@"setCurrentRequest:");
5047
if ([sessionTask respondsToSelector:setCurrentRequestSelector]) {
5148
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-
}
49+
[SentryTracePropagation addHeaderFieldsToRequest:newRequest
50+
traceHeader:traceHeader
51+
baggageHeader:baggageHeader
52+
propagateTraceparent:propagateTraceparent];
6053

6154
void (*func)(id, SEL, id param)
6255
= (void *)[sessionTask methodForSelector:setCurrentRequestSelector];
@@ -73,6 +66,24 @@ + (BOOL)sessionTaskRequiresPropagation:(NSURLSessionTask *)sessionTask
7366
withTargets:tracePropagationTargets];
7467
}
7568

69+
+ (void)addHeaderFieldsToRequest:(NSMutableURLRequest *)request
70+
traceHeader:(SentryTraceHeader *)traceHeader
71+
baggageHeader:(NSString *)baggageHeader
72+
propagateTraceparent:(BOOL)propagateTraceparent
73+
{
74+
if ([request valueForHTTPHeaderField:SENTRY_TRACE_HEADER] == nil) {
75+
[request setValue:traceHeader.value forHTTPHeaderField:SENTRY_TRACE_HEADER];
76+
}
77+
78+
if (propagateTraceparent && [request valueForHTTPHeaderField:SENTRY_TRACEPARENT] == nil) {
79+
[request setValue:traceHeader.traceParentValue forHTTPHeaderField:SENTRY_TRACEPARENT];
80+
}
81+
82+
if (baggageHeader.length > 0) {
83+
[request setValue:baggageHeader forHTTPHeaderField:SENTRY_BAGGAGE_HEADER];
84+
}
85+
}
86+
7687
+ (BOOL)isTargetMatch:(NSURL *)URL withTargets:(NSArray *)targets
7788
{
7889
for (id targetCheck in targets) {

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: 43 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,48 @@ 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+
let expectedTraceHeader = networkSpan.toTraceHeader().traceParentValue()
932+
XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["traceparent"] ?? "", expectedTraceHeader)
933+
}
934+
935+
func testPropagateTraceparent_WhenDisabled_NotAdded() throws {
936+
// Arrange
937+
let sut = fixture.getSut()
938+
let task = createDataTask()
939+
_ = try XCTUnwrap(startTransaction() as? SentryTracer)
940+
fixture.options.enablePropagateTraceparent = false
941+
942+
// Act
943+
sut.urlSessionTaskResume(task)
944+
945+
// Assert
946+
XCTAssertNil(task.currentRequest?.allHTTPHeaderFields?["traceparent"])
947+
}
948+
949+
func testDontOverrideTraceparent() {
950+
let sut = fixture.getSut()
951+
let task = createDataTask {
952+
var request = $0
953+
request.setValue("test", forHTTPHeaderField: "traceparent")
954+
return request
955+
}
956+
sut.urlSessionTaskResume(task)
957+
958+
XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["traceparent"] ?? "", "test")
959+
}
960+
918961
@available(*, deprecated)
919962
func testDefaultHeadersWhenDisabled() throws {
920963
let sut = fixture.getSut()

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,58 @@ 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_NotAddedWhenTargetDoesntMatch() throws {
42+
// Arrange
43+
let emptyBaggage = Baggage()
44+
let sessionTask = try createSessionTask()
45+
46+
let traceID = SentryId()
47+
let spanID = SpanId()
48+
let traceHeader = TraceHeader(trace: traceID, spanId: spanID, sampled: SentrySampleDecision.no)
49+
50+
// Act
51+
SentryTracePropagation.addBaggageHeader(emptyBaggage, traceHeader: traceHeader, propagateTraceparent: true, tracePropagationTargets: ["localhost"], toRequest: sessionTask)
52+
53+
// Assert
54+
XCTAssertNil(sessionTask.currentRequest?.allHTTPHeaderFields?["traceparent"])
55+
}
56+
557
func testIsTargetMatchWithDefaultRegex_MatchesAllURLs() throws {
658
// Arrange
759
let defaultRegex = try XCTUnwrap(NSRegularExpression(pattern: ".*"))
@@ -77,4 +129,11 @@ final class SentryTracePropagationTests: XCTestCase {
77129
XCTAssertTrue(SentryTracePropagation.isTargetMatch(localhostURL, withTargets: targetsWithInvalidType))
78130
}
79131

132+
private func createSessionTask(method: String = "GET") throws -> URLSessionDownloadTaskMock {
133+
let url = try XCTUnwrap(URL(string: "https://www.domain.com/api?query=value&query2=value2#fragment"))
134+
var request = URLRequest(url: url)
135+
request.httpMethod = method
136+
return URLSessionDownloadTaskMock(request: request)
137+
}
138+
80139
}

0 commit comments

Comments
 (0)