@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.
- package/README.md +29 -0
- package/android/build.gradle.kts +135 -0
- package/android/consumer-rules.pro +10 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +860 -0
- package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +290 -0
- package/android/src/main/java/com/rejourney/engine/DiagnosticLog.kt +385 -0
- package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +512 -0
- package/android/src/main/java/com/rejourney/platform/OEMDetector.kt +173 -0
- package/android/src/main/java/com/rejourney/platform/PerfTiming.kt +384 -0
- package/android/src/main/java/com/rejourney/platform/SessionLifecycleService.kt +160 -0
- package/android/src/main/java/com/rejourney/platform/Telemetry.kt +301 -0
- package/android/src/main/java/com/rejourney/platform/WindowUtils.kt +100 -0
- package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +129 -0
- package/android/src/main/java/com/rejourney/recording/EventBuffer.kt +330 -0
- package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +519 -0
- package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +740 -0
- package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +559 -0
- package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +238 -0
- package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +633 -0
- package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +232 -0
- package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +474 -0
- package/android/src/main/java/com/rejourney/utility/DataCompression.kt +63 -0
- package/android/src/main/java/com/rejourney/utility/ImageBlur.kt +412 -0
- package/android/src/main/java/com/rejourney/utility/ViewIdentifier.kt +169 -0
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +232 -0
- package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +268 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
- package/ios/Engine/DeviceRegistrar.swift +288 -0
- package/ios/Engine/DiagnosticLog.swift +387 -0
- package/ios/Engine/RejourneyImpl.swift +719 -0
- package/ios/Recording/AnrSentinel.swift +142 -0
- package/ios/Recording/EventBuffer.swift +326 -0
- package/ios/Recording/InteractionRecorder.swift +428 -0
- package/ios/Recording/ReplayOrchestrator.swift +624 -0
- package/ios/Recording/SegmentDispatcher.swift +492 -0
- package/ios/Recording/StabilityMonitor.swift +223 -0
- package/ios/Recording/TelemetryPipeline.swift +547 -0
- package/ios/Recording/ViewHierarchyScanner.swift +156 -0
- package/ios/Recording/VisualCapture.swift +675 -0
- package/ios/Rejourney.h +38 -0
- package/ios/Rejourney.mm +375 -0
- package/ios/Utility/DataCompression.swift +55 -0
- package/ios/Utility/ImageBlur.swift +89 -0
- package/ios/Utility/RuntimeMethodSwap.swift +41 -0
- package/ios/Utility/ViewIdentifier.swift +37 -0
- package/lib/commonjs/NativeRejourney.js +40 -0
- package/lib/commonjs/components/Mask.js +88 -0
- package/lib/commonjs/index.js +1443 -0
- package/lib/commonjs/sdk/autoTracking.js +1087 -0
- package/lib/commonjs/sdk/constants.js +166 -0
- package/lib/commonjs/sdk/errorTracking.js +187 -0
- package/lib/commonjs/sdk/index.js +50 -0
- package/lib/commonjs/sdk/metricsTracking.js +205 -0
- package/lib/commonjs/sdk/navigation.js +128 -0
- package/lib/commonjs/sdk/networkInterceptor.js +375 -0
- package/lib/commonjs/sdk/utils.js +433 -0
- package/lib/commonjs/sdk/version.js +13 -0
- package/lib/commonjs/types/expo-router.d.js +2 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/module/NativeRejourney.js +38 -0
- package/lib/module/components/Mask.js +83 -0
- package/lib/module/index.js +1341 -0
- package/lib/module/sdk/autoTracking.js +1059 -0
- package/lib/module/sdk/constants.js +154 -0
- package/lib/module/sdk/errorTracking.js +177 -0
- package/lib/module/sdk/index.js +26 -0
- package/lib/module/sdk/metricsTracking.js +187 -0
- package/lib/module/sdk/navigation.js +120 -0
- package/lib/module/sdk/networkInterceptor.js +364 -0
- package/lib/module/sdk/utils.js +412 -0
- package/lib/module/sdk/version.js +7 -0
- package/lib/module/types/expo-router.d.js +2 -0
- package/lib/module/types/index.js +2 -0
- package/lib/typescript/NativeRejourney.d.ts +160 -0
- package/lib/typescript/components/Mask.d.ts +54 -0
- package/lib/typescript/index.d.ts +117 -0
- package/lib/typescript/sdk/autoTracking.d.ts +226 -0
- package/lib/typescript/sdk/constants.d.ts +138 -0
- package/lib/typescript/sdk/errorTracking.d.ts +47 -0
- package/lib/typescript/sdk/index.d.ts +24 -0
- package/lib/typescript/sdk/metricsTracking.d.ts +75 -0
- package/lib/typescript/sdk/navigation.d.ts +48 -0
- package/lib/typescript/sdk/networkInterceptor.d.ts +62 -0
- package/lib/typescript/sdk/utils.d.ts +193 -0
- package/lib/typescript/sdk/version.d.ts +6 -0
- package/lib/typescript/types/index.d.ts +618 -0
- package/package.json +122 -0
- package/rejourney.podspec +23 -0
- package/src/NativeRejourney.ts +185 -0
- package/src/components/Mask.tsx +93 -0
- package/src/index.ts +1555 -0
- package/src/sdk/autoTracking.ts +1245 -0
- package/src/sdk/constants.ts +155 -0
- package/src/sdk/errorTracking.ts +231 -0
- package/src/sdk/index.ts +25 -0
- package/src/sdk/metricsTracking.ts +227 -0
- package/src/sdk/navigation.ts +152 -0
- package/src/sdk/networkInterceptor.ts +423 -0
- package/src/sdk/utils.ts +442 -0
- package/src/sdk/version.ts +6 -0
- package/src/types/expo-router.d.ts +7 -0
- 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
|
+
}
|