From 3ccf58cdced73890dc2361d8e8adeaac57fa0a2a Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Mon, 30 Jun 2025 15:09:24 +0530 Subject: [PATCH 01/13] feat: inject rrweb in browser context --- .../src/browser-management/classes/RemoteBrowser.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 277760fa..9a7b4ec8 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -670,7 +670,7 @@ export class RemoteBrowser { }; return { - snapshot: processedSnapshot, + snapshot: snapshot, resources, baseUrl, viewport, @@ -1253,11 +1253,11 @@ export class RemoteBrowser { patchedGetter.apply(navigator); patchedGetter.toString();` ); - - this.currentPage = await this.context.newPage(); await this.currentPage.addInitScript({ path: './server/src/browser-management/classes/rrweb-bundle.js' }); + this.currentPage = await this.context.newPage(); + await this.setupPageEventListeners(this.currentPage); const viewportSize = await this.currentPage.viewportSize(); @@ -2221,7 +2221,10 @@ export class RemoteBrowser { if (typeof window.rrwebSnapshot === "undefined") { throw new Error("rrweb-snapshot library not available"); } - return window.rrwebSnapshot.snapshot(document); + return window.rrwebSnapshot.snapshot(document, { + inlineImages: true, + collectFonts: true, + }); }); // Process the snapshot to proxy resources @@ -2479,8 +2482,6 @@ export class RemoteBrowser { await this.currentPage?.close(); this.currentPage = newPage; if (this.currentPage) { - await this.currentPage.addInitScript({ path: './server/src/browser-management/classes/rrweb-bundle.js' }); - await this.setupPageEventListeners(this.currentPage); this.client = await this.currentPage.context().newCDPSession(this.currentPage); From a770d69094167910440ffabeb6066e003a2e868e Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Mon, 30 Jun 2025 15:10:50 +0530 Subject: [PATCH 02/13] feat: simplify process snapshot logic --- .../classes/RemoteBrowser.ts | 177 +----------------- 1 file changed, 1 insertion(+), 176 deletions(-) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 9a7b4ec8..be424cb2 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -488,182 +488,7 @@ export class RemoteBrowser { scripts: [] as Array<{ src: string; content: string; type?: string }>, media: [] as Array<{ src: string; dataUrl: string; type: string }>, }; - - const processNode = (node: RRWebSnapshot): RRWebSnapshot => { - const processedNode = { ...node }; - - // Process attributes if they exist - if (node.attributes) { - const newAttributes = { ...node.attributes }; - - // Process common attributes that contain URLs - const urlAttributes = ["src", "href", "data", "poster", "background"]; - - for (const attr of urlAttributes) { - if (newAttributes[attr]) { - const originalUrl = newAttributes[attr]; - - // Categorize and collect the resource instead of proxying - const lowerAttr = attr.toLowerCase(); - const lowerUrl = originalUrl.toLowerCase(); - - if (lowerAttr === "src" && node.tagName?.toLowerCase() === "img") { - const cachedResource = this.networkResourceCache.get(originalUrl); - if ( - cachedResource && - cachedResource.mimeType.includes("image/") - ) { - const dataUrl = cachedResource.base64Encoded - ? `data:${cachedResource.mimeType};base64,${cachedResource.content}` - : `data:${cachedResource.mimeType};base64,${Buffer.from( - cachedResource.content, - "utf-8" - ).toString("base64")}`; - - resources.images.push({ - src: originalUrl, - dataUrl, - alt: newAttributes.alt, - }); - } - } else if ( - lowerAttr === "href" && - node.tagName?.toLowerCase() === "link" - ) { - const rel = newAttributes.rel?.toLowerCase() || ""; - - if (rel.includes("stylesheet")) { - const cachedResource = - this.networkResourceCache.get(originalUrl); - if (cachedResource && cachedResource.mimeType.includes("css")) { - let content = cachedResource.base64Encoded - ? Buffer.from(cachedResource.content, "base64").toString( - "utf-8" - ) - : cachedResource.content; - - // Process CSS to collect embedded resources - content = this.processCSS( - content, - originalUrl, - baseUrl, - resources - ); - - resources.stylesheets.push({ - href: originalUrl, - content, - media: newAttributes.media || "all", - }); - } - } else if ( - rel.includes("font") || - lowerUrl.match(/\.(woff2?|ttf|otf|eot)(\?.*)?$/i) - ) { - const cachedResource = - this.networkResourceCache.get(originalUrl); - if (cachedResource) { - const dataUrl = cachedResource.base64Encoded - ? `data:${cachedResource.mimeType};base64,${cachedResource.content}` - : `data:${cachedResource.mimeType};base64,${Buffer.from( - cachedResource.content, - "utf-8" - ).toString("base64")}`; - - resources.fonts.push({ - url: originalUrl, - dataUrl, - format: lowerUrl.split(".").pop()?.split("?")[0], - }); - } - } - } - } - } - - // Process srcset attribute - collect resources but keep original URLs - if (newAttributes.srcset) { - const originalSrcset = newAttributes.srcset; - originalSrcset.split(",").forEach((srcsetItem) => { - const parts = srcsetItem.trim().split(/\s+/); - const url = parts[0]; - - if (url && !url.startsWith("data:") && !url.startsWith("blob:")) { - const cachedResource = this.networkResourceCache.get(url); - if ( - cachedResource && - cachedResource.mimeType.includes("image/") - ) { - const dataUrl = cachedResource.base64Encoded - ? `data:${cachedResource.mimeType};base64,${cachedResource.content}` - : `data:${cachedResource.mimeType};base64,${Buffer.from( - cachedResource.content, - "utf-8" - ).toString("base64")}`; - - resources.images.push({ - src: url, - dataUrl, - alt: newAttributes.alt, - }); - } - } - }); - } - - processedNode.attributes = newAttributes; - } - - // Process text content for style elements - if (node.tagName?.toLowerCase() === "style" && node.textContent) { - let content = node.textContent; - - // Process CSS content to collect embedded resources - content = this.processCSS(content, "", baseUrl, resources); - processedNode.textContent = content; - } - - // Recursively process child nodes - if (node.childNodes) { - processedNode.childNodes = node.childNodes.map(processNode); - } - - return processedNode; - }; - - const processedSnapshot = processNode(snapshot); - - // Add cached scripts and media - for (const [url, resource] of this.networkResourceCache.entries()) { - if (resource.mimeType.toLowerCase().includes("javascript")) { - const content = resource.base64Encoded - ? Buffer.from(resource.content, "base64").toString("utf-8") - : resource.content; - - resources.scripts.push({ - src: url, - content, - type: "text/javascript", - }); - } else if ( - resource.mimeType.toLowerCase().includes("video/") || - resource.mimeType.toLowerCase().includes("audio/") - ) { - const dataUrl = resource.base64Encoded - ? `data:${resource.mimeType};base64,${resource.content}` - : `data:${resource.mimeType};base64,${Buffer.from( - resource.content, - "utf-8" - ).toString("base64")}`; - - resources.media.push({ - src: url, - dataUrl, - type: resource.mimeType, - }); - } - } - + const viewport = (await this.currentPage?.viewportSize()) || { width: 1280, height: 720, From d44892de94670bb6234d099cc3ddaa6c814f156c Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Mon, 30 Jun 2025 15:14:42 +0530 Subject: [PATCH 03/13] feat: remove network monitoring --- .../classes/RemoteBrowser.ts | 191 +----------------- 1 file changed, 1 insertion(+), 190 deletions(-) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index be424cb2..12ad89c7 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -80,7 +80,6 @@ interface ProcessedSnapshot { scripts: number; media: number; }; - totalCacheSize: number; }; } @@ -199,36 +198,6 @@ export class RemoteBrowser { private snapshotDebounceTimeout: NodeJS.Timeout | null = null; private isScrollTriggeredSnapshot = false; - /** - * Cache for network resources captured via CDP - * @private - */ - private networkResourceCache: Map< - string, - { - url: string; - content: string; - mimeType: string; - base64Encoded: boolean; - timestamp: number; - resourceType?: string; - statusCode?: number; - headers?: Record; - } - > = new Map(); - - /** - * Set to track active network requests - * @private - */ - private activeRequests: Set = new Set(); - - /** - * Flag to indicate if network monitoring is active - * @private - */ - private isNetworkMonitoringActive: boolean = false; - /** * Initializes a new instances of the {@link Generator} and {@link WorkflowInterpreter} classes and * assigns the socket instance everywhere. @@ -515,7 +484,6 @@ export class RemoteBrowser { scripts: resources.scripts.length, media: resources.media.length, }, - totalCacheSize: this.networkResourceCache.size, }, }; } @@ -640,121 +608,6 @@ export class RemoteBrowser { ); } - /** - * Initialize network monitoring via CDP to capture all resources - * @private - */ - private async initializeNetworkMonitoring(): Promise { - if (!this.client || this.isNetworkMonitoringActive) { - return; - } - - try { - await this.client.send("Network.enable"); - await this.client.send("Runtime.enable"); - await this.client.send("Page.enable"); - - await this.client.send("Network.setRequestInterception", { - patterns: [ - { urlPattern: "*", resourceType: "Stylesheet" }, - { urlPattern: "*", resourceType: "Image" }, - { urlPattern: "*", resourceType: "Font" }, - { urlPattern: "*", resourceType: "Script" }, - { urlPattern: "*", resourceType: "Media" }, - { urlPattern: "*", resourceType: "Document" }, - { urlPattern: "*", resourceType: "Manifest" }, - { urlPattern: "*", resourceType: "Other" }, - ], - }); - - this.isNetworkMonitoringActive = true; - logger.info("Enhanced network monitoring enabled via CDP"); - - this.client.on( - "Network.responseReceived", - async ({ requestId, response, type }) => { - const mimeType = response.mimeType?.toLowerCase() || ""; - const url = response.url; - const resourceType = type; - - logger.debug( - `Resource received: ${resourceType} - ${mimeType} - ${url}` - ); - - if (this.shouldCacheResource(mimeType, url)) { - this.activeRequests.add(requestId); - - try { - const { body, base64Encoded } = await this.client!.send( - "Network.getResponseBody", - { requestId } - ); - - this.networkResourceCache.set(url, { - url, - content: body, - mimeType: response.mimeType || "application/octet-stream", - base64Encoded, - timestamp: Date.now(), - resourceType, - statusCode: response.status, - headers: response.headers, - }); - - logger.debug( - `Cached ${resourceType} resource: ${url} (${mimeType})` - ); - } catch (error) { - logger.warn( - `Failed to capture ${resourceType} resource body for ${url}:`, - error - ); - } finally { - this.activeRequests.delete(requestId); - } - } - } - ); - - this.client.on( - "Network.requestIntercepted", - async ({ interceptionId, request }) => { - try { - await this.client!.send("Network.continueInterceptedRequest", { - interceptionId, - }); - logger.debug(`Request intercepted and continued: ${request.url}`); - } catch (error) { - logger.warn( - `Failed to continue intercepted request for ${request.url}:`, - error - ); - } - } - ); - - this.client.on( - "Network.loadingFailed", - ({ requestId, errorText, type }) => { - this.activeRequests.delete(requestId); - logger.debug(`Network request failed (${type}): ${errorText}`); - } - ); - - this.client.on("Network.loadingFinished", ({ requestId }) => { - this.activeRequests.delete(requestId); - }); - - // Clean up cache periodically - setInterval(() => { - this.cleanupResourceCache(); - }, 60000); - } catch (error) { - logger.error("Failed to initialize enhanced network monitoring:", error); - this.isNetworkMonitoringActive = false; - } - } - private initializeMemoryManagement(): void { setInterval(() => { const memoryUsage = process.memoryUsage(); @@ -1944,45 +1797,7 @@ export class RemoteBrowser { this.isDOMStreamingActive = false; } } - - /** - * Wait for network requests to become idle - * @private - */ - private async waitForNetworkIdle(timeout: number = 2000): Promise { - const startTime = Date.now(); - - while (this.activeRequests.size > 0 && Date.now() - startTime < timeout) { - await new Promise((resolve) => setTimeout(resolve, 100)); - } - - if (this.activeRequests.size > 0) { - logger.debug( - `Network idle timeout reached with ${this.activeRequests.size} pending requests` - ); - } - } - - /** - * Stop network monitoring - * @private - */ - private async stopNetworkMonitoring(): Promise { - if (!this.client || !this.isNetworkMonitoringActive) { - return; - } - - try { - await this.client.send("Network.disable"); - this.isNetworkMonitoringActive = false; - this.networkResourceCache.clear(); - this.activeRequests.clear(); - logger.info("Network monitoring stopped"); - } catch (error) { - logger.error("Error stopping network monitoring:", error); - } - } - + /** * CDP-based DOM snapshot creation using captured network resources */ @@ -2001,9 +1816,6 @@ export class RemoteBrowser { return; } - // Wait for network to become idle - await this.waitForNetworkIdle(); - // Double-check page state after network wait if (this.currentPage.isClosed()) { logger.debug("Skipping DOM snapshot - page closed during network wait"); @@ -2137,7 +1949,6 @@ export class RemoteBrowser { if (this.client) { await this.stopScreencast(); await this.stopDOM(); - await this.stopNetworkMonitoring(); } if (this.browser) { From 830bf15c9d0f8ee12da0e472136696be6d79dde4 Mon Sep 17 00:00:00 2001 From: Karishma Shukla Date: Mon, 30 Jun 2025 15:16:10 +0530 Subject: [PATCH 04/13] feat: rerender only on dom content loaded --- .../classes/RemoteBrowser.ts | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 12ad89c7..2ae24d04 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -769,18 +769,19 @@ export class RemoteBrowser { await this.makeAndEmitDOMSnapshot(); }); - this.currentPage.on("response", async (response) => { - const url = response.url(); - if ( - response.request().resourceType() === "document" || - url.includes("api/") || - url.includes("ajax") - ) { - setTimeout(async () => { - await this.makeAndEmitDOMSnapshot(); - }, 800); - } - }); + // DO NOT REMOVE THIS CODE - MIGHT BE NEEDED LATER + // this.currentPage.on("response", async (response) => { + // const url = response.url(); + // if ( + // response.request().resourceType() === "document" || + // url.includes("api/") || + // url.includes("ajax") + // ) { + // setTimeout(async () => { + // await this.makeAndEmitDOMSnapshot(); + // }, 800); + // } + // }); } private async setupPageEventListeners(page: Page) { From f17c7299e35ce0b718a2983cc4c891fa016aae7c Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 30 Jun 2025 16:13:54 +0530 Subject: [PATCH 05/13] feat: rm network caching logic --- .../classes/RemoteBrowser.ts | 344 +----------------- 1 file changed, 5 insertions(+), 339 deletions(-) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 2ae24d04..52281fd3 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -198,6 +198,10 @@ export class RemoteBrowser { private snapshotDebounceTimeout: NodeJS.Timeout | null = null; private isScrollTriggeredSnapshot = false; + private networkRequestTimeout: NodeJS.Timeout | null = null; + private pendingNetworkRequests: string[] = []; + private readonly NETWORK_QUIET_PERIOD = 8000; + /** * Initializes a new instances of the {@link Generator} and {@link WorkflowInterpreter} classes and * assigns the socket instance everywhere. @@ -223,224 +227,6 @@ export class RemoteBrowser { }, 30000); // Every 30 seconds } - private processCSS( - cssContent: string, - cssUrl: string, - baseUrl: string, - resources?: any - ): string { - try { - let processedContent = cssContent; - - logger.debug(`Processing CSS from: ${cssUrl}`); - - // Process @font-face declarations and collect font resources - processedContent = processedContent.replace( - /@font-face\s*\{([^}]*)\}/gi, - (fontFaceMatch, fontFaceContent) => { - let newFontFaceContent = fontFaceContent; - - logger.debug( - `Processing @font-face block: ${fontFaceContent.substring( - 0, - 100 - )}...` - ); - - newFontFaceContent = newFontFaceContent.replace( - /src\s*:\s*([^;}]+)[;}]/gi, - (srcMatch: any, srcValue: any) => { - let newSrcValue = srcValue; - - newSrcValue = newSrcValue.replace( - /url\s*\(\s*['"]?([^'")]+)['"]?\s*\)(\s*format\s*\(\s*['"]?[^'")]*['"]?\s*\))?/gi, - (urlMatch: any, url: string, formatPart: any) => { - const originalUrl = url.trim(); - - logger.debug(`Found font URL in @font-face: ${originalUrl}`); - - if ( - originalUrl.startsWith("data:") || - originalUrl.startsWith("blob:") - ) { - return urlMatch; - } - - try { - let absoluteUrl: string; - try { - absoluteUrl = new URL(originalUrl).href; - } catch (e) { - absoluteUrl = new URL(originalUrl, cssUrl || baseUrl) - .href; - } - - const cachedResource = - this.networkResourceCache.get(absoluteUrl); - if (cachedResource && resources) { - const dataUrl = cachedResource.base64Encoded - ? `data:${cachedResource.mimeType};base64,${cachedResource.content}` - : `data:${cachedResource.mimeType};base64,${Buffer.from( - cachedResource.content, - "utf-8" - ).toString("base64")}`; - - resources.fonts.push({ - url: absoluteUrl, - dataUrl, - format: originalUrl.split(".").pop()?.split("?")[0], - }); - } - - // Keep original URL in CSS - return urlMatch; - } catch (e) { - logger.warn( - "Failed to process font URL in @font-face:", - originalUrl, - e - ); - return urlMatch; - } - } - ); - - return `src: ${newSrcValue};`; - } - ); - - return `@font-face {${newFontFaceContent}}`; - } - ); - - // Process other url() references and collect resources - processedContent = processedContent.replace( - /url\s*\(\s*['"]?([^'")]+)['"]?\s*\)/gi, - (match, url) => { - const originalUrl = url.trim(); - - if ( - originalUrl.startsWith("data:") || - originalUrl.startsWith("blob:") - ) { - return match; - } - - try { - let absoluteUrl: string; - try { - absoluteUrl = new URL(originalUrl).href; - } catch (e) { - absoluteUrl = new URL(originalUrl, cssUrl || baseUrl).href; - } - - const cachedResource = this.networkResourceCache.get(absoluteUrl); - if (cachedResource && resources) { - const lowerMimeType = cachedResource.mimeType.toLowerCase(); - - if (lowerMimeType.includes("image/")) { - const dataUrl = cachedResource.base64Encoded - ? `data:${cachedResource.mimeType};base64,${cachedResource.content}` - : `data:${cachedResource.mimeType};base64,${Buffer.from( - cachedResource.content, - "utf-8" - ).toString("base64")}`; - - resources.images.push({ - src: absoluteUrl, - dataUrl, - alt: "", - }); - } else if ( - lowerMimeType.includes("font/") || - lowerMimeType.includes("application/font") - ) { - const dataUrl = cachedResource.base64Encoded - ? `data:${cachedResource.mimeType};base64,${cachedResource.content}` - : `data:${cachedResource.mimeType};base64,${Buffer.from( - cachedResource.content, - "utf-8" - ).toString("base64")}`; - - resources.fonts.push({ - url: absoluteUrl, - dataUrl, - format: originalUrl.split(".").pop()?.split("?")[0], - }); - } - } - - // Keep original URL in CSS - return match; - } catch (e) { - logger.warn(`Failed to process CSS URL: ${originalUrl}`, e); - return match; - } - } - ); - - // Process @import statements and collect stylesheets - processedContent = processedContent.replace( - /@import\s+(?:url\s*\(\s*)?['"]?([^'")]+)['"]?\s*\)?([^;]*);?/gi, - (match, url, mediaQuery) => { - const originalUrl = url.trim(); - - if ( - originalUrl.startsWith("data:") || - originalUrl.startsWith("blob:") - ) { - return match; - } - - try { - let absoluteUrl: string; - try { - absoluteUrl = new URL(originalUrl).href; - } catch (e) { - absoluteUrl = new URL(originalUrl, cssUrl || baseUrl).href; - } - - const cachedResource = this.networkResourceCache.get(absoluteUrl); - if ( - cachedResource && - resources && - cachedResource.mimeType.includes("css") - ) { - const content = cachedResource.base64Encoded - ? Buffer.from(cachedResource.content, "base64").toString( - "utf-8" - ) - : cachedResource.content; - - resources.stylesheets.push({ - href: absoluteUrl, - content: this.processCSS( - content, - absoluteUrl, - baseUrl, - resources - ), - media: mediaQuery ? mediaQuery.trim() : "all", - }); - } - - // Keep original @import - return match; - } catch (e) { - logger.warn(`Failed to process CSS @import: ${originalUrl}`, e); - return match; - } - } - ); - - logger.debug(`CSS processing completed for: ${cssUrl}`); - return processedContent; - } catch (error) { - logger.error("Failed to process CSS content:", error); - return cssContent; // Return original content if processing fails - } - } - private async processRRWebSnapshot( snapshot: RRWebSnapshot ): Promise { @@ -464,7 +250,7 @@ export class RemoteBrowser { }; return { - snapshot: snapshot, + snapshot, resources, baseUrl, viewport, @@ -488,126 +274,6 @@ export class RemoteBrowser { }; } - /** - * Check if a resource should be cached based on its MIME type and URL - * @private - */ - private shouldCacheResource(mimeType: string, url: string): boolean { - const lowerMimeType = mimeType.toLowerCase(); - const lowerUrl = url.toLowerCase(); - - // CSS Resources - if ( - lowerMimeType.includes("text/css") || - lowerMimeType.includes("application/css") || - lowerUrl.endsWith(".css") - ) { - return true; - } - - // Font Resources - if ( - lowerMimeType.includes("font/") || - lowerMimeType.includes("application/font") || - lowerMimeType.includes("application/x-font") || - lowerUrl.match(/\.(woff2?|ttf|otf|eot)(\?.*)?$/) - ) { - return true; - } - - // Image Resources - if ( - lowerMimeType.includes("image/") || - lowerUrl.match(/\.(jpg|jpeg|png|gif|webp|svg|ico|bmp|tiff|avif)(\?.*)?$/) - ) { - return true; - } - - // JavaScript Resources - if ( - lowerMimeType.includes("javascript") || - lowerMimeType.includes("text/js") || - lowerMimeType.includes("application/js") || - lowerUrl.match(/\.js(\?.*)?$/) - ) { - return true; - } - - // Media Resources - if ( - lowerMimeType.includes("video/") || - lowerMimeType.includes("audio/") || - lowerUrl.match( - /\.(mp4|webm|ogg|avi|mov|wmv|flv|mp3|wav|m4a|aac|flac)(\?.*)?$/ - ) - ) { - return true; - } - - // Document Resources - if ( - lowerMimeType.includes("application/pdf") || - lowerMimeType.includes("application/msword") || - lowerMimeType.includes("application/vnd.ms-") || - lowerMimeType.includes("application/vnd.openxmlformats-") || - lowerUrl.match(/\.(pdf|doc|docx|xls|xlsx|ppt|pptx)(\?.*)?$/) - ) { - return true; - } - - // Manifest and Icon Resources - if ( - lowerMimeType.includes("application/manifest+json") || - lowerUrl.includes("manifest.json") || - lowerUrl.includes("browserconfig.xml") - ) { - return true; - } - - // SVG Resources (can be images or fonts) - if (lowerMimeType.includes("image/svg+xml") || lowerUrl.endsWith(".svg")) { - return true; - } - - // Other common web resources - if ( - lowerMimeType.includes("application/octet-stream") && - lowerUrl.match(/\.(woff2?|ttf|otf|eot|css|js)(\?.*)?$/) - ) { - return true; - } - - return false; - } - - /** - * Clean up old cached resources to prevent memory leaks - * @private - */ - private cleanupResourceCache(): void { - const now = Date.now(); - const maxAge = 5 * 60 * 1000; // 5 minutes - - for (const [url, resource] of this.networkResourceCache.entries()) { - if (now - resource.timestamp > maxAge) { - this.networkResourceCache.delete(url); - } - } - - if (this.networkResourceCache.size > 200) { - const entries = Array.from(this.networkResourceCache.entries()); - entries.sort((a, b) => a[1].timestamp - b[1].timestamp); - - for (let i = 0; i < 50; i++) { - this.networkResourceCache.delete(entries[i][0]); - } - } - - logger.debug( - `Resource cache cleaned up. Current size: ${this.networkResourceCache.size}` - ); - } - private initializeMemoryManagement(): void { setInterval(() => { const memoryUsage = process.memoryUsage(); From eecfa4319896219f4b1b9eadc0dc14ae46c5ad1a Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 30 Jun 2025 16:15:52 +0530 Subject: [PATCH 06/13] feat: rerender on network requests --- .../classes/RemoteBrowser.ts | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 52281fd3..74f86dfe 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -435,19 +435,36 @@ export class RemoteBrowser { await this.makeAndEmitDOMSnapshot(); }); - // DO NOT REMOVE THIS CODE - MIGHT BE NEEDED LATER - // this.currentPage.on("response", async (response) => { - // const url = response.url(); - // if ( - // response.request().resourceType() === "document" || - // url.includes("api/") || - // url.includes("ajax") - // ) { - // setTimeout(async () => { - // await this.makeAndEmitDOMSnapshot(); - // }, 800); - // } - // }); + this.currentPage.on("response", async (response) => { + const url = response.url(); + if ( + response.request().resourceType() === "document" || + url.includes("api/") || + url.includes("ajax") + ) { + this.pendingNetworkRequests.push(url); + + if (this.networkRequestTimeout) { + clearTimeout(this.networkRequestTimeout); + this.networkRequestTimeout = null; + } + + logger.debug( + `Network request received: ${url}. Total pending: ${this.pendingNetworkRequests.length}` + ); + + this.networkRequestTimeout = setTimeout(async () => { + logger.info( + `Network quiet period reached. Processing ${this.pendingNetworkRequests.length} requests` + ); + + this.pendingNetworkRequests = []; + this.networkRequestTimeout = null; + + await this.makeAndEmitDOMSnapshot(); + }, this.NETWORK_QUIET_PERIOD); + } + }); } private async setupPageEventListeners(page: Page) { From c9249965b86d63e7b377c4d367bcac97dece4675 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 30 Jun 2025 16:18:31 +0530 Subject: [PATCH 07/13] feat: inject rrweb script in context --- .../browser-management/classes/RemoteBrowser.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 74f86dfe..9cbf897d 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -616,7 +616,9 @@ export class RemoteBrowser { patchedGetter.toString();` ); - await this.currentPage.addInitScript({ path: './server/src/browser-management/classes/rrweb-bundle.js' }); + await this.context.addInitScript({ + path: "./browser-management/classes/rrweb-bundle.js", + }); this.currentPage = await this.context.newPage(); @@ -637,20 +639,10 @@ export class RemoteBrowser { this.client = await this.currentPage.context().newCDPSession(this.currentPage); await blocker.disableBlockingInPage(this.currentPage); console.log('Adblocker initialized'); - - if (this.client) { - await this.initializeNetworkMonitoring(); - logger.info("Network monitoring initialized successfully"); - } } catch (error: any) { console.warn('Failed to initialize adblocker, continuing without it:', error.message); // Still need to set up the CDP session even if blocker fails this.client = await this.currentPage.context().newCDPSession(this.currentPage); - - if (this.client) { - await this.initializeNetworkMonitoring(); - logger.info("Network monitoring initialized successfully"); - } } success = true; From e0020437f5172df9289fba7724921bd0cbb08ece Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 30 Jun 2025 16:19:46 +0530 Subject: [PATCH 08/13] feat: add page closed check --- server/src/browser-management/classes/RemoteBrowser.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 9cbf897d..675dfc6d 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -705,6 +705,11 @@ export class RemoteBrowser { }>, limit: number = 5 ): Promise>> { + if (page.isClosed()) { + logger.warn("Page is closed, cannot extract list data"); + return []; + } + return await page.evaluate( async ({ listSelector, fields, limit }: { listSelector: string; From 0b765ff756144f6a46572764f7f30e53ff11b734 Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 30 Jun 2025 16:23:05 +0530 Subject: [PATCH 09/13] feat: clear net timeout and update interval --- .../src/browser-management/classes/RemoteBrowser.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 675dfc6d..5fe1f249 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -1599,6 +1599,13 @@ export class RemoteBrowser { this.domUpdateInterval = null; } + if (this.networkRequestTimeout) { + clearTimeout(this.networkRequestTimeout); + this.networkRequestTimeout = null; + } + + this.pendingNetworkRequests = []; + if (this.client) { try { await this.client.send("DOM.disable"); @@ -1627,6 +1634,10 @@ export class RemoteBrowser { clearInterval(this.screencastInterval); } + if (this.domUpdateInterval) { + clearInterval(this.domUpdateInterval); + } + if (this.client) { await this.stopScreencast(); await this.stopDOM(); From 166b676b22cd98e610a096f4d6b4da6216677f2b Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 30 Jun 2025 16:29:07 +0530 Subject: [PATCH 10/13] fix: rrweb inject path --- server/src/browser-management/classes/RemoteBrowser.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 5fe1f249..e5c974da 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -616,9 +616,7 @@ export class RemoteBrowser { patchedGetter.toString();` ); - await this.context.addInitScript({ - path: "./browser-management/classes/rrweb-bundle.js", - }); + await this.context.addInitScript({ path: './server/src/browser-management/classes/rrweb-bundle.js' }); this.currentPage = await this.context.newPage(); @@ -1618,7 +1616,7 @@ export class RemoteBrowser { logger.info("DOM streaming stopped successfully"); } - /** + /**rrweb-bundle * Terminates the screencast session and closes the remote browser. * If an interpretation was running it will be stopped. * @returns {Promise} @@ -1776,8 +1774,6 @@ export class RemoteBrowser { await this.stopScreencast(); this.currentPage = page; - await this.currentPage.addInitScript({ path: './server/src/browser-management/classes/rrweb-bundle.js' }); - await this.setupPageEventListeners(this.currentPage); //await this.currentPage.setViewportSize({ height: 400, width: 900 }) From 5d8436d95bf42de50cc12b29a1c4218e76724e7f Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 30 Jun 2025 16:31:00 +0530 Subject: [PATCH 11/13] feat: simplify dom browser rendering --- .../recorder/DOMBrowserRenderer.tsx | 110 ++++-------------- 1 file changed, 24 insertions(+), 86 deletions(-) diff --git a/src/components/recorder/DOMBrowserRenderer.tsx b/src/components/recorder/DOMBrowserRenderer.tsx index a7cc2d26..82c93d92 100644 --- a/src/components/recorder/DOMBrowserRenderer.tsx +++ b/src/components/recorder/DOMBrowserRenderer.tsx @@ -790,96 +790,34 @@ export const DOMBrowserRenderer: React.FC = ({ rebuiltHTML = "\n" + rebuiltHTML; - const additionalCSS = []; + const minimalCSS = ` + + `; - if (snapshotData.resources.fonts?.length > 0) { - const fontCSS = snapshotData.resources.fonts - .map((font) => { - const format = font.format || "woff2"; - return ` - @font-face { - font-family: 'ProxiedFont-${ - font.url.split("/").pop()?.split(".")[0] || - "unknown" - }'; - src: url("${font.dataUrl}") format("${format}"); - font-display: swap; - } - `; - }) - .join("\n"); - additionalCSS.push(fontCSS); - } - - if (snapshotData.resources.stylesheets?.length > 0) { - const externalCSS = snapshotData.resources.stylesheets - .map((stylesheet) => stylesheet.content) - .join("\n\n"); - additionalCSS.push(externalCSS); - } - - const enhancedCSS = ` - /* rrweb rebuilt content styles */ - html, body { - margin: 0 !important; - padding: 8px !important; - font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif !important; - background: white !important; - overflow-x: hidden !important; - } - - html::-webkit-scrollbar, - body::-webkit-scrollbar { - display: none !important; - width: 0 !important; - height: 0 !important; - background: transparent !important; - } - - /* Hide scrollbars for all elements */ - *::-webkit-scrollbar { - display: none !important; - width: 0 !important; - height: 0 !important; - background: transparent !important; - } - - * { - scrollbar-width: none !important; /* Firefox */ - -ms-overflow-style: none !important; /* Internet Explorer 10+ */ - } - - img { - max-width: 100% !important; - height: auto !important; - } - - /* Make everything interactive */ - * { - cursor: "pointer" !important; - } - - /* Additional CSS from resources */ - ${additionalCSS.join("\n\n")} - `; - - const headTagRegex = /]*>/i; - const cssInjection = ` - - - - - `; - - if (headTagRegex.test(rebuiltHTML)) { + if (rebuiltHTML.includes("")) { rebuiltHTML = rebuiltHTML.replace( - headTagRegex, - `${cssInjection}` + "", + `${minimalCSS}` ); - } else { + } else if (rebuiltHTML.includes("")) { rebuiltHTML = rebuiltHTML.replace( - /]*>/i, - `${cssInjection}` + "", + `${minimalCSS}` ); } From ae525894a476b271abdcd96711ea892df00de23d Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 30 Jun 2025 16:36:37 +0530 Subject: [PATCH 12/13] feat: navigate on dom content load --- server/src/browser-management/inputHandlers.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/browser-management/inputHandlers.ts b/server/src/browser-management/inputHandlers.ts index b0be33e6..602807ae 100644 --- a/server/src/browser-management/inputHandlers.ts +++ b/server/src/browser-management/inputHandlers.ts @@ -492,7 +492,8 @@ const handleChangeUrl = async (activeBrowser: RemoteBrowser, page: Page, url: st await generator.onChangeUrl(url, page); try { - await page.goto(url, { waitUntil: "networkidle", timeout: 100000 }); + await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 }); + await page.waitForTimeout(2000); logger.log("debug", `Went to ${url}`); } catch (e) { const { message } = e as Error; From 98770e3abc4bb892a7a36b0d25b77e381ba9e32b Mon Sep 17 00:00:00 2001 From: Rohit Date: Mon, 30 Jun 2025 21:10:06 +0530 Subject: [PATCH 13/13] feat: add enhance css for iframe --- .../recorder/DOMBrowserRenderer.tsx | 82 ++++++++++++------- 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/src/components/recorder/DOMBrowserRenderer.tsx b/src/components/recorder/DOMBrowserRenderer.tsx index 82c93d92..213488fd 100644 --- a/src/components/recorder/DOMBrowserRenderer.tsx +++ b/src/components/recorder/DOMBrowserRenderer.tsx @@ -296,23 +296,23 @@ export const DOMBrowserRenderer: React.FC = ({ if (element) { setCurrentHighlight({ element, - rect: rect, + rect: rect, selector, elementInfo: { ...elementInfo, tagName: elementInfo?.tagName ?? "", isDOMMode: true, }, - childSelectors, + childSelectors, }); if (onHighlight) { onHighlight({ - rect: rect, + rect: rect, elementInfo: { ...elementInfo, tagName: elementInfo?.tagName ?? "", - isDOMMode: true, + isDOMMode: true, }, selector, childSelectors, @@ -670,8 +670,8 @@ export const DOMBrowserRenderer: React.FC = ({ if (socket) { socket.emit("dom:scroll", { deltaX, - deltaY - }) + deltaY, + }); } notifyLastAction("scroll"); } @@ -790,34 +790,58 @@ export const DOMBrowserRenderer: React.FC = ({ rebuiltHTML = "\n" + rebuiltHTML; - const minimalCSS = ` - + const enhancedCSS = ` + /* rrweb rebuilt content styles */ + html, body { + margin: 0 !important; + padding: 8px !important; + overflow-x: hidden !important; + } + + html::-webkit-scrollbar, + body::-webkit-scrollbar { + display: none !important; + width: 0 !important; + height: 0 !important; + background: transparent !important; + } + + /* Hide scrollbars for all elements */ + *::-webkit-scrollbar { + display: none !important; + width: 0 !important; + height: 0 !important; + background: transparent !important; + } + + * { + scrollbar-width: none !important; /* Firefox */ + -ms-overflow-style: none !important; /* Internet Explorer 10+ */ + } + + /* Make everything interactive */ + * { + cursor: "pointer" !important; + } `; - if (rebuiltHTML.includes("")) { + const headTagRegex = /]*>/i; + const cssInjection = ` + + + + + `; + + if (headTagRegex.test(rebuiltHTML)) { rebuiltHTML = rebuiltHTML.replace( - "", - `${minimalCSS}` + headTagRegex, + `${cssInjection}` ); - } else if (rebuiltHTML.includes("")) { + } else { rebuiltHTML = rebuiltHTML.replace( - "", - `${minimalCSS}` + /]*>/i, + `${cssInjection}` ); }