@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,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
+ }