@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.
- package/README.md +77 -3
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +54 -0
- package/android/src/main/java/com/rejourney/RejourneyOkHttpInitProvider.kt +68 -0
- package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +3 -0
- package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +0 -7
- package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +4 -1
- package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +3 -0
- package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +26 -0
- package/android/src/main/java/com/rejourney/utility/DataCompression.kt +14 -2
- package/ios/Engine/RejourneyImpl.swift +5 -0
- package/ios/Recording/RejourneyURLProtocol.swift +58 -10
- package/ios/Recording/ReplayOrchestrator.swift +3 -1
- package/ios/Recording/TelemetryPipeline.swift +28 -2
- package/ios/Recording/VisualCapture.swift +25 -21
- package/ios/Utility/DataCompression.swift +2 -2
- package/lib/commonjs/expoRouterTracking.js +137 -0
- package/lib/commonjs/index.js +176 -19
- package/lib/commonjs/sdk/autoTracking.js +100 -89
- package/lib/module/expoRouterTracking.js +135 -0
- package/lib/module/index.js +175 -13
- package/lib/module/sdk/autoTracking.js +98 -89
- package/lib/typescript/expoRouterTracking.d.ts +14 -0
- package/lib/typescript/index.d.ts +2 -2
- package/lib/typescript/sdk/autoTracking.d.ts +11 -0
- package/lib/typescript/types/index.d.ts +42 -3
- package/package.json +22 -2
- package/src/expoRouterTracking.ts +167 -0
- package/src/index.ts +184 -16
- package/src/sdk/autoTracking.ts +110 -103
- package/src/types/index.ts +43 -3
package/lib/module/index.js
CHANGED
|
@@ -160,7 +160,12 @@ const noopAutoTracking = {
|
|
|
160
160
|
getSessionMetrics: () => ({}),
|
|
161
161
|
resetMetrics: () => {},
|
|
162
162
|
collectDeviceInfo: async () => ({}),
|
|
163
|
-
ensurePersistentAnonymousId: async () => 'anonymous'
|
|
163
|
+
ensurePersistentAnonymousId: async () => 'anonymous',
|
|
164
|
+
useNavigationTracking: () => ({
|
|
165
|
+
ref: null,
|
|
166
|
+
onReady: () => {},
|
|
167
|
+
onStateChange: () => {}
|
|
168
|
+
})
|
|
164
169
|
};
|
|
165
170
|
function getAutoTracking() {
|
|
166
171
|
if (_sdkDisabled) return noopAutoTracking;
|
|
@@ -183,6 +188,11 @@ let _appStateSubscription = null;
|
|
|
183
188
|
let _authErrorSubscription = null;
|
|
184
189
|
let _currentAppState = 'active'; // Default to active, will be updated on init
|
|
185
190
|
let _userIdentity = null;
|
|
191
|
+
let _backgroundEntryTime = null; // Track when app went to background
|
|
192
|
+
let _storedMetadata = {}; // Accumulate metadata for session rollover
|
|
193
|
+
|
|
194
|
+
// Session timeout - must match native side (60 seconds)
|
|
195
|
+
const SESSION_TIMEOUT_MS = 60_000;
|
|
186
196
|
|
|
187
197
|
// Scroll throttling - reduce native bridge calls from 60fps to at most 10/sec
|
|
188
198
|
let _lastScrollTime = 0;
|
|
@@ -437,7 +447,7 @@ function safeNativeCallSync(methodName, fn, defaultValue) {
|
|
|
437
447
|
/**
|
|
438
448
|
* Main Rejourney API (Internal)
|
|
439
449
|
*/
|
|
440
|
-
const Rejourney = {
|
|
450
|
+
export const Rejourney = {
|
|
441
451
|
/**
|
|
442
452
|
* SDK Version
|
|
443
453
|
*/
|
|
@@ -573,7 +583,8 @@ const Rejourney = {
|
|
|
573
583
|
trackPromiseRejections: true,
|
|
574
584
|
trackReactNativeErrors: true,
|
|
575
585
|
trackConsoleLogs: _storedConfig?.trackConsoleLogs ?? true,
|
|
576
|
-
collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false
|
|
586
|
+
collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false,
|
|
587
|
+
autoTrackExpoRouter: _storedConfig?.autoTrackExpoRouter !== false
|
|
577
588
|
}, {
|
|
578
589
|
// Rage tap callback - log as frustration event
|
|
579
590
|
onRageTap: (count, x, y) => {
|
|
@@ -606,7 +617,7 @@ const Rejourney = {
|
|
|
606
617
|
// RejourneyNetworkInterceptor on Android) are supplementary — they capture
|
|
607
618
|
// native-originated HTTP calls that bypass JS fetch(), but cannot intercept
|
|
608
619
|
// RN's own networking since it creates its NSURLSession/OkHttpClient at init time.
|
|
609
|
-
const ignoreUrls = [apiUrl, '/api/sdk/config', '/api/ingest/presign', '/api/ingest/batch/complete', '/api/ingest/session/end', ...(_storedConfig?.networkIgnoreUrls || [])];
|
|
620
|
+
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 || [])];
|
|
610
621
|
getNetworkInterceptor().initNetworkInterceptor(request => {
|
|
611
622
|
getAutoTracking().trackAPIRequest(request.success || false, request.statusCode, request.duration || 0, request.responseBodySize || 0);
|
|
612
623
|
Rejourney.logNetworkRequest(request);
|
|
@@ -695,17 +706,57 @@ const Rejourney = {
|
|
|
695
706
|
}
|
|
696
707
|
},
|
|
697
708
|
/**
|
|
698
|
-
|
|
709
|
+
/**
|
|
710
|
+
* Set custom session metadata.
|
|
711
|
+
* Can be called with a single key-value pair or an object of properties.
|
|
712
|
+
* Useful for filtering sessions later (e.g., plan: 'premium', role: 'admin').
|
|
713
|
+
* Caps at 100 properties per session.
|
|
714
|
+
*
|
|
715
|
+
* @param keyOrProperties Property name string, or an object containing key-value pairs
|
|
716
|
+
* @param value Property value (if first argument is a string)
|
|
717
|
+
*/
|
|
718
|
+
setMetadata(keyOrProperties, value) {
|
|
719
|
+
if (typeof keyOrProperties === 'string') {
|
|
720
|
+
const key = keyOrProperties;
|
|
721
|
+
if (!key) {
|
|
722
|
+
getLogger().warn('setMetadata requires a non-empty string key');
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (value !== undefined && typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
|
|
726
|
+
getLogger().warn('setMetadata value must be a string, number, or boolean when using a key string');
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
this.logEvent('$user_property', {
|
|
730
|
+
key,
|
|
731
|
+
value
|
|
732
|
+
});
|
|
733
|
+
// Track for session rollover restoration
|
|
734
|
+
_storedMetadata[key] = value;
|
|
735
|
+
} else if (keyOrProperties && typeof keyOrProperties === 'object') {
|
|
736
|
+
const properties = keyOrProperties;
|
|
737
|
+
const validProps = {};
|
|
738
|
+
for (const [k, v] of Object.entries(properties)) {
|
|
739
|
+
if (typeof k === 'string' && k && (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')) {
|
|
740
|
+
validProps[k] = v;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (Object.keys(validProps).length > 0) {
|
|
744
|
+
this.logEvent('$user_property', validProps);
|
|
745
|
+
// Track for session rollover restoration
|
|
746
|
+
Object.assign(_storedMetadata, validProps);
|
|
747
|
+
}
|
|
748
|
+
} else {
|
|
749
|
+
getLogger().warn('setMetadata requires a string key and value, or a properties object');
|
|
750
|
+
}
|
|
751
|
+
},
|
|
752
|
+
/**
|
|
753
|
+
* Track current screen (manual)
|
|
699
754
|
*
|
|
700
755
|
* @param screenName - Screen name
|
|
701
756
|
* @param params - Optional screen parameters
|
|
702
757
|
*/
|
|
703
|
-
|
|
758
|
+
trackScreen(screenName, _params) {
|
|
704
759
|
getAutoTracking().trackScreen(screenName);
|
|
705
|
-
getAutoTracking().notifyStateChange();
|
|
706
|
-
safeNativeCallSync('tagScreen', () => {
|
|
707
|
-
getRejourneyNative().screenChanged(screenName).catch(() => {});
|
|
708
|
-
}, undefined);
|
|
709
760
|
},
|
|
710
761
|
/**
|
|
711
762
|
* Mark a view as sensitive (will be occluded in recordings)
|
|
@@ -1094,13 +1145,109 @@ const Rejourney = {
|
|
|
1094
1145
|
safeNativeCallSync('unmaskView', () => {
|
|
1095
1146
|
getRejourneyNative().unmaskViewByNativeID(nativeID).catch(() => {});
|
|
1096
1147
|
}, undefined);
|
|
1148
|
+
},
|
|
1149
|
+
/**
|
|
1150
|
+
* Initialize Rejourney SDK
|
|
1151
|
+
*/
|
|
1152
|
+
init(publicRouteKey, options) {
|
|
1153
|
+
initRejourney(publicRouteKey, options);
|
|
1154
|
+
},
|
|
1155
|
+
/**
|
|
1156
|
+
* Start recording
|
|
1157
|
+
*/
|
|
1158
|
+
start() {
|
|
1159
|
+
startRejourney();
|
|
1160
|
+
},
|
|
1161
|
+
/**
|
|
1162
|
+
* Stop recording
|
|
1163
|
+
*/
|
|
1164
|
+
stop() {
|
|
1165
|
+
stopRejourney();
|
|
1166
|
+
},
|
|
1167
|
+
/**
|
|
1168
|
+
* Hook for automatic React Navigation tracking.
|
|
1169
|
+
*/
|
|
1170
|
+
useNavigationTracking() {
|
|
1171
|
+
return getAutoTracking().useNavigationTracking();
|
|
1097
1172
|
}
|
|
1098
1173
|
};
|
|
1099
1174
|
|
|
1175
|
+
/**
|
|
1176
|
+
* Reinitialize JS-side auto-tracking for a new session after background timeout.
|
|
1177
|
+
*
|
|
1178
|
+
* When the app was in background for >60s the native layer rolls over to a
|
|
1179
|
+
* fresh session automatically. The JS side must tear down stale tracking
|
|
1180
|
+
* state (metrics, console-log counter, screen history, error handlers) and
|
|
1181
|
+
* re-initialize so that trackScreen, logEvent, setMetadata, etc. work
|
|
1182
|
+
* correctly against the new native session.
|
|
1183
|
+
*/
|
|
1184
|
+
function _reinitAutoTrackingForNewSession() {
|
|
1185
|
+
try {
|
|
1186
|
+
// 1. Tear down old session's auto-tracking state
|
|
1187
|
+
getAutoTracking().cleanupAutoTracking();
|
|
1188
|
+
|
|
1189
|
+
// 2. Re-initialize auto-tracking with the same config
|
|
1190
|
+
getAutoTracking().initAutoTracking({
|
|
1191
|
+
rageTapThreshold: _storedConfig?.rageTapThreshold ?? 3,
|
|
1192
|
+
rageTapTimeWindow: _storedConfig?.rageTapTimeWindow ?? 500,
|
|
1193
|
+
rageTapRadius: 50,
|
|
1194
|
+
trackJSErrors: true,
|
|
1195
|
+
trackPromiseRejections: true,
|
|
1196
|
+
trackReactNativeErrors: true,
|
|
1197
|
+
trackConsoleLogs: _storedConfig?.trackConsoleLogs ?? true,
|
|
1198
|
+
collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false,
|
|
1199
|
+
autoTrackExpoRouter: _storedConfig?.autoTrackExpoRouter !== false
|
|
1200
|
+
}, {
|
|
1201
|
+
onRageTap: (count, x, y) => {
|
|
1202
|
+
Rejourney.logEvent('frustration', {
|
|
1203
|
+
frustrationKind: 'rage_tap',
|
|
1204
|
+
tapCount: count,
|
|
1205
|
+
x,
|
|
1206
|
+
y
|
|
1207
|
+
});
|
|
1208
|
+
getLogger().logFrustration(`Rage tap (${count} taps)`);
|
|
1209
|
+
},
|
|
1210
|
+
onError: error => {
|
|
1211
|
+
getLogger().logError(error.message);
|
|
1212
|
+
},
|
|
1213
|
+
onScreen: (_screenName, _previousScreen) => {}
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
// 3. Re-collect device info for the new session
|
|
1217
|
+
if (_storedConfig?.collectDeviceInfo !== false) {
|
|
1218
|
+
getAutoTracking().collectDeviceInfo().then(deviceInfo => {
|
|
1219
|
+
Rejourney.logEvent('device_info', deviceInfo);
|
|
1220
|
+
}).catch(() => {});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// 4. Re-send user identity to the new native session
|
|
1224
|
+
if (_userIdentity) {
|
|
1225
|
+
safeNativeCallSync('setUserIdentity', () => {
|
|
1226
|
+
getRejourneyNative().setUserIdentity(_userIdentity).catch(() => {});
|
|
1227
|
+
}, undefined);
|
|
1228
|
+
getLogger().debug(`Restored user identity '${_userIdentity}' to new session`);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// 5. Re-send any stored metadata to the new native session
|
|
1232
|
+
if (Object.keys(_storedMetadata).length > 0) {
|
|
1233
|
+
for (const [key, value] of Object.entries(_storedMetadata)) {
|
|
1234
|
+
if (value !== undefined && value !== null) {
|
|
1235
|
+
Rejourney.setMetadata(key, value);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
getLogger().debug('Restored metadata to new session');
|
|
1239
|
+
}
|
|
1240
|
+
getLogger().logLifecycleEvent('JS auto-tracking reinitialized for new session');
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
getLogger().warn('Failed to reinitialize auto-tracking after session rollover:', error);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1100
1246
|
/**
|
|
1101
1247
|
* Handle app state changes for automatic session management
|
|
1102
1248
|
* - Pauses recording when app goes to background
|
|
1103
1249
|
* - Resumes recording when app comes back to foreground
|
|
1250
|
+
* - Reinitializes JS-side auto-tracking when native rolls over to a new session
|
|
1104
1251
|
* - Cleans up properly when app is terminated
|
|
1105
1252
|
*/
|
|
1106
1253
|
function handleAppStateChange(nextAppState) {
|
|
@@ -1109,9 +1256,22 @@ function handleAppStateChange(nextAppState) {
|
|
|
1109
1256
|
if (_currentAppState.match(/active/) && nextAppState === 'background') {
|
|
1110
1257
|
// App going to background - native module handles this automatically
|
|
1111
1258
|
getLogger().logLifecycleEvent('App moving to background');
|
|
1259
|
+
_backgroundEntryTime = Date.now();
|
|
1112
1260
|
} else if (_currentAppState.match(/inactive|background/) && nextAppState === 'active') {
|
|
1113
1261
|
// App coming back to foreground
|
|
1114
1262
|
getLogger().logLifecycleEvent('App returning to foreground');
|
|
1263
|
+
|
|
1264
|
+
// Check if we exceeded the session timeout (60s).
|
|
1265
|
+
// Native side will have already ended the old session and started a new
|
|
1266
|
+
// one — we need to reset JS-side auto-tracking state to match.
|
|
1267
|
+
if (_backgroundEntryTime && _isRecording) {
|
|
1268
|
+
const backgroundDurationMs = Date.now() - _backgroundEntryTime;
|
|
1269
|
+
if (backgroundDurationMs > SESSION_TIMEOUT_MS) {
|
|
1270
|
+
getLogger().debug(`Session rollover: background ${Math.round(backgroundDurationMs / 1000)}s > ${SESSION_TIMEOUT_MS / 1000}s timeout`);
|
|
1271
|
+
_reinitAutoTrackingForNewSession();
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
_backgroundEntryTime = null;
|
|
1115
1275
|
}
|
|
1116
1276
|
_currentAppState = nextAppState;
|
|
1117
1277
|
} catch (error) {
|
|
@@ -1176,8 +1336,10 @@ function setupAuthErrorListener() {
|
|
|
1176
1336
|
}
|
|
1177
1337
|
});
|
|
1178
1338
|
}
|
|
1179
|
-
} catch
|
|
1180
|
-
|
|
1339
|
+
} catch {
|
|
1340
|
+
// Expected on some architectures where NativeEventEmitter isn't fully supported.
|
|
1341
|
+
// Auth errors are still handled synchronously via native callback — this listener
|
|
1342
|
+
// is purely supplementary. No need to log.
|
|
1181
1343
|
}
|
|
1182
1344
|
}
|
|
1183
1345
|
|
|
@@ -1314,7 +1476,7 @@ export function stopRejourney() {
|
|
|
1314
1476
|
}
|
|
1315
1477
|
export default Rejourney;
|
|
1316
1478
|
export * from './types';
|
|
1317
|
-
export { trackTap, trackScroll, trackGesture, trackInput,
|
|
1479
|
+
export { trackTap, trackScroll, trackGesture, trackInput, captureError, getSessionMetrics } from './sdk/autoTracking';
|
|
1318
1480
|
export { trackNavigationState, useNavigationTracking } from './sdk/autoTracking';
|
|
1319
1481
|
export { LogLevel } from './sdk/utils';
|
|
1320
1482
|
|
|
@@ -119,6 +119,7 @@ export function initAutoTracking(trackingConfig, callbacks = {}) {
|
|
|
119
119
|
trackReactNativeErrors: true,
|
|
120
120
|
trackConsoleLogs: true,
|
|
121
121
|
collectDeviceInfo: true,
|
|
122
|
+
autoTrackExpoRouter: true,
|
|
122
123
|
maxSessionDurationMs: trackingConfig.maxSessionDurationMs,
|
|
123
124
|
...trackingConfig
|
|
124
125
|
};
|
|
@@ -568,10 +569,26 @@ function restoreConsoleHandlers() {
|
|
|
568
569
|
// Note: console.error is restored in restoreErrorHandlers via originalConsoleError
|
|
569
570
|
}
|
|
570
571
|
let navigationPollingInterval = null;
|
|
572
|
+
/** Interval ID from optional expo-router entry; cleared in cleanupNavigationTracking */
|
|
573
|
+
let expoRouterPollingIntervalId = null;
|
|
571
574
|
let lastDetectedScreen = '';
|
|
572
575
|
let navigationSetupDone = false;
|
|
573
|
-
|
|
574
|
-
|
|
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
|
+
}
|
|
575
592
|
|
|
576
593
|
/**
|
|
577
594
|
* Track a navigation state change from React Navigation.
|
|
@@ -668,91 +685,94 @@ export function useNavigationTracking() {
|
|
|
668
685
|
}
|
|
669
686
|
|
|
670
687
|
/**
|
|
671
|
-
* Setup automatic
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
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.
|
|
675
697
|
*/
|
|
676
698
|
function setupNavigationTracking() {
|
|
677
699
|
if (navigationSetupDone) return;
|
|
678
700
|
navigationSetupDone = true;
|
|
679
|
-
|
|
680
|
-
|
|
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();
|
|
681
707
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
|
|
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;
|
|
698
729
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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.');
|
|
704
738
|
}
|
|
705
739
|
}
|
|
706
|
-
};
|
|
707
|
-
setTimeout(trySetup, 200);
|
|
740
|
+
}, delay);
|
|
708
741
|
}
|
|
709
742
|
|
|
710
743
|
/**
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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;
|
|
716
752
|
try {
|
|
717
|
-
const expoRouter = require('expo-router');
|
|
718
|
-
const router = expoRouter.router;
|
|
719
|
-
if (!router) {
|
|
720
|
-
if (__DEV__) {
|
|
721
|
-
logger.debug('Expo Router: router object not found');
|
|
722
|
-
}
|
|
723
|
-
return false;
|
|
724
|
-
}
|
|
725
|
-
if (__DEV__) {
|
|
726
|
-
logger.debug('Expo Router: Setting up navigation tracking');
|
|
727
|
-
}
|
|
728
753
|
const {
|
|
729
754
|
normalizeScreenName,
|
|
730
755
|
getScreenNameFromPath
|
|
731
756
|
} = require('./navigation');
|
|
732
|
-
|
|
757
|
+
const intervalId = setInterval(() => {
|
|
733
758
|
try {
|
|
734
759
|
let state = null;
|
|
735
|
-
let stateSource = '';
|
|
736
760
|
if (typeof router.getState === 'function') {
|
|
737
761
|
state = router.getState();
|
|
738
|
-
stateSource = 'router.getState()';
|
|
739
762
|
} else if (router.rootState) {
|
|
740
763
|
state = router.rootState;
|
|
741
|
-
stateSource = 'router.rootState';
|
|
742
764
|
}
|
|
743
765
|
if (!state) {
|
|
744
766
|
try {
|
|
745
|
-
const
|
|
767
|
+
const STORE_PATH = 'expo-router/build/global-state/router-store';
|
|
768
|
+
const storeModule = require(STORE_PATH);
|
|
746
769
|
if (storeModule?.store) {
|
|
747
770
|
state = storeModule.store.state;
|
|
748
|
-
if (state) stateSource = 'store.state';
|
|
749
771
|
if (!state && storeModule.store.navigationRef?.current) {
|
|
750
772
|
state = storeModule.store.navigationRef.current.getRootState?.();
|
|
751
|
-
if (state) stateSource = 'navigationRef.getRootState()';
|
|
752
773
|
}
|
|
753
774
|
if (!state) {
|
|
754
775
|
state = storeModule.store.rootState || storeModule.store.initialState;
|
|
755
|
-
if (state) stateSource = 'store.rootState/initialState';
|
|
756
776
|
}
|
|
757
777
|
}
|
|
758
778
|
} catch {
|
|
@@ -761,67 +781,53 @@ function trySetupExpoRouter() {
|
|
|
761
781
|
}
|
|
762
782
|
if (!state) {
|
|
763
783
|
try {
|
|
764
|
-
const
|
|
784
|
+
const IMPERATIVE_PATH = 'expo-router/build/imperative-api';
|
|
785
|
+
const imperative = require(IMPERATIVE_PATH);
|
|
765
786
|
if (imperative?.router) {
|
|
766
787
|
state = imperative.router.getState?.();
|
|
767
|
-
if (state) stateSource = 'imperative-api';
|
|
768
788
|
}
|
|
769
789
|
} catch {
|
|
770
790
|
// Ignore
|
|
771
791
|
}
|
|
772
792
|
}
|
|
773
793
|
if (state) {
|
|
774
|
-
|
|
775
|
-
navigationPollingErrors = 0;
|
|
794
|
+
pollingErrors = 0;
|
|
776
795
|
const screenName = extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName);
|
|
777
796
|
if (screenName && screenName !== lastDetectedScreen) {
|
|
778
|
-
if (__DEV__) {
|
|
779
|
-
logger.debug('Screen changed:', lastDetectedScreen, '->', screenName, `(source: ${stateSource})`);
|
|
780
|
-
}
|
|
781
797
|
lastDetectedScreen = screenName;
|
|
782
798
|
trackScreen(screenName);
|
|
783
799
|
}
|
|
784
800
|
} else {
|
|
785
|
-
|
|
786
|
-
if (
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
|
|
790
|
-
cleanupNavigationTracking();
|
|
801
|
+
pollingErrors++;
|
|
802
|
+
if (pollingErrors >= MAX_POLLING_ERRORS) {
|
|
803
|
+
clearInterval(intervalId);
|
|
804
|
+
expoRouterPollingIntervalId = null;
|
|
791
805
|
}
|
|
792
806
|
}
|
|
793
|
-
} catch
|
|
794
|
-
|
|
795
|
-
if (
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
|
|
799
|
-
cleanupNavigationTracking();
|
|
807
|
+
} catch {
|
|
808
|
+
pollingErrors++;
|
|
809
|
+
if (pollingErrors >= MAX_POLLING_ERRORS) {
|
|
810
|
+
clearInterval(intervalId);
|
|
811
|
+
expoRouterPollingIntervalId = null;
|
|
800
812
|
}
|
|
801
813
|
}
|
|
802
814
|
}, 500);
|
|
803
|
-
|
|
804
|
-
} catch
|
|
805
|
-
|
|
806
|
-
logger.debug('Expo Router not available:', e);
|
|
807
|
-
}
|
|
808
|
-
return false;
|
|
815
|
+
expoRouterPollingIntervalId = intervalId;
|
|
816
|
+
} catch {
|
|
817
|
+
// navigation module not available — ignore
|
|
809
818
|
}
|
|
810
819
|
}
|
|
811
820
|
|
|
812
821
|
/**
|
|
813
|
-
* Extract screen name from
|
|
814
|
-
*
|
|
815
|
-
* Handles complex nested structures like Drawer → Tabs → Stack
|
|
816
|
-
* by recursively accumulating segments from each navigation level.
|
|
822
|
+
* Extract the active screen name from expo-router navigation state.
|
|
817
823
|
*/
|
|
818
|
-
function extractScreenNameFromRouterState(state,
|
|
824
|
+
function extractScreenNameFromRouterState(state, getScreenNameFromPathFn, normalizeScreenNameFn, accumulatedSegments = []) {
|
|
819
825
|
if (!state?.routes) return null;
|
|
820
826
|
const route = state.routes[state.index ?? state.routes.length - 1];
|
|
821
827
|
if (!route) return null;
|
|
822
828
|
const newSegments = [...accumulatedSegments, route.name];
|
|
823
829
|
if (route.state) {
|
|
824
|
-
return extractScreenNameFromRouterState(route.state,
|
|
830
|
+
return extractScreenNameFromRouterState(route.state, getScreenNameFromPathFn, normalizeScreenNameFn, newSegments);
|
|
825
831
|
}
|
|
826
832
|
const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
|
|
827
833
|
if (cleanSegments.length === 0) {
|
|
@@ -834,7 +840,7 @@ function extractScreenNameFromRouterState(state, getScreenNameFromPath, normaliz
|
|
|
834
840
|
}
|
|
835
841
|
}
|
|
836
842
|
const pathname = '/' + cleanSegments.join('/');
|
|
837
|
-
return
|
|
843
|
+
return getScreenNameFromPathFn(pathname, newSegments);
|
|
838
844
|
}
|
|
839
845
|
|
|
840
846
|
/**
|
|
@@ -845,9 +851,12 @@ function cleanupNavigationTracking() {
|
|
|
845
851
|
clearInterval(navigationPollingInterval);
|
|
846
852
|
navigationPollingInterval = null;
|
|
847
853
|
}
|
|
854
|
+
if (expoRouterPollingIntervalId != null) {
|
|
855
|
+
clearInterval(expoRouterPollingIntervalId);
|
|
856
|
+
expoRouterPollingIntervalId = null;
|
|
857
|
+
}
|
|
848
858
|
navigationSetupDone = false;
|
|
849
859
|
lastDetectedScreen = '';
|
|
850
|
-
navigationPollingErrors = 0;
|
|
851
860
|
}
|
|
852
861
|
|
|
853
862
|
/**
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=expoRouterTracking.d.ts.map
|
|
@@ -26,7 +26,7 @@ import type { RejourneyConfig, RejourneyAPI } from './types';
|
|
|
26
26
|
/**
|
|
27
27
|
* Main Rejourney API (Internal)
|
|
28
28
|
*/
|
|
29
|
-
declare const Rejourney: RejourneyAPI;
|
|
29
|
+
export declare const Rejourney: RejourneyAPI;
|
|
30
30
|
/**
|
|
31
31
|
* Initialize Rejourney SDK - STEP 1 of 3
|
|
32
32
|
*
|
|
@@ -81,7 +81,7 @@ export declare function startRejourney(): void;
|
|
|
81
81
|
export declare function stopRejourney(): void;
|
|
82
82
|
export default Rejourney;
|
|
83
83
|
export * from './types';
|
|
84
|
-
export { trackTap, trackScroll, trackGesture, trackInput,
|
|
84
|
+
export { trackTap, trackScroll, trackGesture, trackInput, captureError, getSessionMetrics, } from './sdk/autoTracking';
|
|
85
85
|
export { trackNavigationState, useNavigationTracking } from './sdk/autoTracking';
|
|
86
86
|
export { LogLevel } from './sdk/utils';
|
|
87
87
|
/**
|
|
@@ -68,6 +68,7 @@ export interface AutoTrackingConfig {
|
|
|
68
68
|
collectDeviceInfo?: boolean;
|
|
69
69
|
maxSessionDurationMs?: number;
|
|
70
70
|
detectDeadTaps?: boolean;
|
|
71
|
+
autoTrackExpoRouter?: boolean;
|
|
71
72
|
}
|
|
72
73
|
/**
|
|
73
74
|
* Mark a tap as handled.
|
|
@@ -101,6 +102,16 @@ export declare function notifyStateChange(): void;
|
|
|
101
102
|
* Manually track an error (for API errors, etc.)
|
|
102
103
|
*/
|
|
103
104
|
export declare function captureError(message: string, stack?: string, name?: string): void;
|
|
105
|
+
/**
|
|
106
|
+
* Register the polling interval from the optional expo-router entry so we can clear it on cleanup.
|
|
107
|
+
* Used by src/expoRouterTracking.ts (only loaded when app imports '@rejourneyco/react-native/expo-router').
|
|
108
|
+
*/
|
|
109
|
+
export declare function setExpoRouterPollingInterval(id: ReturnType<typeof setInterval> | null): void;
|
|
110
|
+
/**
|
|
111
|
+
* Check if Expo Router auto-tracking is enabled in the current configuration.
|
|
112
|
+
* Used by src/expoRouterTracking.ts.
|
|
113
|
+
*/
|
|
114
|
+
export declare function isExpoRouterTrackingEnabled(): boolean;
|
|
104
115
|
/**
|
|
105
116
|
* Track a navigation state change from React Navigation.
|
|
106
117
|
*
|