traceView.tsx 16 KB

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