@rejourneyco/react-native 1.0.9 → 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 (31) 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 +54 -0
  4. package/android/src/main/java/com/rejourney/RejourneyOkHttpInitProvider.kt +68 -0
  5. package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +3 -0
  6. package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +0 -7
  7. package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +4 -1
  8. package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +3 -0
  9. package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +26 -0
  10. package/android/src/main/java/com/rejourney/utility/DataCompression.kt +14 -2
  11. package/ios/Engine/RejourneyImpl.swift +5 -0
  12. package/ios/Recording/RejourneyURLProtocol.swift +58 -10
  13. package/ios/Recording/ReplayOrchestrator.swift +3 -1
  14. package/ios/Recording/TelemetryPipeline.swift +28 -2
  15. package/ios/Recording/VisualCapture.swift +25 -21
  16. package/ios/Utility/DataCompression.swift +2 -2
  17. package/lib/commonjs/expoRouterTracking.js +137 -0
  18. package/lib/commonjs/index.js +176 -19
  19. package/lib/commonjs/sdk/autoTracking.js +100 -89
  20. package/lib/module/expoRouterTracking.js +135 -0
  21. package/lib/module/index.js +175 -13
  22. package/lib/module/sdk/autoTracking.js +98 -89
  23. package/lib/typescript/expoRouterTracking.d.ts +14 -0
  24. package/lib/typescript/index.d.ts +2 -2
  25. package/lib/typescript/sdk/autoTracking.d.ts +11 -0
  26. package/lib/typescript/types/index.d.ts +42 -3
  27. package/package.json +22 -2
  28. package/src/expoRouterTracking.ts +167 -0
  29. package/src/index.ts +184 -16
  30. package/src/sdk/autoTracking.ts +110 -103
  31. package/src/types/index.ts +43 -3
@@ -25,6 +25,8 @@ export interface RejourneyConfig {
25
25
  maxStorageSize?: number;
26
26
  /** Enable automatic screen name detection with React Navigation (default: true) */
27
27
  autoScreenTracking?: boolean;
28
+ /** Enable automatic screen name detection with Expo Router (default: true) */
29
+ autoTrackExpoRouter?: boolean;
28
30
  /** Enable automatic gesture detection (default: true) */
29
31
  autoGestureTracking?: boolean;
30
32
  /** Enable privacy occlusion for text inputs (default: true) */
@@ -433,7 +435,17 @@ export interface RejourneyNativeModule {
433
435
  export interface RejourneyAPI {
434
436
  /** SDK version */
435
437
  readonly version: string;
436
- /** Internal method to start recording session (called by startRejourney) */
438
+ /**
439
+ * Initialize Rejourney SDK
440
+ * @param publicRouteKey - Your public route key from the Rejourney dashboard
441
+ * @param options - Optional configuration options
442
+ */
443
+ init(publicRouteKey: string, options?: Omit<RejourneyConfig, 'publicRouteKey'>): void;
444
+ /** Start recording (call after user consent) */
445
+ start(): void;
446
+ /** Stop recording */
447
+ stop(): void;
448
+ /** Internal method to start recording session (called by start() / startRejourney()) */
437
449
  _startSession(): Promise<boolean>;
438
450
  /** Internal method to stop recording session (called by stopRejourney) */
439
451
  _stopSession(): Promise<void>;
@@ -443,8 +455,18 @@ export interface RejourneyAPI {
443
455
  setUserIdentity(userId: string): void;
444
456
  /** Clear user identity */
445
457
  clearUserIdentity(): void;
446
- /** Tag current screen */
447
- tagScreen(screenName: string, params?: Record<string, unknown>): void;
458
+ /**
459
+ * Set custom session metadata.
460
+ * Can be called with a single key-value pair or an object of properties.
461
+ * Useful for filtering sessions later (e.g., plan: 'premium', role: 'admin').
462
+ * Caps at 100 properties per session.
463
+ *
464
+ * @param keyOrProperties Property name string, or an object containing key-value pairs
465
+ * @param value Property value (if first argument is a string)
466
+ */
467
+ setMetadata(keyOrProperties: string | Record<string, string | number | boolean>, value?: string | number | boolean): void;
468
+ /** Track current screen (manual) */
469
+ trackScreen(screenName: string, params?: Record<string, unknown>): void;
448
470
  /** Mark a view as sensitive (will be occluded in recording) */
449
471
  setOccluded(viewRef: {
450
472
  current: any;
@@ -540,6 +562,21 @@ export interface RejourneyAPI {
540
562
  * @param nativeID - The nativeID prop of the view to unmask
541
563
  */
542
564
  unmaskView(nativeID: string): void;
565
+ /**
566
+ * Hook for automatic React Navigation tracking.
567
+ * Pass the returned object to your NavigationContainer props.
568
+ *
569
+ * @example
570
+ * ```tsx
571
+ * const navigationTracking = Rejourney.useNavigationTracking();
572
+ * <NavigationContainer {...navigationTracking}>
573
+ * ```
574
+ */
575
+ useNavigationTracking(): {
576
+ ref: any;
577
+ onReady: () => void;
578
+ onStateChange: (state: any) => void;
579
+ };
543
580
  }
544
581
  /**
545
582
  * SDK telemetry metrics for observability
@@ -607,6 +644,8 @@ export interface UseRejourneyResult {
607
644
  stopRecording: () => Promise<void>;
608
645
  /** Log custom event */
609
646
  logEvent: (name: string, properties?: Record<string, unknown>) => void;
647
+ /** Set custom session metadata */
648
+ setMetadata: (keyOrProperties: string | Record<string, string | number | boolean>, value?: string | number | boolean) => void;
610
649
  /** Error if any */
611
650
  error: Error | null;
612
651
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rejourneyco/react-native",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Rejourney Session Recording SDK for React Native",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -92,6 +92,26 @@
92
92
  "@react-navigation/native": ">=6.0.0",
93
93
  "expo-router": ">=3.0.0"
94
94
  },
95
+ "peerDependenciesMeta": {
96
+ "expo-router": {
97
+ "optional": true
98
+ }
99
+ },
100
+ "exports": {
101
+ ".": {
102
+ "types": "./lib/typescript/index.d.ts",
103
+ "react-native": "./lib/module/index.js",
104
+ "import": "./lib/module/index.js",
105
+ "require": "./lib/commonjs/index.js"
106
+ },
107
+ "./expo-router": {
108
+ "types": "./lib/typescript/expoRouterTracking.d.ts",
109
+ "react-native": "./lib/module/expoRouterTracking.js",
110
+ "import": "./lib/module/expoRouterTracking.js",
111
+ "require": "./lib/commonjs/expoRouterTracking.js",
112
+ "default": "./lib/module/expoRouterTracking.js"
113
+ }
114
+ },
95
115
  "codegenConfig": {
96
116
  "name": "RejourneySpec",
97
117
  "type": "modules",
@@ -119,4 +139,4 @@
119
139
  ]
120
140
  ]
121
141
  }
122
- }
142
+ }
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Optional Expo Router integration for @rejourneyco/react-native
3
+ *
4
+ * This file is only loaded when you import '@rejourneyco/react-native/expo-router'.
5
+ * It contains require('expo-router') and related subpaths. Metro bundles require()
6
+ * at build time, so keeping this in a separate entry ensures apps that use
7
+ * Expo with react-navigation (without expo-router) never pull in expo-router
8
+ * and avoid "Requiring unknown module" crashes.
9
+ *
10
+ * If you use expo-router, add this once (e.g. in your root _layout.tsx):
11
+ * import '@rejourneyco/react-native/expo-router';
12
+ */
13
+
14
+ import { trackScreen, setExpoRouterPollingInterval, isExpoRouterTrackingEnabled } from './sdk/autoTracking';
15
+ import { normalizeScreenName, getScreenNameFromPath } from './sdk/navigation';
16
+
17
+ const MAX_POLLING_ERRORS = 10;
18
+
19
+ function extractScreenNameFromRouterState(
20
+ state: any,
21
+ getScreenNameFromPathFn: (path: string, segments: string[]) => string,
22
+ normalizeScreenNameFn: (name: string) => string,
23
+ accumulatedSegments: string[] = []
24
+ ): string | null {
25
+ if (!state?.routes) return null;
26
+
27
+ const route = state.routes[state.index ?? state.routes.length - 1];
28
+ if (!route) return null;
29
+
30
+ const newSegments = [...accumulatedSegments, route.name];
31
+
32
+ if (route.state) {
33
+ return extractScreenNameFromRouterState(
34
+ route.state,
35
+ getScreenNameFromPathFn,
36
+ normalizeScreenNameFn,
37
+ newSegments
38
+ );
39
+ }
40
+
41
+ const cleanSegments = newSegments.filter((s) => !s.startsWith('(') && !s.endsWith(')'));
42
+
43
+ if (cleanSegments.length === 0) {
44
+ for (let i = newSegments.length - 1; i >= 0; i--) {
45
+ const seg = newSegments[i];
46
+ if (seg && !seg.startsWith('(') && !seg.endsWith(')')) {
47
+ cleanSegments.push(seg);
48
+ break;
49
+ }
50
+ }
51
+ }
52
+
53
+ const pathname = '/' + cleanSegments.join('/');
54
+ return getScreenNameFromPathFn(pathname, newSegments);
55
+ }
56
+
57
+ function setupExpoRouterPolling(): void {
58
+ let lastDetectedScreen = '';
59
+ let pollingErrors = 0;
60
+
61
+ try {
62
+ const EXPO_ROUTER = 'expo-router';
63
+ const expoRouter = require(EXPO_ROUTER);
64
+ const router = expoRouter.router;
65
+
66
+ if (!router) {
67
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
68
+ console.debug('[Rejourney] Expo Router: router object not found');
69
+ }
70
+ return;
71
+ }
72
+
73
+ const intervalId = setInterval(() => {
74
+ try {
75
+ let state: any = null;
76
+ if (typeof router.getState === 'function') {
77
+ state = router.getState();
78
+ } else if ((router as any).rootState) {
79
+ state = (router as any).rootState;
80
+ }
81
+
82
+ if (!state) {
83
+ try {
84
+ const STORE_PATH = 'expo-router/build/global-state/router-store';
85
+ const storeModule = require(STORE_PATH);
86
+ if (storeModule?.store) {
87
+ state = storeModule.store.state;
88
+ if (!state && storeModule.store.navigationRef?.current) {
89
+ state = storeModule.store.navigationRef.current.getRootState?.();
90
+ }
91
+ if (!state) {
92
+ state = storeModule.store.rootState || storeModule.store.initialState;
93
+ }
94
+ }
95
+ } catch {
96
+ // Ignore
97
+ }
98
+ }
99
+
100
+ if (!state) {
101
+ try {
102
+ const IMPERATIVE_PATH = 'expo-router/build/imperative-api';
103
+ const imperative = require(IMPERATIVE_PATH);
104
+ if (imperative?.router) {
105
+ state = imperative.router.getState?.();
106
+ }
107
+ } catch {
108
+ // Ignore
109
+ }
110
+ }
111
+
112
+ if (state) {
113
+ pollingErrors = 0;
114
+ const screenName = extractScreenNameFromRouterState(
115
+ state,
116
+ getScreenNameFromPath,
117
+ normalizeScreenName
118
+ );
119
+ if (screenName && screenName !== lastDetectedScreen) {
120
+ lastDetectedScreen = screenName;
121
+ trackScreen(screenName);
122
+ }
123
+ } else {
124
+ pollingErrors++;
125
+ if (pollingErrors >= MAX_POLLING_ERRORS) {
126
+ clearInterval(intervalId);
127
+ setExpoRouterPollingInterval(null);
128
+ }
129
+ }
130
+ } catch {
131
+ pollingErrors++;
132
+ if (pollingErrors >= MAX_POLLING_ERRORS) {
133
+ clearInterval(intervalId);
134
+ setExpoRouterPollingInterval(null);
135
+ }
136
+ }
137
+ }, 500);
138
+
139
+ setExpoRouterPollingInterval(intervalId);
140
+ } catch (e) {
141
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
142
+ console.debug('[Rejourney] Expo Router not available:', e);
143
+ }
144
+ }
145
+ }
146
+
147
+ let attempts = 0;
148
+ const maxAttempts = 5;
149
+
150
+ function trySetup(): void {
151
+ attempts++;
152
+ try {
153
+ const EXPO_ROUTER = 'expo-router';
154
+ const expoRouter = require(EXPO_ROUTER);
155
+ if (expoRouter?.router && isExpoRouterTrackingEnabled()) {
156
+ setupExpoRouterPolling();
157
+ return;
158
+ }
159
+ } catch {
160
+ // Not ready or not installed
161
+ }
162
+ if (attempts < maxAttempts) {
163
+ setTimeout(trySetup, 200 * attempts);
164
+ }
165
+ }
166
+
167
+ setTimeout(trySetup, 200);
package/src/index.ts CHANGED
@@ -171,6 +171,7 @@ let _autoTracking: {
171
171
  resetMetrics: typeof import('./sdk/autoTracking').resetMetrics;
172
172
  collectDeviceInfo: typeof import('./sdk/autoTracking').collectDeviceInfo;
173
173
  ensurePersistentAnonymousId: typeof import('./sdk/autoTracking').ensurePersistentAnonymousId;
174
+ useNavigationTracking: typeof import('./sdk/autoTracking').useNavigationTracking;
174
175
  } | null = null;
175
176
 
176
177
  // No-op auto tracking for when SDK is disabled
@@ -185,6 +186,7 @@ const noopAutoTracking = {
185
186
  resetMetrics: () => { },
186
187
  collectDeviceInfo: async () => ({} as any),
187
188
  ensurePersistentAnonymousId: async () => 'anonymous',
189
+ useNavigationTracking: () => ({ ref: null, onReady: () => { }, onStateChange: () => { } }),
188
190
  };
189
191
 
190
192
  function getAutoTracking() {
@@ -209,6 +211,11 @@ let _appStateSubscription: { remove: () => void } | null = null;
209
211
  let _authErrorSubscription: { remove: () => void } | null = null;
210
212
  let _currentAppState: string = 'active'; // Default to active, will be updated on init
211
213
  let _userIdentity: string | null = null;
214
+ let _backgroundEntryTime: number | null = null; // Track when app went to background
215
+ let _storedMetadata: Record<string, string | number | boolean> = {}; // Accumulate metadata for session rollover
216
+
217
+ // Session timeout - must match native side (60 seconds)
218
+ const SESSION_TIMEOUT_MS = 60_000;
212
219
 
213
220
  // Scroll throttling - reduce native bridge calls from 60fps to at most 10/sec
214
221
  let _lastScrollTime: number = 0;
@@ -481,7 +488,7 @@ function safeNativeCallSync<T>(
481
488
  /**
482
489
  * Main Rejourney API (Internal)
483
490
  */
484
- const Rejourney: RejourneyAPI = {
491
+ export const Rejourney: RejourneyAPI = {
485
492
  /**
486
493
  * SDK Version
487
494
  */
@@ -637,6 +644,7 @@ const Rejourney: RejourneyAPI = {
637
644
  trackReactNativeErrors: true,
638
645
  trackConsoleLogs: _storedConfig?.trackConsoleLogs ?? true,
639
646
  collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false,
647
+ autoTrackExpoRouter: _storedConfig?.autoTrackExpoRouter !== false,
640
648
  },
641
649
  {
642
650
  // Rage tap callback - log as frustration event
@@ -680,6 +688,8 @@ const Rejourney: RejourneyAPI = {
680
688
  '/api/ingest/presign',
681
689
  '/api/ingest/batch/complete',
682
690
  '/api/ingest/session/end',
691
+ '/api/ingest/segment/presign',
692
+ '/api/ingest/segment/complete',
683
693
  ...(_storedConfig?.networkIgnoreUrls || []),
684
694
  ];
685
695
 
@@ -806,22 +816,56 @@ const Rejourney: RejourneyAPI = {
806
816
  },
807
817
 
808
818
  /**
809
- * Tag the current screen
819
+ /**
820
+ * Set custom session metadata.
821
+ * Can be called with a single key-value pair or an object of properties.
822
+ * Useful for filtering sessions later (e.g., plan: 'premium', role: 'admin').
823
+ * Caps at 100 properties per session.
824
+ *
825
+ * @param keyOrProperties Property name string, or an object containing key-value pairs
826
+ * @param value Property value (if first argument is a string)
827
+ */
828
+ setMetadata(keyOrProperties: string | Record<string, string | number | boolean>, value?: string | number | boolean): void {
829
+ if (typeof keyOrProperties === 'string') {
830
+ const key = keyOrProperties;
831
+ if (!key) {
832
+ getLogger().warn('setMetadata requires a non-empty string key');
833
+ return;
834
+ }
835
+ if (value !== undefined && typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
836
+ getLogger().warn('setMetadata value must be a string, number, or boolean when using a key string');
837
+ return;
838
+ }
839
+ this.logEvent('$user_property', { key, value });
840
+ // Track for session rollover restoration
841
+ _storedMetadata[key] = value!;
842
+ } else if (keyOrProperties && typeof keyOrProperties === 'object') {
843
+ const properties = keyOrProperties;
844
+ const validProps: Record<string, any> = {};
845
+ for (const [k, v] of Object.entries(properties)) {
846
+ if (typeof k === 'string' && k &&
847
+ (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')) {
848
+ validProps[k] = v;
849
+ }
850
+ }
851
+ if (Object.keys(validProps).length > 0) {
852
+ this.logEvent('$user_property', validProps);
853
+ // Track for session rollover restoration
854
+ Object.assign(_storedMetadata, validProps);
855
+ }
856
+ } else {
857
+ getLogger().warn('setMetadata requires a string key and value, or a properties object');
858
+ }
859
+ },
860
+
861
+ /**
862
+ * Track current screen (manual)
810
863
  *
811
864
  * @param screenName - Screen name
812
865
  * @param params - Optional screen parameters
813
866
  */
814
- tagScreen(screenName: string, _params?: Record<string, unknown>): void {
867
+ trackScreen(screenName: string, _params?: Record<string, unknown>): void {
815
868
  getAutoTracking().trackScreen(screenName);
816
- getAutoTracking().notifyStateChange();
817
-
818
- safeNativeCallSync(
819
- 'tagScreen',
820
- () => {
821
- getRejourneyNative()!.screenChanged(screenName).catch(() => { });
822
- },
823
- undefined
824
- );
825
869
  },
826
870
 
827
871
  /**
@@ -1271,12 +1315,121 @@ const Rejourney: RejourneyAPI = {
1271
1315
  undefined
1272
1316
  );
1273
1317
  },
1318
+
1319
+ /**
1320
+ * Initialize Rejourney SDK
1321
+ */
1322
+ init(publicRouteKey: string, options?: Omit<RejourneyConfig, 'publicRouteKey'>): void {
1323
+ initRejourney(publicRouteKey, options);
1324
+ },
1325
+
1326
+ /**
1327
+ * Start recording
1328
+ */
1329
+ start(): void {
1330
+ startRejourney();
1331
+ },
1332
+
1333
+ /**
1334
+ * Stop recording
1335
+ */
1336
+ stop(): void {
1337
+ stopRejourney();
1338
+ },
1339
+
1340
+ /**
1341
+ * Hook for automatic React Navigation tracking.
1342
+ */
1343
+ useNavigationTracking() {
1344
+ return getAutoTracking().useNavigationTracking();
1345
+ },
1274
1346
  };
1275
1347
 
1348
+ /**
1349
+ * Reinitialize JS-side auto-tracking for a new session after background timeout.
1350
+ *
1351
+ * When the app was in background for >60s the native layer rolls over to a
1352
+ * fresh session automatically. The JS side must tear down stale tracking
1353
+ * state (metrics, console-log counter, screen history, error handlers) and
1354
+ * re-initialize so that trackScreen, logEvent, setMetadata, etc. work
1355
+ * correctly against the new native session.
1356
+ */
1357
+ function _reinitAutoTrackingForNewSession(): void {
1358
+ try {
1359
+ // 1. Tear down old session's auto-tracking state
1360
+ getAutoTracking().cleanupAutoTracking();
1361
+
1362
+ // 2. Re-initialize auto-tracking with the same config
1363
+ getAutoTracking().initAutoTracking(
1364
+ {
1365
+ rageTapThreshold: _storedConfig?.rageTapThreshold ?? 3,
1366
+ rageTapTimeWindow: _storedConfig?.rageTapTimeWindow ?? 500,
1367
+ rageTapRadius: 50,
1368
+ trackJSErrors: true,
1369
+ trackPromiseRejections: true,
1370
+ trackReactNativeErrors: true,
1371
+ trackConsoleLogs: _storedConfig?.trackConsoleLogs ?? true,
1372
+ collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false,
1373
+ autoTrackExpoRouter: _storedConfig?.autoTrackExpoRouter !== false,
1374
+ },
1375
+ {
1376
+ onRageTap: (count: number, x: number, y: number) => {
1377
+ Rejourney.logEvent('frustration', {
1378
+ frustrationKind: 'rage_tap',
1379
+ tapCount: count,
1380
+ x,
1381
+ y,
1382
+ });
1383
+ getLogger().logFrustration(`Rage tap (${count} taps)`);
1384
+ },
1385
+ onError: (error: { message: string; stack?: string; name?: string }) => {
1386
+ getLogger().logError(error.message);
1387
+ },
1388
+ onScreen: (_screenName: string, _previousScreen?: string) => {
1389
+ },
1390
+ }
1391
+ );
1392
+
1393
+ // 3. Re-collect device info for the new session
1394
+ if (_storedConfig?.collectDeviceInfo !== false) {
1395
+ getAutoTracking().collectDeviceInfo().then((deviceInfo) => {
1396
+ Rejourney.logEvent('device_info', deviceInfo as unknown as Record<string, unknown>);
1397
+ }).catch(() => { });
1398
+ }
1399
+
1400
+ // 4. Re-send user identity to the new native session
1401
+ if (_userIdentity) {
1402
+ safeNativeCallSync(
1403
+ 'setUserIdentity',
1404
+ () => {
1405
+ getRejourneyNative()!.setUserIdentity(_userIdentity!).catch(() => { });
1406
+ },
1407
+ undefined
1408
+ );
1409
+ getLogger().debug(`Restored user identity '${_userIdentity}' to new session`);
1410
+ }
1411
+
1412
+ // 5. Re-send any stored metadata to the new native session
1413
+ if (Object.keys(_storedMetadata).length > 0) {
1414
+ for (const [key, value] of Object.entries(_storedMetadata)) {
1415
+ if (value !== undefined && value !== null) {
1416
+ Rejourney.setMetadata(key, value);
1417
+ }
1418
+ }
1419
+ getLogger().debug('Restored metadata to new session');
1420
+ }
1421
+
1422
+ getLogger().logLifecycleEvent('JS auto-tracking reinitialized for new session');
1423
+ } catch (error) {
1424
+ getLogger().warn('Failed to reinitialize auto-tracking after session rollover:', error);
1425
+ }
1426
+ }
1427
+
1276
1428
  /**
1277
1429
  * Handle app state changes for automatic session management
1278
1430
  * - Pauses recording when app goes to background
1279
1431
  * - Resumes recording when app comes back to foreground
1432
+ * - Reinitializes JS-side auto-tracking when native rolls over to a new session
1280
1433
  * - Cleans up properly when app is terminated
1281
1434
  */
1282
1435
  function handleAppStateChange(nextAppState: string): void {
@@ -1286,9 +1439,24 @@ function handleAppStateChange(nextAppState: string): void {
1286
1439
  if (_currentAppState.match(/active/) && nextAppState === 'background') {
1287
1440
  // App going to background - native module handles this automatically
1288
1441
  getLogger().logLifecycleEvent('App moving to background');
1442
+ _backgroundEntryTime = Date.now();
1289
1443
  } else if (_currentAppState.match(/inactive|background/) && nextAppState === 'active') {
1290
1444
  // App coming back to foreground
1291
1445
  getLogger().logLifecycleEvent('App returning to foreground');
1446
+
1447
+ // Check if we exceeded the session timeout (60s).
1448
+ // Native side will have already ended the old session and started a new
1449
+ // one — we need to reset JS-side auto-tracking state to match.
1450
+ if (_backgroundEntryTime && _isRecording) {
1451
+ const backgroundDurationMs = Date.now() - _backgroundEntryTime;
1452
+ if (backgroundDurationMs > SESSION_TIMEOUT_MS) {
1453
+ getLogger().debug(
1454
+ `Session rollover: background ${Math.round(backgroundDurationMs / 1000)}s > ${SESSION_TIMEOUT_MS / 1000}s timeout`
1455
+ );
1456
+ _reinitAutoTrackingForNewSession();
1457
+ }
1458
+ }
1459
+ _backgroundEntryTime = null;
1292
1460
  }
1293
1461
  _currentAppState = nextAppState;
1294
1462
  } catch (error) {
@@ -1371,8 +1539,10 @@ function setupAuthErrorListener(): void {
1371
1539
  }
1372
1540
  );
1373
1541
  }
1374
- } catch (error) {
1375
- getLogger().debug('Auth error listener not available:', error);
1542
+ } catch {
1543
+ // Expected on some architectures where NativeEventEmitter isn't fully supported.
1544
+ // Auth errors are still handled synchronously via native callback — this listener
1545
+ // is purely supplementary. No need to log.
1376
1546
  }
1377
1547
  }
1378
1548
 
@@ -1518,7 +1688,6 @@ export function stopRejourney(): void {
1518
1688
  getLogger().warn('Error stopping Rejourney:', error);
1519
1689
  }
1520
1690
  }
1521
-
1522
1691
  export default Rejourney;
1523
1692
 
1524
1693
  export * from './types';
@@ -1528,7 +1697,6 @@ export {
1528
1697
  trackScroll,
1529
1698
  trackGesture,
1530
1699
  trackInput,
1531
- trackScreen,
1532
1700
  captureError,
1533
1701
  getSessionMetrics,
1534
1702
  } from './sdk/autoTracking';