@rejourneyco/react-native 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +109 -26
- package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +18 -3
- package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +69 -17
- package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +27 -2
- package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +30 -0
- package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +100 -0
- package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +260 -174
- package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +246 -34
- package/android/src/main/java/com/rejourney/recording/SpecialCases.kt +572 -0
- package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +3 -0
- package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +19 -4
- package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +8 -0
- package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +251 -85
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +14 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +18 -0
- package/ios/Engine/DeviceRegistrar.swift +13 -3
- package/ios/Engine/RejourneyImpl.swift +202 -133
- package/ios/Recording/AnrSentinel.swift +58 -25
- package/ios/Recording/InteractionRecorder.swift +29 -0
- package/ios/Recording/RejourneyURLProtocol.swift +168 -0
- package/ios/Recording/ReplayOrchestrator.swift +241 -147
- package/ios/Recording/SegmentDispatcher.swift +155 -13
- package/ios/Recording/SpecialCases.swift +614 -0
- package/ios/Recording/StabilityMonitor.swift +42 -34
- package/ios/Recording/TelemetryPipeline.swift +38 -3
- package/ios/Recording/ViewHierarchyScanner.swift +1 -0
- package/ios/Recording/VisualCapture.swift +104 -28
- package/ios/Rejourney.mm +27 -8
- package/ios/Utility/ImageBlur.swift +0 -1
- package/lib/commonjs/index.js +32 -20
- package/lib/commonjs/sdk/autoTracking.js +162 -11
- package/lib/commonjs/sdk/constants.js +2 -2
- package/lib/commonjs/sdk/networkInterceptor.js +84 -4
- package/lib/commonjs/sdk/utils.js +1 -1
- package/lib/module/index.js +32 -20
- package/lib/module/sdk/autoTracking.js +162 -11
- package/lib/module/sdk/constants.js +2 -2
- package/lib/module/sdk/networkInterceptor.js +84 -4
- package/lib/module/sdk/utils.js +1 -1
- package/lib/typescript/NativeRejourney.d.ts +5 -2
- package/lib/typescript/sdk/autoTracking.d.ts +3 -1
- package/lib/typescript/sdk/constants.d.ts +2 -2
- package/lib/typescript/types/index.d.ts +15 -8
- package/package.json +4 -4
- package/src/NativeRejourney.ts +8 -5
- package/src/index.ts +46 -29
- package/src/sdk/autoTracking.ts +176 -11
- package/src/sdk/constants.ts +2 -2
- package/src/sdk/networkInterceptor.ts +110 -1
- package/src/sdk/utils.ts +1 -1
- package/src/types/index.ts +16 -9
package/src/index.ts
CHANGED
|
@@ -247,13 +247,13 @@ interface RemoteConfig {
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
// Result type for fetchRemoteConfig - distinguishes network errors from access denial
|
|
250
|
-
type ConfigFetchResult =
|
|
250
|
+
type ConfigFetchResult =
|
|
251
251
|
| { status: 'success'; config: RemoteConfig }
|
|
252
252
|
| { status: 'network_error' } // Proceed with defaults (fail-open)
|
|
253
253
|
| { status: 'access_denied'; httpStatus: number }; // Abort recording (fail-closed)
|
|
254
254
|
|
|
255
255
|
let _remoteConfig: RemoteConfig | null = null;
|
|
256
|
-
let _sessionSampledOut: boolean = false; // True = telemetry only, no replay
|
|
256
|
+
let _sessionSampledOut: boolean = false; // True = telemetry only, no visual replay capture
|
|
257
257
|
|
|
258
258
|
/**
|
|
259
259
|
* Fetch project configuration from backend
|
|
@@ -323,7 +323,7 @@ function shouldRecordSession(sampleRate: number): boolean {
|
|
|
323
323
|
// sampleRate is 0-100 (percentage)
|
|
324
324
|
if (sampleRate >= 100) return true;
|
|
325
325
|
if (sampleRate <= 0) return false;
|
|
326
|
-
|
|
326
|
+
|
|
327
327
|
const randomValue = Math.random() * 100;
|
|
328
328
|
return randomValue < sampleRate;
|
|
329
329
|
}
|
|
@@ -522,7 +522,7 @@ const Rejourney: RejourneyAPI = {
|
|
|
522
522
|
// This determines if recording is enabled and at what rate
|
|
523
523
|
// =========================================================
|
|
524
524
|
const configResult = await fetchRemoteConfig(apiUrl, publicKey);
|
|
525
|
-
|
|
525
|
+
|
|
526
526
|
// =========================================================
|
|
527
527
|
// CASE 0: Access denied (401/403) - abort immediately
|
|
528
528
|
// This means project disabled, invalid key, etc - HARD STOP
|
|
@@ -531,10 +531,10 @@ const Rejourney: RejourneyAPI = {
|
|
|
531
531
|
getLogger().info(`Recording disabled - access denied (${configResult.httpStatus})`);
|
|
532
532
|
return false;
|
|
533
533
|
}
|
|
534
|
-
|
|
534
|
+
|
|
535
535
|
// For success, extract the config; for network_error, proceed with null
|
|
536
536
|
_remoteConfig = configResult.status === 'success' ? configResult.config : null;
|
|
537
|
-
|
|
537
|
+
|
|
538
538
|
if (_remoteConfig) {
|
|
539
539
|
// =========================================================
|
|
540
540
|
// CASE 1: Rejourney completely disabled - abort early, nothing captured
|
|
@@ -560,7 +560,7 @@ const Rejourney: RejourneyAPI = {
|
|
|
560
560
|
// =========================================================
|
|
561
561
|
_sessionSampledOut = !shouldRecordSession(_remoteConfig.sampleRate ?? 100);
|
|
562
562
|
if (_sessionSampledOut) {
|
|
563
|
-
getLogger().info(`Session sampled out (rate: ${_remoteConfig.sampleRate}%) - telemetry only, no replay
|
|
563
|
+
getLogger().info(`Session sampled out (rate: ${_remoteConfig.sampleRate}%) - telemetry only, no visual replay capture`);
|
|
564
564
|
}
|
|
565
565
|
|
|
566
566
|
// =========================================================
|
|
@@ -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',
|
|
@@ -868,7 +869,6 @@ const Rejourney: RejourneyAPI = {
|
|
|
868
869
|
duration: 0,
|
|
869
870
|
deviceInfo: { model: '', os: 'ios', osVersion: '', screenWidth: 0, screenHeight: 0, pixelRatio: 1 },
|
|
870
871
|
eventCount: 0,
|
|
871
|
-
videoSegmentCount: 0,
|
|
872
872
|
storageSize: 0,
|
|
873
873
|
sdkVersion: SDK_VERSION,
|
|
874
874
|
isComplete: false,
|
|
@@ -957,10 +957,10 @@ const Rejourney: RejourneyAPI = {
|
|
|
957
957
|
},
|
|
958
958
|
|
|
959
959
|
/**
|
|
960
|
-
* Report a scroll event for
|
|
960
|
+
* Report a scroll event for visual replay timing
|
|
961
961
|
*
|
|
962
962
|
* Call this from your ScrollView's onScroll handler to improve scroll capture.
|
|
963
|
-
* The SDK captures
|
|
963
|
+
* The SDK captures visual replay frames continuously, and this helps log scroll events
|
|
964
964
|
* for timeline correlation during replay.
|
|
965
965
|
*
|
|
966
966
|
* @param scrollOffset - Current scroll offset (vertical or horizontal)
|
|
@@ -1155,6 +1155,28 @@ const Rejourney: RejourneyAPI = {
|
|
|
1155
1155
|
);
|
|
1156
1156
|
},
|
|
1157
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
|
+
|
|
1158
1180
|
/**
|
|
1159
1181
|
* Get SDK telemetry metrics for observability
|
|
1160
1182
|
*
|
|
@@ -1195,21 +1217,16 @@ const Rejourney: RejourneyAPI = {
|
|
|
1195
1217
|
},
|
|
1196
1218
|
|
|
1197
1219
|
/**
|
|
1198
|
-
* Trigger
|
|
1199
|
-
* Blocks the main thread for the specified duration
|
|
1220
|
+
* Trigger an ANR test by blocking the main thread for the specified duration.
|
|
1200
1221
|
*/
|
|
1201
1222
|
debugTriggerANR(durationMs: number): void {
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
()
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
);
|
|
1210
|
-
} else {
|
|
1211
|
-
getLogger().warn('debugTriggerANR is only available in development mode');
|
|
1212
|
-
}
|
|
1223
|
+
safeNativeCallSync(
|
|
1224
|
+
'debugTriggerANR',
|
|
1225
|
+
() => {
|
|
1226
|
+
getRejourneyNative()!.debugTriggerANR(durationMs);
|
|
1227
|
+
},
|
|
1228
|
+
undefined
|
|
1229
|
+
);
|
|
1213
1230
|
},
|
|
1214
1231
|
|
|
1215
1232
|
/**
|
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 {
|
package/src/sdk/constants.ts
CHANGED
|
@@ -26,7 +26,7 @@ export { SDK_VERSION };
|
|
|
26
26
|
/** Default configuration values */
|
|
27
27
|
export const DEFAULT_CONFIG = {
|
|
28
28
|
enabled: true,
|
|
29
|
-
captureFPS: 0
|
|
29
|
+
captureFPS: 1.0,
|
|
30
30
|
captureOnEvents: true,
|
|
31
31
|
maxSessionDuration: 10 * 60 * 1000,
|
|
32
32
|
maxStorageSize: 50 * 1024 * 1024,
|
|
@@ -79,7 +79,7 @@ export const PLAYBACK_SPEEDS = [0.5, 1, 2, 4] as const;
|
|
|
79
79
|
|
|
80
80
|
/** Capture settings */
|
|
81
81
|
export const CAPTURE_SETTINGS = {
|
|
82
|
-
DEFAULT_FPS: 0
|
|
82
|
+
DEFAULT_FPS: 1.0,
|
|
83
83
|
MIN_FPS: 0.1,
|
|
84
84
|
MAX_FPS: 2,
|
|
85
85
|
CAPTURE_SCALE: 0.25,
|
|
@@ -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
|
|