@rejourneyco/react-native 1.0.1 → 1.0.2
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 +35 -29
- 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/Core/Rejourney.mm +32 -95
- package/ios/Utils/RJPerfTiming.m +0 -5
- package/ios/Utils/RJWindowUtils.m +0 -1
- package/lib/commonjs/components/Mask.js +1 -6
- package/lib/commonjs/index.js +4 -87
- package/lib/commonjs/sdk/autoTracking.js +39 -310
- 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 -49
- package/lib/commonjs/sdk/utils.js +0 -5
- package/lib/module/components/Mask.js +1 -6
- package/lib/module/index.js +3 -91
- package/lib/module/sdk/autoTracking.js +39 -311
- 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 -49
- package/lib/module/sdk/utils.js +0 -5
- package/lib/typescript/NativeRejourney.d.ts +1 -0
- package/lib/typescript/sdk/autoTracking.d.ts +4 -4
- package/lib/typescript/types/index.d.ts +0 -1
- package/package.json +2 -8
- package/src/NativeRejourney.ts +2 -0
- package/src/components/Mask.tsx +0 -3
- package/src/index.ts +3 -73
- package/src/sdk/autoTracking.ts +51 -282
- 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 -33
- package/src/sdk/utils.ts +0 -5
- 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,
|
|
@@ -580,8 +468,7 @@ function setupNavigationTracking() {
|
|
|
580
468
|
logger.debug('Expo Router setup: SUCCESS on attempt', attempts);
|
|
581
469
|
}
|
|
582
470
|
} else if (attempts < maxAttempts) {
|
|
583
|
-
|
|
584
|
-
const delay = 200 * attempts; // 200, 400, 600, 800ms
|
|
471
|
+
const delay = 200 * attempts;
|
|
585
472
|
if (__DEV__) {
|
|
586
473
|
logger.debug('Expo Router not ready, retrying in', delay, 'ms');
|
|
587
474
|
}
|
|
@@ -593,8 +480,6 @@ function setupNavigationTracking() {
|
|
|
593
480
|
}
|
|
594
481
|
}
|
|
595
482
|
};
|
|
596
|
-
|
|
597
|
-
// Start first attempt after 200ms
|
|
598
483
|
setTimeout(trySetup, 200);
|
|
599
484
|
}
|
|
600
485
|
|
|
@@ -620,14 +505,10 @@ function trySetupExpoRouter() {
|
|
|
620
505
|
normalizeScreenName,
|
|
621
506
|
getScreenNameFromPath
|
|
622
507
|
} = require('./navigation');
|
|
623
|
-
|
|
624
|
-
// Poll for route changes (expo-router doesn't expose a listener API outside hooks)
|
|
625
508
|
navigationPollingInterval = setInterval(() => {
|
|
626
509
|
try {
|
|
627
510
|
let state = null;
|
|
628
511
|
let stateSource = '';
|
|
629
|
-
|
|
630
|
-
// Method 1: Public accessors on router object
|
|
631
512
|
if (typeof router.getState === 'function') {
|
|
632
513
|
state = router.getState();
|
|
633
514
|
stateSource = 'router.getState()';
|
|
@@ -635,34 +516,25 @@ function trySetupExpoRouter() {
|
|
|
635
516
|
state = router.rootState;
|
|
636
517
|
stateSource = 'router.rootState';
|
|
637
518
|
}
|
|
638
|
-
|
|
639
|
-
// Method 2: Internal store access (works for both v3 and v6+)
|
|
640
519
|
if (!state) {
|
|
641
520
|
try {
|
|
642
521
|
const storeModule = require('expo-router/build/global-state/router-store');
|
|
643
522
|
if (storeModule?.store) {
|
|
644
|
-
// v6+: store.state or store.navigationRef
|
|
645
523
|
state = storeModule.store.state;
|
|
646
524
|
if (state) stateSource = 'store.state';
|
|
647
|
-
|
|
648
|
-
// v6+: Try navigationRef if state is undefined
|
|
649
525
|
if (!state && storeModule.store.navigationRef?.current) {
|
|
650
526
|
state = storeModule.store.navigationRef.current.getRootState?.();
|
|
651
527
|
if (state) stateSource = 'navigationRef.getRootState()';
|
|
652
528
|
}
|
|
653
|
-
|
|
654
|
-
// v3: store.rootState or store.initialState
|
|
655
529
|
if (!state) {
|
|
656
530
|
state = storeModule.store.rootState || storeModule.store.initialState;
|
|
657
531
|
if (state) stateSource = 'store.rootState/initialState';
|
|
658
532
|
}
|
|
659
533
|
}
|
|
660
534
|
} catch {
|
|
661
|
-
//
|
|
535
|
+
// Ignore
|
|
662
536
|
}
|
|
663
537
|
}
|
|
664
|
-
|
|
665
|
-
// Method 3: Try accessing via a different export path for v6
|
|
666
538
|
if (!state) {
|
|
667
539
|
try {
|
|
668
540
|
const imperative = require('expo-router/build/imperative-api');
|
|
@@ -671,11 +543,11 @@ function trySetupExpoRouter() {
|
|
|
671
543
|
if (state) stateSource = 'imperative-api';
|
|
672
544
|
}
|
|
673
545
|
} catch {
|
|
674
|
-
//
|
|
546
|
+
// Ignore
|
|
675
547
|
}
|
|
676
548
|
}
|
|
677
549
|
if (state) {
|
|
678
|
-
|
|
550
|
+
navigationPollingErrors = 0;
|
|
679
551
|
navigationPollingErrors = 0;
|
|
680
552
|
const screenName = extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName);
|
|
681
553
|
if (screenName && screenName !== lastDetectedScreen) {
|
|
@@ -686,21 +558,15 @@ function trySetupExpoRouter() {
|
|
|
686
558
|
trackScreen(screenName);
|
|
687
559
|
}
|
|
688
560
|
} else {
|
|
689
|
-
// Track consecutive failures to get state
|
|
690
561
|
navigationPollingErrors++;
|
|
691
562
|
if (__DEV__ && navigationPollingErrors === 1) {
|
|
692
563
|
logger.debug('Expo Router: Could not get navigation state');
|
|
693
564
|
}
|
|
694
565
|
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
566
|
cleanupNavigationTracking();
|
|
700
567
|
}
|
|
701
568
|
}
|
|
702
569
|
} catch (e) {
|
|
703
|
-
// Error - track and potentially stop
|
|
704
570
|
navigationPollingErrors++;
|
|
705
571
|
if (__DEV__ && navigationPollingErrors === 1) {
|
|
706
572
|
logger.debug('Expo Router polling error:', e);
|
|
@@ -709,14 +575,12 @@ function trySetupExpoRouter() {
|
|
|
709
575
|
cleanupNavigationTracking();
|
|
710
576
|
}
|
|
711
577
|
}
|
|
712
|
-
}, 500);
|
|
713
|
-
|
|
578
|
+
}, 500);
|
|
714
579
|
return true;
|
|
715
580
|
} catch (e) {
|
|
716
581
|
if (__DEV__) {
|
|
717
582
|
logger.debug('Expo Router not available:', e);
|
|
718
583
|
}
|
|
719
|
-
// expo-router not installed
|
|
720
584
|
return false;
|
|
721
585
|
}
|
|
722
586
|
}
|
|
@@ -731,22 +595,12 @@ function extractScreenNameFromRouterState(state, getScreenNameFromPath, normaliz
|
|
|
731
595
|
if (!state?.routes) return null;
|
|
732
596
|
const route = state.routes[state.index ?? state.routes.length - 1];
|
|
733
597
|
if (!route) return null;
|
|
734
|
-
|
|
735
|
-
// Add current route name to accumulated segments
|
|
736
598
|
const newSegments = [...accumulatedSegments, route.name];
|
|
737
|
-
|
|
738
|
-
// If this route has nested state, recurse deeper
|
|
739
599
|
if (route.state) {
|
|
740
600
|
return extractScreenNameFromRouterState(route.state, getScreenNameFromPath, normalizeScreenName, newSegments);
|
|
741
601
|
}
|
|
742
|
-
|
|
743
|
-
// We've reached the deepest level - build the screen name
|
|
744
|
-
// Filter out group markers like (tabs), (main), (auth)
|
|
745
602
|
const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
|
|
746
|
-
|
|
747
|
-
// If after filtering we have no segments, use the last meaningful name
|
|
748
603
|
if (cleanSegments.length === 0) {
|
|
749
|
-
// Find the last non-group segment
|
|
750
604
|
for (let i = newSegments.length - 1; i >= 0; i--) {
|
|
751
605
|
const seg = newSegments[i];
|
|
752
606
|
if (seg && !seg.startsWith('(') && !seg.endsWith(')')) {
|
|
@@ -785,27 +639,17 @@ export function trackScreen(screenName) {
|
|
|
785
639
|
}
|
|
786
640
|
const previousScreen = currentScreen;
|
|
787
641
|
currentScreen = screenName;
|
|
788
|
-
// Add to screens visited (only track for unique set, avoid large array copies)
|
|
789
642
|
screensVisited.push(screenName);
|
|
790
|
-
|
|
791
|
-
// Update unique screens count
|
|
792
643
|
const uniqueScreens = new Set(screensVisited);
|
|
793
644
|
metrics.uniqueScreensCount = uniqueScreens.size;
|
|
794
|
-
|
|
795
|
-
// Update navigation count
|
|
796
645
|
metrics.navigationCount++;
|
|
797
646
|
metrics.totalEvents++;
|
|
798
647
|
if (__DEV__) {
|
|
799
648
|
logger.debug('trackScreen:', screenName, '(total screens:', metrics.uniqueScreensCount, ')');
|
|
800
649
|
}
|
|
801
|
-
|
|
802
|
-
// Notify callback
|
|
803
650
|
if (onScreenChange) {
|
|
804
651
|
onScreenChange(screenName, previousScreen);
|
|
805
652
|
}
|
|
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
653
|
try {
|
|
810
654
|
const RejourneyNative = getRejourneyNativeModule();
|
|
811
655
|
if (RejourneyNative?.screenChanged) {
|
|
@@ -827,18 +671,12 @@ export function trackScreen(screenName) {
|
|
|
827
671
|
}
|
|
828
672
|
}
|
|
829
673
|
|
|
830
|
-
// =============================================================================
|
|
831
|
-
// API Metrics Tracking
|
|
832
|
-
// =============================================================================
|
|
833
|
-
|
|
834
674
|
/**
|
|
835
675
|
* Track an API request with timing data
|
|
836
676
|
*/
|
|
837
677
|
export function trackAPIRequest(success, _statusCode, durationMs = 0, responseBytes = 0) {
|
|
838
678
|
if (!isInitialized) return;
|
|
839
679
|
metrics.apiTotalCount++;
|
|
840
|
-
|
|
841
|
-
// Accumulate timing and size for avg calculation
|
|
842
680
|
if (durationMs > 0) {
|
|
843
681
|
metrics.netTotalDurationMs += durationMs;
|
|
844
682
|
}
|
|
@@ -849,16 +687,10 @@ export function trackAPIRequest(success, _statusCode, durationMs = 0, responseBy
|
|
|
849
687
|
metrics.apiSuccessCount++;
|
|
850
688
|
} else {
|
|
851
689
|
metrics.apiErrorCount++;
|
|
852
|
-
|
|
853
|
-
// API errors also count toward error count for UX score
|
|
854
690
|
metrics.errorCount++;
|
|
855
691
|
}
|
|
856
692
|
}
|
|
857
693
|
|
|
858
|
-
// =============================================================================
|
|
859
|
-
// Session Metrics
|
|
860
|
-
// =============================================================================
|
|
861
|
-
|
|
862
694
|
/**
|
|
863
695
|
* Create empty metrics object
|
|
864
696
|
*/
|
|
@@ -916,18 +748,11 @@ export function trackInput() {
|
|
|
916
748
|
* Get current session metrics
|
|
917
749
|
*/
|
|
918
750
|
export function getSessionMetrics() {
|
|
919
|
-
// Calculate scores before returning
|
|
920
751
|
calculateScores();
|
|
921
|
-
|
|
922
|
-
// Compute average API response time
|
|
923
752
|
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
753
|
return {
|
|
928
754
|
...metrics,
|
|
929
755
|
screensVisited: [...screensVisited],
|
|
930
|
-
// Only copy here when needed
|
|
931
756
|
netAvgDurationMs
|
|
932
757
|
};
|
|
933
758
|
}
|
|
@@ -936,34 +761,15 @@ export function getSessionMetrics() {
|
|
|
936
761
|
* Calculate session scores
|
|
937
762
|
*/
|
|
938
763
|
function calculateScores() {
|
|
939
|
-
// Interaction Score (0-100)
|
|
940
|
-
// Based on total interactions normalized to a baseline
|
|
941
764
|
const totalInteractions = metrics.touchCount + metrics.scrollCount + metrics.gestureCount + metrics.inputCount;
|
|
942
|
-
|
|
943
|
-
// Assume 50 interactions is "average" for a session
|
|
944
765
|
const avgInteractions = 50;
|
|
945
766
|
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
767
|
const avgScreens = 5;
|
|
951
768
|
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
769
|
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)
|
|
770
|
+
uxScore -= Math.min(30, metrics.errorCount * 15);
|
|
771
|
+
uxScore -= Math.min(24, metrics.rageTapCount * 8);
|
|
772
|
+
uxScore -= Math.min(20, metrics.apiErrorCount * 10);
|
|
967
773
|
if (metrics.uniqueScreensCount >= 3) {
|
|
968
774
|
uxScore += 5;
|
|
969
775
|
}
|
|
@@ -981,43 +787,29 @@ export function resetMetrics() {
|
|
|
981
787
|
tapCount = 0;
|
|
982
788
|
sessionStartTime = Date.now();
|
|
983
789
|
}
|
|
984
|
-
|
|
985
|
-
// =============================================================================
|
|
986
|
-
// Session duration helpers
|
|
987
|
-
// =============================================================================
|
|
988
|
-
|
|
989
|
-
/** Clamp and set max session duration in minutes (1–10). Defaults to 10. */
|
|
990
790
|
export function setMaxSessionDurationMinutes(minutes) {
|
|
991
791
|
const clampedMinutes = Math.min(10, Math.max(1, minutes ?? 10));
|
|
992
792
|
maxSessionDurationMs = clampedMinutes * 60 * 1000;
|
|
993
793
|
}
|
|
994
|
-
|
|
995
|
-
/** Returns true if the current session exceeded the configured max duration. */
|
|
996
794
|
export function hasExceededMaxSessionDuration() {
|
|
997
795
|
if (!sessionStartTime) return false;
|
|
998
796
|
return Date.now() - sessionStartTime >= maxSessionDurationMs;
|
|
999
797
|
}
|
|
1000
|
-
|
|
1001
|
-
/** Returns remaining milliseconds until the session should stop. */
|
|
1002
798
|
export function getRemainingSessionDurationMs() {
|
|
1003
799
|
if (!sessionStartTime) return maxSessionDurationMs;
|
|
1004
800
|
const remaining = maxSessionDurationMs - (Date.now() - sessionStartTime);
|
|
1005
801
|
return Math.max(0, remaining);
|
|
1006
802
|
}
|
|
1007
803
|
|
|
1008
|
-
// =============================================================================
|
|
1009
|
-
// Device Info Collection
|
|
1010
|
-
// =============================================================================
|
|
1011
|
-
|
|
1012
804
|
/**
|
|
1013
805
|
* Collect device information
|
|
1014
806
|
*/
|
|
1015
|
-
|
|
807
|
+
/**
|
|
808
|
+
* Collect device information
|
|
809
|
+
*/
|
|
810
|
+
export async function collectDeviceInfo() {
|
|
1016
811
|
const Dimensions = getDimensions();
|
|
1017
812
|
const Platform = getPlatform();
|
|
1018
|
-
const NativeModules = getNativeModules();
|
|
1019
|
-
|
|
1020
|
-
// Default values if react-native isn't available
|
|
1021
813
|
let width = 0,
|
|
1022
814
|
height = 0,
|
|
1023
815
|
scale = 1;
|
|
@@ -1029,100 +821,43 @@ export function collectDeviceInfo() {
|
|
|
1029
821
|
scale = screenDims?.scale || 1;
|
|
1030
822
|
}
|
|
1031
823
|
|
|
1032
|
-
//
|
|
1033
|
-
let model = 'Unknown';
|
|
1034
|
-
let manufacturer;
|
|
1035
|
-
let osVersion = 'Unknown';
|
|
1036
|
-
let appVersion;
|
|
1037
|
-
let appId;
|
|
824
|
+
// Basic JS-side info
|
|
1038
825
|
let locale;
|
|
1039
826
|
let timezone;
|
|
1040
827
|
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;
|
|
828
|
+
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
829
|
+
locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
1051
830
|
} 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
|
-
}
|
|
831
|
+
// Ignore
|
|
1085
832
|
}
|
|
1086
833
|
|
|
1087
|
-
// Get
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
} catch {
|
|
1092
|
-
timezone = undefined;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
// Get locale
|
|
1097
|
-
if (!locale) {
|
|
834
|
+
// Get native info
|
|
835
|
+
const nativeModule = getRejourneyNativeModule();
|
|
836
|
+
let nativeInfo = {};
|
|
837
|
+
if (nativeModule && nativeModule.getDeviceInfo) {
|
|
1098
838
|
try {
|
|
1099
|
-
|
|
1100
|
-
} catch {
|
|
1101
|
-
|
|
839
|
+
nativeInfo = await nativeModule.getDeviceInfo();
|
|
840
|
+
} catch (e) {
|
|
841
|
+
if (__DEV__) {
|
|
842
|
+
console.warn('[Rejourney] Failed to get native device info:', e);
|
|
843
|
+
}
|
|
1102
844
|
}
|
|
1103
845
|
}
|
|
1104
846
|
return {
|
|
1105
|
-
model,
|
|
1106
|
-
manufacturer,
|
|
847
|
+
model: nativeInfo.model || 'Unknown',
|
|
848
|
+
manufacturer: nativeInfo.brand,
|
|
1107
849
|
os: Platform?.OS || 'ios',
|
|
1108
|
-
osVersion,
|
|
850
|
+
osVersion: nativeInfo.systemVersion || Platform?.Version?.toString() || 'Unknown',
|
|
1109
851
|
screenWidth: Math.round(width),
|
|
1110
852
|
screenHeight: Math.round(height),
|
|
1111
853
|
pixelRatio: scale,
|
|
1112
|
-
appVersion,
|
|
1113
|
-
appId,
|
|
1114
|
-
locale,
|
|
1115
|
-
timezone
|
|
854
|
+
appVersion: nativeInfo.appVersion,
|
|
855
|
+
appId: nativeInfo.bundleId,
|
|
856
|
+
locale: locale,
|
|
857
|
+
timezone: timezone
|
|
1116
858
|
};
|
|
1117
859
|
}
|
|
1118
860
|
|
|
1119
|
-
// =============================================================================
|
|
1120
|
-
// Anonymous ID Generation
|
|
1121
|
-
// =============================================================================
|
|
1122
|
-
|
|
1123
|
-
// Storage key for anonymous ID
|
|
1124
|
-
// const ANONYMOUS_ID_KEY = '@rejourney_anonymous_id';
|
|
1125
|
-
|
|
1126
861
|
/**
|
|
1127
862
|
* Generate a persistent anonymous ID
|
|
1128
863
|
*/
|
|
@@ -1150,7 +885,6 @@ export async function ensurePersistentAnonymousId() {
|
|
|
1150
885
|
if (anonymousId) return anonymousId;
|
|
1151
886
|
if (!anonymousIdPromise) {
|
|
1152
887
|
anonymousIdPromise = (async () => {
|
|
1153
|
-
// Just use the load logic which now delegates to native or memory
|
|
1154
888
|
const id = await loadAnonymousId();
|
|
1155
889
|
anonymousId = id;
|
|
1156
890
|
return id;
|
|
@@ -1180,13 +914,7 @@ export async function loadAnonymousId() {
|
|
|
1180
914
|
*/
|
|
1181
915
|
export function setAnonymousId(id) {
|
|
1182
916
|
anonymousId = id;
|
|
1183
|
-
// No-op for async persistence as we moved to native-only or memory-only
|
|
1184
917
|
}
|
|
1185
|
-
|
|
1186
|
-
// =============================================================================
|
|
1187
|
-
// Exports
|
|
1188
|
-
// =============================================================================
|
|
1189
|
-
|
|
1190
918
|
export default {
|
|
1191
919
|
init: initAutoTracking,
|
|
1192
920
|
cleanup: cleanupAutoTracking,
|