@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.
- package/README.md +77 -3
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +143 -8
- package/android/src/main/java/com/rejourney/RejourneyOkHttpInitProvider.kt +68 -0
- package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +21 -3
- package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +69 -17
- package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +27 -2
- package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +3 -1
- package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +93 -0
- package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +226 -146
- package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +7 -0
- package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +3 -0
- package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +39 -0
- package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +8 -0
- package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +95 -21
- package/android/src/main/java/com/rejourney/utility/DataCompression.kt +14 -2
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +14 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +18 -0
- package/ios/Engine/DeviceRegistrar.swift +13 -3
- package/ios/Engine/RejourneyImpl.swift +204 -115
- package/ios/Recording/AnrSentinel.swift +58 -25
- package/ios/Recording/InteractionRecorder.swift +1 -0
- package/ios/Recording/RejourneyURLProtocol.swift +216 -0
- package/ios/Recording/ReplayOrchestrator.swift +207 -144
- package/ios/Recording/SegmentDispatcher.swift +8 -0
- package/ios/Recording/StabilityMonitor.swift +40 -32
- package/ios/Recording/TelemetryPipeline.swift +45 -2
- package/ios/Recording/ViewHierarchyScanner.swift +1 -0
- package/ios/Recording/VisualCapture.swift +79 -29
- package/ios/Rejourney.mm +27 -8
- package/ios/Utility/DataCompression.swift +2 -2
- package/ios/Utility/ImageBlur.swift +0 -1
- package/lib/commonjs/expoRouterTracking.js +137 -0
- package/lib/commonjs/index.js +204 -34
- package/lib/commonjs/sdk/autoTracking.js +262 -100
- package/lib/commonjs/sdk/networkInterceptor.js +84 -4
- package/lib/module/expoRouterTracking.js +135 -0
- package/lib/module/index.js +203 -28
- package/lib/module/sdk/autoTracking.js +260 -100
- package/lib/module/sdk/networkInterceptor.js +84 -4
- package/lib/typescript/NativeRejourney.d.ts +5 -2
- package/lib/typescript/expoRouterTracking.d.ts +14 -0
- package/lib/typescript/index.d.ts +2 -2
- package/lib/typescript/sdk/autoTracking.d.ts +14 -1
- package/lib/typescript/types/index.d.ts +56 -5
- package/package.json +23 -3
- package/src/NativeRejourney.ts +8 -5
- package/src/expoRouterTracking.ts +167 -0
- package/src/index.ts +221 -35
- package/src/sdk/autoTracking.ts +286 -114
- package/src/sdk/networkInterceptor.ts +110 -1
- package/src/types/index.ts +58 -6
package/src/sdk/autoTracking.ts
CHANGED
|
@@ -142,9 +142,11 @@ export interface AutoTrackingConfig {
|
|
|
142
142
|
trackJSErrors?: boolean;
|
|
143
143
|
trackPromiseRejections?: boolean;
|
|
144
144
|
trackReactNativeErrors?: boolean;
|
|
145
|
+
trackConsoleLogs?: boolean;
|
|
145
146
|
collectDeviceInfo?: boolean;
|
|
146
147
|
maxSessionDurationMs?: number;
|
|
147
148
|
detectDeadTaps?: boolean;
|
|
149
|
+
autoTrackExpoRouter?: boolean;
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
let isInitialized = false;
|
|
@@ -183,6 +185,7 @@ let originalOnError: OnErrorEventHandler | null = null;
|
|
|
183
185
|
let originalOnUnhandledRejection: ((event: PromiseRejectionEvent) => void) | null = null;
|
|
184
186
|
let originalConsoleError: ((...args: any[]) => void) | null = null;
|
|
185
187
|
let _promiseRejectionTrackingDisable: (() => void) | null = null;
|
|
188
|
+
const FATAL_ERROR_FLUSH_DELAY_MS = 1200;
|
|
186
189
|
|
|
187
190
|
/**
|
|
188
191
|
* Initialize auto tracking features
|
|
@@ -205,7 +208,9 @@ export function initAutoTracking(
|
|
|
205
208
|
trackJSErrors: true,
|
|
206
209
|
trackPromiseRejections: true,
|
|
207
210
|
trackReactNativeErrors: true,
|
|
211
|
+
trackConsoleLogs: true,
|
|
208
212
|
collectDeviceInfo: true,
|
|
213
|
+
autoTrackExpoRouter: true,
|
|
209
214
|
maxSessionDurationMs: trackingConfig.maxSessionDurationMs,
|
|
210
215
|
...trackingConfig,
|
|
211
216
|
};
|
|
@@ -221,6 +226,9 @@ export function initAutoTracking(
|
|
|
221
226
|
onErrorCaptured = callbacks.onError || null;
|
|
222
227
|
onScreenChange = callbacks.onScreen || null;
|
|
223
228
|
setupErrorTracking();
|
|
229
|
+
if (config.trackConsoleLogs) {
|
|
230
|
+
setupConsoleTracking();
|
|
231
|
+
}
|
|
224
232
|
setupNavigationTracking();
|
|
225
233
|
loadAnonymousId().then(id => {
|
|
226
234
|
anonymousId = id;
|
|
@@ -236,11 +244,13 @@ export function cleanupAutoTracking(): void {
|
|
|
236
244
|
if (!isInitialized) return;
|
|
237
245
|
|
|
238
246
|
restoreErrorHandlers();
|
|
247
|
+
restoreConsoleHandlers();
|
|
239
248
|
cleanupNavigationTracking();
|
|
240
249
|
|
|
241
250
|
// Reset state
|
|
242
251
|
tapHead = 0;
|
|
243
252
|
tapCount = 0;
|
|
253
|
+
consoleLogCount = 0;
|
|
244
254
|
metrics = createEmptyMetrics();
|
|
245
255
|
screensVisited = [];
|
|
246
256
|
currentScreen = '';
|
|
@@ -357,7 +367,7 @@ function setupErrorTracking(): void {
|
|
|
357
367
|
/**
|
|
358
368
|
* Setup React Native ErrorUtils handler
|
|
359
369
|
*
|
|
360
|
-
* CRITICAL FIX: For fatal errors, we delay calling the original handler
|
|
370
|
+
* CRITICAL FIX: For fatal errors, we delay calling the original handler briefly
|
|
361
371
|
* to give the React Native bridge time to flush the logEvent('error') call to the
|
|
362
372
|
* native TelemetryPipeline. Without this delay, the error event is queued on the
|
|
363
373
|
* JS→native bridge but the app crashes (via originalErrorHandler) before the bridge
|
|
@@ -384,10 +394,10 @@ function setupReactNativeErrorHandler(): void {
|
|
|
384
394
|
if (isFatal) {
|
|
385
395
|
// For fatal errors, delay the original handler so the native bridge
|
|
386
396
|
// has time to deliver the error event to TelemetryPipeline before
|
|
387
|
-
// the app terminates.
|
|
397
|
+
// the app terminates.
|
|
388
398
|
setTimeout(() => {
|
|
389
399
|
originalErrorHandler!(error, isFatal);
|
|
390
|
-
},
|
|
400
|
+
}, FATAL_ERROR_FLUSH_DELAY_MS);
|
|
391
401
|
} else {
|
|
392
402
|
originalErrorHandler(error, isFatal);
|
|
393
403
|
}
|
|
@@ -450,7 +460,6 @@ function setupPromiseRejectionHandler(): void {
|
|
|
450
460
|
|
|
451
461
|
// Strategy 1: RN-specific promise rejection tracking polyfill
|
|
452
462
|
try {
|
|
453
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
454
463
|
const tracking = require('promise/setimmediate/rejection-tracking');
|
|
455
464
|
if (tracking && typeof tracking.enable === 'function') {
|
|
456
465
|
tracking.enable({
|
|
@@ -565,8 +574,30 @@ function trackError(error: ErrorEvent): void {
|
|
|
565
574
|
metrics.errorCount++;
|
|
566
575
|
metrics.totalEvents++;
|
|
567
576
|
|
|
577
|
+
forwardErrorToNative(error);
|
|
578
|
+
|
|
568
579
|
if (onErrorCaptured) {
|
|
569
|
-
|
|
580
|
+
try {
|
|
581
|
+
onErrorCaptured(error);
|
|
582
|
+
} catch {
|
|
583
|
+
// Ignore callback exceptions so SDK error forwarding keeps working.
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function forwardErrorToNative(error: ErrorEvent): void {
|
|
589
|
+
try {
|
|
590
|
+
const nativeModule = getRejourneyNativeModule();
|
|
591
|
+
if (!nativeModule || typeof nativeModule.logEvent !== 'function') return;
|
|
592
|
+
|
|
593
|
+
nativeModule.logEvent('error', {
|
|
594
|
+
message: error.message,
|
|
595
|
+
stack: error.stack,
|
|
596
|
+
name: error.name || 'Error',
|
|
597
|
+
timestamp: error.timestamp,
|
|
598
|
+
}).catch(() => { });
|
|
599
|
+
} catch {
|
|
600
|
+
// Ignore native forwarding failures; SDK should never crash app code.
|
|
570
601
|
}
|
|
571
602
|
}
|
|
572
603
|
|
|
@@ -587,11 +618,113 @@ export function captureError(
|
|
|
587
618
|
});
|
|
588
619
|
}
|
|
589
620
|
|
|
621
|
+
let originalConsoleLog: ((...args: any[]) => void) | null = null;
|
|
622
|
+
let originalConsoleInfo: ((...args: any[]) => void) | null = null;
|
|
623
|
+
let originalConsoleWarn: ((...args: any[]) => void) | null = null;
|
|
624
|
+
|
|
625
|
+
// Cap console logs to prevent flooding the event pipeline
|
|
626
|
+
const MAX_CONSOLE_LOGS_PER_SESSION = 1000;
|
|
627
|
+
let consoleLogCount = 0;
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Setup console tracking to capture log statements
|
|
631
|
+
*/
|
|
632
|
+
function setupConsoleTracking(): void {
|
|
633
|
+
if (typeof console === 'undefined') return;
|
|
634
|
+
|
|
635
|
+
if (!originalConsoleLog) originalConsoleLog = console.log;
|
|
636
|
+
if (!originalConsoleInfo) originalConsoleInfo = console.info;
|
|
637
|
+
if (!originalConsoleWarn) originalConsoleWarn = console.warn;
|
|
638
|
+
|
|
639
|
+
const createConsoleInterceptor = (level: 'log' | 'info' | 'warn' | 'error', originalFn: (...args: any[]) => void) => {
|
|
640
|
+
return (...args: any[]) => {
|
|
641
|
+
try {
|
|
642
|
+
const message = args.map(arg => {
|
|
643
|
+
if (typeof arg === 'string') return arg;
|
|
644
|
+
if (arg instanceof Error) return `${arg.name}: ${arg.message}${arg.stack ? `\n...` : ''}`;
|
|
645
|
+
try {
|
|
646
|
+
return JSON.stringify(arg);
|
|
647
|
+
} catch {
|
|
648
|
+
return String(arg);
|
|
649
|
+
}
|
|
650
|
+
}).join(' ');
|
|
651
|
+
|
|
652
|
+
// Enforce per-session cap and skip React Native unhandled-rejection noise.
|
|
653
|
+
if (
|
|
654
|
+
consoleLogCount < MAX_CONSOLE_LOGS_PER_SESSION &&
|
|
655
|
+
!message.includes('Possible Unhandled Promise Rejection')
|
|
656
|
+
) {
|
|
657
|
+
consoleLogCount++;
|
|
658
|
+
const nativeModule = getRejourneyNativeModule();
|
|
659
|
+
if (nativeModule) {
|
|
660
|
+
const logEvent = {
|
|
661
|
+
type: 'log',
|
|
662
|
+
timestamp: Date.now(),
|
|
663
|
+
level,
|
|
664
|
+
message: message.length > 2000 ? message.substring(0, 2000) + '...' : message,
|
|
665
|
+
};
|
|
666
|
+
nativeModule.logEvent('log', logEvent).catch(() => { });
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
} catch {
|
|
670
|
+
// Ignore any errors during interception
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (originalFn) {
|
|
674
|
+
originalFn.apply(console, args);
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
console.log = createConsoleInterceptor('log', originalConsoleLog!);
|
|
680
|
+
console.info = createConsoleInterceptor('info', originalConsoleInfo!);
|
|
681
|
+
console.warn = createConsoleInterceptor('warn', originalConsoleWarn!);
|
|
682
|
+
|
|
683
|
+
const currentConsoleError = console.error;
|
|
684
|
+
if (!originalConsoleError) originalConsoleError = currentConsoleError;
|
|
685
|
+
console.error = createConsoleInterceptor('error', currentConsoleError);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Restore console standard functions
|
|
690
|
+
*/
|
|
691
|
+
function restoreConsoleHandlers(): void {
|
|
692
|
+
if (originalConsoleLog) {
|
|
693
|
+
console.log = originalConsoleLog;
|
|
694
|
+
originalConsoleLog = null;
|
|
695
|
+
}
|
|
696
|
+
if (originalConsoleInfo) {
|
|
697
|
+
console.info = originalConsoleInfo;
|
|
698
|
+
originalConsoleInfo = null;
|
|
699
|
+
}
|
|
700
|
+
if (originalConsoleWarn) {
|
|
701
|
+
console.warn = originalConsoleWarn;
|
|
702
|
+
originalConsoleWarn = null;
|
|
703
|
+
}
|
|
704
|
+
// Note: console.error is restored in restoreErrorHandlers via originalConsoleError
|
|
705
|
+
}
|
|
706
|
+
|
|
590
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;
|
|
591
710
|
let lastDetectedScreen = '';
|
|
592
711
|
let navigationSetupDone = false;
|
|
593
|
-
|
|
594
|
-
|
|
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
|
+
}
|
|
595
728
|
|
|
596
729
|
/**
|
|
597
730
|
* Track a navigation state change from React Navigation.
|
|
@@ -688,101 +821,97 @@ export function useNavigationTracking() {
|
|
|
688
821
|
}
|
|
689
822
|
|
|
690
823
|
/**
|
|
691
|
-
* Setup automatic
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
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.
|
|
695
833
|
*/
|
|
696
834
|
function setupNavigationTracking(): void {
|
|
697
835
|
if (navigationSetupDone) return;
|
|
698
836
|
navigationSetupDone = true;
|
|
699
837
|
|
|
700
|
-
if
|
|
701
|
-
|
|
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();
|
|
702
843
|
}
|
|
844
|
+
}
|
|
703
845
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
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
|
|
714
853
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
if (
|
|
722
|
-
|
|
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;
|
|
723
866
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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.');
|
|
729
875
|
}
|
|
730
876
|
}
|
|
731
|
-
};
|
|
732
|
-
|
|
733
|
-
setTimeout(trySetup, 200);
|
|
877
|
+
}, delay);
|
|
734
878
|
}
|
|
735
879
|
|
|
736
880
|
/**
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const expoRouter = require('expo-router');
|
|
744
|
-
const router = expoRouter.router;
|
|
745
|
-
|
|
746
|
-
if (!router) {
|
|
747
|
-
if (__DEV__) {
|
|
748
|
-
logger.debug('Expo Router: router object not found');
|
|
749
|
-
}
|
|
750
|
-
return false;
|
|
751
|
-
}
|
|
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;
|
|
752
887
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
}
|
|
888
|
+
const MAX_POLLING_ERRORS = 10;
|
|
889
|
+
let pollingErrors = 0;
|
|
756
890
|
|
|
891
|
+
try {
|
|
757
892
|
const { normalizeScreenName, getScreenNameFromPath } = require('./navigation');
|
|
758
893
|
|
|
759
|
-
|
|
894
|
+
const intervalId = setInterval(() => {
|
|
760
895
|
try {
|
|
761
|
-
let state = null;
|
|
762
|
-
|
|
896
|
+
let state: any = null;
|
|
897
|
+
|
|
763
898
|
if (typeof router.getState === 'function') {
|
|
764
899
|
state = router.getState();
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
state = (router as any).rootState;
|
|
768
|
-
stateSource = 'router.rootState';
|
|
900
|
+
} else if (router.rootState) {
|
|
901
|
+
state = router.rootState;
|
|
769
902
|
}
|
|
770
903
|
|
|
771
904
|
if (!state) {
|
|
772
905
|
try {
|
|
773
|
-
const
|
|
906
|
+
const STORE_PATH = 'expo-router/build/global-state/router-store';
|
|
907
|
+
const storeModule = require(STORE_PATH);
|
|
774
908
|
if (storeModule?.store) {
|
|
775
909
|
state = storeModule.store.state;
|
|
776
|
-
if (state) stateSource = 'store.state';
|
|
777
|
-
|
|
778
910
|
if (!state && storeModule.store.navigationRef?.current) {
|
|
779
911
|
state = storeModule.store.navigationRef.current.getRootState?.();
|
|
780
|
-
if (state) stateSource = 'navigationRef.getRootState()';
|
|
781
912
|
}
|
|
782
|
-
|
|
783
913
|
if (!state) {
|
|
784
914
|
state = storeModule.store.rootState || storeModule.store.initialState;
|
|
785
|
-
if (state) stateSource = 'store.rootState/initialState';
|
|
786
915
|
}
|
|
787
916
|
}
|
|
788
917
|
} catch {
|
|
@@ -792,10 +921,10 @@ function trySetupExpoRouter(): boolean {
|
|
|
792
921
|
|
|
793
922
|
if (!state) {
|
|
794
923
|
try {
|
|
795
|
-
const
|
|
924
|
+
const IMPERATIVE_PATH = 'expo-router/build/imperative-api';
|
|
925
|
+
const imperative = require(IMPERATIVE_PATH);
|
|
796
926
|
if (imperative?.router) {
|
|
797
927
|
state = imperative.router.getState?.();
|
|
798
|
-
if (state) stateSource = 'imperative-api';
|
|
799
928
|
}
|
|
800
929
|
} catch {
|
|
801
930
|
// Ignore
|
|
@@ -803,55 +932,45 @@ function trySetupExpoRouter(): boolean {
|
|
|
803
932
|
}
|
|
804
933
|
|
|
805
934
|
if (state) {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
935
|
+
pollingErrors = 0;
|
|
936
|
+
const screenName = extractScreenNameFromRouterState(
|
|
937
|
+
state,
|
|
938
|
+
getScreenNameFromPath,
|
|
939
|
+
normalizeScreenName
|
|
940
|
+
);
|
|
809
941
|
if (screenName && screenName !== lastDetectedScreen) {
|
|
810
|
-
if (__DEV__) {
|
|
811
|
-
logger.debug('Screen changed:', lastDetectedScreen, '->', screenName, `(source: ${stateSource})`);
|
|
812
|
-
}
|
|
813
942
|
lastDetectedScreen = screenName;
|
|
814
943
|
trackScreen(screenName);
|
|
815
944
|
}
|
|
816
945
|
} else {
|
|
817
|
-
|
|
818
|
-
if (
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
|
|
822
|
-
cleanupNavigationTracking();
|
|
946
|
+
pollingErrors++;
|
|
947
|
+
if (pollingErrors >= MAX_POLLING_ERRORS) {
|
|
948
|
+
clearInterval(intervalId);
|
|
949
|
+
expoRouterPollingIntervalId = null;
|
|
823
950
|
}
|
|
824
951
|
}
|
|
825
|
-
} catch
|
|
826
|
-
|
|
827
|
-
if (
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
|
|
831
|
-
cleanupNavigationTracking();
|
|
952
|
+
} catch {
|
|
953
|
+
pollingErrors++;
|
|
954
|
+
if (pollingErrors >= MAX_POLLING_ERRORS) {
|
|
955
|
+
clearInterval(intervalId);
|
|
956
|
+
expoRouterPollingIntervalId = null;
|
|
832
957
|
}
|
|
833
958
|
}
|
|
834
959
|
}, 500);
|
|
835
960
|
|
|
836
|
-
|
|
837
|
-
} catch
|
|
838
|
-
|
|
839
|
-
logger.debug('Expo Router not available:', e);
|
|
840
|
-
}
|
|
841
|
-
return false;
|
|
961
|
+
expoRouterPollingIntervalId = intervalId;
|
|
962
|
+
} catch {
|
|
963
|
+
// navigation module not available — ignore
|
|
842
964
|
}
|
|
843
965
|
}
|
|
844
966
|
|
|
845
967
|
/**
|
|
846
|
-
* Extract screen name from
|
|
847
|
-
*
|
|
848
|
-
* Handles complex nested structures like Drawer → Tabs → Stack
|
|
849
|
-
* by recursively accumulating segments from each navigation level.
|
|
968
|
+
* Extract the active screen name from expo-router navigation state.
|
|
850
969
|
*/
|
|
851
970
|
function extractScreenNameFromRouterState(
|
|
852
971
|
state: any,
|
|
853
|
-
|
|
854
|
-
|
|
972
|
+
getScreenNameFromPathFn: (path: string, segments: string[]) => string,
|
|
973
|
+
normalizeScreenNameFn: (name: string) => string,
|
|
855
974
|
accumulatedSegments: string[] = []
|
|
856
975
|
): string | null {
|
|
857
976
|
if (!state?.routes) return null;
|
|
@@ -864,13 +983,13 @@ function extractScreenNameFromRouterState(
|
|
|
864
983
|
if (route.state) {
|
|
865
984
|
return extractScreenNameFromRouterState(
|
|
866
985
|
route.state,
|
|
867
|
-
|
|
868
|
-
|
|
986
|
+
getScreenNameFromPathFn,
|
|
987
|
+
normalizeScreenNameFn,
|
|
869
988
|
newSegments
|
|
870
989
|
);
|
|
871
990
|
}
|
|
872
991
|
|
|
873
|
-
const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
|
|
992
|
+
const cleanSegments = newSegments.filter((s) => !s.startsWith('(') && !s.endsWith(')'));
|
|
874
993
|
|
|
875
994
|
if (cleanSegments.length === 0) {
|
|
876
995
|
for (let i = newSegments.length - 1; i >= 0; i--) {
|
|
@@ -883,7 +1002,7 @@ function extractScreenNameFromRouterState(
|
|
|
883
1002
|
}
|
|
884
1003
|
|
|
885
1004
|
const pathname = '/' + cleanSegments.join('/');
|
|
886
|
-
return
|
|
1005
|
+
return getScreenNameFromPathFn(pathname, newSegments);
|
|
887
1006
|
}
|
|
888
1007
|
|
|
889
1008
|
/**
|
|
@@ -894,9 +1013,12 @@ function cleanupNavigationTracking(): void {
|
|
|
894
1013
|
clearInterval(navigationPollingInterval);
|
|
895
1014
|
navigationPollingInterval = null;
|
|
896
1015
|
}
|
|
1016
|
+
if (expoRouterPollingIntervalId != null) {
|
|
1017
|
+
clearInterval(expoRouterPollingIntervalId);
|
|
1018
|
+
expoRouterPollingIntervalId = null;
|
|
1019
|
+
}
|
|
897
1020
|
navigationSetupDone = false;
|
|
898
1021
|
lastDetectedScreen = '';
|
|
899
|
-
navigationPollingErrors = 0;
|
|
900
1022
|
}
|
|
901
1023
|
|
|
902
1024
|
/**
|
|
@@ -1174,7 +1296,27 @@ export async function collectDeviceInfo(): Promise<DeviceInfo> {
|
|
|
1174
1296
|
function generateAnonymousId(): string {
|
|
1175
1297
|
const timestamp = Date.now().toString(36);
|
|
1176
1298
|
const random = Math.random().toString(36).substring(2, 15);
|
|
1177
|
-
|
|
1299
|
+
const id = `anon_${timestamp}_${random}`;
|
|
1300
|
+
// Persist so the same ID survives app restarts
|
|
1301
|
+
_persistAnonymousId(id);
|
|
1302
|
+
return id;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Best-effort async persist of anonymous ID to native storage
|
|
1307
|
+
*/
|
|
1308
|
+
function _persistAnonymousId(id: string): void {
|
|
1309
|
+
const nativeModule = getRejourneyNativeModule();
|
|
1310
|
+
if (!nativeModule?.setAnonymousId) return;
|
|
1311
|
+
|
|
1312
|
+
try {
|
|
1313
|
+
const result = nativeModule.setAnonymousId(id);
|
|
1314
|
+
if (result && typeof result.catch === 'function') {
|
|
1315
|
+
result.catch(() => { });
|
|
1316
|
+
}
|
|
1317
|
+
} catch {
|
|
1318
|
+
// Native storage unavailable — ID will still be stable for this session
|
|
1319
|
+
}
|
|
1178
1320
|
}
|
|
1179
1321
|
|
|
1180
1322
|
/**
|
|
@@ -1205,17 +1347,41 @@ export async function ensurePersistentAnonymousId(): Promise<string> {
|
|
|
1205
1347
|
|
|
1206
1348
|
/**
|
|
1207
1349
|
* Load anonymous ID from persistent storage
|
|
1208
|
-
*
|
|
1350
|
+
* Checks native anonymous storage first, then falls back to native getUserIdentity,
|
|
1351
|
+
* and finally generates a new ID if nothing is persisted.
|
|
1209
1352
|
*/
|
|
1210
1353
|
export async function loadAnonymousId(): Promise<string> {
|
|
1211
1354
|
const nativeModule = getRejourneyNativeModule();
|
|
1212
|
-
|
|
1355
|
+
|
|
1356
|
+
// 1. Try native anonymous ID storage
|
|
1357
|
+
if (nativeModule?.getAnonymousId) {
|
|
1358
|
+
try {
|
|
1359
|
+
const stored = await nativeModule.getAnonymousId();
|
|
1360
|
+
if (stored && typeof stored === 'string') return stored;
|
|
1361
|
+
} catch {
|
|
1362
|
+
// Continue to fallbacks
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// 2. Backward compatibility fallback for older native modules
|
|
1367
|
+
if (nativeModule?.getUserIdentity) {
|
|
1213
1368
|
try {
|
|
1214
|
-
|
|
1369
|
+
const nativeId = await nativeModule.getUserIdentity();
|
|
1370
|
+
if (nativeId && typeof nativeId === 'string') {
|
|
1371
|
+
const normalized = nativeId.trim();
|
|
1372
|
+
// Only migrate legacy anonymous identifiers. Never treat explicit user identities
|
|
1373
|
+
// as anonymous fingerprints, or session correlation becomes unstable.
|
|
1374
|
+
if (normalized.startsWith('anon_')) {
|
|
1375
|
+
_persistAnonymousId(normalized);
|
|
1376
|
+
return normalized;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1215
1379
|
} catch {
|
|
1216
|
-
|
|
1380
|
+
// Continue to fallback
|
|
1217
1381
|
}
|
|
1218
1382
|
}
|
|
1383
|
+
|
|
1384
|
+
// 3. Generate and persist new ID
|
|
1219
1385
|
return generateAnonymousId();
|
|
1220
1386
|
}
|
|
1221
1387
|
|
|
@@ -1223,7 +1389,13 @@ export async function loadAnonymousId(): Promise<string> {
|
|
|
1223
1389
|
* Set a custom anonymous ID
|
|
1224
1390
|
*/
|
|
1225
1391
|
export function setAnonymousId(id: string): void {
|
|
1226
|
-
|
|
1392
|
+
const normalized = (id || '').trim();
|
|
1393
|
+
if (!normalized) {
|
|
1394
|
+
anonymousId = generateAnonymousId();
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
anonymousId = normalized;
|
|
1398
|
+
_persistAnonymousId(normalized);
|
|
1227
1399
|
}
|
|
1228
1400
|
|
|
1229
1401
|
export default {
|