trace.tsx 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268
  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 PerformanceDuration from 'sentry/components/performanceDuration';
  11. import Placeholder from 'sentry/components/placeholder';
  12. import {IconChevron} from 'sentry/icons';
  13. import {t} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. import type {Project} from 'sentry/types';
  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.trace_space[0] ||
  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?.registerVirtualizedList(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. backgroundColor:
  308. props.index % 2 ? undefined : props.theme.backgroundSecondary,
  309. }}
  310. >
  311. {isParentAutogroupedNode(props.node) ? (
  312. <TraceBar
  313. virtualizedIndex={virtualizedIndex}
  314. viewManager={props.viewManager}
  315. color={props.theme.blue300}
  316. node_space={props.node.space}
  317. />
  318. ) : (
  319. <SiblingAutogroupedBar
  320. virtualizedIndex={virtualizedIndex}
  321. viewManager={props.viewManager}
  322. color={props.theme.blue300}
  323. node={props.node}
  324. />
  325. )}
  326. </div>
  327. </div>
  328. );
  329. }
  330. if (isTransactionNode(props.node)) {
  331. return (
  332. <div
  333. key={props.index}
  334. className="TraceRow"
  335. onClick={() => props.onRowClick(props.node)}
  336. style={{
  337. top: props.style.top,
  338. height: props.style.height,
  339. }}
  340. >
  341. <div
  342. className="TraceLeftColumn"
  343. ref={r =>
  344. props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
  345. }
  346. style={{
  347. width: props.viewManager.columns.list.width * 100 + '%',
  348. }}
  349. >
  350. <div
  351. className="TraceLeftColumnInner"
  352. style={{
  353. paddingLeft: props.node.depth * 24,
  354. }}
  355. >
  356. <div
  357. className={`TraceChildrenCountWrapper ${
  358. props.node.isOrphaned ? 'Orphaned' : ''
  359. }`}
  360. >
  361. <Connectors node={props.node} />
  362. {props.node.children.length > 0 ? (
  363. <ChildrenCountButton
  364. expanded={props.node.expanded || props.node.zoomedIn}
  365. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  366. >
  367. {props.node.children.length}{' '}
  368. </ChildrenCountButton>
  369. ) : null}
  370. </div>
  371. <ProjectBadge project={props.projects[props.node.value.project_slug]} />
  372. <span className="TraceOperation">{props.node.value['transaction.op']}</span>
  373. <strong className="TraceEmDash"> — </strong>
  374. <span>{props.node.value.transaction}</span>
  375. {props.node.canFetchData ? (
  376. <button
  377. onClick={() => props.onFetchChildren(props.node, !props.node.zoomedIn)}
  378. >
  379. {props.node.zoomedIn ? 'Zoom Out' : 'Zoom In'}
  380. </button>
  381. ) : null}
  382. </div>
  383. </div>
  384. <div
  385. ref={r =>
  386. props.viewManager.registerColumnRef(
  387. 'span_list',
  388. r,
  389. virtualizedIndex,
  390. props.node
  391. )
  392. }
  393. className="TraceRightColumn"
  394. style={{
  395. width: props.viewManager.columns.span_list.width * 100 + '%',
  396. backgroundColor:
  397. props.index % 2 ? undefined : props.theme.backgroundSecondary,
  398. }}
  399. >
  400. <TraceBar
  401. virtualizedIndex={virtualizedIndex}
  402. viewManager={props.viewManager}
  403. color={pickBarColor(props.node.value['transaction.op'])}
  404. node_space={props.node.space}
  405. />
  406. </div>
  407. </div>
  408. );
  409. }
  410. if (isSpanNode(props.node)) {
  411. return (
  412. <div
  413. key={props.index}
  414. className="TraceRow"
  415. onClick={() => props.onRowClick(props.node)}
  416. style={{
  417. top: props.style.top,
  418. height: props.style.height,
  419. }}
  420. >
  421. <div
  422. className="TraceLeftColumn"
  423. ref={r =>
  424. props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
  425. }
  426. style={{
  427. width: props.viewManager.columns.list.width * 100 + '%',
  428. }}
  429. >
  430. <div
  431. className="TraceLeftColumnInner"
  432. style={{
  433. paddingLeft: props.node.depth * 24,
  434. }}
  435. >
  436. <div
  437. className={`TraceChildrenCountWrapper ${
  438. props.node.isOrphaned ? 'Orphaned' : ''
  439. }`}
  440. >
  441. <Connectors node={props.node} />
  442. {props.node.children.length > 0 ? (
  443. <ChildrenCountButton
  444. expanded={props.node.expanded || props.node.zoomedIn}
  445. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  446. >
  447. {props.node.children.length}{' '}
  448. </ChildrenCountButton>
  449. ) : null}
  450. </div>
  451. <span className="TraceOperation">{props.node.value.op ?? '<unknown>'}</span>
  452. <strong className="TraceEmDash"> — </strong>
  453. <span className="TraceDescription" title={props.node.value.description}>
  454. {!props.node.value.description
  455. ? 'unknown'
  456. : props.node.value.description.length > 100
  457. ? props.node.value.description.slice(0, 100).trim() + '\u2026'
  458. : props.node.value.description}
  459. </span>
  460. {props.node.canFetchData ? (
  461. <button
  462. onClick={() => props.onFetchChildren(props.node, !props.node.zoomedIn)}
  463. >
  464. {props.node.zoomedIn ? 'Zoom Out' : 'Zoom In'}
  465. </button>
  466. ) : null}
  467. </div>
  468. </div>
  469. <div
  470. ref={r =>
  471. props.viewManager.registerColumnRef(
  472. 'span_list',
  473. r,
  474. virtualizedIndex,
  475. props.node
  476. )
  477. }
  478. className="TraceRightColumn"
  479. style={{
  480. width: props.viewManager.columns.span_list.width * 100 + '%',
  481. backgroundColor:
  482. props.index % 2 ? undefined : props.theme.backgroundSecondary,
  483. }}
  484. >
  485. <TraceBar
  486. virtualizedIndex={virtualizedIndex}
  487. viewManager={props.viewManager}
  488. color={pickBarColor(props.node.value.op)}
  489. node_space={props.node.space}
  490. />
  491. </div>
  492. </div>
  493. );
  494. }
  495. if (isMissingInstrumentationNode(props.node)) {
  496. return (
  497. <div
  498. key={props.index}
  499. className="TraceRow"
  500. onClick={() => props.onRowClick(props.node)}
  501. style={{
  502. top: props.style.top,
  503. height: props.style.height,
  504. }}
  505. >
  506. <div
  507. className="TraceLeftColumn"
  508. ref={r =>
  509. props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
  510. }
  511. style={{
  512. width: props.viewManager.columns.list.width * 100 + '%',
  513. }}
  514. >
  515. <div
  516. className="TraceLeftColumnInner"
  517. style={{
  518. paddingLeft: props.node.depth * 24,
  519. }}
  520. >
  521. <div className="TraceChildrenCountWrapper">
  522. <Connectors node={props.node} />
  523. </div>
  524. <span className="TraceOperation">{t('Missing instrumentation')}</span>
  525. </div>
  526. </div>
  527. <div
  528. ref={r =>
  529. props.viewManager.registerColumnRef(
  530. 'span_list',
  531. r,
  532. virtualizedIndex,
  533. props.node
  534. )
  535. }
  536. className="TraceRightColumn"
  537. style={{
  538. width: props.viewManager.columns.span_list.width * 100 + '%',
  539. backgroundColor:
  540. props.index % 2 ? undefined : props.theme.backgroundSecondary,
  541. }}
  542. >
  543. <TraceBar
  544. virtualizedIndex={virtualizedIndex}
  545. viewManager={props.viewManager}
  546. color={pickBarColor('missing-instrumentation')}
  547. node_space={props.node.space}
  548. />
  549. </div>
  550. </div>
  551. );
  552. }
  553. if (isTraceNode(props.node)) {
  554. return (
  555. <div
  556. key={props.index}
  557. className="TraceRow"
  558. onClick={() => props.onRowClick(props.node)}
  559. style={{
  560. top: props.style.top,
  561. height: props.style.height,
  562. }}
  563. >
  564. <div
  565. className="TraceLeftColumn"
  566. ref={r =>
  567. props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
  568. }
  569. style={{
  570. width: props.viewManager.columns.list.width * 100 + '%',
  571. }}
  572. >
  573. <div
  574. className="TraceLeftColumnInner"
  575. style={{
  576. paddingLeft: props.node.depth * 24,
  577. }}
  578. >
  579. <div className="TraceChildrenCountWrapper Root">
  580. <Connectors node={props.node} />
  581. {props.node.children.length > 0 ? (
  582. <ChildrenCountButton
  583. expanded={props.node.expanded || props.node.zoomedIn}
  584. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  585. >
  586. {props.node.children.length}{' '}
  587. </ChildrenCountButton>
  588. ) : null}
  589. </div>
  590. <span className="TraceOperation">{t('Trace')}</span>
  591. <strong className="TraceEmDash"> — </strong>
  592. <span className="TraceDescription">{props.trace_id}</span>
  593. </div>
  594. </div>
  595. <div
  596. ref={r =>
  597. props.viewManager.registerColumnRef(
  598. 'span_list',
  599. r,
  600. virtualizedIndex,
  601. props.node
  602. )
  603. }
  604. className="TraceRightColumn"
  605. style={{
  606. width: props.viewManager.columns.span_list.width * 100 + '%',
  607. backgroundColor:
  608. props.index % 2 ? undefined : props.theme.backgroundSecondary,
  609. }}
  610. >
  611. <TraceBar
  612. virtualizedIndex={virtualizedIndex}
  613. viewManager={props.viewManager}
  614. color={pickBarColor('missing-instrumentation')}
  615. node_space={props.node.space}
  616. />
  617. </div>
  618. </div>
  619. );
  620. }
  621. if (isTraceErrorNode(props.node)) {
  622. return (
  623. <div
  624. key={props.index}
  625. className="TraceRow"
  626. onClick={() => props.onRowClick(props.node)}
  627. style={{
  628. top: props.style.top,
  629. height: props.style.height,
  630. }}
  631. >
  632. <div
  633. className="TraceLeftColumn"
  634. ref={r =>
  635. props.viewManager.registerColumnRef('list', r, virtualizedIndex, props.node)
  636. }
  637. style={{
  638. width: props.viewManager.columns.list.width * 100 + '%',
  639. }}
  640. >
  641. <div
  642. className="TraceLeftColumnInner"
  643. style={{
  644. paddingLeft: props.node.depth * 24,
  645. }}
  646. >
  647. <div className="TraceChildrenCountWrapper">
  648. <Connectors node={props.node} />
  649. {props.node.children.length > 0 ? (
  650. <ChildrenCountButton
  651. expanded={props.node.expanded || props.node.zoomedIn}
  652. onClick={() => props.onExpandNode(props.node, !props.node.expanded)}
  653. >
  654. {props.node.children.length}{' '}
  655. </ChildrenCountButton>
  656. ) : null}
  657. </div>
  658. <span className="TraceOperation">{t('Error')}</span>
  659. <strong className="TraceEmDash"> — </strong>
  660. <span className="TraceDescription">{props.node.value.title}</span>
  661. </div>
  662. </div>
  663. <div
  664. ref={r =>
  665. props.viewManager.registerColumnRef(
  666. 'span_list',
  667. r,
  668. virtualizedIndex,
  669. props.node
  670. )
  671. }
  672. className="TraceRightColumn"
  673. style={{
  674. width: props.viewManager.columns.span_list.width * 100 + '%',
  675. backgroundColor:
  676. props.index % 2 ? undefined : props.theme.backgroundSecondary,
  677. }}
  678. >
  679. {/* @TODO: figure out what to do with trace errors */}
  680. {/* <TraceBar
  681. space={props.space}
  682. start_timestamp={props.node.value.start_timestamp}
  683. timestamp={props.node.value.timestamp}
  684. /> */}
  685. </div>
  686. </div>
  687. );
  688. }
  689. return null;
  690. }
  691. function RenderPlaceholderRow(props: {
  692. index: number;
  693. node: TraceTreeNode<TraceTree.NodeValue>;
  694. projects: Record<Project['slug'], Project>;
  695. startIndex: number;
  696. style: React.CSSProperties;
  697. theme: Theme;
  698. viewManager: VirtualizedViewManager;
  699. }) {
  700. const virtualizedIndex = props.index - props.startIndex;
  701. return (
  702. <div
  703. className="TraceRow"
  704. style={{
  705. top: props.style.top,
  706. height: props.style.height,
  707. pointerEvents: 'none',
  708. color: props.theme.subText,
  709. animationDelay: `${virtualizedIndex * 0.05}s`,
  710. paddingLeft: space(1),
  711. }}
  712. >
  713. <div
  714. className="TraceLeftColumn"
  715. style={{width: props.viewManager.columns.list.width * 100 + '%'}}
  716. >
  717. <div
  718. className="TraceLeftColumnInner"
  719. style={{
  720. paddingLeft: props.node.depth * 24,
  721. }}
  722. >
  723. <div className="TraceChildrenCountWrapper">
  724. <Connectors node={props.node} />
  725. {props.node.children.length > 0 ? (
  726. <ChildrenCountButton
  727. expanded={props.node.expanded || props.node.zoomedIn}
  728. onClick={() => void 0}
  729. >
  730. {props.node.children.length}{' '}
  731. </ChildrenCountButton>
  732. ) : null}
  733. </div>
  734. {isTraceNode(props.node) ? <SmallLoadingIndicator /> : null}
  735. {isTraceNode(props.node) ? (
  736. 'Loading trace...'
  737. ) : (
  738. <Placeholder className="Placeholder" height="10px" width="86%" />
  739. )}
  740. </div>
  741. </div>
  742. <div
  743. className="TraceRightColumn"
  744. style={{
  745. width: props.viewManager.columns.span_list.width * 100 + '%',
  746. }}
  747. >
  748. {isTraceNode(props.node) ? null : (
  749. <Placeholder
  750. className="Placeholder"
  751. height="14px"
  752. width="90%"
  753. style={{margin: 'auto'}}
  754. />
  755. )}
  756. </div>
  757. </div>
  758. );
  759. }
  760. function Connectors(props: {node: TraceTreeNode<TraceTree.NodeValue>}) {
  761. const showVerticalConnector =
  762. ((props.node.expanded || props.node.zoomedIn) && props.node.children.length > 0) ||
  763. (props.node.value && isParentAutogroupedNode(props.node));
  764. // If the tail node of the collapsed node has no children,
  765. // we don't want to render the vertical connector as no children
  766. // are being rendered as the chain is entirely collapsed
  767. const hideVerticalConnector =
  768. showVerticalConnector &&
  769. props.node.value &&
  770. props.node instanceof ParentAutogroupNode &&
  771. !props.node.tail.children.length;
  772. return (
  773. <Fragment>
  774. {/*
  775. @TODO count of rendered connectors could be % 3 as we can
  776. have up to 3 connectors per node, 1 div, 1 before and 1 after
  777. */}
  778. {props.node.connectors.map((c, i) => {
  779. return (
  780. <div
  781. key={i}
  782. style={{left: -(Math.abs(Math.abs(c) - props.node.depth) * 24)}}
  783. className={`TraceVerticalConnector ${c < 0 ? 'Orphaned' : ''}`}
  784. />
  785. );
  786. })}
  787. {showVerticalConnector && !hideVerticalConnector ? (
  788. <div className="TraceExpandedVerticalConnector" />
  789. ) : null}
  790. {props.node.isLastChild ? (
  791. <div className="TraceVerticalLastChildConnector" />
  792. ) : (
  793. <div className="TraceVerticalConnector" />
  794. )}
  795. </Fragment>
  796. );
  797. }
  798. function SmallLoadingIndicator() {
  799. return (
  800. <StyledLoadingIndicator
  801. style={{display: 'inline-block', margin: 0}}
  802. size={8}
  803. hideMessage
  804. relative
  805. />
  806. );
  807. }
  808. const StyledLoadingIndicator = styled(LoadingIndicator)`
  809. transform: translate(-5px, 0);
  810. div:first-child {
  811. border-left: 6px solid ${p => p.theme.gray300};
  812. animation: loading 900ms infinite linear;
  813. }
  814. `;
  815. function ProjectBadge(props: {project: Project}) {
  816. return <ProjectAvatar project={props.project} />;
  817. }
  818. function ChildrenCountButton(props: {
  819. children: React.ReactNode;
  820. expanded: boolean;
  821. onClick: () => void;
  822. }) {
  823. return (
  824. <button className="TraceChildrenCount" onClick={props.onClick}>
  825. {props.children}
  826. <IconChevron
  827. size="xs"
  828. direction={props.expanded ? 'up' : 'down'}
  829. style={{marginLeft: 2}}
  830. />
  831. </button>
  832. );
  833. }
  834. interface TraceBarProps {
  835. color: string;
  836. node_space: [number, number] | null;
  837. viewManager: VirtualizedViewManager;
  838. virtualizedIndex: number;
  839. duration?: number;
  840. }
  841. type SiblingAutogroupedBarProps = Omit<TraceBarProps, 'node_space' | 'duration'> & {
  842. node: TraceTreeNode<TraceTree.NodeValue>;
  843. };
  844. // Render collapsed representation of sibling autogrouping, using multiple bars for when
  845. // there are gaps between siblings.
  846. function SiblingAutogroupedBar(props: SiblingAutogroupedBarProps) {
  847. const bars: React.ReactNode[] = [];
  848. // Start and end represents the earliest start_timestamp and the latest
  849. // end_timestamp for a set of overlapping siblings.
  850. let start = isSpanNode(props.node.children[0])
  851. ? props.node.children[0].value.start_timestamp
  852. : Number.POSITIVE_INFINITY;
  853. let end = isSpanNode(props.node.children[0])
  854. ? props.node.children[0].value.timestamp
  855. : Number.NEGATIVE_INFINITY;
  856. let totalDuration = 0;
  857. for (let i = 0; i < props.node.children.length; i++) {
  858. const node = props.node.children[i];
  859. if (!isSpanNode(node)) {
  860. throw new TypeError('Invalid type of autogrouped child');
  861. }
  862. const hasGap = node.value.start_timestamp > end;
  863. if (!(hasGap || node.isLastChild)) {
  864. start = Math.min(start, node.value.start_timestamp);
  865. end = Math.max(end, node.value.timestamp);
  866. continue;
  867. }
  868. // Render a bar for already collapsed set.
  869. totalDuration += end - start;
  870. bars.push(
  871. <TraceBar
  872. virtualizedIndex={props.virtualizedIndex}
  873. viewManager={props.viewManager}
  874. color={props.color}
  875. node_space={[start, end - start]}
  876. duration={!hasGap ? totalDuration : undefined}
  877. />
  878. );
  879. if (hasGap) {
  880. // Start a new set.
  881. start = node.value.start_timestamp;
  882. end = node.value.timestamp;
  883. // Render a bar if the sibling with a gap is the last sibling.
  884. if (node.isLastChild) {
  885. totalDuration += end - start;
  886. bars.push(
  887. <TraceBar
  888. virtualizedIndex={props.virtualizedIndex}
  889. viewManager={props.viewManager}
  890. color={props.color}
  891. duration={totalDuration}
  892. node_space={[start, end - start]}
  893. />
  894. );
  895. }
  896. }
  897. }
  898. return <Fragment>{bars}</Fragment>;
  899. }
  900. function TraceBar(props: TraceBarProps) {
  901. if (!props.node_space) {
  902. return null;
  903. }
  904. const spanTransform = props.viewManager.computeSpanCSSMatrixTransform(props.node_space);
  905. const inverseTransform = 1 / spanTransform[0];
  906. const textPosition = props.viewManager.computeSpanTextPlacement(
  907. spanTransform[4],
  908. props.node_space
  909. );
  910. return (
  911. <div
  912. ref={r =>
  913. props.viewManager.registerSpanBarRef(r, props.node_space!, props.virtualizedIndex)
  914. }
  915. className="TraceBar"
  916. style={{
  917. transform: `matrix(${spanTransform.join(',')})`,
  918. backgroundColor: props.color,
  919. }}
  920. >
  921. <div
  922. className={`TraceBarDuration ${textPosition === 'inside left' ? 'Inside' : ''}`}
  923. style={{
  924. left: textPosition === 'left' || textPosition === 'inside left' ? '0' : '100%',
  925. transform: `scaleX(${inverseTransform}) translate(${
  926. textPosition === 'left' ? 'calc(-100% - 4px)' : '4px'
  927. }, 0)`,
  928. }}
  929. >
  930. <PerformanceDuration milliseconds={props.node_space[1]} abbreviation />
  931. </div>
  932. </div>
  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. &:hover {
  1005. background-color: ${p => p.theme.backgroundSecondary};
  1006. }
  1007. &.Autogrouped {
  1008. color: ${p => p.theme.blue300};
  1009. .TraceDescription {
  1010. font-weight: bold;
  1011. }
  1012. .TraceChildrenCountWrapper {
  1013. button {
  1014. color: ${p => p.theme.white};
  1015. background-color: ${p => p.theme.blue300};
  1016. }
  1017. }
  1018. }
  1019. }
  1020. .TraceLeftColumn {
  1021. height: 100%;
  1022. white-space: nowrap;
  1023. display: flex;
  1024. align-items: center;
  1025. overflow: hidden;
  1026. will-change: width;
  1027. .TraceLeftColumnInner {
  1028. height: 100%;
  1029. white-space: nowrap;
  1030. display: flex;
  1031. align-items: center;
  1032. will-change: transform;
  1033. transform-origin: left center;
  1034. transform: translateX(var(--column-translate-x));
  1035. }
  1036. }
  1037. .TraceRightColumn {
  1038. height: 100%;
  1039. overflow: hidden;
  1040. position: relative;
  1041. display: flex;
  1042. align-items: center;
  1043. will-change: width;
  1044. z-index: 1;
  1045. }
  1046. .TraceBar {
  1047. position: absolute;
  1048. height: 64%;
  1049. width: 100%;
  1050. background-color: black;
  1051. transform-origin: left center;
  1052. }
  1053. .TraceBarDuration {
  1054. display: inline-block;
  1055. transform-origin: left center;
  1056. font-size: ${p => p.theme.fontSizeExtraSmall};
  1057. color: ${p => p.theme.gray300};
  1058. white-space: nowrap;
  1059. font-variant-numeric: tabular-nums;
  1060. position: absolute;
  1061. &.Inside {
  1062. color: ${p => p.theme.gray100};
  1063. }
  1064. }
  1065. .TraceChildrenCount {
  1066. height: 16px;
  1067. white-space: nowrap;
  1068. min-width: 30px;
  1069. display: flex;
  1070. align-items: center;
  1071. justify-content: center;
  1072. border-radius: 99px;
  1073. padding: 0px ${space(0.5)};
  1074. transition: all 0.15s ease-in-out;
  1075. background: ${p => p.theme.background};
  1076. border: 2px solid ${p => p.theme.border};
  1077. line-height: 0;
  1078. z-index: 1;
  1079. font-size: 10px;
  1080. box-shadow: ${p => p.theme.dropShadowLight};
  1081. margin-right: ${space(1)};
  1082. svg {
  1083. width: 7px;
  1084. transition: none;
  1085. }
  1086. }
  1087. .TraceChildrenCountWrapper {
  1088. display: flex;
  1089. justify-content: flex-end;
  1090. align-items: center;
  1091. min-width: 48px;
  1092. height: 100%;
  1093. position: relative;
  1094. button {
  1095. transition: none;
  1096. }
  1097. &.Orphaned {
  1098. .TraceVerticalConnector,
  1099. .TraceVerticalLastChildConnector,
  1100. .TraceExpandedVerticalConnector {
  1101. border-left: 2px dashed ${p => p.theme.border};
  1102. }
  1103. &::before {
  1104. border-bottom: 2px dashed ${p => p.theme.border};
  1105. }
  1106. }
  1107. &.Root {
  1108. &:before,
  1109. .TraceVerticalLastChildConnector {
  1110. visibility: hidden;
  1111. }
  1112. }
  1113. &::before {
  1114. content: '';
  1115. display: block;
  1116. width: 60%;
  1117. height: 2px;
  1118. border-bottom: 2px solid ${p => p.theme.border};
  1119. position: absolute;
  1120. left: 0;
  1121. top: 50%;
  1122. transform: translateY(-50%);
  1123. }
  1124. &::after {
  1125. content: '';
  1126. background-color: rgb(224, 220, 229);
  1127. border-radius: 50%;
  1128. height: 6px;
  1129. width: 6px;
  1130. position: absolute;
  1131. left: 60%;
  1132. top: 50%;
  1133. transform: translateY(-50%);
  1134. }
  1135. }
  1136. .TraceVerticalConnector {
  1137. position: absolute;
  1138. left: 0;
  1139. top: 0;
  1140. bottom: 0;
  1141. height: 100%;
  1142. width: 2px;
  1143. border-left: 2px solid ${p => p.theme.border};
  1144. &.Orphaned {
  1145. border-left: 2px dashed ${p => p.theme.border};
  1146. }
  1147. }
  1148. .TraceVerticalLastChildConnector {
  1149. position: absolute;
  1150. left: 0;
  1151. top: 0;
  1152. bottom: 0;
  1153. height: 50%;
  1154. width: 2px;
  1155. border-left: 2px solid ${p => p.theme.border};
  1156. border-bottom-left-radius: 4px;
  1157. }
  1158. .TraceExpandedVerticalConnector {
  1159. position: absolute;
  1160. bottom: 0;
  1161. height: 50%;
  1162. left: 50%;
  1163. width: 2px;
  1164. border-left: 2px solid ${p => p.theme.border};
  1165. }
  1166. .TraceOperation {
  1167. margin-left: ${space(0.5)};
  1168. text-overflow: ellipsis;
  1169. white-space: nowrap;
  1170. font-weight: bold;
  1171. }
  1172. .TraceEmDash {
  1173. margin-left: ${space(0.5)};
  1174. margin-right: ${space(0.5)};
  1175. }
  1176. .TraceDescription {
  1177. white-space: nowrap;
  1178. }
  1179. `;