Browse Source

feat(replays): Adds some spans as breadcrumbs (#35713)

<!-- Describe your PR here. -->

Creates breadcrumbs from specific spans (e.g. LCP and navigation). Refactors components to support this + some cleanups as well.

![image](https://user-images.githubusercontent.com/79684/174094351-26d891be-c407-4099-874a-c35a50a2e52e.png)


Closes https://github.com/getsentry/sentry/issues/35511
Closes https://github.com/getsentry/sentry/issues/35681
Billy Vong 2 years ago
parent
commit
8595efca32

+ 0 - 67
static/app/components/replays/actionCategory.tsx

@@ -1,67 +0,0 @@
-import {memo} from 'react';
-import styled from '@emotion/styled';
-
-import Tooltip from 'sentry/components/tooltip';
-import {t} from 'sentry/locale';
-import {BreadcrumbType, Crumb} from 'sentry/types/breadcrumbs';
-
-type Props = {
-  action: Crumb;
-};
-
-type ActionCategoryInfo = {
-  description: string;
-  title: string;
-};
-
-function getActionCategoryInfo(crumb: Crumb): ActionCategoryInfo {
-  switch (crumb.type) {
-    case BreadcrumbType.USER:
-    case BreadcrumbType.UI:
-      return {
-        title: t('UI Click'),
-        description: `${crumb.category}: ${crumb.message}`,
-      };
-    case BreadcrumbType.NAVIGATION:
-      return {
-        title: t('Navigation'),
-        description: `${crumb.category}: ${crumb.data?.from ?? ''} => ${
-          crumb.data?.to ?? ''
-        }`,
-      };
-    case BreadcrumbType.ERROR:
-      return {
-        title: t('Error'),
-        description: `${crumb.data?.type}: ${crumb.data?.value}`,
-      };
-    case BreadcrumbType.INIT:
-      return {
-        title: t('Replay Start'),
-        description: crumb.data?.url,
-      };
-    default:
-      return {
-        title: t('Default'),
-        description: '',
-      };
-  }
-}
-
-const ActionCategory = memo(({action}: Props) => {
-  const {title, description} = getActionCategoryInfo(action);
-
-  return (
-    <Tooltip title={description} disabled={!description} skipWrapper disableForVisualTest>
-      <Value>{title}</Value>
-    </Tooltip>
-  );
-});
-
-const Value = styled('div')`
-  color: ${p => p.theme.headingColor};
-  font-size: ${p => p.theme.fontSizeMedium};
-  font-weight: 400;
-  text-transform: capitalize;
-`;
-
-export default ActionCategory;

+ 5 - 8
static/app/components/replays/breadcrumbs/replayTimelineEvents.tsx

@@ -10,6 +10,8 @@ import type {Color} from 'sentry/utils/theme';
 
 import {getCrumbsByColumn} from '../utils';
 
+import {getDescription, getTitle} from './utils';
+
 const EVENT_STICK_MARKER_WIDTH = 4;
 
 type Props = {
@@ -58,15 +60,10 @@ const EventColumn = styled(Timeline.Col)<{column: number}>`
 
 function getCrumbDetail(crumb: Crumb) {
   switch (crumb.type) {
-    case BreadcrumbType.USER:
-    case BreadcrumbType.UI:
-      return crumb.message ?? crumb.description;
-    case BreadcrumbType.NAVIGATION:
-      return crumb.data?.to ?? crumb.description;
     case BreadcrumbType.ERROR:
-      return `${crumb.data?.type}: ${crumb.data?.value}`;
+      return `${crumb.data?.label}: ${crumb.message}`;
     default:
-      return crumb.message;
+      return getDescription(crumb);
   }
 }
 
@@ -87,7 +84,7 @@ function Event({crumbs}: {crumbs: Crumb[]; className?: string}) {
       {crumbs.map(crumb => (
         <HoverListItem key={crumb.id}>
           <Type type={crumb.type} color={crumb.color} description={crumb.description} />
-          <small>{getCrumbDetail(crumb)}</small>
+          <small>{getCrumbDetail(crumb) || getTitle(crumb)}</small>
         </HoverListItem>
       ))}
     </HoverList>

+ 36 - 0
static/app/components/replays/breadcrumbs/utils.tsx

@@ -0,0 +1,36 @@
+import {BreadcrumbType, Crumb} from 'sentry/types/breadcrumbs';
+
+/**
+ * Generate breadcrumb descriptions based on type
+ */
+export function getDescription(crumb: Crumb) {
+  switch (crumb.type) {
+    case BreadcrumbType.NAVIGATION:
+      return `${crumb.data?.from ? `${crumb.data?.from} => ` : ''}${
+        crumb.data?.to ?? ''
+      }`;
+    default:
+      return crumb.message || '';
+  }
+}
+
+/**
+ * Get title of breadcrumb
+ */
+export function getTitle(crumb: Crumb) {
+  const [type, action] = crumb.category?.split('.') || [];
+
+  // Supports replay specific breadcrumbs
+  if (crumb.data && 'label' in crumb.data) {
+    return crumb.data.label;
+  }
+
+  return `${type === 'ui' ? 'User' : type} ${action || ''}`;
+}
+
+/**
+ * Generate breadcrumb title + descriptions
+ */
+export function getDetails(crumb: Crumb) {
+  return {title: getTitle(crumb), description: getDescription(crumb)};
+}

+ 2 - 2
static/app/components/replays/playerRelativeTime.tsx

@@ -31,8 +31,8 @@ const PlayerRelativeTime = ({relativeTime, timestamp}: Props) => {
 
 const Value = styled('p')`
   color: ${p => p.theme.subText};
-  font-size: ${p => p.theme.fontSizeSmall};
-  font-weight: normal;
+  font-size: 0.7em;
+  font-family: ${p => p.theme.text.familyMono};
 `;
 
 export default PlayerRelativeTime;

+ 68 - 30
static/app/utils/replays/replayDataUtils.tsx

@@ -2,7 +2,12 @@ import first from 'lodash/first';
 
 import {transformCrumbs} from 'sentry/components/events/interfaces/breadcrumbs/utils';
 import {t} from 'sentry/locale';
-import type {BreadcrumbTypeDefault, Crumb, RawCrumb} from 'sentry/types/breadcrumbs';
+import type {
+  BreadcrumbTypeDefault,
+  BreadcrumbTypeNavigation,
+  Crumb,
+  RawCrumb,
+} from 'sentry/types/breadcrumbs';
 import {BreadcrumbLevelType, BreadcrumbType} from 'sentry/types/breadcrumbs';
 import {Event} from 'sentry/types/event';
 import type {
@@ -15,30 +20,15 @@ import type {
 export function rrwebEventListFactory(
   startTimestampMS: number,
   endTimestampMS: number,
-  rawSpanData: ReplaySpan[],
   rrwebEvents: RecordingEvent[]
 ) {
-  const highlights = rawSpanData
-    .filter(({op, data}) => op === 'largest-contentful-paint' && data?.nodeId > 0)
-    .map(({startTimestamp, data: {nodeId}}) => ({
-      type: 6, // plugin type
-      data: {
-        nodeId,
-        text: 'LCP',
-      },
-      timestamp: Math.floor(startTimestamp * 1000),
-    }));
-
-  const events = ([] as RecordingEvent[])
-    .concat(rrwebEvents)
-    .concat(highlights)
-    .concat({
-      type: 5, // EventType.Custom,
-      timestamp: endTimestampMS,
-      data: {
-        tag: 'replay-end',
-      },
-    });
+  const events = ([] as RecordingEvent[]).concat(rrwebEvents).concat({
+    type: 5, // EventType.Custom,
+    timestamp: endTimestampMS,
+    data: {
+      tag: 'replay-end',
+    },
+  });
   events.sort((a, b) => a.timestamp - b.timestamp);
 
   const firstRRWebEvent = first(events);
@@ -53,17 +43,20 @@ export function breadcrumbFactory(
   startTimestamp: number,
   rootEvent: Event,
   errors: ReplayError[],
-  rawCrumbs: ReplayCrumb[]
+  rawCrumbs: ReplayCrumb[],
+  spans: ReplaySpan[]
 ): Crumb[] {
   const {tags} = rootEvent;
+  const initialUrl = tags.find(tag => tag.key === 'url')?.value;
   const initBreadcrumb = {
     type: BreadcrumbType.INIT,
     timestamp: new Date(startTimestamp).toISOString(),
     level: BreadcrumbLevelType.INFO,
-    action: 'replay-init',
-    message: t('Start recording'),
+    message: initialUrl,
     data: {
-      url: tags.find(tag => tag.key === 'url')?.value,
+      action: 'replay-init',
+      label: t('Start recording'),
+      url: initialUrl,
     },
   } as BreadcrumbTypeDefault;
 
@@ -71,21 +64,66 @@ export function breadcrumbFactory(
     type: BreadcrumbType.ERROR,
     level: BreadcrumbLevelType.ERROR,
     category: 'exception',
+    message: error['error.value'],
     data: {
-      type: error['error.type'],
-      value: error['error.value'],
+      label: error['error.type'],
     },
     timestamp: error.timestamp,
   }));
 
+  const spanCrumbs: (BreadcrumbTypeDefault | BreadcrumbTypeNavigation)[] = spans
+    .filter(span =>
+      ['navigation.navigate', 'navigation.reload', 'largest-contentful-paint'].includes(
+        span.op
+      )
+    )
+    .map(span => {
+      if (span.op.startsWith('navigation')) {
+        const [, action] = span.op.split('.');
+        return {
+          category: 'default',
+          type: BreadcrumbType.NAVIGATION,
+          timestamp: new Date(span.startTimestamp * 1000).toISOString(),
+          level: BreadcrumbLevelType.INFO,
+          message: span.description,
+          action,
+          data: {
+            to: span.description,
+            label:
+              action === 'reload'
+                ? t('Reload')
+                : action === 'navigate'
+                ? t('Page load')
+                : t('Navigation'),
+            ...span.data,
+          },
+        };
+      }
+
+      return {
+        type: BreadcrumbType.DEBUG,
+        timestamp: new Date(span.startTimestamp * 1000).toISOString(),
+        level: BreadcrumbLevelType.INFO,
+        category: 'default',
+        data: {
+          action: span.op,
+          ...span.data,
+          label: span.op === 'largest-contentful-paint' ? t('LCP') : span.op,
+        },
+      };
+    });
+
+  const hasPageLoad = spans.find(span => span.op === 'navigation.navigate');
+
   const result = transformCrumbs([
-    initBreadcrumb,
+    ...(!hasPageLoad ? [initBreadcrumb] : []),
     ...(rawCrumbs.map(({timestamp, ...crumb}) => ({
       ...crumb,
       type: BreadcrumbType.DEFAULT,
       timestamp: new Date(timestamp * 1000).toISOString(),
     })) as RawCrumb[]),
     ...errorCrumbs,
+    ...spanCrumbs,
   ]);
 
   return result.sort((a, b) => +new Date(a.timestamp || 0) - +new Date(b.timestamp || 0));

+ 7 - 2
static/app/utils/replays/replayReader.tsx

@@ -59,12 +59,17 @@ export default class ReplayReader {
     );
 
     this.spans = spansFactory(spans);
-    this.breadcrumbs = breadcrumbFactory(startTimestampMS, event, errors, breadcrumbs);
+    this.breadcrumbs = breadcrumbFactory(
+      startTimestampMS,
+      event,
+      errors,
+      breadcrumbs,
+      this.spans
+    );
 
     this.rrwebEvents = rrwebEventListFactory(
       startTimestampMS,
       endTimestampMS,
-      spans,
       rrwebEvents
     );
 

+ 144 - 0
static/app/views/replays/detail/breadcrumbs/breadcrumbItem.tsx

@@ -0,0 +1,144 @@
+import {memo, useCallback} from 'react';
+import styled from '@emotion/styled';
+
+import BreadcrumbIcon from 'sentry/components/events/interfaces/breadcrumbs/breadcrumb/type/icon';
+import {PanelItem} from 'sentry/components/panels';
+import {getDetails} from 'sentry/components/replays/breadcrumbs/utils';
+import PlayerRelativeTime from 'sentry/components/replays/playerRelativeTime';
+import SvgIcon from 'sentry/icons/svgIcon';
+import space from 'sentry/styles/space';
+import type {Crumb} from 'sentry/types/breadcrumbs';
+
+type MouseCallback = (crumb: Crumb, e: React.MouseEvent<HTMLElement>) => void;
+
+interface Props {
+  crumb: Crumb;
+  isHovered: boolean;
+  isSelected: boolean;
+  onClick: MouseCallback;
+  onMouseEnter: MouseCallback;
+  onMouseLeave: MouseCallback;
+  startTimestamp: number;
+}
+
+function BreadcrumbItem({
+  crumb,
+  isHovered,
+  isSelected,
+  startTimestamp,
+  onMouseEnter,
+  onMouseLeave,
+  onClick,
+}: Props) {
+  const {title, description} = getDetails(crumb);
+
+  const handleMouseEnter = useCallback(
+    (e: React.MouseEvent<HTMLElement>) => onMouseEnter(crumb, e),
+    [onMouseEnter, crumb]
+  );
+  const handleMouseLeave = useCallback(
+    (e: React.MouseEvent<HTMLElement>) => onMouseLeave(crumb, e),
+    [onMouseLeave, crumb]
+  );
+  const handleClick = useCallback(
+    (e: React.MouseEvent<HTMLElement>) => onClick(crumb, e),
+    [onClick, crumb]
+  );
+
+  return (
+    <CrumbItem
+      as="button"
+      onMouseEnter={handleMouseEnter}
+      onMouseLeave={handleMouseLeave}
+      onClick={handleClick}
+      isHovered={isHovered}
+      isSelected={isSelected}
+    >
+      <IconWrapper color={crumb.color}>
+        <BreadcrumbIcon type={crumb.type} />
+      </IconWrapper>
+      <CrumbDetails>
+        <Title>{title}</Title>
+        <Description title={description}>{description}</Description>
+      </CrumbDetails>
+      <PlayerRelativeTime relativeTime={startTimestamp} timestamp={crumb.timestamp} />
+    </CrumbItem>
+  );
+}
+
+const CrumbDetails = styled('div')`
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  line-height: 1.2;
+  padding: ${space(1)} 0;
+`;
+
+const Title = styled('span')`
+  ${p => p.theme.overflowEllipsis};
+  text-transform: capitalize;
+`;
+
+const Description = styled('span')`
+  ${p => p.theme.overflowEllipsis};
+  font-size: 0.7rem;
+  font-family: ${p => p.theme.text.familyMono};
+`;
+
+type CrumbItemProps = {
+  isHovered: boolean;
+  isSelected: boolean;
+};
+
+const CrumbItem = styled(PanelItem)<CrumbItemProps>`
+  display: grid;
+  grid-template-columns: max-content max-content auto max-content;
+  align-items: center;
+  gap: ${space(1)};
+  width: 100%;
+
+  font-size: ${p => p.theme.fontSizeMedium};
+  background: transparent;
+  padding: 0;
+  padding-right: ${space(1)};
+  text-align: left;
+
+  border: none;
+  border-bottom: 1px solid ${p => p.theme.innerBorder};
+  ${p => p.isHovered && `background: ${p.theme.surface400};`}
+
+  /* overrides PanelItem css */
+  &:last-child {
+    border-bottom: 1px solid ${p => p.theme.innerBorder};
+  }
+
+  /* Selected state */
+  ::before {
+    content: '';
+    width: 4px;
+    height: 100%;
+    ${p => p.isSelected && `background-color: ${p.theme.purple300};`}
+  }
+`;
+
+/**
+ * Taken `from events/interfaces/.../breadcrumbs/types`
+ */
+const IconWrapper = styled('div')<
+  Required<Pick<React.ComponentProps<typeof SvgIcon>, 'color'>>
+>`
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 22px;
+  height: 22px;
+  border-radius: 50%;
+  color: ${p => p.theme.white};
+  background: ${p => p.theme[p.color] ?? p.color};
+  box-shadow: ${p => p.theme.dropShadowLightest};
+  position: relative;
+`;
+
+const MemoizedBreadcrumbItem = memo(BreadcrumbItem);
+
+export default MemoizedBreadcrumbItem;

+ 33 - 83
static/app/views/replays/detail/userActionsNavigator.tsx → static/app/views/replays/detail/breadcrumbs/index.tsx

@@ -1,24 +1,22 @@
 import {Fragment, useCallback} from 'react';
 import styled from '@emotion/styled';
 
-import Type from 'sentry/components/events/interfaces/breadcrumbs/breadcrumb/type';
 import {
   Panel as BasePanel,
   PanelBody as BasePanelBody,
   PanelHeader as BasePanelHeader,
-  PanelItem,
 } from 'sentry/components/panels';
 import Placeholder from 'sentry/components/placeholder';
-import ActionCategory from 'sentry/components/replays/actionCategory';
-import PlayerRelativeTime from 'sentry/components/replays/playerRelativeTime';
 import {useReplayContext} from 'sentry/components/replays/replayContext';
 import {relativeTimeInMs} from 'sentry/components/replays/utils';
 import {t} from 'sentry/locale';
 import space from 'sentry/styles/space';
-import {BreadcrumbType, Crumb} from 'sentry/types/breadcrumbs';
+import {Crumb} from 'sentry/types/breadcrumbs';
 import {EventTransaction} from 'sentry/types/event';
 import {getPrevBreadcrumb} from 'sentry/utils/replays/getBreadcrumb';
 
+import BreadcrumbItem from './breadcrumbItem';
+
 function CrumbPlaceholder({number}: {number: number}) {
   return (
     <Fragment>
@@ -40,20 +38,7 @@ type Props = {
   event: EventTransaction | undefined;
 };
 
-type ContainerProps = {
-  isHovered: boolean;
-  isSelected: boolean;
-};
-
-const USER_ACTIONS = [
-  BreadcrumbType.ERROR,
-  BreadcrumbType.INIT,
-  BreadcrumbType.NAVIGATION,
-  BreadcrumbType.UI,
-  BreadcrumbType.USER,
-];
-
-function UserActionsNavigator({event, crumbs}: Props) {
+function Breadcrumbs({event, crumbs: allCrumbs}: Props) {
   const {
     setCurrentTime,
     setCurrentHoverTime,
@@ -65,13 +50,14 @@ function UserActionsNavigator({event, crumbs}: Props) {
   } = useReplayContext();
 
   const startTimestamp = event?.startTimestamp || 0;
-  const userActionCrumbs =
-    crumbs?.filter(crumb => USER_ACTIONS.includes(crumb.type)) || [];
 
   const isLoaded = Boolean(event);
 
+  const crumbs =
+    allCrumbs?.filter(crumb => !['console'].includes(crumb.category || '')) || [];
+
   const currentUserAction = getPrevBreadcrumb({
-    crumbs: userActionCrumbs,
+    crumbs,
     targetTimestampMs: startTimestamp * 1000 + currentTime,
     allowExact: true,
   });
@@ -79,13 +65,13 @@ function UserActionsNavigator({event, crumbs}: Props) {
   const closestUserAction =
     currentHoverTime !== undefined
       ? getPrevBreadcrumb({
-          crumbs: userActionCrumbs,
+          crumbs,
           targetTimestampMs: startTimestamp * 1000 + (currentHoverTime ?? 0),
           allowExact: true,
         })
       : undefined;
 
-  const onMouseEnter = useCallback(
+  const handleMouseEnter = useCallback(
     (item: Crumb) => {
       if (startTimestamp) {
         setCurrentHoverTime(relativeTimeInMs(item.timestamp ?? '', startTimestamp));
@@ -101,7 +87,7 @@ function UserActionsNavigator({event, crumbs}: Props) {
     [setCurrentHoverTime, startTimestamp, highlight, clearAllHighlights]
   );
 
-  const onMouseLeave = useCallback(
+  const handleMouseLeave = useCallback(
     (item: Crumb) => {
       setCurrentHoverTime(undefined);
 
@@ -112,42 +98,33 @@ function UserActionsNavigator({event, crumbs}: Props) {
     [setCurrentHoverTime, removeHighlight]
   );
 
+  const handleClick = useCallback(
+    (crumb: Crumb) => {
+      crumb.timestamp !== undefined
+        ? setCurrentTime(relativeTimeInMs(crumb.timestamp, startTimestamp))
+        : null;
+    },
+    [setCurrentTime, startTimestamp]
+  );
+
   return (
     <Panel>
-      <PanelHeader>{t('Event Chapters')}</PanelHeader>
+      <PanelHeader>{t('Breadcrumbs')}</PanelHeader>
 
       <PanelBody>
         {!isLoaded && <CrumbPlaceholder number={4} />}
         {isLoaded &&
-          userActionCrumbs.map(item => (
-            <PanelItemCenter
-              key={item.id}
-              onMouseEnter={() => onMouseEnter(item)}
-              onMouseLeave={() => onMouseLeave(item)}
-            >
-              <Container
-                isHovered={closestUserAction?.id === item.id}
-                isSelected={currentUserAction?.id === item.id}
-                onClick={() =>
-                  item.timestamp !== undefined
-                    ? setCurrentTime(relativeTimeInMs(item.timestamp, startTimestamp))
-                    : null
-                }
-              >
-                <Wrapper>
-                  <Type
-                    type={item.type}
-                    color={item.color}
-                    description={item.description}
-                  />
-                  <ActionCategory action={item} />
-                </Wrapper>
-                <PlayerRelativeTime
-                  relativeTime={startTimestamp}
-                  timestamp={item.timestamp}
-                />
-              </Container>
-            </PanelItemCenter>
+          crumbs.map(crumb => (
+            <BreadcrumbItem
+              key={crumb.id}
+              crumb={crumb}
+              startTimestamp={startTimestamp}
+              isHovered={closestUserAction?.id === crumb.id}
+              isSelected={currentUserAction?.id === crumb.id}
+              onMouseEnter={handleMouseEnter}
+              onMouseLeave={handleMouseLeave}
+              onClick={handleClick}
+            />
           ))}
       </PanelBody>
     </Panel>
@@ -184,36 +161,9 @@ const PanelBody = styled(BasePanelBody)`
   overflow-y: auto;
 `;
 
-const PanelItemCenter = styled(PanelItem)`
-  display: block;
-  padding: ${space(0)};
-  cursor: pointer;
-`;
-
-const Container = styled('button')<ContainerProps>`
-  display: inline-flex;
-  width: 100%;
-  border: none;
-  background: transparent;
-  justify-content: space-between;
-  align-items: center;
-  border-left: 4px solid transparent;
-  padding: ${space(1)} ${space(1.5)};
-  ${p => p.isHovered && `background: ${p.theme.surface400};`}
-  ${p => p.isSelected && `border-left: 4px solid ${p.theme.purple300};`}
-`;
-
-const Wrapper = styled('div')`
-  display: flex;
-  align-items: center;
-  gap: ${space(1)};
-  font-size: ${p => p.theme.fontSizeMedium};
-  color: ${p => p.theme.gray500};
-`;
-
 const PlaceholderMargin = styled(Placeholder)`
   margin: ${space(1)} ${space(1.5)};
   width: auto;
 `;
 
-export default UserActionsNavigator;
+export default Breadcrumbs;

+ 5 - 8
static/app/views/replays/details.tsx

@@ -16,10 +16,10 @@ import useFullscreen from 'sentry/utils/replays/hooks/useFullscreen';
 import useReplayData from 'sentry/utils/replays/hooks/useReplayData';
 import {useRouteContext} from 'sentry/utils/useRouteContext';
 
+import Breadcrumbs from './detail/breadcrumbs';
 import DetailLayout from './detail/detailLayout';
 import FocusArea from './detail/focusArea';
 import FocusTabs from './detail/focusTabs';
-import UserActionsNavigator from './detail/userActionsNavigator';
 
 function ReplayDetails() {
   const {
@@ -85,23 +85,20 @@ function ReplayDetails() {
           </Layout.Main>
 
           <Layout.Side>
-            <ErrorBoundary>
-              <UserActionsNavigator
-                crumbs={replay?.getRawCrumbs()}
-                event={replay?.getEvent()}
-              />
+            <ErrorBoundary mini>
+              <Breadcrumbs crumbs={replay?.getRawCrumbs()} event={replay?.getEvent()} />
             </ErrorBoundary>
           </Layout.Side>
 
           <StickyMain fullWidth>
-            <ErrorBoundary>
+            <ErrorBoundary mini>
               <ReplayTimeline />
             </ErrorBoundary>
             <FocusTabs />
           </StickyMain>
 
           <StyledLayoutMain fullWidth>
-            <ErrorBoundary>
+            <ErrorBoundary mini>
               <FocusArea replay={replay} />
             </ErrorBoundary>
           </StyledLayoutMain>

+ 81 - 59
tests/js/spec/utils/replays/replayDataUtils.spec.tsx

@@ -1,5 +1,5 @@
 import {
-  // breadcrumbEntryFactory,
+  breadcrumbFactory,
   // breadcrumbValuesFromEvents,
   // replayTimestamps,
   rrwebEventListFactory,
@@ -7,19 +7,20 @@ import {
   // spanEntryFactory,
 } from 'sentry/utils/replays/replayDataUtils';
 
-describe('rrwebEventListFactory', () => {
-  function createSpan(extra: {op: string; data?: Record<string, any>}) {
+describe('breadcrumbFactory', () => {
+  function createSpan(extra: {
+    op: string;
+    data?: Record<string, any>;
+    description?: string;
+  }) {
     return {
-      // span_id: 'spanid',
       startTimestamp: 1,
       endTimestamp: 2,
-      // trace_id: 'traceid',
       data: {},
       ...extra,
     };
   }
-
-  it('returns a list of replay events for highlights', function () {
+  it('adds LCP as a breadcrumb', () => {
     const rawSpans = [
       createSpan({
         op: 'foo',
@@ -31,58 +32,92 @@ describe('rrwebEventListFactory', () => {
           nodeId: 2,
         },
       }),
-      createSpan({
-        op: 'largest-contentful-paint',
-        data: {
-          nodeId: null,
+    ];
+
+    const results = breadcrumbFactory(0, TestStubs.Event(), [], [], rawSpans);
+
+    expect(results).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "color": "gray300",
+          "data": Object {
+            "action": "replay-init",
+            "label": "Start recording",
+            "url": undefined,
+          },
+          "description": "Default",
+          "id": 0,
+          "level": "info",
+          "message": undefined,
+          "timestamp": "1970-01-01T00:00:00.000Z",
+          "type": "init",
         },
-      }),
-      createSpan({
-        op: 'largest-contentful-paint',
-        data: {
-          nodeId: 0,
+        Object {
+          "category": "default",
+          "color": "purple300",
+          "data": Object {
+            "action": "largest-contentful-paint",
+            "label": "LCP",
+            "nodeId": 2,
+          },
+          "description": "Debug",
+          "id": 1,
+          "level": "info",
+          "timestamp": "1970-01-01T00:00:01.000Z",
+          "type": "debug",
         },
-      }),
+      ]
+    `);
+  });
+
+  it('adds navigation as a breadcrumb', () => {
+    const rawSpans = [
       createSpan({
-        op: 'largest-contentful-paint',
-        data: {
-          nodeId: -1,
-        },
+        op: 'foo',
+        data: {},
       }),
       createSpan({
-        op: 'largest-contentful-paint',
-        data: {
-          nodeId: 10,
-        },
+        op: 'navigation.navigate',
+        description: 'http://test.com',
       }),
     ];
 
-    const results = rrwebEventListFactory(0, 0, rawSpans, []);
+    const results = breadcrumbFactory(0, TestStubs.Event(), [], [], rawSpans);
 
     expect(results).toMatchInlineSnapshot(`
       Array [
         Object {
+          "action": "navigate",
+          "category": "default",
+          "color": "green300",
           "data": Object {
-            "tag": "replay-end",
-          },
-          "timestamp": 0,
-          "type": 5,
-        },
-        Object {
-          "data": Object {
-            "nodeId": 2,
-            "text": "LCP",
+            "label": "Page load",
+            "to": "http://test.com",
           },
-          "timestamp": 1000,
-          "type": 6,
+          "description": "Navigation",
+          "id": 0,
+          "level": "info",
+          "message": "http://test.com",
+          "timestamp": "1970-01-01T00:00:01.000Z",
+          "type": "navigation",
         },
+      ]
+    `);
+  });
+});
+
+describe('rrwebEventListFactory', () => {
+  it('returns a list of replay events for highlights', function () {
+    const results = rrwebEventListFactory(0, 0, []);
+
+    expect(results).toMatchInlineSnapshot(`
+      Array [
         Object {
           "data": Object {
-            "nodeId": 10,
-            "text": "LCP",
+            "tag": "replay-end",
           },
-          "timestamp": 1000,
-          "type": 6,
+          "timestamp": 0,
+          "type": 5,
         },
       ]
     `);
@@ -93,26 +128,13 @@ describe('rrwebEventListFactory', () => {
     const endTimestampMS = 10_000;
 
     expect(
-      rrwebEventListFactory(
-        startTimestampMS,
-        endTimestampMS,
-        [
-          createSpan({
-            op: 'largest-contentful-paint',
-            data: {
-              nodeId: 2,
-            },
-          }),
-        ],
-        [
-          {type: 0, timestamp: 5_000, data: {}},
-          {type: 1, timestamp: 1_000, data: {}},
-          {type: 2, timestamp: 3_000, data: {}},
-        ]
-      )
+      rrwebEventListFactory(startTimestampMS, endTimestampMS, [
+        {type: 0, timestamp: 5_000, data: {}},
+        {type: 1, timestamp: 1_000, data: {}},
+        {type: 2, timestamp: 3_000, data: {}},
+      ])
     ).toEqual([
       {type: 1, timestamp: 0, data: {}},
-      {type: 6, timestamp: 1_000, data: {nodeId: 2, text: 'LCP'}},
       {type: 2, timestamp: 3_000, data: {}},
       {type: 0, timestamp: 5_000, data: {}},
       {type: 5, timestamp: 10_000, data: {tag: 'replay-end'}},