@rejourneyco/react-native 1.0.0 → 1.0.2

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 (49) hide show
  1. package/README.md +29 -0
  2. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +47 -30
  3. package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +25 -1
  4. package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +70 -32
  5. package/android/src/main/java/com/rejourney/core/Constants.kt +4 -4
  6. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +14 -0
  7. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +9 -0
  8. package/ios/Capture/RJCaptureEngine.m +72 -34
  9. package/ios/Capture/RJCaptureHeuristics.h +7 -5
  10. package/ios/Capture/RJCaptureHeuristics.m +138 -112
  11. package/ios/Capture/RJVideoEncoder.m +0 -26
  12. package/ios/Core/Rejourney.mm +64 -102
  13. package/ios/Utils/RJPerfTiming.m +0 -5
  14. package/ios/Utils/RJWindowUtils.m +0 -1
  15. package/lib/commonjs/components/Mask.js +1 -6
  16. package/lib/commonjs/index.js +12 -101
  17. package/lib/commonjs/sdk/autoTracking.js +55 -353
  18. package/lib/commonjs/sdk/constants.js +2 -13
  19. package/lib/commonjs/sdk/errorTracking.js +1 -29
  20. package/lib/commonjs/sdk/metricsTracking.js +3 -24
  21. package/lib/commonjs/sdk/navigation.js +3 -42
  22. package/lib/commonjs/sdk/networkInterceptor.js +7 -49
  23. package/lib/commonjs/sdk/utils.js +0 -5
  24. package/lib/module/components/Mask.js +1 -6
  25. package/lib/module/index.js +11 -105
  26. package/lib/module/sdk/autoTracking.js +55 -354
  27. package/lib/module/sdk/constants.js +2 -13
  28. package/lib/module/sdk/errorTracking.js +1 -29
  29. package/lib/module/sdk/index.js +0 -2
  30. package/lib/module/sdk/metricsTracking.js +3 -24
  31. package/lib/module/sdk/navigation.js +3 -42
  32. package/lib/module/sdk/networkInterceptor.js +7 -49
  33. package/lib/module/sdk/utils.js +0 -5
  34. package/lib/typescript/NativeRejourney.d.ts +2 -0
  35. package/lib/typescript/sdk/autoTracking.d.ts +5 -6
  36. package/lib/typescript/types/index.d.ts +0 -1
  37. package/package.json +11 -3
  38. package/src/NativeRejourney.ts +4 -0
  39. package/src/components/Mask.tsx +0 -3
  40. package/src/index.ts +11 -88
  41. package/src/sdk/autoTracking.ts +72 -331
  42. package/src/sdk/constants.ts +13 -13
  43. package/src/sdk/errorTracking.ts +1 -17
  44. package/src/sdk/index.ts +0 -2
  45. package/src/sdk/metricsTracking.ts +5 -33
  46. package/src/sdk/navigation.ts +8 -29
  47. package/src/sdk/networkInterceptor.ts +9 -33
  48. package/src/sdk/utils.ts +0 -5
  49. package/src/types/index.ts +0 -29
@@ -42,9 +42,7 @@ function getDimensions() {
42
42
  return getRN()?.Dimensions;
43
43
  }
44
44
 
45
- function getNativeModules() {
46
- return getRN()?.NativeModules;
47
- }
45
+
48
46
 
49
47
  function getRejourneyNativeModule() {
50
48
  const RN = getRN();
@@ -57,7 +55,7 @@ function getRejourneyNativeModule() {
57
55
  try {
58
56
  nativeModule = TurboModuleRegistry.get('Rejourney');
59
57
  } catch {
60
- // Ignore and fall back to NativeModules
58
+ // Ignore
61
59
  }
62
60
  }
63
61
 
@@ -68,8 +66,6 @@ function getRejourneyNativeModule() {
68
66
  return nativeModule;
69
67
  }
70
68
 
71
- // Type declarations for browser globals (only used in hybrid apps where DOM is available)
72
- // These don't exist in pure React Native but are needed for error tracking in hybrid scenarios
73
69
  type OnErrorEventHandler = ((
74
70
  event: Event | string,
75
71
  source?: string,
@@ -83,7 +79,6 @@ interface PromiseRejectionEvent {
83
79
  promise?: Promise<any>;
84
80
  }
85
81
 
86
- // Cast globalThis to work with both RN and hybrid scenarios
87
82
  const _globalThis = globalThis as typeof globalThis & {
88
83
  onerror?: OnErrorEventHandler;
89
84
  addEventListener?: (type: string, handler: (event: any) => void) => void;
@@ -94,10 +89,6 @@ const _globalThis = globalThis as typeof globalThis & {
94
89
  };
95
90
  };
96
91
 
97
- // =============================================================================
98
- // Types
99
- // =============================================================================
100
-
101
92
  export interface TapEvent {
102
93
  x: number;
103
94
  y: number;
@@ -106,93 +97,63 @@ export interface TapEvent {
106
97
  }
107
98
 
108
99
  export interface SessionMetrics {
109
- // Core counts
110
100
  totalEvents: number;
111
101
  touchCount: number;
112
102
  scrollCount: number;
113
103
  gestureCount: number;
114
104
  inputCount: number;
115
105
  navigationCount: number;
116
-
117
- // Issue tracking
118
106
  errorCount: number;
119
107
  rageTapCount: number;
120
-
121
- // API metrics
122
108
  apiSuccessCount: number;
123
109
  apiErrorCount: number;
124
110
  apiTotalCount: number;
125
-
126
- // Network timing (for avg calculation)
127
- netTotalDurationMs: number; // Sum of all API durations
128
- netTotalBytes: number; // Sum of all response bytes
129
-
130
- // Screen tracking for funnels
111
+ netTotalDurationMs: number;
112
+ netTotalBytes: number;
131
113
  screensVisited: string[];
132
114
  uniqueScreensCount: number;
133
115
 
134
- // Scores (0-100)
135
116
  interactionScore: number;
136
117
  explorationScore: number;
137
118
  uxScore: number;
138
119
  }
139
120
 
140
121
  export interface AutoTrackingConfig {
141
- // Rage tap detection
142
- rageTapThreshold?: number; // Number of taps (default: 3)
143
- rageTapTimeWindow?: number; // Time window in ms (default: 500)
144
- rageTapRadius?: number; // Max radius in pixels (default: 50)
145
-
146
- // Error tracking
147
- trackJSErrors?: boolean; // Track window errors (default: true)
148
- trackPromiseRejections?: boolean; // Track unhandled rejections (default: true)
149
- trackReactNativeErrors?: boolean; // Track RN ErrorUtils (default: true)
150
-
151
- // Session settings
152
- collectDeviceInfo?: boolean; // Collect device info (default: true)
153
- maxSessionDurationMs?: number; // Clamp to 1–10 minutes; default set from server/config
122
+ rageTapThreshold?: number;
123
+ rageTapTimeWindow?: number;
124
+ rageTapRadius?: number;
125
+ trackJSErrors?: boolean;
126
+ trackPromiseRejections?: boolean;
127
+ trackReactNativeErrors?: boolean;
128
+ collectDeviceInfo?: boolean;
129
+ maxSessionDurationMs?: number;
154
130
  }
155
131
 
156
- // =============================================================================
157
- // State
158
- // =============================================================================
159
-
160
132
  let isInitialized = false;
161
133
  let config: AutoTrackingConfig = {};
162
134
 
163
- // Rage tap tracking
164
135
  const recentTaps: TapEvent[] = [];
165
- let tapHead = 0; // Circular buffer head pointer
166
- let tapCount = 0; // Actual count of taps in buffer
136
+ let tapHead = 0;
137
+ let tapCount = 0;
167
138
  const MAX_RECENT_TAPS = 10;
168
139
 
169
- // Session metrics
170
140
  let metrics: SessionMetrics = createEmptyMetrics();
171
141
  let sessionStartTime: number = 0;
172
142
  let maxSessionDurationMs: number = 10 * 60 * 1000;
173
-
174
- // Screen tracking
175
143
  let currentScreen = '';
176
144
  let screensVisited: string[] = [];
177
145
 
178
- // Anonymous ID
179
146
  let anonymousId: string | null = null;
180
147
  let anonymousIdPromise: Promise<string> | null = null;
181
148
 
182
- // Callbacks
183
149
  let onRageTapDetected: ((count: number, x: number, y: number) => void) | null = null;
184
150
  let onErrorCaptured: ((error: ErrorEvent) => void) | null = null;
185
151
  let onScreenChange: ((screenName: string, previousScreen?: string) => void) | null = null;
186
152
 
187
- // Original error handlers (for restoration)
188
153
  let originalErrorHandler: ((error: Error, isFatal: boolean) => void) | undefined;
189
154
  let originalOnError: OnErrorEventHandler | null = null;
190
155
  let originalOnUnhandledRejection: ((event: PromiseRejectionEvent) => void) | null = null;
191
156
 
192
- // =============================================================================
193
- // Initialization
194
- // =============================================================================
195
-
196
157
  /**
197
158
  * Initialize auto tracking features
198
159
  * Called automatically by Rejourney.init() - no user action needed
@@ -219,7 +180,6 @@ export function initAutoTracking(
219
180
  ...trackingConfig,
220
181
  };
221
182
 
222
- // Session timing
223
183
  sessionStartTime = Date.now();
224
184
  setMaxSessionDurationMinutes(
225
185
  trackingConfig.maxSessionDurationMs
@@ -227,21 +187,14 @@ export function initAutoTracking(
227
187
  : undefined
228
188
  );
229
189
 
230
- // Set callbacks
231
190
  onRageTapDetected = callbacks.onRageTap || null;
232
191
  onErrorCaptured = callbacks.onError || null;
233
192
  onScreenChange = callbacks.onScreen || null;
234
-
235
- // Initialize metrics
236
- metrics = createEmptyMetrics();
237
- sessionStartTime = Date.now();
238
- anonymousId = generateAnonymousId();
239
-
240
- // Setup error tracking
241
193
  setupErrorTracking();
242
-
243
- // Setup React Navigation tracking (if available)
244
194
  setupNavigationTracking();
195
+ loadAnonymousId().then(id => {
196
+ anonymousId = id;
197
+ });
245
198
 
246
199
  isInitialized = true;
247
200
  }
@@ -252,10 +205,7 @@ export function initAutoTracking(
252
205
  export function cleanupAutoTracking(): void {
253
206
  if (!isInitialized) return;
254
207
 
255
- // Restore original error handlers
256
208
  restoreErrorHandlers();
257
-
258
- // Cleanup navigation tracking
259
209
  cleanupNavigationTracking();
260
210
 
261
211
  // Reset state
@@ -269,10 +219,6 @@ export function cleanupAutoTracking(): void {
269
219
  isInitialized = false;
270
220
  }
271
221
 
272
- // =============================================================================
273
- // Rage Tap Detection
274
- // =============================================================================
275
-
276
222
  /**
277
223
  * Track a tap event for rage tap detection
278
224
  * Called automatically from touch interceptor
@@ -282,18 +228,14 @@ export function trackTap(tap: TapEvent): void {
282
228
 
283
229
  const now = Date.now();
284
230
 
285
- // Add to circular buffer (O(1) instead of shift() which is O(n))
286
231
  const insertIndex = (tapHead + tapCount) % MAX_RECENT_TAPS;
287
232
  if (tapCount < MAX_RECENT_TAPS) {
288
233
  recentTaps[insertIndex] = { ...tap, timestamp: now };
289
234
  tapCount++;
290
235
  } else {
291
- // Buffer full, overwrite oldest
292
236
  recentTaps[tapHead] = { ...tap, timestamp: now };
293
237
  tapHead = (tapHead + 1) % MAX_RECENT_TAPS;
294
238
  }
295
-
296
- // Evict old taps outside time window
297
239
  const windowStart = now - (config.rageTapTimeWindow || 500);
298
240
  while (tapCount > 0) {
299
241
  const oldestTap = recentTaps[tapHead];
@@ -305,10 +247,7 @@ export function trackTap(tap: TapEvent): void {
305
247
  }
306
248
  }
307
249
 
308
- // Check for rage tap
309
250
  detectRageTap();
310
-
311
- // Update metrics
312
251
  metrics.touchCount++;
313
252
  metrics.totalEvents++;
314
253
  }
@@ -321,14 +260,12 @@ function detectRageTap(): void {
321
260
  const radius = config.rageTapRadius || 50;
322
261
 
323
262
  if (tapCount < threshold) return;
324
- // Check last N taps from circular buffer
325
263
  const tapsToCheck: TapEvent[] = [];
326
264
  for (let i = 0; i < threshold; i++) {
327
265
  const idx = (tapHead + tapCount - threshold + i) % MAX_RECENT_TAPS;
328
266
  tapsToCheck.push(recentTaps[idx]!);
329
267
  }
330
268
 
331
- // Calculate center point
332
269
  let centerX = 0;
333
270
  let centerY = 0;
334
271
  for (const tap of tapsToCheck) {
@@ -338,7 +275,6 @@ function detectRageTap(): void {
338
275
  centerX /= tapsToCheck.length;
339
276
  centerY /= tapsToCheck.length;
340
277
 
341
- // Check if all taps are within radius of center
342
278
  let allWithinRadius = true;
343
279
  for (const tap of tapsToCheck) {
344
280
  const dx = tap.x - centerX;
@@ -351,9 +287,7 @@ function detectRageTap(): void {
351
287
  }
352
288
 
353
289
  if (allWithinRadius) {
354
- // Rage tap detected!
355
290
  metrics.rageTapCount++;
356
- // Clear circular buffer to prevent duplicate detection
357
291
  tapHead = 0;
358
292
  tapCount = 0;
359
293
 
@@ -364,10 +298,6 @@ function detectRageTap(): void {
364
298
  }
365
299
  }
366
300
 
367
- // =============================================================================
368
- // State Change Notification
369
- // =============================================================================
370
-
371
301
  /**
372
302
  * Notify that a state change occurred (navigation, modal, etc.)
373
303
  * Kept for API compatibility
@@ -376,25 +306,18 @@ export function notifyStateChange(): void {
376
306
  // No-op - kept for backward compatibility
377
307
  }
378
308
 
379
- // =============================================================================
380
- // Error Tracking
381
- // =============================================================================
382
-
383
309
  /**
384
310
  * Setup automatic error tracking
385
311
  */
386
312
  function setupErrorTracking(): void {
387
- // Track React Native errors
388
313
  if (config.trackReactNativeErrors !== false) {
389
314
  setupReactNativeErrorHandler();
390
315
  }
391
316
 
392
- // Track JavaScript errors (only works in web/debug)
393
317
  if (config.trackJSErrors !== false && typeof _globalThis !== 'undefined') {
394
318
  setupJSErrorHandler();
395
319
  }
396
320
 
397
- // Track unhandled promise rejections
398
321
  if (config.trackPromiseRejections !== false && typeof _globalThis !== 'undefined') {
399
322
  setupPromiseRejectionHandler();
400
323
  }
@@ -405,16 +328,12 @@ function setupErrorTracking(): void {
405
328
  */
406
329
  function setupReactNativeErrorHandler(): void {
407
330
  try {
408
- // Access ErrorUtils from global scope
409
331
  const ErrorUtils = _globalThis.ErrorUtils;
410
332
  if (!ErrorUtils) return;
411
333
 
412
- // Store original handler
413
334
  originalErrorHandler = ErrorUtils.getGlobalHandler();
414
335
 
415
- // Set new handler
416
336
  ErrorUtils.setGlobalHandler((error: Error, isFatal: boolean) => {
417
- // Track the error
418
337
  trackError({
419
338
  type: 'error',
420
339
  timestamp: Date.now(),
@@ -423,13 +342,12 @@ function setupReactNativeErrorHandler(): void {
423
342
  name: error.name || 'Error',
424
343
  });
425
344
 
426
- // Call original handler
427
345
  if (originalErrorHandler) {
428
346
  originalErrorHandler(error, isFatal);
429
347
  }
430
348
  });
431
349
  } catch {
432
- // ErrorUtils not available
350
+ // Ignore
433
351
  }
434
352
  }
435
353
 
@@ -438,8 +356,6 @@ function setupReactNativeErrorHandler(): void {
438
356
  */
439
357
  function setupJSErrorHandler(): void {
440
358
  if (typeof _globalThis.onerror !== 'undefined') {
441
- // Note: In React Native, this typically doesn't fire
442
- // But we set it up anyway for hybrid apps
443
359
  originalOnError = _globalThis.onerror;
444
360
 
445
361
  _globalThis.onerror = (
@@ -457,7 +373,6 @@ function setupJSErrorHandler(): void {
457
373
  name: error?.name || 'Error',
458
374
  });
459
375
 
460
- // Call original handler
461
376
  if (originalOnError) {
462
377
  return originalOnError(message, source, lineno, colno, error);
463
378
  }
@@ -491,7 +406,6 @@ function setupPromiseRejectionHandler(): void {
491
406
  * Restore original error handlers
492
407
  */
493
408
  function restoreErrorHandlers(): void {
494
- // Restore React Native handler
495
409
  if (originalErrorHandler) {
496
410
  try {
497
411
  const ErrorUtils = _globalThis.ErrorUtils;
@@ -504,13 +418,11 @@ function restoreErrorHandlers(): void {
504
418
  originalErrorHandler = undefined;
505
419
  }
506
420
 
507
- // Restore global onerror
508
421
  if (originalOnError !== null) {
509
422
  _globalThis.onerror = originalOnError;
510
423
  originalOnError = null;
511
424
  }
512
425
 
513
- // Remove promise rejection handler
514
426
  if (originalOnUnhandledRejection && typeof _globalThis.removeEventListener !== 'undefined') {
515
427
  _globalThis.removeEventListener!('unhandledrejection', originalOnUnhandledRejection);
516
428
  originalOnUnhandledRejection = null;
@@ -546,15 +458,10 @@ export function captureError(
546
458
  });
547
459
  }
548
460
 
549
- // =============================================================================
550
- // Screen/Funnel Tracking - Automatic Navigation Detection
551
- // =============================================================================
552
-
553
- // Navigation detection state
554
461
  let navigationPollingInterval: ReturnType<typeof setInterval> | null = null;
555
462
  let lastDetectedScreen = '';
556
463
  let navigationSetupDone = false;
557
- let navigationPollingErrors = 0; // Track consecutive errors
464
+ let navigationPollingErrors = 0;
558
465
  const MAX_POLLING_ERRORS = 10; // Stop polling after 10 consecutive errors
559
466
 
560
467
  /**
@@ -578,7 +485,6 @@ export function trackNavigationState(state: any): void {
578
485
  try {
579
486
  const { normalizeScreenName } = require('./navigation');
580
487
 
581
- // Find the active screen recursively
582
488
  const findActiveScreen = (navState: any): string | null => {
583
489
  if (!navState?.routes) return null;
584
490
  const index = navState.index ?? navState.routes.length - 1;
@@ -594,7 +500,7 @@ export function trackNavigationState(state: any): void {
594
500
  trackScreen(screenName);
595
501
  }
596
502
  } catch {
597
- // Navigation tracking error
503
+ // Ignore
598
504
  }
599
505
  }
600
506
 
@@ -624,14 +530,11 @@ export function trackNavigationState(state: any): void {
624
530
  * ```
625
531
  */
626
532
  export function useNavigationTracking() {
627
- // Use React's useRef and useCallback to create stable references
628
533
  const React = require('react');
629
534
  const { createNavigationContainerRef } = require('@react-navigation/native');
630
535
 
631
- // Create a stable navigation ref
632
536
  const navigationRef = React.useRef(createNavigationContainerRef());
633
537
 
634
- // Track initial screen when navigation is ready
635
538
  const onReady = React.useCallback(() => {
636
539
  try {
637
540
  const currentRoute = navigationRef.current?.getCurrentRoute?.();
@@ -644,11 +547,10 @@ export function useNavigationTracking() {
644
547
  }
645
548
  }
646
549
  } catch {
647
- // Navigation not ready yet
550
+ // Ignore
648
551
  }
649
552
  }, []);
650
553
 
651
- // Return props to spread on NavigationContainer
652
554
  return {
653
555
  ref: navigationRef.current,
654
556
  onReady,
@@ -674,22 +576,21 @@ function setupNavigationTracking(): void {
674
576
  // We retry a few times with increasing delays
675
577
  let attempts = 0;
676
578
  const maxAttempts = 5;
677
-
579
+
678
580
  const trySetup = () => {
679
581
  attempts++;
680
582
  if (__DEV__) {
681
583
  logger.debug('Navigation setup attempt', attempts, 'of', maxAttempts);
682
584
  }
683
-
585
+
684
586
  const success = trySetupExpoRouter();
685
-
587
+
686
588
  if (success) {
687
589
  if (__DEV__) {
688
590
  logger.debug('Expo Router setup: SUCCESS on attempt', attempts);
689
591
  }
690
592
  } else if (attempts < maxAttempts) {
691
- // Retry with exponential backoff
692
- const delay = 200 * attempts; // 200, 400, 600, 800ms
593
+ const delay = 200 * attempts;
693
594
  if (__DEV__) {
694
595
  logger.debug('Expo Router not ready, retrying in', delay, 'ms');
695
596
  }
@@ -702,7 +603,6 @@ function setupNavigationTracking(): void {
702
603
  }
703
604
  };
704
605
 
705
- // Start first attempt after 200ms
706
606
  setTimeout(trySetup, 200);
707
607
  }
708
608
 
@@ -729,13 +629,10 @@ function trySetupExpoRouter(): boolean {
729
629
 
730
630
  const { normalizeScreenName, getScreenNameFromPath } = require('./navigation');
731
631
 
732
- // Poll for route changes (expo-router doesn't expose a listener API outside hooks)
733
632
  navigationPollingInterval = setInterval(() => {
734
633
  try {
735
634
  let state = null;
736
635
  let stateSource = '';
737
-
738
- // Method 1: Public accessors on router object
739
636
  if (typeof router.getState === 'function') {
740
637
  state = router.getState();
741
638
  stateSource = 'router.getState()';
@@ -744,33 +641,28 @@ function trySetupExpoRouter(): boolean {
744
641
  stateSource = 'router.rootState';
745
642
  }
746
643
 
747
- // Method 2: Internal store access (works for both v3 and v6+)
748
644
  if (!state) {
749
645
  try {
750
646
  const storeModule = require('expo-router/build/global-state/router-store');
751
647
  if (storeModule?.store) {
752
- // v6+: store.state or store.navigationRef
753
648
  state = storeModule.store.state;
754
649
  if (state) stateSource = 'store.state';
755
650
 
756
- // v6+: Try navigationRef if state is undefined
757
651
  if (!state && storeModule.store.navigationRef?.current) {
758
652
  state = storeModule.store.navigationRef.current.getRootState?.();
759
653
  if (state) stateSource = 'navigationRef.getRootState()';
760
654
  }
761
655
 
762
- // v3: store.rootState or store.initialState
763
656
  if (!state) {
764
657
  state = storeModule.store.rootState || storeModule.store.initialState;
765
658
  if (state) stateSource = 'store.rootState/initialState';
766
659
  }
767
660
  }
768
661
  } catch {
769
- // Store not available
662
+ // Ignore
770
663
  }
771
664
  }
772
665
 
773
- // Method 3: Try accessing via a different export path for v6
774
666
  if (!state) {
775
667
  try {
776
668
  const imperative = require('expo-router/build/imperative-api');
@@ -779,12 +671,12 @@ function trySetupExpoRouter(): boolean {
779
671
  if (state) stateSource = 'imperative-api';
780
672
  }
781
673
  } catch {
782
- // Imperative API not available
674
+ // Ignore
783
675
  }
784
676
  }
785
677
 
786
678
  if (state) {
787
- // Reset error count on success
679
+ navigationPollingErrors = 0;
788
680
  navigationPollingErrors = 0;
789
681
  const screenName = extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName);
790
682
  if (screenName && screenName !== lastDetectedScreen) {
@@ -795,21 +687,15 @@ function trySetupExpoRouter(): boolean {
795
687
  trackScreen(screenName);
796
688
  }
797
689
  } else {
798
- // Track consecutive failures to get state
799
690
  navigationPollingErrors++;
800
691
  if (__DEV__ && navigationPollingErrors === 1) {
801
692
  logger.debug('Expo Router: Could not get navigation state');
802
693
  }
803
694
  if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
804
- // Stop polling after too many errors to save CPU
805
- if (__DEV__) {
806
- logger.debug('Expo Router: Stopped polling after', MAX_POLLING_ERRORS, 'errors');
807
- }
808
695
  cleanupNavigationTracking();
809
696
  }
810
697
  }
811
698
  } catch (e) {
812
- // Error - track and potentially stop
813
699
  navigationPollingErrors++;
814
700
  if (__DEV__ && navigationPollingErrors === 1) {
815
701
  logger.debug('Expo Router polling error:', e);
@@ -818,14 +704,13 @@ function trySetupExpoRouter(): boolean {
818
704
  cleanupNavigationTracking();
819
705
  }
820
706
  }
821
- }, 500); // 500ms polling (reduced from 300ms for lower CPU usage)
707
+ }, 500);
822
708
 
823
709
  return true;
824
710
  } catch (e) {
825
711
  if (__DEV__) {
826
712
  logger.debug('Expo Router not available:', e);
827
713
  }
828
- // expo-router not installed
829
714
  return false;
830
715
  }
831
716
  }
@@ -847,10 +732,8 @@ function extractScreenNameFromRouterState(
847
732
  const route = state.routes[state.index ?? state.routes.length - 1];
848
733
  if (!route) return null;
849
734
 
850
- // Add current route name to accumulated segments
851
735
  const newSegments = [...accumulatedSegments, route.name];
852
736
 
853
- // If this route has nested state, recurse deeper
854
737
  if (route.state) {
855
738
  return extractScreenNameFromRouterState(
856
739
  route.state,
@@ -860,13 +743,9 @@ function extractScreenNameFromRouterState(
860
743
  );
861
744
  }
862
745
 
863
- // We've reached the deepest level - build the screen name
864
- // Filter out group markers like (tabs), (main), (auth)
865
746
  const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
866
747
 
867
- // If after filtering we have no segments, use the last meaningful name
868
748
  if (cleanSegments.length === 0) {
869
- // Find the last non-group segment
870
749
  for (let i = newSegments.length - 1; i >= 0; i--) {
871
750
  const seg = newSegments[i];
872
751
  if (seg && !seg.startsWith('(') && !seg.endsWith(')')) {
@@ -907,14 +786,10 @@ export function trackScreen(screenName: string): void {
907
786
 
908
787
  const previousScreen = currentScreen;
909
788
  currentScreen = screenName;
910
- // Add to screens visited (only track for unique set, avoid large array copies)
911
789
  screensVisited.push(screenName);
912
790
 
913
- // Update unique screens count
914
791
  const uniqueScreens = new Set(screensVisited);
915
792
  metrics.uniqueScreensCount = uniqueScreens.size;
916
-
917
- // Update navigation count
918
793
  metrics.navigationCount++;
919
794
  metrics.totalEvents++;
920
795
 
@@ -922,13 +797,10 @@ export function trackScreen(screenName: string): void {
922
797
  logger.debug('trackScreen:', screenName, '(total screens:', metrics.uniqueScreensCount, ')');
923
798
  }
924
799
 
925
- // Notify callback
926
800
  if (onScreenChange) {
927
801
  onScreenChange(screenName, previousScreen);
928
802
  }
929
803
 
930
- // IMPORTANT: Also notify native module to send to backend
931
- // This is the key fix - without this, screens don't get recorded!
932
804
  try {
933
805
  const RejourneyNative = getRejourneyNativeModule();
934
806
  if (RejourneyNative?.screenChanged) {
@@ -950,10 +822,6 @@ export function trackScreen(screenName: string): void {
950
822
  }
951
823
  }
952
824
 
953
- // =============================================================================
954
- // API Metrics Tracking
955
- // =============================================================================
956
-
957
825
  /**
958
826
  * Track an API request with timing data
959
827
  */
@@ -967,7 +835,6 @@ export function trackAPIRequest(
967
835
 
968
836
  metrics.apiTotalCount++;
969
837
 
970
- // Accumulate timing and size for avg calculation
971
838
  if (durationMs > 0) {
972
839
  metrics.netTotalDurationMs += durationMs;
973
840
  }
@@ -979,16 +846,10 @@ export function trackAPIRequest(
979
846
  metrics.apiSuccessCount++;
980
847
  } else {
981
848
  metrics.apiErrorCount++;
982
-
983
- // API errors also count toward error count for UX score
984
849
  metrics.errorCount++;
985
850
  }
986
851
  }
987
852
 
988
- // =============================================================================
989
- // Session Metrics
990
- // =============================================================================
991
-
992
853
  /**
993
854
  * Create empty metrics object
994
855
  */
@@ -1046,19 +907,15 @@ export function trackInput(): void {
1046
907
  * Get current session metrics
1047
908
  */
1048
909
  export function getSessionMetrics(): SessionMetrics & { netAvgDurationMs: number } {
1049
- // Calculate scores before returning
1050
910
  calculateScores();
1051
911
 
1052
- // Compute average API response time
1053
912
  const netAvgDurationMs = metrics.apiTotalCount > 0
1054
913
  ? Math.round(metrics.netTotalDurationMs / metrics.apiTotalCount)
1055
914
  : 0;
1056
915
 
1057
- // Lazily populate screensVisited only when metrics are retrieved
1058
- // This avoids expensive array copies on every screen change
1059
916
  return {
1060
917
  ...metrics,
1061
- screensVisited: [...screensVisited], // Only copy here when needed
918
+ screensVisited: [...screensVisited],
1062
919
  netAvgDurationMs,
1063
920
  };
1064
921
  }
@@ -1067,38 +924,26 @@ export function getSessionMetrics(): SessionMetrics & { netAvgDurationMs: number
1067
924
  * Calculate session scores
1068
925
  */
1069
926
  function calculateScores(): void {
1070
- // Interaction Score (0-100)
1071
- // Based on total interactions normalized to a baseline
1072
927
  const totalInteractions =
1073
928
  metrics.touchCount +
1074
929
  metrics.scrollCount +
1075
930
  metrics.gestureCount +
1076
931
  metrics.inputCount;
1077
932
 
1078
- // Assume 50 interactions is "average" for a session
1079
933
  const avgInteractions = 50;
1080
934
  metrics.interactionScore = Math.min(100, Math.round((totalInteractions / avgInteractions) * 100));
1081
935
 
1082
- // Exploration Score (0-100)
1083
- // Based on unique screens visited
1084
- // Assume 5 screens is "average" exploration
1085
936
  const avgScreens = 5;
1086
937
  metrics.explorationScore = Math.min(100, Math.round((metrics.uniqueScreensCount / avgScreens) * 100));
1087
938
 
1088
- // UX Score (0-100)
1089
- // Starts at 100, deducts for issues
1090
939
  let uxScore = 100;
1091
940
 
1092
- // Deduct for errors
1093
- uxScore -= Math.min(30, metrics.errorCount * 15); // Max 30 point deduction
941
+ uxScore -= Math.min(30, metrics.errorCount * 15);
1094
942
 
1095
- // Deduct for rage taps
1096
- uxScore -= Math.min(24, metrics.rageTapCount * 8); // Max 24 point deduction
943
+ uxScore -= Math.min(24, metrics.rageTapCount * 8);
1097
944
 
1098
- // Deduct for API errors
1099
- uxScore -= Math.min(20, metrics.apiErrorCount * 10); // Max 20 point deduction
945
+ uxScore -= Math.min(20, metrics.apiErrorCount * 10);
1100
946
 
1101
- // Bonus for completing funnel (if screens > 3)
1102
947
  if (metrics.uniqueScreensCount >= 3) {
1103
948
  uxScore += 5;
1104
949
  }
@@ -1118,44 +963,33 @@ export function resetMetrics(): void {
1118
963
  sessionStartTime = Date.now();
1119
964
  }
1120
965
 
1121
- // =============================================================================
1122
- // Session duration helpers
1123
- // =============================================================================
1124
-
1125
- /** Clamp and set max session duration in minutes (1–10). Defaults to 10. */
1126
966
  export function setMaxSessionDurationMinutes(minutes?: number): void {
1127
967
  const clampedMinutes = Math.min(10, Math.max(1, minutes ?? 10));
1128
968
  maxSessionDurationMs = clampedMinutes * 60 * 1000;
1129
969
  }
1130
-
1131
- /** Returns true if the current session exceeded the configured max duration. */
1132
970
  export function hasExceededMaxSessionDuration(): boolean {
1133
971
  if (!sessionStartTime) return false;
1134
972
  return Date.now() - sessionStartTime >= maxSessionDurationMs;
1135
973
  }
1136
974
 
1137
- /** Returns remaining milliseconds until the session should stop. */
1138
975
  export function getRemainingSessionDurationMs(): number {
1139
976
  if (!sessionStartTime) return maxSessionDurationMs;
1140
977
  const remaining = maxSessionDurationMs - (Date.now() - sessionStartTime);
1141
978
  return Math.max(0, remaining);
1142
979
  }
1143
980
 
1144
- // =============================================================================
1145
- // Device Info Collection
1146
- // =============================================================================
1147
-
1148
981
  /**
1149
982
  * Collect device information
1150
983
  */
1151
- export function collectDeviceInfo(): DeviceInfo {
984
+ /**
985
+ * Collect device information
986
+ */
987
+ export async function collectDeviceInfo(): Promise<DeviceInfo> {
1152
988
  const Dimensions = getDimensions();
1153
989
  const Platform = getPlatform();
1154
- const NativeModules = getNativeModules();
1155
-
1156
- // Default values if react-native isn't available
990
+
1157
991
  let width = 0, height = 0, scale = 1;
1158
-
992
+
1159
993
  if (Dimensions) {
1160
994
  const windowDims = Dimensions.get('window');
1161
995
  const screenDims = Dimensions.get('screen');
@@ -1164,105 +998,46 @@ export function collectDeviceInfo(): DeviceInfo {
1164
998
  scale = screenDims?.scale || 1;
1165
999
  }
1166
1000
 
1167
- // Get device model - this varies by platform
1168
- let model = 'Unknown';
1169
- let manufacturer: string | undefined;
1170
- let osVersion = 'Unknown';
1171
- let appVersion: string | undefined;
1172
- let appId: string | undefined;
1001
+ // Basic JS-side info
1173
1002
  let locale: string | undefined;
1174
1003
  let timezone: string | undefined;
1175
1004
 
1176
1005
  try {
1177
- // Try to get from react-native-device-info if available
1178
- // This is optional - falls back to basic info if not installed
1179
- const DeviceInfo = require('react-native-device-info');
1180
- model = DeviceInfo.getModel?.() || model;
1181
- manufacturer = DeviceInfo.getBrand?.() || undefined;
1182
- osVersion = DeviceInfo.getSystemVersion?.() || osVersion;
1183
- appVersion = DeviceInfo.getVersion?.() || undefined;
1184
- appId = DeviceInfo.getBundleId?.() || undefined;
1185
- locale = DeviceInfo.getDeviceLocale?.() || undefined;
1186
- timezone = DeviceInfo.getTimezone?.() || undefined;
1006
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
1007
+ locale = Intl.DateTimeFormat().resolvedOptions().locale;
1187
1008
  } catch {
1188
- // react-native-device-info not installed, try Expo packages
1189
- try {
1190
- // Try expo-application for app version/id
1191
- const Application = require('expo-application');
1192
- appVersion = Application.nativeApplicationVersion || Application.applicationVersion || undefined;
1193
- appId = Application.applicationId || undefined;
1194
- } catch {
1195
- // expo-application not available
1196
- }
1197
-
1198
- try {
1199
- // Try expo-constants for additional info
1200
- const Constants = require('expo-constants');
1201
- const expoConfig = Constants.expoConfig || Constants.manifest2?.extra?.expoClient || Constants.manifest;
1202
- if (!appVersion && expoConfig?.version) {
1203
- appVersion = expoConfig.version;
1204
- }
1205
- if (!appId && (expoConfig?.ios?.bundleIdentifier || expoConfig?.android?.package)) {
1206
- appId = Platform?.OS === 'ios'
1207
- ? expoConfig?.ios?.bundleIdentifier
1208
- : expoConfig?.android?.package;
1209
- }
1210
- } catch {
1211
- // expo-constants not available
1212
- }
1213
-
1214
- // Fall back to basic platform info
1215
- if (Platform?.OS === 'ios') {
1216
- // Get basic info from constants
1217
- const PlatformConstants = NativeModules?.PlatformConstants;
1218
- osVersion = Platform.Version?.toString() || osVersion;
1219
- model = PlatformConstants?.interfaceIdiom === 'pad' ? 'iPad' : 'iPhone';
1220
- } else if (Platform?.OS === 'android') {
1221
- osVersion = Platform.Version?.toString() || osVersion;
1222
- model = 'Android Device';
1223
- }
1009
+ // Ignore
1224
1010
  }
1225
1011
 
1226
- // Get timezone
1227
- if (!timezone) {
1228
- try {
1229
- timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
1230
- } catch {
1231
- timezone = undefined;
1232
- }
1233
- }
1012
+ // Get native info
1013
+ const nativeModule = getRejourneyNativeModule();
1014
+ let nativeInfo: any = {};
1234
1015
 
1235
- // Get locale
1236
- if (!locale) {
1016
+ if (nativeModule && nativeModule.getDeviceInfo) {
1237
1017
  try {
1238
- locale = Intl.DateTimeFormat().resolvedOptions().locale;
1239
- } catch {
1240
- locale = undefined;
1018
+ nativeInfo = await nativeModule.getDeviceInfo();
1019
+ } catch (e) {
1020
+ if (__DEV__) {
1021
+ console.warn('[Rejourney] Failed to get native device info:', e);
1022
+ }
1241
1023
  }
1242
1024
  }
1243
1025
 
1244
1026
  return {
1245
- model,
1246
- manufacturer,
1027
+ model: nativeInfo.model || 'Unknown',
1028
+ manufacturer: nativeInfo.brand,
1247
1029
  os: (Platform?.OS || 'ios') as 'ios' | 'android',
1248
- osVersion,
1030
+ osVersion: nativeInfo.systemVersion || Platform?.Version?.toString() || 'Unknown',
1249
1031
  screenWidth: Math.round(width),
1250
1032
  screenHeight: Math.round(height),
1251
1033
  pixelRatio: scale,
1252
- appVersion,
1253
- appId,
1254
- locale,
1255
- timezone,
1034
+ appVersion: nativeInfo.appVersion,
1035
+ appId: nativeInfo.bundleId,
1036
+ locale: locale,
1037
+ timezone: timezone,
1256
1038
  };
1257
1039
  }
1258
1040
 
1259
- // =============================================================================
1260
- // Anonymous ID Generation
1261
- // =============================================================================
1262
-
1263
- // Storage key for anonymous ID
1264
- const ANONYMOUS_ID_KEY = '@rejourney_anonymous_id';
1265
-
1266
1041
  /**
1267
1042
  * Generate a persistent anonymous ID
1268
1043
  */
@@ -1272,40 +1047,12 @@ function generateAnonymousId(): string {
1272
1047
  return `anon_${timestamp}_${random}`;
1273
1048
  }
1274
1049
 
1275
- /**
1276
- * Initialize anonymous ID - tries to load from storage, generates new if not found
1277
- * This is called internally and runs asynchronously
1278
- */
1279
- async function initAnonymousId(): Promise<void> {
1280
- try {
1281
- // Try to load from AsyncStorage
1282
- const AsyncStorage = require('@react-native-async-storage/async-storage').default;
1283
- const storedId = await AsyncStorage.getItem(ANONYMOUS_ID_KEY);
1284
-
1285
- if (storedId) {
1286
- anonymousId = storedId;
1287
- } else {
1288
- // Generate new ID and persist
1289
- anonymousId = generateAnonymousId();
1290
- await AsyncStorage.setItem(ANONYMOUS_ID_KEY, anonymousId);
1291
- }
1292
- } catch {
1293
- // AsyncStorage not available or error - just generate without persistence
1294
- if (!anonymousId) {
1295
- anonymousId = generateAnonymousId();
1296
- }
1297
- }
1298
- }
1299
-
1300
1050
  /**
1301
1051
  * Get the anonymous ID (synchronous - returns generated ID immediately)
1302
- * For persistent ID, call initAnonymousId() first
1303
1052
  */
1304
1053
  export function getAnonymousId(): string {
1305
1054
  if (!anonymousId) {
1306
1055
  anonymousId = generateAnonymousId();
1307
- // Try to persist asynchronously (fire and forget)
1308
- initAnonymousId().catch(() => { });
1309
1056
  }
1310
1057
  return anonymousId;
1311
1058
  }
@@ -1318,11 +1065,9 @@ export async function ensurePersistentAnonymousId(): Promise<string> {
1318
1065
  if (anonymousId) return anonymousId;
1319
1066
  if (!anonymousIdPromise) {
1320
1067
  anonymousIdPromise = (async () => {
1321
- await initAnonymousId();
1322
- if (!anonymousId) {
1323
- anonymousId = generateAnonymousId();
1324
- }
1325
- return anonymousId;
1068
+ const id = await loadAnonymousId();
1069
+ anonymousId = id;
1070
+ return id;
1326
1071
  })();
1327
1072
  }
1328
1073
  return anonymousIdPromise;
@@ -1333,28 +1078,24 @@ export async function ensurePersistentAnonymousId(): Promise<string> {
1333
1078
  * Call this at app startup for best results
1334
1079
  */
1335
1080
  export async function loadAnonymousId(): Promise<string> {
1336
- await initAnonymousId();
1337
- return anonymousId || generateAnonymousId();
1081
+ const nativeModule = getRejourneyNativeModule();
1082
+ if (nativeModule && nativeModule.getUserIdentity) {
1083
+ try {
1084
+ return await nativeModule.getUserIdentity() || generateAnonymousId();
1085
+ } catch {
1086
+ return generateAnonymousId();
1087
+ }
1088
+ }
1089
+ return generateAnonymousId();
1338
1090
  }
1339
1091
 
1340
1092
  /**
1341
- * Set a custom anonymous ID (e.g., from persistent storage)
1093
+ * Set a custom anonymous ID
1342
1094
  */
1343
1095
  export function setAnonymousId(id: string): void {
1344
1096
  anonymousId = id;
1345
- // Try to persist asynchronously
1346
- try {
1347
- const AsyncStorage = require('@react-native-async-storage/async-storage').default;
1348
- AsyncStorage.setItem(ANONYMOUS_ID_KEY, id).catch(() => { });
1349
- } catch {
1350
- // Ignore if AsyncStorage not available
1351
- }
1352
1097
  }
1353
1098
 
1354
- // =============================================================================
1355
- // Exports
1356
- // =============================================================================
1357
-
1358
1099
  export default {
1359
1100
  init: initAutoTracking,
1360
1101
  cleanup: cleanupAutoTracking,