@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,1233 @@
1
+ /**
2
+ * Rejourney Auto Tracking Module
3
+ *
4
+ * Automatic tracking features that work with just init() - no additional code needed.
5
+ * This module handles:
6
+ * - Rage tap detection
7
+ * - Error tracking (JS + React Native)
8
+ * - Session metrics aggregation
9
+ * - Device info collection
10
+ * - Anonymous ID generation
11
+ * - Funnel/screen tracking
12
+ * - Score calculations
13
+ *
14
+ * IMPORTANT: This file uses lazy loading for react-native imports to avoid
15
+ * "PlatformConstants could not be found" errors on RN 0.81+.
16
+ */
17
+
18
+ import { logger } from './utils';
19
+
20
+ // Lazy-loaded React Native modules
21
+ let _RN = null;
22
+ function getRN() {
23
+ if (_RN) return _RN;
24
+ try {
25
+ _RN = require('react-native');
26
+ return _RN;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ function getPlatform() {
32
+ return getRN()?.Platform;
33
+ }
34
+ function getDimensions() {
35
+ return getRN()?.Dimensions;
36
+ }
37
+ function getNativeModules() {
38
+ return getRN()?.NativeModules;
39
+ }
40
+ function getRejourneyNativeModule() {
41
+ const RN = getRN();
42
+ if (!RN) return null;
43
+ const {
44
+ TurboModuleRegistry,
45
+ NativeModules
46
+ } = RN;
47
+ let nativeModule = null;
48
+ if (TurboModuleRegistry && typeof TurboModuleRegistry.get === 'function') {
49
+ try {
50
+ nativeModule = TurboModuleRegistry.get('Rejourney');
51
+ } catch {
52
+ // Ignore and fall back to NativeModules
53
+ }
54
+ }
55
+ if (!nativeModule && NativeModules) {
56
+ nativeModule = NativeModules.Rejourney ?? null;
57
+ }
58
+ return nativeModule;
59
+ }
60
+
61
+ // Type declarations for browser globals (only used in hybrid apps where DOM is available)
62
+ // These don't exist in pure React Native but are needed for error tracking in hybrid scenarios
63
+
64
+ // Cast globalThis to work with both RN and hybrid scenarios
65
+ const _globalThis = globalThis;
66
+
67
+ // =============================================================================
68
+ // Types
69
+ // =============================================================================
70
+
71
+ // =============================================================================
72
+ // State
73
+ // =============================================================================
74
+
75
+ let isInitialized = false;
76
+ let config = {};
77
+
78
+ // Rage tap tracking
79
+ const recentTaps = [];
80
+ let tapHead = 0; // Circular buffer head pointer
81
+ let tapCount = 0; // Actual count of taps in buffer
82
+ const MAX_RECENT_TAPS = 10;
83
+
84
+ // Session metrics
85
+ let metrics = createEmptyMetrics();
86
+ let sessionStartTime = 0;
87
+ let maxSessionDurationMs = 10 * 60 * 1000;
88
+
89
+ // Screen tracking
90
+ let currentScreen = '';
91
+ let screensVisited = [];
92
+
93
+ // Anonymous ID
94
+ let anonymousId = null;
95
+ let anonymousIdPromise = null;
96
+
97
+ // Callbacks
98
+ let onRageTapDetected = null;
99
+ let onErrorCaptured = null;
100
+ let onScreenChange = null;
101
+
102
+ // Original error handlers (for restoration)
103
+ let originalErrorHandler;
104
+ let originalOnError = null;
105
+ let originalOnUnhandledRejection = null;
106
+
107
+ // =============================================================================
108
+ // Initialization
109
+ // =============================================================================
110
+
111
+ /**
112
+ * Initialize auto tracking features
113
+ * Called automatically by Rejourney.init() - no user action needed
114
+ */
115
+ export function initAutoTracking(trackingConfig, callbacks = {}) {
116
+ if (isInitialized) return;
117
+ config = {
118
+ rageTapThreshold: 3,
119
+ rageTapTimeWindow: 500,
120
+ rageTapRadius: 50,
121
+ trackJSErrors: true,
122
+ trackPromiseRejections: true,
123
+ trackReactNativeErrors: true,
124
+ collectDeviceInfo: true,
125
+ maxSessionDurationMs: trackingConfig.maxSessionDurationMs,
126
+ ...trackingConfig
127
+ };
128
+
129
+ // Session timing
130
+ sessionStartTime = Date.now();
131
+ setMaxSessionDurationMinutes(trackingConfig.maxSessionDurationMs ? trackingConfig.maxSessionDurationMs / 60000 : undefined);
132
+
133
+ // Set callbacks
134
+ onRageTapDetected = callbacks.onRageTap || null;
135
+ onErrorCaptured = callbacks.onError || null;
136
+ onScreenChange = callbacks.onScreen || null;
137
+
138
+ // Initialize metrics
139
+ metrics = createEmptyMetrics();
140
+ sessionStartTime = Date.now();
141
+ anonymousId = generateAnonymousId();
142
+
143
+ // Setup error tracking
144
+ setupErrorTracking();
145
+
146
+ // Setup React Navigation tracking (if available)
147
+ setupNavigationTracking();
148
+ isInitialized = true;
149
+ }
150
+
151
+ /**
152
+ * Cleanup auto tracking features
153
+ */
154
+ export function cleanupAutoTracking() {
155
+ if (!isInitialized) return;
156
+
157
+ // Restore original error handlers
158
+ restoreErrorHandlers();
159
+
160
+ // Cleanup navigation tracking
161
+ cleanupNavigationTracking();
162
+
163
+ // Reset state
164
+ tapHead = 0;
165
+ tapCount = 0;
166
+ metrics = createEmptyMetrics();
167
+ screensVisited = [];
168
+ currentScreen = '';
169
+ sessionStartTime = 0;
170
+ maxSessionDurationMs = 10 * 60 * 1000;
171
+ isInitialized = false;
172
+ }
173
+
174
+ // =============================================================================
175
+ // Rage Tap Detection
176
+ // =============================================================================
177
+
178
+ /**
179
+ * Track a tap event for rage tap detection
180
+ * Called automatically from touch interceptor
181
+ */
182
+ export function trackTap(tap) {
183
+ if (!isInitialized) return;
184
+ const now = Date.now();
185
+
186
+ // Add to circular buffer (O(1) instead of shift() which is O(n))
187
+ const insertIndex = (tapHead + tapCount) % MAX_RECENT_TAPS;
188
+ if (tapCount < MAX_RECENT_TAPS) {
189
+ recentTaps[insertIndex] = {
190
+ ...tap,
191
+ timestamp: now
192
+ };
193
+ tapCount++;
194
+ } else {
195
+ // Buffer full, overwrite oldest
196
+ recentTaps[tapHead] = {
197
+ ...tap,
198
+ timestamp: now
199
+ };
200
+ tapHead = (tapHead + 1) % MAX_RECENT_TAPS;
201
+ }
202
+
203
+ // Evict old taps outside time window
204
+ const windowStart = now - (config.rageTapTimeWindow || 500);
205
+ while (tapCount > 0) {
206
+ const oldestTap = recentTaps[tapHead];
207
+ if (oldestTap && oldestTap.timestamp < windowStart) {
208
+ tapHead = (tapHead + 1) % MAX_RECENT_TAPS;
209
+ tapCount--;
210
+ } else {
211
+ break;
212
+ }
213
+ }
214
+
215
+ // Check for rage tap
216
+ detectRageTap();
217
+
218
+ // Update metrics
219
+ metrics.touchCount++;
220
+ metrics.totalEvents++;
221
+ }
222
+
223
+ /**
224
+ * Detect if recent taps form a rage tap pattern
225
+ */
226
+ function detectRageTap() {
227
+ const threshold = config.rageTapThreshold || 3;
228
+ const radius = config.rageTapRadius || 50;
229
+ if (tapCount < threshold) return;
230
+ // Check last N taps from circular buffer
231
+ const tapsToCheck = [];
232
+ for (let i = 0; i < threshold; i++) {
233
+ const idx = (tapHead + tapCount - threshold + i) % MAX_RECENT_TAPS;
234
+ tapsToCheck.push(recentTaps[idx]);
235
+ }
236
+
237
+ // Calculate center point
238
+ let centerX = 0;
239
+ let centerY = 0;
240
+ for (const tap of tapsToCheck) {
241
+ centerX += tap.x;
242
+ centerY += tap.y;
243
+ }
244
+ centerX /= tapsToCheck.length;
245
+ centerY /= tapsToCheck.length;
246
+
247
+ // Check if all taps are within radius of center
248
+ let allWithinRadius = true;
249
+ for (const tap of tapsToCheck) {
250
+ const dx = tap.x - centerX;
251
+ const dy = tap.y - centerY;
252
+ const distance = Math.sqrt(dx * dx + dy * dy);
253
+ if (distance > radius) {
254
+ allWithinRadius = false;
255
+ break;
256
+ }
257
+ }
258
+ if (allWithinRadius) {
259
+ // Rage tap detected!
260
+ metrics.rageTapCount++;
261
+ // Clear circular buffer to prevent duplicate detection
262
+ tapHead = 0;
263
+ tapCount = 0;
264
+
265
+ // Notify callback
266
+ if (onRageTapDetected) {
267
+ onRageTapDetected(threshold, centerX, centerY);
268
+ }
269
+ }
270
+ }
271
+
272
+ // =============================================================================
273
+ // State Change Notification
274
+ // =============================================================================
275
+
276
+ /**
277
+ * Notify that a state change occurred (navigation, modal, etc.)
278
+ * Kept for API compatibility
279
+ */
280
+ export function notifyStateChange() {
281
+ // No-op - kept for backward compatibility
282
+ }
283
+
284
+ // =============================================================================
285
+ // Error Tracking
286
+ // =============================================================================
287
+
288
+ /**
289
+ * Setup automatic error tracking
290
+ */
291
+ function setupErrorTracking() {
292
+ // Track React Native errors
293
+ if (config.trackReactNativeErrors !== false) {
294
+ setupReactNativeErrorHandler();
295
+ }
296
+
297
+ // Track JavaScript errors (only works in web/debug)
298
+ if (config.trackJSErrors !== false && typeof _globalThis !== 'undefined') {
299
+ setupJSErrorHandler();
300
+ }
301
+
302
+ // Track unhandled promise rejections
303
+ if (config.trackPromiseRejections !== false && typeof _globalThis !== 'undefined') {
304
+ setupPromiseRejectionHandler();
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Setup React Native ErrorUtils handler
310
+ */
311
+ function setupReactNativeErrorHandler() {
312
+ try {
313
+ // Access ErrorUtils from global scope
314
+ const ErrorUtils = _globalThis.ErrorUtils;
315
+ if (!ErrorUtils) return;
316
+
317
+ // Store original handler
318
+ originalErrorHandler = ErrorUtils.getGlobalHandler();
319
+
320
+ // Set new handler
321
+ ErrorUtils.setGlobalHandler((error, isFatal) => {
322
+ // Track the error
323
+ trackError({
324
+ type: 'error',
325
+ timestamp: Date.now(),
326
+ message: error.message || String(error),
327
+ stack: error.stack,
328
+ name: error.name || 'Error'
329
+ });
330
+
331
+ // Call original handler
332
+ if (originalErrorHandler) {
333
+ originalErrorHandler(error, isFatal);
334
+ }
335
+ });
336
+ } catch {
337
+ // ErrorUtils not available
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Setup global JS error handler
343
+ */
344
+ function setupJSErrorHandler() {
345
+ if (typeof _globalThis.onerror !== 'undefined') {
346
+ // Note: In React Native, this typically doesn't fire
347
+ // But we set it up anyway for hybrid apps
348
+ originalOnError = _globalThis.onerror;
349
+ _globalThis.onerror = (message, source, lineno, colno, error) => {
350
+ trackError({
351
+ type: 'error',
352
+ timestamp: Date.now(),
353
+ message: typeof message === 'string' ? message : 'Unknown error',
354
+ stack: error?.stack || `${source}:${lineno}:${colno}`,
355
+ name: error?.name || 'Error'
356
+ });
357
+
358
+ // Call original handler
359
+ if (originalOnError) {
360
+ return originalOnError(message, source, lineno, colno, error);
361
+ }
362
+ return false;
363
+ };
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Setup unhandled promise rejection handler
369
+ */
370
+ function setupPromiseRejectionHandler() {
371
+ if (typeof _globalThis.addEventListener !== 'undefined') {
372
+ const handler = event => {
373
+ const reason = event.reason;
374
+ trackError({
375
+ type: 'error',
376
+ timestamp: Date.now(),
377
+ message: reason?.message || String(reason) || 'Unhandled Promise Rejection',
378
+ stack: reason?.stack,
379
+ name: reason?.name || 'UnhandledRejection'
380
+ });
381
+ };
382
+ originalOnUnhandledRejection = handler;
383
+ _globalThis.addEventListener('unhandledrejection', handler);
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Restore original error handlers
389
+ */
390
+ function restoreErrorHandlers() {
391
+ // Restore React Native handler
392
+ if (originalErrorHandler) {
393
+ try {
394
+ const ErrorUtils = _globalThis.ErrorUtils;
395
+ if (ErrorUtils) {
396
+ ErrorUtils.setGlobalHandler(originalErrorHandler);
397
+ }
398
+ } catch {
399
+ // Ignore
400
+ }
401
+ originalErrorHandler = undefined;
402
+ }
403
+
404
+ // Restore global onerror
405
+ if (originalOnError !== null) {
406
+ _globalThis.onerror = originalOnError;
407
+ originalOnError = null;
408
+ }
409
+
410
+ // Remove promise rejection handler
411
+ if (originalOnUnhandledRejection && typeof _globalThis.removeEventListener !== 'undefined') {
412
+ _globalThis.removeEventListener('unhandledrejection', originalOnUnhandledRejection);
413
+ originalOnUnhandledRejection = null;
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Track an error
419
+ */
420
+ function trackError(error) {
421
+ metrics.errorCount++;
422
+ metrics.totalEvents++;
423
+ if (onErrorCaptured) {
424
+ onErrorCaptured(error);
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Manually track an error (for API errors, etc.)
430
+ */
431
+ export function captureError(message, stack, name) {
432
+ trackError({
433
+ type: 'error',
434
+ timestamp: Date.now(),
435
+ message,
436
+ stack,
437
+ name: name || 'Error'
438
+ });
439
+ }
440
+
441
+ // =============================================================================
442
+ // Screen/Funnel Tracking - Automatic Navigation Detection
443
+ // =============================================================================
444
+
445
+ // Navigation detection state
446
+ let navigationPollingInterval = null;
447
+ let lastDetectedScreen = '';
448
+ let navigationSetupDone = false;
449
+ let navigationPollingErrors = 0; // Track consecutive errors
450
+ const MAX_POLLING_ERRORS = 10; // Stop polling after 10 consecutive errors
451
+
452
+ /**
453
+ * Track a navigation state change from React Navigation.
454
+ *
455
+ * For bare React Native apps using @react-navigation/native.
456
+ * Just add this to your NavigationContainer's onStateChange prop.
457
+ *
458
+ * @example
459
+ * ```tsx
460
+ * import { trackNavigationState } from 'rejourney';
461
+ *
462
+ * <NavigationContainer onStateChange={trackNavigationState}>
463
+ * ...
464
+ * </NavigationContainer>
465
+ * ```
466
+ */
467
+ export function trackNavigationState(state) {
468
+ if (!state?.routes) return;
469
+ try {
470
+ const {
471
+ normalizeScreenName
472
+ } = require('./navigation');
473
+
474
+ // Find the active screen recursively
475
+ const findActiveScreen = navState => {
476
+ if (!navState?.routes) return null;
477
+ const index = navState.index ?? navState.routes.length - 1;
478
+ const route = navState.routes[index];
479
+ if (!route) return null;
480
+ if (route.state) return findActiveScreen(route.state);
481
+ return normalizeScreenName(route.name || 'Unknown');
482
+ };
483
+ const screenName = findActiveScreen(state);
484
+ if (screenName && screenName !== lastDetectedScreen) {
485
+ lastDetectedScreen = screenName;
486
+ trackScreen(screenName);
487
+ }
488
+ } catch {
489
+ // Navigation tracking error
490
+ }
491
+ }
492
+
493
+ /**
494
+ * React hook for navigation tracking.
495
+ *
496
+ * Returns props to spread on NavigationContainer that will:
497
+ * 1. Track the initial screen on mount (via onReady)
498
+ * 2. Track all subsequent navigations (via onStateChange)
499
+ *
500
+ * This is the RECOMMENDED approach for bare React Native apps.
501
+ *
502
+ * @example
503
+ * ```tsx
504
+ * import { useNavigationTracking } from 'rejourney';
505
+ * import { NavigationContainer } from '@react-navigation/native';
506
+ *
507
+ * function App() {
508
+ * const navigationTracking = useNavigationTracking();
509
+ *
510
+ * return (
511
+ * <NavigationContainer {...navigationTracking}>
512
+ * <RootNavigator />
513
+ * </NavigationContainer>
514
+ * );
515
+ * }
516
+ * ```
517
+ */
518
+ export function useNavigationTracking() {
519
+ // Use React's useRef and useCallback to create stable references
520
+ const React = require('react');
521
+ const {
522
+ createNavigationContainerRef
523
+ } = require('@react-navigation/native');
524
+
525
+ // Create a stable navigation ref
526
+ const navigationRef = React.useRef(createNavigationContainerRef());
527
+
528
+ // Track initial screen when navigation is ready
529
+ const onReady = React.useCallback(() => {
530
+ try {
531
+ const currentRoute = navigationRef.current?.getCurrentRoute?.();
532
+ if (currentRoute?.name) {
533
+ const {
534
+ normalizeScreenName
535
+ } = require('./navigation');
536
+ const screenName = normalizeScreenName(currentRoute.name);
537
+ if (screenName && screenName !== lastDetectedScreen) {
538
+ lastDetectedScreen = screenName;
539
+ trackScreen(screenName);
540
+ }
541
+ }
542
+ } catch {
543
+ // Navigation not ready yet
544
+ }
545
+ }, []);
546
+
547
+ // Return props to spread on NavigationContainer
548
+ return {
549
+ ref: navigationRef.current,
550
+ onReady,
551
+ onStateChange: trackNavigationState
552
+ };
553
+ }
554
+
555
+ /**
556
+ * Setup automatic Expo Router tracking
557
+ *
558
+ * For Expo apps using expo-router - works automatically.
559
+ * For bare React Native apps - use trackNavigationState instead.
560
+ */
561
+ function setupNavigationTracking() {
562
+ if (navigationSetupDone) return;
563
+ navigationSetupDone = true;
564
+ if (__DEV__) {
565
+ logger.debug('Setting up navigation tracking...');
566
+ }
567
+
568
+ // Delay to ensure navigation is initialized - Expo Router needs more time
569
+ // We retry a few times with increasing delays
570
+ let attempts = 0;
571
+ const maxAttempts = 5;
572
+ const trySetup = () => {
573
+ attempts++;
574
+ if (__DEV__) {
575
+ logger.debug('Navigation setup attempt', attempts, 'of', maxAttempts);
576
+ }
577
+ const success = trySetupExpoRouter();
578
+ if (success) {
579
+ if (__DEV__) {
580
+ logger.debug('Expo Router setup: SUCCESS on attempt', attempts);
581
+ }
582
+ } else if (attempts < maxAttempts) {
583
+ // Retry with exponential backoff
584
+ const delay = 200 * attempts; // 200, 400, 600, 800ms
585
+ if (__DEV__) {
586
+ logger.debug('Expo Router not ready, retrying in', delay, 'ms');
587
+ }
588
+ setTimeout(trySetup, delay);
589
+ } else {
590
+ if (__DEV__) {
591
+ logger.debug('Expo Router setup: FAILED after', maxAttempts, 'attempts');
592
+ logger.debug('For manual navigation tracking, use trackNavigationState() on your NavigationContainer');
593
+ }
594
+ }
595
+ };
596
+
597
+ // Start first attempt after 200ms
598
+ setTimeout(trySetup, 200);
599
+ }
600
+
601
+ /**
602
+ * Set up Expo Router auto-tracking by polling the internal router store
603
+ *
604
+ * Supports both expo-router v3 (store.rootState) and v6+ (store.state, store.navigationRef)
605
+ */
606
+ function trySetupExpoRouter() {
607
+ try {
608
+ const expoRouter = require('expo-router');
609
+ const router = expoRouter.router;
610
+ if (!router) {
611
+ if (__DEV__) {
612
+ logger.debug('Expo Router: router object not found');
613
+ }
614
+ return false;
615
+ }
616
+ if (__DEV__) {
617
+ logger.debug('Expo Router: Setting up navigation tracking');
618
+ }
619
+ const {
620
+ normalizeScreenName,
621
+ getScreenNameFromPath
622
+ } = require('./navigation');
623
+
624
+ // Poll for route changes (expo-router doesn't expose a listener API outside hooks)
625
+ navigationPollingInterval = setInterval(() => {
626
+ try {
627
+ let state = null;
628
+ let stateSource = '';
629
+
630
+ // Method 1: Public accessors on router object
631
+ if (typeof router.getState === 'function') {
632
+ state = router.getState();
633
+ stateSource = 'router.getState()';
634
+ } else if (router.rootState) {
635
+ state = router.rootState;
636
+ stateSource = 'router.rootState';
637
+ }
638
+
639
+ // Method 2: Internal store access (works for both v3 and v6+)
640
+ if (!state) {
641
+ try {
642
+ const storeModule = require('expo-router/build/global-state/router-store');
643
+ if (storeModule?.store) {
644
+ // v6+: store.state or store.navigationRef
645
+ state = storeModule.store.state;
646
+ if (state) stateSource = 'store.state';
647
+
648
+ // v6+: Try navigationRef if state is undefined
649
+ if (!state && storeModule.store.navigationRef?.current) {
650
+ state = storeModule.store.navigationRef.current.getRootState?.();
651
+ if (state) stateSource = 'navigationRef.getRootState()';
652
+ }
653
+
654
+ // v3: store.rootState or store.initialState
655
+ if (!state) {
656
+ state = storeModule.store.rootState || storeModule.store.initialState;
657
+ if (state) stateSource = 'store.rootState/initialState';
658
+ }
659
+ }
660
+ } catch {
661
+ // Store not available
662
+ }
663
+ }
664
+
665
+ // Method 3: Try accessing via a different export path for v6
666
+ if (!state) {
667
+ try {
668
+ const imperative = require('expo-router/build/imperative-api');
669
+ if (imperative?.router) {
670
+ state = imperative.router.getState?.();
671
+ if (state) stateSource = 'imperative-api';
672
+ }
673
+ } catch {
674
+ // Imperative API not available
675
+ }
676
+ }
677
+ if (state) {
678
+ // Reset error count on success
679
+ navigationPollingErrors = 0;
680
+ const screenName = extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName);
681
+ if (screenName && screenName !== lastDetectedScreen) {
682
+ if (__DEV__) {
683
+ logger.debug('Screen changed:', lastDetectedScreen, '->', screenName, `(source: ${stateSource})`);
684
+ }
685
+ lastDetectedScreen = screenName;
686
+ trackScreen(screenName);
687
+ }
688
+ } else {
689
+ // Track consecutive failures to get state
690
+ navigationPollingErrors++;
691
+ if (__DEV__ && navigationPollingErrors === 1) {
692
+ logger.debug('Expo Router: Could not get navigation state');
693
+ }
694
+ if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
695
+ // Stop polling after too many errors to save CPU
696
+ if (__DEV__) {
697
+ logger.debug('Expo Router: Stopped polling after', MAX_POLLING_ERRORS, 'errors');
698
+ }
699
+ cleanupNavigationTracking();
700
+ }
701
+ }
702
+ } catch (e) {
703
+ // Error - track and potentially stop
704
+ navigationPollingErrors++;
705
+ if (__DEV__ && navigationPollingErrors === 1) {
706
+ logger.debug('Expo Router polling error:', e);
707
+ }
708
+ if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
709
+ cleanupNavigationTracking();
710
+ }
711
+ }
712
+ }, 500); // 500ms polling (reduced from 300ms for lower CPU usage)
713
+
714
+ return true;
715
+ } catch (e) {
716
+ if (__DEV__) {
717
+ logger.debug('Expo Router not available:', e);
718
+ }
719
+ // expo-router not installed
720
+ return false;
721
+ }
722
+ }
723
+
724
+ /**
725
+ * Extract screen name from Expo Router navigation state
726
+ *
727
+ * Handles complex nested structures like Drawer → Tabs → Stack
728
+ * by recursively accumulating segments from each navigation level.
729
+ */
730
+ function extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName, accumulatedSegments = []) {
731
+ if (!state?.routes) return null;
732
+ const route = state.routes[state.index ?? state.routes.length - 1];
733
+ if (!route) return null;
734
+
735
+ // Add current route name to accumulated segments
736
+ const newSegments = [...accumulatedSegments, route.name];
737
+
738
+ // If this route has nested state, recurse deeper
739
+ if (route.state) {
740
+ return extractScreenNameFromRouterState(route.state, getScreenNameFromPath, normalizeScreenName, newSegments);
741
+ }
742
+
743
+ // We've reached the deepest level - build the screen name
744
+ // Filter out group markers like (tabs), (main), (auth)
745
+ const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
746
+
747
+ // If after filtering we have no segments, use the last meaningful name
748
+ if (cleanSegments.length === 0) {
749
+ // Find the last non-group segment
750
+ for (let i = newSegments.length - 1; i >= 0; i--) {
751
+ const seg = newSegments[i];
752
+ if (seg && !seg.startsWith('(') && !seg.endsWith(')')) {
753
+ cleanSegments.push(seg);
754
+ break;
755
+ }
756
+ }
757
+ }
758
+ const pathname = '/' + cleanSegments.join('/');
759
+ return getScreenNameFromPath(pathname, newSegments);
760
+ }
761
+
762
+ /**
763
+ * Cleanup navigation tracking
764
+ */
765
+ function cleanupNavigationTracking() {
766
+ if (navigationPollingInterval) {
767
+ clearInterval(navigationPollingInterval);
768
+ navigationPollingInterval = null;
769
+ }
770
+ navigationSetupDone = false;
771
+ lastDetectedScreen = '';
772
+ navigationPollingErrors = 0;
773
+ }
774
+
775
+ /**
776
+ * Track a screen view
777
+ * This updates JS metrics AND notifies the native module to send to backend
778
+ */
779
+ export function trackScreen(screenName) {
780
+ if (!isInitialized) {
781
+ if (__DEV__) {
782
+ logger.debug('trackScreen called but not initialized, screen:', screenName);
783
+ }
784
+ return;
785
+ }
786
+ const previousScreen = currentScreen;
787
+ currentScreen = screenName;
788
+ // Add to screens visited (only track for unique set, avoid large array copies)
789
+ screensVisited.push(screenName);
790
+
791
+ // Update unique screens count
792
+ const uniqueScreens = new Set(screensVisited);
793
+ metrics.uniqueScreensCount = uniqueScreens.size;
794
+
795
+ // Update navigation count
796
+ metrics.navigationCount++;
797
+ metrics.totalEvents++;
798
+ if (__DEV__) {
799
+ logger.debug('trackScreen:', screenName, '(total screens:', metrics.uniqueScreensCount, ')');
800
+ }
801
+
802
+ // Notify callback
803
+ if (onScreenChange) {
804
+ onScreenChange(screenName, previousScreen);
805
+ }
806
+
807
+ // IMPORTANT: Also notify native module to send to backend
808
+ // This is the key fix - without this, screens don't get recorded!
809
+ try {
810
+ const RejourneyNative = getRejourneyNativeModule();
811
+ if (RejourneyNative?.screenChanged) {
812
+ if (__DEV__) {
813
+ logger.debug('Notifying native screenChanged:', screenName);
814
+ }
815
+ RejourneyNative.screenChanged(screenName).catch(e => {
816
+ if (__DEV__) {
817
+ logger.debug('Native screenChanged error:', e);
818
+ }
819
+ });
820
+ } else if (__DEV__) {
821
+ logger.debug('Native screenChanged method not available');
822
+ }
823
+ } catch (e) {
824
+ if (__DEV__) {
825
+ logger.debug('trackScreen native call error:', e);
826
+ }
827
+ }
828
+ }
829
+
830
+ // =============================================================================
831
+ // API Metrics Tracking
832
+ // =============================================================================
833
+
834
+ /**
835
+ * Track an API request with timing data
836
+ */
837
+ export function trackAPIRequest(success, _statusCode, durationMs = 0, responseBytes = 0) {
838
+ if (!isInitialized) return;
839
+ metrics.apiTotalCount++;
840
+
841
+ // Accumulate timing and size for avg calculation
842
+ if (durationMs > 0) {
843
+ metrics.netTotalDurationMs += durationMs;
844
+ }
845
+ if (responseBytes > 0) {
846
+ metrics.netTotalBytes += responseBytes;
847
+ }
848
+ if (success) {
849
+ metrics.apiSuccessCount++;
850
+ } else {
851
+ metrics.apiErrorCount++;
852
+
853
+ // API errors also count toward error count for UX score
854
+ metrics.errorCount++;
855
+ }
856
+ }
857
+
858
+ // =============================================================================
859
+ // Session Metrics
860
+ // =============================================================================
861
+
862
+ /**
863
+ * Create empty metrics object
864
+ */
865
+ function createEmptyMetrics() {
866
+ return {
867
+ totalEvents: 0,
868
+ touchCount: 0,
869
+ scrollCount: 0,
870
+ gestureCount: 0,
871
+ inputCount: 0,
872
+ navigationCount: 0,
873
+ errorCount: 0,
874
+ rageTapCount: 0,
875
+ apiSuccessCount: 0,
876
+ apiErrorCount: 0,
877
+ apiTotalCount: 0,
878
+ netTotalDurationMs: 0,
879
+ netTotalBytes: 0,
880
+ screensVisited: [],
881
+ uniqueScreensCount: 0,
882
+ interactionScore: 100,
883
+ explorationScore: 100,
884
+ uxScore: 100
885
+ };
886
+ }
887
+
888
+ /**
889
+ * Track a scroll event
890
+ */
891
+ export function trackScroll() {
892
+ if (!isInitialized) return;
893
+ metrics.scrollCount++;
894
+ metrics.totalEvents++;
895
+ }
896
+
897
+ /**
898
+ * Track a gesture event
899
+ */
900
+ export function trackGesture() {
901
+ if (!isInitialized) return;
902
+ metrics.gestureCount++;
903
+ metrics.totalEvents++;
904
+ }
905
+
906
+ /**
907
+ * Track an input event (keyboard)
908
+ */
909
+ export function trackInput() {
910
+ if (!isInitialized) return;
911
+ metrics.inputCount++;
912
+ metrics.totalEvents++;
913
+ }
914
+
915
+ /**
916
+ * Get current session metrics
917
+ */
918
+ export function getSessionMetrics() {
919
+ // Calculate scores before returning
920
+ calculateScores();
921
+
922
+ // Compute average API response time
923
+ const netAvgDurationMs = metrics.apiTotalCount > 0 ? Math.round(metrics.netTotalDurationMs / metrics.apiTotalCount) : 0;
924
+
925
+ // Lazily populate screensVisited only when metrics are retrieved
926
+ // This avoids expensive array copies on every screen change
927
+ return {
928
+ ...metrics,
929
+ screensVisited: [...screensVisited],
930
+ // Only copy here when needed
931
+ netAvgDurationMs
932
+ };
933
+ }
934
+
935
+ /**
936
+ * Calculate session scores
937
+ */
938
+ function calculateScores() {
939
+ // Interaction Score (0-100)
940
+ // Based on total interactions normalized to a baseline
941
+ const totalInteractions = metrics.touchCount + metrics.scrollCount + metrics.gestureCount + metrics.inputCount;
942
+
943
+ // Assume 50 interactions is "average" for a session
944
+ const avgInteractions = 50;
945
+ metrics.interactionScore = Math.min(100, Math.round(totalInteractions / avgInteractions * 100));
946
+
947
+ // Exploration Score (0-100)
948
+ // Based on unique screens visited
949
+ // Assume 5 screens is "average" exploration
950
+ const avgScreens = 5;
951
+ metrics.explorationScore = Math.min(100, Math.round(metrics.uniqueScreensCount / avgScreens * 100));
952
+
953
+ // UX Score (0-100)
954
+ // Starts at 100, deducts for issues
955
+ let uxScore = 100;
956
+
957
+ // Deduct for errors
958
+ uxScore -= Math.min(30, metrics.errorCount * 15); // Max 30 point deduction
959
+
960
+ // Deduct for rage taps
961
+ uxScore -= Math.min(24, metrics.rageTapCount * 8); // Max 24 point deduction
962
+
963
+ // Deduct for API errors
964
+ uxScore -= Math.min(20, metrics.apiErrorCount * 10); // Max 20 point deduction
965
+
966
+ // Bonus for completing funnel (if screens > 3)
967
+ if (metrics.uniqueScreensCount >= 3) {
968
+ uxScore += 5;
969
+ }
970
+ metrics.uxScore = Math.max(0, Math.min(100, uxScore));
971
+ }
972
+
973
+ /**
974
+ * Reset metrics for new session
975
+ */
976
+ export function resetMetrics() {
977
+ metrics = createEmptyMetrics();
978
+ screensVisited = [];
979
+ currentScreen = '';
980
+ tapHead = 0;
981
+ tapCount = 0;
982
+ sessionStartTime = Date.now();
983
+ }
984
+
985
+ // =============================================================================
986
+ // Session duration helpers
987
+ // =============================================================================
988
+
989
+ /** Clamp and set max session duration in minutes (1–10). Defaults to 10. */
990
+ export function setMaxSessionDurationMinutes(minutes) {
991
+ const clampedMinutes = Math.min(10, Math.max(1, minutes ?? 10));
992
+ maxSessionDurationMs = clampedMinutes * 60 * 1000;
993
+ }
994
+
995
+ /** Returns true if the current session exceeded the configured max duration. */
996
+ export function hasExceededMaxSessionDuration() {
997
+ if (!sessionStartTime) return false;
998
+ return Date.now() - sessionStartTime >= maxSessionDurationMs;
999
+ }
1000
+
1001
+ /** Returns remaining milliseconds until the session should stop. */
1002
+ export function getRemainingSessionDurationMs() {
1003
+ if (!sessionStartTime) return maxSessionDurationMs;
1004
+ const remaining = maxSessionDurationMs - (Date.now() - sessionStartTime);
1005
+ return Math.max(0, remaining);
1006
+ }
1007
+
1008
+ // =============================================================================
1009
+ // Device Info Collection
1010
+ // =============================================================================
1011
+
1012
+ /**
1013
+ * Collect device information
1014
+ */
1015
+ export function collectDeviceInfo() {
1016
+ const Dimensions = getDimensions();
1017
+ const Platform = getPlatform();
1018
+ const NativeModules = getNativeModules();
1019
+
1020
+ // Default values if react-native isn't available
1021
+ let width = 0,
1022
+ height = 0,
1023
+ scale = 1;
1024
+ if (Dimensions) {
1025
+ const windowDims = Dimensions.get('window');
1026
+ const screenDims = Dimensions.get('screen');
1027
+ width = windowDims?.width || 0;
1028
+ height = windowDims?.height || 0;
1029
+ scale = screenDims?.scale || 1;
1030
+ }
1031
+
1032
+ // Get device model - this varies by platform
1033
+ let model = 'Unknown';
1034
+ let manufacturer;
1035
+ let osVersion = 'Unknown';
1036
+ let appVersion;
1037
+ let appId;
1038
+ let locale;
1039
+ let timezone;
1040
+ try {
1041
+ // Try to get from react-native-device-info if available
1042
+ // This is optional - falls back to basic info if not installed
1043
+ const DeviceInfo = require('react-native-device-info');
1044
+ model = DeviceInfo.getModel?.() || model;
1045
+ manufacturer = DeviceInfo.getBrand?.() || undefined;
1046
+ osVersion = DeviceInfo.getSystemVersion?.() || osVersion;
1047
+ appVersion = DeviceInfo.getVersion?.() || undefined;
1048
+ appId = DeviceInfo.getBundleId?.() || undefined;
1049
+ locale = DeviceInfo.getDeviceLocale?.() || undefined;
1050
+ timezone = DeviceInfo.getTimezone?.() || undefined;
1051
+ } catch {
1052
+ // react-native-device-info not installed, try Expo packages
1053
+ try {
1054
+ // Try expo-application for app version/id
1055
+ const Application = require('expo-application');
1056
+ appVersion = Application.nativeApplicationVersion || Application.applicationVersion || undefined;
1057
+ appId = Application.applicationId || undefined;
1058
+ } catch {
1059
+ // expo-application not available
1060
+ }
1061
+ try {
1062
+ // Try expo-constants for additional info
1063
+ const Constants = require('expo-constants');
1064
+ const expoConfig = Constants.expoConfig || Constants.manifest2?.extra?.expoClient || Constants.manifest;
1065
+ if (!appVersion && expoConfig?.version) {
1066
+ appVersion = expoConfig.version;
1067
+ }
1068
+ if (!appId && (expoConfig?.ios?.bundleIdentifier || expoConfig?.android?.package)) {
1069
+ appId = Platform?.OS === 'ios' ? expoConfig?.ios?.bundleIdentifier : expoConfig?.android?.package;
1070
+ }
1071
+ } catch {
1072
+ // expo-constants not available
1073
+ }
1074
+
1075
+ // Fall back to basic platform info
1076
+ if (Platform?.OS === 'ios') {
1077
+ // Get basic info from constants
1078
+ const PlatformConstants = NativeModules?.PlatformConstants;
1079
+ osVersion = Platform.Version?.toString() || osVersion;
1080
+ model = PlatformConstants?.interfaceIdiom === 'pad' ? 'iPad' : 'iPhone';
1081
+ } else if (Platform?.OS === 'android') {
1082
+ osVersion = Platform.Version?.toString() || osVersion;
1083
+ model = 'Android Device';
1084
+ }
1085
+ }
1086
+
1087
+ // Get timezone
1088
+ if (!timezone) {
1089
+ try {
1090
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
1091
+ } catch {
1092
+ timezone = undefined;
1093
+ }
1094
+ }
1095
+
1096
+ // Get locale
1097
+ if (!locale) {
1098
+ try {
1099
+ locale = Intl.DateTimeFormat().resolvedOptions().locale;
1100
+ } catch {
1101
+ locale = undefined;
1102
+ }
1103
+ }
1104
+ return {
1105
+ model,
1106
+ manufacturer,
1107
+ os: Platform?.OS || 'ios',
1108
+ osVersion,
1109
+ screenWidth: Math.round(width),
1110
+ screenHeight: Math.round(height),
1111
+ pixelRatio: scale,
1112
+ appVersion,
1113
+ appId,
1114
+ locale,
1115
+ timezone
1116
+ };
1117
+ }
1118
+
1119
+ // =============================================================================
1120
+ // Anonymous ID Generation
1121
+ // =============================================================================
1122
+
1123
+ // Storage key for anonymous ID
1124
+ const ANONYMOUS_ID_KEY = '@rejourney_anonymous_id';
1125
+
1126
+ /**
1127
+ * Generate a persistent anonymous ID
1128
+ */
1129
+ function generateAnonymousId() {
1130
+ const timestamp = Date.now().toString(36);
1131
+ const random = Math.random().toString(36).substring(2, 15);
1132
+ return `anon_${timestamp}_${random}`;
1133
+ }
1134
+
1135
+ /**
1136
+ * Initialize anonymous ID - tries to load from storage, generates new if not found
1137
+ * This is called internally and runs asynchronously
1138
+ */
1139
+ async function initAnonymousId() {
1140
+ try {
1141
+ // Try to load from AsyncStorage
1142
+ const AsyncStorage = require('@react-native-async-storage/async-storage').default;
1143
+ const storedId = await AsyncStorage.getItem(ANONYMOUS_ID_KEY);
1144
+ if (storedId) {
1145
+ anonymousId = storedId;
1146
+ } else {
1147
+ // Generate new ID and persist
1148
+ anonymousId = generateAnonymousId();
1149
+ await AsyncStorage.setItem(ANONYMOUS_ID_KEY, anonymousId);
1150
+ }
1151
+ } catch {
1152
+ // AsyncStorage not available or error - just generate without persistence
1153
+ if (!anonymousId) {
1154
+ anonymousId = generateAnonymousId();
1155
+ }
1156
+ }
1157
+ }
1158
+
1159
+ /**
1160
+ * Get the anonymous ID (synchronous - returns generated ID immediately)
1161
+ * For persistent ID, call initAnonymousId() first
1162
+ */
1163
+ export function getAnonymousId() {
1164
+ if (!anonymousId) {
1165
+ anonymousId = generateAnonymousId();
1166
+ // Try to persist asynchronously (fire and forget)
1167
+ initAnonymousId().catch(() => {});
1168
+ }
1169
+ return anonymousId;
1170
+ }
1171
+
1172
+ /**
1173
+ * Ensure a stable, persisted anonymous/device ID is available.
1174
+ * Returns the stored ID if present, otherwise generates and persists one.
1175
+ */
1176
+ export async function ensurePersistentAnonymousId() {
1177
+ if (anonymousId) return anonymousId;
1178
+ if (!anonymousIdPromise) {
1179
+ anonymousIdPromise = (async () => {
1180
+ await initAnonymousId();
1181
+ if (!anonymousId) {
1182
+ anonymousId = generateAnonymousId();
1183
+ }
1184
+ return anonymousId;
1185
+ })();
1186
+ }
1187
+ return anonymousIdPromise;
1188
+ }
1189
+
1190
+ /**
1191
+ * Load anonymous ID from persistent storage
1192
+ * Call this at app startup for best results
1193
+ */
1194
+ export async function loadAnonymousId() {
1195
+ await initAnonymousId();
1196
+ return anonymousId || generateAnonymousId();
1197
+ }
1198
+
1199
+ /**
1200
+ * Set a custom anonymous ID (e.g., from persistent storage)
1201
+ */
1202
+ export function setAnonymousId(id) {
1203
+ anonymousId = id;
1204
+ // Try to persist asynchronously
1205
+ try {
1206
+ const AsyncStorage = require('@react-native-async-storage/async-storage').default;
1207
+ AsyncStorage.setItem(ANONYMOUS_ID_KEY, id).catch(() => {});
1208
+ } catch {
1209
+ // Ignore if AsyncStorage not available
1210
+ }
1211
+ }
1212
+
1213
+ // =============================================================================
1214
+ // Exports
1215
+ // =============================================================================
1216
+
1217
+ export default {
1218
+ init: initAutoTracking,
1219
+ cleanup: cleanupAutoTracking,
1220
+ trackTap,
1221
+ trackScroll,
1222
+ trackGesture,
1223
+ trackInput,
1224
+ trackScreen,
1225
+ trackAPIRequest,
1226
+ captureError,
1227
+ getMetrics: getSessionMetrics,
1228
+ resetMetrics,
1229
+ collectDeviceInfo,
1230
+ getAnonymousId,
1231
+ setAnonymousId
1232
+ };
1233
+ //# sourceMappingURL=autoTracking.js.map