@rejourneyco/react-native 1.0.8 → 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 (52) 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 +143 -8
  4. package/android/src/main/java/com/rejourney/RejourneyOkHttpInitProvider.kt +68 -0
  5. package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +21 -3
  6. package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +69 -17
  7. package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +27 -2
  8. package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +3 -1
  9. package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +93 -0
  10. package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +226 -146
  11. package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +7 -0
  12. package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +3 -0
  13. package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +39 -0
  14. package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +8 -0
  15. package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +95 -21
  16. package/android/src/main/java/com/rejourney/utility/DataCompression.kt +14 -2
  17. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +14 -0
  18. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +18 -0
  19. package/ios/Engine/DeviceRegistrar.swift +13 -3
  20. package/ios/Engine/RejourneyImpl.swift +204 -115
  21. package/ios/Recording/AnrSentinel.swift +58 -25
  22. package/ios/Recording/InteractionRecorder.swift +1 -0
  23. package/ios/Recording/RejourneyURLProtocol.swift +216 -0
  24. package/ios/Recording/ReplayOrchestrator.swift +207 -144
  25. package/ios/Recording/SegmentDispatcher.swift +8 -0
  26. package/ios/Recording/StabilityMonitor.swift +40 -32
  27. package/ios/Recording/TelemetryPipeline.swift +45 -2
  28. package/ios/Recording/ViewHierarchyScanner.swift +1 -0
  29. package/ios/Recording/VisualCapture.swift +79 -29
  30. package/ios/Rejourney.mm +27 -8
  31. package/ios/Utility/DataCompression.swift +2 -2
  32. package/ios/Utility/ImageBlur.swift +0 -1
  33. package/lib/commonjs/expoRouterTracking.js +137 -0
  34. package/lib/commonjs/index.js +204 -34
  35. package/lib/commonjs/sdk/autoTracking.js +262 -100
  36. package/lib/commonjs/sdk/networkInterceptor.js +84 -4
  37. package/lib/module/expoRouterTracking.js +135 -0
  38. package/lib/module/index.js +203 -28
  39. package/lib/module/sdk/autoTracking.js +260 -100
  40. package/lib/module/sdk/networkInterceptor.js +84 -4
  41. package/lib/typescript/NativeRejourney.d.ts +5 -2
  42. package/lib/typescript/expoRouterTracking.d.ts +14 -0
  43. package/lib/typescript/index.d.ts +2 -2
  44. package/lib/typescript/sdk/autoTracking.d.ts +14 -1
  45. package/lib/typescript/types/index.d.ts +56 -5
  46. package/package.json +23 -3
  47. package/src/NativeRejourney.ts +8 -5
  48. package/src/expoRouterTracking.ts +167 -0
  49. package/src/index.ts +221 -35
  50. package/src/sdk/autoTracking.ts +286 -114
  51. package/src/sdk/networkInterceptor.ts +110 -1
  52. package/src/types/index.ts +58 -6
@@ -106,8 +106,7 @@ export interface Spec extends TurboModule {
106
106
  */
107
107
  debugCrash(): void;
108
108
  /**
109
- * Trigger a debug ANR (Dev only)
110
- * Blocks the main thread for the specified duration
109
+ * Trigger an ANR test by blocking the main thread for the specified duration.
111
110
  */
112
111
  debugTriggerANR(durationMs: number): void;
113
112
  /**
@@ -130,6 +129,10 @@ export interface Spec extends TurboModule {
130
129
  success: boolean;
131
130
  }>;
132
131
  getUserIdentity(): Promise<string | null>;
132
+ setAnonymousId(anonymousId: string): Promise<{
133
+ success: boolean;
134
+ }>;
135
+ getAnonymousId(): Promise<string | null>;
133
136
  setDebugMode(enabled: boolean): Promise<{
134
137
  success: boolean;
135
138
  }>;
@@ -0,0 +1,14 @@
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
+ export {};
14
+ //# sourceMappingURL=expoRouterTracking.d.ts.map
@@ -26,7 +26,7 @@ import type { RejourneyConfig, RejourneyAPI } from './types';
26
26
  /**
27
27
  * Main Rejourney API (Internal)
28
28
  */
29
- declare const Rejourney: RejourneyAPI;
29
+ export declare const Rejourney: RejourneyAPI;
30
30
  /**
31
31
  * Initialize Rejourney SDK - STEP 1 of 3
32
32
  *
@@ -81,7 +81,7 @@ export declare function startRejourney(): void;
81
81
  export declare function stopRejourney(): void;
82
82
  export default Rejourney;
83
83
  export * from './types';
84
- export { trackTap, trackScroll, trackGesture, trackInput, trackScreen, captureError, getSessionMetrics, } from './sdk/autoTracking';
84
+ export { trackTap, trackScroll, trackGesture, trackInput, captureError, getSessionMetrics, } from './sdk/autoTracking';
85
85
  export { trackNavigationState, useNavigationTracking } from './sdk/autoTracking';
86
86
  export { LogLevel } from './sdk/utils';
87
87
  /**
@@ -64,9 +64,11 @@ export interface AutoTrackingConfig {
64
64
  trackJSErrors?: boolean;
65
65
  trackPromiseRejections?: boolean;
66
66
  trackReactNativeErrors?: boolean;
67
+ trackConsoleLogs?: boolean;
67
68
  collectDeviceInfo?: boolean;
68
69
  maxSessionDurationMs?: number;
69
70
  detectDeadTaps?: boolean;
71
+ autoTrackExpoRouter?: boolean;
70
72
  }
71
73
  /**
72
74
  * Mark a tap as handled.
@@ -100,6 +102,16 @@ export declare function notifyStateChange(): void;
100
102
  * Manually track an error (for API errors, etc.)
101
103
  */
102
104
  export declare function captureError(message: string, stack?: string, name?: string): void;
105
+ /**
106
+ * Register the polling interval from the optional expo-router entry so we can clear it on cleanup.
107
+ * Used by src/expoRouterTracking.ts (only loaded when app imports '@rejourneyco/react-native/expo-router').
108
+ */
109
+ export declare function setExpoRouterPollingInterval(id: ReturnType<typeof setInterval> | null): void;
110
+ /**
111
+ * Check if Expo Router auto-tracking is enabled in the current configuration.
112
+ * Used by src/expoRouterTracking.ts.
113
+ */
114
+ export declare function isExpoRouterTrackingEnabled(): boolean;
103
115
  /**
104
116
  * Track a navigation state change from React Navigation.
105
117
  *
@@ -198,7 +210,8 @@ export declare function getAnonymousId(): string;
198
210
  export declare function ensurePersistentAnonymousId(): Promise<string>;
199
211
  /**
200
212
  * Load anonymous ID from persistent storage
201
- * Call this at app startup for best results
213
+ * Checks native anonymous storage first, then falls back to native getUserIdentity,
214
+ * and finally generates a new ID if nothing is persisted.
202
215
  */
203
216
  export declare function loadAnonymousId(): Promise<string>;
204
217
  /**
@@ -25,6 +25,8 @@ export interface RejourneyConfig {
25
25
  maxStorageSize?: number;
26
26
  /** Enable automatic screen name detection with React Navigation (default: true) */
27
27
  autoScreenTracking?: boolean;
28
+ /** Enable automatic screen name detection with Expo Router (default: true) */
29
+ autoTrackExpoRouter?: boolean;
28
30
  /** Enable automatic gesture detection (default: true) */
29
31
  autoGestureTracking?: boolean;
30
32
  /** Enable privacy occlusion for text inputs (default: true) */
@@ -90,6 +92,11 @@ export interface RejourneyConfig {
90
92
  * Disable if you want minimal network tracking overhead.
91
93
  */
92
94
  networkCaptureSizes?: boolean;
95
+ /**
96
+ * Automatically intercept console.log/info/warn/error and include them in session recordings.
97
+ * Useful for debugging sessions. Capped at 1,000 logs per session. (default: true)
98
+ */
99
+ trackConsoleLogs?: boolean;
93
100
  }
94
101
  export type GestureType = 'tap' | 'double_tap' | 'long_press' | 'force_touch' | 'swipe_left' | 'swipe_right' | 'swipe_up' | 'swipe_down' | 'pinch' | 'pinch_in' | 'pinch_out' | 'pan_up' | 'pan_down' | 'pan_left' | 'pan_right' | 'rotate_cw' | 'rotate_ccw' | 'scroll' | 'scroll_up' | 'scroll_down' | 'two_finger_tap' | 'three_finger_gesture' | 'multi_touch' | 'keyboard_tap' | 'rage_tap';
95
102
  export type EventType = 'gesture' | 'screen_change' | 'custom' | 'app_state' | 'app_lifecycle' | 'keyboard_show' | 'keyboard_hide' | 'keyboard_typing' | 'oauth_started' | 'oauth_completed' | 'oauth_returned' | 'external_url_opened' | 'session_start' | 'session_timeout' | 'frustration' | 'error';
@@ -428,7 +435,17 @@ export interface RejourneyNativeModule {
428
435
  export interface RejourneyAPI {
429
436
  /** SDK version */
430
437
  readonly version: string;
431
- /** Internal method to start recording session (called by startRejourney) */
438
+ /**
439
+ * Initialize Rejourney SDK
440
+ * @param publicRouteKey - Your public route key from the Rejourney dashboard
441
+ * @param options - Optional configuration options
442
+ */
443
+ init(publicRouteKey: string, options?: Omit<RejourneyConfig, 'publicRouteKey'>): void;
444
+ /** Start recording (call after user consent) */
445
+ start(): void;
446
+ /** Stop recording */
447
+ stop(): void;
448
+ /** Internal method to start recording session (called by start() / startRejourney()) */
432
449
  _startSession(): Promise<boolean>;
433
450
  /** Internal method to stop recording session (called by stopRejourney) */
434
451
  _stopSession(): Promise<void>;
@@ -438,8 +455,18 @@ export interface RejourneyAPI {
438
455
  setUserIdentity(userId: string): void;
439
456
  /** Clear user identity */
440
457
  clearUserIdentity(): void;
441
- /** Tag current screen */
442
- tagScreen(screenName: string, params?: Record<string, unknown>): void;
458
+ /**
459
+ * Set custom session metadata.
460
+ * Can be called with a single key-value pair or an object of properties.
461
+ * Useful for filtering sessions later (e.g., plan: 'premium', role: 'admin').
462
+ * Caps at 100 properties per session.
463
+ *
464
+ * @param keyOrProperties Property name string, or an object containing key-value pairs
465
+ * @param value Property value (if first argument is a string)
466
+ */
467
+ setMetadata(keyOrProperties: string | Record<string, string | number | boolean>, value?: string | number | boolean): void;
468
+ /** Track current screen (manual) */
469
+ trackScreen(screenName: string, params?: Record<string, unknown>): void;
443
470
  /** Mark a view as sensitive (will be occluded in recording) */
444
471
  setOccluded(viewRef: {
445
472
  current: any;
@@ -492,15 +519,22 @@ export interface RejourneyAPI {
492
519
  used: number;
493
520
  max: number;
494
521
  }>;
522
+ /**
523
+ * Log customer feedback (e.g. from an in-app survey or NPS widget).
524
+ *
525
+ * @param rating - Numeric rating (e.g. 1 to 5)
526
+ * @param message - Associated feedback text or comment
527
+ */
528
+ logFeedback(rating: number, message: string): void;
495
529
  /**
496
530
  * Get SDK telemetry metrics for observability
531
+
497
532
  * Returns metrics about SDK health including upload success rates,
498
533
  * retry attempts, circuit breaker events, and memory pressure.
499
534
  */
500
535
  getSDKMetrics(): Promise<SDKMetrics>;
501
536
  /**
502
- * Trigger a debug ANR (Dev only)
503
- * Blocks the main thread for the specified duration
537
+ * Trigger an ANR test by blocking the main thread for the specified duration.
504
538
  */
505
539
  debugTriggerANR(durationMs: number): void;
506
540
  /**
@@ -528,6 +562,21 @@ export interface RejourneyAPI {
528
562
  * @param nativeID - The nativeID prop of the view to unmask
529
563
  */
530
564
  unmaskView(nativeID: string): void;
565
+ /**
566
+ * Hook for automatic React Navigation tracking.
567
+ * Pass the returned object to your NavigationContainer props.
568
+ *
569
+ * @example
570
+ * ```tsx
571
+ * const navigationTracking = Rejourney.useNavigationTracking();
572
+ * <NavigationContainer {...navigationTracking}>
573
+ * ```
574
+ */
575
+ useNavigationTracking(): {
576
+ ref: any;
577
+ onReady: () => void;
578
+ onStateChange: (state: any) => void;
579
+ };
531
580
  }
532
581
  /**
533
582
  * SDK telemetry metrics for observability
@@ -595,6 +644,8 @@ export interface UseRejourneyResult {
595
644
  stopRecording: () => Promise<void>;
596
645
  /** Log custom event */
597
646
  logEvent: (name: string, properties?: Record<string, unknown>) => void;
647
+ /** Set custom session metadata */
648
+ setMetadata: (keyOrProperties: string | Record<string, string | number | boolean>, value?: string | number | boolean) => void;
598
649
  /** Error if any */
599
650
  error: Error | null;
600
651
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rejourneyco/react-native",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "Rejourney Session Recording SDK for React Native",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -75,7 +75,7 @@
75
75
  "@types/react-native": "*",
76
76
  "@typescript-eslint/eslint-plugin": "^8.15.0",
77
77
  "@typescript-eslint/parser": "^8.15.0",
78
- "@vitest/coverage-v8": "^2.1.0",
78
+ "@vitest/coverage-v8": "^4.0.18",
79
79
  "dependency-cruiser": "^16.10.4",
80
80
  "@react-navigation/native": "*",
81
81
  "expo-router": "*",
@@ -84,7 +84,7 @@
84
84
  "react-native": "*",
85
85
  "react-native-builder-bob": "^0.23.0",
86
86
  "typescript": "^5.0.0",
87
- "vitest": "^3.2.4"
87
+ "vitest": "^4.0.18"
88
88
  },
89
89
  "peerDependencies": {
90
90
  "react": "*",
@@ -92,6 +92,26 @@
92
92
  "@react-navigation/native": ">=6.0.0",
93
93
  "expo-router": ">=3.0.0"
94
94
  },
95
+ "peerDependenciesMeta": {
96
+ "expo-router": {
97
+ "optional": true
98
+ }
99
+ },
100
+ "exports": {
101
+ ".": {
102
+ "types": "./lib/typescript/index.d.ts",
103
+ "react-native": "./lib/module/index.js",
104
+ "import": "./lib/module/index.js",
105
+ "require": "./lib/commonjs/index.js"
106
+ },
107
+ "./expo-router": {
108
+ "types": "./lib/typescript/expoRouterTracking.d.ts",
109
+ "react-native": "./lib/module/expoRouterTracking.js",
110
+ "import": "./lib/module/expoRouterTracking.js",
111
+ "require": "./lib/commonjs/expoRouterTracking.js",
112
+ "default": "./lib/module/expoRouterTracking.js"
113
+ }
114
+ },
95
115
  "codegenConfig": {
96
116
  "name": "RejourneySpec",
97
117
  "type": "modules",
@@ -128,8 +128,7 @@ export interface Spec extends TurboModule {
128
128
  debugCrash(): void;
129
129
 
130
130
  /**
131
- * Trigger a debug ANR (Dev only)
132
- * Blocks the main thread for the specified duration
131
+ * Trigger an ANR test by blocking the main thread for the specified duration.
133
132
  */
134
133
  debugTriggerANR(durationMs: number): void;
135
134
 
@@ -152,11 +151,15 @@ export interface Spec extends TurboModule {
152
151
 
153
152
  getUserIdentity(): Promise<string | null>;
154
153
 
154
+ setAnonymousId(anonymousId: string): Promise<{ success: boolean }>;
155
+
156
+ getAnonymousId(): Promise<string | null>;
157
+
155
158
  setDebugMode(enabled: boolean): Promise<{ success: boolean }>;
156
159
 
157
- /**
158
- * Set SDK version from JS (called during init with version from package.json)
159
- */
160
+ /**
161
+ * Set SDK version from JS (called during init with version from package.json)
162
+ */
160
163
  setSDKVersion(version: string): void;
161
164
 
162
165
  /**
@@ -0,0 +1,167 @@
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
+
17
+ const MAX_POLLING_ERRORS = 10;
18
+
19
+ function extractScreenNameFromRouterState(
20
+ state: any,
21
+ getScreenNameFromPathFn: (path: string, segments: string[]) => string,
22
+ normalizeScreenNameFn: (name: string) => string,
23
+ accumulatedSegments: string[] = []
24
+ ): string | null {
25
+ if (!state?.routes) return null;
26
+
27
+ const route = state.routes[state.index ?? state.routes.length - 1];
28
+ if (!route) return null;
29
+
30
+ const newSegments = [...accumulatedSegments, route.name];
31
+
32
+ if (route.state) {
33
+ return extractScreenNameFromRouterState(
34
+ route.state,
35
+ getScreenNameFromPathFn,
36
+ normalizeScreenNameFn,
37
+ newSegments
38
+ );
39
+ }
40
+
41
+ const cleanSegments = newSegments.filter((s) => !s.startsWith('(') && !s.endsWith(')'));
42
+
43
+ if (cleanSegments.length === 0) {
44
+ for (let i = newSegments.length - 1; i >= 0; i--) {
45
+ const seg = newSegments[i];
46
+ if (seg && !seg.startsWith('(') && !seg.endsWith(')')) {
47
+ cleanSegments.push(seg);
48
+ break;
49
+ }
50
+ }
51
+ }
52
+
53
+ const pathname = '/' + cleanSegments.join('/');
54
+ return getScreenNameFromPathFn(pathname, newSegments);
55
+ }
56
+
57
+ function setupExpoRouterPolling(): void {
58
+ let lastDetectedScreen = '';
59
+ let pollingErrors = 0;
60
+
61
+ try {
62
+ const EXPO_ROUTER = 'expo-router';
63
+ const expoRouter = require(EXPO_ROUTER);
64
+ const router = expoRouter.router;
65
+
66
+ if (!router) {
67
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
68
+ console.debug('[Rejourney] Expo Router: router object not found');
69
+ }
70
+ return;
71
+ }
72
+
73
+ const intervalId = setInterval(() => {
74
+ try {
75
+ let state: any = null;
76
+ if (typeof router.getState === 'function') {
77
+ state = router.getState();
78
+ } else if ((router as any).rootState) {
79
+ state = (router as any).rootState;
80
+ }
81
+
82
+ if (!state) {
83
+ try {
84
+ const STORE_PATH = 'expo-router/build/global-state/router-store';
85
+ const storeModule = require(STORE_PATH);
86
+ if (storeModule?.store) {
87
+ state = storeModule.store.state;
88
+ if (!state && storeModule.store.navigationRef?.current) {
89
+ state = storeModule.store.navigationRef.current.getRootState?.();
90
+ }
91
+ if (!state) {
92
+ state = storeModule.store.rootState || storeModule.store.initialState;
93
+ }
94
+ }
95
+ } catch {
96
+ // Ignore
97
+ }
98
+ }
99
+
100
+ if (!state) {
101
+ try {
102
+ const IMPERATIVE_PATH = 'expo-router/build/imperative-api';
103
+ const imperative = require(IMPERATIVE_PATH);
104
+ if (imperative?.router) {
105
+ state = imperative.router.getState?.();
106
+ }
107
+ } catch {
108
+ // Ignore
109
+ }
110
+ }
111
+
112
+ if (state) {
113
+ pollingErrors = 0;
114
+ const screenName = extractScreenNameFromRouterState(
115
+ state,
116
+ getScreenNameFromPath,
117
+ normalizeScreenName
118
+ );
119
+ if (screenName && screenName !== lastDetectedScreen) {
120
+ lastDetectedScreen = screenName;
121
+ trackScreen(screenName);
122
+ }
123
+ } else {
124
+ pollingErrors++;
125
+ if (pollingErrors >= MAX_POLLING_ERRORS) {
126
+ clearInterval(intervalId);
127
+ setExpoRouterPollingInterval(null);
128
+ }
129
+ }
130
+ } catch {
131
+ pollingErrors++;
132
+ if (pollingErrors >= MAX_POLLING_ERRORS) {
133
+ clearInterval(intervalId);
134
+ setExpoRouterPollingInterval(null);
135
+ }
136
+ }
137
+ }, 500);
138
+
139
+ setExpoRouterPollingInterval(intervalId);
140
+ } catch (e) {
141
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
142
+ console.debug('[Rejourney] Expo Router not available:', e);
143
+ }
144
+ }
145
+ }
146
+
147
+ let attempts = 0;
148
+ const maxAttempts = 5;
149
+
150
+ function trySetup(): void {
151
+ attempts++;
152
+ try {
153
+ const EXPO_ROUTER = 'expo-router';
154
+ const expoRouter = require(EXPO_ROUTER);
155
+ if (expoRouter?.router && isExpoRouterTrackingEnabled()) {
156
+ setupExpoRouterPolling();
157
+ return;
158
+ }
159
+ } catch {
160
+ // Not ready or not installed
161
+ }
162
+ if (attempts < maxAttempts) {
163
+ setTimeout(trySetup, 200 * attempts);
164
+ }
165
+ }
166
+
167
+ setTimeout(trySetup, 200);