diff --git a/packages/rrweb-snapshot/src/rebuild.ts b/packages/rrweb-snapshot/src/rebuild.ts index 34c8766ffd..789e1fadf1 100644 --- a/packages/rrweb-snapshot/src/rebuild.ts +++ b/packages/rrweb-snapshot/src/rebuild.ts @@ -8,7 +8,7 @@ import { INode, BuildCache, } from './types'; -import { isElement } from './utils'; +import { defineCustomElement, isElement } from './utils'; const tagMap: tagMap = { script: 'noscript', @@ -284,6 +284,9 @@ function buildNode( */ if (!node.shadowRoot) { node.attachShadow({ mode: 'open' }); + if (doc.defaultView) { + defineCustomElement(doc.defaultView, tagName); + } } else { while (node.shadowRoot.firstChild) { node.shadowRoot.removeChild(node.shadowRoot.firstChild); diff --git a/packages/rrweb-snapshot/src/utils.ts b/packages/rrweb-snapshot/src/utils.ts index 6e858e4e18..1be2b7ae35 100644 --- a/packages/rrweb-snapshot/src/utils.ts +++ b/packages/rrweb-snapshot/src/utils.ts @@ -136,3 +136,20 @@ export function is2DCanvasBlank(canvas: HTMLCanvasElement): boolean { } return true; } + +/** + * Ensure we define custom elements as you can have css that targets the + * `:defined` pseudo class (e.g. hide until defined) + */ +export function defineCustomElement(w: Window, elementName: string) { + // We need to define custom elements inside of the correct window (i.e. + // inside of the iframe) + try { + // Can only define custom element once + if (!w.customElements.get(elementName)) { + // @ts-ignore HTMLElement exists on window + const CustomElement = w.HTMLElement as CustomElementConstructor; + w.customElements.define(elementName, class extends CustomElement {}); + } + } catch {} +} diff --git a/packages/rrweb-snapshot/typings/utils.d.ts b/packages/rrweb-snapshot/typings/utils.d.ts index 53b974b31a..1d58a6eeb4 100644 --- a/packages/rrweb-snapshot/typings/utils.d.ts +++ b/packages/rrweb-snapshot/typings/utils.d.ts @@ -18,4 +18,5 @@ interface MaskInputValue extends HasInputMaskOptions { } export declare function maskInputValue({ input, maskInputSelector, unmaskInputSelector, maskInputOptions, tagName, type, value, maskInputFn, }: MaskInputValue): string; export declare function is2DCanvasBlank(canvas: HTMLCanvasElement): boolean; +export declare function defineCustomElement(w: Window, elementName: string): void; export {}; diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts index 1a4224f02b..24aaf159de 100644 --- a/packages/rrweb/src/replay/index.ts +++ b/packages/rrweb/src/replay/index.ts @@ -5,6 +5,7 @@ import { NodeType, BuildCache, createCache, + defineCustomElement, } from '@sentry-internal/rrweb-snapshot'; import * as mittProxy from 'mitt'; import { polyfill as smoothscrollPolyfill } from './smoothscroll'; @@ -1458,8 +1459,12 @@ export class Replayer { if (mutation.node.isShadow) { // If the parent is attached a shadow dom after it's created, it won't have a shadow root. if (!hasShadowRoot(parent)) { - ((parent as Node) as HTMLElement).attachShadow({ mode: 'open' }); - parent = ((parent as Node) as HTMLElement).shadowRoot!; + const parentNode = parent as Node; + (parentNode as HTMLElement).attachShadow({ mode: 'open' }); + parent = (parentNode as HTMLElement).shadowRoot!; + if (this.iframe.contentWindow) { + defineCustomElement(this.iframe.contentWindow, parentNode.nodeName); + } } else parent = parent.shadowRoot; }