@multiplayer-app/session-recorder-react-native 0.0.1-alpha.5 → 0.0.1-alpha.7
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/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.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/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/gestureRecorder.d.ts +57 -3
- package/dist/recorder/gestureRecorder.js +1 -1
- package/dist/recorder/gestureRecorder.js.map +1 -1
- package/dist/recorder/index.d.ts +61 -7
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.js.map +1 -1
- package/dist/recorder/screenRecorder.d.ts +58 -10
- 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 +40 -2
- package/dist/session-recorder.js +1 -1
- package/dist/session-recorder.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/rrweb.d.ts +108 -0
- package/dist/types/rrweb.js +1 -0
- package/dist/types/rrweb.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 +4 -3
- package/src/config/constants.ts +3 -3
- package/src/context/SessionRecorderContext.tsx +93 -16
- package/src/index.ts +1 -0
- package/src/otel/helpers.ts +37 -20
- package/src/otel/index.ts +7 -3
- package/src/otel/instrumentations/index.ts +79 -38
- package/src/otel/instrumentations/reactNavigationInstrumentation.ts +5 -0
- package/src/recorder/eventExporter.ts +138 -0
- package/src/recorder/gestureRecorder.ts +124 -3
- package/src/recorder/index.ts +130 -21
- package/src/recorder/screenRecorder.ts +184 -110
- package/src/services/api.service.ts +1 -8
- package/src/services/storage.service.ts +1 -0
- package/src/session-recorder.ts +91 -12
- package/src/types/index.ts +2 -1
- package/src/types/rrweb.ts +122 -0
- package/src/version.ts +1 -1
- package/dist/exporters.d.ts +0 -3
- package/dist/exporters.js +0 -1
- package/dist/exporters.js.map +0 -1
- package/dist/sessionRecorder.d.ts +0 -133
- package/dist/sessionRecorder.js +0 -1
- package/dist/sessionRecorder.js.map +0 -1
|
@@ -1,22 +1,30 @@
|
|
|
1
|
-
import { ScreenEvent, RecorderConfig } from '../types'
|
|
1
|
+
import { ScreenEvent, RecorderConfig, EventType, FullSnapshotEvent, SerializedNodeWithId, EventRecorder } from '../types'
|
|
2
2
|
import { trace, SpanStatusCode } from '@opentelemetry/api'
|
|
3
|
-
import { Dimensions
|
|
4
|
-
import { captureRef
|
|
3
|
+
import { Dimensions } from 'react-native'
|
|
4
|
+
import { captureRef } from 'react-native-view-shot'
|
|
5
5
|
|
|
6
|
-
export class ScreenRecorder {
|
|
6
|
+
export class ScreenRecorder implements EventRecorder {
|
|
7
7
|
private config?: RecorderConfig
|
|
8
8
|
private isRecording = false
|
|
9
9
|
private events: ScreenEvent[] = []
|
|
10
10
|
private captureInterval?: NodeJS.Timeout
|
|
11
11
|
private captureCount: number = 0
|
|
12
12
|
private maxCaptures: number = 100 // Limit captures to prevent memory issues
|
|
13
|
-
private captureQuality: number = 0.
|
|
14
|
-
private captureFormat: 'png' | 'jpg'
|
|
13
|
+
private captureQuality: number = 0.3
|
|
14
|
+
private captureFormat: 'png' | 'jpg' = 'jpg'
|
|
15
15
|
private screenDimensions: { width: number; height: number } | null = null
|
|
16
16
|
private currentScreen: string | null = null
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
private eventRecorder?: EventRecorder
|
|
18
|
+
private nodeIdCounter: number = 1
|
|
19
|
+
private viewShotRef: any = null
|
|
20
|
+
private lastScreenCapture: string | null = null
|
|
21
|
+
private lastScreenHash: string | null = null
|
|
22
|
+
private enableChangeDetection: boolean = true
|
|
23
|
+
private hashSampleSize: number = 100
|
|
24
|
+
|
|
25
|
+
init(config: RecorderConfig, eventRecorder?: EventRecorder): void {
|
|
19
26
|
this.config = config
|
|
27
|
+
this.eventRecorder = eventRecorder
|
|
20
28
|
this._getScreenDimensions()
|
|
21
29
|
}
|
|
22
30
|
|
|
@@ -24,7 +32,13 @@ export class ScreenRecorder {
|
|
|
24
32
|
this.isRecording = true
|
|
25
33
|
this.events = []
|
|
26
34
|
this.captureCount = 0
|
|
35
|
+
this.lastScreenCapture = null
|
|
36
|
+
this.lastScreenHash = null
|
|
27
37
|
this._startPeriodicCapture()
|
|
38
|
+
|
|
39
|
+
// Capture initial screen immediately
|
|
40
|
+
this._captureScreen()
|
|
41
|
+
|
|
28
42
|
// Screen recording started
|
|
29
43
|
}
|
|
30
44
|
|
|
@@ -41,7 +55,7 @@ export class ScreenRecorder {
|
|
|
41
55
|
|
|
42
56
|
resume(): void {
|
|
43
57
|
this.isRecording = true
|
|
44
|
-
this._startPeriodicCapture()
|
|
58
|
+
// this._startPeriodicCapture()
|
|
45
59
|
}
|
|
46
60
|
|
|
47
61
|
private _getScreenDimensions(): void {
|
|
@@ -75,121 +89,146 @@ export class ScreenRecorder {
|
|
|
75
89
|
if (!this.isRecording || this.captureCount >= this.maxCaptures) return
|
|
76
90
|
|
|
77
91
|
try {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const screenEvent: ScreenEvent = {
|
|
84
|
-
screenName: this.currentScreen || 'unknown',
|
|
85
|
-
type: 'screenCapture',
|
|
86
|
-
timestamp: Date.now(),
|
|
87
|
-
dataUrl: screenData,
|
|
88
|
-
metadata: {
|
|
89
|
-
captureTime,
|
|
90
|
-
captureCount: this.captureCount + 1,
|
|
91
|
-
quality: this.captureQuality,
|
|
92
|
-
format: this.captureFormat,
|
|
93
|
-
screenWidth: this.screenDimensions?.width,
|
|
94
|
-
screenHeight: this.screenDimensions?.height,
|
|
95
|
-
},
|
|
96
|
-
}
|
|
92
|
+
const base64Image = await this._captureScreenBase64()
|
|
93
|
+
|
|
94
|
+
if (base64Image) {
|
|
95
|
+
// Check if screen has changed by comparing with previous capture
|
|
96
|
+
const hasChanged = this.enableChangeDetection ? this._hasScreenChanged(base64Image) : true
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
if (hasChanged) {
|
|
99
|
+
this._createAndEmitFullSnapshotEvent(base64Image)
|
|
100
|
+
this.lastScreenCapture = base64Image
|
|
101
|
+
this.lastScreenHash = this._generateScreenHash(base64Image)
|
|
102
|
+
this.captureCount++
|
|
103
|
+
}
|
|
102
104
|
}
|
|
103
105
|
} catch (error) {
|
|
104
|
-
|
|
105
|
-
this._recordScreenCaptureError(error instanceof Error ? error : new Error(String(error)))
|
|
106
|
+
this._recordScreenCaptureError(error as Error)
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
private async _captureScreenBase64(): Promise<string | null> {
|
|
111
|
+
try {
|
|
112
|
+
if (!this.viewShotRef) {
|
|
113
|
+
// console.warn('ViewShot ref not available for screen capture')
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
109
116
|
|
|
117
|
+
// Capture the screen using react-native-view-shot
|
|
118
|
+
const result = await captureRef(this.viewShotRef, {
|
|
119
|
+
format: this.captureFormat,
|
|
120
|
+
quality: this.captureQuality,
|
|
121
|
+
result: 'base64',
|
|
122
|
+
})
|
|
110
123
|
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
// Try react-native-view-shot screen capture first
|
|
114
|
-
return await this._captureWithViewShot()
|
|
124
|
+
return result
|
|
115
125
|
} catch (error) {
|
|
116
|
-
//
|
|
126
|
+
// console.error('Failed to capture screen:', error)
|
|
127
|
+
return null
|
|
117
128
|
}
|
|
129
|
+
}
|
|
118
130
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
private _createAndEmitFullSnapshotEvent(base64Image: string): void {
|
|
132
|
+
if (!this.screenDimensions) return
|
|
133
|
+
|
|
134
|
+
const { width, height } = this.screenDimensions
|
|
135
|
+
|
|
136
|
+
// Create a virtual DOM node representing the screen as an image
|
|
137
|
+
const imageNode: SerializedNodeWithId = {
|
|
138
|
+
type: 1, // Element node
|
|
139
|
+
id: this.nodeIdCounter++,
|
|
140
|
+
tagName: 'img',
|
|
141
|
+
attributes: {
|
|
142
|
+
src: `data:image/${this.captureFormat};base64,${base64Image}`,
|
|
143
|
+
width: width.toString(),
|
|
144
|
+
height: height.toString(),
|
|
145
|
+
style: `width: ${width}px; height: ${height}px;`,
|
|
146
|
+
},
|
|
127
147
|
}
|
|
128
148
|
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
149
|
+
// Create the root container
|
|
150
|
+
const rootNode: SerializedNodeWithId = {
|
|
151
|
+
type: 1, // Element node
|
|
152
|
+
id: this.nodeIdCounter++,
|
|
153
|
+
tagName: 'div',
|
|
154
|
+
attributes: {
|
|
155
|
+
style: `width: ${width}px; height: ${height}px; position: relative;`,
|
|
156
|
+
},
|
|
157
|
+
childNodes: [imageNode],
|
|
158
|
+
}
|
|
136
159
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
160
|
+
const fullSnapshotEvent: FullSnapshotEvent = {
|
|
161
|
+
type: EventType.FullSnapshot,
|
|
162
|
+
data: {
|
|
163
|
+
node: rootNode,
|
|
164
|
+
initialOffset: {
|
|
165
|
+
left: 0,
|
|
166
|
+
top: 0,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
timestamp: Date.now(),
|
|
140
170
|
}
|
|
141
|
-
return null
|
|
142
|
-
}
|
|
143
171
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const Screenshot = require('react-native-screenshot')
|
|
147
|
-
const options = {
|
|
148
|
-
format: this.captureFormat,
|
|
149
|
-
quality: this.captureQuality,
|
|
150
|
-
}
|
|
172
|
+
this.recordEvent(fullSnapshotEvent)
|
|
173
|
+
}
|
|
151
174
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Check if the screen has changed by comparing with the previous capture
|
|
177
|
+
* @param currentBase64 - Current screen capture as base64
|
|
178
|
+
* @returns true if screen has changed, false otherwise
|
|
179
|
+
*/
|
|
180
|
+
private _hasScreenChanged(currentBase64: string): boolean {
|
|
181
|
+
// If this is the first capture, consider it changed
|
|
182
|
+
if (!this.lastScreenCapture) {
|
|
183
|
+
return true
|
|
155
184
|
}
|
|
156
|
-
|
|
185
|
+
|
|
186
|
+
// Generate hash for current capture
|
|
187
|
+
const currentHash = this._generateScreenHash(currentBase64)
|
|
188
|
+
|
|
189
|
+
// Compare with previous hash
|
|
190
|
+
return currentHash !== this.lastScreenHash
|
|
157
191
|
}
|
|
158
192
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Generate a simple hash for screen comparison
|
|
195
|
+
* This is a lightweight hash that focuses on the beginning and end of the base64 string
|
|
196
|
+
* to detect changes without doing a full comparison
|
|
197
|
+
* @param base64Image - Base64 encoded image
|
|
198
|
+
* @returns Hash string for comparison
|
|
199
|
+
*/
|
|
200
|
+
private _generateScreenHash(base64Image: string): string {
|
|
201
|
+
// Use a simple hash that samples the beginning, middle, and end of the base64 string
|
|
202
|
+
// This is much faster than comparing the entire string
|
|
203
|
+
const sampleSize = this.hashSampleSize
|
|
204
|
+
const start = base64Image.substring(0, sampleSize)
|
|
205
|
+
const middle = base64Image.substring(
|
|
206
|
+
Math.floor(base64Image.length / 2) - sampleSize / 2,
|
|
207
|
+
Math.floor(base64Image.length / 2) + sampleSize / 2,
|
|
208
|
+
)
|
|
209
|
+
const end = base64Image.substring(base64Image.length - sampleSize)
|
|
210
|
+
|
|
211
|
+
// Combine samples and create a simple hash
|
|
212
|
+
const combined = start + middle + end
|
|
213
|
+
return this._simpleHash(combined)
|
|
168
214
|
}
|
|
169
215
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
216
|
+
/**
|
|
217
|
+
* Simple hash function for string comparison
|
|
218
|
+
* @param str - String to hash
|
|
219
|
+
* @returns Hash value as string
|
|
220
|
+
*/
|
|
221
|
+
private _simpleHash(str: string): string {
|
|
222
|
+
let hash = 0
|
|
223
|
+
for (let i = 0; i < str.length; i++) {
|
|
224
|
+
const char = str.charCodeAt(i)
|
|
225
|
+
hash = ((hash << 5) - hash) + char
|
|
226
|
+
hash = hash & hash // Convert to 32-bit integer
|
|
227
|
+
}
|
|
228
|
+
return Math.abs(hash).toString(36)
|
|
229
|
+
}
|
|
175
230
|
|
|
176
|
-
// Return a simple data URL for placeholder
|
|
177
|
-
const svgContent = `
|
|
178
|
-
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
179
|
-
<rect width="100%" height="100%" fill="#f0f0f0"/>
|
|
180
|
-
<text x="50%" y="50%" text-anchor="middle" dy=".3em" fill="#666" font-family="Arial" font-size="16">
|
|
181
|
-
Screen Capture Placeholder
|
|
182
|
-
</text>
|
|
183
|
-
<text x="50%" y="60%" text-anchor="middle" dy=".3em" fill="#666" font-family="Arial" font-size="14">
|
|
184
|
-
Captured at ${new Date().toLocaleTimeString()}
|
|
185
|
-
</text>
|
|
186
|
-
</svg>
|
|
187
|
-
`
|
|
188
231
|
|
|
189
|
-
// Use a simple base64 encoding alternative for React Native
|
|
190
|
-
const base64Content = Buffer.from(svgContent, 'utf8').toString('base64')
|
|
191
|
-
return `data:image/svg+xml;base64,${base64Content}`
|
|
192
|
-
}
|
|
193
232
|
|
|
194
233
|
private _sendEvent(event: ScreenEvent): void {
|
|
195
234
|
// Screen event recorded
|
|
@@ -238,15 +277,6 @@ export class ScreenRecorder {
|
|
|
238
277
|
}
|
|
239
278
|
}
|
|
240
279
|
|
|
241
|
-
// Manual screen capture methods
|
|
242
|
-
async captureScreenNow(): Promise<string | null> {
|
|
243
|
-
if (!this.isRecording) {
|
|
244
|
-
// Screen recording not active - silently continue
|
|
245
|
-
return null
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return await this._performScreenCapture()
|
|
249
|
-
}
|
|
250
280
|
|
|
251
281
|
async captureSpecificElement(elementRef: any, options?: {
|
|
252
282
|
format?: 'png' | 'jpg' | 'webp'
|
|
@@ -279,7 +309,7 @@ export class ScreenRecorder {
|
|
|
279
309
|
this.captureQuality = Math.max(0.1, Math.min(1.0, quality))
|
|
280
310
|
}
|
|
281
311
|
|
|
282
|
-
setCaptureFormat(format: 'png' | 'jpg'
|
|
312
|
+
setCaptureFormat(format: 'png' | 'jpg'): void {
|
|
283
313
|
this.captureFormat = format
|
|
284
314
|
}
|
|
285
315
|
|
|
@@ -287,6 +317,22 @@ export class ScreenRecorder {
|
|
|
287
317
|
this.maxCaptures = Math.max(1, max)
|
|
288
318
|
}
|
|
289
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Enable or disable change detection
|
|
322
|
+
* @param enabled - Whether to enable change detection
|
|
323
|
+
*/
|
|
324
|
+
setChangeDetection(enabled: boolean): void {
|
|
325
|
+
this.enableChangeDetection = enabled
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Set the hash sample size for change detection
|
|
330
|
+
* @param size - Number of characters to sample from each part of the image
|
|
331
|
+
*/
|
|
332
|
+
setHashSampleSize(size: number): void {
|
|
333
|
+
this.hashSampleSize = Math.max(10, Math.min(1000, size))
|
|
334
|
+
}
|
|
335
|
+
|
|
290
336
|
// Performance monitoring
|
|
291
337
|
recordScreenPerformance(screenName: string, loadTime: number): void {
|
|
292
338
|
const event: ScreenEvent = {
|
|
@@ -386,4 +432,32 @@ export class ScreenRecorder {
|
|
|
386
432
|
this.clearEvents()
|
|
387
433
|
// Screen recorder shutdown
|
|
388
434
|
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Set the viewshot ref for screen capture
|
|
438
|
+
* @param ref - React Native View ref for screen capture
|
|
439
|
+
*/
|
|
440
|
+
setViewShotRef(ref: any): void {
|
|
441
|
+
this.viewShotRef = ref
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Force capture screen (useful after touch interactions)
|
|
446
|
+
* This bypasses the change detection and always captures
|
|
447
|
+
*/
|
|
448
|
+
forceCapture(): void {
|
|
449
|
+
if (!this.isRecording) return
|
|
450
|
+
|
|
451
|
+
this._captureScreen()
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Record an rrweb event
|
|
456
|
+
* @param event - The rrweb event to record
|
|
457
|
+
*/
|
|
458
|
+
recordEvent(event: any): void {
|
|
459
|
+
if (this.eventRecorder) {
|
|
460
|
+
this.eventRecorder.recordEvent(event)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
389
463
|
}
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import { SessionRecorderOptions, IResourceAttributes, ISessionAttributes } from '../types'
|
|
2
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
3
|
export interface StartSessionRequest {
|
|
12
4
|
name?: string
|
|
13
5
|
stoppedAt?: string | number
|
|
@@ -37,6 +29,7 @@ export class ApiService {
|
|
|
37
29
|
}
|
|
38
30
|
|
|
39
31
|
init(config: SessionRecorderOptions): void {
|
|
32
|
+
|
|
40
33
|
this.config = {
|
|
41
34
|
...this.config,
|
|
42
35
|
...config,
|
|
@@ -87,6 +87,7 @@ export class StorageService {
|
|
|
87
87
|
saveSessionState(state: SessionState): void {
|
|
88
88
|
try {
|
|
89
89
|
StorageService.cache.sessionState = state
|
|
90
|
+
|
|
90
91
|
AsyncStorage.setItem(StorageService.SESSION_STATE_KEY, state).catch(error => {
|
|
91
92
|
// Failed to persist session state - silently continue
|
|
92
93
|
})
|
package/src/session-recorder.ts
CHANGED
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
SessionState,
|
|
10
10
|
ISessionRecorder,
|
|
11
11
|
SessionRecorderConfigs,
|
|
12
|
-
SessionRecorderOptions
|
|
12
|
+
SessionRecorderOptions,
|
|
13
|
+
RRWebEvent,
|
|
14
|
+
EventRecorder
|
|
13
15
|
} from './types'
|
|
14
16
|
import { getFormattedDate, isSessionActive, getNavigatorInfo } from './utils'
|
|
15
17
|
import { setMaxCapturingHttpPayloadSize, setShouldRecordHttpData } from './patch/xhr'
|
|
@@ -21,7 +23,7 @@ import { ApiService, StartSessionRequest, StopSessionRequest } from './services/
|
|
|
21
23
|
// Utility functions for React Native
|
|
22
24
|
|
|
23
25
|
|
|
24
|
-
class SessionRecorder implements ISessionRecorder {
|
|
26
|
+
class SessionRecorder implements ISessionRecorder, EventRecorder {
|
|
25
27
|
private _isInitialized = false
|
|
26
28
|
private _configs: SessionRecorderConfigs | null = null
|
|
27
29
|
private _apiService = new ApiService()
|
|
@@ -109,13 +111,13 @@ class SessionRecorder implements ISessionRecorder {
|
|
|
109
111
|
*/
|
|
110
112
|
constructor() {
|
|
111
113
|
// Initialize with stored session data if available
|
|
112
|
-
|
|
114
|
+
StorageService.initialize()
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
private async _loadStoredSessionData(): Promise<void> {
|
|
116
118
|
try {
|
|
119
|
+
await StorageService.initialize()
|
|
117
120
|
const storedData = await this._storageService.getAllSessionData()
|
|
118
|
-
|
|
119
121
|
if (isSessionActive(storedData.sessionObject, storedData.sessionType === SessionType.CONTINUOUS)) {
|
|
120
122
|
this.session = storedData.sessionObject
|
|
121
123
|
this.sessionId = storedData.sessionId
|
|
@@ -142,20 +144,22 @@ class SessionRecorder implements ISessionRecorder {
|
|
|
142
144
|
*/
|
|
143
145
|
public async init(configs: SessionRecorderOptions): Promise<void> {
|
|
144
146
|
this._configs = getSessionRecorderConfig({ ...this._configs, ...configs })
|
|
145
|
-
|
|
146
147
|
this._isInitialized = true
|
|
147
148
|
this._checkOperation('init')
|
|
148
|
-
|
|
149
|
-
await StorageService.initialize()
|
|
150
|
-
|
|
149
|
+
await this._loadStoredSessionData()
|
|
151
150
|
setMaxCapturingHttpPayloadSize(this._configs.maxCapturingHttpPayloadSize || DEFAULT_MAX_HTTP_CAPTURING_PAYLOAD_SIZE)
|
|
152
151
|
setShouldRecordHttpData(!this._configs.captureBody, this._configs.captureHeaders)
|
|
153
152
|
|
|
154
|
-
this._tracer.init(this._configs)
|
|
155
|
-
this._apiService.init(this._configs)
|
|
156
153
|
|
|
154
|
+
try {
|
|
155
|
+
this._apiService.init(this._configs)
|
|
156
|
+
this._tracer.init(this._configs)
|
|
157
|
+
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error('Failed to initialize API service:', error)
|
|
160
|
+
}
|
|
157
161
|
if (this._configs.apiKey) {
|
|
158
|
-
this._recorder.init(this._configs)
|
|
162
|
+
this._recorder.init(this._configs, this)
|
|
159
163
|
}
|
|
160
164
|
|
|
161
165
|
if (this.sessionId && (this.sessionState === SessionState.started || this.sessionState === SessionState.paused)) {
|
|
@@ -411,7 +415,7 @@ class SessionRecorder implements ISessionRecorder {
|
|
|
411
415
|
private _setupSessionAndStart(session: ISession, configureExporters: boolean = true): void {
|
|
412
416
|
if (configureExporters && session.tempApiKey) {
|
|
413
417
|
this._configs!.apiKey = session.tempApiKey
|
|
414
|
-
this._recorder.init(this._configs
|
|
418
|
+
this._recorder.init(this._configs!, this)
|
|
415
419
|
this._tracer.init(this._configs!)
|
|
416
420
|
this._apiService.updateConfigs({ apiKey: this._configs!.apiKey })
|
|
417
421
|
}
|
|
@@ -510,6 +514,81 @@ class SessionRecorder implements ISessionRecorder {
|
|
|
510
514
|
this._session.updatedAt = new Date().toISOString()
|
|
511
515
|
}
|
|
512
516
|
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Record a custom rrweb event
|
|
520
|
+
* Note: Screen capture and touch events are recorded automatically when session is started
|
|
521
|
+
* @param event - The rrweb event to record
|
|
522
|
+
*/
|
|
523
|
+
recordEvent(event: RRWebEvent): void {
|
|
524
|
+
if (!this._isInitialized || this.sessionState !== SessionState.started) {
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Forward the event to the recorder SDK
|
|
529
|
+
this._recorder.recordEvent(event)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Record touch start event (internal use - touch recording is automatic)
|
|
534
|
+
* @param x - X coordinate
|
|
535
|
+
* @param y - Y coordinate
|
|
536
|
+
* @param target - Target element identifier
|
|
537
|
+
* @param pressure - Touch pressure
|
|
538
|
+
* @internal
|
|
539
|
+
*/
|
|
540
|
+
recordTouchStart(x: number, y: number, target?: string, pressure?: number): void {
|
|
541
|
+
if (!this._isInitialized || this.sessionState !== SessionState.started) {
|
|
542
|
+
return
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Forward to gesture recorder
|
|
546
|
+
this._recorder.recordTouchStart(x, y, target, pressure)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Record touch move event (internal use - touch recording is automatic)
|
|
551
|
+
* @param x - X coordinate
|
|
552
|
+
* @param y - Y coordinate
|
|
553
|
+
* @param target - Target element identifier
|
|
554
|
+
* @param pressure - Touch pressure
|
|
555
|
+
* @internal
|
|
556
|
+
*/
|
|
557
|
+
recordTouchMove(x: number, y: number, target?: string, pressure?: number): void {
|
|
558
|
+
if (!this._isInitialized || this.sessionState !== SessionState.started) {
|
|
559
|
+
return
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Forward to gesture recorder
|
|
563
|
+
this._recorder.recordTouchMove(x, y, target, pressure)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Record touch end event (internal use - touch recording is automatic)
|
|
568
|
+
* @param x - X coordinate
|
|
569
|
+
* @param y - Y coordinate
|
|
570
|
+
* @param target - Target element identifier
|
|
571
|
+
* @param pressure - Touch pressure
|
|
572
|
+
* @internal
|
|
573
|
+
*/
|
|
574
|
+
recordTouchEnd(x: number, y: number, target?: string, pressure?: number): void {
|
|
575
|
+
if (!this._isInitialized || this.sessionState !== SessionState.started) {
|
|
576
|
+
return
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Forward to gesture recorder
|
|
580
|
+
this._recorder.recordTouchEnd(x, y, target, pressure)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Set the viewshot ref for screen capture
|
|
585
|
+
* @param ref - React Native View ref for screen capture
|
|
586
|
+
*/
|
|
587
|
+
setViewShotRef(ref: any): void {
|
|
588
|
+
if (this._recorder) {
|
|
589
|
+
this._recorder.setViewShotRef(ref)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
513
592
|
}
|
|
514
593
|
|
|
515
594
|
export default new SessionRecorder()
|
package/src/types/index.ts
CHANGED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RRWeb event types for React Native session recording
|
|
3
|
+
* Based on rrweb specification: https://github.com/rrweb-io/rrweb
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export enum EventType {
|
|
7
|
+
DomContentLoaded = 0,
|
|
8
|
+
Load = 1,
|
|
9
|
+
FullSnapshot = 2,
|
|
10
|
+
IncrementalSnapshot = 3,
|
|
11
|
+
Meta = 4,
|
|
12
|
+
Custom = 5,
|
|
13
|
+
Plugin = 6,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export enum IncrementalSource {
|
|
17
|
+
Mutation = 0,
|
|
18
|
+
MouseMove = 1,
|
|
19
|
+
MouseInteraction = 2,
|
|
20
|
+
Scroll = 3,
|
|
21
|
+
ViewportResize = 4,
|
|
22
|
+
Input = 5,
|
|
23
|
+
TouchMove = 6,
|
|
24
|
+
MediaInteraction = 7,
|
|
25
|
+
StyleSheetRule = 8,
|
|
26
|
+
CanvasMutation = 9,
|
|
27
|
+
Font = 10,
|
|
28
|
+
Selection = 11,
|
|
29
|
+
AdoptedStyleSheet = 12,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export enum MouseInteractionType {
|
|
33
|
+
MouseUp = 0,
|
|
34
|
+
MouseDown = 1,
|
|
35
|
+
Click = 2,
|
|
36
|
+
ContextMenu = 3,
|
|
37
|
+
DblClick = 4,
|
|
38
|
+
Focus = 5,
|
|
39
|
+
Blur = 6,
|
|
40
|
+
TouchStart = 7,
|
|
41
|
+
TouchMove = 8,
|
|
42
|
+
TouchEnd = 9,
|
|
43
|
+
TouchCancel = 10,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface MouseInteractionData {
|
|
47
|
+
type: MouseInteractionType
|
|
48
|
+
id: number
|
|
49
|
+
x: number
|
|
50
|
+
y: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface TouchInteractionData {
|
|
54
|
+
type: MouseInteractionType
|
|
55
|
+
id: number
|
|
56
|
+
x: number
|
|
57
|
+
y: number
|
|
58
|
+
pressure?: number
|
|
59
|
+
target?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface FullSnapshotEvent {
|
|
63
|
+
type: EventType.FullSnapshot
|
|
64
|
+
data: {
|
|
65
|
+
node: SerializedNodeWithId
|
|
66
|
+
initialOffset: {
|
|
67
|
+
left: number
|
|
68
|
+
top: number
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
timestamp: number
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface IncrementalSnapshotEvent {
|
|
75
|
+
type: EventType.IncrementalSnapshot
|
|
76
|
+
data: {
|
|
77
|
+
source: IncrementalSource
|
|
78
|
+
id?: number
|
|
79
|
+
x?: number
|
|
80
|
+
y?: number
|
|
81
|
+
type?: MouseInteractionType
|
|
82
|
+
} & Partial<MouseInteractionData> & Partial<TouchInteractionData>
|
|
83
|
+
timestamp: number
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface SerializedNodeWithId {
|
|
87
|
+
type: number
|
|
88
|
+
id: number
|
|
89
|
+
tagName?: string
|
|
90
|
+
attributes?: Record<string, string>
|
|
91
|
+
childNodes?: SerializedNodeWithId[]
|
|
92
|
+
textContent?: string
|
|
93
|
+
style?: Record<string, string>
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface RRWebEvent {
|
|
97
|
+
type: EventType
|
|
98
|
+
data: any
|
|
99
|
+
timestamp: number
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// React Native specific types
|
|
103
|
+
export interface ReactNativeScreenData {
|
|
104
|
+
width: number
|
|
105
|
+
height: number
|
|
106
|
+
base64Image: string
|
|
107
|
+
timestamp: number
|
|
108
|
+
screenName?: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface ReactNativeTouchData {
|
|
112
|
+
pageX: number
|
|
113
|
+
pageY: number
|
|
114
|
+
target?: string
|
|
115
|
+
pressure?: number
|
|
116
|
+
timestamp: number
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Event recording interface
|
|
120
|
+
export interface EventRecorder {
|
|
121
|
+
recordEvent(event: RRWebEvent): void
|
|
122
|
+
}
|