newTraceDetailsTraceView.tsx 17 KB

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