@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
@@ -1,73 +1,115 @@
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
+ import { logger } from '../../utils'
8
+
7
9
  export function getInstrumentations(config: TracerReactNativeConfig) {
8
- const masking = getMaskingConfig(config.masking)
10
+
9
11
  const instrumentations = []
10
12
 
11
13
  // Fetch instrumentation
12
14
  try {
13
15
  instrumentations.push(
14
16
  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
- })
17
+ clearTimingResources: false,
18
+ ignoreUrls: [
19
+ ...OTEL_IGNORE_URLS,
20
+ ...(config.ignoreUrls || []),
21
+ ],
22
+ propagateTraceHeaderCorsUrls: config.propagateTraceHeaderCorsUrls,
23
+ applyCustomAttributesOnSpan: async (span, request, response) => {
24
+ if (!config) return
25
+
26
+ const { captureBody, captureHeaders } = config
27
+
28
+ try {
29
+ if (!captureBody && !captureHeaders) {
30
+ return
31
+ }
32
+
33
+ const requestBody = request.body
34
+ const requestHeaders = headersToObject(request.headers)
35
+ const responseHeaders = headersToObject(response instanceof Response ? response.headers : undefined)
36
+
37
+ let responseBody: string | null = null
38
+ if (response instanceof Response && response.body) {
39
+ responseBody = await extractResponseBody(response)
40
+ }
41
+
42
+ const payload = {
43
+ requestBody,
44
+ responseBody,
45
+ requestHeaders,
46
+ responseHeaders,
29
47
  }
48
+ processHttpPayload(payload, config, span)
49
+ } catch (error) {
50
+ // eslint-disable-next-line
51
+ logger.error('DEBUGGER_LIB', 'Failed to capture fetch payload', error)
30
52
  }
31
- }
53
+ },
32
54
  })
33
55
  )
34
56
  } catch (error) {
35
- console.warn('Fetch instrumentation not available:', error)
57
+ logger.warn('DEBUGGER_LIB', 'Fetch instrumentation not available', error)
36
58
  }
37
59
 
38
60
  // XMLHttpRequest instrumentation
39
61
  try {
40
62
  instrumentations.push(
41
63
  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
- })
64
+ clearTimingResources: false,
65
+ ignoreUrls: [
66
+ ...OTEL_IGNORE_URLS,
67
+ ...(config.ignoreUrls || []),
68
+ ],
69
+ propagateTraceHeaderCorsUrls: config.propagateTraceHeaderCorsUrls,
70
+ applyCustomAttributesOnSpan: (span, xhr) => {
71
+ if (!config) return
72
+
73
+ const { captureBody, captureHeaders } = config
74
+
75
+ try {
76
+ if (!captureBody && !captureHeaders) {
77
+ return
78
+ }
79
+
80
+ // @ts-ignore
81
+ const requestBody = xhr.networkRequest.requestBody
82
+ // @ts-ignore
83
+ const responseBody = xhr.networkRequest.responseBody
84
+ // @ts-ignore
85
+ const requestHeaders = xhr.networkRequest.requestHeaders || {}
86
+ // @ts-ignore
87
+ const responseHeaders = xhr.networkRequest.responseHeaders || {}
88
+
89
+ const payload = {
90
+ requestBody,
91
+ responseBody,
92
+ requestHeaders,
93
+ responseHeaders,
56
94
  }
95
+ processHttpPayload(payload, config, span)
96
+ } catch (error) {
97
+ // eslint-disable-next-line
98
+ logger.error('DEBUGGER_LIB', 'Failed to capture xml-http payload', error)
57
99
  }
58
100
  },
59
101
  })
60
102
  )
61
103
  } catch (error) {
62
- console.warn('XMLHttpRequest instrumentation not available:', error)
104
+ logger.warn('DEBUGGER_LIB', 'XMLHttpRequest instrumentation not available', error)
63
105
  }
64
106
 
65
107
  // Custom React Native instrumentations
66
- try {
67
- instrumentations.push(new ReactNativeInstrumentation())
68
- } catch (error) {
69
- console.warn('React Native instrumentation not available:', error)
70
- }
108
+ // try {
109
+ // instrumentations.push(new ReactNativeInstrumentation())
110
+ // } catch (error) {
111
+ // console.warn('React Native instrumentation not available:', error)
112
+ // }
71
113
 
72
114
  return instrumentations
73
115
  }
@@ -1,4 +1,5 @@
1
1
  import { InstrumentationBase } from '@opentelemetry/instrumentation'
2
+ import { logger } from '../../utils'
2
3
  import { trace, SpanStatusCode } from '@opentelemetry/api'
3
4
  import AsyncStorage from '@react-native-async-storage/async-storage'
4
5
 
@@ -18,7 +19,7 @@ export class ReactNativeInstrumentation extends InstrumentationBase {
18
19
  this._wrap(AsyncStorage, 'setItem', this._wrapAsyncStorage)
19
20
  }
20
21
  } catch (error) {
21
- console.warn('@react-native-async-storage/async-storage is not available. AsyncStorage instrumentation will be disabled.')
22
+ logger.warn('DEBUGGER_LIB', '@react-native-async-storage/async-storage is not available. AsyncStorage instrumentation will be disabled.')
22
23
  }
23
24
  }
24
25
 
@@ -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()
@@ -0,0 +1,141 @@
1
+ import io, { Socket } from 'socket.io-client'
2
+
3
+ import { ISession } from '../types'
4
+ import { logger } from '../utils'
5
+
6
+ import {
7
+ SESSION_ADD_EVENT,
8
+ SESSION_AUTO_CREATED,
9
+ SESSION_STOPPED_EVENT,
10
+ SESSION_SUBSCRIBE_EVENT,
11
+ SESSION_UNSUBSCRIBE_EVENT,
12
+ } from '../config'
13
+
14
+ const MAX_RECONNECTION_ATTEMPTS = 2
15
+
16
+ export class EventExporter {
17
+ private socket: Socket | null = null
18
+ private queue: any[] = []
19
+ private isConnecting: boolean = false
20
+ private isConnected: boolean = false
21
+ private attempts: number = 0
22
+ private sessionId: string | null = null
23
+
24
+ constructor(private options: { socketUrl: string, apiKey: string }) { }
25
+
26
+ private init(): void {
27
+ if (this.isConnecting || this.isConnected) return
28
+ this.attempts++
29
+ this.isConnecting = true
30
+ this.socket = io(this.options.socketUrl, {
31
+ path: '/v0/radar/ws',
32
+ auth: {
33
+ 'x-api-key': this.options.apiKey,
34
+ },
35
+ reconnectionAttempts: 2,
36
+ transports: ['websocket'],
37
+ })
38
+
39
+ // this.socket.on('connect', () => {
40
+ // this.isConnecting = false
41
+ // this.isConnected = true
42
+ // this.usePostMessage = false
43
+ // this.flushQueue()
44
+ // })
45
+
46
+ this.socket.on('ready', () => {
47
+ this.isConnecting = false
48
+ this.isConnected = true
49
+ logger.info('EventExporter', 'Connected to server')
50
+ this.flushQueue()
51
+ })
52
+
53
+ this.socket.on('disconnect', (err: any) => {
54
+ this.isConnecting = false
55
+ this.isConnected = false
56
+ logger.info('EventExporter', 'Disconnected from server')
57
+ })
58
+
59
+ this.socket.on('connect_error', (err: any) => {
60
+ this.isConnecting = false
61
+ this.isConnected = false
62
+ this.checkReconnectionAttempts()
63
+ logger.error('EventExporter', 'Error connecting to server', err)
64
+ })
65
+
66
+ this.socket.on(SESSION_STOPPED_EVENT, (data: any) => {
67
+
68
+ this.unsubscribeFromSession()
69
+ })
70
+
71
+ this.socket.on(SESSION_AUTO_CREATED, (data: any) => {
72
+
73
+ })
74
+ }
75
+
76
+ private checkReconnectionAttempts(): void {
77
+ if (this.attempts >= MAX_RECONNECTION_ATTEMPTS) {
78
+
79
+ this.flushQueue()
80
+ }
81
+ }
82
+
83
+
84
+ private flushQueue(): void {
85
+ while (this.queue.length > 0 && (this.socket?.connected)) {
86
+ const event = this.queue.shift()
87
+ if (!event) continue
88
+
89
+ if (this.socket?.connected) {
90
+ this.socket.emit(event.name, event.data)
91
+ }
92
+ }
93
+ }
94
+
95
+ private unsubscribeFromSession() {
96
+ const payload = {
97
+ debugSessionId: this.sessionId,
98
+ }
99
+ if (this.socket?.connected) {
100
+ this.socket.emit(SESSION_UNSUBSCRIBE_EVENT, payload)
101
+ }
102
+ }
103
+
104
+ public send(event: any): void {
105
+ if (this.socket?.connected) {
106
+ this.socket.emit(SESSION_ADD_EVENT, event)
107
+ } else {
108
+ this.queue.push({ data: event, name: SESSION_ADD_EVENT })
109
+ this.init()
110
+ }
111
+ }
112
+
113
+ public subscribeToSession(session: ISession): void {
114
+ this.sessionId = session.shortId || session._id
115
+ const payload = {
116
+ projectId: session.project,
117
+ workspaceId: session.workspace,
118
+ debugSessionId: this.sessionId,
119
+ sessionType: session.creationType,
120
+ }
121
+ if (this.socket?.connected) {
122
+ this.socket.emit(SESSION_SUBSCRIBE_EVENT, payload)
123
+ } else {
124
+ this.queue.push({ data: payload, name: SESSION_SUBSCRIBE_EVENT })
125
+ this.init()
126
+ }
127
+ }
128
+
129
+ public close(): void {
130
+ if (this.socket?.connected) {
131
+ setTimeout(() => {
132
+ this.unsubscribeFromSession()
133
+ this.attempts = 0
134
+ this.isConnected = false
135
+ this.isConnecting = false
136
+ this.socket?.disconnect()
137
+ this.socket = null
138
+ }, 500)
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,157 @@
1
+ import { Gesture } from 'react-native-gesture-handler'
2
+ import { ReactNode } from 'react'
3
+ import { SessionState } from '../types'
4
+ import { GestureInstrumentation } from '../otel/instrumentations/gestureInstrumentation'
5
+
6
+ export interface GestureHandlerRecorderProps {
7
+ children: ReactNode
8
+ sessionState: SessionState | null
9
+ onGestureRecord: (gestureType: string, data: any) => void
10
+ }
11
+
12
+ export class GestureHandlerRecorder {
13
+ private gestureInstrumentation: GestureInstrumentation
14
+ private onGestureRecord?: (gestureType: string, data: any) => void
15
+
16
+ constructor() {
17
+ this.gestureInstrumentation = new GestureInstrumentation()
18
+ this.gestureInstrumentation.enable()
19
+ }
20
+
21
+ setGestureCallback(callback: (gestureType: string, data: any) => void) {
22
+ this.onGestureRecord = callback
23
+ }
24
+
25
+ // Create tap gesture
26
+ createTapGesture() {
27
+ return Gesture.Tap()
28
+ .onStart((event) => {
29
+ this.recordGesture('tap', {
30
+ x: event.x,
31
+ y: event.y,
32
+ timestamp: Date.now(),
33
+ })
34
+ })
35
+ }
36
+
37
+ // Create pan gesture (for swipes and drags)
38
+ createPanGesture() {
39
+ return Gesture.Pan()
40
+ .onStart((event) => {
41
+ this.recordGesture('pan_start', {
42
+ x: event.x,
43
+ y: event.y,
44
+ timestamp: Date.now(),
45
+ })
46
+ })
47
+ .onUpdate((event) => {
48
+ this.recordGesture('pan_update', {
49
+ x: event.x,
50
+ y: event.y,
51
+ translationX: event.translationX,
52
+ translationY: event.translationY,
53
+ velocityX: event.velocityX,
54
+ velocityY: event.velocityY,
55
+ timestamp: Date.now(),
56
+ })
57
+ })
58
+ .onEnd((event) => {
59
+ this.recordGesture('pan_end', {
60
+ x: event.x,
61
+ y: event.y,
62
+ translationX: event.translationX,
63
+ translationY: event.translationY,
64
+ velocityX: event.velocityX,
65
+ velocityY: event.velocityY,
66
+ timestamp: Date.now(),
67
+ })
68
+ })
69
+ }
70
+
71
+ // Create pinch gesture
72
+ createPinchGesture() {
73
+ return Gesture.Pinch()
74
+ .onStart((event) => {
75
+ this.recordGesture('pinch_start', {
76
+ scale: event.scale,
77
+ focalX: event.focalX,
78
+ focalY: event.focalY,
79
+ timestamp: Date.now(),
80
+ })
81
+ })
82
+ .onUpdate((event) => {
83
+ this.recordGesture('pinch_update', {
84
+ scale: event.scale,
85
+ focalX: event.focalX,
86
+ focalY: event.focalY,
87
+ timestamp: Date.now(),
88
+ })
89
+ })
90
+ .onEnd((event) => {
91
+ this.recordGesture('pinch_end', {
92
+ scale: event.scale,
93
+ focalX: event.focalX,
94
+ focalY: event.focalY,
95
+ timestamp: Date.now(),
96
+ })
97
+ })
98
+ }
99
+
100
+ // Create long press gesture
101
+ createLongPressGesture() {
102
+ return Gesture.LongPress()
103
+ .minDuration(500)
104
+ .onStart((event) => {
105
+ this.recordGesture('long_press', {
106
+ x: event.x,
107
+ y: event.y,
108
+ duration: 500,
109
+ timestamp: Date.now(),
110
+ })
111
+ })
112
+ }
113
+
114
+ private recordGesture(gestureType: string, data: any) {
115
+ // Record with OpenTelemetry
116
+ switch (gestureType) {
117
+ case 'tap':
118
+ this.gestureInstrumentation.recordTap(data.x, data.y)
119
+ break
120
+ case 'pan_start':
121
+ case 'pan_update':
122
+ case 'pan_end':
123
+ this.gestureInstrumentation.recordPan(data.translationX || 0, data.translationY || 0)
124
+ break
125
+ case 'pinch_start':
126
+ case 'pinch_update':
127
+ case 'pinch_end':
128
+ this.gestureInstrumentation.recordPinch(data.scale, undefined)
129
+ break
130
+ case 'long_press':
131
+ this.gestureInstrumentation.recordLongPress(data.duration, undefined)
132
+ break
133
+ }
134
+
135
+ // Record with session recorder
136
+ if (this.onGestureRecord) {
137
+ this.onGestureRecord(gestureType, data)
138
+ }
139
+ }
140
+
141
+ // Create a gesture detector component
142
+ createGestureDetector(children: ReactNode, sessionState: SessionState | null): ReactNode {
143
+ if (sessionState !== SessionState.started) {
144
+ return children
145
+ }
146
+
147
+ const tapGesture = this.createTapGesture()
148
+ const panGesture = this.createPanGesture()
149
+ const pinchGesture = this.createPinchGesture()
150
+ const longPressGesture = this.createLongPressGesture()
151
+
152
+ // Note: This would need to be implemented as a proper React component
153
+ // For now, return children directly - the gesture detection would be handled
154
+ // at the app level by wrapping the entire app with GestureHandlerRootView
155
+ return children
156
+ }
157
+ }