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