content.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742
  1. import * as React from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import * as Sentry from '@sentry/react';
  5. import Alert from 'sentry/components/alert';
  6. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  7. import ButtonBar from 'sentry/components/buttonBar';
  8. import DiscoverFeature from 'sentry/components/discover/discoverFeature';
  9. import DiscoverButton from 'sentry/components/discoverButton';
  10. import * as AnchorLinkManager from 'sentry/components/events/interfaces/spans/anchorLinkManager';
  11. import * as DividerHandlerManager from 'sentry/components/events/interfaces/spans/dividerHandlerManager';
  12. import * as ScrollbarManager from 'sentry/components/events/interfaces/spans/scrollbarManager';
  13. import * as Layout from 'sentry/components/layouts/thirds';
  14. import ExternalLink from 'sentry/components/links/externalLink';
  15. import Link from 'sentry/components/links/link';
  16. import LoadingError from 'sentry/components/loadingError';
  17. import LoadingIndicator from 'sentry/components/loadingIndicator';
  18. import {MessageRow} from 'sentry/components/performance/waterfall/messageRow';
  19. import {
  20. DividerSpacer,
  21. ScrollbarContainer,
  22. VirtualScrollbar,
  23. VirtualScrollbarGrip,
  24. } from 'sentry/components/performance/waterfall/miniHeader';
  25. import {
  26. ErrorDot,
  27. ErrorLevel,
  28. ErrorMessageContent,
  29. ErrorTitle,
  30. } from 'sentry/components/performance/waterfall/rowDetails';
  31. import {pickBarColor, toPercent} from 'sentry/components/performance/waterfall/utils';
  32. import TimeSince from 'sentry/components/timeSince';
  33. import {t, tct, tn} from 'sentry/locale';
  34. import space from 'sentry/styles/space';
  35. import {Organization} from 'sentry/types';
  36. import {defined} from 'sentry/utils';
  37. import DiscoverQuery from 'sentry/utils/discover/discoverQuery';
  38. import EventView from 'sentry/utils/discover/eventView';
  39. import {QueryError} from 'sentry/utils/discover/genericDiscoverQuery';
  40. import {getDuration} from 'sentry/utils/formatters';
  41. import {createFuzzySearch, Fuse} from 'sentry/utils/fuzzySearch';
  42. import getDynamicText from 'sentry/utils/getDynamicText';
  43. import {TraceFullDetailed, TraceMeta} from 'sentry/utils/performance/quickTrace/types';
  44. import {filterTrace, reduceTrace} from 'sentry/utils/performance/quickTrace/utils';
  45. import Breadcrumb from 'sentry/views/performance/breadcrumb';
  46. import {MetaData} from 'sentry/views/performance/transactionDetails/styles';
  47. import {
  48. TraceDetailBody,
  49. TraceDetailHeader,
  50. TracePanel,
  51. TraceSearchBar,
  52. TraceSearchContainer,
  53. TraceViewContainer,
  54. TraceViewHeaderContainer,
  55. } from './styles';
  56. import TransactionGroup from './transactionGroup';
  57. import {TraceInfo, TreeDepth} from './types';
  58. import {getTraceInfo, isRootTransaction} from './utils';
  59. type IndexedFusedTransaction = {
  60. indexed: string[];
  61. transaction: TraceFullDetailed;
  62. };
  63. type AccType = {
  64. lastIndex: number;
  65. numberOfHiddenTransactionsAbove: number;
  66. renderedChildren: React.ReactNode[];
  67. };
  68. type Props = Pick<RouteComponentProps<{traceSlug: string}, {}>, 'params' | 'location'> & {
  69. dateSelected: boolean;
  70. error: QueryError | null;
  71. isLoading: boolean;
  72. meta: TraceMeta | null;
  73. organization: Organization;
  74. traceEventView: EventView;
  75. traceSlug: string;
  76. traces: TraceFullDetailed[] | null;
  77. };
  78. type State = {
  79. filteredTransactionIds: Set<string> | undefined;
  80. searchQuery: string | undefined;
  81. };
  82. class TraceDetailsContent extends React.Component<Props, State> {
  83. state: State = {
  84. searchQuery: undefined,
  85. filteredTransactionIds: undefined,
  86. };
  87. componentDidMount() {
  88. this.initFuse();
  89. }
  90. componentDidUpdate(prevProps: Props) {
  91. if (this.props.traces !== prevProps.traces) {
  92. this.initFuse();
  93. }
  94. }
  95. fuse: Fuse<IndexedFusedTransaction> | null = null;
  96. traceViewRef = React.createRef<HTMLDivElement>();
  97. virtualScrollbarContainerRef = React.createRef<HTMLDivElement>();
  98. async initFuse() {
  99. if (defined(this.props.traces) && this.props.traces.length > 0) {
  100. const transformed: IndexedFusedTransaction[] = this.props.traces.flatMap(trace =>
  101. reduceTrace<IndexedFusedTransaction[]>(
  102. trace,
  103. (acc, transaction) => {
  104. const indexed: string[] = [
  105. transaction['transaction.op'],
  106. transaction.transaction,
  107. transaction.project_slug,
  108. ];
  109. acc.push({
  110. transaction,
  111. indexed,
  112. });
  113. return acc;
  114. },
  115. []
  116. )
  117. );
  118. this.fuse = await createFuzzySearch(transformed, {
  119. keys: ['indexed'],
  120. includeMatches: true,
  121. threshold: 0.6,
  122. location: 0,
  123. distance: 100,
  124. maxPatternLength: 32,
  125. });
  126. }
  127. }
  128. renderTraceLoading() {
  129. return <LoadingIndicator />;
  130. }
  131. renderTraceRequiresDateRangeSelection() {
  132. return <LoadingError message={t('Trace view requires a date range selection.')} />;
  133. }
  134. renderTraceNotFound() {
  135. const {meta, traceEventView, traceSlug, organization, location} = this.props;
  136. const transactions = meta?.transactions ?? 0;
  137. const errors = meta?.errors ?? 0;
  138. if (transactions === 0 && errors > 0) {
  139. const errorsEventView = traceEventView.withColumns([
  140. {kind: 'field', field: 'project'},
  141. {kind: 'field', field: 'title'},
  142. {kind: 'field', field: 'issue.id'},
  143. {kind: 'field', field: 'level'},
  144. ]);
  145. errorsEventView.query = `trace:${traceSlug} !event.type:transaction `;
  146. return (
  147. <DiscoverQuery
  148. eventView={errorsEventView}
  149. orgSlug={organization.slug}
  150. location={location}
  151. referrer="api.trace-view.errors-view"
  152. >
  153. {({isLoading, tableData, error}) => {
  154. if (isLoading) {
  155. return <LoadingIndicator />;
  156. }
  157. if (error) {
  158. return (
  159. <Alert type="error" showIcon>
  160. <ErrorLabel>
  161. {tct(
  162. 'The trace cannot be shown when all events are errors. An error occurred when attempting to fetch these error events: [error]',
  163. {error: error.message}
  164. )}
  165. </ErrorLabel>
  166. </Alert>
  167. );
  168. }
  169. return (
  170. <Alert type="error" showIcon>
  171. <ErrorLabel>
  172. {t('The trace cannot be shown when all events are errors.')}
  173. </ErrorLabel>
  174. <ErrorMessageContent data-test-id="trace-view-errors">
  175. {tableData?.data.map(data => (
  176. <React.Fragment key={data.id}>
  177. <ErrorDot level={data.level as any} />
  178. <ErrorLevel>{data.level}</ErrorLevel>
  179. <ErrorTitle>
  180. <Link
  181. to={`/organizations/${organization.slug}/issues/${data['issue.id']}/events/${data.id}`}
  182. >
  183. {data.title}
  184. </Link>
  185. </ErrorTitle>
  186. </React.Fragment>
  187. ))}
  188. </ErrorMessageContent>
  189. </Alert>
  190. );
  191. }}
  192. </DiscoverQuery>
  193. );
  194. }
  195. return <LoadingError message={t('The trace you are looking for was not found.')} />;
  196. }
  197. handleTransactionFilter = (searchQuery: string) => {
  198. this.setState({searchQuery: searchQuery || undefined}, this.filterTransactions);
  199. };
  200. filterTransactions = () => {
  201. const {traces} = this.props;
  202. const {filteredTransactionIds, searchQuery} = this.state;
  203. if (!searchQuery || traces === null || traces.length <= 0 || !defined(this.fuse)) {
  204. if (filteredTransactionIds !== undefined) {
  205. this.setState({
  206. filteredTransactionIds: undefined,
  207. });
  208. }
  209. return;
  210. }
  211. const fuseMatches = this.fuse
  212. .search<IndexedFusedTransaction>(searchQuery)
  213. /**
  214. * Sometimes, there can be matches that don't include any
  215. * indices. These matches are often noise, so exclude them.
  216. */
  217. .filter(({matches}) => matches?.length)
  218. .map(({item}) => item.transaction.event_id);
  219. /**
  220. * Fuzzy search on ids result in seemingly random results. So switch to
  221. * doing substring matches on ids to provide more meaningful results.
  222. */
  223. const idMatches = traces
  224. .flatMap(trace =>
  225. filterTrace(
  226. trace,
  227. ({event_id, span_id}) =>
  228. event_id.includes(searchQuery) || span_id.includes(searchQuery)
  229. )
  230. )
  231. .map(transaction => transaction.event_id);
  232. this.setState({
  233. filteredTransactionIds: new Set([...fuseMatches, ...idMatches]),
  234. });
  235. };
  236. renderSearchBar() {
  237. return (
  238. <TraceSearchContainer>
  239. <TraceSearchBar
  240. defaultQuery=""
  241. query={this.state.searchQuery || ''}
  242. placeholder={t('Search for transactions')}
  243. onSearch={this.handleTransactionFilter}
  244. />
  245. </TraceSearchContainer>
  246. );
  247. }
  248. isTransactionVisible = (transaction: TraceFullDetailed): boolean => {
  249. const {filteredTransactionIds} = this.state;
  250. return filteredTransactionIds
  251. ? filteredTransactionIds.has(transaction.event_id)
  252. : true;
  253. };
  254. renderTraceHeader(traceInfo: TraceInfo) {
  255. const {meta} = this.props;
  256. return (
  257. <TraceDetailHeader>
  258. <GuideAnchor target="trace_view_guide_breakdown">
  259. <MetaData
  260. headingText={t('Event Breakdown')}
  261. tooltipText={t(
  262. 'The number of transactions and errors there are in this trace.'
  263. )}
  264. bodyText={tct('[transactions] | [errors]', {
  265. transactions: tn(
  266. '%s Transaction',
  267. '%s Transactions',
  268. meta?.transactions ?? traceInfo.transactions.size
  269. ),
  270. errors: tn('%s Error', '%s Errors', meta?.errors ?? traceInfo.errors.size),
  271. })}
  272. subtext={tn(
  273. 'Across %s project',
  274. 'Across %s projects',
  275. meta?.projects ?? traceInfo.projects.size
  276. )}
  277. />
  278. </GuideAnchor>
  279. <MetaData
  280. headingText={t('Total Duration')}
  281. tooltipText={t('The time elapsed between the start and end of this trace.')}
  282. bodyText={getDuration(
  283. traceInfo.endTimestamp - traceInfo.startTimestamp,
  284. 2,
  285. true
  286. )}
  287. subtext={getDynamicText({
  288. value: <TimeSince date={(traceInfo.endTimestamp || 0) * 1000} />,
  289. fixed: '5 days ago',
  290. })}
  291. />
  292. </TraceDetailHeader>
  293. );
  294. }
  295. renderTraceWarnings() {
  296. const {traces} = this.props;
  297. const {roots, orphans} = (traces ?? []).reduce(
  298. (counts, trace) => {
  299. if (isRootTransaction(trace)) {
  300. counts.roots++;
  301. } else {
  302. counts.orphans++;
  303. }
  304. return counts;
  305. },
  306. {roots: 0, orphans: 0}
  307. );
  308. let warning: React.ReactNode = null;
  309. if (roots === 0 && orphans > 0) {
  310. warning = (
  311. <Alert type="info" showIcon>
  312. <ExternalLink href="https://docs.sentry.io/product/performance/trace-view/#orphan-traces-and-broken-subtraces">
  313. {t(
  314. 'A root transaction is missing. Transactions linked by a dashed line have been orphaned and cannot be directly linked to the root.'
  315. )}
  316. </ExternalLink>
  317. </Alert>
  318. );
  319. } else if (roots === 1 && orphans > 0) {
  320. warning = (
  321. <Alert type="info" showIcon>
  322. <ExternalLink href="https://docs.sentry.io/product/performance/trace-view/#orphan-traces-and-broken-subtraces">
  323. {t(
  324. 'This trace has broken subtraces. Transactions linked by a dashed line have been orphaned and cannot be directly linked to the root.'
  325. )}
  326. </ExternalLink>
  327. </Alert>
  328. );
  329. } else if (roots > 1) {
  330. warning = (
  331. <Alert type="info" showIcon>
  332. <ExternalLink href="https://docs.sentry.io/product/performance/trace-view/#multiple-roots">
  333. {t('Multiple root transactions have been found with this trace ID.')}
  334. </ExternalLink>
  335. </Alert>
  336. );
  337. }
  338. return warning;
  339. }
  340. renderInfoMessage({
  341. isVisible,
  342. numberOfHiddenTransactionsAbove,
  343. }: {
  344. isVisible: boolean;
  345. numberOfHiddenTransactionsAbove: number;
  346. }) {
  347. const messages: React.ReactNode[] = [];
  348. if (isVisible) {
  349. if (numberOfHiddenTransactionsAbove === 1) {
  350. messages.push(
  351. <span key="stuff">
  352. {tct('[numOfTransaction] hidden transaction', {
  353. numOfTransaction: <strong>{numberOfHiddenTransactionsAbove}</strong>,
  354. })}
  355. </span>
  356. );
  357. } else if (numberOfHiddenTransactionsAbove > 1) {
  358. messages.push(
  359. <span key="stuff">
  360. {tct('[numOfTransaction] hidden transactions', {
  361. numOfTransaction: <strong>{numberOfHiddenTransactionsAbove}</strong>,
  362. })}
  363. </span>
  364. );
  365. }
  366. }
  367. if (messages.length <= 0) {
  368. return null;
  369. }
  370. return <MessageRow>{messages}</MessageRow>;
  371. }
  372. renderLimitExceededMessage(traceInfo: TraceInfo) {
  373. const {traceEventView, organization, meta} = this.props;
  374. const count = traceInfo.transactions.size;
  375. const totalTransactions = meta?.transactions ?? count;
  376. if (totalTransactions === null || count >= totalTransactions) {
  377. return null;
  378. }
  379. const target = traceEventView.getResultsViewUrlTarget(organization.slug);
  380. return (
  381. <MessageRow>
  382. {tct(
  383. 'Limited to a view of [count] transactions. To view the full list, [discover].',
  384. {
  385. count,
  386. discover: (
  387. <DiscoverFeature>
  388. {({hasFeature}) => (
  389. <Link disabled={!hasFeature} to={target}>
  390. Open in Discover
  391. </Link>
  392. )}
  393. </DiscoverFeature>
  394. ),
  395. }
  396. )}
  397. </MessageRow>
  398. );
  399. }
  400. renderTransaction(
  401. transaction: TraceFullDetailed,
  402. {
  403. continuingDepths,
  404. isOrphan,
  405. isLast,
  406. index,
  407. numberOfHiddenTransactionsAbove,
  408. traceInfo,
  409. hasGuideAnchor,
  410. }: {
  411. continuingDepths: TreeDepth[];
  412. hasGuideAnchor: boolean;
  413. index: number;
  414. isLast: boolean;
  415. isOrphan: boolean;
  416. numberOfHiddenTransactionsAbove: number;
  417. traceInfo: TraceInfo;
  418. }
  419. ) {
  420. const {location, organization} = this.props;
  421. const {children, event_id: eventId} = transaction;
  422. // Add 1 to the generation to make room for the "root trace"
  423. const generation = transaction.generation + 1;
  424. const isVisible = this.isTransactionVisible(transaction);
  425. const accumulated: AccType = children.reduce(
  426. (acc: AccType, child: TraceFullDetailed, idx: number) => {
  427. const isLastChild = idx === children.length - 1;
  428. const hasChildren = child.children.length > 0;
  429. const result = this.renderTransaction(child, {
  430. continuingDepths:
  431. !isLastChild && hasChildren
  432. ? [...continuingDepths, {depth: generation, isOrphanDepth: isOrphan}]
  433. : continuingDepths,
  434. isOrphan,
  435. isLast: isLastChild,
  436. index: acc.lastIndex + 1,
  437. numberOfHiddenTransactionsAbove: acc.numberOfHiddenTransactionsAbove,
  438. traceInfo,
  439. hasGuideAnchor: false,
  440. });
  441. acc.lastIndex = result.lastIndex;
  442. acc.numberOfHiddenTransactionsAbove = result.numberOfHiddenTransactionsAbove;
  443. acc.renderedChildren.push(result.transactionGroup);
  444. return acc;
  445. },
  446. {
  447. renderedChildren: [],
  448. lastIndex: index,
  449. numberOfHiddenTransactionsAbove: isVisible
  450. ? 0
  451. : numberOfHiddenTransactionsAbove + 1,
  452. }
  453. );
  454. return {
  455. transactionGroup: (
  456. <React.Fragment key={eventId}>
  457. {this.renderInfoMessage({
  458. isVisible,
  459. numberOfHiddenTransactionsAbove,
  460. })}
  461. <TransactionGroup
  462. location={location}
  463. organization={organization}
  464. traceInfo={traceInfo}
  465. transaction={{
  466. ...transaction,
  467. generation,
  468. }}
  469. continuingDepths={continuingDepths}
  470. isOrphan={isOrphan}
  471. isLast={isLast}
  472. index={index}
  473. isVisible={isVisible}
  474. hasGuideAnchor={hasGuideAnchor}
  475. renderedChildren={accumulated.renderedChildren}
  476. barColor={pickBarColor(transaction['transaction.op'])}
  477. />
  478. </React.Fragment>
  479. ),
  480. lastIndex: accumulated.lastIndex,
  481. numberOfHiddenTransactionsAbove: accumulated.numberOfHiddenTransactionsAbove,
  482. };
  483. }
  484. renderTraceView(traceInfo: TraceInfo) {
  485. const sentryTransaction = Sentry.getCurrentHub().getScope()?.getTransaction();
  486. const sentrySpan = sentryTransaction?.startChild({
  487. op: 'trace.render',
  488. description: 'trace-view-content',
  489. });
  490. const {location, organization, traces, traceSlug} = this.props;
  491. if (traces === null || traces.length <= 0) {
  492. return this.renderTraceNotFound();
  493. }
  494. const accumulator: {
  495. index: number;
  496. numberOfHiddenTransactionsAbove: number;
  497. traceInfo: TraceInfo;
  498. transactionGroups: React.ReactNode[];
  499. } = {
  500. index: 1,
  501. numberOfHiddenTransactionsAbove: 0,
  502. traceInfo,
  503. transactionGroups: [],
  504. };
  505. const {transactionGroups, numberOfHiddenTransactionsAbove} = traces.reduce(
  506. (acc, trace, index) => {
  507. const isLastTransaction = index === traces.length - 1;
  508. const hasChildren = trace.children.length > 0;
  509. const isNextChildOrphaned =
  510. !isLastTransaction && traces[index + 1].parent_span_id !== null;
  511. const result = this.renderTransaction(trace, {
  512. ...acc,
  513. // if the root of a subtrace has a parent_span_idk, then it must be an orphan
  514. isOrphan: !isRootTransaction(trace),
  515. isLast: isLastTransaction,
  516. continuingDepths:
  517. !isLastTransaction && hasChildren
  518. ? [{depth: 0, isOrphanDepth: isNextChildOrphaned}]
  519. : [],
  520. hasGuideAnchor: index === 0,
  521. });
  522. acc.index = result.lastIndex + 1;
  523. acc.numberOfHiddenTransactionsAbove = result.numberOfHiddenTransactionsAbove;
  524. acc.transactionGroups.push(result.transactionGroup);
  525. return acc;
  526. },
  527. accumulator
  528. );
  529. const traceView = (
  530. <TraceDetailBody>
  531. <DividerHandlerManager.Provider interactiveLayerRef={this.traceViewRef}>
  532. <DividerHandlerManager.Consumer>
  533. {({dividerPosition}) => (
  534. <ScrollbarManager.Provider
  535. dividerPosition={dividerPosition}
  536. interactiveLayerRef={this.virtualScrollbarContainerRef}
  537. >
  538. <TracePanel>
  539. <TraceViewHeaderContainer>
  540. <ScrollbarManager.Consumer>
  541. {({
  542. virtualScrollbarRef,
  543. scrollBarAreaRef,
  544. onDragStart,
  545. onScroll,
  546. }) => {
  547. return (
  548. <ScrollbarContainer
  549. ref={this.virtualScrollbarContainerRef}
  550. style={{
  551. // the width of this component is shrunk to compensate for half of the width of the divider line
  552. width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
  553. }}
  554. onScroll={onScroll}
  555. >
  556. <div
  557. style={{
  558. width: 0,
  559. height: '1px',
  560. }}
  561. ref={scrollBarAreaRef}
  562. />
  563. <VirtualScrollbar
  564. data-type="virtual-scrollbar"
  565. ref={virtualScrollbarRef}
  566. onMouseDown={onDragStart}
  567. >
  568. <VirtualScrollbarGrip />
  569. </VirtualScrollbar>
  570. </ScrollbarContainer>
  571. );
  572. }}
  573. </ScrollbarManager.Consumer>
  574. <DividerSpacer />
  575. </TraceViewHeaderContainer>
  576. <TraceViewContainer ref={this.traceViewRef}>
  577. <AnchorLinkManager.Provider>
  578. <TransactionGroup
  579. location={location}
  580. organization={organization}
  581. traceInfo={traceInfo}
  582. transaction={{
  583. traceSlug,
  584. generation: 0,
  585. 'transaction.duration':
  586. traceInfo.endTimestamp - traceInfo.startTimestamp,
  587. children: traces,
  588. start_timestamp: traceInfo.startTimestamp,
  589. timestamp: traceInfo.endTimestamp,
  590. }}
  591. continuingDepths={[]}
  592. isOrphan={false}
  593. isLast={false}
  594. index={0}
  595. isVisible
  596. hasGuideAnchor={false}
  597. renderedChildren={transactionGroups}
  598. barColor={pickBarColor('')}
  599. />
  600. </AnchorLinkManager.Provider>
  601. {this.renderInfoMessage({
  602. isVisible: true,
  603. numberOfHiddenTransactionsAbove,
  604. })}
  605. {this.renderLimitExceededMessage(traceInfo)}
  606. </TraceViewContainer>
  607. </TracePanel>
  608. </ScrollbarManager.Provider>
  609. )}
  610. </DividerHandlerManager.Consumer>
  611. </DividerHandlerManager.Provider>
  612. </TraceDetailBody>
  613. );
  614. sentrySpan?.finish();
  615. return traceView;
  616. }
  617. renderContent() {
  618. const {dateSelected, isLoading, error, traces} = this.props;
  619. if (!dateSelected) {
  620. return this.renderTraceRequiresDateRangeSelection();
  621. }
  622. if (isLoading) {
  623. return this.renderTraceLoading();
  624. }
  625. if (error !== null || traces === null || traces.length <= 0) {
  626. return this.renderTraceNotFound();
  627. }
  628. const traceInfo = getTraceInfo(traces);
  629. return (
  630. <React.Fragment>
  631. {this.renderTraceWarnings()}
  632. {this.renderTraceHeader(traceInfo)}
  633. {this.renderSearchBar()}
  634. {this.renderTraceView(traceInfo)}
  635. </React.Fragment>
  636. );
  637. }
  638. render() {
  639. const {organization, location, traceEventView, traceSlug} = this.props;
  640. return (
  641. <React.Fragment>
  642. <Layout.Header>
  643. <Layout.HeaderContent>
  644. <Breadcrumb
  645. organization={organization}
  646. location={location}
  647. traceSlug={traceSlug}
  648. />
  649. <Layout.Title data-test-id="trace-header">
  650. {t('Trace ID: %s', traceSlug)}
  651. </Layout.Title>
  652. </Layout.HeaderContent>
  653. <Layout.HeaderActions>
  654. <ButtonBar gap={1}>
  655. <DiscoverButton
  656. to={traceEventView.getResultsViewUrlTarget(organization.slug)}
  657. >
  658. Open in Discover
  659. </DiscoverButton>
  660. </ButtonBar>
  661. </Layout.HeaderActions>
  662. </Layout.Header>
  663. <Layout.Body>
  664. <Layout.Main fullWidth>{this.renderContent()}</Layout.Main>
  665. </Layout.Body>
  666. </React.Fragment>
  667. );
  668. }
  669. }
  670. const ErrorLabel = styled('div')`
  671. margin-bottom: ${space(1)};
  672. `;
  673. export default TraceDetailsContent;