Skip to content

[SDTEST-2694] Switched to the new OpenTelemetry Swift Core SDK#191

Merged
ypopovych merged 3 commits into
mainfrom
sdtest-2694-switch-to-new-opentelemetry-sdk
Feb 10, 2026
Merged

[SDTEST-2694] Switched to the new OpenTelemetry Swift Core SDK#191
ypopovych merged 3 commits into
mainfrom
sdtest-2694-switch-to-new-opentelemetry-sdk

Conversation

@ypopovych

Copy link
Copy Markdown
Contributor

What and why?

Switched OpenTelemetry from my fork to the official version

How?

  • Changed project dependency
  • Copied URLSessionInstrumentation files to the sources from the full OTel SDK
  • Added NOOP exporter instead of the InMemoryExporter.

Review checklist

  • Feature or bugfix MUST have appropriate tests (unit, integration)
  • Make sure each commit and the PR mention the Issue number or JIRA reference

@ypopovych
ypopovych requested review from a team as code owners February 6, 2026 16:58
@ypopovych
ypopovych requested review from Copilot and removed request for a team February 6, 2026 16:58
@ypopovych
ypopovych requested a review from calvinbayer February 6, 2026 16:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR migrates the project’s OpenTelemetry dependency from a fork to the official Swift Core SDK and vendors URLSession instrumentation sources into DatadogSDKTesting to preserve network tracing behavior.

Changes:

  • Switched Xcode project SwiftPM dependency to open-telemetry/opentelemetry-swift-core and removed products from the old package.
  • Added vendored URLSession instrumentation implementation under Sources/DatadogSDKTesting/NetworkInstrumentation.
  • Replaced the InMemoryExporter fallback with a local NoopSpanExporter and adjusted timestamp/duration nanosecond conversions.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
Sources/EventsExporter/Utils/DateFormatting.swift Imports OTel API and adds toNanosecondsUInt helper for UInt64 nanosecond conversions.
Sources/EventsExporter/Spans/SpanEncoder.swift Switches span start/duration fields to use toNanosecondsUInt.
Sources/DatadogSDKTesting/Utils/NoopSpanExporter.swift Adds a NOOP exporter used when tracing is disabled / exporter creation fails.
Sources/DatadogSDKTesting/NetworkInstrumentation/URLSessionLogger.swift Adds vendored request/response span creation and header propagation logic.
Sources/DatadogSDKTesting/NetworkInstrumentation/URLSessionInstrumentationConfiguration.swift Adds configuration surface for URLSession instrumentation + semantic convention selection.
Sources/DatadogSDKTesting/NetworkInstrumentation/URLSessionInstrumentation.swift Adds vendored swizzling-based URLSession instrumentation implementation.
Sources/DatadogSDKTesting/NetworkInstrumentation/InstrumentationUtils.swift Adds runtime helper utilities used by the swizzling implementation.
Sources/DatadogSDKTesting/NetworkInstrumentation/DDNetworkInstrumentation.swift Drops dependency import for external instrumentation product and uses vendored implementation.
Sources/DatadogSDKTesting/DDTracer.swift Replaces InMemoryExporter usage with NoopSpanExporter.
Sources/DatadogSDKTesting/DDTestSuite.swift Switches suite duration to toNanosecondsUInt.
Sources/DatadogSDKTesting/DDTestSession.swift Switches session duration to toNanosecondsUInt.
Sources/DatadogSDKTesting/DDTestModule.swift Switches module duration to toNanosecondsUInt.
Sources/DatadogSDKTesting/DDTest.swift Switches test duration to toNanosecondsUInt.
LICENSE-3rdparty.csv Updates OpenTelemetry Authors attribution string.
DatadogSDKTesting.xcodeproj/project.pbxproj Updates SwiftPM package reference/products and adds new Swift sources to build.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

}()

public extension TimeInterval {
var toNanosecondsUInt: UInt64 { UInt64(toNanoseconds) }

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

toNanosecondsUInt uses UInt64(toNanoseconds), which will trap at runtime if toNanoseconds is negative or not representable in UInt64 (e.g., if endTime < startTime or due to clock adjustments). Consider using a clamping conversion (e.g. max with 0) or a safe initializer like UInt64(clamping:) to avoid crashing.

Suggested change
var toNanosecondsUInt: UInt64 { UInt64(toNanoseconds) }
var toNanosecondsUInt: UInt64 { UInt64(clamping: toNanoseconds) }

Copilot uses AI. Check for mistakes.
Comment on lines +217 to +230
var instrumentedRequest = request
objc_setAssociatedObject(instrumentedRequest, URLSessionInstrumentation.instrumentedKey, true, .OBJC_ASSOCIATION_COPY_NONATOMIC)
let propagators = OpenTelemetry.instance.propagators

var traceHeaders = tracePropagationHTTPHeaders(span: span,
customBaggage: customBaggage,
textMapPropagator: propagators.textMapPropagator,
textMapBaggagePropagator: propagators.textMapBaggagePropagator)

if let originalHeaders = request.allHTTPHeaderFields {
traceHeaders.merge(originalHeaders) { _, new in new }
}
instrumentedRequest.allHTTPHeaderFields = traceHeaders
return instrumentedRequest

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

objc_setAssociatedObject requires an UnsafeRawPointer key, but this call passes URLSessionInstrumentation.instrumentedKey which is a String. This won’t compile, and even if it did, using a non-stable key would make associated objects unreliable. Use a static/global UInt8 (or similar) and pass its address (&keyVar) as the association key.

Copilot uses AI. Check for mistakes.
}
let imp = imp_implementationWithBlock(
unsafeBitCast(block, to: AnyObject.self))
class_addMethod(cls, selector, imp, "@@@")

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

class_addMethod is called with the type encoding "@@@", which doesn’t match the actual selector signature (it should be a void return with several object arguments). An incorrect encoding can cause undefined behavior at runtime. Provide the correct Objective-C type encoding for urlSession(_:task:didFinishCollecting:) when adding the method.

Suggested change
class_addMethod(cls, selector, imp, "@@@")
class_addMethod(cls, selector, imp, "v@:@@@")

Copilot uses AI. Check for mistakes.
Comment on lines 44 to +45
private func internalEnd(endTime: Date? = nil) {
duration = (endTime ?? DDTestMonitor.clock.now).timeIntervalSince(startTime).toNanoseconds
duration = (endTime ?? DDTestMonitor.clock.now).timeIntervalSince(startTime).toNanosecondsUInt

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

toNanosecondsUInt is defined in EventsExporter (in DateFormatting.swift), but this file only imports OpenTelemetryApi. As-is, this will fail to compile with “TimeInterval has no member toNanosecondsUInt”. Add an import EventsExporter (or move the TimeInterval extension into a module that these sources already import).

Copilot uses AI. Check for mistakes.
Comment on lines 40 to +41
private func internalEnd(endTime: Date? = nil) {
duration = (endTime ?? DDTestMonitor.clock.now).timeIntervalSince(startTime).toNanoseconds
duration = (endTime ?? DDTestMonitor.clock.now).timeIntervalSince(startTime).toNanosecondsUInt

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

This file uses toNanosecondsUInt, but it’s defined in the EventsExporter target. Since this file doesn’t import EventsExporter, this will fail to compile. Add the appropriate import or move the TimeInterval extension into a module that DatadogSDKTesting already imports.

Copilot uses AI. Check for mistakes.
Comment on lines +760 to +817
// For iOS 15+/macOS 12+, handle async/await methods differently
if #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) {
// Check if we can determine if this is an async/await call
// For iOS 15/macOS 12, we can't use Task.basePriority, so we check other indicators
var isAsyncContext = false

if #available(OSX 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) {
isAsyncContext = Task.basePriority != nil
} else {
// For iOS 15/macOS 12, check if the task has no delegate and no session delegate
// This is a heuristic that works for async/await methods
isAsyncContext = task.delegate == nil &&
(task.value(forKey: "session") as? URLSession)?.delegate == nil &&
task.state == .suspended
}

if isAsyncContext {
// This is likely an async/await call
let instrumentedRequest = URLSessionLogger.processAndLogRequest(request,
sessionTaskId: taskId,
instrumentation: self,
shouldInjectHeaders: true)
if let instrumentedRequest {
task.setValue(instrumentedRequest, forKey: "currentRequest")
}
self.setIdKey(value: taskId, for: task)

// For async/await methods, we need to ensure the delegate is set
// to capture the completion, but only if there's no existing delegate
// AND no session delegate (session delegates are called for async/await too)
if task.delegate == nil,
task.state == .suspended,
(task.value(forKey: "session") as? URLSession)?.delegate == nil {
task.delegate = AsyncTaskDelegate(instrumentation: self, sessionTaskId: taskId)
}
return
}
}

if #available(OSX 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) {
guard Task.basePriority != nil else {
// If not inside a Task basePriority is nil
return
}

let instrumentedRequest = URLSessionLogger.processAndLogRequest(request,
sessionTaskId: taskId,
instrumentation: self,
shouldInjectHeaders: true)
if let instrumentedRequest {
task.setValue(instrumentedRequest, forKey: "currentRequest")
}
self.setIdKey(value: taskId, for: task)

if task.delegate == nil, task.state == .suspended, (task.value(forKey: "session") as? URLSession)?.delegate == nil {
task.delegate = FakeDelegate()
}
}

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

The async/await instrumentation path relies on heuristics (e.g., Task.basePriority checks, delegate/session delegate checks, KVC for "session" and "currentRequest") and introduces AsyncTaskDelegate/FakeDelegate. There are no tests validating that async/await URLSession APIs are correctly instrumented (span created, headers injected, span ended on completion) across iOS 15/16+ branches. Add tests covering these code paths to prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines 65 to +66
private func internalEnd(endTime: Date? = nil) {
duration = (endTime ?? DDTestMonitor.clock.now).timeIntervalSince(startTime).toNanoseconds
duration = (endTime ?? DDTestMonitor.clock.now).timeIntervalSince(startTime).toNanosecondsUInt

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

toNanosecondsUInt isn’t available in this module unless EventsExporter is imported (the extension is currently defined in Sources/EventsExporter/Utils/DateFormatting.swift). With only OpenTelemetryApi imported here, this will not compile. Import EventsExporter here or relocate the extension into DatadogSDKTesting.

Copilot uses AI. Check for mistakes.
Comment on lines +11 to +21
public init() {}

public func export(spans: [SpanData], explicitTimeout: TimeInterval? = nil) -> SpanExporterResultCode {
.success
}

public func flush(explicitTimeout: TimeInterval? = nil) -> SpanExporterResultCode {
.success
}

public func shutdown(explicitTimeout: TimeInterval? = nil) {}

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

NoopSpanExporter is declared with the default (internal) access level, but its initializer and methods are marked public. Swift doesn’t allow members to be more visible than their enclosing type, so this won’t compile. Either make the type public or drop the public modifiers on its members.

Suggested change
public init() {}
public func export(spans: [SpanData], explicitTimeout: TimeInterval? = nil) -> SpanExporterResultCode {
.success
}
public func flush(explicitTimeout: TimeInterval? = nil) -> SpanExporterResultCode {
.success
}
public func shutdown(explicitTimeout: TimeInterval? = nil) {}
init() {}
func export(spans: [SpanData], explicitTimeout: TimeInterval? = nil) -> SpanExporterResultCode {
.success
}
func flush(explicitTimeout: TimeInterval? = nil) -> SpanExporterResultCode {
.success
}
func shutdown(explicitTimeout: TimeInterval? = nil) {}

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +27
mutating func setData(_ data: URLRequest) {
request = data

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

NetworkRequestState.setData(_:) takes a URLRequest parameter named data and assigns it back to request, which looks like a copy/paste mistake (and the type doesn’t match the name). If the intent is to store received payload bytes, this should probably accept Data and update dataProcessed instead.

Suggested change
mutating func setData(_ data: URLRequest) {
request = data
mutating func setData(_ data: Data) {
self.dataProcessed = data

Copilot uses AI. Check for mistakes.
Comment on lines 248 to +249
let testEndTime = endTime ?? DDTestMonitor.clock.now
duration = testEndTime.timeIntervalSince(startTime).toNanoseconds
duration = testEndTime.timeIntervalSince(startTime).toNanosecondsUInt

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

toNanosecondsUInt is not defined in OpenTelemetryApi; it’s currently added in the EventsExporter target. Since this file doesn’t import EventsExporter, this line will not compile. Add an import EventsExporter here (if appropriate) or move the TimeInterval extension into DatadogSDKTesting.

Copilot uses AI. Check for mistakes.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

private let configurationQueue = DispatchQueue(
label: "io.opentelemetry.configuration")

static var instrumentedKey = "io.opentelemetry.instrumentedCall"

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

instrumentedKey is a String, but objc_setAssociatedObject keys must be an UnsafeRawPointer (typically &someStaticVar). As-is, any use of URLSessionInstrumentation.instrumentedKey as an association key will not compile and/or won’t work correctly. Define a static key storage (e.g. static var instrumentedKey: UInt8 = 0) and always pass &instrumentedKey.

Suggested change
static var instrumentedKey = "io.opentelemetry.instrumentedCall"
private static var instrumentedKey: UInt8 = 0

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +16
import os.log
#if os(iOS) && !targetEnvironment(macCatalyst)
import NetworkStatus
#endif // os(iOS) && !targetEnvironment(macCatalyst)

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

NetworkStatus / NetworkStatusInjector are imported/used here, but there’s no corresponding dependency in the project. This will fail to compile on iOS builds where the #if os(iOS) condition is true. Consider guarding with #if canImport(NetworkStatus) (and/or providing a fallback path that skips network-status injection).

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +75
class URLSessionInstrumentation {
private var requestMap = [String: NetworkRequestState]()

private var _configuration: URLSessionInstrumentationConfiguration
public var configuration: URLSessionInstrumentationConfiguration {
get {
configurationQueue.sync { _configuration }
}
set {
configurationQueue.sync { _configuration = newValue }
}
}

private let queue = DispatchQueue(
label: "io.opentelemetry.ddnetworkinstrumentation")
private let configurationQueue = DispatchQueue(
label: "io.opentelemetry.configuration")

static var instrumentedKey = "io.opentelemetry.instrumentedCall"

static let excludeList: [String] = [
"__NSCFURLProxySessionConnection"
]

static let AVTaskClassList: [AnyClass] = [
"__NSCFBackgroundAVAggregateAssetDownloadTask",
"__NSCFBackgroundAVAssetDownloadTask",
"__NSCFBackgroundAVAggregateAssetDownloadTaskNoChildTask"
]
.compactMap { NSClassFromString($0) }

public var startedRequestSpans: [Span] {
var spans = [Span]()
URLSessionLogger.runningSpansQueue.sync {
spans = Array(URLSessionLogger.runningSpans.values)
}
return spans
}

public init(configuration: URLSessionInstrumentationConfiguration) {
self._configuration = configuration
injectInNSURLClasses()
}

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

URLSessionInstrumentation is an internal class, but it exposes public API (public var configuration, public init, public var startedRequestSpans). Swift doesn’t allow public members on an internal type, so this won’t compile. Either make the class public or remove the public modifiers (likely preferred since it’s only used internally by DDNetworkInstrumentation).

Copilot uses AI. Check for mistakes.
Comment on lines +489 to +491
let key = String(selector.hashValue)
objc_setAssociatedObject(session, key, true, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
let castedIMP = unsafeBitCast(originalIMP,

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

These objc_setAssociatedObject calls pass a String (key = String(selector.hashValue)) as the association key. The Objective-C runtime requires an UnsafeRawPointer key, so this won’t compile (and even if bridged somehow, it wouldn’t be stable/unique). Use dedicated static key variables and pass their addresses (e.g. &someKey) instead.

Copilot uses AI. Check for mistakes.
Comment on lines +217 to +220
var instrumentedRequest = request
objc_setAssociatedObject(instrumentedRequest, URLSessionInstrumentation.instrumentedKey, true, .OBJC_ASSOCIATION_COPY_NONATOMIC)
let propagators = OpenTelemetry.instance.propagators

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

objc_setAssociatedObject is being called with instrumentedRequest (a URLRequest value type) and URLSessionInstrumentation.instrumentedKey (currently a String). Associated objects only work with Objective-C reference types and require an UnsafeRawPointer key. This code won’t compile/work as intended; consider associating on the underlying NSURLRequest object or tracking “instrumented” state via another mechanism (e.g. a dedicated header or mapping by task identifier).

Copilot uses AI. Check for mistakes.

func end(time: Date?) {
duration = (time ?? Date()).timeIntervalSince(startTime).toNanoseconds
duration = (time ?? Date()).timeIntervalSince(startTime).toNanosecondsUInt

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

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

toNanosecondsUInt is defined as a public extension in the EventsExporter module, but this test file only does import protocol EventsExporter.Logger. Scoped import protocol won’t make the TimeInterval extension visible, so this is likely to fail to compile. Import EventsExporter (or move toNanosecondsUInt into a module that this test already imports).

Copilot uses AI. Check for mistakes.
@ypopovych
ypopovych merged commit 8392f8d into main Feb 10, 2026
1 check passed
@ypopovych
ypopovych deleted the sdtest-2694-switch-to-new-opentelemetry-sdk branch February 10, 2026 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants