Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

ref(rrweb): allow multi touch gestures to be shown for mobile replays #190

Merged
merged 28 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5f4dfd3
ref(rrweb): allow multi touch gestures to be shown for mobile replays
michellewzhang May 16, 2024
94e7595
some type fixes
michellewzhang May 16, 2024
d87bbb9
rm files
michellewzhang May 16, 2024
8e44b94
more fixes
michellewzhang May 16, 2024
1d0cce1
touchId -> pointerId
michellewzhang May 16, 2024
673d62a
name changes
michellewzhang May 16, 2024
c92bc1b
types
michellewzhang May 16, 2024
52e4399
some fixes
michellewzhang May 28, 2024
07d1d67
forgot a spot
michellewzhang May 28, 2024
b2b12c8
rm export
michellewzhang May 28, 2024
c7039dd
rm changes to package.json and yarn.lock
michellewzhang May 28, 2024
1b453b7
ts
michellewzhang May 28, 2024
ad7d1ce
fix delete
michellewzhang May 28, 2024
583d808
turn "run for branch" on for size limit GHA
billyvg May 29, 2024
a41c292
Update packages/rrweb/src/replay/index.ts
billyvg May 29, 2024
74a19cd
add size limit entry for Replayer
billyvg May 29, 2024
fd3e3ed
optimizations
michellewzhang May 29, 2024
fbb92f6
Merge remote-tracking branch 'origin/sentry-v2' into mz/rrweb-ref-mul…
michellewzhang May 29, 2024
c424b56
rm -1
michellewzhang May 29, 2024
3830e04
update snapshots for failing tests
michellewzhang May 29, 2024
3044563
add tests
michellewzhang May 29, 2024
a2aac2a
:white-check-mark: add test
michellewzhang May 29, 2024
830d8f3
move func out of the class
michellewzhang May 30, 2024
d9d3075
TESTS.
michellewzhang May 30, 2024
56ed9a0
tests that pass
michellewzhang May 30, 2024
0a7aeee
rm code for creating pointers upfront
michellewzhang May 31, 2024
5e465eb
add touch-device class inside createPointer
michellewzhang May 31, 2024
8fee9ca
rm play() from tests
michellewzhang May 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 119 additions & 55 deletions packages/rrweb/src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
styleSheetRuleData,
styleDeclarationData,
adoptedStyleSheetData,
mouseInteractionData,
mousemoveData,
} from '@sentry-internal/rrweb-types';
import {
polyfill,
Expand Down Expand Up @@ -98,7 +100,14 @@
strokeStyle: 'red',
} as const;

function indicatesTouchDevice(e: eventWithTime) {
type incrementalSnapshotEventWithTime = incrementalSnapshotEvent & {
timestamp: number;
delay?: number;
};

function indicatesTouchDevice(
e: eventWithTime,
): e is incrementalSnapshotEventWithTime {
return (
e.type == EventType.IncrementalSnapshot &&
(e.data.source == IncrementalSource.TouchMove ||
Expand All @@ -107,6 +116,14 @@
);
}

function getPointerId(
d: incrementalData | mousemoveData | mouseInteractionData,
): number {
const pointerId =
'pointerId' in d && typeof d.pointerId === 'number' ? d.pointerId : -1;
return pointerId;
}

export class Replayer {
public wrapper: HTMLDivElement;
public iframe: HTMLIFrameElement;
Expand All @@ -123,9 +140,7 @@
public usingVirtualDom = false;
public virtualDom: RRDocument = new RRDocument();

private mouse: HTMLDivElement;
private mouseTail: HTMLCanvasElement | null = null;
private tailPositions: Array<{ x: number; y: number }> = [];

private emitter: Emitter = mitt();

Expand All @@ -148,8 +163,16 @@

private newDocumentQueue: addedNodeMutation[] = [];

private mousePos: mouseMovePos | null = null;
private touchActive: boolean | null = null;
// Map of pointer ID to the unique vars used to show pointers and their movements
private pointers: Record<
number,
{
touchActive: boolean | null;
pointerEl: HTMLDivElement;
tailPositions: Array<{ x: number; y: number }>;
pointerPosition: mouseMovePos | null;
}
> = {};
private lastMouseDownEvent: [Node, Event] | null = null;

// Keep the rootNode of the last hovered element. So when hovering a new element, we can remove the last hovered element's :hover style.
Expand Down Expand Up @@ -293,23 +316,32 @@
this.adoptedStyleSheets = [];
}

if (this.mousePos) {
this.moveAndHover(
this.mousePos.x,
this.mousePos.y,
this.mousePos.id,
true,
this.mousePos.debugData,
);
this.mousePos = null;
}
for (const [
pointerId,
{ pointerPosition, touchActive },
] of Object.entries(this.pointers)) {
const id = parseInt(pointerId);
const pointer = this.pointers[id];

if (pointerPosition) {
this.moveAndHover(
pointerPosition.x,
pointerPosition.y,
pointerPosition.id,
true,
pointerPosition.debugData,
id,
);
pointer.pointerPosition = null;
}

if (this.touchActive === true) {
this.mouse.classList.add('touch-active');
} else if (this.touchActive === false) {
this.mouse.classList.remove('touch-active');
if (touchActive === true) {
pointer.pointerEl.classList.add('touch-active');
} else if (touchActive === false) {
pointer.pointerEl.classList.remove('touch-active');
}
pointer.touchActive = null;
}
this.touchActive = null;

if (this.lastMouseDownEvent) {
const [target, event] = this.lastMouseDownEvent;
Expand Down Expand Up @@ -402,9 +434,21 @@
);
}, 1);
}
if (this.service.state.context.events.find(indicatesTouchDevice)) {
this.mouse.classList.add('touch-device');
}

private createPointer(pointerId: number, event: eventWithTime) {
const newMouse = document.createElement('div');
newMouse.classList.add('replayer-mouse');
this.pointers[pointerId] = {
touchActive: null,
pointerEl: newMouse,
tailPositions: [],
pointerPosition: null,
};
if (indicatesTouchDevice(event)) {
newMouse.classList.add('touch-device');
}
this.wrapper.appendChild(newMouse);
}

public on(event: string, handler: Handler) {
Expand Down Expand Up @@ -541,9 +585,7 @@
const event = this.config.unpackFn
? this.config.unpackFn(rawEvent as string)
: (rawEvent as eventWithTime);
if (indicatesTouchDevice(event)) {
this.mouse.classList.add('touch-device');
}

void Promise.resolve().then(() =>
this.service.send({ type: 'ADD_EVENT', payload: { event } }),
);
Expand Down Expand Up @@ -572,10 +614,6 @@
this.wrapper.classList.add('replayer-wrapper');
this.config.root.appendChild(this.wrapper);

this.mouse = document.createElement('div');
this.mouse.classList.add('replayer-mouse');
this.wrapper.appendChild(this.mouse);

if (this.config.mouseTail !== false) {
this.mouseTail = document.createElement('canvas');
this.mouseTail.classList.add('replayer-mouse-tail');
Expand Down Expand Up @@ -887,7 +925,7 @@
sn?.type === NodeType.Element &&
sn?.tagName.toUpperCase() === 'HTML'
) {
const { documentElement, head } = iframeEl.contentDocument!;

Check warning on line 928 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L928

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
this.insertStyleRules(
documentElement as HTMLElement | RRElement,
head as HTMLElement | RRElement,
Expand All @@ -906,14 +944,14 @@
};

buildNodeWithSN(mutation.node, {
doc: iframeEl.contentDocument! as Document,

Check warning on line 947 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L947

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
mirror: mirror as Mirror,
hackCss: true,
skipChild: false,
afterAppend,
cache: this.cache,
});
afterAppend(iframeEl.contentDocument! as Document, mutation.node.id);

Check warning on line 954 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L954

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.

for (const { mutationInQueue, builtNode } of collected) {
this.attachDocumentToIframe(mutationInQueue, builtNode);
Expand Down Expand Up @@ -1000,7 +1038,7 @@
* pause when there are some canvas drawImage args need to be loaded
*/
private async preloadAllImages(): Promise<void[]> {
let beforeLoadState = this.service.state;

Check warning on line 1041 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1041

[@typescript-eslint/no-unused-vars] 'beforeLoadState' is assigned a value but never used.
const stateHandler = () => {
beforeLoadState = this.service.state;
};
Expand Down Expand Up @@ -1035,8 +1073,8 @@
const ctx = canvas.getContext('2d');
const imgd = ctx?.createImageData(canvas.width, canvas.height);
let d = imgd?.data;
d = JSON.parse(data.args[0]) as Uint8ClampedArray;

Check warning on line 1076 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1076

[@typescript-eslint/no-unused-vars] 'd' is assigned a value but never used.
ctx?.putImageData(imgd!, 0, 0);

Check warning on line 1077 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1077

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
}
}
private async deserializeAndPreloadCanvasEvents(
Expand Down Expand Up @@ -1085,10 +1123,17 @@
}
case IncrementalSource.Drag:
case IncrementalSource.TouchMove:
case IncrementalSource.MouseMove:
case IncrementalSource.MouseMove: {
const pointerId = getPointerId(d);
if (!this.pointers[pointerId]) {
this.createPointer(pointerId, e);
}

const pointer = this.pointers[pointerId];

if (isSync) {
const lastPosition = d.positions[d.positions.length - 1];
this.mousePos = {
pointer.pointerPosition = {
x: lastPosition.x,
y: lastPosition.y,
id: lastPosition.id,
Expand All @@ -1098,7 +1143,7 @@
d.positions.forEach((p) => {
const action = {
doAction: () => {
this.moveAndHover(p.x, p.y, p.id, isSync, d);
this.moveAndHover(p.x, p.y, p.id, isSync, d, pointerId);
},
delay:
p.timeOffset +
Expand All @@ -1117,7 +1162,15 @@
});
}
break;
}
case IncrementalSource.MouseInteraction: {
const pointerId = getPointerId(d);
if (!this.pointers[pointerId]) {
this.createPointer(pointerId, e);
}

const pointer = this.pointers[pointerId];

/**
* Same as the situation of missing input target.
*/
Expand Down Expand Up @@ -1154,16 +1207,18 @@
case MouseInteractions.MouseUp:
if (isSync) {
if (d.type === MouseInteractions.TouchStart) {
this.touchActive = true;
pointer.touchActive = true;
} else if (d.type === MouseInteractions.TouchEnd) {
this.touchActive = false;
pointer.touchActive = false;
pointer.pointerEl.remove();
delete this.pointers[pointerId];
}
if (d.type === MouseInteractions.MouseDown) {
this.lastMouseDownEvent = [target, event];
} else if (d.type === MouseInteractions.MouseUp) {
this.lastMouseDownEvent = null;
}
this.mousePos = {
pointer.pointerPosition = {
x: d.x || 0,
y: d.y || 0,
id: d.id,
Expand All @@ -1172,9 +1227,9 @@
} else {
if (d.type === MouseInteractions.TouchStart) {
// don't draw a trail as user has lifted finger and is placing at a new point
this.tailPositions.length = 0;
pointer.tailPositions.length = 0;
}
this.moveAndHover(d.x || 0, d.y || 0, d.id, isSync, d);
this.moveAndHover(d.x || 0, d.y || 0, d.id, isSync, d, pointerId);
if (d.type === MouseInteractions.Click) {
/*
* don't want target.click() here as could trigger an iframe navigation
Expand All @@ -1184,14 +1239,15 @@
* removal and addition of .active class (along with void line to trigger repaint)
* triggers the 'click' css animation in styles/style.css
*/
this.mouse.classList.remove('active');
void this.mouse.offsetWidth;
this.mouse.classList.add('active');
pointer.pointerEl.classList.remove('active');
void pointer.pointerEl.offsetWidth;
pointer.pointerEl.classList.add('active');
} else if (d.type === MouseInteractions.TouchStart) {
void this.mouse.offsetWidth; // needed for the position update of moveAndHover to apply without the .touch-active transition
this.mouse.classList.add('touch-active');
void pointer.pointerEl.offsetWidth; // needed for the position update of moveAndHover to apply without the .touch-active transition
pointer.pointerEl.classList.add('touch-active');
} else if (d.type === MouseInteractions.TouchEnd) {
this.mouse.classList.remove('touch-active');
pointer.pointerEl.remove();
delete this.pointers[pointerId];
} else {
// for MouseDown & MouseUp also invoke default behavior
target.dispatchEvent(event);
Expand All @@ -1200,9 +1256,9 @@
break;
case MouseInteractions.TouchCancel:
if (isSync) {
this.touchActive = false;
pointer.touchActive = false;
} else {
this.mouse.classList.remove('touch-active');
pointer.pointerEl.classList.remove('touch-active');
}
break;
default:
Expand Down Expand Up @@ -1373,7 +1429,7 @@
// Only apply virtual dom optimization if the fast-forward process has node mutation. Because the cost of creating a virtual dom tree and executing the diff algorithm is usually higher than directly applying other kind of events.
if (this.config.useVirtualDom && !this.usingVirtualDom && isSync) {
this.usingVirtualDom = true;
buildFromDom(this.iframe.contentDocument!, this.mirror, this.virtualDom);

Check warning on line 1432 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1432

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
// If these legacy missing nodes haven't been resolved, they should be converted to virtual nodes.
if (Object.keys(this.legacy_missingNodeRetryMap).length) {
for (const key in this.legacy_missingNodeRetryMap) {
Expand Down Expand Up @@ -1490,7 +1546,7 @@
// If the parent is attached a shadow dom after it's created, it won't have a shadow root.
if (!hasShadowRoot(parent)) {
(parent as Element | RRElement).attachShadow({ mode: 'open' });
parent = (parent as Element | RRElement).shadowRoot! as Node | RRNode;

Check warning on line 1549 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1549

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
} else parent = parent.shadowRoot as Node | RRNode;
}

Expand Down Expand Up @@ -2080,6 +2136,7 @@
id: number,
isSync: boolean,
debugData: incrementalData,
pointerId: number,
) {
const target = this.mirror.getNode(id);
if (!target) {
Expand All @@ -2090,15 +2147,16 @@
const _x = x * base.absoluteScale + base.x;
const _y = y * base.absoluteScale + base.y;

this.mouse.style.left = `${_x}px`;
this.mouse.style.top = `${_y}px`;
const pointer = this.pointers[pointerId];
pointer.pointerEl.style.left = `${_x}px`;
pointer.pointerEl.style.top = `${_y}px`;
if (!isSync) {
this.drawMouseTail({ x: _x, y: _y });
this.drawMouseTail({ x: _x, y: _y }, pointerId);
}
this.hoverElements(target as Element);
}

private drawMouseTail(position: { x: number; y: number }) {
private drawMouseTail(position: { x: number; y: number }, pointerId: number) {
if (!this.mouseTail) {
return;
}
Expand All @@ -2108,29 +2166,35 @@
? defaultMouseTailConfig
: Object.assign({}, defaultMouseTailConfig, this.config.mouseTail);

const pointer = this.pointers[pointerId];

const draw = () => {
if (!this.mouseTail) {
return;
}
const ctx = this.mouseTail.getContext('2d');
if (!ctx || !this.tailPositions.length) {
if (!ctx || !pointer.tailPositions.length) {
return;
}
ctx.clearRect(0, 0, this.mouseTail.width, this.mouseTail.height);
ctx.beginPath();
ctx.lineWidth = lineWidth;
ctx.lineCap = lineCap;
ctx.strokeStyle = strokeStyle;
ctx.moveTo(this.tailPositions[0].x, this.tailPositions[0].y);
this.tailPositions.forEach((p) => ctx.lineTo(p.x, p.y));
ctx.moveTo(pointer.tailPositions[0].x, pointer.tailPositions[0].y);
pointer.tailPositions.forEach((p) => ctx.lineTo(p.x, p.y));
ctx.stroke();
};

this.tailPositions.push(position);
pointer.tailPositions.push(position);
draw();
setTimeout(() => {
this.tailPositions = this.tailPositions.filter((p) => p !== position);
draw();
if (pointerId in this.pointers) {
michellewzhang marked this conversation as resolved.
Show resolved Hide resolved
pointer.tailPositions = pointer.tailPositions.filter(
(p) => p !== position,
);
draw();
}
}, duration / this.speedService.state.context.timer.speed);
}

Expand Down
5 changes: 0 additions & 5 deletions packages/rrweb/test/__snapshots__/replayer.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ exports[`replayer can fast forward past StyleSheetRule changes on virtual elemen
</head>
<body>
<div class=\\"replayer-wrapper\\">
<div class=\\"replayer-mouse\\"></div>
<canvas
class=\\"replayer-mouse-tail\\"
width=\\"1000\\"
Expand Down Expand Up @@ -88,7 +87,6 @@ exports[`replayer can fast forward past StyleSheetRule deletion on virtual eleme
</head>
<body>
<div class=\\"replayer-wrapper\\">
<div class=\\"replayer-mouse\\"></div>
<canvas
class=\\"replayer-mouse-tail\\"
width=\\"1000\\"
Expand Down Expand Up @@ -166,7 +164,6 @@ exports[`replayer can handle removing style elements 1`] = `
</head>
<body>
<div class=\\"replayer-wrapper\\">
<div class=\\"replayer-mouse\\"></div>
<canvas
class=\\"replayer-mouse-tail\\"
width=\\"1000\\"
Expand Down Expand Up @@ -214,7 +211,6 @@ exports[`replayer replays same timestamp events in correct order (with addAction
</head>
<body>
<div class=\\"replayer-wrapper\\">
<div class=\\"replayer-mouse\\"></div>
<canvas
class=\\"replayer-mouse-tail\\"
width=\\"1000\\"
Expand Down Expand Up @@ -265,7 +261,6 @@ exports[`replayer replays same timestamp events in correct order 1`] = `
</head>
<body>
<div class=\\"replayer-wrapper\\">
<div class=\\"replayer-mouse\\"></div>
<canvas
class=\\"replayer-mouse-tail\\"
width=\\"1000\\"
Expand Down
Loading
Loading