index.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import {
  2. Fragment,
  3. useCallback,
  4. useLayoutEffect,
  5. useMemo,
  6. useReducer,
  7. useState,
  8. } from 'react';
  9. import type {Location} from 'history';
  10. import ButtonBar from 'sentry/components/buttonBar';
  11. import DiscoverButton from 'sentry/components/discoverButton';
  12. import * as Layout from 'sentry/components/layouts/thirds';
  13. import NoProjectMessage from 'sentry/components/noProjectMessage';
  14. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  15. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  16. import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
  17. import {t} from 'sentry/locale';
  18. import type {EventTransaction, Organization} from 'sentry/types';
  19. import {trackAnalytics} from 'sentry/utils/analytics';
  20. import EventView from 'sentry/utils/discover/eventView';
  21. import TraceMetaQuery, {
  22. type TraceMetaQueryChildrenProps,
  23. } from 'sentry/utils/performance/quickTrace/traceMetaQuery';
  24. import type {
  25. TraceFullDetailed,
  26. TraceSplitResults,
  27. } from 'sentry/utils/performance/quickTrace/types';
  28. import {useApiQuery} from 'sentry/utils/queryClient';
  29. import {decodeScalar} from 'sentry/utils/queryString';
  30. import {useLocation} from 'sentry/utils/useLocation';
  31. import useOrganization from 'sentry/utils/useOrganization';
  32. import {useParams} from 'sentry/utils/useParams';
  33. import useProjects from 'sentry/utils/useProjects';
  34. import {rovingTabIndexReducer} from 'sentry/views/performance/newTraceDetails/rovingTabIndex';
  35. import Breadcrumb from '../breadcrumb';
  36. import TraceDetailPanel from './newTraceDetailPanel';
  37. import Trace from './trace';
  38. import {TraceFooter} from './traceFooter';
  39. import TraceHeader from './traceHeader';
  40. import {TraceTree, type TraceTreeNode} from './traceTree';
  41. import TraceWarnings from './traceWarnings';
  42. import {useTrace} from './useTrace';
  43. const DOCUMENT_TITLE = [t('Trace Details'), t('Performance')].join(' — ');
  44. function maybeFocusRow() {
  45. const focused_node = document.querySelector(".TraceRow[tabIndex='0']");
  46. if (
  47. focused_node &&
  48. 'focus' in focused_node &&
  49. typeof focused_node.focus === 'function'
  50. ) {
  51. focused_node.focus();
  52. }
  53. }
  54. export function TraceView() {
  55. const location = useLocation();
  56. const organization = useOrganization();
  57. const params = useParams<{traceSlug?: string}>();
  58. const traceSlug = params.traceSlug?.trim() ?? '';
  59. const queryParams = useMemo(() => {
  60. const normalizedParams = normalizeDateTimeParams(location.query, {
  61. allowAbsolutePageDatetime: true,
  62. });
  63. const start = decodeScalar(normalizedParams.start);
  64. const end = decodeScalar(normalizedParams.end);
  65. const statsPeriod = decodeScalar(normalizedParams.statsPeriod);
  66. return {start, end, statsPeriod, useSpans: 1};
  67. }, [location.query]);
  68. const traceEventView = useMemo(() => {
  69. const {start, end, statsPeriod} = queryParams;
  70. return EventView.fromSavedQuery({
  71. id: undefined,
  72. name: `Events with Trace ID ${traceSlug}`,
  73. fields: ['title', 'event.type', 'project', 'timestamp'],
  74. orderby: '-timestamp',
  75. query: `trace:${traceSlug}`,
  76. projects: [ALL_ACCESS_PROJECTS],
  77. version: 2,
  78. start,
  79. end,
  80. range: statsPeriod,
  81. });
  82. }, [queryParams, traceSlug]);
  83. const trace = useTrace();
  84. return (
  85. <SentryDocumentTitle title={DOCUMENT_TITLE} orgSlug={organization.slug}>
  86. <Layout.Page>
  87. <NoProjectMessage organization={organization}>
  88. <TraceMetaQuery
  89. location={location}
  90. orgSlug={organization.slug}
  91. traceId={traceSlug}
  92. start={queryParams.start}
  93. end={queryParams.end}
  94. statsPeriod={queryParams.statsPeriod}
  95. >
  96. {metaResults => (
  97. <TraceViewContent
  98. status={trace.status}
  99. trace={trace.data}
  100. traceSlug={traceSlug}
  101. organization={organization}
  102. location={location}
  103. traceEventView={traceEventView}
  104. metaResults={metaResults}
  105. />
  106. )}
  107. </TraceMetaQuery>
  108. </NoProjectMessage>
  109. </Layout.Page>
  110. </SentryDocumentTitle>
  111. );
  112. }
  113. type TraceViewContentProps = {
  114. location: Location;
  115. metaResults: TraceMetaQueryChildrenProps;
  116. organization: Organization;
  117. status: 'pending' | 'resolved' | 'error' | 'initial';
  118. trace: TraceSplitResults<TraceFullDetailed> | null;
  119. traceEventView: EventView;
  120. traceSlug: string;
  121. };
  122. function TraceViewContent(props: TraceViewContentProps) {
  123. const {projects} = useProjects();
  124. const root = props.trace?.transactions?.[0];
  125. const rootEvent = useApiQuery<EventTransaction>(
  126. [
  127. `/organizations/${props.organization.slug}/events/${root?.project_slug}:${root?.event_id}/`,
  128. {
  129. query: {
  130. referrer: 'trace-details-summary',
  131. },
  132. },
  133. ],
  134. {
  135. staleTime: 0,
  136. enabled: (props.trace?.transactions.length ?? 0) > 0,
  137. }
  138. );
  139. const tree = useMemo(() => {
  140. if (props.status === 'pending' || rootEvent.status !== 'success') {
  141. return TraceTree.Loading({
  142. project_slug: projects?.[0]?.slug ?? '',
  143. event_id: props.traceSlug,
  144. });
  145. }
  146. if (props.trace) {
  147. return TraceTree.FromTrace(props.trace, rootEvent.data);
  148. }
  149. return TraceTree.Empty();
  150. }, [props.traceSlug, props.trace, props.status, projects, rootEvent]);
  151. const traceType = useMemo(() => {
  152. if (props.status !== 'resolved' || !tree) {
  153. return null;
  154. }
  155. return TraceTree.GetTraceType(tree.root);
  156. }, [props.status, tree]);
  157. const [state, dispatch] = useReducer(rovingTabIndexReducer, {
  158. index: null,
  159. items: null,
  160. node: null,
  161. });
  162. useLayoutEffect(() => {
  163. return dispatch({
  164. type: 'initialize',
  165. items: tree.list.length - 1,
  166. index: null,
  167. node: null,
  168. });
  169. }, [tree.list.length]);
  170. const [detailNode, setDetailNode] = useState<TraceTreeNode<TraceTree.NodeValue> | null>(
  171. null
  172. );
  173. const onDetailClose = useCallback(() => {
  174. setDetailNode(null);
  175. maybeFocusRow();
  176. }, []);
  177. const onSetDetailNode = useCallback(
  178. (node: TraceTreeNode<TraceTree.NodeValue> | null) => {
  179. setDetailNode(prevNode => {
  180. return prevNode === node ? null : node;
  181. });
  182. maybeFocusRow();
  183. },
  184. []
  185. );
  186. return (
  187. <Fragment>
  188. <Layout.Header>
  189. <Layout.HeaderContent>
  190. <Breadcrumb
  191. organization={props.organization}
  192. location={props.location}
  193. transaction={{
  194. project: rootEvent.data?.projectID ?? '',
  195. name: rootEvent.data?.title ?? '',
  196. }}
  197. traceSlug={props.traceSlug}
  198. />
  199. <Layout.Title data-test-id="trace-header">
  200. {t('Trace ID: %s', props.traceSlug)}
  201. </Layout.Title>
  202. </Layout.HeaderContent>
  203. <Layout.HeaderActions>
  204. <ButtonBar gap={1}>
  205. <DiscoverButton
  206. size="sm"
  207. to={props.traceEventView.getResultsViewUrlTarget(props.organization.slug)}
  208. onClick={() => {
  209. trackAnalytics('performance_views.trace_view.open_in_discover', {
  210. organization: props.organization,
  211. });
  212. }}
  213. >
  214. {t('Open in Discover')}
  215. </DiscoverButton>
  216. </ButtonBar>
  217. </Layout.HeaderActions>
  218. </Layout.Header>
  219. <Layout.Body>
  220. <Layout.Main fullWidth>
  221. {traceType ? <TraceWarnings type={traceType} /> : null}
  222. <TraceHeader
  223. rootEventResults={rootEvent}
  224. metaResults={props.metaResults}
  225. organization={props.organization}
  226. traces={props.trace}
  227. />
  228. <Trace
  229. trace={tree}
  230. trace_id={props.traceSlug}
  231. roving_dispatch={dispatch}
  232. roving_state={state}
  233. setDetailNode={onSetDetailNode}
  234. />
  235. <TraceFooter
  236. rootEventResults={rootEvent}
  237. organization={props.organization}
  238. location={props.location}
  239. traces={props.trace}
  240. traceEventView={props.traceEventView}
  241. />
  242. <TraceDetailPanel node={detailNode} onClose={onDetailClose} />
  243. </Layout.Main>
  244. </Layout.Body>
  245. </Fragment>
  246. );
  247. }