Skip to content
Merged
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
31 changes: 31 additions & 0 deletions .changeset/stylesheet-rule-resync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"rrweb": minor
---

Recover CSS rules when another script displaces the `insertRule` patch.

Everything a CSS-in-JS library (emotion, styled-components, goober) adds at
runtime reached the replayer only through the monkey patch on
`CSSStyleSheet.prototype.insertRule`, because the `<style>` elements they own hold
no text and so have no other copy of their contents beyond the `_cssText` taken
when the element was serialized. Any other script that reinstalls a previously
captured `insertRule` — a session replay vendor tearing its own recorder down, or
a second copy of rrweb bundled into the same app — unhooks us silently, and the
replay is then frozen with whatever CSS existed at snapshot time. A production
session was observed keeping 734 of 2929 emotion rules that way, which rendered a
MUI-heavy UI completely unstyled.

Recording now periodically checks that its patch still runs (by inserting into a
throwaway constructed stylesheet, since function identity cannot distinguish a
wrapper that still calls through from one that replaced us), reinstalls it over
whatever displaced it, and re-sends the rules the replayer is missing. Controlled
by the new `styleSheetResyncInterval` record option, in milliseconds; `0` disables
it, default `2000`. The emitted events are ordinary `StyleSheetRule` mutations, so
existing replayers need no changes.

Teardown no longer restores these CSSOM methods unconditionally: it only unwinds
its own layer, so stopping a recorder can no longer blind another script that
patched on top of it. A patch that has to stay installed for that reason falls
silent once recording has stopped, and only the outermost of the recorder's own
patches reports a call, so a method reinstalled over an earlier patch of ours
cannot report the same rule twice.
2 changes: 2 additions & 0 deletions packages/rrweb/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
keepIframeSrcFn = () => false,
privacySetting = 'default',
ignoreCSSAttributes = new Set([]),
styleSheetResyncInterval = 2000,
errorHandler,
logger,
} = options;
Expand Down Expand Up @@ -382,40 +383,40 @@
shadowDomManager.init();

mutationBuffers.forEach((buf) => buf.lock()); // don't allow any mirror modifications during snapshotting
const node = snapshot(document, {

Check warning on line 386 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Check and Report Upload

Unsafe assignment of an `any` value
mirror,
blockClass,
blockSelector,
maskTextClass,
maskTextSelector,
inlineStylesheet,
maskAllInputs: maskInputOptions,
maskTextFn,
maskInputFn,
slimDOM: slimDOMOptions,
dataURLOptions,
recordCanvas,
inlineImages,
inlineVideos,
privacySetting,
onSerialize: (n) => {
if (isSerializedIframe(n, mirror)) {
iframeManager.addIframe(n as HTMLIFrameElement);
}
if (isSerializedStylesheet(n, mirror)) {
stylesheetManager.trackLinkElement(n as HTMLLinkElement);
}
if (hasShadowRoot(n)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
shadowDomManager.addShadowRoot(dom.shadowRoot(n as Node)!, document);
}
},
onIframeLoad: (iframe, childSn) => {
iframeManager.attachIframe(iframe, childSn);

Check warning on line 415 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Check and Report Upload

Unsafe argument of type `serializedElementNodeWithId` assigned to a parameter of type `serializedNodeWithId`

Check warning on line 415 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/record/index.ts#L415

[@typescript-eslint/no-unsafe-argument] Unsafe argument of type `serializedElementNodeWithId` assigned to a parameter of type `serializedNodeWithId`.
shadowDomManager.observeAttachShadow(iframe);
},
onStylesheetLoad: (linkEl, childSn) => {
stylesheetManager.attachLinkElement(linkEl, childSn);

Check warning on line 419 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Check and Report Upload

Unsafe argument of type `serializedElementNodeWithId` assigned to a parameter of type `serializedNodeWithId`

Check warning on line 419 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/record/index.ts#L419

[@typescript-eslint/no-unsafe-argument] Unsafe argument of type `serializedElementNodeWithId` assigned to a parameter of type `serializedNodeWithId`.
},
keepIframeSrcFn,
});

Check warning on line 422 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/record/index.ts#L386-L422

[@typescript-eslint/no-unsafe-assignment] Unsafe assignment of an `any` value.
Expand All @@ -428,7 +429,7 @@
{
type: EventType.FullSnapshot,
data: {
node,

Check warning on line 432 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Check and Report Upload

Unsafe assignment of an `any` value

Check warning on line 432 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/record/index.ts#L432

[@typescript-eslint/no-unsafe-assignment] Unsafe assignment of an `any` value.
initialOffset: getWindowScroll(window),
},
},
Expand Down Expand Up @@ -563,12 +564,13 @@
processedNodeManager,
canvasManager,
ignoreCSSAttributes,
styleSheetResyncInterval,
privacySetting,
plugins:
plugins
?.filter((p) => p.observer)
?.map((p) => ({
observer: p.observer!,

Check warning on line 573 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Check and Report Upload

Forbidden non-null assertion

Check warning on line 573 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/record/index.ts#L573

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
options: p.options,
callback: (payload: object) =>
wrappedEmit({
Expand All @@ -586,7 +588,7 @@

iframeManager.addLoadListener((iframeEl) => {
try {
handlers.push(observe(iframeEl.contentDocument!));

Check warning on line 591 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Check and Report Upload

Forbidden non-null assertion

Check warning on line 591 in packages/rrweb/src/record/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/record/index.ts#L591

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
} catch (error) {
// TODO: handle internal error
console.warn(error);
Expand Down
Loading
Loading