@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.
Files changed (90) hide show
  1. package/RRWEB_INTEGRATION.md +336 -0
  2. package/VIEWSHOT_INTEGRATION_TEST.md +123 -0
  3. package/copy-react-native-dist.sh +38 -0
  4. package/dist/components/GestureCaptureWrapper.d.ts +6 -0
  5. package/dist/components/GestureCaptureWrapper.js +1 -0
  6. package/dist/components/GestureCaptureWrapper.js.map +1 -0
  7. package/dist/config/constants.d.ts +0 -1
  8. package/dist/config/constants.js +1 -1
  9. package/dist/config/constants.js.map +1 -1
  10. package/dist/context/SessionRecorderContext.d.ts +1 -2
  11. package/dist/context/SessionRecorderContext.js +1 -1
  12. package/dist/context/SessionRecorderContext.js.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/otel/helpers.d.ts +4 -4
  15. package/dist/otel/helpers.js +1 -1
  16. package/dist/otel/helpers.js.map +1 -1
  17. package/dist/otel/index.js +1 -1
  18. package/dist/otel/index.js.map +1 -1
  19. package/dist/otel/instrumentations/index.d.ts +2 -3
  20. package/dist/otel/instrumentations/index.js +1 -1
  21. package/dist/otel/instrumentations/index.js.map +1 -1
  22. package/dist/otel/instrumentations/reactNativeInstrumentation.js +1 -1
  23. package/dist/otel/instrumentations/reactNativeInstrumentation.js.map +1 -1
  24. package/dist/otel/instrumentations/reactNavigationInstrumentation.d.ts +1 -0
  25. package/dist/otel/instrumentations/reactNavigationInstrumentation.js +1 -1
  26. package/dist/otel/instrumentations/reactNavigationInstrumentation.js.map +1 -1
  27. package/dist/recorder/eventExporter.d.ts +21 -0
  28. package/dist/recorder/eventExporter.js +1 -0
  29. package/dist/recorder/eventExporter.js.map +1 -0
  30. package/dist/recorder/gestureHandlerRecorder.d.ts +19 -0
  31. package/dist/recorder/gestureHandlerRecorder.js +1 -0
  32. package/dist/recorder/gestureHandlerRecorder.js.map +1 -0
  33. package/dist/recorder/gestureRecorder.d.ts +69 -3
  34. package/dist/recorder/gestureRecorder.js +1 -1
  35. package/dist/recorder/gestureRecorder.js.map +1 -1
  36. package/dist/recorder/index.d.ts +59 -6
  37. package/dist/recorder/index.js +1 -1
  38. package/dist/recorder/index.js.map +1 -1
  39. package/dist/recorder/navigationTracker.js +1 -1
  40. package/dist/recorder/navigationTracker.js.map +1 -1
  41. package/dist/recorder/screenRecorder.d.ts +83 -4
  42. package/dist/recorder/screenRecorder.js +1 -1
  43. package/dist/recorder/screenRecorder.js.map +1 -1
  44. package/dist/services/api.service.js.map +1 -1
  45. package/dist/services/storage.service.js.map +1 -1
  46. package/dist/session-recorder.d.ts +42 -2
  47. package/dist/session-recorder.js +1 -1
  48. package/dist/session-recorder.js.map +1 -1
  49. package/dist/types/index.d.ts +32 -0
  50. package/dist/types/index.js +1 -1
  51. package/dist/types/index.js.map +1 -1
  52. package/dist/types/rrweb.d.ts +118 -0
  53. package/dist/types/rrweb.js +1 -0
  54. package/dist/types/rrweb.js.map +1 -0
  55. package/dist/utils/index.d.ts +2 -0
  56. package/dist/utils/index.js +1 -1
  57. package/dist/utils/index.js.map +1 -1
  58. package/dist/utils/logger.d.ts +112 -0
  59. package/dist/utils/logger.js +1 -0
  60. package/dist/utils/logger.js.map +1 -0
  61. package/dist/utils/rrweb-events.d.ts +65 -0
  62. package/dist/utils/rrweb-events.js +1 -0
  63. package/dist/utils/rrweb-events.js.map +1 -0
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/example-usage.tsx +174 -0
  67. package/package.json +5 -2
  68. package/src/components/GestureCaptureWrapper.tsx +110 -0
  69. package/src/config/constants.ts +3 -3
  70. package/src/context/SessionRecorderContext.tsx +106 -34
  71. package/src/index.ts +1 -0
  72. package/src/otel/helpers.ts +38 -20
  73. package/src/otel/index.ts +7 -3
  74. package/src/otel/instrumentations/index.ts +82 -40
  75. package/src/otel/instrumentations/reactNativeInstrumentation.ts +2 -1
  76. package/src/otel/instrumentations/reactNavigationInstrumentation.ts +5 -0
  77. package/src/recorder/eventExporter.ts +141 -0
  78. package/src/recorder/gestureHandlerRecorder.ts +157 -0
  79. package/src/recorder/gestureRecorder.ts +198 -3
  80. package/src/recorder/index.ts +130 -24
  81. package/src/recorder/navigationTracker.ts +2 -0
  82. package/src/recorder/screenRecorder.ts +261 -22
  83. package/src/services/api.service.ts +1 -8
  84. package/src/services/storage.service.ts +1 -0
  85. package/src/session-recorder.ts +97 -11
  86. package/src/types/index.ts +45 -1
  87. package/src/utils/index.ts +2 -0
  88. package/src/utils/logger.ts +225 -0
  89. package/src/utils/rrweb-events.ts +311 -0
  90. 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.6",
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
+ }
@@ -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, useMemo } from 'react'
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
- client: typeof SessionRecorder
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, client, options }) => {
18
- const sessionRecorder = useMemo(() => {
19
- if (client) return client
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
- return SessionRecorder
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={{ client: sessionRecorder }}>
26
- <TouchEventCapture>{children}</TouchEventCapture>
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
- // Touch event capture component
32
- const TouchEventCapture: React.FC<{ children: ReactNode }> = ({ children }) => {
33
- const context = useContext(SessionRecorderContext)
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
- const handleTouchStart = (event: any) => {
36
- // if (!context?.isRecording) return
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
- const { pageX, pageY, target } = event.nativeEvent
39
- // context.client.recordTap(pageX, pageY, target?.toString())
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
- const { pageX, pageY, target } = event.nativeEvent
46
- // Record pan gesture
47
- // context.gestureRecorder.recordPan(pageX, pageY, target?.toString())
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
- const handleTouchEnd = (event: any) => {
51
- // if (!context?.isRecording) return
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
- const { pageX, pageY, target } = event.nativeEvent
54
- // Record tap end
55
- // context.gestureRecorder.recordTap(pageX, pageY, target?.toString())
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
- <View style={{ flex: 1 }} onTouchStart={handleTouchStart} onTouchMove={handleTouchMove} onTouchEnd={handleTouchEnd}>
60
- {children}
61
- </View>
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
@@ -3,6 +3,7 @@ import SessionRecorder from './session-recorder'
3
3
  export * from '@multiplayer-app/session-recorder-common'
4
4
  export * from './context/SessionRecorderContext'
5
5
 
6
+
6
7
  export { SessionRecorder }
7
8
  // Export the instance as default
8
9
  export default SessionRecorder
@@ -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?.isContentMaskingEnabled) {
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?.headersToInclude?.length &&
108
- !masking?.headersToExclude?.length
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?.headersToInclude) {
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?.headersToExclude?.length) {
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?.maskHeaders?.(requestHeaders, span) || requestHeaders
145
- const maskedResponseHeaders = masking?.maskHeaders?.(responseHeaders, span) || responseHeaders
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 (React Native compatible)
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 (typeof headers === 'object' && !Array.isArray(headers)) {
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 (React Native compatible)
231
+ * Extracts response body as string from Response object
220
232
  */
221
- export async function extractResponseBody(response: any): Promise<string | null> {
222
- if (!response || !response.body) {
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 (typeof response.body === 'string') {
228
- return response.body
229
- } else if (typeof response.body === 'object') {
230
- return JSON.stringify(response.body)
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 String(response.body)
248
+ return JSON.stringify(response.body)
233
249
  }
234
250
  } catch (error) {
235
- // Failed to extract response body - silently continue
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
  }