trace.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. import {Fragment, useCallback, useMemo, useRef, useState} from 'react';
  2. import {AutoSizer, List} from 'react-virtualized';
  3. import styled from '@emotion/styled';
  4. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  5. import {IconChevron} from 'sentry/icons';
  6. import {t} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import type {Project} from 'sentry/types';
  9. import type {
  10. TraceFullDetailed,
  11. TraceSplitResults,
  12. } from 'sentry/utils/performance/quickTrace/types';
  13. import useApi from 'sentry/utils/useApi';
  14. import useOrganization from 'sentry/utils/useOrganization';
  15. import useProjects from 'sentry/utils/useProjects';
  16. import {
  17. isAutogroupedNode,
  18. isMissingInstrumentationNode,
  19. isSpanNode,
  20. isTraceErrorNode,
  21. isTransactionNode,
  22. } from './guards';
  23. import {ParentAutogroupNode, TraceTree, type TraceTreeNode} from './traceTree';
  24. interface TraceProps {
  25. trace: TraceSplitResults<TraceFullDetailed> | null;
  26. trace_id: string;
  27. }
  28. export function Trace(props: TraceProps) {
  29. const api = useApi();
  30. const organization = useOrganization();
  31. const virtualizedListRef = useRef<List>(null);
  32. const traceTree = useMemo(() => {
  33. if (!props.trace) {
  34. return TraceTree.Empty();
  35. }
  36. return TraceTree.FromTrace(props.trace);
  37. }, [props.trace]);
  38. const [_rerender, setRender] = useState(0);
  39. const treeRef = useRef<TraceTree>(traceTree);
  40. treeRef.current = traceTree;
  41. const handleFetchChildren = useCallback(
  42. (node: TraceTreeNode<TraceTree.NodeValue>, value: boolean) => {
  43. treeRef.current
  44. .zoomIn(node, value, {
  45. api,
  46. organization,
  47. })
  48. .then(() => {
  49. setRender(a => (a + 1) % 2);
  50. });
  51. },
  52. [api, organization]
  53. );
  54. const handleExpandNode = useCallback(
  55. (node: TraceTreeNode<TraceTree.NodeValue>, value: boolean) => {
  56. treeRef.current.expand(node, value);
  57. setRender(a => (a + 1) % 2);
  58. },
  59. []
  60. );
  61. const {projects} = useProjects();
  62. const projectLookup = useMemo(() => {
  63. return projects.reduce<Record<Project['slug'], Project>>((acc, project) => {
  64. acc[project.slug] = project;
  65. return acc;
  66. }, {});
  67. }, [projects]);
  68. return (
  69. <TraceStylingWrapper
  70. style={{
  71. padding: 24,
  72. backgroundColor: '#FFF',
  73. height: '100%',
  74. width: '100%',
  75. position: 'absolute',
  76. }}
  77. >
  78. <AutoSizer>
  79. {({width, height}) => (
  80. <List
  81. ref={virtualizedListRef}
  82. rowHeight={24}
  83. height={height}
  84. width={width}
  85. overscanRowCount={10}
  86. rowCount={treeRef.current.list.length ?? 0}
  87. rowRenderer={p => (
  88. <RenderRow
  89. trace_id={props.trace_id}
  90. index={p.index}
  91. projects={projectLookup}
  92. node={treeRef.current.list?.[p.index]}
  93. style={p.style}
  94. onFetchChildren={handleFetchChildren}
  95. onExpandNode={handleExpandNode}
  96. />
  97. )}
  98. />
  99. )}
  100. </AutoSizer>
  101. </TraceStylingWrapper>
  102. );
  103. }
  104. function RenderRow(props: {
  105. index: number;
  106. node: TraceTreeNode<TraceTree.NodeValue>;
  107. onExpandNode: (node: TraceTreeNode<TraceTree.NodeValue>, value: boolean) => void;
  108. onFetchChildren: (node: TraceTreeNode<TraceTree.NodeValue>, value: boolean) => void;
  109. projects: Record<Project['slug'], Project>;
  110. style: React.CSSProperties;
  111. trace_id: string;
  112. }) {
  113. if (!props.node.value) {
  114. return null;
  115. }
  116. if (isAutogroupedNode(props.node)) {
  117. return (
  118. <div
  119. className="TraceRow Autogrouped"
  120. style={{
  121. top: props.style.top,
  122. height: props.style.height,
  123. paddingLeft: props.node.depth * 23,
  124. }}
  125. >
  126. <div className="TraceChildrenCountWrapper">
  127. <Connectors node={props.node} />
  128. <ChildrenCountButton
  129. expanded={!props.node.expanded}
  130. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  131. >
  132. {props.node.groupCount}{' '}
  133. </ChildrenCountButton>
  134. </div>
  135. <span className="TraceOperation">{t('Autogrouped')}</span>
  136. <strong className="TraceEmDash"> — </strong>
  137. <span className="TraceDescription">{props.node.value.autogrouped_by.op}</span>
  138. </div>
  139. );
  140. }
  141. if (isTransactionNode(props.node)) {
  142. return (
  143. <div
  144. className="TraceRow"
  145. style={{
  146. top: props.style.top,
  147. height: props.style.height,
  148. paddingLeft: props.node.depth * 23,
  149. }}
  150. >
  151. <div
  152. className={`TraceChildrenCountWrapper ${
  153. props.node.isOrphaned ? 'Orphaned' : ''
  154. }`}
  155. >
  156. <Connectors node={props.node} />
  157. {props.node.children.length > 0 ? (
  158. <ChildrenCountButton
  159. expanded={props.node.expanded || props.node.zoomedIn}
  160. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  161. >
  162. {props.node.children.length}{' '}
  163. </ChildrenCountButton>
  164. ) : null}
  165. </div>
  166. <ProjectBadge project={props.projects[props.node.value.project_slug]} />
  167. <span className="TraceOperation">{props.node.value['transaction.op']}</span>
  168. <strong className="TraceEmDash"> — </strong>
  169. <span>{props.node.value.transaction}</span>
  170. {props.node.canFetchData ? (
  171. <button onClick={() => props.onFetchChildren(props.node, !props.node.zoomedIn)}>
  172. {props.node.zoomedIn ? 'Zoom Out' : 'Zoom In'}
  173. </button>
  174. ) : null}
  175. </div>
  176. );
  177. }
  178. if (isSpanNode(props.node)) {
  179. return (
  180. <div
  181. className="TraceRow"
  182. style={{
  183. top: props.style.top,
  184. height: props.style.height,
  185. paddingLeft: props.node.depth * 23,
  186. }}
  187. >
  188. <div
  189. className={`TraceChildrenCountWrapper ${
  190. props.node.isOrphaned ? 'Orphaned' : ''
  191. }`}
  192. >
  193. <Connectors node={props.node} />
  194. {props.node.children.length > 0 ? (
  195. <ChildrenCountButton
  196. expanded={props.node.expanded || props.node.zoomedIn}
  197. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  198. >
  199. {props.node.children.length}{' '}
  200. </ChildrenCountButton>
  201. ) : null}
  202. </div>
  203. <span className="TraceOperation">{props.node.value.op ?? '<unknown>'}</span>
  204. <strong className="TraceEmDash"> — </strong>
  205. <span className="TraceDescription">
  206. {props.node.value.description ?? '<unknown>'}
  207. </span>
  208. {props.node.canFetchData ? (
  209. <button onClick={() => props.onFetchChildren(props.node, !props.node.zoomedIn)}>
  210. {props.node.zoomedIn ? 'Zoom Out' : 'Zoom In'}
  211. </button>
  212. ) : null}
  213. </div>
  214. );
  215. }
  216. if (isMissingInstrumentationNode(props.node)) {
  217. return (
  218. <div
  219. className="TraceRow"
  220. style={{
  221. top: props.style.top,
  222. height: props.style.height,
  223. paddingLeft: props.node.depth * 23,
  224. }}
  225. >
  226. <div className="TraceChildrenCountWrapper">
  227. <Connectors node={props.node} />
  228. </div>
  229. <span className="TraceOperation">{t('Missing instrumentation')}</span>
  230. </div>
  231. );
  232. }
  233. if ('orphan_errors' in props.node.value) {
  234. return (
  235. <div
  236. className="TraceRow"
  237. style={{
  238. top: props.style.top,
  239. height: props.style.height,
  240. paddingLeft: props.node.depth * 23,
  241. }}
  242. >
  243. <div className="TraceChildrenCountWrapper Root">
  244. <Connectors node={props.node} />
  245. {props.node.children.length > 0 ? (
  246. <ChildrenCountButton
  247. expanded={props.node.expanded || props.node.zoomedIn}
  248. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  249. >
  250. {props.node.children.length}{' '}
  251. </ChildrenCountButton>
  252. ) : null}
  253. </div>
  254. <span className="TraceOperation">{t('Trace')}</span>
  255. <strong className="TraceEmDash"> — </strong>
  256. <span className="TraceDescription">{props.trace_id}</span>
  257. </div>
  258. );
  259. }
  260. if (isTraceErrorNode(props.node)) {
  261. <div
  262. className="TraceRow"
  263. style={{
  264. top: props.style.top,
  265. height: props.style.height,
  266. paddingLeft: props.node.depth * 23,
  267. }}
  268. >
  269. <div className="TraceChildrenCountWrapper">
  270. <Connectors node={props.node} />
  271. {props.node.children.length > 0 ? (
  272. <ChildrenCountButton
  273. expanded={props.node.expanded || props.node.zoomedIn}
  274. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  275. >
  276. {props.node.children.length}{' '}
  277. </ChildrenCountButton>
  278. ) : null}
  279. </div>
  280. <span className="TraceOperation">{t('Error')}</span>
  281. <strong className="TraceEmDash"> — </strong>
  282. <span className="TraceDescription">{props.node.value.title}</span>
  283. </div>;
  284. }
  285. return null;
  286. }
  287. function Connectors(props: {node: TraceTreeNode<TraceTree.NodeValue>}) {
  288. const showVerticalConnector =
  289. ((props.node.expanded || props.node.zoomedIn) && props.node.children.length > 0) ||
  290. (props.node.value && 'autogrouped_by' in props.node.value);
  291. // If the tail node of the collapsed node has no children,
  292. // we don't want to render the vertical connector as no children
  293. // are being rendered as the chain is entirely collapsed
  294. const hideVerticalConnector =
  295. showVerticalConnector &&
  296. props.node.value &&
  297. props.node instanceof ParentAutogroupNode &&
  298. !props.node.tail.children.length;
  299. return (
  300. <Fragment>
  301. {/*
  302. @TODO count of rendered connectors could be % 3 as we can
  303. have up to 3 connectors per node, 1 div, 1 before and 1 after
  304. */}
  305. {props.node.connectors.map((c, i) => {
  306. return (
  307. <div
  308. key={i}
  309. style={{left: -(Math.abs(Math.abs(c) - props.node.depth) * 23)}}
  310. className={`TraceVerticalConnector ${c < 0 ? 'Orphaned' : ''}`}
  311. />
  312. );
  313. })}
  314. {showVerticalConnector && !hideVerticalConnector ? (
  315. <div className="TraceExpandedVerticalConnector" />
  316. ) : null}
  317. {props.node.isLastChild ? (
  318. <div className="TraceVerticalLastChildConnector" />
  319. ) : (
  320. <div className="TraceVerticalConnector" />
  321. )}
  322. </Fragment>
  323. );
  324. }
  325. function ProjectBadge(props: {project: Project}) {
  326. return <ProjectAvatar project={props.project} />;
  327. }
  328. function ChildrenCountButton(props: {
  329. children: React.ReactNode;
  330. expanded: boolean;
  331. onClick: () => void;
  332. }) {
  333. return (
  334. <button className="TraceChildrenCount" onClick={props.onClick}>
  335. {props.children}
  336. <IconChevron
  337. size="xs"
  338. direction={props.expanded ? 'up' : 'down'}
  339. style={{marginLeft: 2}}
  340. />
  341. </button>
  342. );
  343. }
  344. /**
  345. * This is a wrapper around the Trace component to apply styles
  346. * to the trace tree. It exists because we _do not_ want to trigger
  347. * emotion's css parsing logic as it is very slow and will cause
  348. * the scrolling to flicker.
  349. */
  350. const TraceStylingWrapper = styled('div')`
  351. .TraceRow {
  352. display: flex;
  353. align-items: center;
  354. position: absolute;
  355. width: 100%;
  356. font-size: ${p => p.theme.fontSizeSmall};
  357. &:hover {
  358. background-color: ${p => p.theme.backgroundSecondary};
  359. }
  360. &.Autogrouped {
  361. color: ${p => p.theme.blue300};
  362. .TraceDescription {
  363. font-weight: bold;
  364. }
  365. .TraceChildrenCountWrapper {
  366. button {
  367. color: ${p => p.theme.white};
  368. background-color: ${p => p.theme.blue300};
  369. }
  370. }
  371. }
  372. }
  373. .TraceChildrenCount {
  374. height: 16px;
  375. white-space: nowrap;
  376. min-width: 30px;
  377. display: flex;
  378. align-items: center;
  379. justify-content: center;
  380. border-radius: 99px;
  381. padding: 0px ${space(0.5)};
  382. transition: all 0.15s ease-in-out;
  383. background: ${p => p.theme.background};
  384. border: 2px solid ${p => p.theme.border};
  385. line-height: 0;
  386. z-index: 1;
  387. font-size: 10px;
  388. box-shadow: ${p => p.theme.dropShadowLight};
  389. margin-right: ${space(1)};
  390. svg {
  391. width: 7px;
  392. transition: none;
  393. }
  394. }
  395. .TraceChildrenCountWrapper {
  396. display: flex;
  397. justify-content: flex-end;
  398. align-items: center;
  399. min-width: 46px;
  400. height: 100%;
  401. position: relative;
  402. button {
  403. transition: none;
  404. }
  405. &.Orphaned {
  406. .TraceVerticalConnector,
  407. .TraceVerticalLastChildConnector,
  408. .TraceExpandedVerticalConnector {
  409. border-left: 2px dashed ${p => p.theme.border};
  410. }
  411. &::before {
  412. border-bottom: 2px dashed ${p => p.theme.border};
  413. }
  414. }
  415. &.Root {
  416. &:before,
  417. .TraceVerticalLastChildConnector {
  418. visibility: hidden;
  419. }
  420. }
  421. &::before {
  422. content: '';
  423. display: block;
  424. width: 60%;
  425. height: 2px;
  426. border-bottom: 2px solid ${p => p.theme.border};
  427. position: absolute;
  428. left: 0;
  429. top: 50%;
  430. transform: translateY(-50%);
  431. }
  432. &::after {
  433. content: "";
  434. background-color: rgb(224, 220, 229);
  435. border-radius: 50%;
  436. height: 6px;
  437. width: 6px;
  438. position: absolute;
  439. left: 60%;
  440. top: 50%;
  441. transform: translateY(-50%);
  442. }
  443. }
  444. .TraceVerticalConnector {
  445. position: absolute;
  446. left: 0;
  447. top: 0;
  448. bottom: 0;
  449. height: 100%;
  450. width: 2px;
  451. border-left: 2px solid ${p => p.theme.border};
  452. &.Orphaned {
  453. border-left: 2px dashed ${p => p.theme.border};
  454. }
  455. }
  456. .TraceVerticalLastChildConnector {
  457. position: absolute;
  458. left: 0;
  459. top: 0;
  460. bottom: 0;
  461. height: 50%;
  462. width: 2px;
  463. border-left: 2px solid ${p => p.theme.border};
  464. border-bottom-left-radius: 4px;
  465. }
  466. .TraceExpandedVerticalConnector {
  467. position: absolute;
  468. bottom: 0;
  469. height: 50%;
  470. left: 50%;
  471. width: 2px;
  472. border-left: 2px solid ${p => p.theme.border};
  473. }
  474. .TraceOperation {
  475. margin-left: ${space(0.5)};
  476. text-overflow: ellipsis;
  477. white-space: nowrap;
  478. font-weight: bold;
  479. }
  480. .TraceEmDash {
  481. margin-left: ${space(0.5)};
  482. margin-right: ${space(0.5)};
  483. }
  484. .TraceDescription {
  485. white-space: nowrap;
  486. }
  487. `;