Fix missed adopted style sheets of shadow doms in checkout full snapshot (#1086)

* fix: adoptedStyleSheets in shadow doms are missed when a full snapshot is checked out after recording has started

* fix: avoid removing monkey patch of all existed shadow doms when take a new full snapshot

* Apply formatting changes

* Update packages/rrweb/test/record.test.ts

Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com>

* fix typo

* update outdated snapshot

Co-authored-by: Justin Halsall <Juice10@users.noreply.github.com>
This commit is contained in:
Yun Feng
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 80ca04ddd6
commit 7f5310aaec
4 changed files with 253 additions and 10 deletions

View File

@@ -335,6 +335,8 @@ function record<T = eventWithTime>(
// When we take a full snapshot, old tracked StyleSheets need to be removed.
stylesheetManager.reset();
// Old shadow doms cache need to be cleared.
shadowDomManager.clearCache();
mutationBuffers.forEach((buf) => buf.lock()); // don't allow any mirror modifications during snapshotting
const node = snapshot(document, {

View File

@@ -129,8 +129,12 @@ export class ShadowDomManager {
}
}
public reset() {
this.restorePatches.forEach((restorePatch) => restorePatch());
public clearCache() {
this.shadowDoms = new WeakSet();
}
public reset() {
this.restorePatches.forEach((restorePatch) => restorePatch());
this.clearCache();
}
}

View File

@@ -1059,6 +1059,211 @@ exports[`record captures adopted stylesheets in shadow doms and iframe 1`] = `
]"
`;
exports[`record captures adopted stylesheets of shadow doms in checkout full snapshot 1`] = `
"[
{
\\"type\\": 4,
\\"data\\": {
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
}
},
{
\\"type\\": 2,
\\"data\\": {
\\"node\\": {
\\"type\\": 0,
\\"childNodes\\": [
{
\\"type\\": 1,
\\"name\\": \\"html\\",
\\"publicId\\": \\"\\",
\\"systemId\\": \\"\\",
\\"id\\": 2
},
{
\\"type\\": 2,
\\"tagName\\": \\"html\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 4
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 6
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"id\\": \\"shadow-host-1\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"entry\\",
\\"id\\": 8
}
],
\\"id\\": 7,
\\"isShadowHost\\": true
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 9
}
],
\\"id\\": 5
}
],
\\"id\\": 3
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 15,
\\"id\\": 7,
\\"styleIds\\": [
1
],
\\"styles\\": [
{
\\"styleId\\": 1,
\\"rules\\": [
{
\\"rule\\": \\"h1 { color: blue; }\\",
\\"index\\": 0
}
]
}
]
}
},
{
\\"type\\": 4,
\\"data\\": {
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
}
},
{
\\"type\\": 2,
\\"data\\": {
\\"node\\": {
\\"type\\": 0,
\\"childNodes\\": [
{
\\"type\\": 1,
\\"name\\": \\"html\\",
\\"publicId\\": \\"\\",
\\"systemId\\": \\"\\",
\\"id\\": 2
},
{
\\"type\\": 2,
\\"tagName\\": \\"html\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 4
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 6
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"id\\": \\"shadow-host-1\\"
},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"entry\\",
\\"id\\": 8
}
],
\\"id\\": 7,
\\"isShadowHost\\": true
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 9
}
],
\\"id\\": 5
}
],
\\"id\\": 3
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 15,
\\"id\\": 7,
\\"styleIds\\": [
1
],
\\"styles\\": [
{
\\"styleId\\": 1,
\\"rules\\": [
{
\\"rule\\": \\"h1 { color: blue; }\\",
\\"index\\": 0
}
]
}
]
}
}
]"
`;
exports[`record captures inserted style text nodes correctly 1`] = `
"[
{

View File

@@ -29,9 +29,12 @@ interface ISuite {
interface IWindow extends Window {
rrweb: {
record: (
record: ((
options: recordOptions<eventWithTime>,
) => listenerHandler | undefined;
) => listenerHandler | undefined) & {
takeFullSnapshot: (isCheckout?: boolean | undefined) => void;
};
addCustomEvent<T>(tag: string, payload: T): void;
};
emit: (e: eventWithTime) => undefined;
@@ -491,9 +494,9 @@ describe('record', function (this: ISuite) {
iframe!.contentDocument!.adoptedStyleSheets = [sheet2];
iframe!.contentDocument!.body.innerHTML = '<h1>h1 in iframe</h1>';
const { record } = ((window as unknown) as IWindow).rrweb;
record({
emit: ((window as unknown) as IWindow).emit,
const { rrweb, emit } = (window as unknown) as IWindow;
rrweb.record({
emit,
});
setTimeout(() => {
@@ -565,9 +568,9 @@ describe('record', function (this: ISuite) {
sheet2.replaceSync!('div {font-size: large;}');
shadowHost.shadowRoot!.adoptedStyleSheets = [sheet2];
const { record } = ((window as unknown) as IWindow).rrweb;
record({
emit: ((window as unknown) as IWindow).emit,
const { rrweb, emit } = (window as unknown) as IWindow;
rrweb.record({
emit,
});
setTimeout(() => {
@@ -586,6 +589,35 @@ describe('record', function (this: ISuite) {
assertSnapshot(ctx.events);
});
it('captures adopted stylesheets of shadow doms in checkout full snapshot', async () => {
await ctx.page.evaluate(() => {
return new Promise((resolve) => {
document.body.innerHTML = `
<div id="shadow-host-1">entry</div>
`;
let shadowHost = document.querySelector('div')!;
shadowHost!.attachShadow({ mode: 'open' });
const sheet = new CSSStyleSheet();
sheet.replaceSync!('h1 {color: blue;}');
shadowHost.shadowRoot!.adoptedStyleSheets = [sheet];
const { rrweb, emit } = (window as unknown) as IWindow;
rrweb.record({
emit,
});
setTimeout(() => {
// When a full snapshot is checked out manually, all adoptedStylesheets should also be captured.
rrweb.record.takeFullSnapshot(true);
resolve(undefined);
}, 10);
});
});
await waitForRAF(ctx.page);
assertSnapshot(ctx.events);
});
it('captures stylesheets in iframes with `blob:` url', async () => {
await ctx.page.evaluate(() => {
const iframe = document.createElement('iframe');