traceView.tsx 14 KB

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