@rejourneyco/react-native 1.0.7

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 (105) hide show
  1. package/README.md +29 -0
  2. package/android/build.gradle.kts +135 -0
  3. package/android/consumer-rules.pro +10 -0
  4. package/android/proguard-rules.pro +1 -0
  5. package/android/src/main/AndroidManifest.xml +15 -0
  6. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +860 -0
  7. package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +290 -0
  8. package/android/src/main/java/com/rejourney/engine/DiagnosticLog.kt +385 -0
  9. package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +512 -0
  10. package/android/src/main/java/com/rejourney/platform/OEMDetector.kt +173 -0
  11. package/android/src/main/java/com/rejourney/platform/PerfTiming.kt +384 -0
  12. package/android/src/main/java/com/rejourney/platform/SessionLifecycleService.kt +160 -0
  13. package/android/src/main/java/com/rejourney/platform/Telemetry.kt +301 -0
  14. package/android/src/main/java/com/rejourney/platform/WindowUtils.kt +100 -0
  15. package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +129 -0
  16. package/android/src/main/java/com/rejourney/recording/EventBuffer.kt +330 -0
  17. package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +519 -0
  18. package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +740 -0
  19. package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +559 -0
  20. package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +238 -0
  21. package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +633 -0
  22. package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +232 -0
  23. package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +474 -0
  24. package/android/src/main/java/com/rejourney/utility/DataCompression.kt +63 -0
  25. package/android/src/main/java/com/rejourney/utility/ImageBlur.kt +412 -0
  26. package/android/src/main/java/com/rejourney/utility/ViewIdentifier.kt +169 -0
  27. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +232 -0
  28. package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
  29. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +268 -0
  30. package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
  31. package/ios/Engine/DeviceRegistrar.swift +288 -0
  32. package/ios/Engine/DiagnosticLog.swift +387 -0
  33. package/ios/Engine/RejourneyImpl.swift +719 -0
  34. package/ios/Recording/AnrSentinel.swift +142 -0
  35. package/ios/Recording/EventBuffer.swift +326 -0
  36. package/ios/Recording/InteractionRecorder.swift +428 -0
  37. package/ios/Recording/ReplayOrchestrator.swift +624 -0
  38. package/ios/Recording/SegmentDispatcher.swift +492 -0
  39. package/ios/Recording/StabilityMonitor.swift +223 -0
  40. package/ios/Recording/TelemetryPipeline.swift +547 -0
  41. package/ios/Recording/ViewHierarchyScanner.swift +156 -0
  42. package/ios/Recording/VisualCapture.swift +675 -0
  43. package/ios/Rejourney.h +38 -0
  44. package/ios/Rejourney.mm +375 -0
  45. package/ios/Utility/DataCompression.swift +55 -0
  46. package/ios/Utility/ImageBlur.swift +89 -0
  47. package/ios/Utility/RuntimeMethodSwap.swift +41 -0
  48. package/ios/Utility/ViewIdentifier.swift +37 -0
  49. package/lib/commonjs/NativeRejourney.js +40 -0
  50. package/lib/commonjs/components/Mask.js +88 -0
  51. package/lib/commonjs/index.js +1443 -0
  52. package/lib/commonjs/sdk/autoTracking.js +1087 -0
  53. package/lib/commonjs/sdk/constants.js +166 -0
  54. package/lib/commonjs/sdk/errorTracking.js +187 -0
  55. package/lib/commonjs/sdk/index.js +50 -0
  56. package/lib/commonjs/sdk/metricsTracking.js +205 -0
  57. package/lib/commonjs/sdk/navigation.js +128 -0
  58. package/lib/commonjs/sdk/networkInterceptor.js +375 -0
  59. package/lib/commonjs/sdk/utils.js +433 -0
  60. package/lib/commonjs/sdk/version.js +13 -0
  61. package/lib/commonjs/types/expo-router.d.js +2 -0
  62. package/lib/commonjs/types/index.js +2 -0
  63. package/lib/module/NativeRejourney.js +38 -0
  64. package/lib/module/components/Mask.js +83 -0
  65. package/lib/module/index.js +1341 -0
  66. package/lib/module/sdk/autoTracking.js +1059 -0
  67. package/lib/module/sdk/constants.js +154 -0
  68. package/lib/module/sdk/errorTracking.js +177 -0
  69. package/lib/module/sdk/index.js +26 -0
  70. package/lib/module/sdk/metricsTracking.js +187 -0
  71. package/lib/module/sdk/navigation.js +120 -0
  72. package/lib/module/sdk/networkInterceptor.js +364 -0
  73. package/lib/module/sdk/utils.js +412 -0
  74. package/lib/module/sdk/version.js +7 -0
  75. package/lib/module/types/expo-router.d.js +2 -0
  76. package/lib/module/types/index.js +2 -0
  77. package/lib/typescript/NativeRejourney.d.ts +160 -0
  78. package/lib/typescript/components/Mask.d.ts +54 -0
  79. package/lib/typescript/index.d.ts +117 -0
  80. package/lib/typescript/sdk/autoTracking.d.ts +226 -0
  81. package/lib/typescript/sdk/constants.d.ts +138 -0
  82. package/lib/typescript/sdk/errorTracking.d.ts +47 -0
  83. package/lib/typescript/sdk/index.d.ts +24 -0
  84. package/lib/typescript/sdk/metricsTracking.d.ts +75 -0
  85. package/lib/typescript/sdk/navigation.d.ts +48 -0
  86. package/lib/typescript/sdk/networkInterceptor.d.ts +62 -0
  87. package/lib/typescript/sdk/utils.d.ts +193 -0
  88. package/lib/typescript/sdk/version.d.ts +6 -0
  89. package/lib/typescript/types/index.d.ts +618 -0
  90. package/package.json +122 -0
  91. package/rejourney.podspec +23 -0
  92. package/src/NativeRejourney.ts +185 -0
  93. package/src/components/Mask.tsx +93 -0
  94. package/src/index.ts +1555 -0
  95. package/src/sdk/autoTracking.ts +1245 -0
  96. package/src/sdk/constants.ts +155 -0
  97. package/src/sdk/errorTracking.ts +231 -0
  98. package/src/sdk/index.ts +25 -0
  99. package/src/sdk/metricsTracking.ts +227 -0
  100. package/src/sdk/navigation.ts +152 -0
  101. package/src/sdk/networkInterceptor.ts +423 -0
  102. package/src/sdk/utils.ts +442 -0
  103. package/src/sdk/version.ts +6 -0
  104. package/src/types/expo-router.d.ts +7 -0
  105. package/src/types/index.ts +709 -0
@@ -0,0 +1,301 @@
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
+ /**
18
+ * SDK telemetry and metrics collection.
19
+ * Android implementation aligned with iOS telemetry.
20
+ */
21
+ package com.rejourney.platform
22
+
23
+ import com.rejourney.engine.DiagnosticLog
24
+ import java.util.concurrent.locks.ReentrantLock
25
+ import kotlin.concurrent.withLock
26
+
27
+ enum class TelemetryEventType {
28
+ UPLOAD_SUCCESS,
29
+ UPLOAD_FAILURE,
30
+ RETRY_ATTEMPT,
31
+ CIRCUIT_BREAKER_OPEN,
32
+ CIRCUIT_BREAKER_CLOSE,
33
+ MEMORY_PRESSURE_EVICTION,
34
+ OFFLINE_QUEUE_PERSIST,
35
+ OFFLINE_QUEUE_RESTORE,
36
+ SESSION_START,
37
+ SESSION_END,
38
+ CRASH_DETECTED,
39
+ TOKEN_REFRESH
40
+ }
41
+
42
+ data class SDKMetrics(
43
+ val uploadSuccessCount: Int = 0,
44
+ val uploadFailureCount: Int = 0,
45
+ val retryAttemptCount: Int = 0,
46
+ val circuitBreakerOpenCount: Int = 0,
47
+ val memoryEvictionCount: Int = 0,
48
+ val offlinePersistCount: Int = 0,
49
+ val sessionStartCount: Int = 0,
50
+ val crashCount: Int = 0,
51
+ val anrCount: Int = 0,
52
+ val uploadSuccessRate: Float = 1.0f,
53
+ val avgUploadDurationMs: Long = 0,
54
+ val currentQueueDepth: Int = 0,
55
+ val lastUploadTime: Long? = null,
56
+ val lastRetryTime: Long? = null,
57
+ val totalBytesUploaded: Long = 0,
58
+ val totalBytesEvicted: Long = 0
59
+ )
60
+
61
+ class Telemetry private constructor() {
62
+
63
+ companion object {
64
+ @Volatile
65
+ private var instance: Telemetry? = null
66
+
67
+ fun getInstance(): Telemetry {
68
+ return instance ?: synchronized(this) {
69
+ instance ?: Telemetry().also { instance = it }
70
+ }
71
+ }
72
+ }
73
+
74
+ private val lock = ReentrantLock()
75
+
76
+ private var uploadSuccessCount = 0
77
+ private var uploadFailureCount = 0
78
+ private var retryAttemptCount = 0
79
+ private var circuitBreakerOpenCount = 0
80
+ private var memoryEvictionCount = 0
81
+ private var offlinePersistCount = 0
82
+ private var sessionStartCount = 0
83
+ private var crashCount = 0
84
+ private var anrCount = 0
85
+ private var uploadSuccessRate = 1.0
86
+ private var avgUploadDurationMs = 0.0
87
+ private var currentQueueDepth = 0
88
+ private var lastUploadTime: Long? = null
89
+ private var lastRetryTime: Long? = null
90
+
91
+ private var totalUploadCount = 0
92
+ private var totalUploadDurationMs = 0L
93
+ private var totalBytesUploaded = 0L
94
+ private var totalBytesEvicted = 0L
95
+
96
+ fun recordEvent(eventType: TelemetryEventType, metadata: Map<String, Any?>? = null) {
97
+ lock.withLock {
98
+ when (eventType) {
99
+ TelemetryEventType.UPLOAD_SUCCESS -> {
100
+ uploadSuccessCount++
101
+ lastUploadTime = System.currentTimeMillis()
102
+ }
103
+ TelemetryEventType.UPLOAD_FAILURE -> {
104
+ uploadFailureCount++
105
+ }
106
+ TelemetryEventType.RETRY_ATTEMPT -> {
107
+ retryAttemptCount++
108
+ lastRetryTime = System.currentTimeMillis()
109
+ }
110
+ TelemetryEventType.CIRCUIT_BREAKER_OPEN -> {
111
+ circuitBreakerOpenCount++
112
+ DiagnosticLog.caution("[Telemetry] Circuit breaker opened (total: $circuitBreakerOpenCount)")
113
+ }
114
+ TelemetryEventType.CIRCUIT_BREAKER_CLOSE -> {
115
+ DiagnosticLog.trace("[Telemetry] Circuit breaker closed")
116
+ }
117
+ TelemetryEventType.MEMORY_PRESSURE_EVICTION -> {
118
+ memoryEvictionCount++
119
+ }
120
+ TelemetryEventType.OFFLINE_QUEUE_PERSIST -> {
121
+ offlinePersistCount++
122
+ DiagnosticLog.trace("[Telemetry] Offline queue persisted (total: $offlinePersistCount)")
123
+ }
124
+ TelemetryEventType.OFFLINE_QUEUE_RESTORE -> {
125
+ DiagnosticLog.trace("[Telemetry] Offline queue restored")
126
+ }
127
+ TelemetryEventType.SESSION_START -> {
128
+ sessionStartCount++
129
+ }
130
+ TelemetryEventType.SESSION_END -> {
131
+ logCurrentMetricsInternal()
132
+ }
133
+ TelemetryEventType.CRASH_DETECTED -> {
134
+ crashCount++
135
+ DiagnosticLog.caution("[Telemetry] Crash detected (total: $crashCount)")
136
+ }
137
+ TelemetryEventType.TOKEN_REFRESH -> {
138
+ DiagnosticLog.trace("[Telemetry] Token refresh triggered")
139
+ }
140
+ }
141
+
142
+ updateSuccessRate()
143
+ }
144
+
145
+ if (metadata != null) {
146
+ DiagnosticLog.trace("[Telemetry] Event metadata: $metadata")
147
+ }
148
+ }
149
+
150
+ fun recordUploadDuration(durationMs: Long, success: Boolean, byteCount: Long) {
151
+ lock.withLock {
152
+ totalUploadCount++
153
+ totalUploadDurationMs += durationMs
154
+ avgUploadDurationMs = totalUploadDurationMs.toDouble() / totalUploadCount.toDouble()
155
+
156
+ if (success) {
157
+ totalBytesUploaded += byteCount
158
+ uploadSuccessCount++
159
+ lastUploadTime = System.currentTimeMillis()
160
+ } else {
161
+ uploadFailureCount++
162
+ }
163
+
164
+ updateSuccessRate()
165
+ }
166
+ }
167
+
168
+ fun recordFrameEviction(bytesEvicted: Long, frameCount: Int) {
169
+ lock.withLock {
170
+ memoryEvictionCount += frameCount
171
+ totalBytesEvicted += bytesEvicted
172
+ DiagnosticLog.caution(
173
+ "[Telemetry] Memory eviction: $frameCount frames, ${totalBytesEvicted / 1024.0} KB total evicted"
174
+ )
175
+ }
176
+ }
177
+
178
+ fun recordQueueDepth(depth: Int) {
179
+ lock.withLock {
180
+ currentQueueDepth = depth
181
+ }
182
+ }
183
+
184
+ fun recordANR() {
185
+ lock.withLock {
186
+ anrCount++
187
+ DiagnosticLog.caution("[Telemetry] ANR detected (total: $anrCount)")
188
+ }
189
+ }
190
+
191
+ fun currentMetrics(): SDKMetrics {
192
+ return lock.withLock {
193
+ SDKMetrics(
194
+ uploadSuccessCount = uploadSuccessCount,
195
+ uploadFailureCount = uploadFailureCount,
196
+ retryAttemptCount = retryAttemptCount,
197
+ circuitBreakerOpenCount = circuitBreakerOpenCount,
198
+ memoryEvictionCount = memoryEvictionCount,
199
+ offlinePersistCount = offlinePersistCount,
200
+ sessionStartCount = sessionStartCount,
201
+ crashCount = crashCount,
202
+ anrCount = anrCount,
203
+ uploadSuccessRate = uploadSuccessRate.toFloat(),
204
+ avgUploadDurationMs = avgUploadDurationMs.toLong(),
205
+ currentQueueDepth = currentQueueDepth,
206
+ lastUploadTime = lastUploadTime,
207
+ lastRetryTime = lastRetryTime,
208
+ totalBytesUploaded = totalBytesUploaded,
209
+ totalBytesEvicted = totalBytesEvicted
210
+ )
211
+ }
212
+ }
213
+
214
+ fun metricsAsMap(): Map<String, Any?> {
215
+ val metrics = currentMetrics()
216
+ return mapOf(
217
+ "uploadSuccessCount" to metrics.uploadSuccessCount,
218
+ "uploadFailureCount" to metrics.uploadFailureCount,
219
+ "retryAttemptCount" to metrics.retryAttemptCount,
220
+ "circuitBreakerOpenCount" to metrics.circuitBreakerOpenCount,
221
+ "memoryEvictionCount" to metrics.memoryEvictionCount,
222
+ "offlinePersistCount" to metrics.offlinePersistCount,
223
+ "sessionStartCount" to metrics.sessionStartCount,
224
+ "crashCount" to metrics.crashCount,
225
+ "anrCount" to metrics.anrCount,
226
+ "uploadSuccessRate" to metrics.uploadSuccessRate,
227
+ "avgUploadDurationMs" to metrics.avgUploadDurationMs,
228
+ "currentQueueDepth" to metrics.currentQueueDepth,
229
+ "lastUploadTime" to metrics.lastUploadTime,
230
+ "lastRetryTime" to metrics.lastRetryTime,
231
+ "totalBytesUploaded" to metrics.totalBytesUploaded,
232
+ "totalBytesEvicted" to metrics.totalBytesEvicted
233
+ )
234
+ }
235
+
236
+ fun reset() {
237
+ lock.withLock {
238
+ uploadSuccessCount = 0
239
+ uploadFailureCount = 0
240
+ retryAttemptCount = 0
241
+ circuitBreakerOpenCount = 0
242
+ memoryEvictionCount = 0
243
+ offlinePersistCount = 0
244
+ sessionStartCount = 0
245
+ crashCount = 0
246
+ anrCount = 0
247
+ uploadSuccessRate = 1.0
248
+ avgUploadDurationMs = 0.0
249
+ currentQueueDepth = 0
250
+ lastUploadTime = null
251
+ lastRetryTime = null
252
+ totalUploadCount = 0
253
+ totalUploadDurationMs = 0
254
+ totalBytesUploaded = 0
255
+ totalBytesEvicted = 0
256
+ }
257
+ }
258
+
259
+ fun logCurrentMetrics() {
260
+ lock.withLock {
261
+ logCurrentMetricsInternal()
262
+ }
263
+ }
264
+
265
+ private fun logCurrentMetricsInternal() {
266
+ DiagnosticLog.notice("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
267
+ DiagnosticLog.notice(" SDK Telemetry Summary")
268
+ DiagnosticLog.notice(
269
+ " Uploads: $uploadSuccessCount success, $uploadFailureCount failed (${uploadSuccessRate * 100}% success rate)"
270
+ )
271
+ DiagnosticLog.notice(" Avg upload latency: ${"%.1f".format(avgUploadDurationMs)} ms")
272
+ DiagnosticLog.notice(" Retries: $retryAttemptCount attempts")
273
+ DiagnosticLog.notice(" Circuit breaker opens: $circuitBreakerOpenCount")
274
+ DiagnosticLog.notice(
275
+ " Memory evictions: $memoryEvictionCount frames (${totalBytesEvicted / 1024.0} KB)"
276
+ )
277
+ DiagnosticLog.notice(" Offline persists: $offlinePersistCount")
278
+ DiagnosticLog.notice(" Data uploaded: ${totalBytesUploaded / 1024.0} KB")
279
+ DiagnosticLog.notice("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
280
+ }
281
+
282
+ private fun updateSuccessRate() {
283
+ val total = uploadSuccessCount + uploadFailureCount
284
+ uploadSuccessRate = if (total > 0) {
285
+ uploadSuccessCount.toDouble() / total.toDouble()
286
+ } else {
287
+ 1.0
288
+ }
289
+ }
290
+
291
+ // Convenience methods
292
+ fun recordUploadSuccess(durationMs: Long, bytes: Long = 0) = recordUploadDuration(durationMs, true, bytes)
293
+ fun recordUploadFailure() = recordEvent(TelemetryEventType.UPLOAD_FAILURE)
294
+ fun recordRetryAttempt() = recordEvent(TelemetryEventType.RETRY_ATTEMPT)
295
+ fun recordCircuitBreakerOpen() = recordEvent(TelemetryEventType.CIRCUIT_BREAKER_OPEN)
296
+ fun recordCircuitBreakerClose() = recordEvent(TelemetryEventType.CIRCUIT_BREAKER_CLOSE)
297
+ fun recordOfflinePersist() = recordEvent(TelemetryEventType.OFFLINE_QUEUE_PERSIST)
298
+ fun recordSessionStart() = recordEvent(TelemetryEventType.SESSION_START)
299
+ fun recordCrash() = recordEvent(TelemetryEventType.CRASH_DETECTED)
300
+ fun updateQueueDepth(depth: Int) = recordQueueDepth(depth)
301
+ }
@@ -0,0 +1,100 @@
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
+ /**
18
+ * Window and view utilities.
19
+ * Android implementation aligned with iOS utilities.
20
+ */
21
+ package com.rejourney.platform
22
+
23
+ import android.app.Activity
24
+ import android.content.Context
25
+ import android.view.View
26
+ import android.view.Window
27
+ import com.facebook.react.bridge.ReactApplicationContext
28
+ import java.security.SecureRandom
29
+
30
+ object WindowUtils {
31
+ private val random = SecureRandom()
32
+
33
+ /**
34
+ * Returns the current key window.
35
+ */
36
+ fun keyWindow(context: Context): Window? {
37
+ return try {
38
+ when (context) {
39
+ is ReactApplicationContext -> context.currentActivity?.window
40
+ is Activity -> context.window
41
+ else -> null
42
+ }
43
+ } catch (_: Exception) {
44
+ null
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Get the current activity's window.
50
+ */
51
+ fun getCurrentWindow(context: Context): Window? = keyWindow(context)
52
+
53
+ /**
54
+ * Get the current activity.
55
+ */
56
+ fun getCurrentActivity(context: Context): Activity? {
57
+ return try {
58
+ when (context) {
59
+ is ReactApplicationContext -> context.currentActivity
60
+ is Activity -> context
61
+ else -> null
62
+ }
63
+ } catch (_: Exception) {
64
+ null
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Finds the accessibility label for a view or its ancestors.
70
+ */
71
+ fun accessibilityLabelForView(view: View?): String? {
72
+ var current = view
73
+ while (current != null) {
74
+ val label = current.contentDescription?.toString()?.trim()
75
+ if (!label.isNullOrEmpty()) {
76
+ return label
77
+ }
78
+ val parent = current.parent
79
+ current = if (parent is View) parent else null
80
+ }
81
+ return null
82
+ }
83
+
84
+ /**
85
+ * Generates a unique session ID.
86
+ * Format: session_{timestamp}_{random_hex}
87
+ */
88
+ fun generateSessionId(): String {
89
+ val timestamp = System.currentTimeMillis()
90
+ val bytes = ByteArray(4)
91
+ random.nextBytes(bytes)
92
+ val hex = bytes.joinToString("") { "%02X".format(it) }
93
+ return "session_${timestamp}_$hex"
94
+ }
95
+
96
+ /**
97
+ * Returns the current timestamp in milliseconds.
98
+ */
99
+ fun currentTimestampMillis(): Long = System.currentTimeMillis()
100
+ }
@@ -0,0 +1,129 @@
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
+ package com.rejourney.recording
18
+
19
+ import android.os.Handler
20
+ import android.os.Looper
21
+ import com.rejourney.engine.DiagnosticLog
22
+ import java.util.concurrent.atomic.AtomicBoolean
23
+ import java.util.concurrent.atomic.AtomicInteger
24
+ import java.util.concurrent.atomic.AtomicLong
25
+ import kotlin.concurrent.thread
26
+
27
+ /**
28
+ * ANR (Application Not Responding) detection sentinel
29
+ * Android implementation aligned with iOS AnrSentinel.swift
30
+ *
31
+ * Uses a watchdog thread to detect main thread hangs > threshold
32
+ */
33
+ class AnrSentinel private constructor() {
34
+
35
+ companion object {
36
+ @Volatile
37
+ private var instance: AnrSentinel? = null
38
+
39
+ val shared: AnrSentinel
40
+ get() = instance ?: synchronized(this) {
41
+ instance ?: AnrSentinel().also { instance = it }
42
+ }
43
+ }
44
+
45
+ var currentSessionId: String? = null
46
+ var anrThresholdMs: Long = 5000L
47
+
48
+ private var watchdogThread: Thread? = null
49
+ private val isActive = AtomicBoolean(false)
50
+ private val lastResponseTime = AtomicLong(System.currentTimeMillis())
51
+ private val pingSequence = AtomicInteger(0)
52
+ private val pongSequence = AtomicInteger(0)
53
+
54
+ private val mainHandler = Handler(Looper.getMainLooper())
55
+
56
+ fun activate() {
57
+ if (isActive.getAndSet(true)) return
58
+
59
+ startWatchdog()
60
+ }
61
+
62
+ fun deactivate() {
63
+ if (!isActive.getAndSet(false)) return
64
+
65
+ watchdogThread?.interrupt()
66
+ watchdogThread = null
67
+ }
68
+
69
+ private fun startWatchdog() {
70
+ watchdogThread = thread(name = "RJ-ANR-Watchdog", isDaemon = true) {
71
+ val checkInterval = 1000L // 1 second
72
+
73
+ while (isActive.get() && !Thread.currentThread().isInterrupted) {
74
+ try {
75
+ // Send ping to main thread
76
+ val currentPing = pingSequence.incrementAndGet()
77
+
78
+ mainHandler.post {
79
+ // Main thread is responsive, update pong
80
+ pongSequence.set(currentPing)
81
+ lastResponseTime.set(System.currentTimeMillis())
82
+ }
83
+
84
+ Thread.sleep(checkInterval)
85
+
86
+ // Check if main thread responded
87
+ val elapsed = System.currentTimeMillis() - lastResponseTime.get()
88
+ val missedPongs = pingSequence.get() - pongSequence.get()
89
+
90
+ if (elapsed > anrThresholdMs && missedPongs > 0) {
91
+ captureAnr(elapsed)
92
+
93
+ // Reset to avoid duplicate reports
94
+ lastResponseTime.set(System.currentTimeMillis())
95
+ pongSequence.set(pingSequence.get())
96
+ }
97
+ } catch (e: InterruptedException) {
98
+ Thread.currentThread().interrupt()
99
+ break
100
+ } catch (e: Exception) {
101
+ DiagnosticLog.fault("ANR watchdog error: ${e.message}")
102
+ }
103
+ }
104
+ }
105
+ }
106
+
107
+ private fun captureAnr(durationMs: Long) {
108
+ try {
109
+ val mainThread = Looper.getMainLooper().thread
110
+ val stackTrace = mainThread.stackTrace
111
+
112
+ val frames = stackTrace.map { element ->
113
+ "${element.className}.${element.methodName}(${element.fileName}:${element.lineNumber})"
114
+ }
115
+
116
+ ReplayOrchestrator.shared?.incrementFaultTally()
117
+
118
+ // Route ANR through TelemetryPipeline so it arrives in the events
119
+ // batch and the backend ingest worker can insert it into the anrs table
120
+ val stackStr = frames.joinToString("\n")
121
+ TelemetryPipeline.shared?.recordAnrEvent(durationMs, stackStr)
122
+
123
+ DiagnosticLog.fault("ANR detected: ${durationMs}ms hang")
124
+
125
+ } catch (e: Exception) {
126
+ DiagnosticLog.fault("Failed to capture ANR: ${e.message}")
127
+ }
128
+ }
129
+ }