@rejourneyco/react-native 1.0.9 → 1.0.10

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 (31) hide show
  1. package/README.md +77 -3
  2. package/android/src/main/AndroidManifest.xml +6 -0
  3. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +54 -0
  4. package/android/src/main/java/com/rejourney/RejourneyOkHttpInitProvider.kt +68 -0
  5. package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +3 -0
  6. package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +0 -7
  7. package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +4 -1
  8. package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +3 -0
  9. package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +26 -0
  10. package/android/src/main/java/com/rejourney/utility/DataCompression.kt +14 -2
  11. package/ios/Engine/RejourneyImpl.swift +5 -0
  12. package/ios/Recording/RejourneyURLProtocol.swift +58 -10
  13. package/ios/Recording/ReplayOrchestrator.swift +3 -1
  14. package/ios/Recording/TelemetryPipeline.swift +28 -2
  15. package/ios/Recording/VisualCapture.swift +25 -21
  16. package/ios/Utility/DataCompression.swift +2 -2
  17. package/lib/commonjs/expoRouterTracking.js +137 -0
  18. package/lib/commonjs/index.js +176 -19
  19. package/lib/commonjs/sdk/autoTracking.js +100 -89
  20. package/lib/module/expoRouterTracking.js +135 -0
  21. package/lib/module/index.js +175 -13
  22. package/lib/module/sdk/autoTracking.js +98 -89
  23. package/lib/typescript/expoRouterTracking.d.ts +14 -0
  24. package/lib/typescript/index.d.ts +2 -2
  25. package/lib/typescript/sdk/autoTracking.d.ts +11 -0
  26. package/lib/typescript/types/index.d.ts +42 -3
  27. package/package.json +22 -2
  28. package/src/expoRouterTracking.ts +167 -0
  29. package/src/index.ts +184 -16
  30. package/src/sdk/autoTracking.ts +110 -103
  31. package/src/types/index.ts +43 -3
@@ -13,11 +13,13 @@ exports.getRemainingSessionDurationMs = getRemainingSessionDurationMs;
13
13
  exports.getSessionMetrics = getSessionMetrics;
14
14
  exports.hasExceededMaxSessionDuration = hasExceededMaxSessionDuration;
15
15
  exports.initAutoTracking = initAutoTracking;
16
+ exports.isExpoRouterTrackingEnabled = isExpoRouterTrackingEnabled;
16
17
  exports.loadAnonymousId = loadAnonymousId;
17
18
  exports.markTapHandled = markTapHandled;
18
19
  exports.notifyStateChange = notifyStateChange;
19
20
  exports.resetMetrics = resetMetrics;
20
21
  exports.setAnonymousId = setAnonymousId;
22
+ exports.setExpoRouterPollingInterval = setExpoRouterPollingInterval;
21
23
  exports.setMaxSessionDurationMinutes = setMaxSessionDurationMinutes;
22
24
  exports.trackAPIRequest = trackAPIRequest;
23
25
  exports.trackGesture = trackGesture;
@@ -147,6 +149,7 @@ function initAutoTracking(trackingConfig, callbacks = {}) {
147
149
  trackReactNativeErrors: true,
148
150
  trackConsoleLogs: true,
149
151
  collectDeviceInfo: true,
152
+ autoTrackExpoRouter: true,
150
153
  maxSessionDurationMs: trackingConfig.maxSessionDurationMs,
151
154
  ...trackingConfig
152
155
  };
@@ -596,10 +599,26 @@ function restoreConsoleHandlers() {
596
599
  // Note: console.error is restored in restoreErrorHandlers via originalConsoleError
597
600
  }
598
601
  let navigationPollingInterval = null;
602
+ /** Interval ID from optional expo-router entry; cleared in cleanupNavigationTracking */
603
+ let expoRouterPollingIntervalId = null;
599
604
  let lastDetectedScreen = '';
600
605
  let navigationSetupDone = false;
601
- let navigationPollingErrors = 0;
602
- const MAX_POLLING_ERRORS = 10; // Stop polling after 10 consecutive errors
606
+
607
+ /**
608
+ * Register the polling interval from the optional expo-router entry so we can clear it on cleanup.
609
+ * Used by src/expoRouterTracking.ts (only loaded when app imports '@rejourneyco/react-native/expo-router').
610
+ */
611
+ function setExpoRouterPollingInterval(id) {
612
+ expoRouterPollingIntervalId = id;
613
+ }
614
+
615
+ /**
616
+ * Check if Expo Router auto-tracking is enabled in the current configuration.
617
+ * Used by src/expoRouterTracking.ts.
618
+ */
619
+ function isExpoRouterTrackingEnabled() {
620
+ return config.autoTrackExpoRouter !== false;
621
+ }
603
622
 
604
623
  /**
605
624
  * Track a navigation state change from React Navigation.
@@ -696,91 +715,94 @@ function useNavigationTracking() {
696
715
  }
697
716
 
698
717
  /**
699
- * Setup automatic Expo Router tracking
700
- *
701
- * For Expo apps using expo-router - works automatically.
702
- * For bare React Native apps - use trackNavigationState instead.
718
+ * Setup automatic navigation tracking.
719
+ *
720
+ * Expo Router: not set up here to avoid pulling expo-router into the main bundle
721
+ * (Metro resolves require() at build time, which causes "Requiring unknown module"
722
+ * in apps that use Expo + react-navigation without expo-router). If you use
723
+ * expo-router, add: import '@rejourneyco/react-native/expo-router';
724
+ *
725
+ * For React Navigation (non–expo-router), use trackNavigationState() on your
726
+ * NavigationContainer's onStateChange.
703
727
  */
704
728
  function setupNavigationTracking() {
705
729
  if (navigationSetupDone) return;
706
730
  navigationSetupDone = true;
707
- if (__DEV__) {
708
- _utils.logger.debug('Setting up navigation tracking...');
731
+
732
+ // Auto-detect expo-router and set up screen tracking if available.
733
+ // This is safe: if expo-router isn't installed, the require fails silently.
734
+ // We defer slightly so the router has time to initialize after JS bundle load.
735
+ if (config.autoTrackExpoRouter !== false) {
736
+ tryAutoSetupExpoRouter();
709
737
  }
710
- let attempts = 0;
711
- const maxAttempts = 5;
712
- const trySetup = () => {
713
- attempts++;
714
- if (__DEV__) {
715
- _utils.logger.debug('Navigation setup attempt', attempts, 'of', maxAttempts);
716
- }
717
- const success = trySetupExpoRouter();
718
- if (success) {
719
- if (__DEV__) {
720
- _utils.logger.debug('Expo Router setup: SUCCESS on attempt', attempts);
721
- }
722
- } else if (attempts < maxAttempts) {
723
- const delay = 200 * attempts;
724
- if (__DEV__) {
725
- _utils.logger.debug('Expo Router not ready, retrying in', delay, 'ms');
738
+ }
739
+
740
+ /**
741
+ * Attempt to auto-detect and set up expo-router screen tracking.
742
+ * Uses a retry mechanism because the router may not be ready immediately
743
+ * after JS bundle load.
744
+ */
745
+ function tryAutoSetupExpoRouter(attempt = 0, maxAttempts = 5) {
746
+ const delay = 200 * (attempt + 1); // 200, 400, 600, 800, 1000ms
747
+
748
+ setTimeout(() => {
749
+ try {
750
+ // Dynamic require wrapped in a variable to prevent Metro from statically resolving it
751
+ const EXPO_ROUTER = 'expo-router';
752
+ const expoRouter = require(EXPO_ROUTER);
753
+ if (!expoRouter?.router) {
754
+ // expo-router exists but router not ready yet — retry
755
+ if (attempt < maxAttempts - 1) {
756
+ tryAutoSetupExpoRouter(attempt + 1, maxAttempts);
757
+ }
758
+ return;
726
759
  }
727
- setTimeout(trySetup, delay);
728
- } else {
729
- if (__DEV__) {
730
- _utils.logger.debug('Expo Router setup: FAILED after', maxAttempts, 'attempts');
731
- _utils.logger.debug('For manual navigation tracking, use trackNavigationState() on your NavigationContainer');
760
+
761
+ // Router is ready — set up the polling-based screen tracker
762
+ setupExpoRouterPolling(expoRouter.router);
763
+ } catch {
764
+ // expo-router not installed — this is fine, just means the app
765
+ // uses bare React Navigation or no navigation at all.
766
+ if (__DEV__ && attempt === 0) {
767
+ _utils.logger.debug('Expo Router not detected, skipping auto screen tracking. Use trackNavigationState() for React Navigation.');
732
768
  }
733
769
  }
734
- };
735
- setTimeout(trySetup, 200);
770
+ }, delay);
736
771
  }
737
772
 
738
773
  /**
739
- * Set up Expo Router auto-tracking by polling the internal router store
740
- *
741
- * Supports both expo-router v3 (store.rootState) and v6+ (store.state, store.navigationRef)
742
- */
743
- function trySetupExpoRouter() {
774
+ * Poll expo-router state for screen changes.
775
+ * Inlined from expoRouterTracking.ts so no separate import is needed.
776
+ */
777
+ function setupExpoRouterPolling(router) {
778
+ // Guard against double-setup (core auto-detection + legacy expoRouterTracking.ts import)
779
+ if (expoRouterPollingIntervalId != null) return;
780
+ const MAX_POLLING_ERRORS = 10;
781
+ let pollingErrors = 0;
744
782
  try {
745
- const expoRouter = require('expo-router');
746
- const router = expoRouter.router;
747
- if (!router) {
748
- if (__DEV__) {
749
- _utils.logger.debug('Expo Router: router object not found');
750
- }
751
- return false;
752
- }
753
- if (__DEV__) {
754
- _utils.logger.debug('Expo Router: Setting up navigation tracking');
755
- }
756
783
  const {
757
784
  normalizeScreenName,
758
785
  getScreenNameFromPath
759
786
  } = require('./navigation');
760
- navigationPollingInterval = setInterval(() => {
787
+ const intervalId = setInterval(() => {
761
788
  try {
762
789
  let state = null;
763
- let stateSource = '';
764
790
  if (typeof router.getState === 'function') {
765
791
  state = router.getState();
766
- stateSource = 'router.getState()';
767
792
  } else if (router.rootState) {
768
793
  state = router.rootState;
769
- stateSource = 'router.rootState';
770
794
  }
771
795
  if (!state) {
772
796
  try {
773
- const storeModule = require('expo-router/build/global-state/router-store');
797
+ const STORE_PATH = 'expo-router/build/global-state/router-store';
798
+ const storeModule = require(STORE_PATH);
774
799
  if (storeModule?.store) {
775
800
  state = storeModule.store.state;
776
- if (state) stateSource = 'store.state';
777
801
  if (!state && storeModule.store.navigationRef?.current) {
778
802
  state = storeModule.store.navigationRef.current.getRootState?.();
779
- if (state) stateSource = 'navigationRef.getRootState()';
780
803
  }
781
804
  if (!state) {
782
805
  state = storeModule.store.rootState || storeModule.store.initialState;
783
- if (state) stateSource = 'store.rootState/initialState';
784
806
  }
785
807
  }
786
808
  } catch {
@@ -789,67 +811,53 @@ function trySetupExpoRouter() {
789
811
  }
790
812
  if (!state) {
791
813
  try {
792
- const imperative = require('expo-router/build/imperative-api');
814
+ const IMPERATIVE_PATH = 'expo-router/build/imperative-api';
815
+ const imperative = require(IMPERATIVE_PATH);
793
816
  if (imperative?.router) {
794
817
  state = imperative.router.getState?.();
795
- if (state) stateSource = 'imperative-api';
796
818
  }
797
819
  } catch {
798
820
  // Ignore
799
821
  }
800
822
  }
801
823
  if (state) {
802
- navigationPollingErrors = 0;
803
- navigationPollingErrors = 0;
824
+ pollingErrors = 0;
804
825
  const screenName = extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName);
805
826
  if (screenName && screenName !== lastDetectedScreen) {
806
- if (__DEV__) {
807
- _utils.logger.debug('Screen changed:', lastDetectedScreen, '->', screenName, `(source: ${stateSource})`);
808
- }
809
827
  lastDetectedScreen = screenName;
810
828
  trackScreen(screenName);
811
829
  }
812
830
  } else {
813
- navigationPollingErrors++;
814
- if (__DEV__ && navigationPollingErrors === 1) {
815
- _utils.logger.debug('Expo Router: Could not get navigation state');
816
- }
817
- if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
818
- cleanupNavigationTracking();
831
+ pollingErrors++;
832
+ if (pollingErrors >= MAX_POLLING_ERRORS) {
833
+ clearInterval(intervalId);
834
+ expoRouterPollingIntervalId = null;
819
835
  }
820
836
  }
821
- } catch (e) {
822
- navigationPollingErrors++;
823
- if (__DEV__ && navigationPollingErrors === 1) {
824
- _utils.logger.debug('Expo Router polling error:', e);
825
- }
826
- if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
827
- cleanupNavigationTracking();
837
+ } catch {
838
+ pollingErrors++;
839
+ if (pollingErrors >= MAX_POLLING_ERRORS) {
840
+ clearInterval(intervalId);
841
+ expoRouterPollingIntervalId = null;
828
842
  }
829
843
  }
830
844
  }, 500);
831
- return true;
832
- } catch (e) {
833
- if (__DEV__) {
834
- _utils.logger.debug('Expo Router not available:', e);
835
- }
836
- return false;
845
+ expoRouterPollingIntervalId = intervalId;
846
+ } catch {
847
+ // navigation module not available — ignore
837
848
  }
838
849
  }
839
850
 
840
851
  /**
841
- * Extract screen name from Expo Router navigation state
842
- *
843
- * Handles complex nested structures like Drawer → Tabs → Stack
844
- * by recursively accumulating segments from each navigation level.
852
+ * Extract the active screen name from expo-router navigation state.
845
853
  */
846
- function extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName, accumulatedSegments = []) {
854
+ function extractScreenNameFromRouterState(state, getScreenNameFromPathFn, normalizeScreenNameFn, accumulatedSegments = []) {
847
855
  if (!state?.routes) return null;
848
856
  const route = state.routes[state.index ?? state.routes.length - 1];
849
857
  if (!route) return null;
850
858
  const newSegments = [...accumulatedSegments, route.name];
851
859
  if (route.state) {
852
- return extractScreenNameFromRouterState(route.state, getScreenNameFromPath, normalizeScreenName, newSegments);
860
+ return extractScreenNameFromRouterState(route.state, getScreenNameFromPathFn, normalizeScreenNameFn, newSegments);
853
861
  }
854
862
  const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
855
863
  if (cleanSegments.length === 0) {
@@ -862,7 +870,7 @@ function extractScreenNameFromRouterState(state, getScreenNameFromPath, normaliz
862
870
  }
863
871
  }
864
872
  const pathname = '/' + cleanSegments.join('/');
865
- return getScreenNameFromPath(pathname, newSegments);
873
+ return getScreenNameFromPathFn(pathname, newSegments);
866
874
  }
867
875
 
868
876
  /**
@@ -873,9 +881,12 @@ function cleanupNavigationTracking() {
873
881
  clearInterval(navigationPollingInterval);
874
882
  navigationPollingInterval = null;
875
883
  }
884
+ if (expoRouterPollingIntervalId != null) {
885
+ clearInterval(expoRouterPollingIntervalId);
886
+ expoRouterPollingIntervalId = null;
887
+ }
876
888
  navigationSetupDone = false;
877
889
  lastDetectedScreen = '';
878
- navigationPollingErrors = 0;
879
890
  }
880
891
 
881
892
  /**
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Optional Expo Router integration for @rejourneyco/react-native
3
+ *
4
+ * This file is only loaded when you import '@rejourneyco/react-native/expo-router'.
5
+ * It contains require('expo-router') and related subpaths. Metro bundles require()
6
+ * at build time, so keeping this in a separate entry ensures apps that use
7
+ * Expo with react-navigation (without expo-router) never pull in expo-router
8
+ * and avoid "Requiring unknown module" crashes.
9
+ *
10
+ * If you use expo-router, add this once (e.g. in your root _layout.tsx):
11
+ * import '@rejourneyco/react-native/expo-router';
12
+ */
13
+
14
+ import { trackScreen, setExpoRouterPollingInterval, isExpoRouterTrackingEnabled } from './sdk/autoTracking';
15
+ import { normalizeScreenName, getScreenNameFromPath } from './sdk/navigation';
16
+ const MAX_POLLING_ERRORS = 10;
17
+ function extractScreenNameFromRouterState(state, getScreenNameFromPathFn, normalizeScreenNameFn, accumulatedSegments = []) {
18
+ if (!state?.routes) return null;
19
+ const route = state.routes[state.index ?? state.routes.length - 1];
20
+ if (!route) return null;
21
+ const newSegments = [...accumulatedSegments, route.name];
22
+ if (route.state) {
23
+ return extractScreenNameFromRouterState(route.state, getScreenNameFromPathFn, normalizeScreenNameFn, newSegments);
24
+ }
25
+ const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
26
+ if (cleanSegments.length === 0) {
27
+ for (let i = newSegments.length - 1; i >= 0; i--) {
28
+ const seg = newSegments[i];
29
+ if (seg && !seg.startsWith('(') && !seg.endsWith(')')) {
30
+ cleanSegments.push(seg);
31
+ break;
32
+ }
33
+ }
34
+ }
35
+ const pathname = '/' + cleanSegments.join('/');
36
+ return getScreenNameFromPathFn(pathname, newSegments);
37
+ }
38
+ function setupExpoRouterPolling() {
39
+ let lastDetectedScreen = '';
40
+ let pollingErrors = 0;
41
+ try {
42
+ const EXPO_ROUTER = 'expo-router';
43
+ const expoRouter = require(EXPO_ROUTER);
44
+ const router = expoRouter.router;
45
+ if (!router) {
46
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
47
+ console.debug('[Rejourney] Expo Router: router object not found');
48
+ }
49
+ return;
50
+ }
51
+ const intervalId = setInterval(() => {
52
+ try {
53
+ let state = null;
54
+ if (typeof router.getState === 'function') {
55
+ state = router.getState();
56
+ } else if (router.rootState) {
57
+ state = router.rootState;
58
+ }
59
+ if (!state) {
60
+ try {
61
+ const STORE_PATH = 'expo-router/build/global-state/router-store';
62
+ const storeModule = require(STORE_PATH);
63
+ if (storeModule?.store) {
64
+ state = storeModule.store.state;
65
+ if (!state && storeModule.store.navigationRef?.current) {
66
+ state = storeModule.store.navigationRef.current.getRootState?.();
67
+ }
68
+ if (!state) {
69
+ state = storeModule.store.rootState || storeModule.store.initialState;
70
+ }
71
+ }
72
+ } catch {
73
+ // Ignore
74
+ }
75
+ }
76
+ if (!state) {
77
+ try {
78
+ const IMPERATIVE_PATH = 'expo-router/build/imperative-api';
79
+ const imperative = require(IMPERATIVE_PATH);
80
+ if (imperative?.router) {
81
+ state = imperative.router.getState?.();
82
+ }
83
+ } catch {
84
+ // Ignore
85
+ }
86
+ }
87
+ if (state) {
88
+ pollingErrors = 0;
89
+ const screenName = extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName);
90
+ if (screenName && screenName !== lastDetectedScreen) {
91
+ lastDetectedScreen = screenName;
92
+ trackScreen(screenName);
93
+ }
94
+ } else {
95
+ pollingErrors++;
96
+ if (pollingErrors >= MAX_POLLING_ERRORS) {
97
+ clearInterval(intervalId);
98
+ setExpoRouterPollingInterval(null);
99
+ }
100
+ }
101
+ } catch {
102
+ pollingErrors++;
103
+ if (pollingErrors >= MAX_POLLING_ERRORS) {
104
+ clearInterval(intervalId);
105
+ setExpoRouterPollingInterval(null);
106
+ }
107
+ }
108
+ }, 500);
109
+ setExpoRouterPollingInterval(intervalId);
110
+ } catch (e) {
111
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
112
+ console.debug('[Rejourney] Expo Router not available:', e);
113
+ }
114
+ }
115
+ }
116
+ let attempts = 0;
117
+ const maxAttempts = 5;
118
+ function trySetup() {
119
+ attempts++;
120
+ try {
121
+ const EXPO_ROUTER = 'expo-router';
122
+ const expoRouter = require(EXPO_ROUTER);
123
+ if (expoRouter?.router && isExpoRouterTrackingEnabled()) {
124
+ setupExpoRouterPolling();
125
+ return;
126
+ }
127
+ } catch {
128
+ // Not ready or not installed
129
+ }
130
+ if (attempts < maxAttempts) {
131
+ setTimeout(trySetup, 200 * attempts);
132
+ }
133
+ }
134
+ setTimeout(trySetup, 200);
135
+ //# sourceMappingURL=expoRouterTracking.js.map