newTraceDetailsTraceView.tsx 17 KB

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