@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
package/src/sdk/autoTracking.ts
CHANGED
|
@@ -142,6 +142,7 @@ export interface AutoTrackingConfig {
|
|
|
142
142
|
trackJSErrors?: boolean;
|
|
143
143
|
trackPromiseRejections?: boolean;
|
|
144
144
|
trackReactNativeErrors?: boolean;
|
|
145
|
+
trackConsoleLogs?: boolean;
|
|
145
146
|
collectDeviceInfo?: boolean;
|
|
146
147
|
maxSessionDurationMs?: number;
|
|
147
148
|
detectDeadTaps?: boolean;
|
|
@@ -183,6 +184,7 @@ let originalOnError: OnErrorEventHandler | null = null;
|
|
|
183
184
|
let originalOnUnhandledRejection: ((event: PromiseRejectionEvent) => void) | null = null;
|
|
184
185
|
let originalConsoleError: ((...args: any[]) => void) | null = null;
|
|
185
186
|
let _promiseRejectionTrackingDisable: (() => void) | null = null;
|
|
187
|
+
const FATAL_ERROR_FLUSH_DELAY_MS = 1200;
|
|
186
188
|
|
|
187
189
|
/**
|
|
188
190
|
* Initialize auto tracking features
|
|
@@ -205,6 +207,7 @@ export function initAutoTracking(
|
|
|
205
207
|
trackJSErrors: true,
|
|
206
208
|
trackPromiseRejections: true,
|
|
207
209
|
trackReactNativeErrors: true,
|
|
210
|
+
trackConsoleLogs: true,
|
|
208
211
|
collectDeviceInfo: true,
|
|
209
212
|
maxSessionDurationMs: trackingConfig.maxSessionDurationMs,
|
|
210
213
|
...trackingConfig,
|
|
@@ -221,6 +224,9 @@ export function initAutoTracking(
|
|
|
221
224
|
onErrorCaptured = callbacks.onError || null;
|
|
222
225
|
onScreenChange = callbacks.onScreen || null;
|
|
223
226
|
setupErrorTracking();
|
|
227
|
+
if (config.trackConsoleLogs) {
|
|
228
|
+
setupConsoleTracking();
|
|
229
|
+
}
|
|
224
230
|
setupNavigationTracking();
|
|
225
231
|
loadAnonymousId().then(id => {
|
|
226
232
|
anonymousId = id;
|
|
@@ -236,11 +242,13 @@ export function cleanupAutoTracking(): void {
|
|
|
236
242
|
if (!isInitialized) return;
|
|
237
243
|
|
|
238
244
|
restoreErrorHandlers();
|
|
245
|
+
restoreConsoleHandlers();
|
|
239
246
|
cleanupNavigationTracking();
|
|
240
247
|
|
|
241
248
|
// Reset state
|
|
242
249
|
tapHead = 0;
|
|
243
250
|
tapCount = 0;
|
|
251
|
+
consoleLogCount = 0;
|
|
244
252
|
metrics = createEmptyMetrics();
|
|
245
253
|
screensVisited = [];
|
|
246
254
|
currentScreen = '';
|
|
@@ -357,7 +365,7 @@ function setupErrorTracking(): void {
|
|
|
357
365
|
/**
|
|
358
366
|
* Setup React Native ErrorUtils handler
|
|
359
367
|
*
|
|
360
|
-
* CRITICAL FIX: For fatal errors, we delay calling the original handler
|
|
368
|
+
* CRITICAL FIX: For fatal errors, we delay calling the original handler briefly
|
|
361
369
|
* to give the React Native bridge time to flush the logEvent('error') call to the
|
|
362
370
|
* native TelemetryPipeline. Without this delay, the error event is queued on the
|
|
363
371
|
* JS→native bridge but the app crashes (via originalErrorHandler) before the bridge
|
|
@@ -384,10 +392,10 @@ function setupReactNativeErrorHandler(): void {
|
|
|
384
392
|
if (isFatal) {
|
|
385
393
|
// For fatal errors, delay the original handler so the native bridge
|
|
386
394
|
// has time to deliver the error event to TelemetryPipeline before
|
|
387
|
-
// the app terminates.
|
|
395
|
+
// the app terminates.
|
|
388
396
|
setTimeout(() => {
|
|
389
397
|
originalErrorHandler!(error, isFatal);
|
|
390
|
-
},
|
|
398
|
+
}, FATAL_ERROR_FLUSH_DELAY_MS);
|
|
391
399
|
} else {
|
|
392
400
|
originalErrorHandler(error, isFatal);
|
|
393
401
|
}
|
|
@@ -450,7 +458,6 @@ function setupPromiseRejectionHandler(): void {
|
|
|
450
458
|
|
|
451
459
|
// Strategy 1: RN-specific promise rejection tracking polyfill
|
|
452
460
|
try {
|
|
453
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
454
461
|
const tracking = require('promise/setimmediate/rejection-tracking');
|
|
455
462
|
if (tracking && typeof tracking.enable === 'function') {
|
|
456
463
|
tracking.enable({
|
|
@@ -565,8 +572,30 @@ function trackError(error: ErrorEvent): void {
|
|
|
565
572
|
metrics.errorCount++;
|
|
566
573
|
metrics.totalEvents++;
|
|
567
574
|
|
|
575
|
+
forwardErrorToNative(error);
|
|
576
|
+
|
|
568
577
|
if (onErrorCaptured) {
|
|
569
|
-
|
|
578
|
+
try {
|
|
579
|
+
onErrorCaptured(error);
|
|
580
|
+
} catch {
|
|
581
|
+
// Ignore callback exceptions so SDK error forwarding keeps working.
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function forwardErrorToNative(error: ErrorEvent): void {
|
|
587
|
+
try {
|
|
588
|
+
const nativeModule = getRejourneyNativeModule();
|
|
589
|
+
if (!nativeModule || typeof nativeModule.logEvent !== 'function') return;
|
|
590
|
+
|
|
591
|
+
nativeModule.logEvent('error', {
|
|
592
|
+
message: error.message,
|
|
593
|
+
stack: error.stack,
|
|
594
|
+
name: error.name || 'Error',
|
|
595
|
+
timestamp: error.timestamp,
|
|
596
|
+
}).catch(() => { });
|
|
597
|
+
} catch {
|
|
598
|
+
// Ignore native forwarding failures; SDK should never crash app code.
|
|
570
599
|
}
|
|
571
600
|
}
|
|
572
601
|
|
|
@@ -587,6 +616,92 @@ export function captureError(
|
|
|
587
616
|
});
|
|
588
617
|
}
|
|
589
618
|
|
|
619
|
+
let originalConsoleLog: ((...args: any[]) => void) | null = null;
|
|
620
|
+
let originalConsoleInfo: ((...args: any[]) => void) | null = null;
|
|
621
|
+
let originalConsoleWarn: ((...args: any[]) => void) | null = null;
|
|
622
|
+
|
|
623
|
+
// Cap console logs to prevent flooding the event pipeline
|
|
624
|
+
const MAX_CONSOLE_LOGS_PER_SESSION = 1000;
|
|
625
|
+
let consoleLogCount = 0;
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Setup console tracking to capture log statements
|
|
629
|
+
*/
|
|
630
|
+
function setupConsoleTracking(): void {
|
|
631
|
+
if (typeof console === 'undefined') return;
|
|
632
|
+
|
|
633
|
+
if (!originalConsoleLog) originalConsoleLog = console.log;
|
|
634
|
+
if (!originalConsoleInfo) originalConsoleInfo = console.info;
|
|
635
|
+
if (!originalConsoleWarn) originalConsoleWarn = console.warn;
|
|
636
|
+
|
|
637
|
+
const createConsoleInterceptor = (level: 'log' | 'info' | 'warn' | 'error', originalFn: (...args: any[]) => void) => {
|
|
638
|
+
return (...args: any[]) => {
|
|
639
|
+
try {
|
|
640
|
+
const message = args.map(arg => {
|
|
641
|
+
if (typeof arg === 'string') return arg;
|
|
642
|
+
if (arg instanceof Error) return `${arg.name}: ${arg.message}${arg.stack ? `\n...` : ''}`;
|
|
643
|
+
try {
|
|
644
|
+
return JSON.stringify(arg);
|
|
645
|
+
} catch {
|
|
646
|
+
return String(arg);
|
|
647
|
+
}
|
|
648
|
+
}).join(' ');
|
|
649
|
+
|
|
650
|
+
// Enforce per-session cap and skip React Native unhandled-rejection noise.
|
|
651
|
+
if (
|
|
652
|
+
consoleLogCount < MAX_CONSOLE_LOGS_PER_SESSION &&
|
|
653
|
+
!message.includes('Possible Unhandled Promise Rejection')
|
|
654
|
+
) {
|
|
655
|
+
consoleLogCount++;
|
|
656
|
+
const nativeModule = getRejourneyNativeModule();
|
|
657
|
+
if (nativeModule) {
|
|
658
|
+
const logEvent = {
|
|
659
|
+
type: 'log',
|
|
660
|
+
timestamp: Date.now(),
|
|
661
|
+
level,
|
|
662
|
+
message: message.length > 2000 ? message.substring(0, 2000) + '...' : message,
|
|
663
|
+
};
|
|
664
|
+
nativeModule.logEvent('log', logEvent).catch(() => { });
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
} catch {
|
|
668
|
+
// Ignore any errors during interception
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (originalFn) {
|
|
672
|
+
originalFn.apply(console, args);
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
console.log = createConsoleInterceptor('log', originalConsoleLog!);
|
|
678
|
+
console.info = createConsoleInterceptor('info', originalConsoleInfo!);
|
|
679
|
+
console.warn = createConsoleInterceptor('warn', originalConsoleWarn!);
|
|
680
|
+
|
|
681
|
+
const currentConsoleError = console.error;
|
|
682
|
+
if (!originalConsoleError) originalConsoleError = currentConsoleError;
|
|
683
|
+
console.error = createConsoleInterceptor('error', currentConsoleError);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Restore console standard functions
|
|
688
|
+
*/
|
|
689
|
+
function restoreConsoleHandlers(): void {
|
|
690
|
+
if (originalConsoleLog) {
|
|
691
|
+
console.log = originalConsoleLog;
|
|
692
|
+
originalConsoleLog = null;
|
|
693
|
+
}
|
|
694
|
+
if (originalConsoleInfo) {
|
|
695
|
+
console.info = originalConsoleInfo;
|
|
696
|
+
originalConsoleInfo = null;
|
|
697
|
+
}
|
|
698
|
+
if (originalConsoleWarn) {
|
|
699
|
+
console.warn = originalConsoleWarn;
|
|
700
|
+
originalConsoleWarn = null;
|
|
701
|
+
}
|
|
702
|
+
// Note: console.error is restored in restoreErrorHandlers via originalConsoleError
|
|
703
|
+
}
|
|
704
|
+
|
|
590
705
|
let navigationPollingInterval: ReturnType<typeof setInterval> | null = null;
|
|
591
706
|
let lastDetectedScreen = '';
|
|
592
707
|
let navigationSetupDone = false;
|
|
@@ -1174,7 +1289,27 @@ export async function collectDeviceInfo(): Promise<DeviceInfo> {
|
|
|
1174
1289
|
function generateAnonymousId(): string {
|
|
1175
1290
|
const timestamp = Date.now().toString(36);
|
|
1176
1291
|
const random = Math.random().toString(36).substring(2, 15);
|
|
1177
|
-
|
|
1292
|
+
const id = `anon_${timestamp}_${random}`;
|
|
1293
|
+
// Persist so the same ID survives app restarts
|
|
1294
|
+
_persistAnonymousId(id);
|
|
1295
|
+
return id;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/**
|
|
1299
|
+
* Best-effort async persist of anonymous ID to native storage
|
|
1300
|
+
*/
|
|
1301
|
+
function _persistAnonymousId(id: string): void {
|
|
1302
|
+
const nativeModule = getRejourneyNativeModule();
|
|
1303
|
+
if (!nativeModule?.setAnonymousId) return;
|
|
1304
|
+
|
|
1305
|
+
try {
|
|
1306
|
+
const result = nativeModule.setAnonymousId(id);
|
|
1307
|
+
if (result && typeof result.catch === 'function') {
|
|
1308
|
+
result.catch(() => { });
|
|
1309
|
+
}
|
|
1310
|
+
} catch {
|
|
1311
|
+
// Native storage unavailable — ID will still be stable for this session
|
|
1312
|
+
}
|
|
1178
1313
|
}
|
|
1179
1314
|
|
|
1180
1315
|
/**
|
|
@@ -1205,17 +1340,41 @@ export async function ensurePersistentAnonymousId(): Promise<string> {
|
|
|
1205
1340
|
|
|
1206
1341
|
/**
|
|
1207
1342
|
* Load anonymous ID from persistent storage
|
|
1208
|
-
*
|
|
1343
|
+
* Checks native anonymous storage first, then falls back to native getUserIdentity,
|
|
1344
|
+
* and finally generates a new ID if nothing is persisted.
|
|
1209
1345
|
*/
|
|
1210
1346
|
export async function loadAnonymousId(): Promise<string> {
|
|
1211
1347
|
const nativeModule = getRejourneyNativeModule();
|
|
1212
|
-
|
|
1348
|
+
|
|
1349
|
+
// 1. Try native anonymous ID storage
|
|
1350
|
+
if (nativeModule?.getAnonymousId) {
|
|
1213
1351
|
try {
|
|
1214
|
-
|
|
1352
|
+
const stored = await nativeModule.getAnonymousId();
|
|
1353
|
+
if (stored && typeof stored === 'string') return stored;
|
|
1215
1354
|
} catch {
|
|
1216
|
-
|
|
1355
|
+
// Continue to fallbacks
|
|
1217
1356
|
}
|
|
1218
1357
|
}
|
|
1358
|
+
|
|
1359
|
+
// 2. Backward compatibility fallback for older native modules
|
|
1360
|
+
if (nativeModule?.getUserIdentity) {
|
|
1361
|
+
try {
|
|
1362
|
+
const nativeId = await nativeModule.getUserIdentity();
|
|
1363
|
+
if (nativeId && typeof nativeId === 'string') {
|
|
1364
|
+
const normalized = nativeId.trim();
|
|
1365
|
+
// Only migrate legacy anonymous identifiers. Never treat explicit user identities
|
|
1366
|
+
// as anonymous fingerprints, or session correlation becomes unstable.
|
|
1367
|
+
if (normalized.startsWith('anon_')) {
|
|
1368
|
+
_persistAnonymousId(normalized);
|
|
1369
|
+
return normalized;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
} catch {
|
|
1373
|
+
// Continue to fallback
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// 3. Generate and persist new ID
|
|
1219
1378
|
return generateAnonymousId();
|
|
1220
1379
|
}
|
|
1221
1380
|
|
|
@@ -1223,7 +1382,13 @@ export async function loadAnonymousId(): Promise<string> {
|
|
|
1223
1382
|
* Set a custom anonymous ID
|
|
1224
1383
|
*/
|
|
1225
1384
|
export function setAnonymousId(id: string): void {
|
|
1226
|
-
|
|
1385
|
+
const normalized = (id || '').trim();
|
|
1386
|
+
if (!normalized) {
|
|
1387
|
+
anonymousId = generateAnonymousId();
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
anonymousId = normalized;
|
|
1391
|
+
_persistAnonymousId(normalized);
|
|
1227
1392
|
}
|
|
1228
1393
|
|
|
1229
1394
|
export default {
|
|
@@ -59,6 +59,94 @@ const config = {
|
|
|
59
59
|
|
|
60
60
|
const SENSITIVE_KEYS = ['token', 'key', 'secret', 'password', 'auth', 'access_token', 'api_key'];
|
|
61
61
|
|
|
62
|
+
function getUtf8Size(text: string): number {
|
|
63
|
+
if (!text) return 0;
|
|
64
|
+
if (typeof TextEncoder !== 'undefined') {
|
|
65
|
+
return new TextEncoder().encode(text).length;
|
|
66
|
+
}
|
|
67
|
+
return text.length;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getBodySize(body: unknown): number {
|
|
71
|
+
if (body == null) return 0;
|
|
72
|
+
|
|
73
|
+
if (typeof body === 'string') return getUtf8Size(body);
|
|
74
|
+
|
|
75
|
+
if (typeof ArrayBuffer !== 'undefined' && body instanceof ArrayBuffer) {
|
|
76
|
+
return body.byteLength;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(body as any)) {
|
|
80
|
+
return (body as ArrayBufferView).byteLength;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof Blob !== 'undefined' && body instanceof Blob) {
|
|
84
|
+
return body.size;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
|
|
88
|
+
return getUtf8Size(body.toString());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function getFetchResponseSize(response: Response): Promise<number> {
|
|
95
|
+
const contentLength = response.headers?.get?.('content-length');
|
|
96
|
+
if (contentLength) {
|
|
97
|
+
const parsed = parseInt(contentLength, 10);
|
|
98
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const cloned = response.clone();
|
|
103
|
+
const buffer = await cloned.arrayBuffer();
|
|
104
|
+
return buffer.byteLength;
|
|
105
|
+
} catch {
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getXhrResponseSize(xhr: XMLHttpRequest): number {
|
|
111
|
+
try {
|
|
112
|
+
const contentLength = xhr.getResponseHeader('content-length');
|
|
113
|
+
if (contentLength) {
|
|
114
|
+
const parsed = parseInt(contentLength, 10);
|
|
115
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// Ignore header access errors and fall through to body inspection.
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const responseType = xhr.responseType;
|
|
122
|
+
|
|
123
|
+
if (responseType === '' || responseType === 'text') {
|
|
124
|
+
return getUtf8Size(xhr.responseText || '');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (responseType === 'arraybuffer') {
|
|
128
|
+
return typeof ArrayBuffer !== 'undefined' && xhr.response instanceof ArrayBuffer
|
|
129
|
+
? xhr.response.byteLength
|
|
130
|
+
: 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (responseType === 'blob') {
|
|
134
|
+
return typeof Blob !== 'undefined' && xhr.response instanceof Blob
|
|
135
|
+
? xhr.response.size
|
|
136
|
+
: 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (responseType === 'json') {
|
|
140
|
+
try {
|
|
141
|
+
return getUtf8Size(JSON.stringify(xhr.response ?? ''));
|
|
142
|
+
} catch {
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
62
150
|
/**
|
|
63
151
|
* Scrub sensitive data from URL
|
|
64
152
|
*/
|
|
@@ -225,8 +313,15 @@ function interceptFetch(): void {
|
|
|
225
313
|
|
|
226
314
|
const startTime = Date.now();
|
|
227
315
|
const method = ((init?.method || 'GET').toUpperCase()) as NetworkRequestParams['method'];
|
|
316
|
+
|
|
317
|
+
const requestBodySize = config.captureSizes ? getBodySize(init?.body) : 0;
|
|
318
|
+
|
|
228
319
|
return originalFetch!(input, init).then(
|
|
229
|
-
(response) => {
|
|
320
|
+
async (response) => {
|
|
321
|
+
const responseBodySize = config.captureSizes
|
|
322
|
+
? await getFetchResponseSize(response)
|
|
323
|
+
: 0;
|
|
324
|
+
|
|
230
325
|
queueRequest({
|
|
231
326
|
requestId: `f${startTime}`,
|
|
232
327
|
method,
|
|
@@ -236,6 +331,8 @@ function interceptFetch(): void {
|
|
|
236
331
|
startTimestamp: startTime,
|
|
237
332
|
endTimestamp: Date.now(),
|
|
238
333
|
success: response.ok,
|
|
334
|
+
requestBodySize,
|
|
335
|
+
responseBodySize,
|
|
239
336
|
});
|
|
240
337
|
return response;
|
|
241
338
|
},
|
|
@@ -250,6 +347,7 @@ function interceptFetch(): void {
|
|
|
250
347
|
endTimestamp: Date.now(),
|
|
251
348
|
success: false,
|
|
252
349
|
errorMessage: error?.message || 'Network error',
|
|
350
|
+
requestBodySize,
|
|
253
351
|
});
|
|
254
352
|
throw error;
|
|
255
353
|
}
|
|
@@ -296,10 +394,19 @@ function interceptXHR(): void {
|
|
|
296
394
|
return originalXHRSend!.call(this, body);
|
|
297
395
|
}
|
|
298
396
|
|
|
397
|
+
if (config.captureSizes && body) {
|
|
398
|
+
data.reqSize = getBodySize(body);
|
|
399
|
+
} else {
|
|
400
|
+
data.reqSize = 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
299
403
|
data.t = Date.now();
|
|
300
404
|
|
|
301
405
|
const onComplete = () => {
|
|
302
406
|
const endTime = Date.now();
|
|
407
|
+
|
|
408
|
+
const responseBodySize = config.captureSizes ? getXhrResponseSize(this) : 0;
|
|
409
|
+
|
|
303
410
|
queueRequest({
|
|
304
411
|
requestId: `x${data.t}`,
|
|
305
412
|
method: data.m as NetworkRequestParams['method'],
|
|
@@ -310,6 +417,8 @@ function interceptXHR(): void {
|
|
|
310
417
|
endTimestamp: endTime,
|
|
311
418
|
success: this.status >= 200 && this.status < 400,
|
|
312
419
|
errorMessage: this.status === 0 ? 'Network error' : undefined,
|
|
420
|
+
requestBodySize: data.reqSize,
|
|
421
|
+
responseBodySize,
|
|
313
422
|
});
|
|
314
423
|
};
|
|
315
424
|
|
package/src/types/index.ts
CHANGED
|
@@ -102,8 +102,12 @@ export interface RejourneyConfig {
|
|
|
102
102
|
* Disable if you want minimal network tracking overhead.
|
|
103
103
|
*/
|
|
104
104
|
networkCaptureSizes?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Automatically intercept console.log/info/warn/error and include them in session recordings.
|
|
107
|
+
* Useful for debugging sessions. Capped at 1,000 logs per session. (default: true)
|
|
108
|
+
*/
|
|
109
|
+
trackConsoleLogs?: boolean;
|
|
105
110
|
}
|
|
106
|
-
|
|
107
111
|
export type GestureType =
|
|
108
112
|
| 'tap'
|
|
109
113
|
| 'double_tap'
|
|
@@ -577,16 +581,24 @@ export interface RejourneyAPI {
|
|
|
577
581
|
/** Get storage usage */
|
|
578
582
|
getStorageUsage(): Promise<{ used: number; max: number }>;
|
|
579
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Log customer feedback (e.g. from an in-app survey or NPS widget).
|
|
586
|
+
*
|
|
587
|
+
* @param rating - Numeric rating (e.g. 1 to 5)
|
|
588
|
+
* @param message - Associated feedback text or comment
|
|
589
|
+
*/
|
|
590
|
+
logFeedback(rating: number, message: string): void;
|
|
591
|
+
|
|
580
592
|
/**
|
|
581
593
|
* Get SDK telemetry metrics for observability
|
|
594
|
+
|
|
582
595
|
* Returns metrics about SDK health including upload success rates,
|
|
583
596
|
* retry attempts, circuit breaker events, and memory pressure.
|
|
584
597
|
*/
|
|
585
598
|
getSDKMetrics(): Promise<SDKMetrics>;
|
|
586
599
|
|
|
587
600
|
/**
|
|
588
|
-
* Trigger
|
|
589
|
-
* Blocks the main thread for the specified duration
|
|
601
|
+
* Trigger an ANR test by blocking the main thread for the specified duration.
|
|
590
602
|
*/
|
|
591
603
|
debugTriggerANR(durationMs: number): void;
|
|
592
604
|
|