new sandbox strategy

In this commit we switched the sandbox strategy to use iframe's
sandbox attribute. Indeed we do not need delegate event anymore,
but need to add some styles into the iframe.
The details were documented in the sandbox part of internal design.
This commit is contained in:
Yanzhen Yu
2026-04-01 12:00:00 +08:00
parent df990cd731
commit efa7a8fa1b
6 changed files with 102 additions and 28 deletions

View File

@@ -1,6 +1,5 @@
import { rebuild, buildNodeWithSN } from 'rrweb-snapshot';
import * as mittProxy from 'mitt';
import { on, off } from 'delegated-events';
import Timer from './timer';
import {
EventType,
@@ -18,6 +17,7 @@ import {
actionWithDelay,
} from '../types';
import { mirror } from '../utils';
import injectStyleRules from './styles/inject-style';
import './styles/style.css';
// https://github.com/rollup/rollup/issues/1267#issuecomment-296395734
@@ -138,6 +138,7 @@ export class Replayer {
this.wrapper.appendChild(this.mouse);
this.iframe = document.createElement('iframe');
this.iframe.setAttribute('sandbox', 'allow-same-origin');
this.wrapper.appendChild(this.iframe);
}
@@ -208,21 +209,13 @@ export class Replayer {
event: fullSnapshotEvent & { timestamp: number },
) {
mirror.map = rebuild(event.data.node, this.iframe.contentDocument!)[1];
const styleEl = document.createElement('style');
const { documentElement, head } = this.iframe.contentDocument!;
documentElement!.insertBefore(styleEl, head);
for (let idx = 0; idx < injectStyleRules.length; idx++) {
(styleEl.sheet! as CSSStyleSheet).insertRule(injectStyleRules[idx], idx);
}
this.waitForStylesheetLoad();
// avoid form submit to refresh the iframe
off('submit', 'form', this.preventDefault, {
document: this.iframe.contentDocument!,
});
on('submit', 'form', this.preventDefault, {
document: this.iframe.contentDocument!,
});
// avoid a link click to refresh the iframe
off('click', 'a', this.preventDefault, {
document: this.iframe.contentDocument!,
});
on('click', 'a', this.preventDefault, {
document: this.iframe.contentDocument!,
});
}
/**
@@ -262,10 +255,6 @@ export class Replayer {
}
}
private preventDefault(evt: Event) {
evt.preventDefault();
}
private applyIncremental(d: incrementalData, isSync: boolean) {
switch (d.source) {
case IncrementalSource.Mutation: {
@@ -377,13 +366,24 @@ export class Replayer {
}
const event = new Event(MouseInteractions[d.type].toLowerCase());
const target = (mirror.getNode(d.id) as Node) as HTMLElement;
target.dispatchEvent(event);
if (d.type === MouseInteractions.Blur) {
target.blur();
} else if (d.type === MouseInteractions.Click) {
target.click();
/**
* Click has no visual impact when replaying and may
* trigger navigation when apply to an <a> link.
* So we will not call click(), instead we add an
* animation to the mouse element which indicate user
* clicked at this moment.
*/
this.mouse.classList.remove('active');
// tslint:disable-next-line
void this.mouse.offsetWidth;
this.mouse.classList.add('active');
} else if (d.type === MouseInteractions.Focus) {
target.focus();
} else {
target.dispatchEvent(event);
}
break;
}

View File

@@ -0,0 +1,6 @@
const rules: string[] = [
'iframe { background: #ccc }',
'noscript { display: none !important; }',
];
export default rules;

View File

@@ -3,8 +3,12 @@
}
.replayer-mouse {
position: absolute;
width: 1px;
height: 1px;
width: 20px;
height: 20px;
background-size: contain;
background-position: center center;
background-repeat: no-repeat;
background-image: url('data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGRhdGEtbmFtZT0iTGF5ZXIgMSIgdmlld0JveD0iMCAwIDUwIDUwIiB4PSIwcHgiIHk9IjBweCI+PHRpdGxlPkRlc2lnbl90bnA8L3RpdGxlPjxwYXRoIGQ9Ik00OC43MSw0Mi45MUwzNC4wOCwyOC4yOSw0NC4zMywxOEExLDEsMCwwLDAsNDQsMTYuMzlMMi4zNSwxLjA2QTEsMSwwLDAsMCwxLjA2LDIuMzVMMTYuMzksNDRhMSwxLDAsMCwwLDEuNjUuMzZMMjguMjksMzQuMDgsNDIuOTEsNDguNzFhMSwxLDAsMCwwLDEuNDEsMGw0LjM4LTQuMzhBMSwxLDAsMCwwLDQ4LjcxLDQyLjkxWm0tNS4wOSwzLjY3TDI5LDMyYTEsMSwwLDAsMC0xLjQxLDBsLTkuODUsOS44NUwzLjY5LDMuNjlsMzguMTIsMTRMMzIsMjcuNThBMSwxLDAsMCwwLDMyLDI5TDQ2LjU5LDQzLjYyWiI+PC9wYXRoPjwvc3ZnPg==');
}
.replayer-mouse::after {
content: '';
@@ -12,7 +16,27 @@
width: 20px;
height: 20px;
border-radius: 10px;
background: thistle;
background: rgb(73, 80, 246);
transform: translate(-10px, -10px);
opacity: 0.5;
opacity: 0.3;
}
.replayer-mouse.active::after {
animation: click 0.3s ease-in-out 1;
}
@keyframes click {
0% {
opacity: 0.3;
width: 20px;
height: 20px;
border-radius: 10px;
transform: translate(-10px, -10px);
}
50% {
opacity: 0.5;
width: 10px;
height: 10px;
border-radius: 5px;
transform: translate(-5px, -5px);
}
}