index.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853
  1. import type React from 'react';
  2. import {
  3. useCallback,
  4. useEffect,
  5. useLayoutEffect,
  6. useMemo,
  7. useReducer,
  8. useRef,
  9. } from 'react';
  10. import {browserHistory} from 'react-router';
  11. import styled from '@emotion/styled';
  12. import type {Location} from 'history';
  13. import * as qs from 'query-string';
  14. import Alert from 'sentry/components/alert';
  15. import {Button} from 'sentry/components/button';
  16. import ButtonBar from 'sentry/components/buttonBar';
  17. import DiscoverButton from 'sentry/components/discoverButton';
  18. import useFeedbackWidget from 'sentry/components/feedback/widget/useFeedbackWidget';
  19. import * as Layout from 'sentry/components/layouts/thirds';
  20. import LoadingIndicator from 'sentry/components/loadingIndicator';
  21. import NoProjectMessage from 'sentry/components/noProjectMessage';
  22. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  23. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  24. import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
  25. import {IconClose} from 'sentry/icons';
  26. import {t, tct} from 'sentry/locale';
  27. import {space} from 'sentry/styles/space';
  28. import type {EventTransaction, Organization} from 'sentry/types';
  29. import {trackAnalytics} from 'sentry/utils/analytics';
  30. import EventView from 'sentry/utils/discover/eventView';
  31. import type {
  32. TraceFullDetailed,
  33. TraceMeta,
  34. TraceSplitResults,
  35. } from 'sentry/utils/performance/quickTrace/types';
  36. import {useApiQuery, type UseApiQueryResult} from 'sentry/utils/queryClient';
  37. import {decodeScalar} from 'sentry/utils/queryString';
  38. import useApi from 'sentry/utils/useApi';
  39. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  40. import {useLocation} from 'sentry/utils/useLocation';
  41. import useOnClickOutside from 'sentry/utils/useOnClickOutside';
  42. import useOrganization from 'sentry/utils/useOrganization';
  43. import {useParams} from 'sentry/utils/useParams';
  44. import useProjects from 'sentry/utils/useProjects';
  45. import {rovingTabIndexReducer} from 'sentry/views/performance/newTraceDetails/rovingTabIndex';
  46. import {
  47. searchInTraceTree,
  48. traceSearchReducer,
  49. } from 'sentry/views/performance/newTraceDetails/traceSearch';
  50. import {TraceSearchInput} from 'sentry/views/performance/newTraceDetails/traceSearchInput';
  51. import {
  52. traceTabsReducer,
  53. type TraceTabsReducerState,
  54. } from 'sentry/views/performance/newTraceDetails/traceTabs';
  55. import {VirtualizedViewManager} from 'sentry/views/performance/newTraceDetails/virtualizedViewManager';
  56. import {
  57. cancelAnimationTimeout,
  58. requestAnimationTimeout,
  59. } from '../../../utils/profiling/hooks/useVirtualizedTree/virtualizedTreeUtils';
  60. import Breadcrumb from '../breadcrumb';
  61. import TraceDrawer from './traceDrawer/traceDrawer';
  62. import {isTraceNode} from './guards';
  63. import Trace from './trace';
  64. import TraceHeader from './traceHeader';
  65. import {TraceTree, type TraceTreeNode} from './traceTree';
  66. import {useTrace} from './useTrace';
  67. import {useTraceMeta} from './useTraceMeta';
  68. const DOCUMENT_TITLE = [t('Trace')].join(' — ');
  69. function maybeFocusRow() {
  70. const focused_node = document.querySelector(".TraceRow[tabIndex='0']");
  71. if (
  72. focused_node &&
  73. 'focus' in focused_node &&
  74. typeof focused_node.focus === 'function'
  75. ) {
  76. focused_node.focus();
  77. }
  78. }
  79. export function TraceView() {
  80. const location = useLocation();
  81. const organization = useOrganization();
  82. const params = useParams<{traceSlug?: string}>();
  83. const traceSlug = params.traceSlug?.trim() ?? '';
  84. const queryParams = useMemo(() => {
  85. const normalizedParams = normalizeDateTimeParams(location.query, {
  86. allowAbsolutePageDatetime: true,
  87. });
  88. const start = decodeScalar(normalizedParams.start);
  89. const end = decodeScalar(normalizedParams.end);
  90. const statsPeriod = decodeScalar(normalizedParams.statsPeriod);
  91. return {start, end, statsPeriod, useSpans: 1};
  92. }, [location.query]);
  93. const traceEventView = useMemo(() => {
  94. const {start, end, statsPeriod} = queryParams;
  95. return EventView.fromSavedQuery({
  96. id: undefined,
  97. name: `Events with Trace ID ${traceSlug}`,
  98. fields: ['title', 'event.type', 'project', 'timestamp'],
  99. orderby: '-timestamp',
  100. query: `trace:${traceSlug}`,
  101. projects: [ALL_ACCESS_PROJECTS],
  102. version: 2,
  103. start,
  104. end,
  105. range: statsPeriod,
  106. });
  107. }, [queryParams, traceSlug]);
  108. const trace = useTrace();
  109. const meta = useTraceMeta();
  110. return (
  111. <SentryDocumentTitle title={DOCUMENT_TITLE} orgSlug={organization.slug}>
  112. <NoProjectMessage organization={organization}>
  113. <TraceViewContent
  114. status={trace.status}
  115. trace={trace.data ?? null}
  116. traceSlug={traceSlug}
  117. organization={organization}
  118. location={location}
  119. traceEventView={traceEventView}
  120. metaResults={meta}
  121. />
  122. </NoProjectMessage>
  123. </SentryDocumentTitle>
  124. );
  125. }
  126. const TRACE_TAB: TraceTabsReducerState['tabs'][0] = {
  127. node: 'Trace',
  128. };
  129. const STATIC_DRAWER_TABS: TraceTabsReducerState['tabs'] = [TRACE_TAB];
  130. type TraceViewContentProps = {
  131. location: Location;
  132. metaResults: UseApiQueryResult<TraceMeta | null, any>;
  133. organization: Organization;
  134. status: UseApiQueryResult<any, any>['status'];
  135. trace: TraceSplitResults<TraceFullDetailed> | null;
  136. traceEventView: EventView;
  137. traceSlug: string;
  138. };
  139. function TraceViewContent(props: TraceViewContentProps) {
  140. const api = useApi();
  141. const {projects} = useProjects();
  142. const [tracePreferences, setTracePreferences] = useLocalStorageState<{
  143. drawer: number;
  144. layout: 'drawer right' | 'drawer bottom' | 'drawer left';
  145. list_width: number;
  146. }>('trace_preferences', {
  147. layout: 'drawer bottom',
  148. list_width: 0.66,
  149. drawer: 0,
  150. });
  151. const rootEvent = useRootEvent(props.trace);
  152. const viewManager = useMemo(() => {
  153. return new VirtualizedViewManager({
  154. list: {width: tracePreferences.list_width},
  155. span_list: {width: 1 - tracePreferences.list_width},
  156. });
  157. // We only care about initial state when we initialize the view manager
  158. // eslint-disable-next-line react-hooks/exhaustive-deps
  159. }, []);
  160. useEffect(() => {
  161. function onDividerResizeEnd(list_width: number) {
  162. setTracePreferences(previousPreferences => {
  163. return {...previousPreferences, list_width};
  164. });
  165. }
  166. viewManager.on('divider resize end', onDividerResizeEnd);
  167. return () => {
  168. viewManager.off('divider resize end', onDividerResizeEnd);
  169. };
  170. }, [viewManager, setTracePreferences]);
  171. const loadingTraceRef = useRef<TraceTree | null>(null);
  172. const tree = useMemo(() => {
  173. if (props.status === 'error') {
  174. const errorTree = TraceTree.Error(
  175. {
  176. project_slug: projects?.[0]?.slug ?? '',
  177. event_id: props.traceSlug,
  178. },
  179. loadingTraceRef.current
  180. );
  181. return errorTree;
  182. }
  183. if (
  184. props.trace?.transactions.length === 0 &&
  185. props.trace?.orphan_errors.length === 0
  186. ) {
  187. return TraceTree.Empty();
  188. }
  189. if (props.status === 'loading' || rootEvent.status === 'loading') {
  190. const loadingTrace =
  191. loadingTraceRef.current ??
  192. TraceTree.Loading(
  193. {
  194. project_slug: projects?.[0]?.slug ?? '',
  195. event_id: props.traceSlug,
  196. },
  197. loadingTraceRef.current
  198. );
  199. loadingTraceRef.current = loadingTrace;
  200. return loadingTrace;
  201. }
  202. if (props.trace && rootEvent.status === 'success') {
  203. return TraceTree.FromTrace(props.trace, rootEvent.data);
  204. }
  205. throw new Error('Invalid trace state');
  206. }, [
  207. props.traceSlug,
  208. props.trace,
  209. props.status,
  210. projects,
  211. rootEvent.data,
  212. rootEvent.status,
  213. ]);
  214. const [rovingTabIndexState, rovingTabIndexDispatch] = useReducer(
  215. rovingTabIndexReducer,
  216. {
  217. index: null,
  218. items: null,
  219. node: null,
  220. }
  221. );
  222. useLayoutEffect(() => {
  223. return rovingTabIndexDispatch({
  224. type: 'initialize',
  225. items: tree.list.length - 1,
  226. index: null,
  227. node: null,
  228. });
  229. }, [tree.list.length]);
  230. const initialQuery = useMemo((): string | undefined => {
  231. const query = qs.parse(location.search);
  232. if (typeof query.search === 'string') {
  233. return query.search;
  234. }
  235. return undefined;
  236. // We only want to decode on load
  237. // eslint-disable-next-line react-hooks/exhaustive-deps
  238. }, []);
  239. const [searchState, searchDispatch] = useReducer(traceSearchReducer, {
  240. query: initialQuery,
  241. resultIteratorIndex: undefined,
  242. resultIndex: undefined,
  243. results: undefined,
  244. status: undefined,
  245. resultsLookup: new Map(),
  246. });
  247. const [tabs, tabsDispatch] = useReducer(traceTabsReducer, {
  248. tabs: STATIC_DRAWER_TABS,
  249. current: STATIC_DRAWER_TABS[0] ?? null,
  250. last_clicked: null,
  251. });
  252. const onRowClick = useCallback(
  253. (
  254. node: TraceTreeNode<TraceTree.NodeValue> | null,
  255. event: React.MouseEvent<HTMLElement> | null
  256. ) => {
  257. if (!node) {
  258. tabsDispatch({type: 'clear clicked tab'});
  259. return;
  260. }
  261. if (isTraceNode(node)) {
  262. tabsDispatch({type: 'activate tab', payload: TRACE_TAB.node});
  263. maybeFocusRow();
  264. return;
  265. }
  266. tabsDispatch({type: 'activate tab', payload: node, pin_previous: event?.metaKey});
  267. maybeFocusRow();
  268. },
  269. []
  270. );
  271. const searchingRaf = useRef<{id: number | null} | null>(null);
  272. const onTraceSearch = useCallback(
  273. (query: string) => {
  274. if (searchingRaf.current?.id) {
  275. window.cancelAnimationFrame(searchingRaf.current.id);
  276. }
  277. searchingRaf.current = searchInTraceTree(query, tree, results => {
  278. searchDispatch({
  279. type: 'set results',
  280. results: results[0],
  281. resultsLookup: results[1],
  282. });
  283. });
  284. },
  285. [tree]
  286. );
  287. const onSearchChange = useCallback(
  288. (event: React.ChangeEvent<HTMLInputElement>) => {
  289. if (!event.currentTarget.value) {
  290. searchDispatch({type: 'clear query'});
  291. return;
  292. }
  293. onTraceSearch(event.currentTarget.value);
  294. searchDispatch({type: 'set query', query: event.currentTarget.value});
  295. },
  296. [onTraceSearch]
  297. );
  298. const onSearchClear = useCallback(() => {
  299. searchDispatch({type: 'clear query'});
  300. }, []);
  301. const onSearchKeyDown = useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
  302. if (event.key === 'ArrowDown') {
  303. searchDispatch({type: 'go to next match'});
  304. } else {
  305. if (event.key === 'ArrowUp') {
  306. searchDispatch({type: 'go to previous match'});
  307. }
  308. }
  309. }, []);
  310. const onNextSearchClick = useCallback(() => {
  311. searchDispatch({type: 'go to next match'});
  312. }, []);
  313. const onPreviousSearchClick = useCallback(() => {
  314. searchDispatch({type: 'go to previous match'});
  315. }, []);
  316. const breadcrumbTransaction = useMemo(() => {
  317. return {
  318. project: rootEvent.data?.projectID ?? '',
  319. name: rootEvent.data?.title ?? '',
  320. };
  321. }, [rootEvent.data]);
  322. const trackOpenInDiscover = useCallback(() => {
  323. trackAnalytics('performance_views.trace_view.open_in_discover', {
  324. organization: props.organization,
  325. });
  326. }, [props.organization]);
  327. const syncQuery = useMemo(() => {
  328. return {search: searchState.query};
  329. }, [searchState.query]);
  330. useQueryParamSync(syncQuery);
  331. const onOutsideClick = useCallback(() => {
  332. if (tree.type !== 'trace') {
  333. // Dont clear the URL in case the trace is still loading or failed for some reason,
  334. // we want to keep the eventId in the URL so the user can share the URL with support
  335. return;
  336. }
  337. // we will drop eventId such that after users clicks outside and shares the URL,
  338. // we will no longer scroll to the event or node
  339. const {
  340. node: _node,
  341. eventId: _eventId,
  342. ...queryParamsWithoutNode
  343. } = qs.parse(location.search);
  344. browserHistory.push({
  345. pathname: location.pathname,
  346. query: queryParamsWithoutNode,
  347. });
  348. // eslint-disable-next-line react-hooks/exhaustive-deps
  349. }, []);
  350. const traceContainerRef = useRef<HTMLElement | null>(null);
  351. useOnClickOutside(traceContainerRef, onOutsideClick);
  352. const previouslyFocusedIndexRef = useRef<number | null>(null);
  353. const scrollToNode = useCallback(
  354. (
  355. node: TraceTreeNode<TraceTree.NodeValue>
  356. ): Promise<{index: number; node: TraceTreeNode<TraceTree.NodeValue>} | null> => {
  357. previouslyFocusedIndexRef.current = null;
  358. return viewManager
  359. .scrollToPath(tree, [...node.path], () => void 0, {
  360. api,
  361. organization: props.organization,
  362. })
  363. .then(maybeNode => {
  364. if (!maybeNode) {
  365. return null;
  366. }
  367. viewManager.onScrollEndOutOfBoundsCheck();
  368. rovingTabIndexDispatch({
  369. type: 'set index',
  370. index: maybeNode.index,
  371. node: maybeNode.node,
  372. });
  373. if (searchState.query) {
  374. onTraceSearch(searchState.query);
  375. }
  376. // Re-focus the row if in view as well
  377. maybeFocusRow();
  378. return maybeNode;
  379. });
  380. },
  381. [api, props.organization, tree, viewManager, searchState, onTraceSearch]
  382. );
  383. const onLayoutChange = useCallback(
  384. (layout: 'drawer bottom' | 'drawer left' | 'drawer right') => {
  385. setTracePreferences(previousPreferences => {
  386. return {...previousPreferences, layout, drawer: 0};
  387. });
  388. },
  389. [setTracePreferences]
  390. );
  391. const resizeAnimationTimeoutRef = useRef<{id: number} | null>(null);
  392. const onDrawerResize = useCallback(
  393. (size: number) => {
  394. if (resizeAnimationTimeoutRef.current !== null) {
  395. cancelAnimationTimeout(resizeAnimationTimeoutRef.current);
  396. }
  397. resizeAnimationTimeoutRef.current = requestAnimationTimeout(() => {
  398. setTracePreferences(previousPreferences => {
  399. return {
  400. ...previousPreferences,
  401. drawer:
  402. size /
  403. (previousPreferences.layout === 'drawer bottom'
  404. ? window.innerHeight
  405. : window.innerWidth),
  406. };
  407. });
  408. }, 1000);
  409. },
  410. [setTracePreferences]
  411. );
  412. const initialDrawerSize = useMemo(() => {
  413. if (tracePreferences.drawer < 0) {
  414. return 0;
  415. }
  416. const base =
  417. tracePreferences.layout === 'drawer bottom'
  418. ? window.innerHeight
  419. : window.innerWidth;
  420. return tracePreferences.drawer * base;
  421. }, [tracePreferences.drawer, tracePreferences.layout]);
  422. const scrollQueueRef = useRef<{eventId?: string; path?: TraceTree.NodePath[]} | null>(
  423. null
  424. );
  425. const onResetZoom = useCallback(() => {
  426. viewManager.resetZoom();
  427. }, [viewManager]);
  428. const [dismiss, setDismissed] = useLocalStorageState('trace-view-dismissed', false);
  429. const onTabScrollToNode = useCallback(
  430. (node: TraceTreeNode<TraceTree.NodeValue>) => {
  431. scrollToNode(node).then(maybeNode => {
  432. if (!maybeNode) {
  433. return;
  434. }
  435. viewManager.scrollRowIntoViewHorizontally(maybeNode.node, 0, 12, 'exact');
  436. if (maybeNode.node.space) {
  437. viewManager.animateViewTo(maybeNode.node.space);
  438. }
  439. });
  440. },
  441. [scrollToNode, viewManager]
  442. );
  443. return (
  444. <TraceExternalLayout>
  445. {dismiss ? null : (
  446. <Alert
  447. type="info"
  448. system
  449. trailingItems={
  450. <Button
  451. aria-label="dismiss"
  452. priority="link"
  453. size="xs"
  454. icon={<IconClose />}
  455. onClick={() => setDismissed(true)}
  456. />
  457. }
  458. >
  459. {tct(
  460. 'Events now provide richer context by linking directly inside traces. Read [why] we are doing this and what it enables.',
  461. {
  462. why: (
  463. <a href="https://docs.sentry.io/product/sentry-basics/concepts/tracing/trace-view/">
  464. {t('why')}
  465. </a>
  466. ),
  467. }
  468. )}
  469. </Alert>
  470. )}
  471. <Layout.Header>
  472. <Layout.HeaderContent>
  473. <Breadcrumb
  474. organization={props.organization}
  475. location={props.location}
  476. transaction={breadcrumbTransaction}
  477. traceSlug={props.traceSlug}
  478. />
  479. </Layout.HeaderContent>
  480. <Layout.HeaderActions>
  481. <ButtonBar gap={1}>
  482. <DiscoverButton
  483. size="sm"
  484. to={props.traceEventView.getResultsViewUrlTarget(props.organization.slug)}
  485. onClick={trackOpenInDiscover}
  486. >
  487. {t('Open in Discover')}
  488. </DiscoverButton>
  489. </ButtonBar>
  490. </Layout.HeaderActions>
  491. </Layout.Header>
  492. <TraceInnerLayout>
  493. <TraceHeader
  494. tree={tree}
  495. rootEventResults={rootEvent}
  496. metaResults={props.metaResults}
  497. organization={props.organization}
  498. traces={props.trace}
  499. traceID={props.traceSlug}
  500. />
  501. <TraceToolbar>
  502. <TraceSearchInput
  503. query={searchState.query}
  504. status={searchState.status}
  505. onChange={onSearchChange}
  506. onSearchClear={onSearchClear}
  507. onKeyDown={onSearchKeyDown}
  508. onNextSearchClick={onNextSearchClick}
  509. onPreviousSearchClick={onPreviousSearchClick}
  510. resultCount={searchState.results?.length}
  511. resultIteratorIndex={searchState.resultIteratorIndex}
  512. />
  513. <Button size="xs" onClick={onResetZoom}>
  514. {t('Reset Zoom')}
  515. </Button>
  516. </TraceToolbar>
  517. <TraceGrid
  518. layout={tracePreferences.layout}
  519. ref={r => (traceContainerRef.current = r)}
  520. >
  521. <Trace
  522. trace={tree}
  523. trace_id={props.traceSlug}
  524. roving_dispatch={rovingTabIndexDispatch}
  525. roving_state={rovingTabIndexState}
  526. search_dispatch={searchDispatch}
  527. search_state={searchState}
  528. onRowClick={onRowClick}
  529. scrollQueueRef={scrollQueueRef}
  530. searchResultsIteratorIndex={searchState.resultIndex}
  531. searchResultsMap={searchState.resultsLookup}
  532. onTraceSearch={onTraceSearch}
  533. previouslyFocusedIndexRef={previouslyFocusedIndexRef}
  534. manager={viewManager}
  535. />
  536. {tree.type === 'loading' ? (
  537. <TraceLoading />
  538. ) : tree.type === 'error' ? (
  539. <TraceError />
  540. ) : tree.type === 'empty' ? (
  541. <TraceEmpty />
  542. ) : scrollQueueRef.current ? (
  543. <TraceLoading />
  544. ) : null}
  545. <TraceDrawer
  546. tabs={tabs}
  547. trace={tree}
  548. manager={viewManager}
  549. scrollToNode={onTabScrollToNode}
  550. tabsDispatch={tabsDispatch}
  551. drawerSize={initialDrawerSize}
  552. layout={tracePreferences.layout}
  553. onLayoutChange={onLayoutChange}
  554. onDrawerResize={onDrawerResize}
  555. rootEventResults={rootEvent}
  556. organization={props.organization}
  557. location={props.location}
  558. traces={props.trace}
  559. traceEventView={props.traceEventView}
  560. />
  561. </TraceGrid>
  562. </TraceInnerLayout>
  563. </TraceExternalLayout>
  564. );
  565. }
  566. function useQueryParamSync(query: Record<string, string | undefined>) {
  567. const previousQueryRef = useRef<Record<string, string | undefined>>(query);
  568. const syncStateTimeoutRef = useRef<number | null>(null);
  569. useEffect(() => {
  570. const keys = Object.keys(query);
  571. const previousKeys = Object.keys(previousQueryRef.current);
  572. if (
  573. keys.length === previousKeys.length &&
  574. keys.every(key => {
  575. return query[key] === previousQueryRef.current[key];
  576. })
  577. ) {
  578. previousQueryRef.current = query;
  579. return;
  580. }
  581. if (syncStateTimeoutRef.current !== null) {
  582. window.clearTimeout(syncStateTimeoutRef.current);
  583. }
  584. previousQueryRef.current = query;
  585. syncStateTimeoutRef.current = window.setTimeout(() => {
  586. browserHistory.replace({
  587. pathname: location.pathname,
  588. query: {
  589. ...qs.parse(location.search),
  590. ...previousQueryRef.current,
  591. },
  592. });
  593. }, 1000);
  594. }, [query]);
  595. }
  596. function useRootEvent(trace: TraceSplitResults<TraceFullDetailed> | null) {
  597. const root = trace?.transactions[0] || trace?.orphan_errors[0];
  598. const organization = useOrganization();
  599. return useApiQuery<EventTransaction>(
  600. [
  601. `/organizations/${organization.slug}/events/${root?.project_slug}:${root?.event_id}/`,
  602. {
  603. query: {
  604. referrer: 'trace-details-summary',
  605. },
  606. },
  607. ],
  608. {
  609. staleTime: 0,
  610. enabled: !!trace && !!root,
  611. }
  612. );
  613. }
  614. const TraceExternalLayout = styled('div')`
  615. display: flex;
  616. flex-direction: column;
  617. flex: 1 1 100%;
  618. ~ footer {
  619. display: none;
  620. }
  621. `;
  622. const TraceInnerLayout = styled('div')`
  623. display: flex;
  624. flex-direction: column;
  625. flex: 1 1 100%;
  626. padding: ${space(2)} ${space(2)} 0 ${space(2)};
  627. background-color: ${p => p.theme.background};
  628. `;
  629. const TraceToolbar = styled('div')`
  630. flex-grow: 0;
  631. display: grid;
  632. grid-template-columns: 1fr min-content;
  633. gap: ${space(1)};
  634. `;
  635. const TraceGrid = styled('div')<{
  636. layout: 'drawer bottom' | 'drawer left' | 'drawer right';
  637. }>`
  638. box-shadow: 0 0 0 1px ${p => p.theme.border};
  639. flex: 1 1 100%;
  640. display: grid;
  641. border-top-left-radius: ${p => p.theme.borderRadius};
  642. border-top-right-radius: ${p => p.theme.borderRadius};
  643. overflow: hidden;
  644. position: relative;
  645. /* false positive for grid layout */
  646. /* stylelint-disable */
  647. grid-template-areas: ${p =>
  648. p.layout === 'drawer bottom'
  649. ? `
  650. 'trace'
  651. 'drawer'
  652. `
  653. : p.layout === 'drawer left'
  654. ? `'drawer trace'`
  655. : `'trace drawer'`};
  656. grid-template-columns: ${p =>
  657. p.layout === 'drawer bottom'
  658. ? '1fr'
  659. : p.layout === 'drawer left'
  660. ? 'min-content 1fr'
  661. : '1fr min-content'};
  662. grid-template-rows: 1fr auto;
  663. `;
  664. const LoadingContainer = styled('div')<{animate: boolean; error?: boolean}>`
  665. display: flex;
  666. justify-content: center;
  667. align-items: center;
  668. flex-direction: column;
  669. left: 50%;
  670. top: 50%;
  671. position: absolute;
  672. height: auto;
  673. font-size: ${p => p.theme.fontSizeMedium};
  674. color: ${p => p.theme.gray300};
  675. z-index: 30;
  676. padding: 24px;
  677. background-color: ${p => p.theme.background};
  678. border-radius: ${p => p.theme.borderRadius};
  679. border: 1px solid ${p => p.theme.border};
  680. transform-origin: 50% 50%;
  681. transform: translate(-50%, -50%);
  682. animation: ${p =>
  683. p.animate
  684. ? `${p.error ? 'showLoadingContainerShake' : 'showLoadingContainer'} 300ms cubic-bezier(0.61, 1, 0.88, 1) forwards`
  685. : 'none'};
  686. @keyframes showLoadingContainer {
  687. from {
  688. opacity: 0.6;
  689. transform: scale(0.99) translate(-50%, -50%);
  690. }
  691. to {
  692. opacity: 1;
  693. transform: scale(1) translate(-50%, -50%);
  694. }
  695. }
  696. @keyframes showLoadingContainerShake {
  697. 0% {
  698. transform: translate(-50%, -50%);
  699. }
  700. 25% {
  701. transform: translate(-51%, -50%);
  702. }
  703. 75% {
  704. transform: translate(-49%, -50%);
  705. }
  706. 100% {
  707. transform: translate(-50%, -50%);
  708. }
  709. }
  710. `;
  711. function TraceLoading() {
  712. return (
  713. // Dont flash the animation on load because it's annoying
  714. <LoadingContainer animate={false}>
  715. <NoMarginIndicator size={24}>
  716. <div>{t('Assembling the trace')}</div>
  717. </NoMarginIndicator>
  718. </LoadingContainer>
  719. );
  720. }
  721. function TraceError() {
  722. const linkref = useRef<HTMLAnchorElement>(null);
  723. const feedback = useFeedbackWidget({buttonRef: linkref});
  724. return (
  725. <LoadingContainer animate error>
  726. <div>{t('Ughhhhh, we failed to load your trace...')}</div>
  727. <div>
  728. {t('Seeing this often? Send us ')}
  729. {feedback ? (
  730. <a href="#" ref={linkref}>
  731. {t('feedback')}
  732. </a>
  733. ) : (
  734. <a href="mailto:support@sentry.io?subject=Trace%20fails%20to%20load">
  735. {t('feedback')}
  736. </a>
  737. )}
  738. </div>
  739. </LoadingContainer>
  740. );
  741. }
  742. function TraceEmpty() {
  743. const linkref = useRef<HTMLAnchorElement>(null);
  744. const feedback = useFeedbackWidget({buttonRef: linkref});
  745. return (
  746. <LoadingContainer animate>
  747. <div>{t('This trace does not contain any data?!')}</div>
  748. <div>
  749. {t('Seeing this often? Send us ')}
  750. {feedback ? (
  751. <a href="#" ref={linkref}>
  752. {t('feedback')}
  753. </a>
  754. ) : (
  755. <a href="mailto:support@sentry.io?subject=Trace%20does%20not%20contain%20data">
  756. {t('feedback')}
  757. </a>
  758. )}
  759. </div>
  760. </LoadingContainer>
  761. );
  762. }
  763. const NoMarginIndicator = styled(LoadingIndicator)`
  764. margin: 0;
  765. `;