traceHeader.tsx 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. import {Fragment, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  4. import {CopyToClipboardButton} from 'sentry/components/copyToClipboardButton';
  5. import Link from 'sentry/components/links/link';
  6. import LoadingIndicator from 'sentry/components/loadingIndicator';
  7. import {Tooltip} from 'sentry/components/tooltip';
  8. import {IconPlay} from 'sentry/icons';
  9. import {t, tn} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import type {EventTransaction, Organization} from 'sentry/types';
  12. import getDuration from 'sentry/utils/duration/getDuration';
  13. import {getShortEventId} from 'sentry/utils/events';
  14. import type {
  15. TraceErrorOrIssue,
  16. TraceFullDetailed,
  17. TraceMeta,
  18. TraceSplitResults,
  19. } from 'sentry/utils/performance/quickTrace/types';
  20. import type {UseApiQueryResult} from 'sentry/utils/queryClient';
  21. import type RequestError from 'sentry/utils/requestError/requestError';
  22. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  23. import {BrowserDisplay} from '../transactionDetails/eventMetas';
  24. import {MetaData} from '../transactionDetails/styles';
  25. import {TraceDrawerComponents} from './traceDrawer/details/styles';
  26. import type {TraceTree} from './traceModels/traceTree';
  27. import {isTraceNode} from './guards';
  28. function TraceHeaderEmptyTrace() {
  29. return (
  30. <TraceHeaderContainer>
  31. <TraceHeaderRow textAlign="left">
  32. <MetaData
  33. headingText={t('User')}
  34. tooltipText=""
  35. bodyText={'\u2014'}
  36. subtext={null}
  37. />
  38. <MetaData
  39. headingText={t('Browser')}
  40. tooltipText=""
  41. bodyText={'\u2014'}
  42. subtext={null}
  43. />
  44. </TraceHeaderRow>
  45. <TraceHeaderRow textAlign="right">
  46. <GuideAnchor target="trace_view_guide_breakdown">
  47. <MetaData
  48. headingText={t('Events')}
  49. tooltipText=""
  50. bodyText={'\u2014'}
  51. subtext={null}
  52. />
  53. </GuideAnchor>
  54. <MetaData
  55. headingText={t('Issues')}
  56. tooltipText=""
  57. bodyText={'\u2014'}
  58. subtext={null}
  59. />
  60. <MetaData
  61. headingText={t('Total Duration')}
  62. tooltipText=""
  63. bodyText={'\u2014'}
  64. subtext={null}
  65. />
  66. </TraceHeaderRow>
  67. </TraceHeaderContainer>
  68. );
  69. }
  70. type TraceHeaderProps = {
  71. metaResults: UseApiQueryResult<TraceMeta | null, any>;
  72. organization: Organization;
  73. rootEventResults: UseApiQueryResult<EventTransaction, RequestError>;
  74. traceID: string | undefined;
  75. traces: TraceSplitResults<TraceFullDetailed> | null;
  76. tree: TraceTree;
  77. };
  78. export function TraceHeader({
  79. metaResults,
  80. rootEventResults,
  81. traces,
  82. organization,
  83. tree,
  84. traceID,
  85. }: TraceHeaderProps) {
  86. const traceNode = tree.root.children[0];
  87. const replay_id = rootEventResults?.data?.contexts?.replay?.replay_id;
  88. const showLoadingIndicator =
  89. (rootEventResults.isLoading && rootEventResults.fetchStatus !== 'idle') ||
  90. metaResults.isLoading;
  91. const uniqueErrorIssues = useMemo(() => {
  92. if (!traceNode) {
  93. return [];
  94. }
  95. const unique: TraceErrorOrIssue[] = [];
  96. const seenIssues: Set<number> = new Set();
  97. for (const issue of traceNode.errors) {
  98. if (seenIssues.has(issue.issue_id)) {
  99. continue;
  100. }
  101. seenIssues.add(issue.issue_id);
  102. unique.push(issue);
  103. }
  104. return unique;
  105. }, [traceNode]);
  106. const uniquePerformanceIssues = useMemo(() => {
  107. if (!traceNode) {
  108. return [];
  109. }
  110. const unique: TraceErrorOrIssue[] = [];
  111. const seenIssues: Set<number> = new Set();
  112. for (const issue of traceNode.performance_issues) {
  113. if (seenIssues.has(issue.issue_id)) {
  114. continue;
  115. }
  116. seenIssues.add(issue.issue_id);
  117. unique.push(issue);
  118. }
  119. return unique;
  120. }, [traceNode]);
  121. const uniqueIssuesCount = uniqueErrorIssues.length + uniquePerformanceIssues.length;
  122. if (traces?.transactions.length === 0 && traces.orphan_errors.length === 0) {
  123. return <TraceHeaderEmptyTrace />;
  124. }
  125. if (!(traceNode && isTraceNode(traceNode))) {
  126. throw new Error('Expected a trace node');
  127. }
  128. return (
  129. <TraceHeaderContainer>
  130. <TraceHeaderRow textAlign="left">
  131. <MetaData
  132. headingText={t('User')}
  133. tooltipText=""
  134. bodyText={
  135. showLoadingIndicator ? (
  136. <LoadingIndicator size={20} mini />
  137. ) : (
  138. rootEventResults?.data?.user?.email ??
  139. rootEventResults?.data?.user?.name ??
  140. '\u2014'
  141. )
  142. }
  143. subtext={null}
  144. />
  145. <MetaData
  146. headingText={t('Browser')}
  147. tooltipText=""
  148. bodyText={
  149. showLoadingIndicator ? (
  150. <LoadingIndicator size={20} mini />
  151. ) : rootEventResults?.data ? (
  152. <BrowserDisplay event={rootEventResults?.data} showVersion />
  153. ) : (
  154. '\u2014'
  155. )
  156. }
  157. subtext={null}
  158. />
  159. <MetaData
  160. headingText={t('Trace')}
  161. tooltipText=""
  162. bodyText={
  163. showLoadingIndicator ? (
  164. <LoadingIndicator size={20} mini />
  165. ) : traceID ? (
  166. <Fragment>
  167. {getShortEventId(traceID)}
  168. <CopyToClipboardButton
  169. borderless
  170. size="zero"
  171. iconSize="xs"
  172. text={traceID}
  173. />
  174. </Fragment>
  175. ) : (
  176. '\u2014'
  177. )
  178. }
  179. subtext={null}
  180. />
  181. {replay_id && (
  182. <MetaData
  183. headingText={t('Replay')}
  184. tooltipText=""
  185. bodyText={
  186. <Link
  187. to={normalizeUrl(
  188. `/organizations/${organization.slug}/replays/${replay_id}/`
  189. )}
  190. >
  191. <ReplayLinkBody>
  192. {getShortEventId(replay_id)}
  193. <IconPlay size="xs" />
  194. </ReplayLinkBody>
  195. </Link>
  196. }
  197. subtext={null}
  198. />
  199. )}
  200. </TraceHeaderRow>
  201. <TraceHeaderRow textAlign="right">
  202. <GuideAnchor target="trace_view_guide_breakdown">
  203. <MetaData
  204. headingText={t('Events')}
  205. tooltipText=""
  206. bodyText={
  207. metaResults.isLoading ? (
  208. <LoadingIndicator size={20} mini />
  209. ) : metaResults.data ? (
  210. metaResults.data.transactions + metaResults.data.errors
  211. ) : (
  212. '\u2014'
  213. )
  214. }
  215. subtext={null}
  216. />
  217. </GuideAnchor>
  218. <MetaData
  219. headingText={t('Issues')}
  220. tooltipText=""
  221. bodyText={
  222. <Tooltip
  223. title={
  224. uniqueIssuesCount > 0 ? (
  225. <Fragment>
  226. <div>
  227. {tn('%s error issue', '%s error issues', uniqueErrorIssues.length)}
  228. </div>
  229. <div>
  230. {tn(
  231. '%s performance issue',
  232. '%s performance issues',
  233. uniquePerformanceIssues.length
  234. )}
  235. </div>
  236. </Fragment>
  237. ) : null
  238. }
  239. showUnderline
  240. position="bottom"
  241. >
  242. {metaResults.isLoading ? (
  243. <LoadingIndicator size={20} mini />
  244. ) : uniqueIssuesCount > 0 ? (
  245. <TraceDrawerComponents.IssuesLink>
  246. {uniqueIssuesCount}
  247. </TraceDrawerComponents.IssuesLink>
  248. ) : (
  249. '\u2014'
  250. )}
  251. </Tooltip>
  252. }
  253. subtext={null}
  254. />
  255. <MetaData
  256. headingText={t('Total Duration')}
  257. tooltipText=""
  258. bodyText={
  259. metaResults.isLoading ? (
  260. <LoadingIndicator size={20} mini />
  261. ) : traceNode.space?.[1] ? (
  262. getDuration(traceNode.space[1] / 1000, 2, true)
  263. ) : (
  264. '\u2014'
  265. )
  266. }
  267. subtext={null}
  268. />
  269. </TraceHeaderRow>
  270. </TraceHeaderContainer>
  271. );
  272. }
  273. const FlexBox = styled('div')`
  274. display: flex;
  275. align-items: center;
  276. `;
  277. const TraceHeaderContainer = styled(FlexBox)`
  278. justify-content: space-between;
  279. background-color: ${p => p.theme.background};
  280. padding: ${space(2)} ${space(2)} 0 ${space(2)};
  281. `;
  282. const TraceHeaderRow = styled(FlexBox)<{textAlign: 'left' | 'right'}>`
  283. gap: ${space(2)};
  284. text-align: ${p => p.textAlign};
  285. `;
  286. const ReplayLinkBody = styled(FlexBox)`
  287. gap: ${space(0.25)};
  288. `;