@multiplayer-app/session-recorder-react-native 0.0.1-alpha.5 → 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 (70) 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 -10
  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 +4 -3
  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 +184 -110
  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
  65. package/dist/exporters.d.ts +0 -3
  66. package/dist/exporters.js +0 -1
  67. package/dist/exporters.js.map +0 -1
  68. package/dist/sessionRecorder.d.ts +0 -133
  69. package/dist/sessionRecorder.js +0 -1
  70. package/dist/sessionRecorder.js.map +0 -1
@@ -0,0 +1,138 @@
1
+ import io, { Socket } from 'socket.io-client'
2
+
3
+ import { ISession } from '../types'
4
+
5
+ import {
6
+ SESSION_ADD_EVENT,
7
+ SESSION_AUTO_CREATED,
8
+ SESSION_STOPPED_EVENT,
9
+ SESSION_SUBSCRIBE_EVENT,
10
+ SESSION_UNSUBSCRIBE_EVENT,
11
+ } from '../config'
12
+
13
+ const MAX_RECONNECTION_ATTEMPTS = 2
14
+
15
+ export class EventExporter {
16
+ private socket: Socket | null = null
17
+ private queue: any[] = []
18
+ private isConnecting: boolean = false
19
+ private isConnected: boolean = false
20
+ private attempts: number = 0
21
+ private sessionId: string | null = null
22
+
23
+ constructor(private options: { socketUrl: string, apiKey: string }) { }
24
+
25
+ private init(): void {
26
+ if (this.isConnecting || this.isConnected) return
27
+ this.attempts++
28
+ this.isConnecting = true
29
+ this.socket = io(this.options.socketUrl, {
30
+ path: '/v0/radar/ws',
31
+ auth: {
32
+ 'x-api-key': this.options.apiKey,
33
+ },
34
+ reconnectionAttempts: 2,
35
+ transports: ['websocket'],
36
+ })
37
+
38
+ // this.socket.on('connect', () => {
39
+ // this.isConnecting = false
40
+ // this.isConnected = true
41
+ // this.usePostMessage = false
42
+ // this.flushQueue()
43
+ // })
44
+
45
+ this.socket.on('ready', () => {
46
+ this.isConnecting = false
47
+ this.isConnected = true
48
+
49
+ this.flushQueue()
50
+ })
51
+
52
+ this.socket.on('disconnect', (err: any) => {
53
+ this.isConnecting = false
54
+ this.isConnected = false
55
+ })
56
+
57
+ this.socket.on('connect_error', (err: any) => {
58
+ this.isConnecting = false
59
+ this.isConnected = false
60
+ this.checkReconnectionAttempts()
61
+ })
62
+
63
+ this.socket.on(SESSION_STOPPED_EVENT, (data: any) => {
64
+
65
+ this.unsubscribeFromSession()
66
+ })
67
+
68
+ this.socket.on(SESSION_AUTO_CREATED, (data: any) => {
69
+
70
+ })
71
+ }
72
+
73
+ private checkReconnectionAttempts(): void {
74
+ if (this.attempts >= MAX_RECONNECTION_ATTEMPTS) {
75
+
76
+ this.flushQueue()
77
+ }
78
+ }
79
+
80
+
81
+ private flushQueue(): void {
82
+ while (this.queue.length > 0 && (this.socket?.connected)) {
83
+ const event = this.queue.shift()
84
+ if (!event) continue
85
+
86
+ if (this.socket?.connected) {
87
+ this.socket.emit(event.name, event.data)
88
+ }
89
+ }
90
+ }
91
+
92
+ private unsubscribeFromSession() {
93
+ const payload = {
94
+ debugSessionId: this.sessionId,
95
+ }
96
+ if (this.socket?.connected) {
97
+ this.socket.emit(SESSION_UNSUBSCRIBE_EVENT, payload)
98
+ }
99
+ }
100
+
101
+ public send(event: any): void {
102
+ if (this.socket?.connected) {
103
+ this.socket.emit(SESSION_ADD_EVENT, event)
104
+ } else {
105
+ this.queue.push({ data: event, name: SESSION_ADD_EVENT })
106
+ this.init()
107
+ }
108
+ }
109
+
110
+ public subscribeToSession(session: ISession): void {
111
+ this.sessionId = session.shortId || session._id
112
+ const payload = {
113
+ projectId: session.project,
114
+ workspaceId: session.workspace,
115
+ debugSessionId: this.sessionId,
116
+ sessionType: session.creationType,
117
+ }
118
+ if (this.socket?.connected) {
119
+ this.socket.emit(SESSION_SUBSCRIBE_EVENT, payload)
120
+ } else {
121
+ this.queue.push({ data: payload, name: SESSION_SUBSCRIBE_EVENT })
122
+ this.init()
123
+ }
124
+ }
125
+
126
+ public close(): void {
127
+ if (this.socket?.connected) {
128
+ setTimeout(() => {
129
+ this.unsubscribeFromSession()
130
+ this.attempts = 0
131
+ this.isConnected = false
132
+ this.isConnecting = false
133
+ this.socket?.disconnect()
134
+ this.socket = null
135
+ }, 500)
136
+ }
137
+ }
138
+ }
@@ -1,8 +1,8 @@
1
- import { GestureEvent, RecorderConfig } from '../types'
1
+ import { GestureEvent, RecorderConfig, EventType, IncrementalSource, IncrementalSnapshotEvent, MouseInteractionType, EventRecorder } from '../types'
2
2
  import { trace, SpanStatusCode } from '@opentelemetry/api'
3
3
  import { Dimensions } from 'react-native'
4
4
 
5
- export class GestureRecorder {
5
+ export class GestureRecorder implements EventRecorder {
6
6
  private config?: RecorderConfig
7
7
  private isRecording = false
8
8
  private events: GestureEvent[] = []
@@ -10,8 +10,13 @@ export class GestureRecorder {
10
10
  private screenDimensions: { width: number; height: number } | null = null
11
11
  private lastGestureTime: number = 0
12
12
  private gestureThrottleMs: number = 50 // Throttle gestures to avoid spam
13
- init(config: RecorderConfig): void {
13
+ private eventRecorder?: EventRecorder
14
+ private imageNodeId: number = 1 // ID of the image node for touch interactions
15
+ private screenRecorder?: any // Reference to screen recorder for force capture
16
+ init(config: RecorderConfig, eventRecorder?: EventRecorder, screenRecorder?: any): void {
14
17
  this.config = config
18
+ this.eventRecorder = eventRecorder
19
+ this.screenRecorder = screenRecorder
15
20
  this._getScreenDimensions()
16
21
  }
17
22
 
@@ -19,6 +24,7 @@ export class GestureRecorder {
19
24
  this.isRecording = true
20
25
  this.events = []
21
26
  this._setupGestureHandlers()
27
+ this._setupAutomaticTouchCapture()
22
28
  // Gesture recording started
23
29
  }
24
30
 
@@ -77,6 +83,19 @@ export class GestureRecorder {
77
83
  }
78
84
  }
79
85
 
86
+ private _setupAutomaticTouchCapture(): void {
87
+ try {
88
+ // This method sets up automatic touch capture
89
+ // The actual touch capture is handled by the TouchEventCapture component
90
+ // in the SessionRecorderContext, which automatically calls our recording methods
91
+
92
+ // We can add any additional setup here if needed
93
+ // For now, the TouchEventCapture component handles everything automatically
94
+ } catch (error) {
95
+ // Failed to setup automatic touch capture - silently continue
96
+ }
97
+ }
98
+
80
99
  private _removeGestureHandlers(): void {
81
100
  this.gestureHandlers.clear()
82
101
  // Gesture handlers removed
@@ -424,4 +443,106 @@ export class GestureRecorder {
424
443
  isRecordingEnabled(): boolean {
425
444
  return this.isRecording
426
445
  }
446
+
447
+ /**
448
+ * Record an rrweb event
449
+ * @param event - The rrweb event to record
450
+ */
451
+ recordEvent(event: any): void {
452
+ if (this.eventRecorder) {
453
+ this.eventRecorder.recordEvent(event)
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Create and emit a rrweb MouseInteraction event for touch interactions
459
+ * @param x - X coordinate
460
+ * @param y - Y coordinate
461
+ * @param interactionType - Type of interaction (TouchStart, TouchMove, TouchEnd, etc.)
462
+ * @param target - Target element identifier
463
+ */
464
+ private _createMouseInteractionEvent(
465
+ x: number,
466
+ y: number,
467
+ interactionType: MouseInteractionType,
468
+ target?: string,
469
+ ): void {
470
+ const incrementalSnapshotEvent: IncrementalSnapshotEvent = {
471
+ type: EventType.IncrementalSnapshot,
472
+ data: {
473
+ source: IncrementalSource.MouseInteraction,
474
+ type: interactionType,
475
+ id: this.imageNodeId, // Reference to the image node
476
+ x: Math.round(x),
477
+ y: Math.round(y),
478
+ },
479
+ timestamp: Date.now(),
480
+ }
481
+
482
+ this.recordEvent(incrementalSnapshotEvent)
483
+ }
484
+
485
+ /**
486
+ * Record touch start event as rrweb MouseInteraction
487
+ * @param x - X coordinate
488
+ * @param y - Y coordinate
489
+ * @param target - Target element identifier
490
+ * @param pressure - Touch pressure (optional)
491
+ */
492
+ recordTouchStart(x: number, y: number, target?: string, pressure?: number): void {
493
+ // Record as both gesture event and rrweb event
494
+ this.recordTap(x, y, target, pressure)
495
+ this._createMouseInteractionEvent(x, y, MouseInteractionType.TouchStart, target)
496
+
497
+ // Force screen capture after touch interaction
498
+ this.screenRecorder?.forceCapture()
499
+ }
500
+
501
+ /**
502
+ * Record touch move event as rrweb MouseInteraction
503
+ * @param x - X coordinate
504
+ * @param y - Y coordinate
505
+ * @param target - Target element identifier
506
+ * @param pressure - Touch pressure (optional)
507
+ */
508
+ recordTouchMove(x: number, y: number, target?: string, pressure?: number): void {
509
+ // Record as both gesture event and rrweb event
510
+ this.recordPan(x - (this.lastGestureTime || 0), y - (this.lastGestureTime || 0), target)
511
+ this._createMouseInteractionEvent(x, y, MouseInteractionType.TouchMove, target)
512
+ }
513
+
514
+ /**
515
+ * Record touch end event as rrweb MouseInteraction
516
+ * @param x - X coordinate
517
+ * @param y - Y coordinate
518
+ * @param target - Target element identifier
519
+ * @param pressure - Touch pressure (optional)
520
+ */
521
+ recordTouchEnd(x: number, y: number, target?: string, pressure?: number): void {
522
+ // Record as both gesture event and rrweb event
523
+ this.recordTap(x, y, target, pressure)
524
+ this._createMouseInteractionEvent(x, y, MouseInteractionType.TouchEnd, target)
525
+
526
+ // Force screen capture after touch interaction
527
+ this.screenRecorder?.forceCapture()
528
+ }
529
+
530
+ /**
531
+ * Record touch cancel event as rrweb MouseInteraction
532
+ * @param x - X coordinate
533
+ * @param y - Y coordinate
534
+ * @param target - Target element identifier
535
+ */
536
+ recordTouchCancel(x: number, y: number, target?: string): void {
537
+ this._createMouseInteractionEvent(x, y, MouseInteractionType.TouchCancel, target)
538
+ }
539
+
540
+ /**
541
+ * Set the image node ID for touch interactions
542
+ * This should be called when a new screen snapshot is created
543
+ * @param nodeId - The ID of the image node in the current snapshot
544
+ */
545
+ setImageNodeId(nodeId: number): void {
546
+ this.imageNodeId = nodeId
547
+ }
427
548
  }
@@ -1,15 +1,22 @@
1
- import { GestureRecorder } from './gestureRecorder'
2
- import { NavigationTracker } from './navigationTracker'
3
- import { ScreenRecorder } from './screenRecorder'
1
+ import { pack } from '@rrweb/packer'
4
2
  import { SessionType } from '@multiplayer-app/session-recorder-common'
5
- import { RecorderConfig } from '../types'
6
3
 
7
- export class RecorderReactNativeSDK {
4
+ import { EventExporter } from './eventExporter'
5
+ import { ScreenRecorder } from './screenRecorder'
6
+ import { GestureRecorder } from './gestureRecorder'
7
+ import { NavigationTracker } from './navigationTracker'
8
+ import { RecorderConfig, EventRecorder, RRWebEvent } from '../types'
9
+ export class RecorderReactNativeSDK implements EventRecorder {
10
+ private isRecording = false
8
11
  private config?: RecorderConfig
12
+ private eventRecorder?: EventRecorder
13
+ private screenRecorder: ScreenRecorder
9
14
  private gestureRecorder: GestureRecorder
10
15
  private navigationTracker: NavigationTracker
11
- private screenRecorder: ScreenRecorder
12
- private isRecording = false
16
+ private recordedEvents: RRWebEvent[] = []
17
+ private exporter: EventExporter | undefined
18
+ private sessionId: string | null = null
19
+ private sessionType: SessionType = SessionType.PLAIN
13
20
 
14
21
  constructor() {
15
22
  this.gestureRecorder = new GestureRecorder()
@@ -17,11 +24,16 @@ export class RecorderReactNativeSDK {
17
24
  this.screenRecorder = new ScreenRecorder()
18
25
  }
19
26
 
20
- init(config: RecorderConfig): void {
27
+ init(config: RecorderConfig, eventRecorder?: EventRecorder): void {
21
28
  this.config = config
22
- this.gestureRecorder.init(config)
29
+ this.eventRecorder = eventRecorder
30
+ this.gestureRecorder.init(config, this, this.screenRecorder)
23
31
  this.navigationTracker.init(config)
24
- this.screenRecorder.init(config)
32
+ this.screenRecorder.init(config, this)
33
+ this.exporter = new EventExporter({
34
+ socketUrl: config.apiBaseUrl || '',
35
+ apiKey: config.apiKey,
36
+ })
25
37
  }
26
38
 
27
39
  start(sessionId: string | null, sessionType: SessionType): void {
@@ -29,6 +41,8 @@ export class RecorderReactNativeSDK {
29
41
  throw new Error('Configuration not initialized. Call init() before start().')
30
42
  }
31
43
 
44
+ this.sessionId = sessionId
45
+ this.sessionType = sessionType
32
46
  this.isRecording = true
33
47
 
34
48
  if (this.config.recordGestures) {
@@ -49,23 +63,118 @@ export class RecorderReactNativeSDK {
49
63
  this.gestureRecorder.stop()
50
64
  this.navigationTracker.stop()
51
65
  this.screenRecorder.stop()
66
+ this.exporter?.close()
67
+ }
68
+
69
+
70
+ setNavigationRef(ref: any): void {
71
+ this.navigationTracker.setNavigationRef(ref)
72
+ }
73
+
74
+ /**
75
+ * Set the viewshot ref for screen capture
76
+ * @param ref - React Native View ref for screen capture
77
+ */
78
+ setViewShotRef(ref: any): void {
79
+ this.screenRecorder.setViewShotRef(ref)
80
+ }
81
+
82
+ /**
83
+ * Record an rrweb event
84
+ * @param event - The rrweb event to record
85
+ */
86
+ recordEvent(event: RRWebEvent): void {
87
+ if (!this.isRecording) {
88
+ return
89
+ }
90
+
91
+ // Store the event locally
92
+ this.recordedEvents.push(event)
93
+ if (this.exporter) {
94
+ const packedEvent = pack(event)
95
+ this.exporter.send({
96
+ event: packedEvent,
97
+ eventType: event.type,
98
+ timestamp: event.timestamp,
99
+ debugSessionId: this.sessionId,
100
+ debugSessionType: this.sessionType,
101
+ })
102
+ }
103
+
104
+ // Forward to parent event recorder if available
105
+ if (this.eventRecorder) {
106
+ this.eventRecorder.recordEvent(event)
107
+ }
52
108
  }
53
109
 
54
- pause(): void {
55
- this.gestureRecorder.pause()
56
- this.navigationTracker.pause()
57
- this.screenRecorder.pause()
110
+ /**
111
+ * Record touch start event
112
+ * @param x - X coordinate
113
+ * @param y - Y coordinate
114
+ * @param target - Target element identifier
115
+ * @param pressure - Touch pressure
116
+ */
117
+ recordTouchStart(x: number, y: number, target?: string, pressure?: number): void {
118
+ if (!this.isRecording) {
119
+ return
120
+ }
121
+
122
+ this.gestureRecorder.recordTouchStart(x, y, target, pressure)
58
123
  }
59
124
 
60
- resume(): void {
61
- if (this.isRecording) {
62
- this.gestureRecorder.resume()
63
- this.navigationTracker.resume()
64
- this.screenRecorder.resume()
125
+ /**
126
+ * Record touch move event
127
+ * @param x - X coordinate
128
+ * @param y - Y coordinate
129
+ * @param target - Target element identifier
130
+ * @param pressure - Touch pressure
131
+ */
132
+ recordTouchMove(x: number, y: number, target?: string, pressure?: number): void {
133
+ if (!this.isRecording) {
134
+ return
65
135
  }
136
+
137
+ this.gestureRecorder.recordTouchMove(x, y, target, pressure)
66
138
  }
67
139
 
68
- setNavigationRef(ref: any): void {
69
- this.navigationTracker.setNavigationRef(ref)
140
+ /**
141
+ * Record touch end event
142
+ * @param x - X coordinate
143
+ * @param y - Y coordinate
144
+ * @param target - Target element identifier
145
+ * @param pressure - Touch pressure
146
+ */
147
+ recordTouchEnd(x: number, y: number, target?: string, pressure?: number): void {
148
+ if (!this.isRecording) {
149
+ return
150
+ }
151
+
152
+ this.gestureRecorder.recordTouchEnd(x, y, target, pressure)
153
+ }
154
+
155
+ /**
156
+ * Get all recorded events
157
+ * @returns Array of recorded rrweb events
158
+ */
159
+ getRecordedEvents(): RRWebEvent[] {
160
+ return [...this.recordedEvents]
161
+ }
162
+
163
+ /**
164
+ * Clear all recorded events
165
+ */
166
+ clearRecordedEvents(): void {
167
+ this.recordedEvents = []
168
+ }
169
+
170
+ /**
171
+ * Get recording statistics
172
+ * @returns Recording statistics
173
+ */
174
+ getRecordingStats(): { totalEvents: number; isRecording: boolean } {
175
+ return {
176
+ totalEvents: this.recordedEvents.length,
177
+ isRecording: this.isRecording,
178
+ }
70
179
  }
71
180
  }