@rejourneyco/react-native 1.0.9 → 1.0.11

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 (34) 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/Rejourney.h +4 -0
  17. package/ios/Rejourney.mm +3 -15
  18. package/ios/Utility/DataCompression.swift +2 -2
  19. package/lib/commonjs/expoRouterTracking.js +137 -0
  20. package/lib/commonjs/index.js +176 -19
  21. package/lib/commonjs/sdk/autoTracking.js +100 -89
  22. package/lib/module/expoRouterTracking.js +135 -0
  23. package/lib/module/index.js +175 -13
  24. package/lib/module/sdk/autoTracking.js +98 -89
  25. package/lib/typescript/expoRouterTracking.d.ts +14 -0
  26. package/lib/typescript/index.d.ts +2 -2
  27. package/lib/typescript/sdk/autoTracking.d.ts +11 -0
  28. package/lib/typescript/types/index.d.ts +42 -3
  29. package/package.json +22 -2
  30. package/rejourney.podspec +11 -2
  31. package/src/expoRouterTracking.ts +167 -0
  32. package/src/index.ts +184 -16
  33. package/src/sdk/autoTracking.ts +110 -103
  34. package/src/types/index.ts +43 -3
@@ -146,6 +146,7 @@ export interface AutoTrackingConfig {
146
146
  collectDeviceInfo?: boolean;
147
147
  maxSessionDurationMs?: number;
148
148
  detectDeadTaps?: boolean;
149
+ autoTrackExpoRouter?: boolean;
149
150
  }
150
151
 
151
152
  let isInitialized = false;
@@ -209,6 +210,7 @@ export function initAutoTracking(
209
210
  trackReactNativeErrors: true,
210
211
  trackConsoleLogs: true,
211
212
  collectDeviceInfo: true,
213
+ autoTrackExpoRouter: true,
212
214
  maxSessionDurationMs: trackingConfig.maxSessionDurationMs,
213
215
  ...trackingConfig,
214
216
  };
@@ -703,10 +705,26 @@ function restoreConsoleHandlers(): void {
703
705
  }
704
706
 
705
707
  let navigationPollingInterval: ReturnType<typeof setInterval> | null = null;
708
+ /** Interval ID from optional expo-router entry; cleared in cleanupNavigationTracking */
709
+ let expoRouterPollingIntervalId: ReturnType<typeof setInterval> | null = null;
706
710
  let lastDetectedScreen = '';
707
711
  let navigationSetupDone = false;
708
- let navigationPollingErrors = 0;
709
- const MAX_POLLING_ERRORS = 10; // Stop polling after 10 consecutive errors
712
+
713
+ /**
714
+ * Register the polling interval from the optional expo-router entry so we can clear it on cleanup.
715
+ * Used by src/expoRouterTracking.ts (only loaded when app imports '@rejourneyco/react-native/expo-router').
716
+ */
717
+ export function setExpoRouterPollingInterval(id: ReturnType<typeof setInterval> | null): void {
718
+ expoRouterPollingIntervalId = id;
719
+ }
720
+
721
+ /**
722
+ * Check if Expo Router auto-tracking is enabled in the current configuration.
723
+ * Used by src/expoRouterTracking.ts.
724
+ */
725
+ export function isExpoRouterTrackingEnabled(): boolean {
726
+ return config.autoTrackExpoRouter !== false;
727
+ }
710
728
 
711
729
  /**
712
730
  * Track a navigation state change from React Navigation.
@@ -803,101 +821,97 @@ export function useNavigationTracking() {
803
821
  }
804
822
 
805
823
  /**
806
- * Setup automatic Expo Router tracking
807
- *
808
- * For Expo apps using expo-router - works automatically.
809
- * For bare React Native apps - use trackNavigationState instead.
824
+ * Setup automatic navigation tracking.
825
+ *
826
+ * Expo Router: not set up here to avoid pulling expo-router into the main bundle
827
+ * (Metro resolves require() at build time, which causes "Requiring unknown module"
828
+ * in apps that use Expo + react-navigation without expo-router). If you use
829
+ * expo-router, add: import '@rejourneyco/react-native/expo-router';
830
+ *
831
+ * For React Navigation (non–expo-router), use trackNavigationState() on your
832
+ * NavigationContainer's onStateChange.
810
833
  */
811
834
  function setupNavigationTracking(): void {
812
835
  if (navigationSetupDone) return;
813
836
  navigationSetupDone = true;
814
837
 
815
- if (__DEV__) {
816
- logger.debug('Setting up navigation tracking...');
838
+ // Auto-detect expo-router and set up screen tracking if available.
839
+ // This is safe: if expo-router isn't installed, the require fails silently.
840
+ // We defer slightly so the router has time to initialize after JS bundle load.
841
+ if (config.autoTrackExpoRouter !== false) {
842
+ tryAutoSetupExpoRouter();
817
843
  }
844
+ }
818
845
 
819
- let attempts = 0;
820
- const maxAttempts = 5;
821
-
822
- const trySetup = () => {
823
- attempts++;
824
- if (__DEV__) {
825
- logger.debug('Navigation setup attempt', attempts, 'of', maxAttempts);
826
- }
827
-
828
- const success = trySetupExpoRouter();
846
+ /**
847
+ * Attempt to auto-detect and set up expo-router screen tracking.
848
+ * Uses a retry mechanism because the router may not be ready immediately
849
+ * after JS bundle load.
850
+ */
851
+ function tryAutoSetupExpoRouter(attempt: number = 0, maxAttempts: number = 5): void {
852
+ const delay = 200 * (attempt + 1); // 200, 400, 600, 800, 1000ms
829
853
 
830
- if (success) {
831
- if (__DEV__) {
832
- logger.debug('Expo Router setup: SUCCESS on attempt', attempts);
833
- }
834
- } else if (attempts < maxAttempts) {
835
- const delay = 200 * attempts;
836
- if (__DEV__) {
837
- logger.debug('Expo Router not ready, retrying in', delay, 'ms');
854
+ setTimeout(() => {
855
+ try {
856
+ // Dynamic require wrapped in a variable to prevent Metro from statically resolving it
857
+ const EXPO_ROUTER = 'expo-router';
858
+ const expoRouter = require(EXPO_ROUTER);
859
+
860
+ if (!expoRouter?.router) {
861
+ // expo-router exists but router not ready yet retry
862
+ if (attempt < maxAttempts - 1) {
863
+ tryAutoSetupExpoRouter(attempt + 1, maxAttempts);
864
+ }
865
+ return;
838
866
  }
839
- setTimeout(trySetup, delay);
840
- } else {
841
- if (__DEV__) {
842
- logger.debug('Expo Router setup: FAILED after', maxAttempts, 'attempts');
843
- logger.debug('For manual navigation tracking, use trackNavigationState() on your NavigationContainer');
867
+
868
+ // Router is ready — set up the polling-based screen tracker
869
+ setupExpoRouterPolling(expoRouter.router);
870
+ } catch {
871
+ // expo-router not installed — this is fine, just means the app
872
+ // uses bare React Navigation or no navigation at all.
873
+ if (__DEV__ && attempt === 0) {
874
+ logger.debug('Expo Router not detected, skipping auto screen tracking. Use trackNavigationState() for React Navigation.');
844
875
  }
845
876
  }
846
- };
847
-
848
- setTimeout(trySetup, 200);
877
+ }, delay);
849
878
  }
850
879
 
851
880
  /**
852
- * Set up Expo Router auto-tracking by polling the internal router store
853
- *
854
- * Supports both expo-router v3 (store.rootState) and v6+ (store.state, store.navigationRef)
855
- */
856
- function trySetupExpoRouter(): boolean {
857
- try {
858
- const expoRouter = require('expo-router');
859
- const router = expoRouter.router;
860
-
861
- if (!router) {
862
- if (__DEV__) {
863
- logger.debug('Expo Router: router object not found');
864
- }
865
- return false;
866
- }
881
+ * Poll expo-router state for screen changes.
882
+ * Inlined from expoRouterTracking.ts so no separate import is needed.
883
+ */
884
+ function setupExpoRouterPolling(router: any): void {
885
+ // Guard against double-setup (core auto-detection + legacy expoRouterTracking.ts import)
886
+ if (expoRouterPollingIntervalId != null) return;
867
887
 
868
- if (__DEV__) {
869
- logger.debug('Expo Router: Setting up navigation tracking');
870
- }
888
+ const MAX_POLLING_ERRORS = 10;
889
+ let pollingErrors = 0;
871
890
 
891
+ try {
872
892
  const { normalizeScreenName, getScreenNameFromPath } = require('./navigation');
873
893
 
874
- navigationPollingInterval = setInterval(() => {
894
+ const intervalId = setInterval(() => {
875
895
  try {
876
- let state = null;
877
- let stateSource = '';
896
+ let state: any = null;
897
+
878
898
  if (typeof router.getState === 'function') {
879
899
  state = router.getState();
880
- stateSource = 'router.getState()';
881
- } else if ((router as any).rootState) {
882
- state = (router as any).rootState;
883
- stateSource = 'router.rootState';
900
+ } else if (router.rootState) {
901
+ state = router.rootState;
884
902
  }
885
903
 
886
904
  if (!state) {
887
905
  try {
888
- const storeModule = require('expo-router/build/global-state/router-store');
906
+ const STORE_PATH = 'expo-router/build/global-state/router-store';
907
+ const storeModule = require(STORE_PATH);
889
908
  if (storeModule?.store) {
890
909
  state = storeModule.store.state;
891
- if (state) stateSource = 'store.state';
892
-
893
910
  if (!state && storeModule.store.navigationRef?.current) {
894
911
  state = storeModule.store.navigationRef.current.getRootState?.();
895
- if (state) stateSource = 'navigationRef.getRootState()';
896
912
  }
897
-
898
913
  if (!state) {
899
914
  state = storeModule.store.rootState || storeModule.store.initialState;
900
- if (state) stateSource = 'store.rootState/initialState';
901
915
  }
902
916
  }
903
917
  } catch {
@@ -907,10 +921,10 @@ function trySetupExpoRouter(): boolean {
907
921
 
908
922
  if (!state) {
909
923
  try {
910
- const imperative = require('expo-router/build/imperative-api');
924
+ const IMPERATIVE_PATH = 'expo-router/build/imperative-api';
925
+ const imperative = require(IMPERATIVE_PATH);
911
926
  if (imperative?.router) {
912
927
  state = imperative.router.getState?.();
913
- if (state) stateSource = 'imperative-api';
914
928
  }
915
929
  } catch {
916
930
  // Ignore
@@ -918,55 +932,45 @@ function trySetupExpoRouter(): boolean {
918
932
  }
919
933
 
920
934
  if (state) {
921
- navigationPollingErrors = 0;
922
- navigationPollingErrors = 0;
923
- const screenName = extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName);
935
+ pollingErrors = 0;
936
+ const screenName = extractScreenNameFromRouterState(
937
+ state,
938
+ getScreenNameFromPath,
939
+ normalizeScreenName
940
+ );
924
941
  if (screenName && screenName !== lastDetectedScreen) {
925
- if (__DEV__) {
926
- logger.debug('Screen changed:', lastDetectedScreen, '->', screenName, `(source: ${stateSource})`);
927
- }
928
942
  lastDetectedScreen = screenName;
929
943
  trackScreen(screenName);
930
944
  }
931
945
  } else {
932
- navigationPollingErrors++;
933
- if (__DEV__ && navigationPollingErrors === 1) {
934
- logger.debug('Expo Router: Could not get navigation state');
946
+ pollingErrors++;
947
+ if (pollingErrors >= MAX_POLLING_ERRORS) {
948
+ clearInterval(intervalId);
949
+ expoRouterPollingIntervalId = null;
935
950
  }
936
- if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
937
- cleanupNavigationTracking();
938
- }
939
- }
940
- } catch (e) {
941
- navigationPollingErrors++;
942
- if (__DEV__ && navigationPollingErrors === 1) {
943
- logger.debug('Expo Router polling error:', e);
944
951
  }
945
- if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
946
- cleanupNavigationTracking();
952
+ } catch {
953
+ pollingErrors++;
954
+ if (pollingErrors >= MAX_POLLING_ERRORS) {
955
+ clearInterval(intervalId);
956
+ expoRouterPollingIntervalId = null;
947
957
  }
948
958
  }
949
959
  }, 500);
950
960
 
951
- return true;
952
- } catch (e) {
953
- if (__DEV__) {
954
- logger.debug('Expo Router not available:', e);
955
- }
956
- return false;
961
+ expoRouterPollingIntervalId = intervalId;
962
+ } catch {
963
+ // navigation module not available — ignore
957
964
  }
958
965
  }
959
966
 
960
967
  /**
961
- * Extract screen name from Expo Router navigation state
962
- *
963
- * Handles complex nested structures like Drawer → Tabs → Stack
964
- * by recursively accumulating segments from each navigation level.
968
+ * Extract the active screen name from expo-router navigation state.
965
969
  */
966
970
  function extractScreenNameFromRouterState(
967
971
  state: any,
968
- getScreenNameFromPath: (path: string, segments: string[]) => string,
969
- normalizeScreenName: (name: string) => string,
972
+ getScreenNameFromPathFn: (path: string, segments: string[]) => string,
973
+ normalizeScreenNameFn: (name: string) => string,
970
974
  accumulatedSegments: string[] = []
971
975
  ): string | null {
972
976
  if (!state?.routes) return null;
@@ -979,13 +983,13 @@ function extractScreenNameFromRouterState(
979
983
  if (route.state) {
980
984
  return extractScreenNameFromRouterState(
981
985
  route.state,
982
- getScreenNameFromPath,
983
- normalizeScreenName,
986
+ getScreenNameFromPathFn,
987
+ normalizeScreenNameFn,
984
988
  newSegments
985
989
  );
986
990
  }
987
991
 
988
- const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
992
+ const cleanSegments = newSegments.filter((s) => !s.startsWith('(') && !s.endsWith(')'));
989
993
 
990
994
  if (cleanSegments.length === 0) {
991
995
  for (let i = newSegments.length - 1; i >= 0; i--) {
@@ -998,7 +1002,7 @@ function extractScreenNameFromRouterState(
998
1002
  }
999
1003
 
1000
1004
  const pathname = '/' + cleanSegments.join('/');
1001
- return getScreenNameFromPath(pathname, newSegments);
1005
+ return getScreenNameFromPathFn(pathname, newSegments);
1002
1006
  }
1003
1007
 
1004
1008
  /**
@@ -1009,9 +1013,12 @@ function cleanupNavigationTracking(): void {
1009
1013
  clearInterval(navigationPollingInterval);
1010
1014
  navigationPollingInterval = null;
1011
1015
  }
1016
+ if (expoRouterPollingIntervalId != null) {
1017
+ clearInterval(expoRouterPollingIntervalId);
1018
+ expoRouterPollingIntervalId = null;
1019
+ }
1012
1020
  navigationSetupDone = false;
1013
1021
  lastDetectedScreen = '';
1014
- navigationPollingErrors = 0;
1015
1022
  }
1016
1023
 
1017
1024
  /**
@@ -31,6 +31,8 @@ export interface RejourneyConfig {
31
31
  maxStorageSize?: number;
32
32
  /** Enable automatic screen name detection with React Navigation (default: true) */
33
33
  autoScreenTracking?: boolean;
34
+ /** Enable automatic screen name detection with Expo Router (default: true) */
35
+ autoTrackExpoRouter?: boolean;
34
36
  /** Enable automatic gesture detection (default: true) */
35
37
  autoGestureTracking?: boolean;
36
38
  /** Enable privacy occlusion for text inputs (default: true) */
@@ -521,7 +523,17 @@ export interface RejourneyNativeModule {
521
523
  export interface RejourneyAPI {
522
524
  /** SDK version */
523
525
  readonly version: string;
524
- /** Internal method to start recording session (called by startRejourney) */
526
+ /**
527
+ * Initialize Rejourney SDK
528
+ * @param publicRouteKey - Your public route key from the Rejourney dashboard
529
+ * @param options - Optional configuration options
530
+ */
531
+ init(publicRouteKey: string, options?: Omit<RejourneyConfig, 'publicRouteKey'>): void;
532
+ /** Start recording (call after user consent) */
533
+ start(): void;
534
+ /** Stop recording */
535
+ stop(): void;
536
+ /** Internal method to start recording session (called by start() / startRejourney()) */
525
537
  _startSession(): Promise<boolean>;
526
538
  /** Internal method to stop recording session (called by stopRejourney) */
527
539
  _stopSession(): Promise<void>;
@@ -531,8 +543,18 @@ export interface RejourneyAPI {
531
543
  setUserIdentity(userId: string): void;
532
544
  /** Clear user identity */
533
545
  clearUserIdentity(): void;
534
- /** Tag current screen */
535
- tagScreen(screenName: string, params?: Record<string, unknown>): void;
546
+ /**
547
+ * Set custom session metadata.
548
+ * Can be called with a single key-value pair or an object of properties.
549
+ * Useful for filtering sessions later (e.g., plan: 'premium', role: 'admin').
550
+ * Caps at 100 properties per session.
551
+ *
552
+ * @param keyOrProperties Property name string, or an object containing key-value pairs
553
+ * @param value Property value (if first argument is a string)
554
+ */
555
+ setMetadata(keyOrProperties: string | Record<string, string | number | boolean>, value?: string | number | boolean): void;
556
+ /** Track current screen (manual) */
557
+ trackScreen(screenName: string, params?: Record<string, unknown>): void;
536
558
  /** Mark a view as sensitive (will be occluded in recording) */
537
559
  setOccluded(viewRef: { current: any }, occluded?: boolean): void;
538
560
  /** Add a tag to current session */
@@ -628,6 +650,22 @@ export interface RejourneyAPI {
628
650
  * @param nativeID - The nativeID prop of the view to unmask
629
651
  */
630
652
  unmaskView(nativeID: string): void;
653
+
654
+ /**
655
+ * Hook for automatic React Navigation tracking.
656
+ * Pass the returned object to your NavigationContainer props.
657
+ *
658
+ * @example
659
+ * ```tsx
660
+ * const navigationTracking = Rejourney.useNavigationTracking();
661
+ * <NavigationContainer {...navigationTracking}>
662
+ * ```
663
+ */
664
+ useNavigationTracking(): {
665
+ ref: any;
666
+ onReady: () => void;
667
+ onStateChange: (state: any) => void;
668
+ };
631
669
  }
632
670
 
633
671
  /**
@@ -698,6 +736,8 @@ export interface UseRejourneyResult {
698
736
  stopRecording: () => Promise<void>;
699
737
  /** Log custom event */
700
738
  logEvent: (name: string, properties?: Record<string, unknown>) => void;
739
+ /** Set custom session metadata */
740
+ setMetadata: (keyOrProperties: string | Record<string, string | number | boolean>, value?: string | number | boolean) => void;
701
741
  /** Error if any */
702
742
  error: Error | null;
703
743
  }