@rejourneyco/react-native 1.0.9 → 1.0.11
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 +77 -3
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +54 -0
- package/android/src/main/java/com/rejourney/RejourneyOkHttpInitProvider.kt +68 -0
- package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +3 -0
- package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +0 -7
- package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +4 -1
- package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +3 -0
- package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +26 -0
- package/android/src/main/java/com/rejourney/utility/DataCompression.kt +14 -2
- package/ios/Engine/RejourneyImpl.swift +5 -0
- package/ios/Recording/RejourneyURLProtocol.swift +58 -10
- package/ios/Recording/ReplayOrchestrator.swift +3 -1
- package/ios/Recording/TelemetryPipeline.swift +28 -2
- package/ios/Recording/VisualCapture.swift +25 -21
- package/ios/Rejourney.h +4 -0
- package/ios/Rejourney.mm +3 -15
- package/ios/Utility/DataCompression.swift +2 -2
- package/lib/commonjs/expoRouterTracking.js +137 -0
- package/lib/commonjs/index.js +176 -19
- package/lib/commonjs/sdk/autoTracking.js +100 -89
- package/lib/module/expoRouterTracking.js +135 -0
- package/lib/module/index.js +175 -13
- package/lib/module/sdk/autoTracking.js +98 -89
- package/lib/typescript/expoRouterTracking.d.ts +14 -0
- package/lib/typescript/index.d.ts +2 -2
- package/lib/typescript/sdk/autoTracking.d.ts +11 -0
- package/lib/typescript/types/index.d.ts +42 -3
- package/package.json +22 -2
- package/rejourney.podspec +11 -2
- package/src/expoRouterTracking.ts +167 -0
- package/src/index.ts +184 -16
- package/src/sdk/autoTracking.ts +110 -103
- package/src/types/index.ts +43 -3
|
@@ -25,6 +25,8 @@ export interface RejourneyConfig {
|
|
|
25
25
|
maxStorageSize?: number;
|
|
26
26
|
/** Enable automatic screen name detection with React Navigation (default: true) */
|
|
27
27
|
autoScreenTracking?: boolean;
|
|
28
|
+
/** Enable automatic screen name detection with Expo Router (default: true) */
|
|
29
|
+
autoTrackExpoRouter?: boolean;
|
|
28
30
|
/** Enable automatic gesture detection (default: true) */
|
|
29
31
|
autoGestureTracking?: boolean;
|
|
30
32
|
/** Enable privacy occlusion for text inputs (default: true) */
|
|
@@ -433,7 +435,17 @@ export interface RejourneyNativeModule {
|
|
|
433
435
|
export interface RejourneyAPI {
|
|
434
436
|
/** SDK version */
|
|
435
437
|
readonly version: string;
|
|
436
|
-
/**
|
|
438
|
+
/**
|
|
439
|
+
* Initialize Rejourney SDK
|
|
440
|
+
* @param publicRouteKey - Your public route key from the Rejourney dashboard
|
|
441
|
+
* @param options - Optional configuration options
|
|
442
|
+
*/
|
|
443
|
+
init(publicRouteKey: string, options?: Omit<RejourneyConfig, 'publicRouteKey'>): void;
|
|
444
|
+
/** Start recording (call after user consent) */
|
|
445
|
+
start(): void;
|
|
446
|
+
/** Stop recording */
|
|
447
|
+
stop(): void;
|
|
448
|
+
/** Internal method to start recording session (called by start() / startRejourney()) */
|
|
437
449
|
_startSession(): Promise<boolean>;
|
|
438
450
|
/** Internal method to stop recording session (called by stopRejourney) */
|
|
439
451
|
_stopSession(): Promise<void>;
|
|
@@ -443,8 +455,18 @@ export interface RejourneyAPI {
|
|
|
443
455
|
setUserIdentity(userId: string): void;
|
|
444
456
|
/** Clear user identity */
|
|
445
457
|
clearUserIdentity(): void;
|
|
446
|
-
/**
|
|
447
|
-
|
|
458
|
+
/**
|
|
459
|
+
* Set custom session metadata.
|
|
460
|
+
* Can be called with a single key-value pair or an object of properties.
|
|
461
|
+
* Useful for filtering sessions later (e.g., plan: 'premium', role: 'admin').
|
|
462
|
+
* Caps at 100 properties per session.
|
|
463
|
+
*
|
|
464
|
+
* @param keyOrProperties Property name string, or an object containing key-value pairs
|
|
465
|
+
* @param value Property value (if first argument is a string)
|
|
466
|
+
*/
|
|
467
|
+
setMetadata(keyOrProperties: string | Record<string, string | number | boolean>, value?: string | number | boolean): void;
|
|
468
|
+
/** Track current screen (manual) */
|
|
469
|
+
trackScreen(screenName: string, params?: Record<string, unknown>): void;
|
|
448
470
|
/** Mark a view as sensitive (will be occluded in recording) */
|
|
449
471
|
setOccluded(viewRef: {
|
|
450
472
|
current: any;
|
|
@@ -540,6 +562,21 @@ export interface RejourneyAPI {
|
|
|
540
562
|
* @param nativeID - The nativeID prop of the view to unmask
|
|
541
563
|
*/
|
|
542
564
|
unmaskView(nativeID: string): void;
|
|
565
|
+
/**
|
|
566
|
+
* Hook for automatic React Navigation tracking.
|
|
567
|
+
* Pass the returned object to your NavigationContainer props.
|
|
568
|
+
*
|
|
569
|
+
* @example
|
|
570
|
+
* ```tsx
|
|
571
|
+
* const navigationTracking = Rejourney.useNavigationTracking();
|
|
572
|
+
* <NavigationContainer {...navigationTracking}>
|
|
573
|
+
* ```
|
|
574
|
+
*/
|
|
575
|
+
useNavigationTracking(): {
|
|
576
|
+
ref: any;
|
|
577
|
+
onReady: () => void;
|
|
578
|
+
onStateChange: (state: any) => void;
|
|
579
|
+
};
|
|
543
580
|
}
|
|
544
581
|
/**
|
|
545
582
|
* SDK telemetry metrics for observability
|
|
@@ -607,6 +644,8 @@ export interface UseRejourneyResult {
|
|
|
607
644
|
stopRecording: () => Promise<void>;
|
|
608
645
|
/** Log custom event */
|
|
609
646
|
logEvent: (name: string, properties?: Record<string, unknown>) => void;
|
|
647
|
+
/** Set custom session metadata */
|
|
648
|
+
setMetadata: (keyOrProperties: string | Record<string, string | number | boolean>, value?: string | number | boolean) => void;
|
|
610
649
|
/** Error if any */
|
|
611
650
|
error: Error | null;
|
|
612
651
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rejourneyco/react-native",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "Rejourney Session Recording SDK for React Native",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -92,6 +92,26 @@
|
|
|
92
92
|
"@react-navigation/native": ">=6.0.0",
|
|
93
93
|
"expo-router": ">=3.0.0"
|
|
94
94
|
},
|
|
95
|
+
"peerDependenciesMeta": {
|
|
96
|
+
"expo-router": {
|
|
97
|
+
"optional": true
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"exports": {
|
|
101
|
+
".": {
|
|
102
|
+
"types": "./lib/typescript/index.d.ts",
|
|
103
|
+
"react-native": "./lib/module/index.js",
|
|
104
|
+
"import": "./lib/module/index.js",
|
|
105
|
+
"require": "./lib/commonjs/index.js"
|
|
106
|
+
},
|
|
107
|
+
"./expo-router": {
|
|
108
|
+
"types": "./lib/typescript/expoRouterTracking.d.ts",
|
|
109
|
+
"react-native": "./lib/module/expoRouterTracking.js",
|
|
110
|
+
"import": "./lib/module/expoRouterTracking.js",
|
|
111
|
+
"require": "./lib/commonjs/expoRouterTracking.js",
|
|
112
|
+
"default": "./lib/module/expoRouterTracking.js"
|
|
113
|
+
}
|
|
114
|
+
},
|
|
95
115
|
"codegenConfig": {
|
|
96
116
|
"name": "RejourneySpec",
|
|
97
117
|
"type": "modules",
|
|
@@ -119,4 +139,4 @@
|
|
|
119
139
|
]
|
|
120
140
|
]
|
|
121
141
|
}
|
|
122
|
-
}
|
|
142
|
+
}
|
package/rejourney.podspec
CHANGED
|
@@ -18,6 +18,15 @@ Pod::Spec.new do |s|
|
|
|
18
18
|
s.exclude_files = "ios/build/**/*"
|
|
19
19
|
s.library = "z"
|
|
20
20
|
|
|
21
|
-
#
|
|
22
|
-
|
|
21
|
+
# React Native core dependencies so headers like `React/RCTBridgeModule.h`
|
|
22
|
+
# are always available, regardless of React Native version or architecture.
|
|
23
|
+
# On modern React Native, `React-Core` is the canonical dependency.
|
|
24
|
+
s.dependency "React-Core"
|
|
25
|
+
s.dependency "ReactCommon/turbomodule/core"
|
|
26
|
+
|
|
27
|
+
# New Architecture / Codegen integration (RN 0.71+). On older RN versions
|
|
28
|
+
# this helper is not defined, so we guard it.
|
|
29
|
+
if respond_to?(:install_modules_dependencies)
|
|
30
|
+
install_modules_dependencies(s)
|
|
31
|
+
end
|
|
23
32
|
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional Expo Router integration for @rejourneyco/react-native
|
|
3
|
+
*
|
|
4
|
+
* This file is only loaded when you import '@rejourneyco/react-native/expo-router'.
|
|
5
|
+
* It contains require('expo-router') and related subpaths. Metro bundles require()
|
|
6
|
+
* at build time, so keeping this in a separate entry ensures apps that use
|
|
7
|
+
* Expo with react-navigation (without expo-router) never pull in expo-router
|
|
8
|
+
* and avoid "Requiring unknown module" crashes.
|
|
9
|
+
*
|
|
10
|
+
* If you use expo-router, add this once (e.g. in your root _layout.tsx):
|
|
11
|
+
* import '@rejourneyco/react-native/expo-router';
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { trackScreen, setExpoRouterPollingInterval, isExpoRouterTrackingEnabled } from './sdk/autoTracking';
|
|
15
|
+
import { normalizeScreenName, getScreenNameFromPath } from './sdk/navigation';
|
|
16
|
+
|
|
17
|
+
const MAX_POLLING_ERRORS = 10;
|
|
18
|
+
|
|
19
|
+
function extractScreenNameFromRouterState(
|
|
20
|
+
state: any,
|
|
21
|
+
getScreenNameFromPathFn: (path: string, segments: string[]) => string,
|
|
22
|
+
normalizeScreenNameFn: (name: string) => string,
|
|
23
|
+
accumulatedSegments: string[] = []
|
|
24
|
+
): string | null {
|
|
25
|
+
if (!state?.routes) return null;
|
|
26
|
+
|
|
27
|
+
const route = state.routes[state.index ?? state.routes.length - 1];
|
|
28
|
+
if (!route) return null;
|
|
29
|
+
|
|
30
|
+
const newSegments = [...accumulatedSegments, route.name];
|
|
31
|
+
|
|
32
|
+
if (route.state) {
|
|
33
|
+
return extractScreenNameFromRouterState(
|
|
34
|
+
route.state,
|
|
35
|
+
getScreenNameFromPathFn,
|
|
36
|
+
normalizeScreenNameFn,
|
|
37
|
+
newSegments
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cleanSegments = newSegments.filter((s) => !s.startsWith('(') && !s.endsWith(')'));
|
|
42
|
+
|
|
43
|
+
if (cleanSegments.length === 0) {
|
|
44
|
+
for (let i = newSegments.length - 1; i >= 0; i--) {
|
|
45
|
+
const seg = newSegments[i];
|
|
46
|
+
if (seg && !seg.startsWith('(') && !seg.endsWith(')')) {
|
|
47
|
+
cleanSegments.push(seg);
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const pathname = '/' + cleanSegments.join('/');
|
|
54
|
+
return getScreenNameFromPathFn(pathname, newSegments);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function setupExpoRouterPolling(): void {
|
|
58
|
+
let lastDetectedScreen = '';
|
|
59
|
+
let pollingErrors = 0;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const EXPO_ROUTER = 'expo-router';
|
|
63
|
+
const expoRouter = require(EXPO_ROUTER);
|
|
64
|
+
const router = expoRouter.router;
|
|
65
|
+
|
|
66
|
+
if (!router) {
|
|
67
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
68
|
+
console.debug('[Rejourney] Expo Router: router object not found');
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const intervalId = setInterval(() => {
|
|
74
|
+
try {
|
|
75
|
+
let state: any = null;
|
|
76
|
+
if (typeof router.getState === 'function') {
|
|
77
|
+
state = router.getState();
|
|
78
|
+
} else if ((router as any).rootState) {
|
|
79
|
+
state = (router as any).rootState;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!state) {
|
|
83
|
+
try {
|
|
84
|
+
const STORE_PATH = 'expo-router/build/global-state/router-store';
|
|
85
|
+
const storeModule = require(STORE_PATH);
|
|
86
|
+
if (storeModule?.store) {
|
|
87
|
+
state = storeModule.store.state;
|
|
88
|
+
if (!state && storeModule.store.navigationRef?.current) {
|
|
89
|
+
state = storeModule.store.navigationRef.current.getRootState?.();
|
|
90
|
+
}
|
|
91
|
+
if (!state) {
|
|
92
|
+
state = storeModule.store.rootState || storeModule.store.initialState;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// Ignore
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!state) {
|
|
101
|
+
try {
|
|
102
|
+
const IMPERATIVE_PATH = 'expo-router/build/imperative-api';
|
|
103
|
+
const imperative = require(IMPERATIVE_PATH);
|
|
104
|
+
if (imperative?.router) {
|
|
105
|
+
state = imperative.router.getState?.();
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// Ignore
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (state) {
|
|
113
|
+
pollingErrors = 0;
|
|
114
|
+
const screenName = extractScreenNameFromRouterState(
|
|
115
|
+
state,
|
|
116
|
+
getScreenNameFromPath,
|
|
117
|
+
normalizeScreenName
|
|
118
|
+
);
|
|
119
|
+
if (screenName && screenName !== lastDetectedScreen) {
|
|
120
|
+
lastDetectedScreen = screenName;
|
|
121
|
+
trackScreen(screenName);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
pollingErrors++;
|
|
125
|
+
if (pollingErrors >= MAX_POLLING_ERRORS) {
|
|
126
|
+
clearInterval(intervalId);
|
|
127
|
+
setExpoRouterPollingInterval(null);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
pollingErrors++;
|
|
132
|
+
if (pollingErrors >= MAX_POLLING_ERRORS) {
|
|
133
|
+
clearInterval(intervalId);
|
|
134
|
+
setExpoRouterPollingInterval(null);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}, 500);
|
|
138
|
+
|
|
139
|
+
setExpoRouterPollingInterval(intervalId);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
142
|
+
console.debug('[Rejourney] Expo Router not available:', e);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let attempts = 0;
|
|
148
|
+
const maxAttempts = 5;
|
|
149
|
+
|
|
150
|
+
function trySetup(): void {
|
|
151
|
+
attempts++;
|
|
152
|
+
try {
|
|
153
|
+
const EXPO_ROUTER = 'expo-router';
|
|
154
|
+
const expoRouter = require(EXPO_ROUTER);
|
|
155
|
+
if (expoRouter?.router && isExpoRouterTrackingEnabled()) {
|
|
156
|
+
setupExpoRouterPolling();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
// Not ready or not installed
|
|
161
|
+
}
|
|
162
|
+
if (attempts < maxAttempts) {
|
|
163
|
+
setTimeout(trySetup, 200 * attempts);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
setTimeout(trySetup, 200);
|
package/src/index.ts
CHANGED
|
@@ -171,6 +171,7 @@ let _autoTracking: {
|
|
|
171
171
|
resetMetrics: typeof import('./sdk/autoTracking').resetMetrics;
|
|
172
172
|
collectDeviceInfo: typeof import('./sdk/autoTracking').collectDeviceInfo;
|
|
173
173
|
ensurePersistentAnonymousId: typeof import('./sdk/autoTracking').ensurePersistentAnonymousId;
|
|
174
|
+
useNavigationTracking: typeof import('./sdk/autoTracking').useNavigationTracking;
|
|
174
175
|
} | null = null;
|
|
175
176
|
|
|
176
177
|
// No-op auto tracking for when SDK is disabled
|
|
@@ -185,6 +186,7 @@ const noopAutoTracking = {
|
|
|
185
186
|
resetMetrics: () => { },
|
|
186
187
|
collectDeviceInfo: async () => ({} as any),
|
|
187
188
|
ensurePersistentAnonymousId: async () => 'anonymous',
|
|
189
|
+
useNavigationTracking: () => ({ ref: null, onReady: () => { }, onStateChange: () => { } }),
|
|
188
190
|
};
|
|
189
191
|
|
|
190
192
|
function getAutoTracking() {
|
|
@@ -209,6 +211,11 @@ let _appStateSubscription: { remove: () => void } | null = null;
|
|
|
209
211
|
let _authErrorSubscription: { remove: () => void } | null = null;
|
|
210
212
|
let _currentAppState: string = 'active'; // Default to active, will be updated on init
|
|
211
213
|
let _userIdentity: string | null = null;
|
|
214
|
+
let _backgroundEntryTime: number | null = null; // Track when app went to background
|
|
215
|
+
let _storedMetadata: Record<string, string | number | boolean> = {}; // Accumulate metadata for session rollover
|
|
216
|
+
|
|
217
|
+
// Session timeout - must match native side (60 seconds)
|
|
218
|
+
const SESSION_TIMEOUT_MS = 60_000;
|
|
212
219
|
|
|
213
220
|
// Scroll throttling - reduce native bridge calls from 60fps to at most 10/sec
|
|
214
221
|
let _lastScrollTime: number = 0;
|
|
@@ -481,7 +488,7 @@ function safeNativeCallSync<T>(
|
|
|
481
488
|
/**
|
|
482
489
|
* Main Rejourney API (Internal)
|
|
483
490
|
*/
|
|
484
|
-
const Rejourney: RejourneyAPI = {
|
|
491
|
+
export const Rejourney: RejourneyAPI = {
|
|
485
492
|
/**
|
|
486
493
|
* SDK Version
|
|
487
494
|
*/
|
|
@@ -637,6 +644,7 @@ const Rejourney: RejourneyAPI = {
|
|
|
637
644
|
trackReactNativeErrors: true,
|
|
638
645
|
trackConsoleLogs: _storedConfig?.trackConsoleLogs ?? true,
|
|
639
646
|
collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false,
|
|
647
|
+
autoTrackExpoRouter: _storedConfig?.autoTrackExpoRouter !== false,
|
|
640
648
|
},
|
|
641
649
|
{
|
|
642
650
|
// Rage tap callback - log as frustration event
|
|
@@ -680,6 +688,8 @@ const Rejourney: RejourneyAPI = {
|
|
|
680
688
|
'/api/ingest/presign',
|
|
681
689
|
'/api/ingest/batch/complete',
|
|
682
690
|
'/api/ingest/session/end',
|
|
691
|
+
'/api/ingest/segment/presign',
|
|
692
|
+
'/api/ingest/segment/complete',
|
|
683
693
|
...(_storedConfig?.networkIgnoreUrls || []),
|
|
684
694
|
];
|
|
685
695
|
|
|
@@ -806,22 +816,56 @@ const Rejourney: RejourneyAPI = {
|
|
|
806
816
|
},
|
|
807
817
|
|
|
808
818
|
/**
|
|
809
|
-
|
|
819
|
+
/**
|
|
820
|
+
* Set custom session metadata.
|
|
821
|
+
* Can be called with a single key-value pair or an object of properties.
|
|
822
|
+
* Useful for filtering sessions later (e.g., plan: 'premium', role: 'admin').
|
|
823
|
+
* Caps at 100 properties per session.
|
|
824
|
+
*
|
|
825
|
+
* @param keyOrProperties Property name string, or an object containing key-value pairs
|
|
826
|
+
* @param value Property value (if first argument is a string)
|
|
827
|
+
*/
|
|
828
|
+
setMetadata(keyOrProperties: string | Record<string, string | number | boolean>, value?: string | number | boolean): void {
|
|
829
|
+
if (typeof keyOrProperties === 'string') {
|
|
830
|
+
const key = keyOrProperties;
|
|
831
|
+
if (!key) {
|
|
832
|
+
getLogger().warn('setMetadata requires a non-empty string key');
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (value !== undefined && typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
|
|
836
|
+
getLogger().warn('setMetadata value must be a string, number, or boolean when using a key string');
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
this.logEvent('$user_property', { key, value });
|
|
840
|
+
// Track for session rollover restoration
|
|
841
|
+
_storedMetadata[key] = value!;
|
|
842
|
+
} else if (keyOrProperties && typeof keyOrProperties === 'object') {
|
|
843
|
+
const properties = keyOrProperties;
|
|
844
|
+
const validProps: Record<string, any> = {};
|
|
845
|
+
for (const [k, v] of Object.entries(properties)) {
|
|
846
|
+
if (typeof k === 'string' && k &&
|
|
847
|
+
(typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')) {
|
|
848
|
+
validProps[k] = v;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (Object.keys(validProps).length > 0) {
|
|
852
|
+
this.logEvent('$user_property', validProps);
|
|
853
|
+
// Track for session rollover restoration
|
|
854
|
+
Object.assign(_storedMetadata, validProps);
|
|
855
|
+
}
|
|
856
|
+
} else {
|
|
857
|
+
getLogger().warn('setMetadata requires a string key and value, or a properties object');
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Track current screen (manual)
|
|
810
863
|
*
|
|
811
864
|
* @param screenName - Screen name
|
|
812
865
|
* @param params - Optional screen parameters
|
|
813
866
|
*/
|
|
814
|
-
|
|
867
|
+
trackScreen(screenName: string, _params?: Record<string, unknown>): void {
|
|
815
868
|
getAutoTracking().trackScreen(screenName);
|
|
816
|
-
getAutoTracking().notifyStateChange();
|
|
817
|
-
|
|
818
|
-
safeNativeCallSync(
|
|
819
|
-
'tagScreen',
|
|
820
|
-
() => {
|
|
821
|
-
getRejourneyNative()!.screenChanged(screenName).catch(() => { });
|
|
822
|
-
},
|
|
823
|
-
undefined
|
|
824
|
-
);
|
|
825
869
|
},
|
|
826
870
|
|
|
827
871
|
/**
|
|
@@ -1271,12 +1315,121 @@ const Rejourney: RejourneyAPI = {
|
|
|
1271
1315
|
undefined
|
|
1272
1316
|
);
|
|
1273
1317
|
},
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* Initialize Rejourney SDK
|
|
1321
|
+
*/
|
|
1322
|
+
init(publicRouteKey: string, options?: Omit<RejourneyConfig, 'publicRouteKey'>): void {
|
|
1323
|
+
initRejourney(publicRouteKey, options);
|
|
1324
|
+
},
|
|
1325
|
+
|
|
1326
|
+
/**
|
|
1327
|
+
* Start recording
|
|
1328
|
+
*/
|
|
1329
|
+
start(): void {
|
|
1330
|
+
startRejourney();
|
|
1331
|
+
},
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Stop recording
|
|
1335
|
+
*/
|
|
1336
|
+
stop(): void {
|
|
1337
|
+
stopRejourney();
|
|
1338
|
+
},
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* Hook for automatic React Navigation tracking.
|
|
1342
|
+
*/
|
|
1343
|
+
useNavigationTracking() {
|
|
1344
|
+
return getAutoTracking().useNavigationTracking();
|
|
1345
|
+
},
|
|
1274
1346
|
};
|
|
1275
1347
|
|
|
1348
|
+
/**
|
|
1349
|
+
* Reinitialize JS-side auto-tracking for a new session after background timeout.
|
|
1350
|
+
*
|
|
1351
|
+
* When the app was in background for >60s the native layer rolls over to a
|
|
1352
|
+
* fresh session automatically. The JS side must tear down stale tracking
|
|
1353
|
+
* state (metrics, console-log counter, screen history, error handlers) and
|
|
1354
|
+
* re-initialize so that trackScreen, logEvent, setMetadata, etc. work
|
|
1355
|
+
* correctly against the new native session.
|
|
1356
|
+
*/
|
|
1357
|
+
function _reinitAutoTrackingForNewSession(): void {
|
|
1358
|
+
try {
|
|
1359
|
+
// 1. Tear down old session's auto-tracking state
|
|
1360
|
+
getAutoTracking().cleanupAutoTracking();
|
|
1361
|
+
|
|
1362
|
+
// 2. Re-initialize auto-tracking with the same config
|
|
1363
|
+
getAutoTracking().initAutoTracking(
|
|
1364
|
+
{
|
|
1365
|
+
rageTapThreshold: _storedConfig?.rageTapThreshold ?? 3,
|
|
1366
|
+
rageTapTimeWindow: _storedConfig?.rageTapTimeWindow ?? 500,
|
|
1367
|
+
rageTapRadius: 50,
|
|
1368
|
+
trackJSErrors: true,
|
|
1369
|
+
trackPromiseRejections: true,
|
|
1370
|
+
trackReactNativeErrors: true,
|
|
1371
|
+
trackConsoleLogs: _storedConfig?.trackConsoleLogs ?? true,
|
|
1372
|
+
collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false,
|
|
1373
|
+
autoTrackExpoRouter: _storedConfig?.autoTrackExpoRouter !== false,
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
onRageTap: (count: number, x: number, y: number) => {
|
|
1377
|
+
Rejourney.logEvent('frustration', {
|
|
1378
|
+
frustrationKind: 'rage_tap',
|
|
1379
|
+
tapCount: count,
|
|
1380
|
+
x,
|
|
1381
|
+
y,
|
|
1382
|
+
});
|
|
1383
|
+
getLogger().logFrustration(`Rage tap (${count} taps)`);
|
|
1384
|
+
},
|
|
1385
|
+
onError: (error: { message: string; stack?: string; name?: string }) => {
|
|
1386
|
+
getLogger().logError(error.message);
|
|
1387
|
+
},
|
|
1388
|
+
onScreen: (_screenName: string, _previousScreen?: string) => {
|
|
1389
|
+
},
|
|
1390
|
+
}
|
|
1391
|
+
);
|
|
1392
|
+
|
|
1393
|
+
// 3. Re-collect device info for the new session
|
|
1394
|
+
if (_storedConfig?.collectDeviceInfo !== false) {
|
|
1395
|
+
getAutoTracking().collectDeviceInfo().then((deviceInfo) => {
|
|
1396
|
+
Rejourney.logEvent('device_info', deviceInfo as unknown as Record<string, unknown>);
|
|
1397
|
+
}).catch(() => { });
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// 4. Re-send user identity to the new native session
|
|
1401
|
+
if (_userIdentity) {
|
|
1402
|
+
safeNativeCallSync(
|
|
1403
|
+
'setUserIdentity',
|
|
1404
|
+
() => {
|
|
1405
|
+
getRejourneyNative()!.setUserIdentity(_userIdentity!).catch(() => { });
|
|
1406
|
+
},
|
|
1407
|
+
undefined
|
|
1408
|
+
);
|
|
1409
|
+
getLogger().debug(`Restored user identity '${_userIdentity}' to new session`);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// 5. Re-send any stored metadata to the new native session
|
|
1413
|
+
if (Object.keys(_storedMetadata).length > 0) {
|
|
1414
|
+
for (const [key, value] of Object.entries(_storedMetadata)) {
|
|
1415
|
+
if (value !== undefined && value !== null) {
|
|
1416
|
+
Rejourney.setMetadata(key, value);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
getLogger().debug('Restored metadata to new session');
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
getLogger().logLifecycleEvent('JS auto-tracking reinitialized for new session');
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
getLogger().warn('Failed to reinitialize auto-tracking after session rollover:', error);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1276
1428
|
/**
|
|
1277
1429
|
* Handle app state changes for automatic session management
|
|
1278
1430
|
* - Pauses recording when app goes to background
|
|
1279
1431
|
* - Resumes recording when app comes back to foreground
|
|
1432
|
+
* - Reinitializes JS-side auto-tracking when native rolls over to a new session
|
|
1280
1433
|
* - Cleans up properly when app is terminated
|
|
1281
1434
|
*/
|
|
1282
1435
|
function handleAppStateChange(nextAppState: string): void {
|
|
@@ -1286,9 +1439,24 @@ function handleAppStateChange(nextAppState: string): void {
|
|
|
1286
1439
|
if (_currentAppState.match(/active/) && nextAppState === 'background') {
|
|
1287
1440
|
// App going to background - native module handles this automatically
|
|
1288
1441
|
getLogger().logLifecycleEvent('App moving to background');
|
|
1442
|
+
_backgroundEntryTime = Date.now();
|
|
1289
1443
|
} else if (_currentAppState.match(/inactive|background/) && nextAppState === 'active') {
|
|
1290
1444
|
// App coming back to foreground
|
|
1291
1445
|
getLogger().logLifecycleEvent('App returning to foreground');
|
|
1446
|
+
|
|
1447
|
+
// Check if we exceeded the session timeout (60s).
|
|
1448
|
+
// Native side will have already ended the old session and started a new
|
|
1449
|
+
// one — we need to reset JS-side auto-tracking state to match.
|
|
1450
|
+
if (_backgroundEntryTime && _isRecording) {
|
|
1451
|
+
const backgroundDurationMs = Date.now() - _backgroundEntryTime;
|
|
1452
|
+
if (backgroundDurationMs > SESSION_TIMEOUT_MS) {
|
|
1453
|
+
getLogger().debug(
|
|
1454
|
+
`Session rollover: background ${Math.round(backgroundDurationMs / 1000)}s > ${SESSION_TIMEOUT_MS / 1000}s timeout`
|
|
1455
|
+
);
|
|
1456
|
+
_reinitAutoTrackingForNewSession();
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
_backgroundEntryTime = null;
|
|
1292
1460
|
}
|
|
1293
1461
|
_currentAppState = nextAppState;
|
|
1294
1462
|
} catch (error) {
|
|
@@ -1371,8 +1539,10 @@ function setupAuthErrorListener(): void {
|
|
|
1371
1539
|
}
|
|
1372
1540
|
);
|
|
1373
1541
|
}
|
|
1374
|
-
} catch
|
|
1375
|
-
|
|
1542
|
+
} catch {
|
|
1543
|
+
// Expected on some architectures where NativeEventEmitter isn't fully supported.
|
|
1544
|
+
// Auth errors are still handled synchronously via native callback — this listener
|
|
1545
|
+
// is purely supplementary. No need to log.
|
|
1376
1546
|
}
|
|
1377
1547
|
}
|
|
1378
1548
|
|
|
@@ -1518,7 +1688,6 @@ export function stopRejourney(): void {
|
|
|
1518
1688
|
getLogger().warn('Error stopping Rejourney:', error);
|
|
1519
1689
|
}
|
|
1520
1690
|
}
|
|
1521
|
-
|
|
1522
1691
|
export default Rejourney;
|
|
1523
1692
|
|
|
1524
1693
|
export * from './types';
|
|
@@ -1528,7 +1697,6 @@ export {
|
|
|
1528
1697
|
trackScroll,
|
|
1529
1698
|
trackGesture,
|
|
1530
1699
|
trackInput,
|
|
1531
|
-
trackScreen,
|
|
1532
1700
|
captureError,
|
|
1533
1701
|
getSessionMetrics,
|
|
1534
1702
|
} from './sdk/autoTracking';
|