Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Inline stylesheets on load (#909)
* inline stylesheets when loaded

* set empty link elements to loaded by default

* Clean up stylesheet manager

* Remove attribute mutation code

* Update packages/rrweb/test/record.test.ts

* Update packages/rrweb/test/record.test.ts

* Update packages/rrweb/test/record.test.ts

* Update packages/rrweb/scripts/repl.js

* Update packages/rrweb/test/record.test.ts

* Update packages/rrweb/src/record/index.ts

* Add todo

* Move require out of time sensitive assert

* Add waitForRAF, its more reliable than waitForTimeout

* Remove flaky tests

* Add recording stylesheets in iframes

* Remove variability from flaky test

* Make test more robust

* Fix naming
  • Loading branch information
Juice10 authored Jul 1, 2022
commit d5d877e3808870daefb0dc5ad2ae3e1073098564
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,
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