Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inline stylesheets on load #909

Merged
merged 22 commits into from
Jul 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
42009f3
inline stylesheets when loaded
Juice10 May 31, 2022
b9ddfae
set empty link elements to loaded by default
Juice10 Jun 1, 2022
4c10804
Clean up stylesheet manager
Juice10 Jun 1, 2022
e9f8e06
Remove attribute mutation code
Juice10 Jun 2, 2022
c11e052
Update packages/rrweb/test/record.test.ts
Juice10 Jun 7, 2022
244a26a
Update packages/rrweb/test/record.test.ts
Juice10 Jun 7, 2022
1edb1ea
Update packages/rrweb/test/record.test.ts
Juice10 Jun 7, 2022
45bafbd
Merge branch 'master' of https://github.com/rrweb-io/rrweb into seria…
Juice10 Jun 7, 2022
401c503
Update packages/rrweb/scripts/repl.js
Juice10 Jun 7, 2022
2ec406b
Update packages/rrweb/test/record.test.ts
Juice10 Jun 7, 2022
2e8492e
Update packages/rrweb/src/record/index.ts
Juice10 Jun 7, 2022
217bd7c
Add todo
Juice10 Jun 7, 2022
c0371f0
Move require out of time sensitive assert
Juice10 Jun 7, 2022
fd38d43
Merge branch 'serialize-stylesheet-contents' of https://github.com/rr…
Juice10 Jun 7, 2022
dee14a6
Add waitForRAF, its more reliable than waitForTimeout
Juice10 Jun 7, 2022
1965152
Remove flaky tests
Juice10 Jun 7, 2022
8ff7bc6
Add recording stylesheets in iframes
Juice10 Jun 7, 2022
d5a83be
Remove variability from flaky test
Juice10 Jun 7, 2022
7e3a1a8
Make test more robust
Juice10 Jun 7, 2022
8c7a38f
Fix naming
Juice10 Jun 27, 2022
6878711
Merge branch 'master' of https://github.com/rrweb-io/rrweb into seria…
Juice10 Jun 27, 2022
71a0853
Merge branch 'master' into serialize-stylesheet-contents
Juice10 Jun 30, 2022
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: 2 additions & 4 deletions packages/rrdom-nodejs/test/polyfill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
polyfillNode,
polyfillDocument,
} from '../src/polyfill';
import { performance as nativePerformance } from 'perf_hooks';

describe('polyfill for nodejs', () => {
it('should polyfill performance api', () => {
Expand All @@ -16,10 +17,7 @@ describe('polyfill for nodejs', () => {
expect(global.performance).toBeDefined();
expect(performance).toBeDefined();
expect(performance.now).toBeDefined();
expect(performance.now()).toBeCloseTo(
require('perf_hooks').performance.now(),
1e-10,
);
expect(performance.now()).toBeCloseTo(nativePerformance.now(), 1e-10);
});

it('should not polyfill performance if it already exists', () => {
Expand Down
114 changes: 110 additions & 4 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
MaskInputFn,
KeepIframeSrcFn,
ICanvas,
serializedElementNodeWithId,
} from './types';
import {
Mirror,
Expand Down Expand Up @@ -377,6 +378,40 @@ function onceIframeLoaded(
iframeEl.addEventListener('load', listener);
}

function isStylesheetLoaded(link: HTMLLinkElement) {
if (!link.getAttribute('href')) return true; // nothing to load
return link.sheet !== null;
}

function onceStylesheetLoaded(
link: HTMLLinkElement,
listener: () => unknown,
styleSheetLoadTimeout: number,
) {
let fired = false;
let styleSheetLoaded: StyleSheet | null;
try {
styleSheetLoaded = link.sheet;
} catch (error) {
return;
}

if (styleSheetLoaded) return;

const timer = setTimeout(() => {
if (!fired) {
listener();
fired = true;
}
}, styleSheetLoadTimeout);

link.addEventListener('load', () => {
clearTimeout(timer);
fired = true;
listener();
});
}

function serializeNode(
n: Node,
options: {
Expand Down Expand Up @@ -876,6 +911,7 @@ export function serializeNodeWithId(
maskTextSelector: string | null;
skipChild: boolean;
inlineStylesheet: boolean;
newlyAddedElement?: boolean;
maskInputOptions?: MaskInputOptions;
maskTextFn: MaskTextFn | undefined;
maskInputFn: MaskInputFn | undefined;
Expand All @@ -888,10 +924,14 @@ export function serializeNodeWithId(
onSerialize?: (n: Node) => unknown;
onIframeLoad?: (
iframeNode: HTMLIFrameElement,
node: serializedNodeWithId,
node: serializedElementNodeWithId,
) => unknown;
iframeLoadTimeout?: number;
newlyAddedElement?: boolean;
onStylesheetLoad?: (
linkNode: HTMLLinkElement,
node: serializedElementNodeWithId,
) => unknown;
stylesheetLoadTimeout?: number;
},
): serializedNodeWithId | null {
const {
Expand All @@ -913,6 +953,8 @@ export function serializeNodeWithId(
onSerialize,
onIframeLoad,
iframeLoadTimeout = 5000,
onStylesheetLoad,
stylesheetLoadTimeout = 5000,
keepIframeSrcFn = () => false,
newlyAddedElement = false,
} = options;
Expand Down Expand Up @@ -1006,6 +1048,8 @@ export function serializeNodeWithId(
onSerialize,
onIframeLoad,
iframeLoadTimeout,
onStylesheetLoad,
stylesheetLoadTimeout,
keepIframeSrcFn,
};
for (const childN of Array.from(n.childNodes)) {
Expand Down Expand Up @@ -1059,18 +1103,71 @@ export function serializeNodeWithId(
onSerialize,
onIframeLoad,
iframeLoadTimeout,
onStylesheetLoad,
stylesheetLoadTimeout,
keepIframeSrcFn,
});

if (serializedIframeNode) {
onIframeLoad(n as HTMLIFrameElement, serializedIframeNode);
onIframeLoad(
n as HTMLIFrameElement,
serializedIframeNode as serializedElementNodeWithId,
);
}
}
},
iframeLoadTimeout,
);
}

// <link rel=stylesheet href=...>
if (
serializedNode.type === NodeType.Element &&
serializedNode.tagName === 'link' &&
serializedNode.attributes.rel === 'stylesheet'
) {
onceStylesheetLoaded(
n as HTMLLinkElement,
() => {
if (onStylesheetLoad) {
const serializedLinkNode = serializeNodeWithId(n, {
doc,
mirror,
blockClass,
blockSelector,
maskTextClass,
maskTextSelector,
skipChild: false,
inlineStylesheet,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Juice10 I think this whole if could have been false, if inlineStylesheet is false?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's very much possible. To be honest it's a while ago so I'm not sure if there was a deliberate intention to call this even if inlineStylesheet was false, but looking back at it I don't think so

Copy link
Contributor

@eoghanmurray eoghanmurray May 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like the only upshot of serializeNodeWithId here is to extract out the _cssText from the .sheet (as this link element has already been serialized). Would it be fair to refactor based on that insight? I'll be doing some work with assets related to this.

In the rrweb callback it checks for ._cssText on the serialized node, but also maybe there was an intention to track future stylesheet modifications:

  // TODO: take snapshot on stylesheet reload by applying event listener
  private trackStylesheetInLinkElement(linkEl: HTMLLinkElement) {

maskInputOptions,
maskTextFn,
maskInputFn,
slimDOMOptions,
dataURLOptions,
inlineImages,
recordCanvas,
preserveWhiteSpace,
onSerialize,
onIframeLoad,
iframeLoadTimeout,
onStylesheetLoad,
stylesheetLoadTimeout,
keepIframeSrcFn,
});

if (serializedLinkNode) {
onStylesheetLoad(
n as HTMLLinkElement,
serializedLinkNode as serializedElementNodeWithId,
);
}
}
},
stylesheetLoadTimeout,
);
if (isStylesheetLoaded(n as HTMLLinkElement) === false) return null; // add stylesheet in later mutation
}

return serializedNode;
}

Expand All @@ -1094,9 +1191,14 @@ function snapshot(
onSerialize?: (n: Node) => unknown;
onIframeLoad?: (
iframeNode: HTMLIFrameElement,
node: serializedNodeWithId,
node: serializedElementNodeWithId,
) => unknown;
iframeLoadTimeout?: number;
onStylesheetLoad?: (
linkNode: HTMLLinkElement,
node: serializedElementNodeWithId,
) => unknown;
stylesheetLoadTimeout?: number;
keepIframeSrcFn?: KeepIframeSrcFn;
},
): serializedNodeWithId | null {
Expand All @@ -1118,6 +1220,8 @@ function snapshot(
onSerialize,
onIframeLoad,
iframeLoadTimeout,
onStylesheetLoad,
stylesheetLoadTimeout,
keepIframeSrcFn = () => false,
} = options || {};
const maskInputOptions: MaskInputOptions =
Expand Down Expand Up @@ -1183,6 +1287,8 @@ function snapshot(
onSerialize,
onIframeLoad,
iframeLoadTimeout,
onStylesheetLoad,
stylesheetLoadTimeout,
keepIframeSrcFn,
newlyAddedElement: false,
});
Expand Down
5 changes: 5 additions & 0 deletions packages/rrweb-snapshot/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ export type serializedNode = (

export type serializedNodeWithId = serializedNode & { id: number };

export type serializedElementNodeWithId = Extract<
serializedNodeWithId,
Record<'type', NodeType.Element>
>;

export type tagMap = {
[key: string]: string;
};
Expand Down
12 changes: 8 additions & 4 deletions packages/rrweb-snapshot/typings/snapshot.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { serializedNodeWithId, MaskInputOptions, SlimDOMOptions, DataURLOptions, MaskTextFn, MaskInputFn, KeepIframeSrcFn } from './types';
import { serializedNodeWithId, MaskInputOptions, SlimDOMOptions, DataURLOptions, MaskTextFn, MaskInputFn, KeepIframeSrcFn, serializedElementNodeWithId } from './types';
import { Mirror } from './utils';
export declare const IGNORED_NODE = -2;
export declare function absoluteToStylesheet(cssText: string | null, href: string): string;
Expand All @@ -16,6 +16,7 @@ export declare function serializeNodeWithId(n: Node, options: {
maskTextSelector: string | null;
skipChild: boolean;
inlineStylesheet: boolean;
newlyAddedElement?: boolean;
maskInputOptions?: MaskInputOptions;
maskTextFn: MaskTextFn | undefined;
maskInputFn: MaskInputFn | undefined;
Expand All @@ -26,9 +27,10 @@ export declare function serializeNodeWithId(n: Node, options: {
recordCanvas?: boolean;
preserveWhiteSpace?: boolean;
onSerialize?: (n: Node) => unknown;
onIframeLoad?: (iframeNode: HTMLIFrameElement, node: serializedNodeWithId) => unknown;
onIframeLoad?: (iframeNode: HTMLIFrameElement, node: serializedElementNodeWithId) => unknown;
iframeLoadTimeout?: number;
newlyAddedElement?: boolean;
onStylesheetLoad?: (linkNode: HTMLLinkElement, node: serializedElementNodeWithId) => unknown;
stylesheetLoadTimeout?: number;
}): serializedNodeWithId | null;
declare function snapshot(n: Document, options?: {
mirror?: Mirror;
Expand All @@ -46,8 +48,10 @@ declare function snapshot(n: Document, options?: {
recordCanvas?: boolean;
preserveWhiteSpace?: boolean;
onSerialize?: (n: Node) => unknown;
onIframeLoad?: (iframeNode: HTMLIFrameElement, node: serializedNodeWithId) => unknown;
onIframeLoad?: (iframeNode: HTMLIFrameElement, node: serializedElementNodeWithId) => unknown;
iframeLoadTimeout?: number;
onStylesheetLoad?: (linkNode: HTMLLinkElement, node: serializedElementNodeWithId) => unknown;
stylesheetLoadTimeout?: number;
keepIframeSrcFn?: KeepIframeSrcFn;
}): serializedNodeWithId | null;
export declare function visitSnapshot(node: serializedNodeWithId, onVisit: (node: serializedNodeWithId) => unknown): void;
Expand Down
1 change: 1 addition & 0 deletions packages/rrweb-snapshot/typings/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export declare type serializedNode = (documentNode | documentTypeNode | elementN
export declare type serializedNodeWithId = serializedNode & {
id: number;
};
export declare type serializedElementNodeWithId = Extract<serializedNodeWithId, Record<'type', NodeType.Element>>;
export declare type tagMap = {
[key: string]: string;
};
Expand Down
14 changes: 14 additions & 0 deletions packages/rrweb/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
polyfill,
hasShadowRoot,
isSerializedIframe,
isSerializedStylesheet,
} from '../utils';
import {
EventType,
Expand All @@ -27,6 +28,7 @@ import {
import { IframeManager } from './iframe-manager';
import { ShadowDomManager } from './shadow-dom-manager';
import { CanvasManager } from './observers/canvas/canvas-manager';
import { StylesheetManager } from './stylesheet-manager';

function wrapEvent(e: event): eventWithTime {
return {
Expand Down Expand Up @@ -215,6 +217,10 @@ function record<T = eventWithTime>(
mutationCb: wrappedMutationEmit,
});

const stylesheetManager = new StylesheetManager({
mutationCb: wrappedMutationEmit,
});

const canvasManager = new CanvasManager({
recordCanvas,
mutationCb: wrappedCanvasMutationEmit,
Expand All @@ -241,6 +247,7 @@ function record<T = eventWithTime>(
sampling,
slimDOMOptions,
iframeManager,
stylesheetManager,
canvasManager,
},
mirror,
Expand Down Expand Up @@ -276,6 +283,9 @@ function record<T = eventWithTime>(
if (isSerializedIframe(n, mirror)) {
iframeManager.addIframe(n as HTMLIFrameElement);
}
if (isSerializedStylesheet(n, mirror)) {
stylesheetManager.addStylesheet(n as HTMLLinkElement);
}
if (hasShadowRoot(n)) {
shadowDomManager.addShadowRoot(n.shadowRoot, document);
}
Expand All @@ -284,6 +294,9 @@ function record<T = eventWithTime>(
iframeManager.attachIframe(iframe, childSn, mirror);
shadowDomManager.observeAttachShadow(iframe);
},
onStylesheetLoad: (linkEl, childSn) => {
stylesheetManager.attachStylesheet(linkEl, childSn, mirror);
},
keepIframeSrcFn,
});

Expand Down Expand Up @@ -435,6 +448,7 @@ function record<T = eventWithTime>(
slimDOMOptions,
mirror,
iframeManager,
stylesheetManager,
shadowDomManager,
canvasManager,
plugins:
Expand Down
Loading