Browse Source

ref(replay): Refactor Replay Timeline Events to use *Frame types (#52454)

Ryan Albrecht 1 year ago
parent
commit
ef5cf8be97

+ 36 - 6
static/app/components/replays/breadcrumbs/breadcrumbItem.tsx

@@ -16,14 +16,25 @@ import {getDetails} from 'sentry/components/replays/breadcrumbs/utils';
 import {Tooltip} from 'sentry/components/tooltip';
 import {space} from 'sentry/styles/space';
 import {BreadcrumbType, Crumb} from 'sentry/types/breadcrumbs';
+import {
+  getBreadcrumbType,
+  getColor,
+  getDescription,
+  getTitle,
+} from 'sentry/utils/replays/frame';
+import type {ReplayFrame} from 'sentry/utils/replays/types';
+import {isErrorFrame} from 'sentry/utils/replays/types';
 import useProjects from 'sentry/utils/useProjects';
 import IconWrapper from 'sentry/views/replays/detail/iconWrapper';
 import TimestampButton from 'sentry/views/replays/detail/timestampButton';
 
-type MouseCallback = (crumb: Crumb, e: React.MouseEvent<HTMLElement>) => void;
+type MouseCallback = (
+  crumb: Crumb | ReplayFrame,
+  e: React.MouseEvent<HTMLElement>
+) => void;
 
 interface BaseProps {
-  crumb: Crumb;
+  crumb: Crumb | ReplayFrame;
   onClick: null | MouseCallback;
   startTimestampMs: number;
   className?: string;
@@ -52,6 +63,25 @@ interface WithDimensionChangeProps extends BaseProps {
 
 type Props = NoDimensionChangeProps | WithDimensionChangeProps;
 
+function getCrumbOrFrameData(crumb: Crumb | ReplayFrame) {
+  if ('offsetMs' in crumb) {
+    return {
+      color: getColor(crumb),
+      description: getDescription(crumb),
+      projectSlug: isErrorFrame(crumb) ? crumb.data.projectSlug : null,
+      title: getTitle(crumb),
+      type: getBreadcrumbType(crumb),
+      timestampMs: crumb.timestampMs,
+    };
+  }
+  const details = getDetails(crumb);
+  return {
+    ...details,
+    timestampMs: crumb.timestamp || '',
+    projectSlug: crumb.type === BreadcrumbType.ERROR ? details.projectSlug : undefined,
+  };
+}
+
 function BreadcrumbItem({
   className,
   crumb,
@@ -64,7 +94,8 @@ function BreadcrumbItem({
   startTimestampMs,
   style,
 }: Props) {
-  const {color, description, projectSlug, title, type} = getDetails(crumb);
+  const {color, description, projectSlug, title, type, timestampMs} =
+    getCrumbOrFrameData(crumb);
 
   const handleMouseEnter = useCallback(
     (e: React.MouseEvent<HTMLElement>) => onMouseEnter && onMouseEnter(crumb, e),
@@ -89,7 +120,6 @@ function BreadcrumbItem({
   // Note: use `crumb.type` here as `getDetails()` will return a type based on
   // crumb category for presentation purposes. e.g. if we wanted to use an
   // error icon for a non-Sentry error
-  const shouldShowCrumbProject = crumb.type === BreadcrumbType.ERROR && projectSlug;
 
   return (
     <CrumbItem
@@ -109,7 +139,7 @@ function BreadcrumbItem({
           {onClick ? (
             <TimestampButton
               startTimestampMs={startTimestampMs}
-              timestampMs={crumb.timestamp || ''}
+              timestampMs={timestampMs}
             />
           ) : null}
         </TitleContainer>
@@ -131,7 +161,7 @@ function BreadcrumbItem({
             />
           </InspectorWrapper>
         )}
-        {shouldShowCrumbProject && <CrumbProject projectSlug={projectSlug} />}
+        {projectSlug ? <CrumbProject projectSlug={projectSlug} /> : null}
       </CrumbDetails>
     </CrumbItem>
   );

+ 2 - 2
static/app/components/replays/breadcrumbs/replayTimeline.tsx

@@ -29,7 +29,7 @@ function ReplayTimeline({}: Props) {
 
   const durationMs = replay.getDurationMs();
   const startTimestampMs = replay.getReplay().started_at.getTime();
-  const userCrumbs = replay.getUserActionCrumbs();
+  const chapterFrames = replay.getChapterFrames();
   const networkFrames = replay.getNetworkFrames();
 
   return (
@@ -49,7 +49,7 @@ function ReplayTimeline({}: Props) {
             </UnderTimestamp>
             <UnderTimestamp paddingTop="26px">
               <ReplayTimelineEvents
-                crumbs={userCrumbs}
+                frames={chapterFrames}
                 durationMs={durationMs}
                 startTimestampMs={startTimestampMs}
                 width={width}

+ 27 - 31
static/app/components/replays/breadcrumbs/replayTimelineEvents.tsx

@@ -1,20 +1,22 @@
 import {css, Theme, useTheme} from '@emotion/react';
 import styled from '@emotion/styled';
+import uniq from 'lodash/uniq';
 
 import BreadcrumbItem from 'sentry/components/replays/breadcrumbs/breadcrumbItem';
 import * as Timeline from 'sentry/components/replays/breadcrumbs/timeline';
-import {getCrumbsByColumn} from 'sentry/components/replays/utils';
+import {getFramesByColumn} from 'sentry/components/replays/utils';
 import {Tooltip} from 'sentry/components/tooltip';
 import {space} from 'sentry/styles/space';
-import {Crumb} from 'sentry/types/breadcrumbs';
+import {getColor} from 'sentry/utils/replays/frame';
 import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
+import type {ReplayFrame} from 'sentry/utils/replays/types';
 import type {Color} from 'sentry/utils/theme';
 
 const NODE_SIZES = [8, 12, 16];
 
 type Props = {
-  crumbs: Crumb[];
   durationMs: number;
+  frames: ReplayFrame[];
   startTimestampMs: number;
   width: number;
   className?: string;
@@ -22,27 +24,22 @@ type Props = {
 
 function ReplayTimelineEvents({
   className,
-  crumbs,
+  frames,
   durationMs,
   startTimestampMs,
   width,
 }: Props) {
-  const markerWidth = crumbs.length < 200 ? 4 : crumbs.length < 500 ? 6 : 10;
+  const markerWidth = frames.length < 200 ? 4 : frames.length < 500 ? 6 : 10;
 
   const totalColumns = Math.floor(width / markerWidth);
-  const eventsByCol = getCrumbsByColumn(
-    startTimestampMs,
-    durationMs,
-    crumbs,
-    totalColumns
-  );
+  const framesByCol = getFramesByColumn(durationMs, frames, totalColumns);
 
   return (
     <Timeline.Columns className={className} totalColumns={totalColumns} remainder={0}>
-      {Array.from(eventsByCol.entries()).map(([column, breadcrumbs]) => (
+      {Array.from(framesByCol.entries()).map(([column, colFrames]) => (
         <EventColumn key={column} column={column}>
           <Event
-            crumbs={breadcrumbs}
+            frames={colFrames}
             markerWidth={markerWidth}
             startTimestampMs={startTimestampMs}
           />
@@ -65,11 +62,11 @@ const EventColumn = styled(Timeline.Col)<{column: number}>`
 `;
 
 function Event({
-  crumbs,
+  frames,
   markerWidth,
   startTimestampMs,
 }: {
-  crumbs: Crumb[];
+  frames: ReplayFrame[];
   markerWidth: number;
   startTimestampMs: number;
 }) {
@@ -77,10 +74,10 @@ function Event({
   const {handleMouseEnter, handleMouseLeave, handleClick} =
     useCrumbHandlers(startTimestampMs);
 
-  const buttons = crumbs.map(crumb => (
+  const buttons = frames.map((frame, i) => (
     <BreadcrumbItem
-      crumb={crumb}
-      key={crumb.id}
+      crumb={frame}
+      key={i}
       onClick={handleClick}
       onMouseEnter={handleMouseEnter}
       onMouseLeave={handleMouseLeave}
@@ -101,15 +98,15 @@ function Event({
   `;
 
   // If we have more than 3 events we want to make sure of showing all the different colors that we have
-  const uniqueColors = Array.from(new Set(crumbs.map(crumb => crumb.color)));
+  const uniqueColors = uniq(frames.map(getColor));
 
   // We just need to stack up to 3 times
-  const crumbCount = Math.min(crumbs.length, 3);
+  const frameCount = Math.min(frames.length, 3);
 
   return (
-    <IconPosition markerWidth={markerWidth}>
+    <IconPosition style={{marginLeft: `${markerWidth / 2}px`}}>
       <IconNodeTooltip title={title} overlayStyle={overlayStyle} isHoverable>
-        <IconNode colors={uniqueColors} crumbCount={crumbCount} />
+        <IconNode colors={uniqueColors} frameCount={frameCount} />
       </IconNodeTooltip>
     </IconPosition>
   );
@@ -121,29 +118,28 @@ const IconNodeTooltip = styled(Tooltip)`
   align-items: center;
 `;
 
-const IconPosition = styled('div')<{markerWidth: number}>`
+const IconPosition = styled('div')`
   position: absolute;
   transform: translate(-50%);
-  margin-left: ${p => p.markerWidth / 2}px;
 `;
 
 const getBackgroundGradient = ({
   colors,
-  crumbCount,
+  frameCount,
   theme,
 }: {
   colors: Color[];
-  crumbCount: number;
+  frameCount: number;
   theme: Theme;
 }) => {
   const c0 = theme[colors[0]] ?? colors[0];
   const c1 = theme[colors[1]] ?? colors[1] ?? c0;
   const c2 = theme[colors[2]] ?? colors[2] ?? c1;
 
-  if (crumbCount === 1) {
+  if (frameCount === 1) {
     return `background: ${c0};`;
   }
-  if (crumbCount === 2) {
+  if (frameCount === 2) {
     return `
       background: ${c0};
       background: radial-gradient(
@@ -163,11 +159,11 @@ const getBackgroundGradient = ({
     );`;
 };
 
-const IconNode = styled('div')<{colors: Color[]; crumbCount: number}>`
+const IconNode = styled('div')<{colors: Color[]; frameCount: number}>`
   grid-column: 1;
   grid-row: 1;
-  width: ${p => NODE_SIZES[p.crumbCount - 1]}px;
-  height: ${p => NODE_SIZES[p.crumbCount - 1]}px;
+  width: ${p => NODE_SIZES[p.frameCount - 1]}px;
+  height: ${p => NODE_SIZES[p.frameCount - 1]}px;
   border-radius: 50%;
   color: ${p => p.theme.white};
   ${getBackgroundGradient}

+ 30 - 30
static/app/components/replays/utils.spec.tsx

@@ -3,27 +3,16 @@ import {
   divide,
   flattenFrames,
   formatTime,
-  getCrumbsByColumn,
+  getFramesByColumn,
   relativeTimeInMs,
   showPlayerTime,
 } from 'sentry/components/replays/utils';
-import {BreadcrumbLevelType, BreadcrumbType, Crumb} from 'sentry/types/breadcrumbs';
+// import {BreadcrumbLevelType, BreadcrumbType, Crumb} from 'sentry/types/breadcrumbs';
+import hydrateErrors from 'sentry/utils/replays/hydrateErrors';
 import hydrateSpans from 'sentry/utils/replays/hydrateSpans';
 
 const SECOND = 1000;
 
-function createCrumb({timestamp}: Pick<Crumb, 'timestamp'>): Crumb {
-  return {
-    timestamp,
-    color: 'white',
-    description: 'crumb description',
-    id: 1,
-    type: BreadcrumbType.DEFAULT,
-    data: {},
-    level: BreadcrumbLevelType.DEBUG,
-  };
-}
-
 describe('formatTime', () => {
   it.each([
     ['seconds', 15 * 1000, '00:15'],
@@ -93,30 +82,42 @@ describe('countColumns', () => {
   });
 });
 
-describe('getCrumbsByColumn', () => {
-  const startTimestampMs = 1649945987326; // milliseconds
+describe('getFramesByColumn', () => {
   const durationMs = 25710; // milliseconds
-  const CRUMB_1 = createCrumb({timestamp: '2022-04-14T14:19:47.326000Z'});
-  const CRUMB_2 = createCrumb({timestamp: '2022-04-14T14:19:49.249000Z'});
-  const CRUMB_3 = createCrumb({timestamp: '2022-04-14T14:19:51.512000Z'});
-  const CRUMB_4 = createCrumb({timestamp: '2022-04-14T14:19:57.326000Z'});
-  const CRUMB_5 = createCrumb({timestamp: '2022-04-14T14:20:13.036000Z'});
+
+  const [CRUMB_1, CRUMB_2, CRUMB_3, CRUMB_4, CRUMB_5] = hydrateErrors(
+    TestStubs.ReplayRecord({
+      started_at: new Date('2022-04-14T14:19:47.326000Z'),
+    }),
+    [
+      TestStubs.Replay.RawReplayError({
+        timestamp: new Date('2022-04-14T14:19:47.326000Z'),
+      }),
+      TestStubs.Replay.RawReplayError({
+        timestamp: new Date('2022-04-14T14:19:49.249000Z'),
+      }),
+      TestStubs.Replay.RawReplayError({
+        timestamp: new Date('2022-04-14T14:19:51.512000Z'),
+      }),
+      TestStubs.Replay.RawReplayError({
+        timestamp: new Date('2022-04-14T14:19:57.326000Z'),
+      }),
+      TestStubs.Replay.RawReplayError({
+        timestamp: new Date('2022-04-14T14:20:13.036000Z'),
+      }),
+    ]
+  );
 
   it('should return an empty list when no crumbs exist', () => {
     const columnCount = 3;
-    const columns = getCrumbsByColumn(startTimestampMs, durationMs, [], columnCount);
+    const columns = getFramesByColumn(durationMs, [], columnCount);
     const expectedEntries = [];
     expect(columns).toEqual(new Map(expectedEntries));
   });
 
   it('should put a crumbs in the first and last buckets', () => {
     const columnCount = 3;
-    const columns = getCrumbsByColumn(
-      startTimestampMs,
-      durationMs,
-      [CRUMB_1, CRUMB_5],
-      columnCount
-    );
+    const columns = getFramesByColumn(durationMs, [CRUMB_1, CRUMB_5], columnCount);
     expect(columns).toEqual(
       new Map([
         [1, [CRUMB_1]],
@@ -128,8 +129,7 @@ describe('getCrumbsByColumn', () => {
   it('should group crumbs by bucket', () => {
     // 6 columns gives is 5s granularity
     const columnCount = 6;
-    const columns = getCrumbsByColumn(
-      startTimestampMs,
+    const columns = getFramesByColumn(
       durationMs,
       [CRUMB_1, CRUMB_2, CRUMB_3, CRUMB_4, CRUMB_5],
       columnCount

+ 11 - 19
static/app/components/replays/utils.tsx

@@ -1,6 +1,5 @@
-import {Crumb} from 'sentry/types/breadcrumbs';
 import {formatSecondsToClock} from 'sentry/utils/formatters';
-import type {SpanFrame} from 'sentry/utils/replays/types';
+import type {ReplayFrame, SpanFrame} from 'sentry/utils/replays/types';
 
 const SECOND = 1000;
 const MINUTE = 60 * SECOND;
@@ -96,43 +95,36 @@ export function countColumns(durationMs: number, width: number, minWidth: number
  * This function groups crumbs into columns based on the number of columns available
  * and the timestamp of the crumb.
  */
-export function getCrumbsByColumn(
-  startTimestampMs: number,
+export function getFramesByColumn(
   durationMs: number,
-  crumbs: Crumb[],
+  frames: ReplayFrame[],
   totalColumns: number
 ) {
   const safeDurationMs = isNaN(durationMs) ? 1 : durationMs;
 
-  const columnCrumbPairs = crumbs.map(breadcrumb => {
-    const {timestamp} = breadcrumb;
-    const timestampMilliSeconds = +new Date(String(timestamp));
-    const sinceStart = isNaN(timestampMilliSeconds)
-      ? 0
-      : timestampMilliSeconds - startTimestampMs;
-
+  const columnFramePairs = frames.map(frame => {
     const columnPositionCalc =
-      Math.floor((sinceStart / safeDurationMs) * (totalColumns - 1)) + 1;
+      Math.floor((frame.offsetMs / safeDurationMs) * (totalColumns - 1)) + 1;
 
     // Should start at minimum in the first column
     const column = Math.max(1, columnPositionCalc);
 
-    return [column, breadcrumb] as [number, Crumb];
+    return [column, frame] as [number, ReplayFrame];
   });
 
-  const crumbsByColumn = columnCrumbPairs.reduce<Map<number, Crumb[]>>(
-    (map, [column, breadcrumb]) => {
+  const framesByColumn = columnFramePairs.reduce<Map<number, ReplayFrame[]>>(
+    (map, [column, frame]) => {
       if (map.has(column)) {
-        map.get(column)?.push(breadcrumb);
+        map.get(column)?.push(frame);
       } else {
-        map.set(column, [breadcrumb]);
+        map.set(column, [frame]);
       }
       return map;
     },
     new Map()
   );
 
-  return crumbsByColumn;
+  return framesByColumn;
 }
 
 type FlattenedSpanRange = {

+ 2 - 1
static/app/components/replays/walker/splitCrumbs.tsx

@@ -7,8 +7,9 @@ import {tn} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {BreadcrumbType, Crumb} from 'sentry/types/breadcrumbs';
 import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
+import type {ReplayFrame} from 'sentry/utils/replays/types';
 
-type MaybeOnClickHandler = null | ((crumb: Crumb) => void);
+type MaybeOnClickHandler = null | ((crumb: Crumb | ReplayFrame) => void);
 
 function getUrl(crumb: undefined | Crumb) {
   if (crumb?.type === BreadcrumbType.NAVIGATION) {

+ 47 - 14
static/app/utils/replays/frame.tsx

@@ -7,24 +7,29 @@ import {BreadcrumbType} from 'sentry/types/breadcrumbs';
 import {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab';
 import {
   BreadcrumbFrame,
-  ErrorFrame,
   LargestContentfulPaintFrame,
+  MultiClickFrame,
   MutationFrame,
-  NavigationFrame,
+  NavFrame,
+  ReplayFrame,
   SlowClickFrame,
   SpanFrame,
 } from 'sentry/utils/replays/types';
 import type {Color} from 'sentry/utils/theme';
 
-export function getColor(frame: BreadcrumbFrame | SpanFrame | ErrorFrame): Color {
+export function getColor(frame: ReplayFrame): Color {
   if ('category' in frame) {
     switch (frame.category) {
+      case 'navigation':
+        return 'green300';
       case 'issue':
         return 'red300';
       case 'ui.slowClickDetected':
         return (frame as SlowClickFrame).data.endReason === 'timeout'
           ? 'red300'
           : 'yellow300';
+      case 'ui.multiClick':
+        return 'red300';
       case 'replay.mutations':
         return 'yellow300';
       case 'ui.click':
@@ -61,17 +66,19 @@ export function getColor(frame: BreadcrumbFrame | SpanFrame | ErrorFrame): Color
  *
  * @deprecated
  */
-export function getBreadcrumbType(
-  frame: BreadcrumbFrame | SpanFrame | ErrorFrame
-): BreadcrumbType {
+export function getBreadcrumbType(frame: ReplayFrame): BreadcrumbType {
   if ('category' in frame) {
     switch (frame.category) {
+      case 'navigation':
+        return BreadcrumbType.NAVIGATION;
       case 'issue':
         return BreadcrumbType.ERROR;
       case 'ui.slowClickDetected':
         return (frame as SlowClickFrame).data.endReason === 'timeout'
           ? BreadcrumbType.ERROR
           : BreadcrumbType.WARNING;
+      case 'ui.multiClick':
+        return BreadcrumbType.ERROR;
       case 'replay.mutations':
         return BreadcrumbType.WARNING;
       case 'ui.click':
@@ -105,7 +112,7 @@ export function getBreadcrumbType(
   }
 }
 
-export function getTitle(frame: BreadcrumbFrame | SpanFrame | ErrorFrame): ReactNode {
+export function getTitle(frame: ReplayFrame): ReactNode {
   if (
     typeof frame.data === 'object' &&
     frame.data !== null &&
@@ -118,10 +125,14 @@ export function getTitle(frame: BreadcrumbFrame | SpanFrame | ErrorFrame): React
   if ('category' in frame) {
     const [type, action] = frame.category.split('.');
     switch (frame.category) {
+      case 'navigation':
+        return 'Navigation';
       case 'ui.slowClickDetected':
         return (frame as SlowClickFrame).data.endReason === 'timeout'
           ? 'Dead Click'
           : 'Slow Click';
+      case 'ui.multiClick':
+        return 'Rage Click';
       case 'replay.mutations':
         return 'Replay';
       case 'ui.click':
@@ -138,7 +149,19 @@ export function getTitle(frame: BreadcrumbFrame | SpanFrame | ErrorFrame): React
   if ('message' in frame) {
     return frame.message; // TODO(replay): Included for backwards compat
   }
-  return frame.description;
+
+  switch (frame.op) {
+    case 'navigation.navigate':
+      return 'Page Load';
+    case 'navigation.reload':
+      return 'Reload';
+    case 'navigation.back_forward':
+      return 'Navigate Back';
+    case 'navigation.push':
+      return 'Navigation';
+    default:
+      return frame.description;
+  }
 }
 
 function stringifyNodeAttributes(node: SlowClickFrame['data']['node']) {
@@ -151,11 +174,12 @@ function stringifyNodeAttributes(node: SlowClickFrame['data']['node']) {
   }`;
 }
 
-export function getDescription(
-  frame: BreadcrumbFrame | SpanFrame | ErrorFrame
-): ReactNode {
+export function getDescription(frame: ReplayFrame): ReactNode {
   if ('category' in frame) {
     switch (frame.category) {
+      case 'navigation':
+        const navFrame = frame as NavFrame;
+        return navFrame.data.to;
       case 'issue':
       case 'ui.slowClickDetected': {
         const slowClickFrame = frame as SlowClickFrame;
@@ -173,6 +197,12 @@ export function getDescription(
               duration: slowClickFrame.data.timeAfterClickMs,
             });
       }
+      case 'ui.multiClick':
+        const multiClickFrame = frame as MultiClickFrame;
+        return tct('Rage clicked [clickCount] times on [selector]', {
+          clickCount: multiClickFrame.data.clickCount,
+          selector: stringifyNodeAttributes(multiClickFrame.data.node),
+        });
       case 'replay.mutations': {
         const mutationFrame = frame as MutationFrame;
         return mutationFrame.data.limit
@@ -186,6 +216,7 @@ export function getDescription(
             );
       }
       case 'ui.click':
+        return frame.message ?? ''; // This should be the selector
       case 'ui.input':
       case 'ui.keyDown':
       case 'ui.blur':
@@ -202,8 +233,7 @@ export function getDescription(
     case 'navigation.reload':
     case 'navigation.back_forward':
     case 'navigation.push':
-      // @ts-expect-error `.to` isn't part of the type
-      return (frame as NavigationFrame).data.to ?? '';
+      return frame.description;
     case 'largest-contentful-paint': {
       const lcpFrame = frame as LargestContentfulPaintFrame;
       if (typeof lcpFrame.data.value === 'number') {
@@ -228,13 +258,16 @@ export function getDescription(
 export function getTabKeyForFrame(frame: BreadcrumbFrame | SpanFrame): TabKey {
   if ('category' in frame) {
     switch (frame.category) {
+      case 'navigation':
+        return TabKey.NETWORK;
       case 'issue':
         return TabKey.ERRORS;
-      case 'ui.slowClickDetected':
       case 'replay.mutations':
       case 'ui.click':
       case 'ui.input':
       case 'ui.keyDown':
+      case 'ui.multiClick':
+      case 'ui.slowClickDetected':
         return TabKey.DOM;
       case 'console':
       default: // Custom breadcrumbs will fall through here

+ 5 - 5
static/app/utils/replays/hooks/useCrumbHandlers.tsx

@@ -5,7 +5,7 @@ import {relativeTimeInMs} from 'sentry/components/replays/utils';
 import {BreadcrumbType, Crumb} from 'sentry/types/breadcrumbs';
 import {getTabKeyForFrame} from 'sentry/utils/replays/frame';
 import useActiveReplayTab from 'sentry/utils/replays/hooks/useActiveReplayTab';
-import {BreadcrumbFrame, SpanFrame} from 'sentry/utils/replays/types';
+import {ReplayFrame} from 'sentry/utils/replays/types';
 import type {NetworkSpan} from 'sentry/views/replays/types';
 
 function useCrumbHandlers(startTimestampMs: number = 0) {
@@ -19,7 +19,7 @@ function useCrumbHandlers(startTimestampMs: number = 0) {
   const {setActiveTab} = useActiveReplayTab();
 
   const mouseEnterCallback = useRef<{
-    id: Crumb | NetworkSpan | BreadcrumbFrame | SpanFrame | null;
+    id: Crumb | NetworkSpan | ReplayFrame | null;
     timeoutId: NodeJS.Timeout | null;
   }>({
     id: null,
@@ -27,7 +27,7 @@ function useCrumbHandlers(startTimestampMs: number = 0) {
   });
 
   const handleMouseEnter = useCallback(
-    (item: Crumb | NetworkSpan | BreadcrumbFrame | SpanFrame) => {
+    (item: Crumb | NetworkSpan | ReplayFrame) => {
       // this debounces the mouseEnter callback in unison with mouseLeave
       // we ensure the pointer remains over the target element before dispatching state events in order to minimize unnecessary renders
       // this helps during scrolling or mouse move events which would otherwise fire in rapid succession slowing down our app
@@ -56,7 +56,7 @@ function useCrumbHandlers(startTimestampMs: number = 0) {
   );
 
   const handleMouseLeave = useCallback(
-    (item: Crumb | NetworkSpan | BreadcrumbFrame | SpanFrame) => {
+    (item: Crumb | NetworkSpan | ReplayFrame) => {
       // if there is a mouseEnter callback queued and we're leaving it we can just cancel the timeout
       if (mouseEnterCallback.current.id === item) {
         if (mouseEnterCallback.current.timeoutId) {
@@ -78,7 +78,7 @@ function useCrumbHandlers(startTimestampMs: number = 0) {
   );
 
   const handleClick = useCallback(
-    (crumb: Crumb | NetworkSpan | BreadcrumbFrame | SpanFrame) => {
+    (crumb: Crumb | NetworkSpan | ReplayFrame) => {
       if ('offsetMs' in crumb) {
         const frame = crumb; // Finding `offsetMs` means we have a frame, not a crumb or span
 

+ 1 - 4
static/app/utils/replays/hydrateBreadcrumbs.tsx

@@ -3,12 +3,9 @@ import invariant from 'invariant';
 import {BreadcrumbType} from 'sentry/types/breadcrumbs';
 import isValidDate from 'sentry/utils/date/isValidDate';
 import type {BreadcrumbFrame, RawBreadcrumbFrame} from 'sentry/utils/replays/types';
+import {isBreadcrumbFrame} from 'sentry/utils/replays/types';
 import type {ReplayRecord} from 'sentry/views/replays/types';
 
-function isBreadcrumbFrame(frame: BreadcrumbFrame | undefined): frame is BreadcrumbFrame {
-  return frame !== undefined;
-}
-
 export default function hydrateBreadcrumbs(
   replayRecord: ReplayRecord,
   breadcrumbFrames: RawBreadcrumbFrame[]

+ 1 - 4
static/app/utils/replays/hydrateErrors.tsx

@@ -2,12 +2,9 @@ import invariant from 'invariant';
 
 import isValidDate from 'sentry/utils/date/isValidDate';
 import type {ErrorFrame, RawReplayError} from 'sentry/utils/replays/types';
+import {isErrorFrame} from 'sentry/utils/replays/types';
 import type {ReplayRecord} from 'sentry/views/replays/types';
 
-function isErrorFrame(frame: ErrorFrame | undefined): frame is ErrorFrame {
-  return frame !== undefined;
-}
-
 export default function hydrateErrors(
   replayRecord: ReplayRecord,
   errors: RawReplayError[]

Some files were not shown because too many files changed in this diff