@rejourneyco/react-native 1.0.0 → 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/README.md +29 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +47 -30
- package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +25 -1
- package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +70 -32
- package/android/src/main/java/com/rejourney/core/Constants.kt +4 -4
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +14 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +9 -0
- package/ios/Capture/RJCaptureEngine.m +72 -34
- package/ios/Capture/RJCaptureHeuristics.h +7 -5
- package/ios/Capture/RJCaptureHeuristics.m +138 -112
- package/ios/Capture/RJVideoEncoder.m +0 -26
- package/ios/Core/Rejourney.mm +64 -102
- 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 +12 -101
- package/lib/commonjs/sdk/autoTracking.js +55 -353
- 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 +11 -105
- package/lib/module/sdk/autoTracking.js +55 -354
- 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 +2 -0
- package/lib/typescript/sdk/autoTracking.d.ts +5 -6
- package/lib/typescript/types/index.d.ts +0 -1
- package/package.json +11 -3
- package/src/NativeRejourney.ts +4 -0
- package/src/components/Mask.tsx +0 -3
- package/src/index.ts +11 -88
- package/src/sdk/autoTracking.ts +72 -331
- 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
package/src/sdk/autoTracking.ts
CHANGED
|
@@ -42,9 +42,7 @@ function getDimensions() {
|
|
|
42
42
|
return getRN()?.Dimensions;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
return getRN()?.NativeModules;
|
|
47
|
-
}
|
|
45
|
+
|
|
48
46
|
|
|
49
47
|
function getRejourneyNativeModule() {
|
|
50
48
|
const RN = getRN();
|
|
@@ -57,7 +55,7 @@ function getRejourneyNativeModule() {
|
|
|
57
55
|
try {
|
|
58
56
|
nativeModule = TurboModuleRegistry.get('Rejourney');
|
|
59
57
|
} catch {
|
|
60
|
-
// Ignore
|
|
58
|
+
// Ignore
|
|
61
59
|
}
|
|
62
60
|
}
|
|
63
61
|
|
|
@@ -68,8 +66,6 @@ function getRejourneyNativeModule() {
|
|
|
68
66
|
return nativeModule;
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
// Type declarations for browser globals (only used in hybrid apps where DOM is available)
|
|
72
|
-
// These don't exist in pure React Native but are needed for error tracking in hybrid scenarios
|
|
73
69
|
type OnErrorEventHandler = ((
|
|
74
70
|
event: Event | string,
|
|
75
71
|
source?: string,
|
|
@@ -83,7 +79,6 @@ interface PromiseRejectionEvent {
|
|
|
83
79
|
promise?: Promise<any>;
|
|
84
80
|
}
|
|
85
81
|
|
|
86
|
-
// Cast globalThis to work with both RN and hybrid scenarios
|
|
87
82
|
const _globalThis = globalThis as typeof globalThis & {
|
|
88
83
|
onerror?: OnErrorEventHandler;
|
|
89
84
|
addEventListener?: (type: string, handler: (event: any) => void) => void;
|
|
@@ -94,10 +89,6 @@ const _globalThis = globalThis as typeof globalThis & {
|
|
|
94
89
|
};
|
|
95
90
|
};
|
|
96
91
|
|
|
97
|
-
// =============================================================================
|
|
98
|
-
// Types
|
|
99
|
-
// =============================================================================
|
|
100
|
-
|
|
101
92
|
export interface TapEvent {
|
|
102
93
|
x: number;
|
|
103
94
|
y: number;
|
|
@@ -106,93 +97,63 @@ export interface TapEvent {
|
|
|
106
97
|
}
|
|
107
98
|
|
|
108
99
|
export interface SessionMetrics {
|
|
109
|
-
// Core counts
|
|
110
100
|
totalEvents: number;
|
|
111
101
|
touchCount: number;
|
|
112
102
|
scrollCount: number;
|
|
113
103
|
gestureCount: number;
|
|
114
104
|
inputCount: number;
|
|
115
105
|
navigationCount: number;
|
|
116
|
-
|
|
117
|
-
// Issue tracking
|
|
118
106
|
errorCount: number;
|
|
119
107
|
rageTapCount: number;
|
|
120
|
-
|
|
121
|
-
// API metrics
|
|
122
108
|
apiSuccessCount: number;
|
|
123
109
|
apiErrorCount: number;
|
|
124
110
|
apiTotalCount: number;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
netTotalDurationMs: number; // Sum of all API durations
|
|
128
|
-
netTotalBytes: number; // Sum of all response bytes
|
|
129
|
-
|
|
130
|
-
// Screen tracking for funnels
|
|
111
|
+
netTotalDurationMs: number;
|
|
112
|
+
netTotalBytes: number;
|
|
131
113
|
screensVisited: string[];
|
|
132
114
|
uniqueScreensCount: number;
|
|
133
115
|
|
|
134
|
-
// Scores (0-100)
|
|
135
116
|
interactionScore: number;
|
|
136
117
|
explorationScore: number;
|
|
137
118
|
uxScore: number;
|
|
138
119
|
}
|
|
139
120
|
|
|
140
121
|
export interface AutoTrackingConfig {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
trackReactNativeErrors?: boolean; // Track RN ErrorUtils (default: true)
|
|
150
|
-
|
|
151
|
-
// Session settings
|
|
152
|
-
collectDeviceInfo?: boolean; // Collect device info (default: true)
|
|
153
|
-
maxSessionDurationMs?: number; // Clamp to 1–10 minutes; default set from server/config
|
|
122
|
+
rageTapThreshold?: number;
|
|
123
|
+
rageTapTimeWindow?: number;
|
|
124
|
+
rageTapRadius?: number;
|
|
125
|
+
trackJSErrors?: boolean;
|
|
126
|
+
trackPromiseRejections?: boolean;
|
|
127
|
+
trackReactNativeErrors?: boolean;
|
|
128
|
+
collectDeviceInfo?: boolean;
|
|
129
|
+
maxSessionDurationMs?: number;
|
|
154
130
|
}
|
|
155
131
|
|
|
156
|
-
// =============================================================================
|
|
157
|
-
// State
|
|
158
|
-
// =============================================================================
|
|
159
|
-
|
|
160
132
|
let isInitialized = false;
|
|
161
133
|
let config: AutoTrackingConfig = {};
|
|
162
134
|
|
|
163
|
-
// Rage tap tracking
|
|
164
135
|
const recentTaps: TapEvent[] = [];
|
|
165
|
-
let tapHead = 0;
|
|
166
|
-
let tapCount = 0;
|
|
136
|
+
let tapHead = 0;
|
|
137
|
+
let tapCount = 0;
|
|
167
138
|
const MAX_RECENT_TAPS = 10;
|
|
168
139
|
|
|
169
|
-
// Session metrics
|
|
170
140
|
let metrics: SessionMetrics = createEmptyMetrics();
|
|
171
141
|
let sessionStartTime: number = 0;
|
|
172
142
|
let maxSessionDurationMs: number = 10 * 60 * 1000;
|
|
173
|
-
|
|
174
|
-
// Screen tracking
|
|
175
143
|
let currentScreen = '';
|
|
176
144
|
let screensVisited: string[] = [];
|
|
177
145
|
|
|
178
|
-
// Anonymous ID
|
|
179
146
|
let anonymousId: string | null = null;
|
|
180
147
|
let anonymousIdPromise: Promise<string> | null = null;
|
|
181
148
|
|
|
182
|
-
// Callbacks
|
|
183
149
|
let onRageTapDetected: ((count: number, x: number, y: number) => void) | null = null;
|
|
184
150
|
let onErrorCaptured: ((error: ErrorEvent) => void) | null = null;
|
|
185
151
|
let onScreenChange: ((screenName: string, previousScreen?: string) => void) | null = null;
|
|
186
152
|
|
|
187
|
-
// Original error handlers (for restoration)
|
|
188
153
|
let originalErrorHandler: ((error: Error, isFatal: boolean) => void) | undefined;
|
|
189
154
|
let originalOnError: OnErrorEventHandler | null = null;
|
|
190
155
|
let originalOnUnhandledRejection: ((event: PromiseRejectionEvent) => void) | null = null;
|
|
191
156
|
|
|
192
|
-
// =============================================================================
|
|
193
|
-
// Initialization
|
|
194
|
-
// =============================================================================
|
|
195
|
-
|
|
196
157
|
/**
|
|
197
158
|
* Initialize auto tracking features
|
|
198
159
|
* Called automatically by Rejourney.init() - no user action needed
|
|
@@ -219,7 +180,6 @@ export function initAutoTracking(
|
|
|
219
180
|
...trackingConfig,
|
|
220
181
|
};
|
|
221
182
|
|
|
222
|
-
// Session timing
|
|
223
183
|
sessionStartTime = Date.now();
|
|
224
184
|
setMaxSessionDurationMinutes(
|
|
225
185
|
trackingConfig.maxSessionDurationMs
|
|
@@ -227,21 +187,14 @@ export function initAutoTracking(
|
|
|
227
187
|
: undefined
|
|
228
188
|
);
|
|
229
189
|
|
|
230
|
-
// Set callbacks
|
|
231
190
|
onRageTapDetected = callbacks.onRageTap || null;
|
|
232
191
|
onErrorCaptured = callbacks.onError || null;
|
|
233
192
|
onScreenChange = callbacks.onScreen || null;
|
|
234
|
-
|
|
235
|
-
// Initialize metrics
|
|
236
|
-
metrics = createEmptyMetrics();
|
|
237
|
-
sessionStartTime = Date.now();
|
|
238
|
-
anonymousId = generateAnonymousId();
|
|
239
|
-
|
|
240
|
-
// Setup error tracking
|
|
241
193
|
setupErrorTracking();
|
|
242
|
-
|
|
243
|
-
// Setup React Navigation tracking (if available)
|
|
244
194
|
setupNavigationTracking();
|
|
195
|
+
loadAnonymousId().then(id => {
|
|
196
|
+
anonymousId = id;
|
|
197
|
+
});
|
|
245
198
|
|
|
246
199
|
isInitialized = true;
|
|
247
200
|
}
|
|
@@ -252,10 +205,7 @@ export function initAutoTracking(
|
|
|
252
205
|
export function cleanupAutoTracking(): void {
|
|
253
206
|
if (!isInitialized) return;
|
|
254
207
|
|
|
255
|
-
// Restore original error handlers
|
|
256
208
|
restoreErrorHandlers();
|
|
257
|
-
|
|
258
|
-
// Cleanup navigation tracking
|
|
259
209
|
cleanupNavigationTracking();
|
|
260
210
|
|
|
261
211
|
// Reset state
|
|
@@ -269,10 +219,6 @@ export function cleanupAutoTracking(): void {
|
|
|
269
219
|
isInitialized = false;
|
|
270
220
|
}
|
|
271
221
|
|
|
272
|
-
// =============================================================================
|
|
273
|
-
// Rage Tap Detection
|
|
274
|
-
// =============================================================================
|
|
275
|
-
|
|
276
222
|
/**
|
|
277
223
|
* Track a tap event for rage tap detection
|
|
278
224
|
* Called automatically from touch interceptor
|
|
@@ -282,18 +228,14 @@ export function trackTap(tap: TapEvent): void {
|
|
|
282
228
|
|
|
283
229
|
const now = Date.now();
|
|
284
230
|
|
|
285
|
-
// Add to circular buffer (O(1) instead of shift() which is O(n))
|
|
286
231
|
const insertIndex = (tapHead + tapCount) % MAX_RECENT_TAPS;
|
|
287
232
|
if (tapCount < MAX_RECENT_TAPS) {
|
|
288
233
|
recentTaps[insertIndex] = { ...tap, timestamp: now };
|
|
289
234
|
tapCount++;
|
|
290
235
|
} else {
|
|
291
|
-
// Buffer full, overwrite oldest
|
|
292
236
|
recentTaps[tapHead] = { ...tap, timestamp: now };
|
|
293
237
|
tapHead = (tapHead + 1) % MAX_RECENT_TAPS;
|
|
294
238
|
}
|
|
295
|
-
|
|
296
|
-
// Evict old taps outside time window
|
|
297
239
|
const windowStart = now - (config.rageTapTimeWindow || 500);
|
|
298
240
|
while (tapCount > 0) {
|
|
299
241
|
const oldestTap = recentTaps[tapHead];
|
|
@@ -305,10 +247,7 @@ export function trackTap(tap: TapEvent): void {
|
|
|
305
247
|
}
|
|
306
248
|
}
|
|
307
249
|
|
|
308
|
-
// Check for rage tap
|
|
309
250
|
detectRageTap();
|
|
310
|
-
|
|
311
|
-
// Update metrics
|
|
312
251
|
metrics.touchCount++;
|
|
313
252
|
metrics.totalEvents++;
|
|
314
253
|
}
|
|
@@ -321,14 +260,12 @@ function detectRageTap(): void {
|
|
|
321
260
|
const radius = config.rageTapRadius || 50;
|
|
322
261
|
|
|
323
262
|
if (tapCount < threshold) return;
|
|
324
|
-
// Check last N taps from circular buffer
|
|
325
263
|
const tapsToCheck: TapEvent[] = [];
|
|
326
264
|
for (let i = 0; i < threshold; i++) {
|
|
327
265
|
const idx = (tapHead + tapCount - threshold + i) % MAX_RECENT_TAPS;
|
|
328
266
|
tapsToCheck.push(recentTaps[idx]!);
|
|
329
267
|
}
|
|
330
268
|
|
|
331
|
-
// Calculate center point
|
|
332
269
|
let centerX = 0;
|
|
333
270
|
let centerY = 0;
|
|
334
271
|
for (const tap of tapsToCheck) {
|
|
@@ -338,7 +275,6 @@ function detectRageTap(): void {
|
|
|
338
275
|
centerX /= tapsToCheck.length;
|
|
339
276
|
centerY /= tapsToCheck.length;
|
|
340
277
|
|
|
341
|
-
// Check if all taps are within radius of center
|
|
342
278
|
let allWithinRadius = true;
|
|
343
279
|
for (const tap of tapsToCheck) {
|
|
344
280
|
const dx = tap.x - centerX;
|
|
@@ -351,9 +287,7 @@ function detectRageTap(): void {
|
|
|
351
287
|
}
|
|
352
288
|
|
|
353
289
|
if (allWithinRadius) {
|
|
354
|
-
// Rage tap detected!
|
|
355
290
|
metrics.rageTapCount++;
|
|
356
|
-
// Clear circular buffer to prevent duplicate detection
|
|
357
291
|
tapHead = 0;
|
|
358
292
|
tapCount = 0;
|
|
359
293
|
|
|
@@ -364,10 +298,6 @@ function detectRageTap(): void {
|
|
|
364
298
|
}
|
|
365
299
|
}
|
|
366
300
|
|
|
367
|
-
// =============================================================================
|
|
368
|
-
// State Change Notification
|
|
369
|
-
// =============================================================================
|
|
370
|
-
|
|
371
301
|
/**
|
|
372
302
|
* Notify that a state change occurred (navigation, modal, etc.)
|
|
373
303
|
* Kept for API compatibility
|
|
@@ -376,25 +306,18 @@ export function notifyStateChange(): void {
|
|
|
376
306
|
// No-op - kept for backward compatibility
|
|
377
307
|
}
|
|
378
308
|
|
|
379
|
-
// =============================================================================
|
|
380
|
-
// Error Tracking
|
|
381
|
-
// =============================================================================
|
|
382
|
-
|
|
383
309
|
/**
|
|
384
310
|
* Setup automatic error tracking
|
|
385
311
|
*/
|
|
386
312
|
function setupErrorTracking(): void {
|
|
387
|
-
// Track React Native errors
|
|
388
313
|
if (config.trackReactNativeErrors !== false) {
|
|
389
314
|
setupReactNativeErrorHandler();
|
|
390
315
|
}
|
|
391
316
|
|
|
392
|
-
// Track JavaScript errors (only works in web/debug)
|
|
393
317
|
if (config.trackJSErrors !== false && typeof _globalThis !== 'undefined') {
|
|
394
318
|
setupJSErrorHandler();
|
|
395
319
|
}
|
|
396
320
|
|
|
397
|
-
// Track unhandled promise rejections
|
|
398
321
|
if (config.trackPromiseRejections !== false && typeof _globalThis !== 'undefined') {
|
|
399
322
|
setupPromiseRejectionHandler();
|
|
400
323
|
}
|
|
@@ -405,16 +328,12 @@ function setupErrorTracking(): void {
|
|
|
405
328
|
*/
|
|
406
329
|
function setupReactNativeErrorHandler(): void {
|
|
407
330
|
try {
|
|
408
|
-
// Access ErrorUtils from global scope
|
|
409
331
|
const ErrorUtils = _globalThis.ErrorUtils;
|
|
410
332
|
if (!ErrorUtils) return;
|
|
411
333
|
|
|
412
|
-
// Store original handler
|
|
413
334
|
originalErrorHandler = ErrorUtils.getGlobalHandler();
|
|
414
335
|
|
|
415
|
-
// Set new handler
|
|
416
336
|
ErrorUtils.setGlobalHandler((error: Error, isFatal: boolean) => {
|
|
417
|
-
// Track the error
|
|
418
337
|
trackError({
|
|
419
338
|
type: 'error',
|
|
420
339
|
timestamp: Date.now(),
|
|
@@ -423,13 +342,12 @@ function setupReactNativeErrorHandler(): void {
|
|
|
423
342
|
name: error.name || 'Error',
|
|
424
343
|
});
|
|
425
344
|
|
|
426
|
-
// Call original handler
|
|
427
345
|
if (originalErrorHandler) {
|
|
428
346
|
originalErrorHandler(error, isFatal);
|
|
429
347
|
}
|
|
430
348
|
});
|
|
431
349
|
} catch {
|
|
432
|
-
//
|
|
350
|
+
// Ignore
|
|
433
351
|
}
|
|
434
352
|
}
|
|
435
353
|
|
|
@@ -438,8 +356,6 @@ function setupReactNativeErrorHandler(): void {
|
|
|
438
356
|
*/
|
|
439
357
|
function setupJSErrorHandler(): void {
|
|
440
358
|
if (typeof _globalThis.onerror !== 'undefined') {
|
|
441
|
-
// Note: In React Native, this typically doesn't fire
|
|
442
|
-
// But we set it up anyway for hybrid apps
|
|
443
359
|
originalOnError = _globalThis.onerror;
|
|
444
360
|
|
|
445
361
|
_globalThis.onerror = (
|
|
@@ -457,7 +373,6 @@ function setupJSErrorHandler(): void {
|
|
|
457
373
|
name: error?.name || 'Error',
|
|
458
374
|
});
|
|
459
375
|
|
|
460
|
-
// Call original handler
|
|
461
376
|
if (originalOnError) {
|
|
462
377
|
return originalOnError(message, source, lineno, colno, error);
|
|
463
378
|
}
|
|
@@ -491,7 +406,6 @@ function setupPromiseRejectionHandler(): void {
|
|
|
491
406
|
* Restore original error handlers
|
|
492
407
|
*/
|
|
493
408
|
function restoreErrorHandlers(): void {
|
|
494
|
-
// Restore React Native handler
|
|
495
409
|
if (originalErrorHandler) {
|
|
496
410
|
try {
|
|
497
411
|
const ErrorUtils = _globalThis.ErrorUtils;
|
|
@@ -504,13 +418,11 @@ function restoreErrorHandlers(): void {
|
|
|
504
418
|
originalErrorHandler = undefined;
|
|
505
419
|
}
|
|
506
420
|
|
|
507
|
-
// Restore global onerror
|
|
508
421
|
if (originalOnError !== null) {
|
|
509
422
|
_globalThis.onerror = originalOnError;
|
|
510
423
|
originalOnError = null;
|
|
511
424
|
}
|
|
512
425
|
|
|
513
|
-
// Remove promise rejection handler
|
|
514
426
|
if (originalOnUnhandledRejection && typeof _globalThis.removeEventListener !== 'undefined') {
|
|
515
427
|
_globalThis.removeEventListener!('unhandledrejection', originalOnUnhandledRejection);
|
|
516
428
|
originalOnUnhandledRejection = null;
|
|
@@ -546,15 +458,10 @@ export function captureError(
|
|
|
546
458
|
});
|
|
547
459
|
}
|
|
548
460
|
|
|
549
|
-
// =============================================================================
|
|
550
|
-
// Screen/Funnel Tracking - Automatic Navigation Detection
|
|
551
|
-
// =============================================================================
|
|
552
|
-
|
|
553
|
-
// Navigation detection state
|
|
554
461
|
let navigationPollingInterval: ReturnType<typeof setInterval> | null = null;
|
|
555
462
|
let lastDetectedScreen = '';
|
|
556
463
|
let navigationSetupDone = false;
|
|
557
|
-
let navigationPollingErrors = 0;
|
|
464
|
+
let navigationPollingErrors = 0;
|
|
558
465
|
const MAX_POLLING_ERRORS = 10; // Stop polling after 10 consecutive errors
|
|
559
466
|
|
|
560
467
|
/**
|
|
@@ -578,7 +485,6 @@ export function trackNavigationState(state: any): void {
|
|
|
578
485
|
try {
|
|
579
486
|
const { normalizeScreenName } = require('./navigation');
|
|
580
487
|
|
|
581
|
-
// Find the active screen recursively
|
|
582
488
|
const findActiveScreen = (navState: any): string | null => {
|
|
583
489
|
if (!navState?.routes) return null;
|
|
584
490
|
const index = navState.index ?? navState.routes.length - 1;
|
|
@@ -594,7 +500,7 @@ export function trackNavigationState(state: any): void {
|
|
|
594
500
|
trackScreen(screenName);
|
|
595
501
|
}
|
|
596
502
|
} catch {
|
|
597
|
-
//
|
|
503
|
+
// Ignore
|
|
598
504
|
}
|
|
599
505
|
}
|
|
600
506
|
|
|
@@ -624,14 +530,11 @@ export function trackNavigationState(state: any): void {
|
|
|
624
530
|
* ```
|
|
625
531
|
*/
|
|
626
532
|
export function useNavigationTracking() {
|
|
627
|
-
// Use React's useRef and useCallback to create stable references
|
|
628
533
|
const React = require('react');
|
|
629
534
|
const { createNavigationContainerRef } = require('@react-navigation/native');
|
|
630
535
|
|
|
631
|
-
// Create a stable navigation ref
|
|
632
536
|
const navigationRef = React.useRef(createNavigationContainerRef());
|
|
633
537
|
|
|
634
|
-
// Track initial screen when navigation is ready
|
|
635
538
|
const onReady = React.useCallback(() => {
|
|
636
539
|
try {
|
|
637
540
|
const currentRoute = navigationRef.current?.getCurrentRoute?.();
|
|
@@ -644,11 +547,10 @@ export function useNavigationTracking() {
|
|
|
644
547
|
}
|
|
645
548
|
}
|
|
646
549
|
} catch {
|
|
647
|
-
//
|
|
550
|
+
// Ignore
|
|
648
551
|
}
|
|
649
552
|
}, []);
|
|
650
553
|
|
|
651
|
-
// Return props to spread on NavigationContainer
|
|
652
554
|
return {
|
|
653
555
|
ref: navigationRef.current,
|
|
654
556
|
onReady,
|
|
@@ -674,22 +576,21 @@ function setupNavigationTracking(): void {
|
|
|
674
576
|
// We retry a few times with increasing delays
|
|
675
577
|
let attempts = 0;
|
|
676
578
|
const maxAttempts = 5;
|
|
677
|
-
|
|
579
|
+
|
|
678
580
|
const trySetup = () => {
|
|
679
581
|
attempts++;
|
|
680
582
|
if (__DEV__) {
|
|
681
583
|
logger.debug('Navigation setup attempt', attempts, 'of', maxAttempts);
|
|
682
584
|
}
|
|
683
|
-
|
|
585
|
+
|
|
684
586
|
const success = trySetupExpoRouter();
|
|
685
|
-
|
|
587
|
+
|
|
686
588
|
if (success) {
|
|
687
589
|
if (__DEV__) {
|
|
688
590
|
logger.debug('Expo Router setup: SUCCESS on attempt', attempts);
|
|
689
591
|
}
|
|
690
592
|
} else if (attempts < maxAttempts) {
|
|
691
|
-
|
|
692
|
-
const delay = 200 * attempts; // 200, 400, 600, 800ms
|
|
593
|
+
const delay = 200 * attempts;
|
|
693
594
|
if (__DEV__) {
|
|
694
595
|
logger.debug('Expo Router not ready, retrying in', delay, 'ms');
|
|
695
596
|
}
|
|
@@ -702,7 +603,6 @@ function setupNavigationTracking(): void {
|
|
|
702
603
|
}
|
|
703
604
|
};
|
|
704
605
|
|
|
705
|
-
// Start first attempt after 200ms
|
|
706
606
|
setTimeout(trySetup, 200);
|
|
707
607
|
}
|
|
708
608
|
|
|
@@ -729,13 +629,10 @@ function trySetupExpoRouter(): boolean {
|
|
|
729
629
|
|
|
730
630
|
const { normalizeScreenName, getScreenNameFromPath } = require('./navigation');
|
|
731
631
|
|
|
732
|
-
// Poll for route changes (expo-router doesn't expose a listener API outside hooks)
|
|
733
632
|
navigationPollingInterval = setInterval(() => {
|
|
734
633
|
try {
|
|
735
634
|
let state = null;
|
|
736
635
|
let stateSource = '';
|
|
737
|
-
|
|
738
|
-
// Method 1: Public accessors on router object
|
|
739
636
|
if (typeof router.getState === 'function') {
|
|
740
637
|
state = router.getState();
|
|
741
638
|
stateSource = 'router.getState()';
|
|
@@ -744,33 +641,28 @@ function trySetupExpoRouter(): boolean {
|
|
|
744
641
|
stateSource = 'router.rootState';
|
|
745
642
|
}
|
|
746
643
|
|
|
747
|
-
// Method 2: Internal store access (works for both v3 and v6+)
|
|
748
644
|
if (!state) {
|
|
749
645
|
try {
|
|
750
646
|
const storeModule = require('expo-router/build/global-state/router-store');
|
|
751
647
|
if (storeModule?.store) {
|
|
752
|
-
// v6+: store.state or store.navigationRef
|
|
753
648
|
state = storeModule.store.state;
|
|
754
649
|
if (state) stateSource = 'store.state';
|
|
755
650
|
|
|
756
|
-
// v6+: Try navigationRef if state is undefined
|
|
757
651
|
if (!state && storeModule.store.navigationRef?.current) {
|
|
758
652
|
state = storeModule.store.navigationRef.current.getRootState?.();
|
|
759
653
|
if (state) stateSource = 'navigationRef.getRootState()';
|
|
760
654
|
}
|
|
761
655
|
|
|
762
|
-
// v3: store.rootState or store.initialState
|
|
763
656
|
if (!state) {
|
|
764
657
|
state = storeModule.store.rootState || storeModule.store.initialState;
|
|
765
658
|
if (state) stateSource = 'store.rootState/initialState';
|
|
766
659
|
}
|
|
767
660
|
}
|
|
768
661
|
} catch {
|
|
769
|
-
//
|
|
662
|
+
// Ignore
|
|
770
663
|
}
|
|
771
664
|
}
|
|
772
665
|
|
|
773
|
-
// Method 3: Try accessing via a different export path for v6
|
|
774
666
|
if (!state) {
|
|
775
667
|
try {
|
|
776
668
|
const imperative = require('expo-router/build/imperative-api');
|
|
@@ -779,12 +671,12 @@ function trySetupExpoRouter(): boolean {
|
|
|
779
671
|
if (state) stateSource = 'imperative-api';
|
|
780
672
|
}
|
|
781
673
|
} catch {
|
|
782
|
-
//
|
|
674
|
+
// Ignore
|
|
783
675
|
}
|
|
784
676
|
}
|
|
785
677
|
|
|
786
678
|
if (state) {
|
|
787
|
-
|
|
679
|
+
navigationPollingErrors = 0;
|
|
788
680
|
navigationPollingErrors = 0;
|
|
789
681
|
const screenName = extractScreenNameFromRouterState(state, getScreenNameFromPath, normalizeScreenName);
|
|
790
682
|
if (screenName && screenName !== lastDetectedScreen) {
|
|
@@ -795,21 +687,15 @@ function trySetupExpoRouter(): boolean {
|
|
|
795
687
|
trackScreen(screenName);
|
|
796
688
|
}
|
|
797
689
|
} else {
|
|
798
|
-
// Track consecutive failures to get state
|
|
799
690
|
navigationPollingErrors++;
|
|
800
691
|
if (__DEV__ && navigationPollingErrors === 1) {
|
|
801
692
|
logger.debug('Expo Router: Could not get navigation state');
|
|
802
693
|
}
|
|
803
694
|
if (navigationPollingErrors >= MAX_POLLING_ERRORS) {
|
|
804
|
-
// Stop polling after too many errors to save CPU
|
|
805
|
-
if (__DEV__) {
|
|
806
|
-
logger.debug('Expo Router: Stopped polling after', MAX_POLLING_ERRORS, 'errors');
|
|
807
|
-
}
|
|
808
695
|
cleanupNavigationTracking();
|
|
809
696
|
}
|
|
810
697
|
}
|
|
811
698
|
} catch (e) {
|
|
812
|
-
// Error - track and potentially stop
|
|
813
699
|
navigationPollingErrors++;
|
|
814
700
|
if (__DEV__ && navigationPollingErrors === 1) {
|
|
815
701
|
logger.debug('Expo Router polling error:', e);
|
|
@@ -818,14 +704,13 @@ function trySetupExpoRouter(): boolean {
|
|
|
818
704
|
cleanupNavigationTracking();
|
|
819
705
|
}
|
|
820
706
|
}
|
|
821
|
-
}, 500);
|
|
707
|
+
}, 500);
|
|
822
708
|
|
|
823
709
|
return true;
|
|
824
710
|
} catch (e) {
|
|
825
711
|
if (__DEV__) {
|
|
826
712
|
logger.debug('Expo Router not available:', e);
|
|
827
713
|
}
|
|
828
|
-
// expo-router not installed
|
|
829
714
|
return false;
|
|
830
715
|
}
|
|
831
716
|
}
|
|
@@ -847,10 +732,8 @@ function extractScreenNameFromRouterState(
|
|
|
847
732
|
const route = state.routes[state.index ?? state.routes.length - 1];
|
|
848
733
|
if (!route) return null;
|
|
849
734
|
|
|
850
|
-
// Add current route name to accumulated segments
|
|
851
735
|
const newSegments = [...accumulatedSegments, route.name];
|
|
852
736
|
|
|
853
|
-
// If this route has nested state, recurse deeper
|
|
854
737
|
if (route.state) {
|
|
855
738
|
return extractScreenNameFromRouterState(
|
|
856
739
|
route.state,
|
|
@@ -860,13 +743,9 @@ function extractScreenNameFromRouterState(
|
|
|
860
743
|
);
|
|
861
744
|
}
|
|
862
745
|
|
|
863
|
-
// We've reached the deepest level - build the screen name
|
|
864
|
-
// Filter out group markers like (tabs), (main), (auth)
|
|
865
746
|
const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
|
|
866
747
|
|
|
867
|
-
// If after filtering we have no segments, use the last meaningful name
|
|
868
748
|
if (cleanSegments.length === 0) {
|
|
869
|
-
// Find the last non-group segment
|
|
870
749
|
for (let i = newSegments.length - 1; i >= 0; i--) {
|
|
871
750
|
const seg = newSegments[i];
|
|
872
751
|
if (seg && !seg.startsWith('(') && !seg.endsWith(')')) {
|
|
@@ -907,14 +786,10 @@ export function trackScreen(screenName: string): void {
|
|
|
907
786
|
|
|
908
787
|
const previousScreen = currentScreen;
|
|
909
788
|
currentScreen = screenName;
|
|
910
|
-
// Add to screens visited (only track for unique set, avoid large array copies)
|
|
911
789
|
screensVisited.push(screenName);
|
|
912
790
|
|
|
913
|
-
// Update unique screens count
|
|
914
791
|
const uniqueScreens = new Set(screensVisited);
|
|
915
792
|
metrics.uniqueScreensCount = uniqueScreens.size;
|
|
916
|
-
|
|
917
|
-
// Update navigation count
|
|
918
793
|
metrics.navigationCount++;
|
|
919
794
|
metrics.totalEvents++;
|
|
920
795
|
|
|
@@ -922,13 +797,10 @@ export function trackScreen(screenName: string): void {
|
|
|
922
797
|
logger.debug('trackScreen:', screenName, '(total screens:', metrics.uniqueScreensCount, ')');
|
|
923
798
|
}
|
|
924
799
|
|
|
925
|
-
// Notify callback
|
|
926
800
|
if (onScreenChange) {
|
|
927
801
|
onScreenChange(screenName, previousScreen);
|
|
928
802
|
}
|
|
929
803
|
|
|
930
|
-
// IMPORTANT: Also notify native module to send to backend
|
|
931
|
-
// This is the key fix - without this, screens don't get recorded!
|
|
932
804
|
try {
|
|
933
805
|
const RejourneyNative = getRejourneyNativeModule();
|
|
934
806
|
if (RejourneyNative?.screenChanged) {
|
|
@@ -950,10 +822,6 @@ export function trackScreen(screenName: string): void {
|
|
|
950
822
|
}
|
|
951
823
|
}
|
|
952
824
|
|
|
953
|
-
// =============================================================================
|
|
954
|
-
// API Metrics Tracking
|
|
955
|
-
// =============================================================================
|
|
956
|
-
|
|
957
825
|
/**
|
|
958
826
|
* Track an API request with timing data
|
|
959
827
|
*/
|
|
@@ -967,7 +835,6 @@ export function trackAPIRequest(
|
|
|
967
835
|
|
|
968
836
|
metrics.apiTotalCount++;
|
|
969
837
|
|
|
970
|
-
// Accumulate timing and size for avg calculation
|
|
971
838
|
if (durationMs > 0) {
|
|
972
839
|
metrics.netTotalDurationMs += durationMs;
|
|
973
840
|
}
|
|
@@ -979,16 +846,10 @@ export function trackAPIRequest(
|
|
|
979
846
|
metrics.apiSuccessCount++;
|
|
980
847
|
} else {
|
|
981
848
|
metrics.apiErrorCount++;
|
|
982
|
-
|
|
983
|
-
// API errors also count toward error count for UX score
|
|
984
849
|
metrics.errorCount++;
|
|
985
850
|
}
|
|
986
851
|
}
|
|
987
852
|
|
|
988
|
-
// =============================================================================
|
|
989
|
-
// Session Metrics
|
|
990
|
-
// =============================================================================
|
|
991
|
-
|
|
992
853
|
/**
|
|
993
854
|
* Create empty metrics object
|
|
994
855
|
*/
|
|
@@ -1046,19 +907,15 @@ export function trackInput(): void {
|
|
|
1046
907
|
* Get current session metrics
|
|
1047
908
|
*/
|
|
1048
909
|
export function getSessionMetrics(): SessionMetrics & { netAvgDurationMs: number } {
|
|
1049
|
-
// Calculate scores before returning
|
|
1050
910
|
calculateScores();
|
|
1051
911
|
|
|
1052
|
-
// Compute average API response time
|
|
1053
912
|
const netAvgDurationMs = metrics.apiTotalCount > 0
|
|
1054
913
|
? Math.round(metrics.netTotalDurationMs / metrics.apiTotalCount)
|
|
1055
914
|
: 0;
|
|
1056
915
|
|
|
1057
|
-
// Lazily populate screensVisited only when metrics are retrieved
|
|
1058
|
-
// This avoids expensive array copies on every screen change
|
|
1059
916
|
return {
|
|
1060
917
|
...metrics,
|
|
1061
|
-
screensVisited: [...screensVisited],
|
|
918
|
+
screensVisited: [...screensVisited],
|
|
1062
919
|
netAvgDurationMs,
|
|
1063
920
|
};
|
|
1064
921
|
}
|
|
@@ -1067,38 +924,26 @@ export function getSessionMetrics(): SessionMetrics & { netAvgDurationMs: number
|
|
|
1067
924
|
* Calculate session scores
|
|
1068
925
|
*/
|
|
1069
926
|
function calculateScores(): void {
|
|
1070
|
-
// Interaction Score (0-100)
|
|
1071
|
-
// Based on total interactions normalized to a baseline
|
|
1072
927
|
const totalInteractions =
|
|
1073
928
|
metrics.touchCount +
|
|
1074
929
|
metrics.scrollCount +
|
|
1075
930
|
metrics.gestureCount +
|
|
1076
931
|
metrics.inputCount;
|
|
1077
932
|
|
|
1078
|
-
// Assume 50 interactions is "average" for a session
|
|
1079
933
|
const avgInteractions = 50;
|
|
1080
934
|
metrics.interactionScore = Math.min(100, Math.round((totalInteractions / avgInteractions) * 100));
|
|
1081
935
|
|
|
1082
|
-
// Exploration Score (0-100)
|
|
1083
|
-
// Based on unique screens visited
|
|
1084
|
-
// Assume 5 screens is "average" exploration
|
|
1085
936
|
const avgScreens = 5;
|
|
1086
937
|
metrics.explorationScore = Math.min(100, Math.round((metrics.uniqueScreensCount / avgScreens) * 100));
|
|
1087
938
|
|
|
1088
|
-
// UX Score (0-100)
|
|
1089
|
-
// Starts at 100, deducts for issues
|
|
1090
939
|
let uxScore = 100;
|
|
1091
940
|
|
|
1092
|
-
|
|
1093
|
-
uxScore -= Math.min(30, metrics.errorCount * 15); // Max 30 point deduction
|
|
941
|
+
uxScore -= Math.min(30, metrics.errorCount * 15);
|
|
1094
942
|
|
|
1095
|
-
|
|
1096
|
-
uxScore -= Math.min(24, metrics.rageTapCount * 8); // Max 24 point deduction
|
|
943
|
+
uxScore -= Math.min(24, metrics.rageTapCount * 8);
|
|
1097
944
|
|
|
1098
|
-
|
|
1099
|
-
uxScore -= Math.min(20, metrics.apiErrorCount * 10); // Max 20 point deduction
|
|
945
|
+
uxScore -= Math.min(20, metrics.apiErrorCount * 10);
|
|
1100
946
|
|
|
1101
|
-
// Bonus for completing funnel (if screens > 3)
|
|
1102
947
|
if (metrics.uniqueScreensCount >= 3) {
|
|
1103
948
|
uxScore += 5;
|
|
1104
949
|
}
|
|
@@ -1118,44 +963,33 @@ export function resetMetrics(): void {
|
|
|
1118
963
|
sessionStartTime = Date.now();
|
|
1119
964
|
}
|
|
1120
965
|
|
|
1121
|
-
// =============================================================================
|
|
1122
|
-
// Session duration helpers
|
|
1123
|
-
// =============================================================================
|
|
1124
|
-
|
|
1125
|
-
/** Clamp and set max session duration in minutes (1–10). Defaults to 10. */
|
|
1126
966
|
export function setMaxSessionDurationMinutes(minutes?: number): void {
|
|
1127
967
|
const clampedMinutes = Math.min(10, Math.max(1, minutes ?? 10));
|
|
1128
968
|
maxSessionDurationMs = clampedMinutes * 60 * 1000;
|
|
1129
969
|
}
|
|
1130
|
-
|
|
1131
|
-
/** Returns true if the current session exceeded the configured max duration. */
|
|
1132
970
|
export function hasExceededMaxSessionDuration(): boolean {
|
|
1133
971
|
if (!sessionStartTime) return false;
|
|
1134
972
|
return Date.now() - sessionStartTime >= maxSessionDurationMs;
|
|
1135
973
|
}
|
|
1136
974
|
|
|
1137
|
-
/** Returns remaining milliseconds until the session should stop. */
|
|
1138
975
|
export function getRemainingSessionDurationMs(): number {
|
|
1139
976
|
if (!sessionStartTime) return maxSessionDurationMs;
|
|
1140
977
|
const remaining = maxSessionDurationMs - (Date.now() - sessionStartTime);
|
|
1141
978
|
return Math.max(0, remaining);
|
|
1142
979
|
}
|
|
1143
980
|
|
|
1144
|
-
// =============================================================================
|
|
1145
|
-
// Device Info Collection
|
|
1146
|
-
// =============================================================================
|
|
1147
|
-
|
|
1148
981
|
/**
|
|
1149
982
|
* Collect device information
|
|
1150
983
|
*/
|
|
1151
|
-
|
|
984
|
+
/**
|
|
985
|
+
* Collect device information
|
|
986
|
+
*/
|
|
987
|
+
export async function collectDeviceInfo(): Promise<DeviceInfo> {
|
|
1152
988
|
const Dimensions = getDimensions();
|
|
1153
989
|
const Platform = getPlatform();
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
// Default values if react-native isn't available
|
|
990
|
+
|
|
1157
991
|
let width = 0, height = 0, scale = 1;
|
|
1158
|
-
|
|
992
|
+
|
|
1159
993
|
if (Dimensions) {
|
|
1160
994
|
const windowDims = Dimensions.get('window');
|
|
1161
995
|
const screenDims = Dimensions.get('screen');
|
|
@@ -1164,105 +998,46 @@ export function collectDeviceInfo(): DeviceInfo {
|
|
|
1164
998
|
scale = screenDims?.scale || 1;
|
|
1165
999
|
}
|
|
1166
1000
|
|
|
1167
|
-
//
|
|
1168
|
-
let model = 'Unknown';
|
|
1169
|
-
let manufacturer: string | undefined;
|
|
1170
|
-
let osVersion = 'Unknown';
|
|
1171
|
-
let appVersion: string | undefined;
|
|
1172
|
-
let appId: string | undefined;
|
|
1001
|
+
// Basic JS-side info
|
|
1173
1002
|
let locale: string | undefined;
|
|
1174
1003
|
let timezone: string | undefined;
|
|
1175
1004
|
|
|
1176
1005
|
try {
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
const DeviceInfo = require('react-native-device-info');
|
|
1180
|
-
model = DeviceInfo.getModel?.() || model;
|
|
1181
|
-
manufacturer = DeviceInfo.getBrand?.() || undefined;
|
|
1182
|
-
osVersion = DeviceInfo.getSystemVersion?.() || osVersion;
|
|
1183
|
-
appVersion = DeviceInfo.getVersion?.() || undefined;
|
|
1184
|
-
appId = DeviceInfo.getBundleId?.() || undefined;
|
|
1185
|
-
locale = DeviceInfo.getDeviceLocale?.() || undefined;
|
|
1186
|
-
timezone = DeviceInfo.getTimezone?.() || undefined;
|
|
1006
|
+
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1007
|
+
locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
1187
1008
|
} catch {
|
|
1188
|
-
//
|
|
1189
|
-
try {
|
|
1190
|
-
// Try expo-application for app version/id
|
|
1191
|
-
const Application = require('expo-application');
|
|
1192
|
-
appVersion = Application.nativeApplicationVersion || Application.applicationVersion || undefined;
|
|
1193
|
-
appId = Application.applicationId || undefined;
|
|
1194
|
-
} catch {
|
|
1195
|
-
// expo-application not available
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
try {
|
|
1199
|
-
// Try expo-constants for additional info
|
|
1200
|
-
const Constants = require('expo-constants');
|
|
1201
|
-
const expoConfig = Constants.expoConfig || Constants.manifest2?.extra?.expoClient || Constants.manifest;
|
|
1202
|
-
if (!appVersion && expoConfig?.version) {
|
|
1203
|
-
appVersion = expoConfig.version;
|
|
1204
|
-
}
|
|
1205
|
-
if (!appId && (expoConfig?.ios?.bundleIdentifier || expoConfig?.android?.package)) {
|
|
1206
|
-
appId = Platform?.OS === 'ios'
|
|
1207
|
-
? expoConfig?.ios?.bundleIdentifier
|
|
1208
|
-
: expoConfig?.android?.package;
|
|
1209
|
-
}
|
|
1210
|
-
} catch {
|
|
1211
|
-
// expo-constants not available
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
// Fall back to basic platform info
|
|
1215
|
-
if (Platform?.OS === 'ios') {
|
|
1216
|
-
// Get basic info from constants
|
|
1217
|
-
const PlatformConstants = NativeModules?.PlatformConstants;
|
|
1218
|
-
osVersion = Platform.Version?.toString() || osVersion;
|
|
1219
|
-
model = PlatformConstants?.interfaceIdiom === 'pad' ? 'iPad' : 'iPhone';
|
|
1220
|
-
} else if (Platform?.OS === 'android') {
|
|
1221
|
-
osVersion = Platform.Version?.toString() || osVersion;
|
|
1222
|
-
model = 'Android Device';
|
|
1223
|
-
}
|
|
1009
|
+
// Ignore
|
|
1224
1010
|
}
|
|
1225
1011
|
|
|
1226
|
-
// Get
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1230
|
-
} catch {
|
|
1231
|
-
timezone = undefined;
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1012
|
+
// Get native info
|
|
1013
|
+
const nativeModule = getRejourneyNativeModule();
|
|
1014
|
+
let nativeInfo: any = {};
|
|
1234
1015
|
|
|
1235
|
-
|
|
1236
|
-
if (!locale) {
|
|
1016
|
+
if (nativeModule && nativeModule.getDeviceInfo) {
|
|
1237
1017
|
try {
|
|
1238
|
-
|
|
1239
|
-
} catch {
|
|
1240
|
-
|
|
1018
|
+
nativeInfo = await nativeModule.getDeviceInfo();
|
|
1019
|
+
} catch (e) {
|
|
1020
|
+
if (__DEV__) {
|
|
1021
|
+
console.warn('[Rejourney] Failed to get native device info:', e);
|
|
1022
|
+
}
|
|
1241
1023
|
}
|
|
1242
1024
|
}
|
|
1243
1025
|
|
|
1244
1026
|
return {
|
|
1245
|
-
model,
|
|
1246
|
-
manufacturer,
|
|
1027
|
+
model: nativeInfo.model || 'Unknown',
|
|
1028
|
+
manufacturer: nativeInfo.brand,
|
|
1247
1029
|
os: (Platform?.OS || 'ios') as 'ios' | 'android',
|
|
1248
|
-
osVersion,
|
|
1030
|
+
osVersion: nativeInfo.systemVersion || Platform?.Version?.toString() || 'Unknown',
|
|
1249
1031
|
screenWidth: Math.round(width),
|
|
1250
1032
|
screenHeight: Math.round(height),
|
|
1251
1033
|
pixelRatio: scale,
|
|
1252
|
-
appVersion,
|
|
1253
|
-
appId,
|
|
1254
|
-
locale,
|
|
1255
|
-
timezone,
|
|
1034
|
+
appVersion: nativeInfo.appVersion,
|
|
1035
|
+
appId: nativeInfo.bundleId,
|
|
1036
|
+
locale: locale,
|
|
1037
|
+
timezone: timezone,
|
|
1256
1038
|
};
|
|
1257
1039
|
}
|
|
1258
1040
|
|
|
1259
|
-
// =============================================================================
|
|
1260
|
-
// Anonymous ID Generation
|
|
1261
|
-
// =============================================================================
|
|
1262
|
-
|
|
1263
|
-
// Storage key for anonymous ID
|
|
1264
|
-
const ANONYMOUS_ID_KEY = '@rejourney_anonymous_id';
|
|
1265
|
-
|
|
1266
1041
|
/**
|
|
1267
1042
|
* Generate a persistent anonymous ID
|
|
1268
1043
|
*/
|
|
@@ -1272,40 +1047,12 @@ function generateAnonymousId(): string {
|
|
|
1272
1047
|
return `anon_${timestamp}_${random}`;
|
|
1273
1048
|
}
|
|
1274
1049
|
|
|
1275
|
-
/**
|
|
1276
|
-
* Initialize anonymous ID - tries to load from storage, generates new if not found
|
|
1277
|
-
* This is called internally and runs asynchronously
|
|
1278
|
-
*/
|
|
1279
|
-
async function initAnonymousId(): Promise<void> {
|
|
1280
|
-
try {
|
|
1281
|
-
// Try to load from AsyncStorage
|
|
1282
|
-
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
|
|
1283
|
-
const storedId = await AsyncStorage.getItem(ANONYMOUS_ID_KEY);
|
|
1284
|
-
|
|
1285
|
-
if (storedId) {
|
|
1286
|
-
anonymousId = storedId;
|
|
1287
|
-
} else {
|
|
1288
|
-
// Generate new ID and persist
|
|
1289
|
-
anonymousId = generateAnonymousId();
|
|
1290
|
-
await AsyncStorage.setItem(ANONYMOUS_ID_KEY, anonymousId);
|
|
1291
|
-
}
|
|
1292
|
-
} catch {
|
|
1293
|
-
// AsyncStorage not available or error - just generate without persistence
|
|
1294
|
-
if (!anonymousId) {
|
|
1295
|
-
anonymousId = generateAnonymousId();
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
1050
|
/**
|
|
1301
1051
|
* Get the anonymous ID (synchronous - returns generated ID immediately)
|
|
1302
|
-
* For persistent ID, call initAnonymousId() first
|
|
1303
1052
|
*/
|
|
1304
1053
|
export function getAnonymousId(): string {
|
|
1305
1054
|
if (!anonymousId) {
|
|
1306
1055
|
anonymousId = generateAnonymousId();
|
|
1307
|
-
// Try to persist asynchronously (fire and forget)
|
|
1308
|
-
initAnonymousId().catch(() => { });
|
|
1309
1056
|
}
|
|
1310
1057
|
return anonymousId;
|
|
1311
1058
|
}
|
|
@@ -1318,11 +1065,9 @@ export async function ensurePersistentAnonymousId(): Promise<string> {
|
|
|
1318
1065
|
if (anonymousId) return anonymousId;
|
|
1319
1066
|
if (!anonymousIdPromise) {
|
|
1320
1067
|
anonymousIdPromise = (async () => {
|
|
1321
|
-
await
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
}
|
|
1325
|
-
return anonymousId;
|
|
1068
|
+
const id = await loadAnonymousId();
|
|
1069
|
+
anonymousId = id;
|
|
1070
|
+
return id;
|
|
1326
1071
|
})();
|
|
1327
1072
|
}
|
|
1328
1073
|
return anonymousIdPromise;
|
|
@@ -1333,28 +1078,24 @@ export async function ensurePersistentAnonymousId(): Promise<string> {
|
|
|
1333
1078
|
* Call this at app startup for best results
|
|
1334
1079
|
*/
|
|
1335
1080
|
export async function loadAnonymousId(): Promise<string> {
|
|
1336
|
-
|
|
1337
|
-
|
|
1081
|
+
const nativeModule = getRejourneyNativeModule();
|
|
1082
|
+
if (nativeModule && nativeModule.getUserIdentity) {
|
|
1083
|
+
try {
|
|
1084
|
+
return await nativeModule.getUserIdentity() || generateAnonymousId();
|
|
1085
|
+
} catch {
|
|
1086
|
+
return generateAnonymousId();
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return generateAnonymousId();
|
|
1338
1090
|
}
|
|
1339
1091
|
|
|
1340
1092
|
/**
|
|
1341
|
-
* Set a custom anonymous ID
|
|
1093
|
+
* Set a custom anonymous ID
|
|
1342
1094
|
*/
|
|
1343
1095
|
export function setAnonymousId(id: string): void {
|
|
1344
1096
|
anonymousId = id;
|
|
1345
|
-
// Try to persist asynchronously
|
|
1346
|
-
try {
|
|
1347
|
-
const AsyncStorage = require('@react-native-async-storage/async-storage').default;
|
|
1348
|
-
AsyncStorage.setItem(ANONYMOUS_ID_KEY, id).catch(() => { });
|
|
1349
|
-
} catch {
|
|
1350
|
-
// Ignore if AsyncStorage not available
|
|
1351
|
-
}
|
|
1352
1097
|
}
|
|
1353
1098
|
|
|
1354
|
-
// =============================================================================
|
|
1355
|
-
// Exports
|
|
1356
|
-
// =============================================================================
|
|
1357
|
-
|
|
1358
1099
|
export default {
|
|
1359
1100
|
init: initAutoTracking,
|
|
1360
1101
|
cleanup: cleanupAutoTracking,
|