@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
|
@@ -34,9 +34,6 @@ function getPlatform() {
|
|
|
34
34
|
function getDimensions() {
|
|
35
35
|
return getRN()?.Dimensions;
|
|
36
36
|
}
|
|
37
|
-
function getNativeModules() {
|
|
38
|
-
return getRN()?.NativeModules;
|
|
39
|
-
}
|
|
40
37
|
function getRejourneyNativeModule() {
|
|
41
38
|
const RN = getRN();
|
|
42
39
|
if (!RN) return null;
|
|
@@ -49,7 +46,7 @@ function getRejourneyNativeModule() {
|
|
|
49
46
|
try {
|
|
50
47
|
nativeModule = TurboModuleRegistry.get('Rejourney');
|
|
51
48
|
} catch {
|
|
52
|
-
// Ignore
|
|
49
|
+
// Ignore
|
|
53
50
|
}
|
|
54
51
|
}
|
|
55
52
|
if (!nativeModule && NativeModules) {
|
|
@@ -57,57 +54,27 @@ function getRejourneyNativeModule() {
|
|
|
57
54
|
}
|
|
58
55
|
return nativeModule;
|
|
59
56
|
}
|
|
60
|
-
|
|
61
|
-
// Type declarations for browser globals (only used in hybrid apps where DOM is available)
|
|
62
|
-
// These don't exist in pure React Native but are needed for error tracking in hybrid scenarios
|
|
63
|
-
|
|
64
|
-
// Cast globalThis to work with both RN and hybrid scenarios
|
|
65
57
|
const _globalThis = globalThis;
|
|
66
|
-
|
|
67
|
-
// =============================================================================
|
|
68
|
-
// Types
|
|
69
|
-
// =============================================================================
|
|
70
|
-
|
|
71
|
-
// =============================================================================
|
|
72
|
-
// State
|
|
73
|
-
// =============================================================================
|
|
74
|
-
|
|
75
58
|
let isInitialized = false;
|
|
76
59
|
let config = {};
|
|
77
|
-
|
|
78
|
-
// Rage tap tracking
|
|
79
60
|
const recentTaps = [];
|
|
80
|
-
let tapHead = 0;
|
|
81
|
-
let tapCount = 0;
|
|
61
|
+
let tapHead = 0;
|
|
62
|
+
let tapCount = 0;
|
|
82
63
|
const MAX_RECENT_TAPS = 10;
|
|
83
|
-
|
|
84
|
-
// Session metrics
|
|
85
64
|
let metrics = createEmptyMetrics();
|
|
86
65
|
let sessionStartTime = 0;
|
|
87
66
|
let maxSessionDurationMs = 10 * 60 * 1000;
|
|
88
|
-
|
|
89
|
-
// Screen tracking
|
|
90
67
|
let currentScreen = '';
|
|
91
68
|
let screensVisited = [];
|
|
92
|
-
|
|
93
|
-
// Anonymous ID
|
|
94
69
|
let anonymousId = null;
|
|
95
70
|
let anonymousIdPromise = null;
|
|
96
|
-
|
|
97
|
-
// Callbacks
|
|
98
71
|
let onRageTapDetected = null;
|
|
99
72
|
let onErrorCaptured = null;
|
|
100
73
|
let onScreenChange = null;
|
|
101
|
-
|
|
102
|
-
// Original error handlers (for restoration)
|
|
103
74
|
let originalErrorHandler;
|
|
104
75
|
let originalOnError = null;
|
|
105
76
|
let originalOnUnhandledRejection = null;
|
|
106
77
|
|
|
107
|
-
// =============================================================================
|
|
108
|
-
// Initialization
|
|
109
|
-
// =============================================================================
|
|
110
|
-
|
|
111
78
|
/**
|
|
112
79
|
* Initialize auto tracking features
|
|
113
80
|
* Called automatically by Rejourney.init() - no user action needed
|
|
@@ -125,23 +92,13 @@ export function initAutoTracking(trackingConfig, callbacks = {}) {
|
|
|
125
92
|
maxSessionDurationMs: trackingConfig.maxSessionDurationMs,
|
|
126
93
|
...trackingConfig
|
|
127
94
|
};
|
|
128
|
-
|
|
129
|
-
// Session timing
|
|
130
95
|
sessionStartTime = Date.now();
|
|
131
96
|
setMaxSessionDurationMinutes(trackingConfig.maxSessionDurationMs ? trackingConfig.maxSessionDurationMs / 60000 : undefined);
|
|
132
|
-
|
|
133
|
-
// Set callbacks
|
|
134
97
|
onRageTapDetected = callbacks.onRageTap || null;
|
|
135
98
|
onErrorCaptured = callbacks.onError || null;
|
|
136
99
|
onScreenChange = callbacks.onScreen || null;
|
|
137
|
-
|
|
138
|
-
// Setup error tracking
|
|
139
100
|
setupErrorTracking();
|
|
140
|
-
|
|
141
|
-
// Setup React Navigation tracking (if available)
|
|
142
101
|
setupNavigationTracking();
|
|
143
|
-
|
|
144
|
-
// Load anonymous ID from native storage (or generate new one)
|
|
145
102
|
loadAnonymousId().then(id => {
|
|
146
103
|
anonymousId = id;
|
|
147
104
|
});
|
|
@@ -153,11 +110,7 @@ export function initAutoTracking(trackingConfig, callbacks = {}) {
|
|
|
153
110
|
*/
|
|
154
111
|
export function cleanupAutoTracking() {
|
|
155
112
|
if (!isInitialized) return;
|
|
156
|
-
|
|
157
|
-
// Restore original error handlers
|
|
158
113
|
restoreErrorHandlers();
|
|
159
|
-
|
|
160
|
-
// Cleanup navigation tracking
|
|
161
114
|
cleanupNavigationTracking();
|
|
162
115
|
|
|
163
116
|
// Reset state
|
|
@@ -171,10 +124,6 @@ export function cleanupAutoTracking() {
|
|
|
171
124
|
isInitialized = false;
|
|
172
125
|
}
|
|
173
126
|
|
|
174
|
-
// =============================================================================
|
|
175
|
-
// Rage Tap Detection
|
|
176
|
-
// =============================================================================
|
|
177
|
-
|
|
178
127
|
/**
|
|
179
128
|
* Track a tap event for rage tap detection
|
|
180
129
|
* Called automatically from touch interceptor
|
|
@@ -182,8 +131,6 @@ export function cleanupAutoTracking() {
|
|
|
182
131
|
export function trackTap(tap) {
|
|
183
132
|
if (!isInitialized) return;
|
|
184
133
|
const now = Date.now();
|
|
185
|
-
|
|
186
|
-
// Add to circular buffer (O(1) instead of shift() which is O(n))
|
|
187
134
|
const insertIndex = (tapHead + tapCount) % MAX_RECENT_TAPS;
|
|
188
135
|
if (tapCount < MAX_RECENT_TAPS) {
|
|
189
136
|
recentTaps[insertIndex] = {
|
|
@@ -192,15 +139,12 @@ export function trackTap(tap) {
|
|
|
192
139
|
};
|
|
193
140
|
tapCount++;
|
|
194
141
|
} else {
|
|
195
|
-
// Buffer full, overwrite oldest
|
|
196
142
|
recentTaps[tapHead] = {
|
|
197
143
|
...tap,
|
|
198
144
|
timestamp: now
|
|
199
145
|
};
|
|
200
146
|
tapHead = (tapHead + 1) % MAX_RECENT_TAPS;
|
|
201
147
|
}
|
|
202
|
-
|
|
203
|
-
// Evict old taps outside time window
|
|
204
148
|
const windowStart = now - (config.rageTapTimeWindow || 500);
|
|
205
149
|
while (tapCount > 0) {
|
|
206
150
|
const oldestTap = recentTaps[tapHead];
|
|
@@ -211,11 +155,7 @@ export function trackTap(tap) {
|
|
|
211
155
|
break;
|
|
212
156
|
}
|
|
213
157
|
}
|
|
214
|
-
|
|
215
|
-
// Check for rage tap
|
|
216
158
|
detectRageTap();
|
|
217
|
-
|
|
218
|
-
// Update metrics
|
|
219
159
|
metrics.touchCount++;
|
|
220
160
|
metrics.totalEvents++;
|
|
221
161
|
}
|
|
@@ -227,14 +167,11 @@ function detectRageTap() {
|
|
|
227
167
|
const threshold = config.rageTapThreshold || 3;
|
|
228
168
|
const radius = config.rageTapRadius || 50;
|
|
229
169
|
if (tapCount < threshold) return;
|
|
230
|
-
// Check last N taps from circular buffer
|
|
231
170
|
const tapsToCheck = [];
|
|
232
171
|
for (let i = 0; i < threshold; i++) {
|
|
233
172
|
const idx = (tapHead + tapCount - threshold + i) % MAX_RECENT_TAPS;
|
|
234
173
|
tapsToCheck.push(recentTaps[idx]);
|
|
235
174
|
}
|
|
236
|
-
|
|
237
|
-
// Calculate center point
|
|
238
175
|
let centerX = 0;
|
|
239
176
|
let centerY = 0;
|
|
240
177
|
for (const tap of tapsToCheck) {
|
|
@@ -243,8 +180,6 @@ function detectRageTap() {
|
|
|
243
180
|
}
|
|
244
181
|
centerX /= tapsToCheck.length;
|
|
245
182
|
centerY /= tapsToCheck.length;
|
|
246
|
-
|
|
247
|
-
// Check if all taps are within radius of center
|
|
248
183
|
let allWithinRadius = true;
|
|
249
184
|
for (const tap of tapsToCheck) {
|
|
250
185
|
const dx = tap.x - centerX;
|
|
@@ -256,9 +191,7 @@ function detectRageTap() {
|
|
|
256
191
|
}
|
|
257
192
|
}
|
|
258
193
|
if (allWithinRadius) {
|
|
259
|
-
// Rage tap detected!
|
|
260
194
|
metrics.rageTapCount++;
|
|
261
|
-
// Clear circular buffer to prevent duplicate detection
|
|
262
195
|
tapHead = 0;
|
|
263
196
|
tapCount = 0;
|
|
264
197
|
|
|
@@ -269,10 +202,6 @@ function detectRageTap() {
|
|
|
269
202
|
}
|
|
270
203
|
}
|
|
271
204
|
|
|
272
|
-
// =============================================================================
|
|
273
|
-
// State Change Notification
|
|
274
|
-
// =============================================================================
|
|
275
|
-
|
|
276
205
|
/**
|
|
277
206
|
* Notify that a state change occurred (navigation, modal, etc.)
|
|
278
207
|
* Kept for API compatibility
|
|
@@ -281,25 +210,16 @@ export function notifyStateChange() {
|
|
|
281
210
|
// No-op - kept for backward compatibility
|
|
282
211
|
}
|
|
283
212
|
|
|
284
|
-
// =============================================================================
|
|
285
|
-
// Error Tracking
|
|
286
|
-
// =============================================================================
|
|
287
|
-
|
|
288
213
|
/**
|
|
289
214
|
* Setup automatic error tracking
|
|
290
215
|
*/
|
|
291
216
|
function setupErrorTracking() {
|
|
292
|
-
// Track React Native errors
|
|
293
217
|
if (config.trackReactNativeErrors !== false) {
|
|
294
218
|
setupReactNativeErrorHandler();
|
|
295
219
|
}
|
|
296
|
-
|
|
297
|
-
// Track JavaScript errors (only works in web/debug)
|
|
298
220
|
if (config.trackJSErrors !== false && typeof _globalThis !== 'undefined') {
|
|
299
221
|
setupJSErrorHandler();
|
|
300
222
|
}
|
|
301
|
-
|
|
302
|
-
// Track unhandled promise rejections
|
|
303
223
|
if (config.trackPromiseRejections !== false && typeof _globalThis !== 'undefined') {
|
|
304
224
|
setupPromiseRejectionHandler();
|
|
305
225
|
}
|
|
@@ -310,16 +230,10 @@ function setupErrorTracking() {
|
|
|
310
230
|
*/
|
|
311
231
|
function setupReactNativeErrorHandler() {
|
|
312
232
|
try {
|
|
313
|
-
// Access ErrorUtils from global scope
|
|
314
233
|
const ErrorUtils = _globalThis.ErrorUtils;
|
|
315
234
|
if (!ErrorUtils) return;
|
|
316
|
-
|
|
317
|
-
// Store original handler
|
|
318
235
|
originalErrorHandler = ErrorUtils.getGlobalHandler();
|
|
319
|
-
|
|
320
|
-
// Set new handler
|
|
321
236
|
ErrorUtils.setGlobalHandler((error, isFatal) => {
|
|
322
|
-
// Track the error
|
|
323
237
|
trackError({
|
|
324
238
|
type: 'error',
|
|
325
239
|
timestamp: Date.now(),
|
|
@@ -327,14 +241,12 @@ function setupReactNativeErrorHandler() {
|
|
|
327
241
|
stack: error.stack,
|
|
328
242
|
name: error.name || 'Error'
|
|
329
243
|
});
|
|
330
|
-
|
|
331
|
-
// Call original handler
|
|
332
244
|
if (originalErrorHandler) {
|
|
333
245
|
originalErrorHandler(error, isFatal);
|
|
334
246
|
}
|
|
335
247
|
});
|
|
336
248
|
} catch {
|
|
337
|
-
//
|
|
249
|
+
// Ignore
|
|
338
250
|
}
|
|
339
251
|
}
|
|
340
252
|
|
|
@@ -343,8 +255,6 @@ function setupReactNativeErrorHandler() {
|
|
|
343
255
|
*/
|
|
344
256
|
function setupJSErrorHandler() {
|
|
345
257
|
if (typeof _globalThis.onerror !== 'undefined') {
|
|
346
|
-
// Note: In React Native, this typically doesn't fire
|
|
347
|
-
// But we set it up anyway for hybrid apps
|
|
348
258
|
originalOnError = _globalThis.onerror;
|
|
349
259
|
_globalThis.onerror = (message, source, lineno, colno, error) => {
|
|
350
260
|
trackError({
|
|
@@ -354,8 +264,6 @@ function setupJSErrorHandler() {
|
|
|
354
264
|
stack: error?.stack || `${source}:${lineno}:${colno}`,
|
|
355
265
|
name: error?.name || 'Error'
|
|
356
266
|
});
|
|
357
|
-
|
|
358
|
-
// Call original handler
|
|
359
267
|
if (originalOnError) {
|
|
360
268
|
return originalOnError(message, source, lineno, colno, error);
|
|
361
269
|
}
|
|
@@ -388,7 +296,6 @@ function setupPromiseRejectionHandler() {
|
|
|
388
296
|
* Restore original error handlers
|
|
389
297
|
*/
|
|
390
298
|
function restoreErrorHandlers() {
|
|
391
|
-
// Restore React Native handler
|
|
392
299
|
if (originalErrorHandler) {
|
|
393
300
|
try {
|
|
394
301
|
const ErrorUtils = _globalThis.ErrorUtils;
|
|
@@ -400,14 +307,10 @@ function restoreErrorHandlers() {
|
|
|
400
307
|
}
|
|
401
308
|
originalErrorHandler = undefined;
|
|
402
309
|
}
|
|
403
|
-
|
|
404
|
-
// Restore global onerror
|
|
405
310
|
if (originalOnError !== null) {
|
|
406
311
|
_globalThis.onerror = originalOnError;
|
|
407
312
|
originalOnError = null;
|
|
408
313
|
}
|
|
409
|
-
|
|
410
|
-
// Remove promise rejection handler
|
|
411
314
|
if (originalOnUnhandledRejection && typeof _globalThis.removeEventListener !== 'undefined') {
|
|
412
315
|
_globalThis.removeEventListener('unhandledrejection', originalOnUnhandledRejection);
|
|
413
316
|
originalOnUnhandledRejection = null;
|
|
@@ -437,16 +340,10 @@ export function captureError(message, stack, name) {
|
|
|
437
340
|
name: name || 'Error'
|
|
438
341
|
});
|
|
439
342
|
}
|
|
440
|
-
|
|
441
|
-
// =============================================================================
|
|
442
|
-
// Screen/Funnel Tracking - Automatic Navigation Detection
|
|
443
|
-
// =============================================================================
|
|
444
|
-
|
|
445
|
-
// Navigation detection state
|
|
446
343
|
let navigationPollingInterval = null;
|
|
447
344
|
let lastDetectedScreen = '';
|
|
448
345
|
let navigationSetupDone = false;
|
|
449
|
-
let navigationPollingErrors = 0;
|
|
346
|
+
let navigationPollingErrors = 0;
|
|
450
347
|
const MAX_POLLING_ERRORS = 10; // Stop polling after 10 consecutive errors
|
|
451
348
|
|
|
452
349
|
/**
|
|
@@ -470,8 +367,6 @@ export function trackNavigationState(state) {
|
|
|
470
367
|
const {
|
|
471
368
|
normalizeScreenName
|
|
472
369
|
} = require('./navigation');
|
|
473
|
-
|
|
474
|
-
// Find the active screen recursively
|
|
475
370
|
const findActiveScreen = navState => {
|
|
476
371
|
if (!navState?.routes) return null;
|
|
477
372
|
const index = navState.index ?? navState.routes.length - 1;
|
|
@@ -486,7 +381,7 @@ export function trackNavigationState(state) {
|
|
|
486
381
|
trackScreen(screenName);
|
|
487
382
|
}
|
|
488
383
|
} catch {
|
|
489
|
-
//
|
|
384
|
+
// Ignore
|
|
490
385
|
}
|
|
491
386
|
}
|
|
492
387
|
|
|
@@ -516,16 +411,11 @@ export function trackNavigationState(state) {
|
|
|
516
411
|
* ```
|
|
517
412
|
*/
|
|
518
413
|
export function useNavigationTracking() {
|
|
519
|
-
// Use React's useRef and useCallback to create stable references
|
|
520
414
|
const React = require('react');
|
|
521
415
|
const {
|
|
522
416
|
createNavigationContainerRef
|
|
523
417
|
} = require('@react-navigation/native');
|
|
524
|
-
|
|
525
|
-
// Create a stable navigation ref
|
|
526
418
|
const navigationRef = React.useRef(createNavigationContainerRef());
|
|
527
|
-
|
|
528
|
-
// Track initial screen when navigation is ready
|
|
529
419
|
const onReady = React.useCallback(() => {
|
|
530
420
|
try {
|
|
531
421
|
const currentRoute = navigationRef.current?.getCurrentRoute?.();
|
|
@@ -540,11 +430,9 @@ export function useNavigationTracking() {
|
|
|
540
430
|
}
|
|
541
431
|
}
|
|
542
432
|
} catch {
|
|
543
|
-
//
|
|
433
|
+
// Ignore
|
|
544
434
|
}
|
|
545
435
|
}, []);
|
|
546
|
-
|
|
547
|
-
// Return props to spread on NavigationContainer
|
|
548
436
|
return {
|
|
549
437
|
ref: navigationRef.current,
|
|
550
438
|
onReady,
|
|
@@ -564,9 +452,6 @@ function setupNavigationTracking() {
|
|
|
564
452
|
if (__DEV__) {
|
|
565
453
|
logger.debug('Setting up navigation tracking...');
|
|
566
454
|
}
|
|
567
|
-
|
|
568
|
-
// Delay to ensure navigation is initialized - Expo Router needs more time
|
|
569
|
-
// We retry a few times with increasing delays
|
|
570
455
|
let attempts = 0;
|
|
571
456
|
const maxAttempts = 5;
|
|
572
457
|
const trySetup = () => {
|
|
@@ -580,8 +465,7 @@ function setupNavigationTracking() {
|
|
|
580
465
|
logger.debug('Expo Router setup: SUCCESS on attempt', attempts);
|
|
581
466
|
}
|
|
582
467
|
} else if (attempts < maxAttempts) {
|
|
583
|
-
|
|
584
|
-
const delay = 200 * attempts; // 200, 400, 600, 800ms
|
|
468
|
+
const delay = 200 * attempts;
|
|
585
469
|
if (__DEV__) {
|
|
586
470
|
logger.debug('Expo Router not ready, retrying in', delay, 'ms');
|
|
587
471
|
}
|
|
@@ -593,8 +477,6 @@ function setupNavigationTracking() {
|
|
|
593
477
|
}
|
|
594
478
|
}
|
|
595
479
|
};
|
|
596
|
-
|
|
597
|
-
// Start first attempt after 200ms
|
|
598
480
|
setTimeout(trySetup, 200);
|
|
599
481
|
}
|
|
600
482
|
|
|
@@ -620,14 +502,10 @@ function trySetupExpoRouter() {
|
|
|
620
502
|
normalizeScreenName,
|
|
621
503
|
getScreenNameFromPath
|
|
622
504
|
} = require('./navigation');
|
|
623
|
-
|
|
624
|
-
// Poll for route changes (expo-router doesn't expose a listener API outside hooks)
|
|
625
505
|
navigationPollingInterval = setInterval(() => {
|
|
626
506
|
try {
|
|
627
507
|
let state = null;
|
|
628
508
|
let stateSource = '';
|
|
629
|
-
|
|
630
|
-
// Method 1: Public accessors on router object
|
|
631
509
|
if (typeof router.getState === 'function') {
|
|
632
510
|
state = router.getState();
|
|
633
511
|
stateSource = 'router.getState()';
|
|
@@ -635,34 +513,25 @@ function trySetupExpoRouter() {
|
|
|
635
513
|
state = router.rootState;
|
|
636
514
|
stateSource = 'router.rootState';
|
|
637
515
|
}
|
|
638
|
-
|
|
639
|
-
// Method 2: Internal store access (works for both v3 and v6+)
|
|
640
516
|
if (!state) {
|
|
641
517
|
try {
|
|
642
518
|
const storeModule = require('expo-router/build/global-state/router-store');
|
|
643
519
|
if (storeModule?.store) {
|
|
644
|
-
// v6+: store.state or store.navigationRef
|
|
645
520
|
state = storeModule.store.state;
|
|
646
521
|
if (state) stateSource = 'store.state';
|
|
647
|
-
|
|
648
|
-
// v6+: Try navigationRef if state is undefined
|
|
649
522
|
if (!state && storeModule.store.navigationRef?.current) {
|
|
650
523
|
state = storeModule.store.navigationRef.current.getRootState?.();
|
|
651
524
|
if (state) stateSource = 'navigationRef.getRootState()';
|
|
652
525
|
}
|
|
653
|
-
|
|
654
|
-
// v3: store.rootState or store.initialState
|
|
655
526
|
if (!state) {
|
|
656
527
|
state = storeModule.store.rootState || storeModule.store.initialState;
|
|
657
528
|
if (state) stateSource = 'store.rootState/initialState';
|
|
658
529
|
}
|
|
659
530
|
}
|
|
660
531
|
} catch {
|
|
661
|
-
//
|
|
532
|
+
// Ignore
|
|
662
533
|
}
|
|
663
534
|
}
|
|
664
|
-
|
|
665
|
-
// Method 3: Try accessing via a different export path for v6
|
|
666
535
|
if (!state) {
|
|
667
536
|
try {
|
|
668
537
|
const imperative = require('expo-router/build/imperative-api');
|
|
@@ -671,11 +540,11 @@ function trySetupExpoRouter() {
|
|
|
671
540
|
if (state) stateSource = 'imperative-api';
|
|
672
541
|
}
|
|
673
542
|
} catch {
|
|
674
|
-
//
|
|
543
|
+
// Ignore
|
|
675
544
|
}
|
|
676
545
|
}
|
|
677
546
|
if (state) {
|
|
678
|
-
|
|
547
|
+
navigationPollingErrors = 0;
|
|
679
548
|
navigationPollingErrors = 0;
|
|
680
549
|
const screenName = extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName);
|
|
681
550
|
if (screenName && screenName !== lastDetectedScreen) {
|
|
@@ -686,21 +555,15 @@ function trySetupExpoRouter() {
|
|
|
686
555
|
trackScreen(screenName);
|
|
687
556
|
}
|
|
688
557
|
} else {
|
|
689
|
-
// Track consecutive failures to get state
|
|
690
558
|
navigationPollingErrors++;
|
|
691
559
|
if (__DEV__ && navigationPollingErrors === 1) {
|
|
692
560
|
logger.debug('Expo Router: Could not get navigation state');
|
|
693
561
|
}
|
|
694
562
|
if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
|
|
695
|
-
// Stop polling after too many errors to save CPU
|
|
696
|
-
if (__DEV__) {
|
|
697
|
-
logger.debug('Expo Router: Stopped polling after', MAX_POLLING_ERRORS, 'errors');
|
|
698
|
-
}
|
|
699
563
|
cleanupNavigationTracking();
|
|
700
564
|
}
|
|
701
565
|
}
|
|
702
566
|
} catch (e) {
|
|
703
|
-
// Error - track and potentially stop
|
|
704
567
|
navigationPollingErrors++;
|
|
705
568
|
if (__DEV__ && navigationPollingErrors === 1) {
|
|
706
569
|
logger.debug('Expo Router polling error:', e);
|
|
@@ -709,14 +572,12 @@ function trySetupExpoRouter() {
|
|
|
709
572
|
cleanupNavigationTracking();
|
|
710
573
|
}
|
|
711
574
|
}
|
|
712
|
-
}, 500);
|
|
713
|
-
|
|
575
|
+
}, 500);
|
|
714
576
|
return true;
|
|
715
577
|
} catch (e) {
|
|
716
578
|
if (__DEV__) {
|
|
717
579
|
logger.debug('Expo Router not available:', e);
|
|
718
580
|
}
|
|
719
|
-
// expo-router not installed
|
|
720
581
|
return false;
|
|
721
582
|
}
|
|
722
583
|
}
|
|
@@ -731,22 +592,12 @@ function extractScreenNameFromRouterState(state, getScreenNameFromPath, normaliz
|
|
|
731
592
|
if (!state?.routes) return null;
|
|
732
593
|
const route = state.routes[state.index ?? state.routes.length - 1];
|
|
733
594
|
if (!route) return null;
|
|
734
|
-
|
|
735
|
-
// Add current route name to accumulated segments
|
|
736
595
|
const newSegments = [...accumulatedSegments, route.name];
|
|
737
|
-
|
|
738
|
-
// If this route has nested state, recurse deeper
|
|
739
596
|
if (route.state) {
|
|
740
597
|
return extractScreenNameFromRouterState(route.state, getScreenNameFromPath, normalizeScreenName, newSegments);
|
|
741
598
|
}
|
|
742
|
-
|
|
743
|
-
// We've reached the deepest level - build the screen name
|
|
744
|
-
// Filter out group markers like (tabs), (main), (auth)
|
|
745
599
|
const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
|
|
746
|
-
|
|
747
|
-
// If after filtering we have no segments, use the last meaningful name
|
|
748
600
|
if (cleanSegments.length === 0) {
|
|
749
|
-
// Find the last non-group segment
|
|
750
601
|
for (let i = newSegments.length - 1; i >= 0; i--) {
|
|
751
602
|
const seg = newSegments[i];
|
|
752
603
|
if (seg && !seg.startsWith('(') && !seg.endsWith(')')) {
|
|
@@ -785,27 +636,17 @@ export function trackScreen(screenName) {
|
|
|
785
636
|
}
|
|
786
637
|
const previousScreen = currentScreen;
|
|
787
638
|
currentScreen = screenName;
|
|
788
|
-
// Add to screens visited (only track for unique set, avoid large array copies)
|
|
789
639
|
screensVisited.push(screenName);
|
|
790
|
-
|
|
791
|
-
// Update unique screens count
|
|
792
640
|
const uniqueScreens = new Set(screensVisited);
|
|
793
641
|
metrics.uniqueScreensCount = uniqueScreens.size;
|
|
794
|
-
|
|
795
|
-
// Update navigation count
|
|
796
642
|
metrics.navigationCount++;
|
|
797
643
|
metrics.totalEvents++;
|
|
798
644
|
if (__DEV__) {
|
|
799
645
|
logger.debug('trackScreen:', screenName, '(total screens:', metrics.uniqueScreensCount, ')');
|
|
800
646
|
}
|
|
801
|
-
|
|
802
|
-
// Notify callback
|
|
803
647
|
if (onScreenChange) {
|
|
804
648
|
onScreenChange(screenName, previousScreen);
|
|
805
649
|
}
|
|
806
|
-
|
|
807
|
-
// IMPORTANT: Also notify native module to send to backend
|
|
808
|
-
// This is the key fix - without this, screens don't get recorded!
|
|
809
650
|
try {
|
|
810
651
|
const RejourneyNative = getRejourneyNativeModule();
|
|
811
652
|
if (RejourneyNative?.screenChanged) {
|
|
@@ -827,18 +668,12 @@ export function trackScreen(screenName) {
|
|
|
827
668
|
}
|
|
828
669
|
}
|
|
829
670
|
|
|
830
|
-
// =============================================================================
|
|
831
|
-
// API Metrics Tracking
|
|
832
|
-
// =============================================================================
|
|
833
|
-
|
|
834
671
|
/**
|
|
835
672
|
* Track an API request with timing data
|
|
836
673
|
*/
|
|
837
674
|
export function trackAPIRequest(success, _statusCode, durationMs = 0, responseBytes = 0) {
|
|
838
675
|
if (!isInitialized) return;
|
|
839
676
|
metrics.apiTotalCount++;
|
|
840
|
-
|
|
841
|
-
// Accumulate timing and size for avg calculation
|
|
842
677
|
if (durationMs > 0) {
|
|
843
678
|
metrics.netTotalDurationMs += durationMs;
|
|
844
679
|
}
|
|
@@ -849,16 +684,10 @@ export function trackAPIRequest(success, _statusCode, durationMs = 0, responseBy
|
|
|
849
684
|
metrics.apiSuccessCount++;
|
|
850
685
|
} else {
|
|
851
686
|
metrics.apiErrorCount++;
|
|
852
|
-
|
|
853
|
-
// API errors also count toward error count for UX score
|
|
854
687
|
metrics.errorCount++;
|
|
855
688
|
}
|
|
856
689
|
}
|
|
857
690
|
|
|
858
|
-
// =============================================================================
|
|
859
|
-
// Session Metrics
|
|
860
|
-
// =============================================================================
|
|
861
|
-
|
|
862
691
|
/**
|
|
863
692
|
* Create empty metrics object
|
|
864
693
|
*/
|
|
@@ -916,18 +745,11 @@ export function trackInput() {
|
|
|
916
745
|
* Get current session metrics
|
|
917
746
|
*/
|
|
918
747
|
export function getSessionMetrics() {
|
|
919
|
-
// Calculate scores before returning
|
|
920
748
|
calculateScores();
|
|
921
|
-
|
|
922
|
-
// Compute average API response time
|
|
923
749
|
const netAvgDurationMs = metrics.apiTotalCount > 0 ? Math.round(metrics.netTotalDurationMs / metrics.apiTotalCount) : 0;
|
|
924
|
-
|
|
925
|
-
// Lazily populate screensVisited only when metrics are retrieved
|
|
926
|
-
// This avoids expensive array copies on every screen change
|
|
927
750
|
return {
|
|
928
751
|
...metrics,
|
|
929
752
|
screensVisited: [...screensVisited],
|
|
930
|
-
// Only copy here when needed
|
|
931
753
|
netAvgDurationMs
|
|
932
754
|
};
|
|
933
755
|
}
|
|
@@ -936,34 +758,15 @@ export function getSessionMetrics() {
|
|
|
936
758
|
* Calculate session scores
|
|
937
759
|
*/
|
|
938
760
|
function calculateScores() {
|
|
939
|
-
// Interaction Score (0-100)
|
|
940
|
-
// Based on total interactions normalized to a baseline
|
|
941
761
|
const totalInteractions = metrics.touchCount + metrics.scrollCount + metrics.gestureCount + metrics.inputCount;
|
|
942
|
-
|
|
943
|
-
// Assume 50 interactions is "average" for a session
|
|
944
762
|
const avgInteractions = 50;
|
|
945
763
|
metrics.interactionScore = Math.min(100, Math.round(totalInteractions / avgInteractions * 100));
|
|
946
|
-
|
|
947
|
-
// Exploration Score (0-100)
|
|
948
|
-
// Based on unique screens visited
|
|
949
|
-
// Assume 5 screens is "average" exploration
|
|
950
764
|
const avgScreens = 5;
|
|
951
765
|
metrics.explorationScore = Math.min(100, Math.round(metrics.uniqueScreensCount / avgScreens * 100));
|
|
952
|
-
|
|
953
|
-
// UX Score (0-100)
|
|
954
|
-
// Starts at 100, deducts for issues
|
|
955
766
|
let uxScore = 100;
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
uxScore -= Math.min(
|
|
959
|
-
|
|
960
|
-
// Deduct for rage taps
|
|
961
|
-
uxScore -= Math.min(24, metrics.rageTapCount * 8); // Max 24 point deduction
|
|
962
|
-
|
|
963
|
-
// Deduct for API errors
|
|
964
|
-
uxScore -= Math.min(20, metrics.apiErrorCount * 10); // Max 20 point deduction
|
|
965
|
-
|
|
966
|
-
// Bonus for completing funnel (if screens > 3)
|
|
767
|
+
uxScore -= Math.min(30, metrics.errorCount * 15);
|
|
768
|
+
uxScore -= Math.min(24, metrics.rageTapCount * 8);
|
|
769
|
+
uxScore -= Math.min(20, metrics.apiErrorCount * 10);
|
|
967
770
|
if (metrics.uniqueScreensCount >= 3) {
|
|
968
771
|
uxScore += 5;
|
|
969
772
|
}
|
|
@@ -981,43 +784,29 @@ export function resetMetrics() {
|
|
|
981
784
|
tapCount = 0;
|
|
982
785
|
sessionStartTime = Date.now();
|
|
983
786
|
}
|
|
984
|
-
|
|
985
|
-
// =============================================================================
|
|
986
|
-
// Session duration helpers
|
|
987
|
-
// =============================================================================
|
|
988
|
-
|
|
989
|
-
/** Clamp and set max session duration in minutes (1–10). Defaults to 10. */
|
|
990
787
|
export function setMaxSessionDurationMinutes(minutes) {
|
|
991
788
|
const clampedMinutes = Math.min(10, Math.max(1, minutes ?? 10));
|
|
992
789
|
maxSessionDurationMs = clampedMinutes * 60 * 1000;
|
|
993
790
|
}
|
|
994
|
-
|
|
995
|
-
/** Returns true if the current session exceeded the configured max duration. */
|
|
996
791
|
export function hasExceededMaxSessionDuration() {
|
|
997
792
|
if (!sessionStartTime) return false;
|
|
998
793
|
return Date.now() - sessionStartTime >= maxSessionDurationMs;
|
|
999
794
|
}
|
|
1000
|
-
|
|
1001
|
-
/** Returns remaining milliseconds until the session should stop. */
|
|
1002
795
|
export function getRemainingSessionDurationMs() {
|
|
1003
796
|
if (!sessionStartTime) return maxSessionDurationMs;
|
|
1004
797
|
const remaining = maxSessionDurationMs - (Date.now() - sessionStartTime);
|
|
1005
798
|
return Math.max(0, remaining);
|
|
1006
799
|
}
|
|
1007
800
|
|
|
1008
|
-
// =============================================================================
|
|
1009
|
-
// Device Info Collection
|
|
1010
|
-
// =============================================================================
|
|
1011
|
-
|
|
1012
801
|
/**
|
|
1013
802
|
* Collect device information
|
|
1014
803
|
*/
|
|
1015
|
-
|
|
804
|
+
/**
|
|
805
|
+
* Collect device information
|
|
806
|
+
*/
|
|
807
|
+
export async function collectDeviceInfo() {
|
|
1016
808
|
const Dimensions = getDimensions();
|
|
1017
809
|
const Platform = getPlatform();
|
|
1018
|
-
const NativeModules = getNativeModules();
|
|
1019
|
-
|
|
1020
|
-
// Default values if react-native isn't available
|
|
1021
810
|
let width = 0,
|
|
1022
811
|
height = 0,
|
|
1023
812
|
scale = 1;
|
|
@@ -1029,100 +818,43 @@ export function collectDeviceInfo() {
|
|
|
1029
818
|
scale = screenDims?.scale || 1;
|
|
1030
819
|
}
|
|
1031
820
|
|
|
1032
|
-
//
|
|
1033
|
-
let model = 'Unknown';
|
|
1034
|
-
let manufacturer;
|
|
1035
|
-
let osVersion = 'Unknown';
|
|
1036
|
-
let appVersion;
|
|
1037
|
-
let appId;
|
|
821
|
+
// Basic JS-side info
|
|
1038
822
|
let locale;
|
|
1039
823
|
let timezone;
|
|
1040
824
|
try {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const DeviceInfo = require('react-native-device-info');
|
|
1044
|
-
model = DeviceInfo.getModel?.() || model;
|
|
1045
|
-
manufacturer = DeviceInfo.getBrand?.() || undefined;
|
|
1046
|
-
osVersion = DeviceInfo.getSystemVersion?.() || osVersion;
|
|
1047
|
-
appVersion = DeviceInfo.getVersion?.() || undefined;
|
|
1048
|
-
appId = DeviceInfo.getBundleId?.() || undefined;
|
|
1049
|
-
locale = DeviceInfo.getDeviceLocale?.() || undefined;
|
|
1050
|
-
timezone = DeviceInfo.getTimezone?.() || undefined;
|
|
825
|
+
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
826
|
+
locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
1051
827
|
} catch {
|
|
1052
|
-
//
|
|
1053
|
-
try {
|
|
1054
|
-
// Try expo-application for app version/id
|
|
1055
|
-
const Application = require('expo-application');
|
|
1056
|
-
appVersion = Application.nativeApplicationVersion || Application.applicationVersion || undefined;
|
|
1057
|
-
appId = Application.applicationId || undefined;
|
|
1058
|
-
} catch {
|
|
1059
|
-
// expo-application not available
|
|
1060
|
-
}
|
|
1061
|
-
try {
|
|
1062
|
-
// Try expo-constants for additional info
|
|
1063
|
-
const Constants = require('expo-constants');
|
|
1064
|
-
const expoConfig = Constants.expoConfig || Constants.manifest2?.extra?.expoClient || Constants.manifest;
|
|
1065
|
-
if (!appVersion && expoConfig?.version) {
|
|
1066
|
-
appVersion = expoConfig.version;
|
|
1067
|
-
}
|
|
1068
|
-
if (!appId && (expoConfig?.ios?.bundleIdentifier || expoConfig?.android?.package)) {
|
|
1069
|
-
appId = Platform?.OS === 'ios' ? expoConfig?.ios?.bundleIdentifier : expoConfig?.android?.package;
|
|
1070
|
-
}
|
|
1071
|
-
} catch {
|
|
1072
|
-
// expo-constants not available
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
// Fall back to basic platform info
|
|
1076
|
-
if (Platform?.OS === 'ios') {
|
|
1077
|
-
// Get basic info from constants
|
|
1078
|
-
const PlatformConstants = NativeModules?.PlatformConstants;
|
|
1079
|
-
osVersion = Platform.Version?.toString() || osVersion;
|
|
1080
|
-
model = PlatformConstants?.interfaceIdiom === 'pad' ? 'iPad' : 'iPhone';
|
|
1081
|
-
} else if (Platform?.OS === 'android') {
|
|
1082
|
-
osVersion = Platform.Version?.toString() || osVersion;
|
|
1083
|
-
model = 'Android Device';
|
|
1084
|
-
}
|
|
828
|
+
// Ignore
|
|
1085
829
|
}
|
|
1086
830
|
|
|
1087
|
-
// Get
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
} catch {
|
|
1092
|
-
timezone = undefined;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
// Get locale
|
|
1097
|
-
if (!locale) {
|
|
831
|
+
// Get native info
|
|
832
|
+
const nativeModule = getRejourneyNativeModule();
|
|
833
|
+
let nativeInfo = {};
|
|
834
|
+
if (nativeModule && nativeModule.getDeviceInfo) {
|
|
1098
835
|
try {
|
|
1099
|
-
|
|
1100
|
-
} catch {
|
|
1101
|
-
|
|
836
|
+
nativeInfo = await nativeModule.getDeviceInfo();
|
|
837
|
+
} catch (e) {
|
|
838
|
+
if (__DEV__) {
|
|
839
|
+
console.warn('[Rejourney] Failed to get native device info:', e);
|
|
840
|
+
}
|
|
1102
841
|
}
|
|
1103
842
|
}
|
|
1104
843
|
return {
|
|
1105
|
-
model,
|
|
1106
|
-
manufacturer,
|
|
844
|
+
model: nativeInfo.model || 'Unknown',
|
|
845
|
+
manufacturer: nativeInfo.brand,
|
|
1107
846
|
os: Platform?.OS || 'ios',
|
|
1108
|
-
osVersion,
|
|
847
|
+
osVersion: nativeInfo.systemVersion || Platform?.Version?.toString() || 'Unknown',
|
|
1109
848
|
screenWidth: Math.round(width),
|
|
1110
849
|
screenHeight: Math.round(height),
|
|
1111
850
|
pixelRatio: scale,
|
|
1112
|
-
appVersion,
|
|
1113
|
-
appId,
|
|
1114
|
-
locale,
|
|
1115
|
-
timezone
|
|
851
|
+
appVersion: nativeInfo.appVersion,
|
|
852
|
+
appId: nativeInfo.bundleId,
|
|
853
|
+
locale: locale,
|
|
854
|
+
timezone: timezone
|
|
1116
855
|
};
|
|
1117
856
|
}
|
|
1118
857
|
|
|
1119
|
-
// =============================================================================
|
|
1120
|
-
// Anonymous ID Generation
|
|
1121
|
-
// =============================================================================
|
|
1122
|
-
|
|
1123
|
-
// Storage key for anonymous ID
|
|
1124
|
-
// const ANONYMOUS_ID_KEY = '@rejourney_anonymous_id';
|
|
1125
|
-
|
|
1126
858
|
/**
|
|
1127
859
|
* Generate a persistent anonymous ID
|
|
1128
860
|
*/
|
|
@@ -1150,7 +882,6 @@ export async function ensurePersistentAnonymousId() {
|
|
|
1150
882
|
if (anonymousId) return anonymousId;
|
|
1151
883
|
if (!anonymousIdPromise) {
|
|
1152
884
|
anonymousIdPromise = (async () => {
|
|
1153
|
-
// Just use the load logic which now delegates to native or memory
|
|
1154
885
|
const id = await loadAnonymousId();
|
|
1155
886
|
anonymousId = id;
|
|
1156
887
|
return id;
|
|
@@ -1180,13 +911,7 @@ export async function loadAnonymousId() {
|
|
|
1180
911
|
*/
|
|
1181
912
|
export function setAnonymousId(id) {
|
|
1182
913
|
anonymousId = id;
|
|
1183
|
-
// No-op for async persistence as we moved to native-only or memory-only
|
|
1184
914
|
}
|
|
1185
|
-
|
|
1186
|
-
// =============================================================================
|
|
1187
|
-
// Exports
|
|
1188
|
-
// =============================================================================
|
|
1189
|
-
|
|
1190
915
|
export default {
|
|
1191
916
|
init: initAutoTracking,
|
|
1192
917
|
cleanup: cleanupAutoTracking,
|