From 791fe6d5e05943bb96c49768af06be2a01d32ddb Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 16 Sep 2025 13:30:54 +0100 Subject: [PATCH 1/5] Unify trace context injection behaviour --- .../B3/B3HTTPHeadersReader.swift | 2 +- .../B3/B3HTTPHeadersWriter.swift | 57 +++++++------- .../Datadog/HTTPHeadersWriter.swift | 52 +++++++------ .../W3C/W3CHTTPHeadersWriter.swift | 78 ++++++++++--------- .../B3HTTPHeadersReaderTests.swift | 5 +- .../B3HTTPHeadersWriterTests.swift | 20 ++--- 6 files changed, 114 insertions(+), 100 deletions(-) diff --git a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift index 4d2ce0f0d7..5d51c522d4 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift @@ -45,7 +45,7 @@ public class B3HTTPHeadersReader: TracePropagationHeadersReader { public var sampled: Bool? { if let single = httpHeaderFields[B3HTTPHeaders.Single.b3Field] { - return single != B3HTTPHeaders.Constants.unsampledValue + return single.components(separatedBy: B3HTTPHeaders.Constants.b3Separator)[safe: 2] != B3HTTPHeaders.Constants.unsampledValue } else if let multiple = httpHeaderFields[B3HTTPHeaders.Multiple.sampledField] { return multiple == B3HTTPHeaders.Constants.sampledValue } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift index bd64405ee9..522ce7c634 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift @@ -78,39 +78,38 @@ public class B3HTTPHeadersWriter: TracePropagationHeadersWriter { /// - Parameter spanID: The span ID. /// - Parameter parentSpanID: The parent span ID, if applicable. public func write(traceContext: TraceContext) { - let sampled = traceContext.isKept - typealias Constants = B3HTTPHeaders.Constants - switch (traceContextInjection, sampled) { - case (.all, _), (.sampled, true): - switch injectEncoding { - case .multiple: - traceHeaderFields = [ - B3HTTPHeaders.Multiple.sampledField: sampled ? Constants.sampledValue : Constants.unsampledValue - ] + let sampled = traceContext.isKept + let shouldInject: Bool = { + switch traceContextInjection { + case .all: return true + case .sampled: return sampled + } + }() + guard shouldInject else { + return + } - if sampled { - traceHeaderFields[B3HTTPHeaders.Multiple.traceIDField] = String(traceContext.traceID, representation: .hexadecimal32Chars) - traceHeaderFields[B3HTTPHeaders.Multiple.spanIDField] = String(traceContext.spanID, representation: .hexadecimal16Chars) - traceHeaderFields[B3HTTPHeaders.Multiple.parentSpanIDField] = traceContext.parentSpanID.map { String($0, representation: .hexadecimal16Chars) } - } - case .single: - if sampled { - traceHeaderFields[B3HTTPHeaders.Single.b3Field] = [ - String(traceContext.traceID, representation: .hexadecimal32Chars), - String(traceContext.spanID, representation: .hexadecimal16Chars), - sampled ? Constants.sampledValue : Constants.unsampledValue, - traceContext.parentSpanID.map { String($0, representation: .hexadecimal16Chars) } - ] - .compactMap { $0 } - .joined(separator: Constants.b3Separator) - } else { - traceHeaderFields[B3HTTPHeaders.Single.b3Field] = Constants.unsampledValue - } + switch injectEncoding { + case .multiple: + traceHeaderFields = [ + B3HTTPHeaders.Multiple.sampledField: sampled ? Constants.sampledValue : Constants.unsampledValue, + B3HTTPHeaders.Multiple.traceIDField: String(traceContext.traceID, representation: .hexadecimal32Chars), + B3HTTPHeaders.Multiple.spanIDField: String(traceContext.spanID, representation: .hexadecimal16Chars), + ] + if let parentSpanId = traceContext.parentSpanID.map({ String($0, representation: .hexadecimal16Chars) }) { + traceHeaderFields[B3HTTPHeaders.Multiple.parentSpanIDField] = parentSpanId } - case (.sampled, false): - break + case .single: + traceHeaderFields[B3HTTPHeaders.Single.b3Field] = [ + String(traceContext.traceID, representation: .hexadecimal32Chars), + String(traceContext.spanID, representation: .hexadecimal16Chars), + sampled ? Constants.sampledValue : Constants.unsampledValue, + traceContext.parentSpanID.map { String($0, representation: .hexadecimal16Chars) } + ] + .compactMap { $0 } + .joined(separator: Constants.b3Separator) } } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift index da132f115f..b256266d5f 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift @@ -49,29 +49,37 @@ public class HTTPHeadersWriter: TracePropagationHeadersWriter { /// - Parameter spanID: The span ID. /// - Parameter parentSpanID: The parent span ID, if applicable. public func write(traceContext: TraceContext) { - switch (traceContextInjection, traceContext.isKept) { - case (.all, _), (.sampled, true): - traceHeaderFields = [ - TracingHTTPHeaders.samplingPriorityField: traceContext.isKept ? "1" : "0" - ] - traceHeaderFields[TracingHTTPHeaders.traceIDField] = String(traceContext.traceID.idLo) - traceHeaderFields[TracingHTTPHeaders.parentSpanIDField] = String(traceContext.spanID, representation: .decimal) - traceHeaderFields[TracingHTTPHeaders.tagsField] = "_dd.p.tid=\(traceContext.traceID.idHiHex)" - var baggageItems: [String] = [] - if let sessionId = traceContext.rumSessionId { - baggageItems.append("\(W3CHTTPHeaders.Constants.rumSessionBaggageKey)=\(sessionId)") - } - if let userId = traceContext.userId { - baggageItems.append("\(W3CHTTPHeaders.Constants.userBaggageKey)=\(userId)") - } - if let accountId = traceContext.accountId { - baggageItems.append("\(W3CHTTPHeaders.Constants.accountBaggageKey)=\(accountId)") - } - if baggageItems.isEmpty == false { - traceHeaderFields[W3CHTTPHeaders.baggage] = baggageItems.joined(separator: ",") + typealias Constants = W3CHTTPHeaders.Constants + + let sampled = traceContext.isKept + let shouldInject: Bool = { + switch traceContextInjection { + case .all: return true + case .sampled: return sampled } - case (.sampled, false): - break + }() + guard shouldInject else { + return + } + + traceHeaderFields = [ + TracingHTTPHeaders.samplingPriorityField: traceContext.isKept ? "1" : "0" + ] + traceHeaderFields[TracingHTTPHeaders.traceIDField] = String(traceContext.traceID.idLo) + traceHeaderFields[TracingHTTPHeaders.parentSpanIDField] = String(traceContext.spanID, representation: .decimal) + traceHeaderFields[TracingHTTPHeaders.tagsField] = "_dd.p.tid=\(traceContext.traceID.idHiHex)" + var baggageItems: [String] = [] + if let sessionId = traceContext.rumSessionId { + baggageItems.append("\(W3CHTTPHeaders.Constants.rumSessionBaggageKey)=\(sessionId)") + } + if let userId = traceContext.userId { + baggageItems.append("\(W3CHTTPHeaders.Constants.userBaggageKey)=\(userId)") + } + if let accountId = traceContext.accountId { + baggageItems.append("\(W3CHTTPHeaders.Constants.accountBaggageKey)=\(accountId)") + } + if baggageItems.isEmpty == false { + traceHeaderFields[W3CHTTPHeaders.baggage] = baggageItems.joined(separator: ",") } } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift index eaef396f6c..a95d4a6e88 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift @@ -61,48 +61,52 @@ public class W3CHTTPHeadersWriter: TracePropagationHeadersWriter { typealias Constants = W3CHTTPHeaders.Constants let sampled = traceContext.isKept + let shouldInject: Bool = { + switch traceContextInjection { + case .all: return true + case .sampled: return sampled + } + }() + guard shouldInject else { + return + } - switch (traceContextInjection, sampled) { - case (.all, _), (.sampled, true): - traceHeaderFields[W3CHTTPHeaders.traceparent] = [ - Constants.version, - String(traceContext.traceID, representation: .hexadecimal32Chars), - String(traceContext.spanID, representation: .hexadecimal16Chars), - sampled ? Constants.sampledValue : Constants.unsampledValue - ] - .joined(separator: Constants.separator) + traceHeaderFields[W3CHTTPHeaders.traceparent] = [ + Constants.version, + String(traceContext.traceID, representation: .hexadecimal32Chars), + String(traceContext.spanID, representation: .hexadecimal16Chars), + sampled ? Constants.sampledValue : Constants.unsampledValue + ] + .joined(separator: Constants.separator) - // while merging, the tracestate values from the tracestate property take precedence - // over the ones from the trace context - let tracestate: [String: String] = [ - Constants.sampling: "\(sampled ? 1 : 0)", - Constants.parentId: String(traceContext.spanID, representation: .hexadecimal16Chars) - ].merging(tracestate) { old, new in - return new - } + // while merging, the tracestate values from the tracestate property take precedence + // over the ones from the trace context + let tracestate: [String: String] = [ + Constants.sampling: "\(sampled ? 1 : 0)", + Constants.parentId: String(traceContext.spanID, representation: .hexadecimal16Chars) + ].merging(tracestate) { old, new in + return new + } - let ddtracestate = tracestate - .map { "\($0.key)\(Constants.tracestateKeyValueSeparator)\($0.value)" } - .sorted() - .joined(separator: Constants.tracestatePairSeparator) + let ddtracestate = tracestate + .map { "\($0.key)\(Constants.tracestateKeyValueSeparator)\($0.value)" } + .sorted() + .joined(separator: Constants.tracestatePairSeparator) - traceHeaderFields[W3CHTTPHeaders.tracestate] = "\(Constants.dd)=\(ddtracestate)" + traceHeaderFields[W3CHTTPHeaders.tracestate] = "\(Constants.dd)=\(ddtracestate)" - var baggageItems: [String] = [] - if let sessionId = traceContext.rumSessionId { - baggageItems.append("\(Constants.rumSessionBaggageKey)=\(sessionId)") - } - if let userId = traceContext.userId { - baggageItems.append("\(Constants.userBaggageKey)=\(userId)") - } - if let accountId = traceContext.accountId { - baggageItems.append("\(Constants.accountBaggageKey)=\(accountId)") - } - if !baggageItems.isEmpty { - traceHeaderFields[W3CHTTPHeaders.baggage] = baggageItems.joined(separator: ",") - } - case (.sampled, false): - break + var baggageItems: [String] = [] + if let sessionId = traceContext.rumSessionId { + baggageItems.append("\(Constants.rumSessionBaggageKey)=\(sessionId)") + } + if let userId = traceContext.userId { + baggageItems.append("\(Constants.userBaggageKey)=\(userId)") + } + if let accountId = traceContext.accountId { + baggageItems.append("\(Constants.accountBaggageKey)=\(accountId)") + } + if !baggageItems.isEmpty { + traceHeaderFields[W3CHTTPHeaders.baggage] = baggageItems.joined(separator: ",") } } } diff --git a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift index eda054462f..41f4f8df4f 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift @@ -95,7 +95,10 @@ class B3HTTPHeadersReaderTests: XCTestCase { writer.write(traceContext: .mockWith(isKept: false)) let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) - XCTAssertNil(reader.read(), "When not sampled, it should return no trace context") + let ids = reader.read() + XCTAssertEqual(ids?.traceID, 0) + XCTAssertEqual(ids?.spanID, 0) + XCTAssertNil(ids?.parentSpanID) XCTAssertEqual(reader.sampled, false) } diff --git a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift index 3fdc33f71c..76e6c6c9a6 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift @@ -44,13 +44,13 @@ class B3HTTPHeadersWriterTests: XCTestCase { ) let headers = writer.traceHeaderFields - XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "0") + XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "00000000000004d200000000000004d2-0000000000000929-0-000000000000162e") } func testWritingSampledTraceContext_withSingleEncoding_andCustomSamplingStrategy() { let writer = B3HTTPHeadersWriter( injectEncoding: .single, - traceContextInjection: .all + traceContextInjection: .sampled ) writer.write( @@ -69,7 +69,7 @@ class B3HTTPHeadersWriterTests: XCTestCase { func testWritingDroppedTraceContext_withSingleEncoding_andCustomSamplingStrategy() { let writer = B3HTTPHeadersWriter( injectEncoding: .single, - traceContextInjection: .all + traceContextInjection: .sampled ) writer.write( @@ -82,7 +82,7 @@ class B3HTTPHeadersWriterTests: XCTestCase { ) let headers = writer.traceHeaderFields - XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "0") + XCTAssertNil(headers[B3HTTPHeaders.Single.b3Field]) } func testItWritesSingleHeaderWithoutOptionalValues() { @@ -141,10 +141,10 @@ class B3HTTPHeadersWriterTests: XCTestCase { ) let headers = writer.traceHeaderFields - XCTAssertNil(headers[B3HTTPHeaders.Multiple.traceIDField]) - XCTAssertNil(headers[B3HTTPHeaders.Multiple.spanIDField]) + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.traceIDField], "00000000000004d200000000000004d2") + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.spanIDField], "0000000000000929") XCTAssertEqual(headers[B3HTTPHeaders.Multiple.sampledField], "0") - XCTAssertNil(headers[B3HTTPHeaders.Multiple.parentSpanIDField]) + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.parentSpanIDField], "000000000000162e") } func testWritingSampledTraceContext_withMultipleEncoding_andCustomSamplingStrategy() { @@ -185,10 +185,10 @@ class B3HTTPHeadersWriterTests: XCTestCase { ) let headers = writer.traceHeaderFields - XCTAssertNil(headers[B3HTTPHeaders.Multiple.traceIDField]) - XCTAssertNil(headers[B3HTTPHeaders.Multiple.spanIDField]) + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.traceIDField], "00000000000004d200000000000004d2") + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.spanIDField], "0000000000000929") XCTAssertEqual(headers[B3HTTPHeaders.Multiple.sampledField], "0") - XCTAssertNil(headers[B3HTTPHeaders.Multiple.parentSpanIDField]) + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.parentSpanIDField], "000000000000162e") } func testItWritesMultipleHeaderWithoutOptionalValues() { From 4957d0a4e667bf675556b268b068a5e550b8a9fa Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 16 Sep 2025 13:50:27 +0100 Subject: [PATCH 2/5] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4e087992d..7f8eb2e621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - [IMPROVEMENT] Add missing `versionMajor` property to the `DDLogEventOperatingSystem` definition in Objective-C. See [#2463][] - [IMPROVEMENT] Add `ddtags` to RUM events. See [#2436][] - [FIX] Fix `LogEvent` device types. See [#2474][] +- [BUGFIX] Fix tracing header injection for sampled out requests. See [#2473][] # 3.0.0 / 02-09-2025 @@ -967,6 +968,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#2436]: https://github.com/DataDog/dd-sdk-ios/pull/2436 [#2469]: https://github.com/DataDog/dd-sdk-ios/pull/2469 [#2474]: https://github.com/DataDog/dd-sdk-ios/pull/2474 +[#2473]: https://github.com/DataDog/dd-sdk-ios/pull/2473 [@00fa9a]: https://github.com/00FA9A [@britton-earnin]: https://github.com/Britton-Earnin From 6f9c61b8043617947763a0e8cbbdf22f6d8ff4f8 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 16 Sep 2025 14:17:09 +0100 Subject: [PATCH 3/5] Fix linter issues --- .../NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift | 4 ++-- .../NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift index b256266d5f..d7ff11d6a9 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift @@ -54,8 +54,8 @@ public class HTTPHeadersWriter: TracePropagationHeadersWriter { let sampled = traceContext.isKept let shouldInject: Bool = { switch traceContextInjection { - case .all: return true - case .sampled: return sampled + case .all: return true + case .sampled: return sampled } }() guard shouldInject else { diff --git a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift index a95d4a6e88..780c08627e 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift @@ -63,8 +63,8 @@ public class W3CHTTPHeadersWriter: TracePropagationHeadersWriter { let sampled = traceContext.isKept let shouldInject: Bool = { switch traceContextInjection { - case .all: return true - case .sampled: return sampled + case .all: return true + case .sampled: return sampled } }() guard shouldInject else { From dccd67a8d11afcd962f69a5794ad719b3f0c3e1c Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Tue, 16 Sep 2025 14:25:54 +0100 Subject: [PATCH 4/5] Update unit tests --- DatadogCore/Tests/Objc/DDTracerTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DatadogCore/Tests/Objc/DDTracerTests.swift b/DatadogCore/Tests/Objc/DDTracerTests.swift index 315c887358..8f3b236ba0 100644 --- a/DatadogCore/Tests/Objc/DDTracerTests.swift +++ b/DatadogCore/Tests/Objc/DDTracerTests.swift @@ -284,7 +284,7 @@ class DDTracerTests: XCTestCase { try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ - "b3": "0", + "b3": "000000000000000a0000000000000064-00000000000000c8-0-0000000000000000", ] XCTAssertEqual(objcWriter.traceHeaderFields, expectedHTTPHeaders) } From 81e6bf7875d1f558fbd5dbf5ec2dc4bb98a9b7a4 Mon Sep 17 00:00:00 2001 From: Maciej Burda Date: Thu, 18 Sep 2025 12:04:56 +0100 Subject: [PATCH 5/5] Fix tests --- .../Resources/URLSessionRUMResourcesHandlerTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DatadogRUM/Tests/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift b/DatadogRUM/Tests/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift index 64d2fa31c9..5f9f99b09c 100644 --- a/DatadogRUM/Tests/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift +++ b/DatadogRUM/Tests/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift @@ -236,7 +236,7 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.originField)) - XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "0") + XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "000000000000000a0000000000000064-0000000000000064-0") XCTAssertNil(traceContext, "It must return no trace context") } @@ -266,8 +266,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.originField)) - XCTAssertNil(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField)) - XCTAssertNil(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField)) + XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField), "000000000000000a0000000000000064") + XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField), "0000000000000064") XCTAssertNil(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.parentSpanIDField)) XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField), "0")