From 233bd63bdbb11abdb13b8bcced0adead555f1582 Mon Sep 17 00:00:00 2001 From: "da.fei" Date: Mon, 4 Aug 2025 10:41:19 +0800 Subject: [PATCH] =?UTF-8?q?refactor(rrweb/scripts/repl.js):=20=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E5=BD=95=E5=88=B6=E8=84=9A=E6=9C=AC=E6=B3=A8=E5=85=A5?= =?UTF-8?q?=E7=9A=84=E9=B2=81=E6=A3=92=E6=80=A7=E4=B8=8E=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 此次重构增强了录制脚本注入的鲁棒性和安全性,包括防止并发注入、检测并避免无效帧注入、添加全局错误处理以及优化注入时机。同时增加了对跨域iframe的安全设置。 --- packages/rrweb/scripts/repl.js | 110 +++++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 18 deletions(-) diff --git a/packages/rrweb/scripts/repl.js b/packages/rrweb/scripts/repl.js index 30cf0808ef..a5af4ce3ee 100644 --- a/packages/rrweb/scripts/repl.js +++ b/packages/rrweb/scripts/repl.js @@ -20,9 +20,42 @@ function getCode() { void (async () => { const code = getCode(); let events = []; + let injectionInProgress = false; async function injectRecording(frame) { + // 防止并发注入 + if (injectionInProgress) { + return; + } + try { + injectionInProgress = true; + + // 检查 frame 是否仍然有效 + if (frame.isDetached()) { + console.log('Frame is detached, skipping injection'); + return; + } + + // 等待页面稳定 + await new Promise(resolve => setTimeout(resolve, 1000)); + + // 再次检查 frame 状态 + if (frame.isDetached()) { + console.log('Frame became detached while waiting, skipping injection'); + return; + } + + // 检查是否已经注入过 + const alreadyInjected = await frame.evaluate(() => { + return window.__IS_RECORDING__ === true; + }).catch(() => false); + + if (alreadyInjected) { + console.log('Recording script already injected'); + return; + } + await frame.evaluate((rrwebCode) => { const win = window; if (win.__IS_RECORDING__) return; @@ -38,27 +71,45 @@ void (async () => { document.head.append(s); } else { requestAnimationFrame(() => { - document.head.append(s); + if (document.head) { + document.head.append(s); + } }); } } loadScript(rrwebCode); win.events = []; - rrweb.record({ - emit: (event) => { - win.events.push(event); - win._replLog(event); - }, - plugins: [], - recordCanvas: true, - recordCrossOriginIframes: true, - collectFonts: true, - }); + // 添加全局错误处理 + try { + rrweb.record({ + emit: (event) => { + win.events.push(event); + if (win._replLog) { + win._replLog(event); + } + }, + plugins: [], + recordCanvas: true, + recordCrossOriginIframes: true, + collectFonts: true, + }); + console.log('rrweb recording started successfully'); + } catch (e) { + console.error('Failed to start rrweb recording:', e); + } })(); }, code); + + console.log('Recording script injected successfully'); } catch (e) { - console.error('failed to inject recording script:', e); + // 只在非上下文销毁错误时输出错误信息 + if (!e.message.includes('Execution context was destroyed') && + !e.message.includes('detached frame')) { + console.error('Failed to inject recording script:', e.message); + } + } finally { + injectionInProgress = false; } } @@ -153,6 +204,8 @@ void (async () => { '--start-maximized', '--ignore-certificate-errors', '--no-sandbox', + '--disable-web-security', + '--disable-features=VizDisplayCompositor' ], }); const page = await browser.newPage(); @@ -161,15 +214,36 @@ void (async () => { events.push(event); }); - page.on('framenavigated', async (frame) => { - await injectRecording(frame); - }); + // 使用去抖动的注入函数 + let injectionTimeout; + const debouncedInject = (frame) => { + clearTimeout(injectionTimeout); + injectionTimeout = setTimeout(() => { + injectRecording(frame); + }, 500); + }; + + page.on('framenavigated', debouncedInject); - await page.goto(url, { - waitUntil: 'domcontentloaded', - timeout: 300000, + // 监听页面加载完成事件 + page.on('load', () => { + setTimeout(() => { + injectRecording(page.mainFrame()); + }, 1000); }); + try { + await page.goto(url, { + waitUntil: 'domcontentloaded', + timeout: 300000, + }); + + // 初始注入 + await injectRecording(page.mainFrame()); + } catch (e) { + console.error('Failed to navigate to URL:', e.message); + } + emitter.once('done', async (shouldReplay) => { const pages = await browser.pages(); await Promise.all(pages.map((page) => page.close()));