@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
|
@@ -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,
|
|
@@ -25,7 +25,7 @@ export { SDK_VERSION };
|
|
|
25
25
|
/** Default configuration values */
|
|
26
26
|
export const DEFAULT_CONFIG = {
|
|
27
27
|
enabled: true,
|
|
28
|
-
captureFPS: 0
|
|
28
|
+
captureFPS: 1.0,
|
|
29
29
|
captureOnEvents: true,
|
|
30
30
|
maxSessionDuration: 10 * 60 * 1000,
|
|
31
31
|
maxStorageSize: 50 * 1024 * 1024,
|
|
@@ -78,7 +78,7 @@ export const PLAYBACK_SPEEDS = [0.5, 1, 2, 4];
|
|
|
78
78
|
|
|
79
79
|
/** Capture settings */
|
|
80
80
|
export const CAPTURE_SETTINGS = {
|
|
81
|
-
DEFAULT_FPS: 0
|
|
81
|
+
DEFAULT_FPS: 1.0,
|
|
82
82
|
MIN_FPS: 0.1,
|
|
83
83
|
MAX_FPS: 2,
|
|
84
84
|
CAPTURE_SCALE: 0.25,
|
|
@@ -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);
|
package/lib/module/sdk/utils.js
CHANGED
|
@@ -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
|
/**
|
|
@@ -21,7 +21,7 @@ export { SDK_VERSION };
|
|
|
21
21
|
/** Default configuration values */
|
|
22
22
|
export declare const DEFAULT_CONFIG: {
|
|
23
23
|
readonly enabled: true;
|
|
24
|
-
readonly captureFPS:
|
|
24
|
+
readonly captureFPS: 1;
|
|
25
25
|
readonly captureOnEvents: true;
|
|
26
26
|
readonly maxSessionDuration: number;
|
|
27
27
|
readonly maxStorageSize: number;
|
|
@@ -70,7 +70,7 @@ export declare const GESTURE_TYPES: {
|
|
|
70
70
|
export declare const PLAYBACK_SPEEDS: readonly [0.5, 1, 2, 4];
|
|
71
71
|
/** Capture settings */
|
|
72
72
|
export declare const CAPTURE_SETTINGS: {
|
|
73
|
-
readonly DEFAULT_FPS:
|
|
73
|
+
readonly DEFAULT_FPS: 1;
|
|
74
74
|
readonly MIN_FPS: 0.1;
|
|
75
75
|
readonly MAX_FPS: 2;
|
|
76
76
|
readonly CAPTURE_SCALE: 0.25;
|
|
@@ -17,7 +17,7 @@ export interface RejourneyConfig {
|
|
|
17
17
|
packageName?: string;
|
|
18
18
|
/** Enable or disable recording (default: true) */
|
|
19
19
|
enabled?: boolean;
|
|
20
|
-
/**
|
|
20
|
+
/** Visual capture FPS (default: 1 = capture every 1000ms) */
|
|
21
21
|
captureFPS?: number;
|
|
22
22
|
/** Maximum session duration in milliseconds (default: 30 minutes) */
|
|
23
23
|
maxSessionDuration?: number;
|
|
@@ -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';
|
|
@@ -325,8 +330,6 @@ export interface SessionMetadata {
|
|
|
325
330
|
geoLocation?: GeoLocation;
|
|
326
331
|
/** Number of events in session */
|
|
327
332
|
eventCount: number;
|
|
328
|
-
/** Number of video segments */
|
|
329
|
-
videoSegmentCount?: number;
|
|
330
333
|
/** Total storage size in bytes */
|
|
331
334
|
storageSize: number;
|
|
332
335
|
/** Session tags */
|
|
@@ -349,7 +352,6 @@ export interface SessionSummary {
|
|
|
349
352
|
endTime?: number;
|
|
350
353
|
duration?: number;
|
|
351
354
|
eventCount: number;
|
|
352
|
-
videoSegmentCount?: number;
|
|
353
355
|
storageSize: number;
|
|
354
356
|
isComplete: boolean;
|
|
355
357
|
filePath: string;
|
|
@@ -363,8 +365,6 @@ export interface ReplayState {
|
|
|
363
365
|
speed: 0.5 | 1 | 2 | 4;
|
|
364
366
|
/** Session duration */
|
|
365
367
|
duration: number;
|
|
366
|
-
/** Current video segment time position */
|
|
367
|
-
currentVideoTime?: number;
|
|
368
368
|
/** Events at or near current time */
|
|
369
369
|
activeEvents: SessionEvent[];
|
|
370
370
|
}
|
|
@@ -497,15 +497,22 @@ export interface RejourneyAPI {
|
|
|
497
497
|
used: number;
|
|
498
498
|
max: number;
|
|
499
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;
|
|
500
507
|
/**
|
|
501
508
|
* Get SDK telemetry metrics for observability
|
|
509
|
+
|
|
502
510
|
* Returns metrics about SDK health including upload success rates,
|
|
503
511
|
* retry attempts, circuit breaker events, and memory pressure.
|
|
504
512
|
*/
|
|
505
513
|
getSDKMetrics(): Promise<SDKMetrics>;
|
|
506
514
|
/**
|
|
507
|
-
* Trigger
|
|
508
|
-
* Blocks the main thread for the specified duration
|
|
515
|
+
* Trigger an ANR test by blocking the main thread for the specified duration.
|
|
509
516
|
*/
|
|
510
517
|
debugTriggerANR(durationMs: number): void;
|
|
511
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
|
/**
|