@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,8 +1,9 @@
1
- import { GestureEvent, RecorderConfig } from '../types'
1
+ import { GestureEvent, RecorderConfig, EventType, IncrementalSource, MouseInteractionType, EventRecorder, eventWithTime } from '../types'
2
2
  import { trace, SpanStatusCode } from '@opentelemetry/api'
3
3
  import { Dimensions } from 'react-native'
4
+ import { logger } from '../utils'
4
5
 
5
- export class GestureRecorder {
6
+ export class GestureRecorder implements EventRecorder {
6
7
  private config?: RecorderConfig
7
8
  private isRecording = false
8
9
  private events: GestureEvent[] = []
@@ -10,15 +11,29 @@ export class GestureRecorder {
10
11
  private screenDimensions: { width: number; height: number } | null = null
11
12
  private lastGestureTime: number = 0
12
13
  private gestureThrottleMs: number = 50 // Throttle gestures to avoid spam
13
- init(config: RecorderConfig): void {
14
+ private lastTouchTime: number = 0
15
+ private touchThrottleMs: number = 100 // Throttle touch events to max 10 per second
16
+
17
+ // Cyclic call detection
18
+ private isRecordingGesture = false
19
+ private gestureCallStack: string[] = []
20
+ private maxGestureCallDepth = 5
21
+ private eventRecorder?: EventRecorder
22
+ private imageNodeId: number = 1 // ID of the image node for touch interactions
23
+ private screenRecorder?: any // Reference to screen recorder for force capture
24
+ init(config: RecorderConfig, eventRecorder?: EventRecorder, screenRecorder?: any): void {
14
25
  this.config = config
26
+ this.eventRecorder = eventRecorder
27
+ this.screenRecorder = screenRecorder
15
28
  this._getScreenDimensions()
16
29
  }
17
30
 
18
31
  start(): void {
32
+ logger.info('GestureRecorder', 'Gesture recording started')
19
33
  this.isRecording = true
20
34
  this.events = []
21
35
  this._setupGestureHandlers()
36
+ this._setupAutomaticTouchCapture()
22
37
  // Gesture recording started
23
38
  }
24
39
 
@@ -77,6 +92,19 @@ export class GestureRecorder {
77
92
  }
78
93
  }
79
94
 
95
+ private _setupAutomaticTouchCapture(): void {
96
+ try {
97
+ // This method sets up automatic touch capture
98
+ // The actual touch capture is handled by the TouchEventCapture component
99
+ // in the SessionRecorderContext, which automatically calls our recording methods
100
+
101
+ // We can add any additional setup here if needed
102
+ // For now, the TouchEventCapture component handles everything automatically
103
+ } catch (error) {
104
+ // Failed to setup automatic touch capture - silently continue
105
+ }
106
+ }
107
+
80
108
  private _removeGestureHandlers(): void {
81
109
  this.gestureHandlers.clear()
82
110
  // Gesture handlers removed
@@ -424,4 +452,171 @@ export class GestureRecorder {
424
452
  isRecordingEnabled(): boolean {
425
453
  return this.isRecording
426
454
  }
455
+
456
+ /**
457
+ * Record an rrweb event
458
+ * @param event - The rrweb event to record
459
+ */
460
+ recordEvent(event: any): void {
461
+ if (this.eventRecorder) {
462
+ this.eventRecorder.recordEvent(event)
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Create and emit a rrweb MouseInteraction event for touch interactions
468
+ * @param x - X coordinate
469
+ * @param y - Y coordinate
470
+ * @param interactionType - Type of interaction (TouchStart, TouchMove, TouchEnd, etc.)
471
+ * @param target - Target element identifier
472
+ */
473
+ private _createMouseInteractionEvent(
474
+ x: number,
475
+ y: number,
476
+ interactionType: MouseInteractionType,
477
+ target?: string,
478
+ ): void {
479
+ const incrementalSnapshotEvent: eventWithTime = {
480
+ type: EventType.IncrementalSnapshot,
481
+ data: {
482
+ source: IncrementalSource.MouseInteraction,
483
+ type: interactionType,
484
+ id: this.imageNodeId, // Reference to the image node
485
+ x: x, // Preserve decimal precision like web rrweb
486
+ y: y, // Preserve decimal precision like web rrweb
487
+ pointerType: 2, // 2 = Touch for React Native (0=Mouse, 1=Pen, 2=Touch)
488
+ },
489
+ timestamp: Date.now(),
490
+ }
491
+
492
+ this.recordEvent(incrementalSnapshotEvent)
493
+ }
494
+
495
+ /**
496
+ * Create mouse move event with positions array (like web rrweb)
497
+ * @param x - X coordinate
498
+ * @param y - Y coordinate
499
+ * @param target - Target element identifier
500
+ */
501
+ private _createMouseMoveEvent(x: number, y: number, target?: string): void {
502
+ const incrementalSnapshotEvent: eventWithTime = {
503
+ type: EventType.IncrementalSnapshot,
504
+ data: {
505
+ source: IncrementalSource.MouseMove, // Use MouseMove instead of MouseInteraction
506
+ positions: [
507
+ {
508
+ x: x, // Preserve decimal precision like web rrweb
509
+ y: y, // Preserve decimal precision like web rrweb
510
+ id: this.imageNodeId, // Reference to the image node
511
+ timeOffset: 0, // No time offset for single position
512
+ },
513
+ ],
514
+ },
515
+ timestamp: Date.now(),
516
+ }
517
+
518
+ this.recordEvent(incrementalSnapshotEvent)
519
+ }
520
+
521
+ /**
522
+ * Record touch start event as rrweb MouseInteraction
523
+ * @param x - X coordinate
524
+ * @param y - Y coordinate
525
+ * @param target - Target element identifier
526
+ * @param pressure - Touch pressure (optional)
527
+ */
528
+ recordTouchStart(x: number, y: number, target?: string, pressure?: number): void {
529
+ // Throttle touch events to prevent spam
530
+ const now = Date.now()
531
+ if (now - this.lastTouchTime < this.touchThrottleMs) {
532
+ logger.debug('GestureRecorder', `Touch start throttled (${now - this.lastTouchTime}ms < ${this.touchThrottleMs}ms)`)
533
+ return
534
+ }
535
+ this.lastTouchTime = now
536
+
537
+ logger.debug('GestureRecorder', 'Touch start recorded', { x, y, target, pressure })
538
+ // Record as MouseDown (type: 1) like web rrweb
539
+ this._createMouseInteractionEvent(x, y, MouseInteractionType.MouseDown, target)
540
+ }
541
+
542
+ /**
543
+ * Record touch move event as rrweb MouseMove with positions array
544
+ * @param x - X coordinate
545
+ * @param y - Y coordinate
546
+ * @param target - Target element identifier
547
+ * @param pressure - Touch pressure (optional)
548
+ */
549
+ recordTouchMove(x: number, y: number, target?: string, pressure?: number): void {
550
+ // Throttle touch move events more aggressively
551
+ const now = Date.now()
552
+ if (now - this.lastTouchTime < this.touchThrottleMs * 2) { // 200ms throttle for move events
553
+ logger.debug('GestureRecorder', `Touch move throttled (${now - this.lastTouchTime}ms < ${this.touchThrottleMs * 2}ms)`)
554
+ return
555
+ }
556
+ this.lastTouchTime = now
557
+
558
+ logger.debug('GestureRecorder', 'Touch move recorded', { x, y, target, pressure })
559
+ // Record as MouseMove with positions array (like web rrweb)
560
+ this._createMouseMoveEvent(x, y, target)
561
+ }
562
+
563
+ /**
564
+ * Record touch end event as rrweb MouseInteraction
565
+ * @param x - X coordinate
566
+ * @param y - Y coordinate
567
+ * @param target - Target element identifier
568
+ * @param pressure - Touch pressure (optional)
569
+ */
570
+ recordTouchEnd(x: number, y: number, target?: string, pressure?: number): void {
571
+ // Cyclic call detection
572
+ if (this.isRecordingGesture) {
573
+ logger.error('GestureRecorder', 'CYCLIC CALL DETECTED! Already recording gesture', this.gestureCallStack)
574
+ return
575
+ }
576
+
577
+ if (this.gestureCallStack.length >= this.maxGestureCallDepth) {
578
+ logger.error('GestureRecorder', 'MAX GESTURE CALL DEPTH REACHED!', this.gestureCallStack)
579
+ return
580
+ }
581
+
582
+ this.isRecordingGesture = true
583
+ this.gestureCallStack.push('recordTouchEnd')
584
+
585
+ try {
586
+ logger.debug('GestureRecorder', 'Touch end recorded', { x, y, target, pressure })
587
+ // Always record touch end (no throttling for completion)
588
+ this.recordTap(x, y, target, pressure)
589
+ // Record as MouseUp (type: 0) like web rrweb
590
+ this._createMouseInteractionEvent(x, y, MouseInteractionType.MouseUp, target)
591
+ // Also record Click (type: 2) like web rrweb
592
+ this._createMouseInteractionEvent(x, y, MouseInteractionType.Click, target)
593
+
594
+ // Only force screen capture on touch end (not on every touch event)
595
+ logger.debug('GestureRecorder', 'Forcing screen capture after touch end')
596
+ this.screenRecorder?.forceCapture()
597
+ } finally {
598
+ this.isRecordingGesture = false
599
+ this.gestureCallStack.pop()
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Record touch cancel event as rrweb MouseInteraction
605
+ * @param x - X coordinate
606
+ * @param y - Y coordinate
607
+ * @param target - Target element identifier
608
+ */
609
+ recordTouchCancel(x: number, y: number, target?: string): void {
610
+ // Record as MouseUp (type: 0) like web rrweb for touch cancel
611
+ this._createMouseInteractionEvent(x, y, MouseInteractionType.MouseUp, target)
612
+ }
613
+
614
+ /**
615
+ * Set the image node ID for touch interactions
616
+ * This should be called when a new screen snapshot is created
617
+ * @param nodeId - The ID of the image node in the current snapshot
618
+ */
619
+ setImageNodeId(nodeId: number): void {
620
+ this.imageNodeId = nodeId
621
+ }
427
622
  }
@@ -1,27 +1,38 @@
1
+ import { SessionType } from '@multiplayer-app/session-recorder-common'
2
+ import { pack } from '@rrweb/packer'
3
+ import { EventExporter } from './eventExporter'
4
+ import { logger } from '../utils'
5
+ import { ScreenRecorder } from './screenRecorder'
1
6
  import { GestureRecorder } from './gestureRecorder'
2
7
  import { NavigationTracker } from './navigationTracker'
3
- import { ScreenRecorder } from './screenRecorder'
4
- import { SessionType } from '@multiplayer-app/session-recorder-common'
5
- import { RecorderConfig } from '../types'
6
-
7
- export class RecorderReactNativeSDK {
8
+ import { RecorderConfig, EventRecorder, RRWebEvent } from '../types'
9
+ export class RecorderReactNativeSDK implements EventRecorder {
10
+ private isRecording = false
8
11
  private config?: RecorderConfig
12
+ private screenRecorder: ScreenRecorder
9
13
  private gestureRecorder: GestureRecorder
10
14
  private navigationTracker: NavigationTracker
11
- private screenRecorder: ScreenRecorder
12
- private isRecording = false
15
+ private recordedEvents: RRWebEvent[] = []
16
+ private exporter: EventExporter | undefined
17
+ private sessionId: string | null = null
18
+ private sessionType: SessionType = SessionType.PLAIN
19
+
13
20
 
14
21
  constructor() {
22
+ this.screenRecorder = new ScreenRecorder()
15
23
  this.gestureRecorder = new GestureRecorder()
16
24
  this.navigationTracker = new NavigationTracker()
17
- this.screenRecorder = new ScreenRecorder()
18
25
  }
19
26
 
20
27
  init(config: RecorderConfig): void {
21
28
  this.config = config
22
- this.gestureRecorder.init(config)
29
+ this.gestureRecorder.init(config, this, this.screenRecorder)
23
30
  this.navigationTracker.init(config)
24
- this.screenRecorder.init(config)
31
+ this.screenRecorder.init(config, this)
32
+ this.exporter = new EventExporter({
33
+ socketUrl: config.apiBaseUrl || '',
34
+ apiKey: config.apiKey,
35
+ })
25
36
  }
26
37
 
27
38
  start(sessionId: string | null, sessionType: SessionType): void {
@@ -29,8 +40,16 @@ export class RecorderReactNativeSDK {
29
40
  throw new Error('Configuration not initialized. Call init() before start().')
30
41
  }
31
42
 
43
+ this.sessionId = sessionId
44
+ this.sessionType = sessionType
32
45
  this.isRecording = true
33
46
 
47
+ // Emit recording started meta event
48
+
49
+ if (this.config.recordScreen) {
50
+ this.screenRecorder.start()
51
+ }
52
+
34
53
  if (this.config.recordGestures) {
35
54
  this.gestureRecorder.start()
36
55
  }
@@ -39,9 +58,7 @@ export class RecorderReactNativeSDK {
39
58
  this.navigationTracker.start()
40
59
  }
41
60
 
42
- if (this.config.recordScreen) {
43
- this.screenRecorder.start()
44
- }
61
+
45
62
  }
46
63
 
47
64
  stop(): void {
@@ -49,23 +66,112 @@ export class RecorderReactNativeSDK {
49
66
  this.gestureRecorder.stop()
50
67
  this.navigationTracker.stop()
51
68
  this.screenRecorder.stop()
69
+ this.exporter?.close()
52
70
  }
53
71
 
54
- pause(): void {
55
- this.gestureRecorder.pause()
56
- this.navigationTracker.pause()
57
- this.screenRecorder.pause()
72
+
73
+ setNavigationRef(ref: any): void {
74
+ this.navigationTracker.setNavigationRef(ref)
58
75
  }
59
76
 
60
- resume(): void {
61
- if (this.isRecording) {
62
- this.gestureRecorder.resume()
63
- this.navigationTracker.resume()
64
- this.screenRecorder.resume()
77
+ /**
78
+ * Set the viewshot ref for screen capture
79
+ * @param ref - React Native View ref for screen capture
80
+ */
81
+ setViewShotRef(ref: any): void {
82
+ this.screenRecorder.setViewShotRef(ref)
83
+ }
84
+
85
+ /**
86
+ * Record an rrweb event
87
+ * @param event - The rrweb event to record
88
+ */
89
+ recordEvent(event: RRWebEvent): void {
90
+ if (!this.isRecording) {
91
+ return
92
+ }
93
+
94
+ if (this.exporter) {
95
+ logger.debug('RecorderReactNativeSDK', 'Sending to exporter', event)
96
+ const packedEvent = pack(event)
97
+ this.exporter.send({
98
+ event: packedEvent,
99
+ eventType: event.type,
100
+ timestamp: event.timestamp,
101
+ debugSessionId: this.sessionId,
102
+ debugSessionType: this.sessionType,
103
+ })
65
104
  }
66
105
  }
67
106
 
68
- setNavigationRef(ref: any): void {
69
- this.navigationTracker.setNavigationRef(ref)
107
+ /**
108
+ * Record touch start event
109
+ * @param x - X coordinate
110
+ * @param y - Y coordinate
111
+ * @param target - Target element identifier
112
+ * @param pressure - Touch pressure
113
+ */
114
+ recordTouchStart(x: number, y: number, target?: string, pressure?: number): void {
115
+ if (!this.isRecording) {
116
+ return
117
+ }
118
+
119
+ this.gestureRecorder.recordTouchStart(x, y, target, pressure)
120
+ }
121
+
122
+ /**
123
+ * Record touch move event
124
+ * @param x - X coordinate
125
+ * @param y - Y coordinate
126
+ * @param target - Target element identifier
127
+ * @param pressure - Touch pressure
128
+ */
129
+ recordTouchMove(x: number, y: number, target?: string, pressure?: number): void {
130
+ if (!this.isRecording) {
131
+ return
132
+ }
133
+
134
+ this.gestureRecorder.recordTouchMove(x, y, target, pressure)
135
+ }
136
+
137
+ /**
138
+ * Record touch end event
139
+ * @param x - X coordinate
140
+ * @param y - Y coordinate
141
+ * @param target - Target element identifier
142
+ * @param pressure - Touch pressure
143
+ */
144
+ recordTouchEnd(x: number, y: number, target?: string, pressure?: number): void {
145
+ if (!this.isRecording) {
146
+ return
147
+ }
148
+
149
+ this.gestureRecorder.recordTouchEnd(x, y, target, pressure)
150
+ }
151
+
152
+ /**
153
+ * Get all recorded events
154
+ * @returns Array of recorded rrweb events
155
+ */
156
+ getRecordedEvents(): RRWebEvent[] {
157
+ return [...this.recordedEvents]
158
+ }
159
+
160
+ /**
161
+ * Clear all recorded events
162
+ */
163
+ clearRecordedEvents(): void {
164
+ this.recordedEvents = []
165
+ }
166
+
167
+ /**
168
+ * Get recording statistics
169
+ * @returns Recording statistics
170
+ */
171
+ getRecordingStats(): { totalEvents: number; isRecording: boolean } {
172
+ return {
173
+ totalEvents: this.recordedEvents.length,
174
+ isRecording: this.isRecording,
175
+ }
70
176
  }
71
177
  }
@@ -1,5 +1,6 @@
1
1
  import { NavigationEvent, RecorderConfig } from '../types'
2
2
  import { trace, SpanStatusCode } from '@opentelemetry/api'
3
+ import { logger } from '../utils'
3
4
 
4
5
  export class NavigationTracker {
5
6
  private config?: RecorderConfig
@@ -23,6 +24,7 @@ export class NavigationTracker {
23
24
  }
24
25
 
25
26
  start(): void {
27
+ logger.info('NavigationTracker', 'Navigation tracking started')
26
28
  this.isRecording = true
27
29
  this.events = []
28
30
  this.navigationStack = []