@rejourneyco/react-native 1.0.9 → 1.0.10
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/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/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
|
@@ -26,8 +26,8 @@ extension Data {
|
|
|
26
26
|
stream.avail_in = uint(self.count)
|
|
27
27
|
stream.total_out = 0
|
|
28
28
|
|
|
29
|
-
// MAX_WBITS + 16 = gzip format
|
|
30
|
-
if deflateInit2_(&stream,
|
|
29
|
+
// MAX_WBITS + 16 = gzip format; level 9 for best ratio (smaller S3 payloads)
|
|
30
|
+
if deflateInit2_(&stream, 9, Z_DEFLATED, MAX_WBITS + 16, 8, Z_DEFAULT_STRATEGY, ZLIB_VERSION, Int32(MemoryLayout<z_stream>.size)) != Z_OK {
|
|
31
31
|
return nil
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _autoTracking = require("./sdk/autoTracking");
|
|
4
|
+
var _navigation = require("./sdk/navigation");
|
|
5
|
+
/**
|
|
6
|
+
* Optional Expo Router integration for @rejourneyco/react-native
|
|
7
|
+
*
|
|
8
|
+
* This file is only loaded when you import '@rejourneyco/react-native/expo-router'.
|
|
9
|
+
* It contains require('expo-router') and related subpaths. Metro bundles require()
|
|
10
|
+
* at build time, so keeping this in a separate entry ensures apps that use
|
|
11
|
+
* Expo with react-navigation (without expo-router) never pull in expo-router
|
|
12
|
+
* and avoid "Requiring unknown module" crashes.
|
|
13
|
+
*
|
|
14
|
+
* If you use expo-router, add this once (e.g. in your root _layout.tsx):
|
|
15
|
+
* import '@rejourneyco/react-native/expo-router';
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const MAX_POLLING_ERRORS = 10;
|
|
19
|
+
function extractScreenNameFromRouterState(state, getScreenNameFromPathFn, normalizeScreenNameFn, accumulatedSegments = []) {
|
|
20
|
+
if (!state?.routes) return null;
|
|
21
|
+
const route = state.routes[state.index ?? state.routes.length - 1];
|
|
22
|
+
if (!route) return null;
|
|
23
|
+
const newSegments = [...accumulatedSegments, route.name];
|
|
24
|
+
if (route.state) {
|
|
25
|
+
return extractScreenNameFromRouterState(route.state, getScreenNameFromPathFn, normalizeScreenNameFn, newSegments);
|
|
26
|
+
}
|
|
27
|
+
const cleanSegments = newSegments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
|
|
28
|
+
if (cleanSegments.length === 0) {
|
|
29
|
+
for (let i = newSegments.length - 1; i >= 0; i--) {
|
|
30
|
+
const seg = newSegments[i];
|
|
31
|
+
if (seg && !seg.startsWith('(') && !seg.endsWith(')')) {
|
|
32
|
+
cleanSegments.push(seg);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const pathname = '/' + cleanSegments.join('/');
|
|
38
|
+
return getScreenNameFromPathFn(pathname, newSegments);
|
|
39
|
+
}
|
|
40
|
+
function setupExpoRouterPolling() {
|
|
41
|
+
let lastDetectedScreen = '';
|
|
42
|
+
let pollingErrors = 0;
|
|
43
|
+
try {
|
|
44
|
+
const EXPO_ROUTER = 'expo-router';
|
|
45
|
+
const expoRouter = require(EXPO_ROUTER);
|
|
46
|
+
const router = expoRouter.router;
|
|
47
|
+
if (!router) {
|
|
48
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
49
|
+
console.debug('[Rejourney] Expo Router: router object not found');
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const intervalId = setInterval(() => {
|
|
54
|
+
try {
|
|
55
|
+
let state = null;
|
|
56
|
+
if (typeof router.getState === 'function') {
|
|
57
|
+
state = router.getState();
|
|
58
|
+
} else if (router.rootState) {
|
|
59
|
+
state = router.rootState;
|
|
60
|
+
}
|
|
61
|
+
if (!state) {
|
|
62
|
+
try {
|
|
63
|
+
const STORE_PATH = 'expo-router/build/global-state/router-store';
|
|
64
|
+
const storeModule = require(STORE_PATH);
|
|
65
|
+
if (storeModule?.store) {
|
|
66
|
+
state = storeModule.store.state;
|
|
67
|
+
if (!state && storeModule.store.navigationRef?.current) {
|
|
68
|
+
state = storeModule.store.navigationRef.current.getRootState?.();
|
|
69
|
+
}
|
|
70
|
+
if (!state) {
|
|
71
|
+
state = storeModule.store.rootState || storeModule.store.initialState;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Ignore
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!state) {
|
|
79
|
+
try {
|
|
80
|
+
const IMPERATIVE_PATH = 'expo-router/build/imperative-api';
|
|
81
|
+
const imperative = require(IMPERATIVE_PATH);
|
|
82
|
+
if (imperative?.router) {
|
|
83
|
+
state = imperative.router.getState?.();
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Ignore
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (state) {
|
|
90
|
+
pollingErrors = 0;
|
|
91
|
+
const screenName = extractScreenNameFromRouterState(state, _navigation.getScreenNameFromPath, _navigation.normalizeScreenName);
|
|
92
|
+
if (screenName && screenName !== lastDetectedScreen) {
|
|
93
|
+
lastDetectedScreen = screenName;
|
|
94
|
+
(0, _autoTracking.trackScreen)(screenName);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
pollingErrors++;
|
|
98
|
+
if (pollingErrors >= MAX_POLLING_ERRORS) {
|
|
99
|
+
clearInterval(intervalId);
|
|
100
|
+
(0, _autoTracking.setExpoRouterPollingInterval)(null);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
pollingErrors++;
|
|
105
|
+
if (pollingErrors >= MAX_POLLING_ERRORS) {
|
|
106
|
+
clearInterval(intervalId);
|
|
107
|
+
(0, _autoTracking.setExpoRouterPollingInterval)(null);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}, 500);
|
|
111
|
+
(0, _autoTracking.setExpoRouterPollingInterval)(intervalId);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
114
|
+
console.debug('[Rejourney] Expo Router not available:', e);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
let attempts = 0;
|
|
119
|
+
const maxAttempts = 5;
|
|
120
|
+
function trySetup() {
|
|
121
|
+
attempts++;
|
|
122
|
+
try {
|
|
123
|
+
const EXPO_ROUTER = 'expo-router';
|
|
124
|
+
const expoRouter = require(EXPO_ROUTER);
|
|
125
|
+
if (expoRouter?.router && (0, _autoTracking.isExpoRouterTrackingEnabled)()) {
|
|
126
|
+
setupExpoRouterPolling();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// Not ready or not installed
|
|
131
|
+
}
|
|
132
|
+
if (attempts < maxAttempts) {
|
|
133
|
+
setTimeout(trySetup, 200 * attempts);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
setTimeout(trySetup, 200);
|
|
137
|
+
//# sourceMappingURL=expoRouterTracking.js.map
|
package/lib/commonjs/index.js
CHANGED
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
var _exportNames = {
|
|
7
|
+
Rejourney: true,
|
|
7
8
|
initRejourney: true,
|
|
8
9
|
startRejourney: true,
|
|
9
10
|
stopRejourney: true,
|
|
@@ -12,7 +13,6 @@ var _exportNames = {
|
|
|
12
13
|
trackScroll: true,
|
|
13
14
|
trackGesture: true,
|
|
14
15
|
trackInput: true,
|
|
15
|
-
trackScreen: true,
|
|
16
16
|
captureError: true,
|
|
17
17
|
getSessionMetrics: true,
|
|
18
18
|
trackNavigationState: true,
|
|
@@ -32,6 +32,7 @@ Object.defineProperty(exports, "Mask", {
|
|
|
32
32
|
return _Mask.Mask;
|
|
33
33
|
}
|
|
34
34
|
});
|
|
35
|
+
exports.Rejourney = void 0;
|
|
35
36
|
Object.defineProperty(exports, "captureError", {
|
|
36
37
|
enumerable: true,
|
|
37
38
|
get: function () {
|
|
@@ -67,12 +68,6 @@ Object.defineProperty(exports, "trackNavigationState", {
|
|
|
67
68
|
return _autoTracking2.trackNavigationState;
|
|
68
69
|
}
|
|
69
70
|
});
|
|
70
|
-
Object.defineProperty(exports, "trackScreen", {
|
|
71
|
-
enumerable: true,
|
|
72
|
-
get: function () {
|
|
73
|
-
return _autoTracking2.trackScreen;
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
71
|
Object.defineProperty(exports, "trackScroll", {
|
|
77
72
|
enumerable: true,
|
|
78
73
|
get: function () {
|
|
@@ -268,7 +263,12 @@ const noopAutoTracking = {
|
|
|
268
263
|
getSessionMetrics: () => ({}),
|
|
269
264
|
resetMetrics: () => {},
|
|
270
265
|
collectDeviceInfo: async () => ({}),
|
|
271
|
-
ensurePersistentAnonymousId: async () => 'anonymous'
|
|
266
|
+
ensurePersistentAnonymousId: async () => 'anonymous',
|
|
267
|
+
useNavigationTracking: () => ({
|
|
268
|
+
ref: null,
|
|
269
|
+
onReady: () => {},
|
|
270
|
+
onStateChange: () => {}
|
|
271
|
+
})
|
|
272
272
|
};
|
|
273
273
|
function getAutoTracking() {
|
|
274
274
|
if (_sdkDisabled) return noopAutoTracking;
|
|
@@ -291,6 +291,11 @@ let _appStateSubscription = null;
|
|
|
291
291
|
let _authErrorSubscription = null;
|
|
292
292
|
let _currentAppState = 'active'; // Default to active, will be updated on init
|
|
293
293
|
let _userIdentity = null;
|
|
294
|
+
let _backgroundEntryTime = null; // Track when app went to background
|
|
295
|
+
let _storedMetadata = {}; // Accumulate metadata for session rollover
|
|
296
|
+
|
|
297
|
+
// Session timeout - must match native side (60 seconds)
|
|
298
|
+
const SESSION_TIMEOUT_MS = 60_000;
|
|
294
299
|
|
|
295
300
|
// Scroll throttling - reduce native bridge calls from 60fps to at most 10/sec
|
|
296
301
|
let _lastScrollTime = 0;
|
|
@@ -545,7 +550,7 @@ function safeNativeCallSync(methodName, fn, defaultValue) {
|
|
|
545
550
|
/**
|
|
546
551
|
* Main Rejourney API (Internal)
|
|
547
552
|
*/
|
|
548
|
-
const Rejourney = {
|
|
553
|
+
const Rejourney = exports.Rejourney = {
|
|
549
554
|
/**
|
|
550
555
|
* SDK Version
|
|
551
556
|
*/
|
|
@@ -681,7 +686,8 @@ const Rejourney = {
|
|
|
681
686
|
trackPromiseRejections: true,
|
|
682
687
|
trackReactNativeErrors: true,
|
|
683
688
|
trackConsoleLogs: _storedConfig?.trackConsoleLogs ?? true,
|
|
684
|
-
collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false
|
|
689
|
+
collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false,
|
|
690
|
+
autoTrackExpoRouter: _storedConfig?.autoTrackExpoRouter !== false
|
|
685
691
|
}, {
|
|
686
692
|
// Rage tap callback - log as frustration event
|
|
687
693
|
onRageTap: (count, x, y) => {
|
|
@@ -714,7 +720,7 @@ const Rejourney = {
|
|
|
714
720
|
// RejourneyNetworkInterceptor on Android) are supplementary — they capture
|
|
715
721
|
// native-originated HTTP calls that bypass JS fetch(), but cannot intercept
|
|
716
722
|
// RN's own networking since it creates its NSURLSession/OkHttpClient at init time.
|
|
717
|
-
const ignoreUrls = [apiUrl, '/api/sdk/config', '/api/ingest/presign', '/api/ingest/batch/complete', '/api/ingest/session/end', ...(_storedConfig?.networkIgnoreUrls || [])];
|
|
723
|
+
const ignoreUrls = [apiUrl, '/api/sdk/config', '/api/ingest/presign', '/api/ingest/batch/complete', '/api/ingest/session/end', '/api/ingest/segment/presign', '/api/ingest/segment/complete', ...(_storedConfig?.networkIgnoreUrls || [])];
|
|
718
724
|
getNetworkInterceptor().initNetworkInterceptor(request => {
|
|
719
725
|
getAutoTracking().trackAPIRequest(request.success || false, request.statusCode, request.duration || 0, request.responseBodySize || 0);
|
|
720
726
|
Rejourney.logNetworkRequest(request);
|
|
@@ -803,17 +809,57 @@ const Rejourney = {
|
|
|
803
809
|
}
|
|
804
810
|
},
|
|
805
811
|
/**
|
|
806
|
-
|
|
812
|
+
/**
|
|
813
|
+
* Set custom session metadata.
|
|
814
|
+
* Can be called with a single key-value pair or an object of properties.
|
|
815
|
+
* Useful for filtering sessions later (e.g., plan: 'premium', role: 'admin').
|
|
816
|
+
* Caps at 100 properties per session.
|
|
817
|
+
*
|
|
818
|
+
* @param keyOrProperties Property name string, or an object containing key-value pairs
|
|
819
|
+
* @param value Property value (if first argument is a string)
|
|
820
|
+
*/
|
|
821
|
+
setMetadata(keyOrProperties, value) {
|
|
822
|
+
if (typeof keyOrProperties === 'string') {
|
|
823
|
+
const key = keyOrProperties;
|
|
824
|
+
if (!key) {
|
|
825
|
+
getLogger().warn('setMetadata requires a non-empty string key');
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (value !== undefined && typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
|
|
829
|
+
getLogger().warn('setMetadata value must be a string, number, or boolean when using a key string');
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
this.logEvent('$user_property', {
|
|
833
|
+
key,
|
|
834
|
+
value
|
|
835
|
+
});
|
|
836
|
+
// Track for session rollover restoration
|
|
837
|
+
_storedMetadata[key] = value;
|
|
838
|
+
} else if (keyOrProperties && typeof keyOrProperties === 'object') {
|
|
839
|
+
const properties = keyOrProperties;
|
|
840
|
+
const validProps = {};
|
|
841
|
+
for (const [k, v] of Object.entries(properties)) {
|
|
842
|
+
if (typeof k === 'string' && k && (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')) {
|
|
843
|
+
validProps[k] = v;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (Object.keys(validProps).length > 0) {
|
|
847
|
+
this.logEvent('$user_property', validProps);
|
|
848
|
+
// Track for session rollover restoration
|
|
849
|
+
Object.assign(_storedMetadata, validProps);
|
|
850
|
+
}
|
|
851
|
+
} else {
|
|
852
|
+
getLogger().warn('setMetadata requires a string key and value, or a properties object');
|
|
853
|
+
}
|
|
854
|
+
},
|
|
855
|
+
/**
|
|
856
|
+
* Track current screen (manual)
|
|
807
857
|
*
|
|
808
858
|
* @param screenName - Screen name
|
|
809
859
|
* @param params - Optional screen parameters
|
|
810
860
|
*/
|
|
811
|
-
|
|
861
|
+
trackScreen(screenName, _params) {
|
|
812
862
|
getAutoTracking().trackScreen(screenName);
|
|
813
|
-
getAutoTracking().notifyStateChange();
|
|
814
|
-
safeNativeCallSync('tagScreen', () => {
|
|
815
|
-
getRejourneyNative().screenChanged(screenName).catch(() => {});
|
|
816
|
-
}, undefined);
|
|
817
863
|
},
|
|
818
864
|
/**
|
|
819
865
|
* Mark a view as sensitive (will be occluded in recordings)
|
|
@@ -1202,13 +1248,109 @@ const Rejourney = {
|
|
|
1202
1248
|
safeNativeCallSync('unmaskView', () => {
|
|
1203
1249
|
getRejourneyNative().unmaskViewByNativeID(nativeID).catch(() => {});
|
|
1204
1250
|
}, undefined);
|
|
1251
|
+
},
|
|
1252
|
+
/**
|
|
1253
|
+
* Initialize Rejourney SDK
|
|
1254
|
+
*/
|
|
1255
|
+
init(publicRouteKey, options) {
|
|
1256
|
+
initRejourney(publicRouteKey, options);
|
|
1257
|
+
},
|
|
1258
|
+
/**
|
|
1259
|
+
* Start recording
|
|
1260
|
+
*/
|
|
1261
|
+
start() {
|
|
1262
|
+
startRejourney();
|
|
1263
|
+
},
|
|
1264
|
+
/**
|
|
1265
|
+
* Stop recording
|
|
1266
|
+
*/
|
|
1267
|
+
stop() {
|
|
1268
|
+
stopRejourney();
|
|
1269
|
+
},
|
|
1270
|
+
/**
|
|
1271
|
+
* Hook for automatic React Navigation tracking.
|
|
1272
|
+
*/
|
|
1273
|
+
useNavigationTracking() {
|
|
1274
|
+
return getAutoTracking().useNavigationTracking();
|
|
1205
1275
|
}
|
|
1206
1276
|
};
|
|
1207
1277
|
|
|
1278
|
+
/**
|
|
1279
|
+
* Reinitialize JS-side auto-tracking for a new session after background timeout.
|
|
1280
|
+
*
|
|
1281
|
+
* When the app was in background for >60s the native layer rolls over to a
|
|
1282
|
+
* fresh session automatically. The JS side must tear down stale tracking
|
|
1283
|
+
* state (metrics, console-log counter, screen history, error handlers) and
|
|
1284
|
+
* re-initialize so that trackScreen, logEvent, setMetadata, etc. work
|
|
1285
|
+
* correctly against the new native session.
|
|
1286
|
+
*/
|
|
1287
|
+
function _reinitAutoTrackingForNewSession() {
|
|
1288
|
+
try {
|
|
1289
|
+
// 1. Tear down old session's auto-tracking state
|
|
1290
|
+
getAutoTracking().cleanupAutoTracking();
|
|
1291
|
+
|
|
1292
|
+
// 2. Re-initialize auto-tracking with the same config
|
|
1293
|
+
getAutoTracking().initAutoTracking({
|
|
1294
|
+
rageTapThreshold: _storedConfig?.rageTapThreshold ?? 3,
|
|
1295
|
+
rageTapTimeWindow: _storedConfig?.rageTapTimeWindow ?? 500,
|
|
1296
|
+
rageTapRadius: 50,
|
|
1297
|
+
trackJSErrors: true,
|
|
1298
|
+
trackPromiseRejections: true,
|
|
1299
|
+
trackReactNativeErrors: true,
|
|
1300
|
+
trackConsoleLogs: _storedConfig?.trackConsoleLogs ?? true,
|
|
1301
|
+
collectDeviceInfo: _storedConfig?.collectDeviceInfo !== false,
|
|
1302
|
+
autoTrackExpoRouter: _storedConfig?.autoTrackExpoRouter !== false
|
|
1303
|
+
}, {
|
|
1304
|
+
onRageTap: (count, x, y) => {
|
|
1305
|
+
Rejourney.logEvent('frustration', {
|
|
1306
|
+
frustrationKind: 'rage_tap',
|
|
1307
|
+
tapCount: count,
|
|
1308
|
+
x,
|
|
1309
|
+
y
|
|
1310
|
+
});
|
|
1311
|
+
getLogger().logFrustration(`Rage tap (${count} taps)`);
|
|
1312
|
+
},
|
|
1313
|
+
onError: error => {
|
|
1314
|
+
getLogger().logError(error.message);
|
|
1315
|
+
},
|
|
1316
|
+
onScreen: (_screenName, _previousScreen) => {}
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
// 3. Re-collect device info for the new session
|
|
1320
|
+
if (_storedConfig?.collectDeviceInfo !== false) {
|
|
1321
|
+
getAutoTracking().collectDeviceInfo().then(deviceInfo => {
|
|
1322
|
+
Rejourney.logEvent('device_info', deviceInfo);
|
|
1323
|
+
}).catch(() => {});
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// 4. Re-send user identity to the new native session
|
|
1327
|
+
if (_userIdentity) {
|
|
1328
|
+
safeNativeCallSync('setUserIdentity', () => {
|
|
1329
|
+
getRejourneyNative().setUserIdentity(_userIdentity).catch(() => {});
|
|
1330
|
+
}, undefined);
|
|
1331
|
+
getLogger().debug(`Restored user identity '${_userIdentity}' to new session`);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// 5. Re-send any stored metadata to the new native session
|
|
1335
|
+
if (Object.keys(_storedMetadata).length > 0) {
|
|
1336
|
+
for (const [key, value] of Object.entries(_storedMetadata)) {
|
|
1337
|
+
if (value !== undefined && value !== null) {
|
|
1338
|
+
Rejourney.setMetadata(key, value);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
getLogger().debug('Restored metadata to new session');
|
|
1342
|
+
}
|
|
1343
|
+
getLogger().logLifecycleEvent('JS auto-tracking reinitialized for new session');
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
getLogger().warn('Failed to reinitialize auto-tracking after session rollover:', error);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1208
1349
|
/**
|
|
1209
1350
|
* Handle app state changes for automatic session management
|
|
1210
1351
|
* - Pauses recording when app goes to background
|
|
1211
1352
|
* - Resumes recording when app comes back to foreground
|
|
1353
|
+
* - Reinitializes JS-side auto-tracking when native rolls over to a new session
|
|
1212
1354
|
* - Cleans up properly when app is terminated
|
|
1213
1355
|
*/
|
|
1214
1356
|
function handleAppStateChange(nextAppState) {
|
|
@@ -1217,9 +1359,22 @@ function handleAppStateChange(nextAppState) {
|
|
|
1217
1359
|
if (_currentAppState.match(/active/) && nextAppState === 'background') {
|
|
1218
1360
|
// App going to background - native module handles this automatically
|
|
1219
1361
|
getLogger().logLifecycleEvent('App moving to background');
|
|
1362
|
+
_backgroundEntryTime = Date.now();
|
|
1220
1363
|
} else if (_currentAppState.match(/inactive|background/) && nextAppState === 'active') {
|
|
1221
1364
|
// App coming back to foreground
|
|
1222
1365
|
getLogger().logLifecycleEvent('App returning to foreground');
|
|
1366
|
+
|
|
1367
|
+
// Check if we exceeded the session timeout (60s).
|
|
1368
|
+
// Native side will have already ended the old session and started a new
|
|
1369
|
+
// one — we need to reset JS-side auto-tracking state to match.
|
|
1370
|
+
if (_backgroundEntryTime && _isRecording) {
|
|
1371
|
+
const backgroundDurationMs = Date.now() - _backgroundEntryTime;
|
|
1372
|
+
if (backgroundDurationMs > SESSION_TIMEOUT_MS) {
|
|
1373
|
+
getLogger().debug(`Session rollover: background ${Math.round(backgroundDurationMs / 1000)}s > ${SESSION_TIMEOUT_MS / 1000}s timeout`);
|
|
1374
|
+
_reinitAutoTrackingForNewSession();
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
_backgroundEntryTime = null;
|
|
1223
1378
|
}
|
|
1224
1379
|
_currentAppState = nextAppState;
|
|
1225
1380
|
} catch (error) {
|
|
@@ -1284,8 +1439,10 @@ function setupAuthErrorListener() {
|
|
|
1284
1439
|
}
|
|
1285
1440
|
});
|
|
1286
1441
|
}
|
|
1287
|
-
} catch
|
|
1288
|
-
|
|
1442
|
+
} catch {
|
|
1443
|
+
// Expected on some architectures where NativeEventEmitter isn't fully supported.
|
|
1444
|
+
// Auth errors are still handled synchronously via native callback — this listener
|
|
1445
|
+
// is purely supplementary. No need to log.
|
|
1289
1446
|
}
|
|
1290
1447
|
}
|
|
1291
1448
|
|