From 3b4f3a18814dc78fa0fd51d2ebc03dbe4b5b686b Mon Sep 17 00:00:00 2001 From: Valentin Pertuisot Date: Fri, 6 Feb 2026 21:56:04 +0100 Subject: [PATCH 1/5] Move BaggageHeaderMerger from DatadogRUM to DatadogInternal BaggageHeaderMerger is needed by TracingURLSessionHandler for merging W3C baggage headers during trace context injection. Move it to DatadogInternal so it can be used across modules. Co-Authored-By: Claude Opus 4.6 --- Datadog/Datadog.xcodeproj/project.pbxproj | 24 +++++++++---------- .../W3C}/BaggageHeaderMerger.swift | 5 ++-- .../BaggageHeaderMergerTests.swift | 5 ++-- 3 files changed, 18 insertions(+), 16 deletions(-) rename {DatadogRUM/Sources/Instrumentation/Resources => DatadogInternal/Sources/NetworkInstrumentation/W3C}/BaggageHeaderMerger.swift (96%) rename {DatadogRUM/Tests/Instrumentation/Resources => DatadogInternal/Tests/NetworkInstrumentation}/BaggageHeaderMergerTests.swift (99%) diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 0014abe54e..dc9081da96 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -396,10 +396,6 @@ 1434A4662B7F8D880072E3BB /* DebugOTelTracingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1434A4652B7F8D880072E3BB /* DebugOTelTracingViewController.swift */; }; 1434A4672B7F8D880072E3BB /* DebugOTelTracingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1434A4652B7F8D880072E3BB /* DebugOTelTracingViewController.swift */; }; 144CDB9AFBDFF0B7EB10B4A3 /* EvaluationAggregatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AA681535D980BA99B12659B /* EvaluationAggregatorTests.swift */; }; - 261255872E2167E40015042B /* BaggageHeaderMerger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261255862E2167E40015042B /* BaggageHeaderMerger.swift */; }; - 261255882E2167E40015042B /* BaggageHeaderMerger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261255862E2167E40015042B /* BaggageHeaderMerger.swift */; }; - 2612558A2E2167F10015042B /* BaggageHeaderMergerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261255892E2167F10015042B /* BaggageHeaderMergerTests.swift */; }; - 2612558B2E2167F10015042B /* BaggageHeaderMergerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 261255892E2167F10015042B /* BaggageHeaderMergerTests.swift */; }; 265496D32D81C5B10094B6E2 /* RUMAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 265496D22D81C5AE0094B6E2 /* RUMAccount.swift */; }; 265496D42D81C5B10094B6E2 /* RUMAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 265496D22D81C5AE0094B6E2 /* RUMAccount.swift */; }; 266BFA5E2D6F4E31003041A5 /* AccountInfoPublisherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 266BFA5D2D6F4E2A003041A5 /* AccountInfoPublisherTests.swift */; }; @@ -412,6 +408,10 @@ 269035A32E41F94800F1A830 /* UserConfigurationContextMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269035A12E41F93F00F1A830 /* UserConfigurationContextMocks.swift */; }; 269035A52E41FAAC00F1A830 /* AccountConfigurationContextMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269035A42E41FAA500F1A830 /* AccountConfigurationContextMocks.swift */; }; 269035A62E41FAAC00F1A830 /* AccountConfigurationContextMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269035A42E41FAA500F1A830 /* AccountConfigurationContextMocks.swift */; }; + 26BE7EF62F362AE100583F0D /* BaggageHeaderMerger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26BE7EF52F362AE100583F0D /* BaggageHeaderMerger.swift */; }; + 26BE7EF72F362AE100583F0D /* BaggageHeaderMerger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26BE7EF52F362AE100583F0D /* BaggageHeaderMerger.swift */; }; + 26BE7EF92F362B0900583F0D /* BaggageHeaderMergerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26BE7EF82F362B0900583F0D /* BaggageHeaderMergerTests.swift */; }; + 26BE7EFA2F362B0900583F0D /* BaggageHeaderMergerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26BE7EF82F362B0900583F0D /* BaggageHeaderMergerTests.swift */; }; 27A047D86EB162480FF0C5AC /* EvaluationLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63A979F48771C3277A41396 /* EvaluationLogger.swift */; }; 3AFF4EF865EAECBA7BEDABDA /* FlagEvaluationEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D115C155135BEF011C5D0A3F /* FlagEvaluationEvent.swift */; }; 3C08F9D02C2D652D002B0FF2 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C08F9CF2C2D652D002B0FF2 /* Storage.swift */; }; @@ -2905,14 +2905,14 @@ 11F55FE82DCE501A00DE4944 /* Trace+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Trace+objc.swift"; sourceTree = ""; }; 11F59BC02EAE2180009F8579 /* LaunchInfoPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchInfoPublisher.swift; sourceTree = ""; }; 1434A4652B7F8D880072E3BB /* DebugOTelTracingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugOTelTracingViewController.swift; sourceTree = ""; }; - 261255862E2167E40015042B /* BaggageHeaderMerger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaggageHeaderMerger.swift; sourceTree = ""; }; - 261255892E2167F10015042B /* BaggageHeaderMergerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaggageHeaderMergerTests.swift; sourceTree = ""; }; 265496D22D81C5AE0094B6E2 /* RUMAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMAccount.swift; sourceTree = ""; }; 266BFA5D2D6F4E2A003041A5 /* AccountInfoPublisherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInfoPublisherTests.swift; sourceTree = ""; }; 2671348C2D688ACD0048CB54 /* AccountInfoPublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInfoPublisher.swift; sourceTree = ""; }; 2671348F2D688D230048CB54 /* AccountInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountInfo.swift; sourceTree = ""; }; 269035A12E41F93F00F1A830 /* UserConfigurationContextMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConfigurationContextMocks.swift; sourceTree = ""; }; 269035A42E41FAA500F1A830 /* AccountConfigurationContextMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountConfigurationContextMocks.swift; sourceTree = ""; }; + 26BE7EF52F362AE100583F0D /* BaggageHeaderMerger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaggageHeaderMerger.swift; sourceTree = ""; }; + 26BE7EF82F362B0900583F0D /* BaggageHeaderMergerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaggageHeaderMergerTests.swift; sourceTree = ""; }; 2B6A3B154836FEB32C07EB50 /* EvaluationAggregator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = EvaluationAggregator.swift; sourceTree = ""; }; 3C08F9CF2C2D652D002B0FF2 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; 3C0CB3442C19A1ED003B0E9B /* WatchdogTerminationReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchdogTerminationReporter.swift; sourceTree = ""; }; @@ -5903,7 +5903,6 @@ 613F23EF252B1287006CD2D7 /* Resources */ = { isa = PBXGroup; children = ( - 261255892E2167F10015042B /* BaggageHeaderMergerTests.swift */, D2BCB12129D34A5F00737A9A /* URLSessionRUMResourcesHandlerTests.swift */, ); path = Resources; @@ -6054,7 +6053,6 @@ 6157FA5C252767B3009A8A3B /* Resources */ = { isa = PBXGroup; children = ( - 261255862E2167E40015042B /* BaggageHeaderMerger.swift */, D2BCB11E29D30AF000737A9A /* URLSessionRUMResourcesHandler.swift */, ); path = Resources; @@ -6838,6 +6836,7 @@ A728AD9C2934CE4400397996 /* W3CHTTPHeaders.swift */, A728AD9E2934CE5000397996 /* W3CHTTPHeadersWriter.swift */, A728ADA02934CE5D00397996 /* W3CHTTPHeadersReader.swift */, + 26BE7EF52F362AE100583F0D /* BaggageHeaderMerger.swift */, ); path = W3C; sourceTree = ""; @@ -7749,6 +7748,7 @@ D2EBEE3A29BA162900B15732 /* NetworkInstrumentation */ = { isa = PBXGroup; children = ( + 26BE7EF82F362B0900583F0D /* BaggageHeaderMergerTests.swift */, D2160CCD29C0DF6700FAA9A5 /* NetworkInstrumentationFeatureTests.swift */, D2160CCF29C0DF6700FAA9A5 /* FirstPartyHostsTests.swift */, D2160CD129C0DF6700FAA9A5 /* HostsSanitizerTests.swift */, @@ -10410,6 +10410,7 @@ 618032042D6F1214007027E3 /* Assert.swift in Sources */, D2EBEE2429BA160F00B15732 /* W3CHTTPHeadersReader.swift in Sources */, A7FA98CE2BA1A6930018D6B5 /* MethodCalledMetric.swift in Sources */, + 26BE7EF72F362AE100583F0D /* BaggageHeaderMerger.swift in Sources */, D27465762E7867B700C47FE2 /* ProfilingContext.swift in Sources */, D23039E8298D5236001A1FA3 /* DatadogContext.swift in Sources */, D23039FF298D5236001A1FA3 /* Foundation+Datadog.swift in Sources */, @@ -10538,7 +10539,6 @@ 3C0D5DED2A54405A00446CF9 /* RUMViewEventsFilter.swift in Sources */, D23F8E7629DDCD28001CFAE8 /* RUMConnectivityInfoProvider.swift in Sources */, D23F8E7729DDCD28001CFAE8 /* UIKitRUMViewsPredicate.swift in Sources */, - 261255882E2167E40015042B /* BaggageHeaderMerger.swift in Sources */, 9629FFDE2D81C317008DFE39 /* SwiftUIViewPath.swift in Sources */, 111201C12E93C13000375DA3 /* AppStateManager.swift in Sources */, 111201C22E93C13000375DA3 /* AppStateInfo.swift in Sources */, @@ -10608,7 +10608,6 @@ 3CEC57782C16FDD80042B5F2 /* AppStateManagerTests.swift in Sources */, D23F8EAE29DDCD38001CFAE8 /* DDTAssertValidRUMUUID.swift in Sources */, D23F8EAF29DDCD38001CFAE8 /* RUMScopeTests.swift in Sources */, - 2612558A2E2167F10015042B /* BaggageHeaderMergerTests.swift in Sources */, A7E6EA842D314A9B00997201 /* AnonymousIdentifierManagerTests.swift in Sources */, D23F8EB029DDCD38001CFAE8 /* SessionReplayDependencyTests.swift in Sources */, 61C713B72A3C600400FA735A /* RUMMonitorProtocol+ConvenienceTests.swift in Sources */, @@ -11031,7 +11030,6 @@ 3C0D5DEC2A54405A00446CF9 /* RUMViewEventsFilter.swift in Sources */, D29A9F5829DD85BB005C54A4 /* RUMConnectivityInfoProvider.swift in Sources */, D29A9F5E29DD85BB005C54A4 /* UIKitRUMViewsPredicate.swift in Sources */, - 261255872E2167E40015042B /* BaggageHeaderMerger.swift in Sources */, 9629FFDF2D81C317008DFE39 /* SwiftUIViewPath.swift in Sources */, 111201C32E93C13000375DA3 /* AppStateManager.swift in Sources */, 111201C42E93C13000375DA3 /* AppStateInfo.swift in Sources */, @@ -11100,7 +11098,6 @@ 3CEC57772C16FDD70042B5F2 /* AppStateManagerTests.swift in Sources */, D29A9FCC29DDBCC5005C54A4 /* DDTAssertValidRUMUUID.swift in Sources */, D29A9FB329DDB483005C54A4 /* RUMScopeTests.swift in Sources */, - 2612558B2E2167F10015042B /* BaggageHeaderMergerTests.swift in Sources */, A7E6EA852D314A9B00997201 /* AnonymousIdentifierManagerTests.swift in Sources */, D29A9FAE29DDB483005C54A4 /* SessionReplayDependencyTests.swift in Sources */, 61C713B62A3C600400FA735A /* RUMMonitorProtocol+ConvenienceTests.swift in Sources */, @@ -11590,6 +11587,7 @@ 6167E6E92B8122E900C3CA2D /* BacktraceReport.swift in Sources */, 110B0ECC2DF0ABC6008ABA19 /* DeterministicSampler.swift in Sources */, D2BEEDB62B3360830065F3AC /* URLSessionSwizzler.swift in Sources */, + 26BE7EF62F362AE100583F0D /* BaggageHeaderMerger.swift in Sources */, D2EBEE2F29BA161100B15732 /* W3CHTTPHeaders.swift in Sources */, 6167E6F72B81E94C00C3CA2D /* DDThread.swift in Sources */, D2BEEDAD2B3356710065F3AC /* URLSessionTaskSwizzler.swift in Sources */, @@ -11732,6 +11730,7 @@ 0904F9F42EE1DA6800ED9A22 /* UIKitExtensionsTests.swift in Sources */, D2EBEE3B29BA163E00B15732 /* B3HTTPHeadersReaderTests.swift in Sources */, D2BEEDB82B3360F50065F3AC /* URLSessionTaskDelegateSwizzlerTests.swift in Sources */, + 26BE7EFA2F362B0900583F0D /* BaggageHeaderMergerTests.swift in Sources */, 3CCECDB22BC68A0A0013C125 /* SpanIDTests.swift in Sources */, D2181A8E2B051B7900A518C0 /* URLSessionSwizzlerTests.swift in Sources */, D2A783DA29A530EF003B03BB /* SwiftExtensionsTests.swift in Sources */, @@ -11750,6 +11749,7 @@ buildActionMask = 2147483647; files = ( D2BEEDB02B335C400065F3AC /* URLSessionTaskSwizzlerTests.swift in Sources */, + 26BE7EF92F362B0900583F0D /* BaggageHeaderMergerTests.swift in Sources */, 96E746AB2F30E535006B3419 /* AttributeEncodingTests.swift in Sources */, D26416B72A30E84F00BCD9F7 /* CoreRegistryTest.swift in Sources */, 61F3E3672BC595F600C7881E /* HTTPHeadersReaderTests.swift in Sources */, diff --git a/DatadogRUM/Sources/Instrumentation/Resources/BaggageHeaderMerger.swift b/DatadogInternal/Sources/NetworkInstrumentation/W3C/BaggageHeaderMerger.swift similarity index 96% rename from DatadogRUM/Sources/Instrumentation/Resources/BaggageHeaderMerger.swift rename to DatadogInternal/Sources/NetworkInstrumentation/W3C/BaggageHeaderMerger.swift index 875696018b..4aa0f941f8 100644 --- a/DatadogRUM/Sources/Instrumentation/Resources/BaggageHeaderMerger.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/W3C/BaggageHeaderMerger.swift @@ -14,13 +14,13 @@ import Foundation /// This includes the SDK-managed keys: "session.id", "user.id" and "account.id". /// - The formatted output is deterministic: keys are sorted lexicographically to stabilize header ordering /// (useful for debugging). -internal struct BaggageHeaderMerger { +public struct BaggageHeaderMerger { /// Merges two baggage header values, with new values taking precedence over existing ones. /// - Parameters: /// - previousHeader: The existing baggage header value /// - newHeader: The new baggage header value to merge /// - Returns: A merged baggage header value - static func merge(previousHeader: String, with newHeader: String) -> String { + public static func merge(previousHeader: String, with newHeader: String) -> String { guard previousHeader != newHeader else { return previousHeader } @@ -79,3 +79,4 @@ internal struct BaggageHeaderMerger { return (key: key, value: value) } } + diff --git a/DatadogRUM/Tests/Instrumentation/Resources/BaggageHeaderMergerTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/BaggageHeaderMergerTests.swift similarity index 99% rename from DatadogRUM/Tests/Instrumentation/Resources/BaggageHeaderMergerTests.swift rename to DatadogInternal/Tests/NetworkInstrumentation/BaggageHeaderMergerTests.swift index c9e2eac55b..b712b74189 100644 --- a/DatadogRUM/Tests/Instrumentation/Resources/BaggageHeaderMergerTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/BaggageHeaderMergerTests.swift @@ -5,8 +5,7 @@ */ import XCTest -import TestUtilities -@testable import DatadogRUM +@testable import DatadogInternal class BaggageHeaderMergerTests: XCTestCase { // MARK: - Basic Functionality Tests @@ -337,3 +336,5 @@ class BaggageHeaderMergerTests: XCTestCase { return dict } } + + From 9350eae9dc9b6441e8aeda94c27d3d334f2c097f Mon Sep 17 00:00:00 2001 From: Valentin Pertuisot Date: Fri, 6 Feb 2026 21:56:21 +0100 Subject: [PATCH 2/5] Add baggage header merging and use networkContext in TracingURLSessionHandler Two changes to the modify() method: 1. Merge W3C baggage headers instead of skipping them when already present on the request. This uses BaggageHeaderMerger to combine existing baggage values with SDK-injected ones (session.id, user.id, account.id), with new values taking precedence. 2. Use the networkContext parameter for populating rumSessionId, userId, and accountId in the TraceContext, falling back to contextReceiver.context. Previously networkContext was accepted but ignored, so these values were never propagated into baggage headers when provided via networkContext. The traceContext nil assertion in the "does not overwrite headers" test was removed because the presence of a returned traceContext depends on whether any header was set (e.g. baggage), which is unrelated to the non-overwriting behavior that test verifies. Co-Authored-By: Claude Opus 4.6 --- .../TracingURLSessionHandler.swift | 23 ++- .../Tests/TracingURLSessionHandlerTests.swift | 177 +++++++++++++++++- 2 files changed, 191 insertions(+), 9 deletions(-) diff --git a/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift b/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift index 6e6cd253ab..a22568b608 100644 --- a/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift +++ b/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift @@ -79,9 +79,9 @@ internal struct TracingURLSessionHandler: DatadogURLSessionHandler { sampleRate: newSpanElements.sampleRate, samplingPriority: newSpanElements.samplingPriority, samplingDecisionMaker: newSpanElements.samplingDecisionMaker, - rumSessionId: contextReceiver.context.rumContext?.sessionID, - userId: contextReceiver.context.userInfo?.id, - accountId: contextReceiver.context.accountInfo?.id + rumSessionId: networkContext?.rumContext?.sessionID ?? contextReceiver.context.rumContext?.sessionID, + userId: networkContext?.userConfigurationContext?.id ?? contextReceiver.context.userInfo?.id, + accountId: networkContext?.accountConfigurationContext?.id ?? contextReceiver.context.accountInfo?.id ) var request = request @@ -111,10 +111,21 @@ internal struct TracingURLSessionHandler: DatadogURLSessionHandler { writer.write(traceContext: injectedSpanContext) writer.traceHeaderFields.forEach { field, value in - // do not overwrite existing header - if request.value(forHTTPHeaderField: field) == nil { + if field.lowercased() == W3CHTTPHeaders.baggage.lowercased() { + // Handle baggage header merging + if let existingValue = request.value(forHTTPHeaderField: field) { + let mergedValue = BaggageHeaderMerger.merge(previousHeader: existingValue, with: value) + request.setValue(mergedValue, forHTTPHeaderField: field) + } else { + request.setValue(value, forHTTPHeaderField: field) + } hasSetAnyHeader = true - request.setValue(value, forHTTPHeaderField: field) + } else { + // do not overwrite existing header + if request.value(forHTTPHeaderField: field) == nil { + hasSetAnyHeader = true + request.setValue(value, forHTTPHeaderField: field) + } } } } diff --git a/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift b/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift index 948d8c9fc4..51ad37a380 100644 --- a/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift +++ b/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift @@ -119,7 +119,7 @@ class TracingURLSessionHandlerTests: XCTestCase { orgRequest.setValue("custom", forHTTPHeaderField: W3CHTTPHeaders.traceparent) orgRequest.setValue("custom", forHTTPHeaderField: W3CHTTPHeaders.tracestate) - let (request, traceContext, capturedState) = handler.modify( + let (request, _, capturedState) = handler.modify( request: orgRequest, headerTypes: [ .datadog, @@ -147,8 +147,6 @@ class TracingURLSessionHandlerTests: XCTestCase { XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.traceparent), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.tracestate), "custom") XCTAssertNil(capturedState) - - XCTAssertNil(traceContext, "It must return no trace context") } func testGivenFirstPartyInterception_withRejectedTrace_itDoesNotInjectTraceHeaders() throws { @@ -844,6 +842,179 @@ class TracingURLSessionHandlerTests: XCTestCase { XCTAssertEqual(span.resource, "404", "404 responses should have resource set to '404'") } + // MARK: - Baggage Header Merging Tests + + func testGivenRequestWithExistingBaggageHeader_whenTraceContextIsInjected_itMergesBaggageHeaders() throws { + // Given + let handler = TracingURLSessionHandler( + tracer: tracer, + contextReceiver: ContextMessageReceiver(), + samplingRate: .maxSampleRate, + firstPartyHosts: .init(), + traceContextInjection: .all + ) + + var request = URLRequest.mockWith(url: "https://www.example.com") + request.setValue("custom.key=custom.value,another.key=another.value", forHTTPHeaderField: W3CHTTPHeaders.baggage) + + // When + let (modifiedRequest, _, _) = handler.modify( + request: request, + headerTypes: [.datadog], + networkContext: NetworkContext( + rumContext: .init( + applicationID: .mockRandom(), + sessionID: "abcdef01-2345-6789-abcd-ef0123456789" + ) + ) + ) + + // Then + let baggageHeader = modifiedRequest.value(forHTTPHeaderField: W3CHTTPHeaders.baggage) + XCTAssertNotNil(baggageHeader) + + // Verify that both existing and new baggage values are present + XCTAssertTrue(baggageHeader?.contains("custom.key=custom.value") == true) + XCTAssertTrue(baggageHeader?.contains("another.key=another.value") == true) + XCTAssertTrue(baggageHeader?.contains("session.id=abcdef01-2345-6789-abcd-ef0123456789") == true) + } + + func testGivenRequestWithExistingBaggageHeader_whenTraceContextIsInjectedWithW3C_itMergesBaggageHeaders() throws { + // Given + let handler = TracingURLSessionHandler( + tracer: tracer, + contextReceiver: ContextMessageReceiver(), + samplingRate: .maxSampleRate, + firstPartyHosts: .init(), + traceContextInjection: .all + ) + + var request = URLRequest.mockWith(url: "https://www.example.com") + request.setValue("custom.key=custom.value,session.id=old.session.id", forHTTPHeaderField: W3CHTTPHeaders.baggage) + + // When + let (modifiedRequest, _, _) = handler.modify( + request: request, + headerTypes: [.tracecontext], + networkContext: NetworkContext( + rumContext: .init( + applicationID: .mockRandom(), + sessionID: "abcdef01-2345-6789-abcd-ef0123456789" + ) + ) + ) + + // Then + let baggageHeader = modifiedRequest.value(forHTTPHeaderField: W3CHTTPHeaders.baggage) + XCTAssertNotNil(baggageHeader) + + // Verify that existing custom key is preserved + XCTAssertTrue(baggageHeader?.contains("custom.key=custom.value") == true) + // Verify that session.id is overridden with new value + XCTAssertTrue(baggageHeader?.contains("session.id=abcdef01-2345-6789-abcd-ef0123456789") == true) + } + + func testGivenRequestWithComplexBaggageHeader_whenTraceContextIsInjected_itMergesBaggageHeadersCorrectly() throws { + // Given + let handler = TracingURLSessionHandler( + tracer: tracer, + contextReceiver: ContextMessageReceiver(), + samplingRate: .maxSampleRate, + firstPartyHosts: .init(), + traceContextInjection: .all + ) + + var request = URLRequest.mockWith(url: "https://www.example.com") + // This is a complex scenario with whitespace and semicolons in values + request.setValue(" toto=1,car= Dacia Sandero ,session.id = 2,testProp=1; testProp2=4;prop3 ", forHTTPHeaderField: W3CHTTPHeaders.baggage) + + // When + let (modifiedRequest, _, _) = handler.modify( + request: request, + headerTypes: [.tracecontext], + networkContext: NetworkContext( + rumContext: .init( + applicationID: .mockRandom(), + sessionID: "abcdef01-2345-6789-abcd-ef0123456789" + ), + userConfigurationContext: .init(id: "user123"), + accountConfigurationContext: .init(id: "account456") + ) + ) + + // Then + let baggageHeader = modifiedRequest.value(forHTTPHeaderField: W3CHTTPHeaders.baggage) + XCTAssertNotNil(baggageHeader) + + // Parse the result to verify merging behavior + let baggageDict = extractBaggageKeyValuePairs(from: baggageHeader!) + + // Verify that new values override previous ones + XCTAssertEqual(baggageDict["session.id"], "abcdef01-2345-6789-abcd-ef0123456789") + + // Verify that previous values are preserved when not overridden + XCTAssertEqual(baggageDict["toto"], "1") + XCTAssertEqual(baggageDict["car"], "Dacia Sandero") + XCTAssertEqual(baggageDict["testProp"], "1; testProp2=4;prop3") // Everything after first = is value + + // Verify that new values are added + XCTAssertEqual(baggageDict["account.id"], "account456") + XCTAssertEqual(baggageDict["user.id"], "user123") + + // Verify all expected keys are present + XCTAssertEqual(baggageDict.keys.count, 6) + } + + func testGivenRequestWithNoBaggageHeader_whenTraceContextIsInjected_itSetsBaggageHeader() throws { + // Given + let handler = TracingURLSessionHandler( + tracer: tracer, + contextReceiver: ContextMessageReceiver(), + samplingRate: .maxSampleRate, + firstPartyHosts: .init(), + traceContextInjection: .all + ) + + let request = URLRequest.mockWith(url: "https://www.example.com") + + // When + let (modifiedRequest, _, _) = handler.modify( + request: request, + headerTypes: [.tracecontext], + networkContext: NetworkContext( + rumContext: .init( + applicationID: .mockRandom(), + sessionID: "abcdef01-2345-6789-abcd-ef0123456789" + ) + ) + ) + + // Then + let baggageHeader = modifiedRequest.value(forHTTPHeaderField: W3CHTTPHeaders.baggage) + XCTAssertNotNil(baggageHeader) + XCTAssertTrue(baggageHeader?.contains("session.id=abcdef01-2345-6789-abcd-ef0123456789") == true) + } + + // MARK: - Helper Methods + + private func extractBaggageKeyValuePairs(from header: String) -> [String: String] { + var dict: [String: String] = [:] + let fields = header.split(separator: ",") + + for field in fields { + let fieldString = String(field) + if let equalIndex = fieldString.firstIndex(of: "=") { + let key = fieldString[.. Date: Fri, 6 Feb 2026 22:23:19 +0100 Subject: [PATCH 3/5] Add changelog entry for baggage header merging fix Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd084f846b..c70a576952 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - [IMPROVEMENT] Skip malformed Logs attributes individually instead of dropping the entire event, and log clear error messages. See [#2665][] - [IMPROVEMENT] Improve span attribute encoding error messages to include attribute name and context. See [#2676][] - [IMPROVEMENT] Expose public entities from `DatadogInternal` to prevent `DatadogInternal` imports in customer code. See [#2666][] +- [FIX] Merge W3C baggage headers and propagate networkContext in `TracingURLSessionHandler`. See [#2683][] # 3.6.1 / 02-02-2026 @@ -1052,6 +1053,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#2665]: https://github.com/DataDog/dd-sdk-ios/pull/2665 [#2665]: https://github.com/DataDog/dd-sdk-ios/pull/2666 [#2676]: https://github.com/DataDog/dd-sdk-ios/pull/2676 +[#2683]: https://github.com/DataDog/dd-sdk-ios/pull/2678 [@00fa9a]: https://github.com/00FA9A [@britton-earnin]: https://github.com/Britton-Earnin From e2fe8e8893ee2d6c809daa25dde0d7fbcda08077 Mon Sep 17 00:00:00 2001 From: Valentin Pertuisot Date: Fri, 6 Feb 2026 22:48:25 +0100 Subject: [PATCH 4/5] Fix trailing newline lint violations in BaggageHeaderMerger files Co-Authored-By: Claude Opus 4.6 --- .../NetworkInstrumentation/W3C/BaggageHeaderMerger.swift | 1 - .../Tests/NetworkInstrumentation/BaggageHeaderMergerTests.swift | 2 -- 2 files changed, 3 deletions(-) diff --git a/DatadogInternal/Sources/NetworkInstrumentation/W3C/BaggageHeaderMerger.swift b/DatadogInternal/Sources/NetworkInstrumentation/W3C/BaggageHeaderMerger.swift index 4aa0f941f8..989ce25354 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/W3C/BaggageHeaderMerger.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/W3C/BaggageHeaderMerger.swift @@ -79,4 +79,3 @@ public struct BaggageHeaderMerger { return (key: key, value: value) } } - diff --git a/DatadogInternal/Tests/NetworkInstrumentation/BaggageHeaderMergerTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/BaggageHeaderMergerTests.swift index b712b74189..4ab4f0b8b3 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/BaggageHeaderMergerTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/BaggageHeaderMergerTests.swift @@ -336,5 +336,3 @@ class BaggageHeaderMergerTests: XCTestCase { return dict } } - - From 86d019d42b304fec9f17b8060520650a5dd26628 Mon Sep 17 00:00:00 2001 From: Valentin Pertuisot Date: Thu, 12 Feb 2026 14:32:58 +0100 Subject: [PATCH 5/5] Fix tests --- .../Tracing/TracingURLSessionHandlerTests.swift | 11 ++++++----- .../Tests/TracingURLSessionHandlerTests.swift | 12 ++++++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift b/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift index 6928763b53..1256dca91c 100644 --- a/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift +++ b/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift @@ -227,19 +227,20 @@ class TracingURLSessionHandlerTests: XCTestCase { ) let message = FeatureMessage.context(fakeContext) _ = handler.contextReceiver.receive(message: message, from: core) + let networkContextSessionId = "abcdef01-2345-6789-abcd-ef0123456789" let (modifiedRequest, _, _) = handler.modify( request: request, headerTypes: [.datadog, .tracecontext, .b3, .b3multi], networkContext: NetworkContext( rumContext: .init( applicationID: .mockRandom(), - sessionID: "abcdef01-2345-6789-abcd-ef0123456789" + sessionID: networkContextSessionId ) ) ) - - XCTAssertEqual( - modifiedRequest.allHTTPHeaderFields, + let resultingHeaders = try XCTUnwrap(modifiedRequest.allHTTPHeaderFields) + DDAssertDictionariesEqual( + resultingHeaders, [ "traceparent": "00-000000000000000a0000000000000064-0000000000000064-01", "X-B3-SpanId": "0000000000000064", @@ -248,7 +249,7 @@ class TracingURLSessionHandlerTests: XCTestCase { "b3": "000000000000000a0000000000000064-0000000000000064-1", "x-datadog-trace-id": "100", "x-datadog-tags": "_dd.p.tid=a,_dd.p.dm=-1", - "baggage": "session.id=\(fakeSessionId)", + "baggage": "session.id=\(networkContextSessionId)", "tracestate": "dd=p:0000000000000064;s:1;t.dm:-1", "x-datadog-parent-id": "100", "x-datadog-sampling-priority": "1" diff --git a/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift b/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift index 51ad37a380..f4b8546efb 100644 --- a/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift +++ b/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift @@ -851,7 +851,8 @@ class TracingURLSessionHandlerTests: XCTestCase { contextReceiver: ContextMessageReceiver(), samplingRate: .maxSampleRate, firstPartyHosts: .init(), - traceContextInjection: .all + traceContextInjection: .all, + telemetry: TelemetryMock() ) var request = URLRequest.mockWith(url: "https://www.example.com") @@ -886,7 +887,8 @@ class TracingURLSessionHandlerTests: XCTestCase { contextReceiver: ContextMessageReceiver(), samplingRate: .maxSampleRate, firstPartyHosts: .init(), - traceContextInjection: .all + traceContextInjection: .all, + telemetry: TelemetryMock() ) var request = URLRequest.mockWith(url: "https://www.example.com") @@ -921,7 +923,8 @@ class TracingURLSessionHandlerTests: XCTestCase { contextReceiver: ContextMessageReceiver(), samplingRate: .maxSampleRate, firstPartyHosts: .init(), - traceContextInjection: .all + traceContextInjection: .all, + telemetry: TelemetryMock() ) var request = URLRequest.mockWith(url: "https://www.example.com") @@ -972,7 +975,8 @@ class TracingURLSessionHandlerTests: XCTestCase { contextReceiver: ContextMessageReceiver(), samplingRate: .maxSampleRate, firstPartyHosts: .init(), - traceContextInjection: .all + traceContextInjection: .all, + telemetry: TelemetryMock() ) let request = URLRequest.mockWith(url: "https://www.example.com")