newTraceDetailsTraceView.tsx 17 KB

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