traceView.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  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. useEffect(() => {
  139. trackAnalytics('performance_views.trace_view.view', {
  140. organization,
  141. });
  142. }, [organization]);
  143. function renderTransaction(
  144. transaction: TraceFullDetailed,
  145. {
  146. continuingDepths,
  147. isOrphan,
  148. isLast,
  149. index,
  150. numberOfHiddenTransactionsAbove,
  151. traceInfo,
  152. hasGuideAnchor,
  153. }: {
  154. continuingDepths: TreeDepth[];
  155. hasGuideAnchor: boolean;
  156. index: number;
  157. isLast: boolean;
  158. isOrphan: boolean;
  159. numberOfHiddenTransactionsAbove: number;
  160. traceInfo: TraceInfo;
  161. }
  162. ) {
  163. const {children, event_id: eventId} = transaction;
  164. // Add 1 to the generation to make room for the "root trace"
  165. const generation = transaction.generation + 1;
  166. const isVisible = isRowVisible(transaction, filteredEventIds);
  167. const accumulated: AccType = children.reduce(
  168. (acc: AccType, child: TraceFullDetailed, idx: number) => {
  169. const isLastChild = idx === children.length - 1;
  170. const hasChildren = child.children.length > 0;
  171. const result = renderTransaction(child, {
  172. continuingDepths:
  173. !isLastChild && hasChildren
  174. ? [...continuingDepths, {depth: generation, isOrphanDepth: isOrphan}]
  175. : continuingDepths,
  176. isOrphan,
  177. isLast: isLastChild,
  178. index: acc.lastIndex + 1,
  179. numberOfHiddenTransactionsAbove: acc.numberOfHiddenTransactionsAbove,
  180. traceInfo,
  181. hasGuideAnchor: false,
  182. });
  183. acc.lastIndex = result.lastIndex;
  184. acc.numberOfHiddenTransactionsAbove = result.numberOfHiddenTransactionsAbove;
  185. acc.renderedChildren.push(result.transactionGroup);
  186. return acc;
  187. },
  188. {
  189. renderedChildren: [],
  190. lastIndex: index,
  191. numberOfHiddenTransactionsAbove: isVisible
  192. ? 0
  193. : numberOfHiddenTransactionsAbove + 1,
  194. }
  195. );
  196. return {
  197. transactionGroup: (
  198. <Fragment key={eventId}>
  199. <TraceHiddenMessage
  200. isVisible={isVisible}
  201. numberOfHiddenTransactionsAbove={numberOfHiddenTransactionsAbove}
  202. numberOfHiddenErrorsAbove={0}
  203. />
  204. <TransactionGroup
  205. location={location}
  206. organization={organization}
  207. traceInfo={traceInfo}
  208. transaction={{
  209. ...transaction,
  210. generation,
  211. }}
  212. measurements={
  213. traces && traces.length > 0
  214. ? getMeasurements(traces[0], generateBounds(traceInfo))
  215. : undefined
  216. }
  217. generateBounds={generateBounds(traceInfo)}
  218. continuingDepths={continuingDepths}
  219. isOrphan={isOrphan}
  220. isLast={isLast}
  221. index={index}
  222. isVisible={isVisible}
  223. hasGuideAnchor={hasGuideAnchor}
  224. renderedChildren={accumulated.renderedChildren}
  225. barColor={pickBarColor(transaction['transaction.op'])}
  226. />
  227. </Fragment>
  228. ),
  229. lastIndex: accumulated.lastIndex,
  230. numberOfHiddenTransactionsAbove: accumulated.numberOfHiddenTransactionsAbove,
  231. };
  232. }
  233. const traceViewRef = createRef<HTMLDivElement>();
  234. const virtualScrollbarContainerRef = createRef<HTMLDivElement>();
  235. if (!hasTraceData(traces, orphanErrors)) {
  236. return (
  237. <TraceNotFound
  238. meta={meta}
  239. traceEventView={traceEventView}
  240. traceSlug={traceSlug}
  241. location={location}
  242. organization={organization}
  243. />
  244. );
  245. }
  246. const traceInfo = props.traceInfo || getTraceInfo(traces);
  247. const accumulator: {
  248. index: number;
  249. numberOfHiddenTransactionsAbove: number;
  250. traceInfo: TraceInfo;
  251. transactionGroups: React.ReactNode[];
  252. } = {
  253. index: 1,
  254. numberOfHiddenTransactionsAbove: 0,
  255. traceInfo,
  256. transactionGroups: [],
  257. };
  258. let lastIndex: number = 0;
  259. const {transactionGroups, numberOfHiddenTransactionsAbove} = traces.reduce(
  260. (acc, trace, index) => {
  261. const isLastTransaction = index === traces.length - 1;
  262. const hasChildren = trace.children.length > 0;
  263. const isNextChildOrphaned =
  264. !isLastTransaction && traces[index + 1].parent_span_id !== null;
  265. const result = renderTransaction(trace, {
  266. ...acc,
  267. // if the root of a subtrace has a parent_span_id, then it must be an orphan
  268. isOrphan: !isRootTransaction(trace),
  269. isLast: isLastTransaction && !hasOrphanErrors,
  270. continuingDepths:
  271. (!isLastTransaction && hasChildren) || hasOrphanErrors
  272. ? [{depth: 0, isOrphanDepth: isNextChildOrphaned || Boolean(hasOrphanErrors)}]
  273. : [],
  274. hasGuideAnchor: index === 0,
  275. });
  276. acc.index = result.lastIndex + 1;
  277. lastIndex = Math.max(lastIndex, result.lastIndex);
  278. acc.numberOfHiddenTransactionsAbove = result.numberOfHiddenTransactionsAbove;
  279. acc.transactionGroups.push(result.transactionGroup);
  280. return acc;
  281. },
  282. accumulator
  283. );
  284. // Build transaction groups for orphan errors
  285. let numOfHiddenErrorsAbove = 0;
  286. let totalNumOfHiddenErrors = 0;
  287. if (hasOrphanErrors) {
  288. orphanErrors.forEach((error, index) => {
  289. const isLastError = index === orphanErrors.length - 1;
  290. const isVisible = isRowVisible(error, filteredEventIds);
  291. const currentHiddenCount = numOfHiddenErrorsAbove;
  292. if (!isVisible) {
  293. numOfHiddenErrorsAbove += 1;
  294. totalNumOfHiddenErrors += 1;
  295. } else {
  296. numOfHiddenErrorsAbove = 0;
  297. }
  298. transactionGroups.push(
  299. <Fragment key={error.event_id}>
  300. <TraceHiddenMessage
  301. isVisible={isVisible}
  302. numberOfHiddenTransactionsAbove={
  303. index === 0 ? numberOfHiddenTransactionsAbove : 0
  304. }
  305. numberOfHiddenErrorsAbove={index > 0 ? currentHiddenCount : 0}
  306. />
  307. <TransactionGroup
  308. location={location}
  309. organization={organization}
  310. traceInfo={traceInfo}
  311. transaction={{
  312. ...error,
  313. generation: 1,
  314. }}
  315. generateBounds={generateBounds(traceInfo)}
  316. measurements={
  317. traces && traces.length > 0
  318. ? getMeasurements(traces[0], generateBounds(traceInfo))
  319. : undefined
  320. }
  321. continuingDepths={[]}
  322. isOrphan
  323. isLast={isLastError}
  324. index={lastIndex + index + 1}
  325. isVisible={isVisible}
  326. hasGuideAnchor
  327. renderedChildren={[]}
  328. />
  329. </Fragment>
  330. );
  331. });
  332. }
  333. const bounds = generateBounds(traceInfo);
  334. const measurements =
  335. traces.length > 0 && Object.keys(traces[0].measurements ?? {}).length > 0
  336. ? getMeasurements(traces[0], bounds)
  337. : undefined;
  338. const traceView = (
  339. <TraceDetailBody>
  340. <DividerHandlerManager.Provider interactiveLayerRef={traceViewRef}>
  341. <DividerHandlerManager.Consumer>
  342. {({dividerPosition}) => (
  343. <ScrollbarManager.Provider
  344. dividerPosition={dividerPosition}
  345. interactiveLayerRef={virtualScrollbarContainerRef}
  346. >
  347. <StyledTracePanel>
  348. <TraceViewHeaderContainer>
  349. <ScrollbarManager.Consumer>
  350. {({virtualScrollbarRef, scrollBarAreaRef, onDragStart, onScroll}) => {
  351. return (
  352. <ScrollbarContainer
  353. ref={virtualScrollbarContainerRef}
  354. style={{
  355. // the width of this component is shrunk to compensate for half of the width of the divider line
  356. width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
  357. }}
  358. onScroll={onScroll}
  359. >
  360. <div
  361. style={{
  362. width: 0,
  363. height: '1px',
  364. }}
  365. ref={scrollBarAreaRef}
  366. />
  367. <VirtualScrollbar
  368. data-type="virtual-scrollbar"
  369. ref={virtualScrollbarRef}
  370. onMouseDown={onDragStart}
  371. >
  372. <VirtualScrollbarGrip />
  373. </VirtualScrollbar>
  374. </ScrollbarContainer>
  375. );
  376. }}
  377. </ScrollbarManager.Consumer>
  378. <DividerSpacer />
  379. {measurements ? (
  380. <MeasurementsPanel
  381. measurements={measurements}
  382. generateBounds={bounds}
  383. dividerPosition={dividerPosition}
  384. />
  385. ) : null}
  386. </TraceViewHeaderContainer>
  387. <TraceViewContainer ref={traceViewRef}>
  388. <TransactionGroup
  389. location={location}
  390. organization={organization}
  391. traceInfo={traceInfo}
  392. transaction={{
  393. traceSlug,
  394. generation: 0,
  395. 'transaction.duration':
  396. traceInfo.endTimestamp - traceInfo.startTimestamp,
  397. children: traces,
  398. start_timestamp: traceInfo.startTimestamp,
  399. timestamp: traceInfo.endTimestamp,
  400. }}
  401. measurements={measurements}
  402. generateBounds={bounds}
  403. continuingDepths={[]}
  404. isOrphan={false}
  405. isLast={false}
  406. index={0}
  407. isVisible
  408. hasGuideAnchor={false}
  409. renderedChildren={transactionGroups}
  410. barColor={pickBarColor('')}
  411. numOfOrphanErrors={orphanErrors?.length}
  412. />
  413. <TraceHiddenMessage
  414. isVisible
  415. numberOfHiddenTransactionsAbove={numberOfHiddenTransactionsAbove}
  416. numberOfHiddenErrorsAbove={totalNumOfHiddenErrors}
  417. />
  418. <LimitExceededMessage
  419. traceInfo={traceInfo}
  420. organization={organization}
  421. traceEventView={traceEventView}
  422. meta={meta}
  423. />
  424. </TraceViewContainer>
  425. </StyledTracePanel>
  426. </ScrollbarManager.Provider>
  427. )}
  428. </DividerHandlerManager.Consumer>
  429. </DividerHandlerManager.Provider>
  430. </TraceDetailBody>
  431. );
  432. sentrySpan?.finish();
  433. return traceView;
  434. }
  435. export const StyledTracePanel = styled(Panel)`
  436. height: 100%;
  437. overflow-x: visible;
  438. ${TraceViewContainer} {
  439. overflow-x: visible;
  440. }
  441. `;