From e9335db86fab6104a18742f21981a16cdfccdf8e Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Tue, 9 Nov 2021 13:34:07 +0100
Subject: [PATCH 01/93] Add very basic webgl support
---
packages/rrweb/package.json | 2 +
packages/rrweb/src/record/observer.ts | 88 ++-----
.../rrweb/src/record/observers/canvas-2d.ts | 94 ++++++++
.../src/record/observers/canvas-web-gl.ts | 75 ++++++
packages/rrweb/src/replay/index.ts | 12 +-
packages/rrweb/src/types.ts | 6 +
.../__snapshots__/integration.test.ts.snap | 221 ++++++++++++++++++
packages/rrweb/test/events/webgl.ts | 120 ++++++++++
packages/rrweb/test/html/canvas-webgl.html | 27 +++
packages/rrweb/test/integration.test.ts | 13 ++
...uld-doutput-simple-webgl-object-1-snap.png | Bin 0 -> 10796 bytes
packages/rrweb/test/replay/webgl.test.ts | 66 ++++++
packages/rrweb/tsconfig.json | 6 +-
.../typings/record/observers/canvas-2d.d.ts | 2 +
.../record/observers/canvas-web-gl.d.ts | 2 +
packages/rrweb/typings/types.d.ts | 5 +
yarn.lock | 67 +++++-
17 files changed, 730 insertions(+), 76 deletions(-)
create mode 100644 packages/rrweb/src/record/observers/canvas-2d.ts
create mode 100644 packages/rrweb/src/record/observers/canvas-web-gl.ts
create mode 100644 packages/rrweb/test/events/webgl.ts
create mode 100644 packages/rrweb/test/html/canvas-webgl.html
create mode 100644 packages/rrweb/test/replay/__image_snapshots__/webgl-test-ts-replayer-webgl-should-doutput-simple-webgl-object-1-snap.png
create mode 100644 packages/rrweb/test/replay/webgl.test.ts
create mode 100644 packages/rrweb/typings/record/observers/canvas-2d.d.ts
create mode 100644 packages/rrweb/typings/record/observers/canvas-web-gl.d.ts
diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json
index deb06da85d..1e6cb8cb40 100644
--- a/packages/rrweb/package.json
+++ b/packages/rrweb/package.json
@@ -46,6 +46,7 @@
"@types/chai": "^4.1.6",
"@types/inquirer": "0.0.43",
"@types/jest": "^27.0.2",
+ "@types/jest-image-snapshot": "^4.3.1",
"@types/jsdom": "^16.2.12",
"@types/node": "^12.20.16",
"@types/prettier": "^2.3.2",
@@ -55,6 +56,7 @@
"ignore-styles": "^5.0.1",
"inquirer": "^6.2.1",
"jest": "^27.2.4",
+ "jest-image-snapshot": "^4.5.1",
"jest-snapshot": "^23.6.0",
"jsdom": "^17.0.0",
"jsdom-global": "^3.0.2",
diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts
index 26cd1ef75f..31134a0ac0 100644
--- a/packages/rrweb/src/record/observer.ts
+++ b/packages/rrweb/src/record/observer.ts
@@ -49,6 +49,8 @@ import {
import MutationBuffer from './mutation';
import { IframeManager } from './iframe-manager';
import { ShadowDomManager } from './shadow-dom-manager';
+import initCanvas2DMutationObserver from './observers/canvas-2d';
+import initCanvasWebGLMutationObserver from './observers/canvas-web-gl';
type WindowWithStoredMutationObserver = IWindow & {
__rrMutationObserver?: MutationObserver;
@@ -712,79 +714,23 @@ function initCanvasMutationObserver(
blockClass: blockClass,
mirror: Mirror,
): listenerHandler {
- const props = Object.getOwnPropertyNames(
- win.CanvasRenderingContext2D.prototype,
+ const canvas2DReset = initCanvas2DMutationObserver(
+ cb,
+ win,
+ blockClass,
+ mirror,
);
- const handlers: listenerHandler[] = [];
- for (const prop of props) {
- try {
- if (
- typeof win.CanvasRenderingContext2D.prototype[
- prop as keyof CanvasRenderingContext2D
- ] !== 'function'
- ) {
- continue;
- }
- const restoreHandler = patch(
- win.CanvasRenderingContext2D.prototype,
- prop,
- function (original) {
- return function (
- this: CanvasRenderingContext2D,
- ...args: Array
- ) {
- if (!isBlocked(this.canvas, blockClass)) {
- setTimeout(() => {
- const recordArgs = [...args];
- if (prop === 'drawImage') {
- if (
- recordArgs[0] &&
- recordArgs[0] instanceof HTMLCanvasElement
- ) {
- const canvas = recordArgs[0];
- const ctx = canvas.getContext('2d');
- let imgd = ctx?.getImageData(
- 0,
- 0,
- canvas.width,
- canvas.height,
- );
- let pix = imgd?.data;
- recordArgs[0] = JSON.stringify(pix);
- }
- }
- 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(
- win.CanvasRenderingContext2D.prototype,
- prop,
- {
- set(v) {
- cb({
- id: mirror.getId((this.canvas as unknown) as INode),
- property: prop,
- args: [v],
- setter: true,
- });
- },
- },
- );
- handlers.push(hookHandler);
- }
- }
+
+ const canvasWebGLReset = initCanvasWebGLMutationObserver(
+ cb,
+ win,
+ blockClass,
+ mirror,
+ );
+
return () => {
- handlers.forEach((h) => h());
+ canvas2DReset();
+ canvasWebGLReset();
};
}
diff --git a/packages/rrweb/src/record/observers/canvas-2d.ts b/packages/rrweb/src/record/observers/canvas-2d.ts
new file mode 100644
index 0000000000..0aa30af391
--- /dev/null
+++ b/packages/rrweb/src/record/observers/canvas-2d.ts
@@ -0,0 +1,94 @@
+import { INode } from 'rrweb-snapshot';
+import {
+ blockClass,
+ CanvasContext,
+ canvasMutationCallback,
+ IWindow,
+ listenerHandler,
+ Mirror,
+} from '../../types';
+import { hookSetter, isBlocked, patch } from '../../utils';
+
+export default function initCanvas2DMutationObserver(
+ cb: canvasMutationCallback,
+ win: IWindow,
+ blockClass: blockClass,
+ mirror: Mirror,
+): listenerHandler {
+ const handlers: listenerHandler[] = [];
+ const props2D = Object.getOwnPropertyNames(
+ win.CanvasRenderingContext2D.prototype,
+ );
+ for (const prop of props2D) {
+ try {
+ if (
+ typeof win.CanvasRenderingContext2D.prototype[
+ prop as keyof CanvasRenderingContext2D
+ ] !== 'function'
+ ) {
+ continue;
+ }
+ const restoreHandler = patch(
+ win.CanvasRenderingContext2D.prototype,
+ prop,
+ function (original) {
+ return function (
+ this: CanvasRenderingContext2D,
+ ...args: Array
+ ) {
+ if (!isBlocked((this.canvas as unknown) as INode, blockClass)) {
+ setTimeout(() => {
+ const recordArgs = [...args];
+ if (prop === 'drawImage') {
+ if (
+ recordArgs[0] &&
+ recordArgs[0] instanceof HTMLCanvasElement
+ ) {
+ const canvas = recordArgs[0];
+ const ctx = canvas.getContext('2d');
+ let imgd = ctx?.getImageData(
+ 0,
+ 0,
+ canvas.width,
+ canvas.height,
+ );
+ let pix = imgd?.data;
+ recordArgs[0] = JSON.stringify(pix);
+ }
+ }
+ cb({
+ id: mirror.getId((this.canvas as unknown) as INode),
+ type: CanvasContext['2D'],
+ property: prop,
+ args: recordArgs,
+ });
+ }, 0);
+ }
+ return original.apply(this, args);
+ };
+ },
+ );
+ handlers.push(restoreHandler);
+ } catch {
+ const hookHandler = hookSetter(
+ win.CanvasRenderingContext2D.prototype,
+ prop,
+ {
+ set(v) {
+ cb({
+ id: mirror.getId((this.canvas as unknown) as INode),
+ type: CanvasContext['2D'],
+ property: prop,
+ args: [v],
+ setter: true,
+ });
+ },
+ },
+ );
+ handlers.push(hookHandler);
+ }
+ }
+ return () => {
+ handlers.forEach((h) => h());
+ };
+}
diff --git a/packages/rrweb/src/record/observers/canvas-web-gl.ts b/packages/rrweb/src/record/observers/canvas-web-gl.ts
new file mode 100644
index 0000000000..498b012f55
--- /dev/null
+++ b/packages/rrweb/src/record/observers/canvas-web-gl.ts
@@ -0,0 +1,75 @@
+import { INode } from 'rrweb-snapshot';
+import {
+ blockClass,
+ CanvasContext,
+ canvasMutationCallback,
+ IWindow,
+ listenerHandler,
+ Mirror,
+} from '../../types';
+import { hookSetter, isBlocked, patch } from '../../utils';
+
+export default function initCanvasWebGLMutationObserver(
+ cb: canvasMutationCallback,
+ win: IWindow,
+ blockClass: blockClass,
+ mirror: Mirror,
+): listenerHandler {
+ const handlers: listenerHandler[] = [];
+ const props = Object.getOwnPropertyNames(win.WebGLRenderingContext.prototype);
+ for (const prop of props) {
+ try {
+ if (
+ typeof win.WebGLRenderingContext.prototype[
+ prop as keyof WebGLRenderingContext
+ ] !== 'function'
+ ) {
+ continue;
+ }
+ const restoreHandler = patch(
+ win.WebGLRenderingContext.prototype,
+ prop,
+ function (original) {
+ return function (
+ this: WebGLRenderingContext,
+ ...args: Array
+ ) {
+ if (!isBlocked((this.canvas as unknown) as INode, blockClass)) {
+ setTimeout(() => {
+ const recordArgs = [...args];
+ cb({
+ id: mirror.getId((this.canvas as unknown) as INode),
+ type: CanvasContext.WebGL,
+ property: prop,
+ args: recordArgs,
+ });
+ }, 0);
+ }
+ return original.apply(this, args);
+ };
+ },
+ );
+ handlers.push(restoreHandler);
+ } catch {
+ const hookHandler = hookSetter(
+ win.WebGLRenderingContext.prototype,
+ prop,
+ {
+ set(v) {
+ cb({
+ id: mirror.getId((this.canvas as unknown) as INode),
+ type: CanvasContext.WebGL,
+ property: prop,
+ args: [v],
+ setter: true,
+ });
+ },
+ },
+ );
+ handlers.push(hookHandler);
+ }
+ }
+ return () => {
+ handlers.forEach((h) => h());
+ };
+}
diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts
index 59a7009c30..372e057205 100644
--- a/packages/rrweb/src/replay/index.ts
+++ b/packages/rrweb/src/replay/index.ts
@@ -39,6 +39,7 @@ import {
styleValueWithPriority,
mouseMovePos,
IWindow,
+ CanvasContext,
} from '../types';
import {
createMirror,
@@ -1203,9 +1204,15 @@ export class Replayer {
if (!target) {
return this.debugNodeNotFound(d, d.id);
}
+ let contextType: '2d' | 'webgl' = '2d';
try {
+ if (d.type === CanvasContext['2D']) {
+ contextType = '2d';
+ } else if (d.type === CanvasContext.WebGL) {
+ contextType = 'webgl';
+ }
const ctx = ((target as unknown) as HTMLCanvasElement).getContext(
- '2d',
+ contextType,
)!;
if (d.setter) {
// skip some read-only type checks
@@ -1214,8 +1221,9 @@ export class Replayer {
return;
}
const original = ctx[
- d.property as keyof CanvasRenderingContext2D
+ d.property as Exclude
] as Function;
+
/**
* We have serialized the image source into base64 string during recording,
* which has been preloaded before replay.
diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts
index 4f881ecb26..866fa858c0 100644
--- a/packages/rrweb/src/types.ts
+++ b/packages/rrweb/src/types.ts
@@ -381,6 +381,11 @@ export enum MouseInteractions {
TouchCancel,
}
+export enum CanvasContext {
+ '2D',
+ WebGL,
+}
+
type mouseInteractionParam = {
type: MouseInteractions;
id: number;
@@ -434,6 +439,7 @@ export type canvasMutationCallback = (p: canvasMutationParam) => void;
export type canvasMutationParam = {
id: number;
+ type: CanvasContext;
property: string;
args: Array;
setter?: true;
diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap
index df33590626..4e7c341c6b 100644
--- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap
+++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap
@@ -7316,6 +7316,7 @@ exports[`record integration tests should record canvas mutations 1`] = `
\\"data\\": {
\\"source\\": 9,
\\"id\\": 16,
+ \\"type\\": 0,
\\"property\\": \\"moveTo\\",
\\"args\\": [
0,
@@ -7328,6 +7329,7 @@ exports[`record integration tests should record canvas mutations 1`] = `
\\"data\\": {
\\"source\\": 9,
\\"id\\": 16,
+ \\"type\\": 0,
\\"property\\": \\"lineTo\\",
\\"args\\": [
200,
@@ -7340,6 +7342,7 @@ exports[`record integration tests should record canvas mutations 1`] = `
\\"data\\": {
\\"source\\": 9,
\\"id\\": 16,
+ \\"type\\": 0,
\\"property\\": \\"stroke\\",
\\"args\\": []
}
@@ -9523,6 +9526,224 @@ exports[`record integration tests should record shadow DOM 1`] = `
]"
`;
+exports[`record integration tests should record webgl canvas mutations 1`] = `
+"[
+ {
+ \\"type\\": 0,
+ \\"data\\": {}
+ },
+ {
+ \\"type\\": 1,
+ \\"data\\": {}
+ },
+ {
+ \\"type\\": 4,
+ \\"data\\": {
+ \\"href\\": \\"about:blank\\",
+ \\"width\\": 1920,
+ \\"height\\": 1080
+ }
+ },
+ {
+ \\"type\\": 2,
+ \\"data\\": {
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"id\\": 2
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {
+ \\"lang\\": \\"en\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 5
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"charset\\": \\"UTF-8\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 6
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 7
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"meta\\",
+ \\"attributes\\": {
+ \\"name\\": \\"viewport\\",
+ \\"content\\": \\"width=device-width, initial-scale=1.0\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 8
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 9
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"title\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"canvas\\",
+ \\"id\\": 11
+ }
+ ],
+ \\"id\\": 10
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 12
+ }
+ ],
+ \\"id\\": 4
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 13
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 15
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"canvas\\",
+ \\"attributes\\": {
+ \\"id\\": \\"myCanvas\\",
+ \\"width\\": \\"200\\",
+ \\"height\\": \\"100\\",
+ \\"style\\": \\"border: 1px solid #000000\\",
+ \\"rr_dataURL\\": \\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAYAAADDhn8LAAACnklEQVR4Xu3VsRHAMAzEsHj/pTOBXbB9pFchyLycz0eAwFXgsCFA4C4gEK+DwENAIJ4HAYF4AwSagD9IczM1IiCQkUNbswkIpLmZGhEQyMihrdkEBNLcTI0ICGTk0NZsAgJpbqZGBAQycmhrNgGBNDdTIwICGTm0NZuAQJqbqREBgYwc2ppNQCDNzdSIgEBGDm3NJiCQ5mZqREAgI4e2ZhMQSHMzNSIgkJFDW7MJCKS5mRoREMjIoa3ZBATS3EyNCAhk5NDWbAICaW6mRgQEMnJoazYBgTQ3UyMCAhk5tDWbgECam6kRAYGMHNqaTUAgzc3UiIBARg5tzSYgkOZmakRAICOHtmYTEEhzMzUiIJCRQ1uzCQikuZkaERDIyKGt2QQE0txMjQgIZOTQ1mwCAmlupkYEBDJyaGs2AYE0N1MjAgIZObQ1m4BAmpupEQGBjBzamk1AIM3N1IiAQEYObc0mIJDmZmpEQCAjh7ZmExBIczM1IiCQkUNbswkIpLmZGhEQyMihrdkEBNLcTI0ICGTk0NZsAgJpbqZGBAQycmhrNgGBNDdTIwICGTm0NZuAQJqbqREBgYwc2ppNQCDNzdSIgEBGDm3NJiCQ5mZqREAgI4e2ZhMQSHMzNSIgkJFDW7MJCKS5mRoREMjIoa3ZBATS3EyNCAhk5NDWbAICaW6mRgQEMnJoazYBgTQ3UyMCAhk5tDWbgECam6kRAYGMHNqaTUAgzc3UiIBARg5tzSYgkOZmakRAICOHtmYTEEhzMzUiIJCRQ1uzCQikuZkaERDIyKGt2QQE0txMjQgIZOTQ1mwCAmlupkYEBDJyaGs2AYE0N1MjAgIZObQ1m4BAmpupEQGBjBzamk1AIM3N1IiAQEYObc0mIJDmZmpE4Af1gABlH0hlGgAAAABJRU5ErkJggg==\\"
+ },
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 17
+ }
+ ],
+ \\"id\\": 16
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 18
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"id\\": 20
+ }
+ ],
+ \\"id\\": 19
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\",
+ \\"id\\": 21
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"script\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
+ \\"id\\": 23
+ }
+ ],
+ \\"id\\": 22
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\\\n\\\\n\\",
+ \\"id\\": 24
+ }
+ ],
+ \\"id\\": 14
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 9,
+ \\"id\\": 16,
+ \\"type\\": 1,
+ \\"property\\": \\"clearColor\\",
+ \\"args\\": [
+ 1,
+ 0,
+ 0,
+ 1
+ ]
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 9,
+ \\"id\\": 16,
+ \\"type\\": 1,
+ \\"property\\": \\"clear\\",
+ \\"args\\": [
+ 16384
+ ]
+ }
+ }
+]"
+`;
+
exports[`record integration tests will serialize node before record 1`] = `
"[
{
diff --git a/packages/rrweb/test/events/webgl.ts b/packages/rrweb/test/events/webgl.ts
new file mode 100644
index 0000000000..4dcdac81f6
--- /dev/null
+++ b/packages/rrweb/test/events/webgl.ts
@@ -0,0 +1,120 @@
+export default [
+ {
+ type: 4,
+ data: {
+ href: '',
+ width: 1600,
+ height: 900,
+ },
+ timestamp: 1636379531385,
+ },
+ {
+ type: 2,
+ data: {
+ node: {
+ type: 0,
+ childNodes: [
+ { type: 1, name: 'html', publicId: '', systemId: '', id: 2 },
+ {
+ type: 2,
+ tagName: 'html',
+ attributes: { lang: 'en' },
+ childNodes: [
+ {
+ type: 2,
+ tagName: 'head',
+ attributes: {},
+ childNodes: [
+ { type: 3, textContent: '\n ', id: 5 },
+ {
+ type: 2,
+ tagName: 'meta',
+ attributes: { charset: 'UTF-8' },
+ childNodes: [],
+ id: 6,
+ },
+ { type: 3, textContent: '\n ', id: 7 },
+ {
+ type: 2,
+ tagName: 'meta',
+ attributes: {
+ name: 'viewport',
+ content: 'width=device-width, initial-scale=1.0',
+ },
+ childNodes: [],
+ id: 8,
+ },
+ { type: 3, textContent: '\n ', id: 9 },
+ {
+ type: 2,
+ tagName: 'title',
+ attributes: {},
+ childNodes: [{ type: 3, textContent: 'canvas', id: 11 }],
+ id: 10,
+ },
+ { type: 3, textContent: '\n ', id: 12 },
+ ],
+ id: 4,
+ },
+ { type: 3, textContent: '\n ', id: 13 },
+ {
+ type: 2,
+ tagName: 'body',
+ attributes: {},
+ childNodes: [
+ { type: 3, textContent: '\n ', id: 15 },
+ {
+ type: 2,
+ tagName: 'canvas',
+ attributes: {
+ id: 'myCanvas',
+ width: '200',
+ height: '100',
+ style: 'border: 1px solid #000000',
+ rr_dataURL:
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAYAAADDhn8LAAACnklEQVR4Xu3VsRHAMAzEsHj/pTOBXbB9pFchyLycz0eAwFXgsCFA4C4gEK+DwENAIJ4HAYF4AwSagD9IczM1IiCQkUNbswkIpLmZGhEQyMihrdkEBNLcTI0ICGTk0NZsAgJpbqZGBAQycmhrNgGBNDdTIwICGTm0NZuAQJqbqREBgYwc2ppNQCDNzdSIgEBGDm3NJiCQ5mZqREAgI4e2ZhMQSHMzNSIgkJFDW7MJCKS5mRoREMjIoa3ZBATS3EyNCAhk5NDWbAICaW6mRgQEMnJoazYBgTQ3UyMCAhk5tDWbgECam6kRAYGMHNqaTUAgzc3UiIBARg5tzSYgkOZmakRAICOHtmYTEEhzMzUiIJCRQ1uzCQikuZkaERDIyKGt2QQE0txMjQgIZOTQ1mwCAmlupkYEBDJyaGs2AYE0N1MjAgIZObQ1m4BAmpupEQGBjBzamk1AIM3N1IiAQEYObc0mIJDmZmpEQCAjh7ZmExBIczM1IiCQkUNbswkIpLmZGhEQyMihrdkEBNLcTI0ICGTk0NZsAgJpbqZGBAQycmhrNgGBNDdTIwICGTm0NZuAQJqbqREBgYwc2ppNQCDNzdSIgEBGDm3NJiCQ5mZqREAgI4e2ZhMQSHMzNSIgkJFDW7MJCKS5mRoREMjIoa3ZBATS3EyNCAhk5NDWbAICaW6mRgQEMnJoazYBgTQ3UyMCAhk5tDWbgECam6kRAYGMHNqaTUAgzc3UiIBARg5tzSYgkOZmakRAICOHtmYTEEhzMzUiIJCRQ1uzCQikuZkaERDIyKGt2QQE0txMjQgIZOTQ1mwCAmlupkYEBDJyaGs2AYE0N1MjAgIZObQ1m4BAmpupEQGBjBzamk1AIM3N1IiAQEYObc0mIJDmZmpE4Af1gABlH0hlGgAAAABJRU5ErkJggg==',
+ },
+ childNodes: [{ type: 3, textContent: '\n ', id: 17 }],
+ id: 16,
+ },
+ { type: 3, textContent: '\n ', id: 18 },
+ {
+ type: 2,
+ tagName: 'script',
+ attributes: {},
+ childNodes: [
+ { type: 3, textContent: 'SCRIPT_PLACEHOLDER', id: 20 },
+ ],
+ id: 19,
+ },
+ { type: 3, textContent: '\n \n\n', id: 21 },
+ ],
+ id: 14,
+ },
+ ],
+ id: 3,
+ },
+ ],
+ id: 1,
+ },
+ initialOffset: { left: 0, top: 0 },
+ },
+ timestamp: 1636379531389,
+ },
+ {
+ type: 3,
+ data: {
+ source: 9,
+ id: 16,
+ type: 1,
+ property: 'clearColor',
+ args: [1, 0, 0, 1],
+ },
+ timestamp: 1636379532355,
+ },
+ {
+ type: 3,
+ data: { source: 9, id: 16, type: 1, property: 'clear', args: [16384] },
+ timestamp: 1636379532356,
+ },
+];
diff --git a/packages/rrweb/test/html/canvas-webgl.html b/packages/rrweb/test/html/canvas-webgl.html
new file mode 100644
index 0000000000..52aacabca6
--- /dev/null
+++ b/packages/rrweb/test/html/canvas-webgl.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+ canvas
+
+
+
+
+
+
diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts
index acddfdf039..58ab0ac950 100644
--- a/packages/rrweb/test/integration.test.ts
+++ b/packages/rrweb/test/integration.test.ts
@@ -405,6 +405,19 @@ describe('record integration tests', function (this: ISuite) {
assertSnapshot(snapshots);
});
+ it('should record webgl canvas mutations', async () => {
+ const page: puppeteer.Page = await browser.newPage();
+ await page.goto('about:blank');
+ await page.setContent(
+ getHtml.call(this, 'canvas-webgl.html', {
+ recordCanvas: true,
+ }),
+ );
+ await page.waitForTimeout(50);
+ const snapshots = await page.evaluate('window.snapshots');
+ assertSnapshot(snapshots);
+ });
+
it('will serialize node before record', async () => {
const page: puppeteer.Page = await browser.newPage();
await page.goto('about:blank');
diff --git a/packages/rrweb/test/replay/__image_snapshots__/webgl-test-ts-replayer-webgl-should-doutput-simple-webgl-object-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/webgl-test-ts-replayer-webgl-should-doutput-simple-webgl-object-1-snap.png
new file mode 100644
index 0000000000000000000000000000000000000000..a153bbfc8b80f2bfa11fca6ce42bffe0d24f5dab
GIT binary patch
literal 10796
zcmeAS@N?(olHy`uVBq!ia0y~yU~gbxV6os}1B$%3e9#$4F%}28J29*~C-ahlL4m>3
z#WAE}&YL?MbD0eV7##O4%(~I^Dlk+lNcF-*XXg!$FCTvO`C}2YuioZbR5nmI2z+>6
zJ7+J1$-q#}aRAB`WHNxT99kH7AS?kz24)D$1&gvHPj9k+|NU3C8lq%Q(mBg@*I(bv
zX2^NSBmoxtvHjVc}B{@wEJ${PIj=&f!nrn{hPxXZk%Ez(N=7Ziz(x^
z>Px<^aUv>v?p`K{a}sYeeUM1@i2h)^-vc7@A!}X(C?J8LL!f~X!ct;MfG`?37$J^k
zP;g=xRmQ+D8X%)7WH9E1j??E7kDp6~L=MBB6WbW}T)SloDl>tgA^ro434}>(F@~)`
z!BX0dDgy^A$+g62xQ&Jz{&vJ@3K>lyqbUTO21e^Fa4;~8)-vF5z}D*;EiFe&%hA$u
zw6tUxEgZm!U^Fj`<^^K&Ld~{vc6FX34gxH?1tzoWx}x%8l&TW
zprkZ9?g!3L45K5%;BXinDhCI{pq>%1G#DUAb);s+o}crf;(?yx=B`(4-?
z>OhMp(5a%qIMO^iBLt46(Si^h45I}hI2ZGfj!ND+EI6#7d^uodJiiO%Hvzz-bf(-C<^>bP0
Hl+XkK4Zbd2
literal 0
HcmV?d00001
diff --git a/packages/rrweb/test/replay/webgl.test.ts b/packages/rrweb/test/replay/webgl.test.ts
new file mode 100644
index 0000000000..e353050132
--- /dev/null
+++ b/packages/rrweb/test/replay/webgl.test.ts
@@ -0,0 +1,66 @@
+import * as fs from 'fs';
+import * as path from 'path';
+import { assertDomSnapshot, launchPuppeteer } from '../utils';
+import { toMatchImageSnapshot } from 'jest-image-snapshot';
+import * as puppeteer from 'puppeteer';
+import events from '../events/webgl';
+
+interface ISuite {
+ code: string;
+ browser: puppeteer.Browser;
+ page: puppeteer.Page;
+}
+
+expect.extend({ toMatchImageSnapshot });
+
+describe('replayer', function () {
+ jest.setTimeout(10_000);
+
+ let code: ISuite['code'];
+ let browser: ISuite['browser'];
+ let page: ISuite['page'];
+
+ beforeAll(async () => {
+ browser = await launchPuppeteer();
+
+ const bundlePath = path.resolve(__dirname, '../../dist/rrweb.min.js');
+ code = fs.readFileSync(bundlePath, 'utf8');
+ });
+
+ beforeEach(async () => {
+ page = await browser.newPage();
+ await page.goto('about:blank');
+ // mouse cursor canvas is large and pushes the replayer below the fold
+ // lets hide it...
+ await page.addStyleTag({
+ content: '.replayer-mouse-tail{display: none !important;}',
+ });
+ await page.evaluate(code);
+ await page.evaluate(`let events = ${JSON.stringify(events)}`);
+
+ page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
+ });
+
+ afterEach(async () => {
+ await page.close();
+ });
+
+ afterAll(async () => {
+ await browser.close();
+ });
+
+ describe('webgl', () => {
+ it('should doutput simple webgl object', async () => {
+ await page.evaluate(`
+ const { Replayer } = rrweb;
+ const replayer = new Replayer(events, {
+ UNSAFE_replayCanvas: true,
+ });
+ replayer.play(2500);
+ `);
+
+ const image = await page.screenshot();
+ expect(image).toMatchImageSnapshot();
+ });
+ });
+});
diff --git a/packages/rrweb/tsconfig.json b/packages/rrweb/tsconfig.json
index 624bc19af8..6ac48c750c 100644
--- a/packages/rrweb/tsconfig.json
+++ b/packages/rrweb/tsconfig.json
@@ -13,5 +13,9 @@
"downlevelIteration": true
},
"exclude": ["test"],
- "include": ["src", "node_modules/@types/css-font-loading-module/index.d.ts"]
+ "include": [
+ "src",
+ "node_modules/@types/css-font-loading-module/index.d.ts",
+ "node_modules/@types/jest-image-snapshot/index.d.ts"
+ ]
}
diff --git a/packages/rrweb/typings/record/observers/canvas-2d.d.ts b/packages/rrweb/typings/record/observers/canvas-2d.d.ts
new file mode 100644
index 0000000000..3912e174f4
--- /dev/null
+++ b/packages/rrweb/typings/record/observers/canvas-2d.d.ts
@@ -0,0 +1,2 @@
+import { blockClass, canvasMutationCallback, IWindow, listenerHandler, Mirror } from '../../types';
+export default function initCanvas2DMutationObserver(cb: canvasMutationCallback, win: IWindow, blockClass: blockClass, mirror: Mirror): listenerHandler;
diff --git a/packages/rrweb/typings/record/observers/canvas-web-gl.d.ts b/packages/rrweb/typings/record/observers/canvas-web-gl.d.ts
new file mode 100644
index 0000000000..9c18d9b3b0
--- /dev/null
+++ b/packages/rrweb/typings/record/observers/canvas-web-gl.d.ts
@@ -0,0 +1,2 @@
+import { blockClass, canvasMutationCallback, IWindow, listenerHandler, Mirror } from '../../types';
+export default function initCanvasWebGLMutationObserver(cb: canvasMutationCallback, win: IWindow, blockClass: blockClass, mirror: Mirror): listenerHandler;
diff --git a/packages/rrweb/typings/types.d.ts b/packages/rrweb/typings/types.d.ts
index 7d9c049435..59bd91a472 100644
--- a/packages/rrweb/typings/types.d.ts
+++ b/packages/rrweb/typings/types.d.ts
@@ -280,6 +280,10 @@ export declare enum MouseInteractions {
TouchEnd = 9,
TouchCancel = 10
}
+export declare enum CanvasContext {
+ '2D' = 0,
+ WebGL = 1
+}
declare type mouseInteractionParam = {
type: MouseInteractions;
id: number;
@@ -322,6 +326,7 @@ export declare type styleDeclarationCallback = (s: styleDeclarationParam) => voi
export declare type canvasMutationCallback = (p: canvasMutationParam) => void;
export declare type canvasMutationParam = {
id: number;
+ type: CanvasContext;
property: string;
args: Array;
setter?: true;
diff --git a/yarn.lock b/yarn.lock
index fe25937f5e..5abff4ff0d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1566,7 +1566,16 @@
dependencies:
"@types/istanbul-lib-report" "*"
-"@types/jest@^27.0.2":
+"@types/jest-image-snapshot@^4.3.1":
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/@types/jest-image-snapshot/-/jest-image-snapshot-4.3.1.tgz#1382e9e155d6e29af0a81efce1056aaba92110c9"
+ integrity sha512-WDdUruGF14C53axe/mNDgQP2YIhtcwXrwmmVP8eOGyfNTVD+FbxWjWR7RTU+lzEy4K6V6+z7nkVDm/auI/r3xQ==
+ dependencies:
+ "@types/jest" "*"
+ "@types/pixelmatch" "*"
+ ssim.js "^3.1.1"
+
+"@types/jest@*", "@types/jest@^27.0.2":
version "27.0.2"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.0.2.tgz#ac383c4d4aaddd29bbf2b916d8d105c304a5fcd7"
integrity sha512-4dRxkS/AFX0c5XW6IPMNOydLn2tEhNhJV7DnYK+0bjoJZ+QTmfucBlihX7aoEsh/ocYtkLC73UbnBXBXIxsULA==
@@ -1628,6 +1637,13 @@
resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.1.tgz#f8ae4fbcd2b9ba4ff934698e28778961f9cb22ca"
integrity sha512-ARATsLdrGPUnaBvxLhUlnltcMgn7pQG312S8ccdYlnyijabrX9RN/KN/iGj9Am96CoW8e/K9628BA7Bv4XHdrA==
+"@types/pixelmatch@*":
+ version "5.2.4"
+ resolved "https://registry.yarnpkg.com/@types/pixelmatch/-/pixelmatch-5.2.4.tgz#ca145cc5ede1388c71c68edf2d1f5190e5ddd0f6"
+ integrity sha512-HDaSHIAv9kwpMN7zlmwfTv6gax0PiporJOipcrGsVNF3Ba+kryOZc0Pio5pn6NhisgWr7TaajlPEKTbTAypIBQ==
+ dependencies:
+ "@types/node" "*"
+
"@types/prettier@^2.1.5":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.4.1.tgz#e1303048d5389563e130f5bdd89d37a99acb75eb"
@@ -4428,6 +4444,11 @@ get-port@^5.1.1:
resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==
+get-stdin@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398"
+ integrity sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=
+
get-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
@@ -4588,6 +4609,11 @@ globby@^6.1.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
+glur@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/glur/-/glur-1.1.2.tgz#f20ea36db103bfc292343921f1f91e83c3467689"
+ integrity sha1-8g6jbbEDv8KSNDkh8fkeg8NGdok=
+
got@^6.7.1:
version "6.7.1"
resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0"
@@ -5591,6 +5617,21 @@ jest-haste-map@^27.2.4:
optionalDependencies:
fsevents "^2.3.2"
+jest-image-snapshot@^4.5.1:
+ version "4.5.1"
+ resolved "https://registry.yarnpkg.com/jest-image-snapshot/-/jest-image-snapshot-4.5.1.tgz#79fe0419c7729eb1be6c873365307a7b60f5cda0"
+ integrity sha512-0YkgupgkkCx0wIZkxvqs/oNiUT0X0d2WTpUhaAp+Dy6CpqBUZMRTIZo4KR1f+dqmx6WXrLCvecjnHLIsLkI+gQ==
+ dependencies:
+ chalk "^1.1.3"
+ get-stdin "^5.0.1"
+ glur "^1.1.2"
+ lodash "^4.17.4"
+ mkdirp "^0.5.1"
+ pixelmatch "^5.1.0"
+ pngjs "^3.4.0"
+ rimraf "^2.6.2"
+ ssim.js "^3.1.1"
+
jest-jasmine2@^27.2.4:
version "27.2.4"
resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-27.2.4.tgz#4a1608133dbdb4d68b5929bfd785503ed9c9ba51"
@@ -7487,6 +7528,13 @@ pirates@^4.0.1:
dependencies:
node-modules-regexp "^1.0.0"
+pixelmatch@^5.1.0:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-5.2.1.tgz#9e4e4f4aa59648208a31310306a5bed5522b0d65"
+ integrity sha512-WjcAdYSnKrrdDdqTcVEY7aB7UhhwjYQKYhHiBXdJef0MOaQeYpUdQ+iVyBLa5YBKS8MPVPPMX7rpOByISLpeEQ==
+ dependencies:
+ pngjs "^4.0.1"
+
pkg-dir@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
@@ -7494,6 +7542,16 @@ pkg-dir@^4.2.0:
dependencies:
find-up "^4.0.0"
+pngjs@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
+ integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
+
+pngjs@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-4.0.1.tgz#f803869bb2fc1bfe1bf99aa4ec21c108117cfdbe"
+ integrity sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg==
+
postcss-calc@^7.0.1:
version "7.0.5"
resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.5.tgz#f8a6e99f12e619c2ebc23cf6c486fdc15860933e"
@@ -8426,7 +8484,7 @@ rgba-regex@^1.0.0:
resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
-rimraf@^2.6.1, rimraf@^2.6.3:
+rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
@@ -8873,6 +8931,11 @@ sshpk@^1.7.0:
safer-buffer "^2.0.2"
tweetnacl "~0.14.0"
+ssim.js@^3.1.1:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/ssim.js/-/ssim.js-3.5.0.tgz#d7276b9ee99b57a5ff0db34035f02f35197e62df"
+ integrity sha512-Aj6Jl2z6oDmgYFFbQqK7fght19bXdOxY7Tj03nF+03M9gCBAjeIiO8/PlEGMfKDwYpw4q6iBqVq2YuREorGg/g==
+
ssri@^8.0.0, ssri@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af"
From 866481624aea2d1382004102ac12aed48f773962 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Tue, 9 Nov 2021 13:44:24 +0100
Subject: [PATCH 02/93] document the default
---
packages/rrweb/src/replay/index.ts | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts
index 372e057205..fe0fbe4e45 100644
--- a/packages/rrweb/src/replay/index.ts
+++ b/packages/rrweb/src/replay/index.ts
@@ -1204,11 +1204,10 @@ export class Replayer {
if (!target) {
return this.debugNodeNotFound(d, d.id);
}
+ // default is '2d' for backwards compatibility (rrweb below 1.1.x)
let contextType: '2d' | 'webgl' = '2d';
try {
- if (d.type === CanvasContext['2D']) {
- contextType = '2d';
- } else if (d.type === CanvasContext.WebGL) {
+ if (d.type === CanvasContext.WebGL) {
contextType = 'webgl';
}
const ctx = ((target as unknown) as HTMLCanvasElement).getContext(
From 4d77c0959166e03b028d967051cbfc74cb5609c9 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Wed, 24 Nov 2021 15:44:09 +0100
Subject: [PATCH 03/93] only capture rr_dataURL in 2d canvas contexts
---
packages/rrweb-snapshot/src/snapshot.ts | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts
index ca54833297..0084b1e1cb 100644
--- a/packages/rrweb-snapshot/src/snapshot.ts
+++ b/packages/rrweb-snapshot/src/snapshot.ts
@@ -484,8 +484,12 @@ function serializeNode(
delete attributes.selected;
}
}
- // canvas image data
- if (tagName === 'canvas' && recordCanvas) {
+ // 2d canvas image data
+ if (
+ tagName === 'canvas' &&
+ recordCanvas &&
+ (n as HTMLCanvasElement).getContext('2d')
+ ) {
attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL();
}
// media elements
From 39eb35ac649703d50fcb7e3584ee65afa5b38eed Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Wed, 24 Nov 2021 15:47:00 +0100
Subject: [PATCH 04/93] rr_dataURL no longer part of webgl snapshot
---
packages/rrweb/test/events/webgl.ts | 2 --
1 file changed, 2 deletions(-)
diff --git a/packages/rrweb/test/events/webgl.ts b/packages/rrweb/test/events/webgl.ts
index 4dcdac81f6..513c1ade48 100644
--- a/packages/rrweb/test/events/webgl.ts
+++ b/packages/rrweb/test/events/webgl.ts
@@ -71,8 +71,6 @@ export default [
width: '200',
height: '100',
style: 'border: 1px solid #000000',
- rr_dataURL:
- 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAYAAADDhn8LAAACnklEQVR4Xu3VsRHAMAzEsHj/pTOBXbB9pFchyLycz0eAwFXgsCFA4C4gEK+DwENAIJ4HAYF4AwSagD9IczM1IiCQkUNbswkIpLmZGhEQyMihrdkEBNLcTI0ICGTk0NZsAgJpbqZGBAQycmhrNgGBNDdTIwICGTm0NZuAQJqbqREBgYwc2ppNQCDNzdSIgEBGDm3NJiCQ5mZqREAgI4e2ZhMQSHMzNSIgkJFDW7MJCKS5mRoREMjIoa3ZBATS3EyNCAhk5NDWbAICaW6mRgQEMnJoazYBgTQ3UyMCAhk5tDWbgECam6kRAYGMHNqaTUAgzc3UiIBARg5tzSYgkOZmakRAICOHtmYTEEhzMzUiIJCRQ1uzCQikuZkaERDIyKGt2QQE0txMjQgIZOTQ1mwCAmlupkYEBDJyaGs2AYE0N1MjAgIZObQ1m4BAmpupEQGBjBzamk1AIM3N1IiAQEYObc0mIJDmZmpEQCAjh7ZmExBIczM1IiCQkUNbswkIpLmZGhEQyMihrdkEBNLcTI0ICGTk0NZsAgJpbqZGBAQycmhrNgGBNDdTIwICGTm0NZuAQJqbqREBgYwc2ppNQCDNzdSIgEBGDm3NJiCQ5mZqREAgI4e2ZhMQSHMzNSIgkJFDW7MJCKS5mRoREMjIoa3ZBATS3EyNCAhk5NDWbAICaW6mRgQEMnJoazYBgTQ3UyMCAhk5tDWbgECam6kRAYGMHNqaTUAgzc3UiIBARg5tzSYgkOZmakRAICOHtmYTEEhzMzUiIJCRQ1uzCQikuZkaERDIyKGt2QQE0txMjQgIZOTQ1mwCAmlupkYEBDJyaGs2AYE0N1MjAgIZObQ1m4BAmpupEQGBjBzamk1AIM3N1IiAQEYObc0mIJDmZmpE4Af1gABlH0hlGgAAAABJRU5ErkJggg==',
},
childNodes: [{ type: 3, textContent: '\n ', id: 17 }],
id: 16,
From 6a87e79e644ed8c728aa6ef1a59f0fb366299fd9 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Wed, 24 Nov 2021 16:47:20 +0100
Subject: [PATCH 05/93] ignore __diff_output__ from jest-image-snapshot
---
packages/rrweb/.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/rrweb/.gitignore b/packages/rrweb/.gitignore
index afbda82bac..4875c32f5c 100644
--- a/packages/rrweb/.gitignore
+++ b/packages/rrweb/.gitignore
@@ -13,3 +13,4 @@ temp
*.log
.env
+__diff_output__
\ No newline at end of file
From 423bab190204bbd81be346693e61905adf1374a2 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Wed, 24 Nov 2021 16:48:34 +0100
Subject: [PATCH 06/93] Rename generic "Monorepo" to "RRWeb Monorepo"
---
.vscode/monorepo.code-workspace | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/.vscode/monorepo.code-workspace b/.vscode/monorepo.code-workspace
index 4caba94749..8b028ddde2 100644
--- a/.vscode/monorepo.code-workspace
+++ b/.vscode/monorepo.code-workspace
@@ -1,7 +1,7 @@
{
"folders": [
{
- "name": "Monorepo",
+ "name": "RRWeb Monorepo",
"path": ".."
},
{
@@ -18,9 +18,6 @@
}
],
"settings": {
- "jest.disabledWorkspaceFolders": [
- "Monorepo",
- "rrweb-player"
- ]
+ "jest.disabledWorkspaceFolders": ["RRWeb Monorepo", "rrweb-player", "rrdom"]
}
}
From 6a614f67218c8b74012e2911736f28b502241829 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 25 Nov 2021 17:36:27 +0100
Subject: [PATCH 07/93] Serialize WebGL variables
---
.../src/record/observers/canvas-web-gl.ts | 39 +++-
packages/rrweb/test/record/webgl.test.ts | 175 ++++++++++++++++++
2 files changed, 213 insertions(+), 1 deletion(-)
create mode 100644 packages/rrweb/test/record/webgl.test.ts
diff --git a/packages/rrweb/src/record/observers/canvas-web-gl.ts b/packages/rrweb/src/record/observers/canvas-web-gl.ts
index 498b012f55..3966fbeb9c 100644
--- a/packages/rrweb/src/record/observers/canvas-web-gl.ts
+++ b/packages/rrweb/src/record/observers/canvas-web-gl.ts
@@ -9,6 +9,43 @@ import {
} from '../../types';
import { hookSetter, isBlocked, patch } from '../../utils';
+// from webgl-recorder: https://github.com/evanw/webgl-recorder/blob/bef0e65596e981ee382126587e2dcbe0fc7748e2/webgl-recorder.js#L50-L77
+const webGLVars: Record> = {};
+function getVariable(value: any) {
+ if (
+ value instanceof WebGLActiveInfo ||
+ value instanceof WebGLBuffer ||
+ value instanceof WebGLFramebuffer ||
+ value instanceof WebGLProgram ||
+ value instanceof WebGLRenderbuffer ||
+ value instanceof WebGLShader ||
+ value instanceof WebGLShaderPrecisionFormat ||
+ value instanceof WebGLTexture ||
+ value instanceof WebGLUniformLocation ||
+ value instanceof WebGLVertexArrayObject ||
+ // In Chrome, value won't be an instanceof WebGLVertexArrayObject.
+ (value && value.constructor.name == 'WebGLVertexArrayObjectOES') ||
+ typeof value === 'object'
+ ) {
+ const name = value.constructor.name;
+ const list = webGLVars[name] || (webGLVars[name] = []);
+ let index = list.indexOf(value);
+
+ if (index === -1) {
+ index = list.length;
+ list.push(value);
+ }
+
+ return `$${name}#${index}`;
+ }
+
+ return value;
+}
+
+const serializeArgs = (args: Array) => {
+ return [...args].map(getVariable);
+};
+
export default function initCanvasWebGLMutationObserver(
cb: canvasMutationCallback,
win: IWindow,
@@ -36,7 +73,7 @@ export default function initCanvasWebGLMutationObserver(
) {
if (!isBlocked((this.canvas as unknown) as INode, blockClass)) {
setTimeout(() => {
- const recordArgs = [...args];
+ const recordArgs = serializeArgs([...args]);
cb({
id: mirror.getId((this.canvas as unknown) as INode),
type: CanvasContext.WebGL,
diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts
new file mode 100644
index 0000000000..12e6d5eee4
--- /dev/null
+++ b/packages/rrweb/test/record/webgl.test.ts
@@ -0,0 +1,175 @@
+/* tslint:disable no-console */
+
+import * as fs from 'fs';
+import * as path from 'path';
+import * as puppeteer from 'puppeteer';
+import {
+ recordOptions,
+ listenerHandler,
+ eventWithTime,
+ EventType,
+ IncrementalSource,
+ styleSheetRuleData,
+ CanvasContext,
+} from '../../src/types';
+import { assertSnapshot, launchPuppeteer } from '../utils';
+
+interface ISuite {
+ code: string;
+ browser: puppeteer.Browser;
+ page: puppeteer.Page;
+ events: eventWithTime[];
+}
+
+interface IWindow extends Window {
+ rrweb: {
+ record: (
+ options: recordOptions,
+ ) => listenerHandler | undefined;
+ addCustomEvent(tag: string, payload: T): void;
+ };
+ emit: (e: eventWithTime) => undefined;
+}
+
+const setup = function (this: ISuite, content: string): ISuite {
+ const ctx = {} as ISuite;
+
+ beforeAll(async () => {
+ ctx.browser = await launchPuppeteer();
+
+ const bundlePath = path.resolve(__dirname, '../../dist/rrweb.min.js');
+ ctx.code = fs.readFileSync(bundlePath, 'utf8');
+ });
+
+ beforeEach(async () => {
+ ctx.page = await ctx.browser.newPage();
+ await ctx.page.goto('about:blank');
+ await ctx.page.setContent(content);
+ await ctx.page.evaluate(ctx.code);
+ ctx.events = [];
+ await ctx.page.exposeFunction('emit', (e: eventWithTime) => {
+ if (e.type === EventType.DomContentLoaded || e.type === EventType.Load) {
+ return;
+ }
+ ctx.events.push(e);
+ });
+
+ ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
+ });
+
+ afterEach(async () => {
+ await ctx.page.close();
+ });
+
+ afterAll(async () => {
+ await ctx.browser.close();
+ });
+
+ return ctx;
+};
+
+describe('record webgl', function (this: ISuite) {
+ jest.setTimeout(100_000);
+
+ const ctx: ISuite = setup.call(
+ this,
+ `
+
+
+
+
+
+
+ `,
+ );
+
+ it('will record changes to a canvas element', async () => {
+ await ctx.page.evaluate(() => {
+ const { record } = ((window as unknown) as IWindow).rrweb;
+ record({
+ recordCanvas: true,
+ emit(event: eventWithTime) {
+ ((window as unknown) as IWindow).emit(event);
+ },
+ });
+ });
+ await ctx.page.evaluate(() => {
+ var canvas = document.getElementById('canvas') as HTMLCanvasElement;
+ var gl = canvas.getContext('webgl')!;
+
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ });
+
+ await ctx.page.waitForTimeout(50);
+
+ const lastEvent = ctx.events[ctx.events.length - 1];
+ expect(lastEvent).toMatchObject({
+ data: {
+ source: IncrementalSource.CanvasMutation,
+ args: [16384],
+ type: CanvasContext.WebGL,
+ property: 'clear',
+ },
+ });
+ });
+
+ it('will record changes to a canvas element before the canvas gets added', async () => {
+ await ctx.page.evaluate(() => {
+ const { record } = ((window as unknown) as IWindow).rrweb;
+ record({
+ recordCanvas: true,
+ emit: ((window as unknown) as IWindow).emit,
+ });
+ });
+ await ctx.page.evaluate(() => {
+ var canvas = document.createElement('canvas');
+ var gl = canvas.getContext('webgl')!;
+ var program = gl.createProgram()!;
+ gl.linkProgram(program);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ document.body.appendChild(canvas);
+ });
+
+ await ctx.page.waitForTimeout(50);
+
+ const lastEvent = ctx.events[ctx.events.length - 1];
+ expect(lastEvent).toMatchObject({
+ data: {
+ source: IncrementalSource.CanvasMutation,
+ type: CanvasContext.WebGL,
+ property: 'clear',
+ },
+ });
+ // TODO: make this a jest snapshot
+ });
+
+ it('will record webgl variables', async () => {
+ await ctx.page.evaluate(() => {
+ const { record } = ((window as unknown) as IWindow).rrweb;
+ record({
+ recordCanvas: true,
+ emit: ((window as unknown) as IWindow).emit,
+ });
+ });
+ await ctx.page.evaluate(() => {
+ var canvas = document.getElementById('canvas') as HTMLCanvasElement;
+ var gl = canvas.getContext('webgl')!;
+ var program0 = gl.createProgram()!;
+ gl.linkProgram(program0);
+ var program1 = gl.createProgram()!;
+ gl.linkProgram(program1);
+ });
+
+ await ctx.page.waitForTimeout(50);
+
+ const lastEvent = ctx.events[ctx.events.length - 1];
+ expect(lastEvent).toMatchObject({
+ data: {
+ source: IncrementalSource.CanvasMutation,
+ property: 'linkProgram',
+ type: CanvasContext.WebGL,
+ args: ['$WebGLProgram#1'], // `program1` is WebGLProgram, this is the second WebGLProgram variable (#1)
+ },
+ });
+ });
+});
From 4c123b7319ea79501b8980e77a98384cd29d67e0 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 25 Nov 2021 17:38:17 +0100
Subject: [PATCH 08/93] Move rrweb test port number to unique port
rrweb-snapshot uses 3030, rrweb uses 3031
---
packages/rrweb/test/integration.test.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts
index 58ab0ac950..d412cebad1 100644
--- a/packages/rrweb/test/integration.test.ts
+++ b/packages/rrweb/test/integration.test.ts
@@ -45,7 +45,7 @@ const startServer = () =>
res.end();
}
});
- s.listen(3030).on('listening', () => {
+ s.listen(3031).on('listening', () => {
resolve(s);
});
});
@@ -500,7 +500,7 @@ describe('record integration tests', function (this: ISuite) {
it('should nest record iframe', async () => {
const page: puppeteer.Page = await browser.newPage();
- await page.goto(`http://localhost:3030/html`);
+ await page.goto(`http://localhost:3031/html`);
await page.setContent(getHtml.call(this, 'main.html'));
await page.waitForTimeout(500);
From f71a598ad2885efc345ca6e92325ade35787732b Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 25 Nov 2021 17:38:35 +0100
Subject: [PATCH 09/93] Prepare for WebGL2
---
packages/rrweb/src/types.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts
index 866fa858c0..6fad33b20c 100644
--- a/packages/rrweb/src/types.ts
+++ b/packages/rrweb/src/types.ts
@@ -384,6 +384,7 @@ export enum MouseInteractions {
export enum CanvasContext {
'2D',
WebGL,
+ WebGL2,
}
type mouseInteractionParam = {
From 59fec7103bfd13ba66e53b49dbdf29b72bbf7963 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 25 Nov 2021 22:00:40 +0100
Subject: [PATCH 10/93] Split up canvas replay and record webgl vars
---
packages/rrweb/src/replay/canvas/2d.ts | 48 ++++++++++
packages/rrweb/src/replay/canvas/index.ts | 34 +++++++
packages/rrweb/src/replay/canvas/webgl.ts | 93 +++++++++++++++++++
packages/rrweb/src/replay/index.ts | 50 +++-------
packages/rrweb/typings/replay/canvas/2d.d.ts | 4 +
.../rrweb/typings/replay/canvas/index.d.ts | 4 +
.../rrweb/typings/replay/canvas/webgl.d.ts | 4 +
7 files changed, 198 insertions(+), 39 deletions(-)
create mode 100644 packages/rrweb/src/replay/canvas/2d.ts
create mode 100644 packages/rrweb/src/replay/canvas/index.ts
create mode 100644 packages/rrweb/src/replay/canvas/webgl.ts
create mode 100644 packages/rrweb/typings/replay/canvas/2d.d.ts
create mode 100644 packages/rrweb/typings/replay/canvas/index.d.ts
create mode 100644 packages/rrweb/typings/replay/canvas/webgl.d.ts
diff --git a/packages/rrweb/src/replay/canvas/2d.ts b/packages/rrweb/src/replay/canvas/2d.ts
new file mode 100644
index 0000000000..44b4f651d2
--- /dev/null
+++ b/packages/rrweb/src/replay/canvas/2d.ts
@@ -0,0 +1,48 @@
+import { Replayer } from '../../../typings/entries/all';
+import { canvasMutationData } from '../../types';
+
+export default function canvasMutation({
+ event,
+ mutation,
+ target,
+ imageMap,
+ errorHandler,
+}: {
+ event: Parameters[0];
+ mutation: canvasMutationData;
+ target: HTMLCanvasElement;
+ imageMap: Replayer['imageMap'];
+ errorHandler: Replayer['warnCanvasMutationFailed'];
+}): void {
+ try {
+ const ctx = ((target as unknown) as HTMLCanvasElement).getContext('2d')!;
+
+ if (mutation.setter) {
+ // skip some read-only type checks
+ // tslint:disable-next-line:no-any
+ (ctx as any)[mutation.property] = mutation.args[0];
+ return;
+ }
+ const original = ctx[
+ mutation.property as Exclude
+ ] as Function;
+
+ /**
+ * We have serialized the image source into base64 string during recording,
+ * which has been preloaded before replay.
+ * So we can get call drawImage SYNCHRONOUSLY which avoid some fragile cast.
+ */
+ if (
+ mutation.property === 'drawImage' &&
+ typeof mutation.args[0] === 'string'
+ ) {
+ const image = imageMap.get(event);
+ mutation.args[0] = image;
+ original.apply(ctx, mutation.args);
+ } else {
+ original.apply(ctx, mutation.args);
+ }
+ } catch (error) {
+ errorHandler(mutation, error);
+ }
+}
diff --git a/packages/rrweb/src/replay/canvas/index.ts b/packages/rrweb/src/replay/canvas/index.ts
new file mode 100644
index 0000000000..6d5d3c53cc
--- /dev/null
+++ b/packages/rrweb/src/replay/canvas/index.ts
@@ -0,0 +1,34 @@
+import { Replayer } from '..';
+import { CanvasContext, canvasMutationData } from '../../types';
+import webglMutation from './webgl';
+import canvas2DMutation from './2d';
+
+export default function canvasMutation({
+ event,
+ mutation,
+ target,
+ imageMap,
+ errorHandler,
+}: {
+ event: Parameters[0];
+ mutation: canvasMutationData;
+ target: HTMLCanvasElement;
+ imageMap: Replayer['imageMap'];
+ errorHandler: Replayer['warnCanvasMutationFailed'];
+}): void {
+ try {
+ if ([CanvasContext.WebGL, CanvasContext.WebGL2].includes(mutation.type)) {
+ return webglMutation({ mutation, target, errorHandler });
+ }
+ // default is '2d' for backwards compatibility (rrweb below 1.1.x)
+ return canvas2DMutation({
+ event,
+ mutation,
+ target,
+ imageMap,
+ errorHandler,
+ });
+ } catch (error) {
+ errorHandler(mutation, error);
+ }
+}
diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts
new file mode 100644
index 0000000000..60ae512659
--- /dev/null
+++ b/packages/rrweb/src/replay/canvas/webgl.ts
@@ -0,0 +1,93 @@
+import { Replayer } from '../../../typings/entries/all';
+import { CanvasContext, canvasMutationData } from '../../types';
+
+const webGLVarMap: Map = new Map();
+function variableListFor(ctor: string) {
+ if (!webGLVarMap.has(ctor)) {
+ webGLVarMap.set(ctor, []);
+ }
+ return webGLVarMap.get(ctor) as any[];
+}
+
+function getContext(
+ target: HTMLCanvasElement,
+ type: CanvasContext,
+): WebGLRenderingContext | WebGL2RenderingContext | null {
+ try {
+ if (type === CanvasContext.WebGL) {
+ return (
+ target.getContext('webgl')! || target.getContext('experimental-webgl')
+ );
+ }
+ return target.getContext('webgl2')!;
+ } catch (e) {
+ return null;
+ }
+}
+
+const WebGLVariableConstructors = [
+ WebGLActiveInfo,
+ WebGLBuffer,
+ WebGLFramebuffer,
+ WebGLProgram,
+ WebGLRenderbuffer,
+ WebGLShader,
+ WebGLShaderPrecisionFormat,
+ WebGLTexture,
+ WebGLUniformLocation,
+ WebGLVertexArrayObject,
+];
+const WebGLVariableConstructorsNames = WebGLVariableConstructors.map(
+ (ctor) => ctor.name,
+);
+
+function saveToWebGLVarMap(result: any) {
+ if (result?.constructor) return; // probably null or undefined
+
+ const { name } = result.constructor;
+ if (!WebGLVariableConstructorsNames.includes(name)) return; // not a WebGL variable
+
+ const variables = variableListFor(name);
+ if (!variables.includes(result)) variables.push(result);
+}
+
+export default function webglMutation({
+ mutation,
+ target,
+ errorHandler,
+}: {
+ mutation: canvasMutationData;
+ target: HTMLCanvasElement;
+ errorHandler: Replayer['warnCanvasMutationFailed'];
+}): void {
+ try {
+ const ctx = getContext(target, mutation.type);
+ if (!ctx) return;
+
+
+ if (mutation.setter) {
+ // skip some read-only type checks
+ // tslint:disable-next-line:no-any
+ (ctx as any)[mutation.property] = mutation.args[0];
+ return;
+ }
+ const original = ctx[
+ mutation.property as Exclude
+ ] as Function;
+
+ const args = mutation.args.map((arg: any) => {
+ if (typeof arg === 'string') {
+ if (arg.startsWith('$')) {
+ const [name, index] = arg.slice(1).split('#');
+ return variableListFor(name)[Number(index)];
+ }
+ return arg;
+ }
+ });
+ const result = original.apply(ctx, args);
+
+ saveToWebGLVarMap(result);
+ } catch (error) {
+ errorHandler(mutation, error);
+ }
+}
diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts
index fe0fbe4e45..e20698a116 100644
--- a/packages/rrweb/src/replay/index.ts
+++ b/packages/rrweb/src/replay/index.ts
@@ -63,6 +63,7 @@ import {
getNestedRule,
getPositionsAndIndex,
} from './virtual-styles';
+import canvasMutation from './canvas';
const SKIP_TIME_THRESHOLD = 10 * 1000;
const SKIP_TIME_INTERVAL = 5 * 1000;
@@ -1204,40 +1205,15 @@ export class Replayer {
if (!target) {
return this.debugNodeNotFound(d, d.id);
}
- // default is '2d' for backwards compatibility (rrweb below 1.1.x)
- let contextType: '2d' | 'webgl' = '2d';
- try {
- if (d.type === CanvasContext.WebGL) {
- contextType = 'webgl';
- }
- const ctx = ((target as unknown) as HTMLCanvasElement).getContext(
- contextType,
- )!;
- if (d.setter) {
- // skip some read-only type checks
- // tslint:disable-next-line:no-any
- (ctx as any)[d.property] = d.args[0];
- return;
- }
- const original = ctx[
- d.property as Exclude
- ] as Function;
- /**
- * We have serialized the image source into base64 string during recording,
- * which has been preloaded before replay.
- * So we can get call drawImage SYNCHRONOUSLY which avoid some fragile cast.
- */
- if (d.property === 'drawImage' && typeof d.args[0] === 'string') {
- const image = this.imageMap.get(e);
- d.args[0] = image;
- original.apply(ctx, d.args);
- } else {
- original.apply(ctx, d.args);
- }
- } catch (error) {
- this.warnCanvasMutationFailed(d, d.id, error);
- }
+ canvasMutation({
+ event: e,
+ mutation: d,
+ target: (target as unknown) as HTMLCanvasElement,
+ imageMap: this.imageMap,
+ errorHandler: this.warnCanvasMutationFailed,
+ });
+
break;
}
case IncrementalSource.Font: {
@@ -1863,12 +1839,8 @@ export class Replayer {
}
}
- private warnCanvasMutationFailed(
- d: canvasMutationData,
- id: number,
- error: unknown,
- ) {
- this.warn(`Has error on update canvas '${id}'`, d, error);
+ private warnCanvasMutationFailed(d: canvasMutationData, error: unknown) {
+ this.warn(`Has error on canvas update`, error, 'canvas mutation:', d);
}
private debugNodeNotFound(d: incrementalData, id: number) {
diff --git a/packages/rrweb/typings/replay/canvas/2d.d.ts b/packages/rrweb/typings/replay/canvas/2d.d.ts
new file mode 100644
index 0000000000..9765ec6780
--- /dev/null
+++ b/packages/rrweb/typings/replay/canvas/2d.d.ts
@@ -0,0 +1,4 @@
+import { INode } from 'rrweb-snapshot';
+import { Replayer } from '../../../typings/entries/all';
+import { canvasMutationData } from '../../types';
+export default function canvasMutation(event: Parameters[0], d: canvasMutationData, target: INode, imageMap: Replayer['imageMap'], warnCanvasMutationFailed: Replayer['warnCanvasMutationFailed']): void;
diff --git a/packages/rrweb/typings/replay/canvas/index.d.ts b/packages/rrweb/typings/replay/canvas/index.d.ts
new file mode 100644
index 0000000000..a207ad4d01
--- /dev/null
+++ b/packages/rrweb/typings/replay/canvas/index.d.ts
@@ -0,0 +1,4 @@
+import { INode } from 'rrweb-snapshot';
+import { Replayer } from '..';
+import { canvasMutationData } from '../../types';
+export default function canvasMutation(event: Parameters[0], d: canvasMutationData, target: INode, imageMap: Replayer['imageMap'], warnCanvasMutationFailed: Replayer['warnCanvasMutationFailed']): void;
diff --git a/packages/rrweb/typings/replay/canvas/webgl.d.ts b/packages/rrweb/typings/replay/canvas/webgl.d.ts
new file mode 100644
index 0000000000..89869c4754
--- /dev/null
+++ b/packages/rrweb/typings/replay/canvas/webgl.d.ts
@@ -0,0 +1,4 @@
+import { INode } from 'rrweb-snapshot';
+import { Replayer } from '../../../typings/entries/all';
+import { canvasMutationData } from '../../types';
+export default function webglMutation(d: canvasMutationData, target: INode, warnCanvasMutationFailed: Replayer['warnCanvasMutationFailed']): void;
From 49d2b5822d7dff3965a3088133cd913f63a8cdfd Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 25 Nov 2021 22:15:13 +0100
Subject: [PATCH 11/93] fix typo
---
packages/rrweb/test/replay/webgl.test.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/rrweb/test/replay/webgl.test.ts b/packages/rrweb/test/replay/webgl.test.ts
index e353050132..f7d0498f17 100644
--- a/packages/rrweb/test/replay/webgl.test.ts
+++ b/packages/rrweb/test/replay/webgl.test.ts
@@ -50,7 +50,7 @@ describe('replayer', function () {
});
describe('webgl', () => {
- it('should doutput simple webgl object', async () => {
+ it('should output simple webgl object', async () => {
await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events, {
From d9a03f259fde29dee350ba30fbc55b9013dbf304 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 25 Nov 2021 22:17:41 +0100
Subject: [PATCH 12/93] fix typo part 2
---
...gl-should-output-simple-webgl-object-1-snap.png} | Bin
1 file changed, 0 insertions(+), 0 deletions(-)
rename packages/rrweb/test/replay/__image_snapshots__/{webgl-test-ts-replayer-webgl-should-doutput-simple-webgl-object-1-snap.png => webgl-test-ts-replayer-webgl-should-output-simple-webgl-object-1-snap.png} (100%)
diff --git a/packages/rrweb/test/replay/__image_snapshots__/webgl-test-ts-replayer-webgl-should-doutput-simple-webgl-object-1-snap.png b/packages/rrweb/test/replay/__image_snapshots__/webgl-test-ts-replayer-webgl-should-output-simple-webgl-object-1-snap.png
similarity index 100%
rename from packages/rrweb/test/replay/__image_snapshots__/webgl-test-ts-replayer-webgl-should-doutput-simple-webgl-object-1-snap.png
rename to packages/rrweb/test/replay/__image_snapshots__/webgl-test-ts-replayer-webgl-should-output-simple-webgl-object-1-snap.png
From c03f53b5b1ea58adc5f4af4503f390487a1606fe Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 25 Nov 2021 22:58:20 +0100
Subject: [PATCH 13/93] fix typo
---
packages/rrweb/src/replay/canvas/webgl.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts
index 60ae512659..8dcb40bc3a 100644
--- a/packages/rrweb/src/replay/canvas/webgl.ts
+++ b/packages/rrweb/src/replay/canvas/webgl.ts
@@ -42,7 +42,7 @@ const WebGLVariableConstructorsNames = WebGLVariableConstructors.map(
);
function saveToWebGLVarMap(result: any) {
- if (result?.constructor) return; // probably null or undefined
+ if (!result?.constructor) return; // probably null or undefined
const { name } = result.constructor;
if (!WebGLVariableConstructorsNames.includes(name)) return; // not a WebGL variable
From 9f0e7d291a457be919cae0d67e437dac80483c86 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 25 Nov 2021 23:19:40 +0100
Subject: [PATCH 14/93] Handle non-variables too
---
packages/rrweb/src/replay/canvas/webgl.ts | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts
index 8dcb40bc3a..115be223f5 100644
--- a/packages/rrweb/src/replay/canvas/webgl.ts
+++ b/packages/rrweb/src/replay/canvas/webgl.ts
@@ -76,13 +76,11 @@ export default function webglMutation({
] as Function;
const args = mutation.args.map((arg: any) => {
- if (typeof arg === 'string') {
- if (arg.startsWith('$')) {
- const [name, index] = arg.slice(1).split('#');
- return variableListFor(name)[Number(index)];
- }
- return arg;
+ if (typeof arg === 'string' && arg.startsWith('$')) {
+ const [name, index] = arg.slice(1).split('#');
+ return variableListFor(name)[Number(index)];
}
+ return arg;
});
const result = original.apply(ctx, args);
From 3b1bf2a35edd5412af6e3376b90ef26a016f1519 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 25 Nov 2021 23:20:29 +0100
Subject: [PATCH 15/93] provide correct context for warning
---
packages/rrweb/src/replay/index.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts
index e20698a116..aa1d05db7b 100644
--- a/packages/rrweb/src/replay/index.ts
+++ b/packages/rrweb/src/replay/index.ts
@@ -1211,7 +1211,7 @@ export class Replayer {
mutation: d,
target: (target as unknown) as HTMLCanvasElement,
imageMap: this.imageMap,
- errorHandler: this.warnCanvasMutationFailed,
+ errorHandler: this.warnCanvasMutationFailed.bind(this),
});
break;
From a2c27a6b827a233d6c1c2816e2c3a5f392617e34 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Fri, 26 Nov 2021 17:37:00 +0100
Subject: [PATCH 16/93] (De)Serialize a lot of different objects
---
.../src/record/observers/canvas-web-gl.ts | 49 ++++++-
packages/rrweb/src/replay/canvas/webgl.ts | 26 +++-
packages/rrweb/src/types.ts | 15 ++
.../rrweb/test/record/serialize-args.test.ts | 109 ++++++++++++++
.../test/replay/deserialize-args.test.ts | 136 ++++++++++++++++++
5 files changed, 330 insertions(+), 5 deletions(-)
create mode 100644 packages/rrweb/test/record/serialize-args.test.ts
create mode 100644 packages/rrweb/test/replay/deserialize-args.test.ts
diff --git a/packages/rrweb/src/record/observers/canvas-web-gl.ts b/packages/rrweb/src/record/observers/canvas-web-gl.ts
index 3966fbeb9c..b106a6002b 100644
--- a/packages/rrweb/src/record/observers/canvas-web-gl.ts
+++ b/packages/rrweb/src/record/observers/canvas-web-gl.ts
@@ -6,13 +6,51 @@ import {
IWindow,
listenerHandler,
Mirror,
+ SerializedWebGlArg,
} from '../../types';
import { hookSetter, isBlocked, patch } from '../../utils';
// from webgl-recorder: https://github.com/evanw/webgl-recorder/blob/bef0e65596e981ee382126587e2dcbe0fc7748e2/webgl-recorder.js#L50-L77
const webGLVars: Record> = {};
-function getVariable(value: any) {
- if (
+export function serializeArg(value: any): SerializedWebGlArg {
+
+ if (value instanceof Array) {
+ return value.map(serializeArg);
+ } else if (value === null) {
+ return value;
+ } else if (
+ value instanceof Float32Array ||
+ value instanceof Float64Array ||
+ value instanceof Int32Array ||
+ value instanceof Uint32Array ||
+ value instanceof Uint8Array ||
+ value instanceof Uint16Array ||
+ value instanceof Int16Array ||
+ value instanceof Int8Array
+ ) {
+ const name = value.constructor.name;
+ return {
+ rr_type: name,
+ args: [Object.values(value)],
+ };
+ } else if (
+ // SharedArrayBuffer disabled on most browsers due to spectre.
+ // More info: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/SharedArrayBuffer
+ // value instanceof SharedArrayBuffer ||
+ value instanceof ArrayBuffer
+ ) {
+ const name = value.constructor.name;
+ return {
+ rr_type: name,
+ args: [value.byteLength],
+ };
+ } else if (value instanceof DataView) {
+ const name = value.constructor.name;
+ return {
+ rr_type: name,
+ args: [serializeArg(value.buffer), value.byteOffset, value.byteLength],
+ };
+ } else if (
value instanceof WebGLActiveInfo ||
value instanceof WebGLBuffer ||
value instanceof WebGLFramebuffer ||
@@ -36,14 +74,17 @@ function getVariable(value: any) {
list.push(value);
}
- return `$${name}#${index}`;
+ return {
+ rr_type: name,
+ index,
+ };
}
return value;
}
const serializeArgs = (args: Array) => {
- return [...args].map(getVariable);
+ return [...args].map(serializeArg);
};
export default function initCanvasWebGLMutationObserver(
diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts
index 115be223f5..4c15cd0f19 100644
--- a/packages/rrweb/src/replay/canvas/webgl.ts
+++ b/packages/rrweb/src/replay/canvas/webgl.ts
@@ -1,5 +1,9 @@
import { Replayer } from '../../../typings/entries/all';
-import { CanvasContext, canvasMutationData } from '../../types';
+import {
+ CanvasContext,
+ canvasMutationData,
+ SerializedWebGlArg,
+} from '../../types';
const webGLVarMap: Map = new Map();
function variableListFor(ctor: string) {
@@ -51,6 +55,26 @@ function saveToWebGLVarMap(result: any) {
if (!variables.includes(result)) variables.push(result);
}
+export function deserializeArg(arg: SerializedWebGlArg): any {
+ if (arg && typeof arg === 'object' && 'rr_type' in arg) {
+ if ('index' in arg) {
+ const { rr_type: name, index } = arg;
+ return variableListFor(name)[index];
+ } else if ('args' in arg) {
+ const { rr_type: name, args } = arg;
+
+ // @ts-ignore
+ const ctor = window[name] as unknown;
+
+ // @ts-ignore
+ return new ctor(...args.map(deserializeArg));
+ }
+ } else if (Array.isArray(arg)) {
+ return arg.map(deserializeArg);
+ }
+ return arg;
+}
+
export default function webglMutation({
mutation,
target,
diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts
index 6fad33b20c..cbfa0d44ac 100644
--- a/packages/rrweb/src/types.ts
+++ b/packages/rrweb/src/types.ts
@@ -387,6 +387,21 @@ export enum CanvasContext {
WebGL2,
}
+export type SerializedWebGlArg =
+ | {
+ rr_type: string;
+ args: Array;
+ }
+ | {
+ rr_type: string;
+ index: number;
+ }
+ | string
+ | number
+ | boolean
+ | null
+ | SerializedWebGlArg[];
+
type mouseInteractionParam = {
type: MouseInteractions;
id: number;
diff --git a/packages/rrweb/test/record/serialize-args.test.ts b/packages/rrweb/test/record/serialize-args.test.ts
new file mode 100644
index 0000000000..8b98feb7d5
--- /dev/null
+++ b/packages/rrweb/test/record/serialize-args.test.ts
@@ -0,0 +1,109 @@
+/**
+ * @jest-environment jsdom
+ */
+import { serializeArg } from '../../src/record/observers/canvas-web-gl';
+
+// polyfill as jsdom does not have support for these classes
+// consider replacing for https://www.npmjs.com/package/canvas
+class FakeConstructor {
+ constructor() {}
+}
+global.WebGLActiveInfo = FakeConstructor as any;
+global.WebGLBuffer = FakeConstructor as any;
+global.WebGLFramebuffer = FakeConstructor as any;
+class WebGLProgram {
+ constructor() {}
+}
+global.WebGLProgram = WebGLProgram as any;
+global.WebGLRenderbuffer = FakeConstructor as any;
+global.WebGLShader = FakeConstructor as any;
+global.WebGLShaderPrecisionFormat = FakeConstructor as any;
+global.WebGLTexture = FakeConstructor as any;
+global.WebGLUniformLocation = FakeConstructor as any;
+global.WebGLVertexArrayObject = FakeConstructor as any;
+
+describe('serializeArg', () => {
+ it('should serialize Float32Array values', async () => {
+ const float32Array = new Float32Array([-1, -1, 3, -1, -1, 3]);
+ const expected = {
+ rr_type: 'Float32Array',
+ args: [[-1, -1, 3, -1, -1, 3]],
+ };
+ expect(serializeArg(float32Array)).toEqual(expected);
+ });
+
+ it('should serialize Float64Array values', async () => {
+ const float64Array = new Float64Array([-1, -1, 3, -1, -1, 3]);
+ const expected = {
+ rr_type: 'Float64Array',
+ args: [[-1, -1, 3, -1, -1, 3]],
+ };
+
+ expect(serializeArg(float64Array)).toEqual(expected);
+ });
+
+ it('should serialize ArrayBuffer values', async () => {
+ const arrayBuffer = new ArrayBuffer(16);
+ const expected = {
+ rr_type: 'ArrayBuffer',
+ args: [16],
+ };
+
+ expect(serializeArg(arrayBuffer)).toEqual(expected);
+ });
+
+ it('should serialize DataView values', async () => {
+ const dataView = new DataView(new ArrayBuffer(16), 0, 16);
+ const expected = {
+ rr_type: 'DataView',
+ args: [
+ {
+ rr_type: 'ArrayBuffer',
+ args: [16],
+ },
+ 0,
+ 16,
+ ],
+ };
+
+ expect(serializeArg(dataView)).toEqual(expected);
+ });
+
+ it('should leave arrays intact', async () => {
+ const array = [1, 2, 3, 4];
+ expect(serializeArg(array)).toEqual(array);
+ });
+
+ it('should serialize complex objects', async () => {
+ const dataView = [new DataView(new ArrayBuffer(16), 0, 16), 5, 6];
+ const expected = [
+ {
+ rr_type: 'DataView',
+ args: [
+ {
+ rr_type: 'ArrayBuffer',
+ args: [16],
+ },
+ 0,
+ 16,
+ ],
+ },
+ 5,
+ 6,
+ ];
+
+ expect(serializeArg(dataView)).toEqual(expected);
+ });
+
+ it('should leave null as-is', async () => {
+ expect(serializeArg(null)).toStrictEqual(null);
+ });
+
+ it('should support indexed variables', async () => {
+ const webGLProgram = new WebGLProgram();
+ expect(serializeArg(webGLProgram)).toEqual({
+ rr_type: 'WebGLProgram',
+ index: 0,
+ });
+ });
+});
diff --git a/packages/rrweb/test/replay/deserialize-args.test.ts b/packages/rrweb/test/replay/deserialize-args.test.ts
new file mode 100644
index 0000000000..cb14794ff4
--- /dev/null
+++ b/packages/rrweb/test/replay/deserialize-args.test.ts
@@ -0,0 +1,136 @@
+/**
+ * @jest-environment jsdom
+ */
+
+// polyfill as jsdom does not have support for these classes
+// consider replacing with https://www.npmjs.com/package/canvas
+class WebGLActiveInfo {
+ constructor() {}
+}
+
+global.WebGLActiveInfo = WebGLActiveInfo as any;
+class WebGLBuffer {
+ constructor() {}
+}
+
+global.WebGLBuffer = WebGLBuffer as any;
+class WebGLFramebuffer {
+ constructor() {}
+}
+
+global.WebGLFramebuffer = WebGLFramebuffer as any;
+class WebGLProgram {
+ constructor() {}
+}
+
+global.WebGLProgram = WebGLProgram as any;
+class WebGLRenderbuffer {
+ constructor() {}
+}
+
+global.WebGLRenderbuffer = WebGLRenderbuffer as any;
+class WebGLShader {
+ constructor() {}
+}
+
+global.WebGLShader = WebGLShader as any;
+class WebGLShaderPrecisionFormat {
+ constructor() {}
+}
+
+global.WebGLShaderPrecisionFormat = WebGLShaderPrecisionFormat as any;
+class WebGLTexture {
+ constructor() {}
+}
+
+global.WebGLTexture = WebGLTexture as any;
+class WebGLUniformLocation {
+ constructor() {}
+}
+
+global.WebGLUniformLocation = WebGLUniformLocation as any;
+class WebGLVertexArrayObject {
+ constructor() {}
+}
+
+global.WebGLVertexArrayObject = WebGLVertexArrayObject as any;
+
+import { deserializeArg } from '../../src/replay/canvas/webgl';
+
+describe('deserializeArg', () => {
+ it('should deserialize Float32Array values', async () => {
+ expect(
+ deserializeArg({
+ rr_type: 'Float32Array',
+ args: [[-1, -1, 3, -1, -1, 3]],
+ }),
+ ).toEqual(new Float32Array([-1, -1, 3, -1, -1, 3]));
+ });
+
+ it('should deserialize Float64Array values', async () => {
+ expect(
+ deserializeArg({
+ rr_type: 'Float64Array',
+ args: [[-1, -1, 3, -1, -1, 3]],
+ }),
+ ).toEqual(new Float64Array([-1, -1, 3, -1, -1, 3]));
+ });
+
+ it('should deserialize ArrayBuffer values', async () => {
+ const arrayBuffer = new ArrayBuffer(16);
+ expect(
+ deserializeArg({
+ rr_type: 'ArrayBuffer',
+ args: [16],
+ }),
+ ).toEqual(new ArrayBuffer(16));
+ });
+
+ it('should deserialize DataView values', async () => {
+ expect(
+ deserializeArg({
+ rr_type: 'DataView',
+ args: [
+ {
+ rr_type: 'ArrayBuffer',
+ args: [16],
+ },
+ 0,
+ 16,
+ ],
+ }),
+ ).toEqual(new DataView(new ArrayBuffer(16), 0, 16));
+ });
+
+ it('should leave arrays intact', async () => {
+ const array = [1, 2, 3, 4];
+ expect(deserializeArg(array)).toEqual(array);
+ });
+
+ it('should deserialize complex objects', async () => {
+ const serializedArg = [
+ {
+ rr_type: 'DataView',
+ args: [
+ {
+ rr_type: 'ArrayBuffer',
+ args: [16],
+ },
+ 0,
+ 16,
+ ],
+ },
+ 5,
+ 6,
+ ];
+ expect(deserializeArg(serializedArg)).toEqual([
+ new DataView(new ArrayBuffer(16), 0, 16),
+ 5,
+ 6,
+ ]);
+ });
+
+ it('should leave null as-is', async () => {
+ expect(deserializeArg(null)).toStrictEqual(null);
+ });
+});
From 45847b0c9f16212cd4c7eb00e10e173988042e71 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Mon, 29 Nov 2021 14:37:48 +0100
Subject: [PATCH 17/93] monorepo root should be the first in the list
---
.vscode/monorepo.code-workspace | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/.vscode/monorepo.code-workspace b/.vscode/monorepo.code-workspace
index 8b028ddde2..e0895cc7ff 100644
--- a/.vscode/monorepo.code-workspace
+++ b/.vscode/monorepo.code-workspace
@@ -1,7 +1,7 @@
{
"folders": [
{
- "name": "RRWeb Monorepo",
+ "name": "_RRWeb Monorepo",
"path": ".."
},
{
@@ -18,6 +18,10 @@
}
],
"settings": {
- "jest.disabledWorkspaceFolders": ["RRWeb Monorepo", "rrweb-player", "rrdom"]
+ "jest.disabledWorkspaceFolders": [
+ "_RRWeb Monorepo",
+ "rrweb-player",
+ "rrdom"
+ ]
}
}
From d3499000fba01ad197ad99149810fff46c1deb1b Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Mon, 29 Nov 2021 15:41:04 +0100
Subject: [PATCH 18/93] Upgrade puppeteer to 11.x
---
packages/rrweb/package.json | 4 +-
yarn.lock | 140 ++++++++++++++++++++++--------------
2 files changed, 87 insertions(+), 57 deletions(-)
diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json
index 31783e8b8a..544b403498 100644
--- a/packages/rrweb/package.json
+++ b/packages/rrweb/package.json
@@ -50,7 +50,7 @@
"@types/jsdom": "^16.2.12",
"@types/node": "^12.20.16",
"@types/prettier": "^2.3.2",
- "@types/puppeteer": "^5.4.3",
+ "@types/puppeteer": "^5.4.4",
"cross-env": "^5.2.0",
"fast-mhtml": "^1.1.9",
"ignore-styles": "^5.0.1",
@@ -61,7 +61,7 @@
"jsdom": "^17.0.0",
"jsdom-global": "^3.0.2",
"prettier": "2.2.1",
- "puppeteer": "^9.1.1",
+ "puppeteer": "^11.0.0",
"rollup": "^2.45.2",
"rollup-plugin-postcss": "^3.1.1",
"rollup-plugin-rename-node-modules": "^1.1.0",
diff --git a/yarn.lock b/yarn.lock
index 5abff4ff0d..48796afcea 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1666,7 +1666,7 @@
dependencies:
"@types/node" "*"
-"@types/puppeteer@^5.4.3":
+"@types/puppeteer@^5.4.4":
version "5.4.4"
resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-5.4.4.tgz#e92abeccc4f46207c3e1b38934a1246be080ccd0"
integrity sha512-3Nau+qi69CN55VwZb0ATtdUAlYlqOOQ3OfQfq0Hqgc4JMFXiQT/XInlwQ9g6LbicDslE6loIFsXFklGh5XmI6Q==
@@ -3381,7 +3381,7 @@ debug@2.6.9, debug@^2.6.9:
dependencies:
ms "2.0.0"
-debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
+debug@4, debug@4.3.2, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
@@ -3497,10 +3497,10 @@ detect-newline@^3.0.0:
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
-devtools-protocol@0.0.869402:
- version "0.0.869402"
- resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.869402.tgz#03ade701761742e43ae4de5dc188bcd80f156d8d"
- integrity sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA==
+devtools-protocol@0.0.901419:
+ version "0.0.901419"
+ resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.901419.tgz#79b5459c48fe7e1c5563c02bd72f8fec3e0cebcd"
+ integrity sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==
dezalgo@^1.0.0:
version "1.0.3"
@@ -4059,17 +4059,7 @@ extglob@^0.3.1:
dependencies:
is-extglob "^1.0.0"
-extract-zip@^1.6.6:
- version "1.7.0"
- resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
- integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
- dependencies:
- concat-stream "^1.6.2"
- debug "^2.6.9"
- mkdirp "^0.5.4"
- yauzl "^2.10.0"
-
-extract-zip@^2.0.0:
+extract-zip@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
@@ -4080,6 +4070,16 @@ extract-zip@^2.0.0:
optionalDependencies:
"@types/yauzl" "^2.9.1"
+extract-zip@^1.6.6:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
+ integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
+ dependencies:
+ concat-stream "^1.6.2"
+ debug "^2.6.9"
+ mkdirp "^0.5.4"
+ yauzl "^2.10.0"
+
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@@ -4809,6 +4809,14 @@ http-signature@~1.2.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
+https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
+ integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
+ dependencies:
+ agent-base "6"
+ debug "4"
+
https-proxy-agent@^2.2.1:
version "2.2.4"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b"
@@ -4817,14 +4825,6 @@ https-proxy-agent@^2.2.1:
agent-base "^4.3.0"
debug "^3.1.0"
-https-proxy-agent@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
- integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
- dependencies:
- agent-base "6"
- debug "4"
-
human-signals@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
@@ -6833,6 +6833,13 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+node-fetch@2.6.5:
+ version "2.6.5"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
+ integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==
+ dependencies:
+ whatwg-url "^5.0.0"
+
node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
@@ -7535,7 +7542,7 @@ pixelmatch@^5.1.0:
dependencies:
pngjs "^4.0.1"
-pkg-dir@^4.2.0:
+pkg-dir@4.2.0, pkg-dir@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
@@ -7975,7 +7982,7 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
-progress@^2.0.0, progress@^2.0.1:
+progress@2.0.3, progress@^2.0.0, progress@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
@@ -8031,7 +8038,7 @@ proxy-addr@~2.0.5:
forwarded "0.2.0"
ipaddr.js "1.9.1"
-proxy-from-env@^1.0.0, proxy-from-env@^1.1.0:
+proxy-from-env@1.1.0, proxy-from-env@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
@@ -8073,23 +8080,23 @@ puppeteer@^1.15.0:
rimraf "^2.6.1"
ws "^6.1.0"
-puppeteer@^9.1.1:
- version "9.1.1"
- resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-9.1.1.tgz#f74b7facf86887efd6c6b9fabb7baae6fdce012c"
- integrity sha512-W+nOulP2tYd/ZG99WuZC/I5ljjQQ7EUw/jQGcIb9eu8mDlZxNY2SgcJXTLG9h5gRvqA3uJOe4hZXYsd3EqioMw==
- dependencies:
- debug "^4.1.0"
- devtools-protocol "0.0.869402"
- extract-zip "^2.0.0"
- https-proxy-agent "^5.0.0"
- node-fetch "^2.6.1"
- pkg-dir "^4.2.0"
- progress "^2.0.1"
- proxy-from-env "^1.1.0"
- rimraf "^3.0.2"
- tar-fs "^2.0.0"
- unbzip2-stream "^1.3.3"
- ws "^7.2.3"
+puppeteer@^11.0.0:
+ version "11.0.0"
+ resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-11.0.0.tgz#0808719c38e15315ecc1b1c28911f1c9054d201f"
+ integrity sha512-6rPFqN1ABjn4shgOICGDBITTRV09EjXVqhDERBDKwCLz0UyBxeeBH6Ay0vQUJ84VACmlxwzOIzVEJXThcF3aNg==
+ dependencies:
+ debug "4.3.2"
+ devtools-protocol "0.0.901419"
+ extract-zip "2.0.1"
+ https-proxy-agent "5.0.0"
+ node-fetch "2.6.5"
+ pkg-dir "4.2.0"
+ progress "2.0.3"
+ proxy-from-env "1.1.0"
+ rimraf "3.0.2"
+ tar-fs "2.1.1"
+ unbzip2-stream "1.4.3"
+ ws "8.2.3"
q@^1.1.2, q@^1.5.1:
version "1.5.1"
@@ -8484,6 +8491,13 @@ rgba-regex@^1.0.0:
resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
+rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+ integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+ dependencies:
+ glob "^7.1.3"
+
rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
@@ -8491,13 +8505,6 @@ rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3:
dependencies:
glob "^7.1.3"
-rimraf@^3.0.0, rimraf@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
- integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
- dependencies:
- glob "^7.1.3"
-
rollup-plugin-css-only@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/rollup-plugin-css-only/-/rollup-plugin-css-only-3.1.0.tgz#6a701cc5b051c6b3f0961e69b108a9a118e1b1df"
@@ -9250,7 +9257,7 @@ table@^6.0.9:
string-width "^4.2.0"
strip-ansi "^6.0.0"
-tar-fs@^2.0.0:
+tar-fs@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
@@ -9453,6 +9460,11 @@ tr46@^2.1.0:
dependencies:
punycode "^2.1.1"
+tr46@~0.0.3:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
+ integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
+
trim-newlines@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
@@ -9656,7 +9668,7 @@ unbox-primitive@^1.0.1:
has-symbols "^1.0.2"
which-boxed-primitive "^1.0.2"
-unbzip2-stream@^1.3.3:
+unbzip2-stream@1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7"
integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==
@@ -9879,6 +9891,11 @@ wcwidth@^1.0.0:
dependencies:
defaults "^1.0.3"
+webidl-conversions@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
+ integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
+
webidl-conversions@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
@@ -9901,6 +9918,14 @@ whatwg-mimetype@^2.3.0:
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
+whatwg-url@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
+ integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
+ dependencies:
+ tr46 "~0.0.3"
+ webidl-conversions "^3.0.0"
+
whatwg-url@^8.0.0, whatwg-url@^8.4.0, whatwg-url@^8.5.0:
version "8.7.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77"
@@ -10038,6 +10063,11 @@ write-pkg@^4.0.0:
type-fest "^0.4.1"
write-json-file "^3.2.0"
+ws@8.2.3:
+ version "8.2.3"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
+ integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
+
ws@^6.1.0:
version "6.2.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e"
@@ -10045,7 +10075,7 @@ ws@^6.1.0:
dependencies:
async-limiter "~1.0.0"
-ws@^7.2.3, ws@^7.4.3, ws@^7.4.5:
+ws@^7.4.3, ws@^7.4.5:
version "7.5.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74"
integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==
From 5c7be1f876655fd0d6f4e326395cb939b98c9d3b Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Mon, 29 Nov 2021 15:44:53 +0100
Subject: [PATCH 19/93] Correctly de-serialize webgl variables
---
packages/rrweb/src/replay/canvas/webgl.ts | 11 +---
.../rrweb/test/record/serialize-args.test.ts | 22 +-------
.../rrweb/test/replay/webgl-mutation.test.ts | 47 ++++++++++++++++
packages/rrweb/test/utils.ts | 55 +++++++++++++++++++
.../record/observers/canvas-web-gl.d.ts | 3 +-
packages/rrweb/typings/replay/canvas/2d.d.ts | 9 ++-
.../rrweb/typings/replay/canvas/index.d.ts | 9 ++-
.../rrweb/typings/replay/canvas/webgl.d.ts | 10 +++-
8 files changed, 131 insertions(+), 35 deletions(-)
create mode 100644 packages/rrweb/test/replay/webgl-mutation.test.ts
diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts
index 4c15cd0f19..b7654952a0 100644
--- a/packages/rrweb/src/replay/canvas/webgl.ts
+++ b/packages/rrweb/src/replay/canvas/webgl.ts
@@ -5,8 +5,9 @@ import {
SerializedWebGlArg,
} from '../../types';
+// TODO: add ability to wipe this list
const webGLVarMap: Map = new Map();
-function variableListFor(ctor: string) {
+export function variableListFor(ctor: string) {
if (!webGLVarMap.has(ctor)) {
webGLVarMap.set(ctor, []);
}
@@ -99,13 +100,7 @@ export default function webglMutation({
mutation.property as Exclude
] as Function;
- const args = mutation.args.map((arg: any) => {
- if (typeof arg === 'string' && arg.startsWith('$')) {
- const [name, index] = arg.slice(1).split('#');
- return variableListFor(name)[Number(index)];
- }
- return arg;
- });
+ const args = mutation.args.map(deserializeArg);
const result = original.apply(ctx, args);
saveToWebGLVarMap(result);
diff --git a/packages/rrweb/test/record/serialize-args.test.ts b/packages/rrweb/test/record/serialize-args.test.ts
index 8b98feb7d5..5f5fa5c143 100644
--- a/packages/rrweb/test/record/serialize-args.test.ts
+++ b/packages/rrweb/test/record/serialize-args.test.ts
@@ -1,26 +1,10 @@
/**
* @jest-environment jsdom
*/
-import { serializeArg } from '../../src/record/observers/canvas-web-gl';
+import { polyfillWebGLGlobals } from '../utils';
+polyfillWebGLGlobals();
-// polyfill as jsdom does not have support for these classes
-// consider replacing for https://www.npmjs.com/package/canvas
-class FakeConstructor {
- constructor() {}
-}
-global.WebGLActiveInfo = FakeConstructor as any;
-global.WebGLBuffer = FakeConstructor as any;
-global.WebGLFramebuffer = FakeConstructor as any;
-class WebGLProgram {
- constructor() {}
-}
-global.WebGLProgram = WebGLProgram as any;
-global.WebGLRenderbuffer = FakeConstructor as any;
-global.WebGLShader = FakeConstructor as any;
-global.WebGLShaderPrecisionFormat = FakeConstructor as any;
-global.WebGLTexture = FakeConstructor as any;
-global.WebGLUniformLocation = FakeConstructor as any;
-global.WebGLVertexArrayObject = FakeConstructor as any;
+import { serializeArg } from '../../src/record/observers/canvas-web-gl';
describe('serializeArg', () => {
it('should serialize Float32Array values', async () => {
diff --git a/packages/rrweb/test/replay/webgl-mutation.test.ts b/packages/rrweb/test/replay/webgl-mutation.test.ts
new file mode 100644
index 0000000000..dc9f1b836a
--- /dev/null
+++ b/packages/rrweb/test/replay/webgl-mutation.test.ts
@@ -0,0 +1,47 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import webglMutation, { variableListFor } from '../../src/replay/canvas/webgl';
+import { CanvasContext, IncrementalSource } from '../../src/types';
+import { polyfillWebGLGlobals } from '../utils';
+
+polyfillWebGLGlobals();
+
+let canvas: HTMLCanvasElement;
+describe('webglMutation', () => {
+ beforeEach(() => {
+ canvas = document.createElement('canvas');
+ });
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should create webgl variables', async () => {
+ const createShaderMock = jest.fn().mockImplementation(() => {
+ return new WebGLShader();
+ });
+ jest.spyOn(canvas, 'getContext').mockImplementation(() => {
+ return ({
+ createShader: createShaderMock,
+ } as unknown) as WebGLRenderingContext;
+ });
+
+ expect(variableListFor('WebGLShader')).toHaveLength(0);
+
+ webglMutation({
+ mutation: {
+ source: IncrementalSource.CanvasMutation,
+ type: CanvasContext.WebGL,
+ id: 1,
+ property: 'createShader',
+ args: [35633],
+ },
+ target: canvas,
+ errorHandler: () => {},
+ });
+
+ expect(createShaderMock).toHaveBeenCalledWith(35633);
+ expect(variableListFor('WebGLShader')).toHaveLength(1);
+ });
+});
diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts
index 45d427eecf..fbe34c8d45 100644
--- a/packages/rrweb/test/utils.ts
+++ b/packages/rrweb/test/utils.ts
@@ -338,3 +338,58 @@ export const sampleStyleSheetRemoveEvents: eventWithTime[] = [
timestamp: now + 2000,
},
];
+
+export const polyfillWebGLGlobals = () => {
+ // polyfill as jsdom does not have support for these classes
+ // consider replacing with https://www.npmjs.com/package/canvas
+ class WebGLActiveInfo {
+ constructor() {}
+ }
+
+ global.WebGLActiveInfo = WebGLActiveInfo as any;
+ class WebGLBuffer {
+ constructor() {}
+ }
+
+ global.WebGLBuffer = WebGLBuffer as any;
+ class WebGLFramebuffer {
+ constructor() {}
+ }
+
+ global.WebGLFramebuffer = WebGLFramebuffer as any;
+ class WebGLProgram {
+ constructor() {}
+ }
+
+ global.WebGLProgram = WebGLProgram as any;
+ class WebGLRenderbuffer {
+ constructor() {}
+ }
+
+ global.WebGLRenderbuffer = WebGLRenderbuffer as any;
+ class WebGLShader {
+ constructor() {}
+ }
+
+ global.WebGLShader = WebGLShader as any;
+ class WebGLShaderPrecisionFormat {
+ constructor() {}
+ }
+
+ global.WebGLShaderPrecisionFormat = WebGLShaderPrecisionFormat as any;
+ class WebGLTexture {
+ constructor() {}
+ }
+
+ global.WebGLTexture = WebGLTexture as any;
+ class WebGLUniformLocation {
+ constructor() {}
+ }
+
+ global.WebGLUniformLocation = WebGLUniformLocation as any;
+ class WebGLVertexArrayObject {
+ constructor() {}
+ }
+
+ global.WebGLVertexArrayObject = WebGLVertexArrayObject as any;
+};
diff --git a/packages/rrweb/typings/record/observers/canvas-web-gl.d.ts b/packages/rrweb/typings/record/observers/canvas-web-gl.d.ts
index 9c18d9b3b0..d2ea3d274a 100644
--- a/packages/rrweb/typings/record/observers/canvas-web-gl.d.ts
+++ b/packages/rrweb/typings/record/observers/canvas-web-gl.d.ts
@@ -1,2 +1,3 @@
-import { blockClass, canvasMutationCallback, IWindow, listenerHandler, Mirror } from '../../types';
+import { blockClass, canvasMutationCallback, IWindow, listenerHandler, Mirror, SerializedWebGlArg } from '../../types';
+export declare function serializeArg(value: any): SerializedWebGlArg;
export default function initCanvasWebGLMutationObserver(cb: canvasMutationCallback, win: IWindow, blockClass: blockClass, mirror: Mirror): listenerHandler;
diff --git a/packages/rrweb/typings/replay/canvas/2d.d.ts b/packages/rrweb/typings/replay/canvas/2d.d.ts
index 9765ec6780..85942bf827 100644
--- a/packages/rrweb/typings/replay/canvas/2d.d.ts
+++ b/packages/rrweb/typings/replay/canvas/2d.d.ts
@@ -1,4 +1,9 @@
-import { INode } from 'rrweb-snapshot';
import { Replayer } from '../../../typings/entries/all';
import { canvasMutationData } from '../../types';
-export default function canvasMutation(event: Parameters[0], d: canvasMutationData, target: INode, imageMap: Replayer['imageMap'], warnCanvasMutationFailed: Replayer['warnCanvasMutationFailed']): void;
+export default function canvasMutation({ event, mutation, target, imageMap, errorHandler, }: {
+ event: Parameters[0];
+ mutation: canvasMutationData;
+ target: HTMLCanvasElement;
+ imageMap: Replayer['imageMap'];
+ errorHandler: Replayer['warnCanvasMutationFailed'];
+}): void;
diff --git a/packages/rrweb/typings/replay/canvas/index.d.ts b/packages/rrweb/typings/replay/canvas/index.d.ts
index a207ad4d01..72c6bdfce3 100644
--- a/packages/rrweb/typings/replay/canvas/index.d.ts
+++ b/packages/rrweb/typings/replay/canvas/index.d.ts
@@ -1,4 +1,9 @@
-import { INode } from 'rrweb-snapshot';
import { Replayer } from '..';
import { canvasMutationData } from '../../types';
-export default function canvasMutation(event: Parameters[0], d: canvasMutationData, target: INode, imageMap: Replayer['imageMap'], warnCanvasMutationFailed: Replayer['warnCanvasMutationFailed']): void;
+export default function canvasMutation({ event, mutation, target, imageMap, errorHandler, }: {
+ event: Parameters[0];
+ mutation: canvasMutationData;
+ target: HTMLCanvasElement;
+ imageMap: Replayer['imageMap'];
+ errorHandler: Replayer['warnCanvasMutationFailed'];
+}): void;
diff --git a/packages/rrweb/typings/replay/canvas/webgl.d.ts b/packages/rrweb/typings/replay/canvas/webgl.d.ts
index 89869c4754..2abcc5afc7 100644
--- a/packages/rrweb/typings/replay/canvas/webgl.d.ts
+++ b/packages/rrweb/typings/replay/canvas/webgl.d.ts
@@ -1,4 +1,8 @@
-import { INode } from 'rrweb-snapshot';
import { Replayer } from '../../../typings/entries/all';
-import { canvasMutationData } from '../../types';
-export default function webglMutation(d: canvasMutationData, target: INode, warnCanvasMutationFailed: Replayer['warnCanvasMutationFailed']): void;
+import { canvasMutationData, SerializedWebGlArg } from '../../types';
+export declare function deserializeArg(arg: SerializedWebGlArg): any;
+export default function webglMutation({ mutation, target, errorHandler, }: {
+ mutation: canvasMutationData;
+ target: HTMLCanvasElement;
+ errorHandler: Replayer['warnCanvasMutationFailed'];
+}): void;
From 42693ca367d633c2f6de90ab3c72494d9f46279b Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Tue, 30 Nov 2021 14:27:34 +0100
Subject: [PATCH 20/93] Encode arrayBuffers contents to base64
---
packages/rrweb/package.json | 1 +
.../src/record/observers/canvas-web-gl.ts | 7 +++++--
packages/rrweb/src/replay/canvas/webgl.ts | 3 +++
packages/rrweb/src/types.ts | 4 ++++
.../rrweb/test/record/serialize-args.test.ts | 18 ++++++++++++++----
.../rrweb/test/replay/deserialize-args.test.ts | 12 ++++++------
.../rrweb/typings/replay/canvas/webgl.d.ts | 1 +
yarn.lock | 5 +++++
8 files changed, 39 insertions(+), 12 deletions(-)
diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json
index 544b403498..2638eeb08f 100644
--- a/packages/rrweb/package.json
+++ b/packages/rrweb/package.json
@@ -75,6 +75,7 @@
"dependencies": {
"@types/css-font-loading-module": "0.0.4",
"@xstate/fsm": "^1.4.0",
+ "base64-arraybuffer": "^1.0.1",
"fflate": "^0.4.4",
"mitt": "^1.1.3",
"rrweb-snapshot": "^1.1.10"
diff --git a/packages/rrweb/src/record/observers/canvas-web-gl.ts b/packages/rrweb/src/record/observers/canvas-web-gl.ts
index b106a6002b..b695180539 100644
--- a/packages/rrweb/src/record/observers/canvas-web-gl.ts
+++ b/packages/rrweb/src/record/observers/canvas-web-gl.ts
@@ -1,3 +1,4 @@
+import { encode } from 'base64-arraybuffer';
import { INode } from 'rrweb-snapshot';
import {
blockClass,
@@ -39,10 +40,12 @@ export function serializeArg(value: any): SerializedWebGlArg {
// value instanceof SharedArrayBuffer ||
value instanceof ArrayBuffer
) {
- const name = value.constructor.name;
+ const name = value.constructor.name as 'ArrayBuffer';
+ const base64 = encode(value);
+
return {
rr_type: name,
- args: [value.byteLength],
+ base64,
};
} else if (value instanceof DataView) {
const name = value.constructor.name;
diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts
index b7654952a0..fa7a196981 100644
--- a/packages/rrweb/src/replay/canvas/webgl.ts
+++ b/packages/rrweb/src/replay/canvas/webgl.ts
@@ -1,3 +1,4 @@
+import { decode } from 'base64-arraybuffer';
import { Replayer } from '../../../typings/entries/all';
import {
CanvasContext,
@@ -69,6 +70,8 @@ export function deserializeArg(arg: SerializedWebGlArg): any {
// @ts-ignore
return new ctor(...args.map(deserializeArg));
+ } else if ('contents' in arg) {
+ return decode(arg.contents);
}
} else if (Array.isArray(arg)) {
return arg.map(deserializeArg);
diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts
index cbfa0d44ac..6dd78b36c2 100644
--- a/packages/rrweb/src/types.ts
+++ b/packages/rrweb/src/types.ts
@@ -388,6 +388,10 @@ export enum CanvasContext {
}
export type SerializedWebGlArg =
+ | {
+ rr_type: 'ArrayBuffer';
+ base64: string; // base64
+ }
| {
rr_type: string;
args: Array;
diff --git a/packages/rrweb/test/record/serialize-args.test.ts b/packages/rrweb/test/record/serialize-args.test.ts
index 5f5fa5c143..9f3fd43862 100644
--- a/packages/rrweb/test/record/serialize-args.test.ts
+++ b/packages/rrweb/test/record/serialize-args.test.ts
@@ -27,10 +27,10 @@ describe('serializeArg', () => {
});
it('should serialize ArrayBuffer values', async () => {
- const arrayBuffer = new ArrayBuffer(16);
+ const arrayBuffer = new Uint8Array([1, 2, 0, 4]).buffer;
const expected = {
rr_type: 'ArrayBuffer',
- args: [16],
+ base64: 'AQIABA==',
};
expect(serializeArg(arrayBuffer)).toEqual(expected);
@@ -43,7 +43,7 @@ describe('serializeArg', () => {
args: [
{
rr_type: 'ArrayBuffer',
- args: [16],
+ base64: 'AAAAAAAAAAAAAAAAAAAAAA==',
},
0,
16,
@@ -66,7 +66,7 @@ describe('serializeArg', () => {
args: [
{
rr_type: 'ArrayBuffer',
- args: [16],
+ base64: 'AAAAAAAAAAAAAAAAAAAAAA==',
},
0,
16,
@@ -79,6 +79,16 @@ describe('serializeArg', () => {
expect(serializeArg(dataView)).toEqual(expected);
});
+ it('should serialize arraybuffer contents', async () => {
+ const buffer = new Float32Array([1, 2, 3, 4]).buffer;
+ const expected = {
+ rr_type: 'ArrayBuffer',
+ base64: 'AACAPwAAAEAAAEBAAACAQA==',
+ };
+
+ expect(serializeArg(buffer)).toEqual(expected);
+ });
+
it('should leave null as-is', async () => {
expect(serializeArg(null)).toStrictEqual(null);
});
diff --git a/packages/rrweb/test/replay/deserialize-args.test.ts b/packages/rrweb/test/replay/deserialize-args.test.ts
index cb14794ff4..c5026867c1 100644
--- a/packages/rrweb/test/replay/deserialize-args.test.ts
+++ b/packages/rrweb/test/replay/deserialize-args.test.ts
@@ -77,13 +77,13 @@ describe('deserializeArg', () => {
});
it('should deserialize ArrayBuffer values', async () => {
- const arrayBuffer = new ArrayBuffer(16);
+ const contents = [1, 2, 0, 4];
expect(
deserializeArg({
rr_type: 'ArrayBuffer',
- args: [16],
+ base64: 'AQIABA==',
}),
- ).toEqual(new ArrayBuffer(16));
+ ).toStrictEqual(new Uint8Array(contents).buffer);
});
it('should deserialize DataView values', async () => {
@@ -93,13 +93,13 @@ describe('deserializeArg', () => {
args: [
{
rr_type: 'ArrayBuffer',
- args: [16],
+ base64: 'AAAAAAAAAAAAAAAAAAAAAA==',
},
0,
16,
],
}),
- ).toEqual(new DataView(new ArrayBuffer(16), 0, 16));
+ ).toStrictEqual(new DataView(new ArrayBuffer(16), 0, 16));
});
it('should leave arrays intact', async () => {
@@ -123,7 +123,7 @@ describe('deserializeArg', () => {
5,
6,
];
- expect(deserializeArg(serializedArg)).toEqual([
+ expect(deserializeArg(serializedArg)).toStrictEqual([
new DataView(new ArrayBuffer(16), 0, 16),
5,
6,
diff --git a/packages/rrweb/typings/replay/canvas/webgl.d.ts b/packages/rrweb/typings/replay/canvas/webgl.d.ts
index 2abcc5afc7..3e87fe9565 100644
--- a/packages/rrweb/typings/replay/canvas/webgl.d.ts
+++ b/packages/rrweb/typings/replay/canvas/webgl.d.ts
@@ -1,5 +1,6 @@
import { Replayer } from '../../../typings/entries/all';
import { canvasMutationData, SerializedWebGlArg } from '../../types';
+export declare function variableListFor(ctor: string): any[];
export declare function deserializeArg(arg: SerializedWebGlArg): any;
export default function webglMutation({ mutation, target, errorHandler, }: {
mutation: canvasMutationData;
diff --git a/yarn.lock b/yarn.lock
index 48796afcea..38976cfc8d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2314,6 +2314,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+base64-arraybuffer@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.1.tgz#87bd13525626db4a9838e00a508c2b73efcf348c"
+ integrity sha512-vFIUq7FdLtjZMhATwDul5RZWv2jpXQ09Pd6jcVEOvIsqCWTRFD/ONHNfyOS8dA/Ippi5dsIgpyKWKZaAKZltbA==
+
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
From ecb4d6bad8e26c2e574b46352812375fd48da29d Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Tue, 30 Nov 2021 14:30:42 +0100
Subject: [PATCH 21/93] rename contents to base64
---
packages/rrweb/src/replay/canvas/webgl.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts
index fa7a196981..ef436cc0fa 100644
--- a/packages/rrweb/src/replay/canvas/webgl.ts
+++ b/packages/rrweb/src/replay/canvas/webgl.ts
@@ -70,8 +70,8 @@ export function deserializeArg(arg: SerializedWebGlArg): any {
// @ts-ignore
return new ctor(...args.map(deserializeArg));
- } else if ('contents' in arg) {
- return decode(arg.contents);
+ } else if ('base64' in arg) {
+ return decode(arg.base64);
}
} else if (Array.isArray(arg)) {
return arg.map(deserializeArg);
From 79e42a7bbd93ec01c3d426364db3a987f798e924 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Tue, 30 Nov 2021 17:49:02 +0100
Subject: [PATCH 22/93] add webgl2 support and serialize HTMLImageElements
---
packages/rrweb/src/record/observer.ts | 13 ++-
.../observers/{canvas-2d.ts => canvas/2d.ts} | 4 +-
.../serialize-args.ts} | 87 ++-----------------
.../src/record/observers/canvas/webgl.ts | 77 ++++++++++++++++
.../src/record/observers/canvas/webgl2.ts | 78 +++++++++++++++++
packages/rrweb/src/replay/canvas/webgl.ts | 4 +
packages/rrweb/src/types.ts | 4 +
.../rrweb/test/record/serialize-args.test.ts | 37 ++++++--
.../test/replay/deserialize-args.test.ts | 11 +++
.../rrweb/test/replay/webgl-mutation.test.ts | 6 +-
.../typings/record/observers/canvas/2d.d.ts | 2 +
.../observers/canvas/serialize-args.d.ts | 3 +
.../record/observers/canvas/webgl.d.ts | 2 +
.../record/observers/canvas/webgl2.d.ts | 2 +
14 files changed, 236 insertions(+), 94 deletions(-)
rename packages/rrweb/src/record/observers/{canvas-2d.ts => canvas/2d.ts} (96%)
rename packages/rrweb/src/record/observers/{canvas-web-gl.ts => canvas/serialize-args.ts} (53%)
create mode 100644 packages/rrweb/src/record/observers/canvas/webgl.ts
create mode 100644 packages/rrweb/src/record/observers/canvas/webgl2.ts
create mode 100644 packages/rrweb/typings/record/observers/canvas/2d.d.ts
create mode 100644 packages/rrweb/typings/record/observers/canvas/serialize-args.d.ts
create mode 100644 packages/rrweb/typings/record/observers/canvas/webgl.d.ts
create mode 100644 packages/rrweb/typings/record/observers/canvas/webgl2.d.ts
diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts
index 31134a0ac0..3efc97290d 100644
--- a/packages/rrweb/src/record/observer.ts
+++ b/packages/rrweb/src/record/observer.ts
@@ -49,8 +49,9 @@ import {
import MutationBuffer from './mutation';
import { IframeManager } from './iframe-manager';
import { ShadowDomManager } from './shadow-dom-manager';
-import initCanvas2DMutationObserver from './observers/canvas-2d';
-import initCanvasWebGLMutationObserver from './observers/canvas-web-gl';
+import initCanvas2DMutationObserver from './observers/canvas/2d';
+import initCanvasWebGLMutationObserver from './observers/canvas/webgl';
+import initCanvasWebGL2MutationObserver from './observers/canvas/webgl2';
type WindowWithStoredMutationObserver = IWindow & {
__rrMutationObserver?: MutationObserver;
@@ -728,9 +729,17 @@ function initCanvasMutationObserver(
mirror,
);
+ const canvasWebGL2Reset = initCanvasWebGL2MutationObserver(
+ cb,
+ win,
+ blockClass,
+ mirror,
+ );
+
return () => {
canvas2DReset();
canvasWebGLReset();
+ canvasWebGL2Reset();
};
}
diff --git a/packages/rrweb/src/record/observers/canvas-2d.ts b/packages/rrweb/src/record/observers/canvas/2d.ts
similarity index 96%
rename from packages/rrweb/src/record/observers/canvas-2d.ts
rename to packages/rrweb/src/record/observers/canvas/2d.ts
index 0aa30af391..68261f826f 100644
--- a/packages/rrweb/src/record/observers/canvas-2d.ts
+++ b/packages/rrweb/src/record/observers/canvas/2d.ts
@@ -6,8 +6,8 @@ import {
IWindow,
listenerHandler,
Mirror,
-} from '../../types';
-import { hookSetter, isBlocked, patch } from '../../utils';
+} from '../../../types';
+import { hookSetter, isBlocked, patch } from '../../../utils';
export default function initCanvas2DMutationObserver(
cb: canvasMutationCallback,
diff --git a/packages/rrweb/src/record/observers/canvas-web-gl.ts b/packages/rrweb/src/record/observers/canvas/serialize-args.ts
similarity index 53%
rename from packages/rrweb/src/record/observers/canvas-web-gl.ts
rename to packages/rrweb/src/record/observers/canvas/serialize-args.ts
index b695180539..f4a50907cd 100644
--- a/packages/rrweb/src/record/observers/canvas-web-gl.ts
+++ b/packages/rrweb/src/record/observers/canvas/serialize-args.ts
@@ -1,20 +1,9 @@
import { encode } from 'base64-arraybuffer';
-import { INode } from 'rrweb-snapshot';
-import {
- blockClass,
- CanvasContext,
- canvasMutationCallback,
- IWindow,
- listenerHandler,
- Mirror,
- SerializedWebGlArg,
-} from '../../types';
-import { hookSetter, isBlocked, patch } from '../../utils';
+import { SerializedWebGlArg } from '../../../types';
// from webgl-recorder: https://github.com/evanw/webgl-recorder/blob/bef0e65596e981ee382126587e2dcbe0fc7748e2/webgl-recorder.js#L50-L77
const webGLVars: Record> = {};
export function serializeArg(value: any): SerializedWebGlArg {
-
if (value instanceof Array) {
return value.map(serializeArg);
} else if (value === null) {
@@ -53,6 +42,13 @@ export function serializeArg(value: any): SerializedWebGlArg {
rr_type: name,
args: [serializeArg(value.buffer), value.byteOffset, value.byteLength],
};
+ } else if (value instanceof HTMLImageElement) {
+ const name = value.constructor.name;
+ const { src } = value;
+ return {
+ rr_type: name,
+ src,
+ };
} else if (
value instanceof WebGLActiveInfo ||
value instanceof WebGLBuffer ||
@@ -86,71 +82,6 @@ export function serializeArg(value: any): SerializedWebGlArg {
return value;
}
-const serializeArgs = (args: Array) => {
+export const serializeArgs = (args: Array) => {
return [...args].map(serializeArg);
};
-
-export default function initCanvasWebGLMutationObserver(
- cb: canvasMutationCallback,
- win: IWindow,
- blockClass: blockClass,
- mirror: Mirror,
-): listenerHandler {
- const handlers: listenerHandler[] = [];
- const props = Object.getOwnPropertyNames(win.WebGLRenderingContext.prototype);
- for (const prop of props) {
- try {
- if (
- typeof win.WebGLRenderingContext.prototype[
- prop as keyof WebGLRenderingContext
- ] !== 'function'
- ) {
- continue;
- }
- const restoreHandler = patch(
- win.WebGLRenderingContext.prototype,
- prop,
- function (original) {
- return function (
- this: WebGLRenderingContext,
- ...args: Array
- ) {
- if (!isBlocked((this.canvas as unknown) as INode, blockClass)) {
- setTimeout(() => {
- const recordArgs = serializeArgs([...args]);
- cb({
- id: mirror.getId((this.canvas as unknown) as INode),
- type: CanvasContext.WebGL,
- property: prop,
- args: recordArgs,
- });
- }, 0);
- }
- return original.apply(this, args);
- };
- },
- );
- handlers.push(restoreHandler);
- } catch {
- const hookHandler = hookSetter(
- win.WebGLRenderingContext.prototype,
- prop,
- {
- set(v) {
- cb({
- id: mirror.getId((this.canvas as unknown) as INode),
- type: CanvasContext.WebGL,
- property: prop,
- args: [v],
- setter: true,
- });
- },
- },
- );
- handlers.push(hookHandler);
- }
- }
- return () => {
- handlers.forEach((h) => h());
- };
-}
diff --git a/packages/rrweb/src/record/observers/canvas/webgl.ts b/packages/rrweb/src/record/observers/canvas/webgl.ts
new file mode 100644
index 0000000000..79559fccfa
--- /dev/null
+++ b/packages/rrweb/src/record/observers/canvas/webgl.ts
@@ -0,0 +1,77 @@
+import { INode } from 'rrweb-snapshot';
+import {
+ blockClass,
+ CanvasContext,
+ canvasMutationCallback,
+ IWindow,
+ listenerHandler,
+ Mirror,
+ SerializedWebGlArg,
+} from '../../../types';
+import { hookSetter, isBlocked, patch } from '../../../utils';
+import { serializeArgs } from './serialize-args';
+
+export default function initCanvasWebGLMutationObserver(
+ cb: canvasMutationCallback,
+ win: IWindow,
+ blockClass: blockClass,
+ mirror: Mirror,
+): listenerHandler {
+ const handlers: listenerHandler[] = [];
+ const props = Object.getOwnPropertyNames(win.WebGLRenderingContext.prototype);
+ for (const prop of props) {
+ try {
+ if (
+ typeof win.WebGLRenderingContext.prototype[
+ prop as keyof WebGLRenderingContext
+ ] !== 'function'
+ ) {
+ continue;
+ }
+ const restoreHandler = patch(
+ win.WebGLRenderingContext.prototype,
+ prop,
+ function (original) {
+ return function (
+ this: WebGLRenderingContext,
+ ...args: Array
+ ) {
+ if (!isBlocked((this.canvas as unknown) as INode, blockClass)) {
+ setTimeout(() => {
+ const recordArgs = serializeArgs([...args]);
+ cb({
+ id: mirror.getId((this.canvas as unknown) as INode),
+ type: CanvasContext.WebGL,
+ property: prop,
+ args: recordArgs,
+ });
+ }, 0);
+ }
+ return original.apply(this, args);
+ };
+ },
+ );
+ handlers.push(restoreHandler);
+ } catch {
+ const hookHandler = hookSetter(
+ win.WebGLRenderingContext.prototype,
+ prop,
+ {
+ set(v) {
+ cb({
+ id: mirror.getId((this.canvas as unknown) as INode),
+ type: CanvasContext.WebGL,
+ property: prop,
+ args: [v],
+ setter: true,
+ });
+ },
+ },
+ );
+ handlers.push(hookHandler);
+ }
+ }
+ return () => {
+ handlers.forEach((h) => h());
+ };
+}
diff --git a/packages/rrweb/src/record/observers/canvas/webgl2.ts b/packages/rrweb/src/record/observers/canvas/webgl2.ts
new file mode 100644
index 0000000000..9589c35a60
--- /dev/null
+++ b/packages/rrweb/src/record/observers/canvas/webgl2.ts
@@ -0,0 +1,78 @@
+import { INode } from 'rrweb-snapshot';
+import {
+ blockClass,
+ CanvasContext,
+ canvasMutationCallback,
+ IWindow,
+ listenerHandler,
+ Mirror,
+} from '../../../types';
+import { hookSetter, isBlocked, patch } from '../../../utils';
+import { serializeArgs } from './serialize-args';
+
+export default function initCanvasWebGLMutationObserver(
+ cb: canvasMutationCallback,
+ win: IWindow,
+ blockClass: blockClass,
+ mirror: Mirror,
+): listenerHandler {
+ const handlers: listenerHandler[] = [];
+ const props = Object.getOwnPropertyNames(
+ win.WebGL2RenderingContext.prototype,
+ );
+ for (const prop of props) {
+ try {
+ if (
+ typeof win.WebGL2RenderingContext.prototype[
+ prop as keyof WebGL2RenderingContext
+ ] !== 'function'
+ ) {
+ continue;
+ }
+ const restoreHandler = patch(
+ win.WebGL2RenderingContext.prototype,
+ prop,
+ function (original) {
+ return function (
+ this: WebGL2RenderingContext,
+ ...args: Array
+ ) {
+ if (!isBlocked((this.canvas as unknown) as INode, blockClass)) {
+ setTimeout(() => {
+ const recordArgs = serializeArgs([...args]);
+ cb({
+ id: mirror.getId((this.canvas as unknown) as INode),
+ type: CanvasContext.WebGL,
+ property: prop,
+ args: recordArgs,
+ });
+ }, 0);
+ }
+ return original.apply(this, args);
+ };
+ },
+ );
+ handlers.push(restoreHandler);
+ } catch {
+ const hookHandler = hookSetter(
+ win.WebGL2RenderingContext.prototype,
+ prop,
+ {
+ set(v) {
+ cb({
+ id: mirror.getId((this.canvas as unknown) as INode),
+ type: CanvasContext.WebGL2,
+ property: prop,
+ args: [v],
+ setter: true,
+ });
+ },
+ },
+ );
+ handlers.push(hookHandler);
+ }
+ }
+ return () => {
+ handlers.forEach((h) => h());
+ };
+}
diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts
index ef436cc0fa..0f3dfee778 100644
--- a/packages/rrweb/src/replay/canvas/webgl.ts
+++ b/packages/rrweb/src/replay/canvas/webgl.ts
@@ -72,6 +72,10 @@ export function deserializeArg(arg: SerializedWebGlArg): any {
return new ctor(...args.map(deserializeArg));
} else if ('base64' in arg) {
return decode(arg.base64);
+ } else if ('src' in arg) {
+ const image = new Image();
+ image.src = arg.src;
+ return image;
}
} else if (Array.isArray(arg)) {
return arg.map(deserializeArg);
diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts
index 6dd78b36c2..bed9ba50b0 100644
--- a/packages/rrweb/src/types.ts
+++ b/packages/rrweb/src/types.ts
@@ -392,6 +392,10 @@ export type SerializedWebGlArg =
rr_type: 'ArrayBuffer';
base64: string; // base64
}
+ | {
+ rr_type: string;
+ src: string; // url of image
+ }
| {
rr_type: string;
args: Array;
diff --git a/packages/rrweb/test/record/serialize-args.test.ts b/packages/rrweb/test/record/serialize-args.test.ts
index 9f3fd43862..3f362e53c3 100644
--- a/packages/rrweb/test/record/serialize-args.test.ts
+++ b/packages/rrweb/test/record/serialize-args.test.ts
@@ -4,7 +4,7 @@
import { polyfillWebGLGlobals } from '../utils';
polyfillWebGLGlobals();
-import { serializeArg } from '../../src/record/observers/canvas-web-gl';
+import { serializeArg } from '../../src/record/observers/canvas/serialize-args';
describe('serializeArg', () => {
it('should serialize Float32Array values', async () => {
@@ -13,7 +13,7 @@ describe('serializeArg', () => {
rr_type: 'Float32Array',
args: [[-1, -1, 3, -1, -1, 3]],
};
- expect(serializeArg(float32Array)).toEqual(expected);
+ expect(serializeArg(float32Array)).toStrictEqual(expected);
});
it('should serialize Float64Array values', async () => {
@@ -23,7 +23,7 @@ describe('serializeArg', () => {
args: [[-1, -1, 3, -1, -1, 3]],
};
- expect(serializeArg(float64Array)).toEqual(expected);
+ expect(serializeArg(float64Array)).toStrictEqual(expected);
});
it('should serialize ArrayBuffer values', async () => {
@@ -33,7 +33,17 @@ describe('serializeArg', () => {
base64: 'AQIABA==',
};
- expect(serializeArg(arrayBuffer)).toEqual(expected);
+ expect(serializeArg(arrayBuffer)).toStrictEqual(expected);
+ });
+
+ it('should serialize Uint8Array values', async () => {
+ const object = new Uint8Array([1, 2, 0, 4]);
+ const expected = {
+ rr_type: 'Uint8Array',
+ args: [[1, 2, 0, 4]],
+ };
+
+ expect(serializeArg(object)).toStrictEqual(expected);
});
it('should serialize DataView values', async () => {
@@ -50,12 +60,12 @@ describe('serializeArg', () => {
],
};
- expect(serializeArg(dataView)).toEqual(expected);
+ expect(serializeArg(dataView)).toStrictEqual(expected);
});
it('should leave arrays intact', async () => {
const array = [1, 2, 3, 4];
- expect(serializeArg(array)).toEqual(array);
+ expect(serializeArg(array)).toStrictEqual(array);
});
it('should serialize complex objects', async () => {
@@ -76,7 +86,7 @@ describe('serializeArg', () => {
6,
];
- expect(serializeArg(dataView)).toEqual(expected);
+ expect(serializeArg(dataView)).toStrictEqual(expected);
});
it('should serialize arraybuffer contents', async () => {
@@ -86,7 +96,7 @@ describe('serializeArg', () => {
base64: 'AACAPwAAAEAAAEBAAACAQA==',
};
- expect(serializeArg(buffer)).toEqual(expected);
+ expect(serializeArg(buffer)).toStrictEqual(expected);
});
it('should leave null as-is', async () => {
@@ -95,9 +105,18 @@ describe('serializeArg', () => {
it('should support indexed variables', async () => {
const webGLProgram = new WebGLProgram();
- expect(serializeArg(webGLProgram)).toEqual({
+ expect(serializeArg(webGLProgram)).toStrictEqual({
rr_type: 'WebGLProgram',
index: 0,
});
});
+
+ it('should support HTMLImageElements', async () => {
+ const image = new Image();
+ image.src = 'http://example.com/image.png';
+ expect(serializeArg(image)).toStrictEqual({
+ rr_type: 'HTMLImageElement',
+ src: 'http://example.com/image.png',
+ });
+ });
});
diff --git a/packages/rrweb/test/replay/deserialize-args.test.ts b/packages/rrweb/test/replay/deserialize-args.test.ts
index c5026867c1..855a9ef13f 100644
--- a/packages/rrweb/test/replay/deserialize-args.test.ts
+++ b/packages/rrweb/test/replay/deserialize-args.test.ts
@@ -133,4 +133,15 @@ describe('deserializeArg', () => {
it('should leave null as-is', async () => {
expect(deserializeArg(null)).toStrictEqual(null);
});
+
+ it('should support HTMLImageElements', async () => {
+ const image = new Image();
+ image.src = 'http://example.com/image.png';
+ expect(
+ deserializeArg({
+ rr_type: 'HTMLImageElement',
+ src: 'http://example.com/image.png',
+ }),
+ ).toStrictEqual(image);
+ });
});
diff --git a/packages/rrweb/test/replay/webgl-mutation.test.ts b/packages/rrweb/test/replay/webgl-mutation.test.ts
index dc9f1b836a..478aee3ed3 100644
--- a/packages/rrweb/test/replay/webgl-mutation.test.ts
+++ b/packages/rrweb/test/replay/webgl-mutation.test.ts
@@ -2,12 +2,12 @@
* @jest-environment jsdom
*/
-import webglMutation, { variableListFor } from '../../src/replay/canvas/webgl';
-import { CanvasContext, IncrementalSource } from '../../src/types';
import { polyfillWebGLGlobals } from '../utils';
-
polyfillWebGLGlobals();
+import webglMutation, { variableListFor } from '../../src/replay/canvas/webgl';
+import { CanvasContext, IncrementalSource } from '../../src/types';
+
let canvas: HTMLCanvasElement;
describe('webglMutation', () => {
beforeEach(() => {
diff --git a/packages/rrweb/typings/record/observers/canvas/2d.d.ts b/packages/rrweb/typings/record/observers/canvas/2d.d.ts
new file mode 100644
index 0000000000..23d51acb6b
--- /dev/null
+++ b/packages/rrweb/typings/record/observers/canvas/2d.d.ts
@@ -0,0 +1,2 @@
+import { blockClass, canvasMutationCallback, IWindow, listenerHandler, Mirror } from '../../../types';
+export default function initCanvas2DMutationObserver(cb: canvasMutationCallback, win: IWindow, blockClass: blockClass, mirror: Mirror): listenerHandler;
diff --git a/packages/rrweb/typings/record/observers/canvas/serialize-args.d.ts b/packages/rrweb/typings/record/observers/canvas/serialize-args.d.ts
new file mode 100644
index 0000000000..b6dc836dbc
--- /dev/null
+++ b/packages/rrweb/typings/record/observers/canvas/serialize-args.d.ts
@@ -0,0 +1,3 @@
+import { SerializedWebGlArg } from '../../../types';
+export declare function serializeArg(value: any): SerializedWebGlArg;
+export declare const serializeArgs: (args: Array) => SerializedWebGlArg[];
diff --git a/packages/rrweb/typings/record/observers/canvas/webgl.d.ts b/packages/rrweb/typings/record/observers/canvas/webgl.d.ts
new file mode 100644
index 0000000000..c1263a8f58
--- /dev/null
+++ b/packages/rrweb/typings/record/observers/canvas/webgl.d.ts
@@ -0,0 +1,2 @@
+import { blockClass, canvasMutationCallback, IWindow, listenerHandler, Mirror } from '../../../types';
+export default function initCanvasWebGLMutationObserver(cb: canvasMutationCallback, win: IWindow, blockClass: blockClass, mirror: Mirror): listenerHandler;
diff --git a/packages/rrweb/typings/record/observers/canvas/webgl2.d.ts b/packages/rrweb/typings/record/observers/canvas/webgl2.d.ts
new file mode 100644
index 0000000000..c1263a8f58
--- /dev/null
+++ b/packages/rrweb/typings/record/observers/canvas/webgl2.d.ts
@@ -0,0 +1,2 @@
+import { blockClass, canvasMutationCallback, IWindow, listenerHandler, Mirror } from '../../../types';
+export default function initCanvasWebGLMutationObserver(cb: canvasMutationCallback, win: IWindow, blockClass: blockClass, mirror: Mirror): listenerHandler;
From 766a4789e5c5e1fe195571b8efb3d7d21ff2214b Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 2 Dec 2021 16:20:45 +0100
Subject: [PATCH 23/93] Support serializing ImageData
---
.../record/observers/canvas/serialize-args.ts | 9 +++++-
.../rrweb/test/record/serialize-args.test.ts | 28 +++++++++++++++++++
packages/rrweb/test/utils.ts | 13 +++++++++
3 files changed, 49 insertions(+), 1 deletion(-)
diff --git a/packages/rrweb/src/record/observers/canvas/serialize-args.ts b/packages/rrweb/src/record/observers/canvas/serialize-args.ts
index f4a50907cd..40e7074298 100644
--- a/packages/rrweb/src/record/observers/canvas/serialize-args.ts
+++ b/packages/rrweb/src/record/observers/canvas/serialize-args.ts
@@ -16,7 +16,8 @@ export function serializeArg(value: any): SerializedWebGlArg {
value instanceof Uint8Array ||
value instanceof Uint16Array ||
value instanceof Int16Array ||
- value instanceof Int8Array
+ value instanceof Int8Array ||
+ value instanceof Uint8ClampedArray
) {
const name = value.constructor.name;
return {
@@ -49,6 +50,12 @@ export function serializeArg(value: any): SerializedWebGlArg {
rr_type: name,
src,
};
+ } else if (value instanceof ImageData) {
+ const name = value.constructor.name;
+ return {
+ rr_type: name,
+ args: [serializeArg(value.data), value.width, value.height],
+ };
} else if (
value instanceof WebGLActiveInfo ||
value instanceof WebGLBuffer ||
diff --git a/packages/rrweb/test/record/serialize-args.test.ts b/packages/rrweb/test/record/serialize-args.test.ts
index 3f362e53c3..6a719e9e94 100644
--- a/packages/rrweb/test/record/serialize-args.test.ts
+++ b/packages/rrweb/test/record/serialize-args.test.ts
@@ -119,4 +119,32 @@ describe('serializeArg', () => {
src: 'http://example.com/image.png',
});
});
+
+ it('should serialize ImageData', async () => {
+ const arr = new Uint8ClampedArray(40000);
+
+ // Iterate through every pixel
+ for (let i = 0; i < arr.length; i += 4) {
+ arr[i + 0] = 0; // R value
+ arr[i + 1] = 190; // G value
+ arr[i + 2] = 0; // B value
+ arr[i + 3] = 255; // A value
+ }
+
+ // Initialize a new ImageData object
+ let imageData = new ImageData(arr, 200, 50);
+
+ const contents = Array.from(arr);
+ expect(serializeArg(imageData)).toStrictEqual({
+ rr_type: 'ImageData',
+ args: [
+ {
+ rr_type: 'Uint8ClampedArray',
+ args: [contents],
+ },
+ 200,
+ 50,
+ ],
+ });
+ });
});
diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts
index fbe34c8d45..bab49946d4 100644
--- a/packages/rrweb/test/utils.ts
+++ b/packages/rrweb/test/utils.ts
@@ -392,4 +392,17 @@ export const polyfillWebGLGlobals = () => {
}
global.WebGLVertexArrayObject = WebGLVertexArrayObject as any;
+
+ class ImageData {
+ public data: Uint8ClampedArray;
+ public width: number;
+ public height: number;
+ constructor(data: Uint8ClampedArray, width: number, height: number) {
+ this.data = data;
+ this.width = width;
+ this.height = height;
+ }
+ }
+
+ global.ImageData = ImageData as any;
};
From 35b60502017fdf10088498013cb03b36dc6b8173 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 2 Dec 2021 16:21:18 +0100
Subject: [PATCH 24/93] Correctly classify WebGL2 events
---
packages/rrweb/src/record/observers/canvas/webgl2.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/rrweb/src/record/observers/canvas/webgl2.ts b/packages/rrweb/src/record/observers/canvas/webgl2.ts
index 9589c35a60..68bd0e1690 100644
--- a/packages/rrweb/src/record/observers/canvas/webgl2.ts
+++ b/packages/rrweb/src/record/observers/canvas/webgl2.ts
@@ -42,7 +42,7 @@ export default function initCanvasWebGLMutationObserver(
const recordArgs = serializeArgs([...args]);
cb({
id: mirror.getId((this.canvas as unknown) as INode),
- type: CanvasContext.WebGL,
+ type: CanvasContext.WebGL2,
property: prop,
args: recordArgs,
});
From af42d0f9b35a54364705016dde8ef253e5010c29 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 2 Dec 2021 16:54:27 +0100
Subject: [PATCH 25/93] Serialize format changed
---
packages/rrweb/test/record/webgl.test.ts | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts
index 12e6d5eee4..be55835009 100644
--- a/packages/rrweb/test/record/webgl.test.ts
+++ b/packages/rrweb/test/record/webgl.test.ts
@@ -168,7 +168,12 @@ describe('record webgl', function (this: ISuite) {
source: IncrementalSource.CanvasMutation,
property: 'linkProgram',
type: CanvasContext.WebGL,
- args: ['$WebGLProgram#1'], // `program1` is WebGLProgram, this is the second WebGLProgram variable (#1)
+ args: [
+ {
+ index: 1,
+ rr_type: 'WebGLProgram',
+ },
+ ], // `program1` is WebGLProgram, this is the second WebGLProgram variable (index #1)
},
});
});
From 637e3580d678fbf88ffc475f8ff47d7d0db30a72 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 2 Dec 2021 20:39:24 +0100
Subject: [PATCH 26/93] check if canvas has contents before we save the dataURL
---
packages/rrweb-snapshot/src/snapshot.ts | 21 ++++++++++++++-------
1 file changed, 14 insertions(+), 7 deletions(-)
diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts
index 00b97a2620..1928649b9e 100644
--- a/packages/rrweb-snapshot/src/snapshot.ts
+++ b/packages/rrweb-snapshot/src/snapshot.ts
@@ -492,13 +492,20 @@ function serializeNode(
delete attributes.selected;
}
}
- // 2d canvas image data
- if (
- tagName === 'canvas' &&
- recordCanvas &&
- (n as HTMLCanvasElement).getContext('2d')
- ) {
- attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL();
+ // canvas image data
+ if (tagName === 'canvas' && recordCanvas) {
+ const canvasDataURL = (n as HTMLCanvasElement).toDataURL();
+
+ // create blank canvas of same dimensions
+ const blankCanvas = document.createElement('canvas');
+ blankCanvas.width = (n as HTMLCanvasElement).width;
+ blankCanvas.height = (n as HTMLCanvasElement).height;
+ const blankCanvasDataURL = blankCanvas.toDataURL();
+
+ // no need to save dataURL if it's the same as blank canvas
+ if (canvasDataURL !== blankCanvasDataURL) {
+ attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL();
+ }
}
// media elements
if (tagName === 'audio' || tagName === 'video') {
From e2e715000b77d7c9c71fbac0830995248e7f4cfe Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 2 Dec 2021 20:41:19 +0100
Subject: [PATCH 27/93] Remove blank dataURL
---
packages/rrweb/test/__snapshots__/integration.test.ts.snap | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/packages/rrweb/test/__snapshots__/integration.test.ts.snap b/packages/rrweb/test/__snapshots__/integration.test.ts.snap
index f2b42154c3..56ea412acf 100644
--- a/packages/rrweb/test/__snapshots__/integration.test.ts.snap
+++ b/packages/rrweb/test/__snapshots__/integration.test.ts.snap
@@ -9646,8 +9646,7 @@ exports[`record integration tests should record webgl canvas mutations 1`] = `
\\"id\\": \\"myCanvas\\",
\\"width\\": \\"200\\",
\\"height\\": \\"100\\",
- \\"style\\": \\"border: 1px solid #000000\\",
- \\"rr_dataURL\\": \\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABkCAYAAADDhn8LAAACnklEQVR4Xu3VsRHAMAzEsHj/pTOBXbB9pFchyLycz0eAwFXgsCFA4C4gEK+DwENAIJ4HAYF4AwSagD9IczM1IiCQkUNbswkIpLmZGhEQyMihrdkEBNLcTI0ICGTk0NZsAgJpbqZGBAQycmhrNgGBNDdTIwICGTm0NZuAQJqbqREBgYwc2ppNQCDNzdSIgEBGDm3NJiCQ5mZqREAgI4e2ZhMQSHMzNSIgkJFDW7MJCKS5mRoREMjIoa3ZBATS3EyNCAhk5NDWbAICaW6mRgQEMnJoazYBgTQ3UyMCAhk5tDWbgECam6kRAYGMHNqaTUAgzc3UiIBARg5tzSYgkOZmakRAICOHtmYTEEhzMzUiIJCRQ1uzCQikuZkaERDIyKGt2QQE0txMjQgIZOTQ1mwCAmlupkYEBDJyaGs2AYE0N1MjAgIZObQ1m4BAmpupEQGBjBzamk1AIM3N1IiAQEYObc0mIJDmZmpEQCAjh7ZmExBIczM1IiCQkUNbswkIpLmZGhEQyMihrdkEBNLcTI0ICGTk0NZsAgJpbqZGBAQycmhrNgGBNDdTIwICGTm0NZuAQJqbqREBgYwc2ppNQCDNzdSIgEBGDm3NJiCQ5mZqREAgI4e2ZhMQSHMzNSIgkJFDW7MJCKS5mRoREMjIoa3ZBATS3EyNCAhk5NDWbAICaW6mRgQEMnJoazYBgTQ3UyMCAhk5tDWbgECam6kRAYGMHNqaTUAgzc3UiIBARg5tzSYgkOZmakRAICOHtmYTEEhzMzUiIJCRQ1uzCQikuZkaERDIyKGt2QQE0txMjQgIZOTQ1mwCAmlupkYEBDJyaGs2AYE0N1MjAgIZObQ1m4BAmpupEQGBjBzamk1AIM3N1IiAQEYObc0mIJDmZmpE4Af1gABlH0hlGgAAAABJRU5ErkJggg==\\"
+ \\"style\\": \\"border: 1px solid #000000\\"
},
\\"childNodes\\": [
{
From 6910276d64cb96519301b8e46f439894a6a1413a Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Mon, 6 Dec 2021 16:30:22 +0100
Subject: [PATCH 28/93] reference original file not type defintion file
---
packages/rrweb/src/replay/canvas/2d.ts | 2 +-
packages/rrweb/src/replay/canvas/webgl.ts | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/packages/rrweb/src/replay/canvas/2d.ts b/packages/rrweb/src/replay/canvas/2d.ts
index 44b4f651d2..7daa7e4224 100644
--- a/packages/rrweb/src/replay/canvas/2d.ts
+++ b/packages/rrweb/src/replay/canvas/2d.ts
@@ -1,4 +1,4 @@
-import { Replayer } from '../../../typings/entries/all';
+import { Replayer } from '../';
import { canvasMutationData } from '../../types';
export default function canvasMutation({
diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts
index 0f3dfee778..e28693de6d 100644
--- a/packages/rrweb/src/replay/canvas/webgl.ts
+++ b/packages/rrweb/src/replay/canvas/webgl.ts
@@ -1,5 +1,5 @@
import { decode } from 'base64-arraybuffer';
-import { Replayer } from '../../../typings/entries/all';
+import { Replayer } from '../';
import {
CanvasContext,
canvasMutationData,
@@ -96,7 +96,6 @@ export default function webglMutation({
const ctx = getContext(target, mutation.type);
if (!ctx) return;
-
if (mutation.setter) {
// skip some read-only type checks
// tslint:disable-next-line:no-any
From 7463e30031fd6c0a2dd555271607534429b1024c Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Mon, 6 Dec 2021 16:41:00 +0100
Subject: [PATCH 29/93] update types
---
packages/rrweb/typings/replay/canvas/2d.d.ts | 2 +-
packages/rrweb/typings/replay/canvas/webgl.d.ts | 2 +-
packages/rrweb/typings/types.d.ts | 16 +++++++++++++++-
3 files changed, 17 insertions(+), 3 deletions(-)
diff --git a/packages/rrweb/typings/replay/canvas/2d.d.ts b/packages/rrweb/typings/replay/canvas/2d.d.ts
index 85942bf827..9f05cc2ef9 100644
--- a/packages/rrweb/typings/replay/canvas/2d.d.ts
+++ b/packages/rrweb/typings/replay/canvas/2d.d.ts
@@ -1,4 +1,4 @@
-import { Replayer } from '../../../typings/entries/all';
+import { Replayer } from '../';
import { canvasMutationData } from '../../types';
export default function canvasMutation({ event, mutation, target, imageMap, errorHandler, }: {
event: Parameters[0];
diff --git a/packages/rrweb/typings/replay/canvas/webgl.d.ts b/packages/rrweb/typings/replay/canvas/webgl.d.ts
index 3e87fe9565..f1cf4b587b 100644
--- a/packages/rrweb/typings/replay/canvas/webgl.d.ts
+++ b/packages/rrweb/typings/replay/canvas/webgl.d.ts
@@ -1,4 +1,4 @@
-import { Replayer } from '../../../typings/entries/all';
+import { Replayer } from '../';
import { canvasMutationData, SerializedWebGlArg } from '../../types';
export declare function variableListFor(ctor: string): any[];
export declare function deserializeArg(arg: SerializedWebGlArg): any;
diff --git a/packages/rrweb/typings/types.d.ts b/packages/rrweb/typings/types.d.ts
index 59bd91a472..44eaa68e26 100644
--- a/packages/rrweb/typings/types.d.ts
+++ b/packages/rrweb/typings/types.d.ts
@@ -282,8 +282,22 @@ export declare enum MouseInteractions {
}
export declare enum CanvasContext {
'2D' = 0,
- WebGL = 1
+ WebGL = 1,
+ WebGL2 = 2
}
+export declare type SerializedWebGlArg = {
+ rr_type: 'ArrayBuffer';
+ base64: string;
+} | {
+ rr_type: string;
+ src: string;
+} | {
+ rr_type: string;
+ args: Array;
+} | {
+ rr_type: string;
+ index: number;
+} | string | number | boolean | null | SerializedWebGlArg[];
declare type mouseInteractionParam = {
type: MouseInteractions;
id: number;
From 76b8efb9889143a8ff0a88a080de9414550057da Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Mon, 6 Dec 2021 16:51:31 +0100
Subject: [PATCH 30/93] rename code worspace
---
.gitignore | 2 +-
.vscode/monorepo.code-workspace | 27 ----------------------
.vscode/rrweb-monorepo.code-workspace | 32 +++++++++++++++++++++++++++
3 files changed, 33 insertions(+), 28 deletions(-)
delete mode 100644 .vscode/monorepo.code-workspace
create mode 100644 .vscode/rrweb-monorepo.code-workspace
diff --git a/.gitignore b/.gitignore
index c70cf42b7f..895e1115aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
.vscode/*
-!/.vscode/monorepo.code-workspace
+!/.vscode/rrweb-monorepo.code-workspace
.idea
node_modules
package-lock.json
diff --git a/.vscode/monorepo.code-workspace b/.vscode/monorepo.code-workspace
deleted file mode 100644
index e0895cc7ff..0000000000
--- a/.vscode/monorepo.code-workspace
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "folders": [
- {
- "name": "_RRWeb Monorepo",
- "path": ".."
- },
- {
- "path": "../packages/rrdom"
- },
- {
- "path": "../packages/rrweb"
- },
- {
- "path": "../packages/rrweb-player"
- },
- {
- "path": "../packages/rrweb-snapshot"
- }
- ],
- "settings": {
- "jest.disabledWorkspaceFolders": [
- "_RRWeb Monorepo",
- "rrweb-player",
- "rrdom"
- ]
- }
-}
diff --git a/.vscode/rrweb-monorepo.code-workspace b/.vscode/rrweb-monorepo.code-workspace
new file mode 100644
index 0000000000..37d44396ae
--- /dev/null
+++ b/.vscode/rrweb-monorepo.code-workspace
@@ -0,0 +1,32 @@
+{
+ "folders": [
+ {
+ "name": " rrweb monorepo", // added a space to bump it to the top
+ "path": ".."
+ },
+ {
+ "name": "rrdom (package)",
+ "path": "../packages/rrdom"
+ },
+ {
+ "name": "rrweb (package)",
+ "path": "../packages/rrweb"
+ },
+ {
+ "name": "rrweb-player (package)",
+ "path": "../packages/rrweb-player"
+ },
+ {
+ "name": "rrweb-snapshot (package)",
+ "path": "../packages/rrweb-snapshot"
+ }
+ ],
+ "settings": {
+ "jest.disabledWorkspaceFolders": [
+ " rrweb monorepo",
+ "rrweb-player (package)",
+ "rrdom (package)"
+ ],
+ "liveServer.settings.multiRootWorkspaceName": "rrweb"
+ }
+}
From e2cfdbb0c229c8a3f7bc07dffd615af82cb3bef5 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Mon, 6 Dec 2021 16:57:49 +0100
Subject: [PATCH 31/93] update dependencies
---
yarn.lock | 135 +++++++++++++++++++++++++++++++++++++++++++-----------
1 file changed, 109 insertions(+), 26 deletions(-)
diff --git a/yarn.lock b/yarn.lock
index 38976cfc8d..634560a808 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2846,9 +2846,9 @@ color-name@^1.0.0, color-name@~1.1.4:
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-string@^1.6.0:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312"
- integrity sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.8.2.tgz#08bd49fa5f3889c27b0c670052ed746dd7a671de"
+ integrity sha512-w5ZkKRdLsc5NOYsmnpS2DpyRW71npwZGwbRpLrJTuqjfTs2Bhrba7UiV59IX9siBlCPl2pne5NtiwnVWUzvYFA==
dependencies:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
@@ -3719,7 +3719,33 @@ error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
-es-abstract@^1.17.2, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2:
+es-abstract@^1.17.2, es-abstract@^1.19.1:
+ version "1.19.1"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3"
+ integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==
+ dependencies:
+ call-bind "^1.0.2"
+ es-to-primitive "^1.2.1"
+ function-bind "^1.1.1"
+ get-intrinsic "^1.1.1"
+ get-symbol-description "^1.0.0"
+ has "^1.0.3"
+ has-symbols "^1.0.2"
+ internal-slot "^1.0.3"
+ is-callable "^1.2.4"
+ is-negative-zero "^2.0.1"
+ is-regex "^1.1.4"
+ is-shared-array-buffer "^1.0.1"
+ is-string "^1.0.7"
+ is-weakref "^1.0.1"
+ object-inspect "^1.11.0"
+ object-keys "^1.1.1"
+ object.assign "^4.1.2"
+ string.prototype.trimend "^1.0.4"
+ string.prototype.trimstart "^1.0.4"
+ unbox-primitive "^1.0.1"
+
+es-abstract@^1.18.0-next.2:
version "1.18.3"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.3.tgz#25c4c3380a27aa203c44b2b685bba94da31b63e0"
integrity sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==
@@ -4415,7 +4441,7 @@ get-caller-file@^2.0.5:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-get-intrinsic@^1.0.2, get-intrinsic@^1.1.1:
+get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
@@ -4471,6 +4497,14 @@ get-stream@^6.0.0:
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
+get-symbol-description@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6"
+ integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==
+ dependencies:
+ call-bind "^1.0.2"
+ get-intrinsic "^1.1.1"
+
getpass@^0.1.1:
version "0.1.7"
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
@@ -4708,6 +4742,13 @@ has-symbols@^1.0.1, has-symbols@^1.0.2:
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
+has-tostringtag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25"
+ integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==
+ dependencies:
+ has-symbols "^1.0.2"
+
has-unicode@^2.0.0, has-unicode@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
@@ -5040,6 +5081,15 @@ inquirer@^7.3.3:
strip-ansi "^6.0.0"
through "^2.3.6"
+internal-slot@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
+ integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==
+ dependencies:
+ get-intrinsic "^1.1.0"
+ has "^1.0.3"
+ side-channel "^1.0.4"
+
ip@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
@@ -5094,6 +5144,11 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.3:
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.3.tgz#8b1e0500b73a1d76c70487636f368e519de8db8e"
integrity sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==
+is-callable@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
+ integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
+
is-ci@^1.0.10:
version "1.2.1"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c"
@@ -5331,6 +5386,14 @@ is-regex@^1.1.3:
call-bind "^1.0.2"
has-symbols "^1.0.2"
+is-regex@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
+ integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
+ dependencies:
+ call-bind "^1.0.2"
+ has-tostringtag "^1.0.0"
+
is-resolvable@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
@@ -5341,6 +5404,11 @@ is-retry-allowed@^1.0.0:
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4"
integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==
+is-shared-array-buffer@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6"
+ integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==
+
is-ssh@^1.3.0:
version "1.3.3"
resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.3.3.tgz#7f133285ccd7f2c2c7fc897b771b53d95a2b2c7e"
@@ -5363,6 +5431,13 @@ is-string@^1.0.5, is-string@^1.0.6:
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.6.tgz#3fe5d5992fb0d93404f32584d4b0179a71b54a5f"
integrity sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==
+is-string@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
+ integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
+ dependencies:
+ has-tostringtag "^1.0.0"
+
is-symbol@^1.0.2, is-symbol@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c"
@@ -5382,6 +5457,13 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0:
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
+is-weakref@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.1.tgz#842dba4ec17fa9ac9850df2d6efbc1737274f2a2"
+ integrity sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==
+ dependencies:
+ call-bind "^1.0.0"
+
isarray@1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -7106,7 +7188,7 @@ object-assign@^4.0.1, object-assign@^4.1.0:
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
-object-inspect@^1.10.3, object-inspect@^1.9.0:
+object-inspect@^1.10.3, object-inspect@^1.11.0, object-inspect@^1.9.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1"
integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==
@@ -7126,7 +7208,7 @@ object.assign@^4.1.2:
has-symbols "^1.0.1"
object-keys "^1.1.1"
-object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0, object.getownpropertydescriptors@^2.1.1:
+object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.2.tgz#1bd63aeacf0d5d2d2f31b5e393b03a7c601a23f7"
integrity sha512-WtxeKSzfBjlzL+F9b7M7hewDzMwy+C8NRssHd1YrNlzHzIDrXcXiNOMrezdAEM4UXixgV+vvnyBeN7Rygl2ttQ==
@@ -7135,6 +7217,15 @@ object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0
define-properties "^1.1.3"
es-abstract "^1.18.0-next.2"
+object.getownpropertydescriptors@^2.1.0:
+ version "2.1.3"
+ resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz#b223cf38e17fefb97a63c10c91df72ccb386df9e"
+ integrity sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw==
+ dependencies:
+ call-bind "^1.0.2"
+ define-properties "^1.1.3"
+ es-abstract "^1.19.1"
+
object.omit@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
@@ -7144,13 +7235,13 @@ object.omit@^2.0.0:
is-extendable "^0.1.1"
object.values@^1.1.0:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30"
- integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac"
+ integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.3"
- es-abstract "^1.18.2"
+ es-abstract "^1.19.1"
on-finished@~2.3.0:
version "2.3.0"
@@ -7931,13 +8022,12 @@ postcss@^6.0.1, postcss@^6.0.11:
supports-color "^5.4.0"
postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.27:
- version "7.0.36"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.36.tgz#056f8cffa939662a8f5905950c07d5285644dfcb"
- integrity sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==
+ version "7.0.39"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309"
+ integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==
dependencies:
- chalk "^2.4.2"
+ picocolors "^0.2.1"
source-map "^0.6.1"
- supports-color "^6.1.0"
prelude-ls@^1.2.1:
version "1.2.1"
@@ -8545,9 +8635,9 @@ rollup-plugin-postcss@^3.1.1:
style-inject "^0.3.0"
rollup-plugin-rename-node-modules@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/rollup-plugin-rename-node-modules/-/rollup-plugin-rename-node-modules-1.1.0.tgz#c73de5fed61b997857993813a7053285e2cca2dd"
- integrity sha512-JpfsJ7NYI/4OdqWvZ/BY6fgjZb5j7sRFvHMv8EU0zrFiNUcW4ke9tw7WXImsHnjq7Bp3xv+UILRPpA7plOa38Q==
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-rename-node-modules/-/rollup-plugin-rename-node-modules-1.2.0.tgz#f1f1bb2192d1bbec258569bf6bda097002d7dbdf"
+ integrity sha512-IKsS3eJXHLAMXIndzNso9ijWJw1V3mqubRc2gb67v7VuLX9t41LObXqciM0JC3j7/WrHeptG47cejFU0qxXUJA==
dependencies:
estree-walker "^2.0.1"
magic-string "^0.25.7"
@@ -9167,13 +9257,6 @@ supports-color@^5.3.0, supports-color@^5.4.0:
dependencies:
has-flag "^3.0.0"
-supports-color@^6.1.0:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
- integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
- dependencies:
- has-flag "^3.0.0"
-
supports-color@^7.0.0, supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
From 5d14caad8fcb55a3e754eaee02108f0110635849 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Mon, 6 Dec 2021 18:12:04 +0100
Subject: [PATCH 32/93] add spector to inspect webgl
---
packages/rrweb/package.json | 1 +
yarn.lock | 19 +++++++++++++++++++
2 files changed, 20 insertions(+)
diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json
index 2638eeb08f..00634a6015 100644
--- a/packages/rrweb/package.json
+++ b/packages/rrweb/package.json
@@ -66,6 +66,7 @@
"rollup-plugin-postcss": "^3.1.1",
"rollup-plugin-rename-node-modules": "^1.1.0",
"rollup-plugin-terser": "^7.0.2",
+ "spector": "^0.0.0",
"ts-jest": "^27.0.5",
"ts-node": "^7.0.1",
"tslib": "^1.9.3",
diff --git a/yarn.lock b/yarn.lock
index 634560a808..bda38821f2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2757,6 +2757,13 @@ cli-cursor@^3.1.0:
dependencies:
restore-cursor "^3.1.0"
+cli-table@*:
+ version "0.3.11"
+ resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.11.tgz#ac69cdecbe81dccdba4889b9a18b7da312a9d3ee"
+ integrity sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==
+ dependencies:
+ colors "1.0.3"
+
cli-width@^2.0.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
@@ -2866,6 +2873,11 @@ colorette@^1.2.2:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
+colors@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
+ integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=
+
colors@^1.1.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
@@ -8994,6 +9006,13 @@ spdx-license-ids@^3.0.0:
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz#0d9becccde7003d6c658d487dd48a32f0bf3014b"
integrity sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==
+spector@^0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/spector/-/spector-0.0.0.tgz#3be8197aa70416d76940ea17f74457871fc7b900"
+ integrity sha1-O+gZeqcEFtdpQOoX90RXhx/HuQA=
+ dependencies:
+ cli-table "*"
+
split-on-first@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
From fabb033d95ea83e0de9929a96f2bd4336e5a9c41 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Mon, 6 Dec 2021 18:12:30 +0100
Subject: [PATCH 33/93] remove live server settings from code workspace
---
.vscode/rrweb-monorepo.code-workspace | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/.vscode/rrweb-monorepo.code-workspace b/.vscode/rrweb-monorepo.code-workspace
index 37d44396ae..79849c8fe9 100644
--- a/.vscode/rrweb-monorepo.code-workspace
+++ b/.vscode/rrweb-monorepo.code-workspace
@@ -26,7 +26,6 @@
" rrweb monorepo",
"rrweb-player (package)",
"rrdom (package)"
- ],
- "liveServer.settings.multiRootWorkspaceName": "rrweb"
+ ]
}
}
From 2641e29c7e1bd8df027647a5616020da336bd092 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Wed, 8 Dec 2021 12:09:16 +0100
Subject: [PATCH 34/93] Save canvas context in the node
Prevents from saving webgl canvases as 2d dataUrls
---
packages/rrweb-snapshot/src/snapshot.ts | 7 +++-
packages/rrweb-snapshot/src/types.ts | 4 ++
packages/rrweb/src/record/observer.ts | 3 ++
.../src/record/observers/canvas/canvas.ts | 38 +++++++++++++++++++
packages/rrweb/test/record/webgl.test.ts | 37 ++++++++++++++++++
5 files changed, 88 insertions(+), 1 deletion(-)
create mode 100644 packages/rrweb/src/record/observers/canvas/canvas.ts
diff --git a/packages/rrweb-snapshot/src/snapshot.ts b/packages/rrweb-snapshot/src/snapshot.ts
index 9d85f01d5c..4a34ed0eae 100644
--- a/packages/rrweb-snapshot/src/snapshot.ts
+++ b/packages/rrweb-snapshot/src/snapshot.ts
@@ -10,6 +10,7 @@ import {
MaskTextFn,
MaskInputFn,
KeepIframeSrcFn,
+ ICanvas,
documentNode,
} from './types';
import { isElement, isShadowRoot, maskInputValue } from './utils';
@@ -493,7 +494,11 @@ function serializeNode(
}
}
// canvas image data
- if (tagName === 'canvas' && recordCanvas) {
+ if (
+ tagName === 'canvas' &&
+ recordCanvas &&
+ (!('__context' in n) || (n as ICanvas).__context === '2d') // only record this on 2d canvas
+ ) {
const canvasDataURL = (n as HTMLCanvasElement).toDataURL();
// create blank canvas of same dimensions
diff --git a/packages/rrweb-snapshot/src/types.ts b/packages/rrweb-snapshot/src/types.ts
index 8720783ee5..f239213d32 100644
--- a/packages/rrweb-snapshot/src/types.ts
+++ b/packages/rrweb-snapshot/src/types.ts
@@ -71,6 +71,10 @@ export interface INode extends Node {
__sn: serializedNodeWithId;
}
+export interface ICanvas extends HTMLCanvasElement {
+ __context: string;
+}
+
export type idNodeMap = {
[key: number]: INode;
};
diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts
index 3efc97290d..6ea1940a8f 100644
--- a/packages/rrweb/src/record/observer.ts
+++ b/packages/rrweb/src/record/observer.ts
@@ -49,6 +49,7 @@ import {
import MutationBuffer from './mutation';
import { IframeManager } from './iframe-manager';
import { ShadowDomManager } from './shadow-dom-manager';
+import initCanvasContextObserver from './observers/canvas/canvas';
import initCanvas2DMutationObserver from './observers/canvas/2d';
import initCanvasWebGLMutationObserver from './observers/canvas/webgl';
import initCanvasWebGL2MutationObserver from './observers/canvas/webgl2';
@@ -715,6 +716,7 @@ function initCanvasMutationObserver(
blockClass: blockClass,
mirror: Mirror,
): listenerHandler {
+ const canvasContextReset = initCanvasContextObserver(win, blockClass);
const canvas2DReset = initCanvas2DMutationObserver(
cb,
win,
@@ -737,6 +739,7 @@ function initCanvasMutationObserver(
);
return () => {
+ canvasContextReset();
canvas2DReset();
canvasWebGLReset();
canvasWebGL2Reset();
diff --git a/packages/rrweb/src/record/observers/canvas/canvas.ts b/packages/rrweb/src/record/observers/canvas/canvas.ts
new file mode 100644
index 0000000000..8808c8acb6
--- /dev/null
+++ b/packages/rrweb/src/record/observers/canvas/canvas.ts
@@ -0,0 +1,38 @@
+import { INode } from 'rrweb-snapshot';
+import { blockClass, IWindow, listenerHandler } from '../../../types';
+import { isBlocked, patch } from '../../../utils';
+
+// TODO: replace me for ICanvas from rrweb-snapshot
+export type OgmentedCanvas = HTMLCanvasElement & { __context: string };
+
+export default function initCanvasContextObserver(
+ win: IWindow,
+ blockClass: blockClass,
+): listenerHandler {
+ const handlers: listenerHandler[] = [];
+ try {
+ const restoreHandler = patch(
+ win.HTMLCanvasElement.prototype,
+ 'getContext',
+ function (original) {
+ return function (
+ this: OgmentedCanvas,
+ contextType: string,
+ ...args: Array
+ ) {
+ if (!isBlocked((this as unknown) as INode, blockClass)) {
+ if (!('__context' in this))
+ (this as OgmentedCanvas).__context = contextType;
+ }
+ return original.apply(this, [contextType, ...args]);
+ };
+ },
+ );
+ handlers.push(restoreHandler);
+ } catch {
+ console.error('failed to patch HTMLCanvasElement.prototype.getContext');
+ }
+ return () => {
+ handlers.forEach((h) => h());
+ };
+}
diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts
index be55835009..8e9d6b5903 100644
--- a/packages/rrweb/test/record/webgl.test.ts
+++ b/packages/rrweb/test/record/webgl.test.ts
@@ -13,6 +13,7 @@ import {
CanvasContext,
} from '../../src/types';
import { assertSnapshot, launchPuppeteer } from '../utils';
+import { OgmentedCanvas } from '../../src/record/observers/canvas/canvas';
interface ISuite {
code: string;
@@ -177,4 +178,40 @@ describe('record webgl', function (this: ISuite) {
},
});
});
+
+ it('sets _context on canvas.getContext()', async () => {
+ await ctx.page.evaluate(() => {
+ const { record } = ((window as unknown) as IWindow).rrweb;
+ record({
+ recordCanvas: true,
+ emit: ((window as unknown) as IWindow).emit,
+ });
+ });
+ const context = await ctx.page.evaluate(() => {
+ var canvas = document.getElementById('canvas') as HTMLCanvasElement;
+ canvas.getContext('webgl')!;
+ return (canvas as OgmentedCanvas).__context;
+ });
+
+ expect(context).toBe('webgl');
+ });
+
+ it('only sets _context on first canvas.getContext() call', async () => {
+ await ctx.page.evaluate(() => {
+ const { record } = ((window as unknown) as IWindow).rrweb;
+ record({
+ recordCanvas: true,
+ emit: ((window as unknown) as IWindow).emit,
+ });
+ });
+ const context = await ctx.page.evaluate(() => {
+ var canvas = document.getElementById('canvas') as HTMLCanvasElement;
+ canvas.getContext('webgl');
+ canvas.getContext('2d'); // returns null
+ return (canvas as OgmentedCanvas).__context;
+ });
+
+ expect(context).toBe('webgl');
+ });
+});
});
From bc7f2baf11b0941b34be06b85775884e3bd52308 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Wed, 8 Dec 2021 12:46:22 +0100
Subject: [PATCH 35/93] remove extra braces
---
packages/rrweb/test/record/webgl.test.ts | 1 -
1 file changed, 1 deletion(-)
diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts
index 8e9d6b5903..4ac6daf3af 100644
--- a/packages/rrweb/test/record/webgl.test.ts
+++ b/packages/rrweb/test/record/webgl.test.ts
@@ -214,4 +214,3 @@ describe('record webgl', function (this: ISuite) {
expect(context).toBe('webgl');
});
});
-});
From fbf67a0ee6158cbb988f9c7b8cd797d737af35e9 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Wed, 8 Dec 2021 12:49:15 +0100
Subject: [PATCH 36/93] add ICanvas type
---
packages/rrweb-snapshot/typings/types.d.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/packages/rrweb-snapshot/typings/types.d.ts b/packages/rrweb-snapshot/typings/types.d.ts
index 5831c7b5fa..262d3f9aef 100644
--- a/packages/rrweb-snapshot/typings/types.d.ts
+++ b/packages/rrweb-snapshot/typings/types.d.ts
@@ -55,6 +55,9 @@ export declare type tagMap = {
export interface INode extends Node {
__sn: serializedNodeWithId;
}
+export interface ICanvas extends HTMLCanvasElement {
+ __context: string;
+}
export declare type idNodeMap = {
[key: number]: INode;
};
From 63adef7bd47a271c2c88f80003774b6170b6397d Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Wed, 8 Dec 2021 12:53:03 +0100
Subject: [PATCH 37/93] use ICanvas from rrweb-snapshot in rrweb instead of
OgmentedCanvas
---
packages/rrweb/src/record/observers/canvas/canvas.ts | 9 +++------
packages/rrweb/test/record/webgl.test.ts | 6 +++---
.../rrweb/typings/record/observers/canvas/canvas.d.ts | 2 ++
3 files changed, 8 insertions(+), 9 deletions(-)
create mode 100644 packages/rrweb/typings/record/observers/canvas/canvas.d.ts
diff --git a/packages/rrweb/src/record/observers/canvas/canvas.ts b/packages/rrweb/src/record/observers/canvas/canvas.ts
index 8808c8acb6..437af7d5f3 100644
--- a/packages/rrweb/src/record/observers/canvas/canvas.ts
+++ b/packages/rrweb/src/record/observers/canvas/canvas.ts
@@ -1,10 +1,7 @@
-import { INode } from 'rrweb-snapshot';
+import { INode, ICanvas } from 'rrweb-snapshot';
import { blockClass, IWindow, listenerHandler } from '../../../types';
import { isBlocked, patch } from '../../../utils';
-// TODO: replace me for ICanvas from rrweb-snapshot
-export type OgmentedCanvas = HTMLCanvasElement & { __context: string };
-
export default function initCanvasContextObserver(
win: IWindow,
blockClass: blockClass,
@@ -16,13 +13,13 @@ export default function initCanvasContextObserver(
'getContext',
function (original) {
return function (
- this: OgmentedCanvas,
+ this: ICanvas,
contextType: string,
...args: Array
) {
if (!isBlocked((this as unknown) as INode, blockClass)) {
if (!('__context' in this))
- (this as OgmentedCanvas).__context = contextType;
+ (this as ICanvas).__context = contextType;
}
return original.apply(this, [contextType, ...args]);
};
diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts
index 4ac6daf3af..8c46e5c69e 100644
--- a/packages/rrweb/test/record/webgl.test.ts
+++ b/packages/rrweb/test/record/webgl.test.ts
@@ -13,7 +13,7 @@ import {
CanvasContext,
} from '../../src/types';
import { assertSnapshot, launchPuppeteer } from '../utils';
-import { OgmentedCanvas } from '../../src/record/observers/canvas/canvas';
+import { ICanvas } from 'rrweb-snapshot';
interface ISuite {
code: string;
@@ -190,7 +190,7 @@ describe('record webgl', function (this: ISuite) {
const context = await ctx.page.evaluate(() => {
var canvas = document.getElementById('canvas') as HTMLCanvasElement;
canvas.getContext('webgl')!;
- return (canvas as OgmentedCanvas).__context;
+ return (canvas as ICanvas).__context;
});
expect(context).toBe('webgl');
@@ -208,7 +208,7 @@ describe('record webgl', function (this: ISuite) {
var canvas = document.getElementById('canvas') as HTMLCanvasElement;
canvas.getContext('webgl');
canvas.getContext('2d'); // returns null
- return (canvas as OgmentedCanvas).__context;
+ return (canvas as ICanvas).__context;
});
expect(context).toBe('webgl');
diff --git a/packages/rrweb/typings/record/observers/canvas/canvas.d.ts b/packages/rrweb/typings/record/observers/canvas/canvas.d.ts
new file mode 100644
index 0000000000..359d95928d
--- /dev/null
+++ b/packages/rrweb/typings/record/observers/canvas/canvas.d.ts
@@ -0,0 +1,2 @@
+import { blockClass, IWindow, listenerHandler } from '../../../types';
+export default function initCanvasContextObserver(win: IWindow, blockClass: blockClass): listenerHandler;
From d958d5e5917d92b4a94a6407d5aa320b99128e0b Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Wed, 8 Dec 2021 16:32:44 +0100
Subject: [PATCH 38/93] add snapshots and webgl 2 tests
---
.../record/__snapshots__/webgl.test.ts.snap | 451 ++++++++++++++++++
packages/rrweb/test/record/webgl.test.ts | 64 ++-
2 files changed, 514 insertions(+), 1 deletion(-)
create mode 100644 packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap
diff --git a/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap b/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap
new file mode 100644
index 0000000000..5638ec5d7c
--- /dev/null
+++ b/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap
@@ -0,0 +1,451 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`record webgl will record changes to a canvas element 1`] = `
+"[
+ {
+ \\"type\\": 4,
+ \\"data\\": {
+ \\"href\\": \\"about:blank\\",
+ \\"width\\": 1920,
+ \\"height\\": 1080
+ }
+ },
+ {
+ \\"type\\": 2,
+ \\"data\\": {
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"id\\": 2
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"id\\": 4
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 6
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"canvas\\",
+ \\"attributes\\": {
+ \\"id\\": \\"canvas\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 7
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
+ \\"id\\": 8
+ }
+ ],
+ \\"id\\": 5
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 9,
+ \\"id\\": 7,
+ \\"type\\": 1,
+ \\"property\\": \\"clear\\",
+ \\"args\\": [
+ 16384
+ ]
+ }
+ }
+]"
+`;
+
+exports[`record webgl will record changes to a canvas element before the canvas gets added (webgl2) 1`] = `
+"[
+ {
+ \\"type\\": 4,
+ \\"data\\": {
+ \\"href\\": \\"about:blank\\",
+ \\"width\\": 1920,
+ \\"height\\": 1080
+ }
+ },
+ {
+ \\"type\\": 2,
+ \\"data\\": {
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"id\\": 2
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"id\\": 4
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 6
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"canvas\\",
+ \\"attributes\\": {
+ \\"id\\": \\"canvas\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 7
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
+ \\"id\\": 8
+ }
+ ],
+ \\"id\\": 5
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"removes\\": [],
+ \\"adds\\": [
+ {
+ \\"parentId\\": 5,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 2,
+ \\"tagName\\": \\"canvas\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"id\\": 9
+ }
+ }
+ ]
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 9,
+ \\"id\\": 9,
+ \\"type\\": 2,
+ \\"property\\": \\"createProgram\\",
+ \\"args\\": []
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 9,
+ \\"id\\": 9,
+ \\"type\\": 2,
+ \\"property\\": \\"linkProgram\\",
+ \\"args\\": [
+ {
+ \\"rr_type\\": \\"WebGLProgram\\",
+ \\"index\\": 0
+ }
+ ]
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 9,
+ \\"id\\": 9,
+ \\"type\\": 2,
+ \\"property\\": \\"clear\\",
+ \\"args\\": [
+ 16384
+ ]
+ }
+ }
+]"
+`;
+
+exports[`record webgl will record changes to a canvas element before the canvas gets added 1`] = `
+"[
+ {
+ \\"type\\": 4,
+ \\"data\\": {
+ \\"href\\": \\"about:blank\\",
+ \\"width\\": 1920,
+ \\"height\\": 1080
+ }
+ },
+ {
+ \\"type\\": 2,
+ \\"data\\": {
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"id\\": 2
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"id\\": 4
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 6
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"canvas\\",
+ \\"attributes\\": {
+ \\"id\\": \\"canvas\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 7
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
+ \\"id\\": 8
+ }
+ ],
+ \\"id\\": 5
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 0,
+ \\"texts\\": [],
+ \\"attributes\\": [],
+ \\"removes\\": [],
+ \\"adds\\": [
+ {
+ \\"parentId\\": 5,
+ \\"nextId\\": null,
+ \\"node\\": {
+ \\"type\\": 2,
+ \\"tagName\\": \\"canvas\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"id\\": 9
+ }
+ }
+ ]
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 9,
+ \\"id\\": 9,
+ \\"type\\": 1,
+ \\"property\\": \\"createProgram\\",
+ \\"args\\": []
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 9,
+ \\"id\\": 9,
+ \\"type\\": 1,
+ \\"property\\": \\"linkProgram\\",
+ \\"args\\": [
+ {
+ \\"rr_type\\": \\"WebGLProgram\\",
+ \\"index\\": 0
+ }
+ ]
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 9,
+ \\"id\\": 9,
+ \\"type\\": 1,
+ \\"property\\": \\"clear\\",
+ \\"args\\": [
+ 16384
+ ]
+ }
+ }
+]"
+`;
+
+exports[`record webgl will record changes to a webgl2 canvas element 1`] = `
+"[
+ {
+ \\"type\\": 4,
+ \\"data\\": {
+ \\"href\\": \\"about:blank\\",
+ \\"width\\": 1920,
+ \\"height\\": 1080
+ }
+ },
+ {
+ \\"type\\": 2,
+ \\"data\\": {
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"id\\": 2
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"id\\": 4
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 6
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"canvas\\",
+ \\"attributes\\": {
+ \\"id\\": \\"canvas\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 7
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
+ \\"id\\": 8
+ }
+ ],
+ \\"id\\": 5
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 9,
+ \\"id\\": 7,
+ \\"type\\": 2,
+ \\"property\\": \\"clear\\",
+ \\"args\\": [
+ 16384
+ ]
+ }
+ }
+]"
+`;
diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts
index 8c46e5c69e..848cb8ac5f 100644
--- a/packages/rrweb/test/record/webgl.test.ts
+++ b/packages/rrweb/test/record/webgl.test.ts
@@ -112,6 +112,38 @@ describe('record webgl', function (this: ISuite) {
property: 'clear',
},
});
+ assertSnapshot(ctx.events);
+ });
+
+ it('will record changes to a webgl2 canvas element', async () => {
+ await ctx.page.evaluate(() => {
+ const { record } = ((window as unknown) as IWindow).rrweb;
+ record({
+ recordCanvas: true,
+ emit(event: eventWithTime) {
+ ((window as unknown) as IWindow).emit(event);
+ },
+ });
+ });
+ await ctx.page.evaluate(() => {
+ var canvas = document.getElementById('canvas') as HTMLCanvasElement;
+ var gl = canvas.getContext('webgl2')!;
+
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ });
+
+ await ctx.page.waitForTimeout(50);
+
+ const lastEvent = ctx.events[ctx.events.length - 1];
+ expect(lastEvent).toMatchObject({
+ data: {
+ source: IncrementalSource.CanvasMutation,
+ args: [16384],
+ type: CanvasContext.WebGL2,
+ property: 'clear',
+ },
+ });
+ assertSnapshot(ctx.events);
});
it('will record changes to a canvas element before the canvas gets added', async () => {
@@ -141,7 +173,37 @@ describe('record webgl', function (this: ISuite) {
property: 'clear',
},
});
- // TODO: make this a jest snapshot
+ assertSnapshot(ctx.events);
+ });
+
+ it('will record changes to a canvas element before the canvas gets added (webgl2)', async () => {
+ await ctx.page.evaluate(() => {
+ const { record } = ((window as unknown) as IWindow).rrweb;
+ record({
+ recordCanvas: true,
+ emit: ((window as unknown) as IWindow).emit,
+ });
+ });
+ await ctx.page.evaluate(() => {
+ var canvas = document.createElement('canvas');
+ var gl = canvas.getContext('webgl2')!;
+ var program = gl.createProgram()!;
+ gl.linkProgram(program);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ document.body.appendChild(canvas);
+ });
+
+ await ctx.page.waitForTimeout(50);
+
+ const lastEvent = ctx.events[ctx.events.length - 1];
+ expect(lastEvent).toMatchObject({
+ data: {
+ source: IncrementalSource.CanvasMutation,
+ type: CanvasContext.WebGL2,
+ property: 'clear',
+ },
+ });
+ assertSnapshot(ctx.events);
});
it('will record webgl variables', async () => {
From f730f578047d166a2ce06012ce35940673b7263b Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 9 Dec 2021 11:57:58 +0100
Subject: [PATCH 39/93] Upgrade to puppeteer 12.0.1
---
packages/rrweb/package.json | 2 +-
yarn.lock | 18 +++++++++---------
2 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json
index 00634a6015..667b848a49 100644
--- a/packages/rrweb/package.json
+++ b/packages/rrweb/package.json
@@ -61,7 +61,7 @@
"jsdom": "^17.0.0",
"jsdom-global": "^3.0.2",
"prettier": "2.2.1",
- "puppeteer": "^11.0.0",
+ "puppeteer": "^12.0.1",
"rollup": "^2.45.2",
"rollup-plugin-postcss": "^3.1.1",
"rollup-plugin-rename-node-modules": "^1.1.0",
diff --git a/yarn.lock b/yarn.lock
index bda38821f2..a9d7e1ae57 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3514,10 +3514,10 @@ detect-newline@^3.0.0:
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
-devtools-protocol@0.0.901419:
- version "0.0.901419"
- resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.901419.tgz#79b5459c48fe7e1c5563c02bd72f8fec3e0cebcd"
- integrity sha512-4INMPwNm9XRpBukhNbF7OB6fNTTCaI8pzy/fXg0xQzAy5h3zL1P8xT3QazgKqBrb/hAYwIBizqDBZ7GtJE74QQ==
+devtools-protocol@0.0.937139:
+ version "0.0.937139"
+ resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.937139.tgz#bdee3751fdfdb81cb701fd3afa94b1065dafafcf"
+ integrity sha512-daj+rzR3QSxsPRy5vjjthn58axO8c11j58uY0lG5vvlJk/EiOdCWOptGdkXDjtuRHr78emKq0udHCXM4trhoDQ==
dezalgo@^1.0.0:
version "1.0.3"
@@ -8187,13 +8187,13 @@ puppeteer@^1.15.0:
rimraf "^2.6.1"
ws "^6.1.0"
-puppeteer@^11.0.0:
- version "11.0.0"
- resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-11.0.0.tgz#0808719c38e15315ecc1b1c28911f1c9054d201f"
- integrity sha512-6rPFqN1ABjn4shgOICGDBITTRV09EjXVqhDERBDKwCLz0UyBxeeBH6Ay0vQUJ84VACmlxwzOIzVEJXThcF3aNg==
+puppeteer@^12.0.1:
+ version "12.0.1"
+ resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-12.0.1.tgz#ae79d0e174a07563e0bf2e05c94ccafce3e70033"
+ integrity sha512-YQ3GRiyZW0ddxTW+iiQcv2/8TT5c3+FcRUCg7F8q2gHqxd5akZN400VRXr9cHQKLWGukmJLDiE72MrcLK9tFHQ==
dependencies:
debug "4.3.2"
- devtools-protocol "0.0.901419"
+ devtools-protocol "0.0.937139"
extract-zip "2.0.1"
https-proxy-agent "5.0.0"
node-fetch "2.6.5"
From cc7c0552768cd82d18bfe2c029b0af488f40ac7a Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 9 Dec 2021 12:18:43 +0100
Subject: [PATCH 40/93] Revert back to puppeteer 9.1.1
---
packages/rrweb/package.json | 2 +-
yarn.lock | 141 +++++++++++++++---------------------
2 files changed, 59 insertions(+), 84 deletions(-)
diff --git a/packages/rrweb/package.json b/packages/rrweb/package.json
index 667b848a49..63602f8f9a 100644
--- a/packages/rrweb/package.json
+++ b/packages/rrweb/package.json
@@ -61,7 +61,7 @@
"jsdom": "^17.0.0",
"jsdom-global": "^3.0.2",
"prettier": "2.2.1",
- "puppeteer": "^12.0.1",
+ "puppeteer": "^9.1.1",
"rollup": "^2.45.2",
"rollup-plugin-postcss": "^3.1.1",
"rollup-plugin-rename-node-modules": "^1.1.0",
diff --git a/yarn.lock b/yarn.lock
index a9d7e1ae57..11bb99098c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3398,7 +3398,7 @@ debug@2.6.9, debug@^2.6.9:
dependencies:
ms "2.0.0"
-debug@4, debug@4.3.2, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
+debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
@@ -3514,10 +3514,10 @@ detect-newline@^3.0.0:
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
-devtools-protocol@0.0.937139:
- version "0.0.937139"
- resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.937139.tgz#bdee3751fdfdb81cb701fd3afa94b1065dafafcf"
- integrity sha512-daj+rzR3QSxsPRy5vjjthn58axO8c11j58uY0lG5vvlJk/EiOdCWOptGdkXDjtuRHr78emKq0udHCXM4trhoDQ==
+devtools-protocol@0.0.869402:
+ version "0.0.869402"
+ resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.869402.tgz#03ade701761742e43ae4de5dc188bcd80f156d8d"
+ integrity sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA==
dezalgo@^1.0.0:
version "1.0.3"
@@ -4102,17 +4102,6 @@ extglob@^0.3.1:
dependencies:
is-extglob "^1.0.0"
-extract-zip@2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
- integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
- dependencies:
- debug "^4.1.1"
- get-stream "^5.1.0"
- yauzl "^2.10.0"
- optionalDependencies:
- "@types/yauzl" "^2.9.1"
-
extract-zip@^1.6.6:
version "1.7.0"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
@@ -4123,6 +4112,17 @@ extract-zip@^1.6.6:
mkdirp "^0.5.4"
yauzl "^2.10.0"
+extract-zip@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a"
+ integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==
+ dependencies:
+ debug "^4.1.1"
+ get-stream "^5.1.0"
+ yauzl "^2.10.0"
+ optionalDependencies:
+ "@types/yauzl" "^2.9.1"
+
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@@ -4867,14 +4867,6 @@ http-signature@~1.2.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
-https-proxy-agent@5.0.0, https-proxy-agent@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
- integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
- dependencies:
- agent-base "6"
- debug "4"
-
https-proxy-agent@^2.2.1:
version "2.2.4"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b"
@@ -4883,6 +4875,14 @@ https-proxy-agent@^2.2.1:
agent-base "^4.3.0"
debug "^3.1.0"
+https-proxy-agent@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
+ integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
+ dependencies:
+ agent-base "6"
+ debug "4"
+
human-signals@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
@@ -6932,13 +6932,6 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
-node-fetch@2.6.5:
- version "2.6.5"
- resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
- integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==
- dependencies:
- whatwg-url "^5.0.0"
-
node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
@@ -7650,7 +7643,7 @@ pixelmatch@^5.1.0:
dependencies:
pngjs "^4.0.1"
-pkg-dir@4.2.0, pkg-dir@^4.2.0:
+pkg-dir@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
@@ -8089,7 +8082,7 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
-progress@2.0.3, progress@^2.0.0, progress@^2.0.1:
+progress@^2.0.0, progress@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
@@ -8145,7 +8138,7 @@ proxy-addr@~2.0.5:
forwarded "0.2.0"
ipaddr.js "1.9.1"
-proxy-from-env@1.1.0, proxy-from-env@^1.0.0:
+proxy-from-env@^1.0.0, proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
@@ -8187,23 +8180,23 @@ puppeteer@^1.15.0:
rimraf "^2.6.1"
ws "^6.1.0"
-puppeteer@^12.0.1:
- version "12.0.1"
- resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-12.0.1.tgz#ae79d0e174a07563e0bf2e05c94ccafce3e70033"
- integrity sha512-YQ3GRiyZW0ddxTW+iiQcv2/8TT5c3+FcRUCg7F8q2gHqxd5akZN400VRXr9cHQKLWGukmJLDiE72MrcLK9tFHQ==
- dependencies:
- debug "4.3.2"
- devtools-protocol "0.0.937139"
- extract-zip "2.0.1"
- https-proxy-agent "5.0.0"
- node-fetch "2.6.5"
- pkg-dir "4.2.0"
- progress "2.0.3"
- proxy-from-env "1.1.0"
- rimraf "3.0.2"
- tar-fs "2.1.1"
- unbzip2-stream "1.4.3"
- ws "8.2.3"
+puppeteer@^9.1.1:
+ version "9.1.1"
+ resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-9.1.1.tgz#f74b7facf86887efd6c6b9fabb7baae6fdce012c"
+ integrity sha512-W+nOulP2tYd/ZG99WuZC/I5ljjQQ7EUw/jQGcIb9eu8mDlZxNY2SgcJXTLG9h5gRvqA3uJOe4hZXYsd3EqioMw==
+ dependencies:
+ debug "^4.1.0"
+ devtools-protocol "0.0.869402"
+ extract-zip "^2.0.0"
+ https-proxy-agent "^5.0.0"
+ node-fetch "^2.6.1"
+ pkg-dir "^4.2.0"
+ progress "^2.0.1"
+ proxy-from-env "^1.1.0"
+ rimraf "^3.0.2"
+ tar-fs "^2.0.0"
+ unbzip2-stream "^1.3.3"
+ ws "^7.2.3"
q@^1.1.2, q@^1.5.1:
version "1.5.1"
@@ -8598,13 +8591,6 @@ rgba-regex@^1.0.0:
resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
-rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
- integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
- dependencies:
- glob "^7.1.3"
-
rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
@@ -8612,6 +8598,13 @@ rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3:
dependencies:
glob "^7.1.3"
+rimraf@^3.0.0, rimraf@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
+ integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
+ dependencies:
+ glob "^7.1.3"
+
rollup-plugin-css-only@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/rollup-plugin-css-only/-/rollup-plugin-css-only-3.1.0.tgz#6a701cc5b051c6b3f0961e69b108a9a118e1b1df"
@@ -9364,7 +9357,7 @@ table@^6.0.9:
string-width "^4.2.0"
strip-ansi "^6.0.0"
-tar-fs@2.1.1:
+tar-fs@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
@@ -9567,11 +9560,6 @@ tr46@^2.1.0:
dependencies:
punycode "^2.1.1"
-tr46@~0.0.3:
- version "0.0.3"
- resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
- integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
-
trim-newlines@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
@@ -9775,7 +9763,7 @@ unbox-primitive@^1.0.1:
has-symbols "^1.0.2"
which-boxed-primitive "^1.0.2"
-unbzip2-stream@1.4.3:
+unbzip2-stream@^1.3.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7"
integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==
@@ -9998,11 +9986,6 @@ wcwidth@^1.0.0:
dependencies:
defaults "^1.0.3"
-webidl-conversions@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
- integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
-
webidl-conversions@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
@@ -10025,14 +10008,6 @@ whatwg-mimetype@^2.3.0:
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
-whatwg-url@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
- integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
- dependencies:
- tr46 "~0.0.3"
- webidl-conversions "^3.0.0"
-
whatwg-url@^8.0.0, whatwg-url@^8.4.0, whatwg-url@^8.5.0:
version "8.7.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77"
@@ -10170,11 +10145,6 @@ write-pkg@^4.0.0:
type-fest "^0.4.1"
write-json-file "^3.2.0"
-ws@8.2.3:
- version "8.2.3"
- resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
- integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
-
ws@^6.1.0:
version "6.2.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e"
@@ -10182,6 +10152,11 @@ ws@^6.1.0:
dependencies:
async-limiter "~1.0.0"
+ws@^7.2.3:
+ version "7.5.6"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b"
+ integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==
+
ws@^7.4.3, ws@^7.4.5:
version "7.5.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74"
From 313adb42d2c8d11700e010f2b30da41a905f36ba Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Tue, 14 Dec 2021 17:18:38 +0100
Subject: [PATCH 41/93] Keep index order consistent between replay and record
---
.../record/observers/canvas/serialize-args.ts | 31 ++++++++++++++++
.../src/record/observers/canvas/webgl.ts | 7 ++--
packages/rrweb/test/record/webgl.test.ts | 35 +++++++++++++++++++
3 files changed, 71 insertions(+), 2 deletions(-)
diff --git a/packages/rrweb/src/record/observers/canvas/serialize-args.ts b/packages/rrweb/src/record/observers/canvas/serialize-args.ts
index 40e7074298..29d4dba44d 100644
--- a/packages/rrweb/src/record/observers/canvas/serialize-args.ts
+++ b/packages/rrweb/src/record/observers/canvas/serialize-args.ts
@@ -92,3 +92,34 @@ export function serializeArg(value: any): SerializedWebGlArg {
export const serializeArgs = (args: Array) => {
return [...args].map(serializeArg);
};
+
+export const saveWebGLVar = (value: any): number | void => {
+ if (
+ !(
+ value instanceof WebGLActiveInfo ||
+ value instanceof WebGLBuffer ||
+ value instanceof WebGLFramebuffer ||
+ value instanceof WebGLProgram ||
+ value instanceof WebGLRenderbuffer ||
+ value instanceof WebGLShader ||
+ value instanceof WebGLShaderPrecisionFormat ||
+ value instanceof WebGLTexture ||
+ value instanceof WebGLUniformLocation ||
+ value instanceof WebGLVertexArrayObject ||
+ // In Chrome, value won't be an instanceof WebGLVertexArrayObject.
+ (value && value.constructor.name == 'WebGLVertexArrayObjectOES') ||
+ typeof value === 'object'
+ )
+ )
+ return;
+
+ const name = value.constructor.name;
+ const list = webGLVars[name] || (webGLVars[name] = []);
+ let index = list.indexOf(value);
+
+ if (index === -1) {
+ index = list.length;
+ list.push(value);
+ }
+ return index;
+};
diff --git a/packages/rrweb/src/record/observers/canvas/webgl.ts b/packages/rrweb/src/record/observers/canvas/webgl.ts
index 79559fccfa..0e2adf680d 100644
--- a/packages/rrweb/src/record/observers/canvas/webgl.ts
+++ b/packages/rrweb/src/record/observers/canvas/webgl.ts
@@ -9,7 +9,7 @@ import {
SerializedWebGlArg,
} from '../../../types';
import { hookSetter, isBlocked, patch } from '../../../utils';
-import { serializeArgs } from './serialize-args';
+import { saveWebGLVar, serializeArgs } from './serialize-args';
export default function initCanvasWebGLMutationObserver(
cb: canvasMutationCallback,
@@ -47,7 +47,10 @@ export default function initCanvasWebGLMutationObserver(
});
}, 0);
}
- return original.apply(this, args);
+
+ const result = original.apply(this, args);
+ saveWebGLVar(result);
+ return result;
};
},
);
diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts
index 848cb8ac5f..f65319cc22 100644
--- a/packages/rrweb/test/record/webgl.test.ts
+++ b/packages/rrweb/test/record/webgl.test.ts
@@ -241,6 +241,41 @@ describe('record webgl', function (this: ISuite) {
});
});
+ it('will record webgl variables in reverse order', async () => {
+ await ctx.page.evaluate(() => {
+ const { record } = ((window as unknown) as IWindow).rrweb;
+ record({
+ recordCanvas: true,
+ emit: ((window as unknown) as IWindow).emit,
+ });
+ });
+ await ctx.page.evaluate(() => {
+ var canvas = document.getElementById('canvas') as HTMLCanvasElement;
+ var gl = canvas.getContext('webgl')!;
+ var program0 = gl.createProgram()!;
+ var program1 = gl.createProgram()!;
+ gl.linkProgram(program1);
+ gl.linkProgram(program0);
+ });
+
+ await ctx.page.waitForTimeout(50);
+
+ const lastEvent = ctx.events[ctx.events.length - 1];
+ expect(lastEvent).toMatchObject({
+ data: {
+ source: IncrementalSource.CanvasMutation,
+ property: 'linkProgram',
+ type: CanvasContext.WebGL,
+ args: [
+ {
+ index: 0,
+ rr_type: 'WebGLProgram',
+ },
+ ], // `program0` is WebGLProgram, this is the first WebGLProgram variable (index #0)
+ },
+ });
+ });
+
it('sets _context on canvas.getContext()', async () => {
await ctx.page.evaluate(() => {
const { record } = ((window as unknown) as IWindow).rrweb;
From 723acf81bb863232a33be0699deefdd81fea5dcf Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Tue, 14 Dec 2021 17:21:14 +0100
Subject: [PATCH 42/93] keep correct index order in webgl2
---
packages/rrweb/src/record/observers/canvas/webgl2.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/packages/rrweb/src/record/observers/canvas/webgl2.ts b/packages/rrweb/src/record/observers/canvas/webgl2.ts
index 68bd0e1690..a2c3c42cc8 100644
--- a/packages/rrweb/src/record/observers/canvas/webgl2.ts
+++ b/packages/rrweb/src/record/observers/canvas/webgl2.ts
@@ -37,6 +37,8 @@ export default function initCanvasWebGLMutationObserver(
this: WebGL2RenderingContext,
...args: Array
) {
+ const result = original.apply(this, args);
+ saveWebGLVar(result);
if (!isBlocked((this.canvas as unknown) as INode, blockClass)) {
setTimeout(() => {
const recordArgs = serializeArgs([...args]);
@@ -48,7 +50,7 @@ export default function initCanvasWebGLMutationObserver(
});
}, 0);
}
- return original.apply(this, args);
+ return result;
};
},
);
From 3f9eebd1f9e85007289c0bf2f77bfb72b401a69e Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Tue, 14 Dec 2021 17:21:46 +0100
Subject: [PATCH 43/93] fixed forgotten import
---
packages/rrweb/src/record/observers/canvas/webgl2.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/rrweb/src/record/observers/canvas/webgl2.ts b/packages/rrweb/src/record/observers/canvas/webgl2.ts
index a2c3c42cc8..99f7e38dae 100644
--- a/packages/rrweb/src/record/observers/canvas/webgl2.ts
+++ b/packages/rrweb/src/record/observers/canvas/webgl2.ts
@@ -8,7 +8,7 @@ import {
Mirror,
} from '../../../types';
import { hookSetter, isBlocked, patch } from '../../../utils';
-import { serializeArgs } from './serialize-args';
+import { saveWebGLVar, serializeArgs } from './serialize-args';
export default function initCanvasWebGLMutationObserver(
cb: canvasMutationCallback,
From 4a6b556470f41431903a860921083825cc6f5817 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Tue, 14 Dec 2021 17:28:28 +0100
Subject: [PATCH 44/93] buffer up pending canvas mutations
---
.../src/record/observers/canvas/webgl2.ts | 68 ++++++++++++++++---
packages/rrweb/test/record/webgl.test.ts | 4 +-
2 files changed, 62 insertions(+), 10 deletions(-)
diff --git a/packages/rrweb/src/record/observers/canvas/webgl2.ts b/packages/rrweb/src/record/observers/canvas/webgl2.ts
index 99f7e38dae..4fce44d729 100644
--- a/packages/rrweb/src/record/observers/canvas/webgl2.ts
+++ b/packages/rrweb/src/record/observers/canvas/webgl2.ts
@@ -3,6 +3,7 @@ import {
blockClass,
CanvasContext,
canvasMutationCallback,
+ canvasMutationParam,
IWindow,
listenerHandler,
Mirror,
@@ -10,6 +11,37 @@ import {
import { hookSetter, isBlocked, patch } from '../../../utils';
import { saveWebGLVar, serializeArgs } from './serialize-args';
+const pendingCanvasMutations = new Map<
+ HTMLCanvasElement,
+ canvasMutationParam[]
+>();
+
+// FIXME: total hack here, we need to find a better way to do this
+function flushPendingCanvasMutations(
+ cb: canvasMutationCallback,
+ mirror: Mirror,
+) {
+ pendingCanvasMutations.forEach(
+ (values: canvasMutationParam[], canvas: HTMLCanvasElement) => {
+ const id = mirror.getId((canvas as unknown) as INode);
+ flushPendingCanvasMutationFor(canvas, id, cb);
+ },
+ );
+ requestAnimationFrame(() => flushPendingCanvasMutations(cb, mirror));
+}
+
+function flushPendingCanvasMutationFor(
+ canvas: HTMLCanvasElement,
+ id: number,
+ cb: canvasMutationCallback,
+) {
+ const values = pendingCanvasMutations.get(canvas);
+ if (!values || id === -1) return;
+
+ values.forEach((p) => cb({ ...p, id }));
+ pendingCanvasMutations.delete(canvas);
+}
+
export default function initCanvasWebGLMutationObserver(
cb: canvasMutationCallback,
win: IWindow,
@@ -20,6 +52,10 @@ export default function initCanvasWebGLMutationObserver(
const props = Object.getOwnPropertyNames(
win.WebGL2RenderingContext.prototype,
);
+
+ // TODO: replace me
+ requestAnimationFrame(() => flushPendingCanvasMutations(cb, mirror));
+
for (const prop of props) {
try {
if (
@@ -40,16 +76,30 @@ export default function initCanvasWebGLMutationObserver(
const result = original.apply(this, args);
saveWebGLVar(result);
if (!isBlocked((this.canvas as unknown) as INode, blockClass)) {
- setTimeout(() => {
- const recordArgs = serializeArgs([...args]);
- cb({
- id: mirror.getId((this.canvas as unknown) as INode),
- type: CanvasContext.WebGL2,
- property: prop,
- args: recordArgs,
- });
- }, 0);
+ const id = mirror.getId((this.canvas as unknown) as INode);
+
+ const recordArgs = serializeArgs([...args]);
+ const mutation = {
+ id,
+ type: CanvasContext.WebGL2,
+ property: prop,
+ args: recordArgs,
+ };
+
+ if (id === -1) {
+ if (!pendingCanvasMutations.has(this.canvas))
+ pendingCanvasMutations.set(this.canvas, []);
+
+ pendingCanvasMutations
+ .get(this.canvas as HTMLCanvasElement)!
+ .push(mutation);
+ } else {
+ // flush all pending mutations
+ flushPendingCanvasMutationFor(this.canvas, id, cb);
+ cb(mutation);
+ }
}
+
return result;
};
},
diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts
index f65319cc22..6abfa6c71c 100644
--- a/packages/rrweb/test/record/webgl.test.ts
+++ b/packages/rrweb/test/record/webgl.test.ts
@@ -190,7 +190,9 @@ describe('record webgl', function (this: ISuite) {
var program = gl.createProgram()!;
gl.linkProgram(program);
gl.clear(gl.COLOR_BUFFER_BIT);
- document.body.appendChild(canvas);
+ setTimeout(() => {
+ document.body.appendChild(canvas);
+ }, 10);
});
await ctx.page.waitForTimeout(50);
From 970ee44bbb224ebe20bc48e01b1c415e354df92e Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Wed, 15 Dec 2021 15:44:29 +0100
Subject: [PATCH 45/93] unify the way webgl and webgl2 get patched
---
packages/rrweb/src/record/observer.ts | 13 +-
.../src/record/observers/canvas/webgl.ts | 165 +++++++++++++-----
.../src/record/observers/canvas/webgl2.ts | 130 --------------
3 files changed, 119 insertions(+), 189 deletions(-)
delete mode 100644 packages/rrweb/src/record/observers/canvas/webgl2.ts
diff --git a/packages/rrweb/src/record/observer.ts b/packages/rrweb/src/record/observer.ts
index 6ea1940a8f..4ec75f4e3c 100644
--- a/packages/rrweb/src/record/observer.ts
+++ b/packages/rrweb/src/record/observer.ts
@@ -52,7 +52,6 @@ import { ShadowDomManager } from './shadow-dom-manager';
import initCanvasContextObserver from './observers/canvas/canvas';
import initCanvas2DMutationObserver from './observers/canvas/2d';
import initCanvasWebGLMutationObserver from './observers/canvas/webgl';
-import initCanvasWebGL2MutationObserver from './observers/canvas/webgl2';
type WindowWithStoredMutationObserver = IWindow & {
__rrMutationObserver?: MutationObserver;
@@ -724,14 +723,7 @@ function initCanvasMutationObserver(
mirror,
);
- const canvasWebGLReset = initCanvasWebGLMutationObserver(
- cb,
- win,
- blockClass,
- mirror,
- );
-
- const canvasWebGL2Reset = initCanvasWebGL2MutationObserver(
+ const canvasWebGL1and2Reset = initCanvasWebGLMutationObserver(
cb,
win,
blockClass,
@@ -741,8 +733,7 @@ function initCanvasMutationObserver(
return () => {
canvasContextReset();
canvas2DReset();
- canvasWebGLReset();
- canvasWebGL2Reset();
+ canvasWebGL1and2Reset();
};
}
diff --git a/packages/rrweb/src/record/observers/canvas/webgl.ts b/packages/rrweb/src/record/observers/canvas/webgl.ts
index 0e2adf680d..eb3aaae4bf 100644
--- a/packages/rrweb/src/record/observers/canvas/webgl.ts
+++ b/packages/rrweb/src/record/observers/canvas/webgl.ts
@@ -3,77 +3,146 @@ import {
blockClass,
CanvasContext,
canvasMutationCallback,
+ canvasMutationParam,
IWindow,
listenerHandler,
Mirror,
- SerializedWebGlArg,
} from '../../../types';
import { hookSetter, isBlocked, patch } from '../../../utils';
import { saveWebGLVar, serializeArgs } from './serialize-args';
-export default function initCanvasWebGLMutationObserver(
+const pendingCanvasMutations = new Map<
+ HTMLCanvasElement,
+ canvasMutationParam[]
+>();
+
+// FIXME: total hack here, we need to find a better way to do this
+function flushPendingCanvasMutations(
+ cb: canvasMutationCallback,
+ mirror: Mirror,
+) {
+ pendingCanvasMutations.forEach(
+ (values: canvasMutationParam[], canvas: HTMLCanvasElement) => {
+ const id = mirror.getId((canvas as unknown) as INode);
+ flushPendingCanvasMutationFor(canvas, id, cb);
+ },
+ );
+ requestAnimationFrame(() => flushPendingCanvasMutations(cb, mirror));
+}
+
+function flushPendingCanvasMutationFor(
+ canvas: HTMLCanvasElement,
+ id: number,
+ cb: canvasMutationCallback,
+) {
+ const values = pendingCanvasMutations.get(canvas);
+ if (!values || id === -1) return;
+
+ values.forEach((p) => cb({ ...p, id }));
+ pendingCanvasMutations.delete(canvas);
+}
+
+function patchGLPrototype(
+ prototype: WebGLRenderingContext | WebGL2RenderingContext,
+ type: CanvasContext,
cb: canvasMutationCallback,
- win: IWindow,
blockClass: blockClass,
mirror: Mirror,
-): listenerHandler {
+): listenerHandler[] {
const handlers: listenerHandler[] = [];
- const props = Object.getOwnPropertyNames(win.WebGLRenderingContext.prototype);
+
+ const props = Object.getOwnPropertyNames(prototype);
+
for (const prop of props) {
try {
- if (
- typeof win.WebGLRenderingContext.prototype[
- prop as keyof WebGLRenderingContext
- ] !== 'function'
- ) {
+ if (typeof prototype[prop as keyof typeof prototype] !== 'function') {
continue;
}
- const restoreHandler = patch(
- win.WebGLRenderingContext.prototype,
- prop,
- function (original) {
- return function (
- this: WebGLRenderingContext,
- ...args: Array
- ) {
- if (!isBlocked((this.canvas as unknown) as INode, blockClass)) {
- setTimeout(() => {
- const recordArgs = serializeArgs([...args]);
- cb({
- id: mirror.getId((this.canvas as unknown) as INode),
- type: CanvasContext.WebGL,
- property: prop,
- args: recordArgs,
- });
- }, 0);
+ const restoreHandler = patch(prototype, prop, function (original) {
+ return function (this: typeof prototype, ...args: Array) {
+ const result = original.apply(this, args);
+ saveWebGLVar(result);
+ if (!isBlocked((this.canvas as unknown) as INode, blockClass)) {
+ const id = mirror.getId((this.canvas as unknown) as INode);
+
+ const recordArgs = serializeArgs([...args]);
+ const mutation = {
+ id,
+ type,
+ property: prop,
+ args: recordArgs,
+ };
+
+ if (id === -1) {
+ if (!pendingCanvasMutations.has(this.canvas))
+ pendingCanvasMutations.set(this.canvas, []);
+
+ pendingCanvasMutations
+ .get(this.canvas as HTMLCanvasElement)!
+ .push(mutation);
+ } else {
+ // flush all pending mutations
+ flushPendingCanvasMutationFor(this.canvas, id, cb);
+ cb(mutation);
}
+ }
- const result = original.apply(this, args);
- saveWebGLVar(result);
- return result;
- };
- },
- );
+ return result;
+ };
+ });
handlers.push(restoreHandler);
} catch {
- const hookHandler = hookSetter(
- win.WebGLRenderingContext.prototype,
- prop,
- {
- set(v) {
- cb({
- id: mirror.getId((this.canvas as unknown) as INode),
- type: CanvasContext.WebGL,
- property: prop,
- args: [v],
- setter: true,
- });
- },
+ const hookHandler = hookSetter(prototype, prop, {
+ set(v) {
+ cb({
+ id: mirror.getId((this.canvas as unknown) as INode),
+ type,
+ property: prop,
+ args: [v],
+ setter: true,
+ });
},
- );
+ });
handlers.push(hookHandler);
}
}
+
+ return handlers;
+}
+
+export default function initCanvasWebGLMutationObserver(
+ cb: canvasMutationCallback,
+ win: IWindow,
+ blockClass: blockClass,
+ mirror: Mirror,
+): listenerHandler {
+ const handlers: listenerHandler[] = [];
+
+ // TODO: replace me
+ requestAnimationFrame(() => flushPendingCanvasMutations(cb, mirror));
+
+ handlers.push(
+ ...patchGLPrototype(
+ win.WebGLRenderingContext.prototype,
+ CanvasContext.WebGL,
+ cb,
+ blockClass,
+ mirror,
+ ),
+ );
+
+ if (typeof win.WebGL2RenderingContext !== 'undefined') {
+ handlers.push(
+ ...patchGLPrototype(
+ win.WebGL2RenderingContext.prototype,
+ CanvasContext.WebGL2,
+ cb,
+ blockClass,
+ mirror,
+ ),
+ );
+ }
+
return () => {
handlers.forEach((h) => h());
};
diff --git a/packages/rrweb/src/record/observers/canvas/webgl2.ts b/packages/rrweb/src/record/observers/canvas/webgl2.ts
deleted file mode 100644
index 4fce44d729..0000000000
--- a/packages/rrweb/src/record/observers/canvas/webgl2.ts
+++ /dev/null
@@ -1,130 +0,0 @@
-import { INode } from 'rrweb-snapshot';
-import {
- blockClass,
- CanvasContext,
- canvasMutationCallback,
- canvasMutationParam,
- IWindow,
- listenerHandler,
- Mirror,
-} from '../../../types';
-import { hookSetter, isBlocked, patch } from '../../../utils';
-import { saveWebGLVar, serializeArgs } from './serialize-args';
-
-const pendingCanvasMutations = new Map<
- HTMLCanvasElement,
- canvasMutationParam[]
->();
-
-// FIXME: total hack here, we need to find a better way to do this
-function flushPendingCanvasMutations(
- cb: canvasMutationCallback,
- mirror: Mirror,
-) {
- pendingCanvasMutations.forEach(
- (values: canvasMutationParam[], canvas: HTMLCanvasElement) => {
- const id = mirror.getId((canvas as unknown) as INode);
- flushPendingCanvasMutationFor(canvas, id, cb);
- },
- );
- requestAnimationFrame(() => flushPendingCanvasMutations(cb, mirror));
-}
-
-function flushPendingCanvasMutationFor(
- canvas: HTMLCanvasElement,
- id: number,
- cb: canvasMutationCallback,
-) {
- const values = pendingCanvasMutations.get(canvas);
- if (!values || id === -1) return;
-
- values.forEach((p) => cb({ ...p, id }));
- pendingCanvasMutations.delete(canvas);
-}
-
-export default function initCanvasWebGLMutationObserver(
- cb: canvasMutationCallback,
- win: IWindow,
- blockClass: blockClass,
- mirror: Mirror,
-): listenerHandler {
- const handlers: listenerHandler[] = [];
- const props = Object.getOwnPropertyNames(
- win.WebGL2RenderingContext.prototype,
- );
-
- // TODO: replace me
- requestAnimationFrame(() => flushPendingCanvasMutations(cb, mirror));
-
- for (const prop of props) {
- try {
- if (
- typeof win.WebGL2RenderingContext.prototype[
- prop as keyof WebGL2RenderingContext
- ] !== 'function'
- ) {
- continue;
- }
- const restoreHandler = patch(
- win.WebGL2RenderingContext.prototype,
- prop,
- function (original) {
- return function (
- this: WebGL2RenderingContext,
- ...args: Array
- ) {
- const result = original.apply(this, args);
- saveWebGLVar(result);
- if (!isBlocked((this.canvas as unknown) as INode, blockClass)) {
- const id = mirror.getId((this.canvas as unknown) as INode);
-
- const recordArgs = serializeArgs([...args]);
- const mutation = {
- id,
- type: CanvasContext.WebGL2,
- property: prop,
- args: recordArgs,
- };
-
- if (id === -1) {
- if (!pendingCanvasMutations.has(this.canvas))
- pendingCanvasMutations.set(this.canvas, []);
-
- pendingCanvasMutations
- .get(this.canvas as HTMLCanvasElement)!
- .push(mutation);
- } else {
- // flush all pending mutations
- flushPendingCanvasMutationFor(this.canvas, id, cb);
- cb(mutation);
- }
- }
-
- return result;
- };
- },
- );
- handlers.push(restoreHandler);
- } catch {
- const hookHandler = hookSetter(
- win.WebGL2RenderingContext.prototype,
- prop,
- {
- set(v) {
- cb({
- id: mirror.getId((this.canvas as unknown) as INode),
- type: CanvasContext.WebGL2,
- property: prop,
- args: [v],
- setter: true,
- });
- },
- },
- );
- handlers.push(hookHandler);
- }
- }
- return () => {
- handlers.forEach((h) => h());
- };
-}
From 0d42d832bc96f63c605a4d3ddc752109353954e0 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Wed, 15 Dec 2021 17:13:41 +0100
Subject: [PATCH 46/93] fix parsing error
---
.../src/record/observers/canvas/webgl.ts | 31 +++++++++++++------
1 file changed, 21 insertions(+), 10 deletions(-)
diff --git a/packages/rrweb/src/record/observers/canvas/webgl.ts b/packages/rrweb/src/record/observers/canvas/webgl.ts
index eb3aaae4bf..1e5533704b 100644
--- a/packages/rrweb/src/record/observers/canvas/webgl.ts
+++ b/packages/rrweb/src/record/observers/canvas/webgl.ts
@@ -74,16 +74,27 @@ function patchGLPrototype(
};
if (id === -1) {
- if (!pendingCanvasMutations.has(this.canvas))
- pendingCanvasMutations.set(this.canvas, []);
-
- pendingCanvasMutations
- .get(this.canvas as HTMLCanvasElement)!
- .push(mutation);
- } else {
- // flush all pending mutations
- flushPendingCanvasMutationFor(this.canvas, id, cb);
- cb(mutation);
+ // FIXME! THIS COULD MAYBE BE AN OFFSCREEN CANVAS
+ if (
+ !pendingCanvasMutations.has(this.canvas as HTMLCanvasElement)
+ ) {
+ pendingCanvasMutations.set(
+ this.canvas as HTMLCanvasElement,
+ [],
+ );
+
+ pendingCanvasMutations
+ .get(this.canvas as HTMLCanvasElement)!
+ .push(mutation);
+ } else {
+ // flush all pending mutations
+ flushPendingCanvasMutationFor(
+ this.canvas as HTMLCanvasElement,
+ id,
+ cb,
+ );
+ cb(mutation);
+ }
}
}
From d26676e3c96f1635f0d53b7ff21903a6e72f320f Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 16 Dec 2021 11:24:16 +0100
Subject: [PATCH 47/93] Add types for serialize-args
---
.../rrweb/typings/record/observers/canvas/serialize-args.d.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/rrweb/typings/record/observers/canvas/serialize-args.d.ts b/packages/rrweb/typings/record/observers/canvas/serialize-args.d.ts
index b6dc836dbc..0f625d7a32 100644
--- a/packages/rrweb/typings/record/observers/canvas/serialize-args.d.ts
+++ b/packages/rrweb/typings/record/observers/canvas/serialize-args.d.ts
@@ -1,3 +1,4 @@
import { SerializedWebGlArg } from '../../../types';
export declare function serializeArg(value: any): SerializedWebGlArg;
export declare const serializeArgs: (args: Array) => SerializedWebGlArg[];
+export declare const saveWebGLVar: (value: any) => number | void;
From 2b2e11f257f7dcb82b77ce7f97891d29de0c7802 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 16 Dec 2021 11:31:22 +0100
Subject: [PATCH 48/93] Add debugging for webgl replay
---
packages/rrweb/src/replay/canvas/webgl.ts | 28 +++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts
index e28693de6d..5540de40c6 100644
--- a/packages/rrweb/src/replay/canvas/webgl.ts
+++ b/packages/rrweb/src/replay/canvas/webgl.ts
@@ -108,6 +108,34 @@ export default function webglMutation({
const args = mutation.args.map(deserializeArg);
const result = original.apply(ctx, args);
+ // const debugMode = false;
+ const debugMode = true;
+ if (debugMode) {
+ if (mutation.property === 'compileShader') {
+ if (!ctx.getShaderParameter(args[0], ctx.COMPILE_STATUS))
+ console.warn(
+ 'something went wrong in replay',
+ ctx.getShaderInfoLog(args[0]),
+ );
+ } else if (mutation.property === 'linkProgram') {
+ ctx.validateProgram(args[0]);
+ if (!ctx.getProgramParameter(args[0], ctx.LINK_STATUS))
+ console.warn(
+ 'something went wrong in replay',
+ ctx.getProgramInfoLog(args[0]),
+ );
+ }
+ const webglError = ctx.getError();
+ if (webglError !== ctx.NO_ERROR) {
+ console.warn(
+ 'WEBGL ERROR',
+ webglError,
+ 'on command:',
+ mutation.property,
+ ...args,
+ );
+ }
+ }
saveToWebGLVarMap(result);
} catch (error) {
From 8ca984eb3880f3fc4095087e9cdb57dfe297db8f Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 16 Dec 2021 15:55:44 +0100
Subject: [PATCH 49/93] Move start-server to utils
---
packages/rrweb/test/integration.test.ts | 46 +++++--------------
packages/rrweb/test/utils.ts | 59 +++++++++++++++++++++++++
2 files changed, 69 insertions(+), 36 deletions(-)
diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts
index d412cebad1..a50bda2831 100644
--- a/packages/rrweb/test/integration.test.ts
+++ b/packages/rrweb/test/integration.test.ts
@@ -1,14 +1,19 @@
import * as fs from 'fs';
import * as path from 'path';
import * as http from 'http';
-import * as url from 'url';
import * as puppeteer from 'puppeteer';
-import { assertSnapshot, launchPuppeteer } from './utils';
+import {
+ assertSnapshot,
+ startServer,
+ getServerURL,
+ launchPuppeteer,
+} from './utils';
import { recordOptions, eventWithTime, EventType } from '../src/types';
import { visitSnapshot, NodeType } from 'rrweb-snapshot';
interface ISuite {
server: http.Server;
+ serverURL: string;
code: string;
browser: puppeteer.Browser;
}
@@ -17,39 +22,6 @@ interface IMimeType {
[key: string]: string;
}
-const startServer = () =>
- new Promise((resolve) => {
- const mimeType: IMimeType = {
- '.html': 'text/html',
- '.js': 'text/javascript',
- '.css': 'text/css',
- };
- const s = http.createServer((req, res) => {
- const parsedUrl = url.parse(req.url!);
- const sanitizePath = path
- .normalize(parsedUrl.pathname!)
- .replace(/^(\.\.[\/\\])+/, '');
- let pathname = path.join(__dirname, sanitizePath);
- try {
- const data = fs.readFileSync(pathname);
- const ext = path.parse(pathname).ext;
- res.setHeader('Content-type', mimeType[ext] || 'text/plain');
- res.setHeader('Access-Control-Allow-Origin', '*');
- res.setHeader('Access-Control-Allow-Methods', 'GET');
- res.setHeader('Access-Control-Allow-Headers', 'Content-type');
- setTimeout(() => {
- res.end(data);
- // mock delay
- }, 100);
- } catch (error) {
- res.end();
- }
- });
- s.listen(3031).on('listening', () => {
- resolve(s);
- });
- });
-
describe('record integration tests', function (this: ISuite) {
jest.setTimeout(10_000);
@@ -85,11 +57,13 @@ describe('record integration tests', function (this: ISuite) {
};
let server: ISuite['server'];
+ let serverURL: string;
let code: ISuite['code'];
let browser: ISuite['browser'];
beforeAll(async () => {
server = await startServer();
+ serverURL = getServerURL(server);
browser = await launchPuppeteer();
const bundlePath = path.resolve(__dirname, '../dist/rrweb.min.js');
@@ -500,7 +474,7 @@ describe('record integration tests', function (this: ISuite) {
it('should nest record iframe', async () => {
const page: puppeteer.Page = await browser.newPage();
- await page.goto(`http://localhost:3031/html`);
+ await page.goto(`${serverURL}/html`);
await page.setContent(getHtml.call(this, 'main.html'));
await page.waitForTimeout(500);
diff --git a/packages/rrweb/test/utils.ts b/packages/rrweb/test/utils.ts
index bab49946d4..8f08920723 100644
--- a/packages/rrweb/test/utils.ts
+++ b/packages/rrweb/test/utils.ts
@@ -7,6 +7,10 @@ import {
} from '../src/types';
import * as puppeteer from 'puppeteer';
import { format } from 'prettier';
+import * as path from 'path';
+import * as http from 'http';
+import * as url from 'url';
+import * as fs from 'fs';
export async function launchPuppeteer() {
return await puppeteer.launch({
@@ -15,10 +19,65 @@ export async function launchPuppeteer() {
width: 1920,
height: 1080,
},
+ devtools: true,
args: ['--no-sandbox'],
});
}
+interface IMimeType {
+ [key: string]: string;
+}
+
+export const startServer = (defaultPort: number = 3030) =>
+ new Promise((resolve) => {
+ const mimeType: IMimeType = {
+ '.html': 'text/html',
+ '.js': 'text/javascript',
+ '.css': 'text/css',
+ };
+ const s = http.createServer((req, res) => {
+ const parsedUrl = url.parse(req.url!);
+ const sanitizePath = path
+ .normalize(parsedUrl.pathname!)
+ .replace(/^(\.\.[\/\\])+/, '');
+ let pathname = path.join(__dirname, sanitizePath);
+
+ try {
+ const data = fs.readFileSync(pathname);
+ const ext = path.parse(pathname).ext;
+ res.setHeader('Content-type', mimeType[ext] || 'text/plain');
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Methods', 'GET');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-type');
+ setTimeout(() => {
+ res.end(data);
+ // mock delay
+ }, 100);
+ } catch (error) {
+ res.end();
+ }
+ });
+ s.listen()
+ .on('listening', () => {
+ resolve(s);
+ })
+ .on('error', (e) => {
+ console.log('port in use, trying next one');
+ s.listen().on('listening', () => {
+ resolve(s);
+ });
+ });
+ });
+
+export function getServerURL(server: http.Server): string {
+ const address = server.address();
+ if (address && typeof address !== 'string') {
+ return `http://localhost:${address.port}`;
+ } else {
+ return `${address}`;
+ }
+}
+
/**
* Puppeteer may cast random mouse move which make our tests flaky.
* So we only do snapshot test with filtered events.
From 88de0f018e39f7cd608b339ffd76ed8ac059ce9c Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 16 Dec 2021 16:22:16 +0100
Subject: [PATCH 50/93] turn off debug mode by default
---
packages/rrweb/src/replay/canvas/webgl.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts
index 5540de40c6..ab094fad35 100644
--- a/packages/rrweb/src/replay/canvas/webgl.ts
+++ b/packages/rrweb/src/replay/canvas/webgl.ts
@@ -108,8 +108,10 @@ export default function webglMutation({
const args = mutation.args.map(deserializeArg);
const result = original.apply(ctx, args);
- // const debugMode = false;
- const debugMode = true;
+ saveToWebGLVarMap(result);
+
+ const debugMode = false;
+ // const debugMode = true;
if (debugMode) {
if (mutation.property === 'compileShader') {
if (!ctx.getShaderParameter(args[0], ctx.COMPILE_STATUS))
@@ -136,8 +138,6 @@ export default function webglMutation({
);
}
}
-
- saveToWebGLVarMap(result);
} catch (error) {
errorHandler(mutation, error);
}
From 317d2ab21b61efdd5b903a4a51ca5288082bcad6 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Thu, 16 Dec 2021 17:10:49 +0100
Subject: [PATCH 51/93] Move pendingCanvasMutations to local object and fix
if/else statement
---
.../src/record/observers/canvas/webgl.ts | 40 ++++++++++++-------
1 file changed, 25 insertions(+), 15 deletions(-)
diff --git a/packages/rrweb/src/record/observers/canvas/webgl.ts b/packages/rrweb/src/record/observers/canvas/webgl.ts
index 1e5533704b..03fdc505ac 100644
--- a/packages/rrweb/src/record/observers/canvas/webgl.ts
+++ b/packages/rrweb/src/record/observers/canvas/webgl.ts
@@ -11,27 +11,28 @@ import {
import { hookSetter, isBlocked, patch } from '../../../utils';
import { saveWebGLVar, serializeArgs } from './serialize-args';
-const pendingCanvasMutations = new Map<
- HTMLCanvasElement,
- canvasMutationParam[]
->();
+type pendingCanvasMutationsMap = Map;
// FIXME: total hack here, we need to find a better way to do this
function flushPendingCanvasMutations(
+ pendingCanvasMutations: pendingCanvasMutationsMap,
cb: canvasMutationCallback,
mirror: Mirror,
) {
pendingCanvasMutations.forEach(
(values: canvasMutationParam[], canvas: HTMLCanvasElement) => {
const id = mirror.getId((canvas as unknown) as INode);
- flushPendingCanvasMutationFor(canvas, id, cb);
+ flushPendingCanvasMutationFor(canvas, pendingCanvasMutations, id, cb);
},
);
- requestAnimationFrame(() => flushPendingCanvasMutations(cb, mirror));
+ requestAnimationFrame(() =>
+ flushPendingCanvasMutations(pendingCanvasMutations, cb, mirror),
+ );
}
function flushPendingCanvasMutationFor(
canvas: HTMLCanvasElement,
+ pendingCanvasMutations: pendingCanvasMutationsMap,
id: number,
cb: canvasMutationCallback,
) {
@@ -48,6 +49,7 @@ function patchGLPrototype(
cb: canvasMutationCallback,
blockClass: blockClass,
mirror: Mirror,
+ pendingCanvasMutations: pendingCanvasMutationsMap,
): listenerHandler[] {
const handlers: listenerHandler[] = [];
@@ -86,15 +88,16 @@ function patchGLPrototype(
pendingCanvasMutations
.get(this.canvas as HTMLCanvasElement)!
.push(mutation);
- } else {
- // flush all pending mutations
- flushPendingCanvasMutationFor(
- this.canvas as HTMLCanvasElement,
- id,
- cb,
- );
- cb(mutation);
}
+ } else {
+ // flush all pending mutations
+ flushPendingCanvasMutationFor(
+ this.canvas as HTMLCanvasElement,
+ pendingCanvasMutations,
+ id,
+ cb,
+ );
+ cb(mutation);
}
}
@@ -128,9 +131,13 @@ export default function initCanvasWebGLMutationObserver(
mirror: Mirror,
): listenerHandler {
const handlers: listenerHandler[] = [];
+ const pendingCanvasMutations: pendingCanvasMutationsMap = new Map();
+
// TODO: replace me
- requestAnimationFrame(() => flushPendingCanvasMutations(cb, mirror));
+ requestAnimationFrame(() =>
+ flushPendingCanvasMutations(pendingCanvasMutations, cb, mirror),
+ );
handlers.push(
...patchGLPrototype(
@@ -139,6 +146,7 @@ export default function initCanvasWebGLMutationObserver(
cb,
blockClass,
mirror,
+ pendingCanvasMutations,
),
);
@@ -150,11 +158,13 @@ export default function initCanvasWebGLMutationObserver(
cb,
blockClass,
mirror,
+ pendingCanvasMutations,
),
);
}
return () => {
+ pendingCanvasMutations.clear();
handlers.forEach((h) => h());
};
}
From 8f862e78f17f835a72dded9bb1390bf749408340 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Fri, 17 Dec 2021 12:49:21 +0100
Subject: [PATCH 52/93] Always save pending mutations
---
packages/rrweb/src/record/observers/canvas/webgl.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/rrweb/src/record/observers/canvas/webgl.ts b/packages/rrweb/src/record/observers/canvas/webgl.ts
index 03fdc505ac..7ce4101df8 100644
--- a/packages/rrweb/src/record/observers/canvas/webgl.ts
+++ b/packages/rrweb/src/record/observers/canvas/webgl.ts
@@ -84,11 +84,11 @@ function patchGLPrototype(
this.canvas as HTMLCanvasElement,
[],
);
-
- pendingCanvasMutations
- .get(this.canvas as HTMLCanvasElement)!
- .push(mutation);
}
+
+ pendingCanvasMutations
+ .get(this.canvas as HTMLCanvasElement)!
+ .push(mutation);
} else {
// flush all pending mutations
flushPendingCanvasMutationFor(
From 8c72988b9d846b9810398e66f1ed5498ea96ff81 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Fri, 17 Dec 2021 13:46:54 +0100
Subject: [PATCH 53/93] only use assert snapshot as it's clearer whats going on
---
packages/rrweb/test/record/webgl.test.ts | 17 -----------------
1 file changed, 17 deletions(-)
diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts
index 6abfa6c71c..b33b3eff85 100644
--- a/packages/rrweb/test/record/webgl.test.ts
+++ b/packages/rrweb/test/record/webgl.test.ts
@@ -164,15 +164,6 @@ describe('record webgl', function (this: ISuite) {
});
await ctx.page.waitForTimeout(50);
-
- const lastEvent = ctx.events[ctx.events.length - 1];
- expect(lastEvent).toMatchObject({
- data: {
- source: IncrementalSource.CanvasMutation,
- type: CanvasContext.WebGL,
- property: 'clear',
- },
- });
assertSnapshot(ctx.events);
});
@@ -197,14 +188,6 @@ describe('record webgl', function (this: ISuite) {
await ctx.page.waitForTimeout(50);
- const lastEvent = ctx.events[ctx.events.length - 1];
- expect(lastEvent).toMatchObject({
- data: {
- source: IncrementalSource.CanvasMutation,
- type: CanvasContext.WebGL2,
- property: 'clear',
- },
- });
assertSnapshot(ctx.events);
});
From 67648cda87af5365b5ae24d4fd60ae06e1b6b15f Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Fri, 17 Dec 2021 14:50:44 +0100
Subject: [PATCH 54/93] Ugly fix for now
---
packages/rrweb/test/record/webgl.test.ts | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts
index b33b3eff85..32199da4ca 100644
--- a/packages/rrweb/test/record/webgl.test.ts
+++ b/packages/rrweb/test/record/webgl.test.ts
@@ -186,7 +186,11 @@ describe('record webgl', function (this: ISuite) {
}, 10);
});
- await ctx.page.waitForTimeout(50);
+ // FIXME: this is a terrible way of getting this test to pass.
+ // But I want to step over this for now.
+ // When `pendingCanvasMutations` isn't run on requestAnimationFrame,
+ // this should work again without timeout 500.
+ await ctx.page.waitForTimeout(500);
assertSnapshot(ctx.events);
});
From ceb97c6f3021c98002a0eab75a4c1c698cb165b4 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Mon, 20 Dec 2021 15:01:20 +0100
Subject: [PATCH 55/93] Making the tests more DRY
---
packages/rrweb/test/record/webgl.test.ts | 85 +++++-------------------
1 file changed, 18 insertions(+), 67 deletions(-)
diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts
index 32199da4ca..ee0a04111b 100644
--- a/packages/rrweb/test/record/webgl.test.ts
+++ b/packages/rrweb/test/record/webgl.test.ts
@@ -56,6 +56,14 @@ const setup = function (this: ISuite, content: string): ISuite {
});
ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
+
+ await ctx.page.evaluate(() => {
+ const { record } = ((window as unknown) as IWindow).rrweb;
+ record({
+ recordCanvas: true,
+ emit: ((window as unknown) as IWindow).emit,
+ });
+ });
});
afterEach(async () => {
@@ -85,15 +93,6 @@ describe('record webgl', function (this: ISuite) {
);
it('will record changes to a canvas element', async () => {
- await ctx.page.evaluate(() => {
- const { record } = ((window as unknown) as IWindow).rrweb;
- record({
- recordCanvas: true,
- emit(event: eventWithTime) {
- ((window as unknown) as IWindow).emit(event);
- },
- });
- });
await ctx.page.evaluate(() => {
var canvas = document.getElementById('canvas') as HTMLCanvasElement;
var gl = canvas.getContext('webgl')!;
@@ -116,15 +115,6 @@ describe('record webgl', function (this: ISuite) {
});
it('will record changes to a webgl2 canvas element', async () => {
- await ctx.page.evaluate(() => {
- const { record } = ((window as unknown) as IWindow).rrweb;
- record({
- recordCanvas: true,
- emit(event: eventWithTime) {
- ((window as unknown) as IWindow).emit(event);
- },
- });
- });
await ctx.page.evaluate(() => {
var canvas = document.getElementById('canvas') as HTMLCanvasElement;
var gl = canvas.getContext('webgl2')!;
@@ -147,13 +137,6 @@ describe('record webgl', function (this: ISuite) {
});
it('will record changes to a canvas element before the canvas gets added', async () => {
- await ctx.page.evaluate(() => {
- const { record } = ((window as unknown) as IWindow).rrweb;
- record({
- recordCanvas: true,
- emit: ((window as unknown) as IWindow).emit,
- });
- });
await ctx.page.evaluate(() => {
var canvas = document.createElement('canvas');
var gl = canvas.getContext('webgl')!;
@@ -169,22 +152,18 @@ describe('record webgl', function (this: ISuite) {
it('will record changes to a canvas element before the canvas gets added (webgl2)', async () => {
await ctx.page.evaluate(() => {
- const { record } = ((window as unknown) as IWindow).rrweb;
- record({
- recordCanvas: true,
- emit: ((window as unknown) as IWindow).emit,
+ return new Promise((resolve) => {
+ var canvas = document.createElement('canvas');
+ var gl = canvas.getContext('webgl2')!;
+ var program = gl.createProgram()!;
+ gl.linkProgram(program);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ setTimeout(() => {
+ document.body.appendChild(canvas);
+ resolve();
+ }, 10);
});
});
- await ctx.page.evaluate(() => {
- var canvas = document.createElement('canvas');
- var gl = canvas.getContext('webgl2')!;
- var program = gl.createProgram()!;
- gl.linkProgram(program);
- gl.clear(gl.COLOR_BUFFER_BIT);
- setTimeout(() => {
- document.body.appendChild(canvas);
- }, 10);
- });
// FIXME: this is a terrible way of getting this test to pass.
// But I want to step over this for now.
@@ -196,13 +175,6 @@ describe('record webgl', function (this: ISuite) {
});
it('will record webgl variables', async () => {
- await ctx.page.evaluate(() => {
- const { record } = ((window as unknown) as IWindow).rrweb;
- record({
- recordCanvas: true,
- emit: ((window as unknown) as IWindow).emit,
- });
- });
await ctx.page.evaluate(() => {
var canvas = document.getElementById('canvas') as HTMLCanvasElement;
var gl = canvas.getContext('webgl')!;
@@ -231,13 +203,6 @@ describe('record webgl', function (this: ISuite) {
});
it('will record webgl variables in reverse order', async () => {
- await ctx.page.evaluate(() => {
- const { record } = ((window as unknown) as IWindow).rrweb;
- record({
- recordCanvas: true,
- emit: ((window as unknown) as IWindow).emit,
- });
- });
await ctx.page.evaluate(() => {
var canvas = document.getElementById('canvas') as HTMLCanvasElement;
var gl = canvas.getContext('webgl')!;
@@ -266,13 +231,6 @@ describe('record webgl', function (this: ISuite) {
});
it('sets _context on canvas.getContext()', async () => {
- await ctx.page.evaluate(() => {
- const { record } = ((window as unknown) as IWindow).rrweb;
- record({
- recordCanvas: true,
- emit: ((window as unknown) as IWindow).emit,
- });
- });
const context = await ctx.page.evaluate(() => {
var canvas = document.getElementById('canvas') as HTMLCanvasElement;
canvas.getContext('webgl')!;
@@ -283,13 +241,6 @@ describe('record webgl', function (this: ISuite) {
});
it('only sets _context on first canvas.getContext() call', async () => {
- await ctx.page.evaluate(() => {
- const { record } = ((window as unknown) as IWindow).rrweb;
- record({
- recordCanvas: true,
- emit: ((window as unknown) as IWindow).emit,
- });
- });
const context = await ctx.page.evaluate(() => {
var canvas = document.getElementById('canvas') as HTMLCanvasElement;
canvas.getContext('webgl');
From 5b37d2eabf9990fba041cc4d817db2952c478288 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Mon, 20 Dec 2021 15:42:31 +0100
Subject: [PATCH 56/93] flush at the end of each request animation frame
---
.../src/record/observers/canvas/webgl.ts | 22 ++-
packages/rrweb/src/replay/canvas/webgl.ts | 2 +
packages/rrweb/src/types.ts | 1 +
.../record/__snapshots__/webgl.test.ts.snap | 153 ++++++++++++++++++
packages/rrweb/test/record/webgl.test.ts | 24 +++
packages/rrweb/typings/types.d.ts | 1 +
6 files changed, 202 insertions(+), 1 deletion(-)
diff --git a/packages/rrweb/src/record/observers/canvas/webgl.ts b/packages/rrweb/src/record/observers/canvas/webgl.ts
index 7ce4101df8..fb85c58731 100644
--- a/packages/rrweb/src/record/observers/canvas/webgl.ts
+++ b/packages/rrweb/src/record/observers/canvas/webgl.ts
@@ -12,6 +12,7 @@ import { hookSetter, isBlocked, patch } from '../../../utils';
import { saveWebGLVar, serializeArgs } from './serialize-args';
type pendingCanvasMutationsMap = Map;
+type rafStampsType = { latestId: number; invokeId: number | null };
// FIXME: total hack here, we need to find a better way to do this
function flushPendingCanvasMutations(
@@ -50,6 +51,7 @@ function patchGLPrototype(
blockClass: blockClass,
mirror: Mirror,
pendingCanvasMutations: pendingCanvasMutationsMap,
+ rafStamps: rafStampsType,
): listenerHandler[] {
const handlers: listenerHandler[] = [];
@@ -62,18 +64,24 @@ function patchGLPrototype(
}
const restoreHandler = patch(prototype, prop, function (original) {
return function (this: typeof prototype, ...args: Array) {
+ const newFrame =
+ rafStamps.invokeId && rafStamps.latestId !== rafStamps.invokeId;
+ if (newFrame || !rafStamps.invokeId)
+ rafStamps.invokeId = rafStamps.latestId;
+
const result = original.apply(this, args);
saveWebGLVar(result);
if (!isBlocked((this.canvas as unknown) as INode, blockClass)) {
const id = mirror.getId((this.canvas as unknown) as INode);
const recordArgs = serializeArgs([...args]);
- const mutation = {
+ const mutation: canvasMutationParam = {
id,
type,
property: prop,
args: recordArgs,
};
+ if (newFrame) mutation.newFrame = true;
if (id === -1) {
// FIXME! THIS COULD MAYBE BE AN OFFSCREEN CANVAS
@@ -133,6 +141,16 @@ export default function initCanvasWebGLMutationObserver(
const handlers: listenerHandler[] = [];
const pendingCanvasMutations: pendingCanvasMutationsMap = new Map();
+ const rafStamps: rafStampsType = {
+ latestId: 0,
+ invokeId: null,
+ };
+
+ const setLatestRAFTimestamp = (timestamp: DOMHighResTimeStamp) => {
+ rafStamps.latestId = timestamp;
+ requestAnimationFrame(setLatestRAFTimestamp);
+ };
+ requestAnimationFrame(setLatestRAFTimestamp);
// TODO: replace me
requestAnimationFrame(() =>
@@ -147,6 +165,7 @@ export default function initCanvasWebGLMutationObserver(
blockClass,
mirror,
pendingCanvasMutations,
+ rafStamps,
),
);
@@ -159,6 +178,7 @@ export default function initCanvasWebGLMutationObserver(
blockClass,
mirror,
pendingCanvasMutations,
+ rafStamps,
),
);
}
diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts
index ab094fad35..3465e0811c 100644
--- a/packages/rrweb/src/replay/canvas/webgl.ts
+++ b/packages/rrweb/src/replay/canvas/webgl.ts
@@ -96,6 +96,8 @@ export default function webglMutation({
const ctx = getContext(target, mutation.type);
if (!ctx) return;
+ if (mutation.newFrame) ctx.flush(); // flush to emulate the ending of the last request animation frame
+
if (mutation.setter) {
// skip some read-only type checks
// tslint:disable-next-line:no-any
diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts
index bed9ba50b0..3ecd81c595 100644
--- a/packages/rrweb/src/types.ts
+++ b/packages/rrweb/src/types.ts
@@ -467,6 +467,7 @@ export type canvasMutationParam = {
property: string;
args: Array;
setter?: true;
+ newFrame?: true;
};
export type fontParam = {
diff --git a/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap b/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap
index 5638ec5d7c..b0ce47df14 100644
--- a/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap
+++ b/packages/rrweb/test/record/__snapshots__/webgl.test.ts.snap
@@ -1,5 +1,158 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`record webgl should mark every RAF as \`newFrame: true\` 1`] = `
+"[
+ {
+ \\"type\\": 4,
+ \\"data\\": {
+ \\"href\\": \\"about:blank\\",
+ \\"width\\": 1920,
+ \\"height\\": 1080
+ }
+ },
+ {
+ \\"type\\": 2,
+ \\"data\\": {
+ \\"node\\": {
+ \\"type\\": 0,
+ \\"childNodes\\": [
+ {
+ \\"type\\": 1,
+ \\"name\\": \\"html\\",
+ \\"publicId\\": \\"\\",
+ \\"systemId\\": \\"\\",
+ \\"id\\": 2
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"html\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"head\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [],
+ \\"id\\": 4
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"body\\",
+ \\"attributes\\": {},
+ \\"childNodes\\": [
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\",
+ \\"id\\": 6
+ },
+ {
+ \\"type\\": 2,
+ \\"tagName\\": \\"canvas\\",
+ \\"attributes\\": {
+ \\"id\\": \\"canvas\\"
+ },
+ \\"childNodes\\": [],
+ \\"id\\": 7
+ },
+ {
+ \\"type\\": 3,
+ \\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
+ \\"id\\": 8
+ }
+ ],
+ \\"id\\": 5
+ }
+ ],
+ \\"id\\": 3
+ }
+ ],
+ \\"id\\": 1
+ },
+ \\"initialOffset\\": {
+ \\"left\\": 0,
+ \\"top\\": 0
+ }
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 9,
+ \\"id\\": 7,
+ \\"type\\": 1,
+ \\"property\\": \\"createProgram\\",
+ \\"args\\": []
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 9,
+ \\"id\\": 7,
+ \\"type\\": 1,
+ \\"property\\": \\"linkProgram\\",
+ \\"args\\": [
+ {
+ \\"rr_type\\": \\"WebGLProgram\\",
+ \\"index\\": 0
+ }
+ ]
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 9,
+ \\"id\\": 7,
+ \\"type\\": 1,
+ \\"property\\": \\"createProgram\\",
+ \\"args\\": [],
+ \\"newFrame\\": true
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 9,
+ \\"id\\": 7,
+ \\"type\\": 1,
+ \\"property\\": \\"linkProgram\\",
+ \\"args\\": [
+ {
+ \\"rr_type\\": \\"WebGLProgram\\",
+ \\"index\\": 1
+ }
+ ]
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 9,
+ \\"id\\": 7,
+ \\"type\\": 1,
+ \\"property\\": \\"clear\\",
+ \\"args\\": [
+ 16384
+ ]
+ }
+ },
+ {
+ \\"type\\": 3,
+ \\"data\\": {
+ \\"source\\": 9,
+ \\"id\\": 7,
+ \\"type\\": 1,
+ \\"property\\": \\"clear\\",
+ \\"args\\": [
+ 16384
+ ],
+ \\"newFrame\\": true
+ }
+ }
+]"
+`;
+
exports[`record webgl will record changes to a canvas element 1`] = `
"[
{
diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts
index ee0a04111b..bf22566c6e 100644
--- a/packages/rrweb/test/record/webgl.test.ts
+++ b/packages/rrweb/test/record/webgl.test.ts
@@ -250,4 +250,28 @@ describe('record webgl', function (this: ISuite) {
expect(context).toBe('webgl');
});
+
+ it('should mark every RAF as `newFrame: true`', async () => {
+ await ctx.page.evaluate(() => {
+ return new Promise((resolve) => {
+ const canvas = document.getElementById('canvas') as HTMLCanvasElement;
+ const gl = canvas.getContext('webgl') as WebGLRenderingContext;
+ const program = gl.createProgram()!;
+ gl.linkProgram(program);
+ requestAnimationFrame(() => {
+ const program2 = gl.createProgram()!;
+ gl.linkProgram(program2);
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ requestAnimationFrame(() => {
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ resolve();
+ });
+ });
+ });
+ });
+
+ await ctx.page.waitForTimeout(50);
+
+ assertSnapshot(ctx.events);
+ });
});
diff --git a/packages/rrweb/typings/types.d.ts b/packages/rrweb/typings/types.d.ts
index 44eaa68e26..508af81702 100644
--- a/packages/rrweb/typings/types.d.ts
+++ b/packages/rrweb/typings/types.d.ts
@@ -344,6 +344,7 @@ export declare type canvasMutationParam = {
property: string;
args: Array;
setter?: true;
+ newFrame?: true;
};
export declare type fontParam = {
family: string;
From abf7064d1e38b7c38004a41bf2a0e6ecb148159c Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Mon, 20 Dec 2021 15:49:25 +0100
Subject: [PATCH 57/93] Looks like the promise made this test more predictable
---
packages/rrweb/test/record/webgl.test.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts
index bf22566c6e..7a5d5af1e6 100644
--- a/packages/rrweb/test/record/webgl.test.ts
+++ b/packages/rrweb/test/record/webgl.test.ts
@@ -168,8 +168,8 @@ describe('record webgl', function (this: ISuite) {
// FIXME: this is a terrible way of getting this test to pass.
// But I want to step over this for now.
// When `pendingCanvasMutations` isn't run on requestAnimationFrame,
- // this should work again without timeout 500.
- await ctx.page.waitForTimeout(500);
+ // this should work again without timeout 100.
+ await ctx.page.waitForTimeout(100);
assertSnapshot(ctx.events);
});
From ee6ec1fc9690731cf178d86046774dc93dce26a0 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Tue, 21 Dec 2021 16:20:40 +0100
Subject: [PATCH 58/93] add waitForRAF
---
packages/rrweb/test/record/webgl.test.ts | 20 +++++++++++++++-----
1 file changed, 15 insertions(+), 5 deletions(-)
diff --git a/packages/rrweb/test/record/webgl.test.ts b/packages/rrweb/test/record/webgl.test.ts
index 7a5d5af1e6..c3e5aaf876 100644
--- a/packages/rrweb/test/record/webgl.test.ts
+++ b/packages/rrweb/test/record/webgl.test.ts
@@ -77,6 +77,16 @@ const setup = function (this: ISuite, content: string): ISuite {
return ctx;
};
+async function waitForRAF(page: puppeteer.Page) {
+ return await page.evaluate(() => {
+ return new Promise((resolve) => {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(resolve);
+ });
+ });
+ });
+}
+
describe('record webgl', function (this: ISuite) {
jest.setTimeout(100_000);
@@ -146,7 +156,8 @@ describe('record webgl', function (this: ISuite) {
document.body.appendChild(canvas);
});
- await ctx.page.waitForTimeout(50);
+ await waitForRAF(ctx.page);
+
assertSnapshot(ctx.events);
});
@@ -165,11 +176,10 @@ describe('record webgl', function (this: ISuite) {
});
});
- // FIXME: this is a terrible way of getting this test to pass.
- // But I want to step over this for now.
+ // FIXME: this wait deeply couples the test to the implementation
// When `pendingCanvasMutations` isn't run on requestAnimationFrame,
- // this should work again without timeout 100.
- await ctx.page.waitForTimeout(100);
+ // we need to change this
+ await waitForRAF(ctx.page);
assertSnapshot(ctx.events);
});
From 21e55bea7aae57b79592286467f536c7dd77a782 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Tue, 21 Dec 2021 16:50:06 +0100
Subject: [PATCH 59/93] Make nested iframe recording robust no matter the test
speed
---
packages/rrweb/test/integration.test.ts | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/packages/rrweb/test/integration.test.ts b/packages/rrweb/test/integration.test.ts
index a50bda2831..b83bf09a4a 100644
--- a/packages/rrweb/test/integration.test.ts
+++ b/packages/rrweb/test/integration.test.ts
@@ -477,7 +477,14 @@ describe('record integration tests', function (this: ISuite) {
await page.goto(`${serverURL}/html`);
await page.setContent(getHtml.call(this, 'main.html'));
- await page.waitForTimeout(500);
+ await page.waitForSelector('#two');
+ const frameIdTwo = await page.frames()[2];
+ await frameIdTwo.waitForSelector('#four');
+ const frameIdFour = frameIdTwo.childFrames()[1];
+ await frameIdFour.waitForSelector('#five');
+
+ await page.waitForTimeout(50);
+
const snapshots = await page.evaluate('window.snapshots');
assertSnapshot(snapshots);
});
From 662fae59661c85d8ab815eabdeff9da3ace0a5fe Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Tue, 21 Dec 2021 16:55:54 +0100
Subject: [PATCH 60/93] mute noisy error in test
---
packages/rrweb/test/packer.test.ts | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/packages/rrweb/test/packer.test.ts b/packages/rrweb/test/packer.test.ts
index f6ffaca92f..08b35c6cca 100644
--- a/packages/rrweb/test/packer.test.ts
+++ b/packages/rrweb/test/packer.test.ts
@@ -27,7 +27,14 @@ describe('unpack', () => {
});
it('stop on unknown data format', () => {
+ const consoleSpy = jest
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+
expect(() => unpack('[""]')).toThrow('');
+
+ expect(consoleSpy).toHaveBeenCalled();
+ jest.resetAllMocks();
});
it('can unpack packed data', () => {
From 1844311391de435173881f78859e6af8f80e3494 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Tue, 21 Dec 2021 17:39:52 +0100
Subject: [PATCH 61/93] force a requestAnimationFrame
---
packages/rrweb/src/replay/index.ts | 2 ++
packages/rrweb/src/replay/machine.ts | 4 ++++
packages/rrweb/src/replay/timer.ts | 5 ++++-
packages/rrweb/src/types.ts | 1 +
packages/rrweb/typings/types.d.ts | 1 +
5 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/packages/rrweb/src/replay/index.ts b/packages/rrweb/src/replay/index.ts
index aa1d05db7b..94521b7068 100644
--- a/packages/rrweb/src/replay/index.ts
+++ b/packages/rrweb/src/replay/index.ts
@@ -880,6 +880,7 @@ export class Replayer {
p.timeOffset +
e.timestamp -
this.service.state.context.baselineTime,
+ newFrame: false,
};
this.timer.addAction(action);
});
@@ -887,6 +888,7 @@ export class Replayer {
this.timer.addAction({
doAction() {},
delay: e.delay! - d.positions[0]?.timeOffset,
+ newFrame: false,
});
}
break;
diff --git a/packages/rrweb/src/replay/machine.ts b/packages/rrweb/src/replay/machine.ts
index 2dbc512b9c..86e0dce152 100644
--- a/packages/rrweb/src/replay/machine.ts
+++ b/packages/rrweb/src/replay/machine.ts
@@ -207,6 +207,8 @@ export function createPlayerService(
emitter.emit(ReplayerEvents.EventCast, event);
},
delay: event.delay!,
+ newFrame:
+ ('newFrame' in event.data && event.data.newFrame) || false,
});
}
}
@@ -272,6 +274,8 @@ export function createPlayerService(
emitter.emit(ReplayerEvents.EventCast, event);
},
delay: event.delay!,
+ newFrame:
+ ('newFrame' in event.data && event.data.newFrame) || false,
});
}
}
diff --git a/packages/rrweb/src/replay/timer.ts b/packages/rrweb/src/replay/timer.ts
index 684c3ff06c..cec6652ece 100644
--- a/packages/rrweb/src/replay/timer.ts
+++ b/packages/rrweb/src/replay/timer.ts
@@ -44,7 +44,10 @@ export class Timer {
lastTimestamp = time;
while (actions.length) {
const action = actions[0];
- if (self.timeOffset >= action.delay) {
+ if (action.newFrame) {
+ action.newFrame = false;
+ break;
+ } else if (self.timeOffset >= action.delay) {
actions.shift();
action.doAction();
} else {
diff --git a/packages/rrweb/src/types.ts b/packages/rrweb/src/types.ts
index 3ecd81c595..a0700ba645 100644
--- a/packages/rrweb/src/types.ts
+++ b/packages/rrweb/src/types.ts
@@ -589,6 +589,7 @@ export type missingNodeMap = {
export type actionWithDelay = {
doAction: () => void;
delay: number;
+ newFrame: boolean;
};
export type Handler = (event?: unknown) => void;
diff --git a/packages/rrweb/typings/types.d.ts b/packages/rrweb/typings/types.d.ts
index 508af81702..fe7c8fe23c 100644
--- a/packages/rrweb/typings/types.d.ts
+++ b/packages/rrweb/typings/types.d.ts
@@ -440,6 +440,7 @@ export declare type missingNodeMap = {
export declare type actionWithDelay = {
doAction: () => void;
delay: number;
+ newFrame: boolean;
};
export declare type Handler = (event?: unknown) => void;
export declare type Emitter = {
From 114a00d44ceb44306426dc9ce96e600ad08e9c5c Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Wed, 22 Dec 2021 12:56:11 +0100
Subject: [PATCH 62/93] Bundle events within one frame together as much as
possible
WebGL events need to be bundled together as much as possible so they don't accidentally get split over multiple animation frames.
`newFrame: true` is used to indicate the start of an new animation frame in the recording, and that the event shouldn't be bundled with the previous events.
---
packages/rrweb/src/replay/machine.ts | 6 ++++--
packages/rrweb/src/replay/timer.ts | 26 +++++++++++++++++++++++++-
2 files changed, 29 insertions(+), 3 deletions(-)
diff --git a/packages/rrweb/src/replay/machine.ts b/packages/rrweb/src/replay/machine.ts
index 86e0dce152..49dd96ef41 100644
--- a/packages/rrweb/src/replay/machine.ts
+++ b/packages/rrweb/src/replay/machine.ts
@@ -8,7 +8,7 @@ import {
Emitter,
IncrementalSource,
} from '../types';
-import { Timer, addDelay } from './timer';
+import { Timer, addDelay, LastDelay } from './timer';
export type PlayerContext = {
events: eventWithTime[];
@@ -167,9 +167,11 @@ export function createPlayerService(
play(ctx) {
const { timer, events, baselineTime, lastPlayedEvent } = ctx;
timer.clear();
+
+ const lastDelay: LastDelay = { at: null };
for (const event of events) {
// TODO: improve this API
- addDelay(event, baselineTime);
+ addDelay(event, baselineTime, lastDelay);
}
const neededEvents = discardPriorSnapshots(events, baselineTime);
diff --git a/packages/rrweb/src/replay/timer.ts b/packages/rrweb/src/replay/timer.ts
index cec6652ece..3638351263 100644
--- a/packages/rrweb/src/replay/timer.ts
+++ b/packages/rrweb/src/replay/timer.ts
@@ -100,8 +100,14 @@ export class Timer {
}
}
+export type LastDelay = { at: number | null };
+
// TODO: add speed to mouse move timestamp calculation
-export function addDelay(event: eventWithTime, baselineTime: number): number {
+export function addDelay(
+ event: eventWithTime,
+ baselineTime: number,
+ lastDelay?: LastDelay,
+): number {
// Mouse move events was recorded in a throttle function,
// so we need to find the real timestamp by traverse the time offsets.
if (
@@ -114,6 +120,24 @@ export function addDelay(event: eventWithTime, baselineTime: number): number {
event.delay = firstTimestamp - baselineTime;
return firstTimestamp - baselineTime;
}
+
+ if (lastDelay) {
+ // WebGL events need to be bundled together as much as possible so they don't
+ // accidentally get split over multiple animation frames.
+ if (
+ event.type === EventType.IncrementalSnapshot &&
+ event.data.source === IncrementalSource.CanvasMutation &&
+ // `newFrame: true` is used to indicate the start of an new animation frame in the recording,
+ // and that the event shouldn't be bundled with the previous events.
+ !event.data.newFrame &&
+ lastDelay.at
+ ) {
+ event.delay = lastDelay.at;
+ } else {
+ lastDelay.at = event.delay!;
+ }
+ }
+
event.delay = event.timestamp - baselineTime;
return event.delay;
}
From 8058c3ae4b08de2d75019cf09ffce03a7ae2fb41 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Wed, 22 Dec 2021 12:58:15 +0100
Subject: [PATCH 63/93] Rename RafStamps
---
packages/rrweb/src/record/observers/canvas/webgl.ts | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/packages/rrweb/src/record/observers/canvas/webgl.ts b/packages/rrweb/src/record/observers/canvas/webgl.ts
index fb85c58731..d93630676a 100644
--- a/packages/rrweb/src/record/observers/canvas/webgl.ts
+++ b/packages/rrweb/src/record/observers/canvas/webgl.ts
@@ -12,7 +12,7 @@ import { hookSetter, isBlocked, patch } from '../../../utils';
import { saveWebGLVar, serializeArgs } from './serialize-args';
type pendingCanvasMutationsMap = Map;
-type rafStampsType = { latestId: number; invokeId: number | null };
+type RafStamps = { latestId: number; invokeId: number | null };
// FIXME: total hack here, we need to find a better way to do this
function flushPendingCanvasMutations(
@@ -51,7 +51,7 @@ function patchGLPrototype(
blockClass: blockClass,
mirror: Mirror,
pendingCanvasMutations: pendingCanvasMutationsMap,
- rafStamps: rafStampsType,
+ rafStamps: RafStamps,
): listenerHandler[] {
const handlers: listenerHandler[] = [];
@@ -141,7 +141,7 @@ export default function initCanvasWebGLMutationObserver(
const handlers: listenerHandler[] = [];
const pendingCanvasMutations: pendingCanvasMutationsMap = new Map();
- const rafStamps: rafStampsType = {
+ const rafStamps: RafStamps = {
latestId: 0,
invokeId: null,
};
From fbaf1880cd8096f05a323d81c7293ce638dd4faf Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Wed, 22 Dec 2021 14:44:08 +0100
Subject: [PATCH 64/93] Override event.delay
---
packages/rrweb/src/replay/timer.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/rrweb/src/replay/timer.ts b/packages/rrweb/src/replay/timer.ts
index 3638351263..9d79a41be0 100644
--- a/packages/rrweb/src/replay/timer.ts
+++ b/packages/rrweb/src/replay/timer.ts
@@ -133,6 +133,7 @@ export function addDelay(
lastDelay.at
) {
event.delay = lastDelay.at;
+ return event.delay;
} else {
lastDelay.at = event.delay!;
}
From 791715824012e367c87ec9b7c7cce24df6fa371a Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Wed, 22 Dec 2021 15:23:17 +0100
Subject: [PATCH 65/93] cleanup
---
packages/rrweb/src/replay/canvas/webgl.ts | 7 ++++++-
packages/rrweb/src/replay/timer.ts | 4 +++-
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/packages/rrweb/src/replay/canvas/webgl.ts b/packages/rrweb/src/replay/canvas/webgl.ts
index 3465e0811c..68937a9008 100644
--- a/packages/rrweb/src/replay/canvas/webgl.ts
+++ b/packages/rrweb/src/replay/canvas/webgl.ts
@@ -19,6 +19,9 @@ function getContext(
target: HTMLCanvasElement,
type: CanvasContext,
): WebGLRenderingContext | WebGL2RenderingContext | null {
+ // Note to whomever is going to implement support for `contextAttributes`:
+ // if `preserveDrawingBuffer` is set to true,
+ // you have to do `ctx.flush()` before every `newFrame: true`
try {
if (type === CanvasContext.WebGL) {
return (
@@ -96,7 +99,9 @@ export default function webglMutation({
const ctx = getContext(target, mutation.type);
if (!ctx) return;
- if (mutation.newFrame) ctx.flush(); // flush to emulate the ending of the last request animation frame
+ // NOTE: if `preserveDrawingBuffer` is set to true,
+ // we must flush the buffers on every newFrame: true
+ // if (mutation.newFrame) ctx.flush();
if (mutation.setter) {
// skip some read-only type checks
diff --git a/packages/rrweb/src/replay/timer.ts b/packages/rrweb/src/replay/timer.ts
index 9d79a41be0..885aa92a28 100644
--- a/packages/rrweb/src/replay/timer.ts
+++ b/packages/rrweb/src/replay/timer.ts
@@ -47,7 +47,9 @@ export class Timer {
if (action.newFrame) {
action.newFrame = false;
break;
- } else if (self.timeOffset >= action.delay) {
+ }
+
+ if (self.timeOffset >= action.delay) {
actions.shift();
action.doAction();
} else {
From 3d1ff8125c26ff9b57651f6cbb53024f5b6b5492 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Wed, 22 Dec 2021 16:40:41 +0100
Subject: [PATCH 66/93] Add tests for addDelay
---
packages/rrweb/src/replay/timer.ts | 5 +-
packages/rrweb/test/replay/timer.test.ts | 61 ++++++++++++++++++++++++
2 files changed, 64 insertions(+), 2 deletions(-)
create mode 100644 packages/rrweb/test/replay/timer.test.ts
diff --git a/packages/rrweb/src/replay/timer.ts b/packages/rrweb/src/replay/timer.ts
index 885aa92a28..1fdbdb0208 100644
--- a/packages/rrweb/src/replay/timer.ts
+++ b/packages/rrweb/src/replay/timer.ts
@@ -123,6 +123,8 @@ export function addDelay(
return firstTimestamp - baselineTime;
}
+ event.delay = event.timestamp - baselineTime;
+
if (lastDelay) {
// WebGL events need to be bundled together as much as possible so they don't
// accidentally get split over multiple animation frames.
@@ -134,13 +136,12 @@ export function addDelay(
!event.data.newFrame &&
lastDelay.at
) {
+ // Override the current delay with the last delay
event.delay = lastDelay.at;
- return event.delay;
} else {
lastDelay.at = event.delay!;
}
}
- event.delay = event.timestamp - baselineTime;
return event.delay;
}
diff --git a/packages/rrweb/test/replay/timer.test.ts b/packages/rrweb/test/replay/timer.test.ts
new file mode 100644
index 0000000000..304f0e7b31
--- /dev/null
+++ b/packages/rrweb/test/replay/timer.test.ts
@@ -0,0 +1,61 @@
+import { addDelay, LastDelay } from '../../src/replay/timer';
+import {
+ CanvasContext,
+ EventType,
+ eventWithTime,
+ IncrementalSource,
+} from '../../src/types';
+
+const canvasMutationEventWithTime = (
+ timestamp: number,
+ newFrame?: boolean,
+): eventWithTime => {
+ const newFrameObj: { newFrame: true } | {} = newFrame
+ ? { newFrame: true }
+ : {};
+
+ return {
+ timestamp,
+ type: EventType.IncrementalSnapshot,
+ data: {
+ source: IncrementalSource.CanvasMutation,
+ property: 'x',
+ args: [],
+ id: 1,
+ type: CanvasContext.WebGL,
+ ...newFrameObj,
+ },
+ };
+};
+
+describe('addDelay', () => {
+ let baselineTime: number;
+ let lastDelay: LastDelay;
+ beforeEach(() => {
+ baselineTime = 0;
+ lastDelay = {
+ at: null,
+ };
+ });
+ it('should bundle canvas mutations together', () => {
+ const event1 = canvasMutationEventWithTime(1000);
+ const event2 = canvasMutationEventWithTime(1001);
+ addDelay(event1, baselineTime, lastDelay);
+ addDelay(event2, baselineTime, lastDelay);
+ expect(event2.delay).toBe(1000);
+ });
+
+ it('should bundle canvas mutations on the same frame together', () => {
+ const event1 = canvasMutationEventWithTime(1000);
+ const event2 = canvasMutationEventWithTime(1001);
+ const event3 = canvasMutationEventWithTime(1002, true);
+ const event4 = canvasMutationEventWithTime(1003);
+ addDelay(event1, baselineTime, lastDelay);
+ addDelay(event2, baselineTime, lastDelay);
+ addDelay(event3, baselineTime, lastDelay);
+ addDelay(event4, baselineTime, lastDelay);
+ expect(event2.delay).toBe(1000);
+ expect(event3.delay).toBe(1002);
+ expect(event4.delay).toBe(1002);
+ });
+});
From 0a2690e4055d9eebe2ac9fd92f9b784ff8c99eb5 Mon Sep 17 00:00:00 2001
From: Justin Halsall
Date: Wed, 22 Dec 2021 17:10:02 +0100
Subject: [PATCH 67/93] Add webgl e2e test
---
...ecord-and-replay-a-webgl-square-1-snap.png | Bin 0 -> 10812 bytes
packages/rrweb/test/e2e/webgl.test.ts | 133 ++++++++++++++++++
.../rrweb/test/html/canvas-webgl-square.html | 115 +++++++++++++++
3 files changed, 248 insertions(+)
create mode 100644 packages/rrweb/test/e2e/__image_snapshots__/webgl-test-ts-e-2-e-webgl-will-record-and-replay-a-webgl-square-1-snap.png
create mode 100644 packages/rrweb/test/e2e/webgl.test.ts
create mode 100644 packages/rrweb/test/html/canvas-webgl-square.html
diff --git a/packages/rrweb/test/e2e/__image_snapshots__/webgl-test-ts-e-2-e-webgl-will-record-and-replay-a-webgl-square-1-snap.png b/packages/rrweb/test/e2e/__image_snapshots__/webgl-test-ts-e-2-e-webgl-will-record-and-replay-a-webgl-square-1-snap.png
new file mode 100644
index 0000000000000000000000000000000000000000..e22bc48831104120ac0c4a19cd3b9f41dc62b426
GIT binary patch
literal 10812
zcmeAS@N?(olHy`uVBq!ia0y~yU~geyV6ov~1Bz6wRMrPljKx9jP7LeL$-HD>P+;(M
zaSW-L^XAUR+`|S8u8vwCOxA3k^!vd>vQoAdb7--lIu=j~*Nh&If%k?S`+Ch<=I$!Ol9Sr{Z><@wFZ*LC!v-SLD_78`D
z-()Te+9?l_PN^`GBF1PUT`;N)9GE1>+-SIsh8zC8jX#Cx?YFA6&S3q2`}j>}NOoTE
z@~Og}YgR?jVur!`bBYN>#$k#A4}>M4$^ljF!g&ZH#1PnIFsh7!VKhL%sbDmPfP-N)
z%RtJ6(ZT^74x@#`XyGtgQ9_D@(Y!F47e@2KXkK6#Z6iSv!Dt%^91f%P!bq(bK3ttA
zJB5LVg^?%5>*|{N`1FNpT7C00}Tl=sjP@{zdC_#)C4&ZDtTJ3^^VYIam
z4hBkw0$$CXdGoI2^E-Laa^`{i`FH2u-FpWynBhUXylc0-o#vIc*B}v3S3j3^P6 {
+ let code: ISuite['code'];
+ let page: ISuite['page'];
+ let browser: ISuite['browser'];
+ let server: ISuite['server'];
+ let serverURL: ISuite['serverURL'];
+
+ beforeAll(async () => {
+ server = await startServer();
+ serverURL = getServerURL(server);
+ browser = await launchPuppeteer();
+
+ const bundlePath = path.resolve(__dirname, '../../dist/rrweb.min.js');
+ code = fs.readFileSync(bundlePath, 'utf8');
+
+ });
+
+ afterEach(async () => {
+ await page.close();
+ });
+
+ afterAll(async () => {
+ await browser.close();
+ });
+
+ const getHtml = (
+ fileName: string,
+ options: recordOptions = {},
+ ): string => {
+ const filePath = path.resolve(__dirname, `../html/${fileName}`);
+ const html = fs.readFileSync(filePath, 'utf8');
+ return html.replace(
+ '
+
+
+
+
+ ',
+ `
+
+
+ `,
+ );
+ };
+
+ const fakeGoto = async (page: puppeteer.Page, url: string) => {
+ const intercept = async (request: puppeteer.HTTPRequest) => {
+ await request.respond({
+ status: 200,
+ contentType: 'text/html',
+ body: ' ', // non-empty string or page will load indefinitely
+ });
+ };
+ page.setRequestInterception(true);
+ page.on('request', intercept);
+ await page.goto(url);
+ page.setRequestInterception(false);
+ page.off('request', intercept);
+ };
+
+ const hideMouseAnimation = async (page: puppeteer.Page) => {
+ await page.addStyleTag({
+ content: '.replayer-mouse-tail{display: none !important;}',
+ });
+ };
+
+ it('will record and replay a webgl square', async () => {
+ page = await browser.newPage();
+ await fakeGoto(page, `${serverURL}/html/canvas-webgl-square.html`);
+
+ await page.setContent(
+ getHtml.call(this, 'canvas-webgl-square.html', { recordCanvas: true }),
+ );
+
+ await page.waitForTimeout(100);
+ const snapshots: eventWithTime[] = await page.evaluate('window.snapshots');
+
+ page = await browser.newPage();
+
+ await page.goto('about:blank');
+ await page.evaluate(code);
+
+ await hideMouseAnimation(page);
+ await page.evaluate(`let events = ${JSON.stringify(snapshots)}`);
+ await page.evaluate(`
+ const { Replayer } = rrweb;
+ const replayer = new Replayer(events, {
+ UNSAFE_replayCanvas: true,
+ });
+ replayer.play(500);
+ `);
+ await page.waitForTimeout(50);
+
+ const element = await page.$('iframe');
+ const frameImage = await element!.screenshot();
+
+ expect(frameImage).toMatchImageSnapshot();
+ });
+});
diff --git a/packages/rrweb/test/html/canvas-webgl-square.html b/packages/rrweb/test/html/canvas-webgl-square.html
new file mode 100644
index 0000000000..272ecd4d36
--- /dev/null
+++ b/packages/rrweb/test/html/canvas-webgl-square.html
@@ -0,0 +1,115 @@
+
+
+