Skip to content

Commit

Permalink
Revert "Single style capture (#1437)"
Browse files Browse the repository at this point in the history
This reverts commit 5fbb904.
  • Loading branch information
Vadman97 committed Nov 15, 2024
1 parent cb01260 commit 2701b9d
Show file tree
Hide file tree
Showing 19 changed files with 405 additions and 1,618 deletions.
6 changes: 0 additions & 6 deletions .changeset/single-style-capture.md

This file was deleted.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"eslint-plugin-compat": "^5.0.0",
"eslint-plugin-jest": "^27.6.0",
"eslint-plugin-tsdoc": "^0.2.17",
"happy-dom": "^14.12.0",
"markdownlint": "^0.25.1",
"markdownlint-cli": "^0.31.1",
"prettier": "2.8.4",
Expand Down
1 change: 1 addition & 0 deletions packages/rrdom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@typescript-eslint/eslint-plugin": "^5.23.0",
"@typescript-eslint/parser": "^5.23.0",
"eslint": "^8.15.0",
"happy-dom": "^14.12.0",
"puppeteer": "^17.1.3",
"turbo": "^2.0.5",
"typescript": "^5.4.5",
Expand Down
102 changes: 13 additions & 89 deletions packages/rrweb-snapshot/src/rebuild.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { mediaSelectorPlugin, pseudoClassPlugin } from './css';
import {
type serializedNodeWithId,
type serializedElementNodeWithId,
type serializedTextNodeWithId,
NodeType,
type tagMap,
type elementNode,
Expand Down Expand Up @@ -86,81 +84,6 @@ export function createCache(): BuildCache {
};
}

/**
* undo splitCssText/markCssSplits
* (would move to utils.ts but uses `adaptCssForReplay`)
*/
export function applyCssSplits(
n: serializedElementNodeWithId,
cssText: string,
hackCss: boolean,
cache: BuildCache,
): void {
const childTextNodes: serializedTextNodeWithId[] = [];
for (const scn of n.childNodes) {
if (scn.type === NodeType.Text) {
childTextNodes.push(scn);
}
}
const cssTextSplits = cssText.split('/* rr_split */');
while (
cssTextSplits.length > 1 &&
cssTextSplits.length > childTextNodes.length
) {
// unexpected: remerge the last two so that we don't discard any css
cssTextSplits.splice(-2, 2, cssTextSplits.slice(-2).join(''));
}
for (let i = 0; i < childTextNodes.length; i++) {
const childTextNode = childTextNodes[i];
const cssTextSection = cssTextSplits[i];
if (childTextNode && cssTextSection) {
// id will be assigned when these child nodes are
// iterated over in buildNodeWithSN
try {
childTextNode.textContent = hackCss
? adaptCssForReplay(cssTextSection, cache)
: cssTextSection;
} catch (err: unknown) {
console.warn(`Highlight failed to set rrweb css ${err}`);
}
}
}
}

/**
* Normally a <style> element has a single textNode containing the rules.
* During serialization, we bypass this (`styleEl.sheet`) to get the rules the
* browser sees and serialize this to a special _cssText attribute, blanking
* out any text nodes. This function reverses that and also handles cases where
* there were no textNode children present (dynamic css/or a <link> element) as
* well as multiple textNodes, which need to be repopulated (based on presence of
* a special `rr_split` marker in case they are modified by subsequent mutations.
*/
export function buildStyleNode(
n: serializedElementNodeWithId,
styleEl: HTMLStyleElement, // when inlined, a <link type="stylesheet"> also gets rebuilt as a <style>
cssText: string,
options: {
doc: Document;
hackCss: boolean;
cache: BuildCache;
},
) {
const { doc, hackCss, cache } = options;
if (n.childNodes.length) {
applyCssSplits(n, cssText, hackCss, cache);
} else {
if (hackCss) {
cssText = adaptCssForReplay(cssText, cache);
}
/**
<link> element or dynamic <style> are serialized without any child nodes
we create the text node without an ID or presence in mirror as it can't
*/
styleEl.appendChild(doc.createTextNode(cssText));
}
}

function buildNode(
n: serializedNodeWithId,
options: {
Expand Down Expand Up @@ -237,13 +160,14 @@ function buildNode(
continue;
}

if (typeof value !== 'string') {
// pass
} else if (tagName === 'style' && name === '_cssText') {
buildStyleNode(n, node as HTMLStyleElement, value, options);
continue; // no need to set _cssText as attribute
} else if (tagName === 'textarea' && name === 'value') {
// create without an ID or presence in mirror
const isTextarea = tagName === 'textarea' && name === 'value';
const isRemoteOrDynamicCss = tagName === 'style' && name === '_cssText';
if (isRemoteOrDynamicCss && hackCss && typeof value === 'string') {
value = adaptCssForReplay(value, cache);
}
if ((isTextarea || isRemoteOrDynamicCss) && typeof value === 'string') {
// https://github.com/rrweb-io/rrweb/issues/112
// https://github.com/rrweb-io/rrweb/pull/1351
node.appendChild(doc.createTextNode(value));
try {
n.childNodes = []; // value overrides childNodes
Expand Down Expand Up @@ -403,11 +327,11 @@ function buildNode(
return node;
}
case NodeType.Text:
if (n.isStyle && hackCss) {
// support legacy style
return doc.createTextNode(adaptCssForReplay(n.textContent, cache));
}
return doc.createTextNode(n.textContent);
return doc.createTextNode(
n.isStyle && hackCss
? adaptCssForReplay(n.textContent, cache)
: n.textContent,
);
case NodeType.CDATA:
return doc.createCDATASection(n.textContent);
case NodeType.Comment:
Expand Down
82 changes: 40 additions & 42 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
toLowerCase,
extractFileExtension,
absolutifyURLs,
markCssSplits,
isElementSrcBlocked,
} from './utils';
import dom from '@rrweb/utils';
Expand Down Expand Up @@ -409,7 +408,6 @@ function serializeNode(
* `newlyAddedElement: true` skips scrollTop and scrollLeft check
*/
newlyAddedElement?: boolean;
cssCaptured?: boolean;
/** Highlight Options Start */
privacySetting: PrivacySettingOption;
/** Highlight Options End */
Expand All @@ -431,7 +429,6 @@ function serializeNode(
recordCanvas,
keepIframeSrcFn,
newlyAddedElement = false,
cssCaptured = false,
privacySetting,
} = options;
// Only record root id when document object is not the base document
Expand Down Expand Up @@ -482,7 +479,6 @@ function serializeNode(
maskTextFn,
privacySetting,
rootId,
cssCaptured,
});
case n.CDATA_SECTION_NODE:
return {
Expand Down Expand Up @@ -515,34 +511,42 @@ function serializeTextNode(
maskTextFn: MaskTextFn | undefined;
privacySetting: PrivacySettingOption;
rootId: number | undefined;
cssCaptured?: boolean;
},
): serializedNode {
const { needsMask, maskTextFn, privacySetting, rootId, cssCaptured } =
options;
const { needsMask, maskTextFn, privacySetting, rootId } = options;
// The parent node may not be a html element which has a tagName attribute.
// So just let it be undefined which is ok in this use case.
const parent = dom.parentNode(n);
const parentTagName = parent && (parent as HTMLElement).tagName;
let textContent: string | null = '';
let text = dom.textContent(n);
const isStyle = parentTagName === 'STYLE' ? true : undefined;
const isScript = parentTagName === 'SCRIPT' ? true : undefined;
if (isScript) {
textContent = 'SCRIPT_PLACEHOLDER';
} else if (!cssCaptured) {
textContent = dom.textContent(n);
if (isStyle && textContent) {
// mutation only: we don't need to use stringifyStylesheet
// as a <style> text node mutation obliterates any previous
// programmatic rule manipulation (.insertRule etc.)
// so the current textContent represents the most up to date state
textContent = absolutifyURLs(textContent, getHref(options.doc));
if (isStyle && text) {
try {
// try to read style sheet
if (n.nextSibling || n.previousSibling) {
// This is not the only child of the stylesheet.
// We can't read all of the sheet's .cssRules and expect them
// to _only_ include the current rule(s) added by the text node.
// So we'll be conservative and keep textContent as-is.
} else if ((parent as HTMLStyleElement).sheet?.cssRules) {
text = stringifyStylesheet((parent as HTMLStyleElement).sheet!);
}
} catch (err) {
console.warn(
`Cannot get CSS styles from text's parentNode. Error: ${err as string}`,
n,
);
}
text = absolutifyURLs(text, getHref(options.doc));
}
if (!isStyle && !isScript && textContent && needsMask) {
textContent = maskTextFn
? maskTextFn(textContent, dom.parentElement(n))
: textContent.replace(/[\S]/g, '*');
if (isScript) {
text = 'SCRIPT_PLACEHOLDER';
}
if (!isStyle && !isScript && text && needsMask) {
text = maskTextFn
? maskTextFn(text, dom.parentElement(n))
: text.replace(/[\S]/g, '*');
}

/* Start of Highlight */
Expand All @@ -551,7 +555,7 @@ function serializeTextNode(
const highlightOverwriteRecord =
n.parentElement?.getAttribute('data-hl-record');
const obfuscateDefaultPrivacy =
privacySetting === 'default' && shouldObfuscateTextByDefault(textContent);
privacySetting === 'default' && shouldObfuscateTextByDefault(text);
if (
(enableStrictPrivacy || obfuscateDefaultPrivacy) &&
!highlightOverwriteRecord &&
Expand All @@ -566,15 +570,16 @@ function serializeTextNode(
'BODY',
'NOSCRIPT',
]);
if (!IGNORE_TAG_NAMES.has(parentTagName) && textContent) {
textContent = obfuscateText(textContent);
if (!IGNORE_TAG_NAMES.has(parentTagName) && text) {
text = obfuscateText(text);
}
}
/* End of Highlight */

return {
type: NodeType.Text,
textContent: textContent || '',
textContent: text || '',
isStyle,
rootId,
};
}
Expand Down Expand Up @@ -650,14 +655,17 @@ function serializeElementNode(
attributes._cssText = cssText;
}
}
if (tagName === 'style' && (n as HTMLStyleElement).sheet) {
let cssText = stringifyStylesheet(
// dynamic stylesheet
if (
tagName === 'style' &&
(n as HTMLStyleElement).sheet &&
// TODO: Currently we only try to get dynamic stylesheet when it is an empty style element
!(n.innerText || dom.textContent(n) || '').trim().length
) {
const cssText = stringifyStylesheet(
(n as HTMLStyleElement).sheet as CSSStyleSheet,
);
if (cssText) {
if (n.childNodes.length > 1) {
cssText = markCssSplits(cssText, n as HTMLStyleElement);
}
attributes._cssText = cssText;
}
}
Expand Down Expand Up @@ -1022,7 +1030,6 @@ export function serializeNodeWithId(
node: serializedElementNodeWithId,
) => unknown;
stylesheetLoadTimeout?: number;
cssCaptured?: boolean;
},
): serializedNodeWithId | null {
const {
Expand All @@ -1048,7 +1055,6 @@ export function serializeNodeWithId(
stylesheetLoadTimeout = 5000,
keepIframeSrcFn = () => false,
newlyAddedElement = false,
cssCaptured = false,
privacySetting,
} = options;
let { needsMask } = options;
Expand Down Expand Up @@ -1081,7 +1087,6 @@ export function serializeNodeWithId(
recordCanvas,
keepIframeSrcFn,
newlyAddedElement,
cssCaptured,
privacySetting,
});
if (!_serializedNode) {
Expand All @@ -1098,6 +1103,7 @@ export function serializeNodeWithId(
slimDOMExcluded(_serializedNode, slimDOMOptions) ||
(!preserveWhiteSpace &&
_serializedNode.type === NodeType.Text &&
!_serializedNode.isStyle &&
!_serializedNode.textContent.replace(/^\s+|\s+$/gm, '').length)
) {
id = IGNORED_NODE;
Expand Down Expand Up @@ -1181,7 +1187,6 @@ export function serializeNodeWithId(
onStylesheetLoad,
stylesheetLoadTimeout,
keepIframeSrcFn,
cssCaptured: false,
privacySetting: overwrittenPrivacySetting,
};

Expand All @@ -1192,13 +1197,6 @@ export function serializeNodeWithId(
) {
// value parameter in DOM reflects the correct value, so ignore childNode
} else {
if (
serializedNode.type === NodeType.Element &&
(serializedNode as elementNode).attributes._cssText !== undefined &&
typeof serializedNode.attributes._cssText === 'string'
) {
bypassOptions.cssCaptured = true;
}
for (const childN of Array.from(dom.childNodes(n))) {
const serializedChildNode = serializeNodeWithId(childN, bypassOptions);
if (serializedChildNode) {
Expand Down
22 changes: 2 additions & 20 deletions packages/rrweb-snapshot/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,9 @@ export type documentTypeNode = {
systemId: string;
};

type cssTextKeyAttr = {
_cssText?: string;
export type attributes = {
[key: string]: string | number | true | null;
};

export type attributes = cssTextKeyAttr & {
[key: string]:
| string
| number // properties e.g. rr_scrollLeft or rr_mediaCurrentTime
| true // e.g. checked on <input type="radio">
| null; // an indication that an attribute was removed (during a mutation)
};

export type legacyAttributes = {
/**
* @deprecated old bug in rrweb was causing these to always be set
Expand All @@ -55,10 +46,6 @@ export type elementNode = {
export type textNode = {
type: NodeType.Text;
textContent: string;
/**
* @deprecated styles are now always snapshotted against parent <style> element
* style mutations can still happen via an added textNode, but they don't need this attribute for correct replay
*/
isStyle?: true;
};

Expand Down Expand Up @@ -92,11 +79,6 @@ export type serializedElementNodeWithId = Extract<
Record<'type', NodeType.Element>
>;

export type serializedTextNodeWithId = Extract<
serializedNodeWithId,
Record<'type', NodeType.Text>
>;

export type tagMap = {
[key: string]: string;
};
Expand Down
Loading

0 comments on commit 2701b9d

Please sign in to comment.