newTraceDetailsTraceView.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. import {createRef, Fragment, memo, useEffect} from 'react';
  2. import {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 {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 {EventTransaction, Organization} from 'sentry/types';
  25. import {trackAnalytics} from 'sentry/utils/analytics';
  26. import EventView from 'sentry/utils/discover/eventView';
  27. import toPercent from 'sentry/utils/number/toPercent';
  28. import {
  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 {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 {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 sentryTransaction = Sentry.getCurrentHub().getScope()?.getTransaction();
  143. const sentrySpan = sentryTransaction?.startChild({
  144. op: 'trace.render',
  145. description: 'trace-view-content',
  146. });
  147. const hasOrphanErrors = orphanErrors && orphanErrors.length > 0;
  148. const onlyOrphanErrors = hasOrphanErrors && (!traces || traces.length === 0);
  149. useEffect(() => {
  150. trackAnalytics('performance_views.trace_view.view', {
  151. organization,
  152. });
  153. }, [organization]);
  154. function renderTransaction(
  155. transaction: TraceFullDetailed,
  156. {
  157. continuingDepths,
  158. isOrphan,
  159. isLast,
  160. index,
  161. numberOfHiddenTransactionsAbove,
  162. traceInfo,
  163. hasGuideAnchor,
  164. }: {
  165. continuingDepths: TreeDepth[];
  166. hasGuideAnchor: boolean;
  167. index: number;
  168. isLast: boolean;
  169. isOrphan: boolean;
  170. numberOfHiddenTransactionsAbove: number;
  171. traceInfo: TraceInfo;
  172. }
  173. ) {
  174. const {children, event_id: eventId} = transaction;
  175. // Add 1 to the generation to make room for the "root trace"
  176. const generation = transaction.generation + 1;
  177. const isVisible = isRowVisible(transaction, filteredEventIds);
  178. const accumulated: AccType = children.reduce(
  179. (acc: AccType, child: TraceFullDetailed, idx: number) => {
  180. const isLastChild = idx === children.length - 1;
  181. const hasChildren = child.children.length > 0;
  182. const result = renderTransaction(child, {
  183. continuingDepths:
  184. !isLastChild && hasChildren
  185. ? [...continuingDepths, {depth: generation, isOrphanDepth: isOrphan}]
  186. : continuingDepths,
  187. isOrphan,
  188. isLast: isLastChild,
  189. index: acc.lastIndex + 1,
  190. numberOfHiddenTransactionsAbove: acc.numberOfHiddenTransactionsAbove,
  191. traceInfo,
  192. hasGuideAnchor: false,
  193. });
  194. acc.lastIndex = result.lastIndex;
  195. acc.numberOfHiddenTransactionsAbove = result.numberOfHiddenTransactionsAbove;
  196. acc.renderedChildren.push(result.transactionGroup);
  197. return acc;
  198. },
  199. {
  200. renderedChildren: [],
  201. lastIndex: index,
  202. numberOfHiddenTransactionsAbove: isVisible
  203. ? 0
  204. : numberOfHiddenTransactionsAbove + 1,
  205. }
  206. );
  207. return {
  208. transactionGroup: (
  209. <Fragment key={eventId}>
  210. <TraceHiddenMessage
  211. isVisible={isVisible}
  212. numberOfHiddenTransactionsAbove={numberOfHiddenTransactionsAbove}
  213. numberOfHiddenErrorsAbove={0}
  214. />
  215. <TransactionGroup
  216. onRowClick={onRowClick}
  217. location={location}
  218. traceViewRef={traceViewRef}
  219. organization={organization}
  220. traceInfo={traceInfo}
  221. transaction={{
  222. ...transaction,
  223. generation,
  224. }}
  225. measurements={
  226. traces && traces.length > 0
  227. ? getMeasurements(traces[0], generateBounds(traceInfo))
  228. : undefined
  229. }
  230. generateBounds={generateBounds(traceInfo)}
  231. continuingDepths={continuingDepths}
  232. isOrphan={isOrphan}
  233. isLast={isLast}
  234. index={index}
  235. isVisible={isVisible}
  236. hasGuideAnchor={hasGuideAnchor}
  237. renderedChildren={accumulated.renderedChildren}
  238. barColor={pickBarColor(transaction['transaction.op'])}
  239. />
  240. </Fragment>
  241. ),
  242. lastIndex: accumulated.lastIndex,
  243. numberOfHiddenTransactionsAbove: accumulated.numberOfHiddenTransactionsAbove,
  244. };
  245. }
  246. const traceViewRef = createRef<HTMLDivElement>();
  247. const virtualScrollbarContainerRef = createRef<HTMLDivElement>();
  248. if (!hasTraceData(traces, orphanErrors)) {
  249. return (
  250. <TraceNotFound
  251. meta={meta}
  252. traceEventView={traceEventView}
  253. traceSlug={traceSlug}
  254. location={location}
  255. organization={organization}
  256. />
  257. );
  258. }
  259. const traceInfo = props.traceInfo || getTraceInfo(traces);
  260. const accumulator: {
  261. index: number;
  262. numberOfHiddenTransactionsAbove: number;
  263. traceInfo: TraceInfo;
  264. transactionGroups: React.ReactNode[];
  265. } = {
  266. index: 1,
  267. numberOfHiddenTransactionsAbove: 0,
  268. traceInfo,
  269. transactionGroups: [],
  270. };
  271. let lastIndex: number = 0;
  272. const {transactionGroups, numberOfHiddenTransactionsAbove} = traces.reduce(
  273. (acc, trace, index) => {
  274. const isLastTransaction = index === traces.length - 1;
  275. const hasChildren = trace.children.length > 0;
  276. const isNextChildOrphaned =
  277. !isLastTransaction && traces[index + 1].parent_span_id !== null;
  278. const result = renderTransaction(trace, {
  279. ...acc,
  280. // if the root of a subtrace has a parent_span_id, then it must be an orphan
  281. isOrphan: !isRootTransaction(trace),
  282. isLast: isLastTransaction && !hasOrphanErrors,
  283. continuingDepths:
  284. (!isLastTransaction && hasChildren) || hasOrphanErrors
  285. ? [{depth: 0, isOrphanDepth: isNextChildOrphaned || Boolean(hasOrphanErrors)}]
  286. : [],
  287. hasGuideAnchor: index === 0,
  288. });
  289. acc.index = result.lastIndex + 1;
  290. lastIndex = Math.max(lastIndex, result.lastIndex);
  291. acc.numberOfHiddenTransactionsAbove = result.numberOfHiddenTransactionsAbove;
  292. acc.transactionGroups.push(result.transactionGroup);
  293. return acc;
  294. },
  295. accumulator
  296. );
  297. // Build transaction groups for orphan errors
  298. let numOfHiddenErrorsAbove = 0;
  299. let totalNumOfHiddenErrors = 0;
  300. if (hasOrphanErrors) {
  301. orphanErrors.forEach((error, index) => {
  302. const isLastError = index === orphanErrors.length - 1;
  303. const isVisible = isRowVisible(error, filteredEventIds);
  304. const currentHiddenCount = numOfHiddenErrorsAbove;
  305. if (!isVisible) {
  306. numOfHiddenErrorsAbove += 1;
  307. totalNumOfHiddenErrors += 1;
  308. } else {
  309. numOfHiddenErrorsAbove = 0;
  310. }
  311. transactionGroups.push(
  312. <Fragment key={error.event_id}>
  313. <TraceHiddenMessage
  314. isVisible={isVisible}
  315. numberOfHiddenTransactionsAbove={
  316. index === 0 ? numberOfHiddenTransactionsAbove : 0
  317. }
  318. numberOfHiddenErrorsAbove={index > 0 ? currentHiddenCount : 0}
  319. />
  320. <TransactionGroup
  321. onRowClick={onRowClick}
  322. location={location}
  323. organization={organization}
  324. traceViewRef={traceViewRef}
  325. traceInfo={traceInfo}
  326. transaction={{
  327. ...error,
  328. generation: 1,
  329. }}
  330. generateBounds={generateBounds(traceInfo)}
  331. measurements={
  332. traces && traces.length > 0
  333. ? getMeasurements(traces[0], generateBounds(traceInfo))
  334. : undefined
  335. }
  336. continuingDepths={[]}
  337. isOrphan
  338. isLast={isLastError}
  339. index={lastIndex + index + 1}
  340. isVisible={isVisible}
  341. hasGuideAnchor={index === 0 && transactionGroups.length === 0}
  342. renderedChildren={[]}
  343. />
  344. </Fragment>
  345. );
  346. });
  347. }
  348. const bounds = generateBounds(traceInfo);
  349. const measurements =
  350. traces.length > 0 && Object.keys(traces[0].measurements ?? {}).length > 0
  351. ? getMeasurements(traces[0], bounds)
  352. : undefined;
  353. const traceView = (
  354. <TraceDetailBody>
  355. <DividerHandlerManager.Provider interactiveLayerRef={traceViewRef}>
  356. <DividerHandlerManager.Consumer>
  357. {({dividerPosition}) => (
  358. <ScrollbarManager.Provider
  359. dividerPosition={dividerPosition}
  360. interactiveLayerRef={virtualScrollbarContainerRef}
  361. isEmbedded
  362. >
  363. <StyledTracePanel>
  364. <TraceViewHeader
  365. traceInfo={traceInfo}
  366. traceType={traceType}
  367. traceViewHeaderRef={traceViewRef}
  368. organization={organization}
  369. event={props.rootEvent}
  370. />
  371. <TraceViewHeaderContainer>
  372. <ScrollbarManager.Consumer>
  373. {({virtualScrollbarRef, scrollBarAreaRef, onDragStart, onScroll}) => {
  374. return (
  375. <ScrollbarContainer
  376. ref={virtualScrollbarContainerRef}
  377. style={{
  378. // the width of this component is shrunk to compensate for half of the width of the divider line
  379. width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
  380. }}
  381. onScroll={onScroll}
  382. >
  383. <div
  384. style={{
  385. width: 0,
  386. height: '1px',
  387. }}
  388. ref={scrollBarAreaRef}
  389. />
  390. <VirtualScrollbar
  391. data-type="virtual-scrollbar"
  392. ref={virtualScrollbarRef}
  393. onMouseDown={onDragStart}
  394. >
  395. <VirtualScrollbarGrip />
  396. </VirtualScrollbar>
  397. </ScrollbarContainer>
  398. );
  399. }}
  400. </ScrollbarManager.Consumer>
  401. <DividerSpacer />
  402. {measurements ? (
  403. <MeasurementsPanel
  404. measurements={measurements}
  405. generateBounds={bounds}
  406. dividerPosition={dividerPosition}
  407. />
  408. ) : null}
  409. </TraceViewHeaderContainer>
  410. <TraceViewContainer ref={traceViewRef}>
  411. <TransactionGroup
  412. onRowClick={onRowClick}
  413. location={location}
  414. organization={organization}
  415. traceInfo={traceInfo}
  416. transaction={{
  417. traceSlug,
  418. generation: 0,
  419. 'transaction.duration':
  420. traceInfo.endTimestamp - traceInfo.startTimestamp,
  421. children: traces,
  422. start_timestamp: traceInfo.startTimestamp,
  423. timestamp: traceInfo.endTimestamp,
  424. }}
  425. measurements={measurements}
  426. generateBounds={bounds}
  427. continuingDepths={[]}
  428. isOrphan={false}
  429. isLast={false}
  430. index={0}
  431. isVisible
  432. hasGuideAnchor={false}
  433. renderedChildren={transactionGroups}
  434. barColor={pickBarColor('')}
  435. onlyOrphanErrors={onlyOrphanErrors}
  436. traceViewRef={traceViewRef}
  437. numOfOrphanErrors={orphanErrors?.length}
  438. />
  439. <TraceHiddenMessage
  440. isVisible
  441. numberOfHiddenTransactionsAbove={numberOfHiddenTransactionsAbove}
  442. numberOfHiddenErrorsAbove={totalNumOfHiddenErrors}
  443. />
  444. <LimitExceededMessage
  445. traceInfo={traceInfo}
  446. organization={organization}
  447. traceEventView={traceEventView}
  448. meta={meta}
  449. handleLimitChange={handleLimitChange}
  450. />
  451. </TraceViewContainer>
  452. </StyledTracePanel>
  453. </ScrollbarManager.Provider>
  454. )}
  455. </DividerHandlerManager.Consumer>
  456. </DividerHandlerManager.Provider>
  457. </TraceDetailBody>
  458. );
  459. sentrySpan?.finish();
  460. return traceView;
  461. }
  462. export default memo(NewTraceView);
  463. export const StyledTracePanel = styled(Panel)`
  464. height: 100%;
  465. overflow-x: visible;
  466. ${TraceViewContainer} {
  467. overflow-x: visible;
  468. }
  469. `;