Repository: getsentry/sentry
Pull request: feat(replays): Additional analytics tracking events
### Changes
- Adds new event type and tracks replay play/pause
- Adds new event type and tracks details page tab changes
- Adds new event type and tracks time spent on replay details page
- Adds new event type and tracks fullscreen mode toggle

### Notes
play/pause and fullscreen toggle events can be triggered from other pages that use the replay player. I assume this is fine if not let me know and I can add some additional logic for it

Closes #38581 

<!--

  Sentry employees and contractors can delete or ignore the following.

-->

### Legal Boilerplate

Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms.


Problem statement:
Session Replay: additional user analytics events for Amplitude based on feedback
Based on feedback from the team, we'd like to instrument the following user interactions:

- Click the "play" button on the Replay Details page and pause (per user)
- Which tabs in Replay Details are users clicking ("Console", "DOM Events", "Network", "Trace", "Issues", "Memory")
- Time spent on Replay Details (per user)
- Clicked into full-screen mode or exiting full-screen mode (per user)

Hints from issue discussion:
cc @ryan953 

Relates to [38315](https://github.com/getsentry/sentry/issues/38315)
Example of recording metrics: https://github.com/getsentry/sentry/pull/38370

Commit message: fix(replays): fixes lint issues

Patch to review:
diff --git a/static/app/components/replays/replayController.tsx b/static/app/components/replays/replayController.tsx
index ab3c54e7817499..eaaeb44a761ac6 100644
--- a/static/app/components/replays/replayController.tsx
+++ b/static/app/components/replays/replayController.tsx
@@ -18,11 +18,15 @@ import {
   IconSettings,
 } from 'sentry/icons';
 import {t} from 'sentry/locale';
+import ConfigStore from 'sentry/stores/configStore';
+import {useLegacyStore} from 'sentry/stores/useLegacyStore';
 import space from 'sentry/styles/space';
 import {SelectValue} from 'sentry/types';
 import {BreadcrumbType} from 'sentry/types/breadcrumbs';
+import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
 import {getNextBreadcrumb} from 'sentry/utils/replays/getBreadcrumb';
 import useFullscreen from 'sentry/utils/replays/hooks/useFullscreen';
+import useOrganization from 'sentry/utils/useOrganization';
 
 const SECOND = 1000;
 
@@ -40,6 +44,9 @@ interface Props {
 }
 
 function ReplayPlayPauseBar({isCompact}: {isCompact: boolean}) {
+  const config = useLegacyStore(ConfigStore);
+  const organization = useOrganization();
+
   const {
     currentTime,
     isFinished,
@@ -50,6 +57,15 @@ function ReplayPlayPauseBar({isCompact}: {isCompact: boolean}) {
     togglePlayPause,
   } = useReplayContext();
 
+  const handlePlayToggle = () => {
+    trackAdvancedAnalyticsEvent('replay.play-pause', {
+      organization,
+      user_email: config.user.email,
+      play: !isPlaying,
+    });
+    togglePlayPause(!isPlaying);
+  };
+
   return (
     <ButtonBar merged>
       {!isCompact && (
@@ -74,7 +90,7 @@ function ReplayPlayPauseBar({isCompact}: {isCompact: boolean}) {
           size="sm"
           title={isPlaying ? t('Pause') : t('Play')}
           icon={isPlaying ? <IconPause size="sm" /> : <IconPlay size="sm" />}
-          onClick={() => togglePlayPause(!isPlaying)}
+          onClick={handlePlayToggle}
           aria-label={isPlaying ? t('Pause') : t('Play')}
         />
       )}
@@ -170,10 +186,21 @@ const ReplayControls = ({
   toggleFullscreen = () => {},
   speedOptions = [0.1, 0.25, 0.5, 1, 2, 4],
 }: Props) => {
+  const config = useLegacyStore(ConfigStore);
+  const organization = useOrganization();
   const barRef = useRef<HTMLDivElement>(null);
   const [compactLevel, setCompactLevel] = useState(0);
   const {isFullscreen} = useFullscreen();
 
+  const handleFullscreenToggle = () => {
+    trackAdvancedAnalyticsEvent('replay.toggle-fullscreen', {
+      organization,
+      user_email: config.user.email,
+      fullscreen: !isFullscreen,
+    });
+    toggleFullscreen();
+  };
+
   const updateCompactLevel = useCallback(() => {
     const {width} = barRef.current?.getBoundingClientRect() ?? {width: 500};
     if (width < 400) {
@@ -201,7 +228,7 @@ const ReplayControls = ({
         title={isFullscreen ? t('Exit full screen') : t('Enter full screen')}
         aria-label={isFullscreen ? t('Exit full screen') : t('Enter full screen')}
         icon={isFullscreen ? <IconContract size="sm" /> : <IconExpand size="sm" />}
-        onClick={toggleFullscreen}
+        onClick={handleFullscreenToggle}
       />
     </ButtonGrid>
   );
diff --git a/static/app/utils/analytics/replayAnalyticsEvents.tsx b/static/app/utils/analytics/replayAnalyticsEvents.tsx
index c500b4fb05a635..c0d9feefe89fff 100644
--- a/static/app/utils/analytics/replayAnalyticsEvents.tsx
+++ b/static/app/utils/analytics/replayAnalyticsEvents.tsx
@@ -9,10 +9,25 @@ export type ReplayEventParameters = {
     layout: LayoutKey;
     slide_motion: 'toTop' | 'toBottom' | 'toLeft' | 'toRight';
   };
+  'replay.details-tab-changed': {
+    tab: string;
+  };
+  'replay.details-time-spent': {
+    seconds: number;
+    user_email: string;
+  };
   'replay.details-viewed': {
     referrer: undefined | string;
     user_email: string;
   };
+  'replay.play-pause': {
+    play: boolean;
+    user_email: string;
+  };
+  'replay.toggle-fullscreen': {
+    fullscreen: boolean;
+    user_email: string;
+  };
 };
 
 export type ReplayEventKey = keyof ReplayEventParameters;
@@ -21,4 +36,8 @@ export const replayEventMap: Record<ReplayEventKey, string | null> = {
   'replay.details-layout-changed': 'Changed Replay Details Layout',
   'replay.details-resized-panel': 'Resized Replay Details Panel',
   'replay.details-viewed': 'Viewed Replay Details',
+  'replay.play-pause': 'Played/Paused Replay',
+  'replay.details-tab-changed': 'Changed Replay Details Tab',
+  'replay.details-time-spent': 'Time Spent Viewing Replay Details',
+  'replay.toggle-fullscreen': 'Toggled Replay Fullscreen',
 };
diff --git a/static/app/utils/replays/hooks/useReplayPageview.tsx b/static/app/utils/replays/hooks/useReplayPageview.tsx
index cc3be1d5d4f866..101e8916bd401a 100644
--- a/static/app/utils/replays/hooks/useReplayPageview.tsx
+++ b/static/app/utils/replays/hooks/useReplayPageview.tsx
@@ -1,4 +1,4 @@
-import {useEffect} from 'react';
+import {useEffect, useRef} from 'react';
 
 import ConfigStore from 'sentry/stores/configStore';
 import {useLegacyStore} from 'sentry/stores/useLegacyStore';
@@ -11,13 +11,25 @@ function useReplayPageview() {
   const config = useLegacyStore(ConfigStore);
   const location = useLocation();
   const organization = useOrganization();
+  const startTimeRef = useRef(Date.now());
 
   useEffect(() => {
+    const startTime = startTimeRef.current;
+
     trackAdvancedAnalyticsEvent('replay.details-viewed', {
       organization,
       referrer: decodeScalar(location.query.referrer),
       user_email: config.user.email,
     });
+
+    return () => {
+      const endTime = Date.now();
+      trackAdvancedAnalyticsEvent('replay.details-time-spent', {
+        organization,
+        seconds: (endTime - startTime) / 1000,
+        user_email: config.user.email,
+      });
+    };
   }, [organization, location.query.referrer, config.user.email]);
 }
 
diff --git a/static/app/views/replays/detail/sideTabs.tsx b/static/app/views/replays/detail/sideTabs.tsx
index 2fd8461d4505b1..133183c6ae4e8b 100644
--- a/static/app/views/replays/detail/sideTabs.tsx
+++ b/static/app/views/replays/detail/sideTabs.tsx
@@ -1,5 +1,7 @@
 import NavTabs from 'sentry/components/navTabs';
 import {t} from 'sentry/locale';
+import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
+import useOrganization from 'sentry/utils/useOrganization';
 import useUrlParams from 'sentry/utils/useUrlParams';
 
 const TABS = {
@@ -12,15 +14,24 @@ type Props = {
 };
 
 function SideTabs({className}: Props) {
+  const organization = useOrganization();
   const {getParamValue, setParamValue} = useUrlParams('t_side', 'crumbs');
   const active = getParamValue();
 
+  const createTabChangeHandler = (tab: string) => () => {
+    trackAdvancedAnalyticsEvent('replay.details-tab-changed', {
+      tab,
+      organization,
+    });
+    setParamValue(tab);
+  };
+
   return (
     <NavTabs underlined className={className}>
       {Object.entries(TABS).map(([tab, label]) => {
         return (
           <li key={tab} className={active === tab ? 'active' : ''}>
-            <a onClick={() => setParamValue(tab)}>{label}</a>
+            <a onClick={createTabChangeHandler(tab)}>{label}</a>
           </li>
         );
       })}


Code review comments:
diff_hunk: @@ -50,6 +57,15 @@ function ReplayPlayPauseBar({isCompact}: {isCompact: boolean}) {
     togglePlayPause,
   } = useReplayContext();
 
+  const handlePlayToggle = () => {
+    trackAdvancedAnalyticsEvent('replay.play-pause', {
+      organization,
+      user_email: config.user.email,
+      play: !isPlaying,
+    });
path: static/app/components/replays/replayController.tsx
text: @user1:
this could be better off if it were inside of ReplayContext. Then if we had other ways to trigger play/pause (like a keyboard shortcut, or if we wanted to log that someone got to the end of the replay) it'd be centralized

Also, it'll help keep the react components lean and focused on rendering stuff. The callbacks are already coming from the hook/context, so lets keep stuffing the code up there.

diff_hunk: @@ -170,10 +186,21 @@ const ReplayControls = ({
   toggleFullscreen = () => {},
   speedOptions = [0.1, 0.25, 0.5, 1, 2, 4],
 }: Props) => {
+  const config = useLegacyStore(ConfigStore);
+  const organization = useOrganization();
   const barRef = useRef<HTMLDivElement>(null);
   const [compactLevel, setCompactLevel] = useState(0);
   const {isFullscreen} = useFullscreen();
 
+  const handleFullscreenToggle = () => {
+    trackAdvancedAnalyticsEvent('replay.toggle-fullscreen', {
+      organization,
+      user_email: config.user.email,
+      fullscreen: !isFullscreen,
+    });
path: static/app/components/replays/replayController.tsx
text: @user1:
i would also shove this inside the `toggleFullscreen` prop

because like, if someone makes a call like this we shouldn't be logging: `<ReplayControls toggleFullscreen={() => void} />`