@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.
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +89 -8
- 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 +3 -1
- package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +100 -0
- package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +222 -145
- package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +4 -0
- package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +3 -0
- package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +13 -0
- package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +8 -0
- package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +95 -21
- 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 +199 -115
- package/ios/Recording/AnrSentinel.swift +58 -25
- package/ios/Recording/InteractionRecorder.swift +1 -0
- package/ios/Recording/RejourneyURLProtocol.swift +168 -0
- package/ios/Recording/ReplayOrchestrator.swift +204 -143
- package/ios/Recording/SegmentDispatcher.swift +8 -0
- package/ios/Recording/StabilityMonitor.swift +40 -32
- package/ios/Recording/TelemetryPipeline.swift +17 -0
- package/ios/Recording/ViewHierarchyScanner.swift +1 -0
- package/ios/Recording/VisualCapture.swift +54 -8
- package/ios/Rejourney.mm +27 -8
- package/ios/Utility/ImageBlur.swift +0 -1
- package/lib/commonjs/index.js +28 -15
- package/lib/commonjs/sdk/autoTracking.js +162 -11
- package/lib/commonjs/sdk/networkInterceptor.js +84 -4
- package/lib/module/index.js +28 -15
- package/lib/module/sdk/autoTracking.js +162 -11
- package/lib/module/sdk/networkInterceptor.js +84 -4
- package/lib/typescript/NativeRejourney.d.ts +5 -2
- package/lib/typescript/sdk/autoTracking.d.ts +3 -1
- package/lib/typescript/types/index.d.ts +14 -2
- package/package.json +4 -4
- package/src/NativeRejourney.ts +8 -5
- package/src/index.ts +37 -19
- package/src/sdk/autoTracking.ts +176 -11
- package/src/sdk/networkInterceptor.ts +110 -1
- package/src/types/index.ts +15 -3
|
@@ -173,6 +173,8 @@ final class SegmentDispatcher {
|
|
|
173
173
|
backgroundDurationMs: UInt64,
|
|
174
174
|
metrics: [String: Any]?,
|
|
175
175
|
currentQueueDepth: Int = 0,
|
|
176
|
+
endReason: String? = nil,
|
|
177
|
+
lifecycleVersion: Int? = nil,
|
|
176
178
|
completion: @escaping (Bool) -> Void
|
|
177
179
|
) {
|
|
178
180
|
guard let url = URL(string: "\(endpoint)/api/ingest/session/end") else {
|
|
@@ -190,6 +192,12 @@ final class SegmentDispatcher {
|
|
|
190
192
|
if backgroundDurationMs > 0 { body["totalBackgroundTimeMs"] = backgroundDurationMs }
|
|
191
193
|
if let m = metrics { body["metrics"] = m }
|
|
192
194
|
body["sdkTelemetry"] = sdkTelemetrySnapshot(currentQueueDepth: currentQueueDepth)
|
|
195
|
+
if let endReason, !endReason.isEmpty {
|
|
196
|
+
body["endReason"] = endReason
|
|
197
|
+
}
|
|
198
|
+
if let lifecycleVersion, lifecycleVersion > 0 {
|
|
199
|
+
body["lifecycleVersion"] = lifecycleVersion
|
|
200
|
+
}
|
|
193
201
|
|
|
194
202
|
do {
|
|
195
203
|
req.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
@@ -38,7 +38,7 @@ private func _rjSignalHandler(_ signum: Int32) {
|
|
|
38
38
|
case SIGTRAP: name = "SIGTRAP"
|
|
39
39
|
default: name = "SIG\(signum)"
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
let incident = IncidentRecord(
|
|
43
43
|
sessionId: StabilityMonitor.shared.currentSessionId ?? "unknown",
|
|
44
44
|
timestampMs: UInt64(Date().timeIntervalSince1970 * 1000),
|
|
@@ -52,60 +52,63 @@ private func _rjSignalHandler(_ signum: Int32) {
|
|
|
52
52
|
"priority": String(format: "%.2f", Thread.current.threadPriority)
|
|
53
53
|
]
|
|
54
54
|
)
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
ReplayOrchestrator.shared.incrementFaultTally()
|
|
57
57
|
StabilityMonitor.shared.persistIncidentSync(incident)
|
|
58
|
-
|
|
58
|
+
|
|
59
|
+
// Flush visual frames to disk for crash safety
|
|
60
|
+
VisualCapture.shared.flushToDisk()
|
|
61
|
+
|
|
59
62
|
signal(signum, SIG_DFL)
|
|
60
63
|
raise(signum)
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
@objc(StabilityMonitor)
|
|
64
67
|
public final class StabilityMonitor: NSObject {
|
|
65
|
-
|
|
68
|
+
|
|
66
69
|
@objc public static let shared = StabilityMonitor()
|
|
67
70
|
@objc public var isMonitoring = false
|
|
68
71
|
@objc public var currentSessionId: String?
|
|
69
|
-
|
|
72
|
+
|
|
70
73
|
private let _incidentStore: URL
|
|
71
74
|
private let _workerQueue = DispatchQueue(label: "co.rejourney.stability", qos: .utility)
|
|
72
|
-
|
|
75
|
+
|
|
73
76
|
private static var _chainedExceptionHandler: NSUncaughtExceptionHandler?
|
|
74
77
|
private static var _chainedSignalHandlers: [Int32: sig_t] = [:]
|
|
75
78
|
private static let _trackedSignals: [Int32] = [SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGSEGV, SIGTRAP]
|
|
76
|
-
|
|
79
|
+
|
|
77
80
|
private override init() {
|
|
78
81
|
let cache = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
|
|
79
82
|
_incidentStore = cache.appendingPathComponent("rj_incidents.json")
|
|
80
83
|
super.init()
|
|
81
84
|
}
|
|
82
|
-
|
|
85
|
+
|
|
83
86
|
@objc public func activate() {
|
|
84
87
|
guard !isMonitoring else { return }
|
|
85
88
|
isMonitoring = true
|
|
86
|
-
|
|
89
|
+
|
|
87
90
|
StabilityMonitor._chainedExceptionHandler = NSGetUncaughtExceptionHandler()
|
|
88
91
|
NSSetUncaughtExceptionHandler { ex in
|
|
89
92
|
StabilityMonitor.shared._captureException(ex)
|
|
90
93
|
StabilityMonitor._chainedExceptionHandler?(ex)
|
|
91
94
|
}
|
|
92
|
-
|
|
95
|
+
|
|
93
96
|
for sig in StabilityMonitor._trackedSignals {
|
|
94
97
|
StabilityMonitor._chainedSignalHandlers[sig] = signal(sig, _rjSignalHandler)
|
|
95
98
|
}
|
|
96
|
-
|
|
99
|
+
|
|
97
100
|
_workerQueue.async { [weak self] in
|
|
98
101
|
self?._uploadStoredIncidents()
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
|
-
|
|
104
|
+
|
|
102
105
|
@objc public func deactivate() {
|
|
103
106
|
guard isMonitoring else { return }
|
|
104
107
|
isMonitoring = false
|
|
105
|
-
|
|
108
|
+
|
|
106
109
|
NSSetUncaughtExceptionHandler(nil)
|
|
107
110
|
StabilityMonitor._chainedExceptionHandler = nil
|
|
108
|
-
|
|
111
|
+
|
|
109
112
|
for sig in StabilityMonitor._trackedSignals {
|
|
110
113
|
if let prev = StabilityMonitor._chainedSignalHandlers[sig] {
|
|
111
114
|
signal(sig, prev)
|
|
@@ -115,13 +118,13 @@ public final class StabilityMonitor: NSObject {
|
|
|
115
118
|
}
|
|
116
119
|
StabilityMonitor._chainedSignalHandlers.removeAll()
|
|
117
120
|
}
|
|
118
|
-
|
|
121
|
+
|
|
119
122
|
@objc public func transmitStoredReport() {
|
|
120
123
|
_workerQueue.async { [weak self] in
|
|
121
124
|
self?._uploadStoredIncidents()
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
|
-
|
|
127
|
+
|
|
125
128
|
private func _captureException(_ exception: NSException) {
|
|
126
129
|
let incident = IncidentRecord(
|
|
127
130
|
sessionId: currentSessionId ?? "unknown",
|
|
@@ -132,12 +135,16 @@ public final class StabilityMonitor: NSObject {
|
|
|
132
135
|
frames: _formatFrames(exception.callStackSymbols),
|
|
133
136
|
context: _captureContext()
|
|
134
137
|
)
|
|
135
|
-
|
|
138
|
+
|
|
136
139
|
ReplayOrchestrator.shared.incrementFaultTally()
|
|
137
140
|
_persistIncident(incident)
|
|
141
|
+
|
|
142
|
+
// Flush visual frames to disk for crash safety
|
|
143
|
+
VisualCapture.shared.flushToDisk()
|
|
144
|
+
|
|
138
145
|
Thread.sleep(forTimeInterval: 0.15)
|
|
139
146
|
}
|
|
140
|
-
|
|
147
|
+
|
|
141
148
|
func persistIncidentSync(_ incident: IncidentRecord) {
|
|
142
149
|
do {
|
|
143
150
|
let data = try JSONEncoder().encode(incident)
|
|
@@ -146,11 +153,11 @@ public final class StabilityMonitor: NSObject {
|
|
|
146
153
|
DiagnosticLog.fault("[StabilityMonitor] Incident persist failed: \(error)")
|
|
147
154
|
}
|
|
148
155
|
}
|
|
149
|
-
|
|
156
|
+
|
|
150
157
|
private func _formatFrames(_ raw: [String]) -> [String] {
|
|
151
158
|
raw.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
152
159
|
}
|
|
153
|
-
|
|
160
|
+
|
|
154
161
|
private func _captureContext() -> [String: String] {
|
|
155
162
|
[
|
|
156
163
|
"threadName": Thread.current.name ?? "unnamed",
|
|
@@ -158,7 +165,7 @@ public final class StabilityMonitor: NSObject {
|
|
|
158
165
|
"priority": String(format: "%.2f", Thread.current.threadPriority)
|
|
159
166
|
]
|
|
160
167
|
}
|
|
161
|
-
|
|
168
|
+
|
|
162
169
|
private func _persistIncident(_ incident: IncidentRecord) {
|
|
163
170
|
do {
|
|
164
171
|
let data = try JSONEncoder().encode(incident)
|
|
@@ -167,39 +174,40 @@ public final class StabilityMonitor: NSObject {
|
|
|
167
174
|
DiagnosticLog.fault("[StabilityMonitor] Incident persist failed: \(error)")
|
|
168
175
|
}
|
|
169
176
|
}
|
|
170
|
-
|
|
177
|
+
|
|
171
178
|
private func _uploadStoredIncidents() {
|
|
172
179
|
guard FileManager.default.fileExists(atPath: _incidentStore.path),
|
|
173
180
|
let data = try? Data(contentsOf: _incidentStore),
|
|
174
181
|
let incident = try? JSONDecoder().decode(IncidentRecord.self, from: data) else { return }
|
|
175
|
-
|
|
182
|
+
|
|
176
183
|
_transmitIncident(incident) { [weak self] ok in
|
|
177
|
-
|
|
184
|
+
guard ok, let self else { return }
|
|
185
|
+
try? FileManager.default.removeItem(at: self._incidentStore)
|
|
178
186
|
}
|
|
179
187
|
}
|
|
180
|
-
|
|
188
|
+
|
|
181
189
|
private func _transmitIncident(_ incident: IncidentRecord, completion: @escaping (Bool) -> Void) {
|
|
182
190
|
let base = SegmentDispatcher.shared.endpoint
|
|
183
191
|
guard let url = URL(string: "\(base)/api/ingest/fault") else {
|
|
184
192
|
completion(false)
|
|
185
193
|
return
|
|
186
194
|
}
|
|
187
|
-
|
|
195
|
+
|
|
188
196
|
var req = URLRequest(url: url)
|
|
189
197
|
req.httpMethod = "POST"
|
|
190
198
|
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
191
|
-
|
|
199
|
+
|
|
192
200
|
if let key = SegmentDispatcher.shared.apiToken {
|
|
193
201
|
req.setValue(key, forHTTPHeaderField: "x-rejourney-key")
|
|
194
202
|
}
|
|
195
|
-
|
|
203
|
+
|
|
196
204
|
do {
|
|
197
205
|
req.httpBody = try JSONEncoder().encode(incident)
|
|
198
206
|
} catch {
|
|
199
207
|
completion(false)
|
|
200
208
|
return
|
|
201
209
|
}
|
|
202
|
-
|
|
210
|
+
|
|
203
211
|
URLSession.shared.dataTask(with: req) { _, resp, _ in
|
|
204
212
|
let code = (resp as? HTTPURLResponse)?.statusCode ?? 0
|
|
205
213
|
completion(code >= 200 && code < 300)
|
|
@@ -210,13 +218,13 @@ public final class StabilityMonitor: NSObject {
|
|
|
210
218
|
@objc(FaultTracker)
|
|
211
219
|
public final class FaultTracker: NSObject {
|
|
212
220
|
@objc public static let shared = FaultTracker()
|
|
213
|
-
|
|
221
|
+
|
|
214
222
|
private override init() { super.init() }
|
|
215
|
-
|
|
223
|
+
|
|
216
224
|
@objc public func activate() {
|
|
217
225
|
StabilityMonitor.shared.activate()
|
|
218
226
|
}
|
|
219
|
-
|
|
227
|
+
|
|
220
228
|
@objc public func deactivate() {
|
|
221
229
|
StabilityMonitor.shared.deactivate()
|
|
222
230
|
}
|
|
@@ -306,6 +306,15 @@ public final class TelemetryPipeline: NSObject {
|
|
|
306
306
|
_enqueue(["type": "custom", "timestamp": _ts(), "name": name, "payload": payload])
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
@objc public func recordConsoleLogEvent(level: String, message: String) {
|
|
310
|
+
_enqueue([
|
|
311
|
+
"type": "log",
|
|
312
|
+
"timestamp": _ts(),
|
|
313
|
+
"level": level,
|
|
314
|
+
"message": message
|
|
315
|
+
])
|
|
316
|
+
}
|
|
317
|
+
|
|
309
318
|
@objc public func recordJSErrorEvent(name: String, message: String, stack: String?) {
|
|
310
319
|
var event: [String: Any] = [
|
|
311
320
|
"type": "error",
|
|
@@ -317,6 +326,10 @@ public final class TelemetryPipeline: NSObject {
|
|
|
317
326
|
event["stack"] = stack
|
|
318
327
|
}
|
|
319
328
|
_enqueue(event)
|
|
329
|
+
// Prioritize JS error delivery to reduce loss on fatal terminations.
|
|
330
|
+
_serialWorker.async { [weak self] in
|
|
331
|
+
self?._shipPendingEvents()
|
|
332
|
+
}
|
|
320
333
|
}
|
|
321
334
|
|
|
322
335
|
@objc public func recordAnrEvent(durationMs: Int, stack: String?) {
|
|
@@ -330,6 +343,10 @@ public final class TelemetryPipeline: NSObject {
|
|
|
330
343
|
event["stack"] = stack
|
|
331
344
|
}
|
|
332
345
|
_enqueue(event)
|
|
346
|
+
// Prioritize ANR delivery while the process is still alive.
|
|
347
|
+
_serialWorker.async { [weak self] in
|
|
348
|
+
self?._shipPendingEvents()
|
|
349
|
+
}
|
|
333
350
|
}
|
|
334
351
|
|
|
335
352
|
@objc public func recordUserAssociation(_ userId: String) {
|
|
@@ -136,6 +136,7 @@ import UIKit
|
|
|
136
136
|
private func _typeName(_ v: UIView) -> String { String(describing: type(of: v)) }
|
|
137
137
|
|
|
138
138
|
private func _isSensitive(_ v: UIView) -> Bool {
|
|
139
|
+
if v.accessibilityHint == "rejourney_occlude" { return true }
|
|
139
140
|
if let tf = v as? UITextField, tf.isSecureTextEntry { return true }
|
|
140
141
|
return false
|
|
141
142
|
}
|
|
@@ -162,6 +162,11 @@ public final class VisualCapture: NSObject {
|
|
|
162
162
|
_redactionMask.remove(view)
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
@objc public func invalidateMaskCache() {
|
|
166
|
+
_redactionMask.invalidateCache()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
165
170
|
@objc public func configure(snapshotInterval: Double, jpegQuality: Double, captureScale: CGFloat = 1.25) {
|
|
166
171
|
self.snapshotInterval = snapshotInterval
|
|
167
172
|
self.quality = CGFloat(jpegQuality)
|
|
@@ -374,10 +379,20 @@ public final class VisualCapture: NSObject {
|
|
|
374
379
|
|
|
375
380
|
/// Load and upload any pending frames from disk for a session
|
|
376
381
|
@objc public func uploadPendingFrames(sessionId: String) {
|
|
377
|
-
|
|
382
|
+
uploadPendingFrames(sessionId: sessionId, completion: nil)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
public func uploadPendingFrames(sessionId: String, completion: ((Bool) -> Void)? = nil) {
|
|
386
|
+
guard let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
|
|
387
|
+
completion?(false)
|
|
388
|
+
return
|
|
389
|
+
}
|
|
378
390
|
let framesPath = cacheDir.appendingPathComponent("rj_pending").appendingPathComponent(sessionId).appendingPathComponent("frames")
|
|
379
391
|
|
|
380
|
-
guard let frameFiles = try? FileManager.default.contentsOfDirectory(at: framesPath, includingPropertiesForKeys: nil) else {
|
|
392
|
+
guard let frameFiles = try? FileManager.default.contentsOfDirectory(at: framesPath, includingPropertiesForKeys: nil) else {
|
|
393
|
+
completion?(true)
|
|
394
|
+
return
|
|
395
|
+
}
|
|
381
396
|
|
|
382
397
|
var frames: [(Data, UInt64)] = []
|
|
383
398
|
for file in frameFiles.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }) {
|
|
@@ -392,18 +407,24 @@ public final class VisualCapture: NSObject {
|
|
|
392
407
|
frames.append((data, ts))
|
|
393
408
|
}
|
|
394
409
|
|
|
395
|
-
guard !frames.isEmpty, let bundle = _packageFrameBundle(images: frames, sessionEpoch: frames.first?.1 ?? 0) else {
|
|
410
|
+
guard !frames.isEmpty, let bundle = _packageFrameBundle(images: frames, sessionEpoch: frames.first?.1 ?? 0) else {
|
|
411
|
+
completion?(frames.isEmpty)
|
|
412
|
+
return
|
|
413
|
+
}
|
|
396
414
|
|
|
397
415
|
let endTs = frames.last?.1 ?? 0
|
|
398
|
-
let fname = "\(sessionId)-\(endTs).tar.gz"
|
|
399
416
|
|
|
400
|
-
|
|
417
|
+
SegmentDispatcher.shared.transmitFrameBundle(
|
|
401
418
|
payload: bundle,
|
|
402
|
-
filename: fname,
|
|
403
419
|
startMs: frames.first?.1 ?? 0,
|
|
404
420
|
endMs: endTs,
|
|
405
421
|
frameCount: frames.count
|
|
406
|
-
)
|
|
422
|
+
) { ok in
|
|
423
|
+
if ok {
|
|
424
|
+
try? FileManager.default.removeItem(at: framesPath)
|
|
425
|
+
}
|
|
426
|
+
completion?(ok)
|
|
427
|
+
}
|
|
407
428
|
}
|
|
408
429
|
|
|
409
430
|
/// Clear pending frames for a session after successful upload
|
|
@@ -512,7 +533,28 @@ private final class RedactionMask {
|
|
|
512
533
|
// sensitive views (text inputs, cameras) don't appear/disappear at 3fps.
|
|
513
534
|
private var _cachedAutoRects: [CGRect] = []
|
|
514
535
|
private var _lastScanTime: CFAbsoluteTime = 0
|
|
515
|
-
private let _scanCacheDurationSec: CFAbsoluteTime =
|
|
536
|
+
private let _scanCacheDurationSec: CFAbsoluteTime = 0.5
|
|
537
|
+
|
|
538
|
+
private var _observers: [Any] = []
|
|
539
|
+
|
|
540
|
+
init() {
|
|
541
|
+
_observers.append(NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: nil) { [weak self] _ in self?.invalidateCache() })
|
|
542
|
+
_observers.append(NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: nil) { [weak self] _ in self?.invalidateCache() })
|
|
543
|
+
_observers.append(NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, object: nil, queue: nil) { [weak self] _ in self?.invalidateCache() })
|
|
544
|
+
_observers.append(NotificationCenter.default.addObserver(forName: UITextView.textDidChangeNotification, object: nil, queue: nil) { [weak self] _ in self?.invalidateCache() })
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
deinit {
|
|
548
|
+
for observer in _observers {
|
|
549
|
+
NotificationCenter.default.removeObserver(observer)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
func invalidateCache() {
|
|
554
|
+
_lock.lock()
|
|
555
|
+
_lastScanTime = 0
|
|
556
|
+
_lock.unlock()
|
|
557
|
+
}
|
|
516
558
|
|
|
517
559
|
// View class names that should always be masked (privacy sensitive)
|
|
518
560
|
private let _sensitiveClassNames: Set<String> = [
|
|
@@ -668,6 +710,10 @@ private final class RedactionMask {
|
|
|
668
710
|
}
|
|
669
711
|
|
|
670
712
|
private func _shouldMask(_ view: UIView) -> Bool {
|
|
713
|
+
if view.accessibilityHint == "rejourney_occlude" {
|
|
714
|
+
return true
|
|
715
|
+
}
|
|
716
|
+
|
|
671
717
|
// 1. Mask ALL text input fields by default (privacy first)
|
|
672
718
|
// This includes password fields, instructions, notes, etc.
|
|
673
719
|
if view is UITextField {
|
package/ios/Rejourney.mm
CHANGED
|
@@ -70,11 +70,11 @@ RCT_EXPORT_MODULE()
|
|
|
70
70
|
|
|
71
71
|
#pragma mark - Tap Event Emission (no-ops, dead tap detection is native-side)
|
|
72
72
|
|
|
73
|
-
RCT_EXPORT_METHOD(addListener:(NSString *)eventName) {
|
|
73
|
+
RCT_EXPORT_METHOD(addListener : (NSString *)eventName) {
|
|
74
74
|
// No-op: dead tap detection is handled natively in TelemetryPipeline
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
RCT_EXPORT_METHOD(removeListeners:(double)count) {
|
|
77
|
+
RCT_EXPORT_METHOD(removeListeners : (double)count) {
|
|
78
78
|
// No-op: dead tap detection is handled natively in TelemetryPipeline
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -195,6 +195,26 @@ RCT_EXPORT_METHOD(getUserIdentity : (RCTPromiseResolveBlock)
|
|
|
195
195
|
[impl getUserIdentity:resolve reject:reject];
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
RCT_EXPORT_METHOD(setAnonymousId : (NSString *)anonymousId resolve : (
|
|
199
|
+
RCTPromiseResolveBlock)resolve reject : (RCTPromiseRejectBlock)reject) {
|
|
200
|
+
RejourneyImpl *impl = [self ensureImpl];
|
|
201
|
+
if (!impl) {
|
|
202
|
+
resolve(@{@"success" : @NO});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
[impl setAnonymousId:anonymousId resolve:resolve reject:reject];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
RCT_EXPORT_METHOD(getAnonymousId : (RCTPromiseResolveBlock)
|
|
209
|
+
resolve reject : (RCTPromiseRejectBlock)reject) {
|
|
210
|
+
RejourneyImpl *impl = [self ensureImpl];
|
|
211
|
+
if (!impl) {
|
|
212
|
+
resolve([NSNull null]);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
[impl getAnonymousId:resolve reject:reject];
|
|
216
|
+
}
|
|
217
|
+
|
|
198
218
|
#pragma mark - Events and Tracking
|
|
199
219
|
|
|
200
220
|
RCT_EXPORT_METHOD(logEvent : (NSString *)eventType details : (NSDictionary *)
|
|
@@ -312,12 +332,11 @@ RCT_EXPORT_METHOD(setDebugMode : (BOOL)enabled resolve : (
|
|
|
312
332
|
[impl setDebugMode:enabled resolve:resolve reject:reject];
|
|
313
333
|
}
|
|
314
334
|
|
|
315
|
-
RCT_EXPORT_METHOD(setRemoteConfig : (BOOL)rejourneyEnabled
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
reject : (RCTPromiseRejectBlock)reject) {
|
|
335
|
+
RCT_EXPORT_METHOD(setRemoteConfig : (BOOL)rejourneyEnabled recordingEnabled : (
|
|
336
|
+
BOOL)recordingEnabled sampleRate : (double)
|
|
337
|
+
sampleRate maxRecordingMinutes : (double)
|
|
338
|
+
maxRecordingMinutes resolve : (RCTPromiseResolveBlock)
|
|
339
|
+
resolve reject : (RCTPromiseRejectBlock)reject) {
|
|
321
340
|
RejourneyImpl *impl = [self ensureImpl];
|
|
322
341
|
if (!impl) {
|
|
323
342
|
resolve(@{@"success" : @NO});
|
package/lib/commonjs/index.js
CHANGED
|
@@ -680,6 +680,7 @@ const Rejourney = {
|
|
|
680
680
|
trackJSErrors: true,
|
|
681
681
|
trackPromiseRejections: true,
|
|
682
682
|
trackReactNativeErrors: true,
|
|
683
|
+
trackConsoleLogs: _storedConfig?.trackConsoleLogs ?? true,
|
|
683
684
|
collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false
|
|
684
685
|
}, {
|
|
685
686
|
// Rage tap callback - log as frustration event
|
|
@@ -692,13 +693,8 @@ const Rejourney = {
|
|
|
692
693
|
});
|
|
693
694
|
getLogger().logFrustration(`Rage tap (${count} taps)`);
|
|
694
695
|
},
|
|
695
|
-
// Error callback -
|
|
696
|
+
// Error callback - SDK forwarding is handled in autoTracking.trackError
|
|
696
697
|
onError: error => {
|
|
697
|
-
this.logEvent('error', {
|
|
698
|
-
message: error.message,
|
|
699
|
-
stack: error.stack,
|
|
700
|
-
name: error.name
|
|
701
|
-
});
|
|
702
698
|
getLogger().logError(error.message);
|
|
703
699
|
},
|
|
704
700
|
onScreen: (_screenName, _previousScreen) => {}
|
|
@@ -713,6 +709,11 @@ const Rejourney = {
|
|
|
713
709
|
}
|
|
714
710
|
if (_storedConfig?.autoTrackNetwork !== false) {
|
|
715
711
|
try {
|
|
712
|
+
// JS-level fetch/XHR patching is the primary mechanism for capturing network
|
|
713
|
+
// calls within React Native. Native interceptors (RejourneyURLProtocol on iOS,
|
|
714
|
+
// RejourneyNetworkInterceptor on Android) are supplementary — they capture
|
|
715
|
+
// native-originated HTTP calls that bypass JS fetch(), but cannot intercept
|
|
716
|
+
// RN's own networking since it creates its NSURLSession/OkHttpClient at init time.
|
|
716
717
|
const ignoreUrls = [apiUrl, '/api/sdk/config', '/api/ingest/presign', '/api/ingest/batch/complete', '/api/ingest/session/end', ...(_storedConfig?.networkIgnoreUrls || [])];
|
|
717
718
|
getNetworkInterceptor().initNetworkInterceptor(request => {
|
|
718
719
|
getAutoTracking().trackAPIRequest(request.success || false, request.statusCode, request.duration || 0, request.responseBodySize || 0);
|
|
@@ -1110,6 +1111,23 @@ const Rejourney = {
|
|
|
1110
1111
|
getRejourneyNative().logEvent('network_request', networkEvent).catch(() => {});
|
|
1111
1112
|
}, undefined);
|
|
1112
1113
|
},
|
|
1114
|
+
/**
|
|
1115
|
+
* Log customer feedback (e.g. from an in-app survey or NPS widget).
|
|
1116
|
+
*
|
|
1117
|
+
* @param rating - Numeric rating (e.g. 1 to 5)
|
|
1118
|
+
* @param message - Associated feedback text or comment
|
|
1119
|
+
*/
|
|
1120
|
+
logFeedback(rating, message) {
|
|
1121
|
+
safeNativeCallSync('logFeedback', () => {
|
|
1122
|
+
const feedbackEvent = {
|
|
1123
|
+
type: 'feedback',
|
|
1124
|
+
timestamp: Date.now(),
|
|
1125
|
+
rating,
|
|
1126
|
+
message
|
|
1127
|
+
};
|
|
1128
|
+
getRejourneyNative().logEvent('feedback', feedbackEvent).catch(() => {});
|
|
1129
|
+
}, undefined);
|
|
1130
|
+
},
|
|
1113
1131
|
/**
|
|
1114
1132
|
* Get SDK telemetry metrics for observability
|
|
1115
1133
|
*
|
|
@@ -1145,17 +1163,12 @@ const Rejourney = {
|
|
|
1145
1163
|
});
|
|
1146
1164
|
},
|
|
1147
1165
|
/**
|
|
1148
|
-
* Trigger
|
|
1149
|
-
* Blocks the main thread for the specified duration
|
|
1166
|
+
* Trigger an ANR test by blocking the main thread for the specified duration.
|
|
1150
1167
|
*/
|
|
1151
1168
|
debugTriggerANR(durationMs) {
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}, undefined);
|
|
1156
|
-
} else {
|
|
1157
|
-
getLogger().warn('debugTriggerANR is only available in development mode');
|
|
1158
|
-
}
|
|
1169
|
+
safeNativeCallSync('debugTriggerANR', () => {
|
|
1170
|
+
getRejourneyNative().debugTriggerANR(durationMs);
|
|
1171
|
+
}, undefined);
|
|
1159
1172
|
},
|
|
1160
1173
|
/**
|
|
1161
1174
|
* Mask a view by its nativeID prop (will be occluded in recordings)
|