@rejourneyco/react-native 1.0.8 → 1.0.9

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 (42) hide show
  1. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +89 -8
  2. package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +18 -3
  3. package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +69 -17
  4. package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +27 -2
  5. package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +3 -1
  6. package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +100 -0
  7. package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +222 -145
  8. package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +4 -0
  9. package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +3 -0
  10. package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +13 -0
  11. package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +8 -0
  12. package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +95 -21
  13. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +14 -0
  14. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +18 -0
  15. package/ios/Engine/DeviceRegistrar.swift +13 -3
  16. package/ios/Engine/RejourneyImpl.swift +199 -115
  17. package/ios/Recording/AnrSentinel.swift +58 -25
  18. package/ios/Recording/InteractionRecorder.swift +1 -0
  19. package/ios/Recording/RejourneyURLProtocol.swift +168 -0
  20. package/ios/Recording/ReplayOrchestrator.swift +204 -143
  21. package/ios/Recording/SegmentDispatcher.swift +8 -0
  22. package/ios/Recording/StabilityMonitor.swift +40 -32
  23. package/ios/Recording/TelemetryPipeline.swift +17 -0
  24. package/ios/Recording/ViewHierarchyScanner.swift +1 -0
  25. package/ios/Recording/VisualCapture.swift +54 -8
  26. package/ios/Rejourney.mm +27 -8
  27. package/ios/Utility/ImageBlur.swift +0 -1
  28. package/lib/commonjs/index.js +28 -15
  29. package/lib/commonjs/sdk/autoTracking.js +162 -11
  30. package/lib/commonjs/sdk/networkInterceptor.js +84 -4
  31. package/lib/module/index.js +28 -15
  32. package/lib/module/sdk/autoTracking.js +162 -11
  33. package/lib/module/sdk/networkInterceptor.js +84 -4
  34. package/lib/typescript/NativeRejourney.d.ts +5 -2
  35. package/lib/typescript/sdk/autoTracking.d.ts +3 -1
  36. package/lib/typescript/types/index.d.ts +14 -2
  37. package/package.json +4 -4
  38. package/src/NativeRejourney.ts +8 -5
  39. package/src/index.ts +37 -19
  40. package/src/sdk/autoTracking.ts +176 -11
  41. package/src/sdk/networkInterceptor.ts +110 -1
  42. package/src/types/index.ts +15 -3
@@ -102,6 +102,7 @@ let originalOnError = null;
102
102
  let originalOnUnhandledRejection = null;
103
103
  let originalConsoleError = null;
104
104
  let _promiseRejectionTrackingDisable = null;
105
+ const FATAL_ERROR_FLUSH_DELAY_MS = 1200;
105
106
 
106
107
  /**
107
108
  * Initialize auto tracking features
@@ -116,6 +117,7 @@ export function initAutoTracking(trackingConfig, callbacks = {}) {
116
117
  trackJSErrors: true,
117
118
  trackPromiseRejections: true,
118
119
  trackReactNativeErrors: true,
120
+ trackConsoleLogs: true,
119
121
  collectDeviceInfo: true,
120
122
  maxSessionDurationMs: trackingConfig.maxSessionDurationMs,
121
123
  ...trackingConfig
@@ -126,6 +128,9 @@ export function initAutoTracking(trackingConfig, callbacks = {}) {
126
128
  onErrorCaptured = callbacks.onError || null;
127
129
  onScreenChange = callbacks.onScreen || null;
128
130
  setupErrorTracking();
131
+ if (config.trackConsoleLogs) {
132
+ setupConsoleTracking();
133
+ }
129
134
  setupNavigationTracking();
130
135
  loadAnonymousId().then(id => {
131
136
  anonymousId = id;
@@ -139,11 +144,13 @@ export function initAutoTracking(trackingConfig, callbacks = {}) {
139
144
  export function cleanupAutoTracking() {
140
145
  if (!isInitialized) return;
141
146
  restoreErrorHandlers();
147
+ restoreConsoleHandlers();
142
148
  cleanupNavigationTracking();
143
149
 
144
150
  // Reset state
145
151
  tapHead = 0;
146
152
  tapCount = 0;
153
+ consoleLogCount = 0;
147
154
  metrics = createEmptyMetrics();
148
155
  screensVisited = [];
149
156
  currentScreen = '';
@@ -257,7 +264,7 @@ function setupErrorTracking() {
257
264
  /**
258
265
  * Setup React Native ErrorUtils handler
259
266
  *
260
- * CRITICAL FIX: For fatal errors, we delay calling the original handler by 500ms
267
+ * CRITICAL FIX: For fatal errors, we delay calling the original handler briefly
261
268
  * to give the React Native bridge time to flush the logEvent('error') call to the
262
269
  * native TelemetryPipeline. Without this delay, the error event is queued on the
263
270
  * JS→native bridge but the app crashes (via originalErrorHandler) before the bridge
@@ -281,10 +288,10 @@ function setupReactNativeErrorHandler() {
281
288
  if (isFatal) {
282
289
  // For fatal errors, delay the original handler so the native bridge
283
290
  // has time to deliver the error event to TelemetryPipeline before
284
- // the app terminates. 500ms is enough for the bridge to flush.
291
+ // the app terminates.
285
292
  setTimeout(() => {
286
293
  originalErrorHandler(error, isFatal);
287
- }, 500);
294
+ }, FATAL_ERROR_FLUSH_DELAY_MS);
288
295
  } else {
289
296
  originalErrorHandler(error, isFatal);
290
297
  }
@@ -339,7 +346,6 @@ function setupPromiseRejectionHandler() {
339
346
 
340
347
  // Strategy 1: RN-specific promise rejection tracking polyfill
341
348
  try {
342
- // eslint-disable-next-line @typescript-eslint/no-var-requires
343
349
  const tracking = require('promise/setimmediate/rejection-tracking');
344
350
  if (tracking && typeof tracking.enable === 'function') {
345
351
  tracking.enable({
@@ -448,8 +454,27 @@ function restoreErrorHandlers() {
448
454
  function trackError(error) {
449
455
  metrics.errorCount++;
450
456
  metrics.totalEvents++;
457
+ forwardErrorToNative(error);
451
458
  if (onErrorCaptured) {
452
- onErrorCaptured(error);
459
+ try {
460
+ onErrorCaptured(error);
461
+ } catch {
462
+ // Ignore callback exceptions so SDK error forwarding keeps working.
463
+ }
464
+ }
465
+ }
466
+ function forwardErrorToNative(error) {
467
+ try {
468
+ const nativeModule = getRejourneyNativeModule();
469
+ if (!nativeModule || typeof nativeModule.logEvent !== 'function') return;
470
+ nativeModule.logEvent('error', {
471
+ message: error.message,
472
+ stack: error.stack,
473
+ name: error.name || 'Error',
474
+ timestamp: error.timestamp
475
+ }).catch(() => {});
476
+ } catch {
477
+ // Ignore native forwarding failures; SDK should never crash app code.
453
478
  }
454
479
  }
455
480
 
@@ -465,6 +490,83 @@ export function captureError(message, stack, name) {
465
490
  name: name || 'Error'
466
491
  });
467
492
  }
493
+ let originalConsoleLog = null;
494
+ let originalConsoleInfo = null;
495
+ let originalConsoleWarn = null;
496
+
497
+ // Cap console logs to prevent flooding the event pipeline
498
+ const MAX_CONSOLE_LOGS_PER_SESSION = 1000;
499
+ let consoleLogCount = 0;
500
+
501
+ /**
502
+ * Setup console tracking to capture log statements
503
+ */
504
+ function setupConsoleTracking() {
505
+ if (typeof console === 'undefined') return;
506
+ if (!originalConsoleLog) originalConsoleLog = console.log;
507
+ if (!originalConsoleInfo) originalConsoleInfo = console.info;
508
+ if (!originalConsoleWarn) originalConsoleWarn = console.warn;
509
+ const createConsoleInterceptor = (level, originalFn) => {
510
+ return (...args) => {
511
+ try {
512
+ const message = args.map(arg => {
513
+ if (typeof arg === 'string') return arg;
514
+ if (arg instanceof Error) return `${arg.name}: ${arg.message}${arg.stack ? `\n...` : ''}`;
515
+ try {
516
+ return JSON.stringify(arg);
517
+ } catch {
518
+ return String(arg);
519
+ }
520
+ }).join(' ');
521
+
522
+ // Enforce per-session cap and skip React Native unhandled-rejection noise.
523
+ if (consoleLogCount < MAX_CONSOLE_LOGS_PER_SESSION && !message.includes('Possible Unhandled Promise Rejection')) {
524
+ consoleLogCount++;
525
+ const nativeModule = getRejourneyNativeModule();
526
+ if (nativeModule) {
527
+ const logEvent = {
528
+ type: 'log',
529
+ timestamp: Date.now(),
530
+ level,
531
+ message: message.length > 2000 ? message.substring(0, 2000) + '...' : message
532
+ };
533
+ nativeModule.logEvent('log', logEvent).catch(() => {});
534
+ }
535
+ }
536
+ } catch {
537
+ // Ignore any errors during interception
538
+ }
539
+ if (originalFn) {
540
+ originalFn.apply(console, args);
541
+ }
542
+ };
543
+ };
544
+ console.log = createConsoleInterceptor('log', originalConsoleLog);
545
+ console.info = createConsoleInterceptor('info', originalConsoleInfo);
546
+ console.warn = createConsoleInterceptor('warn', originalConsoleWarn);
547
+ const currentConsoleError = console.error;
548
+ if (!originalConsoleError) originalConsoleError = currentConsoleError;
549
+ console.error = createConsoleInterceptor('error', currentConsoleError);
550
+ }
551
+
552
+ /**
553
+ * Restore console standard functions
554
+ */
555
+ function restoreConsoleHandlers() {
556
+ if (originalConsoleLog) {
557
+ console.log = originalConsoleLog;
558
+ originalConsoleLog = null;
559
+ }
560
+ if (originalConsoleInfo) {
561
+ console.info = originalConsoleInfo;
562
+ originalConsoleInfo = null;
563
+ }
564
+ if (originalConsoleWarn) {
565
+ console.warn = originalConsoleWarn;
566
+ originalConsoleWarn = null;
567
+ }
568
+ // Note: console.error is restored in restoreErrorHandlers via originalConsoleError
569
+ }
468
570
  let navigationPollingInterval = null;
469
571
  let lastDetectedScreen = '';
470
572
  let navigationSetupDone = false;
@@ -988,7 +1090,26 @@ export async function collectDeviceInfo() {
988
1090
  function generateAnonymousId() {
989
1091
  const timestamp = Date.now().toString(36);
990
1092
  const random = Math.random().toString(36).substring(2, 15);
991
- return `anon_${timestamp}_${random}`;
1093
+ const id = `anon_${timestamp}_${random}`;
1094
+ // Persist so the same ID survives app restarts
1095
+ _persistAnonymousId(id);
1096
+ return id;
1097
+ }
1098
+
1099
+ /**
1100
+ * Best-effort async persist of anonymous ID to native storage
1101
+ */
1102
+ function _persistAnonymousId(id) {
1103
+ const nativeModule = getRejourneyNativeModule();
1104
+ if (!nativeModule?.setAnonymousId) return;
1105
+ try {
1106
+ const result = nativeModule.setAnonymousId(id);
1107
+ if (result && typeof result.catch === 'function') {
1108
+ result.catch(() => {});
1109
+ }
1110
+ } catch {
1111
+ // Native storage unavailable — ID will still be stable for this session
1112
+ }
992
1113
  }
993
1114
 
994
1115
  /**
@@ -1019,17 +1140,41 @@ export async function ensurePersistentAnonymousId() {
1019
1140
 
1020
1141
  /**
1021
1142
  * Load anonymous ID from persistent storage
1022
- * Call this at app startup for best results
1143
+ * Checks native anonymous storage first, then falls back to native getUserIdentity,
1144
+ * and finally generates a new ID if nothing is persisted.
1023
1145
  */
1024
1146
  export async function loadAnonymousId() {
1025
1147
  const nativeModule = getRejourneyNativeModule();
1026
- if (nativeModule && nativeModule.getUserIdentity) {
1148
+
1149
+ // 1. Try native anonymous ID storage
1150
+ if (nativeModule?.getAnonymousId) {
1027
1151
  try {
1028
- return (await nativeModule.getUserIdentity()) || generateAnonymousId();
1152
+ const stored = await nativeModule.getAnonymousId();
1153
+ if (stored && typeof stored === 'string') return stored;
1029
1154
  } catch {
1030
- return generateAnonymousId();
1155
+ // Continue to fallbacks
1031
1156
  }
1032
1157
  }
1158
+
1159
+ // 2. Backward compatibility fallback for older native modules
1160
+ if (nativeModule?.getUserIdentity) {
1161
+ try {
1162
+ const nativeId = await nativeModule.getUserIdentity();
1163
+ if (nativeId && typeof nativeId === 'string') {
1164
+ const normalized = nativeId.trim();
1165
+ // Only migrate legacy anonymous identifiers. Never treat explicit user identities
1166
+ // as anonymous fingerprints, or session correlation becomes unstable.
1167
+ if (normalized.startsWith('anon_')) {
1168
+ _persistAnonymousId(normalized);
1169
+ return normalized;
1170
+ }
1171
+ }
1172
+ } catch {
1173
+ // Continue to fallback
1174
+ }
1175
+ }
1176
+
1177
+ // 3. Generate and persist new ID
1033
1178
  return generateAnonymousId();
1034
1179
  }
1035
1180
 
@@ -1037,7 +1182,13 @@ export async function loadAnonymousId() {
1037
1182
  * Set a custom anonymous ID
1038
1183
  */
1039
1184
  export function setAnonymousId(id) {
1040
- anonymousId = id;
1185
+ const normalized = (id || '').trim();
1186
+ if (!normalized) {
1187
+ anonymousId = generateAnonymousId();
1188
+ return;
1189
+ }
1190
+ anonymousId = normalized;
1191
+ _persistAnonymousId(normalized);
1041
1192
  }
1042
1193
  export default {
1043
1194
  init: initAutoTracking,
@@ -50,6 +50,73 @@ const config = {
50
50
  captureSizes: false
51
51
  };
52
52
  const SENSITIVE_KEYS = ['token', 'key', 'secret', 'password', 'auth', 'access_token', 'api_key'];
53
+ function getUtf8Size(text) {
54
+ if (!text) return 0;
55
+ if (typeof TextEncoder !== 'undefined') {
56
+ return new TextEncoder().encode(text).length;
57
+ }
58
+ return text.length;
59
+ }
60
+ function getBodySize(body) {
61
+ if (body == null) return 0;
62
+ if (typeof body === 'string') return getUtf8Size(body);
63
+ if (typeof ArrayBuffer !== 'undefined' && body instanceof ArrayBuffer) {
64
+ return body.byteLength;
65
+ }
66
+ if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(body)) {
67
+ return body.byteLength;
68
+ }
69
+ if (typeof Blob !== 'undefined' && body instanceof Blob) {
70
+ return body.size;
71
+ }
72
+ if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
73
+ return getUtf8Size(body.toString());
74
+ }
75
+ return 0;
76
+ }
77
+ async function getFetchResponseSize(response) {
78
+ const contentLength = response.headers?.get?.('content-length');
79
+ if (contentLength) {
80
+ const parsed = parseInt(contentLength, 10);
81
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
82
+ }
83
+ try {
84
+ const cloned = response.clone();
85
+ const buffer = await cloned.arrayBuffer();
86
+ return buffer.byteLength;
87
+ } catch {
88
+ return 0;
89
+ }
90
+ }
91
+ function getXhrResponseSize(xhr) {
92
+ try {
93
+ const contentLength = xhr.getResponseHeader('content-length');
94
+ if (contentLength) {
95
+ const parsed = parseInt(contentLength, 10);
96
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
97
+ }
98
+ } catch {
99
+ // Ignore header access errors and fall through to body inspection.
100
+ }
101
+ const responseType = xhr.responseType;
102
+ if (responseType === '' || responseType === 'text') {
103
+ return getUtf8Size(xhr.responseText || '');
104
+ }
105
+ if (responseType === 'arraybuffer') {
106
+ return typeof ArrayBuffer !== 'undefined' && xhr.response instanceof ArrayBuffer ? xhr.response.byteLength : 0;
107
+ }
108
+ if (responseType === 'blob') {
109
+ return typeof Blob !== 'undefined' && xhr.response instanceof Blob ? xhr.response.size : 0;
110
+ }
111
+ if (responseType === 'json') {
112
+ try {
113
+ return getUtf8Size(JSON.stringify(xhr.response ?? ''));
114
+ } catch {
115
+ return 0;
116
+ }
117
+ }
118
+ return 0;
119
+ }
53
120
 
54
121
  /**
55
122
  * Scrub sensitive data from URL
@@ -200,7 +267,9 @@ function interceptFetch() {
200
267
  }
201
268
  const startTime = Date.now();
202
269
  const method = (init?.method || 'GET').toUpperCase();
203
- return originalFetch(input, init).then(response => {
270
+ const requestBodySize = config.captureSizes ? getBodySize(init?.body) : 0;
271
+ return originalFetch(input, init).then(async response => {
272
+ const responseBodySize = config.captureSizes ? await getFetchResponseSize(response) : 0;
204
273
  queueRequest({
205
274
  requestId: `f${startTime}`,
206
275
  method,
@@ -209,7 +278,9 @@ function interceptFetch() {
209
278
  duration: Date.now() - startTime,
210
279
  startTimestamp: startTime,
211
280
  endTimestamp: Date.now(),
212
- success: response.ok
281
+ success: response.ok,
282
+ requestBodySize,
283
+ responseBodySize
213
284
  });
214
285
  return response;
215
286
  }, error => {
@@ -222,7 +293,8 @@ function interceptFetch() {
222
293
  startTimestamp: startTime,
223
294
  endTimestamp: Date.now(),
224
295
  success: false,
225
- errorMessage: error?.message || 'Network error'
296
+ errorMessage: error?.message || 'Network error',
297
+ requestBodySize
226
298
  });
227
299
  throw error;
228
300
  });
@@ -257,9 +329,15 @@ function interceptXHR() {
257
329
  if (!shouldSampleRequest(path)) {
258
330
  return originalXHRSend.call(this, body);
259
331
  }
332
+ if (config.captureSizes && body) {
333
+ data.reqSize = getBodySize(body);
334
+ } else {
335
+ data.reqSize = 0;
336
+ }
260
337
  data.t = Date.now();
261
338
  const onComplete = () => {
262
339
  const endTime = Date.now();
340
+ const responseBodySize = config.captureSizes ? getXhrResponseSize(this) : 0;
263
341
  queueRequest({
264
342
  requestId: `x${data.t}`,
265
343
  method: data.m,
@@ -269,7 +347,9 @@ function interceptXHR() {
269
347
  startTimestamp: data.t,
270
348
  endTimestamp: endTime,
271
349
  success: this.status >= 200 && this.status < 400,
272
- errorMessage: this.status === 0 ? 'Network error' : undefined
350
+ errorMessage: this.status === 0 ? 'Network error' : undefined,
351
+ requestBodySize: data.reqSize,
352
+ responseBodySize
273
353
  });
274
354
  };
275
355
  this.addEventListener('load', onComplete);
@@ -106,8 +106,7 @@ export interface Spec extends TurboModule {
106
106
  */
107
107
  debugCrash(): void;
108
108
  /**
109
- * Trigger a debug ANR (Dev only)
110
- * Blocks the main thread for the specified duration
109
+ * Trigger an ANR test by blocking the main thread for the specified duration.
111
110
  */
112
111
  debugTriggerANR(durationMs: number): void;
113
112
  /**
@@ -130,6 +129,10 @@ export interface Spec extends TurboModule {
130
129
  success: boolean;
131
130
  }>;
132
131
  getUserIdentity(): Promise<string | null>;
132
+ setAnonymousId(anonymousId: string): Promise<{
133
+ success: boolean;
134
+ }>;
135
+ getAnonymousId(): Promise<string | null>;
133
136
  setDebugMode(enabled: boolean): Promise<{
134
137
  success: boolean;
135
138
  }>;
@@ -64,6 +64,7 @@ export interface AutoTrackingConfig {
64
64
  trackJSErrors?: boolean;
65
65
  trackPromiseRejections?: boolean;
66
66
  trackReactNativeErrors?: boolean;
67
+ trackConsoleLogs?: boolean;
67
68
  collectDeviceInfo?: boolean;
68
69
  maxSessionDurationMs?: number;
69
70
  detectDeadTaps?: boolean;
@@ -198,7 +199,8 @@ export declare function getAnonymousId(): string;
198
199
  export declare function ensurePersistentAnonymousId(): Promise<string>;
199
200
  /**
200
201
  * Load anonymous ID from persistent storage
201
- * Call this at app startup for best results
202
+ * Checks native anonymous storage first, then falls back to native getUserIdentity,
203
+ * and finally generates a new ID if nothing is persisted.
202
204
  */
203
205
  export declare function loadAnonymousId(): Promise<string>;
204
206
  /**
@@ -90,6 +90,11 @@ export interface RejourneyConfig {
90
90
  * Disable if you want minimal network tracking overhead.
91
91
  */
92
92
  networkCaptureSizes?: boolean;
93
+ /**
94
+ * Automatically intercept console.log/info/warn/error and include them in session recordings.
95
+ * Useful for debugging sessions. Capped at 1,000 logs per session. (default: true)
96
+ */
97
+ trackConsoleLogs?: boolean;
93
98
  }
94
99
  export type GestureType = 'tap' | 'double_tap' | 'long_press' | 'force_touch' | 'swipe_left' | 'swipe_right' | 'swipe_up' | 'swipe_down' | 'pinch' | 'pinch_in' | 'pinch_out' | 'pan_up' | 'pan_down' | 'pan_left' | 'pan_right' | 'rotate_cw' | 'rotate_ccw' | 'scroll' | 'scroll_up' | 'scroll_down' | 'two_finger_tap' | 'three_finger_gesture' | 'multi_touch' | 'keyboard_tap' | 'rage_tap';
95
100
  export type EventType = 'gesture' | 'screen_change' | 'custom' | 'app_state' | 'app_lifecycle' | 'keyboard_show' | 'keyboard_hide' | 'keyboard_typing' | 'oauth_started' | 'oauth_completed' | 'oauth_returned' | 'external_url_opened' | 'session_start' | 'session_timeout' | 'frustration' | 'error';
@@ -492,15 +497,22 @@ export interface RejourneyAPI {
492
497
  used: number;
493
498
  max: number;
494
499
  }>;
500
+ /**
501
+ * Log customer feedback (e.g. from an in-app survey or NPS widget).
502
+ *
503
+ * @param rating - Numeric rating (e.g. 1 to 5)
504
+ * @param message - Associated feedback text or comment
505
+ */
506
+ logFeedback(rating: number, message: string): void;
495
507
  /**
496
508
  * Get SDK telemetry metrics for observability
509
+
497
510
  * Returns metrics about SDK health including upload success rates,
498
511
  * retry attempts, circuit breaker events, and memory pressure.
499
512
  */
500
513
  getSDKMetrics(): Promise<SDKMetrics>;
501
514
  /**
502
- * Trigger a debug ANR (Dev only)
503
- * Blocks the main thread for the specified duration
515
+ * Trigger an ANR test by blocking the main thread for the specified duration.
504
516
  */
505
517
  debugTriggerANR(durationMs: number): void;
506
518
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rejourneyco/react-native",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Rejourney Session Recording SDK for React Native",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -75,7 +75,7 @@
75
75
  "@types/react-native": "*",
76
76
  "@typescript-eslint/eslint-plugin": "^8.15.0",
77
77
  "@typescript-eslint/parser": "^8.15.0",
78
- "@vitest/coverage-v8": "^2.1.0",
78
+ "@vitest/coverage-v8": "^4.0.18",
79
79
  "dependency-cruiser": "^16.10.4",
80
80
  "@react-navigation/native": "*",
81
81
  "expo-router": "*",
@@ -84,7 +84,7 @@
84
84
  "react-native": "*",
85
85
  "react-native-builder-bob": "^0.23.0",
86
86
  "typescript": "^5.0.0",
87
- "vitest": "^3.2.4"
87
+ "vitest": "^4.0.18"
88
88
  },
89
89
  "peerDependencies": {
90
90
  "react": "*",
@@ -119,4 +119,4 @@
119
119
  ]
120
120
  ]
121
121
  }
122
- }
122
+ }
@@ -128,8 +128,7 @@ export interface Spec extends TurboModule {
128
128
  debugCrash(): void;
129
129
 
130
130
  /**
131
- * Trigger a debug ANR (Dev only)
132
- * Blocks the main thread for the specified duration
131
+ * Trigger an ANR test by blocking the main thread for the specified duration.
133
132
  */
134
133
  debugTriggerANR(durationMs: number): void;
135
134
 
@@ -152,11 +151,15 @@ export interface Spec extends TurboModule {
152
151
 
153
152
  getUserIdentity(): Promise<string | null>;
154
153
 
154
+ setAnonymousId(anonymousId: string): Promise<{ success: boolean }>;
155
+
156
+ getAnonymousId(): Promise<string | null>;
157
+
155
158
  setDebugMode(enabled: boolean): Promise<{ success: boolean }>;
156
159
 
157
- /**
158
- * Set SDK version from JS (called during init with version from package.json)
159
- */
160
+ /**
161
+ * Set SDK version from JS (called during init with version from package.json)
162
+ */
160
163
  setSDKVersion(version: string): void;
161
164
 
162
165
  /**
package/src/index.ts CHANGED
@@ -635,6 +635,7 @@ const Rejourney: RejourneyAPI = {
635
635
  trackJSErrors: true,
636
636
  trackPromiseRejections: true,
637
637
  trackReactNativeErrors: true,
638
+ trackConsoleLogs: _storedConfig?.trackConsoleLogs ?? true,
638
639
  collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false,
639
640
  },
640
641
  {
@@ -648,13 +649,8 @@ const Rejourney: RejourneyAPI = {
648
649
  });
649
650
  getLogger().logFrustration(`Rage tap (${count} taps)`);
650
651
  },
651
- // Error callback - log as error event
652
+ // Error callback - SDK forwarding is handled in autoTracking.trackError
652
653
  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
654
  getLogger().logError(error.message);
659
655
  },
660
656
  onScreen: (_screenName: string, _previousScreen?: string) => {
@@ -673,6 +669,11 @@ const Rejourney: RejourneyAPI = {
673
669
 
674
670
  if (_storedConfig?.autoTrackNetwork !== false) {
675
671
  try {
672
+ // JS-level fetch/XHR patching is the primary mechanism for capturing network
673
+ // calls within React Native. Native interceptors (RejourneyURLProtocol on iOS,
674
+ // RejourneyNetworkInterceptor on Android) are supplementary — they capture
675
+ // native-originated HTTP calls that bypass JS fetch(), but cannot intercept
676
+ // RN's own networking since it creates its NSURLSession/OkHttpClient at init time.
676
677
  const ignoreUrls: (string | RegExp)[] = [
677
678
  apiUrl,
678
679
  '/api/sdk/config',
@@ -1154,6 +1155,28 @@ const Rejourney: RejourneyAPI = {
1154
1155
  );
1155
1156
  },
1156
1157
 
1158
+ /**
1159
+ * Log customer feedback (e.g. from an in-app survey or NPS widget).
1160
+ *
1161
+ * @param rating - Numeric rating (e.g. 1 to 5)
1162
+ * @param message - Associated feedback text or comment
1163
+ */
1164
+ logFeedback(rating: number, message: string): void {
1165
+ safeNativeCallSync(
1166
+ 'logFeedback',
1167
+ () => {
1168
+ const feedbackEvent = {
1169
+ type: 'feedback',
1170
+ timestamp: Date.now(),
1171
+ rating,
1172
+ message,
1173
+ };
1174
+ getRejourneyNative()!.logEvent('feedback', feedbackEvent).catch(() => { });
1175
+ },
1176
+ undefined
1177
+ );
1178
+ },
1179
+
1157
1180
  /**
1158
1181
  * Get SDK telemetry metrics for observability
1159
1182
  *
@@ -1194,21 +1217,16 @@ const Rejourney: RejourneyAPI = {
1194
1217
  },
1195
1218
 
1196
1219
  /**
1197
- * Trigger a debug ANR (Dev only)
1198
- * Blocks the main thread for the specified duration
1220
+ * Trigger an ANR test by blocking the main thread for the specified duration.
1199
1221
  */
1200
1222
  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
- }
1223
+ safeNativeCallSync(
1224
+ 'debugTriggerANR',
1225
+ () => {
1226
+ getRejourneyNative()!.debugTriggerANR(durationMs);
1227
+ },
1228
+ undefined
1229
+ );
1212
1230
  },
1213
1231
 
1214
1232
  /**