feat: revamp rrweb snapshot rendering

This commit is contained in:
Rohit
2025-07-06 09:32:38 +05:30
parent a75471cb31
commit 9d83a16644

View File

@@ -296,23 +296,23 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
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<RRWebDOMBrowserRendererProps> = ({
if (socket) {
socket.emit("dom:scroll", {
deltaX,
deltaY
})
deltaY,
});
}
notifyLastAction("scroll");
}
@@ -749,20 +749,86 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
return;
}
const iframe = iframeRef.current;
if (isInCaptureMode) {
return; // Skip rendering in capture mode
}
try {
setRenderError(null);
setIsRendered(false);
const tempDoc =
document.implementation.createHTMLDocument("RRWeb Snapshot");
const iframe = iframeRef.current!;
const iframeDoc = iframe.contentDocument!;
const styleTags = Array.from(
document.querySelectorAll('link[rel="stylesheet"], style')
)
.map((tag) => tag.outerHTML)
.join("\n");
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;
}
`;
const skeleton = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<base href="${snapshotData.baseUrl}">
${styleTags}
<style>${enhancedCSS}</style>
</head>
<body></body>
</html>
`;
if (!iframeDoc) {
throw new Error("Cannot access iframe document");
}
// Write the skeleton into the iframe
iframeDoc.open();
iframeDoc.write(skeleton);
iframeDoc.close();
const mirror = createMirror();
try {
rebuild(snapshotData.snapshot, {
doc: tempDoc,
doc: iframeDoc,
mirror: mirror,
cache: { stylesWithHoverClass: new Map() },
afterAppend: (node) => {
@@ -786,125 +852,8 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
throw new Error(`rrweb rebuild failed: ${rebuildError}`);
}
let rebuiltHTML = tempDoc.documentElement.outerHTML;
rebuiltHTML = "<!DOCTYPE html>\n" + rebuiltHTML;
const additionalCSS = [];
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 = /<head[^>]*>/i;
const cssInjection = `
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<base href="${snapshotData.baseUrl}">
<style>${enhancedCSS}</style>
`;
if (headTagRegex.test(rebuiltHTML)) {
rebuiltHTML = rebuiltHTML.replace(
headTagRegex,
`<head>${cssInjection}`
);
} else {
rebuiltHTML = rebuiltHTML.replace(
/<html[^>]*>/i,
`<html><head>${cssInjection}</head>`
);
}
rebuiltHTML = rebuiltHTML
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
.replace(/\s*on\w+\s*=\s*"[^"]*"/gi, "")
.replace(/\s*on\w+\s*=\s*'[^']*'/gi, "")
.replace(/javascript:/gi, "void:")
.replace(/<form\b/gi, '<form onsubmit="return false;"');
const iframeDoc =
iframe.contentDocument || iframe.contentWindow?.document;
if (!iframeDoc) {
throw new Error("Cannot access iframe document");
}
iframeDoc.open();
iframeDoc.write(rebuiltHTML);
iframeDoc.close();
iframe.onload = () => {
setIsRendered(true);
setupIframeInteractions(iframeDoc);
};
setIsRendered(true);
setupIframeInteractions(iframeDoc);
} catch (error) {
console.error("Error rendering rrweb snapshot:", error);
setRenderError(error instanceof Error ? error.message : String(error));