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 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/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/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..989ce25354 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 } 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..4ab4f0b8b3 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 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..f4b8546efb 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,183 @@ 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, + telemetry: TelemetryMock() + ) + + 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, + telemetry: TelemetryMock() + ) + + 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, + telemetry: TelemetryMock() + ) + + 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, + telemetry: TelemetryMock() + ) + + 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[..