@multiplayer-app/session-recorder-react-native 0.0.1-alpha.7 → 0.0.1-alpha.9
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/dist/components/GestureCaptureWrapper.d.ts +6 -0
- package/dist/components/GestureCaptureWrapper.js +1 -0
- package/dist/components/GestureCaptureWrapper.js.map +1 -0
- 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/otel/helpers.js +1 -1
- package/dist/otel/helpers.js.map +1 -1
- 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/patch/xhr.js +1 -1
- package/dist/patch/xhr.js.map +1 -1
- package/dist/recorder/eventExporter.js +1 -1
- package/dist/recorder/eventExporter.js.map +1 -1
- 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 +13 -1
- package/dist/recorder/gestureRecorder.js +1 -1
- package/dist/recorder/gestureRecorder.js.map +1 -1
- package/dist/recorder/index.d.ts +1 -2
- 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 +31 -6
- package/dist/recorder/screenRecorder.js +1 -1
- package/dist/recorder/screenRecorder.js.map +1 -1
- package/dist/session-recorder.d.ts +3 -1
- package/dist/session-recorder.js +1 -1
- package/dist/session-recorder.js.map +1 -1
- package/dist/types/index.d.ts +32 -1
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/rrweb.d.ts +10 -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/package.json +3 -1
- package/src/components/GestureCaptureWrapper.tsx +110 -0
- package/src/context/SessionRecorderContext.tsx +76 -81
- package/src/otel/helpers.ts +2 -1
- package/src/otel/instrumentations/index.ts +5 -4
- package/src/otel/instrumentations/reactNativeInstrumentation.ts +2 -1
- package/src/patch/xhr.ts +2 -2
- package/src/recorder/eventExporter.ts +4 -1
- package/src/recorder/gestureHandlerRecorder.ts +157 -0
- package/src/recorder/gestureRecorder.ts +93 -19
- package/src/recorder/index.ts +16 -18
- package/src/recorder/navigationTracker.ts +2 -0
- package/src/recorder/screenRecorder.ts +125 -82
- package/src/session-recorder.ts +12 -5
- package/src/types/index.ts +44 -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
- package/example-usage.tsx +0 -174
- package/src/types/rrweb.ts +0 -122
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import { ScreenEvent, RecorderConfig,
|
|
1
|
+
import { ScreenEvent, RecorderConfig, EventRecorder } from '../types'
|
|
2
|
+
import { eventWithTime } from '@rrweb/types'
|
|
2
3
|
import { trace, SpanStatusCode } from '@opentelemetry/api'
|
|
3
4
|
import { Dimensions } from 'react-native'
|
|
4
5
|
import { captureRef } from 'react-native-view-shot'
|
|
6
|
+
import {
|
|
7
|
+
createRecordingMetaEvent,
|
|
8
|
+
createFullSnapshotEvent,
|
|
9
|
+
createIncrementalSnapshotWithImageUpdate as createIncrementalSnapshotUtil,
|
|
10
|
+
generateScreenHash,
|
|
11
|
+
logger,
|
|
12
|
+
} from '../utils'
|
|
5
13
|
|
|
6
14
|
export class ScreenRecorder implements EventRecorder {
|
|
7
15
|
private config?: RecorderConfig
|
|
@@ -21,6 +29,7 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
21
29
|
private lastScreenHash: string | null = null
|
|
22
30
|
private enableChangeDetection: boolean = true
|
|
23
31
|
private hashSampleSize: number = 100
|
|
32
|
+
private currentImageNodeId: number | null = null
|
|
24
33
|
|
|
25
34
|
init(config: RecorderConfig, eventRecorder?: EventRecorder): void {
|
|
26
35
|
this.config = config
|
|
@@ -34,6 +43,12 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
34
43
|
this.captureCount = 0
|
|
35
44
|
this.lastScreenCapture = null
|
|
36
45
|
this.lastScreenHash = null
|
|
46
|
+
this.currentImageNodeId = null // Reset image node ID for new session
|
|
47
|
+
logger.info('ScreenRecorder', 'Screen recording started')
|
|
48
|
+
// Emit screen recording started meta event
|
|
49
|
+
|
|
50
|
+
this.recordEvent(createRecordingMetaEvent())
|
|
51
|
+
|
|
37
52
|
this._startPeriodicCapture()
|
|
38
53
|
|
|
39
54
|
// Capture initial screen immediately
|
|
@@ -72,7 +87,7 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
72
87
|
clearInterval(this.captureInterval)
|
|
73
88
|
}
|
|
74
89
|
|
|
75
|
-
// Capture screen every 5 seconds
|
|
90
|
+
// Capture screen every 5 seconds (reduced frequency)
|
|
76
91
|
this.captureInterval = setInterval(() => {
|
|
77
92
|
this._captureScreen()
|
|
78
93
|
}, 5000)
|
|
@@ -96,7 +111,18 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
96
111
|
const hasChanged = this.enableChangeDetection ? this._hasScreenChanged(base64Image) : true
|
|
97
112
|
|
|
98
113
|
if (hasChanged) {
|
|
99
|
-
|
|
114
|
+
// Use incremental snapshot if we have an existing image node, otherwise create full snapshot
|
|
115
|
+
if (this.currentImageNodeId && this.lastScreenCapture) {
|
|
116
|
+
const success = this.updateScreenWithIncrementalSnapshot(base64Image)
|
|
117
|
+
if (!success) {
|
|
118
|
+
// Fallback to full snapshot if incremental update fails
|
|
119
|
+
this._createAndEmitFullSnapshotEvent(base64Image)
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
// First capture or no existing image node - create full snapshot
|
|
123
|
+
this._createAndEmitFullSnapshotEvent(base64Image)
|
|
124
|
+
}
|
|
125
|
+
|
|
100
126
|
this.lastScreenCapture = base64Image
|
|
101
127
|
this.lastScreenHash = this._generateScreenHash(base64Image)
|
|
102
128
|
this.captureCount++
|
|
@@ -110,7 +136,7 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
110
136
|
private async _captureScreenBase64(): Promise<string | null> {
|
|
111
137
|
try {
|
|
112
138
|
if (!this.viewShotRef) {
|
|
113
|
-
|
|
139
|
+
logger.warn('ScreenRecorder', 'ViewShot ref not available for screen capture')
|
|
114
140
|
return null
|
|
115
141
|
}
|
|
116
142
|
|
|
@@ -123,7 +149,7 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
123
149
|
|
|
124
150
|
return result
|
|
125
151
|
} catch (error) {
|
|
126
|
-
|
|
152
|
+
logger.error('ScreenRecorder', 'Failed to capture screen', error)
|
|
127
153
|
return null
|
|
128
154
|
}
|
|
129
155
|
}
|
|
@@ -131,45 +157,87 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
131
157
|
private _createAndEmitFullSnapshotEvent(base64Image: string): void {
|
|
132
158
|
if (!this.screenDimensions) return
|
|
133
159
|
|
|
134
|
-
|
|
160
|
+
// Use the new createFullSnapshot method
|
|
161
|
+
const fullSnapshotEvent = this.createFullSnapshot(base64Image)
|
|
162
|
+
this.recordEvent(fullSnapshotEvent)
|
|
163
|
+
}
|
|
135
164
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
height: height.toString(),
|
|
145
|
-
style: `width: ${width}px; height: ${height}px;`,
|
|
146
|
-
},
|
|
165
|
+
/**
|
|
166
|
+
* Create a full snapshot event with the given base64 image
|
|
167
|
+
* @param base64Image - Base64 encoded image data
|
|
168
|
+
* @returns Full snapshot event
|
|
169
|
+
*/
|
|
170
|
+
createFullSnapshot(base64Image: string): eventWithTime {
|
|
171
|
+
if (!this.screenDimensions) {
|
|
172
|
+
throw new Error('Screen dimensions not available')
|
|
147
173
|
}
|
|
148
174
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
175
|
+
const { width, height } = this.screenDimensions
|
|
176
|
+
this.nodeIdCounter = 1
|
|
177
|
+
|
|
178
|
+
// Use utility function to create full snapshot event
|
|
179
|
+
const fullSnapshotEvent = createFullSnapshotEvent(
|
|
180
|
+
base64Image,
|
|
181
|
+
width,
|
|
182
|
+
height,
|
|
183
|
+
this.captureFormat,
|
|
184
|
+
{ current: this.nodeIdCounter },
|
|
185
|
+
)
|
|
159
186
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
187
|
+
// Store the image node ID for future incremental updates
|
|
188
|
+
// The image node ID is the first node created (after the document)
|
|
189
|
+
this.currentImageNodeId = 2 // First element node is the image
|
|
190
|
+
|
|
191
|
+
return fullSnapshotEvent
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Create an incremental snapshot event with mutation data to update image src
|
|
196
|
+
* @param base64Image - New base64 encoded image data
|
|
197
|
+
* @param imageNodeId - ID of the image node to update
|
|
198
|
+
* @returns Incremental snapshot event with mutation data
|
|
199
|
+
*/
|
|
200
|
+
createIncrementalSnapshotWithImageUpdate(base64Image: string, imageNodeId: number): eventWithTime {
|
|
201
|
+
return createIncrementalSnapshotUtil(
|
|
202
|
+
base64Image,
|
|
203
|
+
imageNodeId,
|
|
204
|
+
this.captureFormat,
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get the current image node ID
|
|
210
|
+
* @returns The current image node ID or null if not set
|
|
211
|
+
*/
|
|
212
|
+
getCurrentImageNodeId(): number | null {
|
|
213
|
+
return this.currentImageNodeId
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Update the screen with a new image using incremental snapshot
|
|
218
|
+
* @param base64Image - New base64 encoded image data
|
|
219
|
+
* @returns true if update was successful, false otherwise
|
|
220
|
+
*/
|
|
221
|
+
updateScreenWithIncrementalSnapshot(base64Image: string): boolean {
|
|
222
|
+
if (!this.currentImageNodeId) {
|
|
223
|
+
logger.warn('ScreenRecorder', 'No image node ID available for incremental update')
|
|
224
|
+
return false
|
|
170
225
|
}
|
|
171
226
|
|
|
172
|
-
this.
|
|
227
|
+
const incrementalEvent = this.createIncrementalSnapshotWithImageUpdate(base64Image, this.currentImageNodeId)
|
|
228
|
+
this.recordEvent(incrementalEvent)
|
|
229
|
+
return true
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Force a full snapshot (useful when screen dimensions change or for debugging)
|
|
234
|
+
* @param base64Image - Base64 encoded image data
|
|
235
|
+
*/
|
|
236
|
+
forceFullSnapshot(base64Image: string): void {
|
|
237
|
+
this._createAndEmitFullSnapshotEvent(base64Image)
|
|
238
|
+
this.lastScreenCapture = base64Image
|
|
239
|
+
this.lastScreenHash = this._generateScreenHash(base64Image)
|
|
240
|
+
this.captureCount++
|
|
173
241
|
}
|
|
174
242
|
|
|
175
243
|
/**
|
|
@@ -198,38 +266,9 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
198
266
|
* @returns Hash string for comparison
|
|
199
267
|
*/
|
|
200
268
|
private _generateScreenHash(base64Image: string): string {
|
|
201
|
-
|
|
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)
|
|
269
|
+
return generateScreenHash(base64Image, this.hashSampleSize)
|
|
214
270
|
}
|
|
215
271
|
|
|
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
|
-
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
272
|
private _sendEvent(event: ScreenEvent): void {
|
|
234
273
|
// Screen event recorded
|
|
235
274
|
// Send event to backend or store locally
|
|
@@ -277,14 +316,14 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
277
316
|
}
|
|
278
317
|
}
|
|
279
318
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
319
|
+
async captureSpecificElement(
|
|
320
|
+
elementRef: any,
|
|
321
|
+
options?: {
|
|
322
|
+
format?: 'png' | 'jpg' | 'webp'
|
|
323
|
+
quality?: number
|
|
324
|
+
},
|
|
325
|
+
): Promise<string | null> {
|
|
285
326
|
try {
|
|
286
|
-
|
|
287
|
-
|
|
288
327
|
return await captureRef(elementRef)
|
|
289
328
|
} catch (error) {
|
|
290
329
|
// Failed to capture specific element - silently continue
|
|
@@ -347,7 +386,9 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
347
386
|
},
|
|
348
387
|
}
|
|
349
388
|
|
|
350
|
-
this.events.push(event)
|
|
389
|
+
this.events.push(event)
|
|
390
|
+
this._sendEvent(event)
|
|
391
|
+
this._recordOpenTelemetrySpan(event)
|
|
351
392
|
this.events.push(event)
|
|
352
393
|
this._sendEvent(event)
|
|
353
394
|
this._recordOpenTelemetrySpan(event)
|
|
@@ -368,7 +409,9 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
368
409
|
},
|
|
369
410
|
}
|
|
370
411
|
|
|
371
|
-
this.events.push(event)
|
|
412
|
+
this.events.push(event)
|
|
413
|
+
this._sendEvent(event)
|
|
414
|
+
this._recordOpenTelemetrySpan(event)
|
|
372
415
|
this.events.push(event)
|
|
373
416
|
this._sendEvent(event)
|
|
374
417
|
this._recordScreenCaptureError(error)
|
|
@@ -395,15 +438,13 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
395
438
|
}
|
|
396
439
|
|
|
397
440
|
if (this.events.length > 0) {
|
|
398
|
-
const captureTimes = this.events
|
|
399
|
-
.map(event => event.metadata?.captureTime || 0)
|
|
400
|
-
.filter(time => time > 0)
|
|
441
|
+
const captureTimes = this.events.map((event) => event.metadata?.captureTime || 0).filter((time) => time > 0)
|
|
401
442
|
|
|
402
443
|
if (captureTimes.length > 0) {
|
|
403
444
|
stats.averageCaptureTime = captureTimes.reduce((a, b) => a + b, 0) / captureTimes.length
|
|
404
445
|
}
|
|
405
446
|
|
|
406
|
-
const successfulCaptures = this.events.filter(event => event.dataUrl).length
|
|
447
|
+
const successfulCaptures = this.events.filter((event) => event.dataUrl).length
|
|
407
448
|
stats.successRate = (successfulCaptures / this.events.length) * 100
|
|
408
449
|
}
|
|
409
450
|
|
|
@@ -446,7 +487,9 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
446
487
|
* This bypasses the change detection and always captures
|
|
447
488
|
*/
|
|
448
489
|
forceCapture(): void {
|
|
449
|
-
if (!this.isRecording)
|
|
490
|
+
if (!this.isRecording) {
|
|
491
|
+
return
|
|
492
|
+
}
|
|
450
493
|
|
|
451
494
|
this._captureScreen()
|
|
452
495
|
}
|
package/src/session-recorder.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
|
|
2
2
|
import { SessionType } from '@multiplayer-app/session-recorder-common'
|
|
3
|
+
import { Observable } from 'lib0/observable'
|
|
3
4
|
|
|
4
5
|
import { TracerReactNativeSDK } from './otel'
|
|
5
6
|
import { RecorderReactNativeSDK } from './recorder'
|
|
7
|
+
import { logger } from './utils'
|
|
6
8
|
|
|
7
9
|
import {
|
|
8
10
|
ISession,
|
|
@@ -22,8 +24,11 @@ import { ApiService, StartSessionRequest, StopSessionRequest } from './services/
|
|
|
22
24
|
|
|
23
25
|
// Utility functions for React Native
|
|
24
26
|
|
|
27
|
+
type SessionRecorderEvents =
|
|
28
|
+
| 'state-change'
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
|
|
31
|
+
class SessionRecorder extends Observable<SessionRecorderEvents> implements ISessionRecorder, EventRecorder {
|
|
27
32
|
private _isInitialized = false
|
|
28
33
|
private _configs: SessionRecorderConfigs | null = null
|
|
29
34
|
private _apiService = new ApiService()
|
|
@@ -63,6 +68,7 @@ class SessionRecorder implements ISessionRecorder, EventRecorder {
|
|
|
63
68
|
}
|
|
64
69
|
set sessionState(state: SessionState | null) {
|
|
65
70
|
this._sessionState = state
|
|
71
|
+
this.emit('state-change', [state || SessionState.stopped])
|
|
66
72
|
if (state) {
|
|
67
73
|
this._storageService.saveSessionState(state)
|
|
68
74
|
}
|
|
@@ -110,6 +116,7 @@ class SessionRecorder implements ISessionRecorder, EventRecorder {
|
|
|
110
116
|
* Initialize debugger with default or custom configurations
|
|
111
117
|
*/
|
|
112
118
|
constructor() {
|
|
119
|
+
super()
|
|
113
120
|
// Initialize with stored session data if available
|
|
114
121
|
StorageService.initialize()
|
|
115
122
|
}
|
|
@@ -130,7 +137,7 @@ class SessionRecorder implements ISessionRecorder, EventRecorder {
|
|
|
130
137
|
this.sessionType = SessionType.PLAIN
|
|
131
138
|
}
|
|
132
139
|
} catch (error) {
|
|
133
|
-
|
|
140
|
+
logger.error('SessionRecorder', 'Failed to load stored session data', error)
|
|
134
141
|
this.session = null
|
|
135
142
|
this.sessionId = null
|
|
136
143
|
this.sessionState = null
|
|
@@ -156,10 +163,10 @@ class SessionRecorder implements ISessionRecorder, EventRecorder {
|
|
|
156
163
|
this._tracer.init(this._configs)
|
|
157
164
|
|
|
158
165
|
} catch (error) {
|
|
159
|
-
|
|
166
|
+
logger.error('SessionRecorder', 'Failed to initialize API service', error)
|
|
160
167
|
}
|
|
161
168
|
if (this._configs.apiKey) {
|
|
162
|
-
this._recorder.init(this._configs
|
|
169
|
+
this._recorder.init(this._configs)
|
|
163
170
|
}
|
|
164
171
|
|
|
165
172
|
if (this.sessionId && (this.sessionState === SessionState.started || this.sessionState === SessionState.paused)) {
|
|
@@ -415,7 +422,7 @@ class SessionRecorder implements ISessionRecorder, EventRecorder {
|
|
|
415
422
|
private _setupSessionAndStart(session: ISession, configureExporters: boolean = true): void {
|
|
416
423
|
if (configureExporters && session.tempApiKey) {
|
|
417
424
|
this._configs!.apiKey = session.tempApiKey
|
|
418
|
-
this._recorder.init(this._configs
|
|
425
|
+
this._recorder.init(this._configs!)
|
|
419
426
|
this._tracer.init(this._configs!)
|
|
420
427
|
this._apiService.updateConfigs({ apiKey: this._configs!.apiKey })
|
|
421
428
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -1,3 +1,46 @@
|
|
|
1
1
|
export * from './session-recorder'
|
|
2
2
|
export * from './session'
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
// Re-export rrweb types for convenience
|
|
5
|
+
export { EventType, IncrementalSource, MouseInteractions as MouseInteractionType } from 'rrweb'
|
|
6
|
+
export type { eventWithTime, serializedNodeWithId, mouseInteractionData, metaEvent } from '@rrweb/types'
|
|
7
|
+
|
|
8
|
+
// Import types for use in this file
|
|
9
|
+
import type { MouseInteractions } from 'rrweb'
|
|
10
|
+
import type { eventWithTime, mouseInteractionData, metaEvent } from '@rrweb/types'
|
|
11
|
+
|
|
12
|
+
// React Native specific types
|
|
13
|
+
export interface TouchInteractionData {
|
|
14
|
+
type: MouseInteractions
|
|
15
|
+
id: number
|
|
16
|
+
x: number
|
|
17
|
+
y: number
|
|
18
|
+
pressure?: number
|
|
19
|
+
target?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ReactNativeScreenData {
|
|
23
|
+
width: number
|
|
24
|
+
height: number
|
|
25
|
+
base64Image: string
|
|
26
|
+
timestamp: number
|
|
27
|
+
screenName?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ReactNativeTouchData {
|
|
31
|
+
pageX: number
|
|
32
|
+
pageY: number
|
|
33
|
+
target?: string
|
|
34
|
+
pressure?: number
|
|
35
|
+
timestamp: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Type aliases for convenience
|
|
39
|
+
export type RRWebEvent = eventWithTime
|
|
40
|
+
export type MouseInteractionData = mouseInteractionData
|
|
41
|
+
export type MetaEvent = metaEvent
|
|
42
|
+
|
|
43
|
+
// Event recording interface
|
|
44
|
+
export interface EventRecorder {
|
|
45
|
+
recordEvent(event: RRWebEvent): void
|
|
46
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized logger utility for the session recorder
|
|
3
|
+
* Provides consistent logging across all components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export enum LogLevel {
|
|
7
|
+
DEBUG = 0,
|
|
8
|
+
INFO = 1,
|
|
9
|
+
WARN = 2,
|
|
10
|
+
ERROR = 3
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface LoggerConfig {
|
|
14
|
+
level: LogLevel
|
|
15
|
+
enableConsole: boolean
|
|
16
|
+
enablePrefix: boolean
|
|
17
|
+
prefix: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class Logger {
|
|
21
|
+
private config: LoggerConfig = {
|
|
22
|
+
level: LogLevel.INFO,
|
|
23
|
+
enableConsole: true,
|
|
24
|
+
enablePrefix: true,
|
|
25
|
+
prefix: '[SessionRecorder]',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private componentPrefixes: Map<string, string> = new Map([
|
|
29
|
+
['ScreenRecorder', '📸'],
|
|
30
|
+
['GestureRecorder', '👆'],
|
|
31
|
+
['GestureCaptureWrapper', '📸'],
|
|
32
|
+
['SessionRecorderContext', '🎯'],
|
|
33
|
+
['EventExporter', '📤'],
|
|
34
|
+
['NavigationTracker', '📸'],
|
|
35
|
+
['RecorderReactNativeSDK', '📤'],
|
|
36
|
+
['DEBUGGER_LIB', '🔍'],
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Configure the logger
|
|
41
|
+
* @param config - Logger configuration
|
|
42
|
+
*/
|
|
43
|
+
configure(config: Partial<LoggerConfig>): void {
|
|
44
|
+
this.config = { ...this.config, ...config }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Set the log level
|
|
49
|
+
* @param level - Log level to set
|
|
50
|
+
*/
|
|
51
|
+
setLevel(level: LogLevel): void {
|
|
52
|
+
this.config.level = level
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Enable or disable console output
|
|
57
|
+
* @param enabled - Whether to enable console output
|
|
58
|
+
*/
|
|
59
|
+
setConsoleEnabled(enabled: boolean): void {
|
|
60
|
+
this.config.enableConsole = enabled
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Add or update a component prefix
|
|
65
|
+
* @param component - Component name
|
|
66
|
+
* @param emoji - Emoji prefix for the component
|
|
67
|
+
*/
|
|
68
|
+
setComponentPrefix(component: string, emoji: string): void {
|
|
69
|
+
this.componentPrefixes.set(component, emoji)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get the formatted prefix for a component
|
|
74
|
+
* @param component - Component name
|
|
75
|
+
* @returns Formatted prefix string
|
|
76
|
+
*/
|
|
77
|
+
private getPrefix(component: string): string {
|
|
78
|
+
if (!this.config.enablePrefix) return ''
|
|
79
|
+
|
|
80
|
+
const emoji = this.componentPrefixes.get(component) || '📝'
|
|
81
|
+
return `${this.config.prefix} ${emoji} [${component}]`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if a log level should be output
|
|
86
|
+
* @param level - Log level to check
|
|
87
|
+
* @returns True if should output
|
|
88
|
+
*/
|
|
89
|
+
private shouldLog(level: LogLevel): boolean {
|
|
90
|
+
return level >= this.config.level && this.config.enableConsole
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Format the log message
|
|
95
|
+
* @param component - Component name
|
|
96
|
+
* @param level - Log level
|
|
97
|
+
* @param message - Log message
|
|
98
|
+
* @param data - Additional data to log
|
|
99
|
+
* @returns Formatted log message
|
|
100
|
+
*/
|
|
101
|
+
private formatMessage(component: string, level: LogLevel, message: string, data?: any): string {
|
|
102
|
+
const prefix = this.getPrefix(component)
|
|
103
|
+
const timestamp = new Date().toISOString()
|
|
104
|
+
const levelName = LogLevel[level]
|
|
105
|
+
|
|
106
|
+
let formattedMessage = `${prefix} ${levelName} ${message}`
|
|
107
|
+
|
|
108
|
+
if (data !== undefined) {
|
|
109
|
+
formattedMessage += ` ${JSON.stringify(data)}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return formattedMessage
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Log a debug message
|
|
117
|
+
* @param component - Component name
|
|
118
|
+
* @param message - Log message
|
|
119
|
+
* @param data - Additional data to log
|
|
120
|
+
*/
|
|
121
|
+
debug(component: string, message: string, data?: any): void {
|
|
122
|
+
if (!this.shouldLog(LogLevel.DEBUG)) return
|
|
123
|
+
|
|
124
|
+
const formattedMessage = this.formatMessage(component, LogLevel.DEBUG, message, data)
|
|
125
|
+
// eslint-disable-next-line no-console
|
|
126
|
+
console.log(formattedMessage)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Log an info message
|
|
131
|
+
* @param component - Component name
|
|
132
|
+
* @param message - Log message
|
|
133
|
+
* @param data - Additional data to log
|
|
134
|
+
*/
|
|
135
|
+
info(component: string, message: string, data?: any): void {
|
|
136
|
+
if (!this.shouldLog(LogLevel.INFO)) return
|
|
137
|
+
|
|
138
|
+
const formattedMessage = this.formatMessage(component, LogLevel.INFO, message, data)
|
|
139
|
+
// eslint-disable-next-line no-console
|
|
140
|
+
console.log(formattedMessage)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Log a warning message
|
|
145
|
+
* @param component - Component name
|
|
146
|
+
* @param message - Log message
|
|
147
|
+
* @param data - Additional data to log
|
|
148
|
+
*/
|
|
149
|
+
warn(component: string, message: string, data?: any): void {
|
|
150
|
+
if (!this.shouldLog(LogLevel.WARN)) return
|
|
151
|
+
|
|
152
|
+
const formattedMessage = this.formatMessage(component, LogLevel.WARN, message, data)
|
|
153
|
+
// eslint-disable-next-line no-console
|
|
154
|
+
console.warn(formattedMessage)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Log an error message
|
|
159
|
+
* @param component - Component name
|
|
160
|
+
* @param message - Log message
|
|
161
|
+
* @param data - Additional data to log
|
|
162
|
+
*/
|
|
163
|
+
error(component: string, message: string, data?: any): void {
|
|
164
|
+
if (!this.shouldLog(LogLevel.ERROR)) return
|
|
165
|
+
|
|
166
|
+
const formattedMessage = this.formatMessage(component, LogLevel.ERROR, message, data)
|
|
167
|
+
// eslint-disable-next-line no-console
|
|
168
|
+
console.error(formattedMessage)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Log a success message (info level with success emoji)
|
|
173
|
+
* @param component - Component name
|
|
174
|
+
* @param message - Log message
|
|
175
|
+
* @param data - Additional data to log
|
|
176
|
+
*/
|
|
177
|
+
success(component: string, message: string, data?: any): void {
|
|
178
|
+
if (!this.shouldLog(LogLevel.INFO)) return
|
|
179
|
+
|
|
180
|
+
const prefix = this.getPrefix(component)
|
|
181
|
+
const timestamp = new Date().toISOString()
|
|
182
|
+
const formattedMessage = `${prefix} ✅ ${message}`
|
|
183
|
+
|
|
184
|
+
let fullMessage = formattedMessage
|
|
185
|
+
if (data !== undefined) {
|
|
186
|
+
fullMessage += ` ${JSON.stringify(data)}`
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// eslint-disable-next-line no-console
|
|
190
|
+
console.log(fullMessage)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Log a failure message (error level with failure emoji)
|
|
195
|
+
* @param component - Component name
|
|
196
|
+
* @param message - Log message
|
|
197
|
+
* @param data - Additional data to log
|
|
198
|
+
*/
|
|
199
|
+
failure(component: string, message: string, data?: any): void {
|
|
200
|
+
if (!this.shouldLog(LogLevel.ERROR)) return
|
|
201
|
+
|
|
202
|
+
const prefix = this.getPrefix(component)
|
|
203
|
+
const timestamp = new Date().toISOString()
|
|
204
|
+
const formattedMessage = `${prefix} ❌ ${message}`
|
|
205
|
+
|
|
206
|
+
let fullMessage = formattedMessage
|
|
207
|
+
if (data !== undefined) {
|
|
208
|
+
fullMessage += ` ${JSON.stringify(data)}`
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// eslint-disable-next-line no-console
|
|
212
|
+
console.error(fullMessage)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Export a singleton instance
|
|
217
|
+
export const logger = new Logger()
|
|
218
|
+
|
|
219
|
+
// Export convenience functions for common use cases
|
|
220
|
+
export const logDebug = (component: string, message: string, data?: any) => logger.debug(component, message, data)
|
|
221
|
+
export const logInfo = (component: string, message: string, data?: any) => logger.info(component, message, data)
|
|
222
|
+
export const logWarn = (component: string, message: string, data?: any) => logger.warn(component, message, data)
|
|
223
|
+
export const logError = (component: string, message: string, data?: any) => logger.error(component, message, data)
|
|
224
|
+
export const logSuccess = (component: string, message: string, data?: any) => logger.success(component, message, data)
|
|
225
|
+
export const logFailure = (component: string, message: string, data?: any) => logger.failure(component, message, data)
|