|
@@ -1,6 +1,9 @@
|
|
|
-import {Fragment} from 'react';
|
|
|
+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';
|
|
@@ -9,15 +12,19 @@ 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 type {ReplayRecord} from 'sentry/views/replays/types';
|
|
|
+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}: Props) {
|
|
|
+function ReplayMetaData({replayRecord, replayErrors}: Props) {
|
|
|
const {pathname, query} = useLocation();
|
|
|
+ const {projects} = useProjects();
|
|
|
|
|
|
const errorsTabHref = {
|
|
|
pathname,
|
|
@@ -29,6 +36,18 @@ function ReplayMetaData({replayRecord}: Props) {
|
|
|
},
|
|
|
};
|
|
|
|
|
|
+ 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>
|
|
@@ -62,7 +81,19 @@ function ReplayMetaData({replayRecord}: Props) {
|
|
|
<KeyMetricData>
|
|
|
{replayRecord ? (
|
|
|
<StyledLink to={errorsTabHref}>
|
|
|
- <ErrorCount countErrors={replayRecord.count_errors} />
|
|
|
+ {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" />
|
|
@@ -72,6 +103,48 @@ function ReplayMetaData({replayRecord}: Props) {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+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;
|