@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
|
@@ -102,6 +102,7 @@ let originalOnError = null;
|
|
|
102
102
|
let originalOnUnhandledRejection = null;
|
|
103
103
|
let originalConsoleError = null;
|
|
104
104
|
let _promiseRejectionTrackingDisable = null;
|
|
105
|
+
const FATAL_ERROR_FLUSH_DELAY_MS = 1200;
|
|
105
106
|
|
|
106
107
|
/**
|
|
107
108
|
* Initialize auto tracking features
|
|
@@ -116,7 +117,9 @@ export function initAutoTracking(trackingConfig, callbacks = {}) {
|
|
|
116
117
|
trackJSErrors: true,
|
|
117
118
|
trackPromiseRejections: true,
|
|
118
119
|
trackReactNativeErrors: true,
|
|
120
|
+
trackConsoleLogs: true,
|
|
119
121
|
collectDeviceInfo: true,
|
|
122
|
+
autoTrackExpoRouter: true,
|
|
120
123
|
maxSessionDurationMs: trackingConfig.maxSessionDurationMs,
|
|
121
124
|
...trackingConfig
|
|
122
125
|
};
|
|
@@ -126,6 +129,9 @@ export function initAutoTracking(trackingConfig, callbacks = {}) {
|
|
|
126
129
|
onErrorCaptured = callbacks.onError || null;
|
|
127
130
|
onScreenChange = callbacks.onScreen || null;
|
|
128
131
|
setupErrorTracking();
|
|
132
|
+
if (config.trackConsoleLogs) {
|
|
133
|
+
setupConsoleTracking();
|
|
134
|
+
}
|
|
129
135
|
setupNavigationTracking();
|
|
130
136
|
loadAnonymousId().then(id => {
|
|
131
137
|
anonymousId = id;
|
|
@@ -139,11 +145,13 @@ export function initAutoTracking(trackingConfig, callbacks = {}) {
|
|
|
139
145
|
export function cleanupAutoTracking() {
|
|
140
146
|
if (!isInitialized) return;
|
|
141
147
|
restoreErrorHandlers();
|
|
148
|
+
restoreConsoleHandlers();
|
|
142
149
|
cleanupNavigationTracking();
|
|
143
150
|
|
|
144
151
|
// Reset state
|
|
145
152
|
tapHead = 0;
|
|
146
153
|
tapCount = 0;
|
|
154
|
+
consoleLogCount = 0;
|
|
147
155
|
metrics = createEmptyMetrics();
|
|
148
156
|
screensVisited = [];
|
|
149
157
|
currentScreen = '';
|
|
@@ -257,7 +265,7 @@ function setupErrorTracking() {
|
|
|
257
265
|
/**
|
|
258
266
|
* Setup React Native ErrorUtils handler
|
|
259
267
|
*
|
|
260
|
-
* CRITICAL FIX: For fatal errors, we delay calling the original handler
|
|
268
|
+
* CRITICAL FIX: For fatal errors, we delay calling the original handler briefly
|
|
261
269
|
* to give the React Native bridge time to flush the logEvent('error') call to the
|
|
262
270
|
* native TelemetryPipeline. Without this delay, the error event is queued on the
|
|
263
271
|
* JS→native bridge but the app crashes (via originalErrorHandler) before the bridge
|
|
@@ -281,10 +289,10 @@ function setupReactNativeErrorHandler() {
|
|
|
281
289
|
if (isFatal) {
|
|
282
290
|
// For fatal errors, delay the original handler so the native bridge
|
|
283
291
|
// has time to deliver the error event to TelemetryPipeline before
|
|
284
|
-
// the app terminates.
|
|
292
|
+
// the app terminates.
|
|
285
293
|
setTimeout(() => {
|
|
286
294
|
originalErrorHandler(error, isFatal);
|
|
287
|
-
},
|
|
295
|
+
}, FATAL_ERROR_FLUSH_DELAY_MS);
|
|
288
296
|
} else {
|
|
289
297
|
originalErrorHandler(error, isFatal);
|
|
290
298
|
}
|
|
@@ -339,7 +347,6 @@ function setupPromiseRejectionHandler() {
|
|
|
339
347
|
|
|
340
348
|
// Strategy 1: RN-specific promise rejection tracking polyfill
|
|
341
349
|
try {
|
|
342
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
343
350
|
const tracking = require('promise/setimmediate/rejection-tracking');
|
|
344
351
|
if (tracking && typeof tracking.enable === 'function') {
|
|
345
352
|
tracking.enable({
|
|
@@ -448,8 +455,27 @@ function restoreErrorHandlers() {
|
|
|
448
455
|
function trackError(error) {
|
|
449
456
|
metrics.errorCount++;
|
|
450
457
|
metrics.totalEvents++;
|
|
458
|
+
forwardErrorToNative(error);
|
|
451
459
|
if (onErrorCaptured) {
|
|
452
|
-
|
|
460
|
+
try {
|
|
461
|
+
onErrorCaptured(error);
|
|
462
|
+
} catch {
|
|
463
|
+
// Ignore callback exceptions so SDK error forwarding keeps working.
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function forwardErrorToNative(error) {
|
|
468
|
+
try {
|
|
469
|
+
const nativeModule = getRejourneyNativeModule();
|
|
470
|
+
if (!nativeModule || typeof nativeModule.logEvent !== 'function') return;
|
|
471
|
+
nativeModule.logEvent('error', {
|
|
472
|
+
message: error.message,
|
|
473
|
+
stack: error.stack,
|
|
474
|
+
name: error.name || 'Error',
|
|
475
|
+
timestamp: error.timestamp
|
|
476
|
+
}).catch(() => {});
|
|
477
|
+
} catch {
|
|
478
|
+
// Ignore native forwarding failures; SDK should never crash app code.
|
|
453
479
|
}
|
|
454
480
|
}
|
|
455
481
|
|
|
@@ -465,11 +491,104 @@ export function captureError(message, stack, name) {
|
|
|
465
491
|
name: name || 'Error'
|
|
466
492
|
});
|
|
467
493
|
}
|
|
494
|
+
let originalConsoleLog = null;
|
|
495
|
+
let originalConsoleInfo = null;
|
|
496
|
+
let originalConsoleWarn = null;
|
|
497
|
+
|
|
498
|
+
// Cap console logs to prevent flooding the event pipeline
|
|
499
|
+
const MAX_CONSOLE_LOGS_PER_SESSION = 1000;
|
|
500
|
+
let consoleLogCount = 0;
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Setup console tracking to capture log statements
|
|
504
|
+
*/
|
|
505
|
+
function setupConsoleTracking() {
|
|
506
|
+
if (typeof console === 'undefined') return;
|
|
507
|
+
if (!originalConsoleLog) originalConsoleLog = console.log;
|
|
508
|
+
if (!originalConsoleInfo) originalConsoleInfo = console.info;
|
|
509
|
+
if (!originalConsoleWarn) originalConsoleWarn = console.warn;
|
|
510
|
+
const createConsoleInterceptor = (level, originalFn) => {
|
|
511
|
+
return (...args) => {
|
|
512
|
+
try {
|
|
513
|
+
const message = args.map(arg => {
|
|
514
|
+
if (typeof arg === 'string') return arg;
|
|
515
|
+
if (arg instanceof Error) return `${arg.name}: ${arg.message}${arg.stack ? `\n...` : ''}`;
|
|
516
|
+
try {
|
|
517
|
+
return JSON.stringify(arg);
|
|
518
|
+
} catch {
|
|
519
|
+
return String(arg);
|
|
520
|
+
}
|
|
521
|
+
}).join(' ');
|
|
522
|
+
|
|
523
|
+
// Enforce per-session cap and skip React Native unhandled-rejection noise.
|
|
524
|
+
if (consoleLogCount < MAX_CONSOLE_LOGS_PER_SESSION && !message.includes('Possible Unhandled Promise Rejection')) {
|
|
525
|
+
consoleLogCount++;
|
|
526
|
+
const nativeModule = getRejourneyNativeModule();
|
|
527
|
+
if (nativeModule) {
|
|
528
|
+
const logEvent = {
|
|
529
|
+
type: 'log',
|
|
530
|
+
timestamp: Date.now(),
|
|
531
|
+
level,
|
|
532
|
+
message: message.length > 2000 ? message.substring(0, 2000) + '...' : message
|
|
533
|
+
};
|
|
534
|
+
nativeModule.logEvent('log', logEvent).catch(() => {});
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
} catch {
|
|
538
|
+
// Ignore any errors during interception
|
|
539
|
+
}
|
|
540
|
+
if (originalFn) {
|
|
541
|
+
originalFn.apply(console, args);
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
};
|
|
545
|
+
console.log = createConsoleInterceptor('log', originalConsoleLog);
|
|
546
|
+
console.info = createConsoleInterceptor('info', originalConsoleInfo);
|
|
547
|
+
console.warn = createConsoleInterceptor('warn', originalConsoleWarn);
|
|
548
|
+
const currentConsoleError = console.error;
|
|
549
|
+
if (!originalConsoleError) originalConsoleError = currentConsoleError;
|
|
550
|
+
console.error = createConsoleInterceptor('error', currentConsoleError);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Restore console standard functions
|
|
555
|
+
*/
|
|
556
|
+
function restoreConsoleHandlers() {
|
|
557
|
+
if (originalConsoleLog) {
|
|
558
|
+
console.log = originalConsoleLog;
|
|
559
|
+
originalConsoleLog = null;
|
|
560
|
+
}
|
|
561
|
+
if (originalConsoleInfo) {
|
|
562
|
+
console.info = originalConsoleInfo;
|
|
563
|
+
originalConsoleInfo = null;
|
|
564
|
+
}
|
|
565
|
+
if (originalConsoleWarn) {
|
|
566
|
+
console.warn = originalConsoleWarn;
|
|
567
|
+
originalConsoleWarn = null;
|
|
568
|
+
}
|
|
569
|
+
// Note: console.error is restored in restoreErrorHandlers via originalConsoleError
|
|
570
|
+
}
|
|
468
571
|
let navigationPollingInterval = null;
|
|
572
|
+
/** Interval ID from optional expo-router entry; cleared in cleanupNavigationTracking */
|
|
573
|
+
let expoRouterPollingIntervalId = null;
|
|
469
574
|
let lastDetectedScreen = '';
|
|
470
575
|
let navigationSetupDone = false;
|
|
471
|
-
|
|
472
|
-
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Register the polling interval from the optional expo-router entry so we can clear it on cleanup.
|
|
579
|
+
* Used by src/expoRouterTracking.ts (only loaded when app imports '@rejourneyco/react-native/expo-router').
|
|
580
|
+
*/
|
|
581
|
+
export function setExpoRouterPollingInterval(id) {
|
|
582
|
+
expoRouterPollingIntervalId = id;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Check if Expo Router auto-tracking is enabled in the current configuration.
|
|
587
|
+
* Used by src/expoRouterTracking.ts.
|
|
588
|
+
*/
|
|
589
|
+
export function isExpoRouterTrackingEnabled() {
|
|
590
|
+
return config.autoTrackExpoRouter !== false;
|
|
591
|
+
}
|
|
473
592
|
|
|
474
593
|
/**
|
|
475
594
|
* Track a navigation state change from React Navigation.
|
|
@@ -566,91 +685,94 @@ export function useNavigationTracking() {
|
|
|
566
685
|
}
|
|
567
686
|
|
|
568
687
|
/**
|
|
569
|
-
* Setup automatic
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
688
|
+
* Setup automatic navigation tracking.
|
|
689
|
+
*
|
|
690
|
+
* Expo Router: not set up here to avoid pulling expo-router into the main bundle
|
|
691
|
+
* (Metro resolves require() at build time, which causes "Requiring unknown module"
|
|
692
|
+
* in apps that use Expo + react-navigation without expo-router). If you use
|
|
693
|
+
* expo-router, add: import '@rejourneyco/react-native/expo-router';
|
|
694
|
+
*
|
|
695
|
+
* For React Navigation (non–expo-router), use trackNavigationState() on your
|
|
696
|
+
* NavigationContainer's onStateChange.
|
|
573
697
|
*/
|
|
574
698
|
function setupNavigationTracking() {
|
|
575
699
|
if (navigationSetupDone) return;
|
|
576
700
|
navigationSetupDone = true;
|
|
577
|
-
|
|
578
|
-
|
|
701
|
+
|
|
702
|
+
// Auto-detect expo-router and set up screen tracking if available.
|
|
703
|
+
// This is safe: if expo-router isn't installed, the require fails silently.
|
|
704
|
+
// We defer slightly so the router has time to initialize after JS bundle load.
|
|
705
|
+
if (config.autoTrackExpoRouter !== false) {
|
|
706
|
+
tryAutoSetupExpoRouter();
|
|
579
707
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Attempt to auto-detect and set up expo-router screen tracking.
|
|
712
|
+
* Uses a retry mechanism because the router may not be ready immediately
|
|
713
|
+
* after JS bundle load.
|
|
714
|
+
*/
|
|
715
|
+
function tryAutoSetupExpoRouter(attempt = 0, maxAttempts = 5) {
|
|
716
|
+
const delay = 200 * (attempt + 1); // 200, 400, 600, 800, 1000ms
|
|
717
|
+
|
|
718
|
+
setTimeout(() => {
|
|
719
|
+
try {
|
|
720
|
+
// Dynamic require wrapped in a variable to prevent Metro from statically resolving it
|
|
721
|
+
const EXPO_ROUTER = 'expo-router';
|
|
722
|
+
const expoRouter = require(EXPO_ROUTER);
|
|
723
|
+
if (!expoRouter?.router) {
|
|
724
|
+
// expo-router exists but router not ready yet — retry
|
|
725
|
+
if (attempt < maxAttempts - 1) {
|
|
726
|
+
tryAutoSetupExpoRouter(attempt + 1, maxAttempts);
|
|
727
|
+
}
|
|
728
|
+
return;
|
|
596
729
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
730
|
+
|
|
731
|
+
// Router is ready — set up the polling-based screen tracker
|
|
732
|
+
setupExpoRouterPolling(expoRouter.router);
|
|
733
|
+
} catch {
|
|
734
|
+
// expo-router not installed — this is fine, just means the app
|
|
735
|
+
// uses bare React Navigation or no navigation at all.
|
|
736
|
+
if (__DEV__ && attempt === 0) {
|
|
737
|
+
logger.debug('Expo Router not detected, skipping auto screen tracking. Use trackNavigationState() for React Navigation.');
|
|
602
738
|
}
|
|
603
739
|
}
|
|
604
|
-
};
|
|
605
|
-
setTimeout(trySetup, 200);
|
|
740
|
+
}, delay);
|
|
606
741
|
}
|
|
607
742
|
|
|
608
743
|
/**
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
744
|
+
* Poll expo-router state for screen changes.
|
|
745
|
+
* Inlined from expoRouterTracking.ts so no separate import is needed.
|
|
746
|
+
*/
|
|
747
|
+
function setupExpoRouterPolling(router) {
|
|
748
|
+
// Guard against double-setup (core auto-detection + legacy expoRouterTracking.ts import)
|
|
749
|
+
if (expoRouterPollingIntervalId != null) return;
|
|
750
|
+
const MAX_POLLING_ERRORS = 10;
|
|
751
|
+
let pollingErrors = 0;
|
|
614
752
|
try {
|
|
615
|
-
const expoRouter = require('expo-router');
|
|
616
|
-
const router = expoRouter.router;
|
|
617
|
-
if (!router) {
|
|
618
|
-
if (__DEV__) {
|
|
619
|
-
logger.debug('Expo Router: router object not found');
|
|
620
|
-
}
|
|
621
|
-
return false;
|
|
622
|
-
}
|
|
623
|
-
if (__DEV__) {
|
|
624
|
-
logger.debug('Expo Router: Setting up navigation tracking');
|
|
625
|
-
}
|
|
626
753
|
const {
|
|
627
754
|
normalizeScreenName,
|
|
628
755
|
getScreenNameFromPath
|
|
629
756
|
} = require('./navigation');
|
|
630
|
-
|
|
757
|
+
const intervalId = setInterval(() => {
|
|
631
758
|
try {
|
|
632
759
|
let state = null;
|
|
633
|
-
let stateSource = '';
|
|
634
760
|
if (typeof router.getState === 'function') {
|
|
635
761
|
state = router.getState();
|
|
636
|
-
stateSource = 'router.getState()';
|
|
637
762
|
} else if (router.rootState) {
|
|
638
763
|
state = router.rootState;
|
|
639
|
-
stateSource = 'router.rootState';
|
|
640
764
|
}
|
|
641
765
|
if (!state) {
|
|
642
766
|
try {
|
|
643
|
-
const
|
|
767
|
+
const STORE_PATH = 'expo-router/build/global-state/router-store';
|
|
768
|
+
const storeModule = require(STORE_PATH);
|
|
644
769
|
if (storeModule?.store) {
|
|
645
770
|
state = storeModule.store.state;
|
|
646
|
-
if (state) stateSource = 'store.state';
|
|
647
771
|
if (!state && storeModule.store.navigationRef?.current) {
|
|
648
772
|
state = storeModule.store.navigationRef.current.getRootState?.();
|
|
649
|
-
if (state) stateSource = 'navigationRef.getRootState()';
|
|
650
773
|
}
|
|
651
774
|
if (!state) {
|
|
652
775
|
state = storeModule.store.rootState || storeModule.store.initialState;
|
|
653
|
-
if (state) stateSource = 'store.rootState/initialState';
|
|
654
776
|
}
|
|
655
777
|
}
|
|
656
778
|
} catch {
|
|
@@ -659,67 +781,53 @@ function trySetupExpoRouter() {
|
|
|
659
781
|
}
|
|
660
782
|
if (!state) {
|
|
661
783
|
try {
|
|
662
|
-
const
|
|
784
|
+
const IMPERATIVE_PATH = 'expo-router/build/imperative-api';
|
|
785
|
+
const imperative = require(IMPERATIVE_PATH);
|
|
663
786
|
if (imperative?.router) {
|
|
664
787
|
state = imperative.router.getState?.();
|
|
665
|
-
if (state) stateSource = 'imperative-api';
|
|
666
788
|
}
|
|
667
789
|
} catch {
|
|
668
790
|
// Ignore
|
|
669
791
|
}
|
|
670
792
|
}
|
|
671
793
|
if (state) {
|
|
672
|
-
|
|
673
|
-
navigationPollingErrors = 0;
|
|
794
|
+
pollingErrors = 0;
|
|
674
795
|
const screenName = extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName);
|
|
675
796
|
if (screenName && screenName !== lastDetectedScreen) {
|
|
676
|
-
if (__DEV__) {
|
|
677
|
-
logger.debug('Screen changed:', lastDetectedScreen, '->', screenName, `(source: ${stateSource})`);
|
|
678
|
-
}
|
|
679
797
|
lastDetectedScreen = screenName;
|
|
680
798
|
trackScreen(screenName);
|
|
681
799
|
}
|
|
682
800
|
} else {
|
|
683
|
-
|
|
684
|
-
if (
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
|
|
688
|
-
cleanupNavigationTracking();
|
|
801
|
+
pollingErrors++;
|
|
802
|
+
if (pollingErrors >= MAX_POLLING_ERRORS) {
|
|
803
|
+
clearInterval(intervalId);
|
|
804
|
+
expoRouterPollingIntervalId = null;
|
|
689
805
|
}
|
|
690
806
|
}
|
|
691
|
-
} catch
|
|
692
|
-
|
|
693
|
-
if (
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
|
|
697
|
-
cleanupNavigationTracking();
|
|
807
|
+
} catch {
|
|
808
|
+
pollingErrors++;
|
|
809
|
+
if (pollingErrors >= MAX_POLLING_ERRORS) {
|
|
810
|
+
clearInterval(intervalId);
|
|
811
|
+
expoRouterPollingIntervalId = null;
|
|
698
812
|
}
|
|
699
813
|
}
|
|
700
814
|
}, 500);
|
|
701
|
-
|
|
702
|
-
} catch
|
|
703
|
-
|
|
704
|
-
logger.debug('Expo Router not available:', e);
|
|
705
|
-
}
|
|
706
|
-
return false;
|
|
815
|
+
expoRouterPollingIntervalId = intervalId;
|
|
816
|
+
} catch {
|
|
817
|
+
// navigation module not available — ignore
|
|
707
818
|
}
|
|
708
819
|
}
|
|
709
820
|
|
|
710
821
|
/**
|
|
711
|
-
* Extract screen name from
|
|
712
|
-
*
|
|
713
|
-
* Handles complex nested structures like Drawer → Tabs → Stack
|
|
714
|
-
* by recursively accumulating segments from each navigation level.
|
|
822
|
+
* Extract the active screen name from expo-router navigation state.
|
|
715
823
|
*/
|
|
716
|
-
function extractScreenNameFromRouterState(state,
|
|
824
|
+
function extractScreenNameFromRouterState(state, getScreenNameFromPathFn, normalizeScreenNameFn, accumulatedSegments = []) {
|
|
717
825
|
if (!state?.routes) return null;
|
|
718
826
|
const route = state.routes[state.index ?? state.routes.length - 1];
|
|
719
827
|
if (!route) return null;
|
|
720
828
|
const newSegments = [...accumulatedSegments, route.name];
|
|
721
829
|
if (route.state) {
|
|
722
|
-
return extractScreenNameFromRouterState(route.state,
|
|
830
|
+
return extractScreenNameFromRouterState(route.state, getScreenNameFromPathFn, normalizeScreenNameFn, newSegments);
|
|
723
831
|
}
|
|
724
832
|
const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
|
|
725
833
|
if (cleanSegments.length === 0) {
|
|
@@ -732,7 +840,7 @@ function extractScreenNameFromRouterState(state, getScreenNameFromPath, normaliz
|
|
|
732
840
|
}
|
|
733
841
|
}
|
|
734
842
|
const pathname = '/' + cleanSegments.join('/');
|
|
735
|
-
return
|
|
843
|
+
return getScreenNameFromPathFn(pathname, newSegments);
|
|
736
844
|
}
|
|
737
845
|
|
|
738
846
|
/**
|
|
@@ -743,9 +851,12 @@ function cleanupNavigationTracking() {
|
|
|
743
851
|
clearInterval(navigationPollingInterval);
|
|
744
852
|
navigationPollingInterval = null;
|
|
745
853
|
}
|
|
854
|
+
if (expoRouterPollingIntervalId != null) {
|
|
855
|
+
clearInterval(expoRouterPollingIntervalId);
|
|
856
|
+
expoRouterPollingIntervalId = null;
|
|
857
|
+
}
|
|
746
858
|
navigationSetupDone = false;
|
|
747
859
|
lastDetectedScreen = '';
|
|
748
|
-
navigationPollingErrors = 0;
|
|
749
860
|
}
|
|
750
861
|
|
|
751
862
|
/**
|
|
@@ -988,7 +1099,26 @@ export async function collectDeviceInfo() {
|
|
|
988
1099
|
function generateAnonymousId() {
|
|
989
1100
|
const timestamp = Date.now().toString(36);
|
|
990
1101
|
const random = Math.random().toString(36).substring(2, 15);
|
|
991
|
-
|
|
1102
|
+
const id = `anon_${timestamp}_${random}`;
|
|
1103
|
+
// Persist so the same ID survives app restarts
|
|
1104
|
+
_persistAnonymousId(id);
|
|
1105
|
+
return id;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Best-effort async persist of anonymous ID to native storage
|
|
1110
|
+
*/
|
|
1111
|
+
function _persistAnonymousId(id) {
|
|
1112
|
+
const nativeModule = getRejourneyNativeModule();
|
|
1113
|
+
if (!nativeModule?.setAnonymousId) return;
|
|
1114
|
+
try {
|
|
1115
|
+
const result = nativeModule.setAnonymousId(id);
|
|
1116
|
+
if (result && typeof result.catch === 'function') {
|
|
1117
|
+
result.catch(() => {});
|
|
1118
|
+
}
|
|
1119
|
+
} catch {
|
|
1120
|
+
// Native storage unavailable — ID will still be stable for this session
|
|
1121
|
+
}
|
|
992
1122
|
}
|
|
993
1123
|
|
|
994
1124
|
/**
|
|
@@ -1019,17 +1149,41 @@ export async function ensurePersistentAnonymousId() {
|
|
|
1019
1149
|
|
|
1020
1150
|
/**
|
|
1021
1151
|
* Load anonymous ID from persistent storage
|
|
1022
|
-
*
|
|
1152
|
+
* Checks native anonymous storage first, then falls back to native getUserIdentity,
|
|
1153
|
+
* and finally generates a new ID if nothing is persisted.
|
|
1023
1154
|
*/
|
|
1024
1155
|
export async function loadAnonymousId() {
|
|
1025
1156
|
const nativeModule = getRejourneyNativeModule();
|
|
1026
|
-
|
|
1157
|
+
|
|
1158
|
+
// 1. Try native anonymous ID storage
|
|
1159
|
+
if (nativeModule?.getAnonymousId) {
|
|
1160
|
+
try {
|
|
1161
|
+
const stored = await nativeModule.getAnonymousId();
|
|
1162
|
+
if (stored && typeof stored === 'string') return stored;
|
|
1163
|
+
} catch {
|
|
1164
|
+
// Continue to fallbacks
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// 2. Backward compatibility fallback for older native modules
|
|
1169
|
+
if (nativeModule?.getUserIdentity) {
|
|
1027
1170
|
try {
|
|
1028
|
-
|
|
1171
|
+
const nativeId = await nativeModule.getUserIdentity();
|
|
1172
|
+
if (nativeId && typeof nativeId === 'string') {
|
|
1173
|
+
const normalized = nativeId.trim();
|
|
1174
|
+
// Only migrate legacy anonymous identifiers. Never treat explicit user identities
|
|
1175
|
+
// as anonymous fingerprints, or session correlation becomes unstable.
|
|
1176
|
+
if (normalized.startsWith('anon_')) {
|
|
1177
|
+
_persistAnonymousId(normalized);
|
|
1178
|
+
return normalized;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1029
1181
|
} catch {
|
|
1030
|
-
|
|
1182
|
+
// Continue to fallback
|
|
1031
1183
|
}
|
|
1032
1184
|
}
|
|
1185
|
+
|
|
1186
|
+
// 3. Generate and persist new ID
|
|
1033
1187
|
return generateAnonymousId();
|
|
1034
1188
|
}
|
|
1035
1189
|
|
|
@@ -1037,7 +1191,13 @@ export async function loadAnonymousId() {
|
|
|
1037
1191
|
* Set a custom anonymous ID
|
|
1038
1192
|
*/
|
|
1039
1193
|
export function setAnonymousId(id) {
|
|
1040
|
-
|
|
1194
|
+
const normalized = (id || '').trim();
|
|
1195
|
+
if (!normalized) {
|
|
1196
|
+
anonymousId = generateAnonymousId();
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
anonymousId = normalized;
|
|
1200
|
+
_persistAnonymousId(normalized);
|
|
1041
1201
|
}
|
|
1042
1202
|
export default {
|
|
1043
1203
|
init: initAutoTracking,
|
|
@@ -50,6 +50,73 @@ const config = {
|
|
|
50
50
|
captureSizes: false
|
|
51
51
|
};
|
|
52
52
|
const SENSITIVE_KEYS = ['token', 'key', 'secret', 'password', 'auth', 'access_token', 'api_key'];
|
|
53
|
+
function getUtf8Size(text) {
|
|
54
|
+
if (!text) return 0;
|
|
55
|
+
if (typeof TextEncoder !== 'undefined') {
|
|
56
|
+
return new TextEncoder().encode(text).length;
|
|
57
|
+
}
|
|
58
|
+
return text.length;
|
|
59
|
+
}
|
|
60
|
+
function getBodySize(body) {
|
|
61
|
+
if (body == null) return 0;
|
|
62
|
+
if (typeof body === 'string') return getUtf8Size(body);
|
|
63
|
+
if (typeof ArrayBuffer !== 'undefined' && body instanceof ArrayBuffer) {
|
|
64
|
+
return body.byteLength;
|
|
65
|
+
}
|
|
66
|
+
if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(body)) {
|
|
67
|
+
return body.byteLength;
|
|
68
|
+
}
|
|
69
|
+
if (typeof Blob !== 'undefined' && body instanceof Blob) {
|
|
70
|
+
return body.size;
|
|
71
|
+
}
|
|
72
|
+
if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
|
|
73
|
+
return getUtf8Size(body.toString());
|
|
74
|
+
}
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
async function getFetchResponseSize(response) {
|
|
78
|
+
const contentLength = response.headers?.get?.('content-length');
|
|
79
|
+
if (contentLength) {
|
|
80
|
+
const parsed = parseInt(contentLength, 10);
|
|
81
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const cloned = response.clone();
|
|
85
|
+
const buffer = await cloned.arrayBuffer();
|
|
86
|
+
return buffer.byteLength;
|
|
87
|
+
} catch {
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function getXhrResponseSize(xhr) {
|
|
92
|
+
try {
|
|
93
|
+
const contentLength = xhr.getResponseHeader('content-length');
|
|
94
|
+
if (contentLength) {
|
|
95
|
+
const parsed = parseInt(contentLength, 10);
|
|
96
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Ignore header access errors and fall through to body inspection.
|
|
100
|
+
}
|
|
101
|
+
const responseType = xhr.responseType;
|
|
102
|
+
if (responseType === '' || responseType === 'text') {
|
|
103
|
+
return getUtf8Size(xhr.responseText || '');
|
|
104
|
+
}
|
|
105
|
+
if (responseType === 'arraybuffer') {
|
|
106
|
+
return typeof ArrayBuffer !== 'undefined' && xhr.response instanceof ArrayBuffer ? xhr.response.byteLength : 0;
|
|
107
|
+
}
|
|
108
|
+
if (responseType === 'blob') {
|
|
109
|
+
return typeof Blob !== 'undefined' && xhr.response instanceof Blob ? xhr.response.size : 0;
|
|
110
|
+
}
|
|
111
|
+
if (responseType === 'json') {
|
|
112
|
+
try {
|
|
113
|
+
return getUtf8Size(JSON.stringify(xhr.response ?? ''));
|
|
114
|
+
} catch {
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
53
120
|
|
|
54
121
|
/**
|
|
55
122
|
* Scrub sensitive data from URL
|
|
@@ -200,7 +267,9 @@ function interceptFetch() {
|
|
|
200
267
|
}
|
|
201
268
|
const startTime = Date.now();
|
|
202
269
|
const method = (init?.method || 'GET').toUpperCase();
|
|
203
|
-
|
|
270
|
+
const requestBodySize = config.captureSizes ? getBodySize(init?.body) : 0;
|
|
271
|
+
return originalFetch(input, init).then(async response => {
|
|
272
|
+
const responseBodySize = config.captureSizes ? await getFetchResponseSize(response) : 0;
|
|
204
273
|
queueRequest({
|
|
205
274
|
requestId: `f${startTime}`,
|
|
206
275
|
method,
|
|
@@ -209,7 +278,9 @@ function interceptFetch() {
|
|
|
209
278
|
duration: Date.now() - startTime,
|
|
210
279
|
startTimestamp: startTime,
|
|
211
280
|
endTimestamp: Date.now(),
|
|
212
|
-
success: response.ok
|
|
281
|
+
success: response.ok,
|
|
282
|
+
requestBodySize,
|
|
283
|
+
responseBodySize
|
|
213
284
|
});
|
|
214
285
|
return response;
|
|
215
286
|
}, error => {
|
|
@@ -222,7 +293,8 @@ function interceptFetch() {
|
|
|
222
293
|
startTimestamp: startTime,
|
|
223
294
|
endTimestamp: Date.now(),
|
|
224
295
|
success: false,
|
|
225
|
-
errorMessage: error?.message || 'Network error'
|
|
296
|
+
errorMessage: error?.message || 'Network error',
|
|
297
|
+
requestBodySize
|
|
226
298
|
});
|
|
227
299
|
throw error;
|
|
228
300
|
});
|
|
@@ -257,9 +329,15 @@ function interceptXHR() {
|
|
|
257
329
|
if (!shouldSampleRequest(path)) {
|
|
258
330
|
return originalXHRSend.call(this, body);
|
|
259
331
|
}
|
|
332
|
+
if (config.captureSizes && body) {
|
|
333
|
+
data.reqSize = getBodySize(body);
|
|
334
|
+
} else {
|
|
335
|
+
data.reqSize = 0;
|
|
336
|
+
}
|
|
260
337
|
data.t = Date.now();
|
|
261
338
|
const onComplete = () => {
|
|
262
339
|
const endTime = Date.now();
|
|
340
|
+
const responseBodySize = config.captureSizes ? getXhrResponseSize(this) : 0;
|
|
263
341
|
queueRequest({
|
|
264
342
|
requestId: `x${data.t}`,
|
|
265
343
|
method: data.m,
|
|
@@ -269,7 +347,9 @@ function interceptXHR() {
|
|
|
269
347
|
startTimestamp: data.t,
|
|
270
348
|
endTimestamp: endTime,
|
|
271
349
|
success: this.status >= 200 && this.status < 400,
|
|
272
|
-
errorMessage: this.status === 0 ? 'Network error' : undefined
|
|
350
|
+
errorMessage: this.status === 0 ? 'Network error' : undefined,
|
|
351
|
+
requestBodySize: data.reqSize,
|
|
352
|
+
responseBodySize
|
|
273
353
|
});
|
|
274
354
|
};
|
|
275
355
|
this.addEventListener('load', onComplete);
|