@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
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+
3
+ var _autoTracking = require("./sdk/autoTracking");
4
+ var _navigation = require("./sdk/navigation");
5
+ /**
6
+ * Optional Expo Router integration for @rejourneyco/react-native
7
+ *
8
+ * This file is only loaded when you import '@rejourneyco/react-native/expo-router'.
9
+ * It contains require('expo-router') and related subpaths. Metro bundles require()
10
+ * at build time, so keeping this in a separate entry ensures apps that use
11
+ * Expo with react-navigation (without expo-router) never pull in expo-router
12
+ * and avoid "Requiring unknown module" crashes.
13
+ *
14
+ * If you use expo-router, add this once (e.g. in your root _layout.tsx):
15
+ * import '@rejourneyco/react-native/expo-router';
16
+ */
17
+
18
+ const MAX_POLLING_ERRORS = 10;
19
+ function extractScreenNameFromRouterState(state, getScreenNameFromPathFn, normalizeScreenNameFn, accumulatedSegments = []) {
20
+ if (!state?.routes) return null;
21
+ const route = state.routes[state.index ?? state.routes.length - 1];
22
+ if (!route) return null;
23
+ const newSegments = [...accumulatedSegments, route.name];
24
+ if (route.state) {
25
+ return extractScreenNameFromRouterState(route.state, getScreenNameFromPathFn, normalizeScreenNameFn, newSegments);
26
+ }
27
+ const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
28
+ if (cleanSegments.length === 0) {
29
+ for (let i = newSegments.length - 1; i >= 0; i--) {
30
+ const seg = newSegments[i];
31
+ if (seg && !seg.startsWith('(') && !seg.endsWith(')')) {
32
+ cleanSegments.push(seg);
33
+ break;
34
+ }
35
+ }
36
+ }
37
+ const pathname = '/' + cleanSegments.join('/');
38
+ return getScreenNameFromPathFn(pathname, newSegments);
39
+ }
40
+ function setupExpoRouterPolling() {
41
+ let lastDetectedScreen = '';
42
+ let pollingErrors = 0;
43
+ try {
44
+ const EXPO_ROUTER = 'expo-router';
45
+ const expoRouter = require(EXPO_ROUTER);
46
+ const router = expoRouter.router;
47
+ if (!router) {
48
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
49
+ console.debug('[Rejourney] Expo Router: router object not found');
50
+ }
51
+ return;
52
+ }
53
+ const intervalId = setInterval(() => {
54
+ try {
55
+ let state = null;
56
+ if (typeof router.getState === 'function') {
57
+ state = router.getState();
58
+ } else if (router.rootState) {
59
+ state = router.rootState;
60
+ }
61
+ if (!state) {
62
+ try {
63
+ const STORE_PATH = 'expo-router/build/global-state/router-store';
64
+ const storeModule = require(STORE_PATH);
65
+ if (storeModule?.store) {
66
+ state = storeModule.store.state;
67
+ if (!state && storeModule.store.navigationRef?.current) {
68
+ state = storeModule.store.navigationRef.current.getRootState?.();
69
+ }
70
+ if (!state) {
71
+ state = storeModule.store.rootState || storeModule.store.initialState;
72
+ }
73
+ }
74
+ } catch {
75
+ // Ignore
76
+ }
77
+ }
78
+ if (!state) {
79
+ try {
80
+ const IMPERATIVE_PATH = 'expo-router/build/imperative-api';
81
+ const imperative = require(IMPERATIVE_PATH);
82
+ if (imperative?.router) {
83
+ state = imperative.router.getState?.();
84
+ }
85
+ } catch {
86
+ // Ignore
87
+ }
88
+ }
89
+ if (state) {
90
+ pollingErrors = 0;
91
+ const screenName = extractScreenNameFromRouterState(state, _navigation.getScreenNameFromPath, _navigation.normalizeScreenName);
92
+ if (screenName && screenName !== lastDetectedScreen) {
93
+ lastDetectedScreen = screenName;
94
+ (0, _autoTracking.trackScreen)(screenName);
95
+ }
96
+ } else {
97
+ pollingErrors++;
98
+ if (pollingErrors >= MAX_POLLING_ERRORS) {
99
+ clearInterval(intervalId);
100
+ (0, _autoTracking.setExpoRouterPollingInterval)(null);
101
+ }
102
+ }
103
+ } catch {
104
+ pollingErrors++;
105
+ if (pollingErrors >= MAX_POLLING_ERRORS) {
106
+ clearInterval(intervalId);
107
+ (0, _autoTracking.setExpoRouterPollingInterval)(null);
108
+ }
109
+ }
110
+ }, 500);
111
+ (0, _autoTracking.setExpoRouterPollingInterval)(intervalId);
112
+ } catch (e) {
113
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
114
+ console.debug('[Rejourney] Expo Router not available:', e);
115
+ }
116
+ }
117
+ }
118
+ let attempts = 0;
119
+ const maxAttempts = 5;
120
+ function trySetup() {
121
+ attempts++;
122
+ try {
123
+ const EXPO_ROUTER = 'expo-router';
124
+ const expoRouter = require(EXPO_ROUTER);
125
+ if (expoRouter?.router && (0, _autoTracking.isExpoRouterTrackingEnabled)()) {
126
+ setupExpoRouterPolling();
127
+ return;
128
+ }
129
+ } catch {
130
+ // Not ready or not installed
131
+ }
132
+ if (attempts < maxAttempts) {
133
+ setTimeout(trySetup, 200 * attempts);
134
+ }
135
+ }
136
+ setTimeout(trySetup, 200);
137
+ //# sourceMappingURL=expoRouterTracking.js.map
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  var _exportNames = {
7
+ Rejourney: true,
7
8
  initRejourney: true,
8
9
  startRejourney: true,
9
10
  stopRejourney: true,
@@ -12,7 +13,6 @@ var _exportNames = {
12
13
  trackScroll: true,
13
14
  trackGesture: true,
14
15
  trackInput: true,
15
- trackScreen: true,
16
16
  captureError: true,
17
17
  getSessionMetrics: true,
18
18
  trackNavigationState: true,
@@ -32,6 +32,7 @@ Object.defineProperty(exports, "Mask", {
32
32
  return _Mask.Mask;
33
33
  }
34
34
  });
35
+ exports.Rejourney = void 0;
35
36
  Object.defineProperty(exports, "captureError", {
36
37
  enumerable: true,
37
38
  get: function () {
@@ -67,12 +68,6 @@ Object.defineProperty(exports, "trackNavigationState", {
67
68
  return _autoTracking2.trackNavigationState;
68
69
  }
69
70
  });
70
- Object.defineProperty(exports, "trackScreen", {
71
- enumerable: true,
72
- get: function () {
73
- return _autoTracking2.trackScreen;
74
- }
75
- });
76
71
  Object.defineProperty(exports, "trackScroll", {
77
72
  enumerable: true,
78
73
  get: function () {
@@ -268,7 +263,12 @@ const noopAutoTracking = {
268
263
  getSessionMetrics: () => ({}),
269
264
  resetMetrics: () => {},
270
265
  collectDeviceInfo: async () => ({}),
271
- ensurePersistentAnonymousId: async () => 'anonymous'
266
+ ensurePersistentAnonymousId: async () => 'anonymous',
267
+ useNavigationTracking: () => ({
268
+ ref: null,
269
+ onReady: () => {},
270
+ onStateChange: () => {}
271
+ })
272
272
  };
273
273
  function getAutoTracking() {
274
274
  if (_sdkDisabled) return noopAutoTracking;
@@ -291,6 +291,11 @@ let _appStateSubscription = null;
291
291
  let _authErrorSubscription = null;
292
292
  let _currentAppState = 'active'; // Default to active, will be updated on init
293
293
  let _userIdentity = null;
294
+ let _backgroundEntryTime = null; // Track when app went to background
295
+ let _storedMetadata = {}; // Accumulate metadata for session rollover
296
+
297
+ // Session timeout - must match native side (60 seconds)
298
+ const SESSION_TIMEOUT_MS = 60_000;
294
299
 
295
300
  // Scroll throttling - reduce native bridge calls from 60fps to at most 10/sec
296
301
  let _lastScrollTime = 0;
@@ -545,7 +550,7 @@ function safeNativeCallSync(methodName, fn, defaultValue) {
545
550
  /**
546
551
  * Main Rejourney API (Internal)
547
552
  */
548
- const Rejourney = {
553
+ const Rejourney = exports.Rejourney = {
549
554
  /**
550
555
  * SDK Version
551
556
  */
@@ -680,7 +685,9 @@ const Rejourney = {
680
685
  trackJSErrors: true,
681
686
  trackPromiseRejections: true,
682
687
  trackReactNativeErrors: true,
683
- collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false
688
+ trackConsoleLogs: _storedConfig?.trackConsoleLogs ?? true,
689
+ collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false,
690
+ autoTrackExpoRouter: _storedConfig?.autoTrackExpoRouter !== false
684
691
  }, {
685
692
  // Rage tap callback - log as frustration event
686
693
  onRageTap: (count, x, y) => {
@@ -692,13 +699,8 @@ const Rejourney = {
692
699
  });
693
700
  getLogger().logFrustration(`Rage tap (${count} taps)`);
694
701
  },
695
- // Error callback - log as error event
702
+ // Error callback - SDK forwarding is handled in autoTracking.trackError
696
703
  onError: error => {
697
- this.logEvent('error', {
698
- message: error.message,
699
- stack: error.stack,
700
- name: error.name
701
- });
702
704
  getLogger().logError(error.message);
703
705
  },
704
706
  onScreen: (_screenName, _previousScreen) => {}
@@ -713,7 +715,12 @@ const Rejourney = {
713
715
  }
714
716
  if (_storedConfig?.autoTrackNetwork !== false) {
715
717
  try {
716
- const ignoreUrls = [apiUrl, '/api/sdk/config', '/api/ingest/presign', '/api/ingest/batch/complete', '/api/ingest/session/end', ...(_storedConfig?.networkIgnoreUrls || [])];
718
+ // JS-level fetch/XHR patching is the primary mechanism for capturing network
719
+ // calls within React Native. Native interceptors (RejourneyURLProtocol on iOS,
720
+ // RejourneyNetworkInterceptor on Android) are supplementary — they capture
721
+ // native-originated HTTP calls that bypass JS fetch(), but cannot intercept
722
+ // RN's own networking since it creates its NSURLSession/OkHttpClient at init time.
723
+ const ignoreUrls = [apiUrl, '/api/sdk/config', '/api/ingest/presign', '/api/ingest/batch/complete', '/api/ingest/session/end', '/api/ingest/segment/presign', '/api/ingest/segment/complete', ...(_storedConfig?.networkIgnoreUrls || [])];
717
724
  getNetworkInterceptor().initNetworkInterceptor(request => {
718
725
  getAutoTracking().trackAPIRequest(request.success || false, request.statusCode, request.duration || 0, request.responseBodySize || 0);
719
726
  Rejourney.logNetworkRequest(request);
@@ -802,17 +809,57 @@ const Rejourney = {
802
809
  }
803
810
  },
804
811
  /**
805
- * Tag the current screen
812
+ /**
813
+ * Set custom session metadata.
814
+ * Can be called with a single key-value pair or an object of properties.
815
+ * Useful for filtering sessions later (e.g., plan: 'premium', role: 'admin').
816
+ * Caps at 100 properties per session.
817
+ *
818
+ * @param keyOrProperties Property name string, or an object containing key-value pairs
819
+ * @param value Property value (if first argument is a string)
820
+ */
821
+ setMetadata(keyOrProperties, value) {
822
+ if (typeof keyOrProperties === 'string') {
823
+ const key = keyOrProperties;
824
+ if (!key) {
825
+ getLogger().warn('setMetadata requires a non-empty string key');
826
+ return;
827
+ }
828
+ if (value !== undefined && typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
829
+ getLogger().warn('setMetadata value must be a string, number, or boolean when using a key string');
830
+ return;
831
+ }
832
+ this.logEvent('$user_property', {
833
+ key,
834
+ value
835
+ });
836
+ // Track for session rollover restoration
837
+ _storedMetadata[key] = value;
838
+ } else if (keyOrProperties && typeof keyOrProperties === 'object') {
839
+ const properties = keyOrProperties;
840
+ const validProps = {};
841
+ for (const [k, v] of Object.entries(properties)) {
842
+ if (typeof k === 'string' && k && (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')) {
843
+ validProps[k] = v;
844
+ }
845
+ }
846
+ if (Object.keys(validProps).length > 0) {
847
+ this.logEvent('$user_property', validProps);
848
+ // Track for session rollover restoration
849
+ Object.assign(_storedMetadata, validProps);
850
+ }
851
+ } else {
852
+ getLogger().warn('setMetadata requires a string key and value, or a properties object');
853
+ }
854
+ },
855
+ /**
856
+ * Track current screen (manual)
806
857
  *
807
858
  * @param screenName - Screen name
808
859
  * @param params - Optional screen parameters
809
860
  */
810
- tagScreen(screenName, _params) {
861
+ trackScreen(screenName, _params) {
811
862
  getAutoTracking().trackScreen(screenName);
812
- getAutoTracking().notifyStateChange();
813
- safeNativeCallSync('tagScreen', () => {
814
- getRejourneyNative().screenChanged(screenName).catch(() => {});
815
- }, undefined);
816
863
  },
817
864
  /**
818
865
  * Mark a view as sensitive (will be occluded in recordings)
@@ -1110,6 +1157,23 @@ const Rejourney = {
1110
1157
  getRejourneyNative().logEvent('network_request', networkEvent).catch(() => {});
1111
1158
  }, undefined);
1112
1159
  },
1160
+ /**
1161
+ * Log customer feedback (e.g. from an in-app survey or NPS widget).
1162
+ *
1163
+ * @param rating - Numeric rating (e.g. 1 to 5)
1164
+ * @param message - Associated feedback text or comment
1165
+ */
1166
+ logFeedback(rating, message) {
1167
+ safeNativeCallSync('logFeedback', () => {
1168
+ const feedbackEvent = {
1169
+ type: 'feedback',
1170
+ timestamp: Date.now(),
1171
+ rating,
1172
+ message
1173
+ };
1174
+ getRejourneyNative().logEvent('feedback', feedbackEvent).catch(() => {});
1175
+ }, undefined);
1176
+ },
1113
1177
  /**
1114
1178
  * Get SDK telemetry metrics for observability
1115
1179
  *
@@ -1145,17 +1209,12 @@ const Rejourney = {
1145
1209
  });
1146
1210
  },
1147
1211
  /**
1148
- * Trigger a debug ANR (Dev only)
1149
- * Blocks the main thread for the specified duration
1212
+ * Trigger an ANR test by blocking the main thread for the specified duration.
1150
1213
  */
1151
1214
  debugTriggerANR(durationMs) {
1152
- if (__DEV__) {
1153
- safeNativeCallSync('debugTriggerANR', () => {
1154
- getRejourneyNative().debugTriggerANR(durationMs);
1155
- }, undefined);
1156
- } else {
1157
- getLogger().warn('debugTriggerANR is only available in development mode');
1158
- }
1215
+ safeNativeCallSync('debugTriggerANR', () => {
1216
+ getRejourneyNative().debugTriggerANR(durationMs);
1217
+ }, undefined);
1159
1218
  },
1160
1219
  /**
1161
1220
  * Mask a view by its nativeID prop (will be occluded in recordings)
@@ -1189,13 +1248,109 @@ const Rejourney = {
1189
1248
  safeNativeCallSync('unmaskView', () => {
1190
1249
  getRejourneyNative().unmaskViewByNativeID(nativeID).catch(() => {});
1191
1250
  }, undefined);
1251
+ },
1252
+ /**
1253
+ * Initialize Rejourney SDK
1254
+ */
1255
+ init(publicRouteKey, options) {
1256
+ initRejourney(publicRouteKey, options);
1257
+ },
1258
+ /**
1259
+ * Start recording
1260
+ */
1261
+ start() {
1262
+ startRejourney();
1263
+ },
1264
+ /**
1265
+ * Stop recording
1266
+ */
1267
+ stop() {
1268
+ stopRejourney();
1269
+ },
1270
+ /**
1271
+ * Hook for automatic React Navigation tracking.
1272
+ */
1273
+ useNavigationTracking() {
1274
+ return getAutoTracking().useNavigationTracking();
1192
1275
  }
1193
1276
  };
1194
1277
 
1278
+ /**
1279
+ * Reinitialize JS-side auto-tracking for a new session after background timeout.
1280
+ *
1281
+ * When the app was in background for >60s the native layer rolls over to a
1282
+ * fresh session automatically. The JS side must tear down stale tracking
1283
+ * state (metrics, console-log counter, screen history, error handlers) and
1284
+ * re-initialize so that trackScreen, logEvent, setMetadata, etc. work
1285
+ * correctly against the new native session.
1286
+ */
1287
+ function _reinitAutoTrackingForNewSession() {
1288
+ try {
1289
+ // 1. Tear down old session's auto-tracking state
1290
+ getAutoTracking().cleanupAutoTracking();
1291
+
1292
+ // 2. Re-initialize auto-tracking with the same config
1293
+ getAutoTracking().initAutoTracking({
1294
+ rageTapThreshold: _storedConfig?.rageTapThreshold ?? 3,
1295
+ rageTapTimeWindow: _storedConfig?.rageTapTimeWindow ?? 500,
1296
+ rageTapRadius: 50,
1297
+ trackJSErrors: true,
1298
+ trackPromiseRejections: true,
1299
+ trackReactNativeErrors: true,
1300
+ trackConsoleLogs: _storedConfig?.trackConsoleLogs ?? true,
1301
+ collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false,
1302
+ autoTrackExpoRouter: _storedConfig?.autoTrackExpoRouter !== false
1303
+ }, {
1304
+ onRageTap: (count, x, y) => {
1305
+ Rejourney.logEvent('frustration', {
1306
+ frustrationKind: 'rage_tap',
1307
+ tapCount: count,
1308
+ x,
1309
+ y
1310
+ });
1311
+ getLogger().logFrustration(`Rage tap (${count} taps)`);
1312
+ },
1313
+ onError: error => {
1314
+ getLogger().logError(error.message);
1315
+ },
1316
+ onScreen: (_screenName, _previousScreen) => {}
1317
+ });
1318
+
1319
+ // 3. Re-collect device info for the new session
1320
+ if (_storedConfig?.collectDeviceInfo !== false) {
1321
+ getAutoTracking().collectDeviceInfo().then(deviceInfo => {
1322
+ Rejourney.logEvent('device_info', deviceInfo);
1323
+ }).catch(() => {});
1324
+ }
1325
+
1326
+ // 4. Re-send user identity to the new native session
1327
+ if (_userIdentity) {
1328
+ safeNativeCallSync('setUserIdentity', () => {
1329
+ getRejourneyNative().setUserIdentity(_userIdentity).catch(() => {});
1330
+ }, undefined);
1331
+ getLogger().debug(`Restored user identity '${_userIdentity}' to new session`);
1332
+ }
1333
+
1334
+ // 5. Re-send any stored metadata to the new native session
1335
+ if (Object.keys(_storedMetadata).length > 0) {
1336
+ for (const [key, value] of Object.entries(_storedMetadata)) {
1337
+ if (value !== undefined && value !== null) {
1338
+ Rejourney.setMetadata(key, value);
1339
+ }
1340
+ }
1341
+ getLogger().debug('Restored metadata to new session');
1342
+ }
1343
+ getLogger().logLifecycleEvent('JS auto-tracking reinitialized for new session');
1344
+ } catch (error) {
1345
+ getLogger().warn('Failed to reinitialize auto-tracking after session rollover:', error);
1346
+ }
1347
+ }
1348
+
1195
1349
  /**
1196
1350
  * Handle app state changes for automatic session management
1197
1351
  * - Pauses recording when app goes to background
1198
1352
  * - Resumes recording when app comes back to foreground
1353
+ * - Reinitializes JS-side auto-tracking when native rolls over to a new session
1199
1354
  * - Cleans up properly when app is terminated
1200
1355
  */
1201
1356
  function handleAppStateChange(nextAppState) {
@@ -1204,9 +1359,22 @@ function handleAppStateChange(nextAppState) {
1204
1359
  if (_currentAppState.match(/active/) && nextAppState === 'background') {
1205
1360
  // App going to background - native module handles this automatically
1206
1361
  getLogger().logLifecycleEvent('App moving to background');
1362
+ _backgroundEntryTime = Date.now();
1207
1363
  } else if (_currentAppState.match(/inactive|background/) && nextAppState === 'active') {
1208
1364
  // App coming back to foreground
1209
1365
  getLogger().logLifecycleEvent('App returning to foreground');
1366
+
1367
+ // Check if we exceeded the session timeout (60s).
1368
+ // Native side will have already ended the old session and started a new
1369
+ // one — we need to reset JS-side auto-tracking state to match.
1370
+ if (_backgroundEntryTime && _isRecording) {
1371
+ const backgroundDurationMs = Date.now() - _backgroundEntryTime;
1372
+ if (backgroundDurationMs > SESSION_TIMEOUT_MS) {
1373
+ getLogger().debug(`Session rollover: background ${Math.round(backgroundDurationMs / 1000)}s > ${SESSION_TIMEOUT_MS / 1000}s timeout`);
1374
+ _reinitAutoTrackingForNewSession();
1375
+ }
1376
+ }
1377
+ _backgroundEntryTime = null;
1210
1378
  }
1211
1379
  _currentAppState = nextAppState;
1212
1380
  } catch (error) {
@@ -1271,8 +1439,10 @@ function setupAuthErrorListener() {
1271
1439
  }
1272
1440
  });
1273
1441
  }
1274
- } catch (error) {
1275
- getLogger().debug('Auth error listener not available:', error);
1442
+ } catch {
1443
+ // Expected on some architectures where NativeEventEmitter isn't fully supported.
1444
+ // Auth errors are still handled synchronously via native callback — this listener
1445
+ // is purely supplementary. No need to log.
1276
1446
  }
1277
1447
  }
1278
1448