@multiplayer-app/session-recorder-react-native 0.0.1-alpha.6 → 0.0.1-alpha.8
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/components/GestureCaptureWrapper.d.ts +6 -0
- package/dist/components/GestureCaptureWrapper.js +1 -0
- package/dist/components/GestureCaptureWrapper.js.map +1 -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.d.ts +1 -2
- 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/reactNativeInstrumentation.js +1 -1
- package/dist/otel/instrumentations/reactNativeInstrumentation.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/gestureHandlerRecorder.d.ts +19 -0
- package/dist/recorder/gestureHandlerRecorder.js +1 -0
- package/dist/recorder/gestureHandlerRecorder.js.map +1 -0
- package/dist/recorder/gestureRecorder.d.ts +69 -3
- package/dist/recorder/gestureRecorder.js +1 -1
- package/dist/recorder/gestureRecorder.js.map +1 -1
- package/dist/recorder/index.d.ts +59 -6
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.js.map +1 -1
- package/dist/recorder/navigationTracker.js +1 -1
- package/dist/recorder/navigationTracker.js.map +1 -1
- package/dist/recorder/screenRecorder.d.ts +83 -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 +42 -2
- package/dist/session-recorder.js +1 -1
- package/dist/session-recorder.js.map +1 -1
- package/dist/types/index.d.ts +32 -0
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/rrweb.d.ts +118 -0
- package/dist/types/rrweb.js +1 -0
- package/dist/types/rrweb.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/logger.d.ts +112 -0
- package/dist/utils/logger.js +1 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/rrweb-events.d.ts +65 -0
- package/dist/utils/rrweb-events.js +1 -0
- package/dist/utils/rrweb-events.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 +5 -2
- package/src/components/GestureCaptureWrapper.tsx +110 -0
- package/src/config/constants.ts +3 -3
- package/src/context/SessionRecorderContext.tsx +106 -34
- package/src/index.ts +1 -0
- package/src/otel/helpers.ts +38 -20
- package/src/otel/index.ts +7 -3
- package/src/otel/instrumentations/index.ts +82 -40
- package/src/otel/instrumentations/reactNativeInstrumentation.ts +2 -1
- package/src/otel/instrumentations/reactNavigationInstrumentation.ts +5 -0
- package/src/recorder/eventExporter.ts +141 -0
- package/src/recorder/gestureHandlerRecorder.ts +157 -0
- package/src/recorder/gestureRecorder.ts +198 -3
- package/src/recorder/index.ts +130 -24
- package/src/recorder/navigationTracker.ts +2 -0
- package/src/recorder/screenRecorder.ts +261 -22
- package/src/services/api.service.ts +1 -8
- package/src/services/storage.service.ts +1 -0
- package/src/session-recorder.ts +97 -11
- package/src/types/index.ts +45 -1
- package/src/utils/index.ts +2 -0
- package/src/utils/logger.ts +225 -0
- package/src/utils/rrweb-events.ts +311 -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.8",
|
|
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,11 +78,14 @@
|
|
|
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",
|
|
82
|
+
"@rrweb/types": "^2.0.0-alpha.18",
|
|
81
83
|
"lib0": "0.2.82",
|
|
82
84
|
"react-native-gesture-handler": "^2.14.0",
|
|
83
85
|
"react-native-mmkv": "^2.11.0",
|
|
84
86
|
"react-native-reanimated": "^3.6.0",
|
|
85
87
|
"react-native-view-shot": "^4.0.3",
|
|
88
|
+
"rrweb": "^2.0.0-alpha.15",
|
|
86
89
|
"socket.io-client": "4.7.5"
|
|
87
90
|
},
|
|
88
91
|
"peerDependencies": {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import React, { ReactNode, useCallback, useEffect, useMemo, useRef } from 'react'
|
|
2
|
+
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'
|
|
3
|
+
import { GestureInstrumentation } from '../otel/instrumentations/gestureInstrumentation'
|
|
4
|
+
import { logger } from '../utils'
|
|
5
|
+
|
|
6
|
+
export interface GestureCaptureWrapperProps {
|
|
7
|
+
children: ReactNode
|
|
8
|
+
onGestureRecord: (gestureType: string, data: any) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const GestureCaptureWrapper: React.FC<GestureCaptureWrapperProps> = ({ children, onGestureRecord }) => {
|
|
12
|
+
const gestureInstrumentation = useRef(new GestureInstrumentation())
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
gestureInstrumentation.current.enable()
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
const recordGesture = useCallback(
|
|
19
|
+
(gestureType: string, data: any) => {
|
|
20
|
+
// Record with OpenTelemetry
|
|
21
|
+
logger.debug('GestureCaptureWrapper', 'Recording gesture', { gestureType, data })
|
|
22
|
+
switch (gestureType) {
|
|
23
|
+
case 'tap':
|
|
24
|
+
gestureInstrumentation.current.recordTap(data.x, data.y)
|
|
25
|
+
break
|
|
26
|
+
case 'pan_start':
|
|
27
|
+
case 'pan_update':
|
|
28
|
+
case 'pan_end':
|
|
29
|
+
gestureInstrumentation.current.recordPan(data.translationX || 0, data.translationY || 0)
|
|
30
|
+
break
|
|
31
|
+
case 'long_press':
|
|
32
|
+
gestureInstrumentation.current.recordLongPress(data.duration, undefined)
|
|
33
|
+
break
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Record with session recorder
|
|
37
|
+
onGestureRecord(gestureType, data)
|
|
38
|
+
},
|
|
39
|
+
[onGestureRecord]
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
// Create tap gesture
|
|
43
|
+
const tapGesture = useMemo(() => {
|
|
44
|
+
return Gesture.Tap()
|
|
45
|
+
.runOnJS(true)
|
|
46
|
+
.onStart((event) => {
|
|
47
|
+
recordGesture('tap', {
|
|
48
|
+
x: event.x,
|
|
49
|
+
y: event.y,
|
|
50
|
+
timestamp: Date.now()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
}, [recordGesture])
|
|
54
|
+
|
|
55
|
+
// Create pan gesture (for swipes and drags)
|
|
56
|
+
const panGesture = useMemo(() => {
|
|
57
|
+
return Gesture.Pan()
|
|
58
|
+
.runOnJS(true)
|
|
59
|
+
.onStart((event) => {
|
|
60
|
+
recordGesture('pan_start', {
|
|
61
|
+
x: event.x,
|
|
62
|
+
y: event.y,
|
|
63
|
+
timestamp: Date.now()
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
.onUpdate((event) => {
|
|
67
|
+
recordGesture('pan_update', {
|
|
68
|
+
x: event.x,
|
|
69
|
+
y: event.y,
|
|
70
|
+
translationX: event.translationX,
|
|
71
|
+
translationY: event.translationY,
|
|
72
|
+
velocityX: event.velocityX,
|
|
73
|
+
velocityY: event.velocityY,
|
|
74
|
+
timestamp: Date.now()
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
.onEnd((event) => {
|
|
78
|
+
recordGesture('pan_end', {
|
|
79
|
+
x: event.x,
|
|
80
|
+
y: event.y,
|
|
81
|
+
translationX: event.translationX,
|
|
82
|
+
translationY: event.translationY,
|
|
83
|
+
velocityX: event.velocityX,
|
|
84
|
+
velocityY: event.velocityY,
|
|
85
|
+
timestamp: Date.now()
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
}, [recordGesture])
|
|
89
|
+
|
|
90
|
+
// Create long press gesture
|
|
91
|
+
const longPressGesture = useMemo(() => {
|
|
92
|
+
return Gesture.LongPress()
|
|
93
|
+
.runOnJS(true)
|
|
94
|
+
.minDuration(500)
|
|
95
|
+
.onStart((event) => {
|
|
96
|
+
recordGesture('long_press', {
|
|
97
|
+
x: event.x,
|
|
98
|
+
y: event.y,
|
|
99
|
+
duration: 500,
|
|
100
|
+
timestamp: Date.now()
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
}, [recordGesture])
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
107
|
+
<GestureDetector gesture={Gesture.Simultaneous(tapGesture, panGesture, longPressGesture)}>{children}</GestureDetector>
|
|
108
|
+
</GestureHandlerRootView>
|
|
109
|
+
)
|
|
110
|
+
}
|
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,64 +1,136 @@
|
|
|
1
|
-
import React, { createContext, useContext, ReactNode, PropsWithChildren,
|
|
2
|
-
import { View } from 'react-native'
|
|
3
|
-
import { SessionRecorderOptions } from '../types'
|
|
1
|
+
import React, { createContext, useContext, ReactNode, PropsWithChildren, useState, useEffect, useRef, useCallback } from 'react'
|
|
2
|
+
import { Pressable, Text, View } from 'react-native'
|
|
3
|
+
import { SessionRecorderOptions, SessionState } from '../types'
|
|
4
4
|
import SessionRecorder from '../session-recorder'
|
|
5
|
+
import { GestureCaptureWrapper } from '../components/GestureCaptureWrapper'
|
|
6
|
+
import sessionRecorder from '../session-recorder'
|
|
7
|
+
import { logger } from '../utils'
|
|
5
8
|
|
|
6
9
|
interface SessionRecorderContextType {
|
|
7
|
-
|
|
10
|
+
instance: typeof SessionRecorder
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
const SessionRecorderContext = createContext<SessionRecorderContextType | null>(null)
|
|
11
14
|
|
|
12
15
|
export interface SessionRecorderProviderProps extends PropsWithChildren {
|
|
13
16
|
options: SessionRecorderOptions
|
|
14
|
-
client?: typeof SessionRecorder
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
export const SessionRecorderProvider: React.FC<SessionRecorderProviderProps> = ({ children,
|
|
18
|
-
const
|
|
19
|
-
|
|
19
|
+
export const SessionRecorderProvider: React.FC<SessionRecorderProviderProps> = ({ children, options }) => {
|
|
20
|
+
const [sessionState, setSessionState] = useState<SessionState | null>(null)
|
|
21
|
+
const optionsRef = useRef<string>()
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const newOptions = JSON.stringify(options)
|
|
25
|
+
if (optionsRef.current === JSON.stringify(options)) return
|
|
26
|
+
optionsRef.current = newOptions
|
|
20
27
|
SessionRecorder.init(options)
|
|
21
|
-
|
|
28
|
+
}, [options])
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
setSessionState(SessionRecorder.sessionState)
|
|
32
|
+
SessionRecorder.on('state-change', (state: SessionState) => {
|
|
33
|
+
setSessionState(state)
|
|
34
|
+
})
|
|
22
35
|
}, [])
|
|
23
36
|
|
|
37
|
+
const onToggleSession = () => {
|
|
38
|
+
if (SessionRecorder.sessionState === SessionState.started) {
|
|
39
|
+
SessionRecorder.stop()
|
|
40
|
+
} else {
|
|
41
|
+
SessionRecorder.start()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
24
45
|
return (
|
|
25
|
-
<SessionRecorderContext.Provider value={{
|
|
26
|
-
<
|
|
46
|
+
<SessionRecorderContext.Provider value={{ instance: sessionRecorder }}>
|
|
47
|
+
<GestureEventCapture>
|
|
48
|
+
{children}
|
|
49
|
+
<Pressable onPress={onToggleSession}>
|
|
50
|
+
<View
|
|
51
|
+
style={{
|
|
52
|
+
position: 'absolute',
|
|
53
|
+
right: 0,
|
|
54
|
+
bottom: 100,
|
|
55
|
+
width: 48,
|
|
56
|
+
height: 48,
|
|
57
|
+
paddingTop: 16,
|
|
58
|
+
paddingLeft: 10,
|
|
59
|
+
backgroundColor: 'red',
|
|
60
|
+
borderTopLeftRadius: 24,
|
|
61
|
+
borderBottomLeftRadius: 24
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
<Text style={{ color: 'white' }}>{sessionState === SessionState.started ? 'Stop' : 'Start'}</Text>
|
|
65
|
+
</View>
|
|
66
|
+
</Pressable>
|
|
67
|
+
</GestureEventCapture>
|
|
27
68
|
</SessionRecorderContext.Provider>
|
|
28
69
|
)
|
|
29
70
|
}
|
|
30
71
|
|
|
31
|
-
//
|
|
32
|
-
const
|
|
33
|
-
|
|
72
|
+
// Gesture-based event capture component
|
|
73
|
+
const GestureEventCapture: React.FC<{ children: ReactNode }> = ({ children }) => {
|
|
74
|
+
// Set up gesture recording callback
|
|
75
|
+
const handleGestureRecord = useCallback((gestureType: string, data: any) => {
|
|
76
|
+
if (SessionRecorder.sessionState !== SessionState.started) {
|
|
77
|
+
logger.debug('SessionRecorderContext', 'Gesture recording skipped', {
|
|
78
|
+
client: !!SessionRecorder.sessionState,
|
|
79
|
+
sessionState: SessionRecorder.sessionState
|
|
80
|
+
})
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
logger.debug('SessionRecorderContext', 'Gesture recorded', { gestureType, data })
|
|
84
|
+
try {
|
|
85
|
+
// Record gesture as appropriate touch events
|
|
86
|
+
switch (gestureType) {
|
|
87
|
+
case 'tap':
|
|
88
|
+
// For tap, record both touch start and end
|
|
89
|
+
logger.debug('SessionRecorderContext', 'Recording tap as touch start + end')
|
|
90
|
+
SessionRecorder.recordTouchStart?.(data.x, data.y, undefined, 1.0)
|
|
91
|
+
SessionRecorder.recordTouchEnd?.(data.x, data.y, undefined, 1.0)
|
|
92
|
+
break
|
|
34
93
|
|
|
35
|
-
|
|
36
|
-
|
|
94
|
+
case 'pan_start':
|
|
95
|
+
logger.debug('SessionRecorderContext', 'Recording pan_start as touch start')
|
|
96
|
+
SessionRecorder.recordTouchStart?.(data.x, data.y, undefined, 1.0)
|
|
97
|
+
break
|
|
37
98
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const handleTouchMove = (event: any) => {
|
|
43
|
-
// if (!context?.isRecording) return
|
|
99
|
+
case 'pan_update':
|
|
100
|
+
logger.debug('SessionRecorderContext', 'Recording pan_update as touch move')
|
|
101
|
+
SessionRecorder.recordTouchMove?.(data.x, data.y, undefined, 1.0)
|
|
102
|
+
break
|
|
44
103
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
104
|
+
case 'pan_end':
|
|
105
|
+
logger.debug('SessionRecorderContext', 'Recording pan_end as touch end')
|
|
106
|
+
SessionRecorder.recordTouchEnd?.(data.x, data.y, undefined, 1.0)
|
|
107
|
+
break
|
|
49
108
|
|
|
50
|
-
|
|
51
|
-
|
|
109
|
+
case 'long_press':
|
|
110
|
+
logger.debug('SessionRecorderContext', 'Recording long_press as touch start + end')
|
|
111
|
+
SessionRecorder.recordTouchStart?.(data.x, data.y, undefined, 1.0)
|
|
112
|
+
SessionRecorder.recordTouchEnd?.(data.x, data.y, undefined, 1.0)
|
|
113
|
+
break
|
|
114
|
+
default:
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
logger.error('SessionRecorderContext', 'Failed to record gesture event', error)
|
|
118
|
+
}
|
|
119
|
+
}, [])
|
|
52
120
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
121
|
+
// Callback ref to set the viewshot ref immediately when available
|
|
122
|
+
const setViewShotRef = (ref: View | null) => {
|
|
123
|
+
if (ref) {
|
|
124
|
+
SessionRecorder.setViewShotRef?.(ref)
|
|
125
|
+
}
|
|
56
126
|
}
|
|
57
127
|
|
|
58
128
|
return (
|
|
59
|
-
<
|
|
60
|
-
{
|
|
61
|
-
|
|
129
|
+
<GestureCaptureWrapper onGestureRecord={handleGestureRecord}>
|
|
130
|
+
<View ref={setViewShotRef} style={{ flex: 1 }}>
|
|
131
|
+
{children}
|
|
132
|
+
</View>
|
|
133
|
+
</GestureCaptureWrapper>
|
|
62
134
|
)
|
|
63
135
|
}
|
|
64
136
|
|
package/src/index.ts
CHANGED
package/src/otel/helpers.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ATTR_MULTIPLAYER_HTTP_RESPONSE_BODY,
|
|
8
8
|
ATTR_MULTIPLAYER_HTTP_RESPONSE_HEADERS,
|
|
9
9
|
} from '@multiplayer-app/session-recorder-common'
|
|
10
|
+
import { logger } from '../utils'
|
|
10
11
|
import { SessionRecorderSdk } from '@multiplayer-app/session-recorder-common'
|
|
11
12
|
import { TracerReactNativeConfig } from '../types'
|
|
12
13
|
|
|
@@ -65,7 +66,7 @@ export function processBody(
|
|
|
65
66
|
traceId.startsWith(MULTIPLAYER_TRACE_DEBUG_PREFIX) ||
|
|
66
67
|
traceId.startsWith(MULTIPLAYER_TRACE_CONTINUOUS_DEBUG_PREFIX)
|
|
67
68
|
) {
|
|
68
|
-
if (masking
|
|
69
|
+
if (masking.isContentMaskingEnabled) {
|
|
69
70
|
requestBody = requestBody && masking.maskBody?.(requestBody, span)
|
|
70
71
|
responseBody = responseBody && masking.maskBody?.(responseBody, span)
|
|
71
72
|
}
|
|
@@ -104,8 +105,8 @@ export function processHeaders(
|
|
|
104
105
|
|
|
105
106
|
// Handle header filtering
|
|
106
107
|
if (
|
|
107
|
-
!masking
|
|
108
|
-
!masking
|
|
108
|
+
!masking.headersToInclude?.length &&
|
|
109
|
+
!masking.headersToExclude?.length
|
|
109
110
|
) {
|
|
110
111
|
// Add null checks to prevent JSON.parse error when headers is undefined
|
|
111
112
|
if (requestHeaders !== undefined && requestHeaders !== null) {
|
|
@@ -115,7 +116,7 @@ export function processHeaders(
|
|
|
115
116
|
responseHeaders = JSON.parse(JSON.stringify(responseHeaders))
|
|
116
117
|
}
|
|
117
118
|
} else {
|
|
118
|
-
if (masking
|
|
119
|
+
if (masking.headersToInclude) {
|
|
119
120
|
const _requestHeaders: Record<string, string> = {}
|
|
120
121
|
const _responseHeaders: Record<string, string> = {}
|
|
121
122
|
|
|
@@ -132,7 +133,7 @@ export function processHeaders(
|
|
|
132
133
|
responseHeaders = _responseHeaders
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
if (masking
|
|
136
|
+
if (masking.headersToExclude?.length) {
|
|
136
137
|
for (const headerName of masking.headersToExclude) {
|
|
137
138
|
delete requestHeaders[headerName]
|
|
138
139
|
delete responseHeaders[headerName]
|
|
@@ -141,8 +142,8 @@ export function processHeaders(
|
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
// Apply masking
|
|
144
|
-
const maskedRequestHeaders = masking
|
|
145
|
-
const maskedResponseHeaders = masking
|
|
145
|
+
const maskedRequestHeaders = masking.maskHeaders?.(requestHeaders, span) || requestHeaders
|
|
146
|
+
const maskedResponseHeaders = masking.maskHeaders?.(responseHeaders, span) || responseHeaders
|
|
146
147
|
|
|
147
148
|
// Convert to string
|
|
148
149
|
const requestHeadersStr = typeof maskedRequestHeaders === 'string'
|
|
@@ -195,16 +196,27 @@ export function processHttpPayload(
|
|
|
195
196
|
}
|
|
196
197
|
|
|
197
198
|
/**
|
|
198
|
-
* Converts Headers object to plain object
|
|
199
|
+
* Converts Headers object to plain object
|
|
199
200
|
*/
|
|
200
|
-
export function headersToObject(headers: Record<string, string> | undefined): Record<string, string> {
|
|
201
|
+
export function headersToObject(headers: Headers | Record<string, string> | Record<string, string | string[]> | string[][] | undefined): Record<string, string> {
|
|
201
202
|
const result: Record<string, string> = {}
|
|
202
203
|
|
|
203
204
|
if (!headers) {
|
|
204
205
|
return result
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
if (
|
|
208
|
+
if (headers instanceof Headers) {
|
|
209
|
+
headers.forEach((value: string, key: string) => {
|
|
210
|
+
result[key] = value
|
|
211
|
+
})
|
|
212
|
+
} else if (Array.isArray(headers)) {
|
|
213
|
+
// Handle array of [key, value] pairs
|
|
214
|
+
for (const [key, value] of headers) {
|
|
215
|
+
if (typeof key === 'string' && typeof value === 'string') {
|
|
216
|
+
result[key] = value
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} else if (typeof headers === 'object' && !Array.isArray(headers)) {
|
|
208
220
|
for (const [key, value] of Object.entries(headers)) {
|
|
209
221
|
if (typeof key === 'string' && typeof value === 'string') {
|
|
210
222
|
result[key] = value
|
|
@@ -216,23 +228,29 @@ export function headersToObject(headers: Record<string, string> | undefined): Re
|
|
|
216
228
|
}
|
|
217
229
|
|
|
218
230
|
/**
|
|
219
|
-
* Extracts response body as string
|
|
231
|
+
* Extracts response body as string from Response object
|
|
220
232
|
*/
|
|
221
|
-
export async function extractResponseBody(response:
|
|
222
|
-
if (!response
|
|
233
|
+
export async function extractResponseBody(response: Response): Promise<string | null> {
|
|
234
|
+
if (!response.body) {
|
|
223
235
|
return null
|
|
224
236
|
}
|
|
225
237
|
|
|
226
238
|
try {
|
|
227
|
-
if (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
239
|
+
if (response.body instanceof ReadableStream) {
|
|
240
|
+
// Check if response body is already consumed
|
|
241
|
+
if (response.bodyUsed) {
|
|
242
|
+
return null
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const responseClone = response.clone()
|
|
246
|
+
return responseClone.text()
|
|
231
247
|
} else {
|
|
232
|
-
return
|
|
248
|
+
return JSON.stringify(response.body)
|
|
233
249
|
}
|
|
234
250
|
} catch (error) {
|
|
235
|
-
//
|
|
251
|
+
// If cloning fails (body already consumed), return null
|
|
252
|
+
// eslint-disable-next-line no-console
|
|
253
|
+
logger.warn('DEBUGGER_LIB', 'Failed to extract response body', error)
|
|
236
254
|
return null
|
|
237
255
|
}
|
|
238
256
|
}
|
|
@@ -254,4 +272,4 @@ export const getExporterEndpoint = (exporterEndpoint: string): string => {
|
|
|
254
272
|
const trimmedExporterEndpoint = new URL(exporterEndpoint).origin
|
|
255
273
|
|
|
256
274
|
return `${trimmedExporterEndpoint}/v1/traces`
|
|
257
|
-
}
|
|
275
|
+
}
|
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
|
}
|