content.tsx 19 KB

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