@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
@@ -26,8 +26,8 @@ extension Data {
26
26
  stream.avail_in = uint(self.count)
27
27
  stream.total_out = 0
28
28
 
29
- // MAX_WBITS + 16 = gzip format
30
- if deflateInit2_(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS + 16, 8, Z_DEFAULT_STRATEGY, ZLIB_VERSION, Int32(MemoryLayout<z_stream>.size)) != Z_OK {
29
+ // MAX_WBITS + 16 = gzip format; level 9 for best ratio (smaller S3 payloads)
30
+ if deflateInit2_(&stream, 9, Z_DEFLATED, MAX_WBITS + 16, 8, Z_DEFAULT_STRATEGY, ZLIB_VERSION, Int32(MemoryLayout<z_stream>.size)) != Z_OK {
31
31
  return nil
32
32
  }
33
33
 
@@ -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
  */
@@ -681,7 +686,8 @@ const Rejourney = {
681
686
  trackPromiseRejections: true,
682
687
  trackReactNativeErrors: true,
683
688
  trackConsoleLogs: _storedConfig?.trackConsoleLogs ?? true,
684
- collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false
689
+ collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false,
690
+ autoTrackExpoRouter: _storedConfig?.autoTrackExpoRouter !== false
685
691
  }, {
686
692
  // Rage tap callback - log as frustration event
687
693
  onRageTap: (count, x, y) => {
@@ -714,7 +720,7 @@ const Rejourney = {
714
720
  // RejourneyNetworkInterceptor on Android) are supplementary — they capture
715
721
  // native-originated HTTP calls that bypass JS fetch(), but cannot intercept
716
722
  // RN's own networking since it creates its NSURLSession/OkHttpClient at init time.
717
- const ignoreUrls = [apiUrl, '/api/sdk/config', '/api/ingest/presign', '/api/ingest/batch/complete', '/api/ingest/session/end', ...(_storedConfig?.networkIgnoreUrls || [])];
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 || [])];
718
724
  getNetworkInterceptor().initNetworkInterceptor(request => {
719
725
  getAutoTracking().trackAPIRequest(request.success || false, request.statusCode, request.duration || 0, request.responseBodySize || 0);
720
726
  Rejourney.logNetworkRequest(request);
@@ -803,17 +809,57 @@ const Rejourney = {
803
809
  }
804
810
  },
805
811
  /**
806
- * 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)
807
857
  *
808
858
  * @param screenName - Screen name
809
859
  * @param params - Optional screen parameters
810
860
  */
811
- tagScreen(screenName, _params) {
861
+ trackScreen(screenName, _params) {
812
862
  getAutoTracking().trackScreen(screenName);
813
- getAutoTracking().notifyStateChange();
814
- safeNativeCallSync('tagScreen', () => {
815
- getRejourneyNative().screenChanged(screenName).catch(() => {});
816
- }, undefined);
817
863
  },
818
864
  /**
819
865
  * Mark a view as sensitive (will be occluded in recordings)
@@ -1202,13 +1248,109 @@ const Rejourney = {
1202
1248
  safeNativeCallSync('unmaskView', () => {
1203
1249
  getRejourneyNative().unmaskViewByNativeID(nativeID).catch(() => {});
1204
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();
1205
1275
  }
1206
1276
  };
1207
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
+
1208
1349
  /**
1209
1350
  * Handle app state changes for automatic session management
1210
1351
  * - Pauses recording when app goes to background
1211
1352
  * - Resumes recording when app comes back to foreground
1353
+ * - Reinitializes JS-side auto-tracking when native rolls over to a new session
1212
1354
  * - Cleans up properly when app is terminated
1213
1355
  */
1214
1356
  function handleAppStateChange(nextAppState) {
@@ -1217,9 +1359,22 @@ function handleAppStateChange(nextAppState) {
1217
1359
  if (_currentAppState.match(/active/) && nextAppState === 'background') {
1218
1360
  // App going to background - native module handles this automatically
1219
1361
  getLogger().logLifecycleEvent('App moving to background');
1362
+ _backgroundEntryTime = Date.now();
1220
1363
  } else if (_currentAppState.match(/inactive|background/) && nextAppState === 'active') {
1221
1364
  // App coming back to foreground
1222
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;
1223
1378
  }
1224
1379
  _currentAppState = nextAppState;
1225
1380
  } catch (error) {
@@ -1284,8 +1439,10 @@ function setupAuthErrorListener() {
1284
1439
  }
1285
1440
  });
1286
1441
  }
1287
- } catch (error) {
1288
- 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.
1289
1446
  }
1290
1447
  }
1291
1448