record canvas mutations (#296)

* record canvas mutations

close #60, #261

This patch implements the canvas mutation observer.
It consists of both the record and the replay side changes.

In the record side, we add a `recordCanvas` flag to indicate
whether to record canvas elements and the flag defaults to false.
Different from our other observers, the canvas observer was
disabled by default. Because some applications with heavy canvas
usage may emit a lot of data as canvas changed, especially the
scenarios that use a lot of `drawImage` API.
So the behavior should be audited by users and only record canvas
when the flag was set to true.

In the replay side, we add a `UNSAFE_replayCanvas` flag to indicate
whether to replay canvas mutations.
Similar to the `recordCanvas` flag, `UNSAFE_replayCanvas` defaults
to false. But unlike the record canvas implementation is stable and
safe, the replay canvas implementation is UNSAFE.
It's unsafe because we need to add `allow-scripts` to the replay
sandbox, which may cause some unexpected script execution. Currently,
users should be aware of this implementation detail and enable this
feature carefully.

* update canvas integration test
This commit is contained in:
yz-yu
2020-08-22 16:44:02 +08:00
committed by GitHub
parent a9719e302e
commit 772c0e021a
11 changed files with 528 additions and 31 deletions

View File

@@ -41,6 +41,7 @@ function record<T = eventWithTime>(
packFn,
sampling = {},
mousemoveWait,
recordCanvas = false,
} = options;
// runtime checks for user options
if (!emit) {
@@ -113,6 +114,7 @@ function record<T = eventWithTime>(
blockClass,
inlineStylesheet,
maskInputOptions,
recordCanvas,
);
if (!node) {
@@ -244,11 +246,22 @@ function record<T = eventWithTime>(
},
}),
),
canvasMutationCb: (p) =>
wrappedEmit(
wrapEvent({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.CanvasMutation,
...p,
},
}),
),
blockClass,
ignoreClass,
maskInputOptions,
inlineStylesheet,
sampling,
recordCanvas,
},
hooks,
),

View File

@@ -138,16 +138,19 @@ export default class MutationBuffer {
private blockClass: blockClass;
private inlineStylesheet: boolean;
private maskInputOptions: MaskInputOptions;
private recordCanvas: boolean;
constructor(
cb: mutationCallBack,
blockClass: blockClass,
inlineStylesheet: boolean,
maskInputOptions: MaskInputOptions,
recordCanvas: boolean,
) {
this.blockClass = blockClass;
this.inlineStylesheet = inlineStylesheet;
this.maskInputOptions = maskInputOptions;
this.recordCanvas = recordCanvas;
this.emissionCallback = cb;
}
@@ -187,6 +190,7 @@ export default class MutationBuffer {
true,
this.inlineStylesheet,
this.maskInputOptions,
this.recordCanvas,
)!,
});
};

View File

@@ -8,6 +8,7 @@ import {
getWindowWidth,
isBlocked,
isTouchEvent,
patch,
} from '../utils';
import {
mutationCallBack,
@@ -30,6 +31,7 @@ import {
mediaInteractionCallback,
MediaInteractions,
SamplingStrategy,
canvasMutationCallback,
} from '../types';
import MutationBuffer from './mutation';
@@ -38,6 +40,7 @@ function initMutationObserver(
blockClass: blockClass,
inlineStylesheet: boolean,
maskInputOptions: MaskInputOptions,
recordCanvas: boolean,
): MutationObserver {
// see mutation.ts for details
const mutationBuffer = new MutationBuffer(
@@ -45,6 +48,7 @@ function initMutationObserver(
blockClass,
inlineStylesheet,
maskInputOptions,
recordCanvas,
);
const observer = new MutationObserver(mutationBuffer.processMutations);
observer.observe(document, {
@@ -357,6 +361,75 @@ function initMediaInteractionObserver(
};
}
function initCanvasMutationObserver(
cb: canvasMutationCallback,
blockClass: blockClass,
): listenerHandler {
const props = Object.getOwnPropertyNames(CanvasRenderingContext2D.prototype);
const handlers: listenerHandler[] = [];
for (const prop of props) {
try {
if (
typeof CanvasRenderingContext2D.prototype[
prop as keyof CanvasRenderingContext2D
] !== 'function'
) {
continue;
}
const restoreHandler = patch(
CanvasRenderingContext2D.prototype,
prop,
function (original) {
return function (
this: CanvasRenderingContext2D,
...args: Array<unknown>
) {
if (!isBlocked(this.canvas, blockClass)) {
setTimeout(() => {
const recordArgs = [...args];
if (prop === 'drawImage') {
if (
recordArgs[0] &&
recordArgs[0] instanceof HTMLCanvasElement
) {
recordArgs[0] = recordArgs[0].toDataURL();
}
}
cb({
id: mirror.getId((this.canvas as unknown) as INode),
property: prop,
args: recordArgs,
});
}, 0);
}
return original.apply(this, args);
};
},
);
handlers.push(restoreHandler);
} catch {
const hookHandler = hookSetter<CanvasRenderingContext2D>(
CanvasRenderingContext2D.prototype,
prop,
{
set(v) {
cb({
id: mirror.getId((this.canvas as unknown) as INode),
property: prop,
args: [v],
setter: true,
});
},
},
);
handlers.push(hookHandler);
}
}
return () => {
handlers.forEach((h) => h());
};
}
function mergeHooks(o: observerParam, hooks: hooksParam) {
const {
mutationCb,
@@ -367,6 +440,7 @@ function mergeHooks(o: observerParam, hooks: hooksParam) {
inputCb,
mediaInteractionCb,
styleSheetRuleCb,
canvasMutationCb,
} = o;
o.mutationCb = (...p: Arguments<mutationCallBack>) => {
if (hooks.mutation) {
@@ -416,6 +490,12 @@ function mergeHooks(o: observerParam, hooks: hooksParam) {
}
styleSheetRuleCb(...p);
};
o.canvasMutationCb = (...p: Arguments<canvasMutationCallback>) => {
if (hooks.canvasMutation) {
hooks.canvasMutation(...p);
}
canvasMutationCb(...p);
};
}
export default function initObservers(
@@ -428,6 +508,7 @@ export default function initObservers(
o.blockClass,
o.inlineStylesheet,
o.maskInputOptions,
o.recordCanvas,
);
const mousemoveHandler = initMoveObserver(o.mousemoveCb, o.sampling);
const mouseInteractionHandler = initMouseInteractionObserver(
@@ -453,6 +534,9 @@ export default function initObservers(
o.blockClass,
);
const styleSheetObserver = initStyleSheetObserver(o.styleSheetRuleCb);
const canvasMutationObserver = o.recordCanvas
? initCanvasMutationObserver(o.canvasMutationCb, o.blockClass)
: () => {};
return () => {
mutationObserver.disconnect();
@@ -463,5 +547,6 @@ export default function initObservers(
inputHandler();
mediaInteractionHandler();
styleSheetObserver();
canvasMutationObserver();
};
}