@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
|
@@ -259,6 +259,8 @@ class SegmentDispatcher private constructor() {
|
|
|
259
259
|
backgroundDurationMs: Long,
|
|
260
260
|
metrics: Map<String, Any>?,
|
|
261
261
|
currentQueueDepth: Int = 0,
|
|
262
|
+
endReason: String? = null,
|
|
263
|
+
lifecycleVersion: Int? = null,
|
|
262
264
|
completion: (Boolean) -> Unit
|
|
263
265
|
) {
|
|
264
266
|
val url = "$endpoint/api/ingest/session/end"
|
|
@@ -270,6 +272,8 @@ class SegmentDispatcher private constructor() {
|
|
|
270
272
|
if (backgroundDurationMs > 0) put("totalBackgroundTimeMs", backgroundDurationMs)
|
|
271
273
|
metrics?.let { put("metrics", JSONObject(it)) }
|
|
272
274
|
put("sdkTelemetry", buildSdkTelemetry(currentQueueDepth))
|
|
275
|
+
if (!endReason.isNullOrBlank()) put("endReason", endReason)
|
|
276
|
+
if ((lifecycleVersion ?: 0) > 0) put("lifecycleVersion", lifecycleVersion)
|
|
273
277
|
}
|
|
274
278
|
|
|
275
279
|
val request = buildRequest(url, body)
|
|
@@ -173,6 +173,9 @@ class StabilityMonitor private constructor(private val context: Context) {
|
|
|
173
173
|
ReplayOrchestrator.shared?.incrementFaultTally()
|
|
174
174
|
persistIncident(incident)
|
|
175
175
|
|
|
176
|
+
// Flush visual frames to disk for crash safety
|
|
177
|
+
try { VisualCapture.shared?.flushToDisk() } catch (_: Exception) { }
|
|
178
|
+
|
|
176
179
|
// Give time to write
|
|
177
180
|
Thread.sleep(150)
|
|
178
181
|
}
|
|
@@ -320,6 +320,15 @@ class TelemetryPipeline private constructor(private val context: Context) {
|
|
|
320
320
|
))
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
fun recordConsoleLogEvent(level: String, message: String) {
|
|
324
|
+
enqueue(mapOf(
|
|
325
|
+
"type" to "log",
|
|
326
|
+
"timestamp" to ts(),
|
|
327
|
+
"level" to level,
|
|
328
|
+
"message" to message
|
|
329
|
+
))
|
|
330
|
+
}
|
|
331
|
+
|
|
323
332
|
fun recordJSErrorEvent(name: String, message: String, stack: String?) {
|
|
324
333
|
val event = mutableMapOf<String, Any>(
|
|
325
334
|
"type" to "error",
|
|
@@ -331,6 +340,8 @@ class TelemetryPipeline private constructor(private val context: Context) {
|
|
|
331
340
|
event["stack"] = stack
|
|
332
341
|
}
|
|
333
342
|
enqueue(event)
|
|
343
|
+
// Prioritize JS error delivery to reduce loss on fatal terminations.
|
|
344
|
+
serialWorker.execute { shipPendingEvents() }
|
|
334
345
|
}
|
|
335
346
|
|
|
336
347
|
fun recordAnrEvent(durationMs: Long, stack: String?) {
|
|
@@ -344,6 +355,8 @@ class TelemetryPipeline private constructor(private val context: Context) {
|
|
|
344
355
|
event["stack"] = stack
|
|
345
356
|
}
|
|
346
357
|
enqueue(event)
|
|
358
|
+
// Prioritize ANR delivery while the process is still alive.
|
|
359
|
+
serialWorker.execute { shipPendingEvents() }
|
|
347
360
|
}
|
|
348
361
|
|
|
349
362
|
fun recordUserAssociation(userId: String) {
|
|
@@ -203,6 +203,14 @@ class ViewHierarchyScanner private constructor() {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
private fun isSensitive(view: View): Boolean {
|
|
206
|
+
if (view.contentDescription?.toString() == "rejourney_occlude") return true
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
// Check for React Native accessibility hint tag
|
|
210
|
+
val hint = view.getTag(com.facebook.react.R.id.accessibility_hint) as? String
|
|
211
|
+
if (hint == "rejourney_occlude") return true
|
|
212
|
+
} catch (_: Exception) { }
|
|
213
|
+
|
|
206
214
|
if (view is EditText) {
|
|
207
215
|
val inputType = view.inputType
|
|
208
216
|
// Check for password input types
|
|
@@ -30,6 +30,7 @@ import android.view.TextureView
|
|
|
30
30
|
import android.view.View
|
|
31
31
|
import android.view.ViewGroup
|
|
32
32
|
import android.view.WindowManager
|
|
33
|
+
import android.widget.EditText
|
|
33
34
|
import com.rejourney.engine.DiagnosticLog
|
|
34
35
|
import com.rejourney.utility.gzipCompress
|
|
35
36
|
import java.io.ByteArrayOutputStream
|
|
@@ -170,6 +171,10 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
170
171
|
redactionMask.remove(view)
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
fun invalidateMaskCache() {
|
|
175
|
+
redactionMask.invalidateCache()
|
|
176
|
+
}
|
|
177
|
+
|
|
173
178
|
fun configure(snapshotInterval: Double, jpegQuality: Double) {
|
|
174
179
|
this.snapshotInterval = snapshotInterval
|
|
175
180
|
this.quality = jpegQuality.toFloat()
|
|
@@ -241,7 +246,7 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
241
246
|
|
|
242
247
|
if (bounds.width() <= 0 || bounds.height() <= 0) return
|
|
243
248
|
|
|
244
|
-
val redactRects = redactionMask.computeRects()
|
|
249
|
+
val redactRects = redactionMask.computeRects(decorView)
|
|
245
250
|
|
|
246
251
|
val screenScale = 1.25f
|
|
247
252
|
val scaledWidth = (bounds.width() / screenScale).toInt()
|
|
@@ -466,12 +471,18 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
466
471
|
sendScreenshots()
|
|
467
472
|
}
|
|
468
473
|
|
|
469
|
-
fun uploadPendingFrames(sessionId: String) {
|
|
474
|
+
fun uploadPendingFrames(sessionId: String, completion: ((Boolean) -> Unit)? = null) {
|
|
470
475
|
val framesPath = File(context.cacheDir, "rj_pending/$sessionId/frames")
|
|
471
476
|
|
|
472
|
-
if (!framesPath.exists())
|
|
477
|
+
if (!framesPath.exists()) {
|
|
478
|
+
completion?.invoke(true)
|
|
479
|
+
return
|
|
480
|
+
}
|
|
473
481
|
|
|
474
|
-
val frameFiles = framesPath.listFiles()?.sortedBy { it.name } ?:
|
|
482
|
+
val frameFiles = framesPath.listFiles()?.sortedBy { it.name } ?: run {
|
|
483
|
+
completion?.invoke(true)
|
|
484
|
+
return
|
|
485
|
+
}
|
|
475
486
|
|
|
476
487
|
val frames = mutableListOf<Pair<ByteArray, Long>>()
|
|
477
488
|
for (file in frameFiles) {
|
|
@@ -481,9 +492,15 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
481
492
|
frames.add(Pair(data, ts))
|
|
482
493
|
}
|
|
483
494
|
|
|
484
|
-
if (frames.isEmpty())
|
|
495
|
+
if (frames.isEmpty()) {
|
|
496
|
+
completion?.invoke(true)
|
|
497
|
+
return
|
|
498
|
+
}
|
|
485
499
|
|
|
486
|
-
val bundle = packageFrameBundle(frames, frames.first().second) ?:
|
|
500
|
+
val bundle = packageFrameBundle(frames, frames.first().second) ?: run {
|
|
501
|
+
completion?.invoke(false)
|
|
502
|
+
return
|
|
503
|
+
}
|
|
487
504
|
|
|
488
505
|
SegmentDispatcher.shared.transmitFrameBundle(
|
|
489
506
|
payload = bundle,
|
|
@@ -496,6 +513,7 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
496
513
|
frameFiles.forEach { it.delete() }
|
|
497
514
|
framesPath.delete()
|
|
498
515
|
}
|
|
516
|
+
completion?.invoke(ok)
|
|
499
517
|
}
|
|
500
518
|
}
|
|
501
519
|
}
|
|
@@ -530,6 +548,10 @@ private class CaptureStateMachine {
|
|
|
530
548
|
private class RedactionMask {
|
|
531
549
|
private val views = CopyOnWriteArrayList<WeakReference<View>>()
|
|
532
550
|
|
|
551
|
+
private val cachedAutoRects = mutableListOf<Rect>()
|
|
552
|
+
private var lastScanTime = 0L
|
|
553
|
+
private val scanCacheDurationMs = 500L
|
|
554
|
+
|
|
533
555
|
fun add(view: View) {
|
|
534
556
|
views.add(WeakReference(view))
|
|
535
557
|
}
|
|
@@ -538,29 +560,81 @@ private class RedactionMask {
|
|
|
538
560
|
views.removeIf { it.get() === view || it.get() == null }
|
|
539
561
|
}
|
|
540
562
|
|
|
541
|
-
fun
|
|
563
|
+
fun invalidateCache() {
|
|
564
|
+
lastScanTime = 0L
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
fun computeRects(decorView: View? = null): List<Rect> {
|
|
542
568
|
val rects = mutableListOf<Rect>()
|
|
543
569
|
views.removeIf { it.get() == null }
|
|
544
570
|
|
|
545
571
|
for (ref in views) {
|
|
546
572
|
val view = ref.get() ?: continue
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
val
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
)
|
|
558
|
-
|
|
559
|
-
if (rect.width() > 0 && rect.height() > 0) {
|
|
560
|
-
rects.add(rect)
|
|
573
|
+
val rect = getViewRect(view)
|
|
574
|
+
if (rect != null) rects.add(rect)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (decorView != null) {
|
|
578
|
+
val now = SystemClock.elapsedRealtime()
|
|
579
|
+
if (now - lastScanTime >= scanCacheDurationMs) {
|
|
580
|
+
cachedAutoRects.clear()
|
|
581
|
+
scanForSensitiveViews(decorView, cachedAutoRects)
|
|
582
|
+
lastScanTime = now
|
|
561
583
|
}
|
|
584
|
+
rects.addAll(cachedAutoRects)
|
|
562
585
|
}
|
|
563
586
|
|
|
564
587
|
return rects
|
|
565
588
|
}
|
|
589
|
+
|
|
590
|
+
private fun getViewRect(view: View): Rect? {
|
|
591
|
+
if (!view.isShown || view.width <= 0 || view.height <= 0) return null
|
|
592
|
+
val location = IntArray(2)
|
|
593
|
+
view.getLocationOnScreen(location)
|
|
594
|
+
val rect = Rect(
|
|
595
|
+
location[0],
|
|
596
|
+
location[1],
|
|
597
|
+
location[0] + view.width,
|
|
598
|
+
location[1] + view.height
|
|
599
|
+
)
|
|
600
|
+
if (rect.width() > 0 && rect.height() > 0) return rect
|
|
601
|
+
return null
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private fun scanForSensitiveViews(view: View, rects: MutableList<Rect>, depth: Int = 0) {
|
|
605
|
+
if (depth > 20) return
|
|
606
|
+
if (!view.isShown || view.alpha <= 0.01f || view.width <= 0 || view.height <= 0) return
|
|
607
|
+
|
|
608
|
+
if (shouldMask(view)) {
|
|
609
|
+
val rect = getViewRect(view)
|
|
610
|
+
if (rect != null) {
|
|
611
|
+
rects.add(rect)
|
|
612
|
+
return
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (view is ViewGroup) {
|
|
617
|
+
for (i in 0 until view.childCount) {
|
|
618
|
+
scanForSensitiveViews(view.getChildAt(i), rects, depth + 1)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private fun shouldMask(view: View): Boolean {
|
|
624
|
+
if (view.contentDescription?.toString() == "rejourney_occlude") return true
|
|
625
|
+
|
|
626
|
+
try {
|
|
627
|
+
val hint = view.getTag(com.facebook.react.R.id.accessibility_hint) as? String
|
|
628
|
+
if (hint == "rejourney_occlude") return true
|
|
629
|
+
} catch (_: Exception) { }
|
|
630
|
+
|
|
631
|
+
if (view is EditText) return true
|
|
632
|
+
|
|
633
|
+
val className = view.javaClass.simpleName.lowercase(java.util.Locale.US)
|
|
634
|
+
if (className.contains("camera") || (className.contains("surfaceview") && className.contains("preview"))) {
|
|
635
|
+
return true
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return false
|
|
639
|
+
}
|
|
566
640
|
}
|
|
@@ -191,6 +191,20 @@ class RejourneyModule(reactContext: ReactApplicationContext) :
|
|
|
191
191
|
instance.getUserIdentity(promise)
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
@ReactMethod
|
|
195
|
+
@DoNotStrip
|
|
196
|
+
override fun setAnonymousId(anonymousId: String, promise: Promise) {
|
|
197
|
+
val instance = getImplOrReject(promise) ?: return
|
|
198
|
+
instance.setAnonymousId(anonymousId, promise)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@ReactMethod
|
|
202
|
+
@DoNotStrip
|
|
203
|
+
override fun getAnonymousId(promise: Promise) {
|
|
204
|
+
val instance = getImplOrReject(promise) ?: return
|
|
205
|
+
instance.getAnonymousId(promise)
|
|
206
|
+
}
|
|
207
|
+
|
|
194
208
|
@ReactMethod
|
|
195
209
|
@DoNotStrip
|
|
196
210
|
override fun setDebugMode(enabled: Boolean, promise: Promise) {
|
|
@@ -224,6 +224,24 @@ class RejourneyModule(reactContext: ReactApplicationContext) :
|
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
@ReactMethod
|
|
228
|
+
fun setAnonymousId(anonymousId: String, promise: Promise) {
|
|
229
|
+
try {
|
|
230
|
+
impl.setAnonymousId(anonymousId, promise)
|
|
231
|
+
} catch (e: Exception) {
|
|
232
|
+
promise.resolve(createErrorMap("Module initialization failed: ${e.message}"))
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@ReactMethod
|
|
237
|
+
fun getAnonymousId(promise: Promise) {
|
|
238
|
+
try {
|
|
239
|
+
impl.getAnonymousId(promise)
|
|
240
|
+
} catch (e: Exception) {
|
|
241
|
+
promise.resolve(null)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
227
245
|
@ReactMethod
|
|
228
246
|
fun setSDKVersion(version: String) {
|
|
229
247
|
try {
|
|
@@ -37,6 +37,7 @@ public final class DeviceRegistrar: NSObject {
|
|
|
37
37
|
// MARK: Private State
|
|
38
38
|
|
|
39
39
|
private let _keychainId = "com.rejourney.device.fingerprint"
|
|
40
|
+
private let _fallbackIdKey = "com.rejourney.device.fallbackId"
|
|
40
41
|
|
|
41
42
|
private lazy var _httpSession: URLSession = {
|
|
42
43
|
let config = URLSessionConfiguration.default
|
|
@@ -124,13 +125,22 @@ public final class DeviceRegistrar: NSObject {
|
|
|
124
125
|
|
|
125
126
|
var composite = bundleId
|
|
126
127
|
composite += device.model
|
|
127
|
-
composite += device.
|
|
128
|
-
composite += device.systemVersion
|
|
129
|
-
composite += device.identifierForVendor?.uuidString ?? UUID().uuidString
|
|
128
|
+
composite += device.identifierForVendor?.uuidString ?? _stableDeviceFallback()
|
|
130
129
|
|
|
131
130
|
return _sha256(composite)
|
|
132
131
|
}
|
|
133
132
|
|
|
133
|
+
/// Returns a keychain-persisted UUID so the fingerprint stays stable even when
|
|
134
|
+
/// identifierForVendor is temporarily nil (early boot, App Clips, extensions).
|
|
135
|
+
private func _stableDeviceFallback() -> String {
|
|
136
|
+
if let existing = _keychainLoad(_fallbackIdKey) {
|
|
137
|
+
return existing
|
|
138
|
+
}
|
|
139
|
+
let fresh = UUID().uuidString
|
|
140
|
+
_keychainSave(_fallbackIdKey, value: fresh)
|
|
141
|
+
return fresh
|
|
142
|
+
}
|
|
143
|
+
|
|
134
144
|
// MARK: Server Communication
|
|
135
145
|
|
|
136
146
|
private func _fetchServerCredential(fingerprint: String, apiToken: String, completion: @escaping (Bool, String?) -> Void) {
|