@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.
Files changed (64) 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/config/constants.d.ts +0 -1
  5. package/dist/config/constants.js +1 -1
  6. package/dist/config/constants.js.map +1 -1
  7. package/dist/context/SessionRecorderContext.js +1 -1
  8. package/dist/context/SessionRecorderContext.js.map +1 -1
  9. package/dist/index.js.map +1 -1
  10. package/dist/otel/helpers.d.ts +4 -4
  11. package/dist/otel/helpers.js +1 -1
  12. package/dist/otel/helpers.js.map +1 -1
  13. package/dist/otel/index.js +1 -1
  14. package/dist/otel/index.js.map +1 -1
  15. package/dist/otel/instrumentations/index.d.ts +2 -3
  16. package/dist/otel/instrumentations/index.js +1 -1
  17. package/dist/otel/instrumentations/index.js.map +1 -1
  18. package/dist/otel/instrumentations/reactNavigationInstrumentation.d.ts +1 -0
  19. package/dist/otel/instrumentations/reactNavigationInstrumentation.js +1 -1
  20. package/dist/otel/instrumentations/reactNavigationInstrumentation.js.map +1 -1
  21. package/dist/recorder/eventExporter.d.ts +21 -0
  22. package/dist/recorder/eventExporter.js +1 -0
  23. package/dist/recorder/eventExporter.js.map +1 -0
  24. package/dist/recorder/gestureRecorder.d.ts +57 -3
  25. package/dist/recorder/gestureRecorder.js +1 -1
  26. package/dist/recorder/gestureRecorder.js.map +1 -1
  27. package/dist/recorder/index.d.ts +61 -7
  28. package/dist/recorder/index.js +1 -1
  29. package/dist/recorder/index.js.map +1 -1
  30. package/dist/recorder/screenRecorder.d.ts +58 -4
  31. package/dist/recorder/screenRecorder.js +1 -1
  32. package/dist/recorder/screenRecorder.js.map +1 -1
  33. package/dist/services/api.service.js.map +1 -1
  34. package/dist/services/storage.service.js.map +1 -1
  35. package/dist/session-recorder.d.ts +40 -2
  36. package/dist/session-recorder.js +1 -1
  37. package/dist/session-recorder.js.map +1 -1
  38. package/dist/types/index.d.ts +1 -0
  39. package/dist/types/index.js +1 -1
  40. package/dist/types/index.js.map +1 -1
  41. package/dist/types/rrweb.d.ts +108 -0
  42. package/dist/types/rrweb.js +1 -0
  43. package/dist/types/rrweb.js.map +1 -0
  44. package/dist/version.d.ts +1 -1
  45. package/dist/version.js +1 -1
  46. package/example-usage.tsx +174 -0
  47. package/package.json +3 -2
  48. package/src/config/constants.ts +3 -3
  49. package/src/context/SessionRecorderContext.tsx +93 -16
  50. package/src/index.ts +1 -0
  51. package/src/otel/helpers.ts +37 -20
  52. package/src/otel/index.ts +7 -3
  53. package/src/otel/instrumentations/index.ts +79 -38
  54. package/src/otel/instrumentations/reactNavigationInstrumentation.ts +5 -0
  55. package/src/recorder/eventExporter.ts +138 -0
  56. package/src/recorder/gestureRecorder.ts +124 -3
  57. package/src/recorder/index.ts +130 -21
  58. package/src/recorder/screenRecorder.ts +203 -7
  59. package/src/services/api.service.ts +1 -8
  60. package/src/services/storage.service.ts +1 -0
  61. package/src/session-recorder.ts +91 -12
  62. package/src/types/index.ts +2 -1
  63. package/src/types/rrweb.ts +122 -0
  64. 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.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",
@@ -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>{children}</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
- // if (!context?.isRecording) return
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
- const { pageX, pageY, target } = event.nativeEvent
39
- // context.client.recordTap(pageX, pageY, target?.toString())
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
- // if (!context?.isRecording) return
102
+ if (!context?.client || context.client.sessionState !== SessionState.started) return // SessionState.started
44
103
 
45
- const { pageX, pageY, target } = event.nativeEvent
46
- // Record pan gesture
47
- // context.gestureRecorder.recordPan(pageX, pageY, target?.toString())
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
- // if (!context?.isRecording) return
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
- const { pageX, pageY, target } = event.nativeEvent
54
- // Record tap end
55
- // context.gestureRecorder.recordTap(pageX, pageY, target?.toString())
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 style={{ flex: 1 }} onTouchStart={handleTouchStart} onTouchMove={handleTouchMove} onTouchEnd={handleTouchEnd}>
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
@@ -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
@@ -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?.isContentMaskingEnabled) {
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?.headersToInclude?.length &&
108
- !masking?.headersToExclude?.length
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?.headersToInclude) {
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?.headersToExclude?.length) {
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?.maskHeaders?.(requestHeaders, span) || requestHeaders
145
- const maskedResponseHeaders = masking?.maskHeaders?.(responseHeaders, span) || responseHeaders
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 (React Native compatible)
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 (typeof headers === 'object' && !Array.isArray(headers)) {
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 (React Native compatible)
230
+ * Extracts response body as string from Response object
220
231
  */
221
- export async function extractResponseBody(response: any): Promise<string | null> {
222
- if (!response || !response.body) {
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 (typeof response.body === 'string') {
228
- return response.body
229
- } else if (typeof response.body === 'object') {
230
- return JSON.stringify(response.body)
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 String(response.body)
247
+ return JSON.stringify(response.body)
233
248
  }
234
249
  } catch (error) {
235
- // Failed to extract response body - silently continue
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
- const masking = getMaskingConfig(config.masking)
9
+
9
10
  const instrumentations = []
10
11
 
11
12
  // Fetch instrumentation
12
13
  try {
13
14
  instrumentations.push(
14
15
  new FetchInstrumentation({
15
- ignoreUrls: config.ignoreUrls || [],
16
- propagateTraceHeaderCorsUrls: config.propagateTraceHeaderCorsUrls || [],
17
- applyCustomAttributesOnSpan: (span: any, request: any) => {
18
- if (config.captureHeaders) {
19
- const headers = request.headers
20
- if (headers && masking.maskHeaders) {
21
- const maskedHeaders = masking.maskHeaders(headers, span)
22
- Object.keys(maskedHeaders).forEach(key => {
23
- span.setAttribute(`http.request.header.${key}`, maskedHeaders[key])
24
- })
25
- } else if (headers) {
26
- Object.keys(headers).forEach(key => {
27
- span.setAttribute(`http.request.header.${key}`, headers[key])
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
- ignoreUrls: config.ignoreUrls || [],
43
- propagateTraceHeaderCorsUrls: config.propagateTraceHeaderCorsUrls || [],
44
- applyCustomAttributesOnSpan: (span: any, xhr: any) => {
45
- if (config.captureHeaders) {
46
- const headers = xhr.getAllResponseHeaders()
47
- if (headers && masking.maskHeaders) {
48
- const maskedHeaders = masking.maskHeaders(headers, span)
49
- Object.keys(maskedHeaders).forEach(key => {
50
- span.setAttribute(`http.response.header.${key}`, maskedHeaders[key])
51
- })
52
- } else if (headers) {
53
- Object.keys(headers).forEach(key => {
54
- span.setAttribute(`http.response.header.${key}`, headers[key])
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
- instrumentations.push(new ReactNativeInstrumentation())
68
- } catch (error) {
69
- console.warn('React Native instrumentation not available:', error)
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()