@rejourneyco/react-native 1.0.1 → 1.0.3
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 +72 -391
- package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +11 -113
- package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +1 -15
- package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +1 -61
- package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +3 -1
- package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +1 -22
- package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +3 -26
- package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +0 -2
- package/android/src/main/java/com/rejourney/network/UploadManager.kt +7 -93
- package/android/src/main/java/com/rejourney/network/UploadWorker.kt +5 -41
- package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +2 -58
- package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +4 -4
- package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +36 -7
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +7 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +9 -0
- package/ios/Capture/RJCaptureEngine.m +3 -34
- package/ios/Capture/RJVideoEncoder.m +0 -26
- package/ios/Capture/RJViewHierarchyScanner.m +68 -51
- package/ios/Core/RJLifecycleManager.m +0 -14
- package/ios/Core/Rejourney.mm +53 -129
- package/ios/Network/RJDeviceAuthManager.m +0 -2
- package/ios/Network/RJUploadManager.h +8 -0
- package/ios/Network/RJUploadManager.m +45 -0
- package/ios/Privacy/RJPrivacyMask.m +5 -31
- package/ios/Rejourney.h +0 -14
- package/ios/Touch/RJTouchInterceptor.m +21 -15
- package/ios/Utils/RJEventBuffer.m +57 -69
- package/ios/Utils/RJPerfTiming.m +0 -5
- package/ios/Utils/RJWindowUtils.m +87 -87
- package/lib/commonjs/components/Mask.js +1 -6
- package/lib/commonjs/index.js +46 -117
- package/lib/commonjs/sdk/autoTracking.js +39 -313
- package/lib/commonjs/sdk/constants.js +2 -13
- package/lib/commonjs/sdk/errorTracking.js +1 -29
- package/lib/commonjs/sdk/metricsTracking.js +3 -24
- package/lib/commonjs/sdk/navigation.js +3 -42
- package/lib/commonjs/sdk/networkInterceptor.js +7 -60
- package/lib/commonjs/sdk/utils.js +73 -19
- package/lib/module/components/Mask.js +1 -6
- package/lib/module/index.js +45 -121
- package/lib/module/sdk/autoTracking.js +39 -314
- package/lib/module/sdk/constants.js +2 -13
- package/lib/module/sdk/errorTracking.js +1 -29
- package/lib/module/sdk/index.js +0 -2
- package/lib/module/sdk/metricsTracking.js +3 -24
- package/lib/module/sdk/navigation.js +3 -42
- package/lib/module/sdk/networkInterceptor.js +7 -60
- package/lib/module/sdk/utils.js +73 -19
- package/lib/typescript/NativeRejourney.d.ts +1 -0
- package/lib/typescript/sdk/autoTracking.d.ts +4 -4
- package/lib/typescript/sdk/utils.d.ts +31 -1
- package/lib/typescript/types/index.d.ts +0 -1
- package/package.json +17 -11
- package/src/NativeRejourney.ts +2 -0
- package/src/components/Mask.tsx +0 -3
- package/src/index.ts +43 -92
- package/src/sdk/autoTracking.ts +51 -284
- package/src/sdk/constants.ts +13 -13
- package/src/sdk/errorTracking.ts +1 -17
- package/src/sdk/index.ts +0 -2
- package/src/sdk/metricsTracking.ts +5 -33
- package/src/sdk/navigation.ts +8 -29
- package/src/sdk/networkInterceptor.ts +9 -42
- package/src/sdk/utils.ts +76 -19
- package/src/types/index.ts +0 -29
|
@@ -61,9 +61,6 @@ function getPlatform() {
|
|
|
61
61
|
function getDimensions() {
|
|
62
62
|
return getRN()?.Dimensions;
|
|
63
63
|
}
|
|
64
|
-
function getNativeModules() {
|
|
65
|
-
return getRN()?.NativeModules;
|
|
66
|
-
}
|
|
67
64
|
function getRejourneyNativeModule() {
|
|
68
65
|
const RN = getRN();
|
|
69
66
|
if (!RN) return null;
|
|
@@ -76,7 +73,7 @@ function getRejourneyNativeModule() {
|
|
|
76
73
|
try {
|
|
77
74
|
nativeModule = TurboModuleRegistry.get('Rejourney');
|
|
78
75
|
} catch {
|
|
79
|
-
// Ignore
|
|
76
|
+
// Ignore
|
|
80
77
|
}
|
|
81
78
|
}
|
|
82
79
|
if (!nativeModule && NativeModules) {
|
|
@@ -84,57 +81,27 @@ function getRejourneyNativeModule() {
|
|
|
84
81
|
}
|
|
85
82
|
return nativeModule;
|
|
86
83
|
}
|
|
87
|
-
|
|
88
|
-
// Type declarations for browser globals (only used in hybrid apps where DOM is available)
|
|
89
|
-
// These don't exist in pure React Native but are needed for error tracking in hybrid scenarios
|
|
90
|
-
|
|
91
|
-
// Cast globalThis to work with both RN and hybrid scenarios
|
|
92
84
|
const _globalThis = globalThis;
|
|
93
|
-
|
|
94
|
-
// =============================================================================
|
|
95
|
-
// Types
|
|
96
|
-
// =============================================================================
|
|
97
|
-
|
|
98
|
-
// =============================================================================
|
|
99
|
-
// State
|
|
100
|
-
// =============================================================================
|
|
101
|
-
|
|
102
85
|
let isInitialized = false;
|
|
103
86
|
let config = {};
|
|
104
|
-
|
|
105
|
-
// Rage tap tracking
|
|
106
87
|
const recentTaps = [];
|
|
107
|
-
let tapHead = 0;
|
|
108
|
-
let tapCount = 0;
|
|
88
|
+
let tapHead = 0;
|
|
89
|
+
let tapCount = 0;
|
|
109
90
|
const MAX_RECENT_TAPS = 10;
|
|
110
|
-
|
|
111
|
-
// Session metrics
|
|
112
91
|
let metrics = createEmptyMetrics();
|
|
113
92
|
let sessionStartTime = 0;
|
|
114
93
|
let maxSessionDurationMs = 10 * 60 * 1000;
|
|
115
|
-
|
|
116
|
-
// Screen tracking
|
|
117
94
|
let currentScreen = '';
|
|
118
95
|
let screensVisited = [];
|
|
119
|
-
|
|
120
|
-
// Anonymous ID
|
|
121
96
|
let anonymousId = null;
|
|
122
97
|
let anonymousIdPromise = null;
|
|
123
|
-
|
|
124
|
-
// Callbacks
|
|
125
98
|
let onRageTapDetected = null;
|
|
126
99
|
let onErrorCaptured = null;
|
|
127
100
|
let onScreenChange = null;
|
|
128
|
-
|
|
129
|
-
// Original error handlers (for restoration)
|
|
130
101
|
let originalErrorHandler;
|
|
131
102
|
let originalOnError = null;
|
|
132
103
|
let originalOnUnhandledRejection = null;
|
|
133
104
|
|
|
134
|
-
// =============================================================================
|
|
135
|
-
// Initialization
|
|
136
|
-
// =============================================================================
|
|
137
|
-
|
|
138
105
|
/**
|
|
139
106
|
* Initialize auto tracking features
|
|
140
107
|
* Called automatically by Rejourney.init() - no user action needed
|
|
@@ -152,23 +119,13 @@ function initAutoTracking(trackingConfig, callbacks = {}) {
|
|
|
152
119
|
maxSessionDurationMs: trackingConfig.maxSessionDurationMs,
|
|
153
120
|
...trackingConfig
|
|
154
121
|
};
|
|
155
|
-
|
|
156
|
-
// Session timing
|
|
157
122
|
sessionStartTime = Date.now();
|
|
158
123
|
setMaxSessionDurationMinutes(trackingConfig.maxSessionDurationMs ? trackingConfig.maxSessionDurationMs / 60000 : undefined);
|
|
159
|
-
|
|
160
|
-
// Set callbacks
|
|
161
124
|
onRageTapDetected = callbacks.onRageTap || null;
|
|
162
125
|
onErrorCaptured = callbacks.onError || null;
|
|
163
126
|
onScreenChange = callbacks.onScreen || null;
|
|
164
|
-
|
|
165
|
-
// Setup error tracking
|
|
166
127
|
setupErrorTracking();
|
|
167
|
-
|
|
168
|
-
// Setup React Navigation tracking (if available)
|
|
169
128
|
setupNavigationTracking();
|
|
170
|
-
|
|
171
|
-
// Load anonymous ID from native storage (or generate new one)
|
|
172
129
|
loadAnonymousId().then(id => {
|
|
173
130
|
anonymousId = id;
|
|
174
131
|
});
|
|
@@ -180,11 +137,7 @@ function initAutoTracking(trackingConfig, callbacks = {}) {
|
|
|
180
137
|
*/
|
|
181
138
|
function cleanupAutoTracking() {
|
|
182
139
|
if (!isInitialized) return;
|
|
183
|
-
|
|
184
|
-
// Restore original error handlers
|
|
185
140
|
restoreErrorHandlers();
|
|
186
|
-
|
|
187
|
-
// Cleanup navigation tracking
|
|
188
141
|
cleanupNavigationTracking();
|
|
189
142
|
|
|
190
143
|
// Reset state
|
|
@@ -198,10 +151,6 @@ function cleanupAutoTracking() {
|
|
|
198
151
|
isInitialized = false;
|
|
199
152
|
}
|
|
200
153
|
|
|
201
|
-
// =============================================================================
|
|
202
|
-
// Rage Tap Detection
|
|
203
|
-
// =============================================================================
|
|
204
|
-
|
|
205
154
|
/**
|
|
206
155
|
* Track a tap event for rage tap detection
|
|
207
156
|
* Called automatically from touch interceptor
|
|
@@ -209,8 +158,6 @@ function cleanupAutoTracking() {
|
|
|
209
158
|
function trackTap(tap) {
|
|
210
159
|
if (!isInitialized) return;
|
|
211
160
|
const now = Date.now();
|
|
212
|
-
|
|
213
|
-
// Add to circular buffer (O(1) instead of shift() which is O(n))
|
|
214
161
|
const insertIndex = (tapHead + tapCount) % MAX_RECENT_TAPS;
|
|
215
162
|
if (tapCount < MAX_RECENT_TAPS) {
|
|
216
163
|
recentTaps[insertIndex] = {
|
|
@@ -219,15 +166,12 @@ function trackTap(tap) {
|
|
|
219
166
|
};
|
|
220
167
|
tapCount++;
|
|
221
168
|
} else {
|
|
222
|
-
// Buffer full, overwrite oldest
|
|
223
169
|
recentTaps[tapHead] = {
|
|
224
170
|
...tap,
|
|
225
171
|
timestamp: now
|
|
226
172
|
};
|
|
227
173
|
tapHead = (tapHead + 1) % MAX_RECENT_TAPS;
|
|
228
174
|
}
|
|
229
|
-
|
|
230
|
-
// Evict old taps outside time window
|
|
231
175
|
const windowStart = now - (config.rageTapTimeWindow || 500);
|
|
232
176
|
while (tapCount > 0) {
|
|
233
177
|
const oldestTap = recentTaps[tapHead];
|
|
@@ -238,11 +182,7 @@ function trackTap(tap) {
|
|
|
238
182
|
break;
|
|
239
183
|
}
|
|
240
184
|
}
|
|
241
|
-
|
|
242
|
-
// Check for rage tap
|
|
243
185
|
detectRageTap();
|
|
244
|
-
|
|
245
|
-
// Update metrics
|
|
246
186
|
metrics.touchCount++;
|
|
247
187
|
metrics.totalEvents++;
|
|
248
188
|
}
|
|
@@ -254,14 +194,11 @@ function detectRageTap() {
|
|
|
254
194
|
const threshold = config.rageTapThreshold || 3;
|
|
255
195
|
const radius = config.rageTapRadius || 50;
|
|
256
196
|
if (tapCount < threshold) return;
|
|
257
|
-
// Check last N taps from circular buffer
|
|
258
197
|
const tapsToCheck = [];
|
|
259
198
|
for (let i = 0; i < threshold; i++) {
|
|
260
199
|
const idx = (tapHead + tapCount - threshold + i) % MAX_RECENT_TAPS;
|
|
261
200
|
tapsToCheck.push(recentTaps[idx]);
|
|
262
201
|
}
|
|
263
|
-
|
|
264
|
-
// Calculate center point
|
|
265
202
|
let centerX = 0;
|
|
266
203
|
let centerY = 0;
|
|
267
204
|
for (const tap of tapsToCheck) {
|
|
@@ -270,8 +207,6 @@ function detectRageTap() {
|
|
|
270
207
|
}
|
|
271
208
|
centerX /= tapsToCheck.length;
|
|
272
209
|
centerY /= tapsToCheck.length;
|
|
273
|
-
|
|
274
|
-
// Check if all taps are within radius of center
|
|
275
210
|
let allWithinRadius = true;
|
|
276
211
|
for (const tap of tapsToCheck) {
|
|
277
212
|
const dx = tap.x - centerX;
|
|
@@ -283,9 +218,7 @@ function detectRageTap() {
|
|
|
283
218
|
}
|
|
284
219
|
}
|
|
285
220
|
if (allWithinRadius) {
|
|
286
|
-
// Rage tap detected!
|
|
287
221
|
metrics.rageTapCount++;
|
|
288
|
-
// Clear circular buffer to prevent duplicate detection
|
|
289
222
|
tapHead = 0;
|
|
290
223
|
tapCount = 0;
|
|
291
224
|
|
|
@@ -296,10 +229,6 @@ function detectRageTap() {
|
|
|
296
229
|
}
|
|
297
230
|
}
|
|
298
231
|
|
|
299
|
-
// =============================================================================
|
|
300
|
-
// State Change Notification
|
|
301
|
-
// =============================================================================
|
|
302
|
-
|
|
303
232
|
/**
|
|
304
233
|
* Notify that a state change occurred (navigation, modal, etc.)
|
|
305
234
|
* Kept for API compatibility
|
|
@@ -308,25 +237,16 @@ function notifyStateChange() {
|
|
|
308
237
|
// No-op - kept for backward compatibility
|
|
309
238
|
}
|
|
310
239
|
|
|
311
|
-
// =============================================================================
|
|
312
|
-
// Error Tracking
|
|
313
|
-
// =============================================================================
|
|
314
|
-
|
|
315
240
|
/**
|
|
316
241
|
* Setup automatic error tracking
|
|
317
242
|
*/
|
|
318
243
|
function setupErrorTracking() {
|
|
319
|
-
// Track React Native errors
|
|
320
244
|
if (config.trackReactNativeErrors !== false) {
|
|
321
245
|
setupReactNativeErrorHandler();
|
|
322
246
|
}
|
|
323
|
-
|
|
324
|
-
// Track JavaScript errors (only works in web/debug)
|
|
325
247
|
if (config.trackJSErrors !== false && typeof _globalThis !== 'undefined') {
|
|
326
248
|
setupJSErrorHandler();
|
|
327
249
|
}
|
|
328
|
-
|
|
329
|
-
// Track unhandled promise rejections
|
|
330
250
|
if (config.trackPromiseRejections !== false && typeof _globalThis !== 'undefined') {
|
|
331
251
|
setupPromiseRejectionHandler();
|
|
332
252
|
}
|
|
@@ -337,16 +257,10 @@ function setupErrorTracking() {
|
|
|
337
257
|
*/
|
|
338
258
|
function setupReactNativeErrorHandler() {
|
|
339
259
|
try {
|
|
340
|
-
// Access ErrorUtils from global scope
|
|
341
260
|
const ErrorUtils = _globalThis.ErrorUtils;
|
|
342
261
|
if (!ErrorUtils) return;
|
|
343
|
-
|
|
344
|
-
// Store original handler
|
|
345
262
|
originalErrorHandler = ErrorUtils.getGlobalHandler();
|
|
346
|
-
|
|
347
|
-
// Set new handler
|
|
348
263
|
ErrorUtils.setGlobalHandler((error, isFatal) => {
|
|
349
|
-
// Track the error
|
|
350
264
|
trackError({
|
|
351
265
|
type: 'error',
|
|
352
266
|
timestamp: Date.now(),
|
|
@@ -354,14 +268,12 @@ function setupReactNativeErrorHandler() {
|
|
|
354
268
|
stack: error.stack,
|
|
355
269
|
name: error.name || 'Error'
|
|
356
270
|
});
|
|
357
|
-
|
|
358
|
-
// Call original handler
|
|
359
271
|
if (originalErrorHandler) {
|
|
360
272
|
originalErrorHandler(error, isFatal);
|
|
361
273
|
}
|
|
362
274
|
});
|
|
363
275
|
} catch {
|
|
364
|
-
//
|
|
276
|
+
// Ignore
|
|
365
277
|
}
|
|
366
278
|
}
|
|
367
279
|
|
|
@@ -370,8 +282,6 @@ function setupReactNativeErrorHandler() {
|
|
|
370
282
|
*/
|
|
371
283
|
function setupJSErrorHandler() {
|
|
372
284
|
if (typeof _globalThis.onerror !== 'undefined') {
|
|
373
|
-
// Note: In React Native, this typically doesn't fire
|
|
374
|
-
// But we set it up anyway for hybrid apps
|
|
375
285
|
originalOnError = _globalThis.onerror;
|
|
376
286
|
_globalThis.onerror = (message, source, lineno, colno, error) => {
|
|
377
287
|
trackError({
|
|
@@ -381,8 +291,6 @@ function setupJSErrorHandler() {
|
|
|
381
291
|
stack: error?.stack || `${source}:${lineno}:${colno}`,
|
|
382
292
|
name: error?.name || 'Error'
|
|
383
293
|
});
|
|
384
|
-
|
|
385
|
-
// Call original handler
|
|
386
294
|
if (originalOnError) {
|
|
387
295
|
return originalOnError(message, source, lineno, colno, error);
|
|
388
296
|
}
|
|
@@ -415,7 +323,6 @@ function setupPromiseRejectionHandler() {
|
|
|
415
323
|
* Restore original error handlers
|
|
416
324
|
*/
|
|
417
325
|
function restoreErrorHandlers() {
|
|
418
|
-
// Restore React Native handler
|
|
419
326
|
if (originalErrorHandler) {
|
|
420
327
|
try {
|
|
421
328
|
const ErrorUtils = _globalThis.ErrorUtils;
|
|
@@ -427,14 +334,10 @@ function restoreErrorHandlers() {
|
|
|
427
334
|
}
|
|
428
335
|
originalErrorHandler = undefined;
|
|
429
336
|
}
|
|
430
|
-
|
|
431
|
-
// Restore global onerror
|
|
432
337
|
if (originalOnError !== null) {
|
|
433
338
|
_globalThis.onerror = originalOnError;
|
|
434
339
|
originalOnError = null;
|
|
435
340
|
}
|
|
436
|
-
|
|
437
|
-
// Remove promise rejection handler
|
|
438
341
|
if (originalOnUnhandledRejection && typeof _globalThis.removeEventListener !== 'undefined') {
|
|
439
342
|
_globalThis.removeEventListener('unhandledrejection', originalOnUnhandledRejection);
|
|
440
343
|
originalOnUnhandledRejection = null;
|
|
@@ -464,16 +367,10 @@ function captureError(message, stack, name) {
|
|
|
464
367
|
name: name || 'Error'
|
|
465
368
|
});
|
|
466
369
|
}
|
|
467
|
-
|
|
468
|
-
// =============================================================================
|
|
469
|
-
// Screen/Funnel Tracking - Automatic Navigation Detection
|
|
470
|
-
// =============================================================================
|
|
471
|
-
|
|
472
|
-
// Navigation detection state
|
|
473
370
|
let navigationPollingInterval = null;
|
|
474
371
|
let lastDetectedScreen = '';
|
|
475
372
|
let navigationSetupDone = false;
|
|
476
|
-
let navigationPollingErrors = 0;
|
|
373
|
+
let navigationPollingErrors = 0;
|
|
477
374
|
const MAX_POLLING_ERRORS = 10; // Stop polling after 10 consecutive errors
|
|
478
375
|
|
|
479
376
|
/**
|
|
@@ -497,8 +394,6 @@ function trackNavigationState(state) {
|
|
|
497
394
|
const {
|
|
498
395
|
normalizeScreenName
|
|
499
396
|
} = require('./navigation');
|
|
500
|
-
|
|
501
|
-
// Find the active screen recursively
|
|
502
397
|
const findActiveScreen = navState => {
|
|
503
398
|
if (!navState?.routes) return null;
|
|
504
399
|
const index = navState.index ?? navState.routes.length - 1;
|
|
@@ -513,7 +408,7 @@ function trackNavigationState(state) {
|
|
|
513
408
|
trackScreen(screenName);
|
|
514
409
|
}
|
|
515
410
|
} catch {
|
|
516
|
-
//
|
|
411
|
+
// Ignore
|
|
517
412
|
}
|
|
518
413
|
}
|
|
519
414
|
|
|
@@ -543,16 +438,11 @@ function trackNavigationState(state) {
|
|
|
543
438
|
* ```
|
|
544
439
|
*/
|
|
545
440
|
function useNavigationTracking() {
|
|
546
|
-
// Use React's useRef and useCallback to create stable references
|
|
547
441
|
const React = require('react');
|
|
548
442
|
const {
|
|
549
443
|
createNavigationContainerRef
|
|
550
444
|
} = require('@react-navigation/native');
|
|
551
|
-
|
|
552
|
-
// Create a stable navigation ref
|
|
553
445
|
const navigationRef = React.useRef(createNavigationContainerRef());
|
|
554
|
-
|
|
555
|
-
// Track initial screen when navigation is ready
|
|
556
446
|
const onReady = React.useCallback(() => {
|
|
557
447
|
try {
|
|
558
448
|
const currentRoute = navigationRef.current?.getCurrentRoute?.();
|
|
@@ -567,11 +457,9 @@ function useNavigationTracking() {
|
|
|
567
457
|
}
|
|
568
458
|
}
|
|
569
459
|
} catch {
|
|
570
|
-
//
|
|
460
|
+
// Ignore
|
|
571
461
|
}
|
|
572
462
|
}, []);
|
|
573
|
-
|
|
574
|
-
// Return props to spread on NavigationContainer
|
|
575
463
|
return {
|
|
576
464
|
ref: navigationRef.current,
|
|
577
465
|
onReady,
|
|
@@ -591,9 +479,6 @@ function setupNavigationTracking() {
|
|
|
591
479
|
if (__DEV__) {
|
|
592
480
|
_utils.logger.debug('Setting up navigation tracking...');
|
|
593
481
|
}
|
|
594
|
-
|
|
595
|
-
// Delay to ensure navigation is initialized - Expo Router needs more time
|
|
596
|
-
// We retry a few times with increasing delays
|
|
597
482
|
let attempts = 0;
|
|
598
483
|
const maxAttempts = 5;
|
|
599
484
|
const trySetup = () => {
|
|
@@ -607,8 +492,7 @@ function setupNavigationTracking() {
|
|
|
607
492
|
_utils.logger.debug('Expo Router setup: SUCCESS on attempt', attempts);
|
|
608
493
|
}
|
|
609
494
|
} else if (attempts < maxAttempts) {
|
|
610
|
-
|
|
611
|
-
const delay = 200 * attempts; // 200, 400, 600, 800ms
|
|
495
|
+
const delay = 200 * attempts;
|
|
612
496
|
if (__DEV__) {
|
|
613
497
|
_utils.logger.debug('Expo Router not ready, retrying in', delay, 'ms');
|
|
614
498
|
}
|
|
@@ -620,8 +504,6 @@ function setupNavigationTracking() {
|
|
|
620
504
|
}
|
|
621
505
|
}
|
|
622
506
|
};
|
|
623
|
-
|
|
624
|
-
// Start first attempt after 200ms
|
|
625
507
|
setTimeout(trySetup, 200);
|
|
626
508
|
}
|
|
627
509
|
|
|
@@ -647,14 +529,10 @@ function trySetupExpoRouter() {
|
|
|
647
529
|
normalizeScreenName,
|
|
648
530
|
getScreenNameFromPath
|
|
649
531
|
} = require('./navigation');
|
|
650
|
-
|
|
651
|
-
// Poll for route changes (expo-router doesn't expose a listener API outside hooks)
|
|
652
532
|
navigationPollingInterval = setInterval(() => {
|
|
653
533
|
try {
|
|
654
534
|
let state = null;
|
|
655
535
|
let stateSource = '';
|
|
656
|
-
|
|
657
|
-
// Method 1: Public accessors on router object
|
|
658
536
|
if (typeof router.getState === 'function') {
|
|
659
537
|
state = router.getState();
|
|
660
538
|
stateSource = 'router.getState()';
|
|
@@ -662,34 +540,25 @@ function trySetupExpoRouter() {
|
|
|
662
540
|
state = router.rootState;
|
|
663
541
|
stateSource = 'router.rootState';
|
|
664
542
|
}
|
|
665
|
-
|
|
666
|
-
// Method 2: Internal store access (works for both v3 and v6+)
|
|
667
543
|
if (!state) {
|
|
668
544
|
try {
|
|
669
545
|
const storeModule = require('expo-router/build/global-state/router-store');
|
|
670
546
|
if (storeModule?.store) {
|
|
671
|
-
// v6+: store.state or store.navigationRef
|
|
672
547
|
state = storeModule.store.state;
|
|
673
548
|
if (state) stateSource = 'store.state';
|
|
674
|
-
|
|
675
|
-
// v6+: Try navigationRef if state is undefined
|
|
676
549
|
if (!state && storeModule.store.navigationRef?.current) {
|
|
677
550
|
state = storeModule.store.navigationRef.current.getRootState?.();
|
|
678
551
|
if (state) stateSource = 'navigationRef.getRootState()';
|
|
679
552
|
}
|
|
680
|
-
|
|
681
|
-
// v3: store.rootState or store.initialState
|
|
682
553
|
if (!state) {
|
|
683
554
|
state = storeModule.store.rootState || storeModule.store.initialState;
|
|
684
555
|
if (state) stateSource = 'store.rootState/initialState';
|
|
685
556
|
}
|
|
686
557
|
}
|
|
687
558
|
} catch {
|
|
688
|
-
//
|
|
559
|
+
// Ignore
|
|
689
560
|
}
|
|
690
561
|
}
|
|
691
|
-
|
|
692
|
-
// Method 3: Try accessing via a different export path for v6
|
|
693
562
|
if (!state) {
|
|
694
563
|
try {
|
|
695
564
|
const imperative = require('expo-router/build/imperative-api');
|
|
@@ -698,11 +567,11 @@ function trySetupExpoRouter() {
|
|
|
698
567
|
if (state) stateSource = 'imperative-api';
|
|
699
568
|
}
|
|
700
569
|
} catch {
|
|
701
|
-
//
|
|
570
|
+
// Ignore
|
|
702
571
|
}
|
|
703
572
|
}
|
|
704
573
|
if (state) {
|
|
705
|
-
|
|
574
|
+
navigationPollingErrors = 0;
|
|
706
575
|
navigationPollingErrors = 0;
|
|
707
576
|
const screenName = extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName);
|
|
708
577
|
if (screenName && screenName !== lastDetectedScreen) {
|
|
@@ -713,21 +582,15 @@ function trySetupExpoRouter() {
|
|
|
713
582
|
trackScreen(screenName);
|
|
714
583
|
}
|
|
715
584
|
} else {
|
|
716
|
-
// Track consecutive failures to get state
|
|
717
585
|
navigationPollingErrors++;
|
|
718
586
|
if (__DEV__ && navigationPollingErrors === 1) {
|
|
719
587
|
_utils.logger.debug('Expo Router: Could not get navigation state');
|
|
720
588
|
}
|
|
721
589
|
if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
|
|
722
|
-
// Stop polling after too many errors to save CPU
|
|
723
|
-
if (__DEV__) {
|
|
724
|
-
_utils.logger.debug('Expo Router: Stopped polling after', MAX_POLLING_ERRORS, 'errors');
|
|
725
|
-
}
|
|
726
590
|
cleanupNavigationTracking();
|
|
727
591
|
}
|
|
728
592
|
}
|
|
729
593
|
} catch (e) {
|
|
730
|
-
// Error - track and potentially stop
|
|
731
594
|
navigationPollingErrors++;
|
|
732
595
|
if (__DEV__ && navigationPollingErrors === 1) {
|
|
733
596
|
_utils.logger.debug('Expo Router polling error:', e);
|
|
@@ -736,14 +599,12 @@ function trySetupExpoRouter() {
|
|
|
736
599
|
cleanupNavigationTracking();
|
|
737
600
|
}
|
|
738
601
|
}
|
|
739
|
-
}, 500);
|
|
740
|
-
|
|
602
|
+
}, 500);
|
|
741
603
|
return true;
|
|
742
604
|
} catch (e) {
|
|
743
605
|
if (__DEV__) {
|
|
744
606
|
_utils.logger.debug('Expo Router not available:', e);
|
|
745
607
|
}
|
|
746
|
-
// expo-router not installed
|
|
747
608
|
return false;
|
|
748
609
|
}
|
|
749
610
|
}
|
|
@@ -758,22 +619,12 @@ function extractScreenNameFromRouterState(state, getScreenNameFromPath, normaliz
|
|
|
758
619
|
if (!state?.routes) return null;
|
|
759
620
|
const route = state.routes[state.index ?? state.routes.length - 1];
|
|
760
621
|
if (!route) return null;
|
|
761
|
-
|
|
762
|
-
// Add current route name to accumulated segments
|
|
763
622
|
const newSegments = [...accumulatedSegments, route.name];
|
|
764
|
-
|
|
765
|
-
// If this route has nested state, recurse deeper
|
|
766
623
|
if (route.state) {
|
|
767
624
|
return extractScreenNameFromRouterState(route.state, getScreenNameFromPath, normalizeScreenName, newSegments);
|
|
768
625
|
}
|
|
769
|
-
|
|
770
|
-
// We've reached the deepest level - build the screen name
|
|
771
|
-
// Filter out group markers like (tabs), (main), (auth)
|
|
772
626
|
const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
|
|
773
|
-
|
|
774
|
-
// If after filtering we have no segments, use the last meaningful name
|
|
775
627
|
if (cleanSegments.length === 0) {
|
|
776
|
-
// Find the last non-group segment
|
|
777
628
|
for (let i = newSegments.length - 1; i >= 0; i--) {
|
|
778
629
|
const seg = newSegments[i];
|
|
779
630
|
if (seg && !seg.startsWith('(') && !seg.endsWith(')')) {
|
|
@@ -812,27 +663,17 @@ function trackScreen(screenName) {
|
|
|
812
663
|
}
|
|
813
664
|
const previousScreen = currentScreen;
|
|
814
665
|
currentScreen = screenName;
|
|
815
|
-
// Add to screens visited (only track for unique set, avoid large array copies)
|
|
816
666
|
screensVisited.push(screenName);
|
|
817
|
-
|
|
818
|
-
// Update unique screens count
|
|
819
667
|
const uniqueScreens = new Set(screensVisited);
|
|
820
668
|
metrics.uniqueScreensCount = uniqueScreens.size;
|
|
821
|
-
|
|
822
|
-
// Update navigation count
|
|
823
669
|
metrics.navigationCount++;
|
|
824
670
|
metrics.totalEvents++;
|
|
825
671
|
if (__DEV__) {
|
|
826
672
|
_utils.logger.debug('trackScreen:', screenName, '(total screens:', metrics.uniqueScreensCount, ')');
|
|
827
673
|
}
|
|
828
|
-
|
|
829
|
-
// Notify callback
|
|
830
674
|
if (onScreenChange) {
|
|
831
675
|
onScreenChange(screenName, previousScreen);
|
|
832
676
|
}
|
|
833
|
-
|
|
834
|
-
// IMPORTANT: Also notify native module to send to backend
|
|
835
|
-
// This is the key fix - without this, screens don't get recorded!
|
|
836
677
|
try {
|
|
837
678
|
const RejourneyNative = getRejourneyNativeModule();
|
|
838
679
|
if (RejourneyNative?.screenChanged) {
|
|
@@ -854,18 +695,12 @@ function trackScreen(screenName) {
|
|
|
854
695
|
}
|
|
855
696
|
}
|
|
856
697
|
|
|
857
|
-
// =============================================================================
|
|
858
|
-
// API Metrics Tracking
|
|
859
|
-
// =============================================================================
|
|
860
|
-
|
|
861
698
|
/**
|
|
862
699
|
* Track an API request with timing data
|
|
863
700
|
*/
|
|
864
701
|
function trackAPIRequest(success, _statusCode, durationMs = 0, responseBytes = 0) {
|
|
865
702
|
if (!isInitialized) return;
|
|
866
703
|
metrics.apiTotalCount++;
|
|
867
|
-
|
|
868
|
-
// Accumulate timing and size for avg calculation
|
|
869
704
|
if (durationMs > 0) {
|
|
870
705
|
metrics.netTotalDurationMs += durationMs;
|
|
871
706
|
}
|
|
@@ -876,16 +711,10 @@ function trackAPIRequest(success, _statusCode, durationMs = 0, responseBytes = 0
|
|
|
876
711
|
metrics.apiSuccessCount++;
|
|
877
712
|
} else {
|
|
878
713
|
metrics.apiErrorCount++;
|
|
879
|
-
|
|
880
|
-
// API errors also count toward error count for UX score
|
|
881
714
|
metrics.errorCount++;
|
|
882
715
|
}
|
|
883
716
|
}
|
|
884
717
|
|
|
885
|
-
// =============================================================================
|
|
886
|
-
// Session Metrics
|
|
887
|
-
// =============================================================================
|
|
888
|
-
|
|
889
718
|
/**
|
|
890
719
|
* Create empty metrics object
|
|
891
720
|
*/
|
|
@@ -943,18 +772,11 @@ function trackInput() {
|
|
|
943
772
|
* Get current session metrics
|
|
944
773
|
*/
|
|
945
774
|
function getSessionMetrics() {
|
|
946
|
-
// Calculate scores before returning
|
|
947
775
|
calculateScores();
|
|
948
|
-
|
|
949
|
-
// Compute average API response time
|
|
950
776
|
const netAvgDurationMs = metrics.apiTotalCount > 0 ? Math.round(metrics.netTotalDurationMs / metrics.apiTotalCount) : 0;
|
|
951
|
-
|
|
952
|
-
// Lazily populate screensVisited only when metrics are retrieved
|
|
953
|
-
// This avoids expensive array copies on every screen change
|
|
954
777
|
return {
|
|
955
778
|
...metrics,
|
|
956
779
|
screensVisited: [...screensVisited],
|
|
957
|
-
// Only copy here when needed
|
|
958
780
|
netAvgDurationMs
|
|
959
781
|
};
|
|
960
782
|
}
|
|
@@ -963,34 +785,15 @@ function getSessionMetrics() {
|
|
|
963
785
|
* Calculate session scores
|
|
964
786
|
*/
|
|
965
787
|
function calculateScores() {
|
|
966
|
-
// Interaction Score (0-100)
|
|
967
|
-
// Based on total interactions normalized to a baseline
|
|
968
788
|
const totalInteractions = metrics.touchCount + metrics.scrollCount + metrics.gestureCount + metrics.inputCount;
|
|
969
|
-
|
|
970
|
-
// Assume 50 interactions is "average" for a session
|
|
971
789
|
const avgInteractions = 50;
|
|
972
790
|
metrics.interactionScore = Math.min(100, Math.round(totalInteractions / avgInteractions * 100));
|
|
973
|
-
|
|
974
|
-
// Exploration Score (0-100)
|
|
975
|
-
// Based on unique screens visited
|
|
976
|
-
// Assume 5 screens is "average" exploration
|
|
977
791
|
const avgScreens = 5;
|
|
978
792
|
metrics.explorationScore = Math.min(100, Math.round(metrics.uniqueScreensCount / avgScreens * 100));
|
|
979
|
-
|
|
980
|
-
// UX Score (0-100)
|
|
981
|
-
// Starts at 100, deducts for issues
|
|
982
793
|
let uxScore = 100;
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
uxScore -= Math.min(
|
|
986
|
-
|
|
987
|
-
// Deduct for rage taps
|
|
988
|
-
uxScore -= Math.min(24, metrics.rageTapCount * 8); // Max 24 point deduction
|
|
989
|
-
|
|
990
|
-
// Deduct for API errors
|
|
991
|
-
uxScore -= Math.min(20, metrics.apiErrorCount * 10); // Max 20 point deduction
|
|
992
|
-
|
|
993
|
-
// Bonus for completing funnel (if screens > 3)
|
|
794
|
+
uxScore -= Math.min(30, metrics.errorCount * 15);
|
|
795
|
+
uxScore -= Math.min(24, metrics.rageTapCount * 8);
|
|
796
|
+
uxScore -= Math.min(20, metrics.apiErrorCount * 10);
|
|
994
797
|
if (metrics.uniqueScreensCount >= 3) {
|
|
995
798
|
uxScore += 5;
|
|
996
799
|
}
|
|
@@ -1008,43 +811,29 @@ function resetMetrics() {
|
|
|
1008
811
|
tapCount = 0;
|
|
1009
812
|
sessionStartTime = Date.now();
|
|
1010
813
|
}
|
|
1011
|
-
|
|
1012
|
-
// =============================================================================
|
|
1013
|
-
// Session duration helpers
|
|
1014
|
-
// =============================================================================
|
|
1015
|
-
|
|
1016
|
-
/** Clamp and set max session duration in minutes (1–10). Defaults to 10. */
|
|
1017
814
|
function setMaxSessionDurationMinutes(minutes) {
|
|
1018
815
|
const clampedMinutes = Math.min(10, Math.max(1, minutes ?? 10));
|
|
1019
816
|
maxSessionDurationMs = clampedMinutes * 60 * 1000;
|
|
1020
817
|
}
|
|
1021
|
-
|
|
1022
|
-
/** Returns true if the current session exceeded the configured max duration. */
|
|
1023
818
|
function hasExceededMaxSessionDuration() {
|
|
1024
819
|
if (!sessionStartTime) return false;
|
|
1025
820
|
return Date.now() - sessionStartTime >= maxSessionDurationMs;
|
|
1026
821
|
}
|
|
1027
|
-
|
|
1028
|
-
/** Returns remaining milliseconds until the session should stop. */
|
|
1029
822
|
function getRemainingSessionDurationMs() {
|
|
1030
823
|
if (!sessionStartTime) return maxSessionDurationMs;
|
|
1031
824
|
const remaining = maxSessionDurationMs - (Date.now() - sessionStartTime);
|
|
1032
825
|
return Math.max(0, remaining);
|
|
1033
826
|
}
|
|
1034
827
|
|
|
1035
|
-
// =============================================================================
|
|
1036
|
-
// Device Info Collection
|
|
1037
|
-
// =============================================================================
|
|
1038
|
-
|
|
1039
828
|
/**
|
|
1040
829
|
* Collect device information
|
|
1041
830
|
*/
|
|
1042
|
-
|
|
831
|
+
/**
|
|
832
|
+
* Collect device information
|
|
833
|
+
*/
|
|
834
|
+
async function collectDeviceInfo() {
|
|
1043
835
|
const Dimensions = getDimensions();
|
|
1044
836
|
const Platform = getPlatform();
|
|
1045
|
-
const NativeModules = getNativeModules();
|
|
1046
|
-
|
|
1047
|
-
// Default values if react-native isn't available
|
|
1048
837
|
let width = 0,
|
|
1049
838
|
height = 0,
|
|
1050
839
|
scale = 1;
|
|
@@ -1056,100 +845,43 @@ function collectDeviceInfo() {
|
|
|
1056
845
|
scale = screenDims?.scale || 1;
|
|
1057
846
|
}
|
|
1058
847
|
|
|
1059
|
-
//
|
|
1060
|
-
let model = 'Unknown';
|
|
1061
|
-
let manufacturer;
|
|
1062
|
-
let osVersion = 'Unknown';
|
|
1063
|
-
let appVersion;
|
|
1064
|
-
let appId;
|
|
848
|
+
// Basic JS-side info
|
|
1065
849
|
let locale;
|
|
1066
850
|
let timezone;
|
|
1067
851
|
try {
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
const DeviceInfo = require('react-native-device-info');
|
|
1071
|
-
model = DeviceInfo.getModel?.() || model;
|
|
1072
|
-
manufacturer = DeviceInfo.getBrand?.() || undefined;
|
|
1073
|
-
osVersion = DeviceInfo.getSystemVersion?.() || osVersion;
|
|
1074
|
-
appVersion = DeviceInfo.getVersion?.() || undefined;
|
|
1075
|
-
appId = DeviceInfo.getBundleId?.() || undefined;
|
|
1076
|
-
locale = DeviceInfo.getDeviceLocale?.() || undefined;
|
|
1077
|
-
timezone = DeviceInfo.getTimezone?.() || undefined;
|
|
852
|
+
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
853
|
+
locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
1078
854
|
} catch {
|
|
1079
|
-
//
|
|
1080
|
-
try {
|
|
1081
|
-
// Try expo-application for app version/id
|
|
1082
|
-
const Application = require('expo-application');
|
|
1083
|
-
appVersion = Application.nativeApplicationVersion || Application.applicationVersion || undefined;
|
|
1084
|
-
appId = Application.applicationId || undefined;
|
|
1085
|
-
} catch {
|
|
1086
|
-
// expo-application not available
|
|
1087
|
-
}
|
|
1088
|
-
try {
|
|
1089
|
-
// Try expo-constants for additional info
|
|
1090
|
-
const Constants = require('expo-constants');
|
|
1091
|
-
const expoConfig = Constants.expoConfig || Constants.manifest2?.extra?.expoClient || Constants.manifest;
|
|
1092
|
-
if (!appVersion && expoConfig?.version) {
|
|
1093
|
-
appVersion = expoConfig.version;
|
|
1094
|
-
}
|
|
1095
|
-
if (!appId && (expoConfig?.ios?.bundleIdentifier || expoConfig?.android?.package)) {
|
|
1096
|
-
appId = Platform?.OS === 'ios' ? expoConfig?.ios?.bundleIdentifier : expoConfig?.android?.package;
|
|
1097
|
-
}
|
|
1098
|
-
} catch {
|
|
1099
|
-
// expo-constants not available
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
// Fall back to basic platform info
|
|
1103
|
-
if (Platform?.OS === 'ios') {
|
|
1104
|
-
// Get basic info from constants
|
|
1105
|
-
const PlatformConstants = NativeModules?.PlatformConstants;
|
|
1106
|
-
osVersion = Platform.Version?.toString() || osVersion;
|
|
1107
|
-
model = PlatformConstants?.interfaceIdiom === 'pad' ? 'iPad' : 'iPhone';
|
|
1108
|
-
} else if (Platform?.OS === 'android') {
|
|
1109
|
-
osVersion = Platform.Version?.toString() || osVersion;
|
|
1110
|
-
model = 'Android Device';
|
|
1111
|
-
}
|
|
855
|
+
// Ignore
|
|
1112
856
|
}
|
|
1113
857
|
|
|
1114
|
-
// Get
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
} catch {
|
|
1119
|
-
timezone = undefined;
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
// Get locale
|
|
1124
|
-
if (!locale) {
|
|
858
|
+
// Get native info
|
|
859
|
+
const nativeModule = getRejourneyNativeModule();
|
|
860
|
+
let nativeInfo = {};
|
|
861
|
+
if (nativeModule && nativeModule.getDeviceInfo) {
|
|
1125
862
|
try {
|
|
1126
|
-
|
|
1127
|
-
} catch {
|
|
1128
|
-
|
|
863
|
+
nativeInfo = await nativeModule.getDeviceInfo();
|
|
864
|
+
} catch (e) {
|
|
865
|
+
if (__DEV__) {
|
|
866
|
+
console.warn('[Rejourney] Failed to get native device info:', e);
|
|
867
|
+
}
|
|
1129
868
|
}
|
|
1130
869
|
}
|
|
1131
870
|
return {
|
|
1132
|
-
model,
|
|
1133
|
-
manufacturer,
|
|
871
|
+
model: nativeInfo.model || 'Unknown',
|
|
872
|
+
manufacturer: nativeInfo.brand,
|
|
1134
873
|
os: Platform?.OS || 'ios',
|
|
1135
|
-
osVersion,
|
|
874
|
+
osVersion: nativeInfo.systemVersion || Platform?.Version?.toString() || 'Unknown',
|
|
1136
875
|
screenWidth: Math.round(width),
|
|
1137
876
|
screenHeight: Math.round(height),
|
|
1138
877
|
pixelRatio: scale,
|
|
1139
|
-
appVersion,
|
|
1140
|
-
appId,
|
|
1141
|
-
locale,
|
|
1142
|
-
timezone
|
|
878
|
+
appVersion: nativeInfo.appVersion,
|
|
879
|
+
appId: nativeInfo.bundleId,
|
|
880
|
+
locale: locale,
|
|
881
|
+
timezone: timezone
|
|
1143
882
|
};
|
|
1144
883
|
}
|
|
1145
884
|
|
|
1146
|
-
// =============================================================================
|
|
1147
|
-
// Anonymous ID Generation
|
|
1148
|
-
// =============================================================================
|
|
1149
|
-
|
|
1150
|
-
// Storage key for anonymous ID
|
|
1151
|
-
// const ANONYMOUS_ID_KEY = '@rejourney_anonymous_id';
|
|
1152
|
-
|
|
1153
885
|
/**
|
|
1154
886
|
* Generate a persistent anonymous ID
|
|
1155
887
|
*/
|
|
@@ -1177,7 +909,6 @@ async function ensurePersistentAnonymousId() {
|
|
|
1177
909
|
if (anonymousId) return anonymousId;
|
|
1178
910
|
if (!anonymousIdPromise) {
|
|
1179
911
|
anonymousIdPromise = (async () => {
|
|
1180
|
-
// Just use the load logic which now delegates to native or memory
|
|
1181
912
|
const id = await loadAnonymousId();
|
|
1182
913
|
anonymousId = id;
|
|
1183
914
|
return id;
|
|
@@ -1207,12 +938,7 @@ async function loadAnonymousId() {
|
|
|
1207
938
|
*/
|
|
1208
939
|
function setAnonymousId(id) {
|
|
1209
940
|
anonymousId = id;
|
|
1210
|
-
// No-op for async persistence as we moved to native-only or memory-only
|
|
1211
941
|
}
|
|
1212
|
-
|
|
1213
|
-
// =============================================================================
|
|
1214
|
-
// Exports
|
|
1215
|
-
// =============================================================================
|
|
1216
942
|
var _default = exports.default = {
|
|
1217
943
|
init: initAutoTracking,
|
|
1218
944
|
cleanup: cleanupAutoTracking,
|