From 8a89706532ecf797808ef042ac743e9743666ec2 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 17 Apr 2024 11:59:30 +0200 Subject: [PATCH 1/7] RUM-3470 Support head-based trace sampling for local traces and automatic distributed tracing --- Datadog/Datadog.xcodeproj/project.pbxproj | 6 + .../xcschemes/DatadogCore iOS.xcscheme | 12 +- .../xcschemes/DatadogCore tvOS.xcscheme | 17 +- .../Trace/HeadBasedSamplingTests.swift | 239 ++++++++++++++++-- .../Datadog/Mocks/TracingFeatureMocks.swift | 8 +- DatadogCore/Tests/Datadog/TracerTests.swift | 66 ++--- .../TracingURLSessionHandlerTests.swift | 2 +- .../B3/B3HTTPHeadersReader.swift | 13 + .../B3/B3HTTPHeadersWriter.swift | 4 +- .../Datadog/HTTPHeadersReader.swift | 10 + .../Datadog/HTTPHeadersWriter.swift | 4 +- .../DatadogURLSessionHandler.swift | 26 +- .../NetworkInstrumentationFeature.swift | 80 +++--- .../NetworkInstrumentation/TraceContext.swift | 45 ++++ .../TracePropagationHeadersReader.swift | 6 + .../DatadogURLSessionDelegate.swift | 7 +- .../URLSession/URLSessionInterceptor.swift | 45 +++- .../URLSessionTaskInterception.swift | 16 -- .../W3C/W3CHTTPHeadersReader.swift | 14 + .../W3C/W3CHTTPHeadersWriter.swift | 4 +- DatadogInternal/Sources/Utils/Sampler.swift | 33 ++- .../B3HTTPHeadersReaderTests.swift | 2 + .../HTTPHeadersReaderTests.swift | 2 + .../NetworkInstrumentationFeatureTests.swift | 100 +------- .../W3CHTTPHeadersReaderTests.swift | 2 + .../URLSessionRUMResourcesHandler.swift | 36 +-- .../URLSessionRUMResourcesHandlerTests.swift | 90 +++++-- DatadogTrace/Sources/DDFormat.swift | 4 +- DatadogTrace/Sources/DDSpan.swift | 4 +- DatadogTrace/Sources/DDSpanContext.swift | 6 + DatadogTrace/Sources/DatadogTracer.swift | 14 +- .../TracingURLSessionHandler.swift | 48 ++-- .../Tests/DatadogTracer+SamplingTests.swift | 1 - DatadogTrace/Tests/TracingFeatureMocks.swift | 8 +- .../Tests/TracingURLSessionHandlerTests.swift | 127 ++++------ TestUtilities/Mocks/FoundationMocks.swift | 2 +- .../Mocks/NetworkInstrumentationMocks.swift | 10 +- 37 files changed, 702 insertions(+), 411 deletions(-) create mode 100644 DatadogInternal/Sources/NetworkInstrumentation/TraceContext.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 1da0f2959a..7139e51339 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -361,6 +361,8 @@ 617247B825DAB0E2007085B3 /* DDCrashReportBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617247B725DAB0E2007085B3 /* DDCrashReportBuilder.swift */; }; 6175922B2A6FA8EE0073F431 /* DatadogSessionReplay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6133D1F52A6ED9E100384BEF /* DatadogSessionReplay.framework */; }; 6175922D2A6FADDD0073F431 /* DatadogSessionReplay.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6133D1F52A6ED9E100384BEF /* DatadogSessionReplay.framework */; }; + 6175C3512BCE66DB006FAAB0 /* TraceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6175C3502BCE66DB006FAAB0 /* TraceContext.swift */; }; + 6175C3522BCE66DB006FAAB0 /* TraceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6175C3502BCE66DB006FAAB0 /* TraceContext.swift */; }; 617699182A860D9D0030022B /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617699172A860D9D0030022B /* HTTPClient.swift */; }; 617699192A860D9D0030022B /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617699172A860D9D0030022B /* HTTPClient.swift */; }; 6176991B2A86121B0030022B /* HTTPClientMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6176991A2A86121B0030022B /* HTTPClientMock.swift */; }; @@ -2299,6 +2301,7 @@ 617247AD25DA9BEA007085B3 /* CrashReportingObjcHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CrashReportingObjcHelpers.h; sourceTree = ""; }; 617247AE25DA9BEA007085B3 /* CrashReportingObjcHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CrashReportingObjcHelpers.m; sourceTree = ""; }; 617247B725DAB0E2007085B3 /* DDCrashReportBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDCrashReportBuilder.swift; sourceTree = ""; }; + 6175C3502BCE66DB006FAAB0 /* TraceContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceContext.swift; sourceTree = ""; }; 617699172A860D9D0030022B /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; 6176991A2A86121B0030022B /* HTTPClientMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClientMock.swift; sourceTree = ""; }; 6176991D2A8791880030022B /* Datadog+MultipleInstancesIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Datadog+MultipleInstancesIntegrationTests.swift"; sourceTree = ""; }; @@ -6017,6 +6020,7 @@ children = ( D2160C9829C0DE5700FAA9A5 /* NetworkInstrumentationFeature.swift */, D2160CEC29C0E0E600FAA9A5 /* DatadogURLSessionHandler.swift */, + 6175C3502BCE66DB006FAAB0 /* TraceContext.swift */, D2EBEDCC29B893D800B15732 /* TraceID.swift */, 3C9B27242B9F174700569C07 /* SpanID.swift */, D2160C9429C0DE5600FAA9A5 /* FirstPartyHosts.swift */, @@ -8215,6 +8219,7 @@ D2EBEE2A29BA160F00B15732 /* TracingHTTPHeaders.swift in Sources */, D21A94F22B8397CA00AC4256 /* WebViewMessage.swift in Sources */, D23039EC298D5236001A1FA3 /* LaunchTime.swift in Sources */, + 6175C3512BCE66DB006FAAB0 /* TraceContext.swift in Sources */, D23039EE298D5236001A1FA3 /* FeatureMessageReceiver.swift in Sources */, D23039DE298D5235001A1FA3 /* Writer.swift in Sources */, D23039FA298D5236001A1FA3 /* Telemetry.swift in Sources */, @@ -9097,6 +9102,7 @@ D2EBEE3829BA161100B15732 /* TracingHTTPHeaders.swift in Sources */, D21A94F32B8397CA00AC4256 /* WebViewMessage.swift in Sources */, D2DA2364298D57AA00C6C7E6 /* LaunchTime.swift in Sources */, + 6175C3522BCE66DB006FAAB0 /* TraceContext.swift in Sources */, D2DA2365298D57AA00C6C7E6 /* FeatureMessageReceiver.swift in Sources */, D2DA2366298D57AA00C6C7E6 /* Writer.swift in Sources */, D2DA2367298D57AA00C6C7E6 /* Telemetry.swift in Sources */, diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme index 6dc55f9271..b8ac29093d 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme @@ -198,22 +198,22 @@ + Identifier = "HeadBasedSamplingTests/testSendingDroppedDistributedTraceWithNoParent_throughTracerAPI()"> + Identifier = "HeadBasedSamplingTests/testSendingDroppedDistributedTraceWithNoParent_throughURLSessionInstrumentationAPI()"> + Identifier = "HeadBasedSamplingTests/testSendingDroppedDistributedTraceWithParent_throughTracerAPI()"> + Identifier = "HeadBasedSamplingTests/testSendingDroppedDistributedTraceWithParent_throughURLSessionInstrumentationAPI()"> + Identifier = "HeadBasedSamplingTests/testSendingSampledDistributedTraceWithNoParent_throughTracerAPI()"> + Identifier = "HeadBasedSamplingTests/testSendingSampledDistributedTraceWithParent_throughTracerAPI()"> diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme index 1896fc22ae..d512262352 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore tvOS.xcscheme @@ -184,22 +184,22 @@ + Identifier = "HeadBasedSamplingTests/testSendingDroppedDistributedTraceWithNoParent_throughTracerAPI()"> + Identifier = "HeadBasedSamplingTests/testSendingDroppedDistributedTraceWithNoParent_throughURLSessionInstrumentationAPI()"> + Identifier = "HeadBasedSamplingTests/testSendingDroppedDistributedTraceWithParent_throughTracerAPI()"> + Identifier = "HeadBasedSamplingTests/testSendingDroppedDistributedTraceWithParent_throughURLSessionInstrumentationAPI()"> + Identifier = "HeadBasedSamplingTests/testSendingSampledDistributedTraceWithNoParent_throughTracerAPI()"> + Identifier = "HeadBasedSamplingTests/testSendingSampledDistributedTraceWithParent_throughTracerAPI()"> @@ -232,11 +232,6 @@ BlueprintName = "DatadogTraceTests tvOS" ReferencedContainer = "container:Datadog.xcodeproj"> - - - - diff --git a/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift b/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift index 1e28d4cc7a..840c6de41c 100644 --- a/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift +++ b/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift @@ -30,9 +30,15 @@ class HeadBasedSamplingTests: XCTestCase { // MARK: - Local Tracing - // TODO: RUM-3470 Enable this test when head-based sampling is supported func testSamplingLocalTrace() throws { - let localTraceSampling: Float = 50 + /* + This is the basic situation of local trace with 3 spans: + + client-ios-app: [-------- parent -----------] | + client-ios-app: [----- child --------] | all 3: keep or drop + client-ios-app: [-- grandchild --] | + */ + let localTraceSampling: Float = 50 // keep or drop // Given traceConfig.sampleRate = localTraceSampling @@ -54,9 +60,14 @@ class HeadBasedSamplingTests: XCTestCase { XCTAssertTrue(allKept || allDropped, "All spans must be either kept or dropped") } - // TODO: RUM-3470 Enable this test when head-based sampling is supported func testSamplingLocalTraceWithImplicitParent() throws { - let localTraceSampling: Float = 50 + /* + This is the situation of local trace with active span as a parent: + + client-ios-app: [-------- active.span -----] | + client-ios-app: [- child1 -][- child2 -] | all 3: keep or drop + */ + let localTraceSampling: Float = 50 // keep or drop // Given traceConfig.sampleRate = localTraceSampling @@ -65,8 +76,8 @@ class HeadBasedSamplingTests: XCTestCase { // When let parent = Tracer.shared(in: core).startSpan(operationName: "parent").setActive() let child1 = Tracer.shared(in: core).startSpan(operationName: "child 1") - let child2 = Tracer.shared(in: core).startSpan(operationName: "child 2") child1.finish() + let child2 = Tracer.shared(in: core).startSpan(operationName: "child 2") child2.finish() parent.finish() @@ -78,10 +89,9 @@ class HeadBasedSamplingTests: XCTestCase { XCTAssertTrue(allKept || allDropped, "All spans must be either kept or dropped") } - // MARK: - Distributed Tracing + // MARK: - Distributed Tracing (through network instrumentation API) - // TODO: RUM-3470 Enable this test when head-based sampling is supported - func testSendingSampledDistributedTraceWithNoParent() throws { + func testSendingSampledDistributedTraceWithNoParent_throughURLSessionInstrumentationAPI() throws { /* This is the situation where distributed trace starts with the span created with DatadogTrace network instrumentation (with no parent): @@ -111,15 +121,17 @@ class HeadBasedSamplingTests: XCTestCase { XCTAssertTrue(span.isKept, "Span must be sampled") // Then - let expectedTraceIDField = String(span.traceID, representation: .decimal) - let expectedSpanIDField = String(span.spanID, representation: .decimal) + let expectedTraceIDField = span.traceID.idLoHex + let expectedSpanIDField = String(span.spanID, representation: .hexadecimal) + let expectedTagsField = "_dd.p.tid=\(span.traceID.idHiHex)" XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "1") } // TODO: RUM-3535 Enable this test when trace context injection control is implemented - func testSendingDroppedDistributedTraceWithNoParent() throws { + func testSendingDroppedDistributedTraceWithNoParent_throughURLSessionInstrumentationAPI() throws { /* This is the situation where distributed trace starts with the span created with DatadogTrace network instrumentation (with no parent): @@ -149,15 +161,16 @@ class HeadBasedSamplingTests: XCTestCase { XCTAssertFalse(span.isKept, "Span must be dropped") // Then - let expectedTraceIDField = String(span.traceID, representation: .decimal) - let expectedSpanIDField = String(span.spanID, representation: .decimal) + let expectedTraceIDField = span.traceID.idLoHex + let expectedSpanIDField = String(span.spanID, representation: .hexadecimal) + let expectedTagsField = "_dd.p.tid=\(span.traceID.idHiHex)" XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0") } - // TODO: RUM-3470 Enable this test when head-based sampling is supported - func testSendingSampledDistributedTraceWithParent() throws { + func testSendingSampledDistributedTraceWithParent_throughURLSessionInstrumentationAPI() throws { /* This is the situation where distributed trace starts with an active local span and is continued with the span created with DatadogTrace network instrumentation: @@ -196,15 +209,17 @@ class HeadBasedSamplingTests: XCTestCase { XCTAssertEqual(urlsessionSpan.parentID, activeSpan.spanID) // Then - let expectedTraceIDField = String(activeSpan.traceID, representation: .decimal) - let expectedSpanIDField = String(urlsessionSpan.spanID, representation: .decimal) + let expectedTraceIDField = activeSpan.traceID.idLoHex + let expectedSpanIDField = String(urlsessionSpan.spanID, representation: .hexadecimal) + let expectedTagsField = "_dd.p.tid=\(activeSpan.traceID.idHiHex)" XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "1") } // TODO: RUM-3535 Enable this test when trace context injection control is implemented - func testSendingDroppedDistributedTraceWithParent() throws { + func testSendingDroppedDistributedTraceWithParent_throughURLSessionInstrumentationAPI() throws { /* This is the situation where distributed trace starts with an active local span and is continued with the span created with DatadogTrace network instrumentation: @@ -243,13 +258,197 @@ class HeadBasedSamplingTests: XCTestCase { XCTAssertEqual(urlsessionSpan.parentID, activeSpan.spanID) // Then - let expectedTraceIDField = String(activeSpan.traceID, representation: .decimal) - let expectedSpanIDField = String(urlsessionSpan.spanID, representation: .decimal) + let expectedTraceIDField = activeSpan.traceID.idLoHex + let expectedSpanIDField = String(urlsessionSpan.spanID, representation: .hexadecimal) + let expectedTagsField = "_dd.p.tid=\(activeSpan.traceID.idHiHex)" + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0") + } + + // MARK: - Distributed Tracing (through Tracer API) + + // TODO: RUM-3470 Enable this test when head-based sampling is supported for distributed tracing through Tracer API + func testSendingSampledDistributedTraceWithNoParent_throughTracerAPI() throws { + /* + This is the situation where distributed trace starts with the span created with Datadog tracer: + + client-ios-app: [------ network.span ------] keep + client backend: [--- backend span ---] keep + */ + + let localTraceSampling: Float = 100 // keep all + let distributedTraceSampling: Float = 0 // drop all + + // Given + traceConfig.sampleRate = localTraceSampling + Trace.enable(with: traceConfig, in: core) + + // When + var request: URLRequest = .mockAny() + let writer = HTTPHeadersWriter(sampleRate: distributedTraceSampling) + let span = Tracer.shared(in: core).startSpan(operationName: "network.span") + Tracer.shared(in: core).inject(spanContext: span.context, writer: writer) + writer.traceHeaderFields.forEach { field, value in request.setValue(value, forHTTPHeaderField: field) } + span.finish() + + // Then + let networkSpan = try XCTUnwrap(core.waitAndReturnSpanEvents().first, "It should send span event") + XCTAssertEqual(networkSpan.operationName, "network.span") + XCTAssertEqual(networkSpan.samplingRate, 1, "Span must use local trace sample rate") + XCTAssertTrue(networkSpan.isKept, "Span must be sampled") + + // Then + let expectedTraceIDField = networkSpan.traceID.idLoHex + let expectedSpanIDField = String(networkSpan.spanID, representation: .hexadecimal) + let expectedTagsField = "_dd.p.tid=\(networkSpan.traceID.idHiHex)" XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "1") } + // TODO: RUM-3470 Enable this test when head-based sampling is supported for distributed tracing through Tracer API + func testSendingDroppedDistributedTraceWithNoParent_throughTracerAPI() throws { + /* + This is the situation where distributed trace starts with the span created with Datadog tracer: + + client-ios-app: [------ network.span ------] drop + client backend: [--- backend span ---] drop + */ + + let localTraceSampling: Float = 0 // drop all + let distributedTraceSampling: Float = 100 // keep all + + // Given + traceConfig.sampleRate = localTraceSampling + Trace.enable(with: traceConfig, in: core) + + // When + var request: URLRequest = .mockAny() + let writer = HTTPHeadersWriter(sampleRate: distributedTraceSampling) + let span = Tracer.shared(in: core).startSpan(operationName: "network.span") + Tracer.shared(in: core).inject(spanContext: span.context, writer: writer) + writer.traceHeaderFields.forEach { field, value in request.setValue(value, forHTTPHeaderField: field) } + span.finish() + + // Then + let networkSpan = try XCTUnwrap(core.waitAndReturnSpanEvents().first, "It should send span event") + XCTAssertEqual(networkSpan.operationName, "network.span") + XCTAssertEqual(networkSpan.samplingRate, 0, "Span must use local trace sample rate") + XCTAssertFalse(networkSpan.isKept, "Span must be dropped") + + // Then + let expectedTraceIDField = networkSpan.traceID.idLoHex + let expectedSpanIDField = String(networkSpan.spanID, representation: .hexadecimal) + let expectedTagsField = "_dd.p.tid=\(networkSpan.traceID.idHiHex)" + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0") + } + + // TODO: RUM-3470 Enable this test when head-based sampling is supported for distributed tracing through Tracer API + func testSendingSampledDistributedTraceWithParent_throughTracerAPI() throws { + /* + This is the situation where distributed trace starts with an active local span and is continued with the span + created with Datadog tracer: + + client-ios-app: [-------- active.span -----------] keep + client-ios-app: [------ network.span ------] keep + client backend: [--- backend span ---] keep + */ + + let localTraceSampling: Float = 100 // keep all + let distributedTraceSampling: Float = 0 // drop all + + // Given + traceConfig.sampleRate = localTraceSampling + Trace.enable(with: traceConfig, in: core) + + // When + var request: URLRequest = .mockAny() + let writer = HTTPHeadersWriter(sampleRate: distributedTraceSampling) + let parentSpan = Tracer.shared(in: core).startSpan(operationName: "active.span").setActive() + let span = Tracer.shared(in: core).startSpan(operationName: "network.span") + Tracer.shared(in: core).inject(spanContext: span.context, writer: writer) + writer.traceHeaderFields.forEach { field, value in request.setValue(value, forHTTPHeaderField: field) } + span.finish() + parentSpan.finish() + + // Then + let spanEvents = core.waitAndReturnSpanEvents() + let activeSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "active.span" })) + let networkSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "network.span" })) + + XCTAssertEqual(activeSpan.samplingRate, 1, "Span must use local trace sample rate") + XCTAssertTrue(activeSpan.isKept, "Span must be sampled") + XCTAssertEqual(networkSpan.samplingRate, 1, "Span must use local trace sample rate") + XCTAssertTrue(networkSpan.isKept, "Span must be sampled") + XCTAssertEqual(networkSpan.traceID, activeSpan.traceID) + XCTAssertEqual(networkSpan.parentID, activeSpan.spanID) + + // Then + let expectedTraceIDField = activeSpan.traceID.idLoHex + let expectedSpanIDField = String(networkSpan.spanID, representation: .hexadecimal) + let expectedTagsField = "_dd.p.tid=\(activeSpan.traceID.idHiHex)" + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "1") + } + + // TODO: RUM-3470 Enable this test when head-based sampling is supported for distributed tracing through Tracer API + func testSendingDroppedDistributedTraceWithParent_throughTracerAPI() throws { + /* + This is the situation where distributed trace starts with an active local span and is continued with the span + created with Datadog tracer: + + client-ios-app: [-------- active.span -----------] drop + client-ios-app: [------ network.span ------] drop + client backend: [--- backend span ---] drop + */ + + let localTraceSampling: Float = 0 // drop all + let distributedTraceSampling: Float = 100 // keep all + + // Given + traceConfig.sampleRate = localTraceSampling + Trace.enable(with: traceConfig, in: core) + + // When + var request: URLRequest = .mockAny() + let writer = HTTPHeadersWriter(sampleRate: distributedTraceSampling) + let parentSpan = Tracer.shared(in: core).startSpan(operationName: "active.span").setActive() + let span = Tracer.shared(in: core).startSpan(operationName: "network.span") + Tracer.shared(in: core).inject(spanContext: span.context, writer: writer) + writer.traceHeaderFields.forEach { field, value in request.setValue(value, forHTTPHeaderField: field) } + span.finish() + parentSpan.finish() + + // Then + let spanEvents = core.waitAndReturnSpanEvents() + let activeSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "active.span" })) + let networkSpan = try XCTUnwrap(spanEvents.first(where: { $0.operationName == "network.span" })) + + XCTAssertEqual(activeSpan.samplingRate, 0, "Span must use local trace sample rate") + XCTAssertFalse(activeSpan.isKept, "Span must be dropped") + XCTAssertEqual(networkSpan.samplingRate, 0, "Span must use local trace sample rate") + XCTAssertFalse(networkSpan.isKept, "Span must be dropped") + XCTAssertEqual(networkSpan.traceID, activeSpan.traceID) + XCTAssertEqual(networkSpan.parentID, activeSpan.spanID) + + // Then + let expectedTraceIDField = activeSpan.traceID.idLoHex + let expectedSpanIDField = String(networkSpan.spanID, representation: .hexadecimal) + let expectedTagsField = "_dd.p.tid=\(activeSpan.traceID.idHiHex)" + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), expectedTraceIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), expectedSpanIDField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), expectedTagsField) + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0") + } + // MARK: - Helpers /// Sends request to `url` using real `URLSession` instrumented with provided `delegate`. diff --git a/DatadogCore/Tests/Datadog/Mocks/TracingFeatureMocks.swift b/DatadogCore/Tests/Datadog/Mocks/TracingFeatureMocks.swift index fb7fa8454a..496204d1e4 100644 --- a/DatadogCore/Tests/Datadog/Mocks/TracingFeatureMocks.swift +++ b/DatadogCore/Tests/Datadog/Mocks/TracingFeatureMocks.swift @@ -86,13 +86,17 @@ extension DDSpanContext { traceID: TraceID = .mockAny(), spanID: SpanID = .mockAny(), parentSpanID: SpanID? = .mockAny(), - baggageItems: BaggageItems = .mockAny() + baggageItems: BaggageItems = .mockAny(), + sampleRate: Float = .mockAny(), + isKept: Bool = .mockAny() ) -> DDSpanContext { return DDSpanContext( traceID: traceID, spanID: spanID, parentSpanID: parentSpanID, - baggageItems: baggageItems + baggageItems: baggageItems, + sampleRate: sampleRate, + isKept: isKept ) } } diff --git a/DatadogCore/Tests/Datadog/TracerTests.swift b/DatadogCore/Tests/Datadog/TracerTests.swift index 478cb7b649..57a2daa7c0 100644 --- a/DatadogCore/Tests/Datadog/TracerTests.swift +++ b/DatadogCore/Tests/Datadog/TracerTests.swift @@ -728,10 +728,10 @@ class TracerTests: XCTestCase { func testItInjectsSpanContextWithHTTPHeadersWriter() { Trace.enable(with: config, in: core) let tracer = Tracer.shared(in: core) - let spanContext1 = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny()) - let spanContext2 = DDSpanContext(traceID: 3, spanID: 4, parentSpanID: .mockAny(), baggageItems: .mockAny()) + let spanContext1 = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) + let spanContext2 = DDSpanContext(traceID: 3, spanID: 4, parentSpanID: .mockAny(), baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) - let httpHeadersWriter = HTTPHeadersWriter(sampler: .mockKeepAll()) + let httpHeadersWriter = HTTPHeadersWriter(sampler: Sampler.mockKeepAll()) XCTAssertEqual(httpHeadersWriter.traceHeaderFields, [:]) // When @@ -762,11 +762,11 @@ class TracerTests: XCTestCase { func testItInjectsSpanContextWithB3HTTPHeadersWriter_usingMultipleHeaders() { Trace.enable(with: config, in: core) let tracer = Tracer.shared(in: core) - let spanContext1 = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny()) - let spanContext2 = DDSpanContext(traceID: 4, spanID: 5, parentSpanID: 6, baggageItems: .mockAny()) - let spanContext3 = DDSpanContext(traceID: 77, spanID: 88, parentSpanID: nil, baggageItems: .mockAny()) + let spanContext1 = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) + let spanContext2 = DDSpanContext(traceID: 4, spanID: 5, parentSpanID: 6, baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) + let spanContext3 = DDSpanContext(traceID: 77, spanID: 88, parentSpanID: nil, baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) - let httpHeadersWriter = B3HTTPHeadersWriter(sampler: .mockKeepAll(), injectEncoding: .multiple) + let httpHeadersWriter = B3HTTPHeadersWriter(sampler: Sampler.mockKeepAll(), injectEncoding: .multiple) XCTAssertEqual(httpHeadersWriter.traceHeaderFields, [:]) // When @@ -808,11 +808,11 @@ class TracerTests: XCTestCase { func testItInjectsSpanContextWithB3HTTPHeadersWriter_usingSingleHeader() { Trace.enable(with: config, in: core) let tracer = Tracer.shared(in: core) - let spanContext1 = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny()) - let spanContext2 = DDSpanContext(traceID: 4, spanID: 5, parentSpanID: 6, baggageItems: .mockAny()) - let spanContext3 = DDSpanContext(traceID: 77, spanID: 88, parentSpanID: nil, baggageItems: .mockAny()) + let spanContext1 = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) + let spanContext2 = DDSpanContext(traceID: 4, spanID: 5, parentSpanID: 6, baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) + let spanContext3 = DDSpanContext(traceID: 77, spanID: 88, parentSpanID: nil, baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) - let httpHeadersWriter = B3HTTPHeadersWriter(sampler: .mockKeepAll(), injectEncoding: .single) + let httpHeadersWriter = B3HTTPHeadersWriter(sampler: Sampler.mockKeepAll(), injectEncoding: .single) XCTAssertEqual(httpHeadersWriter.traceHeaderFields, [:]) // When @@ -846,9 +846,9 @@ class TracerTests: XCTestCase { func testItInjectsRejectedSpanContextWithB3HTTPHeadersWriter_usingSingleHeader() { Trace.enable(with: config, in: core) let tracer = Tracer.shared(in: core) - let spanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny()) + let spanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) - let httpHeadersWriter = B3HTTPHeadersWriter(sampler: .mockRejectAll()) + let httpHeadersWriter = B3HTTPHeadersWriter(sampler: Sampler.mockRejectAll()) XCTAssertEqual(httpHeadersWriter.traceHeaderFields, [:]) // When @@ -864,9 +864,9 @@ class TracerTests: XCTestCase { func testItInjectsRejectedSpanContextWithB3HTTPHeadersWriter_usingMultipleHeader() { Trace.enable(with: config, in: core) let tracer = Tracer.shared(in: core) - let spanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny()) + let spanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) - let httpHeadersWriter = B3HTTPHeadersWriter(sampler: .mockRejectAll(), injectEncoding: .multiple) + let httpHeadersWriter = B3HTTPHeadersWriter(sampler: Sampler.mockRejectAll(), injectEncoding: .multiple) XCTAssertEqual(httpHeadersWriter.traceHeaderFields, [:]) // When @@ -882,12 +882,12 @@ class TracerTests: XCTestCase { func testItInjectsSpanContextWithW3CHTTPHeadersWriter() { Trace.enable(with: config, in: core) let tracer = Tracer.shared(in: core) - let spanContext1 = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny()) - let spanContext2 = DDSpanContext(traceID: 4, spanID: 5, parentSpanID: 6, baggageItems: .mockAny()) - let spanContext3 = DDSpanContext(traceID: 77, spanID: 88, parentSpanID: nil, baggageItems: .mockAny()) + let spanContext1 = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) + let spanContext2 = DDSpanContext(traceID: 4, spanID: 5, parentSpanID: 6, baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) + let spanContext3 = DDSpanContext(traceID: 77, spanID: 88, parentSpanID: nil, baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) let httpHeadersWriter = W3CHTTPHeadersWriter( - sampler: .mockKeepAll(), + sampler: Sampler.mockKeepAll(), tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM ] @@ -928,10 +928,10 @@ class TracerTests: XCTestCase { func testItInjectsRejectedSpanContextWithW3CHTTPHeadersWriter() { Trace.enable(with: config, in: core) let tracer = Tracer.shared(in: core) - let spanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny()) + let spanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) let httpHeadersWriter = W3CHTTPHeadersWriter( - sampler: .mockRejectAll(), + sampler: Sampler.mockRejectAll(), tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM ] @@ -952,9 +952,9 @@ class TracerTests: XCTestCase { func testItInjectsRejectedSpanContextWithHTTPHeadersWriter() { Trace.enable(with: config, in: core) let tracer = Tracer.shared(in: core) - let spanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny()) + let spanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) - let httpHeadersWriter = HTTPHeadersWriter(sampler: .mockRejectAll()) + let httpHeadersWriter = HTTPHeadersWriter(sampler: Sampler.mockRejectAll()) XCTAssertEqual(httpHeadersWriter.traceHeaderFields, [:]) // When @@ -970,9 +970,9 @@ class TracerTests: XCTestCase { func testItExtractsSpanContextWithHTTPHeadersReader() { Trace.enable(with: config, in: core) let tracer = Tracer.shared(in: core) - let injectedSpanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny()) + let injectedSpanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: .mockAny(), baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) - let httpHeadersWriter = HTTPHeadersWriter(sampler: .mockKeepAll()) + let httpHeadersWriter = HTTPHeadersWriter(sampler: Sampler.mockKeepAll()) tracer.inject(spanContext: injectedSpanContext, writer: httpHeadersWriter) let httpHeadersReader = HTTPHeadersReader( @@ -983,14 +983,15 @@ class TracerTests: XCTestCase { XCTAssertEqual(extractedSpanContext?.dd.traceID, injectedSpanContext.dd.traceID) XCTAssertEqual(extractedSpanContext?.dd.spanID, injectedSpanContext.dd.spanID) XCTAssertNil(extractedSpanContext?.dd.parentSpanID) + XCTAssertEqual(extractedSpanContext?.dd.sampleRate, config.sampleRate) } func testItExtractsSpanContextWithB3HTTPHeadersReader_forMultipleHeaders() { Trace.enable(with: config, in: core) let tracer = Tracer.shared(in: core) - let injectedSpanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny()) + let injectedSpanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) - let httpHeadersWriter = B3HTTPHeadersWriter(sampler: .mockKeepAll(), injectEncoding: .multiple) + let httpHeadersWriter = B3HTTPHeadersWriter(sampler: Sampler.mockKeepAll(), injectEncoding: .multiple) tracer.inject(spanContext: injectedSpanContext, writer: httpHeadersWriter) let httpHeadersReader = B3HTTPHeadersReader( @@ -1001,14 +1002,15 @@ class TracerTests: XCTestCase { XCTAssertEqual(extractedSpanContext?.dd.traceID, injectedSpanContext.dd.traceID) XCTAssertEqual(extractedSpanContext?.dd.spanID, injectedSpanContext.dd.spanID) XCTAssertEqual(extractedSpanContext?.dd.parentSpanID, injectedSpanContext.dd.parentSpanID) + XCTAssertEqual(extractedSpanContext?.dd.sampleRate, config.sampleRate) } func testItExtractsSpanContextWithB3HTTPHeadersReader_forSingleHeader() { Trace.enable(with: config, in: core) let tracer = Tracer.shared(in: core) - let injectedSpanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny()) + let injectedSpanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) - let httpHeadersWriter = B3HTTPHeadersWriter(sampler: .mockKeepAll(), injectEncoding: .single) + let httpHeadersWriter = B3HTTPHeadersWriter(sampler: Sampler.mockKeepAll(), injectEncoding: .single) tracer.inject(spanContext: injectedSpanContext, writer: httpHeadersWriter) let httpHeadersReader = B3HTTPHeadersReader( @@ -1019,15 +1021,16 @@ class TracerTests: XCTestCase { XCTAssertEqual(extractedSpanContext?.dd.traceID, injectedSpanContext.dd.traceID) XCTAssertEqual(extractedSpanContext?.dd.spanID, injectedSpanContext.dd.spanID) XCTAssertEqual(extractedSpanContext?.dd.parentSpanID, injectedSpanContext.dd.parentSpanID) + XCTAssertEqual(extractedSpanContext?.dd.sampleRate, config.sampleRate) } func testItExtractsSpanContextWithW3CHTTPHeadersReader() { Trace.enable(with: config, in: core) let tracer = Tracer.shared(in: core) - let injectedSpanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny()) + let injectedSpanContext = DDSpanContext(traceID: .init(idHi: 10, idLo: 100), spanID: 200, parentSpanID: 3, baggageItems: .mockAny(), sampleRate: .mockRandom(), isKept: .mockRandom()) let httpHeadersWriter = W3CHTTPHeadersWriter( - sampler: .mockKeepAll(), + sampler: Sampler.mockKeepAll(), tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM ] @@ -1042,6 +1045,7 @@ class TracerTests: XCTestCase { XCTAssertEqual(extractedSpanContext?.dd.traceID, injectedSpanContext.dd.traceID) XCTAssertEqual(extractedSpanContext?.dd.spanID, injectedSpanContext.dd.spanID) XCTAssertNil(extractedSpanContext?.dd.parentSpanID) + XCTAssertEqual(extractedSpanContext?.dd.sampleRate, config.sampleRate) } // MARK: - Span Dates Correction diff --git a/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift b/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift index ccf7c76c62..12725c3174 100644 --- a/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift +++ b/DatadogCore/Tests/Datadog/Tracing/TracingURLSessionHandlerTests.swift @@ -209,7 +209,7 @@ class TracingURLSessionHandlerTests: XCTestCase { func testGivenAllTracingHeaderTypes_itUsesTheSameIds() throws { let request: URLRequest = .mockWith(httpMethod: "GET") - let modifiedRequest = handler.modify(request: request, headerTypes: [.datadog, .tracecontext, .b3, .b3multi]) + let (modifiedRequest, _) = handler.modify(request: request, headerTypes: [.datadog, .tracecontext, .b3, .b3multi]) XCTAssertEqual( modifiedRequest.allHTTPHeaderFields, diff --git a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift index e8d5acd30f..76a1de1713 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift @@ -12,6 +12,9 @@ public typealias OTelHTTPHeadersReader = B3HTTPHeadersReader public class B3HTTPHeadersReader: TracePropagationHeadersReader { private let httpHeaderFields: [String: String] + @ReadWriteLock + public var tracerSampleRate: Float? = nil + public init(httpHeaderFields: [String: String]) { self.httpHeaderFields = httpHeaderFields } @@ -45,4 +48,14 @@ public class B3HTTPHeadersReader: TracePropagationHeadersReader { return nil } + + public var sampled: Bool? { + if let single = httpHeaderFields[B3HTTPHeaders.Single.b3Field] { + return single != B3HTTPHeaders.Constants.unsampledValue + } else if let multiple = httpHeaderFields[B3HTTPHeaders.Multiple.sampledField] { + return multiple == B3HTTPHeaders.Constants.sampledValue + } + + return nil + } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift index b6b219d9e6..ee3d848ed2 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersWriter.swift @@ -61,7 +61,7 @@ public class B3HTTPHeadersWriter: TracePropagationHeadersWriter { /// /// The sample rate determines the `X-B3-Sampled` header field value /// and whether `X-B3-TraceId`, `X-B3-SpanId`, and `X-B3-ParentSpanId` are propagated. - private let sampler: Sampler + private let sampler: Sampling /// The telemetry header encoding used by the writer. private let injectEncoding: InjectEncoding @@ -97,7 +97,7 @@ public class B3HTTPHeadersWriter: TracePropagationHeadersWriter { /// - Parameter sampler: The sampler used for headers injection. /// - Parameter injectEncoding: The B3 header encoding type, with `.single` as the default. public init( - sampler: Sampler, + sampler: Sampling, injectEncoding: InjectEncoding = .single ) { self.sampler = sampler diff --git a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersReader.swift b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersReader.swift index 300eaea577..18c64b4755 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersReader.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersReader.swift @@ -9,6 +9,9 @@ import Foundation public class HTTPHeadersReader: TracePropagationHeadersReader { private let httpHeaderFields: [String: String] + @ReadWriteLock + public var tracerSampleRate: Float? = nil + public init(httpHeaderFields: [String: String]) { self.httpHeaderFields = httpHeaderFields } @@ -43,4 +46,11 @@ public class HTTPHeadersReader: TracePropagationHeadersReader { parentSpanID: nil ) } + + public var sampled: Bool? { + if let sampling = httpHeaderFields[TracingHTTPHeaders.samplingPriorityField] { + return sampling == "1" + } + return nil + } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift index 6f6e26187c..8a1da0705f 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersWriter.swift @@ -38,7 +38,7 @@ public class HTTPHeadersWriter: TracePropagationHeadersWriter { /// /// This value will decide of the `x-datadog-sampling-priority` header field value /// and if `x-datadog-trace-id` and `x-datadog-parent-id` are propagated. - private let sampler: Sampler + private let sampler: Sampling /// Initializes the headers writer. /// @@ -58,7 +58,7 @@ public class HTTPHeadersWriter: TracePropagationHeadersWriter { /// Initializes the headers writer. /// /// - Parameter sampler: The sampler used for headers injection. - public init(sampler: Sampler) { + public init(sampler: Sampling) { self.sampler = sampler } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/DatadogURLSessionHandler.swift b/DatadogInternal/Sources/NetworkInstrumentation/DatadogURLSessionHandler.swift index c8845cc597..91aaf0e760 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/DatadogURLSessionHandler.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/DatadogURLSessionHandler.swift @@ -8,35 +8,33 @@ import Foundation /// An interface for processing `URLSession` task interceptions. public protocol DatadogURLSessionHandler { - /// The interceptor's first party hosts + /// The first party hosts configured for this handler. var firstPartyHosts: FirstPartyHosts { get } - /// Tells the interceptor to modify a URL request. + /// Modifies the provided request by injecting trace headers. /// /// - Parameters: - /// - request: The request to intercept. - /// - additionalFirstPartyHosts: Additional 1st-party hosts. - /// - Returns: The modified request. - func modify(request: URLRequest, headerTypes: Set) -> URLRequest + /// - request: The request to be modified. + /// - headerTypes: The types of tracing headers to inject into the request. + /// - Returns: A tuple containing the modified request and the injected TraceContext. If no trace is injected (e.g., due to sampling), + /// the returned request remains unmodified, and the trace context will be nil. + func modify(request: URLRequest, headerTypes: Set) -> (URLRequest, TraceContext?) - /// Returns the trace of the current execution context. - func traceContext() -> TraceContext? - - /// Tells the interceptor that the session did start. + /// Notifies the handler that the interception has started. /// - /// - Parameter interception: The URLSession interception. + /// - Parameter interception: The URLSession task interception. func interceptionDidStart(interception: URLSessionTaskInterception) - /// Tells the interceptor that the session did complete. + /// Notifies the handler that the interception has completed. /// - /// - Parameter interception: The URLSession interception. + /// - Parameter interception: The URLSession task interception. func interceptionDidComplete(interception: URLSessionTaskInterception) } extension DatadogCoreProtocol { /// Core extension for registering `URLSession` handlers. /// - /// - Parameter urlSessionHandler: The `URLSession` handlers to register. + /// - Parameter urlSessionHandler: The `URLSession` handler to register. public func register(urlSessionHandler: DatadogURLSessionHandler) throws { let feature = get(feature: NetworkInstrumentationFeature.self) ?? .init() feature.handlers.append(urlSessionHandler) diff --git a/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift b/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift index 38a5eeae0a..c1f8f81495 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift @@ -78,12 +78,15 @@ internal final class NetworkInstrumentationFeature: DatadogFeature { return } + var injectedTraceContexts: [TraceContext]? + if let currentRequest = task.currentRequest { - let request = self.intercept(request: currentRequest, additionalFirstPartyHosts: configuredFirstPartyHosts) + let (request, traceContexts) = self.intercept(request: currentRequest, additionalFirstPartyHosts: configuredFirstPartyHosts) task.dd.override(currentRequest: request) + injectedTraceContexts = traceContexts } - self.intercept(task: task, additionalFirstPartyHosts: configuredFirstPartyHosts) + self.intercept(task: task, with: injectedTraceContexts ?? [], additionalFirstPartyHosts: configuredFirstPartyHosts) } ) @@ -128,31 +131,43 @@ internal final class NetworkInstrumentationFeature: DatadogFeature { } extension NetworkInstrumentationFeature { - /// Tells the interceptors to modify a URL request. + /// Intercepts the provided request by injecting trace headers based on first-party hosts configuration. + /// + /// Only requests with URLs that match the list of first-party hosts have tracing headers injected. /// /// - Parameters: /// - request: The request to intercept. - /// - additionalFirstPartyHosts: Extra hosts to consider in the interception - /// - Returns: The modified request. - func intercept(request: URLRequest, additionalFirstPartyHosts: FirstPartyHosts?) -> URLRequest { + /// - additionalFirstPartyHosts: Extra hosts to consider in the interception, used in conjunction with hosts defined in each handler. + /// - Returns: A tuple containing the modified request and the list of injected TraceContexts, one or none for each handler. If no trace is injected (e.g., due to sampling), + /// the list will be empty. + func intercept(request: URLRequest, additionalFirstPartyHosts: FirstPartyHosts?) -> (URLRequest, [TraceContext]) { let headerTypes = firstPartyHosts(with: additionalFirstPartyHosts) .tracingHeaderTypes(for: request.url) guard !headerTypes.isEmpty else { - return request + return (request, []) } - return handlers.reduce(request) { - $1.modify(request: $0, headerTypes: headerTypes) + var request = request + var traceContexts: [TraceContext] = [] // each handler can inject distinct trace context + for handler in handlers { + let (nextRequest, nextTraceContext) = handler.modify(request: request, headerTypes: headerTypes) + request = nextRequest + if let nextTraceContext = nextTraceContext { + traceContexts.append(nextTraceContext) + } } + + return (request, traceContexts) } - /// Tells the interceptors that a task was created. + /// Intercepts the provided URLSession task by creating an interception object and notifying all handlers that the interception has started. /// /// - Parameters: - /// - task: The created task. - /// - additionalFirstPartyHosts: Extra hosts to consider in the interception. - func intercept(task: URLSessionTask, additionalFirstPartyHosts: FirstPartyHosts?) { + /// - task: The URLSession task to intercept. + /// - injectedTraceContexts: The list of trace contexts injected into the task's request, one or none for each handler. + /// - additionalFirstPartyHosts: Extra hosts to consider in the interception, used in conjunction with hosts defined in each handler. + func intercept(task: URLSessionTask, with injectedTraceContexts: [TraceContext], additionalFirstPartyHosts: FirstPartyHosts?) { // In response to https://github.com/DataDog/dd-sdk-ios/issues/1638 capture the current request object on the // caller thread and freeze its attributes through `ImmutableRequest`. This is to avoid changing the request // object from multiple threads: @@ -161,9 +176,6 @@ extension NetworkInstrumentationFeature { } let request = ImmutableRequest(request: currentRequest) - // Get the current trace context from all handlers. - let traceContexts = handlers.compactMap { $0.traceContext() } - queue.async { [weak self] in guard let self = self else { return @@ -179,20 +191,10 @@ extension NetworkInstrumentationFeature { interception.register(request: request) - if let trace = self.extractTrace(firstPartyHosts: firstPartyHosts, request: request) { - // The parent span id is extracted from the headers unless - // the propagation headers does not support it (only B3 does). - // In that case, we register the current trace context as parent - // if the trace ID matches. - let parentSpanID = trace.parentSpanID ?? - traceContexts.first(where: { $0.traceID == trace.traceID })?.spanID - - // Register the trace with parent - interception.register(trace: TraceContext( - traceID: trace.traceID, - spanID: trace.spanID, - parentSpanID: parentSpanID - )) + if let traceContext = injectedTraceContexts.first { + // ^ If multiple trace contexts were injected (one per each handler) take the first one. This mimics the implicit + // behaviour from before RUM-3470. + interception.register(trace: traceContext) } if let origin = request.allHTTPHeaderFields?[TracingHTTPHeaders.originField] { @@ -266,24 +268,6 @@ extension NetworkInstrumentationFeature { handlers.forEach { $0.interceptionDidComplete(interception: interception) } interceptions[task] = nil } - - private func extractTrace(firstPartyHosts: FirstPartyHosts, request: ImmutableRequest) -> (traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?)? { - guard let headers = request.allHTTPHeaderFields else { - return nil - } - - let tracingHeaderTypes = firstPartyHosts.tracingHeaderTypes(for: request.url) - - let reader: TracePropagationHeadersReader - if tracingHeaderTypes.contains(.datadog) { - reader = HTTPHeadersReader(httpHeaderFields: headers) - } else if tracingHeaderTypes.contains(.b3) || tracingHeaderTypes.contains(.b3multi) { - reader = B3HTTPHeadersReader(httpHeaderFields: headers) - } else { - reader = W3CHTTPHeadersReader(httpHeaderFields: headers) - } - return reader.read() - } } extension NetworkInstrumentationFeature: Flushable { diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TraceContext.swift b/DatadogInternal/Sources/NetworkInstrumentation/TraceContext.swift new file mode 100644 index 0000000000..c8a9316068 --- /dev/null +++ b/DatadogInternal/Sources/NetworkInstrumentation/TraceContext.swift @@ -0,0 +1,45 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation + +/// A context used to propagate trace through HTTP request headers. +public struct TraceContext: Equatable { + /// The unique identifier for the trace. + public let traceID: TraceID + /// The unique identifier for the span. + public let spanID: SpanID + /// The unique identifier for the parent span, if any. + public let parentSpanID: SpanID? + /// The sample rate used for injecting the span into a request. + /// + /// It is a value between `0.0` (drop) and `100.0` (keep), determined by the local or distributed trace sampler. + public let sampleRate: Float + /// Indicates whether this span was sampled or rejected by the sampler. + public let isKept: Bool + + /// Initializes a `TraceContext` instance with the provided parameters. + /// + /// - Parameters: + /// - traceID: The unique identifier for the trace. + /// - spanID: The unique identifier for the span. + /// - parentSpanID: The unique identifier for the parent span, if any. + /// - sampleRate: The sample rate used for injecting the span into a request. + /// - isKept: A boolean indicating whether this span was sampled or rejected by the sampler. + public init( + traceID: TraceID, + spanID: SpanID, + parentSpanID: SpanID?, + sampleRate: Float, + isKept: Bool + ) { + self.traceID = traceID + self.spanID = spanID + self.parentSpanID = parentSpanID + self.sampleRate = sampleRate + self.isKept = isKept + } +} diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersReader.swift b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersReader.swift index 9665a57ba7..f9dfcf906b 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersReader.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersReader.swift @@ -13,4 +13,10 @@ public protocol TracePropagationHeadersReader { spanID: SpanID, parentSpanID: SpanID? )? + + /// Indicates whether the trace was sampled based on the provided headers. + var sampled: Bool? { get } + + /// The sample rate used to sample this trace. + var tracerSampleRate: Float? { set get } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift index a141f37fe0..d949b3c5a5 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift @@ -127,12 +127,15 @@ open class DatadogURLSessionDelegate: NSObject, URLSessionDataDelegate { return } + var injectedTraceContexts: [TraceContext]? + if let currentRequest = task.currentRequest { - let request = feature.intercept(request: currentRequest, additionalFirstPartyHosts: firstPartyHosts) + let (request, traceContexts) = feature.intercept(request: currentRequest, additionalFirstPartyHosts: firstPartyHosts) task.dd.override(currentRequest: request) + injectedTraceContexts = traceContexts } - feature.intercept(task: task, additionalFirstPartyHosts: firstPartyHosts) + feature.intercept(task: task, with: injectedTraceContexts ?? [], additionalFirstPartyHosts: firstPartyHosts) } ) diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift index 97158aef2a..6892509bfd 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionInterceptor.swift @@ -6,6 +6,8 @@ import Foundation +// TODO: RUM-3470 Add tests to `URLSessionInterceptor` + /// The `URLSession` Interceptor provides methods for injecting distributed-traces /// headers into a `URLRequest`and to instrument a `URLURLSessionTask` lifcycle, /// from its creation to completion. @@ -27,6 +29,13 @@ public struct URLSessionInterceptor { return URLSessionInterceptor(feature: feature) } + /// Maps the trace ID to the full trace context generated for that trace. + /// + /// This is to bridge the gap between what is encoded into HTTP headers and what is later needed for processing + /// the interception (unlike request headers, the `TraceContext` holds the original information on trace sampling). + @ReadWriteLock + private var contextsByTraceID: [TraceID: [TraceContext]] = [:] + /// Tells the interceptor to modify a URL request. /// /// - Parameters: @@ -34,7 +43,11 @@ public struct URLSessionInterceptor { /// - additionalFirstPartyHosts: Extra hosts to consider in the interception. /// - Returns: The modified request. public func intercept(request: URLRequest, additionalFirstPartyHosts: FirstPartyHosts? = nil) -> URLRequest { - feature.intercept(request: request, additionalFirstPartyHosts: additionalFirstPartyHosts) + let (request, traceContexts) = feature.intercept(request: request, additionalFirstPartyHosts: additionalFirstPartyHosts) + if let traceID = extractTraceID(from: request) { + contextsByTraceID[traceID] = traceContexts + } + return request } /// Tells the interceptors that a task was created. @@ -43,7 +56,12 @@ public struct URLSessionInterceptor { /// - task: The created task. /// - additionalFirstPartyHosts: Extra hosts to consider in the interception. public func intercept(task: URLSessionTask, additionalFirstPartyHosts: FirstPartyHosts? = nil) { - feature.intercept(task: task, additionalFirstPartyHosts: additionalFirstPartyHosts) + var injectedTraceContexts: [TraceContext] = [] + if let request = task.currentRequest, let traceID = extractTraceID(from: request) { + injectedTraceContexts = contextsByTraceID[traceID] ?? [] + } + + feature.intercept(task: task, with: injectedTraceContexts, additionalFirstPartyHosts: additionalFirstPartyHosts) } /// Tells the interceptor that metrics were collected for the given task. @@ -71,5 +89,28 @@ public struct URLSessionInterceptor { /// - error: If an error occurred, an error object indicating how the transfer failed, otherwise NULL. public func task(_ task: URLSessionTask, didCompleteWithError error: Error?) { feature.task(task, didCompleteWithError: error) + + if let request = task.currentRequest, let traceID = extractTraceID(from: request) { + contextsByTraceID[traceID] = nil + } + } + + // MARK: - Private + + private func extractTraceID(from request: URLRequest) -> TraceID? { + guard let headers = request.allHTTPHeaderFields else { + return nil + } + + // Try all supported header types until first one is matched: + if let dd = HTTPHeadersReader(httpHeaderFields: headers).read() { + return dd.traceID + } else if let b3 = B3HTTPHeadersReader(httpHeaderFields: headers).read() { + return b3.traceID + } else if let w3c = W3CHTTPHeadersReader(httpHeaderFields: headers).read() { + return w3c.traceID + } + + return nil } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift index 609bfc1996..22486f7b3b 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTaskInterception.swift @@ -71,22 +71,6 @@ public class URLSessionTaskInterception { } } -public struct TraceContext { - public let traceID: TraceID - public let spanID: SpanID - public let parentSpanID: SpanID? - - public init( - traceID: TraceID, - spanID: SpanID, - parentSpanID: SpanID? = nil - ) { - self.traceID = traceID - self.spanID = spanID - self.parentSpanID = parentSpanID - } -} - public struct ResourceCompletion { public let httpResponse: HTTPURLResponse? public let error: Error? diff --git a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersReader.swift b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersReader.swift index 9b2effe9c9..f36674f1dd 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersReader.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersReader.swift @@ -9,6 +9,9 @@ import Foundation public class W3CHTTPHeadersReader: TracePropagationHeadersReader { private let httpHeaderFields: [String: String] + @ReadWriteLock + public var tracerSampleRate: Float? = nil + public init(httpHeaderFields: [String: String]) { self.httpHeaderFields = httpHeaderFields } @@ -33,4 +36,15 @@ public class W3CHTTPHeadersReader: TracePropagationHeadersReader { parentSpanID: nil ) } + + public var sampled: Bool? { + if let traceparent = httpHeaderFields[W3CHTTPHeaders.traceparent] { + guard let sampled = traceparent.components(separatedBy: W3CHTTPHeaders.Constants.separator).last else { + return nil + } + return sampled == W3CHTTPHeaders.Constants.sampledValue + } + + return nil + } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift index 20f6bb7367..e2eb792570 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift @@ -42,7 +42,7 @@ public class W3CHTTPHeadersWriter: TracePropagationHeadersWriter { /// /// This value will decide of the `FLAG_SAMPLED` header field value /// and if `trace-id`, `span-id` are propagated. - private let sampler: Sampler + private let sampler: Sampling /// Initializes the headers writer. /// @@ -65,7 +65,7 @@ public class W3CHTTPHeadersWriter: TracePropagationHeadersWriter { /// /// - Parameter sampler: The sampler used for headers injection. /// - Parameter tracestate: The tracestate to be injected. - public init(sampler: Sampler, tracestate: [String: String]) { + public init(sampler: Sampling, tracestate: [String: String]) { self.sampler = sampler self.tracestate = tracestate } diff --git a/DatadogInternal/Sources/Utils/Sampler.swift b/DatadogInternal/Sources/Utils/Sampler.swift index 7a3abb009c..19b3329403 100644 --- a/DatadogInternal/Sources/Utils/Sampler.swift +++ b/DatadogInternal/Sources/Utils/Sampler.swift @@ -6,8 +6,17 @@ import Foundation +/// Protocol for determining sampling decisions. +public protocol Sampling { + /// Determines whether sampling should be performed. + /// + /// - Returns: A boolean value indicating whether sampling should occur. + /// `true` if the sample should be kept, `false` if it should be dropped. + func sample() -> Bool +} + /// Sampler, deciding if events should be sent do Datadog or dropped. -public struct Sampler { +public struct Sampler: Sampling { /// Value between `0.0` and `100.0`, where `0.0` means NO event will be sent and `100.0` means ALL events will be sent. public let samplingRate: Float @@ -21,3 +30,25 @@ public struct Sampler { return Float.random(in: 0.0..<100.0) < samplingRate } } + +/// A sampler that determines sampling decisions deterministically (the same each time). +public struct DeterministicSampler: Sampling { + /// Value between `0.0` and `100.0`, where `0.0` means NO event will be sent and `100.0` means ALL events will be sent. + public let samplingRate: Float + /// Persisted sampling decision. + private let shouldSample: Bool + + public init(sampler: Sampler) { + self.init( + shouldSample: sampler.sample(), + samplingRate: sampler.samplingRate + ) + } + + public init(shouldSample: Bool, samplingRate: Float) { + self.samplingRate = samplingRate + self.shouldSample = shouldSample + } + + public func sample() -> Bool { shouldSample } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift index 62575ba8bf..74449f63b9 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift @@ -85,6 +85,7 @@ class B3HTTPHeadersReaderTests: XCTestCase { let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNotNil(reader.read(), "When sampled, it should return trace context") + XCTAssertEqual(reader.sampled, true) } func testReadingNotSampledTraceContext() { @@ -94,5 +95,6 @@ class B3HTTPHeadersReaderTests: XCTestCase { let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNil(reader.read(), "When not sampled, it should return no trace context") + XCTAssertEqual(reader.sampled, false) } } diff --git a/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift index a4af5ee6b4..29d8b79652 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift @@ -14,6 +14,7 @@ class HTTPHeadersReaderTests: XCTestCase { let reader = HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNotNil(reader.read(), "When sampled, it should return trace context") + XCTAssertEqual(reader.sampled, true) } func testReadingNotSampledTraceContext() { @@ -22,5 +23,6 @@ class HTTPHeadersReaderTests: XCTestCase { let reader = HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNil(reader.read(), "When not sampled, it should return no trace context") + XCTAssertEqual(reader.sampled, false) } } diff --git a/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift index dd6023423f..df81412ade 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/NetworkInstrumentationFeatureTests.swift @@ -466,101 +466,23 @@ class NetworkInstrumentationFeatureTests: XCTestCase { ) } - // MARK: - URLRequest Interception + // MARK: - URLSessionTask Interception - func testGivenOpenTracing_whenInterceptingRequests_itInjectsTrace() throws { - // Given - var request: URLRequest = .mockWith(url: "https://test.com") - let writer = HTTPHeadersWriter(sampler: .mockKeepAll()) - handler.firstPartyHosts = .init(["test.com": [.datadog]]) - handler.parentSpan = TraceContext(traceID: .mock(1, 1), spanID: .mock(2)) - - // When - writer.write(traceID: .mock(1, 1), spanID: .mock(3)) - request.allHTTPHeaderFields = writer.traceHeaderFields - - let task: URLSessionTask = .mockWith(request: request, response: .mockAny()) - let feature = try XCTUnwrap(core.get(feature: NetworkInstrumentationFeature.self)) - feature.intercept(task: task, additionalFirstPartyHosts: nil) - feature.flush() - - // Then - let interception = handler.interceptions.first?.value - XCTAssertEqual(interception?.trace?.traceID, .mock(1, 1)) - XCTAssertEqual(interception?.trace?.parentSpanID, .mock(2)) - XCTAssertEqual(interception?.trace?.spanID, .mock(3)) - } - - func testGivenOpenTelemetry_b3single_whenInterceptingRequests_itInjectsTrace() throws { - // Given - var request: URLRequest = .mockWith(url: "https://test.com") - let writer = B3HTTPHeadersWriter(sampler: .mockKeepAll(), injectEncoding: .single) - handler.firstPartyHosts = .init(["test.com": [.b3]]) - - // When - writer.write(traceID: .mock(1, 1), spanID: .mock(3), parentSpanID: .mock(2)) - request.allHTTPHeaderFields = writer.traceHeaderFields - - let task: URLSessionTask = .mockWith(request: request, response: .mockAny()) - let feature = try XCTUnwrap(core.get(feature: NetworkInstrumentationFeature.self)) - feature.intercept(task: task, additionalFirstPartyHosts: nil) - feature.flush() - - // Then - let interception = handler.interceptions.first?.value - XCTAssertEqual(interception?.trace?.traceID, .mock(1, 1)) - XCTAssertEqual(interception?.trace?.parentSpanID, .mock(2)) - XCTAssertEqual(interception?.trace?.spanID, .mock(3)) - } - - func testGivenOpenTelemetry_b3multi_whenInterceptingRequests_itInjectsTrace() throws { - // Given - var request: URLRequest = .mockWith(url: "https://test.com") - let writer = B3HTTPHeadersWriter(sampler: .mockKeepAll(), injectEncoding: .multiple) - handler.firstPartyHosts = .init(["test.com": [.b3multi]]) - - // When - writer.write(traceID: .mock(1, 1), spanID: .mock(3), parentSpanID: .mock(2)) - request.allHTTPHeaderFields = writer.traceHeaderFields - - let task: URLSessionTask = .mockWith(request: request, response: .mockAny()) - let feature = try XCTUnwrap(core.get(feature: NetworkInstrumentationFeature.self)) - feature.intercept(task: task, additionalFirstPartyHosts: nil) - feature.flush() - - // Then - let interception = handler.interceptions.first?.value - XCTAssertEqual(interception?.trace?.traceID, .mock(1, 1)) - XCTAssertEqual(interception?.trace?.parentSpanID, .mock(2)) - XCTAssertEqual(interception?.trace?.spanID, .mock(3)) - } - - func testGivenW3C_whenInterceptingRequests_itInjectsTrace() throws { - // Given - var request: URLRequest = .mockWith(url: "https://test.com") - let writer = W3CHTTPHeadersWriter( - sampler: .mockKeepAll(), - tracestate: [ - W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM - ] - ) - handler.firstPartyHosts = .init(["test.com": [.tracecontext]]) - handler.parentSpan = TraceContext(traceID: .mock(1, 1), spanID: .mock(2)) + func testWhenInterceptingTaskWithMultipleTraceContexts_itTakesTheFirstContext() throws { + let traceContexts = [ + TraceContext(traceID: .mock(1, 1), spanID: .mock(2), parentSpanID: nil, sampleRate: .mockRandom(), isKept: .mockRandom()), + TraceContext(traceID: .mock(2, 2), spanID: .mock(3), parentSpanID: nil, sampleRate: .mockRandom(), isKept: .mockRandom()), + TraceContext(traceID: .mock(3, 3), spanID: .mock(4), parentSpanID: nil, sampleRate: .mockRandom(), isKept: .mockRandom()), + ] // When - writer.write(traceID: .mock(1, 1), spanID: .mock(3)) - request.allHTTPHeaderFields = writer.traceHeaderFields - - let task: URLSessionTask = .mockWith(request: request, response: .mockAny()) let feature = try XCTUnwrap(core.get(feature: NetworkInstrumentationFeature.self)) - feature.intercept(task: task, additionalFirstPartyHosts: nil) + feature.intercept(task: .mockAny(), with: traceContexts, additionalFirstPartyHosts: nil) feature.flush() // Then - let interception = handler.interceptions.first?.value - XCTAssertEqual(interception?.trace?.traceID, .mock(1, 1)) - XCTAssertEqual(interception?.trace?.parentSpanID, .mock(2)) - XCTAssertEqual(interception?.trace?.spanID, .mock(3)) + let interception = try XCTUnwrap(handler.interceptions.first?.value) + XCTAssertEqual(interception.trace, traceContexts.first, "It should register first injected Trace Context") } // MARK: - First Party Hosts @@ -700,7 +622,7 @@ class NetworkInstrumentationFeatureTests: XCTestCase { closures: [ { feature.handlers = [self.handler] }, { _ = feature.intercept(request: requests.randomElement()!, additionalFirstPartyHosts: nil) }, - { feature.intercept(task: tasks.randomElement()!, additionalFirstPartyHosts: nil) }, + { feature.intercept(task: tasks.randomElement()!, with: [], additionalFirstPartyHosts: nil) }, { feature.task(tasks.randomElement()!, didReceive: .mockRandom()) }, { feature.task(tasks.randomElement()!, didFinishCollecting: .mockAny()) }, { feature.task(tasks.randomElement()!, didCompleteWithError: nil) }, diff --git a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift index 69f611e60c..877b477a43 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift @@ -32,6 +32,7 @@ class W3CHTTPHeadersReaderTests: XCTestCase { let reader = W3CHTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNotNil(reader.read(), "When sampled, it should return trace context") + XCTAssertEqual(reader.sampled, true) } func testReadingNotSampledTraceContext() { @@ -40,5 +41,6 @@ class W3CHTTPHeadersReaderTests: XCTestCase { let reader = W3CHTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNil(reader.read(), "When not sampled, it should return no trace context") + XCTAssertEqual(reader.sampled, false) } } diff --git a/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift b/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift index 516d4a746e..162fbae7db 100644 --- a/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift +++ b/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift @@ -65,12 +65,8 @@ internal final class URLSessionRUMResourcesHandler: DatadogURLSessionHandler, RU // MARK: - DatadogURLSessionHandler - func modify(request: URLRequest, headerTypes: Set) -> URLRequest { - distributedTracing?.modify(request: request, headerTypes: headerTypes) ?? request - } - - func traceContext() -> DatadogInternal.TraceContext? { - nil // no-op + func modify(request: URLRequest, headerTypes: Set) -> (URLRequest, TraceContext?) { + distributedTracing?.modify(request: request, headerTypes: headerTypes) ?? (request, nil) } func interceptionDidStart(interception: DatadogInternal.URLSessionTaskInterception) { @@ -148,31 +144,40 @@ internal final class URLSessionRUMResourcesHandler: DatadogURLSessionHandler, RU } extension DistributedTracing { - func modify(request: URLRequest, headerTypes: Set) -> URLRequest { + func modify(request: URLRequest, headerTypes: Set) -> (URLRequest, TraceContext?) { let traceID = traceIDGenerator.generate() let spanID = spanIDGenerator.generate() + let deterministicSampler = DeterministicSampler(sampler: sampler) + let injectedSpanContext = TraceContext( + traceID: traceID, + spanID: spanID, + parentSpanID: nil, + sampleRate: deterministicSampler.samplingRate, + isKept: deterministicSampler.sample() + ) var request = request + var hasSetAnyHeader = false headerTypes.forEach { let writer: TracePropagationHeadersWriter switch $0 { case .datadog: - writer = HTTPHeadersWriter(sampler: sampler) + writer = HTTPHeadersWriter(sampler: deterministicSampler) // To make sure the generated traces from RUM don’t affect APM Index Spans counts. request.setValue("rum", forHTTPHeaderField: TracingHTTPHeaders.originField) case .b3: writer = B3HTTPHeadersWriter( - sampler: sampler, + sampler: deterministicSampler, injectEncoding: .single ) case .b3multi: writer = B3HTTPHeadersWriter( - sampler: sampler, + sampler: deterministicSampler, injectEncoding: .multiple ) case .tracecontext: writer = W3CHTTPHeadersWriter( - sampler: sampler, + sampler: deterministicSampler, tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM ] @@ -180,20 +185,21 @@ extension DistributedTracing { } writer.write( - traceID: traceID, - spanID: spanID, - parentSpanID: nil + traceID: injectedSpanContext.traceID, + spanID: injectedSpanContext.spanID, + parentSpanID: injectedSpanContext.parentSpanID ) writer.traceHeaderFields.forEach { field, value in // do not overwrite existing header if request.value(forHTTPHeaderField: field) == nil { + hasSetAnyHeader = true request.setValue(value, forHTTPHeaderField: field) } } } - return request + return (request, (hasSetAnyHeader && injectedSpanContext.isKept) ? injectedSpanContext : nil) } func trace(from interception: DatadogInternal.URLSessionTaskInterception) -> RUMSpanContext? { diff --git a/DatadogRUM/Tests/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift b/DatadogRUM/Tests/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift index b1c37a3f7e..e8e405382a 100644 --- a/DatadogRUM/Tests/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift +++ b/DatadogRUM/Tests/Instrumentation/Resources/URLSessionRUMResourcesHandlerTests.swift @@ -40,7 +40,7 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [.datadog] ) @@ -50,6 +50,13 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), "_dd.p.tid=a") XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), "64") XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "1") + + let injectedTraceContext = try XCTUnwrap(traceContext, "It must return injected trace context") + XCTAssertEqual(injectedTraceContext.traceID, .init(idHi: 10, idLo: 100)) + XCTAssertEqual(injectedTraceContext.spanID, 100) + XCTAssertNil(injectedTraceContext.parentSpanID) + XCTAssertEqual(injectedTraceContext.sampleRate, 100) + XCTAssertTrue(injectedTraceContext.isKept) } func testGivenFirstPartyInterception_withSampledTrace_itInjectB3TraceHeaders() throws { @@ -64,13 +71,20 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [.b3] ) XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.originField)) XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "000000000000000a0000000000000064-0000000000000064-1") + + let injectedTraceContext = try XCTUnwrap(traceContext, "It must return injected trace context") + XCTAssertEqual(injectedTraceContext.traceID, .init(idHi: 10, idLo: 100)) + XCTAssertEqual(injectedTraceContext.spanID, 100) + XCTAssertNil(injectedTraceContext.parentSpanID) + XCTAssertEqual(injectedTraceContext.sampleRate, 100) + XCTAssertTrue(injectedTraceContext.isKept) } func testGivenFirstPartyInterception_withSampledTrace_itInjectB3MultiTraceHeaders() throws { @@ -85,7 +99,7 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [.b3multi] ) @@ -95,6 +109,13 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField), "0000000000000064") XCTAssertNil(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.parentSpanIDField)) XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField), "1") + + let injectedTraceContext = try XCTUnwrap(traceContext, "It must return injected trace context") + XCTAssertEqual(injectedTraceContext.traceID, .init(idHi: 10, idLo: 100)) + XCTAssertEqual(injectedTraceContext.spanID, 100) + XCTAssertNil(injectedTraceContext.parentSpanID) + XCTAssertEqual(injectedTraceContext.sampleRate, 100) + XCTAssertTrue(injectedTraceContext.isKept) } func testGivenFirstPartyInterception_withSampledTrace_itInjectW3CTraceHeaders() throws { @@ -109,13 +130,20 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [.tracecontext] ) XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.originField)) XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.traceparent), "00-000000000000000a0000000000000064-0000000000000064-01") + + let injectedTraceContext = try XCTUnwrap(traceContext, "It must return injected trace context") + XCTAssertEqual(injectedTraceContext.traceID, .init(idHi: 10, idLo: 100)) + XCTAssertEqual(injectedTraceContext.spanID, 100) + XCTAssertNil(injectedTraceContext.parentSpanID) + XCTAssertEqual(injectedTraceContext.sampleRate, 100) + XCTAssertTrue(injectedTraceContext.isKept) } func testGivenFirstPartyInterception_withRejectedTrace_itDoesNotInjectDDTraceHeaders() throws { @@ -130,7 +158,7 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [.datadog] ) @@ -139,6 +167,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField)) XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField)) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "0") + + XCTAssertNil(traceContext, "It must return no trace context") } func testGivenFirstPartyInterception_withRejectedTrace_itDoesNotInjectB3TraceHeaders() throws { @@ -153,13 +183,15 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [.b3] ) XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.originField)) XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "0") + + XCTAssertNil(traceContext, "It must return no trace context") } func testGivenFirstPartyInterception_withRejectedTrace_itDoesNotInjectB3MultiTraceHeaders() throws { @@ -174,7 +206,7 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [.b3multi] ) @@ -184,6 +216,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { XCTAssertNil(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField)) XCTAssertNil(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.parentSpanIDField)) XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField), "0") + + XCTAssertNil(traceContext, "It must return no trace context") } func testGivenFirstPartyInterception_withRejectedTrace_itDoesNotInjectW3CTraceHeaders() throws { @@ -198,13 +232,15 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [.tracecontext] ) XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.originField)) XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.traceparent), "00-000000000000000a0000000000000064-0000000000000064-00") + + XCTAssertNil(traceContext, "It must return no trace context") } func testGivenFirstPartyInterception_withSampledTrace_itDoesNotOverwriteTraceHeaders() throws { @@ -219,19 +255,21 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) // When - var request: URLRequest = .mockWith(url: "https://www.example.com") - request.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.traceIDField) - request.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField) - request.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.parentSpanIDField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Single.b3Field) - request.setValue("custom", forHTTPHeaderField: W3CHTTPHeaders.traceparent) - - request = handler.modify( - request: request, + var orgRequest: URLRequest = .mockWith(url: "https://www.example.com") + orgRequest.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.traceIDField) + orgRequest.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.tagsField) + orgRequest.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField) + orgRequest.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.parentSpanIDField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Single.b3Field) + orgRequest.setValue("custom", forHTTPHeaderField: W3CHTTPHeaders.traceparent) + orgRequest.setValue("custom", forHTTPHeaderField: W3CHTTPHeaders.tracestate) + + let (request, traceContext) = handler.modify( + request: orgRequest, headerTypes: [ .datadog, .b3, @@ -241,6 +279,7 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), "custom") + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField), "custom") @@ -249,6 +288,9 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.traceparent), "custom") + XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.tracestate), "custom") + + XCTAssertNil(traceContext, "It must return no trace context") } func testGivenTaskInterceptionWithNoSpanContext_whenInterceptionStarts_itStartsRUMResource() throws { @@ -298,7 +340,9 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { taskInterception.register(trace: TraceContext( traceID: 100, spanID: 200, - parentSpanID: nil + parentSpanID: nil, + sampleRate: .mockAny(), + isKept: .mockAny() )) XCTAssertNotNil(taskInterception.trace) @@ -475,7 +519,7 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase { ) ) let request: URLRequest = .mockWith(httpMethod: "GET") - let modifiedRequest = handler.modify(request: request, headerTypes: [.datadog, .tracecontext, .b3, .b3multi]) + let (modifiedRequest, _) = handler.modify(request: request, headerTypes: [.datadog, .tracecontext, .b3, .b3multi]) XCTAssertEqual( modifiedRequest.allHTTPHeaderFields, diff --git a/DatadogTrace/Sources/DDFormat.swift b/DatadogTrace/Sources/DDFormat.swift index 01d52cb045..18e6674a3c 100644 --- a/DatadogTrace/Sources/DDFormat.swift +++ b/DatadogTrace/Sources/DDFormat.swift @@ -38,7 +38,9 @@ extension TracePropagationHeadersReader where Self: OTFormatReader { traceID: ids.traceID, spanID: ids.spanID, parentSpanID: ids.parentSpanID, - baggageItems: BaggageItems() + baggageItems: BaggageItems(), + sampleRate: tracerSampleRate ?? 0, // unreachable default: sample rate is set by the tracer during extraction + isKept: sampled ?? false // unreachable default: we got trace ID, so this request must have been instrumented ) } } diff --git a/DatadogTrace/Sources/DDSpan.swift b/DatadogTrace/Sources/DDSpan.swift index d1c18fcab6..b25dbb6779 100644 --- a/DatadogTrace/Sources/DDSpan.swift +++ b/DatadogTrace/Sources/DDSpan.swift @@ -137,8 +137,8 @@ internal final class DDSpan: OTSpan { operationName: self.operationName, startTime: self.startTime, finishTime: finishTime, - samplingRate: sampler.samplingRate / 100.0, - isKept: sampler.sample(), + samplingRate: self.ddContext.sampleRate / 100.0, + isKept: self.ddContext.isKept, tags: self.tags, baggageItems: self.ddContext.baggageItems.all, logFields: self.logFields diff --git a/DatadogTrace/Sources/DDSpanContext.swift b/DatadogTrace/Sources/DDSpanContext.swift index 825403c0e6..47bac5d818 100644 --- a/DatadogTrace/Sources/DDSpanContext.swift +++ b/DatadogTrace/Sources/DDSpanContext.swift @@ -16,6 +16,12 @@ internal struct DDSpanContext: OTSpanContext { let parentSpanID: SpanID? /// The baggage items of this span. let baggageItems: BaggageItems + /// The sample rate used for sampling this span. + /// + /// It is a value between `0.0` (drop) and `100.0` (keep), determined by the local or distributed trace sampler. + let sampleRate: Float + /// Whether this span was sampled or rejected by the sampler. + let isKept: Bool // MARK: - Open Tracing interface diff --git a/DatadogTrace/Sources/DatadogTracer.swift b/DatadogTrace/Sources/DatadogTracer.swift index 6f59610d6e..e5c53e78c6 100644 --- a/DatadogTrace/Sources/DatadogTracer.swift +++ b/DatadogTrace/Sources/DatadogTracer.swift @@ -79,7 +79,7 @@ internal class DatadogTracer: OTTracer { func startSpan(operationName: String, references: [OTReference]? = nil, tags: [String: Encodable]? = nil, startTime: Date? = nil) -> OTSpan { let parentSpanContext = references?.compactMap { $0.context.dd }.last ?? activeSpan?.context as? DDSpanContext return startSpan( - spanContext: createSpanContext(parentSpanContext: parentSpanContext), + spanContext: createSpanContext(parentSpanContext: parentSpanContext, using: localTraceSampler), operationName: operationName, tags: tags, startTime: startTime @@ -88,7 +88,7 @@ internal class DatadogTracer: OTTracer { func startRootSpan(operationName: String, tags: [String: Encodable]? = nil, startTime: Date? = nil) -> OTSpan { return startSpan( - spanContext: createSpanContext(parentSpanContext: nil), + spanContext: createSpanContext(parentSpanContext: nil, using: localTraceSampler), operationName: operationName, tags: tags, startTime: startTime @@ -101,7 +101,9 @@ internal class DatadogTracer: OTTracer { func extract(reader: OTFormatReader) -> OTSpanContext? { // TODO: RUMM-385 - make `HTTPHeadersReader` available in public API - reader.extract() + var reader = reader as? TracePropagationHeadersReader + reader?.tracerSampleRate = localTraceSampler.samplingRate + return (reader as? OTFormatReader)?.extract() } var activeSpan: OTSpan? { @@ -110,12 +112,14 @@ internal class DatadogTracer: OTTracer { // MARK: - Internal - internal func createSpanContext(parentSpanContext: DDSpanContext? = nil) -> DDSpanContext { + internal func createSpanContext(parentSpanContext: DDSpanContext?, using sampler: Sampler) -> DDSpanContext { return DDSpanContext( traceID: parentSpanContext?.traceID ?? traceIDGenerator.generate(), spanID: spanIDGenerator.generate(), parentSpanID: parentSpanContext?.spanID, - baggageItems: BaggageItems(parent: parentSpanContext?.baggageItems) + baggageItems: BaggageItems(parent: parentSpanContext?.baggageItems), + sampleRate: parentSpanContext?.sampleRate ?? sampler.samplingRate, + isKept: parentSpanContext?.isKept ?? sampler.sample() ) } diff --git a/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift b/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift index 2645277273..43f1a4c793 100644 --- a/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift +++ b/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift @@ -29,63 +29,61 @@ internal struct TracingURLSessionHandler: DatadogURLSessionHandler { self.firstPartyHosts = firstPartyHosts } - func modify(request: URLRequest, headerTypes: Set) -> URLRequest { + func modify(request: URLRequest, headerTypes: Set) -> (URLRequest, TraceContext?) { guard let tracer = tracer else { - return request + return (request, nil) } // Use the current active span as parent if the propagation // headers support it. let parentSpanContext = tracer.activeSpan?.context as? DDSpanContext - let spanContext = tracer.createSpanContext(parentSpanContext: parentSpanContext) + let spanContext = tracer.createSpanContext(parentSpanContext: parentSpanContext, using: distributedTraceSampler) + let injectedSpanContext = TraceContext( + traceID: spanContext.traceID, + spanID: spanContext.spanID, + parentSpanID: spanContext.parentSpanID, + sampleRate: spanContext.sampleRate, + isKept: spanContext.isKept + ) + let sampler = DeterministicSampler(shouldSample: spanContext.isKept, samplingRate: spanContext.sampleRate) var request = request + var hasSetAnyHeader = false headerTypes.forEach { let writer: TracePropagationHeadersWriter switch $0 { case .datadog: - writer = HTTPHeadersWriter(sampler: distributedTraceSampler) + writer = HTTPHeadersWriter(sampler: sampler) case .b3: writer = B3HTTPHeadersWriter( - sampler: distributedTraceSampler, + sampler: sampler, injectEncoding: .single ) case .b3multi: writer = B3HTTPHeadersWriter( - sampler: distributedTraceSampler, + sampler: sampler, injectEncoding: .multiple ) case .tracecontext: - writer = W3CHTTPHeadersWriter(sampler: distributedTraceSampler, tracestate: [:]) + writer = W3CHTTPHeadersWriter(sampler: sampler, tracestate: [:]) } writer.write( - traceID: spanContext.traceID, - spanID: spanContext.spanID, - parentSpanID: spanContext.parentSpanID + traceID: injectedSpanContext.traceID, + spanID: injectedSpanContext.spanID, + parentSpanID: injectedSpanContext.parentSpanID ) writer.traceHeaderFields.forEach { field, value in // do not overwrite existing header if request.value(forHTTPHeaderField: field) == nil { + hasSetAnyHeader = true request.setValue(value, forHTTPHeaderField: field) } } } - return request - } - - func traceContext() -> DatadogInternal.TraceContext? { - guard let context = tracer?.activeSpan?.context as? DDSpanContext else { - return nil - } - - return TraceContext( - traceID: context.traceID, - spanID: context.spanID, - parentSpanID: context.parentSpanID - ) + return (request, (hasSetAnyHeader && injectedSpanContext.isKept) ? injectedSpanContext : nil) } func interceptionDidStart(interception: DatadogInternal.URLSessionTaskInterception) { @@ -110,7 +108,9 @@ internal struct TracingURLSessionHandler: DatadogURLSessionHandler { traceID: trace.traceID, spanID: trace.spanID, parentSpanID: trace.parentSpanID, - baggageItems: .init() + baggageItems: .init(), + sampleRate: trace.sampleRate, + isKept: trace.isKept ) span = tracer.startSpan( diff --git a/DatadogTrace/Tests/DatadogTracer+SamplingTests.swift b/DatadogTrace/Tests/DatadogTracer+SamplingTests.swift index e815d26dd8..55955fafb2 100644 --- a/DatadogTrace/Tests/DatadogTracer+SamplingTests.swift +++ b/DatadogTrace/Tests/DatadogTracer+SamplingTests.swift @@ -81,7 +81,6 @@ class DatadogTracer_SamplingTests: XCTestCase { XCTAssertEqual(events.filter({ $0.samplingRate == 0.42 }).count, 3, "All spans must encode the same sample rate") } - // TODO: RUM-3470 Enable this test when head-based sampling is supported func testWhenRootSpanIsSampled_thenAllChildSpansMustBeSampledTheSameWay() throws { // When let tracer = createTracer(sampleRate: 50) diff --git a/DatadogTrace/Tests/TracingFeatureMocks.swift b/DatadogTrace/Tests/TracingFeatureMocks.swift index 6ffdae242c..59f4bb5b84 100644 --- a/DatadogTrace/Tests/TracingFeatureMocks.swift +++ b/DatadogTrace/Tests/TracingFeatureMocks.swift @@ -21,13 +21,17 @@ extension DDSpanContext { traceID: TraceID = .mockAny(), spanID: SpanID = .mockAny(), parentSpanID: SpanID? = .mockAny(), - baggageItems: BaggageItems = .mockAny() + baggageItems: BaggageItems = .mockAny(), + sampleRate: Float = .mockAny(), + isKept: Bool = .mockAny() ) -> DDSpanContext { return DDSpanContext( traceID: traceID, spanID: spanID, parentSpanID: parentSpanID, - baggageItems: baggageItems + baggageItems: baggageItems, + sampleRate: sampleRate, + isKept: isKept ) } } diff --git a/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift b/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift index dd791ab814..8a7cac2a74 100644 --- a/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift +++ b/DatadogTrace/Tests/TracingURLSessionHandlerTests.swift @@ -53,7 +53,7 @@ class TracingURLSessionHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [ .datadog, @@ -73,6 +73,14 @@ class TracingURLSessionHandlerTests: XCTestCase { XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField), "1") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "000000000000000a0000000000000064-0000000000000064-1") XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.traceparent), "00-000000000000000a0000000000000064-0000000000000064-01") + XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.tracestate), "dd=p:0000000000000064;s:1") + + let injectedTraceContext = try XCTUnwrap(traceContext, "It must return injected trace context") + XCTAssertEqual(injectedTraceContext.traceID, .init(idHi: 10, idLo: 100)) + XCTAssertEqual(injectedTraceContext.spanID, 100) + XCTAssertNil(injectedTraceContext.parentSpanID) + XCTAssertEqual(injectedTraceContext.sampleRate, 100) + XCTAssertTrue(injectedTraceContext.isKept) } func testGivenFirstPartyInterception_withSampledTrace_itDoesNotOverwriteTraceHeaders() throws { @@ -85,19 +93,21 @@ class TracingURLSessionHandlerTests: XCTestCase { ) // When - var request: URLRequest = .mockWith(url: "https://www.example.com") - request.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.traceIDField) - request.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField) - request.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.parentSpanIDField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField) - request.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Single.b3Field) - request.setValue("custom", forHTTPHeaderField: W3CHTTPHeaders.traceparent) - - request = handler.modify( - request: request, + var orgRequest: URLRequest = .mockWith(url: "https://www.example.com") + orgRequest.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.traceIDField) + orgRequest.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.tagsField) + orgRequest.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField) + orgRequest.setValue("custom", forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.parentSpanIDField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField) + orgRequest.setValue("custom", forHTTPHeaderField: B3HTTPHeaders.Single.b3Field) + orgRequest.setValue("custom", forHTTPHeaderField: W3CHTTPHeaders.traceparent) + orgRequest.setValue("custom", forHTTPHeaderField: W3CHTTPHeaders.tracestate) + + let (request, traceContext) = handler.modify( + request: orgRequest, headerTypes: [ .datadog, .b3, @@ -107,6 +117,7 @@ class TracingURLSessionHandlerTests: XCTestCase { ) XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.traceIDField), "custom") + XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.tagsField), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.parentSpanIDField), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: TracingHTTPHeaders.samplingPriorityField), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField), "custom") @@ -115,6 +126,9 @@ class TracingURLSessionHandlerTests: XCTestCase { XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "custom") XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.traceparent), "custom") + XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.tracestate), "custom") + + XCTAssertNil(traceContext, "It must return no trace context") } func testGivenFirstPartyInterception_withRejectedTrace_itDoesNotInjectTraceHeaders() throws { @@ -127,7 +141,7 @@ class TracingURLSessionHandlerTests: XCTestCase { ) // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [ .datadog, @@ -146,6 +160,8 @@ class TracingURLSessionHandlerTests: XCTestCase { XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField), "0") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "0") XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.traceparent), "00-000000000000000a0000000000000064-0000000000000064-00") + + XCTAssertNil(traceContext, "It must return no trace context") } func testGivenFirstPartyInterception_withActiveSpan_itInjectParentSpanID() throws { @@ -161,7 +177,7 @@ class TracingURLSessionHandlerTests: XCTestCase { span.setActive() // When - let request = handler.modify( + let (request, traceContext) = handler.modify( request: .mockWith(url: "https://www.example.com"), headerTypes: [ .datadog, @@ -183,10 +199,19 @@ class TracingURLSessionHandlerTests: XCTestCase { XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField), "1") XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "000000000000000a0000000000000064-0000000000000065-1-0000000000000064") XCTAssertEqual(request.value(forHTTPHeaderField: W3CHTTPHeaders.traceparent), "00-000000000000000a0000000000000064-0000000000000065-01") + + let injectedTraceContext = try XCTUnwrap(traceContext, "It must return injected trace context") + XCTAssertEqual(injectedTraceContext.traceID, .init(idHi: 10, idLo: 100)) + XCTAssertEqual(injectedTraceContext.spanID, 101) + XCTAssertEqual(injectedTraceContext.parentSpanID, span.context.dd.spanID) + XCTAssertEqual(injectedTraceContext.sampleRate, span.context.dd.sampleRate) + XCTAssertEqual(injectedTraceContext.isKept, span.context.dd.isKept) } func testGivenFirstPartyInterceptionWithSpanContext_whenInterceptionCompletes_itUsesInjectedSpanContext() throws { core.expectation = expectation(description: "Send span") + let sampleRate: Float = .mockRandom(min: 1, max: 100) + let isKept: Bool = .mockRandom() // Given let interception = URLSessionTaskInterception( @@ -205,7 +230,9 @@ class TracingURLSessionHandlerTests: XCTestCase { interception.register(trace: TraceContext( traceID: 100, spanID: 200, - parentSpanID: nil + parentSpanID: nil, + sampleRate: sampleRate, + isKept: isKept )) // When @@ -222,6 +249,8 @@ class TracingURLSessionHandlerTests: XCTestCase { XCTAssertEqual(span.operationName, "urlsession.request") XCTAssertFalse(span.isError) XCTAssertEqual(span.duration, 1) + XCTAssertEqual(span.samplingRate, sampleRate / 100) + XCTAssertEqual(span.isKept, isKept) } func testGivenFirstPartyInterceptionWithNoError_whenInterceptionCompletes_itEncodesRequestInfoInSpan() throws { @@ -258,70 +287,6 @@ class TracingURLSessionHandlerTests: XCTestCase { XCTAssertEqual(span.tags.count, 5) } - func testTraceContext_whenInterceptionStarts_withActiveSpan_itReturnCurrentSpan() { - // When - let span = tracer.startRootSpan(operationName: "root") - span.setActive() - // Then - let context = handler.traceContext() - XCTAssertEqual(context?.traceID, TraceID(idHi: 10, idLo: 100)) - XCTAssertEqual(context?.spanID, SpanID(100)) - - // When - span.finish() - // Then - XCTAssertNil(handler.traceContext()) - } - - func testGivenFirstPartyInterception_whenInterceptionStarts_withActiveSpan_itSendParentSpanID() throws { - core.expectation = expectation(description: "Send span") - core.expectation?.expectedFulfillmentCount = 2 - - // Given - let request: ImmutableRequest = .mockWith(httpMethod: "POST") - let interception = URLSessionTaskInterception(request: request, isFirstParty: true) - - // When - let span = tracer.startRootSpan(operationName: "root") - span.setActive() - interception.register(trace: TraceContext( - traceID: span.context.dd.traceID, - spanID: SpanID(300), - parentSpanID: span.context.dd.spanID - )) - handler.interceptionDidStart(interception: interception) - // Then - XCTAssertEqual(interception.trace?.parentSpanID?.rawValue, 100) - - // When - span.finish() - interception.register(response: .mockResponseWith(statusCode: 200), error: nil) - interception.register( - metrics: .mockWith( - fetch: .init( - start: .mockDecember15th2019At10AMUTC(), - end: .mockDecember15th2019At10AMUTC(addingTimeInterval: 2) - ) - ) - ) - handler.interceptionDidComplete(interception: interception) - - // Then - waitForExpectations(timeout: 0.5, handler: nil) - - let envelopes: [SpanEventsEnvelope] = core.events() - let event1 = try XCTUnwrap(envelopes.first?.spans.first) - XCTAssertEqual(event1.operationName, "root") - XCTAssertEqual(event1.traceID, TraceID(idHi: 10, idLo: 100)) - XCTAssertEqual(event1.spanID, SpanID(100)) - XCTAssertNil(event1.parentID) - let event2 = try XCTUnwrap(envelopes.last?.spans.first) - XCTAssertEqual(event2.operationName, "urlsession.request") - XCTAssertEqual(event2.traceID, TraceID(idHi: 10, idLo: 100)) - XCTAssertEqual(event2.parentID, SpanID(100)) - XCTAssertEqual(event2.spanID, SpanID(300)) - } - func testGivenFirstPartyIncompleteInterception_whenInterceptionCompletes_itDoesNotSendTheSpan() throws { core.expectation = expectation(description: "Do not send span") core.expectation?.isInverted = true diff --git a/TestUtilities/Mocks/FoundationMocks.swift b/TestUtilities/Mocks/FoundationMocks.swift index 682b89f7cc..ecfe748c8d 100644 --- a/TestUtilities/Mocks/FoundationMocks.swift +++ b/TestUtilities/Mocks/FoundationMocks.swift @@ -620,7 +620,7 @@ extension URLSessionTask { return URLSessionDataTaskMock(request: .mockAny(), response: .mockAny()) } - public static func mockWith(request: URLRequest, response: HTTPURLResponse) -> URLSessionDataTask { + public static func mockWith(request: URLRequest = .mockAny(), response: HTTPURLResponse = .mockAny()) -> URLSessionDataTask { return URLSessionDataTaskMock(request: request, response: response) } } diff --git a/TestUtilities/Mocks/NetworkInstrumentationMocks.swift b/TestUtilities/Mocks/NetworkInstrumentationMocks.swift index 9d88f1008a..97777b242f 100644 --- a/TestUtilities/Mocks/NetworkInstrumentationMocks.swift +++ b/TestUtilities/Mocks/NetworkInstrumentationMocks.swift @@ -88,7 +88,7 @@ public final class URLSessionHandlerMock: DatadogURLSessionHandler { public var firstPartyHosts: FirstPartyHosts public var modifiedRequest: URLRequest? - public var parentSpan: TraceContext? + public var injectedTraceContext: TraceContext? public var shouldInterceptRequest: ((URLRequest) -> Bool)? public var onRequestMutation: ((URLRequest, Set) -> Void)? @@ -111,13 +111,9 @@ public final class URLSessionHandlerMock: DatadogURLSessionHandler { interceptions.values.first { $0.request.url == url } } - public func modify(request: URLRequest, headerTypes: Set) -> URLRequest { + public func modify(request: URLRequest, headerTypes: Set) -> (URLRequest, TraceContext?) { onRequestMutation?(request, headerTypes) - return modifiedRequest ?? request - } - - public func traceContext() -> TraceContext? { - parentSpan + return (modifiedRequest ?? request, injectedTraceContext) } public func interceptionDidStart(interception: URLSessionTaskInterception) { From c0f09b2cdb291e41e05b5757cbdfb96b8f9405e8 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Fri, 19 Apr 2024 11:21:03 +0200 Subject: [PATCH 2/7] RUM-3470 CR feedback - remove `tracerSampleRate` from HTTP header readers --- .../B3/B3HTTPHeadersReader.swift | 3 --- .../Datadog/HTTPHeadersReader.swift | 3 --- .../TracePropagationHeadersReader.swift | 3 --- .../W3C/W3CHTTPHeadersReader.swift | 3 --- DatadogTrace/Sources/DDFormat.swift | 8 ++++++-- DatadogTrace/Sources/DatadogTracer.swift | 15 ++++++++++++--- 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift index 76a1de1713..58a9fea544 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/B3/B3HTTPHeadersReader.swift @@ -12,9 +12,6 @@ public typealias OTelHTTPHeadersReader = B3HTTPHeadersReader public class B3HTTPHeadersReader: TracePropagationHeadersReader { private let httpHeaderFields: [String: String] - @ReadWriteLock - public var tracerSampleRate: Float? = nil - public init(httpHeaderFields: [String: String]) { self.httpHeaderFields = httpHeaderFields } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersReader.swift b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersReader.swift index 18c64b4755..577cd8a0e8 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersReader.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/Datadog/HTTPHeadersReader.swift @@ -9,9 +9,6 @@ import Foundation public class HTTPHeadersReader: TracePropagationHeadersReader { private let httpHeaderFields: [String: String] - @ReadWriteLock - public var tracerSampleRate: Float? = nil - public init(httpHeaderFields: [String: String]) { self.httpHeaderFields = httpHeaderFields } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersReader.swift b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersReader.swift index f9dfcf906b..9de4c9de12 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersReader.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersReader.swift @@ -16,7 +16,4 @@ public protocol TracePropagationHeadersReader { /// Indicates whether the trace was sampled based on the provided headers. var sampled: Bool? { get } - - /// The sample rate used to sample this trace. - var tracerSampleRate: Float? { set get } } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersReader.swift b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersReader.swift index f36674f1dd..b8034ddaf5 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersReader.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersReader.swift @@ -9,9 +9,6 @@ import Foundation public class W3CHTTPHeadersReader: TracePropagationHeadersReader { private let httpHeaderFields: [String: String] - @ReadWriteLock - public var tracerSampleRate: Float? = nil - public init(httpHeaderFields: [String: String]) { self.httpHeaderFields = httpHeaderFields } diff --git a/DatadogTrace/Sources/DDFormat.swift b/DatadogTrace/Sources/DDFormat.swift index 18e6674a3c..a0e55f4ed7 100644 --- a/DatadogTrace/Sources/DDFormat.swift +++ b/DatadogTrace/Sources/DDFormat.swift @@ -39,8 +39,12 @@ extension TracePropagationHeadersReader where Self: OTFormatReader { spanID: ids.spanID, parentSpanID: ids.parentSpanID, baggageItems: BaggageItems(), - sampleRate: tracerSampleRate ?? 0, // unreachable default: sample rate is set by the tracer during extraction - isKept: sampled ?? false // unreachable default: we got trace ID, so this request must have been instrumented + // RUM-3470: The `0` sample rate set here is only a placeholder value. It is overwritten with + // the actual value in the caller: `Tracer.extract(reader)`. + sampleRate: 0, + // RUM-3470: The `false` default will be never reached. As we got trace and span ID, + // it means that the request has been instrumented, so sampling decision was read as well. + isKept: sampled ?? false ) } } diff --git a/DatadogTrace/Sources/DatadogTracer.swift b/DatadogTrace/Sources/DatadogTracer.swift index e5c53e78c6..eb77ea4af7 100644 --- a/DatadogTrace/Sources/DatadogTracer.swift +++ b/DatadogTrace/Sources/DatadogTracer.swift @@ -101,9 +101,18 @@ internal class DatadogTracer: OTTracer { func extract(reader: OTFormatReader) -> OTSpanContext? { // TODO: RUMM-385 - make `HTTPHeadersReader` available in public API - var reader = reader as? TracePropagationHeadersReader - reader?.tracerSampleRate = localTraceSampler.samplingRate - return (reader as? OTFormatReader)?.extract() + guard let context = reader.extract() as? DDSpanContext else { + return nil + } + + return DDSpanContext( + traceID: context.traceID, + spanID: context.spanID, + parentSpanID: context.parentSpanID, + baggageItems: context.baggageItems, + sampleRate: localTraceSampler.samplingRate, + isKept: context.isKept + ) } var activeSpan: OTSpan? { From d8c2f2bd19f8f016dcfc76c6aa58e6fe3c451bf4 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Wed, 24 Apr 2024 10:31:21 +0200 Subject: [PATCH 3/7] RUM-3470 Support head-based sampling for manual distributed traces --- Datadog/Datadog.xcodeproj/project.pbxproj | 12 + .../xcschemes/DatadogCore iOS.xcscheme | 6 - ...ugManualTraceInjectionViewController.swift | 11 +- .../Trace/HeadBasedSamplingTests.swift | 18 +- DatadogCore/Tests/Datadog/TracerTests.swift | 335 +++--------------- .../B3/B3HTTPHeadersWriter.swift | 42 ++- .../Datadog/HTTPHeadersWriter.swift | 32 +- .../TracePropagationHeadersWriter.swift | 35 +- .../W3C/W3CHTTPHeadersWriter.swift | 26 +- DatadogInternal/Sources/Utils/Sampler.swift | 15 +- .../B3HTTPHeadersReaderTests.swift | 9 +- .../B3HTTPHeadersWriterTests.swift | 165 +++++++-- .../HTTPHeadersReaderTests.swift | 9 +- .../HTTPHeadersWriterTests.swift | 83 +++++ .../W3CHTTPHeadersReaderTests.swift | 8 +- .../W3CHTTPHeadersWriterTests.swift | 83 ++++- .../B3HTTPHeadersWriter+objc.swift | 2 +- .../Propagation/HTTPHeadersWriter+objc.swift | 4 +- .../W3CHTTPHeadersWriter+objc.swift | 5 +- .../URLSessionRUMResourcesHandler.swift | 19 +- DatadogTrace/Sources/DDFormat.swift | 11 +- .../TracingURLSessionHandler.swift | 23 +- DatadogTrace/Sources/TraceConfiguration.swift | 1 + DatadogTrace/Tests/DDNoopTracerTests.swift | 2 +- .../DatadogTracer+InjectAndExtract.swift | 99 ++++++ .../Mocks/NetworkInstrumentationMocks.swift | 44 ++- 26 files changed, 622 insertions(+), 477 deletions(-) create mode 100644 DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift create mode 100644 DatadogTrace/Tests/DatadogTracer+InjectAndExtract.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 7139e51339..131f5f9e00 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -300,6 +300,10 @@ 614CADD72510BAC000B93D2D /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614CADD62510BAC000B93D2D /* Environment.swift */; }; 614E9EB3244719FA007EE3E1 /* BundleType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614E9EB2244719FA007EE3E1 /* BundleType.swift */; }; 614ED36C260352DC00C8C519 /* CrashReporter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */; }; + 615192CD2BD6948B0005A782 /* HTTPHeadersWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615192CC2BD6948B0005A782 /* HTTPHeadersWriterTests.swift */; }; + 615192CE2BD6948B0005A782 /* HTTPHeadersWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615192CC2BD6948B0005A782 /* HTTPHeadersWriterTests.swift */; }; + 615192D02BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615192CF2BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift */; }; + 615192D12BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615192CF2BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift */; }; 61570005246AADFA00E96950 /* DatadogObjc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61133BF0242397DA00786299 /* DatadogObjc.framework */; }; 615A4A8324A3431600233986 /* Trace+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8224A3431600233986 /* Trace+objc.swift */; }; 615A4A8924A34FD700233986 /* DDTracerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 615A4A8824A34FD700233986 /* DDTracerTests.swift */; }; @@ -2244,6 +2248,8 @@ 614CADD62510BAC000B93D2D /* Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; }; 614E9EB2244719FA007EE3E1 /* BundleType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleType.swift; sourceTree = ""; }; 614ED36B260352DC00C8C519 /* CrashReporter.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = CrashReporter.xcframework; path = ../Carthage/Build/CrashReporter.xcframework; sourceTree = ""; }; + 615192CC2BD6948B0005A782 /* HTTPHeadersWriterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeadersWriterTests.swift; sourceTree = ""; }; + 615192CF2BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatadogTracer+InjectAndExtract.swift"; sourceTree = ""; }; 6152C84224BE2165006A1679 /* MockServerAddress.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = MockServerAddress.local.xcconfig; sourceTree = ""; }; 615519252461BCE7002A85CF /* Datadog.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.xcconfig; sourceTree = ""; }; 615519262461BCE7002A85CF /* Datadog.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Datadog.local.xcconfig; sourceTree = ""; }; @@ -5735,6 +5741,7 @@ 619CE75D2A458CE1005588CB /* TraceConfigurationTests.swift */, 61AD4E172451C7FF006E34EA /* TracingFeatureMocks.swift */, 61F3E3622BC5556D00C7881E /* DatadogTracer+SamplingTests.swift */, + 615192CF2BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift */, 61C5A89824509C1100DA608C /* DDSpanTests.swift */, 61F1A620249A45E400075390 /* DDSpanContextTests.swift */, D2F1B81426D8E5FF009F3293 /* DDNoopTracerTests.swift */, @@ -6049,6 +6056,7 @@ 61B558D32469CDD8001460D3 /* TraceIDGeneratorTests.swift */, 3CCECDAE2BC688120013C125 /* SpanIDGeneratorTests.swift */, 61F3E3652BC595F600C7881E /* HTTPHeadersReaderTests.swift */, + 615192CC2BD6948B0005A782 /* HTTPHeadersWriterTests.swift */, A79B0F5A292B7C06008742B3 /* B3HTTPHeadersWriterTests.swift */, A79B0F60292BB071008742B3 /* B3HTTPHeadersReaderTests.swift */, A728ADA22934DB5000397996 /* W3CHTTPHeadersWriterTests.swift */, @@ -8561,6 +8569,7 @@ 61F3E3632BC5556D00C7881E /* DatadogTracer+SamplingTests.swift in Sources */, D2C1A51C29C4C75700946C31 /* ContextMessageReceiverTests.swift in Sources */, 619CE7612A458D66005588CB /* TraceTests.swift in Sources */, + 615192D02BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift in Sources */, D2C1A52029C4C75700946C31 /* DDSpanTests.swift in Sources */, D2C1A51B29C4C75700946C31 /* DDSpanContextTests.swift in Sources */, D2C1A52729C4C7D000946C31 /* TracingFeatureMocks.swift in Sources */, @@ -8764,6 +8773,7 @@ 61F3E3642BC5556D00C7881E /* DatadogTracer+SamplingTests.swift in Sources */, D2C1A56529C4F2E800946C31 /* ContextMessageReceiverTests.swift in Sources */, 619CE7622A458D66005588CB /* TraceTests.swift in Sources */, + 615192D12BD6B7C90005A782 /* DatadogTracer+InjectAndExtract.swift in Sources */, D2C1A56629C4F2E800946C31 /* DDSpanTests.swift in Sources */, D2C1A56729C4F2E800946C31 /* DDSpanContextTests.swift in Sources */, D2C1A56829C4F2E800946C31 /* TracingFeatureMocks.swift in Sources */, @@ -9193,6 +9203,7 @@ D2160CE929C0E00200FAA9A5 /* MethodSwizzlerTests.swift in Sources */, D2216EC32A96649500ADAEC8 /* FeatureBaggageTests.swift in Sources */, D2160CDC29C0DF6700FAA9A5 /* HostsSanitizerTests.swift in Sources */, + 615192CD2BD6948B0005A782 /* HTTPHeadersWriterTests.swift in Sources */, D2F44FB8299AA1DA0074B0D9 /* DataCompressionTests.swift in Sources */, D2160CE029C0DF6700FAA9A5 /* URLSessionDelegateAsSuperclassTests.swift in Sources */, D2EBEE3B29BA163E00B15732 /* B3HTTPHeadersReaderTests.swift in Sources */, @@ -9239,6 +9250,7 @@ D2EBEE4229BA163F00B15732 /* W3CHTTPHeadersReaderTests.swift in Sources */, D2216EC42A96649700ADAEC8 /* FeatureBaggageTests.swift in Sources */, D2160CDD29C0DF6700FAA9A5 /* HostsSanitizerTests.swift in Sources */, + 615192CE2BD6948B0005A782 /* HTTPHeadersWriterTests.swift in Sources */, D2F44FB9299AA1DB0074B0D9 /* DataCompressionTests.swift in Sources */, D2160CE129C0DF6700FAA9A5 /* URLSessionDelegateAsSuperclassTests.swift in Sources */, D2EBEE3F29BA163F00B15732 /* B3HTTPHeadersReaderTests.swift in Sources */, diff --git a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme index b8ac29093d..3992068e2e 100644 --- a/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme +++ b/Datadog/Datadog.xcodeproj/xcshareddata/xcschemes/DatadogCore iOS.xcscheme @@ -209,12 +209,6 @@ - - - - Sampling { + switch self { + case .auto: + return DeterministicSampler(shouldSample: traceContext.isKept, samplingRate: traceContext.sampleRate) + case .custom(let sampleRate): + return Sampler(samplingRate: sampleRate) + } + } +} + /// Write interface for a custom carrier public protocol TracePropagationHeadersWriter { var traceHeaderFields: [String: String] { get } - /// Inject a span context into the custom carrier - /// - /// - parameter spanContext: context to inject into the custom carrier - func write(traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?) -} - -extension TracePropagationHeadersWriter { - public func write(traceID: TraceID, spanID: SpanID) { - write(traceID: traceID, spanID: spanID, parentSpanID: nil) - } + func write(traceContext: TraceContext) } diff --git a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift index e2eb792570..036f07feca 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/W3C/W3CHTTPHeadersWriter.swift @@ -38,17 +38,13 @@ public class W3CHTTPHeadersWriter: TracePropagationHeadersWriter { /// This value will be merged with the tracestate from the trace context. private let tracestate: [String: String] - /// The tracing sampler. - /// - /// This value will decide of the `FLAG_SAMPLED` header field value - /// and if `trace-id`, `span-id` are propagated. - private let sampler: Sampling + private let samplingStrategy: TraceSamplingStrategy /// Initializes the headers writer. /// /// - Parameter samplingRate: The sampling rate applied for headers injection. /// - Parameter tracestate: The tracestate to be injected. - @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(sampleRate:)` instead.") + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public convenience init(samplingRate: Float) { self.init(sampleRate: samplingRate, tracestate: [:]) } @@ -57,16 +53,17 @@ public class W3CHTTPHeadersWriter: TracePropagationHeadersWriter { /// /// - Parameter sampleRate: The sampling rate applied for headers injection, with 20% as the default. /// - Parameter tracestate: The tracestate to be injected. + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public convenience init(sampleRate: Float = 20, tracestate: [String: String] = [:]) { - self.init(sampler: Sampler(samplingRate: sampleRate), tracestate: tracestate) + self.init(samplingStrategy: .custom(sampleRate: sampleRate), tracestate: tracestate) } /// Initializes the headers writer. /// - /// - Parameter sampler: The sampler used for headers injection. + /// - Parameter samplingStrategy: The strategy for sampling trace propagation headers. /// - Parameter tracestate: The tracestate to be injected. - public init(sampler: Sampling, tracestate: [String: String]) { - self.sampler = sampler + public init(samplingStrategy: TraceSamplingStrategy, tracestate: [String: String] = [:]) { + self.samplingStrategy = samplingStrategy self.tracestate = tracestate } @@ -75,15 +72,16 @@ public class W3CHTTPHeadersWriter: TracePropagationHeadersWriter { /// - Parameter traceID: The trace ID. /// - Parameter spanID: The span ID. /// - Parameter parentSpanID: The parent span ID, if applicable. - public func write(traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?) { + public func write(traceContext: TraceContext) { typealias Constants = W3CHTTPHeaders.Constants + let sampler = samplingStrategy.sampler(for: traceContext) let sampled = sampler.sample() traceHeaderFields[W3CHTTPHeaders.traceparent] = [ Constants.version, - String(traceID, representation: .hexadecimal32Chars), - String(spanID, representation: .hexadecimal16Chars), + String(traceContext.traceID, representation: .hexadecimal32Chars), + String(traceContext.spanID, representation: .hexadecimal16Chars), sampled ? Constants.sampledValue : Constants.unsampledValue ] .joined(separator: Constants.separator) @@ -92,7 +90,7 @@ public class W3CHTTPHeadersWriter: TracePropagationHeadersWriter { // over the ones from the trace context let tracestate: [String: String] = [ Constants.sampling: "\(sampled ? 1 : 0)", - Constants.parentId: String(spanID, representation: .hexadecimal16Chars) + Constants.parentId: String(traceContext.spanID, representation: .hexadecimal16Chars) ].merging(tracestate) { old, new in return new } diff --git a/DatadogInternal/Sources/Utils/Sampler.swift b/DatadogInternal/Sources/Utils/Sampler.swift index 19b3329403..e87a82f981 100644 --- a/DatadogInternal/Sources/Utils/Sampler.swift +++ b/DatadogInternal/Sources/Utils/Sampler.swift @@ -32,23 +32,16 @@ public struct Sampler: Sampling { } /// A sampler that determines sampling decisions deterministically (the same each time). -public struct DeterministicSampler: Sampling { +internal struct DeterministicSampler: Sampling { /// Value between `0.0` and `100.0`, where `0.0` means NO event will be sent and `100.0` means ALL events will be sent. - public let samplingRate: Float + let samplingRate: Float /// Persisted sampling decision. private let shouldSample: Bool - public init(sampler: Sampler) { - self.init( - shouldSample: sampler.sample(), - samplingRate: sampler.samplingRate - ) - } - - public init(shouldSample: Bool, samplingRate: Float) { + init(shouldSample: Bool, samplingRate: Float) { self.samplingRate = samplingRate self.shouldSample = shouldSample } - public func sample() -> Bool { shouldSample } + func sample() -> Bool { shouldSample } } diff --git a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift index 74449f63b9..cec2091c1c 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersReaderTests.swift @@ -5,6 +5,7 @@ */ import XCTest +import TestUtilities import DatadogInternal class B3HTTPHeadersReaderTests: XCTestCase { @@ -80,8 +81,8 @@ class B3HTTPHeadersReaderTests: XCTestCase { func testReadingSampledTraceContext() { let encoding: B3HTTPHeadersWriter.InjectEncoding = [.multiple, .single].randomElement()! - let writer = B3HTTPHeadersWriter(sampleRate: 100, injectEncoding: encoding) - writer.write(traceID: .mockAny(), spanID: .mockAny(), parentSpanID: .mockAny()) + let writer = B3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100), injectEncoding: encoding) + writer.write(traceContext: .mockRandom()) let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNotNil(reader.read(), "When sampled, it should return trace context") @@ -90,8 +91,8 @@ class B3HTTPHeadersReaderTests: XCTestCase { func testReadingNotSampledTraceContext() { let encoding: B3HTTPHeadersWriter.InjectEncoding = [.multiple, .single].randomElement()! - let writer = B3HTTPHeadersWriter(sampleRate: 0, injectEncoding: encoding) - writer.write(traceID: .mockAny(), spanID: .mockAny(), parentSpanID: .mockAny()) + let writer = B3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0), injectEncoding: encoding) + writer.write(traceContext: .mockRandom()) let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNil(reader.read(), "When not sampled, it should return no trace context") diff --git a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift index 9565b26f30..c15995db92 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift @@ -5,37 +5,80 @@ */ import XCTest -@testable import DatadogInternal +import TestUtilities +import DatadogInternal class B3HTTPHeadersWriterTests: XCTestCase { - func testItWritesSingleHeader() { - let sampler: Sampler = .mockKeepAll() + func testWritingSampledTraceContext_withSingleEncoding_andAutoSamplingStrategy() { let writer = B3HTTPHeadersWriter( - sampler: sampler, + samplingStrategy: .auto, injectEncoding: .single ) writer.write( - traceID: .init(idHi: 1_234, idLo: 1_234), - spanID: 2_345, - parentSpanID: 5_678 + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: true + ) ) let headers = writer.traceHeaderFields XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "00000000000004d200000000000004d2-0000000000000929-1-000000000000162e") } - func testItWritesSingleHeaderWithSampling() { - let sampler: Sampler = .mockRejectAll() + func testWritingDroppedTraceContext_withSingleEncoding_andAutoSamplingStrategy() { let writer = B3HTTPHeadersWriter( - sampler: sampler, + samplingStrategy: .auto, injectEncoding: .single ) writer.write( - traceID: .init(idHi: 1_234, idLo: 1_234), - spanID: 2_345, - parentSpanID: 5_678 + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: false + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "0") + } + + func testWritingSampledTraceContext_withSingleEncoding_andCustomSamplingStrategy() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + injectEncoding: .single + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "00000000000004d200000000000004d2-0000000000000929-1-000000000000162e") + } + + func testWritingDroppedTraceContext_withSingleEncoding_andCustomSamplingStrategy() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 0), + injectEncoding: .single + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: .random() + ) ) let headers = writer.traceHeaderFields @@ -43,30 +86,80 @@ class B3HTTPHeadersWriterTests: XCTestCase { } func testItWritesSingleHeaderWithoutOptionalValues() { - let sampler: Sampler = .mockKeepAll() let writer = B3HTTPHeadersWriter( - sampler: sampler, + samplingStrategy: .auto, injectEncoding: .single ) + writer.write( - traceID: .init(idHi: 1_234, idLo: 1_234), - spanID: 2_345 + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: true + ) ) let headers = writer.traceHeaderFields XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "00000000000004d200000000000004d2-0000000000000929-1") } - func testItWritesMultipleHeader() { - let sampler: Sampler = .mockKeepAll() + func testWritingSampledTraceContext_withMultipleEncoding_andAutoSamplingStrategy() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .auto, + injectEncoding: .multiple + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: true + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.traceIDField], "00000000000004d200000000000004d2") + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.spanIDField], "0000000000000929") + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.sampledField], "1") + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.parentSpanIDField], "000000000000162e") + } + + func testWritingDroppedTraceContext_withMultipleEncoding_andAutoSamplingStrategy() { let writer = B3HTTPHeadersWriter( - sampler: sampler, + samplingStrategy: .auto, injectEncoding: .multiple ) + writer.write( - traceID: .init(idHi: 1_234, idLo: 1_234), - spanID: 2_345, - parentSpanID: 5_678 + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: false + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertNil(headers[B3HTTPHeaders.Multiple.traceIDField]) + XCTAssertNil(headers[B3HTTPHeaders.Multiple.spanIDField]) + XCTAssertEqual(headers[B3HTTPHeaders.Multiple.sampledField], "0") + XCTAssertNil(headers[B3HTTPHeaders.Multiple.parentSpanIDField]) + } + + func testWritingSampledTraceContext_withMultipleEncoding_andCustomSamplingStrategy() { + let writer = B3HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + injectEncoding: .multiple + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: .random() + ) ) let headers = writer.traceHeaderFields @@ -76,16 +169,19 @@ class B3HTTPHeadersWriterTests: XCTestCase { XCTAssertEqual(headers[B3HTTPHeaders.Multiple.parentSpanIDField], "000000000000162e") } - func testItWritesMultipleHeaderWithSampling() { - let sampler: Sampler = .mockRejectAll() + func testWritingDroppedTraceContext_withMultipleEncoding_andCustomSamplingStrategy() { let writer = B3HTTPHeadersWriter( - sampler: sampler, + samplingStrategy: .custom(sampleRate: 0), injectEncoding: .multiple ) + writer.write( - traceID: .init(idHi: 1_234, idLo: 1_234), - spanID: 2_345, - parentSpanID: 5_678 + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: .random() + ) ) let headers = writer.traceHeaderFields @@ -96,14 +192,17 @@ class B3HTTPHeadersWriterTests: XCTestCase { } func testItWritesMultipleHeaderWithoutOptionalValues() { - let sampler: Sampler = .mockKeepAll() let writer = B3HTTPHeadersWriter( - sampler: sampler, + samplingStrategy: .auto, injectEncoding: .multiple ) + writer.write( - traceID: .init(idHi: 1_234, idLo: 1_234), - spanID: 2_345 + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: true + ) ) let headers = writer.traceHeaderFields diff --git a/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift index 29d8b79652..880aa4f9d3 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersReaderTests.swift @@ -5,12 +5,13 @@ */ import XCTest +import TestUtilities @testable import DatadogInternal class HTTPHeadersReaderTests: XCTestCase { func testReadingSampledTraceContext() { - let writer = HTTPHeadersWriter(sampleRate: 100) - writer.write(traceID: .mockAny(), spanID: .mockAny(), parentSpanID: .mockAny()) + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) + writer.write(traceContext: .mockRandom()) let reader = HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNotNil(reader.read(), "When sampled, it should return trace context") @@ -18,8 +19,8 @@ class HTTPHeadersReaderTests: XCTestCase { } func testReadingNotSampledTraceContext() { - let writer = HTTPHeadersWriter(sampleRate: 0) - writer.write(traceID: .mockAny(), spanID: .mockAny(), parentSpanID: .mockAny()) + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) + writer.write(traceContext: .mockRandom()) let reader = HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNil(reader.read(), "When not sampled, it should return no trace context") diff --git a/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift new file mode 100644 index 0000000000..ae2cd61fd0 --- /dev/null +++ b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift @@ -0,0 +1,83 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +import TestUtilities +import DatadogInternal + +class HTTPHeadersWriterTests: XCTestCase { + func testWritingSampledTraceContext_withAutoSamplingStrategy() { + let writer = HTTPHeadersWriter(samplingStrategy: .auto) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: true + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[TracingHTTPHeaders.samplingPriorityField], "1") + XCTAssertEqual(headers[TracingHTTPHeaders.traceIDField], "4d2") + XCTAssertEqual(headers[TracingHTTPHeaders.parentSpanIDField], "929") + XCTAssertEqual(headers[TracingHTTPHeaders.tagsField], "_dd.p.tid=4d2") + } + + func testWritingDroppedTraceContext_withAutoSamplingStrategy() { + let writer = HTTPHeadersWriter(samplingStrategy: .auto) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: false + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[TracingHTTPHeaders.samplingPriorityField], "0") + XCTAssertNil(headers[TracingHTTPHeaders.traceIDField]) + XCTAssertNil(headers[TracingHTTPHeaders.parentSpanIDField]) + XCTAssertNil(headers[TracingHTTPHeaders.tagsField]) + } + + func testWritingSampledTraceContext_withCustomSamplingStrategy() { + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[TracingHTTPHeaders.samplingPriorityField], "1") + XCTAssertEqual(headers[TracingHTTPHeaders.traceIDField], "4d2") + XCTAssertEqual(headers[TracingHTTPHeaders.parentSpanIDField], "929") + XCTAssertEqual(headers[TracingHTTPHeaders.tagsField], "_dd.p.tid=4d2") + } + + func testWritingDroppedTraceContext_withCustomSamplingStrategy() { + let writer = HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[TracingHTTPHeaders.samplingPriorityField], "0") + XCTAssertNil(headers[TracingHTTPHeaders.traceIDField]) + XCTAssertNil(headers[TracingHTTPHeaders.parentSpanIDField]) + XCTAssertNil(headers[TracingHTTPHeaders.tagsField]) + } +} diff --git a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift index 877b477a43..6778803612 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersReaderTests.swift @@ -27,8 +27,8 @@ class W3CHTTPHeadersReaderTests: XCTestCase { } func testReadingSampledTraceContext() { - let writer = W3CHTTPHeadersWriter(sampleRate: 100) - writer.write(traceID: .mockAny(), spanID: .mockAny(), parentSpanID: .mockAny()) + let writer = W3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) + writer.write(traceContext: .mockRandom()) let reader = W3CHTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNotNil(reader.read(), "When sampled, it should return trace context") @@ -36,8 +36,8 @@ class W3CHTTPHeadersReaderTests: XCTestCase { } func testReadingNotSampledTraceContext() { - let writer = W3CHTTPHeadersWriter(sampleRate: 0) - writer.write(traceID: .mockAny(), spanID: .mockAny(), parentSpanID: .mockAny()) + let writer = W3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) + writer.write(traceContext: .mockRandom()) let reader = W3CHTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) XCTAssertNil(reader.read(), "When not sampled, it should return no trace context") diff --git a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift index bcb45258f6..c81a352511 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift @@ -5,35 +5,92 @@ */ import XCTest -@testable import DatadogInternal +import TestUtilities +import DatadogInternal class W3CHTTPHeadersWriterTests: XCTestCase { - func testW3CHTTPHeadersWriterWritesSingleHeader() { - let sampler: Sampler = .mockKeepAll() - let w3cHTTPHeadersWriter = W3CHTTPHeadersWriter( - sampler: sampler, + func testWritingSampledTraceContext_withAutoSamplingStrategy() { + let writer = W3CHTTPHeadersWriter( + samplingStrategy: .auto, tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM ] ) - w3cHTTPHeadersWriter.write(traceID: .init(idHi: 1_234, idLo: 1_234), spanID: 2_345) - let headers = w3cHTTPHeadersWriter.traceHeaderFields + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: true + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[W3CHTTPHeaders.traceparent], "00-00000000000004d200000000000004d2-0000000000000929-01") + XCTAssertEqual(headers[W3CHTTPHeaders.tracestate], "dd=o:rum;p:0000000000000929;s:1") + } + + func testWritingDroppedTraceContext_withAutoSamplingStrategy() { + let writer = W3CHTTPHeadersWriter( + samplingStrategy: .auto, + tracestate: [ + W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM + ] + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: false + ) + ) + + let headers = writer.traceHeaderFields + XCTAssertEqual(headers[W3CHTTPHeaders.traceparent], "00-00000000000004d200000000000004d2-0000000000000929-00") + XCTAssertEqual(headers[W3CHTTPHeaders.tracestate], "dd=o:rum;p:0000000000000929;s:0") + } + + func testWritingSampledTraceContext_withCustomSamplingStrategy() { + let writer = W3CHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 100), + tracestate: [ + W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM + ] + ) + + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields XCTAssertEqual(headers[W3CHTTPHeaders.traceparent], "00-00000000000004d200000000000004d2-0000000000000929-01") XCTAssertEqual(headers[W3CHTTPHeaders.tracestate], "dd=o:rum;p:0000000000000929;s:1") } - func testW3CHTTPHeadersWriterWritesSingleHeaderWithSampling() { - let sampler: Sampler = .mockRejectAll() - let w3cHTTPHeadersWriter = W3CHTTPHeadersWriter( - sampler: sampler, + func testWritingDroppedTraceContext_withCustomSamplingStrategy() { + let writer = W3CHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: 0), tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM ] ) - w3cHTTPHeadersWriter.write(traceID: .init(idHi: 1_234, idLo: 1_234), spanID: 2_345, parentSpanID: 5_678) - let headers = w3cHTTPHeadersWriter.traceHeaderFields + writer.write( + traceContext: .mockWith( + traceID: .init(idHi: 1_234, idLo: 1_234), + spanID: 2_345, + parentSpanID: 5_678, + isKept: .random() + ) + ) + + let headers = writer.traceHeaderFields XCTAssertEqual(headers[W3CHTTPHeaders.traceparent], "00-00000000000004d200000000000004d2-0000000000000929-00") XCTAssertEqual(headers[W3CHTTPHeaders.tracestate], "dd=o:rum;p:0000000000000929;s:0") } diff --git a/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift index 2bb596ecf3..3357220ded 100644 --- a/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift +++ b/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift @@ -51,7 +51,7 @@ public class DDB3HTTPHeadersWriter: NSObject { injectEncoding: DDInjectEncoding = .single ) { swiftB3HTTPHeadersWriter = B3HTTPHeadersWriter( - sampleRate: sampleRate, + samplingStrategy: .custom(sampleRate: sampleRate), injectEncoding: .init(injectEncoding) ) } diff --git a/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift index 46e4b00ad0..cc6ff050ff 100644 --- a/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift +++ b/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift @@ -23,6 +23,8 @@ public class DDHTTPHeadersWriter: NSObject { @objc public init(sampleRate: Float = 20) { - swiftHTTPHeadersWriter = HTTPHeadersWriter(sampleRate: sampleRate) + swiftHTTPHeadersWriter = HTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: sampleRate) + ) } } diff --git a/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift index f7527cea7f..c27f91d46d 100644 --- a/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift +++ b/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift @@ -23,6 +23,9 @@ public class DDW3CHTTPHeadersWriter: NSObject { @objc public init(sampleRate: Float = 20) { - swiftW3CHTTPHeadersWriter = W3CHTTPHeadersWriter(sampleRate: sampleRate, tracestate: [:]) + swiftW3CHTTPHeadersWriter = W3CHTTPHeadersWriter( + samplingStrategy: .custom(sampleRate: sampleRate), + tracestate: [:] + ) } } diff --git a/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift b/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift index 162fbae7db..02e6812b63 100644 --- a/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift +++ b/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift @@ -147,13 +147,12 @@ extension DistributedTracing { func modify(request: URLRequest, headerTypes: Set) -> (URLRequest, TraceContext?) { let traceID = traceIDGenerator.generate() let spanID = spanIDGenerator.generate() - let deterministicSampler = DeterministicSampler(sampler: sampler) let injectedSpanContext = TraceContext( traceID: traceID, spanID: spanID, parentSpanID: nil, - sampleRate: deterministicSampler.samplingRate, - isKept: deterministicSampler.sample() + sampleRate: sampler.samplingRate, + isKept: sampler.sample() ) var request = request @@ -162,33 +161,29 @@ extension DistributedTracing { let writer: TracePropagationHeadersWriter switch $0 { case .datadog: - writer = HTTPHeadersWriter(sampler: deterministicSampler) + writer = HTTPHeadersWriter(samplingStrategy: .auto) // To make sure the generated traces from RUM don’t affect APM Index Spans counts. request.setValue("rum", forHTTPHeaderField: TracingHTTPHeaders.originField) case .b3: writer = B3HTTPHeadersWriter( - sampler: deterministicSampler, + samplingStrategy: .auto, injectEncoding: .single ) case .b3multi: writer = B3HTTPHeadersWriter( - sampler: deterministicSampler, + samplingStrategy: .auto, injectEncoding: .multiple ) case .tracecontext: writer = W3CHTTPHeadersWriter( - sampler: deterministicSampler, + samplingStrategy: .auto, tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM ] ) } - writer.write( - traceID: injectedSpanContext.traceID, - spanID: injectedSpanContext.spanID, - parentSpanID: injectedSpanContext.parentSpanID - ) + writer.write(traceContext: injectedSpanContext) writer.traceHeaderFields.forEach { field, value in // do not overwrite existing header diff --git a/DatadogTrace/Sources/DDFormat.swift b/DatadogTrace/Sources/DDFormat.swift index a0e55f4ed7..24c96b87a4 100644 --- a/DatadogTrace/Sources/DDFormat.swift +++ b/DatadogTrace/Sources/DDFormat.swift @@ -20,11 +20,14 @@ extension TracePropagationHeadersWriter where Self: OTFormatWriter { guard let spanContext = spanContext.dd else { return } - write( - traceID: spanContext.traceID, - spanID: spanContext.spanID, - parentSpanID: spanContext.parentSpanID + traceContext: TraceContext( + traceID: spanContext.traceID, + spanID: spanContext.spanID, + parentSpanID: spanContext.parentSpanID, + sampleRate: spanContext.sampleRate, + isKept: spanContext.isKept + ) ) } } diff --git a/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift b/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift index 43f1a4c793..e78dfa6bea 100644 --- a/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift +++ b/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift @@ -37,7 +37,10 @@ internal struct TracingURLSessionHandler: DatadogURLSessionHandler { // Use the current active span as parent if the propagation // headers support it. let parentSpanContext = tracer.activeSpan?.context as? DDSpanContext - let spanContext = tracer.createSpanContext(parentSpanContext: parentSpanContext, using: distributedTraceSampler) + let spanContext = tracer.createSpanContext( + parentSpanContext: parentSpanContext, + using: distributedTraceSampler + ) let injectedSpanContext = TraceContext( traceID: spanContext.traceID, spanID: spanContext.spanID, @@ -46,33 +49,31 @@ internal struct TracingURLSessionHandler: DatadogURLSessionHandler { isKept: spanContext.isKept ) - let sampler = DeterministicSampler(shouldSample: spanContext.isKept, samplingRate: spanContext.sampleRate) var request = request var hasSetAnyHeader = false headerTypes.forEach { let writer: TracePropagationHeadersWriter switch $0 { case .datadog: - writer = HTTPHeadersWriter(sampler: sampler) + writer = HTTPHeadersWriter(samplingStrategy: .auto) case .b3: writer = B3HTTPHeadersWriter( - sampler: sampler, + samplingStrategy: .auto, injectEncoding: .single ) case .b3multi: writer = B3HTTPHeadersWriter( - sampler: sampler, + samplingStrategy: .auto, injectEncoding: .multiple ) case .tracecontext: - writer = W3CHTTPHeadersWriter(sampler: sampler, tracestate: [:]) + writer = W3CHTTPHeadersWriter( + samplingStrategy: .auto, + tracestate: [:] + ) } - writer.write( - traceID: injectedSpanContext.traceID, - spanID: injectedSpanContext.spanID, - parentSpanID: injectedSpanContext.parentSpanID - ) + writer.write(traceContext: injectedSpanContext) writer.traceHeaderFields.forEach { field, value in // do not overwrite existing header diff --git a/DatadogTrace/Sources/TraceConfiguration.swift b/DatadogTrace/Sources/TraceConfiguration.swift index eb719200de..0de05d1825 100644 --- a/DatadogTrace/Sources/TraceConfiguration.swift +++ b/DatadogTrace/Sources/TraceConfiguration.swift @@ -17,6 +17,7 @@ import DatadogInternal @_exported import class DatadogInternal.HTTPHeadersWriter @_exported import class DatadogInternal.B3HTTPHeadersWriter @_exported import class DatadogInternal.W3CHTTPHeadersWriter +@_exported import enum DatadogInternal.TraceSamplingStrategy // swiftlint:enable duplicate_imports extension Trace { diff --git a/DatadogTrace/Tests/DDNoopTracerTests.swift b/DatadogTrace/Tests/DDNoopTracerTests.swift index 0b24fa9055..260205e60c 100644 --- a/DatadogTrace/Tests/DDNoopTracerTests.swift +++ b/DatadogTrace/Tests/DDNoopTracerTests.swift @@ -20,7 +20,7 @@ class DDNoopTracerTests: XCTestCase { // When let context = DDSpanContext.mockAny() - noop.inject(spanContext: context, writer: HTTPHeadersWriter()) + noop.inject(spanContext: context, writer: HTTPHeadersWriter(samplingStrategy: .auto)) _ = noop.extract(reader: HTTPHeadersReader(httpHeaderFields: [:])) let root = noop.startRootSpan(operationName: "root operation").setActive() let child = noop.startSpan(operationName: "child operation") diff --git a/DatadogTrace/Tests/DatadogTracer+InjectAndExtract.swift b/DatadogTrace/Tests/DatadogTracer+InjectAndExtract.swift new file mode 100644 index 0000000000..fd644a6466 --- /dev/null +++ b/DatadogTrace/Tests/DatadogTracer+InjectAndExtract.swift @@ -0,0 +1,99 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import XCTest +import TestUtilities +import DatadogInternal +@testable import DatadogTrace + +private class MockWriter: OTFormatWriter, TracePropagationHeadersWriter { + var traceHeaderFields: [String: String] = [:] + var injectedTraceContext: TraceContext? + func write(traceContext: TraceContext) { injectedTraceContext = traceContext } +} + +private class MockReader: OTFormatReader, TracePropagationHeadersReader { + var extractedIDs: (traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?)? = nil + var extractedIsKept: Bool? = nil + + func read() -> (traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?)? { extractedIDs } + var sampled: Bool? { extractedIsKept } +} + +class DatadogTracer_InjectAndExtract: XCTestCase { + private func createTracer(sampleRate: Float) -> DatadogTracer { + return DatadogTracer( + featureScope: NOPFeatureScope(), + localTraceSampler: Sampler(samplingRate: sampleRate), + tags: [:], + traceIDGenerator: DefaultTraceIDGenerator(), + spanIDGenerator: DefaultSpanIDGenerator(), + dateProvider: DateProviderMock(), + loggingIntegration: .mockAny(), + spanEventBuilder: .mockAny() + ) + } + + func testInjectingSpanContextIntoWriter() { + // Given + let spanContext = DDSpanContext( + traceID: .mockRandom(), + spanID: .mockRandom(), + parentSpanID: .mockRandom(), + baggageItems: .mockAny(), + sampleRate: .mockRandom(min: 0, max: 100), + isKept: .random() + ) + + let tracer = createTracer(sampleRate: 42) + let writer = MockWriter() + XCTAssertNil(writer.injectedTraceContext) + + // When + tracer.inject(spanContext: spanContext, writer: writer) + + // Then + let expectedTraceContext = TraceContext( + traceID: spanContext.traceID, + spanID: spanContext.spanID, + parentSpanID: spanContext.parentSpanID, + sampleRate: spanContext.sampleRate, + isKept: spanContext.isKept + ) + XCTAssertEqual(writer.injectedTraceContext, expectedTraceContext) + } + + func testExtractnigSpanContextFromReader() throws { + // Given + let tracer = createTracer(sampleRate: 42) + let reader = MockReader() + let ids: (traceID: TraceID, spanID: SpanID, parentSpanID: SpanID?) = (.mockRandom(), .mockRandom(), .mockRandom()) + let isKept: Bool = .mockRandom() + reader.extractedIDs = ids + reader.extractedIsKept = isKept + + // When + let spanContext = try XCTUnwrap(tracer.extract(reader: reader) as? DDSpanContext) + + // Then + XCTAssertEqual(spanContext.traceID, ids.traceID) + XCTAssertEqual(spanContext.spanID, ids.spanID) + XCTAssertEqual(spanContext.parentSpanID, ids.parentSpanID) + XCTAssertEqual(spanContext.sampleRate, 42) + XCTAssertEqual(spanContext.isKept, isKept) + } + + func testExtractsEmptySpanContextFromReader() throws { + // Given + let tracer = createTracer(sampleRate: 42) + let reader = MockReader() + reader.extractedIDs = nil + reader.extractedIsKept = nil + + // When + XCTAssertNil(tracer.extract(reader: reader)) + } +} diff --git a/TestUtilities/Mocks/NetworkInstrumentationMocks.swift b/TestUtilities/Mocks/NetworkInstrumentationMocks.swift index 97777b242f..23b9c61290 100644 --- a/TestUtilities/Mocks/NetworkInstrumentationMocks.swift +++ b/TestUtilities/Mocks/NetworkInstrumentationMocks.swift @@ -7,22 +7,30 @@ import Foundation import DatadogInternal -extension SpanID { +extension SpanID: AnyMockable, RandomMockable { public static func mockAny() -> SpanID { return SpanID(rawValue: .mockAny()) } + public static func mockRandom() -> SpanID { + return SpanID(rawValue: .mockRandom()) + } + public static func mock(_ rawValue: UInt64) -> SpanID { return SpanID(rawValue: rawValue) } } -extension TraceID { +extension TraceID: AnyMockable, RandomMockable { public static func mockAny() -> TraceID { return TraceID(rawValue: (.mockAny(), .mockAny())) } + public static func mockRandom() -> TraceID { + return TraceID(idHi: .mockRandom(), idLo: .mockRandom()) + } + public static func mock(_ rawValue: (UInt64, UInt64)) -> TraceID { return TraceID(rawValue: rawValue) } @@ -36,6 +44,38 @@ extension TraceID { } } +extension TraceContext: AnyMockable, RandomMockable { + public static func mockAny() -> TraceContext { + return .mockWith() + } + + public static func mockRandom() -> TraceContext { + return .mockWith( + traceID: .mockRandom(), + spanID: .mockRandom(), + parentSpanID: .mockRandom(), + sampleRate: .mockRandom(min: 0, max: 100), + isKept: .random() + ) + } + + public static func mockWith( + traceID: TraceID = .mockAny(), + spanID: SpanID = .mockAny(), + parentSpanID: SpanID? = nil, + sampleRate: Float = .mockAny(), + isKept: Bool = .mockAny() + ) -> TraceContext { + return TraceContext( + traceID: traceID, + spanID: spanID, + parentSpanID: parentSpanID, + sampleRate: sampleRate, + isKept: isKept + ) + } +} + public class RelativeTracingUUIDGenerator: TraceIDGenerator { private(set) var uuid: TraceID internal let count: UInt64 From 713dd0e038aa1b30f961b50b72b776f0adbb0afa Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Thu, 25 Apr 2024 12:24:04 +0200 Subject: [PATCH 4/7] RUM-3470 CR feedback - rename new sampling strategy to `.headBased` --- .../Trace/HeadBasedSamplingTests.swift | 8 ++++---- DatadogCore/Tests/Datadog/TracerTests.swift | 8 ++++---- .../TracePropagationHeadersWriter.swift | 4 ++-- .../B3HTTPHeadersWriterTests.swift | 12 ++++++------ .../HTTPHeadersWriterTests.swift | 4 ++-- .../W3CHTTPHeadersWriterTests.swift | 4 ++-- .../Resources/URLSessionRUMResourcesHandler.swift | 8 ++++---- .../Integrations/TracingURLSessionHandler.swift | 8 ++++---- DatadogTrace/Tests/DDNoopTracerTests.swift | 2 +- 9 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift b/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift index 418c934e56..d05f226309 100644 --- a/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift +++ b/Datadog/IntegrationUnitTests/Trace/HeadBasedSamplingTests.swift @@ -285,7 +285,7 @@ class HeadBasedSamplingTests: XCTestCase { // When var request: URLRequest = .mockAny() - let writer = HTTPHeadersWriter(samplingStrategy: .auto) + let writer = HTTPHeadersWriter(samplingStrategy: .headBased) let span = Tracer.shared(in: core).startSpan(operationName: "network.span") Tracer.shared(in: core).inject(spanContext: span.context, writer: writer) writer.traceHeaderFields.forEach { field, value in request.setValue(value, forHTTPHeaderField: field) } @@ -324,7 +324,7 @@ class HeadBasedSamplingTests: XCTestCase { // When var request: URLRequest = .mockAny() - let writer = HTTPHeadersWriter(samplingStrategy: .auto) + let writer = HTTPHeadersWriter(samplingStrategy: .headBased) let span = Tracer.shared(in: core).startSpan(operationName: "network.span") Tracer.shared(in: core).inject(spanContext: span.context, writer: writer) writer.traceHeaderFields.forEach { field, value in request.setValue(value, forHTTPHeaderField: field) } @@ -364,7 +364,7 @@ class HeadBasedSamplingTests: XCTestCase { // When var request: URLRequest = .mockAny() - let writer = HTTPHeadersWriter(samplingStrategy: .auto) + let writer = HTTPHeadersWriter(samplingStrategy: .headBased) let parentSpan = Tracer.shared(in: core).startSpan(operationName: "active.span").setActive() let span = Tracer.shared(in: core).startSpan(operationName: "network.span") Tracer.shared(in: core).inject(spanContext: span.context, writer: writer) @@ -413,7 +413,7 @@ class HeadBasedSamplingTests: XCTestCase { // When var request: URLRequest = .mockAny() - let writer = HTTPHeadersWriter(samplingStrategy: .auto) + let writer = HTTPHeadersWriter(samplingStrategy: .headBased) let parentSpan = Tracer.shared(in: core).startSpan(operationName: "active.span").setActive() let span = Tracer.shared(in: core).startSpan(operationName: "network.span") Tracer.shared(in: core).inject(spanContext: span.context, writer: writer) diff --git a/DatadogCore/Tests/Datadog/TracerTests.swift b/DatadogCore/Tests/Datadog/TracerTests.swift index 848ab2d71a..c5a1841143 100644 --- a/DatadogCore/Tests/Datadog/TracerTests.swift +++ b/DatadogCore/Tests/Datadog/TracerTests.swift @@ -732,7 +732,7 @@ class TracerTests: XCTestCase { let injectedContext = tracer.startSpan(operationName: .mockAny()).context // When - let writer = HTTPHeadersWriter(samplingStrategy: .auto) + let writer = HTTPHeadersWriter(samplingStrategy: .headBased) tracer.inject(spanContext: injectedContext, writer: writer) let reader = HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) @@ -753,7 +753,7 @@ class TracerTests: XCTestCase { let injectedContext = tracer.startSpan(operationName: .mockAny()).context // When - let writer = B3HTTPHeadersWriter(samplingStrategy: .auto, injectEncoding: .single) + let writer = B3HTTPHeadersWriter(samplingStrategy: .headBased, injectEncoding: .single) tracer.inject(spanContext: injectedContext, writer: writer) let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) @@ -774,7 +774,7 @@ class TracerTests: XCTestCase { let injectedContext = tracer.startSpan(operationName: .mockAny()).context // When - let writer = B3HTTPHeadersWriter(samplingStrategy: .auto, injectEncoding: .multiple) + let writer = B3HTTPHeadersWriter(samplingStrategy: .headBased, injectEncoding: .multiple) tracer.inject(spanContext: injectedContext, writer: writer) let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) @@ -795,7 +795,7 @@ class TracerTests: XCTestCase { let injectedContext = tracer.startSpan(operationName: .mockAny()).context // When - let writer = W3CHTTPHeadersWriter(samplingStrategy: .auto) + let writer = W3CHTTPHeadersWriter(samplingStrategy: .headBased) tracer.inject(spanContext: injectedContext, writer: writer) let reader = W3CHTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields) diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift index ff05a2cf7d..f6fca3509c 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift @@ -13,7 +13,7 @@ public enum TraceSamplingStrategy { /// Use this option to leverage head-based sampling, where the decision to keep or drop the trace /// is determined from the first span of the trace, the head, when the trace is created. With `.auto` /// strategy, this decision is propagated through the request context to downstream services. - case auto + case headBased /// Trace propagation headers will be sampled independently from sampling decision in propagated span. /// /// Use this option to apply the provided `sampleRate` for determining the decision to keep or drop the trace @@ -22,7 +22,7 @@ public enum TraceSamplingStrategy { internal func sampler(for traceContext: TraceContext) -> Sampling { switch self { - case .auto: + case .headBased: return DeterministicSampler(shouldSample: traceContext.isKept, samplingRate: traceContext.sampleRate) case .custom(let sampleRate): return Sampler(samplingRate: sampleRate) diff --git a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift index c15995db92..7491012888 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/B3HTTPHeadersWriterTests.swift @@ -11,7 +11,7 @@ import DatadogInternal class B3HTTPHeadersWriterTests: XCTestCase { func testWritingSampledTraceContext_withSingleEncoding_andAutoSamplingStrategy() { let writer = B3HTTPHeadersWriter( - samplingStrategy: .auto, + samplingStrategy: .headBased, injectEncoding: .single ) @@ -30,7 +30,7 @@ class B3HTTPHeadersWriterTests: XCTestCase { func testWritingDroppedTraceContext_withSingleEncoding_andAutoSamplingStrategy() { let writer = B3HTTPHeadersWriter( - samplingStrategy: .auto, + samplingStrategy: .headBased, injectEncoding: .single ) @@ -87,7 +87,7 @@ class B3HTTPHeadersWriterTests: XCTestCase { func testItWritesSingleHeaderWithoutOptionalValues() { let writer = B3HTTPHeadersWriter( - samplingStrategy: .auto, + samplingStrategy: .headBased, injectEncoding: .single ) @@ -105,7 +105,7 @@ class B3HTTPHeadersWriterTests: XCTestCase { func testWritingSampledTraceContext_withMultipleEncoding_andAutoSamplingStrategy() { let writer = B3HTTPHeadersWriter( - samplingStrategy: .auto, + samplingStrategy: .headBased, injectEncoding: .multiple ) @@ -127,7 +127,7 @@ class B3HTTPHeadersWriterTests: XCTestCase { func testWritingDroppedTraceContext_withMultipleEncoding_andAutoSamplingStrategy() { let writer = B3HTTPHeadersWriter( - samplingStrategy: .auto, + samplingStrategy: .headBased, injectEncoding: .multiple ) @@ -193,7 +193,7 @@ class B3HTTPHeadersWriterTests: XCTestCase { func testItWritesMultipleHeaderWithoutOptionalValues() { let writer = B3HTTPHeadersWriter( - samplingStrategy: .auto, + samplingStrategy: .headBased, injectEncoding: .multiple ) diff --git a/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift index ae2cd61fd0..d7fd7d46e6 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/HTTPHeadersWriterTests.swift @@ -10,7 +10,7 @@ import DatadogInternal class HTTPHeadersWriterTests: XCTestCase { func testWritingSampledTraceContext_withAutoSamplingStrategy() { - let writer = HTTPHeadersWriter(samplingStrategy: .auto) + let writer = HTTPHeadersWriter(samplingStrategy: .headBased) writer.write( traceContext: .mockWith( @@ -28,7 +28,7 @@ class HTTPHeadersWriterTests: XCTestCase { } func testWritingDroppedTraceContext_withAutoSamplingStrategy() { - let writer = HTTPHeadersWriter(samplingStrategy: .auto) + let writer = HTTPHeadersWriter(samplingStrategy: .headBased) writer.write( traceContext: .mockWith( diff --git a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift index c81a352511..931714ca3e 100644 --- a/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift +++ b/DatadogInternal/Tests/NetworkInstrumentation/W3CHTTPHeadersWriterTests.swift @@ -11,7 +11,7 @@ import DatadogInternal class W3CHTTPHeadersWriterTests: XCTestCase { func testWritingSampledTraceContext_withAutoSamplingStrategy() { let writer = W3CHTTPHeadersWriter( - samplingStrategy: .auto, + samplingStrategy: .headBased, tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM ] @@ -32,7 +32,7 @@ class W3CHTTPHeadersWriterTests: XCTestCase { func testWritingDroppedTraceContext_withAutoSamplingStrategy() { let writer = W3CHTTPHeadersWriter( - samplingStrategy: .auto, + samplingStrategy: .headBased, tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM ] diff --git a/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift b/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift index 02e6812b63..ab39b9c1cf 100644 --- a/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift +++ b/DatadogRUM/Sources/Instrumentation/Resources/URLSessionRUMResourcesHandler.swift @@ -161,22 +161,22 @@ extension DistributedTracing { let writer: TracePropagationHeadersWriter switch $0 { case .datadog: - writer = HTTPHeadersWriter(samplingStrategy: .auto) + writer = HTTPHeadersWriter(samplingStrategy: .headBased) // To make sure the generated traces from RUM don’t affect APM Index Spans counts. request.setValue("rum", forHTTPHeaderField: TracingHTTPHeaders.originField) case .b3: writer = B3HTTPHeadersWriter( - samplingStrategy: .auto, + samplingStrategy: .headBased, injectEncoding: .single ) case .b3multi: writer = B3HTTPHeadersWriter( - samplingStrategy: .auto, + samplingStrategy: .headBased, injectEncoding: .multiple ) case .tracecontext: writer = W3CHTTPHeadersWriter( - samplingStrategy: .auto, + samplingStrategy: .headBased, tracestate: [ W3CHTTPHeaders.Constants.origin: W3CHTTPHeaders.Constants.originRUM ] diff --git a/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift b/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift index e78dfa6bea..9dec872ec5 100644 --- a/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift +++ b/DatadogTrace/Sources/Integrations/TracingURLSessionHandler.swift @@ -55,20 +55,20 @@ internal struct TracingURLSessionHandler: DatadogURLSessionHandler { let writer: TracePropagationHeadersWriter switch $0 { case .datadog: - writer = HTTPHeadersWriter(samplingStrategy: .auto) + writer = HTTPHeadersWriter(samplingStrategy: .headBased) case .b3: writer = B3HTTPHeadersWriter( - samplingStrategy: .auto, + samplingStrategy: .headBased, injectEncoding: .single ) case .b3multi: writer = B3HTTPHeadersWriter( - samplingStrategy: .auto, + samplingStrategy: .headBased, injectEncoding: .multiple ) case .tracecontext: writer = W3CHTTPHeadersWriter( - samplingStrategy: .auto, + samplingStrategy: .headBased, tracestate: [:] ) } diff --git a/DatadogTrace/Tests/DDNoopTracerTests.swift b/DatadogTrace/Tests/DDNoopTracerTests.swift index 260205e60c..ecea81ad79 100644 --- a/DatadogTrace/Tests/DDNoopTracerTests.swift +++ b/DatadogTrace/Tests/DDNoopTracerTests.swift @@ -20,7 +20,7 @@ class DDNoopTracerTests: XCTestCase { // When let context = DDSpanContext.mockAny() - noop.inject(spanContext: context, writer: HTTPHeadersWriter(samplingStrategy: .auto)) + noop.inject(spanContext: context, writer: HTTPHeadersWriter(samplingStrategy: .headBased)) _ = noop.extract(reader: HTTPHeadersReader(httpHeaderFields: [:])) let root = noop.startRootSpan(operationName: "root operation").setActive() let child = noop.startSpan(operationName: "child operation") From a662ad1198a5bc3c38317008f63f672886eaae80 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Thu, 25 Apr 2024 12:55:22 +0200 Subject: [PATCH 5/7] RUM-3470 CR feedback - add Objective-C API for `TraceSamplingStrategy` --- Datadog/Datadog.xcodeproj/project.pbxproj | 6 +++ .../DDB3HTTPHeadersWriter+apiTests.m | 5 ++- .../DDHTTPHeadersWriter+apiTests.m | 3 +- .../DDW3CHTTPHeadersWriter+apiTests.m | 4 +- .../TracePropagationHeadersWriter.swift | 2 +- .../B3HTTPHeadersWriter+objc.swift | 14 ++++++- .../Propagation/HTTPHeadersWriter+objc.swift | 10 ++++- .../TraceSamplingStrategy+objc.swift | 37 +++++++++++++++++++ .../W3CHTTPHeadersWriter+objc.swift | 13 ++++++- 9 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 DatadogObjc/Sources/Tracing/Propagation/TraceSamplingStrategy+objc.swift diff --git a/Datadog/Datadog.xcodeproj/project.pbxproj b/Datadog/Datadog.xcodeproj/project.pbxproj index 131f5f9e00..50b613121a 100644 --- a/Datadog/Datadog.xcodeproj/project.pbxproj +++ b/Datadog/Datadog.xcodeproj/project.pbxproj @@ -356,6 +356,8 @@ 6167E72A2B84C11900C3CA2D /* DDCrashReportMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E7282B84C11900C3CA2D /* DDCrashReportMocks.swift */; }; 6167E72C2B84C72B00C3CA2D /* UIKitHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E72B2B84C72B00C3CA2D /* UIKitHelpers.swift */; }; 6167E72D2B84C72B00C3CA2D /* UIKitHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6167E72B2B84C72B00C3CA2D /* UIKitHelpers.swift */; }; + 616AAA6D2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616AAA6C2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift */; }; + 616AAA6E2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616AAA6C2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift */; }; 616B668E259CC28E00968EE8 /* DDRUMMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616B668D259CC28E00968EE8 /* DDRUMMonitorTests.swift */; }; 616F8C272BB1CD990061EA53 /* ProcessIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616F8C262BB1CD990061EA53 /* ProcessIdentifier.swift */; }; 616F8C282BB1CD990061EA53 /* ProcessIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 616F8C262BB1CD990061EA53 /* ProcessIdentifier.swift */; }; @@ -2294,6 +2296,7 @@ 6167E7222B837FF100C3CA2D /* BinaryImageMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryImageMocks.swift; sourceTree = ""; }; 6167E7282B84C11900C3CA2D /* DDCrashReportMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDCrashReportMocks.swift; sourceTree = ""; }; 6167E72B2B84C72B00C3CA2D /* UIKitHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitHelpers.swift; sourceTree = ""; }; + 616AAA6C2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TraceSamplingStrategy+objc.swift"; sourceTree = ""; }; 616B668D259CC28E00968EE8 /* DDRUMMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDRUMMonitorTests.swift; sourceTree = ""; }; 616C0A9D28573DFF00C13264 /* RUMOperatingSystemInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMOperatingSystemInfo.swift; sourceTree = ""; }; 616C0AA028573F6300C13264 /* RUMOperatingSystemInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMOperatingSystemInfoTests.swift; sourceTree = ""; }; @@ -4258,6 +4261,7 @@ 6132BF4A24A49C7200D7BD17 /* Propagation */ = { isa = PBXGroup; children = ( + 616AAA6C2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift */, 6132BF4B24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift */, A79B0F5E292BA435008742B3 /* B3HTTPHeadersWriter+objc.swift */, A728ADAA2934EA2100397996 /* W3CHTTPHeadersWriter+objc.swift */, @@ -7864,6 +7868,7 @@ 6132BF4C24A49C8F00D7BD17 /* HTTPHeadersWriter+objc.swift in Sources */, 6132BF4724A498D800D7BD17 /* DDSpan+objc.swift in Sources */, 615A4A8B24A3568900233986 /* OTSpan+objc.swift in Sources */, + 616AAA6D2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift in Sources */, 611720D52524D9FB00634D9E /* DDURLSessionDelegate+objc.swift in Sources */, 9E55407C25812D1C00F6E3AD /* RUM+objc.swift in Sources */, D2A434AA2A8E40A20028E329 /* SessionReplay+objc.swift in Sources */, @@ -9029,6 +9034,7 @@ 3CCCA5C52ABAF0F80029D7BD /* DDURLSessionInstrumentation+objc.swift in Sources */, D2CB6FA027C5217A00A62B57 /* Trace+objc.swift in Sources */, D2CB6FA127C5217A00A62B57 /* HTTPHeadersWriter+objc.swift in Sources */, + 616AAA6E2BDA674C00AB9DAD /* TraceSamplingStrategy+objc.swift in Sources */, D2CB6FA227C5217A00A62B57 /* DDSpan+objc.swift in Sources */, D2CB6FA327C5217A00A62B57 /* OTSpan+objc.swift in Sources */, D2CB6FA427C5217A00A62B57 /* DDURLSessionDelegate+objc.swift in Sources */, diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDB3HTTPHeadersWriter+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDB3HTTPHeadersWriter+apiTests.m index 57beff0c08..e315e3ec02 100644 --- a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDB3HTTPHeadersWriter+apiTests.m +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDB3HTTPHeadersWriter+apiTests.m @@ -19,7 +19,10 @@ @implementation DDB3HTTPHeadersWriter_apiTests #pragma clang diagnostic ignored "-Wunused-value" - (void)testInitWithSamplingRate { - [[DDB3HTTPHeadersWriter alloc] initWithSampleRate:100 injectEncoding:DDInjectEncodingSingle]; + [[DDB3HTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy headBased] + injectEncoding:DDInjectEncodingSingle]; + [[DDB3HTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy customWithSampleRate:50] + injectEncoding:DDInjectEncodingMultiple]; } #pragma clang diagnostic pop diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDHTTPHeadersWriter+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDHTTPHeadersWriter+apiTests.m index cab2afaeb6..7c0f0e9ffc 100644 --- a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDHTTPHeadersWriter+apiTests.m +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDHTTPHeadersWriter+apiTests.m @@ -19,7 +19,8 @@ @implementation DDHTTPHeadersWriter_apiTests #pragma clang diagnostic ignored "-Wunused-value" - (void)testInitWithSamplingRate { - [[DDHTTPHeadersWriter alloc] initWithSampleRate:50]; + [[DDHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy headBased]]; + [[DDHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy customWithSampleRate:50]]; } #pragma clang diagnostic pop diff --git a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDW3CHTTPHeadersWriter+apiTests.m b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDW3CHTTPHeadersWriter+apiTests.m index f75ea8ce27..1c5486d0ae 100644 --- a/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDW3CHTTPHeadersWriter+apiTests.m +++ b/DatadogCore/Tests/DatadogObjc/ObjcAPITests/DDW3CHTTPHeadersWriter+apiTests.m @@ -19,7 +19,9 @@ @implementation DDW3CHTTPHeadersWriter_apiTests #pragma clang diagnostic ignored "-Wunused-value" - (void)testInitWithSamplingRate { - [[DDW3CHTTPHeadersWriter alloc] initWithSampleRate:50]; + [[DDW3CHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy headBased]]; + [[DDW3CHTTPHeadersWriter alloc] initWithSamplingStrategy:[DDTraceSamplingStrategy customWithSampleRate:50]]; + } #pragma clang diagnostic pop diff --git a/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift index f6fca3509c..1429c1a504 100644 --- a/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift +++ b/DatadogInternal/Sources/NetworkInstrumentation/TracePropagationHeadersWriter.swift @@ -11,7 +11,7 @@ public enum TraceSamplingStrategy { /// Trace propagation headers will be sampled same as propagated span. /// /// Use this option to leverage head-based sampling, where the decision to keep or drop the trace - /// is determined from the first span of the trace, the head, when the trace is created. With `.auto` + /// is determined from the first span of the trace, the head, when the trace is created. With `.headBased` /// strategy, this decision is propagated through the request context to downstream services. case headBased /// Trace propagation headers will be sampled independently from sampling decision in propagated span. diff --git a/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift index 3357220ded..b55ab262c9 100644 --- a/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift +++ b/DatadogObjc/Sources/Tracing/Propagation/B3HTTPHeadersWriter+objc.swift @@ -37,7 +37,7 @@ public class DDB3HTTPHeadersWriter: NSObject { } @objc - @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(sampleRate:injectEncoding:)` instead.") + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public convenience init( samplingRate: Float, injectEncoding: DDInjectEncoding = .single @@ -46,6 +46,7 @@ public class DDB3HTTPHeadersWriter: NSObject { } @objc + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public init( sampleRate: Float = 20, injectEncoding: DDInjectEncoding = .single @@ -55,4 +56,15 @@ public class DDB3HTTPHeadersWriter: NSObject { injectEncoding: .init(injectEncoding) ) } + + @objc + public init( + samplingStrategy: DDTraceSamplingStrategy, + injectEncoding: DDInjectEncoding = .single + ) { + swiftB3HTTPHeadersWriter = B3HTTPHeadersWriter( + samplingStrategy: samplingStrategy.swiftType, + injectEncoding: .init(injectEncoding) + ) + } } diff --git a/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift index cc6ff050ff..24d759f25f 100644 --- a/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift +++ b/DatadogObjc/Sources/Tracing/Propagation/HTTPHeadersWriter+objc.swift @@ -16,15 +16,23 @@ public class DDHTTPHeadersWriter: NSObject { } @objc - @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(sampleRate:)` instead.") + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public convenience init(samplingRate: Float) { self.init(sampleRate: samplingRate) } @objc + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public init(sampleRate: Float = 20) { swiftHTTPHeadersWriter = HTTPHeadersWriter( samplingStrategy: .custom(sampleRate: sampleRate) ) } + + @objc + public init(samplingStrategy: DDTraceSamplingStrategy) { + swiftHTTPHeadersWriter = HTTPHeadersWriter( + samplingStrategy: samplingStrategy.swiftType + ) + } } diff --git a/DatadogObjc/Sources/Tracing/Propagation/TraceSamplingStrategy+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/TraceSamplingStrategy+objc.swift new file mode 100644 index 0000000000..fc1c5b081c --- /dev/null +++ b/DatadogObjc/Sources/Tracing/Propagation/TraceSamplingStrategy+objc.swift @@ -0,0 +1,37 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2019-Present Datadog, Inc. + */ + +import Foundation +import DatadogInternal + +/// Available strategies for sampling trace propagation headers. +@objc +public class DDTraceSamplingStrategy: NSObject { + internal let swiftType: DatadogInternal.TraceSamplingStrategy + + /// Trace propagation headers will be sampled same as propagated span. + /// + /// Use this option to leverage head-based sampling, where the decision to keep or drop the trace + /// is determined from the first span of the trace, the head, when the trace is created. With `.headBased` + /// strategy, this decision is propagated through the request context to downstream services. + @objc + public static func headBased() -> DDTraceSamplingStrategy { + return DDTraceSamplingStrategy(swiftType: .headBased) + } + + /// Trace propagation headers will be sampled independently from sampling decision in propagated span. + /// + /// Use this option to apply the provided `sampleRate` for determining the decision to keep or drop the trace + /// in downstream services independently of sampling their parent span. + @objc + public static func custom(sampleRate: Float) -> DDTraceSamplingStrategy { + return DDTraceSamplingStrategy(swiftType: .custom(sampleRate: sampleRate)) + } + + private init(swiftType: DatadogInternal.TraceSamplingStrategy) { + self.swiftType = swiftType + } +} diff --git a/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift b/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift index c27f91d46d..1ef16bc401 100644 --- a/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift +++ b/DatadogObjc/Sources/Tracing/Propagation/W3CHTTPHeadersWriter+objc.swift @@ -16,16 +16,27 @@ public class DDW3CHTTPHeadersWriter: NSObject { } @objc - @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(sampleRate:)` instead.") + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public convenience init(samplingRate: Float) { self.init(sampleRate: samplingRate) } @objc + @available(*, deprecated, message: "This will be removed in future versions of the SDK. Use `init(samplingStrategy: .custom(sampleRate:))` instead.") public init(sampleRate: Float = 20) { swiftW3CHTTPHeadersWriter = W3CHTTPHeadersWriter( samplingStrategy: .custom(sampleRate: sampleRate), tracestate: [:] ) } + + @objc + public init( + samplingStrategy: DDTraceSamplingStrategy + ) { + swiftW3CHTTPHeadersWriter = W3CHTTPHeadersWriter( + samplingStrategy: samplingStrategy.swiftType, + tracestate: [:] + ) + } } From 8ec55a4b73d7d653057719111465023228251894 Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Thu, 25 Apr 2024 14:13:37 +0200 Subject: [PATCH 6/7] RUM-3470 Fix - use replacement API in tests --- .../Tests/DatadogObjc/DDTracerTests.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/DatadogCore/Tests/DatadogObjc/DDTracerTests.swift b/DatadogCore/Tests/DatadogObjc/DDTracerTests.swift index 8ea10f6fc7..ff04ad6da5 100644 --- a/DatadogCore/Tests/DatadogObjc/DDTracerTests.swift +++ b/DatadogCore/Tests/DatadogObjc/DDTracerTests.swift @@ -203,7 +203,7 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDHTTPHeadersWriter(sampleRate: 100) + let objcWriter = DDHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ @@ -222,7 +222,7 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDHTTPHeadersWriter(sampleRate: 0) + let objcWriter = DDHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ @@ -236,7 +236,7 @@ class DDTracerTests: XCTestCase { let objcTracer = DDTracer.shared() let objcSpanContext = DDSpanContextObjc(swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200)) - let objcValidWriter = DDHTTPHeadersWriter(sampleRate: 100) + let objcValidWriter = DDHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) let objcInvalidFormat = "foo" XCTAssertThrowsError( try objcTracer.inject(objcSpanContext, format: objcInvalidFormat, carrier: objcValidWriter) @@ -256,7 +256,7 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDB3HTTPHeadersWriter(sampleRate: 100) + let objcWriter = DDB3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ @@ -272,7 +272,7 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDB3HTTPHeadersWriter(sampleRate: 0) + let objcWriter = DDB3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ @@ -286,7 +286,7 @@ class DDTracerTests: XCTestCase { let objcTracer = DDTracer.shared() let objcSpanContext = DDSpanContextObjc(swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200)) - let objcValidWriter = DDB3HTTPHeadersWriter(sampleRate: 100) + let objcValidWriter = DDB3HTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) let objcInvalidFormat = "foo" XCTAssertThrowsError( try objcTracer.inject(objcSpanContext, format: objcInvalidFormat, carrier: objcValidWriter) @@ -306,7 +306,7 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDW3CHTTPHeadersWriter(sampleRate: 100) + let objcWriter = DDW3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ @@ -323,7 +323,7 @@ class DDTracerTests: XCTestCase { swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200) ) - let objcWriter = DDW3CHTTPHeadersWriter(sampleRate: 0) + let objcWriter = DDW3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 0)) try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter) let expectedHTTPHeaders = [ @@ -338,7 +338,7 @@ class DDTracerTests: XCTestCase { let objcTracer = DDTracer.shared() let objcSpanContext = DDSpanContextObjc(swiftSpanContext: DDSpanContext.mockWith(traceID: .init(idHi: 10, idLo: 100), spanID: 200)) - let objcValidWriter = DDW3CHTTPHeadersWriter(sampleRate: 100) + let objcValidWriter = DDW3CHTTPHeadersWriter(samplingStrategy: .custom(sampleRate: 100)) let objcInvalidFormat = "foo" XCTAssertThrowsError( try objcTracer.inject(objcSpanContext, format: objcInvalidFormat, carrier: objcValidWriter) From 353dbecc33c80a389080014f76b95ba8819bb8ae Mon Sep 17 00:00:00 2001 From: Maciek Grzybowski Date: Fri, 26 Apr 2024 15:08:08 +0200 Subject: [PATCH 7/7] RUM-3470 Update `CHANGELOG.md` --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2f484bb36..fa9fe95a3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +- [FEATURE] `DatadogTrace` now supports head-based sampling. See [#1794][] - [FEATURE] Support WebView recording in Session Replay. See [#1776][] # 2.10.0 / 23-04-2024 @@ -640,6 +641,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO [#1742]: https://github.com/DataDog/dd-sdk-ios/pull/1742 [#1746]: https://github.com/DataDog/dd-sdk-ios/pull/1746 [#1747]: https://github.com/DataDog/dd-sdk-ios/pull/1747 +[#1794]: https://github.com/DataDog/dd-sdk-ios/pull/1794 [#1774]: https://github.com/DataDog/dd-sdk-ios/pull/1774 [#1763]: https://github.com/DataDog/dd-sdk-ios/pull/1763 [#1767]: https://github.com/DataDog/dd-sdk-ios/pull/1767