traceHeader.tsx 7.6 KB

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