Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [IMPROVEMENT] Add missing `versionMajor` property to the `DDLogEventOperatingSystem` definition in Objective-C. See [#2463][]
- [IMPROVEMENT] Add `ddtags` to RUM events. See [#2436][]
- [FIX] Fix `LogEvent` device types. See [#2474][]
- [BUGFIX] Fix tracing header injection for sampled out requests. See [#2473][]

# 3.0.0 / 02-09-2025

Expand Down Expand Up @@ -967,6 +968,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO
[#2436]: https://github.com/DataDog/dd-sdk-ios/pull/2436
[#2469]: https://github.com/DataDog/dd-sdk-ios/pull/2469
[#2474]: https://github.com/DataDog/dd-sdk-ios/pull/2474
[#2473]: https://github.com/DataDog/dd-sdk-ios/pull/2473

[@00fa9a]: https://github.com/00FA9A
[@britton-earnin]: https://github.com/Britton-Earnin
Expand Down
2 changes: 1 addition & 1 deletion DatadogCore/Tests/Objc/DDTracerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ class DDTracerTests: XCTestCase {
try objcTracer.inject(objcSpanContext, format: OT.formatTextMap, carrier: objcWriter)

let expectedHTTPHeaders = [
"b3": "0",
"b3": "000000000000000a0000000000000064-00000000000000c8-0-0000000000000000",
]
XCTAssertEqual(objcWriter.traceHeaderFields, expectedHTTPHeaders)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public class B3HTTPHeadersReader: TracePropagationHeadersReader {

public var sampled: Bool? {
if let single = httpHeaderFields[B3HTTPHeaders.Single.b3Field] {
return single != B3HTTPHeaders.Constants.unsampledValue
return single.components(separatedBy: B3HTTPHeaders.Constants.b3Separator)[safe: 2] != B3HTTPHeaders.Constants.unsampledValue
Copy link
Member Author

@maciejburda maciejburda Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should read when B3 when it is just "0" as well.

I think that was the main source of confusion, because in public documentation:
https://github.com/openzipkin/b3-propagation
Both b3=0 and b3={TraceId}-{SpanId}-{SamplingState}-{ParentSpanId} are valid.

I think that in our case we always want to use the latter, or not inject header at all.
But maybe if sampled = false and TraceContextInjection = sampled we should inject b3=0?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backend SDK engineer here 👋🏼

I think we should be flexible on what we extract and accept both b3=0 and b3={TraceId}-{SpanId}-{SamplingState}-{ParentSpanId} styles, but only inject in the b3={TraceId}-{SpanId}-{SamplingState}-{ParentSpanId} style

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, if the input is b3=0, then we don't have a trace-id or span-id at all. For reference in our C# library, we don't accept a value of b3=0 so if a new span is created then we get a brand new tracecontext. I wonder what the other libraries do...let me get back to you, but I think the injection behavior you have encoded is accurate so let's not block on this

} else if let multiple = httpHeaderFields[B3HTTPHeaders.Multiple.sampledField] {
return multiple == B3HTTPHeaders.Constants.sampledValue
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,39 +78,38 @@ public class B3HTTPHeadersWriter: TracePropagationHeadersWriter {
/// - Parameter spanID: The span ID.
/// - Parameter parentSpanID: The parent span ID, if applicable.
public func write(traceContext: TraceContext) {
let sampled = traceContext.isKept

typealias Constants = B3HTTPHeaders.Constants

switch (traceContextInjection, sampled) {
case (.all, _), (.sampled, true):
switch injectEncoding {
case .multiple:
traceHeaderFields = [
B3HTTPHeaders.Multiple.sampledField: sampled ? Constants.sampledValue : Constants.unsampledValue
]
let sampled = traceContext.isKept
let shouldInject: Bool = {
switch traceContextInjection {
case .all: return true
case .sampled: return sampled
}
}()
guard shouldInject else {
return
}

if sampled {
traceHeaderFields[B3HTTPHeaders.Multiple.traceIDField] = String(traceContext.traceID, representation: .hexadecimal32Chars)
traceHeaderFields[B3HTTPHeaders.Multiple.spanIDField] = String(traceContext.spanID, representation: .hexadecimal16Chars)
traceHeaderFields[B3HTTPHeaders.Multiple.parentSpanIDField] = traceContext.parentSpanID.map { String($0, representation: .hexadecimal16Chars) }
}
case .single:
if sampled {
traceHeaderFields[B3HTTPHeaders.Single.b3Field] = [
String(traceContext.traceID, representation: .hexadecimal32Chars),
String(traceContext.spanID, representation: .hexadecimal16Chars),
sampled ? Constants.sampledValue : Constants.unsampledValue,
traceContext.parentSpanID.map { String($0, representation: .hexadecimal16Chars) }
]
.compactMap { $0 }
.joined(separator: Constants.b3Separator)
} else {
traceHeaderFields[B3HTTPHeaders.Single.b3Field] = Constants.unsampledValue
}
Comment on lines -98 to -110
Copy link
Member Author

@maciejburda maciejburda Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the issue. Take a look at the dead code in line 103 - dead code because of if statement before.

switch injectEncoding {
case .multiple:
traceHeaderFields = [
B3HTTPHeaders.Multiple.sampledField: sampled ? Constants.sampledValue : Constants.unsampledValue,
B3HTTPHeaders.Multiple.traceIDField: String(traceContext.traceID, representation: .hexadecimal32Chars),
B3HTTPHeaders.Multiple.spanIDField: String(traceContext.spanID, representation: .hexadecimal16Chars),
]
if let parentSpanId = traceContext.parentSpanID.map({ String($0, representation: .hexadecimal16Chars) }) {
traceHeaderFields[B3HTTPHeaders.Multiple.parentSpanIDField] = parentSpanId
}
case (.sampled, false):
break
case .single:
traceHeaderFields[B3HTTPHeaders.Single.b3Field] = [
String(traceContext.traceID, representation: .hexadecimal32Chars),
String(traceContext.spanID, representation: .hexadecimal16Chars),
sampled ? Constants.sampledValue : Constants.unsampledValue,
traceContext.parentSpanID.map { String($0, representation: .hexadecimal16Chars) }
]
.compactMap { $0 }
.joined(separator: Constants.b3Separator)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,29 +49,37 @@ public class HTTPHeadersWriter: TracePropagationHeadersWriter {
/// - Parameter spanID: The span ID.
/// - Parameter parentSpanID: The parent span ID, if applicable.
public func write(traceContext: TraceContext) {
switch (traceContextInjection, traceContext.isKept) {
case (.all, _), (.sampled, true):
Comment on lines -52 to -53
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was actually correct - but decided to rework the code to match b3 implementation.

I think it's slightly better to read now.

traceHeaderFields = [
TracingHTTPHeaders.samplingPriorityField: traceContext.isKept ? "1" : "0"
]
traceHeaderFields[TracingHTTPHeaders.traceIDField] = String(traceContext.traceID.idLo)
traceHeaderFields[TracingHTTPHeaders.parentSpanIDField] = String(traceContext.spanID, representation: .decimal)
traceHeaderFields[TracingHTTPHeaders.tagsField] = "_dd.p.tid=\(traceContext.traceID.idHiHex)"
var baggageItems: [String] = []
if let sessionId = traceContext.rumSessionId {
baggageItems.append("\(W3CHTTPHeaders.Constants.rumSessionBaggageKey)=\(sessionId)")
}
if let userId = traceContext.userId {
baggageItems.append("\(W3CHTTPHeaders.Constants.userBaggageKey)=\(userId)")
}
if let accountId = traceContext.accountId {
baggageItems.append("\(W3CHTTPHeaders.Constants.accountBaggageKey)=\(accountId)")
}
if baggageItems.isEmpty == false {
traceHeaderFields[W3CHTTPHeaders.baggage] = baggageItems.joined(separator: ",")
typealias Constants = W3CHTTPHeaders.Constants

let sampled = traceContext.isKept
let shouldInject: Bool = {
switch traceContextInjection {
case .all: return true
case .sampled: return sampled
}
case (.sampled, false):
break
}()
guard shouldInject else {
return
}

traceHeaderFields = [
TracingHTTPHeaders.samplingPriorityField: traceContext.isKept ? "1" : "0"
]
traceHeaderFields[TracingHTTPHeaders.traceIDField] = String(traceContext.traceID.idLo)
traceHeaderFields[TracingHTTPHeaders.parentSpanIDField] = String(traceContext.spanID, representation: .decimal)
traceHeaderFields[TracingHTTPHeaders.tagsField] = "_dd.p.tid=\(traceContext.traceID.idHiHex)"
var baggageItems: [String] = []
if let sessionId = traceContext.rumSessionId {
baggageItems.append("\(W3CHTTPHeaders.Constants.rumSessionBaggageKey)=\(sessionId)")
}
if let userId = traceContext.userId {
baggageItems.append("\(W3CHTTPHeaders.Constants.userBaggageKey)=\(userId)")
}
if let accountId = traceContext.accountId {
baggageItems.append("\(W3CHTTPHeaders.Constants.accountBaggageKey)=\(accountId)")
}
if baggageItems.isEmpty == false {
traceHeaderFields[W3CHTTPHeaders.baggage] = baggageItems.joined(separator: ",")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,48 +61,52 @@ public class W3CHTTPHeadersWriter: TracePropagationHeadersWriter {
typealias Constants = W3CHTTPHeaders.Constants

let sampled = traceContext.isKept
let shouldInject: Bool = {
switch traceContextInjection {
case .all: return true
case .sampled: return sampled
}
}()
guard shouldInject else {
return
}

switch (traceContextInjection, sampled) {
case (.all, _), (.sampled, true):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. Behaviour was correct according to tests.

I hope it's a bit less cryptic now.

traceHeaderFields[W3CHTTPHeaders.traceparent] = [
Constants.version,
String(traceContext.traceID, representation: .hexadecimal32Chars),
String(traceContext.spanID, representation: .hexadecimal16Chars),
sampled ? Constants.sampledValue : Constants.unsampledValue
]
.joined(separator: Constants.separator)
traceHeaderFields[W3CHTTPHeaders.traceparent] = [
Constants.version,
String(traceContext.traceID, representation: .hexadecimal32Chars),
String(traceContext.spanID, representation: .hexadecimal16Chars),
sampled ? Constants.sampledValue : Constants.unsampledValue
]
.joined(separator: Constants.separator)

// while merging, the tracestate values from the tracestate property take precedence
// over the ones from the trace context
let tracestate: [String: String] = [
Constants.sampling: "\(sampled ? 1 : 0)",
Constants.parentId: String(traceContext.spanID, representation: .hexadecimal16Chars)
].merging(tracestate) { old, new in
return new
}
// while merging, the tracestate values from the tracestate property take precedence
// over the ones from the trace context
let tracestate: [String: String] = [
Constants.sampling: "\(sampled ? 1 : 0)",
Constants.parentId: String(traceContext.spanID, representation: .hexadecimal16Chars)
].merging(tracestate) { old, new in
return new
}

let ddtracestate = tracestate
.map { "\($0.key)\(Constants.tracestateKeyValueSeparator)\($0.value)" }
.sorted()
.joined(separator: Constants.tracestatePairSeparator)
let ddtracestate = tracestate
.map { "\($0.key)\(Constants.tracestateKeyValueSeparator)\($0.value)" }
.sorted()
.joined(separator: Constants.tracestatePairSeparator)

traceHeaderFields[W3CHTTPHeaders.tracestate] = "\(Constants.dd)=\(ddtracestate)"
traceHeaderFields[W3CHTTPHeaders.tracestate] = "\(Constants.dd)=\(ddtracestate)"

var baggageItems: [String] = []
if let sessionId = traceContext.rumSessionId {
baggageItems.append("\(Constants.rumSessionBaggageKey)=\(sessionId)")
}
if let userId = traceContext.userId {
baggageItems.append("\(Constants.userBaggageKey)=\(userId)")
}
if let accountId = traceContext.accountId {
baggageItems.append("\(Constants.accountBaggageKey)=\(accountId)")
}
if !baggageItems.isEmpty {
traceHeaderFields[W3CHTTPHeaders.baggage] = baggageItems.joined(separator: ",")
}
case (.sampled, false):
break
var baggageItems: [String] = []
if let sessionId = traceContext.rumSessionId {
baggageItems.append("\(Constants.rumSessionBaggageKey)=\(sessionId)")
}
if let userId = traceContext.userId {
baggageItems.append("\(Constants.userBaggageKey)=\(userId)")
}
if let accountId = traceContext.accountId {
baggageItems.append("\(Constants.accountBaggageKey)=\(accountId)")
}
if !baggageItems.isEmpty {
traceHeaderFields[W3CHTTPHeaders.baggage] = baggageItems.joined(separator: ",")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ class B3HTTPHeadersReaderTests: XCTestCase {
writer.write(traceContext: .mockWith(isKept: false))

let reader = B3HTTPHeadersReader(httpHeaderFields: writer.traceHeaderFields)
XCTAssertNil(reader.read(), "When not sampled, it should return no trace context")
let ids = reader.read()
XCTAssertEqual(ids?.traceID, 0)
XCTAssertEqual(ids?.spanID, 0)
XCTAssertNil(ids?.parentSpanID)
XCTAssertEqual(reader.sampled, false)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ class B3HTTPHeadersWriterTests: XCTestCase {
)

let headers = writer.traceHeaderFields
XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "0")
XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "00000000000004d200000000000004d2-0000000000000929-0-000000000000162e")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the key difference in the behaviour in this PR.

}

func testWritingSampledTraceContext_withSingleEncoding_andCustomSamplingStrategy() {
let writer = B3HTTPHeadersWriter(
injectEncoding: .single,
traceContextInjection: .all
traceContextInjection: .sampled
)

writer.write(
Expand All @@ -69,7 +69,7 @@ class B3HTTPHeadersWriterTests: XCTestCase {
func testWritingDroppedTraceContext_withSingleEncoding_andCustomSamplingStrategy() {
let writer = B3HTTPHeadersWriter(
injectEncoding: .single,
traceContextInjection: .all
traceContextInjection: .sampled
)

writer.write(
Expand All @@ -82,7 +82,7 @@ class B3HTTPHeadersWriterTests: XCTestCase {
)

let headers = writer.traceHeaderFields
XCTAssertEqual(headers[B3HTTPHeaders.Single.b3Field], "0")
XCTAssertNil(headers[B3HTTPHeaders.Single.b3Field])
}

func testItWritesSingleHeaderWithoutOptionalValues() {
Expand Down Expand Up @@ -141,10 +141,10 @@ class B3HTTPHeadersWriterTests: XCTestCase {
)

let headers = writer.traceHeaderFields
XCTAssertNil(headers[B3HTTPHeaders.Multiple.traceIDField])
XCTAssertNil(headers[B3HTTPHeaders.Multiple.spanIDField])
XCTAssertEqual(headers[B3HTTPHeaders.Multiple.traceIDField], "00000000000004d200000000000004d2")
XCTAssertEqual(headers[B3HTTPHeaders.Multiple.spanIDField], "0000000000000929")
XCTAssertEqual(headers[B3HTTPHeaders.Multiple.sampledField], "0")
XCTAssertNil(headers[B3HTTPHeaders.Multiple.parentSpanIDField])
XCTAssertEqual(headers[B3HTTPHeaders.Multiple.parentSpanIDField], "000000000000162e")
}

func testWritingSampledTraceContext_withMultipleEncoding_andCustomSamplingStrategy() {
Expand Down Expand Up @@ -185,10 +185,10 @@ class B3HTTPHeadersWriterTests: XCTestCase {
)

let headers = writer.traceHeaderFields
XCTAssertNil(headers[B3HTTPHeaders.Multiple.traceIDField])
XCTAssertNil(headers[B3HTTPHeaders.Multiple.spanIDField])
XCTAssertEqual(headers[B3HTTPHeaders.Multiple.traceIDField], "00000000000004d200000000000004d2")
XCTAssertEqual(headers[B3HTTPHeaders.Multiple.spanIDField], "0000000000000929")
XCTAssertEqual(headers[B3HTTPHeaders.Multiple.sampledField], "0")
XCTAssertNil(headers[B3HTTPHeaders.Multiple.parentSpanIDField])
XCTAssertEqual(headers[B3HTTPHeaders.Multiple.parentSpanIDField], "000000000000162e")
}

func testItWritesMultipleHeaderWithoutOptionalValues() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase {
)

XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.originField))
XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "0")
XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Single.b3Field), "000000000000000a0000000000000064-0000000000000064-0")

XCTAssertNil(traceContext, "It must return no trace context")
}
Expand Down Expand Up @@ -266,8 +266,8 @@ class URLSessionRUMResourcesHandlerTests: XCTestCase {
)

XCTAssertNil(request.value(forHTTPHeaderField: TracingHTTPHeaders.originField))
XCTAssertNil(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField))
XCTAssertNil(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField))
XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.traceIDField), "000000000000000a0000000000000064")
XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.spanIDField), "0000000000000064")
XCTAssertNil(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.parentSpanIDField))
XCTAssertEqual(request.value(forHTTPHeaderField: B3HTTPHeaders.Multiple.sampledField), "0")

Expand Down