@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
|
@@ -8,35 +8,25 @@ export const SDK_VERSION = '1.0.0';
|
|
|
8
8
|
export const DEFAULT_CONFIG = {
|
|
9
9
|
enabled: true,
|
|
10
10
|
captureFPS: 0.5,
|
|
11
|
-
// Every 2 seconds (only used in timer mode)
|
|
12
11
|
captureOnEvents: true,
|
|
13
|
-
// Event-driven capture (not time-based)
|
|
14
12
|
maxSessionDuration: 10 * 60 * 1000,
|
|
15
|
-
// 10 minutes (project-level configurable, clamped 1–10)
|
|
16
13
|
maxStorageSize: 50 * 1024 * 1024,
|
|
17
|
-
// 50MB
|
|
18
14
|
autoScreenTracking: true,
|
|
19
15
|
autoGestureTracking: true,
|
|
20
16
|
privacyOcclusion: true,
|
|
21
17
|
enableCompression: true,
|
|
22
18
|
inactivityThreshold: 5000,
|
|
23
|
-
// 5 seconds
|
|
24
19
|
disableInDev: false,
|
|
25
20
|
detectRageTaps: true,
|
|
26
21
|
rageTapThreshold: 3,
|
|
27
22
|
rageTapTimeWindow: 1000,
|
|
28
|
-
// 1 second
|
|
29
23
|
debug: false,
|
|
30
24
|
autoStartRecording: true,
|
|
31
25
|
collectDeviceInfo: true,
|
|
32
|
-
// Collect detailed device information
|
|
33
26
|
collectGeoLocation: true,
|
|
34
|
-
// Collect IP address and geolocation
|
|
35
27
|
postNavigationDelay: 300,
|
|
36
|
-
// 300ms - allow navigation animations to complete
|
|
37
28
|
postGestureDelay: 200,
|
|
38
|
-
|
|
39
|
-
postModalDelay: 400 // 400ms - ensure modals/alerts are fully rendered
|
|
29
|
+
postModalDelay: 400
|
|
40
30
|
};
|
|
41
31
|
|
|
42
32
|
/** Event type constants */
|
|
@@ -72,8 +62,7 @@ export const CAPTURE_SETTINGS = {
|
|
|
72
62
|
MIN_FPS: 0.1,
|
|
73
63
|
MAX_FPS: 2,
|
|
74
64
|
CAPTURE_SCALE: 0.25,
|
|
75
|
-
|
|
76
|
-
MIN_CAPTURE_DELTA_TIME: 0.5 // Minimum 0.5s between captures (rate limiting)
|
|
65
|
+
MIN_CAPTURE_DELTA_TIME: 0.5
|
|
77
66
|
};
|
|
78
67
|
|
|
79
68
|
/** Memory management settings */
|
|
@@ -5,20 +5,11 @@
|
|
|
5
5
|
* Split from autoTracking.ts for better code organization.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
// Type declarations for browser globals (only used in hybrid apps where DOM is available)
|
|
9
|
-
|
|
10
|
-
// Cast globalThis to work with both RN and hybrid scenarios
|
|
11
8
|
const _globalThis = globalThis;
|
|
12
|
-
|
|
13
|
-
// Original error handlers (for restoration)
|
|
14
9
|
let originalErrorHandler;
|
|
15
10
|
let originalOnError = null;
|
|
16
11
|
let originalOnUnhandledRejection = null;
|
|
17
|
-
|
|
18
|
-
// Callbacks
|
|
19
12
|
let onErrorCallback = null;
|
|
20
|
-
|
|
21
|
-
// Metrics
|
|
22
13
|
let errorCount = 0;
|
|
23
14
|
|
|
24
15
|
/**
|
|
@@ -27,18 +18,12 @@ let errorCount = 0;
|
|
|
27
18
|
export function setupErrorTracking(config, onError) {
|
|
28
19
|
onErrorCallback = onError;
|
|
29
20
|
errorCount = 0;
|
|
30
|
-
|
|
31
|
-
// Track React Native errors
|
|
32
21
|
if (config.trackReactNativeErrors !== false) {
|
|
33
22
|
setupReactNativeErrorHandler();
|
|
34
23
|
}
|
|
35
|
-
|
|
36
|
-
// Track JavaScript errors (only works in web/debug)
|
|
37
24
|
if (config.trackJSErrors !== false && typeof _globalThis !== 'undefined') {
|
|
38
25
|
setupJSErrorHandler();
|
|
39
26
|
}
|
|
40
|
-
|
|
41
|
-
// Track unhandled promise rejections
|
|
42
27
|
if (config.trackPromiseRejections !== false && typeof _globalThis !== 'undefined') {
|
|
43
28
|
setupPromiseRejectionHandler();
|
|
44
29
|
}
|
|
@@ -48,7 +33,6 @@ export function setupErrorTracking(config, onError) {
|
|
|
48
33
|
* Cleanup error tracking and restore original handlers
|
|
49
34
|
*/
|
|
50
35
|
export function cleanupErrorTracking() {
|
|
51
|
-
// Restore React Native handler
|
|
52
36
|
if (originalErrorHandler) {
|
|
53
37
|
try {
|
|
54
38
|
const ErrorUtils = _globalThis.ErrorUtils;
|
|
@@ -60,14 +44,10 @@ export function cleanupErrorTracking() {
|
|
|
60
44
|
}
|
|
61
45
|
originalErrorHandler = undefined;
|
|
62
46
|
}
|
|
63
|
-
|
|
64
|
-
// Restore global onerror
|
|
65
47
|
if (originalOnError !== null) {
|
|
66
48
|
_globalThis.onerror = originalOnError;
|
|
67
49
|
originalOnError = null;
|
|
68
50
|
}
|
|
69
|
-
|
|
70
|
-
// Remove promise rejection handler
|
|
71
51
|
if (originalOnUnhandledRejection && typeof _globalThis.removeEventListener !== 'undefined') {
|
|
72
52
|
_globalThis.removeEventListener('unhandledrejection', originalOnUnhandledRejection);
|
|
73
53
|
originalOnUnhandledRejection = null;
|
|
@@ -119,11 +99,7 @@ function setupReactNativeErrorHandler() {
|
|
|
119
99
|
try {
|
|
120
100
|
const ErrorUtils = _globalThis.ErrorUtils;
|
|
121
101
|
if (!ErrorUtils) return;
|
|
122
|
-
|
|
123
|
-
// Store original handler
|
|
124
102
|
originalErrorHandler = ErrorUtils.getGlobalHandler();
|
|
125
|
-
|
|
126
|
-
// Set new handler
|
|
127
103
|
ErrorUtils.setGlobalHandler((error, isFatal) => {
|
|
128
104
|
trackError({
|
|
129
105
|
type: 'error',
|
|
@@ -132,14 +108,12 @@ function setupReactNativeErrorHandler() {
|
|
|
132
108
|
stack: error.stack,
|
|
133
109
|
name: error.name || 'Error'
|
|
134
110
|
});
|
|
135
|
-
|
|
136
|
-
// Call original handler
|
|
137
111
|
if (originalErrorHandler) {
|
|
138
112
|
originalErrorHandler(error, isFatal);
|
|
139
113
|
}
|
|
140
114
|
});
|
|
141
115
|
} catch {
|
|
142
|
-
//
|
|
116
|
+
// Ignore
|
|
143
117
|
}
|
|
144
118
|
}
|
|
145
119
|
|
|
@@ -157,8 +131,6 @@ function setupJSErrorHandler() {
|
|
|
157
131
|
stack: error?.stack || `${source}:${lineno}:${colno}`,
|
|
158
132
|
name: error?.name || 'Error'
|
|
159
133
|
});
|
|
160
|
-
|
|
161
|
-
// Call original handler
|
|
162
134
|
if (originalOnError) {
|
|
163
135
|
return originalOnError(message, source, lineno, colno, error);
|
|
164
136
|
}
|
package/lib/module/sdk/index.js
CHANGED
|
@@ -7,6 +7,4 @@ export * from './constants';
|
|
|
7
7
|
export * from './utils';
|
|
8
8
|
export * from './autoTracking';
|
|
9
9
|
export * from './networkInterceptor';
|
|
10
|
-
// Note: errorTracking and metricsTracking are internal modules
|
|
11
|
-
// Their exports are re-exported through autoTracking for backward compatibility
|
|
12
10
|
//# sourceMappingURL=index.js.map
|
|
@@ -10,10 +10,9 @@
|
|
|
10
10
|
* Session metrics structure
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
// Metrics state
|
|
14
13
|
let metrics = createEmptyMetrics();
|
|
15
14
|
let sessionStartTime = 0;
|
|
16
|
-
let maxSessionDurationMs = 10 * 60 * 1000;
|
|
15
|
+
let maxSessionDurationMs = 10 * 60 * 1000;
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* Create empty metrics object
|
|
@@ -61,11 +60,8 @@ export function initMetrics() {
|
|
|
61
60
|
* Get current session metrics with calculated scores
|
|
62
61
|
*/
|
|
63
62
|
export function getSessionMetrics() {
|
|
64
|
-
// Calculate duration, clamped to max session duration
|
|
65
63
|
const rawDuration = Date.now() - sessionStartTime;
|
|
66
64
|
const durationMs = Math.min(rawDuration, maxSessionDurationMs);
|
|
67
|
-
|
|
68
|
-
// Calculate scores
|
|
69
65
|
const interactionScore = calculateInteractionScore(durationMs);
|
|
70
66
|
const explorationScore = calculateExplorationScore();
|
|
71
67
|
const uxScore = calculateUXScore();
|
|
@@ -87,9 +83,6 @@ export function setMaxSessionDurationMinutes(minutes) {
|
|
|
87
83
|
maxSessionDurationMs = clampedMinutes * 60 * 1000;
|
|
88
84
|
}
|
|
89
85
|
}
|
|
90
|
-
|
|
91
|
-
// ==================== Metric Increment Methods ====================
|
|
92
|
-
|
|
93
86
|
export function incrementTouchCount() {
|
|
94
87
|
metrics.touchCount++;
|
|
95
88
|
metrics.totalEvents++;
|
|
@@ -129,8 +122,6 @@ export function trackAPIMetrics(success, durationMs = 0, responseBytes = 0) {
|
|
|
129
122
|
}
|
|
130
123
|
}
|
|
131
124
|
|
|
132
|
-
// ==================== Score Calculations ====================
|
|
133
|
-
|
|
134
125
|
/**
|
|
135
126
|
* Calculate interaction score based on engagement with app
|
|
136
127
|
* Higher = more engaged (more interactions per minute)
|
|
@@ -139,16 +130,12 @@ function calculateInteractionScore(durationMs) {
|
|
|
139
130
|
if (durationMs <= 0) return 100;
|
|
140
131
|
const durationMinutes = durationMs / 60000;
|
|
141
132
|
const interactionsPerMinute = metrics.touchCount / Math.max(0.5, durationMinutes);
|
|
142
|
-
|
|
143
|
-
// Ideal: 10-30 interactions per minute
|
|
144
|
-
// Low (< 5): user passive/confused
|
|
145
|
-
// Very high (> 60): rage tapping
|
|
146
133
|
if (interactionsPerMinute < 2) return 20;
|
|
147
134
|
if (interactionsPerMinute < 5) return 50;
|
|
148
135
|
if (interactionsPerMinute < 10) return 70;
|
|
149
136
|
if (interactionsPerMinute <= 30) return 100;
|
|
150
137
|
if (interactionsPerMinute <= 60) return 80;
|
|
151
|
-
return 50;
|
|
138
|
+
return 50;
|
|
152
139
|
}
|
|
153
140
|
|
|
154
141
|
/**
|
|
@@ -157,14 +144,12 @@ function calculateInteractionScore(durationMs) {
|
|
|
157
144
|
*/
|
|
158
145
|
function calculateExplorationScore() {
|
|
159
146
|
const uniqueScreens = metrics.uniqueScreensCount;
|
|
160
|
-
|
|
161
|
-
// More unique screens = better exploration
|
|
162
147
|
if (uniqueScreens >= 10) return 100;
|
|
163
148
|
if (uniqueScreens >= 7) return 90;
|
|
164
149
|
if (uniqueScreens >= 5) return 80;
|
|
165
150
|
if (uniqueScreens >= 3) return 60;
|
|
166
151
|
if (uniqueScreens >= 2) return 40;
|
|
167
|
-
return 20;
|
|
152
|
+
return 20;
|
|
168
153
|
}
|
|
169
154
|
|
|
170
155
|
/**
|
|
@@ -173,14 +158,8 @@ function calculateExplorationScore() {
|
|
|
173
158
|
*/
|
|
174
159
|
function calculateUXScore() {
|
|
175
160
|
let score = 100;
|
|
176
|
-
|
|
177
|
-
// Deduct for errors
|
|
178
161
|
score -= metrics.errorCount * 10;
|
|
179
|
-
|
|
180
|
-
// Deduct heavily for rage taps
|
|
181
162
|
score -= metrics.rageTapCount * 20;
|
|
182
|
-
|
|
183
|
-
// Deduct for API errors (less severe)
|
|
184
163
|
score -= metrics.apiErrorCount * 5;
|
|
185
164
|
return Math.max(0, Math.min(100, score));
|
|
186
165
|
}
|
|
@@ -18,46 +18,28 @@
|
|
|
18
18
|
export function normalizeScreenName(raw) {
|
|
19
19
|
if (!raw) return 'Unknown';
|
|
20
20
|
let name = raw;
|
|
21
|
-
|
|
22
|
-
// 1. Remove non-printable characters and weird symbols (often from icons)
|
|
23
21
|
name = name.replace(/[^\x20-\x7E\s]/g, '');
|
|
24
|
-
|
|
25
|
-
// 2. Handle hyphens: my-recipes -> My Recipes
|
|
26
22
|
name = name.split(/[-_]/).map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ');
|
|
27
|
-
|
|
28
|
-
// 3. Remove common suffixes
|
|
29
23
|
const suffixes = ['Screen', 'Page', 'View', 'Controller', 'ViewController', 'VC'];
|
|
30
24
|
for (const suffix of suffixes) {
|
|
31
25
|
if (name.endsWith(suffix) && name.length > suffix.length) {
|
|
32
26
|
name = name.slice(0, -suffix.length).trim();
|
|
33
27
|
}
|
|
34
28
|
}
|
|
35
|
-
|
|
36
|
-
// 4. Remove common prefixes (React Native internals)
|
|
37
29
|
const prefixes = ['RNS', 'RCT', 'RN', 'UI'];
|
|
38
30
|
for (const prefix of prefixes) {
|
|
39
31
|
if (name.startsWith(prefix) && name.length > prefix.length + 2) {
|
|
40
32
|
name = name.slice(prefix.length).trim();
|
|
41
33
|
}
|
|
42
34
|
}
|
|
43
|
-
|
|
44
|
-
// 5. Handle dynamic route params: [id] -> (omit), [userId] -> "User"
|
|
45
35
|
name = name.replace(/\[([a-zA-Z]+)(?:Id)?\]/g, (_, param) => {
|
|
46
36
|
const clean = param.replace(/Id$/i, '');
|
|
47
37
|
if (clean.length < 2) return '';
|
|
48
38
|
return clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
49
39
|
});
|
|
50
|
-
|
|
51
|
-
// 6. Remove leftover brackets
|
|
52
40
|
name = name.replace(/\[\]/g, '');
|
|
53
|
-
|
|
54
|
-
// 7. Convert camelCase/PascalCase to Title Case with spaces
|
|
55
41
|
name = name.replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
56
|
-
|
|
57
|
-
// 8. Clean up multiple spaces and trim
|
|
58
42
|
name = name.replace(/\s+/g, ' ').trim();
|
|
59
|
-
|
|
60
|
-
// 9. Capitalize first letter
|
|
61
43
|
if (name.length > 0) {
|
|
62
44
|
name = name.charAt(0).toUpperCase() + name.slice(1);
|
|
63
45
|
}
|
|
@@ -72,23 +54,17 @@ export function normalizeScreenName(raw) {
|
|
|
72
54
|
* @returns Human-readable screen name
|
|
73
55
|
*/
|
|
74
56
|
export function getScreenNameFromPath(pathname, segments) {
|
|
75
|
-
// Use segments for cleaner names if available
|
|
76
57
|
if (segments.length > 0) {
|
|
77
|
-
// Filter out group markers like (tabs), (auth), etc.
|
|
78
58
|
const cleanSegments = segments.filter(s => !s.startsWith('(') && !s.endsWith(')'));
|
|
79
59
|
if (cleanSegments.length > 0) {
|
|
80
|
-
// Process each segment
|
|
81
60
|
const processedSegments = cleanSegments.map(s => {
|
|
82
|
-
// Handle dynamic params like [id]
|
|
83
61
|
if (s.startsWith('[') && s.endsWith(']')) {
|
|
84
62
|
const param = s.slice(1, -1);
|
|
85
|
-
// Skip pure ID params, keep meaningful ones
|
|
86
63
|
if (param === 'id' || param === 'slug') return null;
|
|
87
|
-
|
|
64
|
+
if (param === 'id' || param === 'slug') return null;
|
|
88
65
|
const clean = param.replace(/Id$/i, '');
|
|
89
66
|
return clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
90
67
|
}
|
|
91
|
-
// Regular segment - capitalize
|
|
92
68
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
93
69
|
}).filter(Boolean);
|
|
94
70
|
if (processedSegments.length > 0) {
|
|
@@ -96,30 +72,17 @@ export function getScreenNameFromPath(pathname, segments) {
|
|
|
96
72
|
}
|
|
97
73
|
}
|
|
98
74
|
}
|
|
99
|
-
|
|
100
|
-
// Fall back to pathname
|
|
101
75
|
if (!pathname || pathname === '/') {
|
|
102
76
|
return 'Home';
|
|
103
77
|
}
|
|
104
|
-
|
|
105
|
-
// Clean up the path
|
|
106
|
-
let cleanPath = pathname.replace(/^\/(tabs)?/, '') // Remove leading slash and (tabs)
|
|
107
|
-
.replace(/\([^)]+\)/g, '') // Remove all group markers like (settings)
|
|
108
|
-
.replace(/\[([^\]]+)\]/g, (_, param) => {
|
|
109
|
-
// Handle dynamic params in path
|
|
78
|
+
const cleanPath = pathname.replace(/^\/(tabs)?/, '').replace(/\([^)]+\)/g, '').replace(/\[([^\]]+)\]/g, (_, param) => {
|
|
110
79
|
if (param === 'id' || param === 'slug') return '';
|
|
111
80
|
const clean = param.replace(/Id$/i, '');
|
|
112
81
|
return clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
113
|
-
}).replace(/\/+/g, '/')
|
|
114
|
-
.replace(/^\//, '') // Remove leading slash
|
|
115
|
-
.replace(/\/$/, '') // Remove trailing slash
|
|
116
|
-
.replace(/\//g, ' > ') // Replace slashes with arrows
|
|
117
|
-
.trim();
|
|
82
|
+
}).replace(/\/+/g, '/').replace(/^\//, '').replace(/\/$/, '').replace(/\//g, ' > ').trim();
|
|
118
83
|
if (!cleanPath) {
|
|
119
84
|
return 'Home';
|
|
120
85
|
}
|
|
121
|
-
|
|
122
|
-
// Capitalize first letter of each word
|
|
123
86
|
return cleanPath.split(' > ').map(s => s.charAt(0).toUpperCase() + s.slice(1)).filter(s => s.length > 0).join(' > ') || 'Home';
|
|
124
87
|
}
|
|
125
88
|
|
|
@@ -133,8 +96,6 @@ export function getCurrentRouteFromState(state) {
|
|
|
133
96
|
if (!state || !state.routes) return null;
|
|
134
97
|
const route = state.routes[state.index ?? state.routes.length - 1];
|
|
135
98
|
if (!route) return null;
|
|
136
|
-
|
|
137
|
-
// If nested state, recurse
|
|
138
99
|
if (route.state) {
|
|
139
100
|
return getCurrentRouteFromState(route.state);
|
|
140
101
|
}
|
|
@@ -13,41 +13,26 @@
|
|
|
13
13
|
* - PII Scrubbing for query parameters
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
// Store original implementations
|
|
17
16
|
let originalFetch = null;
|
|
18
17
|
let originalXHROpen = null;
|
|
19
18
|
let originalXHRSend = null;
|
|
20
|
-
|
|
21
|
-
// Callback to log network requests (called asynchronously)
|
|
22
19
|
let logCallback = null;
|
|
23
|
-
|
|
24
|
-
// Pending requests buffer (circular buffer for memory efficiency)
|
|
25
20
|
const MAX_PENDING_REQUESTS = 100;
|
|
26
21
|
const pendingRequests = new Array(MAX_PENDING_REQUESTS).fill(null);
|
|
27
22
|
let pendingHead = 0;
|
|
28
23
|
let pendingTail = 0;
|
|
29
24
|
let pendingCount = 0;
|
|
30
|
-
|
|
31
|
-
// Flush timer
|
|
32
25
|
let flushTimer = null;
|
|
33
|
-
const FLUSH_INTERVAL = 500;
|
|
34
|
-
|
|
35
|
-
// Sampling for high-frequency endpoints
|
|
26
|
+
const FLUSH_INTERVAL = 500;
|
|
36
27
|
const endpointCounts = new Map();
|
|
37
|
-
const SAMPLE_WINDOW = 10000;
|
|
38
|
-
const MAX_PER_ENDPOINT = 20;
|
|
39
|
-
|
|
40
|
-
// Configuration
|
|
28
|
+
const SAMPLE_WINDOW = 10000;
|
|
29
|
+
const MAX_PER_ENDPOINT = 20;
|
|
41
30
|
const config = {
|
|
42
31
|
enabled: true,
|
|
43
32
|
ignorePatterns: [],
|
|
44
|
-
// Simple string patterns for fast matching
|
|
45
33
|
maxUrlLength: 300,
|
|
46
|
-
|
|
47
|
-
captureSizes: false // Disabled by default for performance
|
|
34
|
+
captureSizes: false
|
|
48
35
|
};
|
|
49
|
-
|
|
50
|
-
// PII Scrubbing - Sensitive keys to look for in query params
|
|
51
36
|
const SENSITIVE_KEYS = ['token', 'key', 'secret', 'password', 'auth', 'access_token', 'api_key'];
|
|
52
37
|
|
|
53
38
|
/**
|
|
@@ -55,7 +40,6 @@ const SENSITIVE_KEYS = ['token', 'key', 'secret', 'password', 'auth', 'access_to
|
|
|
55
40
|
*/
|
|
56
41
|
function scrubUrl(url) {
|
|
57
42
|
try {
|
|
58
|
-
// Fast check if URL might contain params
|
|
59
43
|
if (url.indexOf('?') === -1) return url;
|
|
60
44
|
const urlObj = new URL(url);
|
|
61
45
|
let modified = false;
|
|
@@ -65,13 +49,10 @@ function scrubUrl(url) {
|
|
|
65
49
|
modified = true;
|
|
66
50
|
}
|
|
67
51
|
});
|
|
68
|
-
|
|
69
|
-
// Also scan for partial matches (case-insensitive) if strict scrubbing needed
|
|
70
|
-
// But for performance, we stick to exact keys or common variations
|
|
71
|
-
|
|
72
52
|
return modified ? urlObj.toString() : url;
|
|
73
53
|
} catch {
|
|
74
|
-
//
|
|
54
|
+
// Ignore error, fallback to primitive scrubbing
|
|
55
|
+
|
|
75
56
|
let scrubbed = url;
|
|
76
57
|
SENSITIVE_KEYS.forEach(key => {
|
|
77
58
|
const regex = new RegExp(`([?&])${key}=[^&]*`, 'gi');
|
|
@@ -119,14 +100,10 @@ function queueRequest(request) {
|
|
|
119
100
|
pendingHead = (pendingHead + 1) % MAX_PENDING_REQUESTS;
|
|
120
101
|
pendingCount--;
|
|
121
102
|
}
|
|
122
|
-
|
|
123
|
-
// Scrub URL before queuing
|
|
124
103
|
request.url = scrubUrl(request.url);
|
|
125
104
|
pendingRequests[pendingTail] = request;
|
|
126
105
|
pendingTail = (pendingTail + 1) % MAX_PENDING_REQUESTS;
|
|
127
106
|
pendingCount++;
|
|
128
|
-
|
|
129
|
-
// Schedule flush if not already scheduled
|
|
130
107
|
if (!flushTimer) {
|
|
131
108
|
flushTimer = setTimeout(flushPendingRequests, FLUSH_INTERVAL);
|
|
132
109
|
}
|
|
@@ -149,7 +126,7 @@ function flushPendingRequests() {
|
|
|
149
126
|
try {
|
|
150
127
|
logCallback(request);
|
|
151
128
|
} catch {
|
|
152
|
-
// Ignore
|
|
129
|
+
// Ignore
|
|
153
130
|
}
|
|
154
131
|
}
|
|
155
132
|
}
|
|
@@ -162,12 +139,9 @@ function parseUrlFast(url) {
|
|
|
162
139
|
// Fast path for common patterns
|
|
163
140
|
let hostEnd = -1;
|
|
164
141
|
let pathStart = -1;
|
|
165
|
-
|
|
166
|
-
// Find ://
|
|
167
142
|
const protoEnd = url.indexOf('://');
|
|
168
143
|
if (protoEnd !== -1) {
|
|
169
144
|
const afterProto = protoEnd + 3;
|
|
170
|
-
// Find end of host (first / after ://)
|
|
171
145
|
const slashPos = url.indexOf('/', afterProto);
|
|
172
146
|
if (slashPos !== -1) {
|
|
173
147
|
hostEnd = slashPos;
|
|
@@ -181,8 +155,6 @@ function parseUrlFast(url) {
|
|
|
181
155
|
path: pathStart < url.length ? url.substring(pathStart) : '/'
|
|
182
156
|
};
|
|
183
157
|
}
|
|
184
|
-
|
|
185
|
-
// Relative URL
|
|
186
158
|
return {
|
|
187
159
|
host: '',
|
|
188
160
|
path: url
|
|
@@ -197,15 +169,10 @@ function interceptFetch() {
|
|
|
197
169
|
if (originalFetch) return;
|
|
198
170
|
originalFetch = globalThis.fetch;
|
|
199
171
|
globalThis.fetch = function optimizedFetch(input, init) {
|
|
200
|
-
// Fast path: if disabled or no callback, skip entirely
|
|
201
172
|
if (!config.enabled || !logCallback) {
|
|
202
173
|
return originalFetch(input, init);
|
|
203
174
|
}
|
|
204
|
-
|
|
205
|
-
// Extract URL string (minimal work)
|
|
206
175
|
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
207
|
-
|
|
208
|
-
// Fast ignore check
|
|
209
176
|
if (shouldIgnoreUrl(url)) {
|
|
210
177
|
return originalFetch(input, init);
|
|
211
178
|
}
|
|
@@ -237,7 +204,6 @@ function interceptFetch() {
|
|
|
237
204
|
});
|
|
238
205
|
return response;
|
|
239
206
|
}, error => {
|
|
240
|
-
// Error - queue the log asynchronously
|
|
241
207
|
queueRequest({
|
|
242
208
|
requestId: `f${startTime}`,
|
|
243
209
|
method,
|
|
@@ -264,8 +230,6 @@ function interceptXHR() {
|
|
|
264
230
|
originalXHRSend = XMLHttpRequest.prototype.send;
|
|
265
231
|
XMLHttpRequest.prototype.open = function (method, url, async = true, username, password) {
|
|
266
232
|
const urlString = typeof url === 'string' ? url : url.toString();
|
|
267
|
-
|
|
268
|
-
// Store minimal info
|
|
269
233
|
this.__rj = {
|
|
270
234
|
m: method.toUpperCase(),
|
|
271
235
|
u: urlString,
|
|
@@ -301,9 +265,6 @@ function interceptXHR() {
|
|
|
301
265
|
errorMessage: this.status === 0 ? 'Network error' : undefined
|
|
302
266
|
});
|
|
303
267
|
};
|
|
304
|
-
|
|
305
|
-
// Use load/error events (more efficient than readystatechange)
|
|
306
|
-
// Note: We use basic addEventListener without options for RN compatibility
|
|
307
268
|
this.addEventListener('load', onComplete);
|
|
308
269
|
this.addEventListener('error', onComplete);
|
|
309
270
|
this.addEventListener('abort', onComplete);
|
|
@@ -316,11 +277,8 @@ function interceptXHR() {
|
|
|
316
277
|
*/
|
|
317
278
|
export function initNetworkInterceptor(callback, options) {
|
|
318
279
|
logCallback = callback;
|
|
319
|
-
|
|
320
|
-
// Convert patterns to simple strings for fast matching
|
|
321
280
|
if (options?.ignoreUrls) {
|
|
322
281
|
config.ignorePatterns = options.ignoreUrls.filter(p => typeof p === 'string');
|
|
323
|
-
// Note: RegExp patterns are not supported in optimized version for performance
|
|
324
282
|
}
|
|
325
283
|
if (options?.captureSizes !== undefined) {
|
|
326
284
|
config.captureSizes = options.captureSizes;
|
package/lib/module/sdk/utils.js
CHANGED
|
@@ -280,11 +280,6 @@ class Logger {
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
284
|
-
// Lifecycle Logs - Industry standard minimal logging
|
|
285
|
-
// These are the only logs integrators will see in debug builds
|
|
286
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
287
|
-
|
|
288
283
|
/**
|
|
289
284
|
* Log SDK initialization success.
|
|
290
285
|
* Only shown in development builds - this is the minimal "SDK started" log.
|
|
@@ -129,9 +129,11 @@ export interface Spec extends TurboModule {
|
|
|
129
129
|
setUserIdentity(userId: string): Promise<{
|
|
130
130
|
success: boolean;
|
|
131
131
|
}>;
|
|
132
|
+
getUserIdentity(): Promise<string | null>;
|
|
132
133
|
setDebugMode(enabled: boolean): Promise<{
|
|
133
134
|
success: boolean;
|
|
134
135
|
}>;
|
|
136
|
+
getDeviceInfo(): Promise<Object>;
|
|
135
137
|
}
|
|
136
138
|
/**
|
|
137
139
|
* Default export for Codegen.
|
|
@@ -155,19 +155,18 @@ export declare function getSessionMetrics(): SessionMetrics & {
|
|
|
155
155
|
* Reset metrics for new session
|
|
156
156
|
*/
|
|
157
157
|
export declare function resetMetrics(): void;
|
|
158
|
-
/** Clamp and set max session duration in minutes (1–10). Defaults to 10. */
|
|
159
158
|
export declare function setMaxSessionDurationMinutes(minutes?: number): void;
|
|
160
|
-
/** Returns true if the current session exceeded the configured max duration. */
|
|
161
159
|
export declare function hasExceededMaxSessionDuration(): boolean;
|
|
162
|
-
/** Returns remaining milliseconds until the session should stop. */
|
|
163
160
|
export declare function getRemainingSessionDurationMs(): number;
|
|
164
161
|
/**
|
|
165
162
|
* Collect device information
|
|
166
163
|
*/
|
|
167
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Collect device information
|
|
166
|
+
*/
|
|
167
|
+
export declare function collectDeviceInfo(): Promise<DeviceInfo>;
|
|
168
168
|
/**
|
|
169
169
|
* Get the anonymous ID (synchronous - returns generated ID immediately)
|
|
170
|
-
* For persistent ID, call initAnonymousId() first
|
|
171
170
|
*/
|
|
172
171
|
export declare function getAnonymousId(): string;
|
|
173
172
|
/**
|
|
@@ -181,7 +180,7 @@ export declare function ensurePersistentAnonymousId(): Promise<string>;
|
|
|
181
180
|
*/
|
|
182
181
|
export declare function loadAnonymousId(): Promise<string>;
|
|
183
182
|
/**
|
|
184
|
-
* Set a custom anonymous ID
|
|
183
|
+
* Set a custom anonymous ID
|
|
185
184
|
*/
|
|
186
185
|
export declare function setAnonymousId(id: string): void;
|
|
187
186
|
declare const _default: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rejourneyco/react-native",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Rejourney Session Recording SDK for React Native",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -40,7 +40,10 @@
|
|
|
40
40
|
"test:coverage": "vitest run --coverage",
|
|
41
41
|
"release": "release-it",
|
|
42
42
|
"verify:ios": "./scripts/verify-ios-structure.sh",
|
|
43
|
-
"
|
|
43
|
+
"verify:package": "./scripts/verify-package-content.sh",
|
|
44
|
+
"test:ios-install": "./scripts/test-ios-install.sh",
|
|
45
|
+
"test:android-install": "./scripts/test-android-install.sh",
|
|
46
|
+
"audit:deps": "depcruise src --config .dependency-cruiser.js"
|
|
44
47
|
},
|
|
45
48
|
"keywords": [
|
|
46
49
|
"react-native",
|
|
@@ -61,6 +64,9 @@
|
|
|
61
64
|
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
|
62
65
|
"@typescript-eslint/parser": "^8.15.0",
|
|
63
66
|
"@vitest/coverage-v8": "^2.1.0",
|
|
67
|
+
"dependency-cruiser": "^16.10.4",
|
|
68
|
+
"@react-navigation/native": "*",
|
|
69
|
+
"expo-router": "*",
|
|
64
70
|
"eslint": "^9.39.2",
|
|
65
71
|
"react": "*",
|
|
66
72
|
"react-native": "*",
|
|
@@ -70,7 +76,9 @@
|
|
|
70
76
|
},
|
|
71
77
|
"peerDependencies": {
|
|
72
78
|
"react": "*",
|
|
73
|
-
"react-native": "*"
|
|
79
|
+
"react-native": "*",
|
|
80
|
+
"@react-navigation/native": ">=6.0.0",
|
|
81
|
+
"expo-router": ">=3.0.0"
|
|
74
82
|
},
|
|
75
83
|
"codegenConfig": {
|
|
76
84
|
"name": "RejourneySpec",
|
package/src/NativeRejourney.ts
CHANGED
|
@@ -150,7 +150,11 @@ export interface Spec extends TurboModule {
|
|
|
150
150
|
|
|
151
151
|
setUserIdentity(userId: string): Promise<{ success: boolean }>;
|
|
152
152
|
|
|
153
|
+
getUserIdentity(): Promise<string | null>;
|
|
154
|
+
|
|
153
155
|
setDebugMode(enabled: boolean): Promise<{ success: boolean }>;
|
|
156
|
+
|
|
157
|
+
getDeviceInfo(): Promise<Object>;
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
/**
|
package/src/components/Mask.tsx
CHANGED
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
import React from 'react';
|
|
26
26
|
import type { ViewProps } from 'react-native';
|
|
27
27
|
|
|
28
|
-
// Lazy-loaded React Native modules
|
|
29
28
|
let _RN: typeof import('react-native') | null = null;
|
|
30
29
|
|
|
31
30
|
function getRN(): typeof import('react-native') | null {
|
|
@@ -52,7 +51,6 @@ export interface MaskProps extends ViewProps {
|
|
|
52
51
|
export const Mask: React.FC<MaskProps> = ({ children, style, ...props }) => {
|
|
53
52
|
const RN = getRN();
|
|
54
53
|
|
|
55
|
-
// If RN isn't loaded yet (shouldn't happen in practice), render children directly
|
|
56
54
|
if (!RN) {
|
|
57
55
|
return <>{children}</>;
|
|
58
56
|
}
|
|
@@ -61,7 +59,6 @@ export const Mask: React.FC<MaskProps> = ({ children, style, ...props }) => {
|
|
|
61
59
|
|
|
62
60
|
const styles = StyleSheet.create({
|
|
63
61
|
container: {
|
|
64
|
-
// Minimal container style - doesn't affect layout
|
|
65
62
|
},
|
|
66
63
|
});
|
|
67
64
|
|