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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ artifacts/

# AI files
.claude/settings.local.json
.rum-analysis/
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit/ Seems to be leftover from personal setup:

Suggested change
.rum-analysis/


# GSD planning files (local only)
.planning/
6 changes: 6 additions & 0 deletions Datadog/Datadog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,8 @@
61A2CC342A44A6030000FF25 /* DatadogRUM.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D23F8E9929DDCD28001CFAE8 /* DatadogRUM.framework */; };
61A2CC362A44B0A20000FF25 /* TraceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A2CC352A44B0A20000FF25 /* TraceConfiguration.swift */; };
61A2CC372A44B0A20000FF25 /* TraceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A2CC352A44B0A20000FF25 /* TraceConfiguration.swift */; };
7F859F0296CB41419B09C221 /* InterceptedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4209932100E4E73BADD5A3D /* InterceptedRequest.swift */; };
817358634A3541559D67A261 /* InterceptedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4209932100E4E73BADD5A3D /* InterceptedRequest.swift */; };
61A2CC392A44B0EA0000FF25 /* Trace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A2CC382A44B0EA0000FF25 /* Trace.swift */; };
61A2CC3A2A44B0EA0000FF25 /* Trace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A2CC382A44B0EA0000FF25 /* Trace.swift */; };
61A2CC3C2A44BED30000FF25 /* Tracer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A2CC3B2A44BED30000FF25 /* Tracer.swift */; };
Expand Down Expand Up @@ -3400,6 +3402,7 @@
61A2CC202A443D330000FF25 /* DDRUMConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDRUMConfigurationTests.swift; sourceTree = "<group>"; };
61A2CC232A44454D0000FF25 /* DDRUMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDRUMTests.swift; sourceTree = "<group>"; };
61A2CC352A44B0A20000FF25 /* TraceConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceConfiguration.swift; sourceTree = "<group>"; };
C4209932100E4E73BADD5A3D /* InterceptedRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptedRequest.swift; sourceTree = "<group>"; };
61A2CC382A44B0EA0000FF25 /* Trace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trace.swift; sourceTree = "<group>"; };
61A2CC3B2A44BED30000FF25 /* Tracer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tracer.swift; sourceTree = "<group>"; };
61A614E7276B2BD000A06CE7 /* RUMOffViewEventsHandlingRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RUMOffViewEventsHandlingRule.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -7407,6 +7410,7 @@
61C5A87E24509A0C00DA608C /* DDSpanContext.swift */,
61A2CC382A44B0EA0000FF25 /* Trace.swift */,
61A2CC352A44B0A20000FF25 /* TraceConfiguration.swift */,
C4209932100E4E73BADD5A3D /* InterceptedRequest.swift */,
61A2CC3B2A44BED30000FF25 /* Tracer.swift */,
09D2B7EA2EF4258D0089F05B /* SamplingDecision.swift */,
D2546C0629AF55CE0054E00B /* Feature */,
Expand Down Expand Up @@ -10987,6 +10991,7 @@
D2C1A4FB29C4C4CB00946C31 /* MessageReceivers.swift in Sources */,
3C32359D2B55386C000B4258 /* OTelSpanLink.swift in Sources */,
61A2CC362A44B0A20000FF25 /* TraceConfiguration.swift in Sources */,
7F859F0296CB41419B09C221 /* InterceptedRequest.swift in Sources */,
61A2CC392A44B0EA0000FF25 /* Trace.swift in Sources */,
D2C1A50029C4C4CB00946C31 /* ActiveSpansPool.swift in Sources */,
3CB012DF2B482E0400557951 /* NOPOTelSpanBuilder.swift in Sources */,
Expand Down Expand Up @@ -11296,6 +11301,7 @@
D2C1A54129C4F2DF00946C31 /* MessageReceivers.swift in Sources */,
3C32359E2B55386C000B4258 /* OTelSpanLink.swift in Sources */,
61A2CC372A44B0A20000FF25 /* TraceConfiguration.swift in Sources */,
817358634A3541559D67A261 /* InterceptedRequest.swift in Sources */,
61A2CC3A2A44B0EA0000FF25 /* Trace.swift in Sources */,
D2C1A54229C4F2DF00946C31 /* ActiveSpansPool.swift in Sources */,
3CB012E02B482E0400557951 /* NOPOTelSpanBuilder.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ internal struct TracingURLSessionHandler: DatadogURLSessionHandler {
let traceContextInjection: TraceContextInjection
/// Telemetry interface for tracking SDK usage
let telemetry: Telemetry
/// Optional callback to customize the span for each intercepted request.
let spanCustomization: Trace.Configuration.SpanCustomization?

weak var tracer: DatadogTracer?

Expand All @@ -54,14 +56,16 @@ internal struct TracingURLSessionHandler: DatadogURLSessionHandler {
samplingRate: SampleRate,
firstPartyHosts: FirstPartyHosts,
traceContextInjection: TraceContextInjection,
telemetry: Telemetry
telemetry: Telemetry,
spanCustomization: Trace.Configuration.SpanCustomization? = nil
) {
self.tracer = tracer
self.contextReceiver = contextReceiver
self.samplingRate = samplingRate
self.firstPartyHosts = firstPartyHosts
self.traceContextInjection = traceContextInjection
self.telemetry = telemetry
self.spanCustomization = spanCustomization
}

func modify(request: URLRequest, headerTypes: Set<TracingHeaderType>, networkContext: NetworkContext?) -> (URLRequest, TraceContext?, URLSessionHandlerCapturedState?) {
Expand Down Expand Up @@ -315,6 +319,13 @@ internal struct TracingURLSessionHandler: DatadogURLSessionHandler {
span.setTag(key: SpanTags.isBackground, value: didStartInBackground || doesEndInBackground)
}

spanCustomization?(
.init(from: interception.request),
span,
resourceCompletion.httpResponse,
resourceCompletion.error
)

span.finish(at: endTime)
}

Expand Down
33 changes: 33 additions & 0 deletions DatadogTrace/Sources/InterceptedRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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

extension Trace.Configuration {
/// An immutable snapshot of a network request, passed to ``SpanCustomization`` callbacks.
///
/// Properties are copied from the original `URLRequest` at interception time and are safe to read
/// from any thread.
public struct InterceptedRequest {
/// The URL of the request.
public let url: URL?
/// The HTTP method of the request (e.g. `"GET"`, `"POST"`).
public let httpMethod: String?
/// The body data of the request.
public let httpBody: Data?
}
}

internal extension Trace.Configuration.InterceptedRequest {
init(from request: ImmutableRequest) {
self.url = request.url
self.httpMethod = request.httpMethod
// httpBody is Data? — safe to access; the thread-safety concern in ImmutableRequest
// applies only to allHTTPHeaderFields (bridged NSMutableDictionary).
self.httpBody = request.unsafeOriginal.httpBody
Comment on lines +29 to +31
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit/ This comment seems to be agent's own reflection on this work, rather than sth useful in this context.

Suggested change
// httpBody is Data? — safe to access; the thread-safety concern in ImmutableRequest
// applies only to allHTTPHeaderFields (bridged NSMutableDictionary).
self.httpBody = request.unsafeOriginal.httpBody
self.httpBody = request.unsafeOriginal.httpBody

}
}
3 changes: 2 additions & 1 deletion DatadogTrace/Sources/Trace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ public enum Trace {
samplingRate: configuration.debugSDK ? 100 : tracingSampleRate,
firstPartyHosts: firstPartyHosts,
traceContextInjection: traceContextInjection,
telemetry: core.telemetry
telemetry: core.telemetry,
spanCustomization: configuration.urlSessionTracking?.spanCustomization
)

try core.register(urlSessionHandler: urlSessionHandler)
Expand Down
34 changes: 33 additions & 1 deletion DatadogTrace/Sources/TraceConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,28 @@ extension Trace {
public struct Configuration: SampledTelemetry {
public typealias EventMapper = (SpanEvent) -> SpanEvent

/// A callback that allows customizing the span created for each intercepted network request.
///
/// This closure receives an ``InterceptedRequest`` (a thread-safe snapshot of the original
/// request), the ``OTSpan`` created for it, the HTTP response (if any), and an error (if any).
/// You can use the span's `setTag(key:value:)` or `setOperationName(_:)` methods to add custom
/// attributes.
///
/// Example — tagging GraphQL requests with the operation name:
/// ```swift
/// Trace.Configuration.SpanCustomization { request, span, response, error in
/// if let body = request.httpBody,
/// let json = try? JSONSerialization.jsonObject(with: body) as? [String: Any],
/// let operationName = json["operationName"] as? String {
/// span.setTag(key: "graphql.operation.name", value: operationName)
/// span.setOperationName("graphql.\(operationName)")
/// }
/// }
/// ```
///
/// - Note: Keep the implementation fast and do not make any assumptions on the thread used to run it.
Comment thread
simaoseica-dd marked this conversation as resolved.
public typealias SpanCustomization = (InterceptedRequest, OTSpan, URLResponse?, Error?) -> Void

/// The sampling rate for spans created with the default tracer.
///
/// It must be a number between 0.0 and 100.0, where 0 means no spans will be collected.
Expand Down Expand Up @@ -94,6 +116,11 @@ extension Trace {
/// If your backend is also instrumented with Datadog, you will see the full trace (app → backend).
public var firstPartyHostsTracing: FirstPartyHostsTracing

/// Optional callback to customize spans for intercepted network requests.
///
/// - SeeAlso: ``SpanCustomization``
public var spanCustomization: SpanCustomization?

/// Defines configuration for first-party hosts in distributed tracing.
public enum FirstPartyHostsTracing {
/// Trace the specified hosts using Datadog and W3C `tracecontext` tracing headers.
Expand Down Expand Up @@ -123,8 +150,13 @@ extension Trace {
/// Configuration for automatic network requests tracing.
/// - Parameters:
/// - firstPartyHostsTracing: Distributed tracing configuration for particular first-party hosts.
public init(firstPartyHostsTracing: FirstPartyHostsTracing) {
/// - spanCustomization: Optional callback to customize spans for intercepted requests.
public init(
firstPartyHostsTracing: FirstPartyHostsTracing,
spanCustomization: SpanCustomization? = nil
) {
self.firstPartyHostsTracing = firstPartyHostsTracing
self.spanCustomization = spanCustomization
}
}

Expand Down
Loading