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