@multiplayer-app/session-recorder-react-native 0.0.1-alpha.6 → 0.0.1-alpha.7
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/RRWEB_INTEGRATION.md +336 -0
- package/VIEWSHOT_INTEGRATION_TEST.md +123 -0
- package/copy-react-native-dist.sh +38 -0
- package/dist/config/constants.d.ts +0 -1
- package/dist/config/constants.js +1 -1
- package/dist/config/constants.js.map +1 -1
- package/dist/context/SessionRecorderContext.js +1 -1
- package/dist/context/SessionRecorderContext.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/otel/helpers.d.ts +4 -4
- package/dist/otel/helpers.js +1 -1
- package/dist/otel/helpers.js.map +1 -1
- package/dist/otel/index.js +1 -1
- package/dist/otel/index.js.map +1 -1
- package/dist/otel/instrumentations/index.d.ts +2 -3
- package/dist/otel/instrumentations/index.js +1 -1
- package/dist/otel/instrumentations/index.js.map +1 -1
- package/dist/otel/instrumentations/reactNavigationInstrumentation.d.ts +1 -0
- package/dist/otel/instrumentations/reactNavigationInstrumentation.js +1 -1
- package/dist/otel/instrumentations/reactNavigationInstrumentation.js.map +1 -1
- package/dist/recorder/eventExporter.d.ts +21 -0
- package/dist/recorder/eventExporter.js +1 -0
- package/dist/recorder/eventExporter.js.map +1 -0
- package/dist/recorder/gestureRecorder.d.ts +57 -3
- package/dist/recorder/gestureRecorder.js +1 -1
- package/dist/recorder/gestureRecorder.js.map +1 -1
- package/dist/recorder/index.d.ts +61 -7
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.js.map +1 -1
- package/dist/recorder/screenRecorder.d.ts +58 -4
- package/dist/recorder/screenRecorder.js +1 -1
- package/dist/recorder/screenRecorder.js.map +1 -1
- package/dist/services/api.service.js.map +1 -1
- package/dist/services/storage.service.js.map +1 -1
- package/dist/session-recorder.d.ts +40 -2
- package/dist/session-recorder.js +1 -1
- package/dist/session-recorder.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/rrweb.d.ts +108 -0
- package/dist/types/rrweb.js +1 -0
- package/dist/types/rrweb.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/example-usage.tsx +174 -0
- package/package.json +3 -2
- package/src/config/constants.ts +3 -3
- package/src/context/SessionRecorderContext.tsx +93 -16
- package/src/index.ts +1 -0
- package/src/otel/helpers.ts +37 -20
- package/src/otel/index.ts +7 -3
- package/src/otel/instrumentations/index.ts +79 -38
- package/src/otel/instrumentations/reactNavigationInstrumentation.ts +5 -0
- package/src/recorder/eventExporter.ts +138 -0
- package/src/recorder/gestureRecorder.ts +124 -3
- package/src/recorder/index.ts +130 -21
- package/src/recorder/screenRecorder.ts +203 -7
- package/src/services/api.service.ts +1 -8
- package/src/services/storage.service.ts +1 -0
- package/src/session-recorder.ts +91 -12
- package/src/types/index.ts +2 -1
- package/src/types/rrweb.ts +122 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example usage of the React Native Session Recorder with rrweb integration
|
|
3
|
+
* This file demonstrates how to use the updated session recorder system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react'
|
|
7
|
+
import { View, Text, Button, StyleSheet, Alert } from 'react-native'
|
|
8
|
+
import { SessionRecorderProvider, useSessionRecorder } from './src/context/SessionRecorderContext'
|
|
9
|
+
import { EventType } from './src/types'
|
|
10
|
+
|
|
11
|
+
// Example app component
|
|
12
|
+
function App() {
|
|
13
|
+
return (
|
|
14
|
+
<SessionRecorderProvider
|
|
15
|
+
options={{
|
|
16
|
+
apiKey: 'your-api-key-here',
|
|
17
|
+
version: '1.0.0',
|
|
18
|
+
application: 'ExampleApp',
|
|
19
|
+
environment: 'development',
|
|
20
|
+
recordScreen: true,
|
|
21
|
+
recordGestures: true,
|
|
22
|
+
recordNavigation: true
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
<MainContent />
|
|
26
|
+
</SessionRecorderProvider>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Main content component that will be wrapped by TouchEventCapture
|
|
31
|
+
function MainContent() {
|
|
32
|
+
const { client } = useSessionRecorder()
|
|
33
|
+
|
|
34
|
+
const handleStartSession = () => {
|
|
35
|
+
try {
|
|
36
|
+
client.start()
|
|
37
|
+
Alert.alert('Session Started', 'Recording has begun!')
|
|
38
|
+
} catch (error) {
|
|
39
|
+
Alert.alert('Error', `Failed to start session: ${error}`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const handleStopSession = () => {
|
|
44
|
+
try {
|
|
45
|
+
client.stop()
|
|
46
|
+
Alert.alert('Session Stopped', 'Recording has ended!')
|
|
47
|
+
} catch (error) {
|
|
48
|
+
Alert.alert('Error', `Failed to stop session: ${error}`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const handleRecordCustomEvent = () => {
|
|
53
|
+
// Example of recording a custom rrweb event
|
|
54
|
+
const customEvent = {
|
|
55
|
+
type: EventType.Custom,
|
|
56
|
+
data: {
|
|
57
|
+
customType: 'button_click',
|
|
58
|
+
buttonId: 'example_button',
|
|
59
|
+
timestamp: Date.now()
|
|
60
|
+
},
|
|
61
|
+
timestamp: Date.now()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
client.recordEvent(customEvent)
|
|
65
|
+
Alert.alert('Custom Event', 'Custom event recorded!')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const handleGetRecordingStats = () => {
|
|
69
|
+
// This would need to be implemented in the SessionRecorder
|
|
70
|
+
// const stats = client.getRecordingStats()
|
|
71
|
+
// Alert.alert('Recording Stats', `Events recorded: ${stats.totalEvents}`)
|
|
72
|
+
Alert.alert('Recording Stats', 'Feature coming soon!')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<View style={styles.container}>
|
|
77
|
+
<Text style={styles.title}>Session Recorder Example</Text>
|
|
78
|
+
<Text style={styles.subtitle}>This app demonstrates rrweb-compatible session recording for React Native</Text>
|
|
79
|
+
|
|
80
|
+
<View style={styles.buttonContainer}>
|
|
81
|
+
<Button title='Start Recording' onPress={handleStartSession} color='#4CAF50' />
|
|
82
|
+
</View>
|
|
83
|
+
|
|
84
|
+
<View style={styles.buttonContainer}>
|
|
85
|
+
<Button title='Stop Recording' onPress={handleStopSession} color='#F44336' />
|
|
86
|
+
</View>
|
|
87
|
+
|
|
88
|
+
<View style={styles.buttonContainer}>
|
|
89
|
+
<Button title='Record Custom Event' onPress={handleRecordCustomEvent} color='#2196F3' />
|
|
90
|
+
</View>
|
|
91
|
+
|
|
92
|
+
<View style={styles.buttonContainer}>
|
|
93
|
+
<Button title='Get Recording Stats' onPress={handleGetRecordingStats} color='#FF9800' />
|
|
94
|
+
</View>
|
|
95
|
+
|
|
96
|
+
<Text style={styles.instructions}>
|
|
97
|
+
Recording is now AUTOMATIC! When you start a session, the system will automatically:
|
|
98
|
+
{'\n'}• Capture screen snapshots periodically
|
|
99
|
+
{'\n'}• Record all touch interactions (start, move, end)
|
|
100
|
+
{'\n'}• Generate rrweb-compatible events
|
|
101
|
+
{'\n'}• No manual setup required!
|
|
102
|
+
</Text>
|
|
103
|
+
</View>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const styles = StyleSheet.create({
|
|
108
|
+
container: {
|
|
109
|
+
flex: 1,
|
|
110
|
+
padding: 20,
|
|
111
|
+
backgroundColor: '#f5f5f5',
|
|
112
|
+
justifyContent: 'center'
|
|
113
|
+
},
|
|
114
|
+
title: {
|
|
115
|
+
fontSize: 24,
|
|
116
|
+
fontWeight: 'bold',
|
|
117
|
+
textAlign: 'center',
|
|
118
|
+
marginBottom: 10,
|
|
119
|
+
color: '#333'
|
|
120
|
+
},
|
|
121
|
+
subtitle: {
|
|
122
|
+
fontSize: 16,
|
|
123
|
+
textAlign: 'center',
|
|
124
|
+
marginBottom: 30,
|
|
125
|
+
color: '#666'
|
|
126
|
+
},
|
|
127
|
+
buttonContainer: {
|
|
128
|
+
marginVertical: 10
|
|
129
|
+
},
|
|
130
|
+
instructions: {
|
|
131
|
+
fontSize: 14,
|
|
132
|
+
textAlign: 'center',
|
|
133
|
+
marginTop: 30,
|
|
134
|
+
color: '#888',
|
|
135
|
+
fontStyle: 'italic'
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
export default App
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* AUTOMATIC RECORDING INTEGRATION:
|
|
143
|
+
*
|
|
144
|
+
* 1. Screen Capture (AUTOMATIC with react-native-view-shot):
|
|
145
|
+
* - Install: npm install react-native-view-shot
|
|
146
|
+
* - iOS: Add to Podfile and run pod install
|
|
147
|
+
* - Android: No additional setup needed
|
|
148
|
+
* - Screen capture happens automatically when session starts
|
|
149
|
+
* - Captures the same View element that handles touch events
|
|
150
|
+
*
|
|
151
|
+
* 2. Touch Events (AUTOMATIC):
|
|
152
|
+
* - TouchEventCapture automatically wraps your app content
|
|
153
|
+
* - Touch events are automatically converted to rrweb MouseInteraction events
|
|
154
|
+
* - Coordinates are automatically mapped from React Native to rrweb format
|
|
155
|
+
* - No manual setup required!
|
|
156
|
+
*
|
|
157
|
+
* 3. Event Recording (AUTOMATIC):
|
|
158
|
+
* - All events are automatically stored in the RecorderReactNativeSDK
|
|
159
|
+
* - Events can be exported using getRecordedEvents()
|
|
160
|
+
* - Events are compatible with standard rrweb players
|
|
161
|
+
* - Recording starts/stops automatically with session
|
|
162
|
+
*
|
|
163
|
+
* 4. ViewShot Integration (AUTOMATIC):
|
|
164
|
+
* - The TouchEventCapture View is automatically used for screen capture
|
|
165
|
+
* - No need to manually set up viewshot refs
|
|
166
|
+
* - Screen captures include all touch interactions
|
|
167
|
+
* - Perfect synchronization between touch events and screen captures
|
|
168
|
+
*
|
|
169
|
+
* 5. Customization (Optional):
|
|
170
|
+
* - Modify capture intervals in ScreenRecorder
|
|
171
|
+
* - Adjust touch event throttling in GestureRecorder
|
|
172
|
+
* - Add custom event types as needed
|
|
173
|
+
* - All core functionality works automatically out of the box
|
|
174
|
+
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplayer-app/session-recorder-react-native",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.7",
|
|
4
4
|
"description": "Multiplayer Fullstack Session Recorder for React Native",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Multiplayer Software, Inc.",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"lint:fix": "eslint src/**/*.ts --fix",
|
|
48
48
|
"prepublishOnly": "npm run lint && npm run build",
|
|
49
49
|
"prebuild": "node -p \"'export const version = ' + JSON.stringify(require('./package.json').version)\" > src/version.ts",
|
|
50
|
-
"build": "tsc && babel ./dist --out-dir dist --extensions '.js'"
|
|
50
|
+
"build": "tsc && babel ./dist --out-dir dist --extensions '.js' && ./copy-react-native-dist.sh"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@babel/cli": "^7.19.3",
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
"@opentelemetry/semantic-conventions": "1.36.0",
|
|
79
79
|
"@react-native-async-storage/async-storage": "^1.21.0",
|
|
80
80
|
"@react-native-community/netinfo": "^11.1.0",
|
|
81
|
+
"@rrweb/packer": "^2.0.0-alpha.15",
|
|
81
82
|
"lib0": "0.2.82",
|
|
82
83
|
"react-native-gesture-handler": "^2.14.0",
|
|
83
84
|
"react-native-mmkv": "^2.11.0",
|
package/src/config/constants.ts
CHANGED
|
@@ -33,9 +33,9 @@ export const CONTINUOUS_DEBUGGING_TIMEOUT = 60000 // 1 minutes
|
|
|
33
33
|
|
|
34
34
|
export const DEBUG_SESSION_MAX_DURATION_SECONDS = 10 * 60 + 30 // TODO: move to shared config otel core
|
|
35
35
|
|
|
36
|
-
// Package version - injected by webpack during build
|
|
37
|
-
declare const PACKAGE_VERSION: string
|
|
38
|
-
export const PACKAGE_VERSION_EXPORT = PACKAGE_VERSION || '1.0.0'
|
|
36
|
+
// // Package version - injected by webpack during build
|
|
37
|
+
// declare const PACKAGE_VERSION: string
|
|
38
|
+
// export const PACKAGE_VERSION_EXPORT = PACKAGE_VERSION || '1.0.0'
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
// Regex patterns for OpenTelemetry ignore URLs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, { createContext, useContext, ReactNode, PropsWithChildren, useMemo } from 'react'
|
|
2
|
-
import { View } from 'react-native'
|
|
3
|
-
import { SessionRecorderOptions } from '../types'
|
|
1
|
+
import React, { createContext, useContext, ReactNode, PropsWithChildren, useMemo, useState, useEffect, useRef } from 'react'
|
|
2
|
+
import { Pressable, Text, View } from 'react-native'
|
|
3
|
+
import { SessionRecorderOptions, SessionState } from '../types'
|
|
4
4
|
import SessionRecorder from '../session-recorder'
|
|
5
5
|
|
|
6
6
|
interface SessionRecorderContextType {
|
|
@@ -15,15 +15,52 @@ export interface SessionRecorderProviderProps extends PropsWithChildren {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export const SessionRecorderProvider: React.FC<SessionRecorderProviderProps> = ({ children, client, options }) => {
|
|
18
|
+
const [sessionState, setSessionState] = useState<SessionState | null>(null)
|
|
19
|
+
|
|
18
20
|
const sessionRecorder = useMemo(() => {
|
|
19
21
|
if (client) return client
|
|
20
22
|
SessionRecorder.init(options)
|
|
21
23
|
return SessionRecorder
|
|
22
24
|
}, [])
|
|
23
25
|
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!sessionRecorder) return
|
|
28
|
+
setSessionState(sessionRecorder.sessionState)
|
|
29
|
+
}, [sessionRecorder])
|
|
30
|
+
|
|
31
|
+
const onToggleSession = () => {
|
|
32
|
+
if (sessionState === SessionState.started) {
|
|
33
|
+
setSessionState(SessionState.stopped)
|
|
34
|
+
sessionRecorder.stop()
|
|
35
|
+
} else {
|
|
36
|
+
setSessionState(SessionState.started)
|
|
37
|
+
sessionRecorder.start()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
24
41
|
return (
|
|
25
42
|
<SessionRecorderContext.Provider value={{ client: sessionRecorder }}>
|
|
26
|
-
<TouchEventCapture>
|
|
43
|
+
<TouchEventCapture>
|
|
44
|
+
{children}
|
|
45
|
+
<Pressable onPress={onToggleSession}>
|
|
46
|
+
<View
|
|
47
|
+
style={{
|
|
48
|
+
position: 'absolute',
|
|
49
|
+
right: 0,
|
|
50
|
+
bottom: 100,
|
|
51
|
+
width: 48,
|
|
52
|
+
height: 48,
|
|
53
|
+
paddingTop: 16,
|
|
54
|
+
paddingLeft: 10,
|
|
55
|
+
backgroundColor: 'red',
|
|
56
|
+
borderTopLeftRadius: 24,
|
|
57
|
+
borderBottomLeftRadius: 24
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
<Text style={{ color: 'white' }}>{sessionState === SessionState.started ? 'Stop' : 'Start'}</Text>
|
|
61
|
+
</View>
|
|
62
|
+
</Pressable>
|
|
63
|
+
</TouchEventCapture>
|
|
27
64
|
</SessionRecorderContext.Provider>
|
|
28
65
|
)
|
|
29
66
|
}
|
|
@@ -31,32 +68,72 @@ export const SessionRecorderProvider: React.FC<SessionRecorderProviderProps> = (
|
|
|
31
68
|
// Touch event capture component
|
|
32
69
|
const TouchEventCapture: React.FC<{ children: ReactNode }> = ({ children }) => {
|
|
33
70
|
const context = useContext(SessionRecorderContext)
|
|
71
|
+
const viewShotRef = useRef<View>(null)
|
|
72
|
+
|
|
73
|
+
// Set the viewshot ref in the session recorder when component mounts
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (context?.client && viewShotRef.current) {
|
|
76
|
+
context.client.setViewShotRef?.(viewShotRef.current)
|
|
77
|
+
}
|
|
78
|
+
}, [context?.client])
|
|
79
|
+
|
|
80
|
+
// Callback ref to set the viewshot ref immediately when available
|
|
81
|
+
const setViewShotRef = (ref: View | null) => {
|
|
82
|
+
if (ref && context?.client) {
|
|
83
|
+
context.client.setViewShotRef?.(ref)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
34
86
|
|
|
35
87
|
const handleTouchStart = (event: any) => {
|
|
36
|
-
|
|
88
|
+
if (!context?.client || context.client.sessionState !== SessionState.started) return // SessionState.started
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const { pageX, pageY, target } = event.nativeEvent
|
|
92
|
+
const pressure = event.nativeEvent.force || 1.0
|
|
37
93
|
|
|
38
|
-
|
|
39
|
-
|
|
94
|
+
// Record touch start event automatically
|
|
95
|
+
context.client.recordTouchStart?.(pageX, pageY, target?.toString(), pressure)
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.warn('Failed to record touch start event:', error)
|
|
98
|
+
}
|
|
40
99
|
}
|
|
41
100
|
|
|
42
101
|
const handleTouchMove = (event: any) => {
|
|
43
|
-
|
|
102
|
+
if (!context?.client || context.client.sessionState !== SessionState.started) return // SessionState.started
|
|
44
103
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
104
|
+
try {
|
|
105
|
+
const { pageX, pageY, target } = event.nativeEvent
|
|
106
|
+
const pressure = event.nativeEvent.force || 1.0
|
|
107
|
+
|
|
108
|
+
// Record touch move event automatically
|
|
109
|
+
context.client.recordTouchMove?.(pageX, pageY, target?.toString(), pressure)
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.warn('Failed to record touch move event:', error)
|
|
112
|
+
}
|
|
48
113
|
}
|
|
49
114
|
|
|
50
115
|
const handleTouchEnd = (event: any) => {
|
|
51
|
-
|
|
116
|
+
if (!context?.client || context.client.sessionState !== SessionState.started) return // SessionState.started
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const { pageX, pageY, target } = event.nativeEvent
|
|
120
|
+
const pressure = event.nativeEvent.force || 1.0
|
|
52
121
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
122
|
+
// Record touch end event automatically
|
|
123
|
+
context.client.recordTouchEnd?.(pageX, pageY, target?.toString(), pressure)
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.warn('Failed to record touch end event:', error)
|
|
126
|
+
}
|
|
56
127
|
}
|
|
57
128
|
|
|
58
129
|
return (
|
|
59
|
-
<View
|
|
130
|
+
<View
|
|
131
|
+
ref={setViewShotRef}
|
|
132
|
+
style={{ flex: 1 }}
|
|
133
|
+
onTouchStart={handleTouchStart}
|
|
134
|
+
onTouchMove={handleTouchMove}
|
|
135
|
+
onTouchEnd={handleTouchEnd}
|
|
136
|
+
>
|
|
60
137
|
{children}
|
|
61
138
|
</View>
|
|
62
139
|
)
|
package/src/index.ts
CHANGED
package/src/otel/helpers.ts
CHANGED
|
@@ -65,7 +65,7 @@ export function processBody(
|
|
|
65
65
|
traceId.startsWith(MULTIPLAYER_TRACE_DEBUG_PREFIX) ||
|
|
66
66
|
traceId.startsWith(MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX)
|
|
67
67
|
) {
|
|
68
|
-
if (masking
|
|
68
|
+
if (masking.isContentMaskingEnabled) {
|
|
69
69
|
requestBody = requestBody && masking.maskBody?.(requestBody, span)
|
|
70
70
|
responseBody = responseBody && masking.maskBody?.(responseBody, span)
|
|
71
71
|
}
|
|
@@ -104,8 +104,8 @@ export function processHeaders(
|
|
|
104
104
|
|
|
105
105
|
// Handle header filtering
|
|
106
106
|
if (
|
|
107
|
-
!masking
|
|
108
|
-
!masking
|
|
107
|
+
!masking.headersToInclude?.length &&
|
|
108
|
+
!masking.headersToExclude?.length
|
|
109
109
|
) {
|
|
110
110
|
// Add null checks to prevent JSON.parse error when headers is undefined
|
|
111
111
|
if (requestHeaders !== undefined && requestHeaders !== null) {
|
|
@@ -115,7 +115,7 @@ export function processHeaders(
|
|
|
115
115
|
responseHeaders = JSON.parse(JSON.stringify(responseHeaders))
|
|
116
116
|
}
|
|
117
117
|
} else {
|
|
118
|
-
if (masking
|
|
118
|
+
if (masking.headersToInclude) {
|
|
119
119
|
const _requestHeaders: Record<string, string> = {}
|
|
120
120
|
const _responseHeaders: Record<string, string> = {}
|
|
121
121
|
|
|
@@ -132,7 +132,7 @@ export function processHeaders(
|
|
|
132
132
|
responseHeaders = _responseHeaders
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
if (masking
|
|
135
|
+
if (masking.headersToExclude?.length) {
|
|
136
136
|
for (const headerName of masking.headersToExclude) {
|
|
137
137
|
delete requestHeaders[headerName]
|
|
138
138
|
delete responseHeaders[headerName]
|
|
@@ -141,8 +141,8 @@ export function processHeaders(
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
// Apply masking
|
|
144
|
-
const maskedRequestHeaders = masking
|
|
145
|
-
const maskedResponseHeaders = masking
|
|
144
|
+
const maskedRequestHeaders = masking.maskHeaders?.(requestHeaders, span) || requestHeaders
|
|
145
|
+
const maskedResponseHeaders = masking.maskHeaders?.(responseHeaders, span) || responseHeaders
|
|
146
146
|
|
|
147
147
|
// Convert to string
|
|
148
148
|
const requestHeadersStr = typeof maskedRequestHeaders === 'string'
|
|
@@ -195,16 +195,27 @@ export function processHttpPayload(
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
/**
|
|
198
|
-
* Converts Headers object to plain object
|
|
198
|
+
* Converts Headers object to plain object
|
|
199
199
|
*/
|
|
200
|
-
export function headersToObject(headers: Record<string, string> | undefined): Record<string, string> {
|
|
200
|
+
export function headersToObject(headers: Headers | Record<string, string> | Record<string, string | string[]> | string[][] | undefined): Record<string, string> {
|
|
201
201
|
const result: Record<string, string> = {}
|
|
202
202
|
|
|
203
203
|
if (!headers) {
|
|
204
204
|
return result
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
if (
|
|
207
|
+
if (headers instanceof Headers) {
|
|
208
|
+
headers.forEach((value: string, key: string) => {
|
|
209
|
+
result[key] = value
|
|
210
|
+
})
|
|
211
|
+
} else if (Array.isArray(headers)) {
|
|
212
|
+
// Handle array of [key, value] pairs
|
|
213
|
+
for (const [key, value] of headers) {
|
|
214
|
+
if (typeof key === 'string' && typeof value === 'string') {
|
|
215
|
+
result[key] = value
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} else if (typeof headers === 'object' && !Array.isArray(headers)) {
|
|
208
219
|
for (const [key, value] of Object.entries(headers)) {
|
|
209
220
|
if (typeof key === 'string' && typeof value === 'string') {
|
|
210
221
|
result[key] = value
|
|
@@ -216,23 +227,29 @@ export function headersToObject(headers: Record<string, string> | undefined): Re
|
|
|
216
227
|
}
|
|
217
228
|
|
|
218
229
|
/**
|
|
219
|
-
* Extracts response body as string
|
|
230
|
+
* Extracts response body as string from Response object
|
|
220
231
|
*/
|
|
221
|
-
export async function extractResponseBody(response:
|
|
222
|
-
if (!response
|
|
232
|
+
export async function extractResponseBody(response: Response): Promise<string | null> {
|
|
233
|
+
if (!response.body) {
|
|
223
234
|
return null
|
|
224
235
|
}
|
|
225
236
|
|
|
226
237
|
try {
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
238
|
+
if (response.body instanceof ReadableStream) {
|
|
239
|
+
// Check if response body is already consumed
|
|
240
|
+
if (response.bodyUsed) {
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const responseClone = response.clone()
|
|
245
|
+
return responseClone.text()
|
|
231
246
|
} else {
|
|
232
|
-
return
|
|
247
|
+
return JSON.stringify(response.body)
|
|
233
248
|
}
|
|
234
249
|
} catch (error) {
|
|
235
|
-
//
|
|
250
|
+
// If cloning fails (body already consumed), return null
|
|
251
|
+
// eslint-disable-next-line no-console
|
|
252
|
+
console.warn('[DEBUGGER_LIB] Failed to extract response body:', error)
|
|
236
253
|
return null
|
|
237
254
|
}
|
|
238
255
|
}
|
|
@@ -254,4 +271,4 @@ export const getExporterEndpoint = (exporterEndpoint: string): string => {
|
|
|
254
271
|
const trimmedExporterEndpoint = new URL(exporterEndpoint).origin
|
|
255
272
|
|
|
256
273
|
return `${trimmedExporterEndpoint}/v1/traces`
|
|
257
|
-
}
|
|
274
|
+
}
|
package/src/otel/index.ts
CHANGED
|
@@ -80,9 +80,13 @@ export class TracerReactNativeSDK {
|
|
|
80
80
|
instrumentations: getInstrumentations(this.config),
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
-
// Initialize React Native specific instrumentations
|
|
84
|
-
this.navigationInstrumentation = new ReactNavigationInstrumentation()
|
|
85
|
-
this.gestureInstrumentation = new GestureInstrumentation()
|
|
83
|
+
// // Initialize React Native specific instrumentations
|
|
84
|
+
// this.navigationInstrumentation = new ReactNavigationInstrumentation()
|
|
85
|
+
// this.gestureInstrumentation = new GestureInstrumentation()
|
|
86
|
+
|
|
87
|
+
// // Enable the custom instrumentations
|
|
88
|
+
// this.navigationInstrumentation.enable()
|
|
89
|
+
// this.gestureInstrumentation.enable()
|
|
86
90
|
|
|
87
91
|
this.isInitialized = true
|
|
88
92
|
}
|
|
@@ -1,34 +1,55 @@
|
|
|
1
|
-
import { TracerReactNativeConfig } from '../../types'
|
|
2
|
-
import { ReactNativeInstrumentation } from './reactNativeInstrumentation'
|
|
3
|
-
import { getMaskingConfig } from '../../config/masking'
|
|
4
1
|
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch'
|
|
5
2
|
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request'
|
|
6
3
|
|
|
4
|
+
import { OTEL_IGNORE_URLS } from '../../config'
|
|
5
|
+
import { TracerReactNativeConfig } from '../../types'
|
|
6
|
+
import { extractResponseBody, headersToObject, processHttpPayload } from '../helpers'
|
|
7
|
+
|
|
7
8
|
export function getInstrumentations(config: TracerReactNativeConfig) {
|
|
8
|
-
|
|
9
|
+
|
|
9
10
|
const instrumentations = []
|
|
10
11
|
|
|
11
12
|
// Fetch instrumentation
|
|
12
13
|
try {
|
|
13
14
|
instrumentations.push(
|
|
14
15
|
new FetchInstrumentation({
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
clearTimingResources: false,
|
|
17
|
+
ignoreUrls: [
|
|
18
|
+
...OTEL_IGNORE_URLS,
|
|
19
|
+
...(config.ignoreUrls || []),
|
|
20
|
+
],
|
|
21
|
+
propagateTraceHeaderCorsUrls: config.propagateTraceHeaderCorsUrls,
|
|
22
|
+
applyCustomAttributesOnSpan: async (span, request, response) => {
|
|
23
|
+
if (!config) return
|
|
24
|
+
|
|
25
|
+
const { captureBody, captureHeaders } = config
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
if (!captureBody && !captureHeaders) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const requestBody = request.body
|
|
33
|
+
const requestHeaders = headersToObject(request.headers)
|
|
34
|
+
const responseHeaders = headersToObject(response instanceof Response ? response.headers : undefined)
|
|
35
|
+
|
|
36
|
+
let responseBody: string | null = null
|
|
37
|
+
if (response instanceof Response && response.body) {
|
|
38
|
+
responseBody = await extractResponseBody(response)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const payload = {
|
|
42
|
+
requestBody,
|
|
43
|
+
responseBody,
|
|
44
|
+
requestHeaders,
|
|
45
|
+
responseHeaders,
|
|
29
46
|
}
|
|
47
|
+
processHttpPayload(payload, config, span)
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// eslint-disable-next-line
|
|
50
|
+
console.error('[DEBUGGER_LIB] Failed to capture fetch payload', error)
|
|
30
51
|
}
|
|
31
|
-
}
|
|
52
|
+
},
|
|
32
53
|
})
|
|
33
54
|
)
|
|
34
55
|
} catch (error) {
|
|
@@ -39,21 +60,41 @@ export function getInstrumentations(config: TracerReactNativeConfig) {
|
|
|
39
60
|
try {
|
|
40
61
|
instrumentations.push(
|
|
41
62
|
new XMLHttpRequestInstrumentation({
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
clearTimingResources: false,
|
|
64
|
+
ignoreUrls: [
|
|
65
|
+
...OTEL_IGNORE_URLS,
|
|
66
|
+
...(config.ignoreUrls || []),
|
|
67
|
+
],
|
|
68
|
+
propagateTraceHeaderCorsUrls: config.propagateTraceHeaderCorsUrls,
|
|
69
|
+
applyCustomAttributesOnSpan: (span, xhr) => {
|
|
70
|
+
if (!config) return
|
|
71
|
+
|
|
72
|
+
const { captureBody, captureHeaders } = config
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
if (!captureBody && !captureHeaders) {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
const requestBody = xhr.networkRequest.requestBody
|
|
81
|
+
// @ts-ignore
|
|
82
|
+
const responseBody = xhr.networkRequest.responseBody
|
|
83
|
+
// @ts-ignore
|
|
84
|
+
const requestHeaders = xhr.networkRequest.requestHeaders || {}
|
|
85
|
+
// @ts-ignore
|
|
86
|
+
const responseHeaders = xhr.networkRequest.responseHeaders || {}
|
|
87
|
+
|
|
88
|
+
const payload = {
|
|
89
|
+
requestBody,
|
|
90
|
+
responseBody,
|
|
91
|
+
requestHeaders,
|
|
92
|
+
responseHeaders,
|
|
56
93
|
}
|
|
94
|
+
processHttpPayload(payload, config, span)
|
|
95
|
+
} catch (error) {
|
|
96
|
+
// eslint-disable-next-line
|
|
97
|
+
console.error('[DEBUGGER_LIB] Failed to capture xml-http payload', error)
|
|
57
98
|
}
|
|
58
99
|
},
|
|
59
100
|
})
|
|
@@ -63,11 +104,11 @@ export function getInstrumentations(config: TracerReactNativeConfig) {
|
|
|
63
104
|
}
|
|
64
105
|
|
|
65
106
|
// Custom React Native instrumentations
|
|
66
|
-
try {
|
|
67
|
-
|
|
68
|
-
} catch (error) {
|
|
69
|
-
|
|
70
|
-
}
|
|
107
|
+
// try {
|
|
108
|
+
// instrumentations.push(new ReactNativeInstrumentation())
|
|
109
|
+
// } catch (error) {
|
|
110
|
+
// console.warn('React Native instrumentation not available:', error)
|
|
111
|
+
// }
|
|
71
112
|
|
|
72
113
|
return instrumentations
|
|
73
114
|
}
|
|
@@ -12,6 +12,11 @@ export class ReactNavigationInstrumentation extends InstrumentationBase {
|
|
|
12
12
|
// Initialize the instrumentation
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
enable(): void {
|
|
16
|
+
// Enable the instrumentation
|
|
17
|
+
super.enable()
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
setNavigationRef(ref: any) {
|
|
16
21
|
this.navigationRef = ref
|
|
17
22
|
this._setupNavigationListener()
|