rrweb-io / rrweb

record and replay the web
https://www.rrweb.io/
MIT License
16.55k stars 1.41k forks source link

[Bug]: :not(:defined) CSS selector causes discrepancy between replayed snapshot for shadow DOM elements #1314

Open macalinao opened 11 months ago

macalinao commented 11 months ago

Preflight Checklist

What package is this bug report for?

rrweb

Version

v2.0.0-alpha.10

Expected Behavior

These CSS selectors should be ignored, or the the definition of the custom elements should be captured somehow in the replay

Actual Behavior

These CSS selectors cause large parts of the page to be invisible

Steps to Reproduce

View Reddit in RRWeb

Testcase Gist URL

No response

Additional Information

No response

macalinao commented 11 months ago

I've written a function which rewrites events:

import type { eventWithTime, fullSnapshotEvent } from "@rrweb/types";
import { EventType, IncrementalSource } from "@rrweb/types";
import type { serializedNode } from "rrweb-snapshot";
import { NodeType, stringifyStylesheet } from "rrweb-snapshot";

const removeNotDefinedForNode = <T extends serializedNode>(node: T): T => {
  if (node.type === NodeType.Text && node.isStyle) {
    const s = new CSSStyleSheet();
    s.replaceSync(node.textContent);

    let i = 0;
    while (i < s.cssRules.length) {
      const rule = s.cssRules.item(i);
      if (rule?.cssText.includes(":not(:defined)")) {
        s.deleteRule(i);
      } else {
        i++;
      }
    }
    return {
      ...node,
      textContent: stringifyStylesheet(s) ?? "",
    };
  }
  if (node.type === NodeType.Element || node.type === NodeType.Document) {
    return {
      ...node,
      childNodes: node.childNodes.map(removeNotDefinedForNode),
    };
  }
  return node;
};

const removeNotDefined = (snapshot: fullSnapshotEvent) => {
  return {
    ...snapshot,
    data: {
      ...snapshot.data,
      node: removeNotDefinedForNode(snapshot.data.node),
    },
  };
};

export const filterNotDefinedCSSRules = (
  ev: eventWithTime,
): eventWithTime | null => {
  if (ev.type === EventType.FullSnapshot) {
    return { ...ev, ...removeNotDefined(ev) };
  }
  if (
    ev.type === EventType.IncrementalSnapshot &&
    ev.data.source === IncrementalSource.AdoptedStyleSheet
  ) {
    if (ev.data.styles) {
      const newStyles = ev.data.styles.map((style) => {
        return {
          ...style,
          rules: style.rules.filter((s) => {
            const omit = s.rule.includes(":not(:defined)");
            if (omit) {
              console.log("omitting", s.rule);
            }
            return !omit;
          }),
        };
      });
      return {
        ...ev,
        data: {
          ...ev.data,
          styles: newStyles,
        },
      };
    }
  }
  return ev;
};

It's quite ugly and probably very slow so a fix should be merged in, maybe with a boolean flag behind it