@rejourneyco/react-native 1.0.0
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/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 +2981 -0
- package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
- package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
- package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
- package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
- package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
- package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
- package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
- package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
- package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
- package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
- package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
- package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
- package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
- package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
- package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
- package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
- package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
- package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
- package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
- package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
- package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
- package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
- package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
- package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
- package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
- package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
- package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
- package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
- package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
- package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
- package/ios/Capture/RJANRHandler.h +42 -0
- package/ios/Capture/RJANRHandler.m +328 -0
- package/ios/Capture/RJCaptureEngine.h +275 -0
- package/ios/Capture/RJCaptureEngine.m +2062 -0
- package/ios/Capture/RJCaptureHeuristics.h +80 -0
- package/ios/Capture/RJCaptureHeuristics.m +903 -0
- package/ios/Capture/RJCrashHandler.h +46 -0
- package/ios/Capture/RJCrashHandler.m +313 -0
- package/ios/Capture/RJMotionEvent.h +183 -0
- package/ios/Capture/RJMotionEvent.m +183 -0
- package/ios/Capture/RJPerformanceManager.h +100 -0
- package/ios/Capture/RJPerformanceManager.m +373 -0
- package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
- package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
- package/ios/Capture/RJSegmentUploader.h +146 -0
- package/ios/Capture/RJSegmentUploader.m +778 -0
- package/ios/Capture/RJVideoEncoder.h +247 -0
- package/ios/Capture/RJVideoEncoder.m +1036 -0
- package/ios/Capture/RJViewControllerTracker.h +73 -0
- package/ios/Capture/RJViewControllerTracker.m +508 -0
- package/ios/Capture/RJViewHierarchyScanner.h +215 -0
- package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
- package/ios/Capture/RJViewSerializer.h +119 -0
- package/ios/Capture/RJViewSerializer.m +498 -0
- package/ios/Core/RJConstants.h +124 -0
- package/ios/Core/RJConstants.m +88 -0
- package/ios/Core/RJLifecycleManager.h +85 -0
- package/ios/Core/RJLifecycleManager.m +308 -0
- package/ios/Core/RJLogger.h +61 -0
- package/ios/Core/RJLogger.m +211 -0
- package/ios/Core/RJTypes.h +176 -0
- package/ios/Core/RJTypes.m +66 -0
- package/ios/Core/Rejourney.h +64 -0
- package/ios/Core/Rejourney.mm +2495 -0
- package/ios/Network/RJDeviceAuthManager.h +94 -0
- package/ios/Network/RJDeviceAuthManager.m +967 -0
- package/ios/Network/RJNetworkMonitor.h +68 -0
- package/ios/Network/RJNetworkMonitor.m +267 -0
- package/ios/Network/RJRetryManager.h +73 -0
- package/ios/Network/RJRetryManager.m +325 -0
- package/ios/Network/RJUploadManager.h +267 -0
- package/ios/Network/RJUploadManager.m +2296 -0
- package/ios/Privacy/RJPrivacyMask.h +163 -0
- package/ios/Privacy/RJPrivacyMask.m +922 -0
- package/ios/Rejourney.h +63 -0
- package/ios/Touch/RJGestureClassifier.h +130 -0
- package/ios/Touch/RJGestureClassifier.m +333 -0
- package/ios/Touch/RJTouchInterceptor.h +169 -0
- package/ios/Touch/RJTouchInterceptor.m +772 -0
- package/ios/Utils/RJEventBuffer.h +112 -0
- package/ios/Utils/RJEventBuffer.m +358 -0
- package/ios/Utils/RJGzipUtils.h +33 -0
- package/ios/Utils/RJGzipUtils.m +89 -0
- package/ios/Utils/RJKeychainManager.h +48 -0
- package/ios/Utils/RJKeychainManager.m +111 -0
- package/ios/Utils/RJPerfTiming.h +209 -0
- package/ios/Utils/RJPerfTiming.m +264 -0
- package/ios/Utils/RJTelemetry.h +92 -0
- package/ios/Utils/RJTelemetry.m +320 -0
- package/ios/Utils/RJWindowUtils.h +66 -0
- package/ios/Utils/RJWindowUtils.m +133 -0
- package/lib/commonjs/NativeRejourney.js +40 -0
- package/lib/commonjs/components/Mask.js +79 -0
- package/lib/commonjs/index.js +1381 -0
- package/lib/commonjs/sdk/autoTracking.js +1259 -0
- package/lib/commonjs/sdk/constants.js +151 -0
- package/lib/commonjs/sdk/errorTracking.js +199 -0
- package/lib/commonjs/sdk/index.js +50 -0
- package/lib/commonjs/sdk/metricsTracking.js +204 -0
- package/lib/commonjs/sdk/navigation.js +151 -0
- package/lib/commonjs/sdk/networkInterceptor.js +412 -0
- package/lib/commonjs/sdk/utils.js +363 -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 +72 -0
- package/lib/module/index.js +1284 -0
- package/lib/module/sdk/autoTracking.js +1233 -0
- package/lib/module/sdk/constants.js +145 -0
- package/lib/module/sdk/errorTracking.js +189 -0
- package/lib/module/sdk/index.js +12 -0
- package/lib/module/sdk/metricsTracking.js +187 -0
- package/lib/module/sdk/navigation.js +143 -0
- package/lib/module/sdk/networkInterceptor.js +401 -0
- package/lib/module/sdk/utils.js +342 -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 +147 -0
- package/lib/typescript/components/Mask.d.ts +39 -0
- package/lib/typescript/index.d.ts +117 -0
- package/lib/typescript/sdk/autoTracking.d.ts +204 -0
- package/lib/typescript/sdk/constants.d.ts +120 -0
- package/lib/typescript/sdk/errorTracking.d.ts +32 -0
- package/lib/typescript/sdk/index.d.ts +9 -0
- package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
- package/lib/typescript/sdk/navigation.d.ts +33 -0
- package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
- package/lib/typescript/sdk/utils.d.ts +148 -0
- package/lib/typescript/types/index.d.ts +624 -0
- package/package.json +102 -0
- package/rejourney.podspec +21 -0
- package/src/NativeRejourney.ts +165 -0
- package/src/components/Mask.tsx +80 -0
- package/src/index.ts +1459 -0
- package/src/sdk/autoTracking.ts +1373 -0
- package/src/sdk/constants.ts +134 -0
- package/src/sdk/errorTracking.ts +231 -0
- package/src/sdk/index.ts +11 -0
- package/src/sdk/metricsTracking.ts +232 -0
- package/src/sdk/navigation.ts +157 -0
- package/src/sdk/networkInterceptor.ts +440 -0
- package/src/sdk/utils.ts +369 -0
- package/src/types/expo-router.d.ts +7 -0
- package/src/types/index.ts +739 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Old Architecture (Bridge) module wrapper for Rejourney SDK.
|
|
3
|
+
*
|
|
4
|
+
* This thin wrapper extends ReactContextBaseJavaModule for the Old Architecture
|
|
5
|
+
* and delegates all method implementations to RejourneyModuleImpl.
|
|
6
|
+
*
|
|
7
|
+
* This file is compiled when newArchEnabled=false in gradle.properties.
|
|
8
|
+
*/
|
|
9
|
+
package com.rejourney
|
|
10
|
+
|
|
11
|
+
import com.facebook.react.bridge.*
|
|
12
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
13
|
+
import com.rejourney.core.Logger
|
|
14
|
+
|
|
15
|
+
@ReactModule(name = RejourneyModule.NAME)
|
|
16
|
+
class RejourneyModule(reactContext: ReactApplicationContext) :
|
|
17
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
18
|
+
|
|
19
|
+
companion object {
|
|
20
|
+
const val NAME = RejourneyModuleImpl.NAME
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Lazy initialization - create impl only when first method is called
|
|
24
|
+
// This ensures the module constructor completes successfully for React Native
|
|
25
|
+
private val impl: RejourneyModuleImpl by lazy {
|
|
26
|
+
try {
|
|
27
|
+
RejourneyModuleImpl(reactContext, isNewArchitecture = false)
|
|
28
|
+
} catch (e: Throwable) {
|
|
29
|
+
Logger.error("✗ CRITICAL: Failed to create RejourneyModuleImpl", e)
|
|
30
|
+
throw e // Re-throw to make the error visible
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
override fun getName(): String = NAME
|
|
35
|
+
|
|
36
|
+
override fun invalidate() {
|
|
37
|
+
impl?.invalidate()
|
|
38
|
+
super.invalidate()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@ReactMethod
|
|
42
|
+
fun startSession(userId: String, apiUrl: String, publicKey: String, promise: Promise) {
|
|
43
|
+
try {
|
|
44
|
+
impl.startSession(userId, apiUrl, publicKey, promise)
|
|
45
|
+
} catch (e: Exception) {
|
|
46
|
+
promise.resolve(createErrorMap("Module initialization failed: ${e.message}"))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@ReactMethod
|
|
51
|
+
fun stopSession(promise: Promise) {
|
|
52
|
+
try {
|
|
53
|
+
impl.stopSession(promise)
|
|
54
|
+
} catch (e: Exception) {
|
|
55
|
+
promise.resolve(createErrorMap("Module initialization failed: ${e.message}"))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@ReactMethod
|
|
60
|
+
fun logEvent(eventType: String, details: ReadableMap, promise: Promise) {
|
|
61
|
+
try {
|
|
62
|
+
impl.logEvent(eventType, details, promise)
|
|
63
|
+
} catch (e: Exception) {
|
|
64
|
+
promise.resolve(createErrorMap("Module initialization failed: ${e.message}"))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@ReactMethod
|
|
69
|
+
fun screenChanged(screenName: String, promise: Promise) {
|
|
70
|
+
try {
|
|
71
|
+
impl.screenChanged(screenName, promise)
|
|
72
|
+
} catch (e: Exception) {
|
|
73
|
+
promise.resolve(createErrorMap("Module initialization failed: ${e.message}"))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@ReactMethod
|
|
78
|
+
fun onScroll(offsetY: Double, promise: Promise) {
|
|
79
|
+
try {
|
|
80
|
+
impl.onScroll(offsetY, promise)
|
|
81
|
+
} catch (e: Exception) {
|
|
82
|
+
promise.resolve(createErrorMap("Module initialization failed: ${e.message}"))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@ReactMethod
|
|
87
|
+
fun markVisualChange(reason: String, importance: String, promise: Promise) {
|
|
88
|
+
try {
|
|
89
|
+
impl.markVisualChange(reason, importance, promise)
|
|
90
|
+
} catch (e: Exception) {
|
|
91
|
+
promise.resolve(false)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@ReactMethod
|
|
96
|
+
fun onExternalURLOpened(urlScheme: String, promise: Promise) {
|
|
97
|
+
try {
|
|
98
|
+
impl.onExternalURLOpened(urlScheme, promise)
|
|
99
|
+
} catch (e: Exception) {
|
|
100
|
+
promise.resolve(createErrorMap("Module initialization failed: ${e.message}"))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@ReactMethod
|
|
105
|
+
fun onOAuthStarted(provider: String, promise: Promise) {
|
|
106
|
+
try {
|
|
107
|
+
impl.onOAuthStarted(provider, promise)
|
|
108
|
+
} catch (e: Exception) {
|
|
109
|
+
promise.resolve(createErrorMap("Module initialization failed: ${e.message}"))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@ReactMethod
|
|
114
|
+
fun onOAuthCompleted(provider: String, success: Boolean, promise: Promise) {
|
|
115
|
+
try {
|
|
116
|
+
impl.onOAuthCompleted(provider, success, promise)
|
|
117
|
+
} catch (e: Exception) {
|
|
118
|
+
promise.resolve(createErrorMap("Module initialization failed: ${e.message}"))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@ReactMethod
|
|
123
|
+
fun getSDKMetrics(promise: Promise) {
|
|
124
|
+
try {
|
|
125
|
+
impl.getSDKMetrics(promise)
|
|
126
|
+
} catch (e: Exception) {
|
|
127
|
+
promise.resolve(Arguments.createMap())
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@ReactMethod
|
|
132
|
+
fun debugCrash() {
|
|
133
|
+
try {
|
|
134
|
+
impl.debugCrash()
|
|
135
|
+
} catch (e: Exception) {
|
|
136
|
+
Logger.error("debugCrash failed", e)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@ReactMethod
|
|
141
|
+
fun debugTriggerANR(durationMs: Double) {
|
|
142
|
+
try {
|
|
143
|
+
impl.debugTriggerANR(durationMs)
|
|
144
|
+
} catch (e: Exception) {
|
|
145
|
+
Logger.error("debugTriggerANR failed", e)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@ReactMethod
|
|
150
|
+
fun getSessionId(promise: Promise) {
|
|
151
|
+
try {
|
|
152
|
+
impl.getSessionId(promise)
|
|
153
|
+
} catch (e: Exception) {
|
|
154
|
+
promise.resolve(null)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@ReactMethod
|
|
159
|
+
fun maskViewByNativeID(nativeID: String, promise: Promise) {
|
|
160
|
+
try {
|
|
161
|
+
impl.maskViewByNativeID(nativeID, promise)
|
|
162
|
+
} catch (e: Exception) {
|
|
163
|
+
promise.resolve(createErrorMap("Module initialization failed: ${e.message}"))
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@ReactMethod
|
|
168
|
+
fun unmaskViewByNativeID(nativeID: String, promise: Promise) {
|
|
169
|
+
try {
|
|
170
|
+
impl.unmaskViewByNativeID(nativeID, promise)
|
|
171
|
+
} catch (e: Exception) {
|
|
172
|
+
promise.resolve(createErrorMap("Module initialization failed: ${e.message}"))
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@ReactMethod
|
|
177
|
+
fun setUserIdentity(userId: String, promise: Promise) {
|
|
178
|
+
try {
|
|
179
|
+
impl.setUserIdentity(userId, promise)
|
|
180
|
+
} catch (e: Exception) {
|
|
181
|
+
promise.resolve(createErrorMap("Module initialization failed: ${e.message}"))
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@ReactMethod
|
|
186
|
+
fun setDebugMode(enabled: Boolean, promise: Promise) {
|
|
187
|
+
try {
|
|
188
|
+
impl.setDebugMode(enabled, promise)
|
|
189
|
+
} catch (e: Exception) {
|
|
190
|
+
promise.resolve(createErrorMap("Module initialization failed: ${e.message}"))
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@ReactMethod
|
|
195
|
+
fun setLogLevel(level: String, promise: Promise) {
|
|
196
|
+
try {
|
|
197
|
+
val logLevel = when (level.uppercase()) {
|
|
198
|
+
"DEBUG" -> com.rejourney.core.LogLevel.DEBUG
|
|
199
|
+
"INFO" -> com.rejourney.core.LogLevel.INFO
|
|
200
|
+
"WARNING" -> com.rejourney.core.LogLevel.WARNING
|
|
201
|
+
"ERROR" -> com.rejourney.core.LogLevel.ERROR
|
|
202
|
+
"SILENT" -> com.rejourney.core.LogLevel.SILENT
|
|
203
|
+
else -> com.rejourney.core.LogLevel.ERROR
|
|
204
|
+
}
|
|
205
|
+
com.rejourney.core.Logger.setLogLevel(logLevel)
|
|
206
|
+
promise.resolve(true)
|
|
207
|
+
} catch (e: Exception) {
|
|
208
|
+
promise.resolve(false)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private fun createErrorMap(error: String): WritableMap {
|
|
213
|
+
return Arguments.createMap().apply {
|
|
214
|
+
putBoolean("success", false)
|
|
215
|
+
putString("error", error)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Old Architecture (Bridge) package registration for Rejourney SDK.
|
|
3
|
+
*
|
|
4
|
+
* This package is compiled when newArchEnabled=false in gradle.properties.
|
|
5
|
+
* It uses the standard ReactPackage interface for legacy Native Modules.
|
|
6
|
+
*/
|
|
7
|
+
package com.rejourney
|
|
8
|
+
|
|
9
|
+
import com.facebook.react.ReactPackage
|
|
10
|
+
import com.facebook.react.bridge.NativeModule
|
|
11
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
12
|
+
import com.facebook.react.uimanager.ViewManager
|
|
13
|
+
|
|
14
|
+
class RejourneyPackage : ReactPackage {
|
|
15
|
+
|
|
16
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
17
|
+
return listOf(RejourneyModule(reactContext))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
21
|
+
return emptyList()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
//
|
|
2
|
+
// RJANRHandler.h
|
|
3
|
+
// Rejourney
|
|
4
|
+
//
|
|
5
|
+
// Detects Application Not Responding (ANR) conditions using a watchdog timer.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#import <Foundation/Foundation.h>
|
|
9
|
+
|
|
10
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
11
|
+
|
|
12
|
+
@protocol RJANRHandlerDelegate <NSObject>
|
|
13
|
+
@optional
|
|
14
|
+
/// Called when an ANR is detected
|
|
15
|
+
- (void)anrDetectedWithDuration:(NSTimeInterval)duration
|
|
16
|
+
threadState:(nullable NSString *)threadState;
|
|
17
|
+
@end
|
|
18
|
+
|
|
19
|
+
@interface RJANRHandler : NSObject
|
|
20
|
+
|
|
21
|
+
@property(nonatomic, weak, nullable) id<RJANRHandlerDelegate> delegate;
|
|
22
|
+
|
|
23
|
+
/// ANR threshold in seconds (default: 5.0)
|
|
24
|
+
@property(nonatomic, assign) NSTimeInterval threshold;
|
|
25
|
+
|
|
26
|
+
+ (instancetype)sharedInstance;
|
|
27
|
+
|
|
28
|
+
/// Starts ANR monitoring
|
|
29
|
+
- (void)startMonitoring;
|
|
30
|
+
|
|
31
|
+
/// Stops ANR monitoring
|
|
32
|
+
- (void)stopMonitoring;
|
|
33
|
+
|
|
34
|
+
/// Checks if there is a pending ANR report from a previous launch
|
|
35
|
+
- (BOOL)hasPendingANRReport;
|
|
36
|
+
|
|
37
|
+
/// Loads the pending ANR report and clears it from disk
|
|
38
|
+
- (nullable NSDictionary *)loadAndPurgePendingANRReport;
|
|
39
|
+
|
|
40
|
+
@end
|
|
41
|
+
|
|
42
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
//
|
|
2
|
+
// RJANRHandler.m
|
|
3
|
+
// Rejourney
|
|
4
|
+
//
|
|
5
|
+
// Watchdog-based ANR detection for iOS with main thread stack capture.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
#import "RJANRHandler.h"
|
|
9
|
+
#import "../Core/RJLogger.h"
|
|
10
|
+
#import <UIKit/UIKit.h>
|
|
11
|
+
#import <mach/mach.h>
|
|
12
|
+
#import <pthread.h>
|
|
13
|
+
|
|
14
|
+
static NSString *const kRJANRReportFileName = @"rj_anr_report.json";
|
|
15
|
+
static NSString *const kRJCurrentSessionIdKey = @"rj_current_session_id";
|
|
16
|
+
|
|
17
|
+
static const NSTimeInterval kDefaultANRThreshold = 5.0;
|
|
18
|
+
|
|
19
|
+
static const NSTimeInterval kWatchdogInterval = 2.0;
|
|
20
|
+
|
|
21
|
+
@interface RJANRHandler ()
|
|
22
|
+
@property(nonatomic, strong, nullable) dispatch_queue_t watchdogQueue;
|
|
23
|
+
@property(nonatomic, strong, nullable) dispatch_source_t watchdogTimer;
|
|
24
|
+
@property(nonatomic, assign) BOOL isMonitoring;
|
|
25
|
+
@property(nonatomic, assign) CFAbsoluteTime lastPingTime;
|
|
26
|
+
@property(nonatomic, assign) BOOL mainThreadResponded;
|
|
27
|
+
@end
|
|
28
|
+
|
|
29
|
+
@implementation RJANRHandler
|
|
30
|
+
|
|
31
|
+
+ (instancetype)sharedInstance {
|
|
32
|
+
static RJANRHandler *sharedInstance = nil;
|
|
33
|
+
static dispatch_once_t onceToken;
|
|
34
|
+
dispatch_once(&onceToken, ^{
|
|
35
|
+
sharedInstance = [[self alloc] init];
|
|
36
|
+
});
|
|
37
|
+
return sharedInstance;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
- (instancetype)init {
|
|
41
|
+
self = [super init];
|
|
42
|
+
if (self) {
|
|
43
|
+
_threshold = kDefaultANRThreshold;
|
|
44
|
+
_isMonitoring = NO;
|
|
45
|
+
_mainThreadResponded = YES;
|
|
46
|
+
}
|
|
47
|
+
return self;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
- (void)startMonitoring {
|
|
51
|
+
if (self.isMonitoring) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
self.isMonitoring = YES;
|
|
56
|
+
self.mainThreadResponded = YES;
|
|
57
|
+
self.lastPingTime = CFAbsoluteTimeGetCurrent();
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
self.watchdogQueue = dispatch_queue_create("com.rejourney.anr.watchdog",
|
|
62
|
+
DISPATCH_QUEUE_SERIAL);
|
|
63
|
+
dispatch_set_target_queue(
|
|
64
|
+
self.watchdogQueue,
|
|
65
|
+
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
self.watchdogTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,
|
|
69
|
+
self.watchdogQueue);
|
|
70
|
+
|
|
71
|
+
dispatch_source_set_timer(
|
|
72
|
+
self.watchdogTimer,
|
|
73
|
+
dispatch_time(DISPATCH_TIME_NOW,
|
|
74
|
+
(int64_t)(kWatchdogInterval * NSEC_PER_SEC)),
|
|
75
|
+
(int64_t)(kWatchdogInterval * NSEC_PER_SEC),
|
|
76
|
+
(int64_t)(0.1 * NSEC_PER_SEC));
|
|
77
|
+
|
|
78
|
+
__weak typeof(self) weakSelf = self;
|
|
79
|
+
dispatch_source_set_event_handler(self.watchdogTimer, ^{
|
|
80
|
+
[weakSelf checkMainThread];
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
dispatch_resume(self.watchdogTimer);
|
|
84
|
+
RJLogDebug(@"RJANRHandler started monitoring (threshold: %.1fs)",
|
|
85
|
+
self.threshold);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
- (void)stopMonitoring {
|
|
89
|
+
if (!self.isMonitoring) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
self.isMonitoring = NO;
|
|
94
|
+
|
|
95
|
+
if (self.watchdogTimer) {
|
|
96
|
+
dispatch_source_cancel(self.watchdogTimer);
|
|
97
|
+
self.watchdogTimer = nil;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
self.watchdogQueue = nil;
|
|
101
|
+
RJLogDebug(@"RJANRHandler stopped monitoring");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
- (void)checkMainThread {
|
|
105
|
+
if (!self.isMonitoring) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
CFAbsoluteTime now = CFAbsoluteTimeGetCurrent();
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if (!self.mainThreadResponded) {
|
|
113
|
+
NSTimeInterval elapsed = now - self.lastPingTime;
|
|
114
|
+
|
|
115
|
+
if (elapsed >= self.threshold) {
|
|
116
|
+
|
|
117
|
+
[self handleANRWithDuration:elapsed];
|
|
118
|
+
|
|
119
|
+
self.mainThreadResponded = YES;
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
self.mainThreadResponded = NO;
|
|
126
|
+
self.lastPingTime = now;
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
CFAbsoluteTime pingTime = now;
|
|
131
|
+
|
|
132
|
+
__weak typeof(self) weakSelf = self;
|
|
133
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
134
|
+
CFAbsoluteTime responseTime = CFAbsoluteTimeGetCurrent();
|
|
135
|
+
__strong typeof(weakSelf) strongSelf = weakSelf;
|
|
136
|
+
if (!strongSelf) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
dispatch_async(strongSelf.watchdogQueue, ^{
|
|
142
|
+
if (!strongSelf.isMonitoring) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
NSTimeInterval elapsed = responseTime - pingTime;
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
NSTimeInterval effectiveThreshold = strongSelf.threshold * 0.9;
|
|
152
|
+
if (!strongSelf.mainThreadResponded && elapsed >= effectiveThreshold) {
|
|
153
|
+
[strongSelf handleANRWithDuration:elapsed];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
strongSelf.mainThreadResponded = YES;
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
- (void)handleANRWithDuration:(NSTimeInterval)duration {
|
|
162
|
+
RJLogDebug(@"[ANR] ANR DETECTED! Duration: %.2fs - main thread blocked",
|
|
163
|
+
duration);
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
NSString *threadState = [self captureThreadState];
|
|
168
|
+
RJLogDebug(@"[ANR] Captured thread state (%lu bytes)",
|
|
169
|
+
(unsigned long)threadState.length);
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
NSDictionary *report = [self buildANRReportWithDuration:duration
|
|
173
|
+
threadState:threadState];
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
[self persistANRReport:report];
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if ([self.delegate respondsToSelector:@selector(anrDetectedWithDuration:
|
|
181
|
+
threadState:)]) {
|
|
182
|
+
[self.delegate anrDetectedWithDuration:duration threadState:threadState];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
- (NSString *)captureThreadState {
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
NSMutableString *threadState = [NSMutableString new];
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
thread_act_array_t threads;
|
|
195
|
+
mach_msg_type_number_t threadCount;
|
|
196
|
+
|
|
197
|
+
if (task_threads(mach_task_self(), &threads, &threadCount) == KERN_SUCCESS &&
|
|
198
|
+
threadCount > 0) {
|
|
199
|
+
|
|
200
|
+
thread_t mainThread = threads[0];
|
|
201
|
+
|
|
202
|
+
[threadState appendString:@"Main Thread Stack (blocked):\n"];
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
#if defined(__arm64__)
|
|
206
|
+
arm_thread_state64_t state;
|
|
207
|
+
mach_msg_type_number_t stateCount = ARM_THREAD_STATE64_COUNT;
|
|
208
|
+
if (thread_get_state(mainThread, ARM_THREAD_STATE64, (thread_state_t)&state,
|
|
209
|
+
&stateCount) == KERN_SUCCESS) {
|
|
210
|
+
[threadState
|
|
211
|
+
appendFormat:@"PC: 0x%llx\n", arm_thread_state64_get_pc(state)];
|
|
212
|
+
[threadState appendFormat:@"LR: 0x%llx\n", (uint64_t)state.__lr];
|
|
213
|
+
[threadState
|
|
214
|
+
appendFormat:@"SP: 0x%llx\n", arm_thread_state64_get_sp(state)];
|
|
215
|
+
}
|
|
216
|
+
#elif defined(__x86_64__)
|
|
217
|
+
x86_thread_state64_t state;
|
|
218
|
+
mach_msg_type_number_t stateCount = x86_THREAD_STATE64_COUNT;
|
|
219
|
+
if (thread_get_state(mainThread, x86_THREAD_STATE64, (thread_state_t)&state,
|
|
220
|
+
&stateCount) == KERN_SUCCESS) {
|
|
221
|
+
[threadState appendFormat:@"RIP: 0x%llx\n", state.__rip];
|
|
222
|
+
[threadState appendFormat:@"RSP: 0x%llx\n", state.__rsp];
|
|
223
|
+
}
|
|
224
|
+
#endif
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
for (mach_msg_type_number_t i = 0; i < threadCount; i++) {
|
|
228
|
+
mach_port_deallocate(mach_task_self(), threads[i]);
|
|
229
|
+
}
|
|
230
|
+
vm_deallocate(mach_task_self(), (vm_address_t)threads,
|
|
231
|
+
threadCount * sizeof(thread_act_t));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
NSArray<NSString *> *watchdogSymbols = [NSThread callStackSymbols];
|
|
236
|
+
[threadState appendString:@"\nWatchdog Thread Stack:\n"];
|
|
237
|
+
[threadState appendString:[watchdogSymbols componentsJoinedByString:@"\n"]];
|
|
238
|
+
|
|
239
|
+
return threadState;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
- (NSDictionary *)buildANRReportWithDuration:(NSTimeInterval)duration
|
|
243
|
+
threadState:(nullable NSString *)threadState {
|
|
244
|
+
NSString *sessionId = [[NSUserDefaults standardUserDefaults]
|
|
245
|
+
stringForKey:kRJCurrentSessionIdKey];
|
|
246
|
+
|
|
247
|
+
NSMutableDictionary *report = [NSMutableDictionary new];
|
|
248
|
+
report[@"timestamp"] = @([[NSDate date] timeIntervalSince1970] * 1000);
|
|
249
|
+
report[@"durationMs"] = @((NSInteger)(duration * 1000));
|
|
250
|
+
report[@"type"] = @"anr";
|
|
251
|
+
|
|
252
|
+
if (sessionId) {
|
|
253
|
+
report[@"sessionId"] = sessionId;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (threadState) {
|
|
257
|
+
report[@"threadState"] = threadState;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
UIDevice *device = [UIDevice currentDevice];
|
|
262
|
+
report[@"deviceMetadata"] = @{
|
|
263
|
+
@"model" : device.model,
|
|
264
|
+
@"systemName" : device.systemName,
|
|
265
|
+
@"systemVersion" : device.systemVersion,
|
|
266
|
+
@"identifierForVendor" : device.identifierForVendor.UUIDString ?: @"unknown"
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
return [report copy];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
- (void)persistANRReport:(NSDictionary *)report {
|
|
273
|
+
NSError *error = nil;
|
|
274
|
+
NSData *jsonData =
|
|
275
|
+
[NSJSONSerialization dataWithJSONObject:report
|
|
276
|
+
options:NSJSONWritingPrettyPrinted
|
|
277
|
+
error:&error];
|
|
278
|
+
if (error) {
|
|
279
|
+
RJLogError(@"Failed to serialize ANR report: %@",
|
|
280
|
+
error.localizedDescription);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
NSString *path = [self anrReportPath];
|
|
285
|
+
[jsonData writeToFile:path atomically:YES];
|
|
286
|
+
RJLogDebug(@"ANR report persisted to disk");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
- (NSString *)anrReportPath {
|
|
290
|
+
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,
|
|
291
|
+
NSUserDomainMask, YES);
|
|
292
|
+
NSString *cacheDir = [paths objectAtIndex:0];
|
|
293
|
+
return [cacheDir stringByAppendingPathComponent:kRJANRReportFileName];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#pragma mark - Pending Report Methods
|
|
297
|
+
|
|
298
|
+
- (BOOL)hasPendingANRReport {
|
|
299
|
+
NSString *path = [self anrReportPath];
|
|
300
|
+
return [[NSFileManager defaultManager] fileExistsAtPath:path];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
- (NSDictionary *)loadAndPurgePendingANRReport {
|
|
304
|
+
NSString *path = [self anrReportPath];
|
|
305
|
+
if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
|
|
306
|
+
return nil;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
NSData *data = [NSData dataWithContentsOfFile:path];
|
|
310
|
+
if (!data) {
|
|
311
|
+
return nil;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
NSDictionary *report = [NSJSONSerialization JSONObjectWithData:data
|
|
315
|
+
options:0
|
|
316
|
+
error:nil];
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
[[NSFileManager defaultManager] removeItemAtPath:path error:nil];
|
|
320
|
+
|
|
321
|
+
return report;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
- (void)dealloc {
|
|
325
|
+
[self stopMonitoring];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
@end
|