@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
|
@@ -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;
|
|
@@ -130,6 +132,7 @@ let originalOnError = null;
|
|
|
130
132
|
let originalOnUnhandledRejection = null;
|
|
131
133
|
let originalConsoleError = null;
|
|
132
134
|
let _promiseRejectionTrackingDisable = null;
|
|
135
|
+
const FATAL_ERROR_FLUSH_DELAY_MS = 1200;
|
|
133
136
|
|
|
134
137
|
/**
|
|
135
138
|
* Initialize auto tracking features
|
|
@@ -144,7 +147,9 @@ function initAutoTracking(trackingConfig, callbacks = {}) {
|
|
|
144
147
|
trackJSErrors: true,
|
|
145
148
|
trackPromiseRejections: true,
|
|
146
149
|
trackReactNativeErrors: true,
|
|
150
|
+
trackConsoleLogs: true,
|
|
147
151
|
collectDeviceInfo: true,
|
|
152
|
+
autoTrackExpoRouter: true,
|
|
148
153
|
maxSessionDurationMs: trackingConfig.maxSessionDurationMs,
|
|
149
154
|
...trackingConfig
|
|
150
155
|
};
|
|
@@ -154,6 +159,9 @@ function initAutoTracking(trackingConfig, callbacks = {}) {
|
|
|
154
159
|
onErrorCaptured = callbacks.onError || null;
|
|
155
160
|
onScreenChange = callbacks.onScreen || null;
|
|
156
161
|
setupErrorTracking();
|
|
162
|
+
if (config.trackConsoleLogs) {
|
|
163
|
+
setupConsoleTracking();
|
|
164
|
+
}
|
|
157
165
|
setupNavigationTracking();
|
|
158
166
|
loadAnonymousId().then(id => {
|
|
159
167
|
anonymousId = id;
|
|
@@ -167,11 +175,13 @@ function initAutoTracking(trackingConfig, callbacks = {}) {
|
|
|
167
175
|
function cleanupAutoTracking() {
|
|
168
176
|
if (!isInitialized) return;
|
|
169
177
|
restoreErrorHandlers();
|
|
178
|
+
restoreConsoleHandlers();
|
|
170
179
|
cleanupNavigationTracking();
|
|
171
180
|
|
|
172
181
|
// Reset state
|
|
173
182
|
tapHead = 0;
|
|
174
183
|
tapCount = 0;
|
|
184
|
+
consoleLogCount = 0;
|
|
175
185
|
metrics = createEmptyMetrics();
|
|
176
186
|
screensVisited = [];
|
|
177
187
|
currentScreen = '';
|
|
@@ -285,7 +295,7 @@ function setupErrorTracking() {
|
|
|
285
295
|
/**
|
|
286
296
|
* Setup React Native ErrorUtils handler
|
|
287
297
|
*
|
|
288
|
-
* CRITICAL FIX: For fatal errors, we delay calling the original handler
|
|
298
|
+
* CRITICAL FIX: For fatal errors, we delay calling the original handler briefly
|
|
289
299
|
* to give the React Native bridge time to flush the logEvent('error') call to the
|
|
290
300
|
* native TelemetryPipeline. Without this delay, the error event is queued on the
|
|
291
301
|
* JS→native bridge but the app crashes (via originalErrorHandler) before the bridge
|
|
@@ -309,10 +319,10 @@ function setupReactNativeErrorHandler() {
|
|
|
309
319
|
if (isFatal) {
|
|
310
320
|
// For fatal errors, delay the original handler so the native bridge
|
|
311
321
|
// has time to deliver the error event to TelemetryPipeline before
|
|
312
|
-
// the app terminates.
|
|
322
|
+
// the app terminates.
|
|
313
323
|
setTimeout(() => {
|
|
314
324
|
originalErrorHandler(error, isFatal);
|
|
315
|
-
},
|
|
325
|
+
}, FATAL_ERROR_FLUSH_DELAY_MS);
|
|
316
326
|
} else {
|
|
317
327
|
originalErrorHandler(error, isFatal);
|
|
318
328
|
}
|
|
@@ -367,7 +377,6 @@ function setupPromiseRejectionHandler() {
|
|
|
367
377
|
|
|
368
378
|
// Strategy 1: RN-specific promise rejection tracking polyfill
|
|
369
379
|
try {
|
|
370
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
371
380
|
const tracking = require('promise/setimmediate/rejection-tracking');
|
|
372
381
|
if (tracking && typeof tracking.enable === 'function') {
|
|
373
382
|
tracking.enable({
|
|
@@ -476,8 +485,27 @@ function restoreErrorHandlers() {
|
|
|
476
485
|
function trackError(error) {
|
|
477
486
|
metrics.errorCount++;
|
|
478
487
|
metrics.totalEvents++;
|
|
488
|
+
forwardErrorToNative(error);
|
|
479
489
|
if (onErrorCaptured) {
|
|
480
|
-
|
|
490
|
+
try {
|
|
491
|
+
onErrorCaptured(error);
|
|
492
|
+
} catch {
|
|
493
|
+
// Ignore callback exceptions so SDK error forwarding keeps working.
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function forwardErrorToNative(error) {
|
|
498
|
+
try {
|
|
499
|
+
const nativeModule = getRejourneyNativeModule();
|
|
500
|
+
if (!nativeModule || typeof nativeModule.logEvent !== 'function') return;
|
|
501
|
+
nativeModule.logEvent('error', {
|
|
502
|
+
message: error.message,
|
|
503
|
+
stack: error.stack,
|
|
504
|
+
name: error.name || 'Error',
|
|
505
|
+
timestamp: error.timestamp
|
|
506
|
+
}).catch(() => {});
|
|
507
|
+
} catch {
|
|
508
|
+
// Ignore native forwarding failures; SDK should never crash app code.
|
|
481
509
|
}
|
|
482
510
|
}
|
|
483
511
|
|
|
@@ -493,11 +521,104 @@ function captureError(message, stack, name) {
|
|
|
493
521
|
name: name || 'Error'
|
|
494
522
|
});
|
|
495
523
|
}
|
|
524
|
+
let originalConsoleLog = null;
|
|
525
|
+
let originalConsoleInfo = null;
|
|
526
|
+
let originalConsoleWarn = null;
|
|
527
|
+
|
|
528
|
+
// Cap console logs to prevent flooding the event pipeline
|
|
529
|
+
const MAX_CONSOLE_LOGS_PER_SESSION = 1000;
|
|
530
|
+
let consoleLogCount = 0;
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Setup console tracking to capture log statements
|
|
534
|
+
*/
|
|
535
|
+
function setupConsoleTracking() {
|
|
536
|
+
if (typeof console === 'undefined') return;
|
|
537
|
+
if (!originalConsoleLog) originalConsoleLog = console.log;
|
|
538
|
+
if (!originalConsoleInfo) originalConsoleInfo = console.info;
|
|
539
|
+
if (!originalConsoleWarn) originalConsoleWarn = console.warn;
|
|
540
|
+
const createConsoleInterceptor = (level, originalFn) => {
|
|
541
|
+
return (...args) => {
|
|
542
|
+
try {
|
|
543
|
+
const message = args.map(arg => {
|
|
544
|
+
if (typeof arg === 'string') return arg;
|
|
545
|
+
if (arg instanceof Error) return `${arg.name}: ${arg.message}${arg.stack ? `\n...` : ''}`;
|
|
546
|
+
try {
|
|
547
|
+
return JSON.stringify(arg);
|
|
548
|
+
} catch {
|
|
549
|
+
return String(arg);
|
|
550
|
+
}
|
|
551
|
+
}).join(' ');
|
|
552
|
+
|
|
553
|
+
// Enforce per-session cap and skip React Native unhandled-rejection noise.
|
|
554
|
+
if (consoleLogCount < MAX_CONSOLE_LOGS_PER_SESSION && !message.includes('Possible Unhandled Promise Rejection')) {
|
|
555
|
+
consoleLogCount++;
|
|
556
|
+
const nativeModule = getRejourneyNativeModule();
|
|
557
|
+
if (nativeModule) {
|
|
558
|
+
const logEvent = {
|
|
559
|
+
type: 'log',
|
|
560
|
+
timestamp: Date.now(),
|
|
561
|
+
level,
|
|
562
|
+
message: message.length > 2000 ? message.substring(0, 2000) + '...' : message
|
|
563
|
+
};
|
|
564
|
+
nativeModule.logEvent('log', logEvent).catch(() => {});
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
} catch {
|
|
568
|
+
// Ignore any errors during interception
|
|
569
|
+
}
|
|
570
|
+
if (originalFn) {
|
|
571
|
+
originalFn.apply(console, args);
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
};
|
|
575
|
+
console.log = createConsoleInterceptor('log', originalConsoleLog);
|
|
576
|
+
console.info = createConsoleInterceptor('info', originalConsoleInfo);
|
|
577
|
+
console.warn = createConsoleInterceptor('warn', originalConsoleWarn);
|
|
578
|
+
const currentConsoleError = console.error;
|
|
579
|
+
if (!originalConsoleError) originalConsoleError = currentConsoleError;
|
|
580
|
+
console.error = createConsoleInterceptor('error', currentConsoleError);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Restore console standard functions
|
|
585
|
+
*/
|
|
586
|
+
function restoreConsoleHandlers() {
|
|
587
|
+
if (originalConsoleLog) {
|
|
588
|
+
console.log = originalConsoleLog;
|
|
589
|
+
originalConsoleLog = null;
|
|
590
|
+
}
|
|
591
|
+
if (originalConsoleInfo) {
|
|
592
|
+
console.info = originalConsoleInfo;
|
|
593
|
+
originalConsoleInfo = null;
|
|
594
|
+
}
|
|
595
|
+
if (originalConsoleWarn) {
|
|
596
|
+
console.warn = originalConsoleWarn;
|
|
597
|
+
originalConsoleWarn = null;
|
|
598
|
+
}
|
|
599
|
+
// Note: console.error is restored in restoreErrorHandlers via originalConsoleError
|
|
600
|
+
}
|
|
496
601
|
let navigationPollingInterval = null;
|
|
602
|
+
/** Interval ID from optional expo-router entry; cleared in cleanupNavigationTracking */
|
|
603
|
+
let expoRouterPollingIntervalId = null;
|
|
497
604
|
let lastDetectedScreen = '';
|
|
498
605
|
let navigationSetupDone = false;
|
|
499
|
-
|
|
500
|
-
|
|
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
|
+
}
|
|
501
622
|
|
|
502
623
|
/**
|
|
503
624
|
* Track a navigation state change from React Navigation.
|
|
@@ -594,91 +715,94 @@ function useNavigationTracking() {
|
|
|
594
715
|
}
|
|
595
716
|
|
|
596
717
|
/**
|
|
597
|
-
* Setup automatic
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
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.
|
|
601
727
|
*/
|
|
602
728
|
function setupNavigationTracking() {
|
|
603
729
|
if (navigationSetupDone) return;
|
|
604
730
|
navigationSetupDone = true;
|
|
605
|
-
|
|
606
|
-
|
|
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();
|
|
607
737
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
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;
|
|
624
759
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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.');
|
|
630
768
|
}
|
|
631
769
|
}
|
|
632
|
-
};
|
|
633
|
-
setTimeout(trySetup, 200);
|
|
770
|
+
}, delay);
|
|
634
771
|
}
|
|
635
772
|
|
|
636
773
|
/**
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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;
|
|
642
782
|
try {
|
|
643
|
-
const expoRouter = require('expo-router');
|
|
644
|
-
const router = expoRouter.router;
|
|
645
|
-
if (!router) {
|
|
646
|
-
if (__DEV__) {
|
|
647
|
-
_utils.logger.debug('Expo Router: router object not found');
|
|
648
|
-
}
|
|
649
|
-
return false;
|
|
650
|
-
}
|
|
651
|
-
if (__DEV__) {
|
|
652
|
-
_utils.logger.debug('Expo Router: Setting up navigation tracking');
|
|
653
|
-
}
|
|
654
783
|
const {
|
|
655
784
|
normalizeScreenName,
|
|
656
785
|
getScreenNameFromPath
|
|
657
786
|
} = require('./navigation');
|
|
658
|
-
|
|
787
|
+
const intervalId = setInterval(() => {
|
|
659
788
|
try {
|
|
660
789
|
let state = null;
|
|
661
|
-
let stateSource = '';
|
|
662
790
|
if (typeof router.getState === 'function') {
|
|
663
791
|
state = router.getState();
|
|
664
|
-
stateSource = 'router.getState()';
|
|
665
792
|
} else if (router.rootState) {
|
|
666
793
|
state = router.rootState;
|
|
667
|
-
stateSource = 'router.rootState';
|
|
668
794
|
}
|
|
669
795
|
if (!state) {
|
|
670
796
|
try {
|
|
671
|
-
const
|
|
797
|
+
const STORE_PATH = 'expo-router/build/global-state/router-store';
|
|
798
|
+
const storeModule = require(STORE_PATH);
|
|
672
799
|
if (storeModule?.store) {
|
|
673
800
|
state = storeModule.store.state;
|
|
674
|
-
if (state) stateSource = 'store.state';
|
|
675
801
|
if (!state && storeModule.store.navigationRef?.current) {
|
|
676
802
|
state = storeModule.store.navigationRef.current.getRootState?.();
|
|
677
|
-
if (state) stateSource = 'navigationRef.getRootState()';
|
|
678
803
|
}
|
|
679
804
|
if (!state) {
|
|
680
805
|
state = storeModule.store.rootState || storeModule.store.initialState;
|
|
681
|
-
if (state) stateSource = 'store.rootState/initialState';
|
|
682
806
|
}
|
|
683
807
|
}
|
|
684
808
|
} catch {
|
|
@@ -687,67 +811,53 @@ function trySetupExpoRouter() {
|
|
|
687
811
|
}
|
|
688
812
|
if (!state) {
|
|
689
813
|
try {
|
|
690
|
-
const
|
|
814
|
+
const IMPERATIVE_PATH = 'expo-router/build/imperative-api';
|
|
815
|
+
const imperative = require(IMPERATIVE_PATH);
|
|
691
816
|
if (imperative?.router) {
|
|
692
817
|
state = imperative.router.getState?.();
|
|
693
|
-
if (state) stateSource = 'imperative-api';
|
|
694
818
|
}
|
|
695
819
|
} catch {
|
|
696
820
|
// Ignore
|
|
697
821
|
}
|
|
698
822
|
}
|
|
699
823
|
if (state) {
|
|
700
|
-
|
|
701
|
-
navigationPollingErrors = 0;
|
|
824
|
+
pollingErrors = 0;
|
|
702
825
|
const screenName = extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName);
|
|
703
826
|
if (screenName && screenName !== lastDetectedScreen) {
|
|
704
|
-
if (__DEV__) {
|
|
705
|
-
_utils.logger.debug('Screen changed:', lastDetectedScreen, '->', screenName, `(source: ${stateSource})`);
|
|
706
|
-
}
|
|
707
827
|
lastDetectedScreen = screenName;
|
|
708
828
|
trackScreen(screenName);
|
|
709
829
|
}
|
|
710
830
|
} else {
|
|
711
|
-
|
|
712
|
-
if (
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
|
|
716
|
-
cleanupNavigationTracking();
|
|
831
|
+
pollingErrors++;
|
|
832
|
+
if (pollingErrors >= MAX_POLLING_ERRORS) {
|
|
833
|
+
clearInterval(intervalId);
|
|
834
|
+
expoRouterPollingIntervalId = null;
|
|
717
835
|
}
|
|
718
836
|
}
|
|
719
|
-
} catch
|
|
720
|
-
|
|
721
|
-
if (
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
|
|
725
|
-
cleanupNavigationTracking();
|
|
837
|
+
} catch {
|
|
838
|
+
pollingErrors++;
|
|
839
|
+
if (pollingErrors >= MAX_POLLING_ERRORS) {
|
|
840
|
+
clearInterval(intervalId);
|
|
841
|
+
expoRouterPollingIntervalId = null;
|
|
726
842
|
}
|
|
727
843
|
}
|
|
728
844
|
}, 500);
|
|
729
|
-
|
|
730
|
-
} catch
|
|
731
|
-
|
|
732
|
-
_utils.logger.debug('Expo Router not available:', e);
|
|
733
|
-
}
|
|
734
|
-
return false;
|
|
845
|
+
expoRouterPollingIntervalId = intervalId;
|
|
846
|
+
} catch {
|
|
847
|
+
// navigation module not available — ignore
|
|
735
848
|
}
|
|
736
849
|
}
|
|
737
850
|
|
|
738
851
|
/**
|
|
739
|
-
* Extract screen name from
|
|
740
|
-
*
|
|
741
|
-
* Handles complex nested structures like Drawer → Tabs → Stack
|
|
742
|
-
* by recursively accumulating segments from each navigation level.
|
|
852
|
+
* Extract the active screen name from expo-router navigation state.
|
|
743
853
|
*/
|
|
744
|
-
function extractScreenNameFromRouterState(state,
|
|
854
|
+
function extractScreenNameFromRouterState(state, getScreenNameFromPathFn, normalizeScreenNameFn, accumulatedSegments = []) {
|
|
745
855
|
if (!state?.routes) return null;
|
|
746
856
|
const route = state.routes[state.index ?? state.routes.length - 1];
|
|
747
857
|
if (!route) return null;
|
|
748
858
|
const newSegments = [...accumulatedSegments, route.name];
|
|
749
859
|
if (route.state) {
|
|
750
|
-
return extractScreenNameFromRouterState(route.state,
|
|
860
|
+
return extractScreenNameFromRouterState(route.state, getScreenNameFromPathFn, normalizeScreenNameFn, newSegments);
|
|
751
861
|
}
|
|
752
862
|
const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
|
|
753
863
|
if (cleanSegments.length === 0) {
|
|
@@ -760,7 +870,7 @@ function extractScreenNameFromRouterState(state, getScreenNameFromPath, normaliz
|
|
|
760
870
|
}
|
|
761
871
|
}
|
|
762
872
|
const pathname = '/' + cleanSegments.join('/');
|
|
763
|
-
return
|
|
873
|
+
return getScreenNameFromPathFn(pathname, newSegments);
|
|
764
874
|
}
|
|
765
875
|
|
|
766
876
|
/**
|
|
@@ -771,9 +881,12 @@ function cleanupNavigationTracking() {
|
|
|
771
881
|
clearInterval(navigationPollingInterval);
|
|
772
882
|
navigationPollingInterval = null;
|
|
773
883
|
}
|
|
884
|
+
if (expoRouterPollingIntervalId != null) {
|
|
885
|
+
clearInterval(expoRouterPollingIntervalId);
|
|
886
|
+
expoRouterPollingIntervalId = null;
|
|
887
|
+
}
|
|
774
888
|
navigationSetupDone = false;
|
|
775
889
|
lastDetectedScreen = '';
|
|
776
|
-
navigationPollingErrors = 0;
|
|
777
890
|
}
|
|
778
891
|
|
|
779
892
|
/**
|
|
@@ -1016,7 +1129,26 @@ async function collectDeviceInfo() {
|
|
|
1016
1129
|
function generateAnonymousId() {
|
|
1017
1130
|
const timestamp = Date.now().toString(36);
|
|
1018
1131
|
const random = Math.random().toString(36).substring(2, 15);
|
|
1019
|
-
|
|
1132
|
+
const id = `anon_${timestamp}_${random}`;
|
|
1133
|
+
// Persist so the same ID survives app restarts
|
|
1134
|
+
_persistAnonymousId(id);
|
|
1135
|
+
return id;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Best-effort async persist of anonymous ID to native storage
|
|
1140
|
+
*/
|
|
1141
|
+
function _persistAnonymousId(id) {
|
|
1142
|
+
const nativeModule = getRejourneyNativeModule();
|
|
1143
|
+
if (!nativeModule?.setAnonymousId) return;
|
|
1144
|
+
try {
|
|
1145
|
+
const result = nativeModule.setAnonymousId(id);
|
|
1146
|
+
if (result && typeof result.catch === 'function') {
|
|
1147
|
+
result.catch(() => {});
|
|
1148
|
+
}
|
|
1149
|
+
} catch {
|
|
1150
|
+
// Native storage unavailable — ID will still be stable for this session
|
|
1151
|
+
}
|
|
1020
1152
|
}
|
|
1021
1153
|
|
|
1022
1154
|
/**
|
|
@@ -1047,17 +1179,41 @@ async function ensurePersistentAnonymousId() {
|
|
|
1047
1179
|
|
|
1048
1180
|
/**
|
|
1049
1181
|
* Load anonymous ID from persistent storage
|
|
1050
|
-
*
|
|
1182
|
+
* Checks native anonymous storage first, then falls back to native getUserIdentity,
|
|
1183
|
+
* and finally generates a new ID if nothing is persisted.
|
|
1051
1184
|
*/
|
|
1052
1185
|
async function loadAnonymousId() {
|
|
1053
1186
|
const nativeModule = getRejourneyNativeModule();
|
|
1054
|
-
|
|
1187
|
+
|
|
1188
|
+
// 1. Try native anonymous ID storage
|
|
1189
|
+
if (nativeModule?.getAnonymousId) {
|
|
1190
|
+
try {
|
|
1191
|
+
const stored = await nativeModule.getAnonymousId();
|
|
1192
|
+
if (stored && typeof stored === 'string') return stored;
|
|
1193
|
+
} catch {
|
|
1194
|
+
// Continue to fallbacks
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// 2. Backward compatibility fallback for older native modules
|
|
1199
|
+
if (nativeModule?.getUserIdentity) {
|
|
1055
1200
|
try {
|
|
1056
|
-
|
|
1201
|
+
const nativeId = await nativeModule.getUserIdentity();
|
|
1202
|
+
if (nativeId && typeof nativeId === 'string') {
|
|
1203
|
+
const normalized = nativeId.trim();
|
|
1204
|
+
// Only migrate legacy anonymous identifiers. Never treat explicit user identities
|
|
1205
|
+
// as anonymous fingerprints, or session correlation becomes unstable.
|
|
1206
|
+
if (normalized.startsWith('anon_')) {
|
|
1207
|
+
_persistAnonymousId(normalized);
|
|
1208
|
+
return normalized;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1057
1211
|
} catch {
|
|
1058
|
-
|
|
1212
|
+
// Continue to fallback
|
|
1059
1213
|
}
|
|
1060
1214
|
}
|
|
1215
|
+
|
|
1216
|
+
// 3. Generate and persist new ID
|
|
1061
1217
|
return generateAnonymousId();
|
|
1062
1218
|
}
|
|
1063
1219
|
|
|
@@ -1065,7 +1221,13 @@ async function loadAnonymousId() {
|
|
|
1065
1221
|
* Set a custom anonymous ID
|
|
1066
1222
|
*/
|
|
1067
1223
|
function setAnonymousId(id) {
|
|
1068
|
-
|
|
1224
|
+
const normalized = (id || '').trim();
|
|
1225
|
+
if (!normalized) {
|
|
1226
|
+
anonymousId = generateAnonymousId();
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
anonymousId = normalized;
|
|
1230
|
+
_persistAnonymousId(normalized);
|
|
1069
1231
|
}
|
|
1070
1232
|
var _default = exports.default = {
|
|
1071
1233
|
init: initAutoTracking,
|