@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.
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +89 -8
- 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 +3 -1
- package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +100 -0
- package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +222 -145
- package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +4 -0
- package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +3 -0
- package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +13 -0
- package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +8 -0
- package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +95 -21
- 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 +199 -115
- package/ios/Recording/AnrSentinel.swift +58 -25
- package/ios/Recording/InteractionRecorder.swift +1 -0
- package/ios/Recording/RejourneyURLProtocol.swift +168 -0
- package/ios/Recording/ReplayOrchestrator.swift +204 -143
- package/ios/Recording/SegmentDispatcher.swift +8 -0
- package/ios/Recording/StabilityMonitor.swift +40 -32
- package/ios/Recording/TelemetryPipeline.swift +17 -0
- package/ios/Recording/ViewHierarchyScanner.swift +1 -0
- package/ios/Recording/VisualCapture.swift +54 -8
- package/ios/Rejourney.mm +27 -8
- package/ios/Utility/ImageBlur.swift +0 -1
- package/lib/commonjs/index.js +28 -15
- package/lib/commonjs/sdk/autoTracking.js +162 -11
- package/lib/commonjs/sdk/networkInterceptor.js +84 -4
- package/lib/module/index.js +28 -15
- package/lib/module/sdk/autoTracking.js +162 -11
- package/lib/module/sdk/networkInterceptor.js +84 -4
- package/lib/typescript/NativeRejourney.d.ts +5 -2
- package/lib/typescript/sdk/autoTracking.d.ts +3 -1
- package/lib/typescript/types/index.d.ts +14 -2
- package/package.json +4 -4
- package/src/NativeRejourney.ts +8 -5
- package/src/index.ts +37 -19
- package/src/sdk/autoTracking.ts +176 -11
- package/src/sdk/networkInterceptor.ts +110 -1
- package/src/types/index.ts +15 -3
|
@@ -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,
|
|
@@ -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
|
@@ -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);
|
|
@@ -1002,6 +1003,23 @@ const Rejourney = {
|
|
|
1002
1003
|
getRejourneyNative().logEvent('network_request', networkEvent).catch(() => {});
|
|
1003
1004
|
}, undefined);
|
|
1004
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
|
+
},
|
|
1005
1023
|
/**
|
|
1006
1024
|
* Get SDK telemetry metrics for observability
|
|
1007
1025
|
*
|
|
@@ -1037,17 +1055,12 @@ const Rejourney = {
|
|
|
1037
1055
|
});
|
|
1038
1056
|
},
|
|
1039
1057
|
/**
|
|
1040
|
-
* Trigger
|
|
1041
|
-
* Blocks the main thread for the specified duration
|
|
1058
|
+
* Trigger an ANR test by blocking the main thread for the specified duration.
|
|
1042
1059
|
*/
|
|
1043
1060
|
debugTriggerANR(durationMs) {
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
}, undefined);
|
|
1048
|
-
} else {
|
|
1049
|
-
getLogger().warn('debugTriggerANR is only available in development mode');
|
|
1050
|
-
}
|
|
1061
|
+
safeNativeCallSync('debugTriggerANR', () => {
|
|
1062
|
+
getRejourneyNative().debugTriggerANR(durationMs);
|
|
1063
|
+
}, undefined);
|
|
1051
1064
|
},
|
|
1052
1065
|
/**
|
|
1053
1066
|
* Mask a view by its nativeID prop (will be occluded in recordings)
|