Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 48 additions & 113 deletions src/components/recorder/DOMBrowserRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,6 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
const containerRef = useRef<HTMLDivElement>(null);
const iframeRef = useRef<HTMLIFrameElement>(null);
const [isRendered, setIsRendered] = useState(false);
const [renderError, setRenderError] = useState<string | null>(null);
const [lastMousePosition, setLastMousePosition] = useState({ x: 0, y: 0 });
const [currentHighlight, setCurrentHighlight] = useState<{
element: Element;
Expand Down Expand Up @@ -342,7 +341,10 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
const existingHandlers = (iframeDoc as any)._domRendererHandlers;
if (existingHandlers) {
Object.entries(existingHandlers).forEach(([event, handler]) => {
iframeDoc.removeEventListener(event, handler as EventListener, false); // Changed to false
const options: boolean | AddEventListenerOptions = ['wheel', 'touchstart', 'touchmove'].includes(event)
? { passive: false }
: false;
iframeDoc.removeEventListener(event, handler as EventListener, options);
});
}

Expand Down Expand Up @@ -700,7 +702,11 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
return;
}

e.preventDefault();
if (isInCaptureMode) {
e.preventDefault();
e.stopPropagation();
return;
}

if (!isInCaptureMode) {
const wheelEvent = e as WheelEvent;
Expand Down Expand Up @@ -752,7 +758,10 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
handlers.beforeunload = preventDefaults;

Object.entries(handlers).forEach(([event, handler]) => {
iframeDoc.addEventListener(event, handler, false);
const options: boolean | AddEventListenerOptions = ['wheel', 'touchstart', 'touchmove'].includes(event)
? { passive: false }
: false;
iframeDoc.addEventListener(event, handler, options);
});

// Store handlers for cleanup
Expand Down Expand Up @@ -795,11 +804,39 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
}

try {
setRenderError(null);
setIsRendered(false);

const iframe = iframeRef.current!;
const iframeDoc = iframe.contentDocument!;
let iframeDoc: Document;

try {
iframeDoc = iframe.contentDocument!;
if (!iframeDoc) {
throw new Error("Cannot access iframe document");
}
} catch (crossOriginError) {
console.warn("Cross-origin iframe access blocked, recreating iframe");

const newIframe = document.createElement('iframe');
newIframe.style.cssText = iframe.style.cssText;
newIframe.sandbox = iframe.sandbox.value;
newIframe.title = iframe.title;
newIframe.tabIndex = iframe.tabIndex;
newIframe.id = iframe.id;

iframe.parentNode?.replaceChild(newIframe, iframe);
Object.defineProperty(iframeRef, 'current', {
value: newIframe,
writable: false,
enumerable: true,
configurable: true
});

iframeDoc = newIframe.contentDocument!;
if (!iframeDoc) {
throw new Error("Cannot access new iframe document");
}
}
Comment on lines +812 to +839
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Recreating the iframe: ref mutation and sandbox handling will crash and/or no-op

  • Don’t redefine refs with Object.defineProperty; it makes current read‑only and React will later fail to update it (on re-render/unmount).
  • HTMLIFrameElement.sandbox is a DOMTokenList (read‑only); assigning a string won’t work reliably. Set the attribute instead.

Fix with this minimal patch:

-          const newIframe = document.createElement('iframe');
-          newIframe.style.cssText = iframe.style.cssText;
-          newIframe.sandbox = iframe.sandbox.value;
+          const newIframe = document.createElement('iframe');
+          newIframe.style.cssText = iframe.style.cssText;
+          newIframe.setAttribute('sandbox', iframe.getAttribute('sandbox') ?? '');
           newIframe.title = iframe.title;
           newIframe.tabIndex = iframe.tabIndex;
           newIframe.id = iframe.id;

           iframe.parentNode?.replaceChild(newIframe, iframe);
-          Object.defineProperty(iframeRef, 'current', {
-            value: newIframe,
-            writable: false,
-            enumerable: true,
-            configurable: true
-          });
+          iframeRef.current = newIframe;

Optional: const newIframe = iframe.cloneNode(false) as HTMLIFrameElement; will also preserve attributes like class, data‑*, etc., then replace and assign iframeRef.current.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
iframeDoc = iframe.contentDocument!;
if (!iframeDoc) {
throw new Error("Cannot access iframe document");
}
} catch (crossOriginError) {
console.warn("Cross-origin iframe access blocked, recreating iframe");
const newIframe = document.createElement('iframe');
newIframe.style.cssText = iframe.style.cssText;
newIframe.sandbox = iframe.sandbox.value;
newIframe.title = iframe.title;
newIframe.tabIndex = iframe.tabIndex;
newIframe.id = iframe.id;
iframe.parentNode?.replaceChild(newIframe, iframe);
Object.defineProperty(iframeRef, 'current', {
value: newIframe,
writable: false,
enumerable: true,
configurable: true
});
iframeDoc = newIframe.contentDocument!;
if (!iframeDoc) {
throw new Error("Cannot access new iframe document");
}
}
try {
iframeDoc = iframe.contentDocument!;
if (!iframeDoc) {
throw new Error("Cannot access iframe document");
}
} catch (crossOriginError) {
console.warn("Cross-origin iframe access blocked, recreating iframe");
const newIframe = document.createElement('iframe');
newIframe.style.cssText = iframe.style.cssText;
newIframe.setAttribute('sandbox', iframe.getAttribute('sandbox') ?? '');
newIframe.title = iframe.title;
newIframe.tabIndex = iframe.tabIndex;
newIframe.id = iframe.id;
iframe.parentNode?.replaceChild(newIframe, iframe);
iframeRef.current = newIframe;
iframeDoc = newIframe.contentDocument!;
if (!iframeDoc) {
throw new Error("Cannot access new iframe document");
}
}
🤖 Prompt for AI Agents
In src/components/recorder/DOMBrowserRenderer.tsx around lines 812 to 839, the
current approach recreates the iframe by using Object.defineProperty to mutate
the ref (making current read‑only) and assigns iframe.sandbox as a string
(DOMTokenList is not a string), which will break React ref updates and sandbox
handling; fix by creating the new iframe (or cloneNode(false) to preserve
attributes), copy needed attributes/styles (style.cssText, title, tabIndex, id,
classes, data-*, etc.), set sandbox using setAttribute('sandbox',
original.getAttribute('sandbox') || '') or manipulate newIframe.sandbox.tokens
properly, replace the old iframe in the DOM, and update the ref by assigning
iframeRef.current = newIframe (not via Object.defineProperty) so React can
update/unmount correctly.


const styleTags = Array.from(
document.querySelectorAll('link[rel="stylesheet"], style')
Expand Down Expand Up @@ -897,8 +934,6 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
setupIframeInteractions(iframeDoc);
} catch (error) {
console.error("Error rendering rrweb snapshot:", error);
setRenderError(error instanceof Error ? error.message : String(error));
showErrorInIframe(error);
}
},
[setupIframeInteractions, isInCaptureMode, isCachingChildSelectors]
Expand All @@ -919,89 +954,6 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
}
}, [getText, getList, listSelector, isRendered, setupIframeInteractions]);

/**
* Show error message in iframe
*/
const showErrorInIframe = (error: any) => {
if (!iframeRef.current) return;

const iframe = iframeRef.current;
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;

if (iframeDoc) {
try {
iframeDoc.open();
iframeDoc.write(`
<html>
<head>
<style>
body {
padding: 20px;
font-family: Arial, sans-serif;
background: #f5f5f5;
}
.error-container {
background: white;
border: 1px solid #ff00c3;
border-radius: 5px;
padding: 20px;
margin: 20px 0;
}
.retry-btn {
background: #ff00c3;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="error-container">
<h3 style="color: #ff00c3;">Error Loading DOM Content</h3>
<p>Failed to render the page in DOM mode.</p>
<p><strong>Common causes:</strong></p>
<ul>
<li>Page is still loading or navigating</li>
<li>Resource proxy timeouts or failures</li>
<li>Network connectivity issues</li>
<li>Invalid HTML structure</li>
</ul>
<p><strong>Solutions:</strong></p>
<ul>
<li>Try switching back to Screenshot mode</li>
<li>Wait for the page to fully load and try again</li>
<li>Check your network connection</li>
<li>Refresh the browser page</li>
</ul>
<button class="retry-btn" onclick="window.parent.postMessage('retry-dom-mode', '*')">
Retry DOM Mode
</button>
<details style="margin-top: 15px;">
<summary style="cursor: pointer; color: #666;">Technical details</summary>
<pre style="background: #f0f0f0; padding: 10px; margin-top: 10px; overflow: auto; font-size: 12px;">${error.toString()}</pre>
</details>
</div>
</body>
</html>
`);
iframeDoc.close();

window.addEventListener("message", (event) => {
if (event.data === "retry-dom-mode") {
if (socket) {
socket.emit("enable-dom-streaming");
}
}
});
} catch (e) {
console.error("Failed to write error message to iframe:", e);
}
}
};

useEffect(() => {
return () => {
if (iframeRef.current) {
Expand All @@ -1010,10 +962,13 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
const handlers = (iframeDoc as any)._domRendererHandlers;
if (handlers) {
Object.entries(handlers).forEach(([event, handler]) => {
const options: boolean | AddEventListenerOptions = ['wheel', 'touchstart', 'touchmove'].includes(event)
? { passive: false }
: false;
iframeDoc.removeEventListener(
event,
handler as EventListener,
true
options
);
});
}
Expand Down Expand Up @@ -1051,7 +1006,7 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
/>

{/* Loading indicator */}
{!isRendered && !renderError && (
{!isRendered && (
<div
style={{
position: "absolute",
Expand Down Expand Up @@ -1089,26 +1044,6 @@ export const DOMBrowserRenderer: React.FC<RRWebDOMBrowserRendererProps> = ({
</div>
)}

{/* Error indicator */}
{renderError && (
<div
style={{
position: "absolute",
top: 30,
right: 5,
background: "rgba(255, 0, 0, 0.9)",
color: "white",
padding: "2px 8px",
borderRadius: "3px",
fontSize: "10px",
zIndex: 1000,
maxWidth: "200px",
}}
>
RENDER ERROR
</div>
)}

{/* Capture mode overlay */}
{isInCaptureMode && (
<div
Expand Down