@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.
- package/LICENSE +21 -0
- package/README.md +226 -0
- package/babel.config.js +13 -0
- package/dist/config/masking.d.ts +30 -0
- package/dist/config/masking.js +1 -0
- package/dist/config/masking.js.map +1 -0
- package/dist/expo.d.ts +11 -0
- package/dist/expo.js +1 -0
- package/dist/expo.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/otel/helpers.d.ts +3 -0
- package/dist/otel/helpers.js +1 -0
- package/dist/otel/helpers.js.map +1 -0
- package/dist/otel/index.d.ts +40 -0
- package/dist/otel/index.js +1 -0
- package/dist/otel/index.js.map +1 -0
- package/dist/otel/instrumentations/gestureInstrumentation.d.ts +15 -0
- package/dist/otel/instrumentations/gestureInstrumentation.js +1 -0
- package/dist/otel/instrumentations/gestureInstrumentation.js.map +1 -0
- package/dist/otel/instrumentations/index.d.ts +5 -0
- package/dist/otel/instrumentations/index.js +1 -0
- package/dist/otel/instrumentations/index.js.map +1 -0
- package/dist/otel/instrumentations/reactNativeInstrumentation.d.ts +8 -0
- package/dist/otel/instrumentations/reactNativeInstrumentation.js +1 -0
- package/dist/otel/instrumentations/reactNativeInstrumentation.js.map +1 -0
- package/dist/otel/instrumentations/reactNavigationInstrumentation.d.ts +12 -0
- package/dist/otel/instrumentations/reactNavigationInstrumentation.js +1 -0
- package/dist/otel/instrumentations/reactNavigationInstrumentation.js.map +1 -0
- package/dist/recorder/gestureRecorder.d.ts +42 -0
- package/dist/recorder/gestureRecorder.js +1 -0
- package/dist/recorder/gestureRecorder.js.map +1 -0
- package/dist/recorder/index.d.ts +16 -0
- package/dist/recorder/index.js +1 -0
- package/dist/recorder/index.js.map +1 -0
- package/dist/recorder/navigationTracker.d.ts +43 -0
- package/dist/recorder/navigationTracker.js +1 -0
- package/dist/recorder/navigationTracker.js.map +1 -0
- package/dist/recorder/screenRecorder.d.ts +46 -0
- package/dist/recorder/screenRecorder.js +1 -0
- package/dist/recorder/screenRecorder.js.map +1 -0
- package/dist/services/api.service.d.ts +20 -0
- package/dist/services/api.service.js +1 -0
- package/dist/services/api.service.js.map +1 -0
- package/dist/services/storage.service.d.ts +23 -0
- package/dist/services/storage.service.js +1 -0
- package/dist/services/storage.service.js.map +1 -0
- package/dist/sessionRecorder.d.ts +54 -0
- package/dist/sessionRecorder.js +1 -0
- package/dist/sessionRecorder.js.map +1 -0
- package/dist/types/index.d.ts +81 -0
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/platform.d.ts +9 -0
- package/dist/utils/platform.js +1 -0
- package/dist/utils/platform.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/dist/version.js.map +1 -0
- package/examples/sample-expo-app/README.md +142 -0
- package/examples/sample-expo-app/app/(tabs)/_layout.tsx +60 -0
- package/examples/sample-expo-app/app/(tabs)/explore.tsx +110 -0
- package/examples/sample-expo-app/app/(tabs)/index.tsx +125 -0
- package/examples/sample-expo-app/app/(tabs)/posts.tsx +96 -0
- package/examples/sample-expo-app/app/(tabs)/users.tsx +131 -0
- package/examples/sample-expo-app/app/+not-found.tsx +32 -0
- package/examples/sample-expo-app/app/_layout.tsx +53 -0
- package/examples/sample-expo-app/app/post/[id].tsx +199 -0
- package/examples/sample-expo-app/app/user/[id].tsx +270 -0
- package/examples/sample-expo-app/app.json +42 -0
- package/examples/sample-expo-app/assets/fonts/SpaceMono-Regular.ttf +0 -0
- package/examples/sample-expo-app/assets/images/adaptive-icon.png +0 -0
- package/examples/sample-expo-app/assets/images/favicon.png +0 -0
- package/examples/sample-expo-app/assets/images/icon.png +0 -0
- package/examples/sample-expo-app/assets/images/partial-react-logo.png +0 -0
- package/examples/sample-expo-app/assets/images/react-logo.png +0 -0
- package/examples/sample-expo-app/assets/images/react-logo@2x.png +0 -0
- package/examples/sample-expo-app/assets/images/react-logo@3x.png +0 -0
- package/examples/sample-expo-app/assets/images/splash-icon.png +0 -0
- package/examples/sample-expo-app/components/Collapsible.tsx +45 -0
- package/examples/sample-expo-app/components/ErrorView.tsx +52 -0
- package/examples/sample-expo-app/components/ExternalLink.tsx +24 -0
- package/examples/sample-expo-app/components/HapticTab.tsx +18 -0
- package/examples/sample-expo-app/components/HelloWave.tsx +40 -0
- package/examples/sample-expo-app/components/LoadingSpinner.tsx +34 -0
- package/examples/sample-expo-app/components/ParallaxScrollView.tsx +82 -0
- package/examples/sample-expo-app/components/ThemedText.tsx +60 -0
- package/examples/sample-expo-app/components/ThemedView.tsx +14 -0
- package/examples/sample-expo-app/components/ui/IconSymbol.ios.tsx +32 -0
- package/examples/sample-expo-app/components/ui/IconSymbol.tsx +41 -0
- package/examples/sample-expo-app/components/ui/TabBarBackground.ios.tsx +19 -0
- package/examples/sample-expo-app/components/ui/TabBarBackground.tsx +6 -0
- package/examples/sample-expo-app/constants/Colors.ts +26 -0
- package/examples/sample-expo-app/eslint.config.js +10 -0
- package/examples/sample-expo-app/hooks/useApi.ts +41 -0
- package/examples/sample-expo-app/hooks/useColorScheme.ts +1 -0
- package/examples/sample-expo-app/hooks/useColorScheme.web.ts +21 -0
- package/examples/sample-expo-app/hooks/useThemeColor.ts +21 -0
- package/examples/sample-expo-app/metro.config.js +26 -0
- package/examples/sample-expo-app/package-lock.json +26296 -0
- package/examples/sample-expo-app/package.json +59 -0
- package/examples/sample-expo-app/scripts/reset-project.js +112 -0
- package/examples/sample-expo-app/services/api.ts +98 -0
- package/examples/sample-expo-app/tsconfig.json +17 -0
- package/examples/sample-expo-app/utils/navigation.ts +19 -0
- package/package.json +98 -0
- package/src/config/masking.ts +78 -0
- package/src/expo.ts +41 -0
- package/src/index.ts +20 -0
- package/src/otel/helpers.ts +21 -0
- package/src/otel/index.ts +348 -0
- package/src/otel/instrumentations/gestureInstrumentation.ts +141 -0
- package/src/otel/instrumentations/index.ts +86 -0
- package/src/otel/instrumentations/reactNativeInstrumentation.ts +164 -0
- package/src/otel/instrumentations/reactNavigationInstrumentation.ts +114 -0
- package/src/recorder/gestureRecorder.ts +429 -0
- package/src/recorder/index.ts +71 -0
- package/src/recorder/navigationTracker.ts +447 -0
- package/src/recorder/screenRecorder.ts +411 -0
- package/src/services/api.service.ts +78 -0
- package/src/services/storage.service.ts +130 -0
- package/src/sessionRecorder.ts +367 -0
- package/src/types/expo.d.ts +23 -0
- package/src/types/index.ts +88 -0
- package/src/utils/platform.ts +75 -0
- package/src/version.ts +1 -0
- 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
|
+
}
|