trace.tsx 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273
  1. import type React from 'react';
  2. import {Fragment, useCallback, useEffect, useMemo, useRef, useState} from 'react';
  3. import {browserHistory} from 'react-router';
  4. import {AutoSizer, List} from 'react-virtualized';
  5. import {type Theme, useTheme} from '@emotion/react';
  6. import styled from '@emotion/styled';
  7. import ProjectAvatar from 'sentry/components/avatar/projectAvatar';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
  10. import Placeholder from 'sentry/components/placeholder';
  11. import {IconChevron} from 'sentry/icons';
  12. import {t} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import type {Project} from 'sentry/types';
  15. import {getDuration} from 'sentry/utils/formatters';
  16. import useApi from 'sentry/utils/useApi';
  17. import {useLocation} from 'sentry/utils/useLocation';
  18. import useOrganization from 'sentry/utils/useOrganization';
  19. import useProjects from 'sentry/utils/useProjects';
  20. import {
  21. isAutogroupedNode,
  22. isMissingInstrumentationNode,
  23. isParentAutogroupedNode,
  24. isSpanNode,
  25. isTraceErrorNode,
  26. isTraceNode,
  27. isTransactionNode,
  28. } from './guards';
  29. import {ParentAutogroupNode, type TraceTree, type TraceTreeNode} from './traceTree';
  30. import {VirtualizedViewManager} from './virtualizedViewManager';
  31. function decodeScrollQueue(maybePath: unknown): TraceTree.NodePath[] | null {
  32. if (Array.isArray(maybePath)) {
  33. return maybePath;
  34. }
  35. if (typeof maybePath === 'string') {
  36. return [maybePath as TraceTree.NodePath];
  37. }
  38. return null;
  39. }
  40. interface TraceProps {
  41. trace: TraceTree;
  42. trace_id: string;
  43. }
  44. function Trace({trace, trace_id}: TraceProps) {
  45. const theme = useTheme();
  46. const api = useApi();
  47. const {projects} = useProjects();
  48. const organization = useOrganization();
  49. const location = useLocation();
  50. const viewManager = useRef<VirtualizedViewManager | null>(null);
  51. const [clickedNode, setClickedNode] =
  52. useState<TraceTreeNode<TraceTree.NodeValue> | null>(null);
  53. const [_rerender, setRender] = useState(0);
  54. const scrollQueue = useRef<TraceTree.NodePath[] | null>(null);
  55. const treeRef = useRef<TraceTree>(trace);
  56. treeRef.current = trace;
  57. if (!viewManager.current) {
  58. viewManager.current = new VirtualizedViewManager({
  59. list: {width: 0.5},
  60. span_list: {width: 0.5},
  61. });
  62. }
  63. if (
  64. trace.root.space &&
  65. (trace.root.space[0] !== viewManager.current.to_origin ||
  66. trace.root.space[1] !== viewManager.current.trace_space[1])
  67. ) {
  68. viewManager.current.initializeTraceSpace([
  69. trace.root.space[0],
  70. 0,
  71. trace.root.space[1],
  72. 1,
  73. ]);
  74. scrollQueue.current = decodeScrollQueue(location.query.node);
  75. }
  76. useEffect(() => {
  77. if (
  78. trace.type === 'loading' ||
  79. scrollQueue.current === null ||
  80. !viewManager.current
  81. ) {
  82. return;
  83. }
  84. viewManager.current
  85. .scrollToPath(trace, scrollQueue.current, () => setRender(a => (a + 1) % 2), {
  86. api,
  87. organization,
  88. })
  89. .then(_maybeNode => {
  90. setClickedNode(_maybeNode);
  91. viewManager.current?.onScrollEndOutOfBoundsCheck();
  92. scrollQueue.current = null;
  93. });
  94. }, [api, organization, trace, trace_id]);
  95. const handleFetchChildren = useCallback(
  96. (node: TraceTreeNode<TraceTree.NodeValue>, value: boolean) => {
  97. if (!isTransactionNode(node) && !isSpanNode(node)) {
  98. throw new TypeError('Node must be a transaction or span');
  99. }
  100. treeRef.current
  101. .zoomIn(node, value, {
  102. api,
  103. organization,
  104. })
  105. .then(() => {
  106. setRender(a => (a + 1) % 2);
  107. });
  108. },
  109. [api, organization]
  110. );
  111. const handleExpandNode = useCallback(
  112. (node: TraceTreeNode<TraceTree.NodeValue>, value: boolean) => {
  113. treeRef.current.expand(node, value);
  114. setRender(a => (a + 1) % 2);
  115. },
  116. []
  117. );
  118. const onRowClick = useCallback(
  119. (node: TraceTreeNode<TraceTree.NodeValue>) => {
  120. browserHistory.push({
  121. pathname: location.pathname,
  122. query: {
  123. ...location.query,
  124. node: node.path,
  125. },
  126. });
  127. },
  128. [location.query, location.pathname]
  129. );
  130. const projectLookup = useMemo(() => {
  131. return projects.reduce<Record<Project['slug'], Project>>((acc, project) => {
  132. acc[project.slug] = project;
  133. return acc;
  134. }, {});
  135. }, [projects]);
  136. return (
  137. <Fragment>
  138. <TraceStylingWrapper
  139. ref={r => viewManager.current?.onContainerRef(r)}
  140. className={trace.type === 'loading' ? 'Loading' : ''}
  141. style={{
  142. backgroundColor: '#FFF',
  143. height: '70vh',
  144. width: '100%',
  145. margin: 'auto',
  146. }}
  147. >
  148. <TraceDivider ref={r => viewManager.current?.registerDividerRef(r)} />
  149. <AutoSizer>
  150. {({width, height}) => (
  151. <Fragment>
  152. {trace.indicators.length > 0
  153. ? trace.indicators.map((indicator, i) => {
  154. return (
  155. <div
  156. key={i}
  157. ref={r =>
  158. viewManager.current?.registerIndicatorRef(r, i, indicator)
  159. }
  160. className="TraceIndicator"
  161. >
  162. <div className="TraceIndicatorLine" />
  163. </div>
  164. );
  165. })
  166. : null}
  167. <List
  168. ref={r => viewManager.current?.registerList(r)}
  169. rowHeight={24}
  170. height={height}
  171. width={width}
  172. overscanRowCount={5}
  173. rowCount={treeRef.current.list.length ?? 0}
  174. rowRenderer={p => {
  175. return trace.type === 'loading' ? (
  176. <RenderPlaceholderRow
  177. style={p.style}
  178. node={treeRef.current.list[p.index]}
  179. index={p.index}
  180. theme={theme}
  181. projects={projectLookup}
  182. viewManager={viewManager.current!}
  183. startIndex={
  184. (p.parent as unknown as {_rowStartIndex: number})
  185. ._rowStartIndex ?? 0
  186. }
  187. />
  188. ) : (
  189. <RenderRow
  190. key={p.key}
  191. theme={theme}
  192. startIndex={
  193. (p.parent as unknown as {_rowStartIndex: number})
  194. ._rowStartIndex ?? 0
  195. }
  196. index={p.index}
  197. style={p.style}
  198. trace_id={trace_id}
  199. projects={projectLookup}
  200. node={treeRef.current.list[p.index]}
  201. viewManager={viewManager.current!}
  202. clickedNode={clickedNode}
  203. onFetchChildren={handleFetchChildren}
  204. onExpandNode={handleExpandNode}
  205. onRowClick={onRowClick}
  206. />
  207. );
  208. }}
  209. />
  210. </Fragment>
  211. )}
  212. </AutoSizer>
  213. </TraceStylingWrapper>
  214. </Fragment>
  215. );
  216. }
  217. export default Trace;
  218. const TraceDivider = styled('div')`
  219. position: absolute;
  220. height: 100%;
  221. background-color: transparent;
  222. top: 0;
  223. z-index: 10;
  224. cursor: col-resize;
  225. &:before {
  226. content: '';
  227. position: absolute;
  228. width: 1px;
  229. height: 100%;
  230. background-color: ${p => p.theme.border};
  231. left: 50%;
  232. }
  233. &:hover&:before {
  234. background-color: ${p => p.theme.purple300};
  235. }
  236. `;
  237. function RenderRow(props: {
  238. clickedNode: TraceTreeNode<TraceTree.NodeValue> | null;
  239. index: number;
  240. node: TraceTreeNode<TraceTree.NodeValue>;
  241. onExpandNode: (node: TraceTreeNode<TraceTree.NodeValue>, value: boolean) => void;
  242. onFetchChildren: (node: TraceTreeNode<TraceTree.NodeValue>, value: boolean) => void;
  243. onRowClick: (node: TraceTreeNode<TraceTree.NodeValue>) => void;
  244. projects: Record<Project['slug'], Project>;
  245. startIndex: number;
  246. style: React.CSSProperties;
  247. theme: Theme;
  248. trace_id: string;
  249. viewManager: VirtualizedViewManager;
  250. }) {
  251. const virtualizedIndex = props.index - props.startIndex;
  252. if (!props.node.value) {
  253. return null;
  254. }
  255. if (isAutogroupedNode(props.node)) {
  256. return (
  257. <div
  258. key={props.index}
  259. className="TraceRow Autogrouped"
  260. onClick={() => props.onRowClick(props.node)}
  261. style={{
  262. top: props.style.top,
  263. height: props.style.height,
  264. }}
  265. >
  266. <div
  267. className="TraceLeftColumn"
  268. ref={r =>
  269. props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
  270. }
  271. style={{
  272. width: props.viewManager.columns.list.width * 100 + '%',
  273. }}
  274. >
  275. <div
  276. className="TraceLeftColumnInner"
  277. style={{
  278. paddingLeft: props.node.depth * 24,
  279. }}
  280. >
  281. <div className="TraceChildrenCountWrapper">
  282. <Connectors node={props.node} />
  283. <ChildrenCountButton
  284. expanded={!props.node.expanded}
  285. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  286. >
  287. {props.node.groupCount}{' '}
  288. </ChildrenCountButton>
  289. </div>
  290. <span className="TraceOperation">{t('Autogrouped')}</span>
  291. <strong className="TraceEmDash"> — </strong>
  292. <span className="TraceDescription">{props.node.value.autogrouped_by.op}</span>
  293. </div>
  294. </div>
  295. <div
  296. className="TraceRightColumn"
  297. ref={r =>
  298. props.viewManager.registerColumnRef(
  299. 'span_list',
  300. r,
  301. virtualizedIndex,
  302. props.node
  303. )
  304. }
  305. style={{
  306. width: props.viewManager.columns.span_list.width * 100 + '%',
  307. }}
  308. >
  309. {isParentAutogroupedNode(props.node) ? (
  310. <TraceBar
  311. virtualizedIndex={virtualizedIndex}
  312. viewManager={props.viewManager}
  313. color={props.theme.blue300}
  314. node_space={props.node.space}
  315. />
  316. ) : (
  317. <SiblingAutogroupedBar
  318. virtualizedIndex={virtualizedIndex}
  319. viewManager={props.viewManager}
  320. color={props.theme.blue300}
  321. node={props.node}
  322. />
  323. )}
  324. </div>
  325. </div>
  326. );
  327. }
  328. if (isTransactionNode(props.node)) {
  329. return (
  330. <div
  331. key={props.index}
  332. className="TraceRow"
  333. onClick={() => props.onRowClick(props.node)}
  334. style={{
  335. top: props.style.top,
  336. height: props.style.height,
  337. }}
  338. >
  339. <div
  340. className="TraceLeftColumn"
  341. ref={r =>
  342. props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
  343. }
  344. style={{
  345. width: props.viewManager.columns.list.width * 100 + '%',
  346. }}
  347. >
  348. <div
  349. className="TraceLeftColumnInner"
  350. style={{
  351. paddingLeft: props.node.depth * 24,
  352. }}
  353. >
  354. <div
  355. className={`TraceChildrenCountWrapper ${
  356. props.node.isOrphaned ? 'Orphaned' : ''
  357. }`}
  358. >
  359. <Connectors node={props.node} />
  360. {props.node.children.length > 0 ? (
  361. <ChildrenCountButton
  362. expanded={props.node.expanded || props.node.zoomedIn}
  363. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  364. >
  365. {props.node.children.length}{' '}
  366. </ChildrenCountButton>
  367. ) : null}
  368. </div>
  369. <ProjectBadge project={props.projects[props.node.value.project_slug]} />
  370. <span className="TraceOperation">{props.node.value['transaction.op']}</span>
  371. <strong className="TraceEmDash"> — </strong>
  372. <span>{props.node.value.transaction}</span>
  373. {props.node.canFetchData ? (
  374. <button
  375. onClick={() => props.onFetchChildren(props.node, !props.node.zoomedIn)}
  376. >
  377. {props.node.zoomedIn ? 'Zoom Out' : 'Zoom In'}
  378. </button>
  379. ) : null}
  380. </div>
  381. </div>
  382. <div
  383. ref={r =>
  384. props.viewManager.registerColumnRef(
  385. 'span_list',
  386. r,
  387. virtualizedIndex,
  388. props.node
  389. )
  390. }
  391. className="TraceRightColumn"
  392. style={{
  393. width: props.viewManager.columns.span_list.width * 100 + '%',
  394. }}
  395. >
  396. <TraceBar
  397. virtualizedIndex={virtualizedIndex}
  398. viewManager={props.viewManager}
  399. color={pickBarColor(props.node.value['transaction.op'])}
  400. node_space={props.node.space}
  401. />
  402. </div>
  403. </div>
  404. );
  405. }
  406. if (isSpanNode(props.node)) {
  407. return (
  408. <div
  409. key={props.index}
  410. className="TraceRow"
  411. onClick={() => props.onRowClick(props.node)}
  412. style={{
  413. top: props.style.top,
  414. height: props.style.height,
  415. }}
  416. >
  417. <div
  418. className="TraceLeftColumn"
  419. ref={r =>
  420. props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
  421. }
  422. style={{
  423. width: props.viewManager.columns.list.width * 100 + '%',
  424. }}
  425. >
  426. <div
  427. className="TraceLeftColumnInner"
  428. style={{
  429. paddingLeft: props.node.depth * 24,
  430. }}
  431. >
  432. <div
  433. className={`TraceChildrenCountWrapper ${
  434. props.node.isOrphaned ? 'Orphaned' : ''
  435. }`}
  436. >
  437. <Connectors node={props.node} />
  438. {props.node.children.length > 0 ? (
  439. <ChildrenCountButton
  440. expanded={props.node.expanded || props.node.zoomedIn}
  441. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  442. >
  443. {props.node.children.length}{' '}
  444. </ChildrenCountButton>
  445. ) : null}
  446. </div>
  447. <span className="TraceOperation">{props.node.value.op ?? '<unknown>'}</span>
  448. <strong className="TraceEmDash"> — </strong>
  449. <span className="TraceDescription" title={props.node.value.description}>
  450. {!props.node.value.description
  451. ? 'unknown'
  452. : props.node.value.description.length > 100
  453. ? props.node.value.description.slice(0, 100).trim() + '\u2026'
  454. : props.node.value.description}
  455. </span>
  456. {props.node.canFetchData ? (
  457. <button
  458. onClick={() => props.onFetchChildren(props.node, !props.node.zoomedIn)}
  459. >
  460. {props.node.zoomedIn ? 'Zoom Out' : 'Zoom In'}
  461. </button>
  462. ) : null}
  463. </div>
  464. </div>
  465. <div
  466. ref={r =>
  467. props.viewManager.registerColumnRef(
  468. 'span_list',
  469. r,
  470. virtualizedIndex,
  471. props.node
  472. )
  473. }
  474. className="TraceRightColumn"
  475. style={{
  476. width: props.viewManager.columns.span_list.width * 100 + '%',
  477. }}
  478. >
  479. <TraceBar
  480. virtualizedIndex={virtualizedIndex}
  481. viewManager={props.viewManager}
  482. color={pickBarColor(props.node.value.op)}
  483. node_space={props.node.space}
  484. />
  485. </div>
  486. </div>
  487. );
  488. }
  489. if (isMissingInstrumentationNode(props.node)) {
  490. return (
  491. <div
  492. key={props.index}
  493. className="TraceRow"
  494. onClick={() => props.onRowClick(props.node)}
  495. style={{
  496. top: props.style.top,
  497. height: props.style.height,
  498. }}
  499. >
  500. <div
  501. className="TraceLeftColumn"
  502. ref={r =>
  503. props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
  504. }
  505. style={{
  506. width: props.viewManager.columns.list.width * 100 + '%',
  507. }}
  508. >
  509. <div
  510. className="TraceLeftColumnInner"
  511. style={{
  512. paddingLeft: props.node.depth * 24,
  513. }}
  514. >
  515. <div className="TraceChildrenCountWrapper">
  516. <Connectors node={props.node} />
  517. </div>
  518. <span className="TraceOperation">{t('Missing instrumentation')}</span>
  519. </div>
  520. </div>
  521. <div
  522. ref={r =>
  523. props.viewManager.registerColumnRef(
  524. 'span_list',
  525. r,
  526. virtualizedIndex,
  527. props.node
  528. )
  529. }
  530. className="TraceRightColumn"
  531. style={{
  532. width: props.viewManager.columns.span_list.width * 100 + '%',
  533. }}
  534. >
  535. <TraceBar
  536. virtualizedIndex={virtualizedIndex}
  537. viewManager={props.viewManager}
  538. color={pickBarColor('missing-instrumentation')}
  539. node_space={props.node.space}
  540. />
  541. </div>
  542. </div>
  543. );
  544. }
  545. if (isTraceNode(props.node)) {
  546. return (
  547. <div
  548. key={props.index}
  549. className="TraceRow"
  550. onClick={() => props.onRowClick(props.node)}
  551. style={{
  552. top: props.style.top,
  553. height: props.style.height,
  554. }}
  555. >
  556. <div
  557. className="TraceLeftColumn"
  558. ref={r =>
  559. props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
  560. }
  561. style={{
  562. width: props.viewManager.columns.list.width * 100 + '%',
  563. }}
  564. >
  565. <div
  566. className="TraceLeftColumnInner"
  567. style={{
  568. paddingLeft: props.node.depth * 24,
  569. }}
  570. >
  571. <div className="TraceChildrenCountWrapper Root">
  572. <Connectors node={props.node} />
  573. {props.node.children.length > 0 ? (
  574. <ChildrenCountButton
  575. expanded={props.node.expanded || props.node.zoomedIn}
  576. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  577. >
  578. {props.node.children.length}{' '}
  579. </ChildrenCountButton>
  580. ) : null}
  581. </div>
  582. <span className="TraceOperation">{t('Trace')}</span>
  583. <strong className="TraceEmDash"> — </strong>
  584. <span className="TraceDescription">{props.trace_id}</span>
  585. </div>
  586. </div>
  587. <div
  588. ref={r =>
  589. props.viewManager.registerColumnRef(
  590. 'span_list',
  591. r,
  592. virtualizedIndex,
  593. props.node
  594. )
  595. }
  596. className="TraceRightColumn"
  597. style={{
  598. width: props.viewManager.columns.span_list.width * 100 + '%',
  599. }}
  600. >
  601. {' '}
  602. <TraceBar
  603. virtualizedIndex={virtualizedIndex}
  604. viewManager={props.viewManager}
  605. color={pickBarColor('missing-instrumentation')}
  606. node_space={props.node.space}
  607. />
  608. </div>
  609. </div>
  610. );
  611. }
  612. if (isTraceErrorNode(props.node)) {
  613. return (
  614. <div
  615. key={props.index}
  616. className="TraceRow"
  617. onClick={() => props.onRowClick(props.node)}
  618. style={{
  619. top: props.style.top,
  620. height: props.style.height,
  621. }}
  622. >
  623. <div
  624. className="TraceLeftColumn"
  625. ref={r =>
  626. props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
  627. }
  628. style={{
  629. width: props.viewManager.columns.list.width * 100 + '%',
  630. }}
  631. >
  632. <div
  633. className="TraceLeftColumnInner"
  634. style={{
  635. paddingLeft: props.node.depth * 24,
  636. }}
  637. >
  638. <div className="TraceChildrenCountWrapper">
  639. <Connectors node={props.node} />
  640. {props.node.children.length > 0 ? (
  641. <ChildrenCountButton
  642. expanded={props.node.expanded || props.node.zoomedIn}
  643. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  644. >
  645. {props.node.children.length}{' '}
  646. </ChildrenCountButton>
  647. ) : null}
  648. </div>
  649. <span className="TraceOperation">{t('Error')}</span>
  650. <strong className="TraceEmDash"> — </strong>
  651. <span className="TraceDescription">{props.node.value.title}</span>
  652. </div>
  653. </div>
  654. <div
  655. ref={r =>
  656. props.viewManager.registerColumnRef(
  657. 'span_list',
  658. r,
  659. virtualizedIndex,
  660. props.node
  661. )
  662. }
  663. className="TraceRightColumn"
  664. style={{
  665. width: props.viewManager.columns.span_list.width * 100 + '%',
  666. }}
  667. >
  668. {/* @TODO: figure out what to do with trace errors */}{' '}
  669. {/* <TraceBar
  670. space={props.space}
  671. start_timestamp={props.node.value.start_timestamp}
  672. timestamp={props.node.value.timestamp}
  673. /> */}
  674. </div>
  675. </div>
  676. );
  677. }
  678. return null;
  679. }
  680. function RenderPlaceholderRow(props: {
  681. index: number;
  682. node: TraceTreeNode<TraceTree.NodeValue>;
  683. projects: Record<Project['slug'], Project>;
  684. startIndex: number;
  685. style: React.CSSProperties;
  686. theme: Theme;
  687. viewManager: VirtualizedViewManager;
  688. }) {
  689. const virtualizedIndex = props.index - props.startIndex;
  690. return (
  691. <div
  692. className="TraceRow"
  693. style={{
  694. top: props.style.top,
  695. height: props.style.height,
  696. pointerEvents: 'none',
  697. color: props.theme.subText,
  698. animationDelay: `${virtualizedIndex * 0.05}s`,
  699. paddingLeft: space(1),
  700. }}
  701. >
  702. <div
  703. className="TraceLeftColumn"
  704. style={{width: props.viewManager.columns.list.width * 100 + '%'}}
  705. >
  706. <div
  707. className="TraceLeftColumnInner"
  708. style={{
  709. paddingLeft: props.node.depth * 24,
  710. }}
  711. >
  712. <div className="TraceChildrenCountWrapper">
  713. <Connectors node={props.node} />
  714. {props.node.children.length > 0 ? (
  715. <ChildrenCountButton
  716. expanded={props.node.expanded || props.node.zoomedIn}
  717. onClick={() => void 0}
  718. >
  719. {props.node.children.length}{' '}
  720. </ChildrenCountButton>
  721. ) : null}
  722. </div>
  723. {isTraceNode(props.node) ? <SmallLoadingIndicator /> : null}
  724. {isTraceNode(props.node) ? (
  725. 'Loading trace...'
  726. ) : (
  727. <Placeholder className="Placeholder" height="10px" width="86%" />
  728. )}
  729. </div>
  730. </div>
  731. <div
  732. className="TraceRightColumn"
  733. style={{
  734. width: props.viewManager.columns.span_list.width * 100 + '%',
  735. }}
  736. >
  737. {isTraceNode(props.node) ? null : (
  738. <Placeholder
  739. className="Placeholder"
  740. height="14px"
  741. width="90%"
  742. style={{margin: 'auto'}}
  743. />
  744. )}
  745. </div>
  746. </div>
  747. );
  748. }
  749. function Connectors(props: {node: TraceTreeNode<TraceTree.NodeValue>}) {
  750. const showVerticalConnector =
  751. ((props.node.expanded || props.node.zoomedIn) && props.node.children.length > 0) ||
  752. (props.node.value && isParentAutogroupedNode(props.node));
  753. // If the tail node of the collapsed node has no children,
  754. // we don't want to render the vertical connector as no children
  755. // are being rendered as the chain is entirely collapsed
  756. const hideVerticalConnector =
  757. showVerticalConnector &&
  758. props.node.value &&
  759. props.node instanceof ParentAutogroupNode &&
  760. !props.node.tail.children.length;
  761. return (
  762. <Fragment>
  763. {/*
  764. @TODO count of rendered connectors could be % 3 as we can
  765. have up to 3 connectors per node, 1 div, 1 before and 1 after
  766. */}
  767. {props.node.connectors.map((c, i) => {
  768. return (
  769. <div
  770. key={i}
  771. style={{left: -(Math.abs(Math.abs(c) - props.node.depth) * 24)}}
  772. className={`TraceVerticalConnector ${c < 0 ? 'Orphaned' : ''}`}
  773. />
  774. );
  775. })}
  776. {showVerticalConnector && !hideVerticalConnector ? (
  777. <div className="TraceExpandedVerticalConnector" />
  778. ) : null}
  779. {props.node.isLastChild ? (
  780. <div className="TraceVerticalLastChildConnector" />
  781. ) : (
  782. <div className="TraceVerticalConnector" />
  783. )}
  784. </Fragment>
  785. );
  786. }
  787. function SmallLoadingIndicator() {
  788. return (
  789. <StyledLoadingIndicator
  790. style={{display: 'inline-block', margin: 0}}
  791. size={8}
  792. hideMessage
  793. relative
  794. />
  795. );
  796. }
  797. const StyledLoadingIndicator = styled(LoadingIndicator)`
  798. transform: translate(-5px, 0);
  799. div:first-child {
  800. border-left: 6px solid ${p => p.theme.gray300};
  801. animation: loading 900ms infinite linear;
  802. }
  803. `;
  804. function ProjectBadge(props: {project: Project}) {
  805. return <ProjectAvatar project={props.project} />;
  806. }
  807. function ChildrenCountButton(props: {
  808. children: React.ReactNode;
  809. expanded: boolean;
  810. onClick: () => void;
  811. }) {
  812. return (
  813. <button className="TraceChildrenCount" onClick={props.onClick}>
  814. {props.children}
  815. <IconChevron
  816. size="xs"
  817. direction={props.expanded ? 'up' : 'down'}
  818. style={{marginLeft: 2}}
  819. />
  820. </button>
  821. );
  822. }
  823. interface TraceBarProps {
  824. color: string;
  825. node_space: [number, number] | null;
  826. viewManager: VirtualizedViewManager;
  827. virtualizedIndex: number;
  828. duration?: number;
  829. }
  830. type SiblingAutogroupedBarProps = Omit<TraceBarProps, 'node_space' | 'duration'> & {
  831. node: TraceTreeNode<TraceTree.NodeValue>;
  832. };
  833. // Render collapsed representation of sibling autogrouping, using multiple bars for when
  834. // there are gaps between siblings.
  835. function SiblingAutogroupedBar(props: SiblingAutogroupedBarProps) {
  836. const bars: React.ReactNode[] = [];
  837. // Start and end represents the earliest start_timestamp and the latest
  838. // end_timestamp for a set of overlapping siblings.
  839. let start = isSpanNode(props.node.children[0])
  840. ? props.node.children[0].value.start_timestamp
  841. : Number.POSITIVE_INFINITY;
  842. let end = isSpanNode(props.node.children[0])
  843. ? props.node.children[0].value.timestamp
  844. : Number.NEGATIVE_INFINITY;
  845. let totalDuration = 0;
  846. for (let i = 0; i < props.node.children.length; i++) {
  847. const node = props.node.children[i];
  848. if (!isSpanNode(node)) {
  849. throw new TypeError('Invalid type of autogrouped child');
  850. }
  851. const hasGap = node.value.start_timestamp > end;
  852. if (!(hasGap || node.isLastChild)) {
  853. start = Math.min(start, node.value.start_timestamp);
  854. end = Math.max(end, node.value.timestamp);
  855. continue;
  856. }
  857. // Render a bar for already collapsed set.
  858. totalDuration += end - start;
  859. bars.push(
  860. <TraceBar
  861. virtualizedIndex={props.virtualizedIndex}
  862. viewManager={props.viewManager}
  863. color={props.color}
  864. node_space={[start, end - start]}
  865. duration={!hasGap ? totalDuration : undefined}
  866. />
  867. );
  868. if (hasGap) {
  869. // Start a new set.
  870. start = node.value.start_timestamp;
  871. end = node.value.timestamp;
  872. // Render a bar if the sibling with a gap is the last sibling.
  873. if (node.isLastChild) {
  874. totalDuration += end - start;
  875. bars.push(
  876. <TraceBar
  877. virtualizedIndex={props.virtualizedIndex}
  878. viewManager={props.viewManager}
  879. color={props.color}
  880. duration={totalDuration}
  881. node_space={[start, end - start]}
  882. />
  883. );
  884. }
  885. }
  886. }
  887. return <Fragment>{bars}</Fragment>;
  888. }
  889. function TraceBar(props: TraceBarProps) {
  890. if (!props.node_space) {
  891. return null;
  892. }
  893. const duration = getDuration(props.node_space[1] / 1000, 2, true);
  894. const spanTransform = props.viewManager.computeSpanCSSMatrixTransform(props.node_space);
  895. const [inside, textTransform] = props.viewManager.computeSpanTextPlacement(
  896. props.node_space,
  897. duration
  898. );
  899. return (
  900. <Fragment>
  901. <div
  902. ref={r =>
  903. props.viewManager.registerSpanBarRef(
  904. r,
  905. props.node_space!,
  906. props.virtualizedIndex
  907. )
  908. }
  909. className="TraceBar"
  910. style={{
  911. transform: `matrix(${spanTransform.join(',')})`,
  912. backgroundColor: props.color,
  913. }}
  914. />
  915. <div
  916. ref={r =>
  917. props.viewManager.registerSpanBarTextRef(
  918. r,
  919. duration,
  920. props.node_space!,
  921. props.virtualizedIndex
  922. )
  923. }
  924. className="TraceBarDuration"
  925. style={{
  926. color: inside ? 'white' : '',
  927. transform: `translate(${textTransform ?? 0}px, 0)`,
  928. }}
  929. >
  930. {duration}
  931. </div>
  932. </Fragment>
  933. );
  934. }
  935. /**
  936. * This is a wrapper around the Trace component to apply styles
  937. * to the trace tree. It exists because we _do not_ want to trigger
  938. * emotion's css parsing logic as it is very slow and will cause
  939. * the scrolling to flicker.
  940. */
  941. const TraceStylingWrapper = styled('div')`
  942. overflow: hidden;
  943. position: relative;
  944. box-shadow: 0 0 0 1px ${p => p.theme.border};
  945. border-radius: ${space(0.5)};
  946. @keyframes show {
  947. 0% {
  948. opacity: 0;
  949. transform: translate(0, 2px);
  950. }
  951. 100% {
  952. opacity: 0.7;
  953. transform: translate(0, 0px);
  954. }
  955. }
  956. @keyframes showPlaceholder {
  957. 0% {
  958. opacity: 0;
  959. transform: translate(-8px, 0px);
  960. }
  961. 100% {
  962. opacity: 0.7;
  963. transform: translate(0, 0px);
  964. }
  965. }
  966. .TraceIndicator {
  967. z-index: 1;
  968. width: 3px;
  969. height: 100%;
  970. top: 0;
  971. position: absolute;
  972. .TraceIndicatorLine {
  973. width: 1px;
  974. height: 100%;
  975. position: absolute;
  976. left: 50%;
  977. transform: translateX(-50%);
  978. background: repeating-linear-gradient(
  979. to bottom,
  980. transparent 0 4px,
  981. ${p => p.theme.textColor} 4px 8px
  982. )
  983. 80%/2px 100% no-repeat;
  984. }
  985. }
  986. &.Loading {
  987. .TraceRow {
  988. opacity: 0;
  989. animation: show 0.2s ease-in-out forwards;
  990. }
  991. .Placeholder {
  992. opacity: 0;
  993. transform: translate(-8px, 0px);
  994. animation: showPlaceholder 0.2s ease-in-out forwards;
  995. }
  996. }
  997. .TraceRow {
  998. display: flex;
  999. align-items: center;
  1000. position: absolute;
  1001. width: 100%;
  1002. transition: background-color 0.15s ease-in-out 0s;
  1003. font-size: ${p => p.theme.fontSizeSmall};
  1004. &:nth-of-type(odd) {
  1005. .TraceRightColumn {
  1006. background-color: ${p => p.theme.backgroundSecondary};
  1007. }
  1008. }
  1009. &:hover {
  1010. background-color: ${p => p.theme.backgroundSecondary};
  1011. }
  1012. &.Autogrouped {
  1013. color: ${p => p.theme.blue300};
  1014. .TraceDescription {
  1015. font-weight: bold;
  1016. }
  1017. .TraceChildrenCountWrapper {
  1018. button {
  1019. color: ${p => p.theme.white};
  1020. background-color: ${p => p.theme.blue300};
  1021. }
  1022. }
  1023. }
  1024. }
  1025. .TraceLeftColumn {
  1026. height: 100%;
  1027. white-space: nowrap;
  1028. display: flex;
  1029. align-items: center;
  1030. overflow: hidden;
  1031. will-change: width;
  1032. .TraceLeftColumnInner {
  1033. height: 100%;
  1034. white-space: nowrap;
  1035. display: flex;
  1036. align-items: center;
  1037. will-change: transform;
  1038. transform-origin: left center;
  1039. transform: translateX(var(--column-translate-x));
  1040. }
  1041. }
  1042. .TraceRightColumn {
  1043. height: 100%;
  1044. overflow: hidden;
  1045. position: relative;
  1046. display: flex;
  1047. align-items: center;
  1048. will-change: width;
  1049. z-index: 1;
  1050. }
  1051. .TraceBar {
  1052. position: absolute;
  1053. height: 64%;
  1054. width: 100%;
  1055. background-color: black;
  1056. transform-origin: left center;
  1057. }
  1058. .TraceBarDuration {
  1059. display: inline-block;
  1060. transform-origin: left center;
  1061. font-size: ${p => p.theme.fontSizeExtraSmall};
  1062. color: ${p => p.theme.gray300};
  1063. white-space: nowrap;
  1064. font-variant-numeric: tabular-nums;
  1065. position: absolute;
  1066. transition: color 0.1s ease-in-out;
  1067. }
  1068. .TraceChildrenCount {
  1069. height: 16px;
  1070. white-space: nowrap;
  1071. min-width: 30px;
  1072. display: flex;
  1073. align-items: center;
  1074. justify-content: center;
  1075. border-radius: 99px;
  1076. padding: 0px ${space(0.5)};
  1077. transition: all 0.15s ease-in-out;
  1078. background: ${p => p.theme.background};
  1079. border: 2px solid ${p => p.theme.border};
  1080. line-height: 0;
  1081. z-index: 1;
  1082. font-size: 10px;
  1083. box-shadow: ${p => p.theme.dropShadowLight};
  1084. margin-right: ${space(1)};
  1085. svg {
  1086. width: 7px;
  1087. transition: none;
  1088. }
  1089. }
  1090. .TraceChildrenCountWrapper {
  1091. display: flex;
  1092. justify-content: flex-end;
  1093. align-items: center;
  1094. min-width: 48px;
  1095. height: 100%;
  1096. position: relative;
  1097. button {
  1098. transition: none;
  1099. }
  1100. &.Orphaned {
  1101. .TraceVerticalConnector,
  1102. .TraceVerticalLastChildConnector,
  1103. .TraceExpandedVerticalConnector {
  1104. border-left: 2px dashed ${p => p.theme.border};
  1105. }
  1106. &::before {
  1107. border-bottom: 2px dashed ${p => p.theme.border};
  1108. }
  1109. }
  1110. &.Root {
  1111. &:before,
  1112. .TraceVerticalLastChildConnector {
  1113. visibility: hidden;
  1114. }
  1115. }
  1116. &::before {
  1117. content: '';
  1118. display: block;
  1119. width: 60%;
  1120. height: 2px;
  1121. border-bottom: 2px solid ${p => p.theme.border};
  1122. position: absolute;
  1123. left: 0;
  1124. top: 50%;
  1125. transform: translateY(-50%);
  1126. }
  1127. &::after {
  1128. content: '';
  1129. background-color: rgb(224, 220, 229);
  1130. border-radius: 50%;
  1131. height: 6px;
  1132. width: 6px;
  1133. position: absolute;
  1134. left: 60%;
  1135. top: 50%;
  1136. transform: translateY(-50%);
  1137. }
  1138. }
  1139. .TraceVerticalConnector {
  1140. position: absolute;
  1141. left: 0;
  1142. top: 0;
  1143. bottom: 0;
  1144. height: 100%;
  1145. width: 2px;
  1146. border-left: 2px solid ${p => p.theme.border};
  1147. &.Orphaned {
  1148. border-left: 2px dashed ${p => p.theme.border};
  1149. }
  1150. }
  1151. .TraceVerticalLastChildConnector {
  1152. position: absolute;
  1153. left: 0;
  1154. top: 0;
  1155. bottom: 0;
  1156. height: 50%;
  1157. width: 2px;
  1158. border-left: 2px solid ${p => p.theme.border};
  1159. border-bottom-left-radius: 4px;
  1160. }
  1161. .TraceExpandedVerticalConnector {
  1162. position: absolute;
  1163. bottom: 0;
  1164. height: 50%;
  1165. left: 50%;
  1166. width: 2px;
  1167. border-left: 2px solid ${p => p.theme.border};
  1168. }
  1169. .TraceOperation {
  1170. margin-left: ${space(0.5)};
  1171. text-overflow: ellipsis;
  1172. white-space: nowrap;
  1173. font-weight: bold;
  1174. }
  1175. .TraceEmDash {
  1176. margin-left: ${space(0.5)};
  1177. margin-right: ${space(0.5)};
  1178. }
  1179. .TraceDescription {
  1180. white-space: nowrap;
  1181. }
  1182. `;