@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.
@@ -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
+