tracesTable.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import {Fragment, useCallback, useMemo, useState} from 'react';
  2. import debounce from 'lodash/debounce';
  3. import {Button} from 'sentry/components/button';
  4. import Count from 'sentry/components/count';
  5. import EmptyStateWarning, {EmptyStreamWrapper} from 'sentry/components/emptyStateWarning';
  6. import ExternalLink from 'sentry/components/links/externalLink';
  7. import LoadingIndicator from 'sentry/components/loadingIndicator';
  8. import PerformanceDuration from 'sentry/components/performanceDuration';
  9. import {IconChevron} from 'sentry/icons/iconChevron';
  10. import {IconWarning} from 'sentry/icons/iconWarning';
  11. import {t, tct} from 'sentry/locale';
  12. import {defined} from 'sentry/utils';
  13. import {trackAnalytics} from 'sentry/utils/analytics';
  14. import {decodeList} from 'sentry/utils/queryString';
  15. import {useLocation} from 'sentry/utils/useLocation';
  16. import useOrganization from 'sentry/utils/useOrganization';
  17. import usePageFilters from 'sentry/utils/usePageFilters';
  18. import useProjects from 'sentry/utils/useProjects';
  19. import type {TraceResult} from './hooks/useTraces';
  20. import {
  21. Description,
  22. ProjectBadgeWrapper,
  23. ProjectsRenderer,
  24. SpanTimeRenderer,
  25. TraceBreakdownRenderer,
  26. TraceIdRenderer,
  27. TraceIssuesRenderer,
  28. } from './fieldRenderers';
  29. import {SpanTable} from './spansTable';
  30. import {
  31. BreakdownPanelItem,
  32. EmptyStateText,
  33. EmptyValueContainer,
  34. StyledPanel,
  35. StyledPanelHeader,
  36. StyledPanelItem,
  37. TracePanelContent,
  38. WrappingText,
  39. } from './styles';
  40. import {areQueriesEmpty} from './utils';
  41. const SPAN_PROPS_DOCS_URL =
  42. 'https://docs.sentry.io/concepts/search/searchable-properties/spans/';
  43. interface TracesTableProps {
  44. isEmpty: boolean;
  45. isError: boolean;
  46. isLoading: boolean;
  47. queries: string[];
  48. data?: TraceResult[];
  49. }
  50. export function TracesTable({
  51. isEmpty,
  52. isError,
  53. isLoading,
  54. queries,
  55. data,
  56. }: TracesTableProps) {
  57. return (
  58. <StyledPanel>
  59. <TracePanelContent>
  60. <StyledPanelHeader align="left" lightText>
  61. {t('Trace ID')}
  62. </StyledPanelHeader>
  63. <StyledPanelHeader align="left" lightText>
  64. {t('Trace Root')}
  65. </StyledPanelHeader>
  66. <StyledPanelHeader align="right" lightText>
  67. {areQueriesEmpty(queries) ? t('Total Spans') : t('Matching Spans')}
  68. </StyledPanelHeader>
  69. <StyledPanelHeader align="left" lightText>
  70. {t('Timeline')}
  71. </StyledPanelHeader>
  72. <StyledPanelHeader align="right" lightText>
  73. {t('Duration')}
  74. </StyledPanelHeader>
  75. <StyledPanelHeader align="right" lightText>
  76. {t('Timestamp')}
  77. </StyledPanelHeader>
  78. <StyledPanelHeader align="right" lightText>
  79. {t('Issues')}
  80. </StyledPanelHeader>
  81. {isLoading && (
  82. <StyledPanelItem span={7} overflow>
  83. <LoadingIndicator />
  84. </StyledPanelItem>
  85. )}
  86. {isError && ( // TODO: need an error state
  87. <StyledPanelItem span={7} overflow>
  88. <EmptyStreamWrapper>
  89. <IconWarning color="gray300" size="lg" />
  90. </EmptyStreamWrapper>
  91. </StyledPanelItem>
  92. )}
  93. {isEmpty && (
  94. <StyledPanelItem span={7} overflow>
  95. <EmptyStateWarning withIcon>
  96. <EmptyStateText size="fontSizeExtraLarge">
  97. {t('No trace results found')}
  98. </EmptyStateText>
  99. <EmptyStateText size="fontSizeMedium">
  100. {tct('Try adjusting your filters or refer to [docSearchProps].', {
  101. docSearchProps: (
  102. <ExternalLink href={SPAN_PROPS_DOCS_URL}>
  103. {t('docs for search properties')}
  104. </ExternalLink>
  105. ),
  106. })}
  107. </EmptyStateText>
  108. </EmptyStateWarning>
  109. </StyledPanelItem>
  110. )}
  111. {data?.map((trace, i) => (
  112. <TraceRow
  113. key={trace.trace}
  114. trace={trace}
  115. defaultExpanded={!areQueriesEmpty(queries) && i === 0}
  116. />
  117. ))}
  118. </TracePanelContent>
  119. </StyledPanel>
  120. );
  121. }
  122. function TraceRow({defaultExpanded, trace}: {defaultExpanded; trace: TraceResult}) {
  123. const {selection} = usePageFilters();
  124. const {projects} = useProjects();
  125. const [expanded, setExpanded] = useState<boolean>(defaultExpanded);
  126. const [highlightedSliceName, _setHighlightedSliceName] = useState('');
  127. const location = useLocation();
  128. const organization = useOrganization();
  129. const queries = useMemo(() => {
  130. return decodeList(location.query.query);
  131. }, [location.query.query]);
  132. const setHighlightedSliceName = useMemo(
  133. () =>
  134. debounce(sliceName => _setHighlightedSliceName(sliceName), 100, {
  135. leading: true,
  136. }),
  137. [_setHighlightedSliceName]
  138. );
  139. const onClickExpand = useCallback(() => setExpanded(e => !e), [setExpanded]);
  140. const selectedProjects = useMemo(() => {
  141. const selectedProjectIds = new Set(
  142. selection.projects.map(project => project.toString())
  143. );
  144. return new Set(
  145. projects
  146. .filter(project => selectedProjectIds.has(project.id))
  147. .map(project => project.slug)
  148. );
  149. }, [projects, selection.projects]);
  150. const traceProjects = useMemo(() => {
  151. const seenProjects: Set<string> = new Set();
  152. const leadingProjects: string[] = [];
  153. const trailingProjects: string[] = [];
  154. for (let i = 0; i < trace.breakdowns.length; i++) {
  155. const project = trace.breakdowns[i].project;
  156. if (!defined(project) || seenProjects.has(project)) {
  157. continue;
  158. }
  159. seenProjects.add(project);
  160. // Priotize projects that are selected in the page filters
  161. if (selectedProjects.has(project)) {
  162. leadingProjects.push(project);
  163. } else {
  164. trailingProjects.push(project);
  165. }
  166. }
  167. return [...leadingProjects, ...trailingProjects];
  168. }, [selectedProjects, trace]);
  169. return (
  170. <Fragment>
  171. <StyledPanelItem align="center" center onClick={onClickExpand}>
  172. <Button
  173. icon={<IconChevron size="xs" direction={expanded ? 'down' : 'right'} />}
  174. aria-label={t('Toggle trace details')}
  175. aria-expanded={expanded}
  176. size="zero"
  177. borderless
  178. onClick={() =>
  179. trackAnalytics('trace_explorer.toggle_trace_details', {
  180. organization,
  181. expanded,
  182. })
  183. }
  184. />
  185. <TraceIdRenderer
  186. traceId={trace.trace}
  187. timestamp={trace.end}
  188. onClick={() =>
  189. trackAnalytics('trace_explorer.open_trace', {
  190. organization,
  191. })
  192. }
  193. location={location}
  194. />
  195. </StyledPanelItem>
  196. <StyledPanelItem align="left" overflow>
  197. <Description>
  198. <ProjectBadgeWrapper>
  199. <ProjectsRenderer
  200. projectSlugs={
  201. traceProjects.length > 0
  202. ? traceProjects
  203. : trace.project
  204. ? [trace.project]
  205. : []
  206. }
  207. />
  208. </ProjectBadgeWrapper>
  209. {trace.name ? (
  210. <WrappingText>{trace.name}</WrappingText>
  211. ) : (
  212. <EmptyValueContainer>{t('Missing Trace Root')}</EmptyValueContainer>
  213. )}
  214. </Description>
  215. </StyledPanelItem>
  216. <StyledPanelItem align="right">
  217. {areQueriesEmpty(queries) ? (
  218. <Count value={trace.numSpans} />
  219. ) : (
  220. tct('[numerator][space]of[space][denominator]', {
  221. numerator: <Count value={trace.matchingSpans} />,
  222. denominator: <Count value={trace.numSpans} />,
  223. space: <Fragment>&nbsp;</Fragment>,
  224. })
  225. )}
  226. </StyledPanelItem>
  227. <BreakdownPanelItem
  228. align="right"
  229. highlightedSliceName={highlightedSliceName}
  230. onMouseLeave={() => setHighlightedSliceName('')}
  231. >
  232. <TraceBreakdownRenderer
  233. trace={trace}
  234. setHighlightedSliceName={setHighlightedSliceName}
  235. />
  236. </BreakdownPanelItem>
  237. <StyledPanelItem align="right">
  238. <PerformanceDuration milliseconds={trace.duration} abbreviation />
  239. </StyledPanelItem>
  240. <StyledPanelItem align="right">
  241. <SpanTimeRenderer timestamp={trace.end} tooltipShowSeconds />
  242. </StyledPanelItem>
  243. <StyledPanelItem align="right">
  244. <TraceIssuesRenderer
  245. trace={trace}
  246. onClick={() =>
  247. trackAnalytics('trace_explorer.open_in_issues', {
  248. organization,
  249. })
  250. }
  251. />
  252. </StyledPanelItem>
  253. {expanded && (
  254. <SpanTable trace={trace} setHighlightedSliceName={setHighlightedSliceName} />
  255. )}
  256. </Fragment>
  257. );
  258. }