@rejourneyco/react-native 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +109 -26
- package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +18 -3
- package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +69 -17
- package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +27 -2
- package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +30 -0
- package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +100 -0
- package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +260 -174
- package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +246 -34
- package/android/src/main/java/com/rejourney/recording/SpecialCases.kt +572 -0
- package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +3 -0
- package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +19 -4
- package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +8 -0
- package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +251 -85
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +14 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +18 -0
- package/ios/Engine/DeviceRegistrar.swift +13 -3
- package/ios/Engine/RejourneyImpl.swift +202 -133
- package/ios/Recording/AnrSentinel.swift +58 -25
- package/ios/Recording/InteractionRecorder.swift +29 -0
- package/ios/Recording/RejourneyURLProtocol.swift +168 -0
- package/ios/Recording/ReplayOrchestrator.swift +241 -147
- package/ios/Recording/SegmentDispatcher.swift +155 -13
- package/ios/Recording/SpecialCases.swift +614 -0
- package/ios/Recording/StabilityMonitor.swift +42 -34
- package/ios/Recording/TelemetryPipeline.swift +38 -3
- package/ios/Recording/ViewHierarchyScanner.swift +1 -0
- package/ios/Recording/VisualCapture.swift +104 -28
- package/ios/Rejourney.mm +27 -8
- package/ios/Utility/ImageBlur.swift +0 -1
- package/lib/commonjs/index.js +32 -20
- package/lib/commonjs/sdk/autoTracking.js +162 -11
- package/lib/commonjs/sdk/constants.js +2 -2
- package/lib/commonjs/sdk/networkInterceptor.js +84 -4
- package/lib/commonjs/sdk/utils.js +1 -1
- package/lib/module/index.js +32 -20
- package/lib/module/sdk/autoTracking.js +162 -11
- package/lib/module/sdk/constants.js +2 -2
- package/lib/module/sdk/networkInterceptor.js +84 -4
- package/lib/module/sdk/utils.js +1 -1
- package/lib/typescript/NativeRejourney.d.ts +5 -2
- package/lib/typescript/sdk/autoTracking.d.ts +3 -1
- package/lib/typescript/sdk/constants.d.ts +2 -2
- package/lib/typescript/types/index.d.ts +15 -8
- package/package.json +4 -4
- package/src/NativeRejourney.ts +8 -5
- package/src/index.ts +46 -29
- package/src/sdk/autoTracking.ts +176 -11
- package/src/sdk/constants.ts +2 -2
- package/src/sdk/networkInterceptor.ts +110 -1
- package/src/sdk/utils.ts +1 -1
- package/src/types/index.ts +16 -9
|
@@ -18,64 +18,64 @@ import Foundation
|
|
|
18
18
|
|
|
19
19
|
@objc(AnrSentinel)
|
|
20
20
|
public final class AnrSentinel: NSObject {
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
@objc public static let shared = AnrSentinel()
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
private let _freezeThreshold: TimeInterval = 5.0
|
|
25
25
|
private let _pollFrequency: TimeInterval = 2.0
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
private var _watchThread: Thread?
|
|
28
28
|
private var _volatile = VolatileState()
|
|
29
29
|
private let _stateLock = os_unfair_lock_t.allocate(capacity: 1)
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
private override init() {
|
|
32
32
|
_stateLock.initialize(to: os_unfair_lock())
|
|
33
33
|
super.init()
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
deinit {
|
|
37
37
|
_stateLock.deallocate()
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
@objc public func activate() {
|
|
41
41
|
os_unfair_lock_lock(_stateLock)
|
|
42
42
|
guard _watchThread == nil else {
|
|
43
43
|
os_unfair_lock_unlock(_stateLock)
|
|
44
44
|
return
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
_volatile.running = true
|
|
48
48
|
_volatile.lastResponse = Date().timeIntervalSince1970
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
let t = Thread { [weak self] in self?._watchLoop() }
|
|
51
51
|
t.name = "co.rejourney.anr"
|
|
52
52
|
t.qualityOfService = .utility
|
|
53
53
|
_watchThread = t
|
|
54
54
|
os_unfair_lock_unlock(_stateLock)
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
t.start()
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
@objc public func halt() {
|
|
60
60
|
os_unfair_lock_lock(_stateLock)
|
|
61
61
|
_volatile.running = false
|
|
62
62
|
_watchThread = nil
|
|
63
63
|
os_unfair_lock_unlock(_stateLock)
|
|
64
64
|
}
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
private func _watchLoop() {
|
|
67
67
|
while true {
|
|
68
68
|
os_unfair_lock_lock(_stateLock)
|
|
69
69
|
let running = _volatile.running
|
|
70
70
|
os_unfair_lock_unlock(_stateLock)
|
|
71
71
|
guard running else { break }
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
_sendPing()
|
|
74
74
|
Thread.sleep(forTimeInterval: _pollFrequency)
|
|
75
75
|
_checkPong()
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
private func _sendPing() {
|
|
80
80
|
os_unfair_lock_lock(_stateLock)
|
|
81
81
|
if _volatile.awaitingPong {
|
|
@@ -84,7 +84,7 @@ public final class AnrSentinel: NSObject {
|
|
|
84
84
|
}
|
|
85
85
|
_volatile.awaitingPong = true
|
|
86
86
|
os_unfair_lock_unlock(_stateLock)
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
DispatchQueue.main.async { [weak self] in
|
|
89
89
|
guard let self else { return }
|
|
90
90
|
os_unfair_lock_lock(self._stateLock)
|
|
@@ -93,30 +93,62 @@ public final class AnrSentinel: NSObject {
|
|
|
93
93
|
os_unfair_lock_unlock(self._stateLock)
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
private func _checkPong() {
|
|
98
98
|
os_unfair_lock_lock(_stateLock)
|
|
99
99
|
let awaiting = _volatile.awaitingPong
|
|
100
100
|
let last = _volatile.lastResponse
|
|
101
|
+
let lastReportedAt = _volatile.lastAnrReport
|
|
101
102
|
os_unfair_lock_unlock(_stateLock)
|
|
102
|
-
|
|
103
|
+
|
|
103
104
|
guard awaiting else { return }
|
|
104
|
-
|
|
105
|
-
let
|
|
105
|
+
|
|
106
|
+
let now = Date().timeIntervalSince1970
|
|
107
|
+
let delta = now - last
|
|
106
108
|
if delta >= _freezeThreshold {
|
|
109
|
+
// Avoid spamming duplicate ANRs while one long freeze persists.
|
|
110
|
+
if now - lastReportedAt < _freezeThreshold {
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
os_unfair_lock_lock(_stateLock)
|
|
115
|
+
_volatile.lastAnrReport = now
|
|
116
|
+
_volatile.lastResponse = now
|
|
117
|
+
_volatile.awaitingPong = false
|
|
118
|
+
os_unfair_lock_unlock(_stateLock)
|
|
119
|
+
|
|
107
120
|
_reportFreeze(duration: delta)
|
|
108
121
|
}
|
|
109
122
|
}
|
|
110
|
-
|
|
123
|
+
|
|
111
124
|
private func _reportFreeze(duration: TimeInterval) {
|
|
112
125
|
DiagnosticLog.emit(.caution, "Main thread frozen for \(String(format: "%.1f", duration))s")
|
|
113
|
-
|
|
126
|
+
|
|
114
127
|
ReplayOrchestrator.shared.incrementStalledTally()
|
|
115
|
-
|
|
128
|
+
|
|
116
129
|
let trace = Thread.callStackSymbols.joined(separator: "\n")
|
|
117
130
|
let ms = Int(duration * 1000)
|
|
118
|
-
|
|
131
|
+
|
|
119
132
|
TelemetryPipeline.shared.recordAnrEvent(durationMs: ms, stack: trace)
|
|
133
|
+
|
|
134
|
+
// Persist ANR incident and send through /api/ingest/fault so ANRs survive
|
|
135
|
+
// process termination/background upload loss, similar to crash recovery.
|
|
136
|
+
let incident = IncidentRecord(
|
|
137
|
+
sessionId: StabilityMonitor.shared.currentSessionId ?? ReplayOrchestrator.shared.replayId ?? "unknown",
|
|
138
|
+
timestampMs: UInt64(Date().timeIntervalSince1970 * 1000),
|
|
139
|
+
category: "anr",
|
|
140
|
+
identifier: "MainThreadFrozen",
|
|
141
|
+
detail: "Main thread unresponsive for \(ms)ms",
|
|
142
|
+
frames: trace
|
|
143
|
+
.split(separator: "\n")
|
|
144
|
+
.map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) },
|
|
145
|
+
context: [
|
|
146
|
+
"durationMs": String(ms),
|
|
147
|
+
"threadState": "blocked"
|
|
148
|
+
]
|
|
149
|
+
)
|
|
150
|
+
StabilityMonitor.shared.persistIncidentSync(incident)
|
|
151
|
+
StabilityMonitor.shared.transmitStoredReport()
|
|
120
152
|
}
|
|
121
153
|
}
|
|
122
154
|
|
|
@@ -124,18 +156,19 @@ private struct VolatileState {
|
|
|
124
156
|
var running = false
|
|
125
157
|
var awaitingPong = false
|
|
126
158
|
var lastResponse: TimeInterval = 0
|
|
159
|
+
var lastAnrReport: TimeInterval = 0
|
|
127
160
|
}
|
|
128
161
|
|
|
129
162
|
@objc(ResponsivenessWatcher)
|
|
130
163
|
public final class ResponsivenessWatcher: NSObject {
|
|
131
164
|
@objc public static let shared = ResponsivenessWatcher()
|
|
132
|
-
|
|
165
|
+
|
|
133
166
|
private override init() { super.init() }
|
|
134
|
-
|
|
167
|
+
|
|
135
168
|
@objc public func activate() {
|
|
136
169
|
AnrSentinel.shared.activate()
|
|
137
170
|
}
|
|
138
|
-
|
|
171
|
+
|
|
139
172
|
@objc public func halt() {
|
|
140
173
|
AnrSentinel.shared.halt()
|
|
141
174
|
}
|
|
@@ -28,6 +28,7 @@ public final class InteractionRecorder: NSObject {
|
|
|
28
28
|
private var _inputObservers = NSMapTable<UITextField, AnyObject>.weakToStrongObjects()
|
|
29
29
|
private var _navigationStack: [String] = []
|
|
30
30
|
private let _coalesceWindow: TimeInterval = 0.3
|
|
31
|
+
private var _lastInteractionTimestampMs: UInt64 = 0
|
|
31
32
|
|
|
32
33
|
private override init() {
|
|
33
34
|
super.init()
|
|
@@ -48,6 +49,11 @@ public final class InteractionRecorder: NSObject {
|
|
|
48
49
|
_gestureAggregator = nil
|
|
49
50
|
_inputObservers.removeAllObjects()
|
|
50
51
|
_navigationStack.removeAll()
|
|
52
|
+
_lastInteractionTimestampMs = 0
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@objc public func latestInteractionTimestampMs() -> UInt64 {
|
|
56
|
+
_lastInteractionTimestampMs
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
@objc public func observeTextField(_ field: UITextField) {
|
|
@@ -88,6 +94,22 @@ public final class InteractionRecorder: NSObject {
|
|
|
88
94
|
@objc public func processRawTouches(_ event: UIEvent, in window: UIWindow) {
|
|
89
95
|
guard isTracking, let agg = _gestureAggregator else { return }
|
|
90
96
|
guard let touches = event.allTouches else { return }
|
|
97
|
+
_lastInteractionTimestampMs = UInt64(Date().timeIntervalSince1970 * 1000)
|
|
98
|
+
|
|
99
|
+
// Notify SpecialCases about touch phases for touch-based map idle detection
|
|
100
|
+
// (used by Mapbox v10+ where SDK idle callbacks can't be hooked).
|
|
101
|
+
for touch in touches {
|
|
102
|
+
switch touch.phase {
|
|
103
|
+
case .began:
|
|
104
|
+
VisualCapture.shared.invalidateMaskCache()
|
|
105
|
+
SpecialCases.shared.notifyTouchBegan()
|
|
106
|
+
case .ended, .cancelled:
|
|
107
|
+
SpecialCases.shared.notifyTouchEnded()
|
|
108
|
+
default:
|
|
109
|
+
break
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
91
113
|
for touch in touches {
|
|
92
114
|
agg.processTouch(touch, in: window)
|
|
93
115
|
}
|
|
@@ -304,6 +326,13 @@ private final class GestureAggregator: NSObject {
|
|
|
304
326
|
}
|
|
305
327
|
|
|
306
328
|
private func _resolveTarget(at point: CGPoint, in window: UIWindow) -> (label: String, isInteractive: Bool) {
|
|
329
|
+
// When a map view is visible, skip hitTest entirely — performing
|
|
330
|
+
// hitTest on a deep Metal/OpenGL map hierarchy is expensive and
|
|
331
|
+
// causes micro-stutter during pan/zoom gestures.
|
|
332
|
+
if SpecialCases.shared.mapVisible {
|
|
333
|
+
return ("map", false)
|
|
334
|
+
}
|
|
335
|
+
|
|
307
336
|
guard let hit = window.hitTest(point, with: nil) else { return ("window", false) }
|
|
308
337
|
|
|
309
338
|
let label = hit.accessibilityIdentifier ?? hit.accessibilityLabel ?? String(describing: type(of: hit))
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Rejourney
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import Foundation
|
|
18
|
+
|
|
19
|
+
/// Intercepts URLSession network traffic globally for Rejourney Session Replay.
|
|
20
|
+
@objc(RejourneyURLProtocol)
|
|
21
|
+
public class RejourneyURLProtocol: URLProtocol, URLSessionDataDelegate, URLSessionTaskDelegate {
|
|
22
|
+
|
|
23
|
+
// We tag requests that we've already handled so we don't intercept them repeatedly.
|
|
24
|
+
private static let _handledKey = "co.rejourney.handled"
|
|
25
|
+
|
|
26
|
+
private var _dataTask: URLSessionDataTask?
|
|
27
|
+
private var _startMs: Int64 = 0
|
|
28
|
+
private var _endMs: Int64 = 0
|
|
29
|
+
private var _responseData: Data?
|
|
30
|
+
private var _response: URLResponse?
|
|
31
|
+
private var _error: Error?
|
|
32
|
+
|
|
33
|
+
// Session used to forward the intercepted request execution
|
|
34
|
+
private lazy var _session: URLSession = {
|
|
35
|
+
let config = URLSessionConfiguration.default
|
|
36
|
+
return URLSession(configuration: config, delegate: self, delegateQueue: nil)
|
|
37
|
+
}()
|
|
38
|
+
|
|
39
|
+
@objc public static func enable() {
|
|
40
|
+
URLProtocol.registerClass(RejourneyURLProtocol.self)
|
|
41
|
+
// Hook into default session configs
|
|
42
|
+
if let method = class_getInstanceMethod(URLSessionConfiguration.self, #selector(getter: URLSessionConfiguration.protocolClasses)) {
|
|
43
|
+
let original = method_getImplementation(method)
|
|
44
|
+
// Note: Safest swizzling approach for URLSessionConfiguration here is complex,
|
|
45
|
+
// standard approach is registering the protocol which covers shared sessions and simple setups.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@objc public static func disable() {
|
|
50
|
+
URLProtocol.unregisterClass(RejourneyURLProtocol.self)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public override class func canInit(with request: URLRequest) -> Bool {
|
|
54
|
+
guard let url = request.url,
|
|
55
|
+
let scheme = url.scheme,
|
|
56
|
+
["http", "https"].contains(scheme) else {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Prevent infinite loop by not intercepting our own forwarded requests
|
|
61
|
+
if URLProtocol.property(forKey: RejourneyURLProtocol._handledKey, in: request) != nil {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Ignore requests to the Rejourney API endpoints themselves to prevent ingestion duplication
|
|
66
|
+
if let host = url.host, host.contains("api.rejourney.co") {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return true
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public override class func canonicalRequest(for request: URLRequest) -> URLRequest {
|
|
74
|
+
return request
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public override func startLoading() {
|
|
78
|
+
guard let request = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
URLProtocol.setProperty(true, forKey: RejourneyURLProtocol._handledKey, in: request)
|
|
83
|
+
|
|
84
|
+
_startMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
85
|
+
_dataTask = _session.dataTask(with: request as URLRequest)
|
|
86
|
+
_dataTask?.resume()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public override func stopLoading() {
|
|
90
|
+
_dataTask?.cancel()
|
|
91
|
+
_dataTask = nil
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// MARK: - URLSessionDataDelegate
|
|
95
|
+
|
|
96
|
+
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
|
|
97
|
+
client?.urlProtocol(self, didLoad: data)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
|
|
101
|
+
_response = response
|
|
102
|
+
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
|
|
103
|
+
completionHandler(.allow)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// MARK: - URLSessionTaskDelegate
|
|
107
|
+
|
|
108
|
+
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
|
109
|
+
_endMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
110
|
+
_error = error
|
|
111
|
+
|
|
112
|
+
if let error = error {
|
|
113
|
+
client?.urlProtocol(self, didFailWithError: error)
|
|
114
|
+
} else {
|
|
115
|
+
client?.urlProtocolDidFinishLoading(self)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_logRequest(task: task)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private func _logRequest(task: URLSessionTask) {
|
|
122
|
+
guard let req = task.originalRequest, let url = req.url else { return }
|
|
123
|
+
|
|
124
|
+
let duration = _endMs - _startMs
|
|
125
|
+
let isSuccess = _error == nil && ((_response as? HTTPURLResponse)?.statusCode ?? 0) < 400
|
|
126
|
+
let statusCode = (_response as? HTTPURLResponse)?.statusCode ?? 0
|
|
127
|
+
let method = req.httpMethod ?? "GET"
|
|
128
|
+
|
|
129
|
+
var urlStr = url.absoluteString
|
|
130
|
+
if urlStr.count > 300 {
|
|
131
|
+
urlStr = String(urlStr.prefix(300))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let pathStr = url.path
|
|
135
|
+
let reqSize = req.httpBody?.count ?? 0
|
|
136
|
+
let resSize = task.countOfBytesReceived
|
|
137
|
+
|
|
138
|
+
var event: [String: Any] = [
|
|
139
|
+
"requestId": "n_\(UUID().uuidString)",
|
|
140
|
+
"method": method,
|
|
141
|
+
"url": urlStr,
|
|
142
|
+
"urlPath": pathStr.isEmpty ? "/" : pathStr,
|
|
143
|
+
"urlHost": url.host ?? "",
|
|
144
|
+
"statusCode": statusCode,
|
|
145
|
+
"duration": duration,
|
|
146
|
+
"startTimestamp": _startMs,
|
|
147
|
+
"endTimestamp": _endMs,
|
|
148
|
+
"success": isSuccess
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
if reqSize > 0 { event["requestBodySize"] = reqSize }
|
|
152
|
+
if resSize > 0 { event["responseBodySize"] = resSize }
|
|
153
|
+
|
|
154
|
+
if let cType = req.value(forHTTPHeaderField: "Content-Type") {
|
|
155
|
+
event["requestContentType"] = cType
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if let hr = _response as? HTTPURLResponse, let cType = hr.value(forHTTPHeaderField: "Content-Type") {
|
|
159
|
+
event["responseContentType"] = cType
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if let e = _error {
|
|
163
|
+
event["errorMessage"] = e.localizedDescription
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
TelemetryPipeline.shared.recordNetworkEvent(details: event)
|
|
167
|
+
}
|
|
168
|
+
}
|