@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.
- package/README.md +1 -1
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +109 -26
- package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +18 -3
- package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +69 -17
- package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +27 -2
- package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +30 -0
- package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +100 -0
- package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +260 -174
- package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +246 -34
- package/android/src/main/java/com/rejourney/recording/SpecialCases.kt +572 -0
- package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +3 -0
- package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +19 -4
- package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +8 -0
- package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +251 -85
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +14 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +18 -0
- package/ios/Engine/DeviceRegistrar.swift +13 -3
- package/ios/Engine/RejourneyImpl.swift +202 -133
- package/ios/Recording/AnrSentinel.swift +58 -25
- package/ios/Recording/InteractionRecorder.swift +29 -0
- package/ios/Recording/RejourneyURLProtocol.swift +168 -0
- package/ios/Recording/ReplayOrchestrator.swift +241 -147
- package/ios/Recording/SegmentDispatcher.swift +155 -13
- package/ios/Recording/SpecialCases.swift +614 -0
- package/ios/Recording/StabilityMonitor.swift +42 -34
- package/ios/Recording/TelemetryPipeline.swift +38 -3
- package/ios/Recording/ViewHierarchyScanner.swift +1 -0
- package/ios/Recording/VisualCapture.swift +104 -28
- package/ios/Rejourney.mm +27 -8
- package/ios/Utility/ImageBlur.swift +0 -1
- package/lib/commonjs/index.js +32 -20
- package/lib/commonjs/sdk/autoTracking.js +162 -11
- package/lib/commonjs/sdk/constants.js +2 -2
- package/lib/commonjs/sdk/networkInterceptor.js +84 -4
- package/lib/commonjs/sdk/utils.js +1 -1
- package/lib/module/index.js +32 -20
- package/lib/module/sdk/autoTracking.js +162 -11
- package/lib/module/sdk/constants.js +2 -2
- package/lib/module/sdk/networkInterceptor.js +84 -4
- package/lib/module/sdk/utils.js +1 -1
- package/lib/typescript/NativeRejourney.d.ts +5 -2
- package/lib/typescript/sdk/autoTracking.d.ts +3 -1
- package/lib/typescript/sdk/constants.d.ts +2 -2
- package/lib/typescript/types/index.d.ts +15 -8
- package/package.json +4 -4
- package/src/NativeRejourney.ts +8 -5
- package/src/index.ts +46 -29
- package/src/sdk/autoTracking.ts +176 -11
- package/src/sdk/constants.ts +2 -2
- package/src/sdk/networkInterceptor.ts +110 -1
- package/src/sdk/utils.ts +1 -1
- package/src/types/index.ts +16 -9
package/lib/commonjs/index.js
CHANGED
|
@@ -322,7 +322,7 @@ let _storedConfig = null;
|
|
|
322
322
|
// Abort recording (fail-closed)
|
|
323
323
|
|
|
324
324
|
let _remoteConfig = null;
|
|
325
|
-
let _sessionSampledOut = false; // True = telemetry only, no replay
|
|
325
|
+
let _sessionSampledOut = false; // True = telemetry only, no visual replay capture
|
|
326
326
|
|
|
327
327
|
/**
|
|
328
328
|
* Fetch project configuration from backend
|
|
@@ -618,7 +618,7 @@ const Rejourney = {
|
|
|
618
618
|
// =========================================================
|
|
619
619
|
_sessionSampledOut = !shouldRecordSession(_remoteConfig.sampleRate ?? 100);
|
|
620
620
|
if (_sessionSampledOut) {
|
|
621
|
-
getLogger().info(`Session sampled out (rate: ${_remoteConfig.sampleRate}%) - telemetry only, no replay
|
|
621
|
+
getLogger().info(`Session sampled out (rate: ${_remoteConfig.sampleRate}%) - telemetry only, no visual replay capture`);
|
|
622
622
|
}
|
|
623
623
|
|
|
624
624
|
// =========================================================
|
|
@@ -680,6 +680,7 @@ const Rejourney = {
|
|
|
680
680
|
trackJSErrors: true,
|
|
681
681
|
trackPromiseRejections: true,
|
|
682
682
|
trackReactNativeErrors: true,
|
|
683
|
+
trackConsoleLogs: _storedConfig?.trackConsoleLogs ?? true,
|
|
683
684
|
collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false
|
|
684
685
|
}, {
|
|
685
686
|
// Rage tap callback - log as frustration event
|
|
@@ -692,13 +693,8 @@ const Rejourney = {
|
|
|
692
693
|
});
|
|
693
694
|
getLogger().logFrustration(`Rage tap (${count} taps)`);
|
|
694
695
|
},
|
|
695
|
-
// Error callback -
|
|
696
|
+
// Error callback - SDK forwarding is handled in autoTracking.trackError
|
|
696
697
|
onError: error => {
|
|
697
|
-
this.logEvent('error', {
|
|
698
|
-
message: error.message,
|
|
699
|
-
stack: error.stack,
|
|
700
|
-
name: error.name
|
|
701
|
-
});
|
|
702
698
|
getLogger().logError(error.message);
|
|
703
699
|
},
|
|
704
700
|
onScreen: (_screenName, _previousScreen) => {}
|
|
@@ -713,6 +709,11 @@ const Rejourney = {
|
|
|
713
709
|
}
|
|
714
710
|
if (_storedConfig?.autoTrackNetwork !== false) {
|
|
715
711
|
try {
|
|
712
|
+
// JS-level fetch/XHR patching is the primary mechanism for capturing network
|
|
713
|
+
// calls within React Native. Native interceptors (RejourneyURLProtocol on iOS,
|
|
714
|
+
// RejourneyNetworkInterceptor on Android) are supplementary — they capture
|
|
715
|
+
// native-originated HTTP calls that bypass JS fetch(), but cannot intercept
|
|
716
|
+
// RN's own networking since it creates its NSURLSession/OkHttpClient at init time.
|
|
716
717
|
const ignoreUrls = [apiUrl, '/api/sdk/config', '/api/ingest/presign', '/api/ingest/batch/complete', '/api/ingest/session/end', ...(_storedConfig?.networkIgnoreUrls || [])];
|
|
717
718
|
getNetworkInterceptor().initNetworkInterceptor(request => {
|
|
718
719
|
getAutoTracking().trackAPIRequest(request.success || false, request.statusCode, request.duration || 0, request.responseBodySize || 0);
|
|
@@ -865,7 +866,6 @@ const Rejourney = {
|
|
|
865
866
|
pixelRatio: 1
|
|
866
867
|
},
|
|
867
868
|
eventCount: 0,
|
|
868
|
-
videoSegmentCount: 0,
|
|
869
869
|
storageSize: 0,
|
|
870
870
|
sdkVersion: _constants.SDK_VERSION,
|
|
871
871
|
isComplete: false
|
|
@@ -941,10 +941,10 @@ const Rejourney = {
|
|
|
941
941
|
}, false);
|
|
942
942
|
},
|
|
943
943
|
/**
|
|
944
|
-
* Report a scroll event for
|
|
944
|
+
* Report a scroll event for visual replay timing
|
|
945
945
|
*
|
|
946
946
|
* Call this from your ScrollView's onScroll handler to improve scroll capture.
|
|
947
|
-
* The SDK captures
|
|
947
|
+
* The SDK captures visual replay frames continuously, and this helps log scroll events
|
|
948
948
|
* for timeline correlation during replay.
|
|
949
949
|
*
|
|
950
950
|
* @param scrollOffset - Current scroll offset (vertical or horizontal)
|
|
@@ -1111,6 +1111,23 @@ const Rejourney = {
|
|
|
1111
1111
|
getRejourneyNative().logEvent('network_request', networkEvent).catch(() => {});
|
|
1112
1112
|
}, undefined);
|
|
1113
1113
|
},
|
|
1114
|
+
/**
|
|
1115
|
+
* Log customer feedback (e.g. from an in-app survey or NPS widget).
|
|
1116
|
+
*
|
|
1117
|
+
* @param rating - Numeric rating (e.g. 1 to 5)
|
|
1118
|
+
* @param message - Associated feedback text or comment
|
|
1119
|
+
*/
|
|
1120
|
+
logFeedback(rating, message) {
|
|
1121
|
+
safeNativeCallSync('logFeedback', () => {
|
|
1122
|
+
const feedbackEvent = {
|
|
1123
|
+
type: 'feedback',
|
|
1124
|
+
timestamp: Date.now(),
|
|
1125
|
+
rating,
|
|
1126
|
+
message
|
|
1127
|
+
};
|
|
1128
|
+
getRejourneyNative().logEvent('feedback', feedbackEvent).catch(() => {});
|
|
1129
|
+
}, undefined);
|
|
1130
|
+
},
|
|
1114
1131
|
/**
|
|
1115
1132
|
* Get SDK telemetry metrics for observability
|
|
1116
1133
|
*
|
|
@@ -1146,17 +1163,12 @@ const Rejourney = {
|
|
|
1146
1163
|
});
|
|
1147
1164
|
},
|
|
1148
1165
|
/**
|
|
1149
|
-
* Trigger
|
|
1150
|
-
* Blocks the main thread for the specified duration
|
|
1166
|
+
* Trigger an ANR test by blocking the main thread for the specified duration.
|
|
1151
1167
|
*/
|
|
1152
1168
|
debugTriggerANR(durationMs) {
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
}, undefined);
|
|
1157
|
-
} else {
|
|
1158
|
-
getLogger().warn('debugTriggerANR is only available in development mode');
|
|
1159
|
-
}
|
|
1169
|
+
safeNativeCallSync('debugTriggerANR', () => {
|
|
1170
|
+
getRejourneyNative().debugTriggerANR(durationMs);
|
|
1171
|
+
}, undefined);
|
|
1160
1172
|
},
|
|
1161
1173
|
/**
|
|
1162
1174
|
* Mask a view by its nativeID prop (will be occluded in recordings)
|
|
@@ -130,6 +130,7 @@ let originalOnError = null;
|
|
|
130
130
|
let originalOnUnhandledRejection = null;
|
|
131
131
|
let originalConsoleError = null;
|
|
132
132
|
let _promiseRejectionTrackingDisable = null;
|
|
133
|
+
const FATAL_ERROR_FLUSH_DELAY_MS = 1200;
|
|
133
134
|
|
|
134
135
|
/**
|
|
135
136
|
* Initialize auto tracking features
|
|
@@ -144,6 +145,7 @@ function initAutoTracking(trackingConfig, callbacks = {}) {
|
|
|
144
145
|
trackJSErrors: true,
|
|
145
146
|
trackPromiseRejections: true,
|
|
146
147
|
trackReactNativeErrors: true,
|
|
148
|
+
trackConsoleLogs: true,
|
|
147
149
|
collectDeviceInfo: true,
|
|
148
150
|
maxSessionDurationMs: trackingConfig.maxSessionDurationMs,
|
|
149
151
|
...trackingConfig
|
|
@@ -154,6 +156,9 @@ function initAutoTracking(trackingConfig, callbacks = {}) {
|
|
|
154
156
|
onErrorCaptured = callbacks.onError || null;
|
|
155
157
|
onScreenChange = callbacks.onScreen || null;
|
|
156
158
|
setupErrorTracking();
|
|
159
|
+
if (config.trackConsoleLogs) {
|
|
160
|
+
setupConsoleTracking();
|
|
161
|
+
}
|
|
157
162
|
setupNavigationTracking();
|
|
158
163
|
loadAnonymousId().then(id => {
|
|
159
164
|
anonymousId = id;
|
|
@@ -167,11 +172,13 @@ function initAutoTracking(trackingConfig, callbacks = {}) {
|
|
|
167
172
|
function cleanupAutoTracking() {
|
|
168
173
|
if (!isInitialized) return;
|
|
169
174
|
restoreErrorHandlers();
|
|
175
|
+
restoreConsoleHandlers();
|
|
170
176
|
cleanupNavigationTracking();
|
|
171
177
|
|
|
172
178
|
// Reset state
|
|
173
179
|
tapHead = 0;
|
|
174
180
|
tapCount = 0;
|
|
181
|
+
consoleLogCount = 0;
|
|
175
182
|
metrics = createEmptyMetrics();
|
|
176
183
|
screensVisited = [];
|
|
177
184
|
currentScreen = '';
|
|
@@ -285,7 +292,7 @@ function setupErrorTracking() {
|
|
|
285
292
|
/**
|
|
286
293
|
* Setup React Native ErrorUtils handler
|
|
287
294
|
*
|
|
288
|
-
* CRITICAL FIX: For fatal errors, we delay calling the original handler
|
|
295
|
+
* CRITICAL FIX: For fatal errors, we delay calling the original handler briefly
|
|
289
296
|
* to give the React Native bridge time to flush the logEvent('error') call to the
|
|
290
297
|
* native TelemetryPipeline. Without this delay, the error event is queued on the
|
|
291
298
|
* JS→native bridge but the app crashes (via originalErrorHandler) before the bridge
|
|
@@ -309,10 +316,10 @@ function setupReactNativeErrorHandler() {
|
|
|
309
316
|
if (isFatal) {
|
|
310
317
|
// For fatal errors, delay the original handler so the native bridge
|
|
311
318
|
// has time to deliver the error event to TelemetryPipeline before
|
|
312
|
-
// the app terminates.
|
|
319
|
+
// the app terminates.
|
|
313
320
|
setTimeout(() => {
|
|
314
321
|
originalErrorHandler(error, isFatal);
|
|
315
|
-
},
|
|
322
|
+
}, FATAL_ERROR_FLUSH_DELAY_MS);
|
|
316
323
|
} else {
|
|
317
324
|
originalErrorHandler(error, isFatal);
|
|
318
325
|
}
|
|
@@ -367,7 +374,6 @@ function setupPromiseRejectionHandler() {
|
|
|
367
374
|
|
|
368
375
|
// Strategy 1: RN-specific promise rejection tracking polyfill
|
|
369
376
|
try {
|
|
370
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
371
377
|
const tracking = require('promise/setimmediate/rejection-tracking');
|
|
372
378
|
if (tracking && typeof tracking.enable === 'function') {
|
|
373
379
|
tracking.enable({
|
|
@@ -476,8 +482,27 @@ function restoreErrorHandlers() {
|
|
|
476
482
|
function trackError(error) {
|
|
477
483
|
metrics.errorCount++;
|
|
478
484
|
metrics.totalEvents++;
|
|
485
|
+
forwardErrorToNative(error);
|
|
479
486
|
if (onErrorCaptured) {
|
|
480
|
-
|
|
487
|
+
try {
|
|
488
|
+
onErrorCaptured(error);
|
|
489
|
+
} catch {
|
|
490
|
+
// Ignore callback exceptions so SDK error forwarding keeps working.
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function forwardErrorToNative(error) {
|
|
495
|
+
try {
|
|
496
|
+
const nativeModule = getRejourneyNativeModule();
|
|
497
|
+
if (!nativeModule || typeof nativeModule.logEvent !== 'function') return;
|
|
498
|
+
nativeModule.logEvent('error', {
|
|
499
|
+
message: error.message,
|
|
500
|
+
stack: error.stack,
|
|
501
|
+
name: error.name || 'Error',
|
|
502
|
+
timestamp: error.timestamp
|
|
503
|
+
}).catch(() => {});
|
|
504
|
+
} catch {
|
|
505
|
+
// Ignore native forwarding failures; SDK should never crash app code.
|
|
481
506
|
}
|
|
482
507
|
}
|
|
483
508
|
|
|
@@ -493,6 +518,83 @@ function captureError(message, stack, name) {
|
|
|
493
518
|
name: name || 'Error'
|
|
494
519
|
});
|
|
495
520
|
}
|
|
521
|
+
let originalConsoleLog = null;
|
|
522
|
+
let originalConsoleInfo = null;
|
|
523
|
+
let originalConsoleWarn = null;
|
|
524
|
+
|
|
525
|
+
// Cap console logs to prevent flooding the event pipeline
|
|
526
|
+
const MAX_CONSOLE_LOGS_PER_SESSION = 1000;
|
|
527
|
+
let consoleLogCount = 0;
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Setup console tracking to capture log statements
|
|
531
|
+
*/
|
|
532
|
+
function setupConsoleTracking() {
|
|
533
|
+
if (typeof console === 'undefined') return;
|
|
534
|
+
if (!originalConsoleLog) originalConsoleLog = console.log;
|
|
535
|
+
if (!originalConsoleInfo) originalConsoleInfo = console.info;
|
|
536
|
+
if (!originalConsoleWarn) originalConsoleWarn = console.warn;
|
|
537
|
+
const createConsoleInterceptor = (level, originalFn) => {
|
|
538
|
+
return (...args) => {
|
|
539
|
+
try {
|
|
540
|
+
const message = args.map(arg => {
|
|
541
|
+
if (typeof arg === 'string') return arg;
|
|
542
|
+
if (arg instanceof Error) return `${arg.name}: ${arg.message}${arg.stack ? `\n...` : ''}`;
|
|
543
|
+
try {
|
|
544
|
+
return JSON.stringify(arg);
|
|
545
|
+
} catch {
|
|
546
|
+
return String(arg);
|
|
547
|
+
}
|
|
548
|
+
}).join(' ');
|
|
549
|
+
|
|
550
|
+
// Enforce per-session cap and skip React Native unhandled-rejection noise.
|
|
551
|
+
if (consoleLogCount < MAX_CONSOLE_LOGS_PER_SESSION && !message.includes('Possible Unhandled Promise Rejection')) {
|
|
552
|
+
consoleLogCount++;
|
|
553
|
+
const nativeModule = getRejourneyNativeModule();
|
|
554
|
+
if (nativeModule) {
|
|
555
|
+
const logEvent = {
|
|
556
|
+
type: 'log',
|
|
557
|
+
timestamp: Date.now(),
|
|
558
|
+
level,
|
|
559
|
+
message: message.length > 2000 ? message.substring(0, 2000) + '...' : message
|
|
560
|
+
};
|
|
561
|
+
nativeModule.logEvent('log', logEvent).catch(() => {});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
} catch {
|
|
565
|
+
// Ignore any errors during interception
|
|
566
|
+
}
|
|
567
|
+
if (originalFn) {
|
|
568
|
+
originalFn.apply(console, args);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
};
|
|
572
|
+
console.log = createConsoleInterceptor('log', originalConsoleLog);
|
|
573
|
+
console.info = createConsoleInterceptor('info', originalConsoleInfo);
|
|
574
|
+
console.warn = createConsoleInterceptor('warn', originalConsoleWarn);
|
|
575
|
+
const currentConsoleError = console.error;
|
|
576
|
+
if (!originalConsoleError) originalConsoleError = currentConsoleError;
|
|
577
|
+
console.error = createConsoleInterceptor('error', currentConsoleError);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Restore console standard functions
|
|
582
|
+
*/
|
|
583
|
+
function restoreConsoleHandlers() {
|
|
584
|
+
if (originalConsoleLog) {
|
|
585
|
+
console.log = originalConsoleLog;
|
|
586
|
+
originalConsoleLog = null;
|
|
587
|
+
}
|
|
588
|
+
if (originalConsoleInfo) {
|
|
589
|
+
console.info = originalConsoleInfo;
|
|
590
|
+
originalConsoleInfo = null;
|
|
591
|
+
}
|
|
592
|
+
if (originalConsoleWarn) {
|
|
593
|
+
console.warn = originalConsoleWarn;
|
|
594
|
+
originalConsoleWarn = null;
|
|
595
|
+
}
|
|
596
|
+
// Note: console.error is restored in restoreErrorHandlers via originalConsoleError
|
|
597
|
+
}
|
|
496
598
|
let navigationPollingInterval = null;
|
|
497
599
|
let lastDetectedScreen = '';
|
|
498
600
|
let navigationSetupDone = false;
|
|
@@ -1016,7 +1118,26 @@ async function collectDeviceInfo() {
|
|
|
1016
1118
|
function generateAnonymousId() {
|
|
1017
1119
|
const timestamp = Date.now().toString(36);
|
|
1018
1120
|
const random = Math.random().toString(36).substring(2, 15);
|
|
1019
|
-
|
|
1121
|
+
const id = `anon_${timestamp}_${random}`;
|
|
1122
|
+
// Persist so the same ID survives app restarts
|
|
1123
|
+
_persistAnonymousId(id);
|
|
1124
|
+
return id;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Best-effort async persist of anonymous ID to native storage
|
|
1129
|
+
*/
|
|
1130
|
+
function _persistAnonymousId(id) {
|
|
1131
|
+
const nativeModule = getRejourneyNativeModule();
|
|
1132
|
+
if (!nativeModule?.setAnonymousId) return;
|
|
1133
|
+
try {
|
|
1134
|
+
const result = nativeModule.setAnonymousId(id);
|
|
1135
|
+
if (result && typeof result.catch === 'function') {
|
|
1136
|
+
result.catch(() => {});
|
|
1137
|
+
}
|
|
1138
|
+
} catch {
|
|
1139
|
+
// Native storage unavailable — ID will still be stable for this session
|
|
1140
|
+
}
|
|
1020
1141
|
}
|
|
1021
1142
|
|
|
1022
1143
|
/**
|
|
@@ -1047,17 +1168,41 @@ async function ensurePersistentAnonymousId() {
|
|
|
1047
1168
|
|
|
1048
1169
|
/**
|
|
1049
1170
|
* Load anonymous ID from persistent storage
|
|
1050
|
-
*
|
|
1171
|
+
* Checks native anonymous storage first, then falls back to native getUserIdentity,
|
|
1172
|
+
* and finally generates a new ID if nothing is persisted.
|
|
1051
1173
|
*/
|
|
1052
1174
|
async function loadAnonymousId() {
|
|
1053
1175
|
const nativeModule = getRejourneyNativeModule();
|
|
1054
|
-
|
|
1176
|
+
|
|
1177
|
+
// 1. Try native anonymous ID storage
|
|
1178
|
+
if (nativeModule?.getAnonymousId) {
|
|
1055
1179
|
try {
|
|
1056
|
-
|
|
1180
|
+
const stored = await nativeModule.getAnonymousId();
|
|
1181
|
+
if (stored && typeof stored === 'string') return stored;
|
|
1057
1182
|
} catch {
|
|
1058
|
-
|
|
1183
|
+
// Continue to fallbacks
|
|
1059
1184
|
}
|
|
1060
1185
|
}
|
|
1186
|
+
|
|
1187
|
+
// 2. Backward compatibility fallback for older native modules
|
|
1188
|
+
if (nativeModule?.getUserIdentity) {
|
|
1189
|
+
try {
|
|
1190
|
+
const nativeId = await nativeModule.getUserIdentity();
|
|
1191
|
+
if (nativeId && typeof nativeId === 'string') {
|
|
1192
|
+
const normalized = nativeId.trim();
|
|
1193
|
+
// Only migrate legacy anonymous identifiers. Never treat explicit user identities
|
|
1194
|
+
// as anonymous fingerprints, or session correlation becomes unstable.
|
|
1195
|
+
if (normalized.startsWith('anon_')) {
|
|
1196
|
+
_persistAnonymousId(normalized);
|
|
1197
|
+
return normalized;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
} catch {
|
|
1201
|
+
// Continue to fallback
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// 3. Generate and persist new ID
|
|
1061
1206
|
return generateAnonymousId();
|
|
1062
1207
|
}
|
|
1063
1208
|
|
|
@@ -1065,7 +1210,13 @@ async function loadAnonymousId() {
|
|
|
1065
1210
|
* Set a custom anonymous ID
|
|
1066
1211
|
*/
|
|
1067
1212
|
function setAnonymousId(id) {
|
|
1068
|
-
|
|
1213
|
+
const normalized = (id || '').trim();
|
|
1214
|
+
if (!normalized) {
|
|
1215
|
+
anonymousId = generateAnonymousId();
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
anonymousId = normalized;
|
|
1219
|
+
_persistAnonymousId(normalized);
|
|
1069
1220
|
}
|
|
1070
1221
|
var _default = exports.default = {
|
|
1071
1222
|
init: initAutoTracking,
|
|
@@ -37,7 +37,7 @@ var _version = require("./version");
|
|
|
37
37
|
/** Default configuration values */
|
|
38
38
|
const DEFAULT_CONFIG = exports.DEFAULT_CONFIG = {
|
|
39
39
|
enabled: true,
|
|
40
|
-
captureFPS: 0
|
|
40
|
+
captureFPS: 1.0,
|
|
41
41
|
captureOnEvents: true,
|
|
42
42
|
maxSessionDuration: 10 * 60 * 1000,
|
|
43
43
|
maxStorageSize: 50 * 1024 * 1024,
|
|
@@ -90,7 +90,7 @@ const PLAYBACK_SPEEDS = exports.PLAYBACK_SPEEDS = [0.5, 1, 2, 4];
|
|
|
90
90
|
|
|
91
91
|
/** Capture settings */
|
|
92
92
|
const CAPTURE_SETTINGS = exports.CAPTURE_SETTINGS = {
|
|
93
|
-
DEFAULT_FPS: 0
|
|
93
|
+
DEFAULT_FPS: 1.0,
|
|
94
94
|
MIN_FPS: 0.1,
|
|
95
95
|
MAX_FPS: 2,
|
|
96
96
|
CAPTURE_SCALE: 0.25,
|
|
@@ -61,6 +61,73 @@ const config = {
|
|
|
61
61
|
captureSizes: false
|
|
62
62
|
};
|
|
63
63
|
const SENSITIVE_KEYS = ['token', 'key', 'secret', 'password', 'auth', 'access_token', 'api_key'];
|
|
64
|
+
function getUtf8Size(text) {
|
|
65
|
+
if (!text) return 0;
|
|
66
|
+
if (typeof TextEncoder !== 'undefined') {
|
|
67
|
+
return new TextEncoder().encode(text).length;
|
|
68
|
+
}
|
|
69
|
+
return text.length;
|
|
70
|
+
}
|
|
71
|
+
function getBodySize(body) {
|
|
72
|
+
if (body == null) return 0;
|
|
73
|
+
if (typeof body === 'string') return getUtf8Size(body);
|
|
74
|
+
if (typeof ArrayBuffer !== 'undefined' && body instanceof ArrayBuffer) {
|
|
75
|
+
return body.byteLength;
|
|
76
|
+
}
|
|
77
|
+
if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(body)) {
|
|
78
|
+
return body.byteLength;
|
|
79
|
+
}
|
|
80
|
+
if (typeof Blob !== 'undefined' && body instanceof Blob) {
|
|
81
|
+
return body.size;
|
|
82
|
+
}
|
|
83
|
+
if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
|
|
84
|
+
return getUtf8Size(body.toString());
|
|
85
|
+
}
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
async function getFetchResponseSize(response) {
|
|
89
|
+
const contentLength = response.headers?.get?.('content-length');
|
|
90
|
+
if (contentLength) {
|
|
91
|
+
const parsed = parseInt(contentLength, 10);
|
|
92
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const cloned = response.clone();
|
|
96
|
+
const buffer = await cloned.arrayBuffer();
|
|
97
|
+
return buffer.byteLength;
|
|
98
|
+
} catch {
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function getXhrResponseSize(xhr) {
|
|
103
|
+
try {
|
|
104
|
+
const contentLength = xhr.getResponseHeader('content-length');
|
|
105
|
+
if (contentLength) {
|
|
106
|
+
const parsed = parseInt(contentLength, 10);
|
|
107
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// Ignore header access errors and fall through to body inspection.
|
|
111
|
+
}
|
|
112
|
+
const responseType = xhr.responseType;
|
|
113
|
+
if (responseType === '' || responseType === 'text') {
|
|
114
|
+
return getUtf8Size(xhr.responseText || '');
|
|
115
|
+
}
|
|
116
|
+
if (responseType === 'arraybuffer') {
|
|
117
|
+
return typeof ArrayBuffer !== 'undefined' && xhr.response instanceof ArrayBuffer ? xhr.response.byteLength : 0;
|
|
118
|
+
}
|
|
119
|
+
if (responseType === 'blob') {
|
|
120
|
+
return typeof Blob !== 'undefined' && xhr.response instanceof Blob ? xhr.response.size : 0;
|
|
121
|
+
}
|
|
122
|
+
if (responseType === 'json') {
|
|
123
|
+
try {
|
|
124
|
+
return getUtf8Size(JSON.stringify(xhr.response ?? ''));
|
|
125
|
+
} catch {
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
64
131
|
|
|
65
132
|
/**
|
|
66
133
|
* Scrub sensitive data from URL
|
|
@@ -211,7 +278,9 @@ function interceptFetch() {
|
|
|
211
278
|
}
|
|
212
279
|
const startTime = Date.now();
|
|
213
280
|
const method = (init?.method || 'GET').toUpperCase();
|
|
214
|
-
|
|
281
|
+
const requestBodySize = config.captureSizes ? getBodySize(init?.body) : 0;
|
|
282
|
+
return originalFetch(input, init).then(async response => {
|
|
283
|
+
const responseBodySize = config.captureSizes ? await getFetchResponseSize(response) : 0;
|
|
215
284
|
queueRequest({
|
|
216
285
|
requestId: `f${startTime}`,
|
|
217
286
|
method,
|
|
@@ -220,7 +289,9 @@ function interceptFetch() {
|
|
|
220
289
|
duration: Date.now() - startTime,
|
|
221
290
|
startTimestamp: startTime,
|
|
222
291
|
endTimestamp: Date.now(),
|
|
223
|
-
success: response.ok
|
|
292
|
+
success: response.ok,
|
|
293
|
+
requestBodySize,
|
|
294
|
+
responseBodySize
|
|
224
295
|
});
|
|
225
296
|
return response;
|
|
226
297
|
}, error => {
|
|
@@ -233,7 +304,8 @@ function interceptFetch() {
|
|
|
233
304
|
startTimestamp: startTime,
|
|
234
305
|
endTimestamp: Date.now(),
|
|
235
306
|
success: false,
|
|
236
|
-
errorMessage: error?.message || 'Network error'
|
|
307
|
+
errorMessage: error?.message || 'Network error',
|
|
308
|
+
requestBodySize
|
|
237
309
|
});
|
|
238
310
|
throw error;
|
|
239
311
|
});
|
|
@@ -268,9 +340,15 @@ function interceptXHR() {
|
|
|
268
340
|
if (!shouldSampleRequest(path)) {
|
|
269
341
|
return originalXHRSend.call(this, body);
|
|
270
342
|
}
|
|
343
|
+
if (config.captureSizes && body) {
|
|
344
|
+
data.reqSize = getBodySize(body);
|
|
345
|
+
} else {
|
|
346
|
+
data.reqSize = 0;
|
|
347
|
+
}
|
|
271
348
|
data.t = Date.now();
|
|
272
349
|
const onComplete = () => {
|
|
273
350
|
const endTime = Date.now();
|
|
351
|
+
const responseBodySize = config.captureSizes ? getXhrResponseSize(this) : 0;
|
|
274
352
|
queueRequest({
|
|
275
353
|
requestId: `x${data.t}`,
|
|
276
354
|
method: data.m,
|
|
@@ -280,7 +358,9 @@ function interceptXHR() {
|
|
|
280
358
|
startTimestamp: data.t,
|
|
281
359
|
endTimestamp: endTime,
|
|
282
360
|
success: this.status >= 200 && this.status < 400,
|
|
283
|
-
errorMessage: this.status === 0 ? 'Network error' : undefined
|
|
361
|
+
errorMessage: this.status === 0 ? 'Network error' : undefined,
|
|
362
|
+
requestBodySize: data.reqSize,
|
|
363
|
+
responseBodySize
|
|
284
364
|
});
|
|
285
365
|
};
|
|
286
366
|
this.addEventListener('load', onComplete);
|
package/lib/module/index.js
CHANGED
|
@@ -214,7 +214,7 @@ let _storedConfig = null;
|
|
|
214
214
|
// Abort recording (fail-closed)
|
|
215
215
|
|
|
216
216
|
let _remoteConfig = null;
|
|
217
|
-
let _sessionSampledOut = false; // True = telemetry only, no replay
|
|
217
|
+
let _sessionSampledOut = false; // True = telemetry only, no visual replay capture
|
|
218
218
|
|
|
219
219
|
/**
|
|
220
220
|
* Fetch project configuration from backend
|
|
@@ -510,7 +510,7 @@ const Rejourney = {
|
|
|
510
510
|
// =========================================================
|
|
511
511
|
_sessionSampledOut = !shouldRecordSession(_remoteConfig.sampleRate ?? 100);
|
|
512
512
|
if (_sessionSampledOut) {
|
|
513
|
-
getLogger().info(`Session sampled out (rate: ${_remoteConfig.sampleRate}%) - telemetry only, no replay
|
|
513
|
+
getLogger().info(`Session sampled out (rate: ${_remoteConfig.sampleRate}%) - telemetry only, no visual replay capture`);
|
|
514
514
|
}
|
|
515
515
|
|
|
516
516
|
// =========================================================
|
|
@@ -572,6 +572,7 @@ const Rejourney = {
|
|
|
572
572
|
trackJSErrors: true,
|
|
573
573
|
trackPromiseRejections: true,
|
|
574
574
|
trackReactNativeErrors: true,
|
|
575
|
+
trackConsoleLogs: _storedConfig?.trackConsoleLogs ?? true,
|
|
575
576
|
collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false
|
|
576
577
|
}, {
|
|
577
578
|
// Rage tap callback - log as frustration event
|
|
@@ -584,13 +585,8 @@ const Rejourney = {
|
|
|
584
585
|
});
|
|
585
586
|
getLogger().logFrustration(`Rage tap (${count} taps)`);
|
|
586
587
|
},
|
|
587
|
-
// Error callback -
|
|
588
|
+
// Error callback - SDK forwarding is handled in autoTracking.trackError
|
|
588
589
|
onError: error => {
|
|
589
|
-
this.logEvent('error', {
|
|
590
|
-
message: error.message,
|
|
591
|
-
stack: error.stack,
|
|
592
|
-
name: error.name
|
|
593
|
-
});
|
|
594
590
|
getLogger().logError(error.message);
|
|
595
591
|
},
|
|
596
592
|
onScreen: (_screenName, _previousScreen) => {}
|
|
@@ -605,6 +601,11 @@ const Rejourney = {
|
|
|
605
601
|
}
|
|
606
602
|
if (_storedConfig?.autoTrackNetwork !== false) {
|
|
607
603
|
try {
|
|
604
|
+
// JS-level fetch/XHR patching is the primary mechanism for capturing network
|
|
605
|
+
// calls within React Native. Native interceptors (RejourneyURLProtocol on iOS,
|
|
606
|
+
// RejourneyNetworkInterceptor on Android) are supplementary — they capture
|
|
607
|
+
// native-originated HTTP calls that bypass JS fetch(), but cannot intercept
|
|
608
|
+
// RN's own networking since it creates its NSURLSession/OkHttpClient at init time.
|
|
608
609
|
const ignoreUrls = [apiUrl, '/api/sdk/config', '/api/ingest/presign', '/api/ingest/batch/complete', '/api/ingest/session/end', ...(_storedConfig?.networkIgnoreUrls || [])];
|
|
609
610
|
getNetworkInterceptor().initNetworkInterceptor(request => {
|
|
610
611
|
getAutoTracking().trackAPIRequest(request.success || false, request.statusCode, request.duration || 0, request.responseBodySize || 0);
|
|
@@ -757,7 +758,6 @@ const Rejourney = {
|
|
|
757
758
|
pixelRatio: 1
|
|
758
759
|
},
|
|
759
760
|
eventCount: 0,
|
|
760
|
-
videoSegmentCount: 0,
|
|
761
761
|
storageSize: 0,
|
|
762
762
|
sdkVersion: SDK_VERSION,
|
|
763
763
|
isComplete: false
|
|
@@ -833,10 +833,10 @@ const Rejourney = {
|
|
|
833
833
|
}, false);
|
|
834
834
|
},
|
|
835
835
|
/**
|
|
836
|
-
* Report a scroll event for
|
|
836
|
+
* Report a scroll event for visual replay timing
|
|
837
837
|
*
|
|
838
838
|
* Call this from your ScrollView's onScroll handler to improve scroll capture.
|
|
839
|
-
* The SDK captures
|
|
839
|
+
* The SDK captures visual replay frames continuously, and this helps log scroll events
|
|
840
840
|
* for timeline correlation during replay.
|
|
841
841
|
*
|
|
842
842
|
* @param scrollOffset - Current scroll offset (vertical or horizontal)
|
|
@@ -1003,6 +1003,23 @@ const Rejourney = {
|
|
|
1003
1003
|
getRejourneyNative().logEvent('network_request', networkEvent).catch(() => {});
|
|
1004
1004
|
}, undefined);
|
|
1005
1005
|
},
|
|
1006
|
+
/**
|
|
1007
|
+
* Log customer feedback (e.g. from an in-app survey or NPS widget).
|
|
1008
|
+
*
|
|
1009
|
+
* @param rating - Numeric rating (e.g. 1 to 5)
|
|
1010
|
+
* @param message - Associated feedback text or comment
|
|
1011
|
+
*/
|
|
1012
|
+
logFeedback(rating, message) {
|
|
1013
|
+
safeNativeCallSync('logFeedback', () => {
|
|
1014
|
+
const feedbackEvent = {
|
|
1015
|
+
type: 'feedback',
|
|
1016
|
+
timestamp: Date.now(),
|
|
1017
|
+
rating,
|
|
1018
|
+
message
|
|
1019
|
+
};
|
|
1020
|
+
getRejourneyNative().logEvent('feedback', feedbackEvent).catch(() => {});
|
|
1021
|
+
}, undefined);
|
|
1022
|
+
},
|
|
1006
1023
|
/**
|
|
1007
1024
|
* Get SDK telemetry metrics for observability
|
|
1008
1025
|
*
|
|
@@ -1038,17 +1055,12 @@ const Rejourney = {
|
|
|
1038
1055
|
});
|
|
1039
1056
|
},
|
|
1040
1057
|
/**
|
|
1041
|
-
* Trigger
|
|
1042
|
-
* Blocks the main thread for the specified duration
|
|
1058
|
+
* Trigger an ANR test by blocking the main thread for the specified duration.
|
|
1043
1059
|
*/
|
|
1044
1060
|
debugTriggerANR(durationMs) {
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
}, undefined);
|
|
1049
|
-
} else {
|
|
1050
|
-
getLogger().warn('debugTriggerANR is only available in development mode');
|
|
1051
|
-
}
|
|
1061
|
+
safeNativeCallSync('debugTriggerANR', () => {
|
|
1062
|
+
getRejourneyNative().debugTriggerANR(durationMs);
|
|
1063
|
+
}, undefined);
|
|
1052
1064
|
},
|
|
1053
1065
|
/**
|
|
1054
1066
|
* Mask a view by its nativeID prop (will be occluded in recordings)
|