Monkeypatch each iframe (#716)
* `setTimeout` and `clearTimeout` are global functions. Think the window versions of them were for the following reason: https://stackoverflow.com/questions/60245787/ * Comments and extra test here helped me understand which inserts were expected and which are to be ignored * Add a test for the style setProperty/removeProperty added in #671 * Add a test to ensure that listeners get added correctly in nested iframes - particularly important for those which rely on prototype monkeypatching * Pass in the window object from the current iframe so that monkeypatching applies to all windows * Satisfy typings * No need to insert an iframe as there's one already set up for us * Enable the console logger to also intercept log messages within iframes * There's no tests for FontFace but presumably the monkeypatching here works similarly to the others
This commit is contained in:
@@ -22,7 +22,7 @@ type LogRecordOptions = {
|
||||
level?: LogLevel[];
|
||||
lengthThreshold?: number;
|
||||
stringifyOptions?: StringifyOptions;
|
||||
logger?: Logger;
|
||||
logger?: Logger | string;
|
||||
};
|
||||
|
||||
const defaultLogOptions: LogRecordOptions = {
|
||||
@@ -48,7 +48,7 @@ const defaultLogOptions: LogRecordOptions = {
|
||||
'warn',
|
||||
],
|
||||
lengthThreshold: 1000,
|
||||
logger: console,
|
||||
logger: 'console',
|
||||
};
|
||||
|
||||
export type LogData = {
|
||||
@@ -106,12 +106,19 @@ export type Logger = {
|
||||
|
||||
function initLogObserver(
|
||||
cb: logCallback,
|
||||
win: Window, // top window or in an iframe
|
||||
logOptions: LogRecordOptions,
|
||||
): listenerHandler {
|
||||
const logger = logOptions.logger;
|
||||
if (!logger) {
|
||||
const loggerType = logOptions.logger;
|
||||
if (!loggerType) {
|
||||
return () => {};
|
||||
}
|
||||
let logger: Logger;
|
||||
if (typeof loggerType === 'string') {
|
||||
logger = (win as any)[loggerType];
|
||||
} else {
|
||||
logger = loggerType;
|
||||
}
|
||||
let logCount = 0;
|
||||
const cancelHandlers: listenerHandler[] = [];
|
||||
// add listener to thrown errors
|
||||
|
||||
@@ -519,10 +519,11 @@ function getNestedCSSRulePositions(rule: CSSRule): number[] {
|
||||
|
||||
function initStyleSheetObserver(
|
||||
cb: styleSheetRuleCallback,
|
||||
win: Window,
|
||||
mirror: Mirror,
|
||||
): listenerHandler {
|
||||
const insertRule = CSSStyleSheet.prototype.insertRule;
|
||||
CSSStyleSheet.prototype.insertRule = function (rule: string, index?: number) {
|
||||
const insertRule = (win as any).CSSStyleSheet.prototype.insertRule;
|
||||
(win as any).CSSStyleSheet.prototype.insertRule = function (rule: string, index?: number) {
|
||||
const id = mirror.getId(this.ownerNode as INode);
|
||||
if (id !== -1) {
|
||||
cb({
|
||||
@@ -533,8 +534,8 @@ function initStyleSheetObserver(
|
||||
return insertRule.apply(this, arguments);
|
||||
};
|
||||
|
||||
const deleteRule = CSSStyleSheet.prototype.deleteRule;
|
||||
CSSStyleSheet.prototype.deleteRule = function (index: number) {
|
||||
const deleteRule = (win as any).CSSStyleSheet.prototype.deleteRule;
|
||||
(win as any).CSSStyleSheet.prototype.deleteRule = function (index: number) {
|
||||
const id = mirror.getId(this.ownerNode as INode);
|
||||
if (id !== -1) {
|
||||
cb({
|
||||
@@ -549,20 +550,20 @@ function initStyleSheetObserver(
|
||||
[key: string]: GroupingCSSRuleTypes;
|
||||
} = {};
|
||||
if (isCSSGroupingRuleSupported) {
|
||||
supportedNestedCSSRuleTypes['CSSGroupingRule'] = CSSGroupingRule;
|
||||
supportedNestedCSSRuleTypes['CSSGroupingRule'] = (win as any).CSSGroupingRule;
|
||||
} else {
|
||||
// Some browsers (Safari) don't support CSSGroupingRule
|
||||
// https://caniuse.com/?search=cssgroupingrule
|
||||
// fall back to monkey patching classes that would have inherited from CSSGroupingRule
|
||||
|
||||
if (isCSSMediaRuleSupported) {
|
||||
supportedNestedCSSRuleTypes['CSSMediaRule'] = CSSMediaRule;
|
||||
supportedNestedCSSRuleTypes['CSSMediaRule'] = (win as any).CSSMediaRule;
|
||||
}
|
||||
if (isCSSConditionRuleSupported) {
|
||||
supportedNestedCSSRuleTypes['CSSConditionRule'] = CSSConditionRule;
|
||||
supportedNestedCSSRuleTypes['CSSConditionRule'] = (win as any).CSSConditionRule;
|
||||
}
|
||||
if (isCSSSupportsRuleSupported) {
|
||||
supportedNestedCSSRuleTypes['CSSSupportsRule'] = CSSSupportsRule;
|
||||
supportedNestedCSSRuleTypes['CSSSupportsRule'] = (win as any).CSSSupportsRule;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,8 +612,8 @@ function initStyleSheetObserver(
|
||||
});
|
||||
|
||||
return () => {
|
||||
CSSStyleSheet.prototype.insertRule = insertRule;
|
||||
CSSStyleSheet.prototype.deleteRule = deleteRule;
|
||||
(win as any).CSSStyleSheet.prototype.insertRule = insertRule;
|
||||
(win as any).CSSStyleSheet.prototype.deleteRule = deleteRule;
|
||||
Object.entries(supportedNestedCSSRuleTypes).forEach(([typeKey, type]) => {
|
||||
type.prototype.insertRule = unmodifiedFunctions[typeKey].insertRule;
|
||||
type.prototype.deleteRule = unmodifiedFunctions[typeKey].deleteRule;
|
||||
@@ -622,14 +623,15 @@ function initStyleSheetObserver(
|
||||
|
||||
function initStyleDeclarationObserver(
|
||||
cb: styleDeclarationCallback,
|
||||
win: Window,
|
||||
mirror: Mirror,
|
||||
): listenerHandler {
|
||||
const setProperty = CSSStyleDeclaration.prototype.setProperty;
|
||||
CSSStyleDeclaration.prototype.setProperty = function (
|
||||
const setProperty = (win as any).CSSStyleDeclaration.prototype.setProperty;
|
||||
(win as any).CSSStyleDeclaration.prototype.setProperty = function (
|
||||
this: CSSStyleDeclaration,
|
||||
property,
|
||||
value,
|
||||
priority,
|
||||
property: string,
|
||||
value: string,
|
||||
priority: string,
|
||||
) {
|
||||
const id = mirror.getId(
|
||||
(this.parentRule?.parentStyleSheet?.ownerNode as unknown) as INode,
|
||||
@@ -648,10 +650,10 @@ function initStyleDeclarationObserver(
|
||||
return setProperty.apply(this, arguments);
|
||||
};
|
||||
|
||||
const removeProperty = CSSStyleDeclaration.prototype.removeProperty;
|
||||
CSSStyleDeclaration.prototype.removeProperty = function (
|
||||
const removeProperty = (win as any).CSSStyleDeclaration.prototype.removeProperty;
|
||||
(win as any).CSSStyleDeclaration.prototype.removeProperty = function (
|
||||
this: CSSStyleDeclaration,
|
||||
property,
|
||||
property: string,
|
||||
) {
|
||||
const id = mirror.getId(
|
||||
(this.parentRule?.parentStyleSheet?.ownerNode as unknown) as INode,
|
||||
@@ -669,8 +671,8 @@ function initStyleDeclarationObserver(
|
||||
};
|
||||
|
||||
return () => {
|
||||
CSSStyleDeclaration.prototype.setProperty = setProperty;
|
||||
CSSStyleDeclaration.prototype.removeProperty = removeProperty;
|
||||
(win as any).CSSStyleDeclaration.prototype.setProperty = setProperty;
|
||||
(win as any).CSSStyleDeclaration.prototype.removeProperty = removeProperty;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -702,22 +704,23 @@ function initMediaInteractionObserver(
|
||||
|
||||
function initCanvasMutationObserver(
|
||||
cb: canvasMutationCallback,
|
||||
win: Window,
|
||||
blockClass: blockClass,
|
||||
mirror: Mirror,
|
||||
): listenerHandler {
|
||||
const props = Object.getOwnPropertyNames(CanvasRenderingContext2D.prototype);
|
||||
const props = Object.getOwnPropertyNames((win as any).CanvasRenderingContext2D.prototype);
|
||||
const handlers: listenerHandler[] = [];
|
||||
for (const prop of props) {
|
||||
try {
|
||||
if (
|
||||
typeof CanvasRenderingContext2D.prototype[
|
||||
typeof (win as any).CanvasRenderingContext2D.prototype[
|
||||
prop as keyof CanvasRenderingContext2D
|
||||
] !== 'function'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const restoreHandler = patch(
|
||||
CanvasRenderingContext2D.prototype,
|
||||
(win as any).CanvasRenderingContext2D.prototype,
|
||||
prop,
|
||||
function (original) {
|
||||
return function (
|
||||
@@ -758,7 +761,7 @@ function initCanvasMutationObserver(
|
||||
handlers.push(restoreHandler);
|
||||
} catch {
|
||||
const hookHandler = hookSetter<CanvasRenderingContext2D>(
|
||||
CanvasRenderingContext2D.prototype,
|
||||
(win as any).CanvasRenderingContext2D.prototype,
|
||||
prop,
|
||||
{
|
||||
set(v) {
|
||||
@@ -779,14 +782,19 @@ function initCanvasMutationObserver(
|
||||
};
|
||||
}
|
||||
|
||||
function initFontObserver(cb: fontCallback): listenerHandler {
|
||||
function initFontObserver(
|
||||
cb: fontCallback,
|
||||
doc: Document,
|
||||
): listenerHandler {
|
||||
const win = doc.defaultView;
|
||||
|
||||
const handlers: listenerHandler[] = [];
|
||||
|
||||
const fontMap = new WeakMap<FontFace, fontParam>();
|
||||
|
||||
const originalFontFace = FontFace;
|
||||
const originalFontFace = (win as any).FontFace;
|
||||
// tslint:disable-next-line: no-any
|
||||
(window as any).FontFace = function FontFace(
|
||||
(win as any).FontFace = function FontFace(
|
||||
family: string,
|
||||
source: string | ArrayBufferView,
|
||||
descriptors?: FontFaceDescriptors,
|
||||
@@ -805,7 +813,7 @@ function initFontObserver(cb: fontCallback): listenerHandler {
|
||||
return fontFace;
|
||||
};
|
||||
|
||||
const restoreHandler = patch(document.fonts, 'add', function (original) {
|
||||
const restoreHandler = patch(doc.fonts, 'add', function (original) {
|
||||
return function (this: FontFaceSet, fontFace: FontFace) {
|
||||
setTimeout(() => {
|
||||
const p = fontMap.get(fontFace);
|
||||
@@ -820,7 +828,7 @@ function initFontObserver(cb: fontCallback): listenerHandler {
|
||||
|
||||
handlers.push(() => {
|
||||
// tslint:disable-next-line: no-any
|
||||
(window as any).FontFace = originalFontFace;
|
||||
(win as any).FontFace = originalFontFace;
|
||||
});
|
||||
handlers.push(restoreHandler);
|
||||
|
||||
@@ -971,22 +979,35 @@ export function initObservers(
|
||||
o.blockClass,
|
||||
o.mirror,
|
||||
);
|
||||
|
||||
const currentWindow = o.doc.defaultView as Window; // basically document.window
|
||||
|
||||
const styleSheetObserver = initStyleSheetObserver(
|
||||
o.styleSheetRuleCb,
|
||||
currentWindow,
|
||||
o.mirror,
|
||||
);
|
||||
const styleDeclarationObserver = initStyleDeclarationObserver(
|
||||
o.styleDeclarationCb,
|
||||
currentWindow,
|
||||
o.mirror,
|
||||
);
|
||||
const canvasMutationObserver = o.recordCanvas
|
||||
? initCanvasMutationObserver(o.canvasMutationCb, o.blockClass, o.mirror)
|
||||
: () => {};
|
||||
const fontObserver = o.collectFonts ? initFontObserver(o.fontCb) : () => {};
|
||||
? initCanvasMutationObserver(
|
||||
o.canvasMutationCb,
|
||||
currentWindow,
|
||||
o.blockClass,
|
||||
o.mirror,
|
||||
) : () => {};
|
||||
const fontObserver = o.collectFonts ? initFontObserver(o.fontCb, o.doc) : () => {};
|
||||
// plugins
|
||||
const pluginHandlers: listenerHandler[] = [];
|
||||
for (const plugin of o.plugins) {
|
||||
pluginHandlers.push(plugin.observer(plugin.callback, plugin.options));
|
||||
pluginHandlers.push(plugin.observer(
|
||||
plugin.callback,
|
||||
currentWindow,
|
||||
plugin.options,
|
||||
));
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -759,7 +759,7 @@ export class Replayer {
|
||||
const head = this.iframe.contentDocument?.head;
|
||||
if (head) {
|
||||
const unloadSheets: Set<HTMLLinkElement> = new Set();
|
||||
let timer: number;
|
||||
let timer: ReturnType<typeof setTimeout> | -1;
|
||||
let beforeLoadState = this.service.state;
|
||||
const stateHandler = () => {
|
||||
beforeLoadState = this.service.state;
|
||||
@@ -784,7 +784,7 @@ export class Replayer {
|
||||
}
|
||||
this.emitter.emit(ReplayerEvents.LoadStylesheetEnd);
|
||||
if (timer) {
|
||||
window.clearTimeout(timer);
|
||||
clearTimeout(timer);
|
||||
}
|
||||
unsubscribe();
|
||||
}
|
||||
@@ -796,7 +796,7 @@ export class Replayer {
|
||||
// find some unload sheets after iterate
|
||||
this.service.send({ type: 'PAUSE' });
|
||||
this.emitter.emit(ReplayerEvents.LoadStylesheetStart);
|
||||
timer = window.setTimeout(() => {
|
||||
timer = setTimeout(() => {
|
||||
if (beforeLoadState.matches('playing')) {
|
||||
this.play(this.getCurrentTime());
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ export type SamplingStrategy = Partial<{
|
||||
|
||||
export type RecordPlugin<TOptions = unknown> = {
|
||||
name: string;
|
||||
observer: (cb: Function, options: TOptions) => listenerHandler;
|
||||
observer: (cb: Function, win: Window, options: TOptions) => listenerHandler;
|
||||
options: TOptions;
|
||||
};
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ export function throttle<T>(
|
||||
wait: number,
|
||||
options: throttleOptions = {},
|
||||
) {
|
||||
let timeout: number | null = null;
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let previous = 0;
|
||||
// tslint:disable-next-line: only-arrow-functions
|
||||
return function (arg: T) {
|
||||
@@ -124,13 +124,13 @@ export function throttle<T>(
|
||||
let args = arguments;
|
||||
if (remaining <= 0 || remaining > wait) {
|
||||
if (timeout) {
|
||||
window.clearTimeout(timeout);
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
previous = now;
|
||||
func.apply(context, args);
|
||||
} else if (!timeout && options.trailing !== false) {
|
||||
timeout = window.setTimeout(() => {
|
||||
timeout = setTimeout(() => {
|
||||
previous = options.leading === false ? 0 : Date.now();
|
||||
timeout = null;
|
||||
func.apply(context, args);
|
||||
|
||||
@@ -3347,6 +3347,88 @@ exports[`log 1`] = `
|
||||
\\"payload\\": []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 16,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"iframe\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 21
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 21,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 0,
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"html\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"head\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"rootId\\": 22,
|
||||
\\"id\\": 24
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"body\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"rootId\\": 22,
|
||||
\\"id\\": 25
|
||||
}
|
||||
],
|
||||
\\"rootId\\": 22,
|
||||
\\"id\\": 23
|
||||
}
|
||||
],
|
||||
\\"id\\": 22
|
||||
}
|
||||
}
|
||||
],
|
||||
\\"removes\\": [],
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"isAttachIframe\\": true
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 6,
|
||||
\\"data\\": {
|
||||
\\"plugin\\": \\"rrweb/console@1\\",
|
||||
\\"payload\\": {
|
||||
\\"level\\": \\"log\\",
|
||||
\\"trace\\": [],
|
||||
\\"payload\\": [
|
||||
\\"\\\\\\"from iframe\\\\\\"\\"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]"
|
||||
`;
|
||||
|
||||
@@ -329,6 +329,234 @@ exports[`custom-event 1`] = `
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`iframe-stylesheet-mutations 1`] = `
|
||||
"[
|
||||
{
|
||||
\\"type\\": 4,
|
||||
\\"data\\": {
|
||||
\\"href\\": \\"about:blank\\",
|
||||
\\"width\\": 1920,
|
||||
\\"height\\": 1080
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"data\\": {
|
||||
\\"node\\": {
|
||||
\\"type\\": 0,
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"html\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"head\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 3
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"body\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 5
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"iframe\\",
|
||||
\\"attributes\\": {
|
||||
\\"srcdoc\\": \\"<button>Mysterious Button</button>\\"
|
||||
},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n </body>\\\\n </html>\\\\n \\",
|
||||
\\"id\\": 7
|
||||
}
|
||||
],
|
||||
\\"id\\": 6
|
||||
}
|
||||
],
|
||||
\\"id\\": 4
|
||||
}
|
||||
],
|
||||
\\"id\\": 2
|
||||
}
|
||||
],
|
||||
\\"id\\": 1
|
||||
},
|
||||
\\"initialOffset\\": {
|
||||
\\"left\\": 0,
|
||||
\\"top\\": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 6,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 0,
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"html\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"head\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"rootId\\": 8,
|
||||
\\"id\\": 10
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"body\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"button\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"Mysterious Button\\",
|
||||
\\"rootId\\": 8,
|
||||
\\"id\\": 13
|
||||
}
|
||||
],
|
||||
\\"rootId\\": 8,
|
||||
\\"id\\": 12
|
||||
}
|
||||
],
|
||||
\\"rootId\\": 8,
|
||||
\\"id\\": 11
|
||||
}
|
||||
],
|
||||
\\"rootId\\": 8,
|
||||
\\"id\\": 9
|
||||
}
|
||||
],
|
||||
\\"id\\": 8
|
||||
}
|
||||
}
|
||||
],
|
||||
\\"removes\\": [],
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"isAttachIframe\\": true
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 10,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"style\\",
|
||||
\\"attributes\\": {
|
||||
\\"_cssText\\": \\"body { background: rgb(0, 0, 0); }@media {\\\\n body { background: rgb(0, 0, 0); }\\\\n}\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"rootId\\": 8,
|
||||
\\"id\\": 14
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 8,
|
||||
\\"id\\": 14,
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"rule\\": \\"body { color: #fff; }\\"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 8,
|
||||
\\"id\\": 14,
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"rule\\": \\"body { color: #ccc; }\\",
|
||||
\\"index\\": [
|
||||
2,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 8,
|
||||
\\"id\\": 14,
|
||||
\\"removes\\": [
|
||||
{
|
||||
\\"index\\": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 13,
|
||||
\\"id\\": 14,
|
||||
\\"set\\": {
|
||||
\\"property\\": \\"color\\",
|
||||
\\"value\\": \\"green\\"
|
||||
},
|
||||
\\"index\\": [
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 8,
|
||||
\\"id\\": 14,
|
||||
\\"removes\\": [
|
||||
{
|
||||
\\"index\\": [
|
||||
1,
|
||||
0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`nested-stylesheet-rules 1`] = `
|
||||
"[
|
||||
{
|
||||
@@ -471,6 +699,128 @@ exports[`nested-stylesheet-rules 1`] = `
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`stylesheet-properties 1`] = `
|
||||
"[
|
||||
{
|
||||
\\"type\\": 4,
|
||||
\\"data\\": {
|
||||
\\"href\\": \\"about:blank\\",
|
||||
\\"width\\": 1920,
|
||||
\\"height\\": 1080
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"data\\": {
|
||||
\\"node\\": {
|
||||
\\"type\\": 0,
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"html\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"head\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 3
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"body\\",
|
||||
\\"attributes\\": {},
|
||||
\\"childNodes\\": [
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\",
|
||||
\\"id\\": 5
|
||||
},
|
||||
{
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"input\\",
|
||||
\\"attributes\\": {
|
||||
\\"type\\": \\"text\\",
|
||||
\\"size\\": \\"40\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 6
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
|
||||
\\"id\\": 7
|
||||
}
|
||||
],
|
||||
\\"id\\": 4
|
||||
}
|
||||
],
|
||||
\\"id\\": 2
|
||||
}
|
||||
],
|
||||
\\"id\\": 1
|
||||
},
|
||||
\\"initialOffset\\": {
|
||||
\\"left\\": 0,
|
||||
\\"top\\": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 0,
|
||||
\\"texts\\": [],
|
||||
\\"attributes\\": [],
|
||||
\\"removes\\": [],
|
||||
\\"adds\\": [
|
||||
{
|
||||
\\"parentId\\": 3,
|
||||
\\"nextId\\": null,
|
||||
\\"node\\": {
|
||||
\\"type\\": 2,
|
||||
\\"tagName\\": \\"style\\",
|
||||
\\"attributes\\": {
|
||||
\\"_cssText\\": \\"body { background: rgb(0, 0, 0); }\\"
|
||||
},
|
||||
\\"childNodes\\": [],
|
||||
\\"id\\": 8
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 13,
|
||||
\\"id\\": 8,
|
||||
\\"set\\": {
|
||||
\\"property\\": \\"color\\",
|
||||
\\"value\\": \\"green\\"
|
||||
},
|
||||
\\"index\\": [
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
\\"type\\": 3,
|
||||
\\"data\\": {
|
||||
\\"source\\": 13,
|
||||
\\"id\\": 8,
|
||||
\\"remove\\": {
|
||||
\\"property\\": \\"background\\"
|
||||
},
|
||||
\\"index\\": [
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
]"
|
||||
`;
|
||||
|
||||
exports[`stylesheet-rules 1`] = `
|
||||
"[
|
||||
{
|
||||
|
||||
@@ -471,8 +471,15 @@ describe('record integration tests', function (this: ISuite) {
|
||||
console.trace('trace');
|
||||
console.warn('warn');
|
||||
console.clear();
|
||||
const iframe = document.createElement('iframe');
|
||||
document.body.appendChild(iframe);
|
||||
});
|
||||
|
||||
await page.frames()[1].evaluate(() => {
|
||||
console.log('from iframe');
|
||||
});
|
||||
|
||||
|
||||
const snapshots = await page.evaluate('window.snapshots');
|
||||
assertSnapshot(snapshots, __filename, 'log');
|
||||
});
|
||||
|
||||
@@ -220,9 +220,11 @@ describe('record', function (this: ISuite) {
|
||||
document.head.appendChild(styleElement);
|
||||
|
||||
const styleSheet = <CSSStyleSheet>styleElement.sheet;
|
||||
// begin: pre-serialization
|
||||
const ruleIdx0 = styleSheet.insertRule('body { background: #000; }');
|
||||
const ruleIdx1 = styleSheet.insertRule('body { background: #111; }');
|
||||
styleSheet.deleteRule(ruleIdx1);
|
||||
// end: pre-serialization
|
||||
setTimeout(() => {
|
||||
styleSheet.insertRule('body { color: #fff; }');
|
||||
}, 0);
|
||||
@@ -239,14 +241,15 @@ describe('record', function (this: ISuite) {
|
||||
e.type === EventType.IncrementalSnapshot &&
|
||||
e.data.source === IncrementalSource.StyleSheetRule,
|
||||
);
|
||||
const addRuleCount = styleSheetRuleEvents.filter((e) =>
|
||||
const addRules = styleSheetRuleEvents.filter((e) =>
|
||||
Boolean((e.data as styleSheetRuleData).adds),
|
||||
).length;
|
||||
);
|
||||
const removeRuleCount = styleSheetRuleEvents.filter((e) =>
|
||||
Boolean((e.data as styleSheetRuleData).removes),
|
||||
).length;
|
||||
// sync insert/delete should be ignored
|
||||
expect(addRuleCount).to.equal(2);
|
||||
// pre-serialization insert/delete should be ignored
|
||||
expect(addRules.length).to.equal(2);
|
||||
expect((addRules[0].data as styleSheetRuleData).adds).to.deep.include({rule: "body { color: #fff; }"});
|
||||
expect(removeRuleCount).to.equal(1);
|
||||
assertSnapshot(this.events, __filename, 'stylesheet-rules');
|
||||
});
|
||||
@@ -311,6 +314,30 @@ describe('record', function (this: ISuite) {
|
||||
});
|
||||
it('captures nested stylesheet rules', captureNestedStylesheetRulesTest);
|
||||
});
|
||||
|
||||
it('captures style property changes', async () => {
|
||||
await this.page.evaluate(() => {
|
||||
const { record } = ((window as unknown) as IWindow).rrweb;
|
||||
|
||||
record({
|
||||
emit: ((window as unknown) as IWindow).emit,
|
||||
});
|
||||
|
||||
const styleElement = document.createElement('style');
|
||||
document.head.appendChild(styleElement);
|
||||
|
||||
const styleSheet = <CSSStyleSheet>styleElement.sheet;
|
||||
styleSheet.insertRule('body { background: #000; }');
|
||||
setTimeout(() => {
|
||||
(styleSheet.cssRules[0] as CSSStyleRule).style.setProperty('color', 'green');
|
||||
(styleSheet.cssRules[0] as CSSStyleRule).style.removeProperty('background');
|
||||
}, 0);
|
||||
});
|
||||
await this.page.waitForTimeout(50);
|
||||
assertSnapshot(this.events, __filename, 'stylesheet-properties');
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe('record iframes', function (this: ISuite) {
|
||||
@@ -350,4 +377,60 @@ describe('record iframes', function (this: ISuite) {
|
||||
EventType.IncrementalSnapshot,
|
||||
]);
|
||||
});
|
||||
|
||||
it('captures stylesheet mutations in iframes', async () => {
|
||||
await this.page.evaluate(() => {
|
||||
const { record } = ((window as unknown) as IWindow).rrweb;
|
||||
record({
|
||||
// need to reference window.top for when we are in an iframe!
|
||||
emit: ((window.top as unknown) as IWindow).emit,
|
||||
});
|
||||
|
||||
const iframe = document.querySelector('iframe');
|
||||
// outer timeout is needed to wait for initStyleSheetObserver on iframe to be set up
|
||||
setTimeout(() => {
|
||||
|
||||
const idoc = (iframe as HTMLIFrameElement).contentDocument!;
|
||||
const styleElement = idoc.createElement('style');
|
||||
|
||||
idoc.head.appendChild(styleElement);
|
||||
|
||||
const styleSheet = <CSSStyleSheet>styleElement.sheet;
|
||||
styleSheet.insertRule('@media {}');
|
||||
const atMediaRule = styleSheet.cssRules[0] as CSSMediaRule;
|
||||
const atRuleIdx0 = atMediaRule.insertRule('body { background: #000; }', 0);
|
||||
const ruleIdx0 = styleSheet.insertRule('body { background: #000; }'); // inserted before above
|
||||
// pre-serialization insert/delete above should be ignored
|
||||
setTimeout(() => {
|
||||
styleSheet.insertRule('body { color: #fff; }');
|
||||
atMediaRule.insertRule('body { color: #ccc; }', 0);
|
||||
}, 0);
|
||||
setTimeout(() => {
|
||||
styleSheet.deleteRule(ruleIdx0);
|
||||
(styleSheet.cssRules[0] as CSSStyleRule).style.setProperty('color', 'green');
|
||||
}, 5);
|
||||
setTimeout(() =>{
|
||||
atMediaRule.deleteRule(atRuleIdx0);
|
||||
}, 10);
|
||||
}, 10);
|
||||
});
|
||||
await this.page.waitForTimeout(50);
|
||||
const styleRelatedEvents = this.events.filter(
|
||||
(e) =>
|
||||
e.type === EventType.IncrementalSnapshot &&
|
||||
(e.data.source === IncrementalSource.StyleSheetRule ||
|
||||
e.data.source === IncrementalSource.StyleDeclaration),
|
||||
);
|
||||
const addRuleCount = styleRelatedEvents.filter((e) =>
|
||||
Boolean((e.data as styleSheetRuleData).adds),
|
||||
).length;
|
||||
const removeRuleCount = styleRelatedEvents.filter((e) =>
|
||||
Boolean((e.data as styleSheetRuleData).removes),
|
||||
).length;
|
||||
expect(styleRelatedEvents.length).to.equal(5);
|
||||
expect(addRuleCount).to.equal(2);
|
||||
expect(removeRuleCount).to.equal(2);
|
||||
assertSnapshot(this.events, __filename, 'iframe-stylesheet-mutations');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ declare type LogRecordOptions = {
|
||||
level?: LogLevel[];
|
||||
lengthThreshold?: number;
|
||||
stringifyOptions?: StringifyOptions;
|
||||
logger?: Logger;
|
||||
logger?: Logger | string;
|
||||
};
|
||||
export declare type LogData = {
|
||||
level: LogLevel;
|
||||
|
||||
2
packages/rrweb/typings/types.d.ts
vendored
2
packages/rrweb/typings/types.d.ts
vendored
@@ -126,7 +126,7 @@ export declare type SamplingStrategy = Partial<{
|
||||
}>;
|
||||
export declare type RecordPlugin<TOptions = unknown> = {
|
||||
name: string;
|
||||
observer: (cb: Function, options: TOptions) => listenerHandler;
|
||||
observer: (cb: Function, win: Window, options: TOptions) => listenerHandler;
|
||||
options: TOptions;
|
||||
};
|
||||
export declare type recordOptions<T> = {
|
||||
|
||||
Reference in New Issue
Block a user