@multiplayer-app/session-recorder-react-native 0.0.1-alpha.1

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 (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +226 -0
  3. package/babel.config.js +13 -0
  4. package/dist/config/masking.d.ts +30 -0
  5. package/dist/config/masking.js +1 -0
  6. package/dist/config/masking.js.map +1 -0
  7. package/dist/expo.d.ts +11 -0
  8. package/dist/expo.js +1 -0
  9. package/dist/expo.js.map +1 -0
  10. package/dist/index.d.ts +11 -0
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/otel/helpers.d.ts +3 -0
  14. package/dist/otel/helpers.js +1 -0
  15. package/dist/otel/helpers.js.map +1 -0
  16. package/dist/otel/index.d.ts +40 -0
  17. package/dist/otel/index.js +1 -0
  18. package/dist/otel/index.js.map +1 -0
  19. package/dist/otel/instrumentations/gestureInstrumentation.d.ts +15 -0
  20. package/dist/otel/instrumentations/gestureInstrumentation.js +1 -0
  21. package/dist/otel/instrumentations/gestureInstrumentation.js.map +1 -0
  22. package/dist/otel/instrumentations/index.d.ts +5 -0
  23. package/dist/otel/instrumentations/index.js +1 -0
  24. package/dist/otel/instrumentations/index.js.map +1 -0
  25. package/dist/otel/instrumentations/reactNativeInstrumentation.d.ts +8 -0
  26. package/dist/otel/instrumentations/reactNativeInstrumentation.js +1 -0
  27. package/dist/otel/instrumentations/reactNativeInstrumentation.js.map +1 -0
  28. package/dist/otel/instrumentations/reactNavigationInstrumentation.d.ts +12 -0
  29. package/dist/otel/instrumentations/reactNavigationInstrumentation.js +1 -0
  30. package/dist/otel/instrumentations/reactNavigationInstrumentation.js.map +1 -0
  31. package/dist/recorder/gestureRecorder.d.ts +42 -0
  32. package/dist/recorder/gestureRecorder.js +1 -0
  33. package/dist/recorder/gestureRecorder.js.map +1 -0
  34. package/dist/recorder/index.d.ts +16 -0
  35. package/dist/recorder/index.js +1 -0
  36. package/dist/recorder/index.js.map +1 -0
  37. package/dist/recorder/navigationTracker.d.ts +43 -0
  38. package/dist/recorder/navigationTracker.js +1 -0
  39. package/dist/recorder/navigationTracker.js.map +1 -0
  40. package/dist/recorder/screenRecorder.d.ts +46 -0
  41. package/dist/recorder/screenRecorder.js +1 -0
  42. package/dist/recorder/screenRecorder.js.map +1 -0
  43. package/dist/services/api.service.d.ts +20 -0
  44. package/dist/services/api.service.js +1 -0
  45. package/dist/services/api.service.js.map +1 -0
  46. package/dist/services/storage.service.d.ts +23 -0
  47. package/dist/services/storage.service.js +1 -0
  48. package/dist/services/storage.service.js.map +1 -0
  49. package/dist/sessionRecorder.d.ts +54 -0
  50. package/dist/sessionRecorder.js +1 -0
  51. package/dist/sessionRecorder.js.map +1 -0
  52. package/dist/types/index.d.ts +81 -0
  53. package/dist/types/index.js +1 -0
  54. package/dist/types/index.js.map +1 -0
  55. package/dist/utils/platform.d.ts +9 -0
  56. package/dist/utils/platform.js +1 -0
  57. package/dist/utils/platform.js.map +1 -0
  58. package/dist/version.d.ts +1 -0
  59. package/dist/version.js +1 -0
  60. package/dist/version.js.map +1 -0
  61. package/examples/sample-expo-app/README.md +142 -0
  62. package/examples/sample-expo-app/app/(tabs)/_layout.tsx +60 -0
  63. package/examples/sample-expo-app/app/(tabs)/explore.tsx +110 -0
  64. package/examples/sample-expo-app/app/(tabs)/index.tsx +125 -0
  65. package/examples/sample-expo-app/app/(tabs)/posts.tsx +96 -0
  66. package/examples/sample-expo-app/app/(tabs)/users.tsx +131 -0
  67. package/examples/sample-expo-app/app/+not-found.tsx +32 -0
  68. package/examples/sample-expo-app/app/_layout.tsx +53 -0
  69. package/examples/sample-expo-app/app/post/[id].tsx +199 -0
  70. package/examples/sample-expo-app/app/user/[id].tsx +270 -0
  71. package/examples/sample-expo-app/app.json +42 -0
  72. package/examples/sample-expo-app/assets/fonts/SpaceMono-Regular.ttf +0 -0
  73. package/examples/sample-expo-app/assets/images/adaptive-icon.png +0 -0
  74. package/examples/sample-expo-app/assets/images/favicon.png +0 -0
  75. package/examples/sample-expo-app/assets/images/icon.png +0 -0
  76. package/examples/sample-expo-app/assets/images/partial-react-logo.png +0 -0
  77. package/examples/sample-expo-app/assets/images/react-logo.png +0 -0
  78. package/examples/sample-expo-app/assets/images/react-logo@2x.png +0 -0
  79. package/examples/sample-expo-app/assets/images/react-logo@3x.png +0 -0
  80. package/examples/sample-expo-app/assets/images/splash-icon.png +0 -0
  81. package/examples/sample-expo-app/components/Collapsible.tsx +45 -0
  82. package/examples/sample-expo-app/components/ErrorView.tsx +52 -0
  83. package/examples/sample-expo-app/components/ExternalLink.tsx +24 -0
  84. package/examples/sample-expo-app/components/HapticTab.tsx +18 -0
  85. package/examples/sample-expo-app/components/HelloWave.tsx +40 -0
  86. package/examples/sample-expo-app/components/LoadingSpinner.tsx +34 -0
  87. package/examples/sample-expo-app/components/ParallaxScrollView.tsx +82 -0
  88. package/examples/sample-expo-app/components/ThemedText.tsx +60 -0
  89. package/examples/sample-expo-app/components/ThemedView.tsx +14 -0
  90. package/examples/sample-expo-app/components/ui/IconSymbol.ios.tsx +32 -0
  91. package/examples/sample-expo-app/components/ui/IconSymbol.tsx +41 -0
  92. package/examples/sample-expo-app/components/ui/TabBarBackground.ios.tsx +19 -0
  93. package/examples/sample-expo-app/components/ui/TabBarBackground.tsx +6 -0
  94. package/examples/sample-expo-app/constants/Colors.ts +26 -0
  95. package/examples/sample-expo-app/eslint.config.js +10 -0
  96. package/examples/sample-expo-app/hooks/useApi.ts +41 -0
  97. package/examples/sample-expo-app/hooks/useColorScheme.ts +1 -0
  98. package/examples/sample-expo-app/hooks/useColorScheme.web.ts +21 -0
  99. package/examples/sample-expo-app/hooks/useThemeColor.ts +21 -0
  100. package/examples/sample-expo-app/metro.config.js +26 -0
  101. package/examples/sample-expo-app/package-lock.json +26296 -0
  102. package/examples/sample-expo-app/package.json +59 -0
  103. package/examples/sample-expo-app/scripts/reset-project.js +112 -0
  104. package/examples/sample-expo-app/services/api.ts +98 -0
  105. package/examples/sample-expo-app/tsconfig.json +17 -0
  106. package/examples/sample-expo-app/utils/navigation.ts +19 -0
  107. package/package.json +98 -0
  108. package/src/config/masking.ts +78 -0
  109. package/src/expo.ts +41 -0
  110. package/src/index.ts +20 -0
  111. package/src/otel/helpers.ts +21 -0
  112. package/src/otel/index.ts +348 -0
  113. package/src/otel/instrumentations/gestureInstrumentation.ts +141 -0
  114. package/src/otel/instrumentations/index.ts +86 -0
  115. package/src/otel/instrumentations/reactNativeInstrumentation.ts +164 -0
  116. package/src/otel/instrumentations/reactNavigationInstrumentation.ts +114 -0
  117. package/src/recorder/gestureRecorder.ts +429 -0
  118. package/src/recorder/index.ts +71 -0
  119. package/src/recorder/navigationTracker.ts +447 -0
  120. package/src/recorder/screenRecorder.ts +411 -0
  121. package/src/services/api.service.ts +78 -0
  122. package/src/services/storage.service.ts +130 -0
  123. package/src/sessionRecorder.ts +367 -0
  124. package/src/types/expo.d.ts +23 -0
  125. package/src/types/index.ts +88 -0
  126. package/src/utils/platform.ts +75 -0
  127. package/src/version.ts +1 -0
  128. package/tsconfig.json +24 -0
@@ -0,0 +1,164 @@
1
+ import { InstrumentationBase } from '@opentelemetry/instrumentation'
2
+ import { trace, SpanStatusCode } from '@opentelemetry/api'
3
+
4
+ export class ReactNativeInstrumentation extends InstrumentationBase {
5
+ constructor() {
6
+ super('react-native', '1.0.0', {})
7
+ }
8
+
9
+ init(): void {
10
+ // Initialize the instrumentation
11
+ }
12
+
13
+ enable(): void {
14
+ // Try to wrap AsyncStorage if it's available
15
+ try {
16
+ const asyncStorage = require('@react-native-async-storage/async-storage')
17
+ if (asyncStorage) {
18
+ this._wrap(asyncStorage, 'AsyncStorage', this._wrapAsyncStorage)
19
+ }
20
+ } catch (error) {
21
+ console.warn('@react-native-async-storage/async-storage is not available. AsyncStorage instrumentation will be disabled.')
22
+ }
23
+ }
24
+
25
+ disable(): void {
26
+ // Try to unwrap AsyncStorage if it was wrapped
27
+ try {
28
+ const asyncStorage = require('@react-native-async-storage/async-storage')
29
+ if (asyncStorage) {
30
+ this._unwrap(asyncStorage, 'AsyncStorage')
31
+ }
32
+ } catch (error) {
33
+ // AsyncStorage was not available, nothing to unwrap
34
+ }
35
+ }
36
+
37
+ private _wrapAsyncStorage(originalModule: any) {
38
+ const self = this
39
+
40
+ // Wrap setItem
41
+ const originalSetItem = originalModule.setItem
42
+ originalModule.setItem = async function (key: string, value: string) {
43
+ const startTime = Date.now()
44
+ try {
45
+ const result = await originalSetItem.call(this, key, value)
46
+
47
+ const span = trace.getTracer('react-native').startSpan('AsyncStorage.setItem', {
48
+ attributes: {
49
+ 'storage.operation': 'setItem',
50
+ 'storage.key': key,
51
+ 'storage.value_length': value.length,
52
+ 'storage.duration': Date.now() - startTime,
53
+ },
54
+ })
55
+
56
+ span.setStatus({ code: SpanStatusCode.OK })
57
+ span.end()
58
+
59
+ return result
60
+ } catch (error) {
61
+ const span = trace.getTracer('react-native').startSpan('AsyncStorage.setItem', {
62
+ attributes: {
63
+ 'storage.operation': 'setItem',
64
+ 'storage.key': key,
65
+ 'storage.error': true,
66
+ 'storage.duration': Date.now() - startTime,
67
+ },
68
+ })
69
+
70
+ const errorMessage = error instanceof Error ? error.message : String(error)
71
+ span.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage })
72
+ if (error instanceof Error) {
73
+ span.recordException(error)
74
+ }
75
+ span.end()
76
+
77
+ throw error
78
+ }
79
+ }
80
+
81
+ // Wrap getItem
82
+ const originalGetItem = originalModule.getItem
83
+ originalModule.getItem = async function (key: string) {
84
+ const startTime = Date.now()
85
+ try {
86
+ const result = await originalGetItem.call(this, key)
87
+
88
+ const span = trace.getTracer('react-native').startSpan('AsyncStorage.getItem', {
89
+ attributes: {
90
+ 'storage.operation': 'getItem',
91
+ 'storage.key': key,
92
+ 'storage.value_length': result ? result.length : 0,
93
+ 'storage.duration': Date.now() - startTime,
94
+ },
95
+ })
96
+
97
+ span.setStatus({ code: SpanStatusCode.OK })
98
+ span.end()
99
+
100
+ return result
101
+ } catch (error) {
102
+ const span = trace.getTracer('react-native').startSpan('AsyncStorage.getItem', {
103
+ attributes: {
104
+ 'storage.operation': 'getItem',
105
+ 'storage.key': key,
106
+ 'storage.error': true,
107
+ 'storage.duration': Date.now() - startTime,
108
+ },
109
+ })
110
+
111
+ const errorMessage = error instanceof Error ? error.message : String(error)
112
+ span.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage })
113
+ if (error instanceof Error) {
114
+ span.recordException(error)
115
+ }
116
+ span.end()
117
+
118
+ throw error
119
+ }
120
+ }
121
+
122
+ // Wrap removeItem
123
+ const originalRemoveItem = originalModule.removeItem
124
+ originalModule.removeItem = async function (key: string) {
125
+ const startTime = Date.now()
126
+ try {
127
+ const result = await originalRemoveItem.call(this, key)
128
+
129
+ const span = trace.getTracer('react-native').startSpan('AsyncStorage.removeItem', {
130
+ attributes: {
131
+ 'storage.operation': 'removeItem',
132
+ 'storage.key': key,
133
+ 'storage.duration': Date.now() - startTime,
134
+ },
135
+ })
136
+
137
+ span.setStatus({ code: SpanStatusCode.OK })
138
+ span.end()
139
+
140
+ return result
141
+ } catch (error) {
142
+ const span = trace.getTracer('react-native').startSpan('AsyncStorage.removeItem', {
143
+ attributes: {
144
+ 'storage.operation': 'removeItem',
145
+ 'storage.key': key,
146
+ 'storage.error': true,
147
+ 'storage.duration': Date.now() - startTime,
148
+ },
149
+ })
150
+
151
+ const errorMessage = error instanceof Error ? error.message : String(error)
152
+ span.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage })
153
+ if (error instanceof Error) {
154
+ span.recordException(error)
155
+ }
156
+ span.end()
157
+
158
+ throw error
159
+ }
160
+ }
161
+
162
+ return originalModule
163
+ }
164
+ }
@@ -0,0 +1,114 @@
1
+ import { InstrumentationBase } from '@opentelemetry/instrumentation'
2
+ import { trace, SpanStatusCode } from '@opentelemetry/api'
3
+
4
+ export class ReactNavigationInstrumentation extends InstrumentationBase {
5
+ private navigationRef: any = null
6
+
7
+ constructor() {
8
+ super('react-navigation', '1.0.0', {})
9
+ }
10
+
11
+ init(): void {
12
+ // Initialize the instrumentation
13
+ }
14
+
15
+ setNavigationRef(ref: any) {
16
+ this.navigationRef = ref
17
+ this._setupNavigationListener()
18
+ }
19
+
20
+ private _setupNavigationListener() {
21
+ if (!this.navigationRef) return
22
+
23
+ // Listen to navigation state changes
24
+ this.navigationRef.addListener('state', (e: any) => {
25
+ this._recordNavigationEvent('state_change', e.data)
26
+ })
27
+
28
+ // Listen to focus events
29
+ this.navigationRef.addListener('focus', (e: any) => {
30
+ this._recordNavigationEvent('focus', e.data)
31
+ })
32
+
33
+ // Listen to blur events
34
+ this.navigationRef.addListener('blur', (e: any) => {
35
+ this._recordNavigationEvent('blur', e.data)
36
+ })
37
+ }
38
+
39
+ private _recordNavigationEvent(eventType: string, data: any) {
40
+ const span = trace.getTracer('navigation').startSpan(`Navigation.${eventType}`, {
41
+ attributes: {
42
+ 'navigation.system': 'ReactNavigation',
43
+ 'navigation.operation': eventType,
44
+ 'navigation.type': eventType,
45
+ 'navigation.timestamp': Date.now(),
46
+ },
47
+ })
48
+
49
+ if (data) {
50
+ if (data.routeName) {
51
+ span.setAttribute('navigation.route_name', data.routeName)
52
+ }
53
+ if (data.params) {
54
+ span.setAttribute('navigation.params', JSON.stringify(data.params))
55
+ }
56
+ if (data.key) {
57
+ span.setAttribute('navigation.key', data.key)
58
+ }
59
+ }
60
+
61
+ span.setStatus({ code: SpanStatusCode.OK })
62
+ span.end()
63
+ }
64
+
65
+ // Manual navigation tracking methods
66
+ recordNavigate(routeName: string, params?: Record<string, any>) {
67
+ const span = trace.getTracer('navigation').startSpan('Navigation.navigate', {
68
+ attributes: {
69
+ 'navigation.system': 'ReactNavigation',
70
+ 'navigation.operation': 'navigate',
71
+ 'navigation.route_name': routeName,
72
+ 'navigation.timestamp': Date.now(),
73
+ },
74
+ })
75
+
76
+ if (params) {
77
+ span.setAttribute('navigation.params', JSON.stringify(params))
78
+ }
79
+
80
+ span.setStatus({ code: SpanStatusCode.OK })
81
+ span.end()
82
+ }
83
+
84
+ recordGoBack() {
85
+ const span = trace.getTracer('navigation').startSpan('Navigation.goBack', {
86
+ attributes: {
87
+ 'navigation.system': 'ReactNavigation',
88
+ 'navigation.operation': 'goBack',
89
+ 'navigation.timestamp': Date.now(),
90
+ },
91
+ })
92
+
93
+ span.setStatus({ code: SpanStatusCode.OK })
94
+ span.end()
95
+ }
96
+
97
+ recordReset(routes: any[]) {
98
+ const span = trace.getTracer('navigation').startSpan('Navigation.reset', {
99
+ attributes: {
100
+ 'navigation.system': 'ReactNavigation',
101
+ 'navigation.operation': 'reset',
102
+ 'navigation.routes_count': routes.length,
103
+ 'navigation.timestamp': Date.now(),
104
+ },
105
+ })
106
+
107
+ if (routes.length > 0) {
108
+ span.setAttribute('navigation.initial_route', routes[0].name)
109
+ }
110
+
111
+ span.setStatus({ code: SpanStatusCode.OK })
112
+ span.end()
113
+ }
114
+ }
@@ -0,0 +1,429 @@
1
+ import { GestureEvent, RecorderConfig } from '../types'
2
+ import { trace, SpanStatusCode } from '@opentelemetry/api'
3
+
4
+ export class GestureRecorder {
5
+ private config?: RecorderConfig
6
+ private isRecording = false
7
+ private events: GestureEvent[] = []
8
+ private gestureHandlers: Map<string, any> = new Map()
9
+ private screenDimensions: { width: number; height: number } | null = null
10
+ private lastGestureTime: number = 0
11
+ private gestureThrottleMs: number = 50 // Throttle gestures to avoid spam
12
+ init(config: RecorderConfig): void {
13
+ this.config = config
14
+ this._getScreenDimensions()
15
+ }
16
+
17
+ start(): void {
18
+ this.isRecording = true
19
+ this.events = []
20
+ this._setupGestureHandlers()
21
+ console.log('Gesture recording started')
22
+ }
23
+
24
+ stop(): void {
25
+ this.isRecording = false
26
+ this._removeGestureHandlers()
27
+ console.log('Gesture recording stopped')
28
+ }
29
+
30
+ pause(): void {
31
+ this.isRecording = false
32
+ }
33
+
34
+ resume(): void {
35
+ this.isRecording = true
36
+ }
37
+
38
+ // Input component registration temporarily disabled
39
+
40
+ private _getScreenDimensions(): void {
41
+ try {
42
+ const { Dimensions } = require('react-native')
43
+ this.screenDimensions = Dimensions.get('window')
44
+ } catch (error) {
45
+ console.warn('Failed to get screen dimensions:', error)
46
+ this.screenDimensions = { width: 375, height: 667 } // Default fallback
47
+ }
48
+ }
49
+
50
+ private _setupInputTracking(): void {
51
+ // Set up React Native input component tracking
52
+ try {
53
+ // This would integrate with React Native's component tracking
54
+ // For now, we'll provide methods for manual registration
55
+ console.log('Input tracking setup complete')
56
+ } catch (error) {
57
+ console.warn('Failed to setup input tracking:', error)
58
+ }
59
+ }
60
+
61
+ private _setupGestureHandlers(): void {
62
+ // This would integrate with react-native-gesture-handler
63
+ // For now, we'll create a comprehensive implementation that can be easily integrated
64
+ console.log('Setting up gesture handlers')
65
+
66
+ // Set up global gesture listener
67
+ this._setupGlobalGestureListener()
68
+ }
69
+
70
+ private _setupGlobalGestureListener(): void {
71
+ try {
72
+ // Listen for touch events at the app level
73
+ const { TouchableWithoutFeedback } = require('react-native')
74
+
75
+ // This is a simplified implementation - in production you'd use react-native-gesture-handler
76
+ console.log('Global gesture listener setup complete')
77
+ } catch (error) {
78
+ console.warn('Failed to setup global gesture listener:', error)
79
+ }
80
+ }
81
+
82
+ private _removeGestureHandlers(): void {
83
+ this.gestureHandlers.clear()
84
+ console.log('Gesture handlers removed')
85
+ }
86
+
87
+ private _recordEvent(event: GestureEvent): void {
88
+ if (!this.isRecording) return
89
+
90
+ // Throttle gestures to avoid spam
91
+ const now = Date.now()
92
+ if (now - this.lastGestureTime < this.gestureThrottleMs) {
93
+ return
94
+ }
95
+ this.lastGestureTime = now
96
+
97
+ this.events.push(event)
98
+ this._sendEvent(event)
99
+ this._recordOpenTelemetrySpan(event)
100
+ }
101
+
102
+
103
+
104
+ private _sendEvent(event: GestureEvent): void {
105
+ // Send event to backend or store locally
106
+ console.log('Gesture event recorded:', event)
107
+ }
108
+
109
+ private _recordOpenTelemetrySpan(event: GestureEvent): void {
110
+ try {
111
+ const span = trace.getTracer('gesture').startSpan(`Gesture.${event.type}`, {
112
+ attributes: {
113
+ 'gesture.type': event.type,
114
+ 'gesture.timestamp': event.timestamp,
115
+ 'gesture.platform': 'react-native',
116
+ },
117
+ })
118
+
119
+ if (event.coordinates) {
120
+ span.setAttribute('gesture.coordinates.x', event.coordinates.x)
121
+ span.setAttribute('gesture.coordinates.y', event.coordinates.y)
122
+
123
+ // Calculate relative position
124
+ if (this.screenDimensions) {
125
+ const relativeX = event.coordinates.x / this.screenDimensions.width
126
+ const relativeY = event.coordinates.y / this.screenDimensions.height
127
+ span.setAttribute('gesture.coordinates.relative_x', relativeX)
128
+ span.setAttribute('gesture.coordinates.relative_y', relativeY)
129
+ }
130
+ }
131
+
132
+ if (event.target) {
133
+ span.setAttribute('gesture.target', event.target)
134
+ }
135
+
136
+ if (event.metadata) {
137
+ Object.entries(event.metadata).forEach(([key, value]) => {
138
+ span.setAttribute(`gesture.metadata.${key}`, String(value))
139
+ })
140
+ }
141
+
142
+ span.setStatus({ code: SpanStatusCode.OK })
143
+ span.end()
144
+ } catch (error) {
145
+ console.warn('Failed to record OpenTelemetry span for gesture:', error)
146
+ }
147
+ }
148
+
149
+ // Public methods for manual event recording
150
+ recordTap(x: number, y: number, target?: string, pressure?: number): void {
151
+ const event: GestureEvent = {
152
+ type: 'tap',
153
+ timestamp: Date.now(),
154
+ coordinates: { x, y },
155
+ target,
156
+ metadata: {
157
+ pressure: pressure || 1.0,
158
+ screenWidth: this.screenDimensions?.width,
159
+ screenHeight: this.screenDimensions?.height,
160
+ },
161
+ }
162
+
163
+ this._recordEvent(event)
164
+ }
165
+
166
+ recordSwipe(direction: string, target?: string, velocity?: number, distance?: number): void {
167
+ const event: GestureEvent = {
168
+ type: 'swipe',
169
+ timestamp: Date.now(),
170
+ target,
171
+ metadata: {
172
+ direction,
173
+ velocity: velocity || 0,
174
+ distance: distance || 0,
175
+ screenWidth: this.screenDimensions?.width,
176
+ screenHeight: this.screenDimensions?.height,
177
+ },
178
+ }
179
+
180
+ this._recordEvent(event)
181
+ }
182
+
183
+ recordPinch(scale: number, target?: string, velocity?: number): void {
184
+ const event: GestureEvent = {
185
+ type: 'pinch',
186
+ timestamp: Date.now(),
187
+ target,
188
+ metadata: {
189
+ scale,
190
+ velocity: velocity || 0,
191
+ screenWidth: this.screenDimensions?.width,
192
+ screenHeight: this.screenDimensions?.height,
193
+ },
194
+ }
195
+
196
+ this._recordEvent(event)
197
+ }
198
+
199
+ recordPan(deltaX: number, deltaY: number, target?: string, velocity?: number): void {
200
+ const event: GestureEvent = {
201
+ type: 'pan',
202
+ timestamp: Date.now(),
203
+ target,
204
+ metadata: {
205
+ deltaX,
206
+ deltaY,
207
+ velocity: velocity || 0,
208
+ screenWidth: this.screenDimensions?.width,
209
+ screenHeight: this.screenDimensions?.height,
210
+ },
211
+ }
212
+
213
+ this._recordEvent(event)
214
+ }
215
+
216
+ recordLongPress(duration: number, target?: string, pressure?: number): void {
217
+ const event: GestureEvent = {
218
+ type: 'longPress',
219
+ timestamp: Date.now(),
220
+ target,
221
+ metadata: {
222
+ duration,
223
+ pressure: pressure || 1.0,
224
+ screenWidth: this.screenDimensions?.width,
225
+ screenHeight: this.screenDimensions?.height,
226
+ },
227
+ }
228
+
229
+ this._recordEvent(event)
230
+ }
231
+
232
+ recordDoubleTap(x: number, y: number, target?: string): void {
233
+ const event: GestureEvent = {
234
+ type: 'doubleTap',
235
+ timestamp: Date.now(),
236
+ coordinates: { x, y },
237
+ target,
238
+ metadata: {
239
+ screenWidth: this.screenDimensions?.width,
240
+ screenHeight: this.screenDimensions?.height,
241
+ },
242
+ }
243
+
244
+ this._recordEvent(event)
245
+ }
246
+
247
+ recordRotate(rotation: number, target?: string, velocity?: number): void {
248
+ const event: GestureEvent = {
249
+ type: 'rotate',
250
+ timestamp: Date.now(),
251
+ target,
252
+ metadata: {
253
+ rotation,
254
+ velocity: velocity || 0,
255
+ screenWidth: this.screenDimensions?.width,
256
+ screenHeight: this.screenDimensions?.height,
257
+ },
258
+ }
259
+
260
+ this._recordEvent(event)
261
+ }
262
+
263
+ recordFling(direction: string, velocity: number, target?: string): void {
264
+ const event: GestureEvent = {
265
+ type: 'fling',
266
+ timestamp: Date.now(),
267
+ target,
268
+ metadata: {
269
+ direction,
270
+ velocity,
271
+ screenWidth: this.screenDimensions?.width,
272
+ screenHeight: this.screenDimensions?.height,
273
+ },
274
+ }
275
+
276
+ this._recordEvent(event)
277
+ }
278
+
279
+ // Advanced gesture tracking methods
280
+ recordMultiTouch(touchCount: number, target?: string): void {
281
+ const event: GestureEvent = {
282
+ type: 'multiTouch',
283
+ timestamp: Date.now(),
284
+ target,
285
+ metadata: {
286
+ touchCount,
287
+ screenWidth: this.screenDimensions?.width,
288
+ screenHeight: this.screenDimensions?.height,
289
+ },
290
+ }
291
+
292
+ this._recordEvent(event)
293
+ }
294
+
295
+ recordScroll(direction: string, distance: number, velocity: number, target?: string): void {
296
+ const event: GestureEvent = {
297
+ type: 'scroll',
298
+ timestamp: Date.now(),
299
+ target,
300
+ metadata: {
301
+ direction,
302
+ distance,
303
+ velocity,
304
+ screenWidth: this.screenDimensions?.width,
305
+ screenHeight: this.screenDimensions?.height,
306
+ },
307
+ }
308
+
309
+ this._recordEvent(event)
310
+ }
311
+
312
+ recordZoom(scale: number, target?: string, velocity?: number): void {
313
+ const event: GestureEvent = {
314
+ type: 'zoom',
315
+ timestamp: Date.now(),
316
+ target,
317
+ metadata: {
318
+ scale,
319
+ velocity: velocity || 0,
320
+ screenWidth: this.screenDimensions?.width,
321
+ screenHeight: this.screenDimensions?.height,
322
+ },
323
+ }
324
+
325
+ this._recordEvent(event)
326
+ }
327
+
328
+ // Gesture sequence tracking
329
+ recordGestureSequence(gestures: string[], duration: number, target?: string): void {
330
+ const event: GestureEvent = {
331
+ type: 'gestureSequence',
332
+ timestamp: Date.now(),
333
+ target,
334
+ metadata: {
335
+ gestures: gestures.join(','),
336
+ duration,
337
+ gestureCount: gestures.length,
338
+ screenWidth: this.screenDimensions?.width,
339
+ screenHeight: this.screenDimensions?.height,
340
+ },
341
+ }
342
+
343
+ this._recordEvent(event)
344
+ }
345
+
346
+ // Error tracking for gesture failures
347
+ recordGestureError(error: Error, gestureType: string, target?: string): void {
348
+ const event: GestureEvent = {
349
+ type: 'gestureError',
350
+ timestamp: Date.now(),
351
+ target,
352
+ metadata: {
353
+ errorType: error.name,
354
+ errorMessage: error.message,
355
+ gestureType,
356
+ screenWidth: this.screenDimensions?.width,
357
+ screenHeight: this.screenDimensions?.height,
358
+ },
359
+ }
360
+
361
+ this._recordEvent(event)
362
+
363
+ // Also record as OpenTelemetry error span
364
+ try {
365
+ const span = trace.getTracer('gesture').startSpan(`Gesture.${gestureType}.error`, {
366
+ attributes: {
367
+ 'gesture.type': gestureType,
368
+ 'gesture.error': true,
369
+ 'gesture.error.type': error.name,
370
+ 'gesture.error.message': error.message,
371
+ 'gesture.timestamp': Date.now(),
372
+ },
373
+ })
374
+
375
+ span.setStatus({ code: SpanStatusCode.ERROR, message: error.message })
376
+ span.recordException(error)
377
+ span.end()
378
+ } catch (spanError) {
379
+ console.warn('Failed to record error span:', spanError)
380
+ }
381
+ }
382
+
383
+ // Performance monitoring
384
+ recordGesturePerformance(gestureType: string, duration: number, target?: string): void {
385
+ const event: GestureEvent = {
386
+ type: 'gesturePerformance',
387
+ timestamp: Date.now(),
388
+ target,
389
+ metadata: {
390
+ gestureType,
391
+ duration,
392
+ performance: 'monitoring',
393
+ screenWidth: this.screenDimensions?.width,
394
+ screenHeight: this.screenDimensions?.height,
395
+ },
396
+ }
397
+
398
+ this._recordEvent(event)
399
+ }
400
+
401
+ // Get recorded events
402
+ getEvents(): GestureEvent[] {
403
+ return [...this.events]
404
+ }
405
+
406
+ // Clear events
407
+ clearEvents(): void {
408
+ this.events = []
409
+ }
410
+
411
+ // Get event statistics
412
+ getEventStats(): Record<string, number> {
413
+ const stats: Record<string, number> = {}
414
+ this.events.forEach(event => {
415
+ stats[event.type] = (stats[event.type] || 0) + 1
416
+ })
417
+ return stats
418
+ }
419
+
420
+ // Set gesture throttle
421
+ setGestureThrottle(throttleMs: number): void {
422
+ this.gestureThrottleMs = throttleMs
423
+ }
424
+
425
+ // Get recording status
426
+ isRecordingEnabled(): boolean {
427
+ return this.isRecording
428
+ }
429
+ }