Skip to content

Commit f1f3d40

Browse files
authored
Merge pull request #1696 from DataDog/ganeshnj/fix/resource-attributes-provider
fix: pass through data when network request completes
2 parents 55fae39 + 5cf5f8c commit f1f3d40

10 files changed

+182
-16
lines changed

Datadog/Example/ExampleAppDelegate.swift

+5-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ class ExampleAppDelegate: UIResponder, UIApplicationDelegate {
6666
RUM.enable(
6767
with: RUM.Configuration(
6868
applicationID: Environment.readRUMApplicationID(),
69-
urlSessionTracking: .init(firstPartyHostsTracing: .trace(hosts: [], sampleRate: 100)),
69+
urlSessionTracking: .init(
70+
resourceAttributesProvider: { req, resp, data, err in
71+
print("⭐️ [Attributes Provider] data: \(String(describing: data))")
72+
return [:]
73+
}),
7074
trackBackgroundEvents: true,
7175
customEndpoint: Environment.readCustomRUMURL(),
7276
telemetrySampleRate: 100

Datadog/IntegrationUnitTests/Public/NetworkInstrumentationIntegrationTests.swift

+114-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import XCTest
88
import TestUtilities
99
import DatadogInternal
1010

11+
@testable import DatadogRUM
1112
@testable import DatadogTrace
1213
@testable import DatadogCore
1314

@@ -50,7 +51,7 @@ class NetworkInstrumentationIntegrationTests: XCTestCase {
5051
core.flushAndTearDown()
5152
core = nil
5253
}
53-
54+
5455
func testParentSpanPropagation() throws {
5556
let expectation = expectation(description: "request completes")
5657
// Given
@@ -88,4 +89,116 @@ class NetworkInstrumentationIntegrationTests: XCTestCase {
8889

8990
class MockDelegate: NSObject, URLSessionDataDelegate {
9091
}
92+
93+
func testResourceAttributesProvider_givenURLSessionDataTaskRequest() {
94+
core = DatadogCoreProxy(
95+
context: .mockWith(
96+
env: "test",
97+
version: "1.1.1",
98+
serverTimeOffset: 123
99+
)
100+
)
101+
102+
let providerExpectation = expectation(description: "provider called")
103+
var providerDataCount = 0
104+
RUM.enable(
105+
with: .init(
106+
applicationID: .mockAny(),
107+
urlSessionTracking: .init(
108+
resourceAttributesProvider: { req, resp, data, err in
109+
XCTAssertNotNil(data)
110+
XCTAssertTrue(data!.count > 0)
111+
providerDataCount = data!.count
112+
providerExpectation.fulfill()
113+
return [:]
114+
})
115+
),
116+
in: core
117+
)
118+
119+
URLSessionInstrumentation.enable(
120+
with: .init(
121+
delegateClass: InstrumentedSessionDelegate.self
122+
),
123+
in: core
124+
)
125+
126+
let session = URLSession(
127+
configuration: .ephemeral,
128+
delegate: InstrumentedSessionDelegate(),
129+
delegateQueue: nil
130+
)
131+
var request = URLRequest(url: URL(string: "https://www.datadoghq.com/")!)
132+
request.httpMethod = "GET"
133+
134+
let task = session.dataTask(with: request)
135+
task.resume()
136+
137+
wait(for: [providerExpectation], timeout: 10)
138+
XCTAssertTrue(providerDataCount > 0)
139+
}
140+
141+
func testResourceAttributesProvider_givenURLSessionDataTaskRequestWithCompletionHandler() {
142+
core = DatadogCoreProxy(
143+
context: .mockWith(
144+
env: "test",
145+
version: "1.1.1",
146+
serverTimeOffset: 123
147+
)
148+
)
149+
150+
let providerExpectation = expectation(description: "provider called")
151+
var providerDataCount = 0
152+
var providerData: Data?
153+
RUM.enable(
154+
with: .init(
155+
applicationID: .mockAny(),
156+
urlSessionTracking: .init(
157+
resourceAttributesProvider: { req, resp, data, err in
158+
XCTAssertNotNil(data)
159+
XCTAssertTrue(data!.count > 0)
160+
providerDataCount = data!.count
161+
data.map { providerData = $0 }
162+
providerExpectation.fulfill()
163+
return [:]
164+
})
165+
),
166+
in: core
167+
)
168+
169+
URLSessionInstrumentation.enable(
170+
with: .init(
171+
delegateClass: InstrumentedSessionDelegate.self
172+
),
173+
in: core
174+
)
175+
176+
let session = URLSession(
177+
configuration: .ephemeral,
178+
delegate: InstrumentedSessionDelegate(),
179+
delegateQueue: nil
180+
)
181+
let request = URLRequest(url: URL(string: "https://www.datadoghq.com/")!)
182+
183+
let taskExpectation = self.expectation(description: "task completed")
184+
var taskDataCount = 0
185+
var taskData: Data?
186+
let task = session.dataTask(with: request) { data, _, _ in
187+
XCTAssertNotNil(data)
188+
XCTAssertTrue(data!.count > 0)
189+
taskDataCount = data!.count
190+
data.map { taskData = $0 }
191+
taskExpectation.fulfill()
192+
}
193+
task.resume()
194+
195+
wait(for: [providerExpectation, taskExpectation], timeout: 10)
196+
XCTAssertEqual(providerDataCount, taskDataCount)
197+
XCTAssertEqual(providerData, taskData)
198+
}
199+
200+
class InstrumentedSessionDelegate: NSObject, URLSessionDataDelegate {
201+
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
202+
}
203+
}
91204
}

Datadog/IntegrationUnitTests/RUM/StartingRUMSessionTests.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -143,22 +143,22 @@ class StartingRUMSessionTests: XCTestCase {
143143
let rumTime = DateProviderMock()
144144
rumConfig.dateProvider = rumTime
145145
rumConfig.trackBackgroundEvents = .mockRandom() // no matter BET state
146-
146+
147147
// When
148148
rumTime.now = sdkInitTime
149149
RUM.enable(with: rumConfig, in: core)
150-
150+
151151
rumTime.now = firstRUMTime
152152
RUMMonitor.shared(in: core).startView(key: "key", name: "FirstView")
153-
153+
154154
// Then
155155
let session = try RUMSessionMatcher
156156
.groupMatchersBySessions(try core.waitAndReturnRUMEventMatchers())
157157
.takeSingle()
158158

159159
XCTAssertEqual(session.views.count, 1)
160160
XCTAssertTrue(try session.has(sessionPrecondition: .backgroundLaunch), "Session must be marked as 'background launch'")
161-
161+
162162
let firstView = try XCTUnwrap(session.views.first)
163163
XCTAssertFalse(firstView.isApplicationLaunchView(), "Session should not begin with 'app launch' view")
164164
XCTAssertEqual(firstView.name, "FirstView")

DatadogInternal/Sources/NetworkInstrumentation/NetworkInstrumentationFeature.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ internal final class NetworkInstrumentationFeature: DatadogFeature {
9292
interceptDidFinishCollecting: { [weak self] session, task, metrics in
9393
self?.task(task, didFinishCollecting: metrics)
9494

95-
if #available(iOS 15, tvOS 15, *) {
95+
if #available(iOS 15, tvOS 15, *), !task.dd.hasCompletion {
9696
// iOS 15 and above, didCompleteWithError is not called hence we use task state to detect task completion
9797
// while prior to iOS 15, task state doesn't change to completed hence we use didCompleteWithError to detect task completion
9898
self?.task(task, didCompleteWithError: task.error)
@@ -113,6 +113,8 @@ internal final class NetworkInstrumentationFeature: DatadogFeature {
113113
try swizzler.swizzle(
114114
interceptCompletionHandler: { [weak self] task, _, error in
115115
self?.task(task, didCompleteWithError: error)
116+
}, didReceive: { [weak self] task, data in
117+
self?.task(task, didReceive: data)
116118
}
117119
)
118120
}

DatadogInternal/Sources/NetworkInstrumentation/URLSession/DatadogURLSessionDelegate.swift

+1
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ open class DatadogURLSessionDelegate: NSObject, URLSessionDataDelegate {
139139
try swizzler.swizzle(
140140
interceptCompletionHandler: { [weak self] task, _, error in
141141
self?.interceptor?.task(task, didCompleteWithError: error)
142+
}, didReceive: { _, _ in
142143
}
143144
)
144145
} catch {

DatadogInternal/Sources/NetworkInstrumentation/URLSession/NetworkInstrumentationSwizzler.swift

+6-2
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ internal final class NetworkInstrumentationSwizzler {
2323

2424
/// Swizzles `URLSession.dataTask(with:completionHandler:)` methods (with `URL` and `URLRequest`).
2525
func swizzle(
26-
interceptCompletionHandler: @escaping (URLSessionTask, Data?, Error?) -> Void
26+
interceptCompletionHandler: @escaping (URLSessionTask, Data?, Error?) -> Void,
27+
didReceive: @escaping (URLSessionTask, Data) -> Void
2728
) throws {
28-
try urlSessionSwizzler.swizzle(interceptCompletionHandler: interceptCompletionHandler)
29+
try urlSessionSwizzler.swizzle(
30+
interceptCompletionHandler: interceptCompletionHandler,
31+
didReceive: didReceive
32+
)
2933
}
3034

3135
/// Swizzles `URLSessionTask.resume()` method.

DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionSwizzler.swift

+25-6
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,26 @@ internal final class URLSessionSwizzler {
1818

1919
/// Swizzles `URLSession.dataTask(with:completionHandler:)` methods (with `URL` and `URLRequest`).
2020
func swizzle(
21-
interceptCompletionHandler: @escaping (URLSessionTask, Data?, Error?) -> Void
21+
interceptCompletionHandler: @escaping (URLSessionTask, Data?, Error?) -> Void,
22+
didReceive: @escaping (URLSessionTask, Data) -> Void
2223
) throws {
2324
lock.lock()
2425
defer { lock.unlock() }
2526
dataTaskURLRequestCompletionHandler = try DataTaskURLRequestCompletionHandler.build()
26-
dataTaskURLRequestCompletionHandler?.swizzle(interceptCompletion: interceptCompletionHandler)
27+
dataTaskURLRequestCompletionHandler?.swizzle(
28+
interceptCompletion: interceptCompletionHandler,
29+
didReceive: didReceive
30+
)
2731

2832
if #available(iOS 13.0, *) {
2933
// Prior to iOS 13.0 the `URLSession.dataTask(with:url, completionHandler:handler)` makes an internal
3034
// call to `URLSession.dataTask(with:request, completionHandler:handler)`. To avoid duplicated call
3135
// to the callback, we don't apply below swizzling prior to iOS 13.
3236
dataTaskURLCompletionHandler = try DataTaskURLCompletionHandler.build()
33-
dataTaskURLCompletionHandler?.swizzle(interceptCompletion: interceptCompletionHandler)
37+
dataTaskURLCompletionHandler?.swizzle(
38+
interceptCompletion: interceptCompletionHandler,
39+
didReceive: didReceive
40+
)
3441
}
3542
}
3643

@@ -71,7 +78,8 @@ internal final class URLSessionSwizzler {
7178
}
7279

7380
func swizzle(
74-
interceptCompletion: @escaping (URLSessionTask, Data?, Error?) -> Void
81+
interceptCompletion: @escaping (URLSessionTask, Data?, Error?) -> Void,
82+
didReceive: @escaping (URLSessionTask, Data) -> Void
7583
) {
7684
typealias Signature = @convention(block) (URLSession, URLRequest, CompletionHandler?) -> URLSessionDataTask
7785
swizzle(method) { previousImplementation -> Signature in
@@ -87,13 +95,18 @@ internal final class URLSessionSwizzler {
8795

8896
var _task: URLSessionDataTask?
8997
let task = previousImplementation(session, Self.selector, request) { data, response, error in
90-
completionHandler(data, response, error)
98+
if let task = _task, let data = data {
99+
didReceive(task, data)
100+
}
91101

92102
if let task = _task { // sanity check, should always succeed
93103
interceptCompletion(task, data, error)
94104
}
105+
106+
completionHandler(data, response, error)
95107
}
96108
_task = task
109+
_task?.dd.hasCompletion = true
97110
return task
98111
}
99112
}
@@ -121,7 +134,8 @@ internal final class URLSessionSwizzler {
121134
}
122135

123136
func swizzle(
124-
interceptCompletion: @escaping (URLSessionTask, Data?, Error?) -> Void
137+
interceptCompletion: @escaping (URLSessionTask, Data?, Error?) -> Void,
138+
didReceive: @escaping (URLSessionTask, Data) -> Void
125139
) {
126140
typealias Signature = @convention(block) (URLSession, URL, CompletionHandler?) -> URLSessionDataTask
127141
swizzle(method) { previousImplementation -> Signature in
@@ -137,13 +151,18 @@ internal final class URLSessionSwizzler {
137151

138152
var _task: URLSessionDataTask?
139153
let task = previousImplementation(session, Self.selector, url) { data, response, error in
154+
if let task = _task, let data = data {
155+
didReceive(task, data)
156+
}
157+
140158
completionHandler(data, response, error)
141159

142160
if let task = _task { // sanity check, should always succeed
143161
interceptCompletion(task, data, error)
144162
}
145163
}
146164
_task = task
165+
_task?.dd.hasCompletion = true
147166
return task
148167
}
149168
}

DatadogInternal/Sources/NetworkInstrumentation/URLSession/URLSessionTask+Tracking.swift

+16
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,20 @@ extension DatadogExtension where ExtendedType: URLSessionTask {
3333

3434
return session.delegate
3535
}
36+
37+
var hasCompletion: Bool {
38+
get {
39+
let value = objc_getAssociatedObject(type, &hasCompletionKey) as? Bool
40+
return value == true
41+
}
42+
set {
43+
if newValue {
44+
objc_setAssociatedObject(type, &hasCompletionKey, true, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
45+
} else {
46+
objc_setAssociatedObject(type, &hasCompletionKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
47+
}
48+
}
49+
}
3650
}
51+
52+
private var hasCompletionKey: Void?

DatadogInternal/Tests/NetworkInstrumentation/URLSessionSwizzlerTests.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import XCTest
1010

1111
class URLSessionSwizzlerTests: XCTestCase {
1212
func testSwizzling_dataTaskWithCompletion() throws {
13+
let didReceive = expectation(description: "didReceive")
14+
didReceive.expectedFulfillmentCount = 2
15+
1316
let didInterceptCompletion = expectation(description: "interceptCompletion")
1417
didInterceptCompletion.expectedFulfillmentCount = 2
1518

@@ -18,6 +21,8 @@ class URLSessionSwizzlerTests: XCTestCase {
1821
try swizzler.swizzle(
1922
interceptCompletionHandler: { _, _, _ in
2023
didInterceptCompletion.fulfill()
24+
}, didReceive: { _, _ in
25+
didReceive.fulfill()
2126
}
2227
)
2328

@@ -30,6 +35,6 @@ class URLSessionSwizzlerTests: XCTestCase {
3035
session.dataTask(with: url) { _, _, _ in }.resume() // not intercepted
3136
session.dataTask(with: URLRequest(url: url)) { _, _, _ in }.resume() // not intercepted
3237

33-
wait(for: [didInterceptCompletion], timeout: 5)
38+
wait(for: [didReceive, didInterceptCompletion], timeout: 5)
3439
}
3540
}

DatadogRUM/Sources/RUMConfiguration.swift

+2
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,8 @@ extension RUM {
221221
/// attributes for RUM resource based on the provided request, response, data, and error.
222222
/// Keep the implementation fast and do not make any assumptions on the thread used to run it.
223223
///
224+
/// Note: This is not supported for async-await APIs.
225+
///
224226
/// Default: `nil`.
225227
public var resourceAttributesProvider: RUM.ResourceAttributesProvider?
226228

0 commit comments

Comments
 (0)