replayMetaData.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import {Fragment, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import countBy from 'lodash/countBy';
  4. import Badge from 'sentry/components/badge';
  5. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  6. import Link from 'sentry/components/links/link';
  7. import ContextIcon from 'sentry/components/replays/contextIcon';
  8. import ErrorCount from 'sentry/components/replays/header/errorCount';
  9. import HeaderPlaceholder from 'sentry/components/replays/header/headerPlaceholder';
  10. import TimeSince from 'sentry/components/timeSince';
  11. import {IconCalendar} from 'sentry/icons';
  12. import {t} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import {Project} from 'sentry/types';
  15. import {useLocation} from 'sentry/utils/useLocation';
  16. import useProjects from 'sentry/utils/useProjects';
  17. import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types';
  18. type Props = {
  19. replayErrors: ReplayError[];
  20. replayRecord: ReplayRecord | undefined;
  21. };
  22. function ReplayMetaData({replayRecord, replayErrors}: Props) {
  23. const {pathname, query} = useLocation();
  24. const {projects} = useProjects();
  25. const errorsTabHref = {
  26. pathname,
  27. query: {
  28. ...query,
  29. t_main: 'console',
  30. f_c_logLevel: 'issue',
  31. f_c_search: undefined,
  32. },
  33. };
  34. const errorCountByProject = useMemo(
  35. () =>
  36. Object.entries(countBy(replayErrors, 'project.name'))
  37. .map(([projectSlug, count]) => ({
  38. project: projects.find(p => p.slug === projectSlug),
  39. count,
  40. }))
  41. // sort to prioritize the replay errors first
  42. .sort(a => (a.project?.id !== replayRecord?.project_id ? 1 : -1)),
  43. [replayErrors, projects, replayRecord]
  44. );
  45. return (
  46. <KeyMetrics>
  47. <KeyMetricLabel>{t('OS')}</KeyMetricLabel>
  48. <KeyMetricData>
  49. <ContextIcon
  50. name={replayRecord?.os.name ?? ''}
  51. version={replayRecord?.os.version ?? undefined}
  52. />
  53. </KeyMetricData>
  54. <KeyMetricLabel>{t('Browser')}</KeyMetricLabel>
  55. <KeyMetricData>
  56. <ContextIcon
  57. name={replayRecord?.browser.name ?? ''}
  58. version={replayRecord?.browser.version ?? undefined}
  59. />
  60. </KeyMetricData>
  61. <KeyMetricLabel>{t('Start Time')}</KeyMetricLabel>
  62. <KeyMetricData>
  63. {replayRecord ? (
  64. <Fragment>
  65. <IconCalendar color="gray300" />
  66. <TimeSince date={replayRecord.started_at} unitStyle="regular" />
  67. </Fragment>
  68. ) : (
  69. <HeaderPlaceholder width="80px" height="16px" />
  70. )}
  71. </KeyMetricData>
  72. <KeyMetricLabel>{t('Errors')}</KeyMetricLabel>
  73. <KeyMetricData>
  74. {replayRecord ? (
  75. <StyledLink to={errorsTabHref}>
  76. {errorCountByProject.length > 0 ? (
  77. <Fragment>
  78. {errorCountByProject.length < 3 ? (
  79. errorCountByProject.map(({project, count}, idx) => (
  80. <ErrorCount key={idx} countErrors={count} project={project} />
  81. ))
  82. ) : (
  83. <StackedErrorCount errorCounts={errorCountByProject} />
  84. )}
  85. </Fragment>
  86. ) : (
  87. <ErrorCount countErrors={0} />
  88. )}
  89. </StyledLink>
  90. ) : (
  91. <HeaderPlaceholder width="80px" height="16px" />
  92. )}
  93. </KeyMetricData>
  94. </KeyMetrics>
  95. );
  96. }
  97. function StackedErrorCount({
  98. errorCounts,
  99. }: {
  100. errorCounts: Array<{count: number; project: Project | undefined}>;
  101. }) {
  102. const projectCount = errorCounts.length - 2;
  103. const totalErrors = errorCounts.reduce((acc, val) => acc + val.count, 0);
  104. return (
  105. <Fragment>
  106. <StackedProjectBadges>
  107. {errorCounts.slice(0, 2).map((v, idx) => {
  108. if (!v.project) {
  109. return null;
  110. }
  111. return <ProjectBadge key={idx} project={v.project} hideName disableLink />;
  112. })}
  113. <Badge>+{projectCount}</Badge>
  114. </StackedProjectBadges>
  115. <ErrorCount countErrors={totalErrors} hideIcon />
  116. </Fragment>
  117. );
  118. }
  119. const StackedProjectBadges = styled('div')`
  120. display: flex;
  121. align-items: center;
  122. & * {
  123. margin-left: 0;
  124. margin-right: 0;
  125. cursor: pointer;
  126. }
  127. & *:hover {
  128. z-index: unset;
  129. }
  130. & > :not(:first-child) {
  131. margin-left: -${space(0.5)};
  132. }
  133. `;
  134. const KeyMetrics = styled('dl')`
  135. display: grid;
  136. grid-template-rows: max-content 1fr;
  137. grid-template-columns: repeat(4, max-content);
  138. grid-auto-flow: column;
  139. gap: 0 ${space(3)};
  140. align-items: center;
  141. align-self: end;
  142. color: ${p => p.theme.gray300};
  143. margin: 0;
  144. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  145. justify-self: flex-end;
  146. }
  147. `;
  148. const KeyMetricLabel = styled('dt')`
  149. font-size: ${p => p.theme.fontSizeMedium};
  150. `;
  151. const KeyMetricData = styled('dd')`
  152. font-size: ${p => p.theme.fontSizeExtraLarge};
  153. font-weight: normal;
  154. display: flex;
  155. align-items: center;
  156. gap: ${space(1)};
  157. line-height: ${p => p.theme.text.lineHeightBody};
  158. `;
  159. const StyledLink = styled(Link)`
  160. display: flex;
  161. gap: ${space(1)};
  162. `;
  163. export default ReplayMetaData;