@rejourneyco/react-native 1.0.7 → 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 (52) hide show
  1. package/README.md +1 -1
  2. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +109 -26
  3. package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +18 -3
  4. package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +69 -17
  5. package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +27 -2
  6. package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +30 -0
  7. package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +100 -0
  8. package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +260 -174
  9. package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +246 -34
  10. package/android/src/main/java/com/rejourney/recording/SpecialCases.kt +572 -0
  11. package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +3 -0
  12. package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +19 -4
  13. package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +8 -0
  14. package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +251 -85
  15. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +14 -0
  16. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +18 -0
  17. package/ios/Engine/DeviceRegistrar.swift +13 -3
  18. package/ios/Engine/RejourneyImpl.swift +202 -133
  19. package/ios/Recording/AnrSentinel.swift +58 -25
  20. package/ios/Recording/InteractionRecorder.swift +29 -0
  21. package/ios/Recording/RejourneyURLProtocol.swift +168 -0
  22. package/ios/Recording/ReplayOrchestrator.swift +241 -147
  23. package/ios/Recording/SegmentDispatcher.swift +155 -13
  24. package/ios/Recording/SpecialCases.swift +614 -0
  25. package/ios/Recording/StabilityMonitor.swift +42 -34
  26. package/ios/Recording/TelemetryPipeline.swift +38 -3
  27. package/ios/Recording/ViewHierarchyScanner.swift +1 -0
  28. package/ios/Recording/VisualCapture.swift +104 -28
  29. package/ios/Rejourney.mm +27 -8
  30. package/ios/Utility/ImageBlur.swift +0 -1
  31. package/lib/commonjs/index.js +32 -20
  32. package/lib/commonjs/sdk/autoTracking.js +162 -11
  33. package/lib/commonjs/sdk/constants.js +2 -2
  34. package/lib/commonjs/sdk/networkInterceptor.js +84 -4
  35. package/lib/commonjs/sdk/utils.js +1 -1
  36. package/lib/module/index.js +32 -20
  37. package/lib/module/sdk/autoTracking.js +162 -11
  38. package/lib/module/sdk/constants.js +2 -2
  39. package/lib/module/sdk/networkInterceptor.js +84 -4
  40. package/lib/module/sdk/utils.js +1 -1
  41. package/lib/typescript/NativeRejourney.d.ts +5 -2
  42. package/lib/typescript/sdk/autoTracking.d.ts +3 -1
  43. package/lib/typescript/sdk/constants.d.ts +2 -2
  44. package/lib/typescript/types/index.d.ts +15 -8
  45. package/package.json +4 -4
  46. package/src/NativeRejourney.ts +8 -5
  47. package/src/index.ts +46 -29
  48. package/src/sdk/autoTracking.ts +176 -11
  49. package/src/sdk/constants.ts +2 -2
  50. package/src/sdk/networkInterceptor.ts +110 -1
  51. package/src/sdk/utils.ts +1 -1
  52. package/src/types/index.ts +16 -9
package/src/index.ts CHANGED
@@ -247,13 +247,13 @@ interface RemoteConfig {
247
247
  }
248
248
 
249
249
  // Result type for fetchRemoteConfig - distinguishes network errors from access denial
250
- type ConfigFetchResult =
250
+ type ConfigFetchResult =
251
251
  | { status: 'success'; config: RemoteConfig }
252
252
  | { status: 'network_error' } // Proceed with defaults (fail-open)
253
253
  | { status: 'access_denied'; httpStatus: number }; // Abort recording (fail-closed)
254
254
 
255
255
  let _remoteConfig: RemoteConfig | null = null;
256
- let _sessionSampledOut: boolean = false; // True = telemetry only, no replay video
256
+ let _sessionSampledOut: boolean = false; // True = telemetry only, no visual replay capture
257
257
 
258
258
  /**
259
259
  * Fetch project configuration from backend
@@ -323,7 +323,7 @@ function shouldRecordSession(sampleRate: number): boolean {
323
323
  // sampleRate is 0-100 (percentage)
324
324
  if (sampleRate >= 100) return true;
325
325
  if (sampleRate <= 0) return false;
326
-
326
+
327
327
  const randomValue = Math.random() * 100;
328
328
  return randomValue < sampleRate;
329
329
  }
@@ -522,7 +522,7 @@ const Rejourney: RejourneyAPI = {
522
522
  // This determines if recording is enabled and at what rate
523
523
  // =========================================================
524
524
  const configResult = await fetchRemoteConfig(apiUrl, publicKey);
525
-
525
+
526
526
  // =========================================================
527
527
  // CASE 0: Access denied (401/403) - abort immediately
528
528
  // This means project disabled, invalid key, etc - HARD STOP
@@ -531,10 +531,10 @@ const Rejourney: RejourneyAPI = {
531
531
  getLogger().info(`Recording disabled - access denied (${configResult.httpStatus})`);
532
532
  return false;
533
533
  }
534
-
534
+
535
535
  // For success, extract the config; for network_error, proceed with null
536
536
  _remoteConfig = configResult.status === 'success' ? configResult.config : null;
537
-
537
+
538
538
  if (_remoteConfig) {
539
539
  // =========================================================
540
540
  // CASE 1: Rejourney completely disabled - abort early, nothing captured
@@ -560,7 +560,7 @@ const Rejourney: RejourneyAPI = {
560
560
  // =========================================================
561
561
  _sessionSampledOut = !shouldRecordSession(_remoteConfig.sampleRate ?? 100);
562
562
  if (_sessionSampledOut) {
563
- getLogger().info(`Session sampled out (rate: ${_remoteConfig.sampleRate}%) - telemetry only, no replay video`);
563
+ getLogger().info(`Session sampled out (rate: ${_remoteConfig.sampleRate}%) - telemetry only, no visual replay capture`);
564
564
  }
565
565
 
566
566
  // =========================================================
@@ -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',
@@ -868,7 +869,6 @@ const Rejourney: RejourneyAPI = {
868
869
  duration: 0,
869
870
  deviceInfo: { model: '', os: 'ios', osVersion: '', screenWidth: 0, screenHeight: 0, pixelRatio: 1 },
870
871
  eventCount: 0,
871
- videoSegmentCount: 0,
872
872
  storageSize: 0,
873
873
  sdkVersion: SDK_VERSION,
874
874
  isComplete: false,
@@ -957,10 +957,10 @@ const Rejourney: RejourneyAPI = {
957
957
  },
958
958
 
959
959
  /**
960
- * Report a scroll event for video capture timing
960
+ * Report a scroll event for visual replay timing
961
961
  *
962
962
  * Call this from your ScrollView's onScroll handler to improve scroll capture.
963
- * The SDK captures video at 2 FPS continuously, but this helps log scroll events
963
+ * The SDK captures visual replay frames continuously, and this helps log scroll events
964
964
  * for timeline correlation during replay.
965
965
  *
966
966
  * @param scrollOffset - Current scroll offset (vertical or horizontal)
@@ -1155,6 +1155,28 @@ const Rejourney: RejourneyAPI = {
1155
1155
  );
1156
1156
  },
1157
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
+
1158
1180
  /**
1159
1181
  * Get SDK telemetry metrics for observability
1160
1182
  *
@@ -1195,21 +1217,16 @@ const Rejourney: RejourneyAPI = {
1195
1217
  },
1196
1218
 
1197
1219
  /**
1198
- * Trigger a debug ANR (Dev only)
1199
- * Blocks the main thread for the specified duration
1220
+ * Trigger an ANR test by blocking the main thread for the specified duration.
1200
1221
  */
1201
1222
  debugTriggerANR(durationMs: number): void {
1202
- if (__DEV__) {
1203
- safeNativeCallSync(
1204
- 'debugTriggerANR',
1205
- () => {
1206
- getRejourneyNative()!.debugTriggerANR(durationMs);
1207
- },
1208
- undefined
1209
- );
1210
- } else {
1211
- getLogger().warn('debugTriggerANR is only available in development mode');
1212
- }
1223
+ safeNativeCallSync(
1224
+ 'debugTriggerANR',
1225
+ () => {
1226
+ getRejourneyNative()!.debugTriggerANR(durationMs);
1227
+ },
1228
+ undefined
1229
+ );
1213
1230
  },
1214
1231
 
1215
1232
  /**
@@ -142,6 +142,7 @@ export interface AutoTrackingConfig {
142
142
  trackJSErrors?: boolean;
143
143
  trackPromiseRejections?: boolean;
144
144
  trackReactNativeErrors?: boolean;
145
+ trackConsoleLogs?: boolean;
145
146
  collectDeviceInfo?: boolean;
146
147
  maxSessionDurationMs?: number;
147
148
  detectDeadTaps?: boolean;
@@ -183,6 +184,7 @@ let originalOnError: OnErrorEventHandler | null = null;
183
184
  let originalOnUnhandledRejection: ((event: PromiseRejectionEvent) => void) | null = null;
184
185
  let originalConsoleError: ((...args: any[]) => void) | null = null;
185
186
  let _promiseRejectionTrackingDisable: (() => void) | null = null;
187
+ const FATAL_ERROR_FLUSH_DELAY_MS = 1200;
186
188
 
187
189
  /**
188
190
  * Initialize auto tracking features
@@ -205,6 +207,7 @@ export function initAutoTracking(
205
207
  trackJSErrors: true,
206
208
  trackPromiseRejections: true,
207
209
  trackReactNativeErrors: true,
210
+ trackConsoleLogs: true,
208
211
  collectDeviceInfo: true,
209
212
  maxSessionDurationMs: trackingConfig.maxSessionDurationMs,
210
213
  ...trackingConfig,
@@ -221,6 +224,9 @@ export function initAutoTracking(
221
224
  onErrorCaptured = callbacks.onError || null;
222
225
  onScreenChange = callbacks.onScreen || null;
223
226
  setupErrorTracking();
227
+ if (config.trackConsoleLogs) {
228
+ setupConsoleTracking();
229
+ }
224
230
  setupNavigationTracking();
225
231
  loadAnonymousId().then(id => {
226
232
  anonymousId = id;
@@ -236,11 +242,13 @@ export function cleanupAutoTracking(): void {
236
242
  if (!isInitialized) return;
237
243
 
238
244
  restoreErrorHandlers();
245
+ restoreConsoleHandlers();
239
246
  cleanupNavigationTracking();
240
247
 
241
248
  // Reset state
242
249
  tapHead = 0;
243
250
  tapCount = 0;
251
+ consoleLogCount = 0;
244
252
  metrics = createEmptyMetrics();
245
253
  screensVisited = [];
246
254
  currentScreen = '';
@@ -357,7 +365,7 @@ function setupErrorTracking(): void {
357
365
  /**
358
366
  * Setup React Native ErrorUtils handler
359
367
  *
360
- * CRITICAL FIX: For fatal errors, we delay calling the original handler by 500ms
368
+ * CRITICAL FIX: For fatal errors, we delay calling the original handler briefly
361
369
  * to give the React Native bridge time to flush the logEvent('error') call to the
362
370
  * native TelemetryPipeline. Without this delay, the error event is queued on the
363
371
  * JS→native bridge but the app crashes (via originalErrorHandler) before the bridge
@@ -384,10 +392,10 @@ function setupReactNativeErrorHandler(): void {
384
392
  if (isFatal) {
385
393
  // For fatal errors, delay the original handler so the native bridge
386
394
  // has time to deliver the error event to TelemetryPipeline before
387
- // the app terminates. 500ms is enough for the bridge to flush.
395
+ // the app terminates.
388
396
  setTimeout(() => {
389
397
  originalErrorHandler!(error, isFatal);
390
- }, 500);
398
+ }, FATAL_ERROR_FLUSH_DELAY_MS);
391
399
  } else {
392
400
  originalErrorHandler(error, isFatal);
393
401
  }
@@ -450,7 +458,6 @@ function setupPromiseRejectionHandler(): void {
450
458
 
451
459
  // Strategy 1: RN-specific promise rejection tracking polyfill
452
460
  try {
453
- // eslint-disable-next-line @typescript-eslint/no-var-requires
454
461
  const tracking = require('promise/setimmediate/rejection-tracking');
455
462
  if (tracking && typeof tracking.enable === 'function') {
456
463
  tracking.enable({
@@ -565,8 +572,30 @@ function trackError(error: ErrorEvent): void {
565
572
  metrics.errorCount++;
566
573
  metrics.totalEvents++;
567
574
 
575
+ forwardErrorToNative(error);
576
+
568
577
  if (onErrorCaptured) {
569
- onErrorCaptured(error);
578
+ try {
579
+ onErrorCaptured(error);
580
+ } catch {
581
+ // Ignore callback exceptions so SDK error forwarding keeps working.
582
+ }
583
+ }
584
+ }
585
+
586
+ function forwardErrorToNative(error: ErrorEvent): void {
587
+ try {
588
+ const nativeModule = getRejourneyNativeModule();
589
+ if (!nativeModule || typeof nativeModule.logEvent !== 'function') return;
590
+
591
+ nativeModule.logEvent('error', {
592
+ message: error.message,
593
+ stack: error.stack,
594
+ name: error.name || 'Error',
595
+ timestamp: error.timestamp,
596
+ }).catch(() => { });
597
+ } catch {
598
+ // Ignore native forwarding failures; SDK should never crash app code.
570
599
  }
571
600
  }
572
601
 
@@ -587,6 +616,92 @@ export function captureError(
587
616
  });
588
617
  }
589
618
 
619
+ let originalConsoleLog: ((...args: any[]) => void) | null = null;
620
+ let originalConsoleInfo: ((...args: any[]) => void) | null = null;
621
+ let originalConsoleWarn: ((...args: any[]) => void) | null = null;
622
+
623
+ // Cap console logs to prevent flooding the event pipeline
624
+ const MAX_CONSOLE_LOGS_PER_SESSION = 1000;
625
+ let consoleLogCount = 0;
626
+
627
+ /**
628
+ * Setup console tracking to capture log statements
629
+ */
630
+ function setupConsoleTracking(): void {
631
+ if (typeof console === 'undefined') return;
632
+
633
+ if (!originalConsoleLog) originalConsoleLog = console.log;
634
+ if (!originalConsoleInfo) originalConsoleInfo = console.info;
635
+ if (!originalConsoleWarn) originalConsoleWarn = console.warn;
636
+
637
+ const createConsoleInterceptor = (level: 'log' | 'info' | 'warn' | 'error', originalFn: (...args: any[]) => void) => {
638
+ return (...args: any[]) => {
639
+ try {
640
+ const message = args.map(arg => {
641
+ if (typeof arg === 'string') return arg;
642
+ if (arg instanceof Error) return `${arg.name}: ${arg.message}${arg.stack ? `\n...` : ''}`;
643
+ try {
644
+ return JSON.stringify(arg);
645
+ } catch {
646
+ return String(arg);
647
+ }
648
+ }).join(' ');
649
+
650
+ // Enforce per-session cap and skip React Native unhandled-rejection noise.
651
+ if (
652
+ consoleLogCount < MAX_CONSOLE_LOGS_PER_SESSION &&
653
+ !message.includes('Possible Unhandled Promise Rejection')
654
+ ) {
655
+ consoleLogCount++;
656
+ const nativeModule = getRejourneyNativeModule();
657
+ if (nativeModule) {
658
+ const logEvent = {
659
+ type: 'log',
660
+ timestamp: Date.now(),
661
+ level,
662
+ message: message.length > 2000 ? message.substring(0, 2000) + '...' : message,
663
+ };
664
+ nativeModule.logEvent('log', logEvent).catch(() => { });
665
+ }
666
+ }
667
+ } catch {
668
+ // Ignore any errors during interception
669
+ }
670
+
671
+ if (originalFn) {
672
+ originalFn.apply(console, args);
673
+ }
674
+ };
675
+ };
676
+
677
+ console.log = createConsoleInterceptor('log', originalConsoleLog!);
678
+ console.info = createConsoleInterceptor('info', originalConsoleInfo!);
679
+ console.warn = createConsoleInterceptor('warn', originalConsoleWarn!);
680
+
681
+ const currentConsoleError = console.error;
682
+ if (!originalConsoleError) originalConsoleError = currentConsoleError;
683
+ console.error = createConsoleInterceptor('error', currentConsoleError);
684
+ }
685
+
686
+ /**
687
+ * Restore console standard functions
688
+ */
689
+ function restoreConsoleHandlers(): void {
690
+ if (originalConsoleLog) {
691
+ console.log = originalConsoleLog;
692
+ originalConsoleLog = null;
693
+ }
694
+ if (originalConsoleInfo) {
695
+ console.info = originalConsoleInfo;
696
+ originalConsoleInfo = null;
697
+ }
698
+ if (originalConsoleWarn) {
699
+ console.warn = originalConsoleWarn;
700
+ originalConsoleWarn = null;
701
+ }
702
+ // Note: console.error is restored in restoreErrorHandlers via originalConsoleError
703
+ }
704
+
590
705
  let navigationPollingInterval: ReturnType<typeof setInterval> | null = null;
591
706
  let lastDetectedScreen = '';
592
707
  let navigationSetupDone = false;
@@ -1174,7 +1289,27 @@ export async function collectDeviceInfo(): Promise<DeviceInfo> {
1174
1289
  function generateAnonymousId(): string {
1175
1290
  const timestamp = Date.now().toString(36);
1176
1291
  const random = Math.random().toString(36).substring(2, 15);
1177
- return `anon_${timestamp}_${random}`;
1292
+ const id = `anon_${timestamp}_${random}`;
1293
+ // Persist so the same ID survives app restarts
1294
+ _persistAnonymousId(id);
1295
+ return id;
1296
+ }
1297
+
1298
+ /**
1299
+ * Best-effort async persist of anonymous ID to native storage
1300
+ */
1301
+ function _persistAnonymousId(id: string): void {
1302
+ const nativeModule = getRejourneyNativeModule();
1303
+ if (!nativeModule?.setAnonymousId) return;
1304
+
1305
+ try {
1306
+ const result = nativeModule.setAnonymousId(id);
1307
+ if (result && typeof result.catch === 'function') {
1308
+ result.catch(() => { });
1309
+ }
1310
+ } catch {
1311
+ // Native storage unavailable — ID will still be stable for this session
1312
+ }
1178
1313
  }
1179
1314
 
1180
1315
  /**
@@ -1205,17 +1340,41 @@ export async function ensurePersistentAnonymousId(): Promise<string> {
1205
1340
 
1206
1341
  /**
1207
1342
  * Load anonymous ID from persistent storage
1208
- * Call this at app startup for best results
1343
+ * Checks native anonymous storage first, then falls back to native getUserIdentity,
1344
+ * and finally generates a new ID if nothing is persisted.
1209
1345
  */
1210
1346
  export async function loadAnonymousId(): Promise<string> {
1211
1347
  const nativeModule = getRejourneyNativeModule();
1212
- if (nativeModule && nativeModule.getUserIdentity) {
1348
+
1349
+ // 1. Try native anonymous ID storage
1350
+ if (nativeModule?.getAnonymousId) {
1213
1351
  try {
1214
- return await nativeModule.getUserIdentity() || generateAnonymousId();
1352
+ const stored = await nativeModule.getAnonymousId();
1353
+ if (stored && typeof stored === 'string') return stored;
1215
1354
  } catch {
1216
- return generateAnonymousId();
1355
+ // Continue to fallbacks
1217
1356
  }
1218
1357
  }
1358
+
1359
+ // 2. Backward compatibility fallback for older native modules
1360
+ if (nativeModule?.getUserIdentity) {
1361
+ try {
1362
+ const nativeId = await nativeModule.getUserIdentity();
1363
+ if (nativeId && typeof nativeId === 'string') {
1364
+ const normalized = nativeId.trim();
1365
+ // Only migrate legacy anonymous identifiers. Never treat explicit user identities
1366
+ // as anonymous fingerprints, or session correlation becomes unstable.
1367
+ if (normalized.startsWith('anon_')) {
1368
+ _persistAnonymousId(normalized);
1369
+ return normalized;
1370
+ }
1371
+ }
1372
+ } catch {
1373
+ // Continue to fallback
1374
+ }
1375
+ }
1376
+
1377
+ // 3. Generate and persist new ID
1219
1378
  return generateAnonymousId();
1220
1379
  }
1221
1380
 
@@ -1223,7 +1382,13 @@ export async function loadAnonymousId(): Promise<string> {
1223
1382
  * Set a custom anonymous ID
1224
1383
  */
1225
1384
  export function setAnonymousId(id: string): void {
1226
- anonymousId = id;
1385
+ const normalized = (id || '').trim();
1386
+ if (!normalized) {
1387
+ anonymousId = generateAnonymousId();
1388
+ return;
1389
+ }
1390
+ anonymousId = normalized;
1391
+ _persistAnonymousId(normalized);
1227
1392
  }
1228
1393
 
1229
1394
  export default {
@@ -26,7 +26,7 @@ export { SDK_VERSION };
26
26
  /** Default configuration values */
27
27
  export const DEFAULT_CONFIG = {
28
28
  enabled: true,
29
- captureFPS: 0.5,
29
+ captureFPS: 1.0,
30
30
  captureOnEvents: true,
31
31
  maxSessionDuration: 10 * 60 * 1000,
32
32
  maxStorageSize: 50 * 1024 * 1024,
@@ -79,7 +79,7 @@ export const PLAYBACK_SPEEDS = [0.5, 1, 2, 4] as const;
79
79
 
80
80
  /** Capture settings */
81
81
  export const CAPTURE_SETTINGS = {
82
- DEFAULT_FPS: 0.5,
82
+ DEFAULT_FPS: 1.0,
83
83
  MIN_FPS: 0.1,
84
84
  MAX_FPS: 2,
85
85
  CAPTURE_SCALE: 0.25,
@@ -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
 
package/src/sdk/utils.ts CHANGED
@@ -324,7 +324,7 @@ class Logger {
324
324
  }
325
325
 
326
326
  notice(...args: any[]): void {
327
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
327
+ if (this.minimumLogLevel <= LogLevel.INFO) {
328
328
  console.info(this.prefix, ...args);
329
329
  }
330
330
  }