@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.
- package/README.md +77 -3
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +54 -0
- package/android/src/main/java/com/rejourney/RejourneyOkHttpInitProvider.kt +68 -0
- package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +3 -0
- package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +0 -7
- package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +4 -1
- package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +3 -0
- package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +26 -0
- package/android/src/main/java/com/rejourney/utility/DataCompression.kt +14 -2
- package/ios/Engine/RejourneyImpl.swift +5 -0
- package/ios/Recording/RejourneyURLProtocol.swift +58 -10
- package/ios/Recording/ReplayOrchestrator.swift +3 -1
- package/ios/Recording/TelemetryPipeline.swift +28 -2
- package/ios/Recording/VisualCapture.swift +25 -21
- package/ios/Rejourney.h +4 -0
- package/ios/Rejourney.mm +3 -15
- package/ios/Utility/DataCompression.swift +2 -2
- package/lib/commonjs/expoRouterTracking.js +137 -0
- package/lib/commonjs/index.js +176 -19
- package/lib/commonjs/sdk/autoTracking.js +100 -89
- package/lib/module/expoRouterTracking.js +135 -0
- package/lib/module/index.js +175 -13
- package/lib/module/sdk/autoTracking.js +98 -89
- package/lib/typescript/expoRouterTracking.d.ts +14 -0
- package/lib/typescript/index.d.ts +2 -2
- package/lib/typescript/sdk/autoTracking.d.ts +11 -0
- package/lib/typescript/types/index.d.ts +42 -3
- package/package.json +22 -2
- package/rejourney.podspec +11 -2
- package/src/expoRouterTracking.ts +167 -0
- package/src/index.ts +184 -16
- package/src/sdk/autoTracking.ts +110 -103
- package/src/types/index.ts +43 -3
package/src/sdk/autoTracking.ts
CHANGED
|
@@ -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
|
-
|
|
709
|
-
|
|
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
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
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
|
|
816
|
-
|
|
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
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
if (
|
|
837
|
-
|
|
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
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
-
|
|
869
|
-
|
|
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
|
-
|
|
894
|
+
const intervalId = setInterval(() => {
|
|
875
895
|
try {
|
|
876
|
-
let state = null;
|
|
877
|
-
|
|
896
|
+
let state: any = null;
|
|
897
|
+
|
|
878
898
|
if (typeof router.getState === 'function') {
|
|
879
899
|
state = router.getState();
|
|
880
|
-
|
|
881
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
|
|
933
|
-
if (
|
|
934
|
-
|
|
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
|
-
|
|
946
|
-
|
|
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
|
-
|
|
952
|
-
} catch
|
|
953
|
-
|
|
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
|
|
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
|
-
|
|
969
|
-
|
|
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
|
-
|
|
983
|
-
|
|
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
|
|
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
|
/**
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
535
|
-
|
|
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
|
}
|