@rejourneyco/react-native 1.0.1 → 1.0.3

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 (65) hide show
  1. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +72 -391
  2. package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +11 -113
  3. package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +1 -15
  4. package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +1 -61
  5. package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +3 -1
  6. package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +1 -22
  7. package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +3 -26
  8. package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +0 -2
  9. package/android/src/main/java/com/rejourney/network/UploadManager.kt +7 -93
  10. package/android/src/main/java/com/rejourney/network/UploadWorker.kt +5 -41
  11. package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +2 -58
  12. package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +4 -4
  13. package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +36 -7
  14. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +7 -0
  15. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +9 -0
  16. package/ios/Capture/RJCaptureEngine.m +3 -34
  17. package/ios/Capture/RJVideoEncoder.m +0 -26
  18. package/ios/Capture/RJViewHierarchyScanner.m +68 -51
  19. package/ios/Core/RJLifecycleManager.m +0 -14
  20. package/ios/Core/Rejourney.mm +53 -129
  21. package/ios/Network/RJDeviceAuthManager.m +0 -2
  22. package/ios/Network/RJUploadManager.h +8 -0
  23. package/ios/Network/RJUploadManager.m +45 -0
  24. package/ios/Privacy/RJPrivacyMask.m +5 -31
  25. package/ios/Rejourney.h +0 -14
  26. package/ios/Touch/RJTouchInterceptor.m +21 -15
  27. package/ios/Utils/RJEventBuffer.m +57 -69
  28. package/ios/Utils/RJPerfTiming.m +0 -5
  29. package/ios/Utils/RJWindowUtils.m +87 -87
  30. package/lib/commonjs/components/Mask.js +1 -6
  31. package/lib/commonjs/index.js +46 -117
  32. package/lib/commonjs/sdk/autoTracking.js +39 -313
  33. package/lib/commonjs/sdk/constants.js +2 -13
  34. package/lib/commonjs/sdk/errorTracking.js +1 -29
  35. package/lib/commonjs/sdk/metricsTracking.js +3 -24
  36. package/lib/commonjs/sdk/navigation.js +3 -42
  37. package/lib/commonjs/sdk/networkInterceptor.js +7 -60
  38. package/lib/commonjs/sdk/utils.js +73 -19
  39. package/lib/module/components/Mask.js +1 -6
  40. package/lib/module/index.js +45 -121
  41. package/lib/module/sdk/autoTracking.js +39 -314
  42. package/lib/module/sdk/constants.js +2 -13
  43. package/lib/module/sdk/errorTracking.js +1 -29
  44. package/lib/module/sdk/index.js +0 -2
  45. package/lib/module/sdk/metricsTracking.js +3 -24
  46. package/lib/module/sdk/navigation.js +3 -42
  47. package/lib/module/sdk/networkInterceptor.js +7 -60
  48. package/lib/module/sdk/utils.js +73 -19
  49. package/lib/typescript/NativeRejourney.d.ts +1 -0
  50. package/lib/typescript/sdk/autoTracking.d.ts +4 -4
  51. package/lib/typescript/sdk/utils.d.ts +31 -1
  52. package/lib/typescript/types/index.d.ts +0 -1
  53. package/package.json +17 -11
  54. package/src/NativeRejourney.ts +2 -0
  55. package/src/components/Mask.tsx +0 -3
  56. package/src/index.ts +43 -92
  57. package/src/sdk/autoTracking.ts +51 -284
  58. package/src/sdk/constants.ts +13 -13
  59. package/src/sdk/errorTracking.ts +1 -17
  60. package/src/sdk/index.ts +0 -2
  61. package/src/sdk/metricsTracking.ts +5 -33
  62. package/src/sdk/navigation.ts +8 -29
  63. package/src/sdk/networkInterceptor.ts +9 -42
  64. package/src/sdk/utils.ts +76 -19
  65. package/src/types/index.ts +0 -29
@@ -34,9 +34,6 @@ function getPlatform() {
34
34
  function getDimensions() {
35
35
  return getRN()?.Dimensions;
36
36
  }
37
- function getNativeModules() {
38
- return getRN()?.NativeModules;
39
- }
40
37
  function getRejourneyNativeModule() {
41
38
  const RN = getRN();
42
39
  if (!RN) return null;
@@ -49,7 +46,7 @@ function getRejourneyNativeModule() {
49
46
  try {
50
47
  nativeModule = TurboModuleRegistry.get('Rejourney');
51
48
  } catch {
52
- // Ignore and fall back to NativeModules
49
+ // Ignore
53
50
  }
54
51
  }
55
52
  if (!nativeModule && NativeModules) {
@@ -57,57 +54,27 @@ function getRejourneyNativeModule() {
57
54
  }
58
55
  return nativeModule;
59
56
  }
60
-
61
- // Type declarations for browser globals (only used in hybrid apps where DOM is available)
62
- // These don't exist in pure React Native but are needed for error tracking in hybrid scenarios
63
-
64
- // Cast globalThis to work with both RN and hybrid scenarios
65
57
  const _globalThis = globalThis;
66
-
67
- // =============================================================================
68
- // Types
69
- // =============================================================================
70
-
71
- // =============================================================================
72
- // State
73
- // =============================================================================
74
-
75
58
  let isInitialized = false;
76
59
  let config = {};
77
-
78
- // Rage tap tracking
79
60
  const recentTaps = [];
80
- let tapHead = 0; // Circular buffer head pointer
81
- let tapCount = 0; // Actual count of taps in buffer
61
+ let tapHead = 0;
62
+ let tapCount = 0;
82
63
  const MAX_RECENT_TAPS = 10;
83
-
84
- // Session metrics
85
64
  let metrics = createEmptyMetrics();
86
65
  let sessionStartTime = 0;
87
66
  let maxSessionDurationMs = 10 * 60 * 1000;
88
-
89
- // Screen tracking
90
67
  let currentScreen = '';
91
68
  let screensVisited = [];
92
-
93
- // Anonymous ID
94
69
  let anonymousId = null;
95
70
  let anonymousIdPromise = null;
96
-
97
- // Callbacks
98
71
  let onRageTapDetected = null;
99
72
  let onErrorCaptured = null;
100
73
  let onScreenChange = null;
101
-
102
- // Original error handlers (for restoration)
103
74
  let originalErrorHandler;
104
75
  let originalOnError = null;
105
76
  let originalOnUnhandledRejection = null;
106
77
 
107
- // =============================================================================
108
- // Initialization
109
- // =============================================================================
110
-
111
78
  /**
112
79
  * Initialize auto tracking features
113
80
  * Called automatically by Rejourney.init() - no user action needed
@@ -125,23 +92,13 @@ export function initAutoTracking(trackingConfig, callbacks = {}) {
125
92
  maxSessionDurationMs: trackingConfig.maxSessionDurationMs,
126
93
  ...trackingConfig
127
94
  };
128
-
129
- // Session timing
130
95
  sessionStartTime = Date.now();
131
96
  setMaxSessionDurationMinutes(trackingConfig.maxSessionDurationMs ? trackingConfig.maxSessionDurationMs / 60000 : undefined);
132
-
133
- // Set callbacks
134
97
  onRageTapDetected = callbacks.onRageTap || null;
135
98
  onErrorCaptured = callbacks.onError || null;
136
99
  onScreenChange = callbacks.onScreen || null;
137
-
138
- // Setup error tracking
139
100
  setupErrorTracking();
140
-
141
- // Setup React Navigation tracking (if available)
142
101
  setupNavigationTracking();
143
-
144
- // Load anonymous ID from native storage (or generate new one)
145
102
  loadAnonymousId().then(id => {
146
103
  anonymousId = id;
147
104
  });
@@ -153,11 +110,7 @@ export function initAutoTracking(trackingConfig, callbacks = {}) {
153
110
  */
154
111
  export function cleanupAutoTracking() {
155
112
  if (!isInitialized) return;
156
-
157
- // Restore original error handlers
158
113
  restoreErrorHandlers();
159
-
160
- // Cleanup navigation tracking
161
114
  cleanupNavigationTracking();
162
115
 
163
116
  // Reset state
@@ -171,10 +124,6 @@ export function cleanupAutoTracking() {
171
124
  isInitialized = false;
172
125
  }
173
126
 
174
- // =============================================================================
175
- // Rage Tap Detection
176
- // =============================================================================
177
-
178
127
  /**
179
128
  * Track a tap event for rage tap detection
180
129
  * Called automatically from touch interceptor
@@ -182,8 +131,6 @@ export function cleanupAutoTracking() {
182
131
  export function trackTap(tap) {
183
132
  if (!isInitialized) return;
184
133
  const now = Date.now();
185
-
186
- // Add to circular buffer (O(1) instead of shift() which is O(n))
187
134
  const insertIndex = (tapHead + tapCount) % MAX_RECENT_TAPS;
188
135
  if (tapCount < MAX_RECENT_TAPS) {
189
136
  recentTaps[insertIndex] = {
@@ -192,15 +139,12 @@ export function trackTap(tap) {
192
139
  };
193
140
  tapCount++;
194
141
  } else {
195
- // Buffer full, overwrite oldest
196
142
  recentTaps[tapHead] = {
197
143
  ...tap,
198
144
  timestamp: now
199
145
  };
200
146
  tapHead = (tapHead + 1) % MAX_RECENT_TAPS;
201
147
  }
202
-
203
- // Evict old taps outside time window
204
148
  const windowStart = now - (config.rageTapTimeWindow || 500);
205
149
  while (tapCount > 0) {
206
150
  const oldestTap = recentTaps[tapHead];
@@ -211,11 +155,7 @@ export function trackTap(tap) {
211
155
  break;
212
156
  }
213
157
  }
214
-
215
- // Check for rage tap
216
158
  detectRageTap();
217
-
218
- // Update metrics
219
159
  metrics.touchCount++;
220
160
  metrics.totalEvents++;
221
161
  }
@@ -227,14 +167,11 @@ function detectRageTap() {
227
167
  const threshold = config.rageTapThreshold || 3;
228
168
  const radius = config.rageTapRadius || 50;
229
169
  if (tapCount < threshold) return;
230
- // Check last N taps from circular buffer
231
170
  const tapsToCheck = [];
232
171
  for (let i = 0; i < threshold; i++) {
233
172
  const idx = (tapHead + tapCount - threshold + i) % MAX_RECENT_TAPS;
234
173
  tapsToCheck.push(recentTaps[idx]);
235
174
  }
236
-
237
- // Calculate center point
238
175
  let centerX = 0;
239
176
  let centerY = 0;
240
177
  for (const tap of tapsToCheck) {
@@ -243,8 +180,6 @@ function detectRageTap() {
243
180
  }
244
181
  centerX /= tapsToCheck.length;
245
182
  centerY /= tapsToCheck.length;
246
-
247
- // Check if all taps are within radius of center
248
183
  let allWithinRadius = true;
249
184
  for (const tap of tapsToCheck) {
250
185
  const dx = tap.x - centerX;
@@ -256,9 +191,7 @@ function detectRageTap() {
256
191
  }
257
192
  }
258
193
  if (allWithinRadius) {
259
- // Rage tap detected!
260
194
  metrics.rageTapCount++;
261
- // Clear circular buffer to prevent duplicate detection
262
195
  tapHead = 0;
263
196
  tapCount = 0;
264
197
 
@@ -269,10 +202,6 @@ function detectRageTap() {
269
202
  }
270
203
  }
271
204
 
272
- // =============================================================================
273
- // State Change Notification
274
- // =============================================================================
275
-
276
205
  /**
277
206
  * Notify that a state change occurred (navigation, modal, etc.)
278
207
  * Kept for API compatibility
@@ -281,25 +210,16 @@ export function notifyStateChange() {
281
210
  // No-op - kept for backward compatibility
282
211
  }
283
212
 
284
- // =============================================================================
285
- // Error Tracking
286
- // =============================================================================
287
-
288
213
  /**
289
214
  * Setup automatic error tracking
290
215
  */
291
216
  function setupErrorTracking() {
292
- // Track React Native errors
293
217
  if (config.trackReactNativeErrors !== false) {
294
218
  setupReactNativeErrorHandler();
295
219
  }
296
-
297
- // Track JavaScript errors (only works in web/debug)
298
220
  if (config.trackJSErrors !== false && typeof _globalThis !== 'undefined') {
299
221
  setupJSErrorHandler();
300
222
  }
301
-
302
- // Track unhandled promise rejections
303
223
  if (config.trackPromiseRejections !== false && typeof _globalThis !== 'undefined') {
304
224
  setupPromiseRejectionHandler();
305
225
  }
@@ -310,16 +230,10 @@ function setupErrorTracking() {
310
230
  */
311
231
  function setupReactNativeErrorHandler() {
312
232
  try {
313
- // Access ErrorUtils from global scope
314
233
  const ErrorUtils = _globalThis.ErrorUtils;
315
234
  if (!ErrorUtils) return;
316
-
317
- // Store original handler
318
235
  originalErrorHandler = ErrorUtils.getGlobalHandler();
319
-
320
- // Set new handler
321
236
  ErrorUtils.setGlobalHandler((error, isFatal) => {
322
- // Track the error
323
237
  trackError({
324
238
  type: 'error',
325
239
  timestamp: Date.now(),
@@ -327,14 +241,12 @@ function setupReactNativeErrorHandler() {
327
241
  stack: error.stack,
328
242
  name: error.name || 'Error'
329
243
  });
330
-
331
- // Call original handler
332
244
  if (originalErrorHandler) {
333
245
  originalErrorHandler(error, isFatal);
334
246
  }
335
247
  });
336
248
  } catch {
337
- // ErrorUtils not available
249
+ // Ignore
338
250
  }
339
251
  }
340
252
 
@@ -343,8 +255,6 @@ function setupReactNativeErrorHandler() {
343
255
  */
344
256
  function setupJSErrorHandler() {
345
257
  if (typeof _globalThis.onerror !== 'undefined') {
346
- // Note: In React Native, this typically doesn't fire
347
- // But we set it up anyway for hybrid apps
348
258
  originalOnError = _globalThis.onerror;
349
259
  _globalThis.onerror = (message, source, lineno, colno, error) => {
350
260
  trackError({
@@ -354,8 +264,6 @@ function setupJSErrorHandler() {
354
264
  stack: error?.stack || `${source}:${lineno}:${colno}`,
355
265
  name: error?.name || 'Error'
356
266
  });
357
-
358
- // Call original handler
359
267
  if (originalOnError) {
360
268
  return originalOnError(message, source, lineno, colno, error);
361
269
  }
@@ -388,7 +296,6 @@ function setupPromiseRejectionHandler() {
388
296
  * Restore original error handlers
389
297
  */
390
298
  function restoreErrorHandlers() {
391
- // Restore React Native handler
392
299
  if (originalErrorHandler) {
393
300
  try {
394
301
  const ErrorUtils = _globalThis.ErrorUtils;
@@ -400,14 +307,10 @@ function restoreErrorHandlers() {
400
307
  }
401
308
  originalErrorHandler = undefined;
402
309
  }
403
-
404
- // Restore global onerror
405
310
  if (originalOnError !== null) {
406
311
  _globalThis.onerror = originalOnError;
407
312
  originalOnError = null;
408
313
  }
409
-
410
- // Remove promise rejection handler
411
314
  if (originalOnUnhandledRejection && typeof _globalThis.removeEventListener !== 'undefined') {
412
315
  _globalThis.removeEventListener('unhandledrejection', originalOnUnhandledRejection);
413
316
  originalOnUnhandledRejection = null;
@@ -437,16 +340,10 @@ export function captureError(message, stack, name) {
437
340
  name: name || 'Error'
438
341
  });
439
342
  }
440
-
441
- // =============================================================================
442
- // Screen/Funnel Tracking - Automatic Navigation Detection
443
- // =============================================================================
444
-
445
- // Navigation detection state
446
343
  let navigationPollingInterval = null;
447
344
  let lastDetectedScreen = '';
448
345
  let navigationSetupDone = false;
449
- let navigationPollingErrors = 0; // Track consecutive errors
346
+ let navigationPollingErrors = 0;
450
347
  const MAX_POLLING_ERRORS = 10; // Stop polling after 10 consecutive errors
451
348
 
452
349
  /**
@@ -470,8 +367,6 @@ export function trackNavigationState(state) {
470
367
  const {
471
368
  normalizeScreenName
472
369
  } = require('./navigation');
473
-
474
- // Find the active screen recursively
475
370
  const findActiveScreen = navState => {
476
371
  if (!navState?.routes) return null;
477
372
  const index = navState.index ?? navState.routes.length - 1;
@@ -486,7 +381,7 @@ export function trackNavigationState(state) {
486
381
  trackScreen(screenName);
487
382
  }
488
383
  } catch {
489
- // Navigation tracking error
384
+ // Ignore
490
385
  }
491
386
  }
492
387
 
@@ -516,16 +411,11 @@ export function trackNavigationState(state) {
516
411
  * ```
517
412
  */
518
413
  export function useNavigationTracking() {
519
- // Use React's useRef and useCallback to create stable references
520
414
  const React = require('react');
521
415
  const {
522
416
  createNavigationContainerRef
523
417
  } = require('@react-navigation/native');
524
-
525
- // Create a stable navigation ref
526
418
  const navigationRef = React.useRef(createNavigationContainerRef());
527
-
528
- // Track initial screen when navigation is ready
529
419
  const onReady = React.useCallback(() => {
530
420
  try {
531
421
  const currentRoute = navigationRef.current?.getCurrentRoute?.();
@@ -540,11 +430,9 @@ export function useNavigationTracking() {
540
430
  }
541
431
  }
542
432
  } catch {
543
- // Navigation not ready yet
433
+ // Ignore
544
434
  }
545
435
  }, []);
546
-
547
- // Return props to spread on NavigationContainer
548
436
  return {
549
437
  ref: navigationRef.current,
550
438
  onReady,
@@ -564,9 +452,6 @@ function setupNavigationTracking() {
564
452
  if (__DEV__) {
565
453
  logger.debug('Setting up navigation tracking...');
566
454
  }
567
-
568
- // Delay to ensure navigation is initialized - Expo Router needs more time
569
- // We retry a few times with increasing delays
570
455
  let attempts = 0;
571
456
  const maxAttempts = 5;
572
457
  const trySetup = () => {
@@ -580,8 +465,7 @@ function setupNavigationTracking() {
580
465
  logger.debug('Expo Router setup: SUCCESS on attempt', attempts);
581
466
  }
582
467
  } else if (attempts < maxAttempts) {
583
- // Retry with exponential backoff
584
- const delay = 200 * attempts; // 200, 400, 600, 800ms
468
+ const delay = 200 * attempts;
585
469
  if (__DEV__) {
586
470
  logger.debug('Expo Router not ready, retrying in', delay, 'ms');
587
471
  }
@@ -593,8 +477,6 @@ function setupNavigationTracking() {
593
477
  }
594
478
  }
595
479
  };
596
-
597
- // Start first attempt after 200ms
598
480
  setTimeout(trySetup, 200);
599
481
  }
600
482
 
@@ -620,14 +502,10 @@ function trySetupExpoRouter() {
620
502
  normalizeScreenName,
621
503
  getScreenNameFromPath
622
504
  } = require('./navigation');
623
-
624
- // Poll for route changes (expo-router doesn't expose a listener API outside hooks)
625
505
  navigationPollingInterval = setInterval(() => {
626
506
  try {
627
507
  let state = null;
628
508
  let stateSource = '';
629
-
630
- // Method 1: Public accessors on router object
631
509
  if (typeof router.getState === 'function') {
632
510
  state = router.getState();
633
511
  stateSource = 'router.getState()';
@@ -635,34 +513,25 @@ function trySetupExpoRouter() {
635
513
  state = router.rootState;
636
514
  stateSource = 'router.rootState';
637
515
  }
638
-
639
- // Method 2: Internal store access (works for both v3 and v6+)
640
516
  if (!state) {
641
517
  try {
642
518
  const storeModule = require('expo-router/build/global-state/router-store');
643
519
  if (storeModule?.store) {
644
- // v6+: store.state or store.navigationRef
645
520
  state = storeModule.store.state;
646
521
  if (state) stateSource = 'store.state';
647
-
648
- // v6+: Try navigationRef if state is undefined
649
522
  if (!state && storeModule.store.navigationRef?.current) {
650
523
  state = storeModule.store.navigationRef.current.getRootState?.();
651
524
  if (state) stateSource = 'navigationRef.getRootState()';
652
525
  }
653
-
654
- // v3: store.rootState or store.initialState
655
526
  if (!state) {
656
527
  state = storeModule.store.rootState || storeModule.store.initialState;
657
528
  if (state) stateSource = 'store.rootState/initialState';
658
529
  }
659
530
  }
660
531
  } catch {
661
- // Store not available
532
+ // Ignore
662
533
  }
663
534
  }
664
-
665
- // Method 3: Try accessing via a different export path for v6
666
535
  if (!state) {
667
536
  try {
668
537
  const imperative = require('expo-router/build/imperative-api');
@@ -671,11 +540,11 @@ function trySetupExpoRouter() {
671
540
  if (state) stateSource = 'imperative-api';
672
541
  }
673
542
  } catch {
674
- // Imperative API not available
543
+ // Ignore
675
544
  }
676
545
  }
677
546
  if (state) {
678
- // Reset error count on success
547
+ navigationPollingErrors = 0;
679
548
  navigationPollingErrors = 0;
680
549
  const screenName = extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName);
681
550
  if (screenName && screenName !== lastDetectedScreen) {
@@ -686,21 +555,15 @@ function trySetupExpoRouter() {
686
555
  trackScreen(screenName);
687
556
  }
688
557
  } else {
689
- // Track consecutive failures to get state
690
558
  navigationPollingErrors++;
691
559
  if (__DEV__ && navigationPollingErrors === 1) {
692
560
  logger.debug('Expo Router: Could not get navigation state');
693
561
  }
694
562
  if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
695
- // Stop polling after too many errors to save CPU
696
- if (__DEV__) {
697
- logger.debug('Expo Router: Stopped polling after', MAX_POLLING_ERRORS, 'errors');
698
- }
699
563
  cleanupNavigationTracking();
700
564
  }
701
565
  }
702
566
  } catch (e) {
703
- // Error - track and potentially stop
704
567
  navigationPollingErrors++;
705
568
  if (__DEV__ && navigationPollingErrors === 1) {
706
569
  logger.debug('Expo Router polling error:', e);
@@ -709,14 +572,12 @@ function trySetupExpoRouter() {
709
572
  cleanupNavigationTracking();
710
573
  }
711
574
  }
712
- }, 500); // 500ms polling (reduced from 300ms for lower CPU usage)
713
-
575
+ }, 500);
714
576
  return true;
715
577
  } catch (e) {
716
578
  if (__DEV__) {
717
579
  logger.debug('Expo Router not available:', e);
718
580
  }
719
- // expo-router not installed
720
581
  return false;
721
582
  }
722
583
  }
@@ -731,22 +592,12 @@ function extractScreenNameFromRouterState(state, getScreenNameFromPath, normaliz
731
592
  if (!state?.routes) return null;
732
593
  const route = state.routes[state.index ?? state.routes.length - 1];
733
594
  if (!route) return null;
734
-
735
- // Add current route name to accumulated segments
736
595
  const newSegments = [...accumulatedSegments, route.name];
737
-
738
- // If this route has nested state, recurse deeper
739
596
  if (route.state) {
740
597
  return extractScreenNameFromRouterState(route.state, getScreenNameFromPath, normalizeScreenName, newSegments);
741
598
  }
742
-
743
- // We've reached the deepest level - build the screen name
744
- // Filter out group markers like (tabs), (main), (auth)
745
599
  const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
746
-
747
- // If after filtering we have no segments, use the last meaningful name
748
600
  if (cleanSegments.length === 0) {
749
- // Find the last non-group segment
750
601
  for (let i = newSegments.length - 1; i >= 0; i--) {
751
602
  const seg = newSegments[i];
752
603
  if (seg && !seg.startsWith('(') && !seg.endsWith(')')) {
@@ -785,27 +636,17 @@ export function trackScreen(screenName) {
785
636
  }
786
637
  const previousScreen = currentScreen;
787
638
  currentScreen = screenName;
788
- // Add to screens visited (only track for unique set, avoid large array copies)
789
639
  screensVisited.push(screenName);
790
-
791
- // Update unique screens count
792
640
  const uniqueScreens = new Set(screensVisited);
793
641
  metrics.uniqueScreensCount = uniqueScreens.size;
794
-
795
- // Update navigation count
796
642
  metrics.navigationCount++;
797
643
  metrics.totalEvents++;
798
644
  if (__DEV__) {
799
645
  logger.debug('trackScreen:', screenName, '(total screens:', metrics.uniqueScreensCount, ')');
800
646
  }
801
-
802
- // Notify callback
803
647
  if (onScreenChange) {
804
648
  onScreenChange(screenName, previousScreen);
805
649
  }
806
-
807
- // IMPORTANT: Also notify native module to send to backend
808
- // This is the key fix - without this, screens don't get recorded!
809
650
  try {
810
651
  const RejourneyNative = getRejourneyNativeModule();
811
652
  if (RejourneyNative?.screenChanged) {
@@ -827,18 +668,12 @@ export function trackScreen(screenName) {
827
668
  }
828
669
  }
829
670
 
830
- // =============================================================================
831
- // API Metrics Tracking
832
- // =============================================================================
833
-
834
671
  /**
835
672
  * Track an API request with timing data
836
673
  */
837
674
  export function trackAPIRequest(success, _statusCode, durationMs = 0, responseBytes = 0) {
838
675
  if (!isInitialized) return;
839
676
  metrics.apiTotalCount++;
840
-
841
- // Accumulate timing and size for avg calculation
842
677
  if (durationMs > 0) {
843
678
  metrics.netTotalDurationMs += durationMs;
844
679
  }
@@ -849,16 +684,10 @@ export function trackAPIRequest(success, _statusCode, durationMs = 0, responseBy
849
684
  metrics.apiSuccessCount++;
850
685
  } else {
851
686
  metrics.apiErrorCount++;
852
-
853
- // API errors also count toward error count for UX score
854
687
  metrics.errorCount++;
855
688
  }
856
689
  }
857
690
 
858
- // =============================================================================
859
- // Session Metrics
860
- // =============================================================================
861
-
862
691
  /**
863
692
  * Create empty metrics object
864
693
  */
@@ -916,18 +745,11 @@ export function trackInput() {
916
745
  * Get current session metrics
917
746
  */
918
747
  export function getSessionMetrics() {
919
- // Calculate scores before returning
920
748
  calculateScores();
921
-
922
- // Compute average API response time
923
749
  const netAvgDurationMs = metrics.apiTotalCount > 0 ? Math.round(metrics.netTotalDurationMs / metrics.apiTotalCount) : 0;
924
-
925
- // Lazily populate screensVisited only when metrics are retrieved
926
- // This avoids expensive array copies on every screen change
927
750
  return {
928
751
  ...metrics,
929
752
  screensVisited: [...screensVisited],
930
- // Only copy here when needed
931
753
  netAvgDurationMs
932
754
  };
933
755
  }
@@ -936,34 +758,15 @@ export function getSessionMetrics() {
936
758
  * Calculate session scores
937
759
  */
938
760
  function calculateScores() {
939
- // Interaction Score (0-100)
940
- // Based on total interactions normalized to a baseline
941
761
  const totalInteractions = metrics.touchCount + metrics.scrollCount + metrics.gestureCount + metrics.inputCount;
942
-
943
- // Assume 50 interactions is "average" for a session
944
762
  const avgInteractions = 50;
945
763
  metrics.interactionScore = Math.min(100, Math.round(totalInteractions / avgInteractions * 100));
946
-
947
- // Exploration Score (0-100)
948
- // Based on unique screens visited
949
- // Assume 5 screens is "average" exploration
950
764
  const avgScreens = 5;
951
765
  metrics.explorationScore = Math.min(100, Math.round(metrics.uniqueScreensCount / avgScreens * 100));
952
-
953
- // UX Score (0-100)
954
- // Starts at 100, deducts for issues
955
766
  let uxScore = 100;
956
-
957
- // Deduct for errors
958
- uxScore -= Math.min(30, metrics.errorCount * 15); // Max 30 point deduction
959
-
960
- // Deduct for rage taps
961
- uxScore -= Math.min(24, metrics.rageTapCount * 8); // Max 24 point deduction
962
-
963
- // Deduct for API errors
964
- uxScore -= Math.min(20, metrics.apiErrorCount * 10); // Max 20 point deduction
965
-
966
- // Bonus for completing funnel (if screens > 3)
767
+ uxScore -= Math.min(30, metrics.errorCount * 15);
768
+ uxScore -= Math.min(24, metrics.rageTapCount * 8);
769
+ uxScore -= Math.min(20, metrics.apiErrorCount * 10);
967
770
  if (metrics.uniqueScreensCount >= 3) {
968
771
  uxScore += 5;
969
772
  }
@@ -981,43 +784,29 @@ export function resetMetrics() {
981
784
  tapCount = 0;
982
785
  sessionStartTime = Date.now();
983
786
  }
984
-
985
- // =============================================================================
986
- // Session duration helpers
987
- // =============================================================================
988
-
989
- /** Clamp and set max session duration in minutes (1–10). Defaults to 10. */
990
787
  export function setMaxSessionDurationMinutes(minutes) {
991
788
  const clampedMinutes = Math.min(10, Math.max(1, minutes ?? 10));
992
789
  maxSessionDurationMs = clampedMinutes * 60 * 1000;
993
790
  }
994
-
995
- /** Returns true if the current session exceeded the configured max duration. */
996
791
  export function hasExceededMaxSessionDuration() {
997
792
  if (!sessionStartTime) return false;
998
793
  return Date.now() - sessionStartTime >= maxSessionDurationMs;
999
794
  }
1000
-
1001
- /** Returns remaining milliseconds until the session should stop. */
1002
795
  export function getRemainingSessionDurationMs() {
1003
796
  if (!sessionStartTime) return maxSessionDurationMs;
1004
797
  const remaining = maxSessionDurationMs - (Date.now() - sessionStartTime);
1005
798
  return Math.max(0, remaining);
1006
799
  }
1007
800
 
1008
- // =============================================================================
1009
- // Device Info Collection
1010
- // =============================================================================
1011
-
1012
801
  /**
1013
802
  * Collect device information
1014
803
  */
1015
- export function collectDeviceInfo() {
804
+ /**
805
+ * Collect device information
806
+ */
807
+ export async function collectDeviceInfo() {
1016
808
  const Dimensions = getDimensions();
1017
809
  const Platform = getPlatform();
1018
- const NativeModules = getNativeModules();
1019
-
1020
- // Default values if react-native isn't available
1021
810
  let width = 0,
1022
811
  height = 0,
1023
812
  scale = 1;
@@ -1029,100 +818,43 @@ export function collectDeviceInfo() {
1029
818
  scale = screenDims?.scale || 1;
1030
819
  }
1031
820
 
1032
- // Get device model - this varies by platform
1033
- let model = 'Unknown';
1034
- let manufacturer;
1035
- let osVersion = 'Unknown';
1036
- let appVersion;
1037
- let appId;
821
+ // Basic JS-side info
1038
822
  let locale;
1039
823
  let timezone;
1040
824
  try {
1041
- // Try to get from react-native-device-info if available
1042
- // This is optional - falls back to basic info if not installed
1043
- const DeviceInfo = require('react-native-device-info');
1044
- model = DeviceInfo.getModel?.() || model;
1045
- manufacturer = DeviceInfo.getBrand?.() || undefined;
1046
- osVersion = DeviceInfo.getSystemVersion?.() || osVersion;
1047
- appVersion = DeviceInfo.getVersion?.() || undefined;
1048
- appId = DeviceInfo.getBundleId?.() || undefined;
1049
- locale = DeviceInfo.getDeviceLocale?.() || undefined;
1050
- timezone = DeviceInfo.getTimezone?.() || undefined;
825
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
826
+ locale = Intl.DateTimeFormat().resolvedOptions().locale;
1051
827
  } catch {
1052
- // react-native-device-info not installed, try Expo packages
1053
- try {
1054
- // Try expo-application for app version/id
1055
- const Application = require('expo-application');
1056
- appVersion = Application.nativeApplicationVersion || Application.applicationVersion || undefined;
1057
- appId = Application.applicationId || undefined;
1058
- } catch {
1059
- // expo-application not available
1060
- }
1061
- try {
1062
- // Try expo-constants for additional info
1063
- const Constants = require('expo-constants');
1064
- const expoConfig = Constants.expoConfig || Constants.manifest2?.extra?.expoClient || Constants.manifest;
1065
- if (!appVersion && expoConfig?.version) {
1066
- appVersion = expoConfig.version;
1067
- }
1068
- if (!appId && (expoConfig?.ios?.bundleIdentifier || expoConfig?.android?.package)) {
1069
- appId = Platform?.OS === 'ios' ? expoConfig?.ios?.bundleIdentifier : expoConfig?.android?.package;
1070
- }
1071
- } catch {
1072
- // expo-constants not available
1073
- }
1074
-
1075
- // Fall back to basic platform info
1076
- if (Platform?.OS === 'ios') {
1077
- // Get basic info from constants
1078
- const PlatformConstants = NativeModules?.PlatformConstants;
1079
- osVersion = Platform.Version?.toString() || osVersion;
1080
- model = PlatformConstants?.interfaceIdiom === 'pad' ? 'iPad' : 'iPhone';
1081
- } else if (Platform?.OS === 'android') {
1082
- osVersion = Platform.Version?.toString() || osVersion;
1083
- model = 'Android Device';
1084
- }
828
+ // Ignore
1085
829
  }
1086
830
 
1087
- // Get timezone
1088
- if (!timezone) {
1089
- try {
1090
- timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
1091
- } catch {
1092
- timezone = undefined;
1093
- }
1094
- }
1095
-
1096
- // Get locale
1097
- if (!locale) {
831
+ // Get native info
832
+ const nativeModule = getRejourneyNativeModule();
833
+ let nativeInfo = {};
834
+ if (nativeModule && nativeModule.getDeviceInfo) {
1098
835
  try {
1099
- locale = Intl.DateTimeFormat().resolvedOptions().locale;
1100
- } catch {
1101
- locale = undefined;
836
+ nativeInfo = await nativeModule.getDeviceInfo();
837
+ } catch (e) {
838
+ if (__DEV__) {
839
+ console.warn('[Rejourney] Failed to get native device info:', e);
840
+ }
1102
841
  }
1103
842
  }
1104
843
  return {
1105
- model,
1106
- manufacturer,
844
+ model: nativeInfo.model || 'Unknown',
845
+ manufacturer: nativeInfo.brand,
1107
846
  os: Platform?.OS || 'ios',
1108
- osVersion,
847
+ osVersion: nativeInfo.systemVersion || Platform?.Version?.toString() || 'Unknown',
1109
848
  screenWidth: Math.round(width),
1110
849
  screenHeight: Math.round(height),
1111
850
  pixelRatio: scale,
1112
- appVersion,
1113
- appId,
1114
- locale,
1115
- timezone
851
+ appVersion: nativeInfo.appVersion,
852
+ appId: nativeInfo.bundleId,
853
+ locale: locale,
854
+ timezone: timezone
1116
855
  };
1117
856
  }
1118
857
 
1119
- // =============================================================================
1120
- // Anonymous ID Generation
1121
- // =============================================================================
1122
-
1123
- // Storage key for anonymous ID
1124
- // const ANONYMOUS_ID_KEY = '@rejourney_anonymous_id';
1125
-
1126
858
  /**
1127
859
  * Generate a persistent anonymous ID
1128
860
  */
@@ -1150,7 +882,6 @@ export async function ensurePersistentAnonymousId() {
1150
882
  if (anonymousId) return anonymousId;
1151
883
  if (!anonymousIdPromise) {
1152
884
  anonymousIdPromise = (async () => {
1153
- // Just use the load logic which now delegates to native or memory
1154
885
  const id = await loadAnonymousId();
1155
886
  anonymousId = id;
1156
887
  return id;
@@ -1180,13 +911,7 @@ export async function loadAnonymousId() {
1180
911
  */
1181
912
  export function setAnonymousId(id) {
1182
913
  anonymousId = id;
1183
- // No-op for async persistence as we moved to native-only or memory-only
1184
914
  }
1185
-
1186
- // =============================================================================
1187
- // Exports
1188
- // =============================================================================
1189
-
1190
915
  export default {
1191
916
  init: initAutoTracking,
1192
917
  cleanup: cleanupAutoTracking,