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