@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.
Files changed (69) hide show
  1. package/dist/components/GestureCaptureWrapper.d.ts +6 -0
  2. package/dist/components/GestureCaptureWrapper.js +1 -0
  3. package/dist/components/GestureCaptureWrapper.js.map +1 -0
  4. package/dist/context/SessionRecorderContext.d.ts +1 -2
  5. package/dist/context/SessionRecorderContext.js +1 -1
  6. package/dist/context/SessionRecorderContext.js.map +1 -1
  7. package/dist/otel/helpers.js +1 -1
  8. package/dist/otel/helpers.js.map +1 -1
  9. package/dist/otel/instrumentations/index.js +1 -1
  10. package/dist/otel/instrumentations/index.js.map +1 -1
  11. package/dist/otel/instrumentations/reactNativeInstrumentation.js +1 -1
  12. package/dist/otel/instrumentations/reactNativeInstrumentation.js.map +1 -1
  13. package/dist/patch/xhr.js +1 -1
  14. package/dist/patch/xhr.js.map +1 -1
  15. package/dist/recorder/eventExporter.js +1 -1
  16. package/dist/recorder/eventExporter.js.map +1 -1
  17. package/dist/recorder/gestureHandlerRecorder.d.ts +19 -0
  18. package/dist/recorder/gestureHandlerRecorder.js +1 -0
  19. package/dist/recorder/gestureHandlerRecorder.js.map +1 -0
  20. package/dist/recorder/gestureRecorder.d.ts +13 -1
  21. package/dist/recorder/gestureRecorder.js +1 -1
  22. package/dist/recorder/gestureRecorder.js.map +1 -1
  23. package/dist/recorder/index.d.ts +1 -2
  24. package/dist/recorder/index.js +1 -1
  25. package/dist/recorder/index.js.map +1 -1
  26. package/dist/recorder/navigationTracker.js +1 -1
  27. package/dist/recorder/navigationTracker.js.map +1 -1
  28. package/dist/recorder/screenRecorder.d.ts +31 -6
  29. package/dist/recorder/screenRecorder.js +1 -1
  30. package/dist/recorder/screenRecorder.js.map +1 -1
  31. package/dist/session-recorder.d.ts +3 -1
  32. package/dist/session-recorder.js +1 -1
  33. package/dist/session-recorder.js.map +1 -1
  34. package/dist/types/index.d.ts +32 -1
  35. package/dist/types/index.js +1 -1
  36. package/dist/types/index.js.map +1 -1
  37. package/dist/types/rrweb.d.ts +10 -0
  38. package/dist/utils/index.d.ts +2 -0
  39. package/dist/utils/index.js +1 -1
  40. package/dist/utils/index.js.map +1 -1
  41. package/dist/utils/logger.d.ts +112 -0
  42. package/dist/utils/logger.js +1 -0
  43. package/dist/utils/logger.js.map +1 -0
  44. package/dist/utils/rrweb-events.d.ts +65 -0
  45. package/dist/utils/rrweb-events.js +1 -0
  46. package/dist/utils/rrweb-events.js.map +1 -0
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/package.json +3 -1
  50. package/src/components/GestureCaptureWrapper.tsx +110 -0
  51. package/src/context/SessionRecorderContext.tsx +76 -81
  52. package/src/otel/helpers.ts +2 -1
  53. package/src/otel/instrumentations/index.ts +5 -4
  54. package/src/otel/instrumentations/reactNativeInstrumentation.ts +2 -1
  55. package/src/patch/xhr.ts +2 -2
  56. package/src/recorder/eventExporter.ts +4 -1
  57. package/src/recorder/gestureHandlerRecorder.ts +157 -0
  58. package/src/recorder/gestureRecorder.ts +93 -19
  59. package/src/recorder/index.ts +16 -18
  60. package/src/recorder/navigationTracker.ts +2 -0
  61. package/src/recorder/screenRecorder.ts +125 -82
  62. package/src/session-recorder.ts +12 -5
  63. package/src/types/index.ts +44 -1
  64. package/src/utils/index.ts +2 -0
  65. package/src/utils/logger.ts +225 -0
  66. package/src/utils/rrweb-events.ts +311 -0
  67. package/src/version.ts +1 -1
  68. package/example-usage.tsx +0 -174
  69. package/src/types/rrweb.ts +0 -122
@@ -1,7 +1,15 @@
1
- import { ScreenEvent, RecorderConfig, EventType, FullSnapshotEvent, SerializedNodeWithId, EventRecorder } from '../types'
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
- this._createAndEmitFullSnapshotEvent(base64Image)
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
- // console.warn('ViewShot ref not available for screen capture')
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
- // console.error('Failed to capture screen:', error)
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
- const { width, height } = this.screenDimensions
160
+ // Use the new createFullSnapshot method
161
+ const fullSnapshotEvent = this.createFullSnapshot(base64Image)
162
+ this.recordEvent(fullSnapshotEvent)
163
+ }
135
164
 
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
- },
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
- // 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
- }
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
- 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(),
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.recordEvent(fullSnapshotEvent)
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
- // 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)
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
- async captureSpecificElement(elementRef: any, options?: {
282
- format?: 'png' | 'jpg' | 'webp'
283
- quality?: number
284
- }): Promise<string | null> {
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); this._sendEvent(event); this._recordOpenTelemetrySpan(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); this._sendEvent(event); this._recordOpenTelemetrySpan(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) return
490
+ if (!this.isRecording) {
491
+ return
492
+ }
450
493
 
451
494
  this._captureScreen()
452
495
  }
@@ -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
- class SessionRecorder implements ISessionRecorder, EventRecorder {
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
- console.error('Failed to load stored session data:', error)
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
- console.error('Failed to initialize API service:', error)
166
+ logger.error('SessionRecorder', 'Failed to initialize API service', error)
160
167
  }
161
168
  if (this._configs.apiKey) {
162
- this._recorder.init(this._configs, this)
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!, this)
425
+ this._recorder.init(this._configs!)
419
426
  this._tracer.init(this._configs!)
420
427
  this._apiService.updateConfigs({ apiKey: this._configs!.apiKey })
421
428
  }
@@ -1,3 +1,46 @@
1
1
  export * from './session-recorder'
2
2
  export * from './session'
3
- export * from './rrweb'
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
+ }
@@ -4,3 +4,5 @@ export * from './session'
4
4
  export * from './time'
5
5
  export * from './type-utils'
6
6
  export * from './request-utils'
7
+ export * from './rrweb-events'
8
+ export * from './logger'
@@ -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)