Browse Source

feat(replay): Click in the Replay Details header: Error count to filter the Errors tab (#51322)

Updated the Errors count in the replay details list to filter the errors
list by project. There are some nice tooltips as well:

| One Project (tooltip same as 2 projects) | 2 Projects | N Projects | 
| --- | --- | --- |
| <img width="343" alt="SCR-20230620-nxeq"
src="https://github.com/getsentry/sentry/assets/187460/37bda1f1-8908-4d00-b966-239cafda4390">
| <img width="338" alt="SCR-20230620-nxaw"
src="https://github.com/getsentry/sentry/assets/187460/e0a08361-b4de-4d68-879f-382653befa4f">
| <img width="345" alt="SCR-20230620-nwxi"
src="https://github.com/getsentry/sentry/assets/187460/252f1e98-adfb-435e-bd82-6e37c2c4395d">
|

Overall the icons are smaller, matching the 16px of the Browser and
Start Time icons. No other visual changes.

I refactored the code to make it more sane. I think there's slightly
less nested, and fewer props for styled components.

Fixes https://github.com/getsentry/sentry/issues/49602
Ryan Albrecht 1 year ago
parent
commit
c6bb2cb331

+ 5 - 5
fixtures/js-stubs/replayError.ts

@@ -4,13 +4,13 @@ export function ReplayError(
   error: Partial<TReplayError> & Pick<TReplayError, 'id' | 'issue' | 'timestamp'>
 ): TReplayError {
   return {
-    'error.type': [] as string[],
-    'error.value': [] as string[],
+    'error.type': error['error.type'] ?? ([] as string[]),
+    'error.value': error['error.value'] ?? ([] as string[]),
     id: error.id,
     issue: error.issue,
-    'issue.id': 3740335939,
-    'project.name': 'javascript',
+    'issue.id': error['issue.id'] ?? 3740335939,
+    'project.name': error['project.name'] ?? 'javascript',
     timestamp: error.id,
-    title: 'A Redirect with :orgId param on customer domain',
+    title: error.title ?? 'A Redirect with :orgId param on customer domain',
   };
 }

+ 4 - 11
static/app/components/replays/contextIcon.tsx

@@ -3,7 +3,9 @@ import styled from '@emotion/styled';
 
 import {generateIconName} from 'sentry/components/events/contextSummary/utils';
 import LoadingMask from 'sentry/components/loadingMask';
+import CountTooltipContent from 'sentry/components/replays/header/countTooltipContent';
 import {Tooltip} from 'sentry/components/tooltip';
+import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 
 type Props = {
@@ -21,9 +23,9 @@ const ContextIcon = styled(({className, name, version}: Props) => {
 
   const title = (
     <CountTooltipContent>
-      <dt>Name:</dt>
+      <dt>{t('Name:')}</dt>
       <dd>{name}</dd>
-      {version ? <dt>Version:</dt> : null}
+      {version ? <dt>{t('Version:')}</dt> : null}
       {version ? <dd>{version}</dd> : null}
     </CountTooltipContent>
   );
@@ -42,13 +44,4 @@ const ContextIcon = styled(({className, name, version}: Props) => {
   align-items: center;
 `;
 
-const CountTooltipContent = styled('dl')`
-  display: grid;
-  grid-template-columns: 1fr max-content;
-  gap: ${space(1)} ${space(3)};
-  text-align: left;
-  align-items: center;
-  margin-bottom: 0;
-`;
-
 export default ContextIcon;

+ 14 - 0
static/app/components/replays/header/countTooltipContent.tsx

@@ -0,0 +1,14 @@
+import styled from '@emotion/styled';
+
+import {space} from 'sentry/styles/space';
+
+const CountTooltipContent = styled('dl')`
+  display: grid;
+  grid-template-columns: 1fr minmax(auto, max-content);
+  gap: ${space(1)} ${space(3)};
+  text-align: left;
+  align-items: start;
+  margin-bottom: 0;
+`;
+
+export default CountTooltipContent;

+ 0 - 41
static/app/components/replays/header/errorCount.tsx

@@ -1,41 +0,0 @@
-import {Fragment} from 'react';
-import styled from '@emotion/styled';
-
-import ProjectBadge from 'sentry/components/idBadge/projectBadge';
-import {IconFire} from 'sentry/icons';
-import {space} from 'sentry/styles/space';
-import {Project} from 'sentry/types';
-
-type Props = {
-  countErrors: number;
-  className?: string;
-  hideIcon?: boolean;
-  project?: Project;
-};
-
-const ErrorCount = styled(({countErrors, project, className, hideIcon}: Props) =>
-  countErrors ? (
-    <span className={className}>
-      {!hideIcon && (
-        <Fragment>
-          {project ? (
-            <ProjectBadge project={project} disableLink hideName />
-          ) : (
-            <IconFire />
-          )}
-        </Fragment>
-      )}
-      {countErrors}
-    </span>
-  ) : (
-    <span className={className}>0</span>
-  )
-)`
-  display: flex;
-  align-items: center;
-  gap: ${space(0.5)};
-  color: ${p => (p.countErrors > 0 ? p.theme.red400 : 'inherit')};
-  font-variant-numeric: tabular-nums;
-`;
-
-export default ErrorCount;

+ 134 - 0
static/app/components/replays/header/errorCounts.spec.tsx

@@ -0,0 +1,134 @@
+import {render, screen} from 'sentry-test/reactTestingLibrary';
+
+import ErrorCounts from 'sentry/components/replays/header/errorCounts';
+import useProjects from 'sentry/utils/useProjects';
+
+jest.mock('sentry/utils/useProjects');
+
+const mockUseProjects = useProjects as jest.MockedFunction<typeof useProjects>;
+
+const replayRecord = TestStubs.ReplayRecord();
+const organization = TestStubs.Organization({
+  features: 'session-replay-errors-tab',
+});
+
+describe('ErrorCounts', () => {
+  beforeEach(() => {
+    mockUseProjects.mockReturnValue({
+      fetching: false,
+      projects: [
+        TestStubs.Project({
+          id: replayRecord.project_id,
+          slug: 'my-js-app',
+          platform: 'javascript',
+        }),
+        TestStubs.Project({
+          id: '123123123',
+          slug: 'my-py-backend',
+          platform: 'python',
+        }),
+        TestStubs.Project({
+          id: '234234234',
+          slug: 'my-node-service',
+          platform: 'node',
+        }),
+      ],
+      fetchError: null,
+      hasMore: false,
+      initiallyLoaded: true,
+      onSearch: () => Promise.resolve(),
+      placeholders: [],
+    });
+  });
+
+  it('should render 0 when there are no errors in the array', async () => {
+    const errors = [];
+
+    render(<ErrorCounts replayErrors={errors} replayRecord={replayRecord} />, {
+      organization,
+    });
+    const countNode = await screen.getByLabelText('number of errors');
+    expect(countNode).toHaveTextContent('0');
+  });
+
+  it('should render an icon & count when all errors come from a single project', async () => {
+    const errors = [TestStubs.ReplayError({'project.name': 'my-js-app'})];
+
+    render(<ErrorCounts replayErrors={errors} replayRecord={replayRecord} />, {
+      organization,
+    });
+
+    const countNode = await screen.getByLabelText('number of errors');
+    expect(countNode).toHaveTextContent('1');
+
+    const icon = await screen.findByTestId('platform-icon-javascript');
+    expect(icon).toBeInTheDocument();
+
+    expect(countNode.parentElement).toHaveAttribute(
+      'href',
+      '/mock-pathname/?f_e_project=my-js-app&t_main=errors'
+    );
+  });
+
+  it('should render an icon & count with links when there are errors in two unique projects', async () => {
+    const errors = [
+      TestStubs.ReplayError({'project.name': 'my-js-app'}),
+      TestStubs.ReplayError({'project.name': 'my-py-backend'}),
+      TestStubs.ReplayError({'project.name': 'my-py-backend'}),
+    ];
+
+    render(<ErrorCounts replayErrors={errors} replayRecord={replayRecord} />, {
+      organization,
+    });
+
+    const countNodes = await screen.getAllByLabelText('number of errors');
+    expect(countNodes[0]).toHaveTextContent('1');
+    expect(countNodes[1]).toHaveTextContent('2');
+
+    const jsIcon = await screen.findByTestId('platform-icon-javascript');
+    expect(jsIcon).toBeInTheDocument();
+    const pyIcon = await screen.findByTestId('platform-icon-python');
+    expect(pyIcon).toBeInTheDocument();
+
+    expect(countNodes[0].parentElement).toHaveAttribute(
+      'href',
+      '/mock-pathname/?f_e_project=my-js-app&t_main=errors'
+    );
+    expect(countNodes[1].parentElement).toHaveAttribute(
+      'href',
+      '/mock-pathname/?f_e_project=my-py-backend&t_main=errors'
+    );
+  });
+
+  it('should render multiple icons, but a single count and link, when there are errors in three or more projects', async () => {
+    const errors = [
+      TestStubs.ReplayError({'project.name': 'my-js-app'}),
+      TestStubs.ReplayError({'project.name': 'my-py-backend'}),
+      TestStubs.ReplayError({'project.name': 'my-py-backend'}),
+      TestStubs.ReplayError({'project.name': 'my-node-service'}),
+      TestStubs.ReplayError({'project.name': 'my-node-service'}),
+      TestStubs.ReplayError({'project.name': 'my-node-service'}),
+    ];
+
+    render(<ErrorCounts replayErrors={errors} replayRecord={replayRecord} />, {
+      organization,
+    });
+
+    const countNode = await screen.getByLabelText('total errors');
+    expect(countNode).toHaveTextContent('6');
+
+    const jsIcon = await screen.findByTestId('platform-icon-javascript');
+    expect(jsIcon).toBeInTheDocument();
+
+    const pyIcon = await screen.findByTestId('platform-icon-python');
+    expect(pyIcon).toBeInTheDocument();
+
+    const plusOne = await screen.getByLabelText('hidden projects');
+    expect(plusOne).toHaveTextContent('+1');
+
+    expect(countNode.parentElement).toHaveAttribute(
+      'href',
+      '/mock-pathname/?t_main=errors'
+    );
+  });
+});

+ 152 - 0
static/app/components/replays/header/errorCounts.tsx

@@ -0,0 +1,152 @@
+import {Fragment, useCallback} from 'react';
+import styled from '@emotion/styled';
+
+import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
+import Badge from 'sentry/components/badge';
+import Link from 'sentry/components/links/link';
+import CountTooltipContent from 'sentry/components/replays/header/countTooltipContent';
+import useErrorCountPerProject from 'sentry/components/replays/header/useErrorCountPerProject';
+import {Tooltip} from 'sentry/components/tooltip';
+import {IconFire} from 'sentry/icons';
+import {t, tn} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import type {Project} from 'sentry/types';
+import {useLocation} from 'sentry/utils/useLocation';
+import useOrganization from 'sentry/utils/useOrganization';
+import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types';
+
+type Props = {
+  replayErrors: ReplayError[];
+  replayRecord: ReplayRecord;
+};
+
+export default function ErrorCounts({replayErrors, replayRecord}: Props) {
+  const {pathname, query} = useLocation();
+  const organization = useOrganization();
+  const hasErrorTab = organization.features.includes('session-replay-errors-tab');
+
+  const getLink = useCallback(
+    ({project}: {project?: Project}) => {
+      return hasErrorTab
+        ? {
+            pathname,
+            query: {...query, t_main: 'errors', f_e_project: project?.slug},
+          }
+        : {
+            pathname,
+            query: {
+              ...query,
+              t_main: 'console',
+              f_c_logLevel: 'issue',
+              f_c_search: undefined,
+            },
+          };
+    },
+    [hasErrorTab, pathname, query]
+  );
+
+  const errorCountPerProject = useErrorCountPerProject({replayErrors, replayRecord});
+
+  if (!errorCountPerProject.length) {
+    return <Count aria-label={t('number of errors')}>0</Count>;
+  }
+  if (errorCountPerProject.length < 3) {
+    return (
+      <Fragment>
+        {errorCountPerProject.map(({project, count}) =>
+          project ? (
+            <Tooltip
+              key={project.slug}
+              title={
+                <CountTooltipContent>
+                  <dt>{t('Project:')}</dt>
+                  <dd>{project.slug}</dd>
+                  <dt>{t('Errors:')}</dt>
+                  <dd>{count}</dd>
+                </CountTooltipContent>
+              }
+            >
+              <StyledLink to={getLink({project})}>
+                <ProjectAvatar size={16} project={project} />
+                <ErrorCount aria-label={t('number of errors')}>{count}</ErrorCount>
+              </StyledLink>
+            </Tooltip>
+          ) : null
+        )}
+      </Fragment>
+    );
+  }
+
+  const extraProjectCount = errorCountPerProject.length - 2;
+  const totalErrors = errorCountPerProject.reduce((acc, val) => acc + val.count, 0);
+  return (
+    <Tooltip
+      forceVisible
+      title={
+        <ColumnTooltipContent>
+          {errorCountPerProject.map(({project, count}) => (
+            <Fragment key={project?.slug}>
+              <dt>{project?.slug}</dt>
+              <dd>{tn('1 error', '%s errors', count)}</dd>
+            </Fragment>
+          ))}
+        </ColumnTooltipContent>
+      }
+    >
+      <StyledLink to={getLink({})}>
+        <StackedProjectBadges>
+          {errorCountPerProject.slice(0, 2).map(({project}) => {
+            return project ? (
+              <ProjectAvatar key={project.slug} size={16} project={project} />
+            ) : (
+              <IconFire />
+            );
+          })}
+          <Badge aria-label={t('hidden projects')}>+{extraProjectCount}</Badge>
+        </StackedProjectBadges>
+        <ErrorCount aria-label={t('total errors')}>{totalErrors}</ErrorCount>
+      </StyledLink>
+    </Tooltip>
+  );
+}
+
+const Count = styled('span')`
+  font-variant-numeric: tabular-nums;
+`;
+
+const ErrorCount = styled(Count)`
+  color: ${p => p.theme.red400};
+`;
+
+const ColumnTooltipContent = styled(CountTooltipContent)`
+  grid-template-rows: auto 1fr;
+  grid-template-columns: 1fr 1fr;
+`;
+
+const StyledLink = styled(Link)`
+  display: flex;
+  flex-direction: row;
+  gap: ${space(0.5)};
+  align-items: center;
+  & * {
+    cursor: pointer !important;
+  }
+`;
+
+const StackedProjectBadges = styled('div')`
+  display: flex;
+  align-items: center;
+  & * {
+    margin-left: 0;
+    margin-right: 0;
+    cursor: pointer !important;
+  }
+
+  & *:hover {
+    z-index: unset;
+  }
+
+  & > :not(:first-child) {
+    margin-left: -${space(0.5)};
+  }
+`;

+ 89 - 0
static/app/components/replays/header/replayMetaData.tsx

@@ -0,0 +1,89 @@
+import {Fragment} from 'react';
+import styled from '@emotion/styled';
+
+import ContextIcon from 'sentry/components/replays/contextIcon';
+import ErrorCounts from 'sentry/components/replays/header/errorCounts';
+import HeaderPlaceholder from 'sentry/components/replays/header/headerPlaceholder';
+import TimeSince from 'sentry/components/timeSince';
+import {IconCalendar} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types';
+
+type Props = {
+  replayErrors: ReplayError[];
+  replayRecord: ReplayRecord | undefined;
+};
+
+function ReplayMetaData({replayErrors, replayRecord}: Props) {
+  return (
+    <KeyMetrics>
+      <KeyMetricLabel>{t('OS')}</KeyMetricLabel>
+      <KeyMetricData>
+        <ContextIcon
+          name={replayRecord?.os.name ?? ''}
+          version={replayRecord?.os.version ?? undefined}
+        />
+      </KeyMetricData>
+
+      <KeyMetricLabel>{t('Browser')}</KeyMetricLabel>
+      <KeyMetricData>
+        <ContextIcon
+          name={replayRecord?.browser.name ?? ''}
+          version={replayRecord?.browser.version ?? undefined}
+        />
+      </KeyMetricData>
+
+      <KeyMetricLabel>{t('Start Time')}</KeyMetricLabel>
+      <KeyMetricData>
+        {replayRecord ? (
+          <Fragment>
+            <IconCalendar color="gray300" />
+            <TimeSince date={replayRecord.started_at} unitStyle="regular" />
+          </Fragment>
+        ) : (
+          <HeaderPlaceholder width="80px" height="16px" />
+        )}
+      </KeyMetricData>
+      <KeyMetricLabel>{t('Errors')}</KeyMetricLabel>
+      <KeyMetricData>
+        {replayRecord ? (
+          <ErrorCounts replayErrors={replayErrors} replayRecord={replayRecord} />
+        ) : (
+          <HeaderPlaceholder width="80px" height="16px" />
+        )}
+      </KeyMetricData>
+    </KeyMetrics>
+  );
+}
+
+const KeyMetrics = styled('dl')`
+  display: grid;
+  grid-template-rows: max-content 1fr;
+  grid-template-columns: repeat(4, max-content);
+  grid-auto-flow: column;
+  gap: 0 ${space(3)};
+  align-items: center;
+  align-self: end;
+  color: ${p => p.theme.gray300};
+  margin: 0;
+
+  @media (min-width: ${p => p.theme.breakpoints.medium}) {
+    justify-self: flex-end;
+  }
+`;
+
+const KeyMetricLabel = styled('dt')`
+  font-size: ${p => p.theme.fontSizeMedium};
+`;
+
+const KeyMetricData = styled('dd')`
+  font-size: ${p => p.theme.fontSizeExtraLarge};
+  font-weight: normal;
+  display: flex;
+  align-items: center;
+  gap: ${space(1)};
+  line-height: ${p => p.theme.text.lineHeightBody};
+`;
+
+export default ReplayMetaData;

+ 26 - 0
static/app/components/replays/header/useErrorCountPerProject.tsx

@@ -0,0 +1,26 @@
+import {useMemo} from 'react';
+import countBy from 'lodash/countBy';
+
+import useProjects from 'sentry/utils/useProjects';
+import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types';
+
+type Props = {
+  replayErrors: ReplayError[];
+  replayRecord: ReplayRecord;
+};
+
+export default function useErrorCountByProject({replayErrors, replayRecord}: Props) {
+  const {projects} = useProjects();
+
+  return useMemo(() => {
+    return (
+      Object.entries(countBy(replayErrors, 'project.name'))
+        .map(([projectSlug, count]) => {
+          const project = projects.find(p => p.slug === projectSlug);
+          return {project, count};
+        })
+        // sort to prioritize the replay errors first
+        .sort(a => (a.project?.id !== replayRecord.project_id ? 1 : -1))
+    );
+  }, [projects, replayErrors, replayRecord]);
+}

+ 1 - 1
static/app/views/replays/detail/page.tsx

@@ -7,13 +7,13 @@ import DeleteButton from 'sentry/components/replays/header/deleteButton';
 import DetailsPageBreadcrumbs from 'sentry/components/replays/header/detailsPageBreadcrumbs';
 import FeedbackButton from 'sentry/components/replays/header/feedbackButton';
 import HeaderPlaceholder from 'sentry/components/replays/header/headerPlaceholder';
+import ReplayMetaData from 'sentry/components/replays/header/replayMetaData';
 import ShareButton from 'sentry/components/replays/shareButton';
 import {CrumbWalker, StringWalker} from 'sentry/components/replays/walker/urlWalker';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import type {Crumb} from 'sentry/types/breadcrumbs';
-import ReplayMetaData from 'sentry/views/replays/detail/replayMetaData';
 import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types';
 
 type Props = {

+ 0 - 194
static/app/views/replays/detail/replayMetaData.tsx

@@ -1,194 +0,0 @@
-import {Fragment, useMemo} from 'react';
-import styled from '@emotion/styled';
-import countBy from 'lodash/countBy';
-
-import Badge from 'sentry/components/badge';
-import ProjectBadge from 'sentry/components/idBadge/projectBadge';
-import Link from 'sentry/components/links/link';
-import ContextIcon from 'sentry/components/replays/contextIcon';
-import ErrorCount from 'sentry/components/replays/header/errorCount';
-import HeaderPlaceholder from 'sentry/components/replays/header/headerPlaceholder';
-import TimeSince from 'sentry/components/timeSince';
-import {IconCalendar} from 'sentry/icons';
-import {t} from 'sentry/locale';
-import {space} from 'sentry/styles/space';
-import {Project} from 'sentry/types';
-import {useLocation} from 'sentry/utils/useLocation';
-import useOrganization from 'sentry/utils/useOrganization';
-import useProjects from 'sentry/utils/useProjects';
-import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types';
-
-type Props = {
-  replayErrors: ReplayError[];
-  replayRecord: ReplayRecord | undefined;
-};
-
-function ReplayMetaData({replayRecord, replayErrors}: Props) {
-  const {pathname, query} = useLocation();
-  const organization = useOrganization();
-  const {projects} = useProjects();
-
-  const hasErrorTab = organization.features.includes('session-replay-errors-tab');
-
-  const errorsTabHref = hasErrorTab
-    ? {
-        pathname,
-        query: {
-          ...query,
-          t_main: 'errors',
-        },
-      }
-    : {
-        pathname,
-        query: {
-          ...query,
-          t_main: 'console',
-          f_c_logLevel: 'issue',
-          f_c_search: undefined,
-        },
-      };
-
-  const errorCountByProject = useMemo(
-    () =>
-      Object.entries(countBy(replayErrors, 'project.name'))
-        .map(([projectSlug, count]) => ({
-          project: projects.find(p => p.slug === projectSlug),
-          count,
-        }))
-        // sort to prioritize the replay errors first
-        .sort(a => (a.project?.id !== replayRecord?.project_id ? 1 : -1)),
-    [replayErrors, projects, replayRecord]
-  );
-
-  return (
-    <KeyMetrics>
-      <KeyMetricLabel>{t('OS')}</KeyMetricLabel>
-      <KeyMetricData>
-        <ContextIcon
-          name={replayRecord?.os.name ?? ''}
-          version={replayRecord?.os.version ?? undefined}
-        />
-      </KeyMetricData>
-
-      <KeyMetricLabel>{t('Browser')}</KeyMetricLabel>
-      <KeyMetricData>
-        <ContextIcon
-          name={replayRecord?.browser.name ?? ''}
-          version={replayRecord?.browser.version ?? undefined}
-        />
-      </KeyMetricData>
-
-      <KeyMetricLabel>{t('Start Time')}</KeyMetricLabel>
-      <KeyMetricData>
-        {replayRecord ? (
-          <Fragment>
-            <IconCalendar color="gray300" />
-            <TimeSince date={replayRecord.started_at} unitStyle="regular" />
-          </Fragment>
-        ) : (
-          <HeaderPlaceholder width="80px" height="16px" />
-        )}
-      </KeyMetricData>
-      <KeyMetricLabel>{t('Errors')}</KeyMetricLabel>
-      <KeyMetricData>
-        {replayRecord ? (
-          <StyledLink to={errorsTabHref}>
-            {errorCountByProject.length > 0 ? (
-              <Fragment>
-                {errorCountByProject.length < 3 ? (
-                  errorCountByProject.map(({project, count}, idx) => (
-                    <ErrorCount key={idx} countErrors={count} project={project} />
-                  ))
-                ) : (
-                  <StackedErrorCount errorCounts={errorCountByProject} />
-                )}
-              </Fragment>
-            ) : (
-              <ErrorCount countErrors={0} />
-            )}
-          </StyledLink>
-        ) : (
-          <HeaderPlaceholder width="80px" height="16px" />
-        )}
-      </KeyMetricData>
-    </KeyMetrics>
-  );
-}
-
-function StackedErrorCount({
-  errorCounts,
-}: {
-  errorCounts: Array<{count: number; project: Project | undefined}>;
-}) {
-  const projectCount = errorCounts.length - 2;
-  const totalErrors = errorCounts.reduce((acc, val) => acc + val.count, 0);
-  return (
-    <Fragment>
-      <StackedProjectBadges>
-        {errorCounts.slice(0, 2).map((v, idx) => {
-          if (!v.project) {
-            return null;
-          }
-
-          return <ProjectBadge key={idx} project={v.project} hideName disableLink />;
-        })}
-        <Badge>+{projectCount}</Badge>
-      </StackedProjectBadges>
-      <ErrorCount countErrors={totalErrors} hideIcon />
-    </Fragment>
-  );
-}
-
-const StackedProjectBadges = styled('div')`
-  display: flex;
-  align-items: center;
-  & * {
-    margin-left: 0;
-    margin-right: 0;
-    cursor: pointer;
-  }
-
-  & *:hover {
-    z-index: unset;
-  }
-
-  & > :not(:first-child) {
-    margin-left: -${space(0.5)};
-  }
-`;
-
-const KeyMetrics = styled('dl')`
-  display: grid;
-  grid-template-rows: max-content 1fr;
-  grid-template-columns: repeat(4, max-content);
-  grid-auto-flow: column;
-  gap: 0 ${space(3)};
-  align-items: center;
-  align-self: end;
-  color: ${p => p.theme.gray300};
-  margin: 0;
-
-  @media (min-width: ${p => p.theme.breakpoints.medium}) {
-    justify-self: flex-end;
-  }
-`;
-
-const KeyMetricLabel = styled('dt')`
-  font-size: ${p => p.theme.fontSizeMedium};
-`;
-
-const KeyMetricData = styled('dd')`
-  font-size: ${p => p.theme.fontSizeExtraLarge};
-  font-weight: normal;
-  display: flex;
-  align-items: center;
-  gap: ${space(1)};
-  line-height: ${p => p.theme.text.lineHeightBody};
-`;
-
-const StyledLink = styled(Link)`
-  display: flex;
-  gap: ${space(1)};
-`;
-
-export default ReplayMetaData;

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