Skip to content

Commit fe4d5d5

Browse files
Merge 532f4d6 into 3f3c1fe
2 parents 3f3c1fe + 532f4d6 commit fe4d5d5

File tree

10 files changed

+186
-20
lines changed

10 files changed

+186
-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 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-%@-%@-0%i",
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)