Skip to content

Commit

Permalink
fix(rrweb): clean up pointer tap circles when seeking by breadcrumb (#…
Browse files Browse the repository at this point in the history
…209)

- fixes getsentry/sentry#72959
- cleans up the tap circles when the user seeks by breadcrumb (aka when
`isSync` is true) so that there aren't a bunch left on the screen at
once
- verified that with this change, multitouch still works correctly

before:


https://github.com/getsentry/rrweb/assets/56095982/6ab6b903-1bb3-494d-8f6e-9e27cf97f199

after (video also shows playback working normally):



https://github.com/getsentry/rrweb/assets/56095982/610fe5e5-1615-4eb5-8837-4ac5e4df1a05
  • Loading branch information
michellewzhang authored Jun 24, 2024
1 parent 0e75cb4 commit dbc62a2
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 3 deletions.
14 changes: 14 additions & 0 deletions packages/rrweb/src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,20 @@ export class Replayer {
if (isSync) {
if (d.type === MouseInteractions.TouchStart) {
pointer.touchActive = true;

// prevents multiple touch circles from staying on the screen
// when the user seeks by breadcrumbs
Object.values(this.pointers).forEach((p) => {
// don't set p.touchActive to false if p.touchActive is already true
// so that multitouch still works.
// p.touchActive can be null (in which case
// we still want to set it as false) - it's set as null
// in the ReplayerEvents.Flush handler after
// the 'touch-active' class is added or removed.
if (p !== pointer && !p.touchActive) {
p.touchActive = false;
}
});
} else if (d.type === MouseInteractions.TouchEnd) {
pointer.touchActive = false;
pointer.pointerEl.remove();
Expand Down
181 changes: 181 additions & 0 deletions packages/rrweb/test/events/is-sync-tap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import {
EventType,
IncrementalSource,
MouseInteractions,
} from '@sentry-internal/rrweb-types';
import type { eventWithTime } from '../../../types/src';

const events: eventWithTime[] = [
{
type: 4,
data: {
href: '',
width: 1600,
height: 900,
},
timestamp: 0,
},
{
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: [
{
id: 101,
type: 2,
tagName: 'style',
attributes: {},
childNodes: [
{
id: 102,
type: 3,
isStyle: true,
textContent: 'div:hover { background-color: gold; }',
},
],
},
],
id: 4,
},
{ type: 3, textContent: '\n ', id: 13 },
{
type: 2,
tagName: 'body',
attributes: {},
childNodes: [
{ type: 3, textContent: '\n ', id: 15 },
{
type: 2,
tagName: 'div',
attributes: {
style:
'border: 1px solid #000000; width: 100px; height: 100px;',
},
childNodes: [{ type: 3, textContent: '\n ', id: 17 }],
id: 16,
},
],
id: 14,
},
],
id: 3,
},
],
id: 1,
},
initialOffset: { left: 0, top: 0 },
},
timestamp: 10,
},
{
type: 3,
data: {
source: IncrementalSource.MouseInteraction,
type: MouseInteractions.TouchStart,
id: 16,
x: 30,
y: 30,
pointerId: 0,
},
timestamp: 100,
},
{
type: 3,
data: {
source: IncrementalSource.TouchMove,
positions: [
{
id: 0,
x: 149.436,
y: 433.929,
timeOffset: 30,
},
{
id: 1,
x: 243.436,
y: 155.929,
timeOffset: 0,
},
],
pointerId: 0,
},
timestamp: 150,
},
{
type: 3,
data: {
source: IncrementalSource.MouseInteraction,
type: MouseInteractions.TouchEnd,
id: 16,
x: 30,
y: 30,
pointerId: 0,
},
timestamp: 155,
},
{
type: 3,
data: {
source: IncrementalSource.MouseInteraction,
type: MouseInteractions.TouchStart,
id: 16,
x: 30,
y: 30,
pointerId: 1,
},
timestamp: 160,
},
{
type: 3,
data: {
source: IncrementalSource.TouchMove,
positions: [
{
id: 0,
x: 149.436,
y: 433.929,
timeOffset: 30,
},
{
id: 1,
x: 243.436,
y: 155.929,
timeOffset: 0,
},
],
pointerId: 1,
},
timestamp: 170,
},
{
type: 3,
data: {
source: IncrementalSource.MouseInteraction,
type: MouseInteractions.TouchEnd,
id: 16,
x: 30,
y: 30,
pointerId: 1,
},
timestamp: 180,
},
{
type: EventType.IncrementalSnapshot,
data: { source: IncrementalSource.Scroll, id: 1, x: 0, y: 250 },
timestamp: 220,
},
];

export default events;
39 changes: 36 additions & 3 deletions packages/rrweb/test/replayer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import touchAllPointerEvents from './events/touch-all-pointer-id';
import touchSomePointerEvents from './events/touch-some-pointer-id';
import mouseEvents from './events/mouse';
import scrollEvents from './events/scroll';
import isSyncTapEvents from './events/is-sync-tap';
import scrollWithParentStylesEvents from './events/scroll-with-parent-styles';
import inputEvents from './events/input';
import iframeEvents from './events/iframe';
Expand Down Expand Up @@ -881,7 +882,7 @@ describe('replayer', function () {
`);

// No pointer should exist yet
await expect(
expect(
await page.evaluate(
() => document.querySelectorAll('.replayer-mouse')!.length,
),
Expand All @@ -892,7 +893,7 @@ describe('replayer', function () {
`);

// One mouse pointer should exist
await expect(
expect(
await page.evaluate(
() => document.querySelectorAll('.replayer-mouse')!.length,
),
Expand All @@ -903,13 +904,45 @@ describe('replayer', function () {
`);

// Pointer should still exist after all events execute
await expect(
expect(
await page.evaluate(
() => document.querySelectorAll('.replayer-mouse')!.length,
),
).toEqual(1);
});

it('removes tap circles from the screen when isSync = true', async () => {
await page.evaluate(`events = ${JSON.stringify(isSyncTapEvents)}`);
await page.evaluate(`
const { Replayer } = rrweb;
const replayer = new Replayer(events);
`);

// No pointer should exist yet
expect(
await page.evaluate(
() => document.querySelectorAll('.replayer-mouse')!.length,
),
).toEqual(0);

// Seek to second tap
await page.evaluate(`
replayer.pause(161);
`);

// Seek to first tap
await page.evaluate(`
replayer.pause(101);
`);

// Only one tap should exist, not both
expect(
await page.evaluate(
() => document.querySelectorAll('.touch-active')!.length,
),
).toEqual(1);
});

it('should destroy the replayer after calling destroy()', async () => {
await page.evaluate(`events = ${JSON.stringify(events)}`);
await page.evaluate(`
Expand Down

0 comments on commit dbc62a2

Please sign in to comment.