diff --git a/CHANGELOG.md b/CHANGELOG.md index 54e8655ac2..aea404baf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ ### Features - Add SentryDistribution as Swift Package Manager target (#6149) +- Add option `enablePropagateTraceparent` to support OTel/W3C trace propagation (#6356) ### Fixes diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index c35b78676b..81c54501a5 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -695,6 +695,18 @@ typedef void (^SentryProfilingConfigurationBlock)(SentryProfileOptions *_Nonnull */ @property (nonatomic, assign) BOOL enableAutoBreadcrumbTracking; +/** + * When enabled, the SDK propagates the W3C Trace Context HTTP header traceparent on outgoing HTTP + * requests. + * + * @discussion This is useful when the receiving services only support OTel/W3C propagation. The + * traceparent header is only sent when this option is @c YES and the request matches @c + * tracePropagationTargets. + * + * @note Default value is @c NO. + */ +@property (nonatomic, assign) BOOL enablePropagateTraceparent; + /** * An array of hosts or regexes that determines if outgoing HTTP requests will get * extra @c trace_id and @c baggage headers added. diff --git a/Sources/Sentry/SentryNetworkTracker.m b/Sources/Sentry/SentryNetworkTracker.m index 798ee733ac..4dc36d26d1 100644 --- a/Sources/Sentry/SentryNetworkTracker.m +++ b/Sources/Sentry/SentryNetworkTracker.m @@ -189,10 +189,12 @@ - (void)urlSessionTaskResume:(NSURLSessionTask *)sessionTask } SentryBaggage *baggage = [[[SentryTracer getTracer:span] traceContext] toBaggage]; - [SentryTracePropagation addBaggageHeader:baggage - traceHeader:[netSpan toTraceHeader] - tracePropagationTargets:SentrySDKInternal.options.tracePropagationTargets - toRequest:sessionTask]; + [SentryTracePropagation + addBaggageHeader:baggage + traceHeader:[netSpan toTraceHeader] + propagateTraceparent:SentrySDKInternal.options.enablePropagateTraceparent + tracePropagationTargets:SentrySDKInternal.options.tracePropagationTargets + toRequest:sessionTask]; SENTRY_LOG_DEBUG( @"SentryNetworkTracker automatically started HTTP span for sessionTask: %@", @@ -226,6 +228,7 @@ - (void)addTraceWithoutTransactionToTask:(NSURLSessionTask *)sessionTask [SentryTracePropagation addBaggageHeader:[traceContext toBaggage] traceHeader:[propagationContext traceHeader] + propagateTraceparent:SentrySDKInternal.options.enablePropagateTraceparent tracePropagationTargets:SentrySDKInternal.options.tracePropagationTargets toRequest:sessionTask]; } diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index e57f301797..365cad45a9 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -114,6 +114,7 @@ - (instancetype)init self.enableAppHangTracking = YES; self.appHangTimeoutInterval = 2.0; self.enableAutoBreadcrumbTracking = YES; + self.enablePropagateTraceparent = NO; self.enableNetworkTracking = YES; self.enableFileIOTracing = YES; self.enableNetworkBreadcrumbs = YES; diff --git a/Sources/Sentry/SentryTracePropagation.m b/Sources/Sentry/SentryTracePropagation.m index 53400634e2..ef366cb23e 100644 --- a/Sources/Sentry/SentryTracePropagation.m +++ b/Sources/Sentry/SentryTracePropagation.m @@ -4,10 +4,13 @@ #import #import +static NSString *const SENTRY_TRACEPARENT = @"traceparent"; + @implementation SentryTracePropagation + (void)addBaggageHeader:(SentryBaggage *)baggage traceHeader:(SentryTraceHeader *)traceHeader + propagateTraceparent:(BOOL)propagateTraceparent tracePropagationTargets:(NSArray *)tracePropagationTargets toRequest:(NSURLSessionTask *)sessionTask { @@ -33,14 +36,10 @@ + (void)addBaggageHeader:(SentryBaggage *)baggage // header. if ([sessionTask.currentRequest isKindOfClass:[NSMutableURLRequest class]]) { NSMutableURLRequest *currentRequest = (NSMutableURLRequest *)sessionTask.currentRequest; - - if ([currentRequest valueForHTTPHeaderField:SENTRY_TRACE_HEADER] == nil) { - [currentRequest setValue:traceHeader.value forHTTPHeaderField:SENTRY_TRACE_HEADER]; - } - - if (baggageHeader.length > 0) { - [currentRequest setValue:baggageHeader forHTTPHeaderField:SENTRY_BAGGAGE_HEADER]; - } + [SentryTracePropagation addHeaderFieldsToRequest:currentRequest + traceHeader:traceHeader + baggageHeader:baggageHeader + propagateTraceparent:propagateTraceparent]; } else { // Even though NSURLSessionTask doesn't have 'setCurrentRequest', some subclasses // do. For those subclasses we replace the currentRequest with a mutable one with @@ -49,14 +48,10 @@ + (void)addBaggageHeader:(SentryBaggage *)baggage SEL setCurrentRequestSelector = NSSelectorFromString(@"setCurrentRequest:"); if ([sessionTask respondsToSelector:setCurrentRequestSelector]) { NSMutableURLRequest *newRequest = [sessionTask.currentRequest mutableCopy]; - - if ([newRequest valueForHTTPHeaderField:SENTRY_TRACE_HEADER] == nil) { - [newRequest setValue:traceHeader.value forHTTPHeaderField:SENTRY_TRACE_HEADER]; - } - - if (baggageHeader.length > 0) { - [newRequest setValue:baggageHeader forHTTPHeaderField:SENTRY_BAGGAGE_HEADER]; - } + [SentryTracePropagation addHeaderFieldsToRequest:newRequest + traceHeader:traceHeader + baggageHeader:baggageHeader + propagateTraceparent:propagateTraceparent]; void (*func)(id, SEL, id param) = (void *)[sessionTask methodForSelector:setCurrentRequestSelector]; @@ -73,6 +68,29 @@ + (BOOL)sessionTaskRequiresPropagation:(NSURLSessionTask *)sessionTask withTargets:tracePropagationTargets]; } ++ (void)addHeaderFieldsToRequest:(NSMutableURLRequest *)request + traceHeader:(SentryTraceHeader *)traceHeader + baggageHeader:(NSString *)baggageHeader + propagateTraceparent:(BOOL)propagateTraceparent +{ + if ([request valueForHTTPHeaderField:SENTRY_TRACE_HEADER] == nil) { + [request setValue:traceHeader.value forHTTPHeaderField:SENTRY_TRACE_HEADER]; + } + + if (propagateTraceparent && [request valueForHTTPHeaderField:SENTRY_TRACEPARENT] == nil) { + + NSString *traceparent = [NSString stringWithFormat:@"00-%@-%@-%02x", + traceHeader.traceId.sentryIdString, traceHeader.spanId.sentrySpanIdString, + traceHeader.sampled == kSentrySampleDecisionYes ? 1 : 0]; + + [request setValue:traceparent forHTTPHeaderField:SENTRY_TRACEPARENT]; + } + + if (baggageHeader.length > 0) { + [request setValue:baggageHeader forHTTPHeaderField:SENTRY_BAGGAGE_HEADER]; + } +} + + (BOOL)isTargetMatch:(NSURL *)URL withTargets:(NSArray *)targets { for (id targetCheck in targets) { diff --git a/Sources/Sentry/SentyOptionsInternal.m b/Sources/Sentry/SentyOptionsInternal.m index 63315f6cfd..3c01d98759 100644 --- a/Sources/Sentry/SentyOptionsInternal.m +++ b/Sources/Sentry/SentyOptionsInternal.m @@ -386,6 +386,9 @@ + (BOOL)validateOptions:(NSDictionary *)options [self setBool:options[@"enableAutoBreadcrumbTracking"] block:^(BOOL value) { sentryOptions.enableAutoBreadcrumbTracking = value; }]; + [self setBool:options[@"enablePropagateTraceparent"] + block:^(BOOL value) { sentryOptions.enablePropagateTraceparent = value; }]; + if ([options[@"tracePropagationTargets"] isKindOfClass:[NSArray class]]) { sentryOptions.tracePropagationTargets = SENTRY_UNWRAP_NULLABLE(NSArray, options[@"tracePropagationTargets"]); diff --git a/Sources/Sentry/include/SentryTracePropagation.h b/Sources/Sentry/include/SentryTracePropagation.h index d99d18fe19..384e38adba 100644 --- a/Sources/Sentry/include/SentryTracePropagation.h +++ b/Sources/Sentry/include/SentryTracePropagation.h @@ -9,6 +9,7 @@ NS_ASSUME_NONNULL_BEGIN + (void)addBaggageHeader:(SentryBaggage *)baggage traceHeader:(SentryTraceHeader *)traceHeader + propagateTraceparent:(BOOL)propagateTraceparent tracePropagationTargets:(NSArray *)tracePropagationTargets toRequest:(NSURLSessionTask *)sessionTask; diff --git a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift index ed2cc0cdb7..29a9dfb25e 100644 --- a/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift +++ b/Tests/SentryTests/Integrations/Performance/Network/SentryNetworkTrackerTests.swift @@ -39,6 +39,7 @@ class SentryNetworkTrackerTests: XCTestCase { init() { options = Options() options.dsn = SentryNetworkTrackerTests.dsnAsString + options.enablePropagateTraceparent = true sentryTask = URLSessionDataTaskMock(request: URLRequest(url: URL(string: options.dsn!)!)) scope = Scope() client = TestClient(options: options) @@ -915,6 +916,50 @@ class SentryNetworkTrackerTests: XCTestCase { XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["sentry-trace"] ?? "", "test") } + func testPropagateTraceparent() throws { + // Arrange + let sut = fixture.getSut() + let task = createDataTask() + let transaction = try XCTUnwrap(startTransaction() as? SentryTracer) + + // Act + sut.urlSessionTaskResume(task) + + // Assert + let children = try XCTUnwrap(Dynamic(transaction).children.asArray as? [SentrySpan]) + let networkSpan = try XCTUnwrap(children.first) + + let traceHeader = transaction.toTraceHeader() + let expectedTraceHeader = "00-\(traceHeader.traceId.sentryIdString)-\(networkSpan.spanId.sentrySpanIdString)-00" + XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["traceparent"] ?? "", expectedTraceHeader) + } + + func testPropagateTraceparent_WhenDisabled_NotAdded() throws { + // Arrange + let sut = fixture.getSut() + let task = createDataTask() + _ = try XCTUnwrap(startTransaction() as? SentryTracer) + fixture.options.enablePropagateTraceparent = false + + // Act + sut.urlSessionTaskResume(task) + + // Assert + XCTAssertNil(task.currentRequest?.allHTTPHeaderFields?["traceparent"]) + } + + func testDontOverrideTraceparent() { + let sut = fixture.getSut() + let task = createDataTask { + var request = $0 + request.setValue("test", forHTTPHeaderField: "traceparent") + return request + } + sut.urlSessionTaskResume(task) + + XCTAssertEqual(task.currentRequest?.allHTTPHeaderFields?["traceparent"] ?? "", "test") + } + @available(*, deprecated) func testDefaultHeadersWhenDisabled() throws { let sut = fixture.getSut() diff --git a/Tests/SentryTests/Integrations/Performance/Network/SentryTracePropagationTests.swift b/Tests/SentryTests/Integrations/Performance/Network/SentryTracePropagationTests.swift index 983811ebe6..e12ced6aa4 100644 --- a/Tests/SentryTests/Integrations/Performance/Network/SentryTracePropagationTests.swift +++ b/Tests/SentryTests/Integrations/Performance/Network/SentryTracePropagationTests.swift @@ -2,6 +2,76 @@ import XCTest final class SentryTracePropagationTests: XCTestCase { + func testAddTraceparent_Sampled() throws { + // Arrange + let defaultRegex = try XCTUnwrap(NSRegularExpression(pattern: ".*")) + let emptyBaggage = Baggage() + let sessionTask = try createSessionTask() + + let traceID = SentryId() + let spanID = SpanId() + let traceHeader = TraceHeader(trace: traceID, spanId: spanID, sampled: SentrySampleDecision.yes) + + // Act + SentryTracePropagation.addBaggageHeader(emptyBaggage, traceHeader: traceHeader, propagateTraceparent: true, tracePropagationTargets: [defaultRegex], toRequest: sessionTask) + + // Assert + let traceParent = try XCTUnwrap(sessionTask.currentRequest?.allHTTPHeaderFields?["traceparent"]) + XCTAssertEqual(traceParent, "00-\(traceID.sentryIdString)-\(spanID.sentrySpanIdString)-01") + } + + func testAddTraceparent_NotSampled() throws { + // Arrange + let defaultRegex = try XCTUnwrap(NSRegularExpression(pattern: ".*")) + let emptyBaggage = Baggage() + let sessionTask = try createSessionTask() + + let traceID = SentryId() + let spanID = SpanId() + let traceHeader = TraceHeader(trace: traceID, spanId: spanID, sampled: SentrySampleDecision.no) + + // Act + SentryTracePropagation.addBaggageHeader(emptyBaggage, traceHeader: traceHeader, propagateTraceparent: true, tracePropagationTargets: [defaultRegex], toRequest: sessionTask) + + // Assert + let traceParent = try XCTUnwrap(sessionTask.currentRequest?.allHTTPHeaderFields?["traceparent"]) + XCTAssertEqual(traceParent, "00-\(traceID.sentryIdString)-\(spanID.sentrySpanIdString)-00") + } + + func testAddTraceparent_UndecidedSampled() throws { + // Arrange + let defaultRegex = try XCTUnwrap(NSRegularExpression(pattern: ".*")) + let emptyBaggage = Baggage() + let sessionTask = try createSessionTask() + + let traceID = SentryId() + let spanID = SpanId() + let traceHeader = TraceHeader(trace: traceID, spanId: spanID, sampled: SentrySampleDecision.undecided) + + // Act + SentryTracePropagation.addBaggageHeader(emptyBaggage, traceHeader: traceHeader, propagateTraceparent: true, tracePropagationTargets: [defaultRegex], toRequest: sessionTask) + + // Assert + let traceParent = try XCTUnwrap(sessionTask.currentRequest?.allHTTPHeaderFields?["traceparent"]) + XCTAssertEqual(traceParent, "00-\(traceID.sentryIdString)-\(spanID.sentrySpanIdString)-00") + } + + func testAddTraceparent_NotAddedWhenTargetDoesntMatch() throws { + // Arrange + let emptyBaggage = Baggage() + let sessionTask = try createSessionTask() + + let traceID = SentryId() + let spanID = SpanId() + let traceHeader = TraceHeader(trace: traceID, spanId: spanID, sampled: SentrySampleDecision.no) + + // Act + SentryTracePropagation.addBaggageHeader(emptyBaggage, traceHeader: traceHeader, propagateTraceparent: true, tracePropagationTargets: ["localhost"], toRequest: sessionTask) + + // Assert + XCTAssertNil(sessionTask.currentRequest?.allHTTPHeaderFields?["traceparent"]) + } + func testIsTargetMatchWithDefaultRegex_MatchesAllURLs() throws { // Arrange let defaultRegex = try XCTUnwrap(NSRegularExpression(pattern: ".*")) @@ -77,4 +147,11 @@ final class SentryTracePropagationTests: XCTestCase { XCTAssertTrue(SentryTracePropagation.isTargetMatch(localhostURL, withTargets: targetsWithInvalidType)) } + private func createSessionTask(method: String = "GET") throws -> URLSessionDownloadTaskMock { + let url = try XCTUnwrap(URL(string: "https://www.domain.com/api?query=value&query2=value2#fragment")) + var request = URLRequest(url: url) + request.httpMethod = method + return URLSessionDownloadTaskMock(request: request) + } + } diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 42db4dc1f6..91eedc5b79 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -212,6 +212,11 @@ - (void)testEnableAutoBreadcrumbTracking [self testBooleanField:@"enableAutoBreadcrumbTracking"]; } +- (void)testEnablePropagateTraceparent +{ + [self testBooleanField:@"enablePropagateTraceparent" defaultValue:NO]; +} + - (void)testEnableCoreDataTracking { [self testBooleanField:@"enableCoreDataTracing" defaultValue:YES]; diff --git a/sdk_api.json b/sdk_api.json index 2fd11b5fda..d506d91648 100644 --- a/sdk_api.json +++ b/sdk_api.json @@ -20583,6 +20583,89 @@ } ] }, + { + "kind": "Var", + "name": "enablePropagateTraceparent", + "printedName": "enablePropagateTraceparent", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Var", + "usr": "c:objc(cs)SentryOptions(py)enablePropagateTraceparent", + "moduleName": "Sentry", + "isOpen": true, + "objc_name": "enablePropagateTraceparent", + "declAttributes": [ + "ObjC", + "Dynamic" + ], + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Accessor", + "usr": "c:objc(cs)SentryOptions(im)enablePropagateTraceparent", + "moduleName": "Sentry", + "isOpen": true, + "objc_name": "enablePropagateTraceparent", + "declAttributes": [ + "DiscardableResult", + "ObjC", + "Dynamic" + ], + "accessorKind": "get" + }, + { + "kind": "Accessor", + "name": "Set", + "printedName": "Set()", + "children": [ + { + "kind": "TypeNameAlias", + "name": "Void", + "printedName": "Swift.Void", + "children": [ + { + "kind": "TypeNominal", + "name": "Void", + "printedName": "()" + } + ] + }, + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Accessor", + "usr": "c:objc(cs)SentryOptions(im)setEnablePropagateTraceparent:", + "moduleName": "Sentry", + "isOpen": true, + "objc_name": "setEnablePropagateTraceparent:", + "declAttributes": [ + "ObjC", + "Dynamic" + ], + "accessorKind": "set" + } + ] + }, { "kind": "Var", "name": "tracePropagationTargets",