@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.
- package/RRWEB_INTEGRATION.md +336 -0
- package/VIEWSHOT_INTEGRATION_TEST.md +123 -0
- package/copy-react-native-dist.sh +38 -0
- 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/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.d.ts +1 -2
- 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/reactNativeInstrumentation.js +1 -1
- package/dist/otel/instrumentations/reactNativeInstrumentation.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/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 +69 -3
- package/dist/recorder/gestureRecorder.js +1 -1
- package/dist/recorder/gestureRecorder.js.map +1 -1
- package/dist/recorder/index.d.ts +59 -6
- 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 +83 -4
- 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 +42 -2
- package/dist/session-recorder.js +1 -1
- package/dist/session-recorder.js.map +1 -1
- package/dist/types/index.d.ts +32 -0
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/rrweb.d.ts +118 -0
- package/dist/types/rrweb.js +1 -0
- package/dist/types/rrweb.js.map +1 -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/example-usage.tsx +174 -0
- package/package.json +5 -2
- package/src/components/GestureCaptureWrapper.tsx +110 -0
- package/src/config/constants.ts +3 -3
- package/src/context/SessionRecorderContext.tsx +106 -34
- package/src/index.ts +1 -0
- package/src/otel/helpers.ts +38 -20
- package/src/otel/index.ts +7 -3
- package/src/otel/instrumentations/index.ts +82 -40
- package/src/otel/instrumentations/reactNativeInstrumentation.ts +2 -1
- package/src/otel/instrumentations/reactNavigationInstrumentation.ts +5 -0
- package/src/recorder/eventExporter.ts +141 -0
- package/src/recorder/gestureHandlerRecorder.ts +157 -0
- package/src/recorder/gestureRecorder.ts +198 -3
- package/src/recorder/index.ts +130 -24
- package/src/recorder/navigationTracker.ts +2 -0
- package/src/recorder/screenRecorder.ts +261 -22
- package/src/services/api.service.ts +1 -8
- package/src/services/storage.service.ts +1 -0
- package/src/session-recorder.ts +97 -11
- package/src/types/index.ts +45 -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
|
@@ -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.
|
|
1
|
+
export const version = "0.0.1-alpha.8"
|