-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(replay): create a wrapper class to init rrweb player alongside v…
…ideo replayer (#69927) Fixes #69817 We have a `videoReplayer`, which uses video events to create the replay playback for mobile replays. However, in order to see the gestures (clicks, mouse movements, etc) we need to initialize an rrweb player too (the one that web replay uses). This PR introduces a `videoReplayerWithInteractions` classe which initializes both, so that mobile replays can utilize both players at once. ![image](https://github.com/getsentry/sentry/assets/56095982/8a81da2d-2f8c-4bac-acf0-988c04be08ec) Another key change we had to make was introducing a fake full snapshot event after every meta event in order to trick the rrweb player into thinking we had a node to map the mouse movement to. The rrweb player essentially fails to render any gesture if it doesn't find an element with a matching `id` to the `id` inside the `positions` array (see below picture), so we hardcoded the event to have `id: 0` (which is what the SDK is sending for the mobile rrweb events). This workaround should be safe to do since the full snapshot event doesn't affect the video playback at all. <img width="264" alt="SCR-20240430-nqts" src="https://github.com/getsentry/sentry/assets/56095982/45e73a66-2740-4093-94a2-75cc1f3c2954"> Adding a snapshot event after every meta event also fixes the scrubbing bugs we were experiencing. Fixing mousetails not showing up involved absolutely positioning the `replayer-wrapper`. https://github.com/getsentry/sentry/assets/56095982/4a33cae4-ae1d-43b6-91a3-bf81fb36cf8c
- Loading branch information
1 parent
c1f2d1e
commit 6ed338e
Showing
4 changed files
with
181 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
152 changes: 152 additions & 0 deletions
152
static/app/components/replays/videoReplayerWithInteractions.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import type {Theme} from '@emotion/react'; | ||
import {type eventWithTime, Replayer} from '@sentry-internal/rrweb'; | ||
|
||
import { | ||
VideoReplayer, | ||
type VideoReplayerConfig, | ||
} from 'sentry/components/replays/videoReplayer'; | ||
import type {ClipWindow, VideoEvent} from 'sentry/utils/replays/types'; | ||
|
||
type RootElem = HTMLDivElement | null; | ||
|
||
interface VideoReplayerWithInteractionsOptions { | ||
durationMs: number; | ||
events: eventWithTime[]; | ||
onBuffer: (isBuffering: boolean) => void; | ||
onFinished: () => void; | ||
onLoaded: (event: any) => void; | ||
root: RootElem; | ||
start: number; | ||
theme: Theme; | ||
videoApiPrefix: string; | ||
videoEvents: VideoEvent[]; | ||
clipWindow?: ClipWindow; | ||
} | ||
|
||
/** | ||
* A wrapper replayer that wraps both VideoReplayer and the rrweb Replayer. | ||
* We need both instances in order to render the video playback alongside gestures. | ||
*/ | ||
export class VideoReplayerWithInteractions { | ||
public config: VideoReplayerConfig = { | ||
skipInactive: false, | ||
speed: 1.0, | ||
}; | ||
private videoReplayer: VideoReplayer; | ||
private replayer: Replayer; | ||
|
||
constructor({ | ||
videoEvents, | ||
events, | ||
root, | ||
start, | ||
videoApiPrefix, | ||
onBuffer, | ||
onFinished, | ||
onLoaded, | ||
clipWindow, | ||
durationMs, | ||
theme, | ||
}: VideoReplayerWithInteractionsOptions) { | ||
this.videoReplayer = new VideoReplayer(videoEvents, { | ||
videoApiPrefix, | ||
root, | ||
start, | ||
onFinished, | ||
onLoaded, | ||
onBuffer, | ||
clipWindow, | ||
durationMs, | ||
}); | ||
|
||
root?.classList.add('video-replayer'); | ||
|
||
const eventsWithSnapshots: eventWithTime[] = []; | ||
events.forEach(e => { | ||
eventsWithSnapshots.push(e); | ||
if (e.type === 4) { | ||
// Create a mock full snapshot event, in order to render rrweb gestures properly | ||
// Need to add one for every meta event we see | ||
// The hardcoded data.node.id here should match the ID of the data being sent | ||
// in the `positions` arrays | ||
const fullSnapshotEvent = { | ||
type: 2, | ||
data: { | ||
node: { | ||
type: 0, | ||
childNodes: [ | ||
{ | ||
type: 1, | ||
name: 'html', | ||
publicId: '', | ||
systemId: '', | ||
}, | ||
{ | ||
type: 2, | ||
tagName: 'html', | ||
attributes: { | ||
lang: 'en', | ||
}, | ||
childNodes: [], | ||
}, | ||
], | ||
id: 0, | ||
}, | ||
}, | ||
timestamp: e.timestamp, | ||
}; | ||
eventsWithSnapshots.push(fullSnapshotEvent); | ||
} | ||
}); | ||
|
||
this.replayer = new Replayer(eventsWithSnapshots, { | ||
root: root as Element, | ||
blockClass: 'sentry-block', | ||
mouseTail: { | ||
duration: 0.75 * 1000, | ||
lineCap: 'round', | ||
lineWidth: 2, | ||
strokeStyle: theme.purple200, | ||
}, | ||
plugins: [], | ||
skipInactive: false, | ||
speed: this.config.speed, | ||
}); | ||
} | ||
|
||
public destroy() { | ||
this.videoReplayer.destroy(); | ||
this.replayer.destroy(); | ||
} | ||
|
||
/** | ||
* Returns the current video time, using the video's external timer. | ||
*/ | ||
public getCurrentTime() { | ||
return this.videoReplayer.getCurrentTime(); | ||
} | ||
|
||
/** | ||
* Play both the rrweb and video player. | ||
*/ | ||
public play(videoOffsetMs: number) { | ||
this.videoReplayer.play(videoOffsetMs); | ||
this.replayer.play(videoOffsetMs); | ||
} | ||
|
||
/** | ||
* Pause both the rrweb and video player. | ||
*/ | ||
public pause(videoOffsetMs: number) { | ||
this.videoReplayer.pause(videoOffsetMs); | ||
this.replayer.pause(videoOffsetMs); | ||
} | ||
|
||
/** | ||
* Equivalent to rrweb's `setConfig()`, but here we only support the `speed` configuration. | ||
*/ | ||
public setConfig(config: Partial<VideoReplayerConfig>): void { | ||
this.videoReplayer.setConfig(config); | ||
this.replayer.setConfig(config); | ||
} | ||
} |