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
6 changes: 6 additions & 0 deletions .changeset/adapt-css-in-text-mutations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"rrweb": minor
"@rrweb/replay": minor
---

Add a `adaptCssInTextMutations` player config option (default `true`) so that a consumer which already passes text mutation values through `adaptCssForReplay` can stop the replayer from doing it again. Rewriting is a postcss parse of the whole value, so replaying a stylesheet that is built up over many text mutations otherwise costs one parse of the accumulated CSS per mutation.
8 changes: 7 additions & 1 deletion packages/rrweb/src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export class Replayer {
pauseAnimation: true,
mouseTail: defaultMouseTailConfig,
useVirtualDom: true, // Virtual-dom optimization is enabled by default.
adaptCssInTextMutations: true,
logger: console,
};
this.config = Object.assign({}, defaultConfig, config);
Expand Down Expand Up @@ -1759,7 +1760,12 @@ export class Replayer {
}

const parentEl = target.parentElement as Element | RRElement;
if (mutation.value && parentEl && parentEl.tagName === 'STYLE') {
if (
mutation.value &&
parentEl &&
parentEl.tagName === 'STYLE' &&
this.config.adaptCssInTextMutations
) {
// assumes hackCss: true (which isn't currently configurable from rrweb)
target.textContent = adaptCssForReplay(mutation.value, this.cache);
} else {
Expand Down
15 changes: 15 additions & 0 deletions packages/rrweb/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,21 @@ export type playerConfig = {
};
unpackFn?: UnpackFn;
useVirtualDom: boolean;
/**
* Whether the replayer rewrites CSS for replay (`:hover` -> `.\:hover`,
* `max-device-width` -> `max-width`) when applying a text mutation to a
* child of a `<style>` element. Defaults to `true`.
*
* Set this to `false` when the consumer supplies text mutations whose values
* have already been passed through `adaptCssForReplay`. The rewrite is a
* postcss parse of the whole value, so a consumer that replays a stylesheet
* built up over many text mutations otherwise pays it once per mutation on
* the accumulated CSS, which is quadratic in the number of mutations.
*
* This only covers text mutations. Nodes added via `adds` are always
* rewritten, because their values come straight from the recording.
*/
adaptCssInTextMutations: boolean;
logger: {
log: (...args: Parameters<typeof console.log>) => void;
warn: (...args: Parameters<typeof console.warn>) => void;
Expand Down
96 changes: 96 additions & 0 deletions packages/rrweb/test/events/style-text-mutation-hover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { EventType, IncrementalSource } from '@rrweb/types';
import type { eventWithTime } from '@rrweb/types';

const now = Date.now();

/**
* A `<style>` element whose text node is later replaced by a text mutation,
* with a `:hover` rule in the new value. Used to check whether the replayer
* rewrites CSS in text mutations (see `adaptCssInTextMutations`).
*/
const events: eventWithTime[] = [
{
type: EventType.DomContentLoaded,
data: {},
timestamp: now,
},
{
type: EventType.Load,
data: {},
timestamp: now + 100,
},
{
type: EventType.Meta,
data: {
href: 'http://localhost',
width: 1000,
height: 800,
},
timestamp: now + 100,
},
{
data: {
node: {
id: 1,
type: 0,
childNodes: [
{ id: 2, name: 'html', type: 1, publicId: '', systemId: '' },
{
id: 3,
type: 2,
tagName: 'html',
attributes: { lang: 'en' },
childNodes: [
{
id: 4,
type: 2,
tagName: 'head',
attributes: {},
childNodes: [
{
id: 101,
type: 2,
tagName: 'style',
attributes: {},
childNodes: [
{
id: 102,
type: 3,
isStyle: true,
textContent: '.initial {color: yellow;}',
},
],
},
],
},
{
id: 107,
type: 2,
tagName: 'body',
attributes: {},
childNodes: [],
},
],
},
],
},
initialOffset: { top: 0, left: 0 },
},
type: EventType.FullSnapshot,
timestamp: now + 100,
},
// replaces the stylesheet text with a rule that has a `:hover` selector
{
data: {
texts: [{ id: 102, value: '.mutated:hover {color: red;}' }],
attributes: [],
removes: [],
adds: [],
source: IncrementalSource.Mutation,
},
type: EventType.IncrementalSnapshot,
timestamp: now + 500,
},
];

export default events;
23 changes: 23 additions & 0 deletions packages/rrweb/test/replayer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import shadowDomEvents from './events/shadow-dom';
import badTextareaEvents from './events/bad-textarea';
import badStyleEvents from './events/bad-style';
import StyleSheetTextMutation from './events/style-sheet-text-mutation';
import styleTextMutationHover from './events/style-text-mutation-hover';
import canvasInIframe from './events/canvas-in-iframe';
import adoptedStyleSheet from './events/adopted-style-sheet';
import adoptedStyleSheetModification from './events/adopted-style-sheet-modification';
Expand Down Expand Up @@ -478,6 +479,28 @@ describe('replayer', function () {
expect(result).toEqual(false);
});

it('should adapt css in a text mutation on a style element by default', async () => {
await page.evaluate(`events = ${JSON.stringify(styleTextMutationHover)}`);
const result = await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events);
replayer.pause(600);
replayer.getMirror().getNode(101).textContent;
`);
expect(result).toEqual('.mutated:hover,\n.mutated.\\:hover {color: red;}');
});

it('should not adapt css in a text mutation when adaptCssInTextMutations is false', async () => {
await page.evaluate(`events = ${JSON.stringify(styleTextMutationHover)}`);
const result = await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events, { adaptCssInTextMutations: false });
replayer.pause(600);
replayer.getMirror().getNode(101).textContent;
`);
expect(result).toEqual('.mutated:hover {color: red;}');
});

it('should apply fast-forwarded StyleSheetRules that came after appending text node to stylesheet element', async () => {
await page.evaluate(`events = ${JSON.stringify(StyleSheetTextMutation)}`);
const result = await page.evaluate(`
Expand Down