@capillarytech/cap-ui-dev-tools 1.0.0 → 1.2.0
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/CAPVISION_USAGE.md +488 -0
- package/README.md +199 -48
- package/package.json +39 -10
- package/src/LibraryWatcherPlugin.js +53 -0
- package/src/capvision-recorder/adapters/WebdriverIOAdapter.js +312 -0
- package/src/capvision-recorder/assets/capvision-player.css +1 -0
- package/src/capvision-recorder/assets/capvision-player.min.js +31 -0
- package/src/capvision-recorder/assets/capvision-plugins/console-record.min.js +93 -0
- package/src/capvision-recorder/assets/capvision-plugins/console-replay.min.js +85 -0
- package/src/capvision-recorder/assets/capvision-plugins/network-record.min.js +542 -0
- package/src/capvision-recorder/assets/capvision-plugins/network-replay.min.js +434 -0
- package/src/capvision-recorder/assets/capvision.min.js +19 -0
- package/src/capvision-recorder/core/CapVisionRecorder.js +1338 -0
- package/src/capvision-recorder/core/ReportEnhancer.js +506 -0
- package/src/capvision-recorder/index.js +58 -0
- package/src/index.js +34 -0
|
@@ -0,0 +1,1338 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} CapVisionRecorderConfig
|
|
7
|
+
* @property {boolean} [ENABLE_FEATURE] - Master feature flag - if false, recording and replay are completely disabled (default: true)
|
|
8
|
+
* @property {string[]} [enabledClusters] - Cluster/Environment filtering - only record for these clusters (empty = all)
|
|
9
|
+
* @property {string[]} [enabledModules] - Module filtering - only record for these modules (empty = all)
|
|
10
|
+
* @property {string[]} [enabledTestTypes] - Test type filtering - only record for these test types (default: ['smoke', 'sanity', 'regression'])
|
|
11
|
+
* @property {string} [capVisionScriptPath] - Path to CapVision script file
|
|
12
|
+
* @property {string} [consoleRecordPluginPath] - Path to console record plugin
|
|
13
|
+
* @property {string} [consoleReplayPluginPath] - Path to console replay plugin
|
|
14
|
+
* @property {string} [recordingsOutputDir] - Directory to save recordings
|
|
15
|
+
* @property {string} [sessionStorageKey] - Session storage key for events
|
|
16
|
+
* @property {number} [saveIntervalMs] - Auto-save interval in milliseconds
|
|
17
|
+
* @property {number} [checkoutIntervalMs] - Checkout/snapshot interval in milliseconds
|
|
18
|
+
* @property {number} [pageStabilizationDelayMs] - Delay after page navigation in milliseconds
|
|
19
|
+
* @property {number} [reinitCooldownMs] - Cooldown between re-initialization attempts in milliseconds
|
|
20
|
+
* @property {boolean} [recordCanvas] - Enable canvas recording
|
|
21
|
+
* @property {boolean} [maskAllInputs] - Mask all input fields for privacy
|
|
22
|
+
* @property {boolean} [collectFonts] - Collect custom fonts
|
|
23
|
+
* @property {boolean} [recordCrossOriginIframes] - Record cross-origin iframes
|
|
24
|
+
* @property {boolean} [useTempFile] - Use temporary file storage instead of sessionStorage
|
|
25
|
+
* @property {boolean} [recordConsole] - Enable console recording
|
|
26
|
+
* @property {Object} [consoleRecordOptions] - Console recording options
|
|
27
|
+
* @property {string[]} [consoleRecordOptions.level] - Console levels to record
|
|
28
|
+
* @property {number} [consoleRecordOptions.lengthThreshold] - Maximum log length
|
|
29
|
+
* @property {Object} [consoleRecordOptions.stringifyOptions] - Stringification options
|
|
30
|
+
* @property {number} [consoleRecordOptions.stringifyOptions.stringLengthLimit] - String length limit
|
|
31
|
+
* @property {number} [consoleRecordOptions.stringifyOptions.numOfKeysLimit] - Number of keys limit
|
|
32
|
+
* @property {string} [networkRecordPluginPath] - Path to network record plugin
|
|
33
|
+
* @property {string} [networkReplayPluginPath] - Path to network replay plugin
|
|
34
|
+
* @property {boolean} [recordNetwork] - Enable network recording
|
|
35
|
+
* @property {Object} [networkRecordOptions] - Network recording options
|
|
36
|
+
* @property {boolean} [networkRecordOptions.recordBody] - Record request/response bodies
|
|
37
|
+
* @property {boolean} [networkRecordOptions.recordHeaders] - Record request/response headers
|
|
38
|
+
* @property {boolean} [networkRecordOptions.recordInitiator] - Record stack traces
|
|
39
|
+
* @property {boolean} [networkRecordOptions.recordPerformance] - Record performance metrics
|
|
40
|
+
* @property {number} [networkRecordOptions.maxBodyLength] - Max body length before truncation
|
|
41
|
+
* @property {Function} [networkRecordOptions.ignoreRequestFn] - Filter function for requests
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default CapVision Configuration
|
|
46
|
+
* @type {CapVisionRecorderConfig}
|
|
47
|
+
*/
|
|
48
|
+
const DEFAULT_CONFIG = {
|
|
49
|
+
ENABLE_FEATURE: true,
|
|
50
|
+
enabledClusters: [],
|
|
51
|
+
enabledModules: [],
|
|
52
|
+
enabledTestTypes: ['smoke', 'sanity', 'regression'],
|
|
53
|
+
|
|
54
|
+
capVisionScriptPath: path.join(__dirname, '../assets/capvision.min.js'),
|
|
55
|
+
consoleRecordPluginPath: path.join(__dirname, '../assets/capvision-plugins/console-record.min.js'),
|
|
56
|
+
consoleReplayPluginPath: path.join(__dirname, '../assets/capvision-plugins/console-replay.min.js'),
|
|
57
|
+
networkRecordPluginPath: path.join(__dirname, '../assets/capvision-plugins/network-record.min.js'),
|
|
58
|
+
networkReplayPluginPath: path.join(__dirname, '../assets/capvision-plugins/network-replay.min.js'),
|
|
59
|
+
recordingsOutputDir: path.join(process.cwd(), 'reports', 'recordings'),
|
|
60
|
+
|
|
61
|
+
sessionStorageKey: 'capvision_events',
|
|
62
|
+
saveIntervalMs: 60000,
|
|
63
|
+
checkoutIntervalMs: 30000,
|
|
64
|
+
pageStabilizationDelayMs: 500,
|
|
65
|
+
reinitCooldownMs: 3000,
|
|
66
|
+
recordCanvas: false,
|
|
67
|
+
maskAllInputs: true,
|
|
68
|
+
collectFonts: true,
|
|
69
|
+
recordCrossOriginIframes: false,
|
|
70
|
+
useTempFile: true,
|
|
71
|
+
|
|
72
|
+
recordConsole: true,
|
|
73
|
+
consoleRecordOptions: {
|
|
74
|
+
level: ['log', 'info', 'warn', 'error'],
|
|
75
|
+
lengthThreshold: 10000,
|
|
76
|
+
stringifyOptions: {
|
|
77
|
+
stringLengthLimit: 10000,
|
|
78
|
+
numOfKeysLimit: 100
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
// Network recording options
|
|
82
|
+
recordNetwork: true,
|
|
83
|
+
networkRecordOptions: {
|
|
84
|
+
recordBody: true,
|
|
85
|
+
recordHeaders: true,
|
|
86
|
+
recordInitiator: true,
|
|
87
|
+
recordPerformance: true,
|
|
88
|
+
maxBodyLength: 10000,
|
|
89
|
+
ignoreRequestFn: (url, type) => {
|
|
90
|
+
// Ignore CapVision internal requests and common tracking/analytics
|
|
91
|
+
if (!url) return true;
|
|
92
|
+
if (url.includes('capvision')) return true;
|
|
93
|
+
if (url.includes('google-analytics')) return true;
|
|
94
|
+
if (url.includes('googletagmanager')) return true;
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Browser Executor Interface - allows framework-agnostic browser execution
|
|
102
|
+
* @typedef {Object} BrowserExecutor
|
|
103
|
+
* @property {function(string|Function, ...*): Promise<*>} execute - Execute script in browser context
|
|
104
|
+
* @property {function(number): Promise<void>} pause - Pause execution for specified milliseconds
|
|
105
|
+
*/
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* CapVision Recorder Metadata
|
|
109
|
+
* @typedef {Object} RecordingMetadata
|
|
110
|
+
* @property {string} timestamp - ISO timestamp when recording was saved
|
|
111
|
+
* @property {number} totalEvents - Total number of events recorded
|
|
112
|
+
* @property {string} filename - Name of the saved recording file
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Framework-agnostic CapVision Recorder
|
|
117
|
+
* Works with WebdriverIO, Playwright, Puppeteer, or any browser automation tool
|
|
118
|
+
* @class
|
|
119
|
+
*/
|
|
120
|
+
class CapVisionRecorder {
|
|
121
|
+
/**
|
|
122
|
+
* Create a new CapVision Recorder instance
|
|
123
|
+
* @param {CapVisionRecorderConfig} [config={}] - Recorder configuration
|
|
124
|
+
*/
|
|
125
|
+
constructor(config = {}) {
|
|
126
|
+
/** @type {CapVisionRecorderConfig} */
|
|
127
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
128
|
+
|
|
129
|
+
/** @type {string|null} */
|
|
130
|
+
this.tempFilePath = null;
|
|
131
|
+
|
|
132
|
+
/** @type {NodeJS.Timeout|null} */
|
|
133
|
+
this.saveIntervalHandle = null;
|
|
134
|
+
|
|
135
|
+
/** @type {string|null} */
|
|
136
|
+
this.currentTestType = null;
|
|
137
|
+
|
|
138
|
+
/** @type {BrowserExecutor|null} */
|
|
139
|
+
this.browserExecutor = null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Set browser executor for framework-agnostic execution
|
|
144
|
+
* @param {BrowserExecutor} executor - Browser executor instance
|
|
145
|
+
*/
|
|
146
|
+
setBrowserExecutor(executor) {
|
|
147
|
+
this.browserExecutor = executor;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Update configuration
|
|
152
|
+
* @param {Partial<CapVisionRecorderConfig>} config - Configuration updates
|
|
153
|
+
*/
|
|
154
|
+
updateConfig(config) {
|
|
155
|
+
this.config = { ...this.config, ...config };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get current configuration
|
|
160
|
+
* @returns {CapVisionRecorderConfig} Current configuration object
|
|
161
|
+
*/
|
|
162
|
+
getConfig() {
|
|
163
|
+
return { ...this.config };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Detect and set test type from test file path
|
|
168
|
+
* @param {string} testFilePath - Path to test file
|
|
169
|
+
* @returns {string|null} Detected test type or null
|
|
170
|
+
*/
|
|
171
|
+
setTestTypeFromPath(testFilePath) {
|
|
172
|
+
if (!testFilePath) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const match = testFilePath.match(/\/(smoke|sanity|regression)\//);
|
|
177
|
+
if (match) {
|
|
178
|
+
this.currentTestType = match[1];
|
|
179
|
+
console.log(`🟡 Detected test type from path: ${this.currentTestType}`);
|
|
180
|
+
return this.currentTestType;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log('🟡 Could not detect test type from path:', testFilePath);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get the current test type
|
|
189
|
+
* @returns {string|null} Current test type or null
|
|
190
|
+
*/
|
|
191
|
+
getCurrentTestType() {
|
|
192
|
+
return this.currentTestType;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Check if CapVision recording should be enabled for the current cluster
|
|
197
|
+
* @param {string} [currentCluster] - Override current cluster (defaults to process.env.cluster)
|
|
198
|
+
* @returns {boolean} True if enabled for cluster
|
|
199
|
+
*/
|
|
200
|
+
isCapVisionEnabledForCluster(currentCluster) {
|
|
201
|
+
if (this.config.enabledClusters.length === 0) {
|
|
202
|
+
return true; // No filter means enabled for all
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const cluster = currentCluster || process.env.cluster;
|
|
206
|
+
|
|
207
|
+
if (!cluster) {
|
|
208
|
+
console.log('🟡 No cluster specified in environment, CapVision recording disabled');
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const isEnabled = this.config.enabledClusters.includes(cluster);
|
|
213
|
+
|
|
214
|
+
if (isEnabled) {
|
|
215
|
+
console.log(`🟢 CapVision recording enabled for cluster: ${cluster}`);
|
|
216
|
+
} else {
|
|
217
|
+
console.log(`🟡 CapVision recording disabled for cluster: ${cluster}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return isEnabled;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check if CapVision recording should be enabled for the current module
|
|
225
|
+
* @param {string} [currentModule] - Override current module (defaults to process.env.module)
|
|
226
|
+
* @returns {boolean} True if enabled for module
|
|
227
|
+
*/
|
|
228
|
+
isCapVisionEnabledForModule(currentModule) {
|
|
229
|
+
if (this.config.enabledModules.length === 0) {
|
|
230
|
+
return true; // No filter means enabled for all
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const module = currentModule || process.env.module;
|
|
234
|
+
|
|
235
|
+
if (!module) {
|
|
236
|
+
console.log('🟡 No module specified in environment, CapVision recording disabled');
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const isEnabled = this.config.enabledModules.includes(module);
|
|
241
|
+
|
|
242
|
+
if (isEnabled) {
|
|
243
|
+
console.log(`🟢 CapVision recording enabled for module: ${module}`);
|
|
244
|
+
} else {
|
|
245
|
+
console.log(`🟡 CapVision recording disabled for module: ${module}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return isEnabled;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check if CapVision recording should be enabled for the current test type
|
|
253
|
+
* @returns {boolean} True if enabled for test type
|
|
254
|
+
*/
|
|
255
|
+
isCapVisionEnabledForTestType() {
|
|
256
|
+
if (this.config.enabledTestTypes.length === 0) {
|
|
257
|
+
return true; // No filter means enabled for all
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const currentTestType = this.getCurrentTestType();
|
|
261
|
+
|
|
262
|
+
if (!currentTestType) {
|
|
263
|
+
console.log('🟡 No test type detected, CapVision recording disabled');
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const isEnabled = this.config.enabledTestTypes.includes(currentTestType);
|
|
268
|
+
|
|
269
|
+
if (isEnabled) {
|
|
270
|
+
console.log(`🟢 CapVision recording enabled for test type: ${currentTestType}`);
|
|
271
|
+
} else {
|
|
272
|
+
console.log(`🟡 CapVision recording disabled for test type: ${currentTestType}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return isEnabled;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Check if CapVision recording should be enabled (checks all criteria)
|
|
280
|
+
* @param {string} [cluster] - Override cluster check
|
|
281
|
+
* @param {string} [module] - Override module check
|
|
282
|
+
* @returns {boolean} True if recording is enabled
|
|
283
|
+
*/
|
|
284
|
+
isCapVisionEnabled(cluster, module) {
|
|
285
|
+
// Check master feature flag first
|
|
286
|
+
if (this.config.ENABLE_FEATURE === false) {
|
|
287
|
+
console.log('🟡 CapVision feature disabled (ENABLE_FEATURE=false)');
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const clusterEnabled = this.isCapVisionEnabledForCluster(cluster);
|
|
292
|
+
const moduleEnabled = this.isCapVisionEnabledForModule(module);
|
|
293
|
+
const testTypeEnabled = this.isCapVisionEnabledForTestType();
|
|
294
|
+
|
|
295
|
+
const isEnabled = clusterEnabled && moduleEnabled && testTypeEnabled;
|
|
296
|
+
|
|
297
|
+
if (isEnabled) {
|
|
298
|
+
console.log('🟢 CapVision recording enabled (all criteria met)');
|
|
299
|
+
} else {
|
|
300
|
+
const disabledReasons = [];
|
|
301
|
+
if (!clusterEnabled) disabledReasons.push('cluster');
|
|
302
|
+
if (!moduleEnabled) disabledReasons.push('module');
|
|
303
|
+
if (!testTypeEnabled) disabledReasons.push('test type');
|
|
304
|
+
console.log(`🟡 CapVision recording disabled (${disabledReasons.join(', ')} disabled)`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return isEnabled;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Clear all files in the recordings directory
|
|
312
|
+
*/
|
|
313
|
+
clearRecordingsFolder() {
|
|
314
|
+
try {
|
|
315
|
+
const recordingsDir = this.config.recordingsOutputDir;
|
|
316
|
+
if (fs.existsSync(recordingsDir)) {
|
|
317
|
+
const files = fs.readdirSync(recordingsDir);
|
|
318
|
+
files.forEach((file) => {
|
|
319
|
+
fs.unlinkSync(path.join(recordingsDir, file));
|
|
320
|
+
});
|
|
321
|
+
console.log('🟢 Cleared recordings folder');
|
|
322
|
+
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error('🔴 Failed to clear recordings folder:', error.message);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Save CapVision events to recordings directory
|
|
330
|
+
* @param {Array} events - Array of CapVision events
|
|
331
|
+
* @param {string} [testName='test'] - Name of the test
|
|
332
|
+
* @returns {string} Filename of saved recording
|
|
333
|
+
*/
|
|
334
|
+
saveEventsToFile(events, testName = 'test') {
|
|
335
|
+
try {
|
|
336
|
+
console.log('🟡 Saving CapVision events to file...');
|
|
337
|
+
|
|
338
|
+
const recordingsDir = this.config.recordingsOutputDir;
|
|
339
|
+
|
|
340
|
+
if (!fs.existsSync(recordingsDir)) {
|
|
341
|
+
fs.mkdirSync(recordingsDir, { recursive: true });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Sanitize test name
|
|
345
|
+
const sanitizedTestName = testName
|
|
346
|
+
.replace(/[^a-zA-Z0-9_-]/g, '_')
|
|
347
|
+
.replace(/_{2,}/g, '_')
|
|
348
|
+
.replace(/^_|_$/g, '');
|
|
349
|
+
|
|
350
|
+
// Find existing recordings for this test
|
|
351
|
+
const existingFiles = fs.readdirSync(recordingsDir);
|
|
352
|
+
const testFilePattern = new RegExp(`^${sanitizedTestName}-(\\d+)\\.json$`);
|
|
353
|
+
|
|
354
|
+
let maxNumber = 0;
|
|
355
|
+
existingFiles.forEach((file) => {
|
|
356
|
+
const match = file.match(testFilePattern);
|
|
357
|
+
if (match) {
|
|
358
|
+
const num = parseInt(match[1], 10);
|
|
359
|
+
if (num > maxNumber) {
|
|
360
|
+
maxNumber = num;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Generate new filename
|
|
366
|
+
const recordingNumber = maxNumber + 1;
|
|
367
|
+
const filename = `${sanitizedTestName}-${recordingNumber}.json`;
|
|
368
|
+
const filepath = path.join(recordingsDir, filename);
|
|
369
|
+
|
|
370
|
+
const eventsData = {
|
|
371
|
+
testName: testName,
|
|
372
|
+
recordingNumber: recordingNumber,
|
|
373
|
+
timestamp: new Date().toISOString(),
|
|
374
|
+
totalEvents: events.length,
|
|
375
|
+
events: events
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
fs.writeFileSync(filepath, JSON.stringify(eventsData, null, 2));
|
|
379
|
+
console.log(`🟢 Saved ${events.length} events to: ${filename}`);
|
|
380
|
+
|
|
381
|
+
return filename;
|
|
382
|
+
|
|
383
|
+
} catch (error) {
|
|
384
|
+
console.error('🔴 Failed to save CapVision events:', error.message);
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Create a temporary file for storing events during recording
|
|
391
|
+
* @private
|
|
392
|
+
* @returns {string} Path to temporary file
|
|
393
|
+
*/
|
|
394
|
+
createTempFile() {
|
|
395
|
+
const tempDir = path.join(os.tmpdir(), 'capvision-recordings');
|
|
396
|
+
|
|
397
|
+
if (!fs.existsSync(tempDir)) {
|
|
398
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const timestamp = Date.now();
|
|
402
|
+
const tempFileName = `capvision_temp_${timestamp}.json`;
|
|
403
|
+
const tempFilePath = path.join(tempDir, tempFileName);
|
|
404
|
+
|
|
405
|
+
fs.writeFileSync(tempFilePath, JSON.stringify([]), 'utf8');
|
|
406
|
+
console.log(`🟡 Created temp file: ${tempFilePath}`);
|
|
407
|
+
|
|
408
|
+
return tempFilePath;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Save events to temporary file
|
|
413
|
+
* @private
|
|
414
|
+
* @param {Array} events - Array of events to save
|
|
415
|
+
*/
|
|
416
|
+
saveToTempFile(events) {
|
|
417
|
+
try {
|
|
418
|
+
if (this.tempFilePath) {
|
|
419
|
+
fs.writeFileSync(this.tempFilePath, JSON.stringify(events), 'utf8');
|
|
420
|
+
}
|
|
421
|
+
} catch (error) {
|
|
422
|
+
console.error('🔴 Failed to save events to temp file:', error.message);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Read events from temporary file
|
|
428
|
+
* @private
|
|
429
|
+
* @returns {Array} Array of events
|
|
430
|
+
*/
|
|
431
|
+
readFromTempFile() {
|
|
432
|
+
try {
|
|
433
|
+
if (this.tempFilePath && fs.existsSync(this.tempFilePath)) {
|
|
434
|
+
const data = fs.readFileSync(this.tempFilePath, 'utf8');
|
|
435
|
+
const parsed = JSON.parse(data);
|
|
436
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
437
|
+
}
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.warn('⚠️ Failed to read events from temp file:', error.message);
|
|
440
|
+
}
|
|
441
|
+
return [];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Delete temporary file
|
|
446
|
+
* @private
|
|
447
|
+
*/
|
|
448
|
+
deleteTempFile() {
|
|
449
|
+
try {
|
|
450
|
+
if (this.tempFilePath && fs.existsSync(this.tempFilePath)) {
|
|
451
|
+
fs.unlinkSync(this.tempFilePath);
|
|
452
|
+
console.log('🟢 Temp file deleted');
|
|
453
|
+
this.tempFilePath = null;
|
|
454
|
+
}
|
|
455
|
+
} catch (error) {
|
|
456
|
+
console.warn('⚠️ Failed to delete temp file:', error.message);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Start CapVision recording session
|
|
462
|
+
* @async
|
|
463
|
+
* @returns {Promise<void>}
|
|
464
|
+
* @throws {Error} If browser executor not set
|
|
465
|
+
*/
|
|
466
|
+
async startRecording() {
|
|
467
|
+
if (!this.browserExecutor) {
|
|
468
|
+
throw new Error('Browser executor not set. Call setBrowserExecutor() first.');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!this.isCapVisionEnabled()) {
|
|
472
|
+
console.log("🟡 Skipping CapVision recording (disabled by configuration)");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
console.log("🟡 Starting CapVision recording...");
|
|
477
|
+
|
|
478
|
+
// Create temp file if using file-based storage
|
|
479
|
+
if (this.config.useTempFile) {
|
|
480
|
+
this.tempFilePath = this.createTempFile();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Read CapVision script from bundled file
|
|
484
|
+
let capVisionScriptCode = '';
|
|
485
|
+
try {
|
|
486
|
+
capVisionScriptCode = fs.readFileSync(this.config.capVisionScriptPath, 'utf8');
|
|
487
|
+
} catch (error) {
|
|
488
|
+
console.error('❌ Failed to load CapVision script file:', error.message);
|
|
489
|
+
throw new Error('Cannot start recording without CapVision script');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Read console plugin code
|
|
493
|
+
let consolePluginCode = '';
|
|
494
|
+
if (this.config.recordConsole) {
|
|
495
|
+
try {
|
|
496
|
+
consolePluginCode = fs.readFileSync(this.config.consoleRecordPluginPath, 'utf8');
|
|
497
|
+
} catch (error) {
|
|
498
|
+
console.warn('⚠️ Failed to load console plugin file:', error.message);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Read network plugin code
|
|
503
|
+
let networkPluginCode = '';
|
|
504
|
+
if (this.config.recordNetwork) {
|
|
505
|
+
try {
|
|
506
|
+
console.log('🟡 Loading network plugin from:', this.config.networkRecordPluginPath);
|
|
507
|
+
networkPluginCode = fs.readFileSync(this.config.networkRecordPluginPath, 'utf8');
|
|
508
|
+
console.log(`🟢 Network plugin code loaded: ${networkPluginCode.length} characters`);
|
|
509
|
+
if (networkPluginCode.length === 0) {
|
|
510
|
+
console.warn('⚠️ Network plugin file is empty!');
|
|
511
|
+
}
|
|
512
|
+
} catch (error) {
|
|
513
|
+
console.error('🔴 Failed to load network plugin file:', error.message);
|
|
514
|
+
console.error('🔴 File path:', this.config.networkRecordPluginPath);
|
|
515
|
+
}
|
|
516
|
+
} else {
|
|
517
|
+
console.log('🟡 Network recording disabled in config');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
await this.browserExecutor.execute((config, capVisionScript, pluginCode, networkPluginCode) => {
|
|
521
|
+
// Initialize global state
|
|
522
|
+
window.capVisionInitialized = false;
|
|
523
|
+
window.capVisionLastCheckTime = Date.now();
|
|
524
|
+
window.capVisionEvents = [];
|
|
525
|
+
|
|
526
|
+
console.log('🟢 Initialized empty events array for recording');
|
|
527
|
+
|
|
528
|
+
// Inject CapVision script
|
|
529
|
+
const injectScript = () => {
|
|
530
|
+
return new Promise((resolve) => {
|
|
531
|
+
if (window.rrweb) {
|
|
532
|
+
console.log('🟢 CapVision script already loaded');
|
|
533
|
+
resolve();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
const script = document.createElement('script');
|
|
539
|
+
script.textContent = capVisionScript;
|
|
540
|
+
document.head.appendChild(script);
|
|
541
|
+
console.log('🟢 CapVision script injected from bundled file');
|
|
542
|
+
resolve();
|
|
543
|
+
} catch (error) {
|
|
544
|
+
console.error('❌ Failed to inject CapVision script:', error.message);
|
|
545
|
+
resolve();
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// Inject Console Plugin
|
|
551
|
+
const injectConsolePlugin = (code) => {
|
|
552
|
+
return new Promise((resolve) => {
|
|
553
|
+
if (!config.recordConsole || !code) {
|
|
554
|
+
resolve();
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (window.rrwebPluginConsoleRecord) {
|
|
559
|
+
resolve();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
const script = document.createElement('script');
|
|
565
|
+
script.textContent = code;
|
|
566
|
+
document.head.appendChild(script);
|
|
567
|
+
resolve();
|
|
568
|
+
} catch (error) {
|
|
569
|
+
console.warn('⚠️ Failed to inject console plugin:', error.message);
|
|
570
|
+
resolve();
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// Inject Network Record Plugin script
|
|
576
|
+
const injectNetworkPlugin = (pluginCode) => {
|
|
577
|
+
return new Promise((resolve) => {
|
|
578
|
+
if (!config.recordNetwork || !pluginCode) {
|
|
579
|
+
console.log('🟡 Network recording disabled or plugin code not available');
|
|
580
|
+
resolve();
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (window.rrwebPluginNetworkRecord) {
|
|
585
|
+
console.log('🟢 Network plugin already loaded');
|
|
586
|
+
resolve();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
console.log('🟡 Injecting network plugin script...');
|
|
592
|
+
console.log('🟡 Plugin code length:', pluginCode.length);
|
|
593
|
+
|
|
594
|
+
// Try direct execution first (for immediate availability)
|
|
595
|
+
try {
|
|
596
|
+
eval(pluginCode);
|
|
597
|
+
if (window.rrwebPluginNetworkRecord) {
|
|
598
|
+
console.log('🟢 Network plugin loaded via eval');
|
|
599
|
+
resolve();
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
} catch (evalError) {
|
|
603
|
+
console.warn('⚠️ Eval failed, trying script injection:', evalError);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Fallback: inject as script element
|
|
607
|
+
const script = document.createElement('script');
|
|
608
|
+
script.textContent = pluginCode;
|
|
609
|
+
script.onerror = (error) => {
|
|
610
|
+
console.error('🔴 Script onerror:', error);
|
|
611
|
+
};
|
|
612
|
+
document.head.appendChild(script);
|
|
613
|
+
|
|
614
|
+
// Wait for script to execute and set window.rrwebPluginNetworkRecord
|
|
615
|
+
let checkCount = 0;
|
|
616
|
+
const checkInterval = setInterval(() => {
|
|
617
|
+
checkCount++;
|
|
618
|
+
if (window.rrwebPluginNetworkRecord) {
|
|
619
|
+
clearInterval(checkInterval);
|
|
620
|
+
console.log(`🟢 Network plugin script loaded successfully after ${checkCount * 50}ms`);
|
|
621
|
+
resolve();
|
|
622
|
+
} else if (checkCount > 20) {
|
|
623
|
+
// Stop checking after 1 second
|
|
624
|
+
clearInterval(checkInterval);
|
|
625
|
+
console.warn('⚠️ Network plugin script injected but function not found after 1s');
|
|
626
|
+
console.warn('⚠️ Script element:', script);
|
|
627
|
+
console.warn('⚠️ Script parent:', script.parentElement);
|
|
628
|
+
console.warn('⚠️ Window keys:', Object.keys(window).filter(k => k.includes('capvision') || k.includes('rrweb')));
|
|
629
|
+
resolve();
|
|
630
|
+
}
|
|
631
|
+
}, 50);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
console.error('🔴 Failed to inject network plugin:', error.message);
|
|
634
|
+
console.error('🔴 Error stack:', error.stack);
|
|
635
|
+
resolve();
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// Restore events from sessionStorage (fallback)
|
|
641
|
+
const restoreEventsFromStorage = () => {
|
|
642
|
+
if (!config.useTempFile) {
|
|
643
|
+
try {
|
|
644
|
+
const saved = sessionStorage.getItem(config.sessionStorageKey);
|
|
645
|
+
if (saved) {
|
|
646
|
+
const parsed = JSON.parse(saved);
|
|
647
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
648
|
+
window.capVisionEvents = parsed;
|
|
649
|
+
console.log(`🟢 Restored ${parsed.length} events from sessionStorage`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
} catch (error) {
|
|
653
|
+
console.warn('⚠️ Failed to restore events from sessionStorage:', error.message);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
// Save events to sessionStorage (fallback)
|
|
659
|
+
const saveEventsToStorage = () => {
|
|
660
|
+
if (!config.useTempFile && window.capVisionEvents && window.capVisionEvents.length > 0) {
|
|
661
|
+
try {
|
|
662
|
+
sessionStorage.setItem(config.sessionStorageKey, JSON.stringify(window.capVisionEvents));
|
|
663
|
+
} catch (error) {
|
|
664
|
+
console.warn('⚠️ Failed to save events to sessionStorage:', error.message);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
// Wait for CapVision library to load
|
|
670
|
+
const waitForCapVision = () => {
|
|
671
|
+
return new Promise((resolve) => {
|
|
672
|
+
const check = () => {
|
|
673
|
+
if (window.rrweb && window.rrweb.record) {
|
|
674
|
+
resolve();
|
|
675
|
+
} else {
|
|
676
|
+
setTimeout(check, 100);
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
check();
|
|
680
|
+
});
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// Get console plugin
|
|
684
|
+
const getConsolePlugin = () => {
|
|
685
|
+
try {
|
|
686
|
+
if (!config.recordConsole || !window.rrwebPluginConsoleRecord) {
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const pluginModule = window.rrwebPluginConsoleRecord;
|
|
691
|
+
if (typeof pluginModule === 'function') {
|
|
692
|
+
return pluginModule(config.consoleRecordOptions);
|
|
693
|
+
}
|
|
694
|
+
if (pluginModule.getRecordConsolePlugin) {
|
|
695
|
+
return pluginModule.getRecordConsolePlugin(config.consoleRecordOptions);
|
|
696
|
+
}
|
|
697
|
+
if (pluginModule.default) {
|
|
698
|
+
return pluginModule.default(config.consoleRecordOptions);
|
|
699
|
+
}
|
|
700
|
+
} catch (error) {
|
|
701
|
+
console.warn('⚠️ Failed to initialize console plugin:', error.message);
|
|
702
|
+
}
|
|
703
|
+
return null;
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// Get network plugin if available
|
|
707
|
+
const getNetworkPlugin = () => {
|
|
708
|
+
try {
|
|
709
|
+
if (!config.recordNetwork) {
|
|
710
|
+
console.log('🟡 Network recording disabled in config');
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (!window.rrwebPluginNetworkRecord) {
|
|
715
|
+
console.warn('⚠️ Network plugin function not found on window object');
|
|
716
|
+
console.log('🟡 Available window properties:', Object.keys(window).filter(k => k.includes('capvision') || k.includes('rrweb')));
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
console.log('🟡 Initializing network plugin with options:', config.networkRecordOptions);
|
|
721
|
+
|
|
722
|
+
// Handle different export patterns
|
|
723
|
+
if (typeof window.rrwebPluginNetworkRecord === 'function') {
|
|
724
|
+
const plugin = window.rrwebPluginNetworkRecord(config.networkRecordOptions);
|
|
725
|
+
console.log('🟢 Network plugin initialized successfully (function pattern)');
|
|
726
|
+
console.log('🟡 Plugin structure:', {
|
|
727
|
+
name: plugin?.name,
|
|
728
|
+
hasObserver: typeof plugin?.observer === 'function',
|
|
729
|
+
keys: Object.keys(plugin || {})
|
|
730
|
+
});
|
|
731
|
+
return plugin;
|
|
732
|
+
}
|
|
733
|
+
if (window.rrwebPluginNetworkRecord.getRecordNetworkPlugin) {
|
|
734
|
+
const plugin = window.rrwebPluginNetworkRecord.getRecordNetworkPlugin(config.networkRecordOptions);
|
|
735
|
+
console.log('🟢 Network plugin initialized successfully (getRecordNetworkPlugin pattern)');
|
|
736
|
+
return plugin;
|
|
737
|
+
}
|
|
738
|
+
if (window.rrwebPluginNetworkRecord.default) {
|
|
739
|
+
const plugin = window.rrwebPluginNetworkRecord.default(config.networkRecordOptions);
|
|
740
|
+
console.log('🟢 Network plugin initialized successfully (default pattern)');
|
|
741
|
+
return plugin;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
console.warn('⚠️ Network plugin function found but no matching export pattern');
|
|
745
|
+
console.log('🟡 Plugin type:', typeof window.rrwebPluginNetworkRecord);
|
|
746
|
+
console.log('🟡 Plugin keys:', Object.keys(window.rrwebPluginNetworkRecord || {}));
|
|
747
|
+
} catch (error) {
|
|
748
|
+
console.error('🔴 Failed to initialize network plugin:', error.message);
|
|
749
|
+
console.error('🔴 Error stack:', error.stack);
|
|
750
|
+
}
|
|
751
|
+
return null;
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
// Start CapVision recorder
|
|
755
|
+
const startRecorder = () => {
|
|
756
|
+
waitForCapVision().then(() => {
|
|
757
|
+
if (window.rrwebRecorder) {
|
|
758
|
+
window.rrwebRecorder();
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const plugins = [];
|
|
762
|
+
const consolePlugin = getConsolePlugin();
|
|
763
|
+
if (consolePlugin) {
|
|
764
|
+
plugins.push(consolePlugin);
|
|
765
|
+
console.log('🟢 Console recording enabled');
|
|
766
|
+
}
|
|
767
|
+
const networkPlugin = getNetworkPlugin();
|
|
768
|
+
if (networkPlugin) {
|
|
769
|
+
plugins.push(networkPlugin);
|
|
770
|
+
console.log('🟢 Network recording enabled');
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
window.rrwebRecorder = window.rrweb.record({
|
|
774
|
+
emit: (event) => {
|
|
775
|
+
window.capVisionEvents.push(event);
|
|
776
|
+
|
|
777
|
+
if (!config.useTempFile) {
|
|
778
|
+
saveEventsToStorage();
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
checkoutEveryNms: config.checkoutIntervalMs,
|
|
782
|
+
recordCanvas: config.recordCanvas,
|
|
783
|
+
maskAllInputs: config.maskAllInputs,
|
|
784
|
+
maskInputOptions: {
|
|
785
|
+
password: true,
|
|
786
|
+
email: false,
|
|
787
|
+
},
|
|
788
|
+
collectFonts: config.collectFonts,
|
|
789
|
+
recordCrossOriginIframes: config.recordCrossOriginIframes,
|
|
790
|
+
plugins: plugins.length > 0 ? plugins : undefined
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
window.capVisionInitialized = true;
|
|
794
|
+
window.capVisionLastCheckTime = Date.now();
|
|
795
|
+
console.log('🟢 CapVision recording started');
|
|
796
|
+
});
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// Setup persistence
|
|
800
|
+
const setupPersistence = () => {
|
|
801
|
+
window.addEventListener('beforeunload', () => {
|
|
802
|
+
if (!config.useTempFile) {
|
|
803
|
+
saveEventsToStorage();
|
|
804
|
+
console.log('🟡 Events saved to sessionStorage before unload');
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
const checkInterval = setInterval(() => {
|
|
809
|
+
// Check master feature flag first
|
|
810
|
+
if (config.ENABLE_FEATURE === false) {
|
|
811
|
+
console.log('🟡 CapVision feature disabled (ENABLE_FEATURE=false), stopping reinitialization checks');
|
|
812
|
+
clearInterval(checkInterval);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const now = Date.now();
|
|
817
|
+
if (now - window.capVisionLastCheckTime < config.reinitCooldownMs) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
window.capVisionLastCheckTime = now;
|
|
822
|
+
|
|
823
|
+
if (!window.capVisionInitialized || !window.rrwebRecorder) {
|
|
824
|
+
console.log('🟡 CapVision not active, attempting reinitialization...');
|
|
825
|
+
|
|
826
|
+
if (!document.querySelector('script[src*="capvision"]')) {
|
|
827
|
+
injectScript()
|
|
828
|
+
.then(() => injectConsolePlugin(pluginCode))
|
|
829
|
+
.then(() => injectNetworkPlugin(networkPluginCode))
|
|
830
|
+
.then(() => startRecorder())
|
|
831
|
+
.catch(console.error);
|
|
832
|
+
} else {
|
|
833
|
+
startRecorder();
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}, config.saveIntervalMs);
|
|
837
|
+
|
|
838
|
+
window.capVisionCheckInterval = checkInterval;
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
// Main initialization
|
|
842
|
+
const initialize = async () => {
|
|
843
|
+
try {
|
|
844
|
+
setupPersistence();
|
|
845
|
+
await injectScript();
|
|
846
|
+
await injectConsolePlugin(pluginCode);
|
|
847
|
+
await injectNetworkPlugin(networkPluginCode);
|
|
848
|
+
restoreEventsFromStorage();
|
|
849
|
+
startRecorder();
|
|
850
|
+
} catch (error) {
|
|
851
|
+
console.error('🔴 Failed to initialize CapVision:', error.message);
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
initialize();
|
|
856
|
+
}, this.config, capVisionScriptCode, consolePluginCode, networkPluginCode);
|
|
857
|
+
|
|
858
|
+
// Setup Node.js-side periodic file save
|
|
859
|
+
if (this.config.useTempFile) {
|
|
860
|
+
this.saveIntervalHandle = setInterval(async () => {
|
|
861
|
+
try {
|
|
862
|
+
const events = await this.browserExecutor.execute(() => {
|
|
863
|
+
return window.capVisionEvents || [];
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
if (events && events.length > 0) {
|
|
867
|
+
this.saveToTempFile(events);
|
|
868
|
+
}
|
|
869
|
+
} catch (error) {
|
|
870
|
+
console.warn('⚠️ Periodic file save failed:', error.message);
|
|
871
|
+
}
|
|
872
|
+
}, this.config.saveIntervalMs);
|
|
873
|
+
|
|
874
|
+
console.log(`🟢 Periodic file save enabled (every ${this.config.saveIntervalMs}ms)`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Stop CapVision recording and save events
|
|
880
|
+
* @async
|
|
881
|
+
* @param {string} [testName='test'] - Name of the test for filename
|
|
882
|
+
* @param {boolean} [testPassed=true] - Whether the test passed. If true, recording is not saved permanently but temp file is still deleted
|
|
883
|
+
* @returns {Promise<RecordingMetadata>} Recording metadata
|
|
884
|
+
* @throws {Error} If browser executor not set
|
|
885
|
+
*/
|
|
886
|
+
async stopRecordingAndSave(testName = 'test', testPassed = true) {
|
|
887
|
+
if (!this.browserExecutor) {
|
|
888
|
+
throw new Error('Browser executor not set. Call setBrowserExecutor() first.');
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (!this.isCapVisionEnabled()) {
|
|
892
|
+
console.log("🟡 Skipping CapVision stop (disabled by configuration)");
|
|
893
|
+
return {
|
|
894
|
+
timestamp: new Date().toISOString(),
|
|
895
|
+
totalEvents: 0,
|
|
896
|
+
filename: ''
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
console.log("🟡 Stopping CapVision recording and saving...");
|
|
901
|
+
|
|
902
|
+
// Clear periodic save interval
|
|
903
|
+
if (this.saveIntervalHandle) {
|
|
904
|
+
clearInterval(this.saveIntervalHandle);
|
|
905
|
+
this.saveIntervalHandle = null;
|
|
906
|
+
console.log('🟢 Periodic file save stopped');
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Final save
|
|
910
|
+
if (this.config.useTempFile) {
|
|
911
|
+
try {
|
|
912
|
+
console.log('🟡 Performing final save before stopping...');
|
|
913
|
+
const finalEvents = await this.browserExecutor.execute(() => {
|
|
914
|
+
return window.capVisionEvents || [];
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
if (finalEvents && finalEvents.length > 0) {
|
|
918
|
+
this.saveToTempFile(finalEvents);
|
|
919
|
+
console.log(`🟢 Final save: ${finalEvents.length} events written to temp file`);
|
|
920
|
+
}
|
|
921
|
+
} catch (error) {
|
|
922
|
+
console.warn('⚠️ Final save failed:', error.message);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Stop recorder and get metadata
|
|
927
|
+
const metadata = await this.browserExecutor.execute((config) => {
|
|
928
|
+
const eventCount = window.capVisionEvents ? window.capVisionEvents.length : 0;
|
|
929
|
+
const timestamp = new Date().toISOString();
|
|
930
|
+
|
|
931
|
+
// Stop recorder
|
|
932
|
+
if (window.rrwebRecorder) {
|
|
933
|
+
window.rrwebRecorder();
|
|
934
|
+
console.log('🟢 Recorder stopped');
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Clean up intervals
|
|
938
|
+
if (window.capVisionCheckInterval) {
|
|
939
|
+
clearInterval(window.capVisionCheckInterval);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Clean up sessionStorage
|
|
943
|
+
if (!config.useTempFile) {
|
|
944
|
+
try {
|
|
945
|
+
sessionStorage.removeItem(config.sessionStorageKey);
|
|
946
|
+
} catch (error) {
|
|
947
|
+
console.warn('⚠️ Failed to clean sessionStorage:', error.message);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Clear browser memory
|
|
952
|
+
window.capVisionEvents = [];
|
|
953
|
+
window.capVisionInitialized = false;
|
|
954
|
+
|
|
955
|
+
return {
|
|
956
|
+
timestamp: timestamp,
|
|
957
|
+
totalEvents: eventCount
|
|
958
|
+
};
|
|
959
|
+
}, this.config);
|
|
960
|
+
|
|
961
|
+
// Save to permanent location only if test failed
|
|
962
|
+
let savedFilename = '';
|
|
963
|
+
if (this.config.useTempFile && metadata.totalEvents > 0) {
|
|
964
|
+
try {
|
|
965
|
+
const events = this.readFromTempFile();
|
|
966
|
+
|
|
967
|
+
if (events && events.length > 0) {
|
|
968
|
+
if (!testPassed) {
|
|
969
|
+
// Only save permanently if test failed
|
|
970
|
+
savedFilename = this.saveEventsToFile(events, testName);
|
|
971
|
+
console.log(`🟢 Saved ${events.length} events to: ${savedFilename} (test failed)`);
|
|
972
|
+
} else {
|
|
973
|
+
console.log(`🟡 Test passed - skipping permanent save (${events.length} events discarded)`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Always delete temp file regardless of test result
|
|
978
|
+
this.deleteTempFile();
|
|
979
|
+
console.log('🟢 Temporary file cleaned up');
|
|
980
|
+
} catch (error) {
|
|
981
|
+
console.error('🔴 Failed to process events from temp file:', error.message);
|
|
982
|
+
// Still try to delete temp file even if there was an error
|
|
983
|
+
try {
|
|
984
|
+
this.deleteTempFile();
|
|
985
|
+
} catch (deleteError) {
|
|
986
|
+
console.warn('⚠️ Failed to delete temp file:', deleteError.message);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
console.log(`🟢 Recording stopped. Captured ${metadata.totalEvents} events`);
|
|
992
|
+
|
|
993
|
+
return {
|
|
994
|
+
timestamp: metadata.timestamp,
|
|
995
|
+
totalEvents: metadata.totalEvents,
|
|
996
|
+
filename: savedFilename
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Re-initialize CapVision after browser refresh or navigation
|
|
1002
|
+
* @async
|
|
1003
|
+
* @returns {Promise<void>}
|
|
1004
|
+
* @throws {Error} If browser executor not set
|
|
1005
|
+
*/
|
|
1006
|
+
async ensureCapVisionIsActive() {
|
|
1007
|
+
if (!this.browserExecutor) {
|
|
1008
|
+
throw new Error('Browser executor not set. Call setBrowserExecutor() first.');
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
if (!this.isCapVisionEnabled()) {
|
|
1012
|
+
console.log("🟡 Skipping CapVision re-initialization (disabled by configuration)");
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
console.log("🟡 Re-initializing CapVision after page change...");
|
|
1017
|
+
|
|
1018
|
+
await this.browserExecutor.pause(this.config.pageStabilizationDelayMs);
|
|
1019
|
+
|
|
1020
|
+
// Read scripts
|
|
1021
|
+
let capVisionScriptCode = '';
|
|
1022
|
+
try {
|
|
1023
|
+
capVisionScriptCode = fs.readFileSync(this.config.capVisionScriptPath, 'utf8');
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
console.error('❌ Failed to load CapVision script file:', error.message);
|
|
1026
|
+
throw new Error('Cannot reinitialize recording without CapVision script');
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
let consolePluginCode = '';
|
|
1030
|
+
if (this.config.recordConsole) {
|
|
1031
|
+
try {
|
|
1032
|
+
consolePluginCode = fs.readFileSync(this.config.consoleRecordPluginPath, 'utf8');
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
console.warn('⚠️ Failed to load console plugin file:', error.message);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Read network plugin code
|
|
1039
|
+
let networkPluginCode = '';
|
|
1040
|
+
if (this.config.recordNetwork) {
|
|
1041
|
+
try {
|
|
1042
|
+
networkPluginCode = fs.readFileSync(this.config.networkRecordPluginPath, 'utf8');
|
|
1043
|
+
} catch (error) {
|
|
1044
|
+
console.warn('⚠️ Failed to load network plugin file:', error.message);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Get event count
|
|
1049
|
+
let eventCount = 0;
|
|
1050
|
+
if (this.config.useTempFile) {
|
|
1051
|
+
try {
|
|
1052
|
+
const tempEvents = this.readFromTempFile();
|
|
1053
|
+
eventCount = tempEvents ? tempEvents.length : 0;
|
|
1054
|
+
console.log(`🟢 Will restore ${eventCount} events from temp file`);
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
console.warn('⚠️ Could not read temp file:', error.message);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
await this.browserExecutor.execute((config, count, capVisionScript, pluginCode, networkPluginCode) => {
|
|
1061
|
+
// Re-inject CapVision
|
|
1062
|
+
const injectScript = () => {
|
|
1063
|
+
return new Promise((resolve) => {
|
|
1064
|
+
if (window.rrweb) {
|
|
1065
|
+
console.log('🟢 CapVision script already loaded');
|
|
1066
|
+
resolve();
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
try {
|
|
1071
|
+
const script = document.createElement('script');
|
|
1072
|
+
script.textContent = capVisionScript;
|
|
1073
|
+
document.head.appendChild(script);
|
|
1074
|
+
console.log('🟢 CapVision script re-injected');
|
|
1075
|
+
resolve();
|
|
1076
|
+
} catch (error) {
|
|
1077
|
+
console.error('❌ Failed to re-inject CapVision script:', error.message);
|
|
1078
|
+
resolve();
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
const injectConsolePlugin = (code) => {
|
|
1084
|
+
return new Promise((resolve) => {
|
|
1085
|
+
if (!config.recordConsole || !code) {
|
|
1086
|
+
resolve();
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
if (window.rrwebPluginConsoleRecord) {
|
|
1091
|
+
resolve();
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
try {
|
|
1096
|
+
const script = document.createElement('script');
|
|
1097
|
+
script.textContent = code;
|
|
1098
|
+
document.head.appendChild(script);
|
|
1099
|
+
resolve();
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
console.warn('⚠️ Failed to inject console plugin:', error.message);
|
|
1102
|
+
resolve();
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
};
|
|
1106
|
+
|
|
1107
|
+
const injectNetworkPlugin = (pluginCode) => {
|
|
1108
|
+
return new Promise((resolve) => {
|
|
1109
|
+
if (!config.recordNetwork || !pluginCode) {
|
|
1110
|
+
console.log('🟡 Network recording disabled or plugin code not available');
|
|
1111
|
+
resolve();
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (window.rrwebPluginNetworkRecord) {
|
|
1116
|
+
console.log('🟢 Network plugin already loaded');
|
|
1117
|
+
resolve();
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
try {
|
|
1122
|
+
console.log('🟡 Injecting network plugin script (re-init)...');
|
|
1123
|
+
console.log('🟡 Plugin code length:', pluginCode.length);
|
|
1124
|
+
|
|
1125
|
+
// Try direct execution first (for immediate availability)
|
|
1126
|
+
try {
|
|
1127
|
+
eval(pluginCode);
|
|
1128
|
+
if (window.rrwebPluginNetworkRecord) {
|
|
1129
|
+
console.log('🟢 Network plugin loaded via eval (re-init)');
|
|
1130
|
+
resolve();
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
} catch (evalError) {
|
|
1134
|
+
console.warn('⚠️ Eval failed, trying script injection:', evalError);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// Fallback: inject as script element
|
|
1138
|
+
const script = document.createElement('script');
|
|
1139
|
+
script.textContent = pluginCode;
|
|
1140
|
+
script.onerror = (error) => {
|
|
1141
|
+
console.error('🔴 Script onerror:', error);
|
|
1142
|
+
};
|
|
1143
|
+
document.head.appendChild(script);
|
|
1144
|
+
|
|
1145
|
+
// Wait for script to execute and set window.rrwebPluginNetworkRecord
|
|
1146
|
+
let checkCount = 0;
|
|
1147
|
+
const checkInterval = setInterval(() => {
|
|
1148
|
+
checkCount++;
|
|
1149
|
+
if (window.rrwebPluginNetworkRecord) {
|
|
1150
|
+
clearInterval(checkInterval);
|
|
1151
|
+
console.log(`🟢 Network plugin script loaded successfully after ${checkCount * 50}ms (re-init)`);
|
|
1152
|
+
resolve();
|
|
1153
|
+
} else if (checkCount > 20) {
|
|
1154
|
+
// Stop checking after 1 second
|
|
1155
|
+
clearInterval(checkInterval);
|
|
1156
|
+
console.warn('⚠️ Network plugin script injected but function not found after 1s (re-init)');
|
|
1157
|
+
console.warn('⚠️ Script element:', script);
|
|
1158
|
+
console.warn('⚠️ Script parent:', script.parentElement);
|
|
1159
|
+
console.warn('⚠️ Window keys:', Object.keys(window).filter(k => k.includes('capvision') || k.includes('rrweb')));
|
|
1160
|
+
resolve();
|
|
1161
|
+
}
|
|
1162
|
+
}, 50);
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
console.error('🔴 Failed to inject network plugin:', error.message);
|
|
1165
|
+
console.error('🔴 Error stack:', error.stack);
|
|
1166
|
+
resolve();
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
const restoreEventsFromStorage = () => {
|
|
1172
|
+
if (!window.capVisionEvents) {
|
|
1173
|
+
window.capVisionEvents = [];
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (!config.useTempFile) {
|
|
1177
|
+
try {
|
|
1178
|
+
const saved = sessionStorage.getItem(config.sessionStorageKey);
|
|
1179
|
+
if (saved) {
|
|
1180
|
+
const parsed = JSON.parse(saved);
|
|
1181
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
1182
|
+
window.capVisionEvents = parsed;
|
|
1183
|
+
console.log(`🟢 Restored ${parsed.length} events from sessionStorage`);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
console.warn('⚠️ Failed to restore events from sessionStorage:', error.message);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
const initializeEventsForFileMode = () => {
|
|
1193
|
+
if (config.useTempFile) {
|
|
1194
|
+
window.capVisionEvents = [];
|
|
1195
|
+
if (count > 0) {
|
|
1196
|
+
console.log(`🟢 Temp file has ${count} events`);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
const waitForCapVision = () => {
|
|
1202
|
+
return new Promise((resolve) => {
|
|
1203
|
+
const check = () => {
|
|
1204
|
+
if (window.rrweb && window.rrweb.record) {
|
|
1205
|
+
resolve();
|
|
1206
|
+
} else {
|
|
1207
|
+
setTimeout(check, 100);
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
check();
|
|
1211
|
+
});
|
|
1212
|
+
};
|
|
1213
|
+
|
|
1214
|
+
const getConsolePlugin = () => {
|
|
1215
|
+
try {
|
|
1216
|
+
if (!config.recordConsole || !window.rrwebPluginConsoleRecord) {
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const pluginModule = window.rrwebPluginConsoleRecord;
|
|
1221
|
+
if (typeof pluginModule === 'function') {
|
|
1222
|
+
return pluginModule(config.consoleRecordOptions);
|
|
1223
|
+
}
|
|
1224
|
+
if (pluginModule.getRecordConsolePlugin) {
|
|
1225
|
+
return pluginModule.getRecordConsolePlugin(config.consoleRecordOptions);
|
|
1226
|
+
}
|
|
1227
|
+
if (pluginModule.default) {
|
|
1228
|
+
return pluginModule.default(config.consoleRecordOptions);
|
|
1229
|
+
}
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
console.warn('⚠️ Failed to initialize console plugin:', error.message);
|
|
1232
|
+
}
|
|
1233
|
+
return null;
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
const getNetworkPlugin = () => {
|
|
1237
|
+
try {
|
|
1238
|
+
if (!config.recordNetwork || !window.rrwebPluginNetworkRecord) {
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// Handle different export patterns
|
|
1243
|
+
if (typeof window.rrwebPluginNetworkRecord === 'function') {
|
|
1244
|
+
return window.rrwebPluginNetworkRecord(config.networkRecordOptions);
|
|
1245
|
+
}
|
|
1246
|
+
if (window.rrwebPluginNetworkRecord.getRecordNetworkPlugin) {
|
|
1247
|
+
return window.rrwebPluginNetworkRecord.getRecordNetworkPlugin(config.networkRecordOptions);
|
|
1248
|
+
}
|
|
1249
|
+
if (window.rrwebPluginNetworkRecord.default) {
|
|
1250
|
+
return window.rrwebPluginNetworkRecord.default(config.networkRecordOptions);
|
|
1251
|
+
}
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
console.warn('⚠️ Failed to initialize network plugin:', error.message);
|
|
1254
|
+
}
|
|
1255
|
+
return null;
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
const startRecorder = () => {
|
|
1259
|
+
if (window.rrwebRecorder) {
|
|
1260
|
+
try {
|
|
1261
|
+
window.rrwebRecorder();
|
|
1262
|
+
} catch (e) {
|
|
1263
|
+
console.warn('⚠️ Error stopping existing recorder:', e);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
waitForCapVision().then(() => {
|
|
1268
|
+
const plugins = [];
|
|
1269
|
+
const consolePlugin = getConsolePlugin();
|
|
1270
|
+
if (consolePlugin) {
|
|
1271
|
+
plugins.push(consolePlugin);
|
|
1272
|
+
}
|
|
1273
|
+
const networkPlugin = getNetworkPlugin();
|
|
1274
|
+
if (networkPlugin) {
|
|
1275
|
+
plugins.push(networkPlugin);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
window.rrwebRecorder = window.rrweb.record({
|
|
1279
|
+
emit: (event) => {
|
|
1280
|
+
window.capVisionEvents.push(event);
|
|
1281
|
+
|
|
1282
|
+
if (!config.useTempFile) {
|
|
1283
|
+
try {
|
|
1284
|
+
sessionStorage.setItem(config.sessionStorageKey, JSON.stringify(window.capVisionEvents));
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
console.warn('⚠️ Failed to save events to sessionStorage:', error.message);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (window.capVisionEvents.length % 10 === 0) {
|
|
1291
|
+
console.log(`🟡 Captured ${window.capVisionEvents.length} events`);
|
|
1292
|
+
}
|
|
1293
|
+
},
|
|
1294
|
+
checkoutEveryNms: config.checkoutIntervalMs,
|
|
1295
|
+
recordCanvas: config.recordCanvas,
|
|
1296
|
+
maskAllInputs: config.maskAllInputs,
|
|
1297
|
+
maskInputOptions: {
|
|
1298
|
+
password: true,
|
|
1299
|
+
email: false,
|
|
1300
|
+
},
|
|
1301
|
+
collectFonts: config.collectFonts,
|
|
1302
|
+
recordCrossOriginIframes: config.recordCrossOriginIframes,
|
|
1303
|
+
plugins: plugins.length > 0 ? plugins : undefined
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
window.capVisionInitialized = true;
|
|
1307
|
+
window.capVisionLastCheckTime = Date.now();
|
|
1308
|
+
console.log('🟢 CapVision re-started');
|
|
1309
|
+
});
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
const reinitialize = async () => {
|
|
1313
|
+
try {
|
|
1314
|
+
console.log('🟡 Re-initializing...');
|
|
1315
|
+
await injectScript();
|
|
1316
|
+
await injectConsolePlugin(pluginCode);
|
|
1317
|
+
await injectNetworkPlugin(networkPluginCode);
|
|
1318
|
+
restoreEventsFromStorage();
|
|
1319
|
+
initializeEventsForFileMode();
|
|
1320
|
+
startRecorder();
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
console.error('🔴 Re-initialization failed:', error.message);
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
|
|
1326
|
+
reinitialize();
|
|
1327
|
+
}, this.config, eventCount, capVisionScriptCode, consolePluginCode, networkPluginCode);
|
|
1328
|
+
|
|
1329
|
+
console.log("🟢 Re-initialization complete");
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// Export class and default config
|
|
1334
|
+
module.exports = {
|
|
1335
|
+
CapVisionRecorder,
|
|
1336
|
+
DEFAULT_CONFIG
|
|
1337
|
+
};
|
|
1338
|
+
|