traceView.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. import {createRef, Fragment, useEffect} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import * as Sentry from '@sentry/react';
  5. import * as DividerHandlerManager from 'sentry/components/events/interfaces/spans/dividerHandlerManager';
  6. import MeasurementsPanel from 'sentry/components/events/interfaces/spans/measurementsPanel';
  7. import * as ScrollbarManager from 'sentry/components/events/interfaces/spans/scrollbarManager';
  8. import {
  9. boundsGenerator,
  10. getMeasurements,
  11. } from 'sentry/components/events/interfaces/spans/utils';
  12. import Panel from 'sentry/components/panels/panel';
  13. import {MessageRow} from 'sentry/components/performance/waterfall/messageRow';
  14. import {
  15. DividerSpacer,
  16. ScrollbarContainer,
  17. VirtualScrollbar,
  18. VirtualScrollbarGrip,
  19. } from 'sentry/components/performance/waterfall/miniHeader';
  20. import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
  21. import {tct} from 'sentry/locale';
  22. import {Organization} from 'sentry/types';
  23. import {trackAnalytics} from 'sentry/utils/analytics';
  24. import EventView from 'sentry/utils/discover/eventView';
  25. import toPercent from 'sentry/utils/number/toPercent';
  26. import {
  27. TraceError,
  28. TraceFullDetailed,
  29. TraceMeta,
  30. } from 'sentry/utils/performance/quickTrace/types';
  31. import {
  32. TraceDetailBody,
  33. TraceViewContainer,
  34. TraceViewHeaderContainer,
  35. } from 'sentry/views/performance/traceDetails/styles';
  36. import TransactionGroup from 'sentry/views/performance/traceDetails/transactionGroup';
  37. import {TraceInfo, TreeDepth} from 'sentry/views/performance/traceDetails/types';
  38. import {
  39. getTraceInfo,
  40. hasTraceData,
  41. isRootTransaction,
  42. } from 'sentry/views/performance/traceDetails/utils';
  43. import LimitExceededMessage from './limitExceededMessage';
  44. import TraceNotFound from './traceNotFound';
  45. type AccType = {
  46. lastIndex: number;
  47. numberOfHiddenTransactionsAbove: number;
  48. renderedChildren: React.ReactNode[];
  49. };
  50. type Props = Pick<RouteComponentProps<{}, {}>, 'location'> & {
  51. meta: TraceMeta | null;
  52. organization: Organization;
  53. traceEventView: EventView;
  54. traceSlug: string;
  55. traces: TraceFullDetailed[];
  56. filteredEventIds?: Set<string>;
  57. orphanErrors?: TraceError[];
  58. traceInfo?: TraceInfo;
  59. };
  60. function TraceHiddenMessage({
  61. isVisible,
  62. numberOfHiddenTransactionsAbove,
  63. numberOfHiddenErrorsAbove,
  64. }: {
  65. isVisible: boolean;
  66. numberOfHiddenErrorsAbove: number;
  67. numberOfHiddenTransactionsAbove: number;
  68. }) {
  69. if (
  70. !isVisible ||
  71. (numberOfHiddenTransactionsAbove < 1 && numberOfHiddenErrorsAbove < 1)
  72. ) {
  73. return null;
  74. }
  75. const numOfTransaction = <strong>{numberOfHiddenTransactionsAbove}</strong>;
  76. const numOfErrors = <strong>{numberOfHiddenErrorsAbove}</strong>;
  77. const hiddenTransactionsMessage =
  78. numberOfHiddenTransactionsAbove < 1
  79. ? ''
  80. : numberOfHiddenTransactionsAbove === 1
  81. ? tct('[numOfTransaction] hidden transaction', {
  82. numOfTransaction,
  83. })
  84. : tct('[numOfTransaction] hidden transactions', {
  85. numOfTransaction,
  86. });
  87. const hiddenErrorsMessage =
  88. numberOfHiddenErrorsAbove < 1
  89. ? ''
  90. : numberOfHiddenErrorsAbove === 1
  91. ? tct('[numOfErrors] hidden error', {
  92. numOfErrors,
  93. })
  94. : tct('[numOfErrors] hidden errors', {
  95. numOfErrors,
  96. });
  97. return (
  98. <MessageRow>
  99. <span key="trace-info-message">
  100. {hiddenTransactionsMessage}
  101. {hiddenErrorsMessage && hiddenTransactionsMessage && ', '}
  102. {hiddenErrorsMessage}
  103. </span>
  104. </MessageRow>
  105. );
  106. }
  107. function isRowVisible(
  108. row: TraceFullDetailed | TraceError,
  109. filteredEventIds?: Set<string>
  110. ): boolean {
  111. return filteredEventIds ? filteredEventIds.has(row.event_id) : true;
  112. }
  113. function generateBounds(traceInfo: TraceInfo) {
  114. return boundsGenerator({
  115. traceStartTimestamp: traceInfo.startTimestamp,
  116. traceEndTimestamp: traceInfo.endTimestamp,
  117. viewStart: 0,
  118. viewEnd: 1,
  119. });
  120. }
  121. export default function TraceView({
  122. location,
  123. meta,
  124. organization,
  125. traces,
  126. traceSlug,
  127. traceEventView,
  128. filteredEventIds,
  129. orphanErrors,
  130. ...props
  131. }: Props) {
  132. const sentryTransaction = Sentry.getCurrentHub().getScope()?.getTransaction();
  133. const sentrySpan = sentryTransaction?.startChild({
  134. op: 'trace.render',
  135. description: 'trace-view-content',
  136. });
  137. const hasOrphanErrors = orphanErrors && orphanErrors.length > 0;
  138. const onlyOrphanErrors = hasOrphanErrors && (!traces || traces.length === 0);
  139. useEffect(() => {
  140. trackAnalytics('performance_views.trace_view.view', {
  141. organization,
  142. });
  143. }, [organization]);
  144. function renderTransaction(
  145. transaction: TraceFullDetailed,
  146. {
  147. continuingDepths,
  148. isOrphan,
  149. isLast,
  150. index,
  151. numberOfHiddenTransactionsAbove,
  152. traceInfo,
  153. hasGuideAnchor,
  154. }: {
  155. continuingDepths: TreeDepth[];
  156. hasGuideAnchor: boolean;
  157. index: number;
  158. isLast: boolean;
  159. isOrphan: boolean;
  160. numberOfHiddenTransactionsAbove: number;
  161. traceInfo: TraceInfo;
  162. }
  163. ) {
  164. const {children, event_id: eventId} = transaction;
  165. // Add 1 to the generation to make room for the "root trace"
  166. const generation = transaction.generation + 1;
  167. const isVisible = isRowVisible(transaction, filteredEventIds);
  168. const accumulated: AccType = children.reduce(
  169. (acc: AccType, child: TraceFullDetailed, idx: number) => {
  170. const isLastChild = idx === children.length - 1;
  171. const hasChildren = child.children.length > 0;
  172. const result = renderTransaction(child, {
  173. continuingDepths:
  174. !isLastChild && hasChildren
  175. ? [...continuingDepths, {depth: generation, isOrphanDepth: isOrphan}]
  176. : continuingDepths,
  177. isOrphan,
  178. isLast: isLastChild,
  179. index: acc.lastIndex + 1,
  180. numberOfHiddenTransactionsAbove: acc.numberOfHiddenTransactionsAbove,
  181. traceInfo,
  182. hasGuideAnchor: false,
  183. });
  184. acc.lastIndex = result.lastIndex;
  185. acc.numberOfHiddenTransactionsAbove = result.numberOfHiddenTransactionsAbove;
  186. acc.renderedChildren.push(result.transactionGroup);
  187. return acc;
  188. },
  189. {
  190. renderedChildren: [],
  191. lastIndex: index,
  192. numberOfHiddenTransactionsAbove: isVisible
  193. ? 0
  194. : numberOfHiddenTransactionsAbove + 1,
  195. }
  196. );
  197. return {
  198. transactionGroup: (
  199. <Fragment key={eventId}>
  200. <TraceHiddenMessage
  201. isVisible={isVisible}
  202. numberOfHiddenTransactionsAbove={numberOfHiddenTransactionsAbove}
  203. numberOfHiddenErrorsAbove={0}
  204. />
  205. <TransactionGroup
  206. location={location}
  207. organization={organization}
  208. traceInfo={traceInfo}
  209. transaction={{
  210. ...transaction,
  211. generation,
  212. }}
  213. measurements={
  214. traces && traces.length > 0
  215. ? getMeasurements(traces[0], generateBounds(traceInfo))
  216. : undefined
  217. }
  218. generateBounds={generateBounds(traceInfo)}
  219. continuingDepths={continuingDepths}
  220. isOrphan={isOrphan}
  221. isLast={isLast}
  222. index={index}
  223. isVisible={isVisible}
  224. hasGuideAnchor={hasGuideAnchor}
  225. renderedChildren={accumulated.renderedChildren}
  226. barColor={pickBarColor(transaction['transaction.op'])}
  227. />
  228. </Fragment>
  229. ),
  230. lastIndex: accumulated.lastIndex,
  231. numberOfHiddenTransactionsAbove: accumulated.numberOfHiddenTransactionsAbove,
  232. };
  233. }
  234. const traceViewRef = createRef<HTMLDivElement>();
  235. const virtualScrollbarContainerRef = createRef<HTMLDivElement>();
  236. if (!hasTraceData(traces, orphanErrors)) {
  237. return (
  238. <TraceNotFound
  239. meta={meta}
  240. traceEventView={traceEventView}
  241. traceSlug={traceSlug}
  242. location={location}
  243. organization={organization}
  244. />
  245. );
  246. }
  247. const traceInfo = props.traceInfo || getTraceInfo(traces);
  248. const accumulator: {
  249. index: number;
  250. numberOfHiddenTransactionsAbove: number;
  251. traceInfo: TraceInfo;
  252. transactionGroups: React.ReactNode[];
  253. } = {
  254. index: 1,
  255. numberOfHiddenTransactionsAbove: 0,
  256. traceInfo,
  257. transactionGroups: [],
  258. };
  259. let lastIndex: number = 0;
  260. const {transactionGroups, numberOfHiddenTransactionsAbove} = traces.reduce(
  261. (acc, trace, index) => {
  262. const isLastTransaction = index === traces.length - 1;
  263. const hasChildren = trace.children.length > 0;
  264. const isNextChildOrphaned =
  265. !isLastTransaction && traces[index + 1].parent_span_id !== null;
  266. const result = renderTransaction(trace, {
  267. ...acc,
  268. // if the root of a subtrace has a parent_span_id, then it must be an orphan
  269. isOrphan: !isRootTransaction(trace),
  270. isLast: isLastTransaction && !hasOrphanErrors,
  271. continuingDepths:
  272. (!isLastTransaction && hasChildren) || hasOrphanErrors
  273. ? [{depth: 0, isOrphanDepth: isNextChildOrphaned || Boolean(hasOrphanErrors)}]
  274. : [],
  275. hasGuideAnchor: index === 0,
  276. });
  277. acc.index = result.lastIndex + 1;
  278. lastIndex = Math.max(lastIndex, result.lastIndex);
  279. acc.numberOfHiddenTransactionsAbove = result.numberOfHiddenTransactionsAbove;
  280. acc.transactionGroups.push(result.transactionGroup);
  281. return acc;
  282. },
  283. accumulator
  284. );
  285. // Build transaction groups for orphan errors
  286. let numOfHiddenErrorsAbove = 0;
  287. let totalNumOfHiddenErrors = 0;
  288. if (hasOrphanErrors) {
  289. orphanErrors.forEach((error, index) => {
  290. const isLastError = index === orphanErrors.length - 1;
  291. const isVisible = isRowVisible(error, filteredEventIds);
  292. const currentHiddenCount = numOfHiddenErrorsAbove;
  293. if (!isVisible) {
  294. numOfHiddenErrorsAbove += 1;
  295. totalNumOfHiddenErrors += 1;
  296. } else {
  297. numOfHiddenErrorsAbove = 0;
  298. }
  299. transactionGroups.push(
  300. <Fragment key={error.event_id}>
  301. <TraceHiddenMessage
  302. isVisible={isVisible}
  303. numberOfHiddenTransactionsAbove={
  304. index === 0 ? numberOfHiddenTransactionsAbove : 0
  305. }
  306. numberOfHiddenErrorsAbove={index > 0 ? currentHiddenCount : 0}
  307. />
  308. <TransactionGroup
  309. location={location}
  310. organization={organization}
  311. traceInfo={traceInfo}
  312. transaction={{
  313. ...error,
  314. generation: 1,
  315. }}
  316. generateBounds={generateBounds(traceInfo)}
  317. measurements={
  318. traces && traces.length > 0
  319. ? getMeasurements(traces[0], generateBounds(traceInfo))
  320. : undefined
  321. }
  322. continuingDepths={[]}
  323. isOrphan
  324. isLast={isLastError}
  325. index={lastIndex + index + 1}
  326. isVisible={isVisible}
  327. hasGuideAnchor={index === 0 && transactionGroups.length === 0}
  328. renderedChildren={[]}
  329. />
  330. </Fragment>
  331. );
  332. });
  333. }
  334. const bounds = generateBounds(traceInfo);
  335. const measurements =
  336. traces.length > 0 && Object.keys(traces[0].measurements ?? {}).length > 0
  337. ? getMeasurements(traces[0], bounds)
  338. : undefined;
  339. const traceView = (
  340. <TraceDetailBody>
  341. <DividerHandlerManager.Provider interactiveLayerRef={traceViewRef}>
  342. <DividerHandlerManager.Consumer>
  343. {({dividerPosition}) => (
  344. <ScrollbarManager.Provider
  345. dividerPosition={dividerPosition}
  346. interactiveLayerRef={virtualScrollbarContainerRef}
  347. >
  348. <StyledTracePanel>
  349. <TraceViewHeaderContainer>
  350. <ScrollbarManager.Consumer>
  351. {({virtualScrollbarRef, scrollBarAreaRef, onDragStart, onScroll}) => {
  352. return (
  353. <ScrollbarContainer
  354. ref={virtualScrollbarContainerRef}
  355. style={{
  356. // the width of this component is shrunk to compensate for half of the width of the divider line
  357. width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
  358. }}
  359. onScroll={onScroll}
  360. >
  361. <div
  362. style={{
  363. width: 0,
  364. height: '1px',
  365. }}
  366. ref={scrollBarAreaRef}
  367. />
  368. <VirtualScrollbar
  369. data-type="virtual-scrollbar"
  370. ref={virtualScrollbarRef}
  371. onMouseDown={onDragStart}
  372. >
  373. <VirtualScrollbarGrip />
  374. </VirtualScrollbar>
  375. </ScrollbarContainer>
  376. );
  377. }}
  378. </ScrollbarManager.Consumer>
  379. <DividerSpacer />
  380. {measurements ? (
  381. <MeasurementsPanel
  382. measurements={measurements}
  383. generateBounds={bounds}
  384. dividerPosition={dividerPosition}
  385. />
  386. ) : null}
  387. </TraceViewHeaderContainer>
  388. <TraceViewContainer ref={traceViewRef}>
  389. <TransactionGroup
  390. location={location}
  391. organization={organization}
  392. traceInfo={traceInfo}
  393. transaction={{
  394. traceSlug,
  395. generation: 0,
  396. 'transaction.duration':
  397. traceInfo.endTimestamp - traceInfo.startTimestamp,
  398. children: traces,
  399. start_timestamp: traceInfo.startTimestamp,
  400. timestamp: traceInfo.endTimestamp,
  401. }}
  402. measurements={measurements}
  403. generateBounds={bounds}
  404. continuingDepths={[]}
  405. isOrphan={false}
  406. isLast={false}
  407. index={0}
  408. isVisible
  409. hasGuideAnchor={false}
  410. renderedChildren={transactionGroups}
  411. barColor={pickBarColor('')}
  412. onlyOrphanErrors={onlyOrphanErrors}
  413. numOfOrphanErrors={orphanErrors?.length}
  414. />
  415. <TraceHiddenMessage
  416. isVisible
  417. numberOfHiddenTransactionsAbove={numberOfHiddenTransactionsAbove}
  418. numberOfHiddenErrorsAbove={totalNumOfHiddenErrors}
  419. />
  420. <LimitExceededMessage
  421. traceInfo={traceInfo}
  422. organization={organization}
  423. traceEventView={traceEventView}
  424. meta={meta}
  425. />
  426. </TraceViewContainer>
  427. </StyledTracePanel>
  428. </ScrollbarManager.Provider>
  429. )}
  430. </DividerHandlerManager.Consumer>
  431. </DividerHandlerManager.Provider>
  432. </TraceDetailBody>
  433. );
  434. sentrySpan?.finish();
  435. return traceView;
  436. }
  437. export const StyledTracePanel = styled(Panel)`
  438. height: 100%;
  439. overflow-x: visible;
  440. ${TraceViewContainer} {
  441. overflow-x: visible;
  442. }
  443. `;