index.tsx 24 KB

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