Skip to content
Open
Show file tree
Hide file tree
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
7 changes: 7 additions & 0 deletions .changeset/closed-shadow-root.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"rrweb": patch
"rrweb-snapshot": patch
"utils": patch
---

Add recording of shadow DOM nodes which have been created with the { mode: 'closed' } flag
14 changes: 11 additions & 3 deletions packages/rrweb/src/record/shadow-dom-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ type BypassOptions = Omit<
sampling: SamplingStrategy;
};

type ElementWithShadowRoot = Element & {
__rrClosedShadowRoot: ShadowRoot;
};

export class ShadowDomManager {
private shadowDoms = new WeakSet<ShadowRoot>();
private mutationCb: mutationCallBack;
Expand Down Expand Up @@ -133,9 +137,13 @@ export class ShadowDomManager {
// For the shadow dom elements in the document, monitor their dom mutations.
// For shadow dom elements that aren't in the document yet,
// we start monitoring them once their shadow dom host is appended to the document.
const shadowRootEl = dom.shadowRoot(this);
if (shadowRootEl && inDom(this))
manager.addShadowRoot(shadowRootEl, doc);
if (sRoot && inDom(this)) {
manager.addShadowRoot(sRoot, doc);
}
if (option.mode === 'closed') {
// FIXME: this exposes a closed root
(this as ElementWithShadowRoot).__rrClosedShadowRoot = sRoot;
}
return sRoot;
};
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
[
{
"type": 0,
"data": {}
},
{
"type": 1,
"data": {}
},
{
"type": 4,
"data": {
"href": "about:blank",
"width": 1920,
"height": 1080
}
},
{
"type": 2,
"data": {
"node": {
"type": 0,
"childNodes": [
{
"type": 2,
"tagName": "html",
"attributes": {},
"childNodes": [
{
"type": 2,
"tagName": "head",
"attributes": {},
"childNodes": [],
"id": 3
},
{
"type": 2,
"tagName": "body",
"attributes": {},
"childNodes": [
{
"type": 3,
"textContent": "\n ",
"id": 5
},
{
"type": 2,
"tagName": "script",
"attributes": {},
"childNodes": [
{
"type": 3,
"textContent": "SCRIPT_PLACEHOLDER",
"id": 7
}
],
"id": 6
},
{
"type": 3,
"textContent": "\n \n \n\n",
"id": 8
}
],
"id": 4
}
],
"id": 2
}
],
"compatMode": "BackCompat",
"id": 1
},
"initialOffset": {
"left": 0,
"top": 0
}
}
},
{
"type": 3,
"data": {
"source": 0,
"texts": [],
"attributes": [],
"removes": [],
"adds": [
{
"parentId": 4,
"nextId": null,
"node": {
"type": 2,
"tagName": "div",
"attributes": {},
"childNodes": [],
"id": 9,
"isShadowHost": true
}
},
{
"parentId": 9,
"nextId": null,
"node": {
"type": 2,
"tagName": "input",
"attributes": {},
"childNodes": [],
"id": 10,
"isShadow": true
}
}
]
}
}
]
24 changes: 24 additions & 0 deletions packages/rrweb/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,30 @@ describe('record integration tests', function (this: ISuite) {
await assertSnapshot(snapshots);
});

it('should record closed shadow DOM after monkeypatching', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
page.on('console', (msg) => console.log(msg.text()));
await page.setContent(getHtml.call(this, 'blank.html'));
await page.evaluate(() => {
return new Promise((resolve) => {
const el = document.createElement('div') as HTMLDivElement;
const shadow = el.attachShadow({ mode: 'closed' });
shadow.appendChild(document.createElement('input'));
setTimeout(() => {
document.body.append(el);
resolve(null);
}, 10);
});
});
await waitForRAF(page);

const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
await assertSnapshot(snapshots, true);
});

it('should record shadow DOM 3', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
Expand Down
16 changes: 15 additions & 1 deletion packages/rrweb/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ function stringifyDomSnapshot(mhtml: string): string {

export async function assertSnapshot(
snapshotsOrPage: eventWithTime[] | puppeteer.Page,
useOwnFile: boolean | string = false,
) {
let snapshots: eventWithTime[];
if (!Array.isArray(snapshotsOrPage)) {
Expand All @@ -318,7 +319,20 @@ export async function assertSnapshot(
}

expect(snapshots).toBeDefined();
expect(stringifySnapshots(snapshots)).toMatchSnapshot();
if (useOwnFile) {
// e.g. 'mutation.test.ts > mutation > add elements at once'
const long_fname = expect.getState().currentTestName.split('/').pop();
const file = long_fname.split(' > ')[0].replace('.test.ts', '');
if (typeof useOwnFile !== 'string') {
useOwnFile = long_fname.substring(long_fname.indexOf(' > ') + 3);
}
useOwnFile = useOwnFile.replace(/ > /g, '.').replace(/\s/g, '_');

const fname = `./__${file}.snapshots__/${useOwnFile}.json`;
expect(stringifySnapshots(snapshots)).toMatchFileSnapshot(fname);
} else {
expect(stringifySnapshots(snapshots)).toMatchSnapshot();
}
}

export function replaceLast(str: string, find: string, replace: string) {
Expand Down
3 changes: 3 additions & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@

export function shadowRoot(n: Node): ShadowRoot | null {
if (!n || !('shadowRoot' in n)) return null;
if ('__rrClosedShadowRoot' in n) {
return n.__rrClosedShadowRoot as ShadowRoot;
}
return getUntaintedAccessor('Element', n as Element, 'shadowRoot');
}

Expand All @@ -224,7 +227,7 @@

// copy from https://github.com/getsentry/sentry-javascript/blob/b2109071975af8bf0316d3b5b38f519bdaf5dc15/packages/utils/src/object.ts
export function patch(
source: { [key: string]: any },

Check warning on line 230 in packages/utils/src/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/utils/src/index.ts#L230

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
name: string,
replacement: (...args: unknown[]) => unknown,
): () => void {
Expand Down
Loading