Browse Source

feat(replays): Additional analytics tracking events (#38966)

Adds logging for some new event types:
- Replay play/pause
- Details page tab changes (main and sidebar tabs)
- Time spent on replay details page
- Toggle Fullscreen mode

Closes #38581
Dane Grant 2 years ago
parent
commit
02e024c023

+ 10 - 1
static/app/components/events/eventReplay/replayContent.spec.tsx

@@ -1,7 +1,8 @@
-import {render, screen} from 'sentry-test/reactTestingLibrary';
+import {render as baseRender, screen} from 'sentry-test/reactTestingLibrary';
 
 import useReplayData from 'sentry/utils/replays/hooks/useReplayData';
 import ReplayReader from 'sentry/utils/replays/replayReader';
+import {OrganizationContext} from 'sentry/views/organizationContext';
 
 import ReplayContent from './replayContent';
 
@@ -110,6 +111,14 @@ jest.mock('sentry/utils/replays/hooks/useReplayData', () => {
   };
 });
 
+const render: typeof baseRender = children => {
+  return baseRender(
+    <OrganizationContext.Provider value={TestStubs.Organization()}>
+      {children}
+    </OrganizationContext.Provider>
+  );
+};
+
 describe('ReplayContent', () => {
   it('Should render a placeholder when is fetching the replay data', () => {
     // Change the mocked hook to return a loading state

+ 13 - 1
static/app/components/replays/replayContext.tsx

@@ -2,6 +2,9 @@ import React, {useCallback, useContext, useEffect, useRef, useState} from 'react
 import {useTheme} from '@emotion/react';
 import {Replayer, ReplayerEvents} from 'rrweb';
 
+import ConfigStore from 'sentry/stores/configStore';
+import {useLegacyStore} from 'sentry/stores/useLegacyStore';
+import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
 import localStorage from 'sentry/utils/localStorage';
 import {
   clearAllHighlights,
@@ -10,6 +13,7 @@ import {
 } from 'sentry/utils/replays/highlightNode';
 import useRAF from 'sentry/utils/replays/hooks/useRAF';
 import type ReplayReader from 'sentry/utils/replays/replayReader';
+import useOrganization from 'sentry/utils/useOrganization';
 import usePrevious from 'sentry/utils/usePrevious';
 
 enum ReplayLocalstorageKeys {
@@ -195,6 +199,8 @@ function updateSavedReplayConfig(config: ReplayConfig) {
 }
 
 export function Provider({children, replay, initialTimeOffset = 0, value = {}}: Props) {
+  const config = useLegacyStore(ConfigStore);
+  const organization = useOrganization();
   const events = replay?.getRRWebEvents();
   const savedReplayConfigRef = useRef<ReplayConfig>(
     JSON.parse(localStorage.getItem(ReplayLocalstorageKeys.ReplayConfig) || '{}')
@@ -420,8 +426,14 @@ export function Provider({children, replay, initialTimeOffset = 0, value = {}}:
         replayer.pause(getCurrentTime());
       }
       setIsPlaying(play);
+
+      trackAdvancedAnalyticsEvent('replay.play-pause', {
+        organization,
+        user_email: config.user.email,
+        play,
+      });
     },
-    [getCurrentTime]
+    [getCurrentTime, config.user.email, organization]
   );
 
   const restart = useCallback(() => {

+ 19 - 2
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;
 
@@ -167,13 +171,26 @@ function ReplayOptionsMenu({speedOptions}: {speedOptions: number[]}) {
 }
 
 const ReplayControls = ({
-  toggleFullscreen = () => {},
+  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 = () => {
+    if (toggleFullscreen) {
+      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 +218,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>
   );

+ 19 - 0
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;
@@ -20,5 +35,9 @@ export type ReplayEventKey = keyof ReplayEventParameters;
 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-tab-changed': 'Changed Replay Details Tab',
+  'replay.details-time-spent': 'Time Spent Viewing Replay Details',
   'replay.details-viewed': 'Viewed Replay Details',
+  'replay.play-pause': 'Played/Paused Replay',
+  'replay.toggle-fullscreen': 'Toggled Replay Fullscreen',
 };

+ 4 - 4
static/app/utils/replays/hooks/useFullscreen.tsx

@@ -67,11 +67,11 @@ export default function useFullscreen(): FullscreenHook {
     [enter, exit, isFullscreen]
   );
 
-  const onChange = () => {
-    setIsFullscreen(screenfull.isFullscreen);
-  };
-
   useEffect(() => {
+    const onChange = () => {
+      setIsFullscreen(screenfull.isFullscreen);
+    };
+
     screenfull.on('change', onChange);
     return () => screenfull.off('change', onChange);
   }, []);

+ 13 - 1
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]);
 }
 

+ 14 - 4
static/app/views/replays/detail/focusTabs.tsx

@@ -4,8 +4,10 @@ import queryString from 'query-string';
 
 import NavTabs from 'sentry/components/navTabs';
 import {t} from 'sentry/locale';
+import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
 import useActiveReplayTab, {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab';
 import {useLocation} from 'sentry/utils/useLocation';
+import useOrganization from 'sentry/utils/useOrganization';
 
 const ReplayTabs: Record<TabKey, string> = {
   console: t('Console'),
@@ -19,20 +21,28 @@ const ReplayTabs: Record<TabKey, string> = {
 type Props = {className?: string};
 
 function FocusTabs({className}: Props) {
+  const organization = useOrganization();
   const {pathname, query} = useLocation();
   const {getActiveTab, setActiveTab} = useActiveReplayTab();
   const activeTab = getActiveTab();
 
+  const createTabChangeHandler = (tab: string) => (e: MouseEvent) => {
+    e.preventDefault();
+    setActiveTab(tab);
+
+    trackAdvancedAnalyticsEvent('replay.details-tab-changed', {
+      tab,
+      organization,
+    });
+  };
+
   return (
     <ScrollableNavTabs underlined className={className}>
       {Object.entries(ReplayTabs).map(([tab, label]) => (
         <li key={tab} className={activeTab === tab ? 'active' : ''}>
           <a
             href={`${pathname}?${queryString.stringify({...query, t_main: tab})}`}
-            onClick={(e: MouseEvent) => {
-              setActiveTab(tab);
-              e.preventDefault();
-            }}
+            onClick={createTabChangeHandler(tab)}
           >
             <span>{label}</span>
           </a>

+ 12 - 1
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>
         );
       })}