Browse Source

fix(replay): Force playback speed of replay clips to 1x (#65133)

Fixes https://github.com/getsentry/sentry/issues/65007
Ryan Albrecht 1 year ago
parent
commit
9e90557e6f

+ 3 - 1
static/app/components/events/eventReplay/replayClipPreview.tsx

@@ -12,6 +12,7 @@ import LoadingIndicator from 'sentry/components/loadingIndicator';
 import Panel from 'sentry/components/panels/panel';
 import {Flex} from 'sentry/components/profiling/flex';
 import MissingReplayAlert from 'sentry/components/replays/alerts/missingReplayAlert';
+import {StaticReplayPreferences} from 'sentry/components/replays/preferences/replayPreferences';
 import {
   Provider as ReplayContextProvider,
   useReplayContext,
@@ -208,9 +209,10 @@ function ReplayClipPreview({
 
   return (
     <ReplayContextProvider
+      analyticsContext={analyticsContext}
       isFetching={fetching}
+      prefsStrategy={StaticReplayPreferences}
       replay={replay}
-      analyticsContext={analyticsContext}
     >
       <PlayerContainer data-test-id="player-container">
         {replay?.hasProcessingErrors() ? (

+ 4 - 2
static/app/components/events/eventReplay/staticReplayPreview.tsx

@@ -2,6 +2,7 @@ import {type ComponentProps, Fragment, useMemo} from 'react';
 import styled from '@emotion/styled';
 
 import {LinkButton} from 'sentry/components/button';
+import {StaticReplayPreferences} from 'sentry/components/replays/preferences/replayPreferences';
 import {Provider as ReplayContextProvider} from 'sentry/components/replays/replayContext';
 import ReplayPlayer from 'sentry/components/replays/replayPlayer';
 import ReplayProcessingError from 'sentry/components/replays/replayProcessingError';
@@ -54,10 +55,11 @@ export function StaticReplayPreview({
 
   return (
     <ReplayContextProvider
+      analyticsContext={analyticsContext}
+      initialTimeOffsetMs={offset}
       isFetching={isFetching}
+      prefsStrategy={StaticReplayPreferences}
       replay={replay}
-      initialTimeOffsetMs={offset}
-      analyticsContext={analyticsContext}
     >
       <PlayerContainer data-test-id="player-container">
         {replay?.hasProcessingErrors() ? (

+ 5 - 2
static/app/components/replays/breadcrumbs/replayComparisonModal.tsx

@@ -8,6 +8,7 @@ import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
 import FeatureBadge from 'sentry/components/featureBadge';
 import {GithubFeedbackButton} from 'sentry/components/githubFeedbackButton';
 import {Flex} from 'sentry/components/profiling/flex';
+import {StaticReplayPreferences} from 'sentry/components/replays/preferences/replayPreferences';
 import {
   Provider as ReplayContextProvider,
   useReplayContext,
@@ -120,9 +121,10 @@ export default function ReplayComparisonModal({
             <ReplayGrid>
               <ReplayContextProvider
                 analyticsContext="replay_comparison_modal_left"
+                initialTimeOffsetMs={{offsetMs: startOffset}}
                 isFetching={fetching}
+                prefsStrategy={StaticReplayPreferences}
                 replay={replay}
-                initialTimeOffsetMs={{offsetMs: startOffset}}
               >
                 <ComparisonSideWrapper id="leftSide">
                   <ReplaySide
@@ -134,9 +136,10 @@ export default function ReplayComparisonModal({
               </ReplayContextProvider>
               <ReplayContextProvider
                 analyticsContext="replay_comparison_modal_right"
+                initialTimeOffsetMs={{offsetMs: rightTimestamp + 1}}
                 isFetching={fetching}
+                prefsStrategy={StaticReplayPreferences}
                 replay={replay}
-                initialTimeOffsetMs={{offsetMs: rightTimestamp + 1}}
               >
                 <ComparisonSideWrapper id="rightSide">
                   {rightTimestamp > 0 ? (

+ 32 - 0
static/app/components/replays/preferences/replayPreferences.tsx

@@ -0,0 +1,32 @@
+import localStorage from 'sentry/utils/localStorage';
+
+const LOCAL_STORAGE_KEY = 'replay-config';
+
+export type ReplayPrefs = {
+  isSkippingInactive: boolean;
+  playbackSpeed: number;
+};
+
+const DEFAULT_PREFS = {
+  isSkippingInactive: true,
+  playbackSpeed: 1,
+};
+
+export interface PrefsStrategy {
+  get: () => ReplayPrefs;
+  set: (prefs: ReplayPrefs) => void;
+}
+
+export const StaticReplayPreferences: PrefsStrategy = {
+  get: (): ReplayPrefs => DEFAULT_PREFS,
+  set: () => {},
+};
+
+export const LocalStorageReplayPreferences: PrefsStrategy = {
+  get: (): ReplayPrefs => {
+    const parsed = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || '{}');
+    return {...DEFAULT_PREFS, ...parsed};
+  },
+  set: (prefs: ReplayPrefs) =>
+    localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(prefs)),
+};

+ 36 - 38
static/app/components/replays/replayContext.tsx

@@ -2,9 +2,12 @@ import {createContext, useCallback, useContext, useEffect, useRef, useState} fro
 import {useTheme} from '@emotion/react';
 import {Replayer, ReplayerEvents} from '@sentry-internal/rrweb';
 
+import type {
+  PrefsStrategy,
+  ReplayPrefs,
+} from 'sentry/components/replays/preferences/replayPreferences';
 import useReplayHighlighting from 'sentry/components/replays/useReplayHighlighting';
 import {trackAnalytics} from 'sentry/utils/analytics';
-import localStorage from 'sentry/utils/localStorage';
 import clamp from 'sentry/utils/number/clamp';
 import type useInitialOffsetMs from 'sentry/utils/replays/hooks/useInitialTimeOffsetMs';
 import useRAF from 'sentry/utils/replays/hooks/useRAF';
@@ -15,14 +18,6 @@ import {useUser} from 'sentry/utils/useUser';
 
 import {CanvasReplayerPlugin} from './canvasReplayerPlugin';
 
-enum ReplayLocalstorageKeys {
-  REPLAY_CONFIG = 'replay-config',
-}
-type ReplayConfig = {
-  skip?: boolean;
-  speed?: number;
-};
-
 type Dimensions = {height: number; width: number};
 type RootElem = null | HTMLDivElement;
 
@@ -191,6 +186,11 @@ type Props = {
    */
   isFetching: boolean;
 
+  /**
+   * The strategy for saving/loading preferences, like the playback speed
+   */
+  prefsStrategy: PrefsStrategy;
+
   replay: ReplayReader | null;
 
   /**
@@ -210,24 +210,19 @@ function useCurrentTime(callback: () => number) {
   return currentTime;
 }
 
-function updateSavedReplayConfig(config: ReplayConfig) {
-  localStorage.setItem(ReplayLocalstorageKeys.REPLAY_CONFIG, JSON.stringify(config));
-}
-
 export function Provider({
   analyticsContext,
   children,
   initialTimeOffsetMs,
   isFetching,
+  prefsStrategy,
   replay,
   value = {},
 }: Props) {
   const user = useUser();
   const organization = useOrganization();
   const events = replay?.getRRWebFrames();
-  const savedReplayConfigRef = useRef<ReplayConfig>(
-    JSON.parse(localStorage.getItem(ReplayLocalstorageKeys.REPLAY_CONFIG) || '{}')
-  );
+  const savedReplayConfigRef = useRef<ReplayPrefs>(prefsStrategy.get());
 
   const theme = useTheme();
   const oldEvents = usePrevious(events);
@@ -239,9 +234,9 @@ export function Provider({
   const [isPlaying, setIsPlaying] = useState(false);
   const [finishedAtMS, setFinishedAtMS] = useState<number>(-1);
   const [isSkippingInactive, setIsSkippingInactive] = useState(
-    savedReplayConfigRef.current.skip ?? true
+    savedReplayConfigRef.current.isSkippingInactive
   );
-  const [speed, setSpeedState] = useState(savedReplayConfigRef.current.speed || 1);
+  const [speed, setSpeedState] = useState(savedReplayConfigRef.current.playbackSpeed);
   const [fastForwardSpeed, setFFSpeed] = useState(0);
   const [buffer, setBufferTime] = useState({target: -1, previous: -1});
   const playTimer = useRef<number | undefined>(undefined);
@@ -392,8 +387,8 @@ export function Provider({
         plugins: organization.features.includes('session-replay-enable-canvas-replayer')
           ? [CanvasReplayerPlugin(events)]
           : [],
-        skipInactive: savedReplayConfigRef.current.skip ?? true,
-        speed: savedReplayConfigRef.current.speed || 1,
+        skipInactive: savedReplayConfigRef.current.isSkippingInactive,
+        speed: savedReplayConfigRef.current.playbackSpeed,
       });
 
       // @ts-expect-error: rrweb types event handlers with `unknown` parameters
@@ -427,10 +422,10 @@ export function Provider({
       const replayer = replayerRef.current;
       savedReplayConfigRef.current = {
         ...savedReplayConfigRef.current,
-        speed: newSpeed,
+        playbackSpeed: newSpeed,
       };
 
-      updateSavedReplayConfig(savedReplayConfigRef.current);
+      prefsStrategy.set(savedReplayConfigRef.current);
 
       if (!replayer) {
         return;
@@ -445,7 +440,7 @@ export function Provider({
 
       setSpeedState(newSpeed);
     },
-    [getCurrentPlayerTime, isPlaying]
+    [prefsStrategy, getCurrentPlayerTime, isPlaying]
   );
 
   const togglePlayPause = useCallback(
@@ -496,24 +491,27 @@ export function Provider({
     }
   }, [startTimeOffsetMs]);
 
-  const toggleSkipInactive = useCallback((skip: boolean) => {
-    const replayer = replayerRef.current;
-    savedReplayConfigRef.current = {
-      ...savedReplayConfigRef.current,
-      skip,
-    };
+  const toggleSkipInactive = useCallback(
+    (skip: boolean) => {
+      const replayer = replayerRef.current;
+      savedReplayConfigRef.current = {
+        ...savedReplayConfigRef.current,
+        isSkippingInactive: skip,
+      };
 
-    updateSavedReplayConfig(savedReplayConfigRef.current);
+      prefsStrategy.set(savedReplayConfigRef.current);
 
-    if (!replayer) {
-      return;
-    }
-    if (skip !== replayer.config.skipInactive) {
-      replayer.setConfig({skipInactive: skip});
-    }
+      if (!replayer) {
+        return;
+      }
+      if (skip !== replayer.config.skipInactive) {
+        replayer.setConfig({skipInactive: skip});
+      }
 
-    setIsSkippingInactive(skip);
-  }, []);
+      setIsSkippingInactive(skip);
+    },
+    [prefsStrategy]
+  );
 
   const currentPlayerTime = useCurrentTime(getCurrentPlayerTime);
 

+ 7 - 1
static/app/views/replays/detail/tagPanel/index.spec.tsx

@@ -2,6 +2,7 @@ import {ReplayRecordFixture} from 'sentry-fixture/replayRecord';
 
 import {render, screen} from 'sentry-test/reactTestingLibrary';
 
+import {StaticReplayPreferences} from 'sentry/components/replays/preferences/replayPreferences';
 import {Provider as ReplayContextProvider} from 'sentry/components/replays/replayContext';
 import ReplayReader from 'sentry/utils/replays/replayReader';
 import TagPanel from 'sentry/views/replays/detail/tagPanel';
@@ -23,7 +24,12 @@ const mockReplay = ReplayReader.factory({
 
 const renderComponent = (replay: ReplayReader | null) => {
   return render(
-    <ReplayContextProvider analyticsContext="" isFetching={false} replay={replay}>
+    <ReplayContextProvider
+      analyticsContext=""
+      isFetching={false}
+      prefsStrategy={StaticReplayPreferences}
+      replay={replay}
+    >
       <TagPanel />
     </ReplayContextProvider>
   );

+ 3 - 1
static/app/views/replays/details.tsx

@@ -8,6 +8,7 @@ import * as Layout from 'sentry/components/layouts/thirds';
 import List from 'sentry/components/list';
 import ListItem from 'sentry/components/list/listItem';
 import {Flex} from 'sentry/components/profiling/flex';
+import {LocalStorageReplayPreferences} from 'sentry/components/replays/preferences/replayPreferences';
 import {Provider as ReplayContextProvider} from 'sentry/components/replays/replayContext';
 import {IconDelete} from 'sentry/icons';
 import {t} from 'sentry/locale';
@@ -146,9 +147,10 @@ function ReplayDetails({params: {replaySlug}}: Props) {
   return (
     <ReplayContextProvider
       analyticsContext="replay_details"
+      initialTimeOffsetMs={initialTimeOffsetMs}
       isFetching={fetching}
+      prefsStrategy={LocalStorageReplayPreferences}
       replay={replay}
-      initialTimeOffsetMs={initialTimeOffsetMs}
     >
       <ReplayTransactionContext replayRecord={replayRecord}>
         <Page