index.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848
  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 Details'), t('Performance')].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 (props.status === 'loading' || rootEvent.status === 'loading') {
  184. const loadingTrace =
  185. loadingTraceRef.current ??
  186. TraceTree.Loading(
  187. {
  188. project_slug: projects?.[0]?.slug ?? '',
  189. event_id: props.traceSlug,
  190. },
  191. loadingTraceRef.current
  192. );
  193. loadingTraceRef.current = loadingTrace;
  194. return loadingTrace;
  195. }
  196. if (props.trace && rootEvent.status === 'success') {
  197. return TraceTree.FromTrace(props.trace, rootEvent.data);
  198. }
  199. return TraceTree.Empty();
  200. }, [
  201. props.traceSlug,
  202. props.trace,
  203. props.status,
  204. projects,
  205. rootEvent.data,
  206. rootEvent.status,
  207. ]);
  208. const [rovingTabIndexState, rovingTabIndexDispatch] = useReducer(
  209. rovingTabIndexReducer,
  210. {
  211. index: null,
  212. items: null,
  213. node: null,
  214. }
  215. );
  216. useLayoutEffect(() => {
  217. return rovingTabIndexDispatch({
  218. type: 'initialize',
  219. items: tree.list.length - 1,
  220. index: null,
  221. node: null,
  222. });
  223. }, [tree.list.length]);
  224. const initialQuery = useMemo((): string | undefined => {
  225. const query = qs.parse(location.search);
  226. if (typeof query.search === 'string') {
  227. return query.search;
  228. }
  229. return undefined;
  230. // We only want to decode on load
  231. // eslint-disable-next-line react-hooks/exhaustive-deps
  232. }, []);
  233. const [searchState, searchDispatch] = useReducer(traceSearchReducer, {
  234. query: initialQuery,
  235. resultIteratorIndex: undefined,
  236. resultIndex: undefined,
  237. results: undefined,
  238. status: undefined,
  239. resultsLookup: new Map(),
  240. });
  241. const [tabs, tabsDispatch] = useReducer(traceTabsReducer, {
  242. tabs: STATIC_DRAWER_TABS,
  243. current: STATIC_DRAWER_TABS[0] ?? null,
  244. last_clicked: null,
  245. });
  246. const onRowClick = useCallback(
  247. (
  248. node: TraceTreeNode<TraceTree.NodeValue> | null,
  249. event: React.MouseEvent<HTMLElement> | null
  250. ) => {
  251. if (!node) {
  252. tabsDispatch({type: 'clear clicked tab'});
  253. return;
  254. }
  255. if (isTraceNode(node)) {
  256. tabsDispatch({type: 'activate tab', payload: TRACE_TAB.node});
  257. maybeFocusRow();
  258. return;
  259. }
  260. tabsDispatch({type: 'activate tab', payload: node, pin_previous: event?.metaKey});
  261. maybeFocusRow();
  262. },
  263. []
  264. );
  265. const searchingRaf = useRef<{id: number | null} | null>(null);
  266. const onTraceSearch = useCallback(
  267. (query: string) => {
  268. if (searchingRaf.current?.id) {
  269. window.cancelAnimationFrame(searchingRaf.current.id);
  270. }
  271. searchingRaf.current = searchInTraceTree(query, tree, results => {
  272. searchDispatch({
  273. type: 'set results',
  274. results: results[0],
  275. resultsLookup: results[1],
  276. });
  277. });
  278. },
  279. [tree]
  280. );
  281. const onSearchChange = useCallback(
  282. (event: React.ChangeEvent<HTMLInputElement>) => {
  283. if (!event.currentTarget.value) {
  284. searchDispatch({type: 'clear query'});
  285. return;
  286. }
  287. onTraceSearch(event.currentTarget.value);
  288. searchDispatch({type: 'set query', query: event.currentTarget.value});
  289. },
  290. [onTraceSearch]
  291. );
  292. const onSearchClear = useCallback(() => {
  293. searchDispatch({type: 'clear query'});
  294. }, []);
  295. const onSearchKeyDown = useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
  296. if (event.key === 'ArrowDown') {
  297. searchDispatch({type: 'go to next match'});
  298. } else {
  299. if (event.key === 'ArrowUp') {
  300. searchDispatch({type: 'go to previous match'});
  301. }
  302. }
  303. }, []);
  304. const onNextSearchClick = useCallback(() => {
  305. searchDispatch({type: 'go to next match'});
  306. }, []);
  307. const onPreviousSearchClick = useCallback(() => {
  308. searchDispatch({type: 'go to previous match'});
  309. }, []);
  310. const breadcrumbTransaction = useMemo(() => {
  311. return {
  312. project: rootEvent.data?.projectID ?? '',
  313. name: rootEvent.data?.title ?? '',
  314. };
  315. }, [rootEvent.data]);
  316. const trackOpenInDiscover = useCallback(() => {
  317. trackAnalytics('performance_views.trace_view.open_in_discover', {
  318. organization: props.organization,
  319. });
  320. }, [props.organization]);
  321. const syncQuery = useMemo(() => {
  322. return {search: searchState.query};
  323. }, [searchState.query]);
  324. useQueryParamSync(syncQuery);
  325. const onOutsideClick = useCallback(() => {
  326. if (tree.type !== 'trace') {
  327. // Dont clear the URL in case the trace is still loading or failed for some reason,
  328. // we want to keep the eventId in the URL so the user can share the URL with support
  329. return;
  330. }
  331. // we will drop eventId such that after users clicks outside and shares the URL,
  332. // we will no longer scroll to the event or node
  333. const {
  334. node: _node,
  335. eventId: _eventId,
  336. ...queryParamsWithoutNode
  337. } = qs.parse(location.search);
  338. browserHistory.push({
  339. pathname: location.pathname,
  340. query: queryParamsWithoutNode,
  341. });
  342. // eslint-disable-next-line react-hooks/exhaustive-deps
  343. }, []);
  344. const traceContainerRef = useRef<HTMLElement | null>(null);
  345. useOnClickOutside(traceContainerRef, onOutsideClick);
  346. const previouslyFocusedIndexRef = useRef<number | null>(null);
  347. const scrollToNode = useCallback(
  348. (
  349. node: TraceTreeNode<TraceTree.NodeValue>
  350. ): Promise<{index: number; node: TraceTreeNode<TraceTree.NodeValue>} | null> => {
  351. previouslyFocusedIndexRef.current = null;
  352. return viewManager
  353. .scrollToPath(tree, [...node.path], () => void 0, {
  354. api,
  355. organization: props.organization,
  356. })
  357. .then(maybeNode => {
  358. if (!maybeNode) {
  359. return null;
  360. }
  361. viewManager.onScrollEndOutOfBoundsCheck();
  362. rovingTabIndexDispatch({
  363. type: 'set index',
  364. index: maybeNode.index,
  365. node: maybeNode.node,
  366. });
  367. if (searchState.query) {
  368. onTraceSearch(searchState.query);
  369. }
  370. // Re-focus the row if in view as well
  371. maybeFocusRow();
  372. return maybeNode;
  373. });
  374. },
  375. [api, props.organization, tree, viewManager, searchState, onTraceSearch]
  376. );
  377. const onLayoutChange = useCallback(
  378. (layout: 'drawer bottom' | 'drawer left' | 'drawer right') => {
  379. setTracePreferences(previousPreferences => {
  380. return {...previousPreferences, layout, drawer: 0};
  381. });
  382. },
  383. [setTracePreferences]
  384. );
  385. const resizeAnimationTimeoutRef = useRef<{id: number} | null>(null);
  386. const onDrawerResize = useCallback(
  387. (size: number) => {
  388. if (resizeAnimationTimeoutRef.current !== null) {
  389. cancelAnimationTimeout(resizeAnimationTimeoutRef.current);
  390. }
  391. resizeAnimationTimeoutRef.current = requestAnimationTimeout(() => {
  392. setTracePreferences(previousPreferences => {
  393. return {
  394. ...previousPreferences,
  395. drawer:
  396. size /
  397. (previousPreferences.layout === 'drawer bottom'
  398. ? window.innerHeight
  399. : window.innerWidth),
  400. };
  401. });
  402. }, 1000);
  403. },
  404. [setTracePreferences]
  405. );
  406. const initialDrawerSize = useMemo(() => {
  407. if (tracePreferences.drawer < 0) {
  408. return 0;
  409. }
  410. const base =
  411. tracePreferences.layout === 'drawer bottom'
  412. ? window.innerHeight
  413. : window.innerWidth;
  414. return tracePreferences.drawer * base;
  415. }, [tracePreferences.drawer, tracePreferences.layout]);
  416. const scrollQueueRef = useRef<{eventId?: string; path?: TraceTree.NodePath[]} | null>(
  417. null
  418. );
  419. const onResetZoom = useCallback(() => {
  420. viewManager.resetZoom();
  421. }, [viewManager]);
  422. const [dismiss, setDismissed] = useLocalStorageState('trace-view-dismissed', false);
  423. const onTabScrollToNode = useCallback(
  424. (node: TraceTreeNode<TraceTree.NodeValue>) => {
  425. scrollToNode(node).then(maybeNode => {
  426. if (!maybeNode) {
  427. return;
  428. }
  429. viewManager.scrollRowIntoViewHorizontally(maybeNode.node, 0, 12, 'exact');
  430. if (maybeNode.node.space) {
  431. viewManager.animateViewTo(maybeNode.node.space);
  432. }
  433. });
  434. },
  435. [scrollToNode, viewManager]
  436. );
  437. return (
  438. <TraceExternalLayout>
  439. {dismiss ? null : (
  440. <Alert
  441. type="info"
  442. system
  443. trailingItems={
  444. <Button
  445. aria-label="dismiss"
  446. priority="link"
  447. size="xs"
  448. icon={<IconClose />}
  449. onClick={() => setDismissed(true)}
  450. />
  451. }
  452. >
  453. {tct(
  454. 'Events now provide richer context by linking directly inside traces. Read [why] we are doing this and what it enables.',
  455. {
  456. why: (
  457. <a href="https://docs.sentry.io/product/sentry-basics/concepts/tracing/trace-view/">
  458. {t('why')}
  459. </a>
  460. ),
  461. }
  462. )}
  463. </Alert>
  464. )}
  465. <Layout.Header>
  466. <Layout.HeaderContent>
  467. <Breadcrumb
  468. organization={props.organization}
  469. location={props.location}
  470. transaction={breadcrumbTransaction}
  471. traceSlug={props.traceSlug}
  472. />
  473. <Layout.Title data-test-id="trace-header">
  474. {t('Trace ID: %s', props.traceSlug)}
  475. </Layout.Title>
  476. </Layout.HeaderContent>
  477. <Layout.HeaderActions>
  478. <ButtonBar gap={1}>
  479. <DiscoverButton
  480. size="sm"
  481. to={props.traceEventView.getResultsViewUrlTarget(props.organization.slug)}
  482. onClick={trackOpenInDiscover}
  483. >
  484. {t('Open in Discover')}
  485. </DiscoverButton>
  486. </ButtonBar>
  487. </Layout.HeaderActions>
  488. </Layout.Header>
  489. <TraceInnerLayout>
  490. <TraceHeader
  491. tree={tree}
  492. rootEventResults={rootEvent}
  493. metaResults={props.metaResults}
  494. organization={props.organization}
  495. traces={props.trace}
  496. />
  497. <TraceToolbar>
  498. <TraceSearchInput
  499. query={searchState.query}
  500. status={searchState.status}
  501. onChange={onSearchChange}
  502. onSearchClear={onSearchClear}
  503. onKeyDown={onSearchKeyDown}
  504. onNextSearchClick={onNextSearchClick}
  505. onPreviousSearchClick={onPreviousSearchClick}
  506. resultCount={searchState.results?.length}
  507. resultIteratorIndex={searchState.resultIteratorIndex}
  508. />
  509. <Button size="xs" onClick={onResetZoom}>
  510. {t('Reset Zoom')}
  511. </Button>
  512. </TraceToolbar>
  513. <TraceGrid
  514. layout={tracePreferences.layout}
  515. ref={r => (traceContainerRef.current = r)}
  516. >
  517. <Trace
  518. trace={tree}
  519. trace_id={props.traceSlug}
  520. roving_dispatch={rovingTabIndexDispatch}
  521. roving_state={rovingTabIndexState}
  522. search_dispatch={searchDispatch}
  523. search_state={searchState}
  524. onRowClick={onRowClick}
  525. scrollQueueRef={scrollQueueRef}
  526. searchResultsIteratorIndex={searchState.resultIndex}
  527. searchResultsMap={searchState.resultsLookup}
  528. onTraceSearch={onTraceSearch}
  529. previouslyFocusedIndexRef={previouslyFocusedIndexRef}
  530. manager={viewManager}
  531. />
  532. {tree.type === 'loading' ? (
  533. <TraceLoading />
  534. ) : tree.type === 'error' ? (
  535. <TraceError />
  536. ) : tree.type === 'empty' ? (
  537. <TraceEmpty />
  538. ) : scrollQueueRef.current ? (
  539. <TraceLoading />
  540. ) : null}
  541. <TraceDrawer
  542. tabs={tabs}
  543. trace={tree}
  544. manager={viewManager}
  545. scrollToNode={onTabScrollToNode}
  546. tabsDispatch={tabsDispatch}
  547. drawerSize={initialDrawerSize}
  548. layout={tracePreferences.layout}
  549. onLayoutChange={onLayoutChange}
  550. onDrawerResize={onDrawerResize}
  551. rootEventResults={rootEvent}
  552. organization={props.organization}
  553. location={props.location}
  554. traces={props.trace}
  555. traceEventView={props.traceEventView}
  556. />
  557. </TraceGrid>
  558. </TraceInnerLayout>
  559. </TraceExternalLayout>
  560. );
  561. }
  562. function useQueryParamSync(query: Record<string, string | undefined>) {
  563. const previousQueryRef = useRef<Record<string, string | undefined>>(query);
  564. const syncStateTimeoutRef = useRef<number | null>(null);
  565. useEffect(() => {
  566. const keys = Object.keys(query);
  567. const previousKeys = Object.keys(previousQueryRef.current);
  568. if (
  569. keys.length === previousKeys.length &&
  570. keys.every(key => {
  571. return query[key] === previousQueryRef.current[key];
  572. })
  573. ) {
  574. previousQueryRef.current = query;
  575. return;
  576. }
  577. if (syncStateTimeoutRef.current !== null) {
  578. window.clearTimeout(syncStateTimeoutRef.current);
  579. }
  580. previousQueryRef.current = query;
  581. syncStateTimeoutRef.current = window.setTimeout(() => {
  582. browserHistory.replace({
  583. pathname: location.pathname,
  584. query: {
  585. ...qs.parse(location.search),
  586. ...previousQueryRef.current,
  587. },
  588. });
  589. }, 1000);
  590. }, [query]);
  591. }
  592. function useRootEvent(trace: TraceSplitResults<TraceFullDetailed> | null) {
  593. const root = trace?.transactions[0] || trace?.orphan_errors[0];
  594. const organization = useOrganization();
  595. return useApiQuery<EventTransaction>(
  596. [
  597. `/organizations/${organization.slug}/events/${root?.project_slug}:${root?.event_id}/`,
  598. {
  599. query: {
  600. referrer: 'trace-details-summary',
  601. },
  602. },
  603. ],
  604. {
  605. staleTime: 0,
  606. enabled: !!trace,
  607. }
  608. );
  609. }
  610. const TraceExternalLayout = styled('div')`
  611. display: flex;
  612. flex-direction: column;
  613. flex: 1 1 100%;
  614. ~ footer {
  615. display: none;
  616. }
  617. `;
  618. const TraceInnerLayout = styled('div')`
  619. display: flex;
  620. flex-direction: column;
  621. flex: 1 1 100%;
  622. padding: ${space(2)} ${space(2)} 0 ${space(2)};
  623. background-color: ${p => p.theme.background};
  624. `;
  625. const TraceToolbar = styled('div')`
  626. flex-grow: 0;
  627. display: grid;
  628. grid-template-columns: 1fr min-content;
  629. gap: ${space(1)};
  630. `;
  631. const TraceGrid = styled('div')<{
  632. layout: 'drawer bottom' | 'drawer left' | 'drawer right';
  633. }>`
  634. box-shadow: 0 0 0 1px ${p => p.theme.border};
  635. flex: 1 1 100%;
  636. display: grid;
  637. border-top-left-radius: ${p => p.theme.borderRadius};
  638. border-top-right-radius: ${p => p.theme.borderRadius};
  639. overflow: hidden;
  640. position: relative;
  641. /* false positive for grid layout */
  642. /* stylelint-disable */
  643. grid-template-areas: ${p =>
  644. p.layout === 'drawer bottom'
  645. ? `
  646. 'trace'
  647. 'drawer'
  648. `
  649. : p.layout === 'drawer left'
  650. ? `'drawer trace'`
  651. : `'trace drawer'`};
  652. grid-template-columns: ${p =>
  653. p.layout === 'drawer bottom'
  654. ? '1fr'
  655. : p.layout === 'drawer left'
  656. ? 'min-content 1fr'
  657. : '1fr min-content'};
  658. grid-template-rows: 1fr auto;
  659. `;
  660. const LoadingContainer = styled('div')<{animate: boolean; error?: boolean}>`
  661. display: flex;
  662. justify-content: center;
  663. align-items: center;
  664. flex-direction: column;
  665. left: 50%;
  666. top: 50%;
  667. position: absolute;
  668. height: auto;
  669. font-size: ${p => p.theme.fontSizeMedium};
  670. color: ${p => p.theme.gray300};
  671. z-index: 30;
  672. padding: 24px;
  673. background-color: ${p => p.theme.background};
  674. border-radius: ${p => p.theme.borderRadius};
  675. border: 1px solid ${p => p.theme.border};
  676. transform-origin: 50% 50%;
  677. transform: translate(-50%, -50%);
  678. animation: ${p =>
  679. p.animate
  680. ? `${p.error ? 'showLoadingContainerShake' : 'showLoadingContainer'} 300ms cubic-bezier(0.61, 1, 0.88, 1) forwards`
  681. : 'none'};
  682. @keyframes showLoadingContainer {
  683. from {
  684. opacity: 0.6;
  685. transform: scale(0.99) translate(-50%, -50%);
  686. }
  687. to {
  688. opacity: 1;
  689. transform: scale(1) translate(-50%, -50%);
  690. }
  691. }
  692. @keyframes showLoadingContainerShake {
  693. 0% {
  694. transform: translate(-50%, -50%);
  695. }
  696. 25% {
  697. transform: translate(-51%, -50%);
  698. }
  699. 75% {
  700. transform: translate(-49%, -50%);
  701. }
  702. 100% {
  703. transform: translate(-50%, -50%);
  704. }
  705. }
  706. `;
  707. function TraceLoading() {
  708. return (
  709. // Dont flash the animation on load because it's annoying
  710. <LoadingContainer animate={false}>
  711. <NoMarginIndicator size={24}>
  712. <div>{t('Assembling the trace')}</div>
  713. </NoMarginIndicator>
  714. </LoadingContainer>
  715. );
  716. }
  717. function TraceError() {
  718. const linkref = useRef<HTMLAnchorElement>(null);
  719. const feedback = useFeedbackWidget({buttonRef: linkref});
  720. return (
  721. <LoadingContainer animate error>
  722. <div>{t('Ughhhhh, we failed to load your trace...')}</div>
  723. <div>
  724. {t('Seeing this often? Send us ')}
  725. {feedback ? (
  726. <a href="#" ref={linkref}>
  727. {t('feedback')}
  728. </a>
  729. ) : (
  730. <a href="mailto:support@sentry.io?subject=Trace%20fails%20to%20load">
  731. {t('feedback')}
  732. </a>
  733. )}
  734. </div>
  735. </LoadingContainer>
  736. );
  737. }
  738. function TraceEmpty() {
  739. const linkref = useRef<HTMLAnchorElement>(null);
  740. const feedback = useFeedbackWidget({buttonRef: linkref});
  741. return (
  742. <LoadingContainer animate>
  743. <div>{t('This trace does not contain any data?!')}</div>
  744. <div>
  745. {t('Seeing this often? Send us ')}
  746. {feedback ? (
  747. <a href="#" ref={linkref}>
  748. {t('feedback')}
  749. </a>
  750. ) : (
  751. <a href="mailto:support@sentry.io?subject=Trace%20does%20not%20contain%20data">
  752. {t('feedback')}
  753. </a>
  754. )}
  755. </div>
  756. </LoadingContainer>
  757. );
  758. }
  759. const NoMarginIndicator = styled(LoadingIndicator)`
  760. margin: 0;
  761. `;