@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
@@ -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()) return
477
+ if (!framesPath.exists()) {
478
+ completion?.invoke(true)
479
+ return
480
+ }
473
481
 
474
- val frameFiles = framesPath.listFiles()?.sortedBy { it.name } ?: return
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()) return
495
+ if (frames.isEmpty()) {
496
+ completion?.invoke(true)
497
+ return
498
+ }
485
499
 
486
- val bundle = packageFrameBundle(frames, frames.first().second) ?: return
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 computeRects(): List<Rect> {
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
- if (!view.isShown) continue
548
-
549
- val location = IntArray(2)
550
- view.getLocationOnScreen(location)
551
-
552
- val rect = Rect(
553
- location[0],
554
- location[1],
555
- location[0] + view.width,
556
- location[1] + view.height
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.systemName
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) {