@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
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
  */
@@ -635,7 +642,9 @@ const Rejourney: RejourneyAPI = {
635
642
  trackJSErrors: true,
636
643
  trackPromiseRejections: true,
637
644
  trackReactNativeErrors: true,
645
+ trackConsoleLogs: _storedConfig?.trackConsoleLogs ?? true,
638
646
  collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false,
647
+ autoTrackExpoRouter: _storedConfig?.autoTrackExpoRouter !== false,
639
648
  },
640
649
  {
641
650
  // Rage tap callback - log as frustration event
@@ -648,13 +657,8 @@ const Rejourney: RejourneyAPI = {
648
657
  });
649
658
  getLogger().logFrustration(`Rage tap (${count} taps)`);
650
659
  },
651
- // Error callback - log as error event
660
+ // Error callback - SDK forwarding is handled in autoTracking.trackError
652
661
  onError: (error: { message: string; stack?: string; name?: string }) => {
653
- this.logEvent('error', {
654
- message: error.message,
655
- stack: error.stack,
656
- name: error.name,
657
- });
658
662
  getLogger().logError(error.message);
659
663
  },
660
664
  onScreen: (_screenName: string, _previousScreen?: string) => {
@@ -673,12 +677,19 @@ const Rejourney: RejourneyAPI = {
673
677
 
674
678
  if (_storedConfig?.autoTrackNetwork !== false) {
675
679
  try {
680
+ // JS-level fetch/XHR patching is the primary mechanism for capturing network
681
+ // calls within React Native. Native interceptors (RejourneyURLProtocol on iOS,
682
+ // RejourneyNetworkInterceptor on Android) are supplementary — they capture
683
+ // native-originated HTTP calls that bypass JS fetch(), but cannot intercept
684
+ // RN's own networking since it creates its NSURLSession/OkHttpClient at init time.
676
685
  const ignoreUrls: (string | RegExp)[] = [
677
686
  apiUrl,
678
687
  '/api/sdk/config',
679
688
  '/api/ingest/presign',
680
689
  '/api/ingest/batch/complete',
681
690
  '/api/ingest/session/end',
691
+ '/api/ingest/segment/presign',
692
+ '/api/ingest/segment/complete',
682
693
  ...(_storedConfig?.networkIgnoreUrls || []),
683
694
  ];
684
695
 
@@ -805,22 +816,56 @@ const Rejourney: RejourneyAPI = {
805
816
  },
806
817
 
807
818
  /**
808
- * 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)
809
863
  *
810
864
  * @param screenName - Screen name
811
865
  * @param params - Optional screen parameters
812
866
  */
813
- tagScreen(screenName: string, _params?: Record<string, unknown>): void {
867
+ trackScreen(screenName: string, _params?: Record<string, unknown>): void {
814
868
  getAutoTracking().trackScreen(screenName);
815
- getAutoTracking().notifyStateChange();
816
-
817
- safeNativeCallSync(
818
- 'tagScreen',
819
- () => {
820
- getRejourneyNative()!.screenChanged(screenName).catch(() => { });
821
- },
822
- undefined
823
- );
824
869
  },
825
870
 
826
871
  /**
@@ -1154,6 +1199,28 @@ const Rejourney: RejourneyAPI = {
1154
1199
  );
1155
1200
  },
1156
1201
 
1202
+ /**
1203
+ * Log customer feedback (e.g. from an in-app survey or NPS widget).
1204
+ *
1205
+ * @param rating - Numeric rating (e.g. 1 to 5)
1206
+ * @param message - Associated feedback text or comment
1207
+ */
1208
+ logFeedback(rating: number, message: string): void {
1209
+ safeNativeCallSync(
1210
+ 'logFeedback',
1211
+ () => {
1212
+ const feedbackEvent = {
1213
+ type: 'feedback',
1214
+ timestamp: Date.now(),
1215
+ rating,
1216
+ message,
1217
+ };
1218
+ getRejourneyNative()!.logEvent('feedback', feedbackEvent).catch(() => { });
1219
+ },
1220
+ undefined
1221
+ );
1222
+ },
1223
+
1157
1224
  /**
1158
1225
  * Get SDK telemetry metrics for observability
1159
1226
  *
@@ -1194,21 +1261,16 @@ const Rejourney: RejourneyAPI = {
1194
1261
  },
1195
1262
 
1196
1263
  /**
1197
- * Trigger a debug ANR (Dev only)
1198
- * Blocks the main thread for the specified duration
1264
+ * Trigger an ANR test by blocking the main thread for the specified duration.
1199
1265
  */
1200
1266
  debugTriggerANR(durationMs: number): void {
1201
- if (__DEV__) {
1202
- safeNativeCallSync(
1203
- 'debugTriggerANR',
1204
- () => {
1205
- getRejourneyNative()!.debugTriggerANR(durationMs);
1206
- },
1207
- undefined
1208
- );
1209
- } else {
1210
- getLogger().warn('debugTriggerANR is only available in development mode');
1211
- }
1267
+ safeNativeCallSync(
1268
+ 'debugTriggerANR',
1269
+ () => {
1270
+ getRejourneyNative()!.debugTriggerANR(durationMs);
1271
+ },
1272
+ undefined
1273
+ );
1212
1274
  },
1213
1275
 
1214
1276
  /**
@@ -1253,12 +1315,121 @@ const Rejourney: RejourneyAPI = {
1253
1315
  undefined
1254
1316
  );
1255
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
+ },
1256
1346
  };
1257
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
+
1258
1428
  /**
1259
1429
  * Handle app state changes for automatic session management
1260
1430
  * - Pauses recording when app goes to background
1261
1431
  * - Resumes recording when app comes back to foreground
1432
+ * - Reinitializes JS-side auto-tracking when native rolls over to a new session
1262
1433
  * - Cleans up properly when app is terminated
1263
1434
  */
1264
1435
  function handleAppStateChange(nextAppState: string): void {
@@ -1268,9 +1439,24 @@ function handleAppStateChange(nextAppState: string): void {
1268
1439
  if (_currentAppState.match(/active/) && nextAppState === 'background') {
1269
1440
  // App going to background - native module handles this automatically
1270
1441
  getLogger().logLifecycleEvent('App moving to background');
1442
+ _backgroundEntryTime = Date.now();
1271
1443
  } else if (_currentAppState.match(/inactive|background/) && nextAppState === 'active') {
1272
1444
  // App coming back to foreground
1273
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;
1274
1460
  }
1275
1461
  _currentAppState = nextAppState;
1276
1462
  } catch (error) {
@@ -1353,8 +1539,10 @@ function setupAuthErrorListener(): void {
1353
1539
  }
1354
1540
  );
1355
1541
  }
1356
- } catch (error) {
1357
- 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.
1358
1546
  }
1359
1547
  }
1360
1548
 
@@ -1500,7 +1688,6 @@ export function stopRejourney(): void {
1500
1688
  getLogger().warn('Error stopping Rejourney:', error);
1501
1689
  }
1502
1690
  }
1503
-
1504
1691
  export default Rejourney;
1505
1692
 
1506
1693
  export * from './types';
@@ -1510,7 +1697,6 @@ export {
1510
1697
  trackScroll,
1511
1698
  trackGesture,
1512
1699
  trackInput,
1513
- trackScreen,
1514
1700
  captureError,
1515
1701
  getSessionMetrics,
1516
1702
  } from './sdk/autoTracking';