traceView.tsx 15 KB

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