traceView.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. import {createRef, Fragment, useEffect} from 'react';
  2. import type {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 type {Organization} from 'sentry/types/organization';
  23. import {trackAnalytics} from 'sentry/utils/analytics';
  24. import type EventView from 'sentry/utils/discover/eventView';
  25. import toPercent from 'sentry/utils/number/toPercent';
  26. import type {
  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 type {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 sentrySpan = Sentry.startInactiveSpan({
  135. op: 'trace.render',
  136. name: 'trace-view-content',
  137. onlyIfParent: true,
  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. traceViewRef={traceViewRef}
  210. organization={organization}
  211. traceInfo={traceInfo}
  212. transaction={{
  213. ...transaction,
  214. generation,
  215. }}
  216. measurements={
  217. traces && traces.length > 0
  218. ? getMeasurements(traces[0], generateBounds(traceInfo))
  219. : undefined
  220. }
  221. generateBounds={generateBounds(traceInfo)}
  222. continuingDepths={continuingDepths}
  223. isOrphan={isOrphan}
  224. isLast={isLast}
  225. index={index}
  226. isVisible={isVisible}
  227. hasGuideAnchor={hasGuideAnchor}
  228. renderedChildren={accumulated.renderedChildren}
  229. barColor={pickBarColor(transaction['transaction.op'])}
  230. />
  231. </Fragment>
  232. ),
  233. lastIndex: accumulated.lastIndex,
  234. numberOfHiddenTransactionsAbove: accumulated.numberOfHiddenTransactionsAbove,
  235. };
  236. }
  237. const traceViewRef = createRef<HTMLDivElement>();
  238. const virtualScrollbarContainerRef = createRef<HTMLDivElement>();
  239. if (!hasTraceData(traces, orphanErrors)) {
  240. return (
  241. <TraceNotFound
  242. meta={meta}
  243. traceEventView={traceEventView}
  244. traceSlug={traceSlug}
  245. location={location}
  246. organization={organization}
  247. />
  248. );
  249. }
  250. const traceInfo = props.traceInfo || getTraceInfo(traces);
  251. const accumulator: {
  252. index: number;
  253. numberOfHiddenTransactionsAbove: number;
  254. traceInfo: TraceInfo;
  255. transactionGroups: React.ReactNode[];
  256. } = {
  257. index: 1,
  258. numberOfHiddenTransactionsAbove: 0,
  259. traceInfo,
  260. transactionGroups: [],
  261. };
  262. let lastIndex: number = 0;
  263. const {transactionGroups, numberOfHiddenTransactionsAbove} = traces.reduce(
  264. (acc, trace, index) => {
  265. const isLastTransaction = index === traces.length - 1;
  266. const hasChildren = trace.children.length > 0;
  267. const isNextChildOrphaned =
  268. !isLastTransaction && traces[index + 1].parent_span_id !== null;
  269. const result = renderTransaction(trace, {
  270. ...acc,
  271. // if the root of a subtrace has a parent_span_id, then it must be an orphan
  272. isOrphan: !isRootTransaction(trace),
  273. isLast: isLastTransaction && !hasOrphanErrors,
  274. continuingDepths:
  275. (!isLastTransaction && hasChildren) || hasOrphanErrors
  276. ? [{depth: 0, isOrphanDepth: isNextChildOrphaned || Boolean(hasOrphanErrors)}]
  277. : [],
  278. hasGuideAnchor: index === 0,
  279. });
  280. acc.index = result.lastIndex + 1;
  281. lastIndex = Math.max(lastIndex, result.lastIndex);
  282. acc.numberOfHiddenTransactionsAbove = result.numberOfHiddenTransactionsAbove;
  283. acc.transactionGroups.push(result.transactionGroup);
  284. return acc;
  285. },
  286. accumulator
  287. );
  288. // Build transaction groups for orphan errors
  289. let numOfHiddenErrorsAbove = 0;
  290. let totalNumOfHiddenErrors = 0;
  291. if (hasOrphanErrors) {
  292. orphanErrors.forEach((error, index) => {
  293. const isLastError = index === orphanErrors.length - 1;
  294. const isVisible = isRowVisible(error, filteredEventIds);
  295. const currentHiddenCount = numOfHiddenErrorsAbove;
  296. if (!isVisible) {
  297. numOfHiddenErrorsAbove += 1;
  298. totalNumOfHiddenErrors += 1;
  299. } else {
  300. numOfHiddenErrorsAbove = 0;
  301. }
  302. transactionGroups.push(
  303. <Fragment key={error.event_id}>
  304. <TraceHiddenMessage
  305. isVisible={isVisible}
  306. numberOfHiddenTransactionsAbove={
  307. index === 0 ? numberOfHiddenTransactionsAbove : 0
  308. }
  309. numberOfHiddenErrorsAbove={index > 0 ? currentHiddenCount : 0}
  310. />
  311. <TransactionGroup
  312. location={location}
  313. organization={organization}
  314. traceViewRef={traceViewRef}
  315. traceInfo={traceInfo}
  316. transaction={{
  317. ...error,
  318. generation: 1,
  319. }}
  320. generateBounds={generateBounds(traceInfo)}
  321. measurements={
  322. traces && traces.length > 0
  323. ? getMeasurements(traces[0], generateBounds(traceInfo))
  324. : undefined
  325. }
  326. continuingDepths={[]}
  327. isOrphan
  328. isLast={isLastError}
  329. index={lastIndex + index + 1}
  330. isVisible={isVisible}
  331. hasGuideAnchor={index === 0 && transactionGroups.length === 0}
  332. renderedChildren={[]}
  333. />
  334. </Fragment>
  335. );
  336. });
  337. }
  338. const bounds = generateBounds(traceInfo);
  339. const measurements =
  340. traces.length > 0 && Object.keys(traces[0].measurements ?? {}).length > 0
  341. ? getMeasurements(traces[0], bounds)
  342. : undefined;
  343. const traceView = (
  344. <TraceDetailBody>
  345. <DividerHandlerManager.Provider interactiveLayerRef={traceViewRef}>
  346. <DividerHandlerManager.Consumer>
  347. {({dividerPosition}) => (
  348. <ScrollbarManager.Provider
  349. dividerPosition={dividerPosition}
  350. interactiveLayerRef={virtualScrollbarContainerRef}
  351. isEmbedded
  352. >
  353. <StyledTracePanel>
  354. <TraceViewHeaderContainer>
  355. <ScrollbarManager.Consumer>
  356. {({virtualScrollbarRef, scrollBarAreaRef, onDragStart, onScroll}) => {
  357. return (
  358. <ScrollbarContainer
  359. ref={virtualScrollbarContainerRef}
  360. style={{
  361. // the width of this component is shrunk to compensate for half of the width of the divider line
  362. width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
  363. }}
  364. onScroll={onScroll}
  365. >
  366. <div
  367. style={{
  368. width: 0,
  369. height: '1px',
  370. }}
  371. ref={scrollBarAreaRef}
  372. />
  373. <VirtualScrollbar
  374. data-type="virtual-scrollbar"
  375. ref={virtualScrollbarRef}
  376. onMouseDown={onDragStart}
  377. >
  378. <VirtualScrollbarGrip />
  379. </VirtualScrollbar>
  380. </ScrollbarContainer>
  381. );
  382. }}
  383. </ScrollbarManager.Consumer>
  384. <DividerSpacer />
  385. {measurements ? (
  386. <MeasurementsPanel
  387. measurements={measurements}
  388. generateBounds={bounds}
  389. dividerPosition={dividerPosition}
  390. />
  391. ) : null}
  392. </TraceViewHeaderContainer>
  393. <TraceViewContainer ref={traceViewRef}>
  394. <TransactionGroup
  395. location={location}
  396. organization={organization}
  397. traceInfo={traceInfo}
  398. transaction={{
  399. traceSlug,
  400. generation: 0,
  401. 'transaction.duration':
  402. traceInfo.endTimestamp - traceInfo.startTimestamp,
  403. children: traces,
  404. start_timestamp: traceInfo.startTimestamp,
  405. timestamp: traceInfo.endTimestamp,
  406. }}
  407. measurements={measurements}
  408. generateBounds={bounds}
  409. continuingDepths={[]}
  410. isOrphan={false}
  411. isLast={false}
  412. index={0}
  413. isVisible
  414. hasGuideAnchor={false}
  415. renderedChildren={transactionGroups}
  416. barColor={pickBarColor('')}
  417. onlyOrphanErrors={onlyOrphanErrors}
  418. traceViewRef={traceViewRef}
  419. numOfOrphanErrors={orphanErrors?.length}
  420. />
  421. <TraceHiddenMessage
  422. isVisible
  423. numberOfHiddenTransactionsAbove={numberOfHiddenTransactionsAbove}
  424. numberOfHiddenErrorsAbove={totalNumOfHiddenErrors}
  425. />
  426. <LimitExceededMessage
  427. traceInfo={traceInfo}
  428. organization={organization}
  429. traceEventView={traceEventView}
  430. meta={meta}
  431. handleLimitChange={handleLimitChange}
  432. />
  433. </TraceViewContainer>
  434. </StyledTracePanel>
  435. </ScrollbarManager.Provider>
  436. )}
  437. </DividerHandlerManager.Consumer>
  438. </DividerHandlerManager.Provider>
  439. </TraceDetailBody>
  440. );
  441. sentrySpan?.end();
  442. return traceView;
  443. }
  444. export const StyledTracePanel = styled(Panel)`
  445. height: 100%;
  446. overflow-x: visible;
  447. ${TraceViewContainer} {
  448. overflow-x: visible;
  449. }
  450. `;