Browse Source

ref(replay): Refactor ReplayReader to provide eventsWithSnapshots and touchEvents lists for video replays (#80879)

I wanted to move the loop that creates `eventsWithSnapshots` into
ReplayReader, and also the filter that outputs the `touchEvents` array.
The reader does a lot of stuff like this already, and memoizes the
results consistently, so I think it should all go together in there.

Also this helps to slim down `class VideoReplayerWithInteractions` which
i want to do, so it can have a simpler & similar interface to `class
Replayer`
Ryan Albrecht 3 months ago
parent
commit
393dfe5701

+ 2 - 2
static/app/components/replays/replayContext.tsx

@@ -428,7 +428,8 @@ export function Provider({
         speed: prefs.playbackSpeed,
         // rrweb specific
         theme,
-        events: events ?? [],
+        eventsWithSnapshots: replay?.getRRWebFramesWithSnapshots() ?? [],
+        touchEvents: replay?.getRRwebTouchEvents() ?? [],
         // common to both
         root,
         context: {
@@ -448,7 +449,6 @@ export function Provider({
       applyInitialOffset,
       clipWindow,
       durationMs,
-      events,
       isFetching,
       isVideoReplay,
       organization.slug,

+ 4 - 65
static/app/components/replays/videoReplayerWithInteractions.tsx

@@ -5,20 +5,13 @@ import {Replayer} from '@sentry-internal/rrweb';
 import type {VideoReplayerConfig} from 'sentry/components/replays/videoReplayer';
 import {VideoReplayer} from 'sentry/components/replays/videoReplayer';
 import type {ClipWindow, RecordingFrame, VideoEvent} from 'sentry/utils/replays/types';
-import {
-  EventType,
-  isMetaFrame,
-  isTouchEndFrame,
-  isTouchStartFrame,
-  NodeType,
-} from 'sentry/utils/replays/types';
 
 type RootElem = HTMLDivElement | null;
 
 interface VideoReplayerWithInteractionsOptions {
   context: {sdkName: string | undefined | null; sdkVersion: string | undefined | null};
   durationMs: number;
-  events: RecordingFrame[];
+  eventsWithSnapshots: RecordingFrame[];
   onBuffer: (isBuffering: boolean) => void;
   onFinished: () => void;
   onLoaded: (event: any) => void;
@@ -26,6 +19,7 @@ interface VideoReplayerWithInteractionsOptions {
   speed: number;
   start: number;
   theme: Theme;
+  touchEvents: RecordingFrame[];
   videoApiPrefix: string;
   videoEvents: VideoEvent[];
   clipWindow?: ClipWindow;
@@ -42,7 +36,8 @@ export class VideoReplayerWithInteractions {
 
   constructor({
     videoEvents,
-    events,
+    eventsWithSnapshots,
+    touchEvents,
     root,
     start,
     videoApiPrefix,
@@ -74,62 +69,6 @@ export class VideoReplayerWithInteractions {
 
     root?.classList.add('video-replayer');
 
-    const eventsWithSnapshots: RecordingFrame[] = [];
-    events.forEach((e, index) => {
-      // For taps, sometimes the timestamp difference between TouchStart
-      // and TouchEnd is too small. This clamps the tap to a min time
-      // if the difference is less, so that the rrweb tap is visible and obvious.
-      if (isTouchStartFrame(e) && index < events.length - 2) {
-        const nextEvent = events[index + 1];
-        if (isTouchEndFrame(nextEvent)) {
-          nextEvent.timestamp = Math.max(nextEvent.timestamp, e.timestamp + 500);
-        }
-      }
-      eventsWithSnapshots.push(e);
-      if (isMetaFrame(e)) {
-        // 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
-        eventsWithSnapshots.push({
-          type: EventType.FullSnapshot,
-          data: {
-            node: {
-              type: NodeType.Document,
-              childNodes: [
-                {
-                  type: NodeType.DocumentType,
-                  id: 1,
-                  name: 'html',
-                  publicId: '',
-                  systemId: '',
-                },
-                {
-                  type: NodeType.Element,
-                  id: 2,
-                  tagName: 'html',
-                  attributes: {
-                    lang: 'en',
-                  },
-                  childNodes: [],
-                },
-              ],
-              id: 0,
-            },
-            initialOffset: {
-              top: 0,
-              left: 0,
-            },
-          },
-          timestamp: e.timestamp,
-        });
-      }
-    });
-
-    // log instances where we have a pointer touchStart without a touchEnd
-    const touchEvents = eventsWithSnapshots.filter(
-      e => isTouchEndFrame(e) || isTouchStartFrame(e)
-    );
     const grouped = Object.groupBy(touchEvents, (t: any) => t.data.pointerId);
     Object.values(grouped).forEach(t => {
       if (t?.length !== 2) {

+ 66 - 0
static/app/utils/replays/replayReader.tsx

@@ -42,8 +42,12 @@ import {
   isConsoleFrame,
   isDeadClick,
   isDeadRageClick,
+  isMetaFrame,
   isPaintFrame,
+  isTouchEndFrame,
+  isTouchStartFrame,
   isWebVitalFrame,
+  NodeType,
 } from 'sentry/utils/replays/types';
 import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types';
 
@@ -474,6 +478,68 @@ export default class ReplayReader {
 
   getRRWebFrames = () => this._sortedRRWebEvents;
 
+  getRRWebFramesWithSnapshots = memoize(() => {
+    const eventsWithSnapshots: RecordingFrame[] = [];
+    const events = this._sortedRRWebEvents;
+    events.forEach((e, index) => {
+      // For taps, sometimes the timestamp difference between TouchStart
+      // and TouchEnd is too small. This clamps the tap to a min time
+      // if the difference is less, so that the rrweb tap is visible and obvious.
+      if (isTouchStartFrame(e) && index < events.length - 2) {
+        const nextEvent = events[index + 1];
+        if (isTouchEndFrame(nextEvent)) {
+          nextEvent.timestamp = Math.max(nextEvent.timestamp, e.timestamp + 500);
+        }
+      }
+      eventsWithSnapshots.push(e);
+      if (isMetaFrame(e)) {
+        // 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
+        eventsWithSnapshots.push({
+          type: EventType.FullSnapshot,
+          data: {
+            node: {
+              type: NodeType.Document,
+              childNodes: [
+                {
+                  type: NodeType.DocumentType,
+                  id: 1,
+                  name: 'html',
+                  publicId: '',
+                  systemId: '',
+                },
+                {
+                  type: NodeType.Element,
+                  id: 2,
+                  tagName: 'html',
+                  attributes: {
+                    lang: 'en',
+                  },
+                  childNodes: [],
+                },
+              ],
+              id: 0,
+            },
+            initialOffset: {
+              top: 0,
+              left: 0,
+            },
+          },
+          timestamp: e.timestamp,
+        });
+      }
+    });
+    return eventsWithSnapshots;
+  });
+
+  getRRwebTouchEvents = memoize(() =>
+    this.getRRWebFramesWithSnapshots().filter(
+      e => isTouchEndFrame(e) || isTouchStartFrame(e)
+    )
+  );
+
   getBreadcrumbFrames = () => this._sortedBreadcrumbFrames;
 
   getRRWebMutations = () =>