Browse Source

ref(replay): Refactor replay breadcrumbs component to use *Frame types (#54056)

<!-- Describe your PR here. -->
Ryan Albrecht 1 year ago
parent
commit
04a289b838

+ 97 - 160
static/app/utils/replays/getReplayEvent.spec.tsx

@@ -1,238 +1,175 @@
-import {BreadcrumbLevelType, BreadcrumbType, Crumb} from 'sentry/types/breadcrumbs';
 import {
   getNextReplayFrame,
-  getPrevReplayEvent,
+  getPrevReplayFrame,
 } from 'sentry/utils/replays/getReplayEvent';
 import hydrateBreadcrumbs from 'sentry/utils/replays/hydrateBreadcrumbs';
 
-const START_TIMESTAMP_SEC = 1651693622.951;
-const CURRENT_TIME_MS = 15000;
-
-function createCrumbs(): Crumb[] {
-  return [
-    {
-      color: 'gray300',
-      data: {url: 'https://dev.getsentry.net:7999/organizations/sentry/performance/'},
-      description: 'Default',
-      id: 0,
-      level: BreadcrumbLevelType.INFO,
-      message: 'Start recording',
-      timestamp: '2022-05-11T22:41:32.002Z',
-      type: BreadcrumbType.INIT,
-    },
-    {
-      category: 'ui.click',
-      color: 'purple300',
-      data: undefined,
-      description: 'User Action',
-      event_id: null,
-      id: 3,
-      level: BreadcrumbLevelType.INFO,
-      message: 'div.App > section.padding-b-2 > div.makeStyles-search-input-2 > input',
-      timestamp: '2022-05-04T19:47:08.085000Z',
-      type: BreadcrumbType.UI,
-    },
-    {
-      category: 'ui.input',
-      color: 'purple300',
-      data: undefined,
-      description: 'User Action',
-      event_id: null,
-      id: 4,
-      level: BreadcrumbLevelType.INFO,
-      message: 'div.App > section.padding-b-2 > div.makeStyles-search-input-2 > input',
-      timestamp: '2022-05-04T19:47:11.086000Z',
-      type: BreadcrumbType.UI,
-    },
-    {
-      category: 'ui.click',
-      color: 'purple300',
-      data: undefined,
-      description: 'User Action',
-      event_id: null,
-      id: 20,
-      level: BreadcrumbLevelType.INFO,
-      message: 'div.App > section.padding-b-2 > div.makeStyles-search-input-2 > input',
-      timestamp: '2022-05-04T19:47:52.915000Z',
-      type: BreadcrumbType.UI,
-    },
-    {
-      category: 'navigation',
-      color: 'green300',
-      data: {
-        from: '/organizations/sentry/user-feedback/?project=6380506',
-        to: '/organizations/sentry/issues/',
-      },
-      description: 'Navigation',
-      event_id: null,
-      id: 166,
-      level: BreadcrumbLevelType.INFO,
-      message: undefined,
-      timestamp: '2022-05-04T19:47:59.915000Z',
-      type: BreadcrumbType.NAVIGATION,
-    },
-  ];
-}
+const frames = hydrateBreadcrumbs(
+  TestStubs.ReplayRecord({
+    started_at: new Date('2022-05-04T19:41:30.00Z'),
+  }),
+  [
+    TestStubs.Replay.ClickFrame({
+      timestamp: new Date('2022-05-04T19:41:32.002Z'),
+      message: 'index 0',
+    }),
+    TestStubs.Replay.ClickFrame({
+      timestamp: new Date('2022-05-04T19:47:08.085000Z'),
+      message: 'index 1',
+    }),
+    TestStubs.Replay.ClickFrame({
+      timestamp: new Date('2022-05-04T19:47:11.086000Z'),
+      message: 'index 2',
+    }),
+    TestStubs.Replay.ClickFrame({
+      timestamp: new Date('2022-05-04T19:47:52.915000Z'),
+      message: 'index 3',
+    }),
+    TestStubs.Replay.ClickFrame({
+      timestamp: new Date('2022-05-04T19:47:59.915000Z'),
+      message: 'index 4',
+    }),
+  ]
+);
+
+const CURRENT_OFFSET_MS = frames[0].offsetMs + 15000;
 
 describe('getNextReplayFrame', () => {
-  const frames = hydrateBreadcrumbs(TestStubs.ReplayRecord(), [
-    TestStubs.Replay.ClickFrame({timestamp: new Date('2022-05-11T22:41:32.002Z')}),
-    TestStubs.Replay.ClickFrame({timestamp: new Date('2022-05-04T19:47:08.085000Z')}),
-    TestStubs.Replay.ClickFrame({timestamp: new Date('2022-05-04T19:47:11.086000Z')}),
-    TestStubs.Replay.ClickFrame({timestamp: new Date('2022-05-04T19:47:52.915000Z')}),
-    TestStubs.Replay.ClickFrame({timestamp: new Date('2022-05-04T19:47:59.915000Z')}),
-  ]);
-
-  TestStubs.Replay.ClickEvent;
   it('should return the next crumb', () => {
-    const results = getNextReplayFrame({
+    const result = getNextReplayFrame({
       frames,
-      targetOffsetMs: CURRENT_TIME_MS,
+      targetOffsetMs: CURRENT_OFFSET_MS,
     });
 
-    expect(results).toEqual(frames[1]);
+    expect(result).toEqual(frames[1]);
   });
 
   it('should return the next crumb when the the list is not sorted', () => {
     const [one, two, three, four, five] = frames;
-    const results = getNextReplayFrame({
+    const result = getNextReplayFrame({
       frames: [one, four, five, three, two],
-      targetOffsetMs: CURRENT_TIME_MS,
+      targetOffsetMs: CURRENT_OFFSET_MS,
     });
 
-    expect(results).toEqual(frames[1]);
+    expect(result).toEqual(frames[1]);
   });
 
   it('should return undefined when there are no crumbs', () => {
-    const results = getNextReplayFrame({
+    const result = getNextReplayFrame({
       frames: [],
-      targetOffsetMs: CURRENT_TIME_MS,
+      targetOffsetMs: CURRENT_OFFSET_MS,
     });
 
-    expect(results).toBeUndefined();
+    expect(result).toBeUndefined();
+  });
+
+  it('should return the first crumb when the timestamp is earlier than any crumbs', () => {
+    const result = getNextReplayFrame({
+      frames,
+      targetOffsetMs: -1,
+    });
+
+    expect(result).toEqual(frames[0]);
   });
 
   it('should return undefined when the timestamp is later than any crumbs', () => {
-    const results = getNextReplayFrame({
+    const result = getNextReplayFrame({
       frames,
       targetOffsetMs: 99999999999,
     });
 
-    expect(results).toBeUndefined();
+    expect(result).toBeUndefined();
   });
 
-  it('should return the crumb after when a timestamp exactly matches', () => {
-    const exactTime = 8135;
-    const results = getNextReplayFrame({
+  it('should return the next frame when a timestamp exactly matches', () => {
+    const exactTime = frames[1].offsetMs;
+    const result = getNextReplayFrame({
       frames,
       targetOffsetMs: exactTime,
+      allowExact: false,
     });
 
-    expect(results).toEqual(frames[1]);
+    expect(result).toEqual(frames[2]);
   });
 
-  it('should return the crumb if timestamps exactly match and allowMatch is enabled', () => {
-    const exactTime = 8135;
-    const results = getNextReplayFrame({
+  it('should return the same frame if timestamps exactly match and allowMatch is enabled', () => {
+    const exactTime = frames[1].offsetMs;
+    const result = getNextReplayFrame({
       frames,
       targetOffsetMs: exactTime,
+      allowExact: true,
     });
 
-    expect(results).toEqual(frames[1]);
+    expect(result).toEqual(frames[1]);
   });
 });
 
-describe('getPrevReplayEvent', () => {
-  it('should return the previous crumb even if the timestamp is closer to the next crumb', () => {
-    const crumbs = createCrumbs();
-    const results = getPrevReplayEvent({
-      itemLookup: crumbs
-        .map(({timestamp}, i) => [+new Date(timestamp || ''), i])
-        .sort(([a], [b]) => a - b),
-      items: crumbs,
-      targetTimestampMs: START_TIMESTAMP_SEC * 1000 + CURRENT_TIME_MS,
+describe('getPrevReplayFrame', () => {
+  it('should return the previous crumb', () => {
+    const result = getPrevReplayFrame({
+      frames,
+      targetOffsetMs: CURRENT_OFFSET_MS,
     });
 
-    expect(results?.id).toEqual(4);
+    expect(result).toEqual(frames[0]);
   });
 
   it('should return the previous crumb when the list is not sorted', () => {
-    const [one, two, three, four, five] = createCrumbs();
-    const items = [one, four, five, three, two];
-    const results = getPrevReplayEvent({
-      itemLookup: items
-        .map(({timestamp}, i) => [+new Date(timestamp || ''), i])
-        .sort(([a], [b]) => a - b),
-      items,
-      targetTimestampMs: START_TIMESTAMP_SEC * 1000 + CURRENT_TIME_MS,
+    const [one, two, three, four, five] = frames;
+    const result = getPrevReplayFrame({
+      frames: [one, four, five, three, two],
+      targetOffsetMs: CURRENT_OFFSET_MS,
     });
 
-    expect(results?.id).toEqual(4);
+    expect(result).toEqual(frames[0]);
   });
 
   it('should return undefined when there are no crumbs', () => {
-    const crumbs = [];
-    const results = getPrevReplayEvent({
-      itemLookup: crumbs
-        .map(({timestamp}, i) => [+new Date(timestamp || ''), i])
-        .sort(([a], [b]) => a - b),
-      items: crumbs,
-      targetTimestampMs: START_TIMESTAMP_SEC * 1000 + CURRENT_TIME_MS,
+    const result = getPrevReplayFrame({
+      frames: [],
+      targetOffsetMs: CURRENT_OFFSET_MS,
     });
 
-    expect(results).toBeUndefined();
+    expect(result).toBeUndefined();
   });
 
   it('should return undefined when the timestamp is earlier than any crumbs', () => {
-    const crumbs = createCrumbs();
-    const results = getPrevReplayEvent({
-      itemLookup: crumbs
-        .map(({timestamp}, i) => [+new Date(timestamp || ''), i])
-        .sort(([a], [b]) => a - b),
-      items: crumbs,
-      targetTimestampMs: START_TIMESTAMP_SEC * 1000 - CURRENT_TIME_MS,
+    const result = getPrevReplayFrame({
+      frames,
+      targetOffsetMs: -1,
     });
 
-    expect(results).toBeUndefined();
+    expect(result).toBeUndefined();
   });
 
   it('should return the last crumb if timestamp is later than any crumb', () => {
-    const crumbs = createCrumbs();
-    const results = getPrevReplayEvent({
-      itemLookup: crumbs
-        .map(({timestamp}, i) => [+new Date(timestamp || ''), i])
-        .sort(([a], [b]) => a - b),
-      items: crumbs,
-      targetTimestampMs: 1652308892002 + 10,
+    const result = getPrevReplayFrame({
+      frames,
+      targetOffsetMs: 99999999999,
     });
 
-    expect(results?.id).toEqual(0);
+    expect(result).toEqual(frames[4]);
   });
 
-  it('should return the last crumb if timestamp is exactly the last crumb', () => {
-    const crumbs = createCrumbs();
-    const results = getPrevReplayEvent({
-      itemLookup: crumbs
-        .map(({timestamp}, i) => [+new Date(timestamp || ''), i])
-        .sort(([a], [b]) => a - b),
-      items: crumbs,
-      targetTimestampMs: 1652308892002,
+  it('should return the prev frame if timestamp exactly matches', () => {
+    const exactTime = frames[1].offsetMs;
+    const result = getPrevReplayFrame({
+      frames,
+      targetOffsetMs: exactTime,
+      allowExact: false,
     });
 
-    expect(results?.id).toEqual(0);
+    expect(result).toEqual(frames[0]);
   });
 
-  it('should return the crumb if timestamps exactly match', () => {
-    const crumbs = createCrumbs();
-    const exactCrumbTime = 8135;
-    const results = getPrevReplayEvent({
-      itemLookup: crumbs
-        .map(({timestamp}, i) => [+new Date(timestamp || ''), i])
-        .sort(([a], [b]) => a - b),
-      items: crumbs,
-      targetTimestampMs: START_TIMESTAMP_SEC * 1000 + exactCrumbTime,
+  it('should return the same frame if timestamps exactly match and allowExact is enabled', () => {
+    const exactTime = frames[1].offsetMs;
+    const result = getPrevReplayFrame({
+      frames,
+      targetOffsetMs: exactTime,
+      allowExact: true,
     });
 
-    expect(results?.id).toEqual(4);
+    expect(result).toEqual(frames[1]);
   });
 });

+ 28 - 25
static/app/utils/replays/getReplayEvent.tsx

@@ -1,31 +1,30 @@
-import sortedIndexBy from 'lodash/sortedIndexBy';
-
-import type {Crumb} from 'sentry/types/breadcrumbs';
 import type {ReplayFrame} from 'sentry/utils/replays/types';
-import type {ReplaySpan} from 'sentry/views/replays/types';
 
-export function getPrevReplayEvent<T extends ReplaySpan | Crumb>({
-  itemLookup,
-  items,
-  targetTimestampMs,
+export function getPrevReplayFrame({
+  frames,
+  targetOffsetMs,
+  allowExact = false,
 }: {
-  items: T[];
-  targetTimestampMs: number;
-  itemLookup?: number[][];
+  frames: ReplayFrame[];
+  targetOffsetMs: number;
+  allowExact?: boolean;
 }) {
-  if (!itemLookup || !itemLookup.length) {
-    return undefined;
-  }
-
-  const index = sortedIndexBy(itemLookup, [targetTimestampMs], o => o[0]);
-  if (index !== undefined && index > 0) {
-    const ts = itemLookup[Math.min(index, itemLookup.length - 1)][0];
-    return items[
-      itemLookup[ts === targetTimestampMs ? index : Math.max(0, index - 1)][1]
-    ];
-  }
-
-  return undefined;
+  return frames.reduce<ReplayFrame | undefined>((found, item) => {
+    if (
+      item.offsetMs > targetOffsetMs ||
+      (!allowExact && item.offsetMs === targetOffsetMs)
+    ) {
+      return found;
+    }
+    if (
+      (allowExact && item.offsetMs === targetOffsetMs) ||
+      !found ||
+      item.offsetMs > found.offsetMs
+    ) {
+      return item;
+    }
+    return found;
+  }, undefined);
 }
 
 export function getNextReplayFrame({
@@ -44,7 +43,11 @@ export function getNextReplayFrame({
     ) {
       return found;
     }
-    if (!found || item.timestampMs < found.timestampMs) {
+    if (
+      (allowExact && item.offsetMs === targetOffsetMs) ||
+      !found ||
+      item.offsetMs < found.offsetMs
+    ) {
       return item;
     }
     return found;

+ 33 - 31
static/app/views/replays/detail/breadcrumbs/breadcrumbRow.tsx

@@ -1,14 +1,14 @@
-import {CSSProperties, memo, MouseEvent, useCallback, useMemo} from 'react';
+import {CSSProperties, MouseEvent, useCallback} from 'react';
+import styled from '@emotion/styled';
 import classNames from 'classnames';
 
 import BreadcrumbItem from 'sentry/components/replays/breadcrumbs/breadcrumbItem';
 import {useReplayContext} from 'sentry/components/replays/replayContext';
-import {relativeTimeInMs} from 'sentry/components/replays/utils';
-import type {Crumb} from 'sentry/types/breadcrumbs';
 import useCrumbHandlers from 'sentry/utils/replays/hooks/useCrumbHandlers';
+import type {ReplayFrame} from 'sentry/utils/replays/types';
 
 interface Props {
-  breadcrumb: Crumb;
+  frame: ReplayFrame;
   index: number;
   onDimensionChange: (
     index: number,
@@ -23,7 +23,7 @@ interface Props {
 }
 
 function BreadcrumbRow({
-  breadcrumb,
+  frame,
   expandPaths,
   index,
   onDimensionChange,
@@ -35,46 +35,48 @@ function BreadcrumbRow({
   const {handleMouseEnter, handleMouseLeave, handleClick} =
     useCrumbHandlers(startTimestampMs);
 
-  const onClickTimestamp = useCallback(
-    () => handleClick(breadcrumb),
-    [handleClick, breadcrumb]
-  );
+  const onClickTimestamp = useCallback(() => handleClick(frame), [handleClick, frame]);
   const onMouseEnter = useCallback(
-    () => handleMouseEnter(breadcrumb),
-    [handleMouseEnter, breadcrumb]
+    () => handleMouseEnter(frame),
+    [handleMouseEnter, frame]
   );
   const onMouseLeave = useCallback(
-    () => handleMouseLeave(breadcrumb),
-    [handleMouseLeave, breadcrumb]
-  );
-
-  const crumbTime = useMemo(
-    () => relativeTimeInMs(new Date(breadcrumb.timestamp || ''), startTimestampMs),
-    [breadcrumb.timestamp, startTimestampMs]
+    () => handleMouseLeave(frame),
+    [handleMouseLeave, frame]
   );
 
-  const hasOccurred = currentTime >= crumbTime;
-  const isBeforeHover = currentHoverTime === undefined || currentHoverTime >= crumbTime;
+  const hasOccurred = currentTime >= frame.offsetMs;
+  const isBeforeHover =
+    currentHoverTime === undefined || currentHoverTime >= frame.offsetMs;
 
   return (
-    <BreadcrumbItem
-      index={index}
-      crumb={breadcrumb}
+    <StyledTimeBorder
       className={classNames({
         beforeCurrentTime: hasOccurred,
         afterCurrentTime: !hasOccurred,
         beforeHoverTime: currentHoverTime !== undefined ? isBeforeHover : undefined,
         afterHoverTime: currentHoverTime !== undefined ? !isBeforeHover : undefined,
       })}
-      onClick={onClickTimestamp}
-      onMouseEnter={onMouseEnter}
-      onMouseLeave={onMouseLeave}
-      startTimestampMs={startTimestampMs}
       style={style}
-      expandPaths={expandPaths}
-      onDimensionChange={onDimensionChange}
-    />
+    >
+      <BreadcrumbItem
+        index={index}
+        crumb={frame}
+        onClick={onClickTimestamp}
+        onMouseEnter={onMouseEnter}
+        onMouseLeave={onMouseLeave}
+        startTimestampMs={startTimestampMs}
+        expandPaths={expandPaths}
+        onDimensionChange={onDimensionChange}
+      />
+    </StyledTimeBorder>
   );
 }
 
-export default memo(BreadcrumbRow);
+const StyledTimeBorder = styled('div')`
+  /* Overridden in TabItemContainer, depending on *CurrentTime and *HoverTime classes */
+  border-top: 1px solid transparent;
+  border-bottom: 1px solid transparent;
+`;
+
+export default BreadcrumbRow;

+ 15 - 64
static/app/views/replays/detail/breadcrumbs/index.tsx

@@ -1,25 +1,25 @@
-import {memo, useMemo, useRef} from 'react';
+import {useMemo, useRef} from 'react';
 import {
   AutoSizer,
   CellMeasurer,
   List as ReactVirtualizedList,
   ListRowProps,
 } from 'react-virtualized';
-import styled from '@emotion/styled';
 
 import Placeholder from 'sentry/components/placeholder';
 import {t} from 'sentry/locale';
-import type {Crumb} from 'sentry/types/breadcrumbs';
+import type {ReplayFrame} from 'sentry/utils/replays/types';
 import BreadcrumbRow from 'sentry/views/replays/detail/breadcrumbs/breadcrumbRow';
 import useScrollToCurrentItem from 'sentry/views/replays/detail/breadcrumbs/useScrollToCurrentItem';
 import FluidHeight from 'sentry/views/replays/detail/layout/fluidHeight';
 import NoRowRenderer from 'sentry/views/replays/detail/noRowRenderer';
+import TabItemContainer from 'sentry/views/replays/detail/tabItemContainer';
 import useVirtualizedList from 'sentry/views/replays/detail/useVirtualizedList';
 
 import useVirtualizedInspector from '../useVirtualizedInspector';
 
 type Props = {
-  breadcrumbs: undefined | Crumb[];
+  frames: undefined | ReplayFrame[];
   startTimestampMs: number;
 };
 
@@ -30,7 +30,7 @@ const cellMeasurer = {
   minHeight: 53,
 };
 
-function Breadcrumbs({breadcrumbs, startTimestampMs}: Props) {
+function Breadcrumbs({frames, startTimestampMs}: Props) {
   const listRef = useRef<ReactVirtualizedList>(null);
   // Keep a reference of object paths that are expanded (via <ObjectInspector>)
   // by log row, so they they can be restored as the Console pane is scrolling.
@@ -41,7 +41,7 @@ function Breadcrumbs({breadcrumbs, startTimestampMs}: Props) {
   // re-render when items are expanded/collapsed, though it may work in state as well.
   const expandPathsRef = useRef(new Map<number, Set<string>>());
 
-  const deps = useMemo(() => [breadcrumbs], [breadcrumbs]);
+  const deps = useMemo(() => [frames], [frames]);
   const {cache, updateList} = useVirtualizedList({
     cellMeasurer,
     ref: listRef,
@@ -54,13 +54,12 @@ function Breadcrumbs({breadcrumbs, startTimestampMs}: Props) {
   });
 
   useScrollToCurrentItem({
-    breadcrumbs,
+    frames,
     ref: listRef,
-    startTimestampMs,
   });
 
   const renderRow = ({index, key, style, parent}: ListRowProps) => {
-    const item = (breadcrumbs || [])[index];
+    const item = (frames || [])[index];
 
     return (
       <CellMeasurer
@@ -72,7 +71,7 @@ function Breadcrumbs({breadcrumbs, startTimestampMs}: Props) {
       >
         <BreadcrumbRow
           index={index}
-          breadcrumb={item}
+          frame={item}
           startTimestampMs={startTimestampMs}
           style={style}
           expandPaths={Array.from(expandPathsRef.current?.get(index) || [])}
@@ -84,21 +83,21 @@ function Breadcrumbs({breadcrumbs, startTimestampMs}: Props) {
 
   return (
     <FluidHeight>
-      <BreadcrumbContainer>
-        {breadcrumbs ? (
+      <TabItemContainer>
+        {frames ? (
           <AutoSizer onResize={updateList}>
             {({height, width}) => (
               <ReactVirtualizedList
                 deferredMeasurementCache={cache}
                 height={height}
                 noRowsRenderer={() => (
-                  <NoRowRenderer unfilteredItems={breadcrumbs} clearSearchTerm={() => {}}>
+                  <NoRowRenderer unfilteredItems={frames} clearSearchTerm={() => {}}>
                     {t('No breadcrumbs recorded')}
                   </NoRowRenderer>
                 )}
                 overscanRowCount={5}
                 ref={listRef}
-                rowCount={breadcrumbs.length}
+                rowCount={frames.length}
                 rowHeight={cache.rowHeight}
                 rowRenderer={renderRow}
                 width={width}
@@ -108,57 +107,9 @@ function Breadcrumbs({breadcrumbs, startTimestampMs}: Props) {
         ) : (
           <Placeholder height="100%" />
         )}
-      </BreadcrumbContainer>
+      </TabItemContainer>
     </FluidHeight>
   );
 }
 
-const BreadcrumbContainer = styled('div')`
-  position: relative;
-  height: 100%;
-  overflow: hidden;
-  border: 1px solid ${p => p.theme.border};
-  border-radius: ${p => p.theme.borderRadius};
-
-  .beforeHoverTime + .afterHoverTime:before {
-    background-color: ${p => p.theme.surface200};
-    border-top: 1px solid ${p => p.theme.purple200};
-    content: '';
-    left: 0;
-    position: absolute;
-    top: 0;
-    width: 999999999%;
-  }
-
-  .beforeHoverTime:last-child:before {
-    background-color: ${p => p.theme.surface200};
-    border-bottom: 1px solid ${p => p.theme.purple200};
-    content: '';
-    right: 0;
-    position: absolute;
-    bottom: 0;
-    width: 999999999%;
-  }
-
-  .beforeCurrentTime + .afterCurrentTime:before {
-    background-color: ${p => p.theme.purple100};
-    border-top: 1px solid ${p => p.theme.purple300};
-    content: '';
-    left: 0;
-    position: absolute;
-    top: 0;
-    width: 999999999%;
-  }
-
-  .beforeCurrentTime:last-child:before {
-    background-color: ${p => p.theme.purple100};
-    border-bottom: 1px solid ${p => p.theme.purple300};
-    content: '';
-    right: 0;
-    position: absolute;
-    bottom: 0;
-    width: 999999999%;
-  }
-`;
-
-export default memo(Breadcrumbs);
+export default Breadcrumbs;

+ 15 - 25
static/app/views/replays/detail/breadcrumbs/useScrollToCurrentItem.tsx

@@ -2,41 +2,31 @@ import {RefObject, useEffect, useMemo} from 'react';
 import type {List as ReactVirtualizedList} from 'react-virtualized';
 
 import {useReplayContext} from 'sentry/components/replays/replayContext';
-import type {Crumb} from 'sentry/types/breadcrumbs';
-import {getPrevReplayEvent} from 'sentry/utils/replays/getReplayEvent';
+import {getPrevReplayFrame} from 'sentry/utils/replays/getReplayEvent';
+import type {ReplayFrame} from 'sentry/utils/replays/types';
 
-type Opts = {
-  breadcrumbs: undefined | Crumb[];
+interface Opts {
+  frames: undefined | ReplayFrame[];
   ref: RefObject<ReactVirtualizedList>;
-  startTimestampMs: number;
-};
-function useScrollToCurrentItem({breadcrumbs, ref, startTimestampMs}: Opts) {
-  const {currentTime} = useReplayContext();
-  const itemLookup = useMemo(
-    () =>
-      breadcrumbs &&
-      breadcrumbs
-        .map(({timestamp}, i) => [+new Date(timestamp || ''), i])
-        .sort(([a], [b]) => a - b),
-    [breadcrumbs]
-  );
+}
 
-  const current = useMemo(
+function useScrollToCurrentItem({frames, ref}: Opts) {
+  const {currentTime} = useReplayContext();
+  const currentItem = useMemo(
     () =>
-      getPrevReplayEvent({
-        itemLookup,
-        items: breadcrumbs || [],
-        targetTimestampMs: startTimestampMs + currentTime,
+      getPrevReplayFrame({
+        frames: frames || [],
+        targetOffsetMs: currentTime,
       }),
-    [itemLookup, breadcrumbs, currentTime, startTimestampMs]
+    [frames, currentTime]
   );
 
   useEffect(() => {
-    if (ref.current && current) {
-      const index = breadcrumbs?.findIndex(crumb => crumb.id === current.id);
+    if (ref.current && currentItem) {
+      const index = frames?.findIndex(frame => frame === currentItem);
       ref.current?.scrollToRow(index ? index + 1 : undefined);
     }
-  }, [breadcrumbs, current, ref]);
+  }, [frames, currentItem, ref]);
 }
 
 export default useScrollToCurrentItem;

+ 1 - 1
static/app/views/replays/detail/layout/sidebarArea.tsx

@@ -14,7 +14,7 @@ function SidebarArea() {
     default:
       return (
         <Breadcrumbs
-          breadcrumbs={replay?.getNonConsoleCrumbs()}
+          frames={replay?.getChapterFrames()}
           startTimestampMs={replay?.getReplay()?.started_at?.getTime() || 0}
         />
       );