@rejourneyco/react-native 1.0.8 → 1.0.10

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.
Files changed (52) hide show
  1. package/README.md +77 -3
  2. package/android/src/main/AndroidManifest.xml +6 -0
  3. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +143 -8
  4. package/android/src/main/java/com/rejourney/RejourneyOkHttpInitProvider.kt +68 -0
  5. package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +21 -3
  6. package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +69 -17
  7. package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +27 -2
  8. package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +3 -1
  9. package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +93 -0
  10. package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +226 -146
  11. package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +7 -0
  12. package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +3 -0
  13. package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +39 -0
  14. package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +8 -0
  15. package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +95 -21
  16. package/android/src/main/java/com/rejourney/utility/DataCompression.kt +14 -2
  17. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +14 -0
  18. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +18 -0
  19. package/ios/Engine/DeviceRegistrar.swift +13 -3
  20. package/ios/Engine/RejourneyImpl.swift +204 -115
  21. package/ios/Recording/AnrSentinel.swift +58 -25
  22. package/ios/Recording/InteractionRecorder.swift +1 -0
  23. package/ios/Recording/RejourneyURLProtocol.swift +216 -0
  24. package/ios/Recording/ReplayOrchestrator.swift +207 -144
  25. package/ios/Recording/SegmentDispatcher.swift +8 -0
  26. package/ios/Recording/StabilityMonitor.swift +40 -32
  27. package/ios/Recording/TelemetryPipeline.swift +45 -2
  28. package/ios/Recording/ViewHierarchyScanner.swift +1 -0
  29. package/ios/Recording/VisualCapture.swift +79 -29
  30. package/ios/Rejourney.mm +27 -8
  31. package/ios/Utility/DataCompression.swift +2 -2
  32. package/ios/Utility/ImageBlur.swift +0 -1
  33. package/lib/commonjs/expoRouterTracking.js +137 -0
  34. package/lib/commonjs/index.js +204 -34
  35. package/lib/commonjs/sdk/autoTracking.js +262 -100
  36. package/lib/commonjs/sdk/networkInterceptor.js +84 -4
  37. package/lib/module/expoRouterTracking.js +135 -0
  38. package/lib/module/index.js +203 -28
  39. package/lib/module/sdk/autoTracking.js +260 -100
  40. package/lib/module/sdk/networkInterceptor.js +84 -4
  41. package/lib/typescript/NativeRejourney.d.ts +5 -2
  42. package/lib/typescript/expoRouterTracking.d.ts +14 -0
  43. package/lib/typescript/index.d.ts +2 -2
  44. package/lib/typescript/sdk/autoTracking.d.ts +14 -1
  45. package/lib/typescript/types/index.d.ts +56 -5
  46. package/package.json +23 -3
  47. package/src/NativeRejourney.ts +8 -5
  48. package/src/expoRouterTracking.ts +167 -0
  49. package/src/index.ts +221 -35
  50. package/src/sdk/autoTracking.ts +286 -114
  51. package/src/sdk/networkInterceptor.ts +110 -1
  52. package/src/types/index.ts +58 -6
@@ -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 delta = Date().timeIntervalSince1970 - last
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
  }
@@ -101,6 +101,7 @@ public final class InteractionRecorder: NSObject {
101
101
  for touch in touches {
102
102
  switch touch.phase {
103
103
  case .began:
104
+ VisualCapture.shared.invalidateMaskCache()
104
105
  SpecialCases.shared.notifyTouchBegan()
105
106
  case .ended, .cancelled:
106
107
  SpecialCases.shared.notifyTouchEnded()
@@ -0,0 +1,216 @@
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
+
42
+ // Swizzle URLSessionConfiguration.protocolClasses to automatically inject our protocol
43
+ // into custom sessions (e.g. used by SDWebImage, AlamoFire, etc.)
44
+ swizzleProtocolClasses()
45
+ }
46
+
47
+ private static var isSwizzled = false
48
+
49
+ /// Store the original IMP so we can call through to it safely.
50
+ private static var originalProtocolClassesIMP: IMP?
51
+
52
+ private static func swizzleProtocolClasses() {
53
+ guard !isSwizzled else { return }
54
+
55
+ let configClass: AnyClass = URLSessionConfiguration.self
56
+ let originalSel = #selector(getter: URLSessionConfiguration.protocolClasses)
57
+ let swizzledSel = #selector(RejourneyURLProtocol.rj_protocolClasses)
58
+
59
+ guard let originalMethod = class_getInstanceMethod(configClass, originalSel),
60
+ let swizzledMethod = class_getInstanceMethod(RejourneyURLProtocol.self, swizzledSel) else {
61
+ return
62
+ }
63
+
64
+ // Add the swizzled method onto URLSessionConfiguration itself so that
65
+ // method_exchangeImplementations works within a single class.
66
+ let didAdd = class_addMethod(
67
+ configClass,
68
+ swizzledSel,
69
+ method_getImplementation(swizzledMethod),
70
+ method_getTypeEncoding(swizzledMethod)
71
+ )
72
+
73
+ if didAdd, let addedMethod = class_getInstanceMethod(configClass, swizzledSel) {
74
+ originalProtocolClassesIMP = method_getImplementation(originalMethod)
75
+ method_exchangeImplementations(originalMethod, addedMethod)
76
+ }
77
+
78
+ isSwizzled = true
79
+ }
80
+
81
+ /// Replacement getter injected into URLSessionConfiguration.
82
+ /// After exchange, `self` IS a URLSessionConfiguration instance.
83
+ @objc private func rj_protocolClasses() -> [AnyClass]? {
84
+ // Call through to the original implementation via the saved IMP
85
+ typealias OriginalFunc = @convention(c) (AnyObject, Selector) -> [AnyClass]?
86
+ var classes: [AnyClass] = []
87
+
88
+ if let imp = RejourneyURLProtocol.originalProtocolClassesIMP {
89
+ let original = unsafeBitCast(imp, to: OriginalFunc.self)
90
+ classes = original(self, #selector(getter: URLSessionConfiguration.protocolClasses)) ?? []
91
+ }
92
+
93
+ // Inject our protocol at the beginning if not already present
94
+ if !classes.contains(where: { $0 == RejourneyURLProtocol.self }) {
95
+ classes.insert(RejourneyURLProtocol.self, at: 0)
96
+ }
97
+
98
+ return classes
99
+ }
100
+
101
+
102
+ @objc public static func disable() {
103
+ URLProtocol.unregisterClass(RejourneyURLProtocol.self)
104
+ }
105
+
106
+ public override class func canInit(with request: URLRequest) -> Bool {
107
+ guard let url = request.url,
108
+ let scheme = url.scheme,
109
+ ["http", "https"].contains(scheme) else {
110
+ return false
111
+ }
112
+
113
+ // Prevent infinite loop by not intercepting our own forwarded requests
114
+ if URLProtocol.property(forKey: RejourneyURLProtocol._handledKey, in: request) != nil {
115
+ return false
116
+ }
117
+
118
+ return true
119
+ }
120
+
121
+ public override class func canonicalRequest(for request: URLRequest) -> URLRequest {
122
+ return request
123
+ }
124
+
125
+ public override func startLoading() {
126
+ guard let request = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else {
127
+ return
128
+ }
129
+
130
+ URLProtocol.setProperty(true, forKey: RejourneyURLProtocol._handledKey, in: request)
131
+
132
+ _startMs = Int64(Date().timeIntervalSince1970 * 1000)
133
+ _dataTask = _session.dataTask(with: request as URLRequest)
134
+ _dataTask?.resume()
135
+ }
136
+
137
+ public override func stopLoading() {
138
+ _dataTask?.cancel()
139
+ _dataTask = nil
140
+ }
141
+
142
+ // MARK: - URLSessionDataDelegate
143
+
144
+ public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
145
+ client?.urlProtocol(self, didLoad: data)
146
+ }
147
+
148
+ public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
149
+ _response = response
150
+ client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)
151
+ completionHandler(.allow)
152
+ }
153
+
154
+ // MARK: - URLSessionTaskDelegate
155
+
156
+ public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
157
+ _endMs = Int64(Date().timeIntervalSince1970 * 1000)
158
+ _error = error
159
+
160
+ if let error = error {
161
+ client?.urlProtocol(self, didFailWithError: error)
162
+ } else {
163
+ client?.urlProtocolDidFinishLoading(self)
164
+ }
165
+
166
+ _logRequest(task: task)
167
+ }
168
+
169
+ private func _logRequest(task: URLSessionTask) {
170
+ guard let req = task.originalRequest, let url = req.url else { return }
171
+
172
+ let duration = _endMs - _startMs
173
+ let isSuccess = _error == nil && ((_response as? HTTPURLResponse)?.statusCode ?? 0) < 400
174
+ let statusCode = (_response as? HTTPURLResponse)?.statusCode ?? 0
175
+ let method = req.httpMethod ?? "GET"
176
+
177
+ var urlStr = url.absoluteString
178
+ if urlStr.count > 300 {
179
+ urlStr = String(urlStr.prefix(300))
180
+ }
181
+
182
+ let pathStr = url.path
183
+ let reqSize = req.httpBody?.count ?? 0
184
+ let resSize = task.countOfBytesReceived
185
+
186
+ var event: [String: Any] = [
187
+ "requestId": "n_\(UUID().uuidString)",
188
+ "method": method,
189
+ "url": urlStr,
190
+ "urlPath": pathStr.isEmpty ? "/" : pathStr,
191
+ "urlHost": url.host ?? "",
192
+ "statusCode": statusCode,
193
+ "duration": duration,
194
+ "startTimestamp": _startMs,
195
+ "endTimestamp": _endMs,
196
+ "success": isSuccess
197
+ ]
198
+
199
+ if reqSize > 0 { event["requestBodySize"] = reqSize }
200
+ if resSize > 0 { event["responseBodySize"] = resSize }
201
+
202
+ if let cType = req.value(forHTTPHeaderField: "Content-Type") {
203
+ event["requestContentType"] = cType
204
+ }
205
+
206
+ if let hr = _response as? HTTPURLResponse, let cType = hr.value(forHTTPHeaderField: "Content-Type") {
207
+ event["responseContentType"] = cType
208
+ }
209
+
210
+ if let e = _error {
211
+ event["errorMessage"] = e.localizedDescription
212
+ }
213
+
214
+ TelemetryPipeline.shared.recordNetworkEvent(details: event)
215
+ }
216
+ }