import { Page, Browser, CDPSession, BrowserContext } from 'playwright-core'; import { Socket } from "socket.io"; import { PlaywrightBlocker } from '@cliqz/adblocker-playwright'; import fetch from 'cross-fetch'; import logger from '../../logger'; import { readFileSync } from "fs"; import { InterpreterSettings } from "../../types"; import { WorkflowGenerator } from "../../workflow-management/classes/Generator"; import { WorkflowInterpreter } from "../../workflow-management/classes/Interpreter"; import { getDecryptedProxyConfig } from '../../routes/proxy'; import { getInjectableScript } from 'idcac-playwright'; import { FingerprintInjector } from "fingerprint-injector"; import { FingerprintGenerator } from "fingerprint-generator"; import { connectToRemoteBrowser } from '../browserConnection'; declare global { interface Window { rrweb?: any; isRecording?: boolean; emitEventToBackend?: (event: any) => Promise; } } // const MEMORY_CONFIG = { // gcInterval: 20000, // maxHeapSize: 1536 * 1024 * 1024, // heapUsageThreshold: 0.7 // }; /** * This class represents a remote browser instance. * It is used to allow a variety of interaction with the Playwright's browser instance. * Every remote browser holds an instance of a generator and interpreter classes with * the purpose of generating and interpreting workflows. * @category BrowserManagement */ export class RemoteBrowser { /** * Playwright's [browser](https://playwright.dev/docs/api/class-browser) instance. * @private */ private browser: Browser | null = null; private context: BrowserContext | null = null; /** * The Playwright's [CDPSession](https://playwright.dev/docs/api/class-cdpsession) instance, * used to talk raw Chrome Devtools Protocol. * @private */ private client: CDPSession | null | undefined = null; /** * Socket.io socket instance enabling communication with the client (frontend) side. * @private */ private socket: Socket; /** * The Playwright's [Page](https://playwright.dev/docs/api/class-page) instance * as current interactive remote browser's page. * @private */ private currentPage: Page | null | undefined = null; /** * Interpreter settings for any started interpretation. * @private */ private interpreterSettings: InterpreterSettings = { debug: false, maxConcurrency: 1, maxRepeats: 1, }; /** * The user ID that owns this browser instance * @private */ private userId: string; private lastEmittedUrl: string | null = null; /** * {@link WorkflowGenerator} instance specific to the remote browser. */ public generator: WorkflowGenerator; /** * {@link WorkflowInterpreter} instance specific to the remote browser. */ public interpreter: WorkflowInterpreter; public isDOMStreamingActive: boolean = false; private lastScrollPosition = { x: 0, y: 0 }; private scrollThreshold = 200; // private memoryCleanupInterval: NodeJS.Timeout | null = null; // private memoryManagementInterval: NodeJS.Timeout | null = null; /** * Initializes a new instances of the {@link Generator} and {@link WorkflowInterpreter} classes and * assigns the socket instance everywhere. * @param socket socket.io socket instance used to communicate with the client side * @constructor */ public constructor(socket: Socket, userId: string, poolId: string) { this.socket = socket; this.userId = userId; this.interpreter = new WorkflowInterpreter(socket); this.generator = new WorkflowGenerator(socket, poolId); } // private initializeMemoryManagement(): void { // this.memoryManagementInterval = setInterval(() => { // const memoryUsage = process.memoryUsage(); // const heapUsageRatio = memoryUsage.heapUsed / MEMORY_CONFIG.maxHeapSize; // if (heapUsageRatio > MEMORY_CONFIG.heapUsageThreshold * 1.2) { // logger.warn( // "Critical memory pressure detected, triggering emergency cleanup" // ); // this.performMemoryCleanup(); // } else if (heapUsageRatio > MEMORY_CONFIG.heapUsageThreshold) { // logger.warn("High memory usage detected, triggering cleanup"); // if ( // global.gc && // heapUsageRatio > MEMORY_CONFIG.heapUsageThreshold * 1.1 // ) { // global.gc(); // } // } // }, MEMORY_CONFIG.gcInterval); // } // private async performMemoryCleanup(): Promise { // if (global.gc) { // try { // global.gc(); // logger.info("Garbage collection requested"); // } catch (error) { // logger.error("Error during garbage collection:", error); // } // } // if (this.currentPage) { // try { // await new Promise((resolve) => setTimeout(resolve, 500)); // logger.info("CDP session reset completed"); // } catch (error) { // logger.error("Error resetting CDP session:", error); // } // } // this.socket.emit("memory-cleanup", { // userId: this.userId, // timestamp: Date.now(), // }); // } /** * Normalizes URLs to prevent navigation loops while maintaining consistent format */ private normalizeUrl(url: string): string { try { const parsedUrl = new URL(url); parsedUrl.pathname = parsedUrl.pathname.replace(/\/+$/, '') || '/'; parsedUrl.protocol = parsedUrl.protocol.toLowerCase(); return parsedUrl.toString(); } catch { return url; } } /** * Determines if a URL change is significant enough to emit */ private shouldEmitUrlChange(newUrl: string): boolean { if (!this.lastEmittedUrl) { return true; } const normalizedNew = this.normalizeUrl(newUrl); const normalizedLast = this.normalizeUrl(this.lastEmittedUrl); return normalizedNew !== normalizedLast; } /** * Setup scroll event listener to track user scrolling */ private setupScrollEventListener(): void { try { this.socket.removeAllListeners('dom:scroll'); } catch (error: any) { logger.warn(`Error removing old scroll listener: ${error.message}`); } this.socket.on( "dom:scroll", async (data: { deltaX: number; deltaY: number }) => { if (!this.isDOMStreamingActive || !this.currentPage) return; try { await this.currentPage.mouse.wheel(data.deltaX, data.deltaY); const scrollInfo = await this.currentPage.evaluate(() => ({ x: window.scrollX, y: window.scrollY, maxX: Math.max(0, document.documentElement.scrollWidth - window.innerWidth), maxY: Math.max(0, document.documentElement.scrollHeight - window.innerHeight), documentHeight: document.documentElement.scrollHeight, viewportHeight: window.innerHeight, })); const scrollDelta = Math.abs(scrollInfo.y - this.lastScrollPosition.y) + Math.abs(scrollInfo.x - this.lastScrollPosition.x); if (scrollDelta > this.scrollThreshold) { this.lastScrollPosition = { x: scrollInfo.x, y: scrollInfo.y }; } } catch (error) { logger.error("Error handling scroll event:", error); } } ); } private emitLoadingProgress(progress: number, pendingRequests: number): void { this.socket.emit("domLoadingProgress", { progress: Math.round(progress), pendingRequests, userId: this.userId, timestamp: Date.now(), }); } private async setupPageEventListeners(page: Page) { try { page.removeAllListeners('framenavigated'); page.removeAllListeners('load'); logger.debug('Removed existing page event listeners before re-registering'); } catch (error: any) { logger.warn(`Error removing existing page listeners: ${error.message}`); } page.on('framenavigated', async (frame) => { if (frame === page.mainFrame()) { const currentUrl = page.url(); if (this.shouldEmitUrlChange(currentUrl)) { this.lastEmittedUrl = currentUrl; this.socket.emit('urlChanged', { url: currentUrl, userId: this.userId }); } await page.evaluate(() => { if (window.rrweb && window.isRecording) { window.isRecording = false; } }); await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { logger.warn('[rrweb] Network idle timeout on navigation, proceeding with rrweb initialization'); }); await this.initializeRRWebRecording(page); } }); page.on('load', async () => { const injectScript = async (): Promise => { try { await page.waitForLoadState('networkidle', { timeout: 5000 }); if (page.isClosed()) { logger.debug('Page is closed, cannot inject script'); return false; } await page.evaluate(getInjectableScript()); return true; } catch (error: any) { logger.log('warn', `Script injection attempt failed: ${error.message}`); return false; } }; const success = await injectScript(); console.log("Script injection result:", success); }); } /** * Initialize rrweb recording for real-time DOM streaming * This replaces the snapshot-based approach with live event streaming */ private async initializeRRWebRecording(page: Page): Promise { try { const rrwebJsPath = require.resolve('rrweb/dist/rrweb.min.js'); const rrwebScriptContent = readFileSync(rrwebJsPath, 'utf8'); await page.context().addInitScript(rrwebScriptContent); await page.evaluate((scriptContent) => { if (typeof window.rrweb === 'undefined') { try { (0, eval)(scriptContent); } catch (e) { console.error('[rrweb] eval failed:', e); } } }, rrwebScriptContent); const rrwebLoaded = await page.evaluate(() => typeof window.rrweb !== 'undefined'); if (rrwebLoaded) { logger.debug('[rrweb] Script injected successfully'); } else { logger.warn('[rrweb] Script injection failed - window.rrweb not found'); } const isAlreadyExposed = await page.evaluate(() => { return typeof window.emitEventToBackend === 'function'; }); if (!isAlreadyExposed) { let hasEmittedFullSnapshot = false; await page.exposeFunction('emitEventToBackend', (event: any) => { this.socket.emit('rrweb-event', event); if (event.type === 2 && !hasEmittedFullSnapshot) { hasEmittedFullSnapshot = true; this.emitLoadingProgress(100, 0); logger.debug(`[rrweb] Full snapshot sent, loading progress at 100%`); } }); } const rrwebStatus = await page.evaluate(() => { if (!window.rrweb) { console.error('[rrweb] window.rrweb is not defined!'); return { success: false, error: 'window.rrweb is not defined' }; } if (window.isRecording) { return { success: false, error: 'already recording' }; } window.isRecording = true; try { const recordHandle = window.rrweb.record({ emit(event: any) { if (window.emitEventToBackend) { window.emitEventToBackend(event).catch(() => { }); } }, maskAllInputs: false, recordCanvas: true, input: true }); (window as any).rrwebRecordHandle = recordHandle; return { success: true }; } catch (error: any) { console.error('[rrweb] Failed to start recording:', error); return { success: false, error: error.message }; } }); if (rrwebStatus.success) { this.isDOMStreamingActive = true; this.emitLoadingProgress(80, 0); this.setupScrollEventListener(); } else { logger.error(`Failed to initialize rrweb recording: ${rrwebStatus.error}`); } } catch (error: any) { logger.error(`Failed to initialize rrweb recording: ${error.message}`); } } private getUserAgent() { const userAgents = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.140 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:117.0) Gecko/20100101 Firefox/117.0', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.1938.81 Safari/537.36 Edg/116.0.1938.81', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.96 Safari/537.36 OPR/101.0.4843.25', 'Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.62 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:118.0) Gecko/20100101 Firefox/118.0', ]; return userAgents[Math.floor(Math.random() * userAgents.length)]; } /** * Apply modern fingerprint-suite injection */ private async applyEnhancedFingerprinting(context: BrowserContext): Promise { try { try { const fingerprintGenerator = new FingerprintGenerator(); const fingerprint = fingerprintGenerator.getFingerprint(); const fingerprintInjector = new FingerprintInjector(); await fingerprintInjector.attachFingerprintToPlaywright(context as any, fingerprint); logger.info("Enhanced fingerprinting applied successfully"); } catch (fingerprintError: any) { logger.warn(`Modern fingerprint injection failed: ${fingerprintError.message}. Using existing protection.`); } } catch (error: any) { logger.error(`Enhanced fingerprinting failed: ${error.message}`); } } /** * An asynchronous constructor for asynchronously initialized properties. * Must be called right after creating an instance of RemoteBrowser class. * @param options remote browser options to be used when launching the browser * @returns {Promise} */ public initialize = async (userId: string): Promise => { const MAX_RETRIES = 3; const OVERALL_INIT_TIMEOUT = 120000; let retryCount = 0; let success = false; this.socket.emit("dom-snapshot-loading", { userId: this.userId, timestamp: Date.now(), }); this.emitLoadingProgress(0, 0); const initializationPromise = (async () => { while (!success && retryCount < MAX_RETRIES) { try { this.browser = await connectToRemoteBrowser(); if (!this.browser || this.browser.isConnected() === false) { throw new Error('Browser failed to launch or is not connected'); } this.emitLoadingProgress(20, 0); const proxyConfig = await getDecryptedProxyConfig(userId); let proxyOptions: { server: string, username?: string, password?: string } = { server: '' }; if (proxyConfig.proxy_url) { proxyOptions = { server: proxyConfig.proxy_url, ...(proxyConfig.proxy_username && proxyConfig.proxy_password && { username: proxyConfig.proxy_username, password: proxyConfig.proxy_password, }), }; } const contextOptions: any = { // viewport: { height: 400, width: 900 }, // recordVideo: { dir: 'videos/' } // Force reduced motion to prevent animation issues reducedMotion: 'reduce', // Force JavaScript to be enabled javaScriptEnabled: true, // Set a reasonable timeout timeout: 50000, // Disable hardware acceleration forcedColors: 'none', isMobile: false, hasTouch: false, userAgent: this.getUserAgent(), }; if (proxyOptions.server) { contextOptions.proxy = { server: proxyOptions.server, username: proxyOptions.username ? proxyOptions.username : undefined, password: proxyOptions.password ? proxyOptions.password : undefined, }; } await new Promise(resolve => setTimeout(resolve, 500)); const contextPromise = this.browser.newContext(contextOptions); this.context = await Promise.race([ contextPromise, new Promise((_, reject) => { setTimeout(() => reject(new Error('Context creation timed out after 15s')), 15000); }) ]) as BrowserContext; await this.applyEnhancedFingerprinting(this.context); await this.context.addInitScript( `const defaultGetter = Object.getOwnPropertyDescriptor( Navigator.prototype, "webdriver" ).get; defaultGetter.apply(navigator); defaultGetter.toString(); Object.defineProperty(Navigator.prototype, "webdriver", { set: undefined, enumerable: true, configurable: true, get: new Proxy(defaultGetter, { apply: (target, thisArg, args) => { Reflect.apply(target, thisArg, args); return false; }, }), }); const patchedGetter = Object.getOwnPropertyDescriptor( Navigator.prototype, "webdriver" ).get; patchedGetter.apply(navigator); patchedGetter.toString();` ); this.currentPage = await this.context.newPage(); this.emitLoadingProgress(40, 0); await this.setupPageEventListeners(this.currentPage); await this.currentPage.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { logger.warn('[rrweb] Network idle timeout, proceeding with rrweb initialization'); }); await this.initializeRRWebRecording(this.currentPage); try { const blocker = await PlaywrightBlocker.fromLists(fetch, ['https://easylist.to/easylist/easylist.txt']); await blocker.enableBlockingInPage(this.currentPage as any); this.client = await this.currentPage.context().newCDPSession(this.currentPage); await blocker.disableBlockingInPage(this.currentPage as any); console.log('Adblocker initialized'); } catch (error: any) { console.warn('Failed to initialize adblocker, continuing without it:', error.message); this.client = await this.currentPage.context().newCDPSession(this.currentPage); } success = true; logger.log('debug', `Browser initialized successfully for user ${userId}`); } catch (error: any) { retryCount++; logger.log('error', `Browser initialization failed (attempt ${retryCount}/${MAX_RETRIES}): ${error.message}`); if (this.browser) { try { await this.browser.close(); } catch (closeError) { logger.log('warn', `Failed to close browser during cleanup: ${closeError}`); } this.browser = null; } if (retryCount >= MAX_RETRIES) { throw new Error(`Failed to initialize browser after ${MAX_RETRIES} attempts: ${error.message}`); } await new Promise(resolve => setTimeout(resolve, 1000)); } } })(); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error(`Browser initialization timed out after ${OVERALL_INIT_TIMEOUT}ms`)), OVERALL_INIT_TIMEOUT); }); await Promise.race([initializationPromise, timeoutPromise]); }; /** * Captures a screenshot directly without running the workflow interpreter * @param settings Screenshot settings containing fullPage, type, etc. * @returns Promise */ public captureDirectScreenshot = async (settings: { fullPage: boolean; type: 'png' | 'jpeg'; timeout?: number; animations?: 'disabled' | 'allow'; caret?: 'hide' | 'initial'; scale?: 'css' | 'device'; }): Promise => { if (!this.currentPage) { logger.error("No current page available for screenshot"); this.socket.emit('screenshotError', { userId: this.userId, error: 'No active page available' }); return; } try { this.socket.emit('screenshotCaptureStarted', { userId: this.userId, fullPage: settings.fullPage }); const screenshotBuffer = await this.currentPage.screenshot({ fullPage: settings.fullPage, type: settings.type || 'png', timeout: settings.timeout || 30000, animations: settings.animations || 'allow', caret: settings.caret || 'hide', scale: settings.scale || 'device' }); const base64Data = screenshotBuffer.toString('base64'); const mimeType = `image/${settings.type || 'png'}`; const dataUrl = `data:${mimeType};base64,${base64Data}`; this.socket.emit('directScreenshotCaptured', { userId: this.userId, screenshot: dataUrl, mimeType: mimeType, fullPage: settings.fullPage, timestamp: Date.now() }); } catch (error) { logger.error('Failed to capture direct screenshot:', error); this.socket.emit('screenshotError', { userId: this.userId, error: error instanceof Error ? error.message : 'Unknown error occurred' }); } }; /** * Removes all socket event listeners */ private removeAllSocketListeners(): void { try { this.socket.removeAllListeners('captureDirectScreenshot'); this.socket.removeAllListeners('settings'); this.socket.removeAllListeners('changeTab'); this.socket.removeAllListeners('addTab'); this.socket.removeAllListeners('closeTab'); this.socket.removeAllListeners('dom:scroll'); logger.debug(`Removed all socket listeners for user ${this.userId}`); } catch (error: any) { logger.warn(`Error removing socket listeners: ${error.message}`); } } /** * Registers all event listeners needed for the recording editor session. * Should be called only once after the full initialization of the remote browser. * @returns void */ public registerEditorEvents = (): void => { logger.log("debug", `Registering editor events for user: ${this.userId}`); this.removeAllSocketListeners(); this.socket.on("captureDirectScreenshot", async (settings) => { await this.captureDirectScreenshot(settings); }); this.socket.on( "changeTab", async (tabIndex) => await this.changeTab(tabIndex) ); this.socket.on("addTab", async () => { await this.currentPage?.context().newPage(); const lastTabIndex = this.currentPage ? this.currentPage.context().pages().length - 1 : 0; await this.changeTab(lastTabIndex); }); this.socket.on("closeTab", async (tabInfo) => { const page = this.currentPage?.context().pages()[tabInfo.index]; if (page) { if (tabInfo.isCurrent) { if (this.currentPage?.context().pages()[tabInfo.index + 1]) { await this.changeTab(tabInfo.index + 1); } else { await this.changeTab(tabInfo.index - 1); } } await page.close(); } }); }; /** * Terminates the dom snapshot session and closes the remote browser. * If an interpretation was running it will be stopped. * @returns {Promise} */ public async switchOff(): Promise { this.isDOMStreamingActive = false; // if (this.memoryCleanupInterval) { // clearInterval(this.memoryCleanupInterval); // this.memoryCleanupInterval = null; // } // if (this.memoryManagementInterval) { // clearInterval(this.memoryManagementInterval); // this.memoryManagementInterval = null; // } this.removeAllSocketListeners(); try { if (this.currentPage) { const isClosed = this.currentPage.isClosed(); if (!isClosed) { this.currentPage.removeAllListeners(); logger.debug('Removed all page event listeners'); } else { logger.debug('Page already closed, skipping listener removal'); } } } catch (error: any) { logger.warn(`Error removing page listeners: ${error.message}`); } if (this.generator) { try { this.generator.cleanup(); logger.debug('Generator cleanup completed'); } catch (error: any) { logger.warn(`Error cleaning up generator: ${error.message}`); } } try { await this.interpreter.stopInterpretation(); } catch (error) { logger.error("Error stopping interpretation during shutdown:", error); } try { if (this.client && this.currentPage && !this.currentPage.isClosed()) { const detachPromise = this.client.detach(); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('CDP detach timeout')), 5000) ); await Promise.race([detachPromise, timeoutPromise]); logger.debug('CDP session detached successfully'); } } catch (error: any) { logger.warn(`Error detaching CDP session: ${error.message}`); } finally { this.client = null; } try { if (this.currentPage && !this.currentPage.isClosed()) { const closePromise = this.currentPage.close(); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Page close timeout')), 5000) ); await Promise.race([closePromise, timeoutPromise]); logger.debug('Current page closed successfully'); } } catch (error: any) { logger.warn(`Error closing current page: ${error.message}`); } finally { this.currentPage = null; } try { if (this.context) { const contextClosePromise = this.context.close(); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Context close timeout')), 5000) ); await Promise.race([contextClosePromise, timeoutPromise]); logger.debug('Browser context closed successfully'); } } catch (error: any) { logger.warn(`Error closing browser context: ${error.message}`); } finally { this.context = null; } try { if (this.browser) { const browserClosePromise = this.browser.close(); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Browser close timeout')), 5000) ); await Promise.race([browserClosePromise, timeoutPromise]); logger.debug('Browser closed successfully'); } } catch (error: any) { logger.error("Error during browser close:", error); } finally { this.browser = null; } } /** * Updates the active socket instance. * This will update all registered events for the socket and * all the properties using the socket. * @param socket socket.io socket instance used to communicate with the client side * @returns void */ public updateSocket = (socket: Socket): void => { this.socket = socket; this.registerEditorEvents(); this.generator?.updateSocket(socket); this.interpreter?.updateSocket(socket); if (this.isDOMStreamingActive) { this.setupScrollEventListener(); } }; /** * Starts the interpretation of the currently generated workflow. * @returns {Promise} */ public interpretCurrentRecording = async (): Promise => { logger.log('debug', 'Starting interpretation in the editor'); if (this.generator) { const workflow = this.generator.AddGeneratedFlags(this.generator.getWorkflowFile()); await this.initializeNewPage(); if (this.currentPage) { // this.currentPage.setViewportSize({ height: 400, width: 900 }); const params = this.generator.getParams(); if (params) { this.interpreterSettings.params = params.reduce((acc, param) => { if (this.interpreterSettings.params && Object.keys(this.interpreterSettings.params).includes(param)) { return { ...acc, [param]: this.interpreterSettings.params[param] }; } else { return { ...acc, [param]: '', } } }, {}) } logger.log('debug', `Starting interpretation with settings: ${JSON.stringify(this.interpreterSettings, null, 2)}`); await this.interpreter.interpretRecordingInEditor( workflow, this.currentPage, (newPage: Page) => this.currentPage = newPage, this.interpreterSettings ); // clear the active index from generator this.generator.clearLastIndex(); } else { logger.log('error', 'Could not get a new page, returned undefined'); } } else { logger.log('error', 'Generator is not initialized'); } }; /** * Returns the current page instance. * @returns {Page | null | undefined} */ public getCurrentPage = (): Page | null | undefined => { return this.currentPage; }; /** * Changes the active page to the page instance on the given index * available in pages array on the {@link BrowserContext}. * Automatically stops the screencast session on the previous page and starts the new one. * @param tabIndex index of the page in the pages array on the {@link BrowserContext} * @returns {Promise} */ private changeTab = async (tabIndex: number): Promise => { const page = this.currentPage?.context().pages()[tabIndex]; if (page) { this.currentPage = page; await this.setupPageEventListeners(this.currentPage); //await this.currentPage.setViewportSize({ height: 400, width: 900 }) this.client = await this.currentPage.context().newCDPSession(this.currentPage); // Include userId in the URL change event this.socket.emit('urlChanged', { url: this.currentPage.url(), userId: this.userId }); } else { logger.log('error', `${tabIndex} index out of range of pages`) } } /** * Internal method for a new page initialization. Subscribes this page to the screencast. * @param options optional page options to be used when creating a new page * @returns {Promise} */ private initializeNewPage = async (options?: Object): Promise => { const newPage = options ? await this.browser?.newPage(options) : await this.browser?.newPage(); await newPage?.setExtraHTTPHeaders({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3' }); await this.currentPage?.close(); this.currentPage = newPage; if (this.currentPage) { await this.setupPageEventListeners(this.currentPage); logger.debug('Using rrweb live recording for new page'); } else { logger.log('error', 'Could not get a new page, returned undefined'); } }; }