@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.
Files changed (152) hide show
  1. package/android/build.gradle.kts +135 -0
  2. package/android/consumer-rules.pro +10 -0
  3. package/android/proguard-rules.pro +1 -0
  4. package/android/src/main/AndroidManifest.xml +15 -0
  5. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +2981 -0
  6. package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
  7. package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
  8. package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
  9. package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
  10. package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
  11. package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
  12. package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
  13. package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
  14. package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
  15. package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
  16. package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
  17. package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
  18. package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
  19. package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
  20. package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
  21. package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
  22. package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
  23. package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
  24. package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
  25. package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
  26. package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
  27. package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
  28. package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
  29. package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
  30. package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
  31. package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
  32. package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
  33. package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
  34. package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
  35. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
  36. package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
  37. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
  38. package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
  39. package/ios/Capture/RJANRHandler.h +42 -0
  40. package/ios/Capture/RJANRHandler.m +328 -0
  41. package/ios/Capture/RJCaptureEngine.h +275 -0
  42. package/ios/Capture/RJCaptureEngine.m +2062 -0
  43. package/ios/Capture/RJCaptureHeuristics.h +80 -0
  44. package/ios/Capture/RJCaptureHeuristics.m +903 -0
  45. package/ios/Capture/RJCrashHandler.h +46 -0
  46. package/ios/Capture/RJCrashHandler.m +313 -0
  47. package/ios/Capture/RJMotionEvent.h +183 -0
  48. package/ios/Capture/RJMotionEvent.m +183 -0
  49. package/ios/Capture/RJPerformanceManager.h +100 -0
  50. package/ios/Capture/RJPerformanceManager.m +373 -0
  51. package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
  52. package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
  53. package/ios/Capture/RJSegmentUploader.h +146 -0
  54. package/ios/Capture/RJSegmentUploader.m +778 -0
  55. package/ios/Capture/RJVideoEncoder.h +247 -0
  56. package/ios/Capture/RJVideoEncoder.m +1036 -0
  57. package/ios/Capture/RJViewControllerTracker.h +73 -0
  58. package/ios/Capture/RJViewControllerTracker.m +508 -0
  59. package/ios/Capture/RJViewHierarchyScanner.h +215 -0
  60. package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
  61. package/ios/Capture/RJViewSerializer.h +119 -0
  62. package/ios/Capture/RJViewSerializer.m +498 -0
  63. package/ios/Core/RJConstants.h +124 -0
  64. package/ios/Core/RJConstants.m +88 -0
  65. package/ios/Core/RJLifecycleManager.h +85 -0
  66. package/ios/Core/RJLifecycleManager.m +308 -0
  67. package/ios/Core/RJLogger.h +61 -0
  68. package/ios/Core/RJLogger.m +211 -0
  69. package/ios/Core/RJTypes.h +176 -0
  70. package/ios/Core/RJTypes.m +66 -0
  71. package/ios/Core/Rejourney.h +64 -0
  72. package/ios/Core/Rejourney.mm +2495 -0
  73. package/ios/Network/RJDeviceAuthManager.h +94 -0
  74. package/ios/Network/RJDeviceAuthManager.m +967 -0
  75. package/ios/Network/RJNetworkMonitor.h +68 -0
  76. package/ios/Network/RJNetworkMonitor.m +267 -0
  77. package/ios/Network/RJRetryManager.h +73 -0
  78. package/ios/Network/RJRetryManager.m +325 -0
  79. package/ios/Network/RJUploadManager.h +267 -0
  80. package/ios/Network/RJUploadManager.m +2296 -0
  81. package/ios/Privacy/RJPrivacyMask.h +163 -0
  82. package/ios/Privacy/RJPrivacyMask.m +922 -0
  83. package/ios/Rejourney.h +63 -0
  84. package/ios/Touch/RJGestureClassifier.h +130 -0
  85. package/ios/Touch/RJGestureClassifier.m +333 -0
  86. package/ios/Touch/RJTouchInterceptor.h +169 -0
  87. package/ios/Touch/RJTouchInterceptor.m +772 -0
  88. package/ios/Utils/RJEventBuffer.h +112 -0
  89. package/ios/Utils/RJEventBuffer.m +358 -0
  90. package/ios/Utils/RJGzipUtils.h +33 -0
  91. package/ios/Utils/RJGzipUtils.m +89 -0
  92. package/ios/Utils/RJKeychainManager.h +48 -0
  93. package/ios/Utils/RJKeychainManager.m +111 -0
  94. package/ios/Utils/RJPerfTiming.h +209 -0
  95. package/ios/Utils/RJPerfTiming.m +264 -0
  96. package/ios/Utils/RJTelemetry.h +92 -0
  97. package/ios/Utils/RJTelemetry.m +320 -0
  98. package/ios/Utils/RJWindowUtils.h +66 -0
  99. package/ios/Utils/RJWindowUtils.m +133 -0
  100. package/lib/commonjs/NativeRejourney.js +40 -0
  101. package/lib/commonjs/components/Mask.js +79 -0
  102. package/lib/commonjs/index.js +1381 -0
  103. package/lib/commonjs/sdk/autoTracking.js +1259 -0
  104. package/lib/commonjs/sdk/constants.js +151 -0
  105. package/lib/commonjs/sdk/errorTracking.js +199 -0
  106. package/lib/commonjs/sdk/index.js +50 -0
  107. package/lib/commonjs/sdk/metricsTracking.js +204 -0
  108. package/lib/commonjs/sdk/navigation.js +151 -0
  109. package/lib/commonjs/sdk/networkInterceptor.js +412 -0
  110. package/lib/commonjs/sdk/utils.js +363 -0
  111. package/lib/commonjs/types/expo-router.d.js +2 -0
  112. package/lib/commonjs/types/index.js +2 -0
  113. package/lib/module/NativeRejourney.js +38 -0
  114. package/lib/module/components/Mask.js +72 -0
  115. package/lib/module/index.js +1284 -0
  116. package/lib/module/sdk/autoTracking.js +1233 -0
  117. package/lib/module/sdk/constants.js +145 -0
  118. package/lib/module/sdk/errorTracking.js +189 -0
  119. package/lib/module/sdk/index.js +12 -0
  120. package/lib/module/sdk/metricsTracking.js +187 -0
  121. package/lib/module/sdk/navigation.js +143 -0
  122. package/lib/module/sdk/networkInterceptor.js +401 -0
  123. package/lib/module/sdk/utils.js +342 -0
  124. package/lib/module/types/expo-router.d.js +2 -0
  125. package/lib/module/types/index.js +2 -0
  126. package/lib/typescript/NativeRejourney.d.ts +147 -0
  127. package/lib/typescript/components/Mask.d.ts +39 -0
  128. package/lib/typescript/index.d.ts +117 -0
  129. package/lib/typescript/sdk/autoTracking.d.ts +204 -0
  130. package/lib/typescript/sdk/constants.d.ts +120 -0
  131. package/lib/typescript/sdk/errorTracking.d.ts +32 -0
  132. package/lib/typescript/sdk/index.d.ts +9 -0
  133. package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
  134. package/lib/typescript/sdk/navigation.d.ts +33 -0
  135. package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
  136. package/lib/typescript/sdk/utils.d.ts +148 -0
  137. package/lib/typescript/types/index.d.ts +624 -0
  138. package/package.json +102 -0
  139. package/rejourney.podspec +21 -0
  140. package/src/NativeRejourney.ts +165 -0
  141. package/src/components/Mask.tsx +80 -0
  142. package/src/index.ts +1459 -0
  143. package/src/sdk/autoTracking.ts +1373 -0
  144. package/src/sdk/constants.ts +134 -0
  145. package/src/sdk/errorTracking.ts +231 -0
  146. package/src/sdk/index.ts +11 -0
  147. package/src/sdk/metricsTracking.ts +232 -0
  148. package/src/sdk/navigation.ts +157 -0
  149. package/src/sdk/networkInterceptor.ts +440 -0
  150. package/src/sdk/utils.ts +369 -0
  151. package/src/types/expo-router.d.ts +7 -0
  152. 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