@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
@@ -59,6 +59,94 @@ const config = {
59
59
 
60
60
  const SENSITIVE_KEYS = ['token', 'key', 'secret', 'password', 'auth', 'access_token', 'api_key'];
61
61
 
62
+ function getUtf8Size(text: string): number {
63
+ if (!text) return 0;
64
+ if (typeof TextEncoder !== 'undefined') {
65
+ return new TextEncoder().encode(text).length;
66
+ }
67
+ return text.length;
68
+ }
69
+
70
+ function getBodySize(body: unknown): number {
71
+ if (body == null) return 0;
72
+
73
+ if (typeof body === 'string') return getUtf8Size(body);
74
+
75
+ if (typeof ArrayBuffer !== 'undefined' && body instanceof ArrayBuffer) {
76
+ return body.byteLength;
77
+ }
78
+
79
+ if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(body as any)) {
80
+ return (body as ArrayBufferView).byteLength;
81
+ }
82
+
83
+ if (typeof Blob !== 'undefined' && body instanceof Blob) {
84
+ return body.size;
85
+ }
86
+
87
+ if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
88
+ return getUtf8Size(body.toString());
89
+ }
90
+
91
+ return 0;
92
+ }
93
+
94
+ async function getFetchResponseSize(response: Response): Promise<number> {
95
+ const contentLength = response.headers?.get?.('content-length');
96
+ if (contentLength) {
97
+ const parsed = parseInt(contentLength, 10);
98
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
99
+ }
100
+
101
+ try {
102
+ const cloned = response.clone();
103
+ const buffer = await cloned.arrayBuffer();
104
+ return buffer.byteLength;
105
+ } catch {
106
+ return 0;
107
+ }
108
+ }
109
+
110
+ function getXhrResponseSize(xhr: XMLHttpRequest): number {
111
+ try {
112
+ const contentLength = xhr.getResponseHeader('content-length');
113
+ if (contentLength) {
114
+ const parsed = parseInt(contentLength, 10);
115
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
116
+ }
117
+ } catch {
118
+ // Ignore header access errors and fall through to body inspection.
119
+ }
120
+
121
+ const responseType = xhr.responseType;
122
+
123
+ if (responseType === '' || responseType === 'text') {
124
+ return getUtf8Size(xhr.responseText || '');
125
+ }
126
+
127
+ if (responseType === 'arraybuffer') {
128
+ return typeof ArrayBuffer !== 'undefined' && xhr.response instanceof ArrayBuffer
129
+ ? xhr.response.byteLength
130
+ : 0;
131
+ }
132
+
133
+ if (responseType === 'blob') {
134
+ return typeof Blob !== 'undefined' && xhr.response instanceof Blob
135
+ ? xhr.response.size
136
+ : 0;
137
+ }
138
+
139
+ if (responseType === 'json') {
140
+ try {
141
+ return getUtf8Size(JSON.stringify(xhr.response ?? ''));
142
+ } catch {
143
+ return 0;
144
+ }
145
+ }
146
+
147
+ return 0;
148
+ }
149
+
62
150
  /**
63
151
  * Scrub sensitive data from URL
64
152
  */
@@ -225,8 +313,15 @@ function interceptFetch(): void {
225
313
 
226
314
  const startTime = Date.now();
227
315
  const method = ((init?.method || 'GET').toUpperCase()) as NetworkRequestParams['method'];
316
+
317
+ const requestBodySize = config.captureSizes ? getBodySize(init?.body) : 0;
318
+
228
319
  return originalFetch!(input, init).then(
229
- (response) => {
320
+ async (response) => {
321
+ const responseBodySize = config.captureSizes
322
+ ? await getFetchResponseSize(response)
323
+ : 0;
324
+
230
325
  queueRequest({
231
326
  requestId: `f${startTime}`,
232
327
  method,
@@ -236,6 +331,8 @@ function interceptFetch(): void {
236
331
  startTimestamp: startTime,
237
332
  endTimestamp: Date.now(),
238
333
  success: response.ok,
334
+ requestBodySize,
335
+ responseBodySize,
239
336
  });
240
337
  return response;
241
338
  },
@@ -250,6 +347,7 @@ function interceptFetch(): void {
250
347
  endTimestamp: Date.now(),
251
348
  success: false,
252
349
  errorMessage: error?.message || 'Network error',
350
+ requestBodySize,
253
351
  });
254
352
  throw error;
255
353
  }
@@ -296,10 +394,19 @@ function interceptXHR(): void {
296
394
  return originalXHRSend!.call(this, body);
297
395
  }
298
396
 
397
+ if (config.captureSizes && body) {
398
+ data.reqSize = getBodySize(body);
399
+ } else {
400
+ data.reqSize = 0;
401
+ }
402
+
299
403
  data.t = Date.now();
300
404
 
301
405
  const onComplete = () => {
302
406
  const endTime = Date.now();
407
+
408
+ const responseBodySize = config.captureSizes ? getXhrResponseSize(this) : 0;
409
+
303
410
  queueRequest({
304
411
  requestId: `x${data.t}`,
305
412
  method: data.m as NetworkRequestParams['method'],
@@ -310,6 +417,8 @@ function interceptXHR(): void {
310
417
  endTimestamp: endTime,
311
418
  success: this.status >= 200 && this.status < 400,
312
419
  errorMessage: this.status === 0 ? 'Network error' : undefined,
420
+ requestBodySize: data.reqSize,
421
+ responseBodySize,
313
422
  });
314
423
  };
315
424
 
@@ -31,6 +31,8 @@ export interface RejourneyConfig {
31
31
  maxStorageSize?: number;
32
32
  /** Enable automatic screen name detection with React Navigation (default: true) */
33
33
  autoScreenTracking?: boolean;
34
+ /** Enable automatic screen name detection with Expo Router (default: true) */
35
+ autoTrackExpoRouter?: boolean;
34
36
  /** Enable automatic gesture detection (default: true) */
35
37
  autoGestureTracking?: boolean;
36
38
  /** Enable privacy occlusion for text inputs (default: true) */
@@ -102,8 +104,12 @@ export interface RejourneyConfig {
102
104
  * Disable if you want minimal network tracking overhead.
103
105
  */
104
106
  networkCaptureSizes?: boolean;
107
+ /**
108
+ * Automatically intercept console.log/info/warn/error and include them in session recordings.
109
+ * Useful for debugging sessions. Capped at 1,000 logs per session. (default: true)
110
+ */
111
+ trackConsoleLogs?: boolean;
105
112
  }
106
-
107
113
  export type GestureType =
108
114
  | 'tap'
109
115
  | 'double_tap'
@@ -517,7 +523,17 @@ export interface RejourneyNativeModule {
517
523
  export interface RejourneyAPI {
518
524
  /** SDK version */
519
525
  readonly version: string;
520
- /** Internal method to start recording session (called by startRejourney) */
526
+ /**
527
+ * Initialize Rejourney SDK
528
+ * @param publicRouteKey - Your public route key from the Rejourney dashboard
529
+ * @param options - Optional configuration options
530
+ */
531
+ init(publicRouteKey: string, options?: Omit<RejourneyConfig, 'publicRouteKey'>): void;
532
+ /** Start recording (call after user consent) */
533
+ start(): void;
534
+ /** Stop recording */
535
+ stop(): void;
536
+ /** Internal method to start recording session (called by start() / startRejourney()) */
521
537
  _startSession(): Promise<boolean>;
522
538
  /** Internal method to stop recording session (called by stopRejourney) */
523
539
  _stopSession(): Promise<void>;
@@ -527,8 +543,18 @@ export interface RejourneyAPI {
527
543
  setUserIdentity(userId: string): void;
528
544
  /** Clear user identity */
529
545
  clearUserIdentity(): void;
530
- /** Tag current screen */
531
- tagScreen(screenName: string, params?: Record<string, unknown>): void;
546
+ /**
547
+ * Set custom session metadata.
548
+ * Can be called with a single key-value pair or an object of properties.
549
+ * Useful for filtering sessions later (e.g., plan: 'premium', role: 'admin').
550
+ * Caps at 100 properties per session.
551
+ *
552
+ * @param keyOrProperties Property name string, or an object containing key-value pairs
553
+ * @param value Property value (if first argument is a string)
554
+ */
555
+ setMetadata(keyOrProperties: string | Record<string, string | number | boolean>, value?: string | number | boolean): void;
556
+ /** Track current screen (manual) */
557
+ trackScreen(screenName: string, params?: Record<string, unknown>): void;
532
558
  /** Mark a view as sensitive (will be occluded in recording) */
533
559
  setOccluded(viewRef: { current: any }, occluded?: boolean): void;
534
560
  /** Add a tag to current session */
@@ -577,16 +603,24 @@ export interface RejourneyAPI {
577
603
  /** Get storage usage */
578
604
  getStorageUsage(): Promise<{ used: number; max: number }>;
579
605
 
606
+ /**
607
+ * Log customer feedback (e.g. from an in-app survey or NPS widget).
608
+ *
609
+ * @param rating - Numeric rating (e.g. 1 to 5)
610
+ * @param message - Associated feedback text or comment
611
+ */
612
+ logFeedback(rating: number, message: string): void;
613
+
580
614
  /**
581
615
  * Get SDK telemetry metrics for observability
616
+
582
617
  * Returns metrics about SDK health including upload success rates,
583
618
  * retry attempts, circuit breaker events, and memory pressure.
584
619
  */
585
620
  getSDKMetrics(): Promise<SDKMetrics>;
586
621
 
587
622
  /**
588
- * Trigger a debug ANR (Dev only)
589
- * Blocks the main thread for the specified duration
623
+ * Trigger an ANR test by blocking the main thread for the specified duration.
590
624
  */
591
625
  debugTriggerANR(durationMs: number): void;
592
626
 
@@ -616,6 +650,22 @@ export interface RejourneyAPI {
616
650
  * @param nativeID - The nativeID prop of the view to unmask
617
651
  */
618
652
  unmaskView(nativeID: string): void;
653
+
654
+ /**
655
+ * Hook for automatic React Navigation tracking.
656
+ * Pass the returned object to your NavigationContainer props.
657
+ *
658
+ * @example
659
+ * ```tsx
660
+ * const navigationTracking = Rejourney.useNavigationTracking();
661
+ * <NavigationContainer {...navigationTracking}>
662
+ * ```
663
+ */
664
+ useNavigationTracking(): {
665
+ ref: any;
666
+ onReady: () => void;
667
+ onStateChange: (state: any) => void;
668
+ };
619
669
  }
620
670
 
621
671
  /**
@@ -686,6 +736,8 @@ export interface UseRejourneyResult {
686
736
  stopRecording: () => Promise<void>;
687
737
  /** Log custom event */
688
738
  logEvent: (name: string, properties?: Record<string, unknown>) => void;
739
+ /** Set custom session metadata */
740
+ setMetadata: (keyOrProperties: string | Record<string, string | number | boolean>, value?: string | number | boolean) => void;
689
741
  /** Error if any */
690
742
  error: Error | null;
691
743
  }