@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
@@ -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 by 500ms
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. 500ms is enough for the bridge to flush.
292
+ // the app terminates.
285
293
  setTimeout(() => {
286
294
  originalErrorHandler(error, isFatal);
287
- }, 500);
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
- onErrorCaptured(error);
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
- let navigationPollingErrors = 0;
472
- const MAX_POLLING_ERRORS = 10; // Stop polling after 10 consecutive errors
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 Expo Router tracking
570
- *
571
- * For Expo apps using expo-router - works automatically.
572
- * For bare React Native apps - use trackNavigationState instead.
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
- if (__DEV__) {
578
- logger.debug('Setting up navigation tracking...');
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
- let attempts = 0;
581
- const maxAttempts = 5;
582
- const trySetup = () => {
583
- attempts++;
584
- if (__DEV__) {
585
- logger.debug('Navigation setup attempt', attempts, 'of', maxAttempts);
586
- }
587
- const success = trySetupExpoRouter();
588
- if (success) {
589
- if (__DEV__) {
590
- logger.debug('Expo Router setup: SUCCESS on attempt', attempts);
591
- }
592
- } else if (attempts < maxAttempts) {
593
- const delay = 200 * attempts;
594
- if (__DEV__) {
595
- logger.debug('Expo Router not ready, retrying in', delay, 'ms');
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
- setTimeout(trySetup, delay);
598
- } else {
599
- if (__DEV__) {
600
- logger.debug('Expo Router setup: FAILED after', maxAttempts, 'attempts');
601
- logger.debug('For manual navigation tracking, use trackNavigationState() on your NavigationContainer');
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
- * Set up Expo Router auto-tracking by polling the internal router store
610
- *
611
- * Supports both expo-router v3 (store.rootState) and v6+ (store.state, store.navigationRef)
612
- */
613
- function trySetupExpoRouter() {
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
- navigationPollingInterval = setInterval(() => {
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 storeModule = require('expo-router/build/global-state/router-store');
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 imperative = require('expo-router/build/imperative-api');
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
- navigationPollingErrors = 0;
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
- navigationPollingErrors++;
684
- if (__DEV__ && navigationPollingErrors === 1) {
685
- logger.debug('Expo Router: Could not get navigation state');
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 (e) {
692
- navigationPollingErrors++;
693
- if (__DEV__ && navigationPollingErrors === 1) {
694
- logger.debug('Expo Router polling error:', e);
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
- return true;
702
- } catch (e) {
703
- if (__DEV__) {
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 Expo Router navigation state
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, getScreenNameFromPath, normalizeScreenName, accumulatedSegments = []) {
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, getScreenNameFromPath, normalizeScreenName, newSegments);
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 getScreenNameFromPath(pathname, newSegments);
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
- return `anon_${timestamp}_${random}`;
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
- * Call this at app startup for best results
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
- if (nativeModule && nativeModule.getUserIdentity) {
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
- return (await nativeModule.getUserIdentity()) || generateAnonymousId();
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
- return generateAnonymousId();
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
- anonymousId = id;
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
- return originalFetch(input, init).then(response => {
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);