@multiplayer-app/session-recorder-react-native 0.0.1-alpha.6 → 0.0.1-alpha.8

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 (90) hide show
  1. package/RRWEB_INTEGRATION.md +336 -0
  2. package/VIEWSHOT_INTEGRATION_TEST.md +123 -0
  3. package/copy-react-native-dist.sh +38 -0
  4. package/dist/components/GestureCaptureWrapper.d.ts +6 -0
  5. package/dist/components/GestureCaptureWrapper.js +1 -0
  6. package/dist/components/GestureCaptureWrapper.js.map +1 -0
  7. package/dist/config/constants.d.ts +0 -1
  8. package/dist/config/constants.js +1 -1
  9. package/dist/config/constants.js.map +1 -1
  10. package/dist/context/SessionRecorderContext.d.ts +1 -2
  11. package/dist/context/SessionRecorderContext.js +1 -1
  12. package/dist/context/SessionRecorderContext.js.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/otel/helpers.d.ts +4 -4
  15. package/dist/otel/helpers.js +1 -1
  16. package/dist/otel/helpers.js.map +1 -1
  17. package/dist/otel/index.js +1 -1
  18. package/dist/otel/index.js.map +1 -1
  19. package/dist/otel/instrumentations/index.d.ts +2 -3
  20. package/dist/otel/instrumentations/index.js +1 -1
  21. package/dist/otel/instrumentations/index.js.map +1 -1
  22. package/dist/otel/instrumentations/reactNativeInstrumentation.js +1 -1
  23. package/dist/otel/instrumentations/reactNativeInstrumentation.js.map +1 -1
  24. package/dist/otel/instrumentations/reactNavigationInstrumentation.d.ts +1 -0
  25. package/dist/otel/instrumentations/reactNavigationInstrumentation.js +1 -1
  26. package/dist/otel/instrumentations/reactNavigationInstrumentation.js.map +1 -1
  27. package/dist/recorder/eventExporter.d.ts +21 -0
  28. package/dist/recorder/eventExporter.js +1 -0
  29. package/dist/recorder/eventExporter.js.map +1 -0
  30. package/dist/recorder/gestureHandlerRecorder.d.ts +19 -0
  31. package/dist/recorder/gestureHandlerRecorder.js +1 -0
  32. package/dist/recorder/gestureHandlerRecorder.js.map +1 -0
  33. package/dist/recorder/gestureRecorder.d.ts +69 -3
  34. package/dist/recorder/gestureRecorder.js +1 -1
  35. package/dist/recorder/gestureRecorder.js.map +1 -1
  36. package/dist/recorder/index.d.ts +59 -6
  37. package/dist/recorder/index.js +1 -1
  38. package/dist/recorder/index.js.map +1 -1
  39. package/dist/recorder/navigationTracker.js +1 -1
  40. package/dist/recorder/navigationTracker.js.map +1 -1
  41. package/dist/recorder/screenRecorder.d.ts +83 -4
  42. package/dist/recorder/screenRecorder.js +1 -1
  43. package/dist/recorder/screenRecorder.js.map +1 -1
  44. package/dist/services/api.service.js.map +1 -1
  45. package/dist/services/storage.service.js.map +1 -1
  46. package/dist/session-recorder.d.ts +42 -2
  47. package/dist/session-recorder.js +1 -1
  48. package/dist/session-recorder.js.map +1 -1
  49. package/dist/types/index.d.ts +32 -0
  50. package/dist/types/index.js +1 -1
  51. package/dist/types/index.js.map +1 -1
  52. package/dist/types/rrweb.d.ts +118 -0
  53. package/dist/types/rrweb.js +1 -0
  54. package/dist/types/rrweb.js.map +1 -0
  55. package/dist/utils/index.d.ts +2 -0
  56. package/dist/utils/index.js +1 -1
  57. package/dist/utils/index.js.map +1 -1
  58. package/dist/utils/logger.d.ts +112 -0
  59. package/dist/utils/logger.js +1 -0
  60. package/dist/utils/logger.js.map +1 -0
  61. package/dist/utils/rrweb-events.d.ts +65 -0
  62. package/dist/utils/rrweb-events.js +1 -0
  63. package/dist/utils/rrweb-events.js.map +1 -0
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/example-usage.tsx +174 -0
  67. package/package.json +5 -2
  68. package/src/components/GestureCaptureWrapper.tsx +110 -0
  69. package/src/config/constants.ts +3 -3
  70. package/src/context/SessionRecorderContext.tsx +106 -34
  71. package/src/index.ts +1 -0
  72. package/src/otel/helpers.ts +38 -20
  73. package/src/otel/index.ts +7 -3
  74. package/src/otel/instrumentations/index.ts +82 -40
  75. package/src/otel/instrumentations/reactNativeInstrumentation.ts +2 -1
  76. package/src/otel/instrumentations/reactNavigationInstrumentation.ts +5 -0
  77. package/src/recorder/eventExporter.ts +141 -0
  78. package/src/recorder/gestureHandlerRecorder.ts +157 -0
  79. package/src/recorder/gestureRecorder.ts +198 -3
  80. package/src/recorder/index.ts +130 -24
  81. package/src/recorder/navigationTracker.ts +2 -0
  82. package/src/recorder/screenRecorder.ts +261 -22
  83. package/src/services/api.service.ts +1 -8
  84. package/src/services/storage.service.ts +1 -0
  85. package/src/session-recorder.ts +97 -11
  86. package/src/types/index.ts +45 -1
  87. package/src/utils/index.ts +2 -0
  88. package/src/utils/logger.ts +225 -0
  89. package/src/utils/rrweb-events.ts +311 -0
  90. package/src/version.ts +1 -1
@@ -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)
@@ -0,0 +1,311 @@
1
+ import { Dimensions } from 'react-native'
2
+ import { EventType, eventWithTime, NodeType, serializedNodeWithId, IncrementalSource, mutationData } from '@rrweb/types'
3
+
4
+ /**
5
+ * Creates a meta event to mark the start of recording
6
+ * @param sessionId - The session ID
7
+ * @param sessionType - The type of session (PLAIN or CONTINUOUS)
8
+ * @param additionalData - Additional data to include in the meta event
9
+ * @returns MetaEvent object
10
+ */
11
+ export function createRecordingMetaEvent(): eventWithTime {
12
+ const screenDimensions = Dimensions.get('window')
13
+
14
+ return {
15
+ type: EventType.Meta,
16
+ data: {
17
+ href: 'https://go.multiplayer.app/session-recorder-react-native',
18
+ width: screenDimensions.width,
19
+ height: screenDimensions.height,
20
+ },
21
+ timestamp: Date.now(),
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Create a full snapshot event with the given base64 image
27
+ * @param base64Image - Base64 encoded image data
28
+ * @param width - Screen width
29
+ * @param height - Screen height
30
+ * @param captureFormat - Image format (png, jpg, etc.)
31
+ * @param nodeIdCounter - Starting node ID counter (will be modified)
32
+ * @returns Full snapshot event
33
+ */
34
+ export function createFullSnapshotEvent(
35
+ base64Image: string,
36
+ width: number,
37
+ height: number,
38
+ captureFormat: string = 'jpg',
39
+ nodeIdCounter: { current: number },
40
+ ): eventWithTime {
41
+ // Create a virtual DOM node representing the screen as an image
42
+ const imageNodeId = nodeIdCounter.current++
43
+ const imageNode: serializedNodeWithId = {
44
+ type: NodeType.Element,
45
+ id: imageNodeId,
46
+ tagName: 'img',
47
+ attributes: {
48
+ src: `data:image/${captureFormat};base64,${base64Image}`,
49
+ width: width.toString(),
50
+ height: height.toString(),
51
+ style: `width: ${width}px; height: ${height}px;`,
52
+ },
53
+ childNodes: [],
54
+ }
55
+
56
+ // Create the root container
57
+ const rootNode: serializedNodeWithId = {
58
+ type: NodeType.Element,
59
+ id: nodeIdCounter.current++,
60
+ tagName: 'div',
61
+ attributes: {
62
+ style: `width: ${width}px; height: ${height}px; position: relative;`,
63
+ },
64
+ childNodes: [imageNode],
65
+ }
66
+
67
+ const domNode: serializedNodeWithId = {
68
+ type: NodeType.Document,
69
+ childNodes: [
70
+ {
71
+ type: NodeType.DocumentType,
72
+ name: 'html',
73
+ publicId: '',
74
+ systemId: '',
75
+ id: nodeIdCounter.current++,
76
+ },
77
+ {
78
+ type: NodeType.Element,
79
+ tagName: 'html',
80
+ attributes: {},
81
+ childNodes: [
82
+ {
83
+ type: NodeType.Element,
84
+ tagName: 'head',
85
+ attributes: {},
86
+ childNodes: [
87
+ {
88
+ type: NodeType.Element,
89
+ tagName: 'meta',
90
+ attributes: { charset: 'utf-8' },
91
+ childNodes: [],
92
+ id: nodeIdCounter.current++,
93
+ },
94
+ {
95
+ type: NodeType.Element,
96
+ tagName: 'meta',
97
+ attributes: {
98
+ name: 'viewport',
99
+ content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no',
100
+ },
101
+ childNodes: [],
102
+ id: nodeIdCounter.current++,
103
+ },
104
+ ],
105
+ id: nodeIdCounter.current++,
106
+ },
107
+ {
108
+ type: NodeType.Element,
109
+ tagName: 'body',
110
+ attributes: {},
111
+ childNodes: [rootNode],
112
+ id: nodeIdCounter.current++,
113
+ },
114
+ ],
115
+ id: nodeIdCounter.current++,
116
+ },
117
+ ],
118
+ id: nodeIdCounter.current++,
119
+ }
120
+
121
+ return {
122
+ type: EventType.FullSnapshot,
123
+ data: {
124
+ node: domNode,
125
+ initialOffset: { left: 0, top: 0 },
126
+ },
127
+ timestamp: Date.now(),
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Create an incremental snapshot event with mutation data to update image src
133
+ * @param base64Image - New base64 encoded image data
134
+ * @param imageNodeId - ID of the image node to update
135
+ * @param captureFormat - Image format (png, jpg, etc.)
136
+ * @returns Incremental snapshot event with mutation data
137
+ */
138
+ export function createIncrementalSnapshotWithImageUpdate(
139
+ base64Image: string,
140
+ imageNodeId: number,
141
+ captureFormat: string = 'jpg',
142
+ ): eventWithTime {
143
+ const mutationData: mutationData = {
144
+ source: IncrementalSource.Mutation,
145
+ texts: [],
146
+ attributes: [
147
+ {
148
+ id: imageNodeId,
149
+ attributes: {
150
+ src: `data:image/${captureFormat};base64,${base64Image}`,
151
+ },
152
+ },
153
+ ],
154
+ removes: [],
155
+ adds: [],
156
+ }
157
+
158
+ return {
159
+ type: EventType.IncrementalSnapshot,
160
+ data: mutationData,
161
+ timestamp: Date.now(),
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Create a simple image node for React Native screen capture
167
+ * @param base64Image - Base64 encoded image data
168
+ * @param width - Image width
169
+ * @param height - Image height
170
+ * @param captureFormat - Image format (png, jpg, etc.)
171
+ * @param nodeId - Node ID for the image
172
+ * @returns Serialized node with ID
173
+ */
174
+ export function createImageNode(
175
+ base64Image: string,
176
+ width: number,
177
+ height: number,
178
+ captureFormat: string = 'jpg',
179
+ nodeId: number,
180
+ ): serializedNodeWithId {
181
+ return {
182
+ type: NodeType.Element,
183
+ id: nodeId,
184
+ tagName: 'img',
185
+ attributes: {
186
+ src: `data:image/${captureFormat};base64,${base64Image}`,
187
+ width: width.toString(),
188
+ height: height.toString(),
189
+ style: `width: ${width}px; height: ${height}px;`,
190
+ },
191
+ childNodes: [],
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Create a document node for React Native screen capture
197
+ * @param imageNode - The image node to include
198
+ * @param width - Screen width
199
+ * @param height - Screen height
200
+ * @param nodeIdCounter - Node ID counter (will be modified)
201
+ * @returns Document node
202
+ */
203
+ export function createDocumentNode(
204
+ imageNode: serializedNodeWithId,
205
+ width: number,
206
+ height: number,
207
+ nodeIdCounter: { current: number },
208
+ ): serializedNodeWithId {
209
+ // Create the root container
210
+ const rootNode: serializedNodeWithId = {
211
+ type: NodeType.Element,
212
+ id: nodeIdCounter.current++,
213
+ tagName: 'div',
214
+ attributes: {
215
+ style: `width: ${width}px; height: ${height}px; position: relative;`,
216
+ },
217
+ childNodes: [imageNode],
218
+ }
219
+
220
+ return {
221
+ type: NodeType.Document,
222
+ childNodes: [
223
+ {
224
+ type: NodeType.DocumentType,
225
+ name: 'html',
226
+ publicId: '',
227
+ systemId: '',
228
+ id: nodeIdCounter.current++,
229
+ },
230
+ {
231
+ type: NodeType.Element,
232
+ tagName: 'html',
233
+ attributes: {},
234
+ childNodes: [
235
+ {
236
+ type: NodeType.Element,
237
+ tagName: 'head',
238
+ attributes: {},
239
+ childNodes: [
240
+ {
241
+ type: NodeType.Element,
242
+ tagName: 'meta',
243
+ attributes: { charset: 'utf-8' },
244
+ childNodes: [],
245
+ id: nodeIdCounter.current++,
246
+ },
247
+ {
248
+ type: NodeType.Element,
249
+ tagName: 'meta',
250
+ attributes: {
251
+ name: 'viewport',
252
+ content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no',
253
+ },
254
+ childNodes: [],
255
+ id: nodeIdCounter.current++,
256
+ },
257
+ ],
258
+ id: nodeIdCounter.current++,
259
+ },
260
+ {
261
+ type: NodeType.Element,
262
+ tagName: 'body',
263
+ attributes: {},
264
+ childNodes: [rootNode],
265
+ id: nodeIdCounter.current++,
266
+ },
267
+ ],
268
+ id: nodeIdCounter.current++,
269
+ },
270
+ ],
271
+ id: nodeIdCounter.current++,
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Generate a simple hash for screen comparison
277
+ * This is a lightweight hash that focuses on the beginning and end of the base64 string
278
+ * to detect changes without doing a full comparison
279
+ * @param base64Image - Base64 encoded image
280
+ * @param sampleSize - Number of characters to sample from each part
281
+ * @returns Hash string for comparison
282
+ */
283
+ export function generateScreenHash(base64Image: string, sampleSize: number = 100): string {
284
+ // Use a simple hash that samples the beginning, middle, and end of the base64 string
285
+ // This is much faster than comparing the entire string
286
+ const start = base64Image.substring(0, sampleSize)
287
+ const middle = base64Image.substring(
288
+ Math.floor(base64Image.length / 2) - sampleSize / 2,
289
+ Math.floor(base64Image.length / 2) + sampleSize / 2,
290
+ )
291
+ const end = base64Image.substring(base64Image.length - sampleSize)
292
+
293
+ // Combine samples and create a simple hash
294
+ const combined = start + middle + end
295
+ return simpleHash(combined)
296
+ }
297
+
298
+ /**
299
+ * Simple hash function for string comparison
300
+ * @param str - String to hash
301
+ * @returns Hash value as string
302
+ */
303
+ export function simpleHash(str: string): string {
304
+ let hash = 0
305
+ for (let i = 0; i < str.length; i++) {
306
+ const char = str.charCodeAt(i)
307
+ hash = (hash << 5) - hash + char
308
+ hash = hash & hash // Convert to 32-bit integer
309
+ }
310
+ return Math.abs(hash).toString(36)
311
+ }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const version = "0.0.1-alpha.6"
1
+ export const version = "0.0.1-alpha.8"