@rejourneyco/react-native 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +29 -0
  2. package/android/build.gradle.kts +135 -0
  3. package/android/consumer-rules.pro +10 -0
  4. package/android/proguard-rules.pro +1 -0
  5. package/android/src/main/AndroidManifest.xml +15 -0
  6. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +860 -0
  7. package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +290 -0
  8. package/android/src/main/java/com/rejourney/engine/DiagnosticLog.kt +385 -0
  9. package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +512 -0
  10. package/android/src/main/java/com/rejourney/platform/OEMDetector.kt +173 -0
  11. package/android/src/main/java/com/rejourney/platform/PerfTiming.kt +384 -0
  12. package/android/src/main/java/com/rejourney/platform/SessionLifecycleService.kt +160 -0
  13. package/android/src/main/java/com/rejourney/platform/Telemetry.kt +301 -0
  14. package/android/src/main/java/com/rejourney/platform/WindowUtils.kt +100 -0
  15. package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +129 -0
  16. package/android/src/main/java/com/rejourney/recording/EventBuffer.kt +330 -0
  17. package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +519 -0
  18. package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +740 -0
  19. package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +559 -0
  20. package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +238 -0
  21. package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +633 -0
  22. package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +232 -0
  23. package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +474 -0
  24. package/android/src/main/java/com/rejourney/utility/DataCompression.kt +63 -0
  25. package/android/src/main/java/com/rejourney/utility/ImageBlur.kt +412 -0
  26. package/android/src/main/java/com/rejourney/utility/ViewIdentifier.kt +169 -0
  27. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +232 -0
  28. package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
  29. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +268 -0
  30. package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
  31. package/ios/Engine/DeviceRegistrar.swift +288 -0
  32. package/ios/Engine/DiagnosticLog.swift +387 -0
  33. package/ios/Engine/RejourneyImpl.swift +719 -0
  34. package/ios/Recording/AnrSentinel.swift +142 -0
  35. package/ios/Recording/EventBuffer.swift +326 -0
  36. package/ios/Recording/InteractionRecorder.swift +428 -0
  37. package/ios/Recording/ReplayOrchestrator.swift +624 -0
  38. package/ios/Recording/SegmentDispatcher.swift +492 -0
  39. package/ios/Recording/StabilityMonitor.swift +223 -0
  40. package/ios/Recording/TelemetryPipeline.swift +547 -0
  41. package/ios/Recording/ViewHierarchyScanner.swift +156 -0
  42. package/ios/Recording/VisualCapture.swift +675 -0
  43. package/ios/Rejourney.h +38 -0
  44. package/ios/Rejourney.mm +375 -0
  45. package/ios/Utility/DataCompression.swift +55 -0
  46. package/ios/Utility/ImageBlur.swift +89 -0
  47. package/ios/Utility/RuntimeMethodSwap.swift +41 -0
  48. package/ios/Utility/ViewIdentifier.swift +37 -0
  49. package/lib/commonjs/NativeRejourney.js +40 -0
  50. package/lib/commonjs/components/Mask.js +88 -0
  51. package/lib/commonjs/index.js +1443 -0
  52. package/lib/commonjs/sdk/autoTracking.js +1087 -0
  53. package/lib/commonjs/sdk/constants.js +166 -0
  54. package/lib/commonjs/sdk/errorTracking.js +187 -0
  55. package/lib/commonjs/sdk/index.js +50 -0
  56. package/lib/commonjs/sdk/metricsTracking.js +205 -0
  57. package/lib/commonjs/sdk/navigation.js +128 -0
  58. package/lib/commonjs/sdk/networkInterceptor.js +375 -0
  59. package/lib/commonjs/sdk/utils.js +433 -0
  60. package/lib/commonjs/sdk/version.js +13 -0
  61. package/lib/commonjs/types/expo-router.d.js +2 -0
  62. package/lib/commonjs/types/index.js +2 -0
  63. package/lib/module/NativeRejourney.js +38 -0
  64. package/lib/module/components/Mask.js +83 -0
  65. package/lib/module/index.js +1341 -0
  66. package/lib/module/sdk/autoTracking.js +1059 -0
  67. package/lib/module/sdk/constants.js +154 -0
  68. package/lib/module/sdk/errorTracking.js +177 -0
  69. package/lib/module/sdk/index.js +26 -0
  70. package/lib/module/sdk/metricsTracking.js +187 -0
  71. package/lib/module/sdk/navigation.js +120 -0
  72. package/lib/module/sdk/networkInterceptor.js +364 -0
  73. package/lib/module/sdk/utils.js +412 -0
  74. package/lib/module/sdk/version.js +7 -0
  75. package/lib/module/types/expo-router.d.js +2 -0
  76. package/lib/module/types/index.js +2 -0
  77. package/lib/typescript/NativeRejourney.d.ts +160 -0
  78. package/lib/typescript/components/Mask.d.ts +54 -0
  79. package/lib/typescript/index.d.ts +117 -0
  80. package/lib/typescript/sdk/autoTracking.d.ts +226 -0
  81. package/lib/typescript/sdk/constants.d.ts +138 -0
  82. package/lib/typescript/sdk/errorTracking.d.ts +47 -0
  83. package/lib/typescript/sdk/index.d.ts +24 -0
  84. package/lib/typescript/sdk/metricsTracking.d.ts +75 -0
  85. package/lib/typescript/sdk/navigation.d.ts +48 -0
  86. package/lib/typescript/sdk/networkInterceptor.d.ts +62 -0
  87. package/lib/typescript/sdk/utils.d.ts +193 -0
  88. package/lib/typescript/sdk/version.d.ts +6 -0
  89. package/lib/typescript/types/index.d.ts +618 -0
  90. package/package.json +122 -0
  91. package/rejourney.podspec +23 -0
  92. package/src/NativeRejourney.ts +185 -0
  93. package/src/components/Mask.tsx +93 -0
  94. package/src/index.ts +1555 -0
  95. package/src/sdk/autoTracking.ts +1245 -0
  96. package/src/sdk/constants.ts +155 -0
  97. package/src/sdk/errorTracking.ts +231 -0
  98. package/src/sdk/index.ts +25 -0
  99. package/src/sdk/metricsTracking.ts +227 -0
  100. package/src/sdk/navigation.ts +152 -0
  101. package/src/sdk/networkInterceptor.ts +423 -0
  102. package/src/sdk/utils.ts +442 -0
  103. package/src/sdk/version.ts +6 -0
  104. package/src/types/expo-router.d.ts +7 -0
  105. package/src/types/index.ts +709 -0
@@ -0,0 +1,1087 @@
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.markTapHandled = markTapHandled;
18
+ exports.notifyStateChange = notifyStateChange;
19
+ exports.resetMetrics = resetMetrics;
20
+ exports.setAnonymousId = setAnonymousId;
21
+ exports.setMaxSessionDurationMinutes = setMaxSessionDurationMinutes;
22
+ exports.trackAPIRequest = trackAPIRequest;
23
+ exports.trackGesture = trackGesture;
24
+ exports.trackInput = trackInput;
25
+ exports.trackNavigationState = trackNavigationState;
26
+ exports.trackScreen = trackScreen;
27
+ exports.trackScroll = trackScroll;
28
+ exports.trackTap = trackTap;
29
+ exports.useNavigationTracking = useNavigationTracking;
30
+ var _utils = require("./utils");
31
+ /**
32
+ * Copyright 2026 Rejourney
33
+ *
34
+ * Licensed under the Apache License, Version 2.0 (the "License");
35
+ * you may not use this file except in compliance with the License.
36
+ * You may obtain a copy of the License at
37
+ *
38
+ * http://www.apache.org/licenses/LICENSE-2.0
39
+ *
40
+ * Unless required by applicable law or agreed to in writing, software
41
+ * distributed under the License is distributed on an "AS IS" BASIS,
42
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
43
+ * See the License for the specific language governing permissions and
44
+ * limitations under the License.
45
+ */
46
+
47
+ /**
48
+ * Rejourney Auto Tracking Module
49
+ *
50
+ * Automatic tracking features that work with just init() - no additional code needed.
51
+ * This module handles:
52
+ * - Rage tap detection
53
+ * - Error tracking (JS + React Native)
54
+ * - Session metrics aggregation
55
+ * - Device info collection
56
+ * - Anonymous ID generation
57
+ * - Funnel/screen tracking
58
+ * - Score calculations
59
+ *
60
+ * IMPORTANT: This file uses lazy loading for react-native imports to avoid
61
+ * "PlatformConstants could not be found" errors on RN 0.81+.
62
+ */
63
+
64
+ // Lazy-loaded React Native modules
65
+ let _RN = null;
66
+ function getRN() {
67
+ if (_RN) return _RN;
68
+ try {
69
+ _RN = require('react-native');
70
+ return _RN;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+ function getPlatform() {
76
+ return getRN()?.Platform;
77
+ }
78
+ function getDimensions() {
79
+ return getRN()?.Dimensions;
80
+ }
81
+ function getRejourneyNativeModule() {
82
+ const RN = getRN();
83
+ if (!RN) return null;
84
+ const {
85
+ TurboModuleRegistry,
86
+ NativeModules
87
+ } = RN;
88
+ let nativeModule = null;
89
+ if (TurboModuleRegistry && typeof TurboModuleRegistry.get === 'function') {
90
+ try {
91
+ nativeModule = TurboModuleRegistry.get('Rejourney');
92
+ } catch {
93
+ // Ignore
94
+ }
95
+ }
96
+ if (!nativeModule && NativeModules) {
97
+ nativeModule = NativeModules.Rejourney ?? null;
98
+ }
99
+ return nativeModule;
100
+ }
101
+ const _globalThis = globalThis;
102
+ let isInitialized = false;
103
+ let config = {};
104
+ const recentTaps = [];
105
+ let tapHead = 0;
106
+ let tapCount = 0;
107
+ const MAX_RECENT_TAPS = 10;
108
+ let metrics = createEmptyMetrics();
109
+ let sessionStartTime = 0;
110
+ let maxSessionDurationMs = 10 * 60 * 1000;
111
+ let currentScreen = '';
112
+ let screensVisited = [];
113
+ let anonymousId = null;
114
+ let anonymousIdPromise = null;
115
+ let onRageTapDetected = null;
116
+ let onErrorCaptured = null;
117
+ let onScreenChange = null;
118
+
119
+ /**
120
+ * Mark a tap as handled.
121
+ * No-op — kept for API compatibility. Dead tap detection is now native-side.
122
+ */
123
+ function markTapHandled() {
124
+ // No-op: dead tap detection is handled natively in TelemetryPipeline
125
+ }
126
+ // ========== End Dead Tap Detection ==========
127
+
128
+ let originalErrorHandler;
129
+ let originalOnError = null;
130
+ let originalOnUnhandledRejection = null;
131
+ let originalConsoleError = null;
132
+ let _promiseRejectionTrackingDisable = null;
133
+
134
+ /**
135
+ * Initialize auto tracking features
136
+ * Called automatically by Rejourney.init() - no user action needed
137
+ */
138
+ function initAutoTracking(trackingConfig, callbacks = {}) {
139
+ if (isInitialized) return;
140
+ config = {
141
+ rageTapThreshold: 3,
142
+ rageTapTimeWindow: 500,
143
+ rageTapRadius: 50,
144
+ trackJSErrors: true,
145
+ trackPromiseRejections: true,
146
+ trackReactNativeErrors: true,
147
+ collectDeviceInfo: true,
148
+ maxSessionDurationMs: trackingConfig.maxSessionDurationMs,
149
+ ...trackingConfig
150
+ };
151
+ sessionStartTime = Date.now();
152
+ setMaxSessionDurationMinutes(trackingConfig.maxSessionDurationMs ? trackingConfig.maxSessionDurationMs / 60000 : undefined);
153
+ onRageTapDetected = callbacks.onRageTap || null;
154
+ onErrorCaptured = callbacks.onError || null;
155
+ onScreenChange = callbacks.onScreen || null;
156
+ setupErrorTracking();
157
+ setupNavigationTracking();
158
+ loadAnonymousId().then(id => {
159
+ anonymousId = id;
160
+ });
161
+ isInitialized = true;
162
+ }
163
+
164
+ /**
165
+ * Cleanup auto tracking features
166
+ */
167
+ function cleanupAutoTracking() {
168
+ if (!isInitialized) return;
169
+ restoreErrorHandlers();
170
+ cleanupNavigationTracking();
171
+
172
+ // Reset state
173
+ tapHead = 0;
174
+ tapCount = 0;
175
+ metrics = createEmptyMetrics();
176
+ screensVisited = [];
177
+ currentScreen = '';
178
+ sessionStartTime = 0;
179
+ maxSessionDurationMs = 10 * 60 * 1000;
180
+ isInitialized = false;
181
+ }
182
+
183
+ /**
184
+ * Track a tap event for rage tap detection
185
+ * Called automatically from touch interceptor
186
+ */
187
+ function trackTap(tap) {
188
+ if (!isInitialized) return;
189
+ const now = Date.now();
190
+ const insertIndex = (tapHead + tapCount) % MAX_RECENT_TAPS;
191
+ if (tapCount < MAX_RECENT_TAPS) {
192
+ recentTaps[insertIndex] = {
193
+ ...tap,
194
+ timestamp: now
195
+ };
196
+ tapCount++;
197
+ } else {
198
+ recentTaps[tapHead] = {
199
+ ...tap,
200
+ timestamp: now
201
+ };
202
+ tapHead = (tapHead + 1) % MAX_RECENT_TAPS;
203
+ }
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
+ detectRageTap();
215
+ metrics.touchCount++;
216
+ metrics.totalEvents++;
217
+ // Dead tap detection is now handled natively in TelemetryPipeline
218
+ }
219
+
220
+ /**
221
+ * Detect if recent taps form a rage tap pattern
222
+ */
223
+ function detectRageTap() {
224
+ const threshold = config.rageTapThreshold || 3;
225
+ const radius = config.rageTapRadius || 50;
226
+ if (tapCount < threshold) return;
227
+ const tapsToCheck = [];
228
+ for (let i = 0; i < threshold; i++) {
229
+ const idx = (tapHead + tapCount - threshold + i) % MAX_RECENT_TAPS;
230
+ tapsToCheck.push(recentTaps[idx]);
231
+ }
232
+ let centerX = 0;
233
+ let centerY = 0;
234
+ for (const tap of tapsToCheck) {
235
+ centerX += tap.x;
236
+ centerY += tap.y;
237
+ }
238
+ centerX /= tapsToCheck.length;
239
+ centerY /= tapsToCheck.length;
240
+ let allWithinRadius = true;
241
+ for (const tap of tapsToCheck) {
242
+ const dx = tap.x - centerX;
243
+ const dy = tap.y - centerY;
244
+ const distance = Math.sqrt(dx * dx + dy * dy);
245
+ if (distance > radius) {
246
+ allWithinRadius = false;
247
+ break;
248
+ }
249
+ }
250
+ if (allWithinRadius) {
251
+ metrics.rageTapCount++;
252
+ tapHead = 0;
253
+ tapCount = 0;
254
+
255
+ // Notify callback
256
+ if (onRageTapDetected) {
257
+ onRageTapDetected(threshold, centerX, centerY);
258
+ }
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Notify that a state change occurred (navigation, modal, etc.)
264
+ * Kept for API compatibility
265
+ */
266
+ function notifyStateChange() {
267
+ // No-op: dead tap detection is handled natively in TelemetryPipeline
268
+ }
269
+
270
+ /**
271
+ * Setup automatic error tracking
272
+ */
273
+ function setupErrorTracking() {
274
+ if (config.trackReactNativeErrors !== false) {
275
+ setupReactNativeErrorHandler();
276
+ }
277
+ if (config.trackJSErrors !== false && typeof _globalThis !== 'undefined') {
278
+ setupJSErrorHandler();
279
+ }
280
+ if (config.trackPromiseRejections !== false && typeof _globalThis !== 'undefined') {
281
+ setupPromiseRejectionHandler();
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Setup React Native ErrorUtils handler
287
+ *
288
+ * CRITICAL FIX: For fatal errors, we delay calling the original handler by 500ms
289
+ * to give the React Native bridge time to flush the logEvent('error') call to the
290
+ * native TelemetryPipeline. Without this delay, the error event is queued on the
291
+ * JS→native bridge but the app crashes (via originalErrorHandler) before the bridge
292
+ * flushes, so the error is lost. Crashes are captured separately by native crash
293
+ * handlers, but the corresponding JS error record was never making it to the backend.
294
+ */
295
+ function setupReactNativeErrorHandler() {
296
+ try {
297
+ const ErrorUtils = _globalThis.ErrorUtils;
298
+ if (!ErrorUtils) return;
299
+ originalErrorHandler = ErrorUtils.getGlobalHandler();
300
+ ErrorUtils.setGlobalHandler((error, isFatal) => {
301
+ trackError({
302
+ type: 'error',
303
+ timestamp: Date.now(),
304
+ message: error.message || String(error),
305
+ stack: error.stack,
306
+ name: error.name || 'Error'
307
+ });
308
+ if (originalErrorHandler) {
309
+ if (isFatal) {
310
+ // For fatal errors, delay the original handler so the native bridge
311
+ // has time to deliver the error event to TelemetryPipeline before
312
+ // the app terminates. 500ms is enough for the bridge to flush.
313
+ setTimeout(() => {
314
+ originalErrorHandler(error, isFatal);
315
+ }, 500);
316
+ } else {
317
+ originalErrorHandler(error, isFatal);
318
+ }
319
+ }
320
+ });
321
+ } catch {
322
+ // Ignore
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Setup global JS error handler
328
+ */
329
+ function setupJSErrorHandler() {
330
+ if (typeof _globalThis.onerror !== 'undefined') {
331
+ originalOnError = _globalThis.onerror;
332
+ _globalThis.onerror = (message, source, lineno, colno, error) => {
333
+ trackError({
334
+ type: 'error',
335
+ timestamp: Date.now(),
336
+ message: typeof message === 'string' ? message : 'Unknown error',
337
+ stack: error?.stack || `${source}:${lineno}:${colno}`,
338
+ name: error?.name || 'Error'
339
+ });
340
+ if (originalOnError) {
341
+ return originalOnError(message, source, lineno, colno, error);
342
+ }
343
+ return false;
344
+ };
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Setup unhandled promise rejection handler
350
+ *
351
+ * React Native's Hermes engine does NOT support the web-standard
352
+ * globalThis.addEventListener('unhandledrejection', ...) API.
353
+ * We use two complementary strategies:
354
+ *
355
+ * 1. React Native's built-in promise rejection tracking polyfill
356
+ * (promise/setimmediate/rejection-tracking) — fires for all
357
+ * unhandled rejections, including those that never hit ErrorUtils.
358
+ *
359
+ * 2. console.error interception — newer RN versions (0.73+) report
360
+ * unhandled promise rejections via console.error with a recognizable
361
+ * prefix. We intercept these as a fallback.
362
+ *
363
+ * 3. Web API fallback — for non-RN environments (e.g., testing in a browser).
364
+ */
365
+ function setupPromiseRejectionHandler() {
366
+ let rnTrackingSetUp = false;
367
+
368
+ // Strategy 1: RN-specific promise rejection tracking polyfill
369
+ try {
370
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
371
+ const tracking = require('promise/setimmediate/rejection-tracking');
372
+ if (tracking && typeof tracking.enable === 'function') {
373
+ tracking.enable({
374
+ allRejections: true,
375
+ onUnhandled: (_id, error) => {
376
+ trackError({
377
+ type: 'error',
378
+ timestamp: Date.now(),
379
+ message: error?.message || String(error) || 'Unhandled Promise Rejection',
380
+ stack: error?.stack,
381
+ name: error?.name || 'UnhandledRejection'
382
+ });
383
+ },
384
+ onHandled: () => {/* no-op */}
385
+ });
386
+ _promiseRejectionTrackingDisable = () => {
387
+ try {
388
+ tracking.disable();
389
+ } catch {/* ignore */}
390
+ };
391
+ rnTrackingSetUp = true;
392
+ }
393
+ } catch {
394
+ // Polyfill not available — fall through to other strategies
395
+ }
396
+
397
+ // Strategy 2: Intercept console.error for promise rejection messages
398
+ // Newer RN versions log "Possible Unhandled Promise Rejection" via console.error
399
+ if (!rnTrackingSetUp && typeof console !== 'undefined' && console.error) {
400
+ originalConsoleError = console.error;
401
+ console.error = (...args) => {
402
+ // Detect RN-style promise rejection messages
403
+ const firstArg = args[0];
404
+ if (typeof firstArg === 'string' && firstArg.includes('Possible Unhandled Promise Rejection')) {
405
+ const error = args[1];
406
+ trackError({
407
+ type: 'error',
408
+ timestamp: Date.now(),
409
+ message: error?.message || String(error) || firstArg,
410
+ stack: error?.stack,
411
+ name: error?.name || 'UnhandledRejection'
412
+ });
413
+ }
414
+ // Always call through to original console.error
415
+ if (originalConsoleError) {
416
+ originalConsoleError.apply(console, args);
417
+ }
418
+ };
419
+ }
420
+
421
+ // Strategy 3: Web API fallback (works in browser-based testing, not in RN Hermes)
422
+ if (!rnTrackingSetUp && typeof _globalThis.addEventListener !== 'undefined') {
423
+ const handler = event => {
424
+ const reason = event.reason;
425
+ trackError({
426
+ type: 'error',
427
+ timestamp: Date.now(),
428
+ message: reason?.message || String(reason) || 'Unhandled Promise Rejection',
429
+ stack: reason?.stack,
430
+ name: reason?.name || 'UnhandledRejection'
431
+ });
432
+ };
433
+ originalOnUnhandledRejection = handler;
434
+ _globalThis.addEventListener('unhandledrejection', handler);
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Restore original error handlers
440
+ */
441
+ function restoreErrorHandlers() {
442
+ if (originalErrorHandler) {
443
+ try {
444
+ const ErrorUtils = _globalThis.ErrorUtils;
445
+ if (ErrorUtils) {
446
+ ErrorUtils.setGlobalHandler(originalErrorHandler);
447
+ }
448
+ } catch {
449
+ // Ignore
450
+ }
451
+ originalErrorHandler = undefined;
452
+ }
453
+ if (originalOnError !== null) {
454
+ _globalThis.onerror = originalOnError;
455
+ originalOnError = null;
456
+ }
457
+
458
+ // Restore promise rejection tracking
459
+ if (_promiseRejectionTrackingDisable) {
460
+ _promiseRejectionTrackingDisable();
461
+ _promiseRejectionTrackingDisable = null;
462
+ }
463
+ if (originalConsoleError) {
464
+ console.error = originalConsoleError;
465
+ originalConsoleError = null;
466
+ }
467
+ if (originalOnUnhandledRejection && typeof _globalThis.removeEventListener !== 'undefined') {
468
+ _globalThis.removeEventListener('unhandledrejection', originalOnUnhandledRejection);
469
+ originalOnUnhandledRejection = null;
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Track an error
475
+ */
476
+ function trackError(error) {
477
+ metrics.errorCount++;
478
+ metrics.totalEvents++;
479
+ if (onErrorCaptured) {
480
+ onErrorCaptured(error);
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Manually track an error (for API errors, etc.)
486
+ */
487
+ function captureError(message, stack, name) {
488
+ trackError({
489
+ type: 'error',
490
+ timestamp: Date.now(),
491
+ message,
492
+ stack,
493
+ name: name || 'Error'
494
+ });
495
+ }
496
+ let navigationPollingInterval = null;
497
+ let lastDetectedScreen = '';
498
+ let navigationSetupDone = false;
499
+ let navigationPollingErrors = 0;
500
+ const MAX_POLLING_ERRORS = 10; // Stop polling after 10 consecutive errors
501
+
502
+ /**
503
+ * Track a navigation state change from React Navigation.
504
+ *
505
+ * For bare React Native apps using @react-navigation/native.
506
+ * Just add this to your NavigationContainer's onStateChange prop.
507
+ *
508
+ * @example
509
+ * ```tsx
510
+ * import { trackNavigationState } from 'rejourney';
511
+ *
512
+ * <NavigationContainer onStateChange={trackNavigationState}>
513
+ * ...
514
+ * </NavigationContainer>
515
+ * ```
516
+ */
517
+ function trackNavigationState(state) {
518
+ if (!state?.routes) return;
519
+ try {
520
+ const {
521
+ normalizeScreenName
522
+ } = require('./navigation');
523
+ const findActiveScreen = navState => {
524
+ if (!navState?.routes) return null;
525
+ const index = navState.index ?? navState.routes.length - 1;
526
+ const route = navState.routes[index];
527
+ if (!route) return null;
528
+ if (route.state) return findActiveScreen(route.state);
529
+ return normalizeScreenName(route.name || 'Unknown');
530
+ };
531
+ const screenName = findActiveScreen(state);
532
+ if (screenName && screenName !== lastDetectedScreen) {
533
+ lastDetectedScreen = screenName;
534
+ trackScreen(screenName);
535
+ }
536
+ } catch {
537
+ // Ignore
538
+ }
539
+ }
540
+
541
+ /**
542
+ * React hook for navigation tracking.
543
+ *
544
+ * Returns props to spread on NavigationContainer that will:
545
+ * 1. Track the initial screen on mount (via onReady)
546
+ * 2. Track all subsequent navigations (via onStateChange)
547
+ *
548
+ * This is the RECOMMENDED approach for bare React Native apps.
549
+ *
550
+ * @example
551
+ * ```tsx
552
+ * import { useNavigationTracking } from 'rejourney';
553
+ * import { NavigationContainer } from '@react-navigation/native';
554
+ *
555
+ * function App() {
556
+ * const navigationTracking = useNavigationTracking();
557
+ *
558
+ * return (
559
+ * <NavigationContainer {...navigationTracking}>
560
+ * <RootNavigator />
561
+ * </NavigationContainer>
562
+ * );
563
+ * }
564
+ * ```
565
+ */
566
+ function useNavigationTracking() {
567
+ const React = require('react');
568
+ const {
569
+ createNavigationContainerRef
570
+ } = require('@react-navigation/native');
571
+ const navigationRef = React.useRef(createNavigationContainerRef());
572
+ const onReady = React.useCallback(() => {
573
+ try {
574
+ const currentRoute = navigationRef.current?.getCurrentRoute?.();
575
+ if (currentRoute?.name) {
576
+ const {
577
+ normalizeScreenName
578
+ } = require('./navigation');
579
+ const screenName = normalizeScreenName(currentRoute.name);
580
+ if (screenName && screenName !== lastDetectedScreen) {
581
+ lastDetectedScreen = screenName;
582
+ trackScreen(screenName);
583
+ }
584
+ }
585
+ } catch {
586
+ // Ignore
587
+ }
588
+ }, []);
589
+ return {
590
+ ref: navigationRef.current,
591
+ onReady,
592
+ onStateChange: trackNavigationState
593
+ };
594
+ }
595
+
596
+ /**
597
+ * Setup automatic Expo Router tracking
598
+ *
599
+ * For Expo apps using expo-router - works automatically.
600
+ * For bare React Native apps - use trackNavigationState instead.
601
+ */
602
+ function setupNavigationTracking() {
603
+ if (navigationSetupDone) return;
604
+ navigationSetupDone = true;
605
+ if (__DEV__) {
606
+ _utils.logger.debug('Setting up navigation tracking...');
607
+ }
608
+ let attempts = 0;
609
+ const maxAttempts = 5;
610
+ const trySetup = () => {
611
+ attempts++;
612
+ if (__DEV__) {
613
+ _utils.logger.debug('Navigation setup attempt', attempts, 'of', maxAttempts);
614
+ }
615
+ const success = trySetupExpoRouter();
616
+ if (success) {
617
+ if (__DEV__) {
618
+ _utils.logger.debug('Expo Router setup: SUCCESS on attempt', attempts);
619
+ }
620
+ } else if (attempts < maxAttempts) {
621
+ const delay = 200 * attempts;
622
+ if (__DEV__) {
623
+ _utils.logger.debug('Expo Router not ready, retrying in', delay, 'ms');
624
+ }
625
+ setTimeout(trySetup, delay);
626
+ } else {
627
+ if (__DEV__) {
628
+ _utils.logger.debug('Expo Router setup: FAILED after', maxAttempts, 'attempts');
629
+ _utils.logger.debug('For manual navigation tracking, use trackNavigationState() on your NavigationContainer');
630
+ }
631
+ }
632
+ };
633
+ setTimeout(trySetup, 200);
634
+ }
635
+
636
+ /**
637
+ * Set up Expo Router auto-tracking by polling the internal router store
638
+ *
639
+ * Supports both expo-router v3 (store.rootState) and v6+ (store.state, store.navigationRef)
640
+ */
641
+ function trySetupExpoRouter() {
642
+ try {
643
+ const expoRouter = require('expo-router');
644
+ const router = expoRouter.router;
645
+ if (!router) {
646
+ if (__DEV__) {
647
+ _utils.logger.debug('Expo Router: router object not found');
648
+ }
649
+ return false;
650
+ }
651
+ if (__DEV__) {
652
+ _utils.logger.debug('Expo Router: Setting up navigation tracking');
653
+ }
654
+ const {
655
+ normalizeScreenName,
656
+ getScreenNameFromPath
657
+ } = require('./navigation');
658
+ navigationPollingInterval = setInterval(() => {
659
+ try {
660
+ let state = null;
661
+ let stateSource = '';
662
+ if (typeof router.getState === 'function') {
663
+ state = router.getState();
664
+ stateSource = 'router.getState()';
665
+ } else if (router.rootState) {
666
+ state = router.rootState;
667
+ stateSource = 'router.rootState';
668
+ }
669
+ if (!state) {
670
+ try {
671
+ const storeModule = require('expo-router/build/global-state/router-store');
672
+ if (storeModule?.store) {
673
+ state = storeModule.store.state;
674
+ if (state) stateSource = 'store.state';
675
+ if (!state && storeModule.store.navigationRef?.current) {
676
+ state = storeModule.store.navigationRef.current.getRootState?.();
677
+ if (state) stateSource = 'navigationRef.getRootState()';
678
+ }
679
+ if (!state) {
680
+ state = storeModule.store.rootState || storeModule.store.initialState;
681
+ if (state) stateSource = 'store.rootState/initialState';
682
+ }
683
+ }
684
+ } catch {
685
+ // Ignore
686
+ }
687
+ }
688
+ if (!state) {
689
+ try {
690
+ const imperative = require('expo-router/build/imperative-api');
691
+ if (imperative?.router) {
692
+ state = imperative.router.getState?.();
693
+ if (state) stateSource = 'imperative-api';
694
+ }
695
+ } catch {
696
+ // Ignore
697
+ }
698
+ }
699
+ if (state) {
700
+ navigationPollingErrors = 0;
701
+ navigationPollingErrors = 0;
702
+ const screenName = extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName);
703
+ if (screenName && screenName !== lastDetectedScreen) {
704
+ if (__DEV__) {
705
+ _utils.logger.debug('Screen changed:', lastDetectedScreen, '->', screenName, `(source: ${stateSource})`);
706
+ }
707
+ lastDetectedScreen = screenName;
708
+ trackScreen(screenName);
709
+ }
710
+ } else {
711
+ navigationPollingErrors++;
712
+ if (__DEV__ && navigationPollingErrors === 1) {
713
+ _utils.logger.debug('Expo Router: Could not get navigation state');
714
+ }
715
+ if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
716
+ cleanupNavigationTracking();
717
+ }
718
+ }
719
+ } catch (e) {
720
+ navigationPollingErrors++;
721
+ if (__DEV__ && navigationPollingErrors === 1) {
722
+ _utils.logger.debug('Expo Router polling error:', e);
723
+ }
724
+ if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
725
+ cleanupNavigationTracking();
726
+ }
727
+ }
728
+ }, 500);
729
+ return true;
730
+ } catch (e) {
731
+ if (__DEV__) {
732
+ _utils.logger.debug('Expo Router not available:', e);
733
+ }
734
+ return false;
735
+ }
736
+ }
737
+
738
+ /**
739
+ * Extract screen name from Expo Router navigation state
740
+ *
741
+ * Handles complex nested structures like Drawer → Tabs → Stack
742
+ * by recursively accumulating segments from each navigation level.
743
+ */
744
+ function extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName, accumulatedSegments = []) {
745
+ if (!state?.routes) return null;
746
+ const route = state.routes[state.index ?? state.routes.length - 1];
747
+ if (!route) return null;
748
+ const newSegments = [...accumulatedSegments, route.name];
749
+ if (route.state) {
750
+ return extractScreenNameFromRouterState(route.state, getScreenNameFromPath, normalizeScreenName, newSegments);
751
+ }
752
+ const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
753
+ if (cleanSegments.length === 0) {
754
+ for (let i = newSegments.length - 1; i >= 0; i--) {
755
+ const seg = newSegments[i];
756
+ if (seg && !seg.startsWith('(') && !seg.endsWith(')')) {
757
+ cleanSegments.push(seg);
758
+ break;
759
+ }
760
+ }
761
+ }
762
+ const pathname = '/' + cleanSegments.join('/');
763
+ return getScreenNameFromPath(pathname, newSegments);
764
+ }
765
+
766
+ /**
767
+ * Cleanup navigation tracking
768
+ */
769
+ function cleanupNavigationTracking() {
770
+ if (navigationPollingInterval) {
771
+ clearInterval(navigationPollingInterval);
772
+ navigationPollingInterval = null;
773
+ }
774
+ navigationSetupDone = false;
775
+ lastDetectedScreen = '';
776
+ navigationPollingErrors = 0;
777
+ }
778
+
779
+ /**
780
+ * Track a screen view
781
+ * This updates JS metrics AND notifies the native module to send to backend
782
+ */
783
+ function trackScreen(screenName) {
784
+ if (!isInitialized) {
785
+ if (__DEV__) {
786
+ _utils.logger.debug('trackScreen called but not initialized, screen:', screenName);
787
+ }
788
+ return;
789
+ }
790
+ const previousScreen = currentScreen;
791
+ currentScreen = screenName;
792
+ screensVisited.push(screenName);
793
+ const uniqueScreens = new Set(screensVisited);
794
+ metrics.uniqueScreensCount = uniqueScreens.size;
795
+ metrics.navigationCount++;
796
+ metrics.totalEvents++;
797
+ if (__DEV__) {
798
+ _utils.logger.debug('trackScreen:', screenName, '(total screens:', metrics.uniqueScreensCount, ')');
799
+ }
800
+ if (onScreenChange) {
801
+ onScreenChange(screenName, previousScreen);
802
+ }
803
+ try {
804
+ const RejourneyNative = getRejourneyNativeModule();
805
+ if (RejourneyNative?.screenChanged) {
806
+ if (__DEV__) {
807
+ _utils.logger.debug('Notifying native screenChanged:', screenName);
808
+ }
809
+ RejourneyNative.screenChanged(screenName).catch(e => {
810
+ if (__DEV__) {
811
+ _utils.logger.debug('Native screenChanged error:', e);
812
+ }
813
+ });
814
+ } else if (__DEV__) {
815
+ _utils.logger.debug('Native screenChanged method not available');
816
+ }
817
+ } catch (e) {
818
+ if (__DEV__) {
819
+ _utils.logger.debug('trackScreen native call error:', e);
820
+ }
821
+ }
822
+ }
823
+
824
+ /**
825
+ * Track an API request with timing data
826
+ */
827
+ function trackAPIRequest(success, _statusCode, durationMs = 0, responseBytes = 0) {
828
+ if (!isInitialized) return;
829
+ metrics.apiTotalCount++;
830
+ if (durationMs > 0) {
831
+ metrics.netTotalDurationMs += durationMs;
832
+ }
833
+ if (responseBytes > 0) {
834
+ metrics.netTotalBytes += responseBytes;
835
+ }
836
+ if (success) {
837
+ metrics.apiSuccessCount++;
838
+ } else {
839
+ metrics.apiErrorCount++;
840
+ metrics.errorCount++;
841
+ }
842
+ }
843
+
844
+ /**
845
+ * Create empty metrics object
846
+ */
847
+ function createEmptyMetrics() {
848
+ return {
849
+ totalEvents: 0,
850
+ touchCount: 0,
851
+ scrollCount: 0,
852
+ gestureCount: 0,
853
+ inputCount: 0,
854
+ navigationCount: 0,
855
+ errorCount: 0,
856
+ rageTapCount: 0,
857
+ deadTapCount: 0,
858
+ apiSuccessCount: 0,
859
+ apiErrorCount: 0,
860
+ apiTotalCount: 0,
861
+ netTotalDurationMs: 0,
862
+ netTotalBytes: 0,
863
+ screensVisited: [],
864
+ uniqueScreensCount: 0,
865
+ interactionScore: 100,
866
+ explorationScore: 100,
867
+ uxScore: 100
868
+ };
869
+ }
870
+
871
+ /**
872
+ * Track a scroll event
873
+ */
874
+ function trackScroll() {
875
+ if (!isInitialized) return;
876
+ metrics.scrollCount++;
877
+ metrics.totalEvents++;
878
+ }
879
+
880
+ /**
881
+ * Track a gesture event
882
+ */
883
+ function trackGesture() {
884
+ if (!isInitialized) return;
885
+ metrics.gestureCount++;
886
+ metrics.totalEvents++;
887
+ }
888
+
889
+ /**
890
+ * Track an input event (keyboard)
891
+ */
892
+ function trackInput() {
893
+ if (!isInitialized) return;
894
+ metrics.inputCount++;
895
+ metrics.totalEvents++;
896
+ }
897
+
898
+ /**
899
+ * Get current session metrics
900
+ */
901
+ function getSessionMetrics() {
902
+ calculateScores();
903
+ const netAvgDurationMs = metrics.apiTotalCount > 0 ? Math.round(metrics.netTotalDurationMs / metrics.apiTotalCount) : 0;
904
+ return {
905
+ ...metrics,
906
+ screensVisited: [...screensVisited],
907
+ netAvgDurationMs
908
+ };
909
+ }
910
+
911
+ /**
912
+ * Calculate session scores
913
+ */
914
+ function calculateScores() {
915
+ const totalInteractions = metrics.touchCount + metrics.scrollCount + metrics.gestureCount + metrics.inputCount;
916
+ const avgInteractions = 50;
917
+ metrics.interactionScore = Math.min(100, Math.round(totalInteractions / avgInteractions * 100));
918
+ const avgScreens = 5;
919
+ metrics.explorationScore = Math.min(100, Math.round(metrics.uniqueScreensCount / avgScreens * 100));
920
+ let uxScore = 100;
921
+ uxScore -= Math.min(30, metrics.errorCount * 15);
922
+ uxScore -= Math.min(24, metrics.rageTapCount * 8);
923
+ uxScore -= Math.min(16, metrics.deadTapCount * 4);
924
+ uxScore -= Math.min(20, metrics.apiErrorCount * 10);
925
+ if (metrics.uniqueScreensCount >= 3) {
926
+ uxScore += 5;
927
+ }
928
+ metrics.uxScore = Math.max(0, Math.min(100, uxScore));
929
+ }
930
+
931
+ /**
932
+ * Reset metrics for new session
933
+ */
934
+ function resetMetrics() {
935
+ metrics = createEmptyMetrics();
936
+ screensVisited = [];
937
+ currentScreen = '';
938
+ tapHead = 0;
939
+ tapCount = 0;
940
+ sessionStartTime = Date.now();
941
+ }
942
+ function setMaxSessionDurationMinutes(minutes) {
943
+ const clampedMinutes = Math.min(10, Math.max(1, minutes ?? 10));
944
+ maxSessionDurationMs = clampedMinutes * 60 * 1000;
945
+ }
946
+ function hasExceededMaxSessionDuration() {
947
+ if (!sessionStartTime) return false;
948
+ return Date.now() - sessionStartTime >= maxSessionDurationMs;
949
+ }
950
+ function getRemainingSessionDurationMs() {
951
+ if (!sessionStartTime) return maxSessionDurationMs;
952
+ const remaining = maxSessionDurationMs - (Date.now() - sessionStartTime);
953
+ return Math.max(0, remaining);
954
+ }
955
+
956
+ /**
957
+ * Collect device information
958
+ */
959
+ /**
960
+ * Collect device information
961
+ */
962
+ async function collectDeviceInfo() {
963
+ const Dimensions = getDimensions();
964
+ const Platform = getPlatform();
965
+ let width = 0,
966
+ height = 0,
967
+ scale = 1;
968
+ if (Dimensions) {
969
+ const windowDims = Dimensions.get('window');
970
+ const screenDims = Dimensions.get('screen');
971
+ width = windowDims?.width || 0;
972
+ height = windowDims?.height || 0;
973
+ scale = screenDims?.scale || 1;
974
+ }
975
+
976
+ // Basic JS-side info
977
+ let locale;
978
+ let timezone;
979
+ try {
980
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
981
+ locale = Intl.DateTimeFormat().resolvedOptions().locale;
982
+ } catch {
983
+ // Ignore
984
+ }
985
+
986
+ // Get native info
987
+ const nativeModule = getRejourneyNativeModule();
988
+ let nativeInfo = {};
989
+ if (nativeModule && nativeModule.getDeviceInfo) {
990
+ try {
991
+ nativeInfo = await nativeModule.getDeviceInfo();
992
+ } catch (e) {
993
+ if (__DEV__) {
994
+ console.warn('[Rejourney] Failed to get native device info:', e);
995
+ }
996
+ }
997
+ }
998
+ return {
999
+ model: nativeInfo.model || 'Unknown',
1000
+ manufacturer: nativeInfo.brand,
1001
+ os: Platform?.OS || 'ios',
1002
+ osVersion: nativeInfo.systemVersion || Platform?.Version?.toString() || 'Unknown',
1003
+ screenWidth: Math.round(width),
1004
+ screenHeight: Math.round(height),
1005
+ pixelRatio: scale,
1006
+ appVersion: nativeInfo.appVersion,
1007
+ appId: nativeInfo.bundleId,
1008
+ locale: locale,
1009
+ timezone: timezone
1010
+ };
1011
+ }
1012
+
1013
+ /**
1014
+ * Generate a persistent anonymous ID
1015
+ */
1016
+ function generateAnonymousId() {
1017
+ const timestamp = Date.now().toString(36);
1018
+ const random = Math.random().toString(36).substring(2, 15);
1019
+ return `anon_${timestamp}_${random}`;
1020
+ }
1021
+
1022
+ /**
1023
+ * Get the anonymous ID (synchronous - returns generated ID immediately)
1024
+ */
1025
+ function getAnonymousId() {
1026
+ if (!anonymousId) {
1027
+ anonymousId = generateAnonymousId();
1028
+ }
1029
+ return anonymousId;
1030
+ }
1031
+
1032
+ /**
1033
+ * Ensure a stable, persisted anonymous/device ID is available.
1034
+ * Returns the stored ID if present, otherwise generates and persists one.
1035
+ */
1036
+ async function ensurePersistentAnonymousId() {
1037
+ if (anonymousId) return anonymousId;
1038
+ if (!anonymousIdPromise) {
1039
+ anonymousIdPromise = (async () => {
1040
+ const id = await loadAnonymousId();
1041
+ anonymousId = id;
1042
+ return id;
1043
+ })();
1044
+ }
1045
+ return anonymousIdPromise;
1046
+ }
1047
+
1048
+ /**
1049
+ * Load anonymous ID from persistent storage
1050
+ * Call this at app startup for best results
1051
+ */
1052
+ async function loadAnonymousId() {
1053
+ const nativeModule = getRejourneyNativeModule();
1054
+ if (nativeModule && nativeModule.getUserIdentity) {
1055
+ try {
1056
+ return (await nativeModule.getUserIdentity()) || generateAnonymousId();
1057
+ } catch {
1058
+ return generateAnonymousId();
1059
+ }
1060
+ }
1061
+ return generateAnonymousId();
1062
+ }
1063
+
1064
+ /**
1065
+ * Set a custom anonymous ID
1066
+ */
1067
+ function setAnonymousId(id) {
1068
+ anonymousId = id;
1069
+ }
1070
+ var _default = exports.default = {
1071
+ init: initAutoTracking,
1072
+ cleanup: cleanupAutoTracking,
1073
+ trackTap,
1074
+ trackScroll,
1075
+ trackGesture,
1076
+ trackInput,
1077
+ trackScreen,
1078
+ trackAPIRequest,
1079
+ captureError,
1080
+ getMetrics: getSessionMetrics,
1081
+ resetMetrics,
1082
+ collectDeviceInfo,
1083
+ getAnonymousId,
1084
+ setAnonymousId,
1085
+ markTapHandled
1086
+ };
1087
+ //# sourceMappingURL=autoTracking.js.map