index.tsx 27 KB

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