@rejourneyco/react-native 1.0.8 → 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.
Files changed (42) hide show
  1. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +89 -8
  2. package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +18 -3
  3. package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +69 -17
  4. package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +27 -2
  5. package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +3 -1
  6. package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +100 -0
  7. package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +222 -145
  8. package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +4 -0
  9. package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +3 -0
  10. package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +13 -0
  11. package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +8 -0
  12. package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +95 -21
  13. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +14 -0
  14. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +18 -0
  15. package/ios/Engine/DeviceRegistrar.swift +13 -3
  16. package/ios/Engine/RejourneyImpl.swift +199 -115
  17. package/ios/Recording/AnrSentinel.swift +58 -25
  18. package/ios/Recording/InteractionRecorder.swift +1 -0
  19. package/ios/Recording/RejourneyURLProtocol.swift +168 -0
  20. package/ios/Recording/ReplayOrchestrator.swift +204 -143
  21. package/ios/Recording/SegmentDispatcher.swift +8 -0
  22. package/ios/Recording/StabilityMonitor.swift +40 -32
  23. package/ios/Recording/TelemetryPipeline.swift +17 -0
  24. package/ios/Recording/ViewHierarchyScanner.swift +1 -0
  25. package/ios/Recording/VisualCapture.swift +54 -8
  26. package/ios/Rejourney.mm +27 -8
  27. package/ios/Utility/ImageBlur.swift +0 -1
  28. package/lib/commonjs/index.js +28 -15
  29. package/lib/commonjs/sdk/autoTracking.js +162 -11
  30. package/lib/commonjs/sdk/networkInterceptor.js +84 -4
  31. package/lib/module/index.js +28 -15
  32. package/lib/module/sdk/autoTracking.js +162 -11
  33. package/lib/module/sdk/networkInterceptor.js +84 -4
  34. package/lib/typescript/NativeRejourney.d.ts +5 -2
  35. package/lib/typescript/sdk/autoTracking.d.ts +3 -1
  36. package/lib/typescript/types/index.d.ts +14 -2
  37. package/package.json +4 -4
  38. package/src/NativeRejourney.ts +8 -5
  39. package/src/index.ts +37 -19
  40. package/src/sdk/autoTracking.ts +176 -11
  41. package/src/sdk/networkInterceptor.ts +110 -1
  42. package/src/types/index.ts +15 -3
@@ -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,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
+ }