@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,411 @@
|
|
|
1
|
+
import { ScreenEvent, RecorderConfig } from '../types'
|
|
2
|
+
import { trace, SpanStatusCode } from '@opentelemetry/api'
|
|
3
|
+
|
|
4
|
+
export class ScreenRecorder {
|
|
5
|
+
private config?: RecorderConfig
|
|
6
|
+
private isRecording = false
|
|
7
|
+
private events: ScreenEvent[] = []
|
|
8
|
+
private captureInterval?: NodeJS.Timeout
|
|
9
|
+
private captureCount: number = 0
|
|
10
|
+
private maxCaptures: number = 100 // Limit captures to prevent memory issues
|
|
11
|
+
private captureQuality: number = 0.8
|
|
12
|
+
private captureFormat: 'png' | 'jpg' | 'webp' = 'png'
|
|
13
|
+
private screenDimensions: { width: number; height: number } | null = null
|
|
14
|
+
|
|
15
|
+
init(config: RecorderConfig): void {
|
|
16
|
+
this.config = config
|
|
17
|
+
this._getScreenDimensions()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
start(): void {
|
|
21
|
+
this.isRecording = true
|
|
22
|
+
this.events = []
|
|
23
|
+
this.captureCount = 0
|
|
24
|
+
this._startPeriodicCapture()
|
|
25
|
+
console.log('Screen recording started')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
stop(): void {
|
|
29
|
+
this.isRecording = false
|
|
30
|
+
this._stopPeriodicCapture()
|
|
31
|
+
console.log('Screen recording stopped')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
pause(): void {
|
|
35
|
+
this.isRecording = false
|
|
36
|
+
this._stopPeriodicCapture()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
resume(): void {
|
|
40
|
+
this.isRecording = true
|
|
41
|
+
this._startPeriodicCapture()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private _getScreenDimensions(): void {
|
|
45
|
+
try {
|
|
46
|
+
const { Dimensions } = require('react-native')
|
|
47
|
+
this.screenDimensions = Dimensions.get('window')
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.warn('Failed to get screen dimensions:', error)
|
|
50
|
+
this.screenDimensions = { width: 375, height: 667 } // Default fallback
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private _startPeriodicCapture(): void {
|
|
55
|
+
if (this.captureInterval) {
|
|
56
|
+
clearInterval(this.captureInterval)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Capture screen every 5 seconds
|
|
60
|
+
this.captureInterval = setInterval(() => {
|
|
61
|
+
this._captureScreen()
|
|
62
|
+
}, 5000)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private _stopPeriodicCapture(): void {
|
|
66
|
+
if (this.captureInterval) {
|
|
67
|
+
clearInterval(this.captureInterval)
|
|
68
|
+
this.captureInterval = undefined
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async _captureScreen(): Promise<void> {
|
|
73
|
+
if (!this.isRecording || this.captureCount >= this.maxCaptures) return
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const startTime = Date.now()
|
|
77
|
+
const screenData = await this._performScreenCapture()
|
|
78
|
+
const captureTime = Date.now() - startTime
|
|
79
|
+
|
|
80
|
+
if (screenData) {
|
|
81
|
+
const screenEvent: ScreenEvent = {
|
|
82
|
+
type: 'screenCapture',
|
|
83
|
+
timestamp: Date.now(),
|
|
84
|
+
dataUrl: screenData,
|
|
85
|
+
metadata: {
|
|
86
|
+
captureTime,
|
|
87
|
+
captureCount: this.captureCount + 1,
|
|
88
|
+
quality: this.captureQuality,
|
|
89
|
+
format: this.captureFormat,
|
|
90
|
+
screenWidth: this.screenDimensions?.width,
|
|
91
|
+
screenHeight: this.screenDimensions?.height,
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.events.push(screenEvent)
|
|
96
|
+
this.captureCount++
|
|
97
|
+
this._sendEvent(screenEvent)
|
|
98
|
+
this._recordOpenTelemetrySpan(screenEvent)
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error('Failed to capture screen:', error)
|
|
102
|
+
this._recordScreenCaptureError(error instanceof Error ? error : new Error(String(error)))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
private async _performScreenCapture(): Promise<string | null> {
|
|
109
|
+
try {
|
|
110
|
+
// Try react-native-view-shot first
|
|
111
|
+
const ViewShot = require('react-native-view-shot')
|
|
112
|
+
if (ViewShot && ViewShot.captureRef) {
|
|
113
|
+
return await this._captureWithViewShot()
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.warn('react-native-view-shot not available:', error)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
// Try react-native-screenshot
|
|
121
|
+
// const Screenshot = require('react-native-screenshot')
|
|
122
|
+
// if (Screenshot && Screenshot.takeScreenshot) {
|
|
123
|
+
// return await this._captureWithScreenshot()
|
|
124
|
+
// }
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.warn('react-native-screenshot not available:', error)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Fallback to placeholder
|
|
130
|
+
return this._createPlaceholderImage()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private async _captureWithViewShot(): Promise<string | null> {
|
|
134
|
+
try {
|
|
135
|
+
const ViewShot = require('react-native-view-shot')
|
|
136
|
+
|
|
137
|
+
// Get the root view reference
|
|
138
|
+
const { findNodeHandle } = require('react-native')
|
|
139
|
+
const rootViewRef = this._getRootViewRef()
|
|
140
|
+
|
|
141
|
+
if (rootViewRef) {
|
|
142
|
+
const options = {
|
|
143
|
+
format: this.captureFormat,
|
|
144
|
+
quality: this.captureQuality,
|
|
145
|
+
result: 'data-uri',
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return await ViewShot.captureRef(rootViewRef, options)
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.warn('Failed to capture with ViewShot:', error)
|
|
152
|
+
}
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async _captureWithScreenshot(): Promise<string | null> {
|
|
157
|
+
try {
|
|
158
|
+
// const Screenshot = require('react-native-screenshot')
|
|
159
|
+
|
|
160
|
+
const options = {
|
|
161
|
+
format: this.captureFormat,
|
|
162
|
+
quality: this.captureQuality,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// return await Screenshot.takeScreenshot(options)
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.warn('Failed to capture with Screenshot:', error)
|
|
168
|
+
}
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private _getRootViewRef(): any {
|
|
173
|
+
try {
|
|
174
|
+
// Try to get the root view reference
|
|
175
|
+
const { AppRegistry } = require('react-native')
|
|
176
|
+
const appName = AppRegistry.getAppKeys()[0]
|
|
177
|
+
return appName ? { current: null } : null
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.warn('Failed to get root view ref:', error)
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private _createPlaceholderImage(): string {
|
|
185
|
+
// Create a simple placeholder image data URL
|
|
186
|
+
// Note: This won't work in React Native environment, but provides a fallback
|
|
187
|
+
const width = this.screenDimensions?.width || 375
|
|
188
|
+
const height = this.screenDimensions?.height || 667
|
|
189
|
+
|
|
190
|
+
// Return a simple data URL for placeholder
|
|
191
|
+
const svgContent = `
|
|
192
|
+
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
193
|
+
<rect width="100%" height="100%" fill="#f0f0f0"/>
|
|
194
|
+
<text x="50%" y="50%" text-anchor="middle" dy=".3em" fill="#666" font-family="Arial" font-size="16">
|
|
195
|
+
Screen Capture Placeholder
|
|
196
|
+
</text>
|
|
197
|
+
<text x="50%" y="60%" text-anchor="middle" dy=".3em" fill="#666" font-family="Arial" font-size="14">
|
|
198
|
+
Captured at ${new Date().toLocaleTimeString()}
|
|
199
|
+
</text>
|
|
200
|
+
</svg>
|
|
201
|
+
`
|
|
202
|
+
|
|
203
|
+
// Use a simple base64 encoding alternative for React Native
|
|
204
|
+
const base64Content = Buffer.from(svgContent, 'utf8').toString('base64')
|
|
205
|
+
return `data:image/svg+xml;base64,${base64Content}`
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private _sendEvent(event: ScreenEvent): void {
|
|
209
|
+
console.log('Screen event recorded:', {
|
|
210
|
+
type: event.type,
|
|
211
|
+
timestamp: event.timestamp,
|
|
212
|
+
captureCount: event.metadata?.captureCount,
|
|
213
|
+
captureTime: event.metadata?.captureTime,
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private _recordOpenTelemetrySpan(event: ScreenEvent): void {
|
|
218
|
+
try {
|
|
219
|
+
const span = trace.getTracer('screen').startSpan(`Screen.${event.type}`, {
|
|
220
|
+
attributes: {
|
|
221
|
+
'screen.type': event.type,
|
|
222
|
+
'screen.timestamp': event.timestamp,
|
|
223
|
+
'screen.platform': 'react-native',
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
if (event.metadata) {
|
|
228
|
+
Object.entries(event.metadata).forEach(([key, value]) => {
|
|
229
|
+
span.setAttribute(`screen.metadata.${key}`, String(value))
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
span.setStatus({ code: SpanStatusCode.OK })
|
|
234
|
+
span.end()
|
|
235
|
+
} catch (error) {
|
|
236
|
+
console.warn('Failed to record OpenTelemetry span for screen:', error)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private _recordScreenCaptureError(error: Error): void {
|
|
241
|
+
try {
|
|
242
|
+
const span = trace.getTracer('screen').startSpan('Screen.capture.error', {
|
|
243
|
+
attributes: {
|
|
244
|
+
'screen.error': true,
|
|
245
|
+
'screen.error.type': error.name,
|
|
246
|
+
'screen.error.message': error.message,
|
|
247
|
+
'screen.timestamp': Date.now(),
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message })
|
|
252
|
+
span.recordException(error)
|
|
253
|
+
span.end()
|
|
254
|
+
} catch (spanError) {
|
|
255
|
+
console.warn('Failed to record error span:', spanError)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Manual screen capture methods
|
|
260
|
+
async captureScreenNow(): Promise<string | null> {
|
|
261
|
+
if (!this.isRecording) {
|
|
262
|
+
console.warn('Screen recording not active')
|
|
263
|
+
return null
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return await this._performScreenCapture()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async captureSpecificElement(elementRef: any, options?: {
|
|
270
|
+
format?: 'png' | 'jpg' | 'webp'
|
|
271
|
+
quality?: number
|
|
272
|
+
}): Promise<string | null> {
|
|
273
|
+
try {
|
|
274
|
+
const ViewShot = require('react-native-view-shot')
|
|
275
|
+
|
|
276
|
+
const captureOptions = {
|
|
277
|
+
format: options?.format || this.captureFormat,
|
|
278
|
+
quality: options?.quality || this.captureQuality,
|
|
279
|
+
result: 'data-uri',
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return await ViewShot.captureRef(elementRef, captureOptions)
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error('Failed to capture specific element:', error)
|
|
285
|
+
return null
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Configuration methods
|
|
290
|
+
setCaptureInterval(intervalMs: number): void {
|
|
291
|
+
if (this.captureInterval) {
|
|
292
|
+
clearInterval(this.captureInterval)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (this.isRecording) {
|
|
296
|
+
this.captureInterval = setInterval(() => {
|
|
297
|
+
this._captureScreen()
|
|
298
|
+
}, intervalMs)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
setCaptureQuality(quality: number): void {
|
|
303
|
+
this.captureQuality = Math.max(0.1, Math.min(1.0, quality))
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
setCaptureFormat(format: 'png' | 'jpg' | 'webp'): void {
|
|
307
|
+
this.captureFormat = format
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
setMaxCaptures(max: number): void {
|
|
311
|
+
this.maxCaptures = Math.max(1, max)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Performance monitoring
|
|
315
|
+
recordScreenPerformance(screenName: string, loadTime: number): void {
|
|
316
|
+
const event: ScreenEvent = {
|
|
317
|
+
type: 'screenCapture',
|
|
318
|
+
timestamp: Date.now(),
|
|
319
|
+
metadata: {
|
|
320
|
+
screenName,
|
|
321
|
+
loadTime,
|
|
322
|
+
performance: 'monitoring',
|
|
323
|
+
captureCount: this.captureCount,
|
|
324
|
+
},
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.events.push(event); this._sendEvent(event); this._recordOpenTelemetrySpan(event)
|
|
328
|
+
this.events.push(event)
|
|
329
|
+
this._sendEvent(event)
|
|
330
|
+
this._recordOpenTelemetrySpan(event)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Error tracking
|
|
334
|
+
recordScreenError(error: Error, screenName?: string): void {
|
|
335
|
+
const event: ScreenEvent = {
|
|
336
|
+
type: 'screenCapture',
|
|
337
|
+
timestamp: Date.now(),
|
|
338
|
+
metadata: {
|
|
339
|
+
error: true,
|
|
340
|
+
errorType: error.name,
|
|
341
|
+
errorMessage: error.message,
|
|
342
|
+
screenName,
|
|
343
|
+
captureCount: this.captureCount,
|
|
344
|
+
},
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
this.events.push(event); this._sendEvent(event); this._recordOpenTelemetrySpan(event)
|
|
348
|
+
this.events.push(event)
|
|
349
|
+
this._sendEvent(event)
|
|
350
|
+
this._recordScreenCaptureError(error)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Get recorded events
|
|
354
|
+
getEvents(): ScreenEvent[] {
|
|
355
|
+
return [...this.events]
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Clear events
|
|
359
|
+
clearEvents(): void {
|
|
360
|
+
this.events = []
|
|
361
|
+
this.captureCount = 0
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Get screen capture statistics
|
|
365
|
+
getScreenStats(): Record<string, any> {
|
|
366
|
+
const stats = {
|
|
367
|
+
totalCaptures: this.captureCount,
|
|
368
|
+
totalEvents: this.events.length,
|
|
369
|
+
averageCaptureTime: 0,
|
|
370
|
+
successRate: 0,
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (this.events.length > 0) {
|
|
374
|
+
const captureTimes = this.events
|
|
375
|
+
.map(event => event.metadata?.captureTime || 0)
|
|
376
|
+
.filter(time => time > 0)
|
|
377
|
+
|
|
378
|
+
if (captureTimes.length > 0) {
|
|
379
|
+
stats.averageCaptureTime = captureTimes.reduce((a, b) => a + b, 0) / captureTimes.length
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const successfulCaptures = this.events.filter(event => event.dataUrl).length
|
|
383
|
+
stats.successRate = (successfulCaptures / this.events.length) * 100
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return stats
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Get recording status
|
|
390
|
+
isRecordingEnabled(): boolean {
|
|
391
|
+
return this.isRecording
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Get current configuration
|
|
395
|
+
getConfiguration(): Record<string, any> {
|
|
396
|
+
return {
|
|
397
|
+
captureInterval: this.captureInterval ? 5000 : 0, // Default 5 seconds
|
|
398
|
+
captureQuality: this.captureQuality,
|
|
399
|
+
captureFormat: this.captureFormat,
|
|
400
|
+
maxCaptures: this.maxCaptures,
|
|
401
|
+
screenDimensions: this.screenDimensions,
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Shutdown
|
|
406
|
+
shutdown(): void {
|
|
407
|
+
this.stop()
|
|
408
|
+
this.clearEvents()
|
|
409
|
+
console.log('Screen recorder shutdown')
|
|
410
|
+
}
|
|
411
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ISession, SessionRecorderOptions } from '../types'
|
|
2
|
+
|
|
3
|
+
// Type definitions for fetch API
|
|
4
|
+
type RequestInit = {
|
|
5
|
+
method?: string
|
|
6
|
+
headers?: Record<string, string>
|
|
7
|
+
body?: string
|
|
8
|
+
signal?: AbortSignal
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface StartSessionRequest {
|
|
12
|
+
application: string
|
|
13
|
+
version: string
|
|
14
|
+
environment: string
|
|
15
|
+
metadata?: Record<string, any>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface StopSessionRequest {
|
|
19
|
+
sessionId: string
|
|
20
|
+
comment?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ApiService {
|
|
24
|
+
private config?: SessionRecorderOptions
|
|
25
|
+
private baseUrl: string = 'https://api.multiplayer.app'
|
|
26
|
+
|
|
27
|
+
init(config: SessionRecorderOptions): void {
|
|
28
|
+
this.config = config
|
|
29
|
+
if (config.apiBaseUrl) {
|
|
30
|
+
this.baseUrl = config.apiBaseUrl
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private async makeRequest<T>(
|
|
35
|
+
endpoint: string,
|
|
36
|
+
options: RequestInit = {},
|
|
37
|
+
): Promise<T> {
|
|
38
|
+
if (!this.config?.apiKey) {
|
|
39
|
+
throw new Error('API key not configured')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const url = `${this.baseUrl}${endpoint}`
|
|
43
|
+
const response = await fetch(url, {
|
|
44
|
+
...options,
|
|
45
|
+
headers: {
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
'Authorization': `Bearer ${this.config.apiKey}`,
|
|
48
|
+
...options.headers,
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw new Error(`API request failed: ${response.statusText}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return response.json()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async startSession(request: StartSessionRequest): Promise<ISession> {
|
|
60
|
+
return this.makeRequest<ISession>('/sessions', {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
body: JSON.stringify(request),
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async stopSession(request: StopSessionRequest): Promise<void> {
|
|
67
|
+
return this.makeRequest<void>(`/sessions/${request.sessionId}/stop`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
body: JSON.stringify({ comment: request.comment }),
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async saveSession(sessionId: string): Promise<ISession> {
|
|
74
|
+
return this.makeRequest<ISession>(`/sessions/${sessionId}/save`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
2
|
+
import { ISession, SessionState } from '../types'
|
|
3
|
+
import { SessionType } from '@multiplayer-app/session-recorder-common'
|
|
4
|
+
|
|
5
|
+
export class StorageService {
|
|
6
|
+
private static readonly SESSION_ID_KEY = 'session_id'
|
|
7
|
+
private static readonly SESSION_TYPE_KEY = 'session_type'
|
|
8
|
+
private static readonly SESSION_STATE_KEY = 'session_state'
|
|
9
|
+
private static readonly SESSION_OBJECT_KEY = 'session_object'
|
|
10
|
+
|
|
11
|
+
async saveSessionId(sessionId: string): Promise<void> {
|
|
12
|
+
try {
|
|
13
|
+
await AsyncStorage.setItem(StorageService.SESSION_ID_KEY, sessionId)
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error('Failed to save session ID:', error)
|
|
16
|
+
throw error
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async getSessionId(): Promise<string | null> {
|
|
21
|
+
try {
|
|
22
|
+
return await AsyncStorage.getItem(StorageService.SESSION_ID_KEY)
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('Failed to get session ID:', error)
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async saveSessionType(sessionType: SessionType): Promise<void> {
|
|
30
|
+
try {
|
|
31
|
+
await AsyncStorage.setItem(StorageService.SESSION_TYPE_KEY, sessionType)
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Failed to save session type:', error)
|
|
34
|
+
throw error
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async getSessionType(): Promise<SessionType | null> {
|
|
39
|
+
try {
|
|
40
|
+
const type = await AsyncStorage.getItem(StorageService.SESSION_TYPE_KEY)
|
|
41
|
+
return type as SessionType | null
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('Failed to get session type:', error)
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async saveSessionState(state: SessionState): Promise<void> {
|
|
49
|
+
try {
|
|
50
|
+
await AsyncStorage.setItem(StorageService.SESSION_STATE_KEY, state)
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Failed to save session state:', error)
|
|
53
|
+
throw error
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getSessionState(): Promise<SessionState | null> {
|
|
58
|
+
try {
|
|
59
|
+
const state = await AsyncStorage.getItem(StorageService.SESSION_STATE_KEY)
|
|
60
|
+
return state as SessionState | null
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('Failed to get session state:', error)
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async saveSessionObject(session: ISession): Promise<void> {
|
|
68
|
+
try {
|
|
69
|
+
await AsyncStorage.setItem(StorageService.SESSION_OBJECT_KEY, JSON.stringify(session))
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error('Failed to save session object:', error)
|
|
72
|
+
throw error
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async getSessionObject(): Promise<ISession | null> {
|
|
77
|
+
try {
|
|
78
|
+
const sessionData = await AsyncStorage.getItem(StorageService.SESSION_OBJECT_KEY)
|
|
79
|
+
return sessionData ? JSON.parse(sessionData) : null
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Failed to get session object:', error)
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async clearSessionData(): Promise<void> {
|
|
87
|
+
try {
|
|
88
|
+
await AsyncStorage.multiRemove([
|
|
89
|
+
StorageService.SESSION_ID_KEY,
|
|
90
|
+
StorageService.SESSION_TYPE_KEY,
|
|
91
|
+
StorageService.SESSION_STATE_KEY,
|
|
92
|
+
StorageService.SESSION_OBJECT_KEY,
|
|
93
|
+
])
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('Failed to clear session data:', error)
|
|
96
|
+
throw error
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async getAllSessionData(): Promise<{
|
|
101
|
+
sessionId: string | null
|
|
102
|
+
sessionType: SessionType | null
|
|
103
|
+
sessionState: SessionState | null
|
|
104
|
+
sessionObject: ISession | null
|
|
105
|
+
}> {
|
|
106
|
+
try {
|
|
107
|
+
const [sessionId, sessionType, sessionState, sessionObject] = await Promise.all([
|
|
108
|
+
this.getSessionId(),
|
|
109
|
+
this.getSessionType(),
|
|
110
|
+
this.getSessionState(),
|
|
111
|
+
this.getSessionObject(),
|
|
112
|
+
])
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
sessionId,
|
|
116
|
+
sessionType,
|
|
117
|
+
sessionState,
|
|
118
|
+
sessionObject,
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error('Failed to get all session data:', error)
|
|
122
|
+
return {
|
|
123
|
+
sessionId: null,
|
|
124
|
+
sessionType: null,
|
|
125
|
+
sessionState: null,
|
|
126
|
+
sessionObject: null,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|