newTraceDetailsTraceView.tsx 17 KB

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