@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
@@ -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,26 +92,16 @@ 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
- // Initialize metrics
139
- metrics = createEmptyMetrics();
140
- sessionStartTime = Date.now();
141
- anonymousId = generateAnonymousId();
142
-
143
- // Setup error tracking
144
100
  setupErrorTracking();
145
-
146
- // Setup React Navigation tracking (if available)
147
101
  setupNavigationTracking();
102
+ loadAnonymousId().then(id => {
103
+ anonymousId = id;
104
+ });
148
105
  isInitialized = true;
149
106
  }
150
107
 
@@ -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,
@@ -580,8 +468,7 @@ function setupNavigationTracking() {
580
468
  logger.debug('Expo Router setup: SUCCESS on attempt', attempts);
581
469
  }
582
470
  } else if (attempts < maxAttempts) {
583
- // Retry with exponential backoff
584
- const delay = 200 * attempts; // 200, 400, 600, 800ms
471
+ const delay = 200 * attempts;
585
472
  if (__DEV__) {
586
473
  logger.debug('Expo Router not ready, retrying in', delay, 'ms');
587
474
  }
@@ -593,8 +480,6 @@ function setupNavigationTracking() {
593
480
  }
594
481
  }
595
482
  };
596
-
597
- // Start first attempt after 200ms
598
483
  setTimeout(trySetup, 200);
599
484
  }
600
485
 
@@ -620,14 +505,10 @@ function trySetupExpoRouter() {
620
505
  normalizeScreenName,
621
506
  getScreenNameFromPath
622
507
  } = require('./navigation');
623
-
624
- // Poll for route changes (expo-router doesn't expose a listener API outside hooks)
625
508
  navigationPollingInterval = setInterval(() => {
626
509
  try {
627
510
  let state = null;
628
511
  let stateSource = '';
629
-
630
- // Method 1: Public accessors on router object
631
512
  if (typeof router.getState === 'function') {
632
513
  state = router.getState();
633
514
  stateSource = 'router.getState()';
@@ -635,34 +516,25 @@ function trySetupExpoRouter() {
635
516
  state = router.rootState;
636
517
  stateSource = 'router.rootState';
637
518
  }
638
-
639
- // Method 2: Internal store access (works for both v3 and v6+)
640
519
  if (!state) {
641
520
  try {
642
521
  const storeModule = require('expo-router/build/global-state/router-store');
643
522
  if (storeModule?.store) {
644
- // v6+: store.state or store.navigationRef
645
523
  state = storeModule.store.state;
646
524
  if (state) stateSource = 'store.state';
647
-
648
- // v6+: Try navigationRef if state is undefined
649
525
  if (!state && storeModule.store.navigationRef?.current) {
650
526
  state = storeModule.store.navigationRef.current.getRootState?.();
651
527
  if (state) stateSource = 'navigationRef.getRootState()';
652
528
  }
653
-
654
- // v3: store.rootState or store.initialState
655
529
  if (!state) {
656
530
  state = storeModule.store.rootState || storeModule.store.initialState;
657
531
  if (state) stateSource = 'store.rootState/initialState';
658
532
  }
659
533
  }
660
534
  } catch {
661
- // Store not available
535
+ // Ignore
662
536
  }
663
537
  }
664
-
665
- // Method 3: Try accessing via a different export path for v6
666
538
  if (!state) {
667
539
  try {
668
540
  const imperative = require('expo-router/build/imperative-api');
@@ -671,11 +543,11 @@ function trySetupExpoRouter() {
671
543
  if (state) stateSource = 'imperative-api';
672
544
  }
673
545
  } catch {
674
- // Imperative API not available
546
+ // Ignore
675
547
  }
676
548
  }
677
549
  if (state) {
678
- // Reset error count on success
550
+ navigationPollingErrors = 0;
679
551
  navigationPollingErrors = 0;
680
552
  const screenName = extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName);
681
553
  if (screenName && screenName !== lastDetectedScreen) {
@@ -686,21 +558,15 @@ function trySetupExpoRouter() {
686
558
  trackScreen(screenName);
687
559
  }
688
560
  } else {
689
- // Track consecutive failures to get state
690
561
  navigationPollingErrors++;
691
562
  if (__DEV__ && navigationPollingErrors === 1) {
692
563
  logger.debug('Expo Router: Could not get navigation state');
693
564
  }
694
565
  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
566
  cleanupNavigationTracking();
700
567
  }
701
568
  }
702
569
  } catch (e) {
703
- // Error - track and potentially stop
704
570
  navigationPollingErrors++;
705
571
  if (__DEV__ && navigationPollingErrors === 1) {
706
572
  logger.debug('Expo Router polling error:', e);
@@ -709,14 +575,12 @@ function trySetupExpoRouter() {
709
575
  cleanupNavigationTracking();
710
576
  }
711
577
  }
712
- }, 500); // 500ms polling (reduced from 300ms for lower CPU usage)
713
-
578
+ }, 500);
714
579
  return true;
715
580
  } catch (e) {
716
581
  if (__DEV__) {
717
582
  logger.debug('Expo Router not available:', e);
718
583
  }
719
- // expo-router not installed
720
584
  return false;
721
585
  }
722
586
  }
@@ -731,22 +595,12 @@ function extractScreenNameFromRouterState(state, getScreenNameFromPath, normaliz
731
595
  if (!state?.routes) return null;
732
596
  const route = state.routes[state.index ?? state.routes.length - 1];
733
597
  if (!route) return null;
734
-
735
- // Add current route name to accumulated segments
736
598
  const newSegments = [...accumulatedSegments, route.name];
737
-
738
- // If this route has nested state, recurse deeper
739
599
  if (route.state) {
740
600
  return extractScreenNameFromRouterState(route.state, getScreenNameFromPath, normalizeScreenName, newSegments);
741
601
  }
742
-
743
- // We've reached the deepest level - build the screen name
744
- // Filter out group markers like (tabs), (main), (auth)
745
602
  const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
746
-
747
- // If after filtering we have no segments, use the last meaningful name
748
603
  if (cleanSegments.length === 0) {
749
- // Find the last non-group segment
750
604
  for (let i = newSegments.length - 1; i >= 0; i--) {
751
605
  const seg = newSegments[i];
752
606
  if (seg && !seg.startsWith('(') && !seg.endsWith(')')) {
@@ -785,27 +639,17 @@ export function trackScreen(screenName) {
785
639
  }
786
640
  const previousScreen = currentScreen;
787
641
  currentScreen = screenName;
788
- // Add to screens visited (only track for unique set, avoid large array copies)
789
642
  screensVisited.push(screenName);
790
-
791
- // Update unique screens count
792
643
  const uniqueScreens = new Set(screensVisited);
793
644
  metrics.uniqueScreensCount = uniqueScreens.size;
794
-
795
- // Update navigation count
796
645
  metrics.navigationCount++;
797
646
  metrics.totalEvents++;
798
647
  if (__DEV__) {
799
648
  logger.debug('trackScreen:', screenName, '(total screens:', metrics.uniqueScreensCount, ')');
800
649
  }
801
-
802
- // Notify callback
803
650
  if (onScreenChange) {
804
651
  onScreenChange(screenName, previousScreen);
805
652
  }
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
653
  try {
810
654
  const RejourneyNative = getRejourneyNativeModule();
811
655
  if (RejourneyNative?.screenChanged) {
@@ -827,18 +671,12 @@ export function trackScreen(screenName) {
827
671
  }
828
672
  }
829
673
 
830
- // =============================================================================
831
- // API Metrics Tracking
832
- // =============================================================================
833
-
834
674
  /**
835
675
  * Track an API request with timing data
836
676
  */
837
677
  export function trackAPIRequest(success, _statusCode, durationMs = 0, responseBytes = 0) {
838
678
  if (!isInitialized) return;
839
679
  metrics.apiTotalCount++;
840
-
841
- // Accumulate timing and size for avg calculation
842
680
  if (durationMs > 0) {
843
681
  metrics.netTotalDurationMs += durationMs;
844
682
  }
@@ -849,16 +687,10 @@ export function trackAPIRequest(success, _statusCode, durationMs = 0, responseBy
849
687
  metrics.apiSuccessCount++;
850
688
  } else {
851
689
  metrics.apiErrorCount++;
852
-
853
- // API errors also count toward error count for UX score
854
690
  metrics.errorCount++;
855
691
  }
856
692
  }
857
693
 
858
- // =============================================================================
859
- // Session Metrics
860
- // =============================================================================
861
-
862
694
  /**
863
695
  * Create empty metrics object
864
696
  */
@@ -916,18 +748,11 @@ export function trackInput() {
916
748
  * Get current session metrics
917
749
  */
918
750
  export function getSessionMetrics() {
919
- // Calculate scores before returning
920
751
  calculateScores();
921
-
922
- // Compute average API response time
923
752
  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
753
  return {
928
754
  ...metrics,
929
755
  screensVisited: [...screensVisited],
930
- // Only copy here when needed
931
756
  netAvgDurationMs
932
757
  };
933
758
  }
@@ -936,34 +761,15 @@ export function getSessionMetrics() {
936
761
  * Calculate session scores
937
762
  */
938
763
  function calculateScores() {
939
- // Interaction Score (0-100)
940
- // Based on total interactions normalized to a baseline
941
764
  const totalInteractions = metrics.touchCount + metrics.scrollCount + metrics.gestureCount + metrics.inputCount;
942
-
943
- // Assume 50 interactions is "average" for a session
944
765
  const avgInteractions = 50;
945
766
  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
767
  const avgScreens = 5;
951
768
  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
769
  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)
770
+ uxScore -= Math.min(30, metrics.errorCount * 15);
771
+ uxScore -= Math.min(24, metrics.rageTapCount * 8);
772
+ uxScore -= Math.min(20, metrics.apiErrorCount * 10);
967
773
  if (metrics.uniqueScreensCount >= 3) {
968
774
  uxScore += 5;
969
775
  }
@@ -981,43 +787,29 @@ export function resetMetrics() {
981
787
  tapCount = 0;
982
788
  sessionStartTime = Date.now();
983
789
  }
984
-
985
- // =============================================================================
986
- // Session duration helpers
987
- // =============================================================================
988
-
989
- /** Clamp and set max session duration in minutes (1–10). Defaults to 10. */
990
790
  export function setMaxSessionDurationMinutes(minutes) {
991
791
  const clampedMinutes = Math.min(10, Math.max(1, minutes ?? 10));
992
792
  maxSessionDurationMs = clampedMinutes * 60 * 1000;
993
793
  }
994
-
995
- /** Returns true if the current session exceeded the configured max duration. */
996
794
  export function hasExceededMaxSessionDuration() {
997
795
  if (!sessionStartTime) return false;
998
796
  return Date.now() - sessionStartTime >= maxSessionDurationMs;
999
797
  }
1000
-
1001
- /** Returns remaining milliseconds until the session should stop. */
1002
798
  export function getRemainingSessionDurationMs() {
1003
799
  if (!sessionStartTime) return maxSessionDurationMs;
1004
800
  const remaining = maxSessionDurationMs - (Date.now() - sessionStartTime);
1005
801
  return Math.max(0, remaining);
1006
802
  }
1007
803
 
1008
- // =============================================================================
1009
- // Device Info Collection
1010
- // =============================================================================
1011
-
1012
804
  /**
1013
805
  * Collect device information
1014
806
  */
1015
- export function collectDeviceInfo() {
807
+ /**
808
+ * Collect device information
809
+ */
810
+ export async function collectDeviceInfo() {
1016
811
  const Dimensions = getDimensions();
1017
812
  const Platform = getPlatform();
1018
- const NativeModules = getNativeModules();
1019
-
1020
- // Default values if react-native isn't available
1021
813
  let width = 0,
1022
814
  height = 0,
1023
815
  scale = 1;
@@ -1029,100 +821,43 @@ export function collectDeviceInfo() {
1029
821
  scale = screenDims?.scale || 1;
1030
822
  }
1031
823
 
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;
824
+ // Basic JS-side info
1038
825
  let locale;
1039
826
  let timezone;
1040
827
  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;
828
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
829
+ locale = Intl.DateTimeFormat().resolvedOptions().locale;
1051
830
  } catch {
1052
- // react-native-device-info not installed, try Expo packages
1053
- try {
1054
- // Try expo-application for app version/id
1055
- const Application = require('expo-application');
1056
- appVersion = Application.nativeApplicationVersion || Application.applicationVersion || undefined;
1057
- appId = Application.applicationId || undefined;
1058
- } catch {
1059
- // expo-application not available
1060
- }
1061
- try {
1062
- // Try expo-constants for additional info
1063
- const Constants = require('expo-constants');
1064
- const expoConfig = Constants.expoConfig || Constants.manifest2?.extra?.expoClient || Constants.manifest;
1065
- if (!appVersion && expoConfig?.version) {
1066
- appVersion = expoConfig.version;
1067
- }
1068
- if (!appId && (expoConfig?.ios?.bundleIdentifier || expoConfig?.android?.package)) {
1069
- appId = Platform?.OS === 'ios' ? expoConfig?.ios?.bundleIdentifier : expoConfig?.android?.package;
1070
- }
1071
- } catch {
1072
- // expo-constants not available
1073
- }
1074
-
1075
- // Fall back to basic platform info
1076
- if (Platform?.OS === 'ios') {
1077
- // Get basic info from constants
1078
- const PlatformConstants = NativeModules?.PlatformConstants;
1079
- osVersion = Platform.Version?.toString() || osVersion;
1080
- model = PlatformConstants?.interfaceIdiom === 'pad' ? 'iPad' : 'iPhone';
1081
- } else if (Platform?.OS === 'android') {
1082
- osVersion = Platform.Version?.toString() || osVersion;
1083
- model = 'Android Device';
1084
- }
1085
- }
1086
-
1087
- // Get timezone
1088
- if (!timezone) {
1089
- try {
1090
- timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
1091
- } catch {
1092
- timezone = undefined;
1093
- }
831
+ // Ignore
1094
832
  }
1095
833
 
1096
- // Get locale
1097
- if (!locale) {
834
+ // Get native info
835
+ const nativeModule = getRejourneyNativeModule();
836
+ let nativeInfo = {};
837
+ if (nativeModule && nativeModule.getDeviceInfo) {
1098
838
  try {
1099
- locale = Intl.DateTimeFormat().resolvedOptions().locale;
1100
- } catch {
1101
- locale = undefined;
839
+ nativeInfo = await nativeModule.getDeviceInfo();
840
+ } catch (e) {
841
+ if (__DEV__) {
842
+ console.warn('[Rejourney] Failed to get native device info:', e);
843
+ }
1102
844
  }
1103
845
  }
1104
846
  return {
1105
- model,
1106
- manufacturer,
847
+ model: nativeInfo.model || 'Unknown',
848
+ manufacturer: nativeInfo.brand,
1107
849
  os: Platform?.OS || 'ios',
1108
- osVersion,
850
+ osVersion: nativeInfo.systemVersion || Platform?.Version?.toString() || 'Unknown',
1109
851
  screenWidth: Math.round(width),
1110
852
  screenHeight: Math.round(height),
1111
853
  pixelRatio: scale,
1112
- appVersion,
1113
- appId,
1114
- locale,
1115
- timezone
854
+ appVersion: nativeInfo.appVersion,
855
+ appId: nativeInfo.bundleId,
856
+ locale: locale,
857
+ timezone: timezone
1116
858
  };
1117
859
  }
1118
860
 
1119
- // =============================================================================
1120
- // Anonymous ID Generation
1121
- // =============================================================================
1122
-
1123
- // Storage key for anonymous ID
1124
- const ANONYMOUS_ID_KEY = '@rejourney_anonymous_id';
1125
-
1126
861
  /**
1127
862
  * Generate a persistent anonymous ID
1128
863
  */
@@ -1132,39 +867,12 @@ function generateAnonymousId() {
1132
867
  return `anon_${timestamp}_${random}`;
1133
868
  }
1134
869
 
1135
- /**
1136
- * Initialize anonymous ID - tries to load from storage, generates new if not found
1137
- * This is called internally and runs asynchronously
1138
- */
1139
- async function initAnonymousId() {
1140
- try {
1141
- // Try to load from AsyncStorage
1142
- const AsyncStorage = require('@react-native-async-storage/async-storage').default;
1143
- const storedId = await AsyncStorage.getItem(ANONYMOUS_ID_KEY);
1144
- if (storedId) {
1145
- anonymousId = storedId;
1146
- } else {
1147
- // Generate new ID and persist
1148
- anonymousId = generateAnonymousId();
1149
- await AsyncStorage.setItem(ANONYMOUS_ID_KEY, anonymousId);
1150
- }
1151
- } catch {
1152
- // AsyncStorage not available or error - just generate without persistence
1153
- if (!anonymousId) {
1154
- anonymousId = generateAnonymousId();
1155
- }
1156
- }
1157
- }
1158
-
1159
870
  /**
1160
871
  * Get the anonymous ID (synchronous - returns generated ID immediately)
1161
- * For persistent ID, call initAnonymousId() first
1162
872
  */
1163
873
  export function getAnonymousId() {
1164
874
  if (!anonymousId) {
1165
875
  anonymousId = generateAnonymousId();
1166
- // Try to persist asynchronously (fire and forget)
1167
- initAnonymousId().catch(() => {});
1168
876
  }
1169
877
  return anonymousId;
1170
878
  }
@@ -1177,11 +885,9 @@ export async function ensurePersistentAnonymousId() {
1177
885
  if (anonymousId) return anonymousId;
1178
886
  if (!anonymousIdPromise) {
1179
887
  anonymousIdPromise = (async () => {
1180
- await initAnonymousId();
1181
- if (!anonymousId) {
1182
- anonymousId = generateAnonymousId();
1183
- }
1184
- return anonymousId;
888
+ const id = await loadAnonymousId();
889
+ anonymousId = id;
890
+ return id;
1185
891
  })();
1186
892
  }
1187
893
  return anonymousIdPromise;
@@ -1192,28 +898,23 @@ export async function ensurePersistentAnonymousId() {
1192
898
  * Call this at app startup for best results
1193
899
  */
1194
900
  export async function loadAnonymousId() {
1195
- await initAnonymousId();
1196
- return anonymousId || generateAnonymousId();
901
+ const nativeModule = getRejourneyNativeModule();
902
+ if (nativeModule && nativeModule.getUserIdentity) {
903
+ try {
904
+ return (await nativeModule.getUserIdentity()) || generateAnonymousId();
905
+ } catch {
906
+ return generateAnonymousId();
907
+ }
908
+ }
909
+ return generateAnonymousId();
1197
910
  }
1198
911
 
1199
912
  /**
1200
- * Set a custom anonymous ID (e.g., from persistent storage)
913
+ * Set a custom anonymous ID
1201
914
  */
1202
915
  export function setAnonymousId(id) {
1203
916
  anonymousId = id;
1204
- // Try to persist asynchronously
1205
- try {
1206
- const AsyncStorage = require('@react-native-async-storage/async-storage').default;
1207
- AsyncStorage.setItem(ANONYMOUS_ID_KEY, id).catch(() => {});
1208
- } catch {
1209
- // Ignore if AsyncStorage not available
1210
- }
1211
917
  }
1212
-
1213
- // =============================================================================
1214
- // Exports
1215
- // =============================================================================
1216
-
1217
918
  export default {
1218
919
  init: initAutoTracking,
1219
920
  cleanup: cleanupAutoTracking,