trace.tsx 56 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039
  1. import type React from 'react';
  2. import {Fragment, useCallback, useEffect, useMemo, useRef, useState} from 'react';
  3. import {browserHistory} from 'react-router';
  4. import {type Theme, useTheme} from '@emotion/react';
  5. import styled from '@emotion/styled';
  6. import {PlatformIcon} from 'platformicons';
  7. import * as qs from 'query-string';
  8. import Link from 'sentry/components/links/link';
  9. import LoadingIndicator from 'sentry/components/loadingIndicator';
  10. import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
  11. import Placeholder from 'sentry/components/placeholder';
  12. import {generateIssueEventTarget} from 'sentry/components/quickTrace/utils';
  13. import {IconFire} from 'sentry/icons';
  14. import {t} from 'sentry/locale';
  15. import type {Organization, PlatformKey, Project} from 'sentry/types';
  16. import {getDuration} from 'sentry/utils/formatters';
  17. import useApi from 'sentry/utils/useApi';
  18. import useOrganization from 'sentry/utils/useOrganization';
  19. import useProjects from 'sentry/utils/useProjects';
  20. import {
  21. getRovingIndexActionFromEvent,
  22. type RovingTabIndexAction,
  23. type RovingTabIndexUserActions,
  24. } from 'sentry/views/performance/newTraceDetails/rovingTabIndex';
  25. import type {
  26. TraceSearchAction,
  27. TraceSearchState,
  28. } from 'sentry/views/performance/newTraceDetails/traceSearch';
  29. import {
  30. isAutogroupedNode,
  31. isMissingInstrumentationNode,
  32. isParentAutogroupedNode,
  33. isSpanNode,
  34. isTraceErrorNode,
  35. isTraceNode,
  36. isTransactionNode,
  37. } from './guards';
  38. import {ParentAutogroupNode, type TraceTree, type TraceTreeNode} from './traceTree';
  39. import {
  40. useVirtualizedList,
  41. type VirtualizedRow,
  42. type VirtualizedViewManager,
  43. } from './virtualizedViewManager';
  44. function Chevron(props: {direction: 'up' | 'down' | 'left'}) {
  45. return (
  46. <svg
  47. viewBox="0 0 16 16"
  48. style={{
  49. fill: 'currentcolor',
  50. color: 'currentcolor',
  51. transition: 'transform 120ms ease-in-out',
  52. transform: `rotate(${props.direction === 'up' ? 0 : props.direction === 'down' ? 180 : -90}deg)`,
  53. }}
  54. >
  55. <path d="M14,11.75a.74.74,0,0,1-.53-.22L8,6.06,2.53,11.53a.75.75,0,0,1-1.06-1.06l6-6a.75.75,0,0,1,1.06,0l6,6a.75.75,0,0,1,0,1.06A.74.74,0,0,1,14,11.75Z" />
  56. </svg>
  57. );
  58. }
  59. function decodeScrollQueue(maybePath: unknown): TraceTree.NodePath[] | null {
  60. if (Array.isArray(maybePath)) {
  61. return maybePath;
  62. }
  63. if (typeof maybePath === 'string') {
  64. return [maybePath as TraceTree.NodePath];
  65. }
  66. return null;
  67. }
  68. const COUNT_FORMATTER = Intl.NumberFormat(undefined, {notation: 'compact'});
  69. interface RovingTabIndexState {
  70. index: number | null;
  71. items: number | null;
  72. node: TraceTreeNode<TraceTree.NodeValue> | null;
  73. }
  74. function computeNextIndexFromAction(
  75. current_index: number,
  76. action: RovingTabIndexUserActions,
  77. items: number
  78. ): number {
  79. switch (action) {
  80. case 'next':
  81. if (current_index === items) {
  82. return 0;
  83. }
  84. return current_index + 1;
  85. case 'previous':
  86. if (current_index === 0) {
  87. return items;
  88. }
  89. return current_index - 1;
  90. case 'last':
  91. return items;
  92. case 'first':
  93. return 0;
  94. default:
  95. throw new TypeError(`Invalid or not implemented reducer action - ${action}`);
  96. }
  97. }
  98. function maybeFocusRow(
  99. ref: HTMLDivElement | null,
  100. index: number,
  101. previouslyFocusedIndexRef: React.MutableRefObject<number | null>
  102. ) {
  103. if (!ref) return;
  104. if (index === previouslyFocusedIndexRef.current) return;
  105. ref.focus();
  106. previouslyFocusedIndexRef.current = index;
  107. }
  108. interface TraceProps {
  109. manager: VirtualizedViewManager;
  110. onTraceSearch: (query: string) => void;
  111. previouslyFocusedIndexRef: React.MutableRefObject<number | null>;
  112. roving_dispatch: React.Dispatch<RovingTabIndexAction>;
  113. roving_state: RovingTabIndexState;
  114. searchResultsIteratorIndex: number | undefined;
  115. searchResultsMap: Map<TraceTreeNode<TraceTree.NodeValue>, number>;
  116. search_dispatch: React.Dispatch<TraceSearchAction>;
  117. search_state: TraceSearchState;
  118. setClickedNode: (node: TraceTreeNode<TraceTree.NodeValue> | null) => void;
  119. trace: TraceTree;
  120. trace_id: string;
  121. }
  122. function Trace({
  123. trace,
  124. trace_id,
  125. roving_state,
  126. roving_dispatch,
  127. search_state,
  128. search_dispatch,
  129. setClickedNode: setDetailNode,
  130. manager,
  131. searchResultsIteratorIndex,
  132. searchResultsMap,
  133. previouslyFocusedIndexRef,
  134. onTraceSearch,
  135. }: TraceProps) {
  136. const theme = useTheme();
  137. const api = useApi();
  138. const {projects} = useProjects();
  139. const organization = useOrganization();
  140. const containerRef = useRef<HTMLDivElement | null>(null);
  141. const [_rerender, setRender] = useState(0);
  142. const treePromiseStatusRef =
  143. useRef<Map<TraceTreeNode<TraceTree.NodeValue>, 'loading' | 'error' | 'success'>>();
  144. if (!treePromiseStatusRef.current) {
  145. treePromiseStatusRef.current = new Map();
  146. }
  147. const scrollQueue = useRef<TraceTree.NodePath[] | null>(null);
  148. const treeRef = useRef<TraceTree>(trace);
  149. treeRef.current = trace;
  150. if (
  151. trace.root.space &&
  152. (trace.root.space[0] !== manager.to_origin ||
  153. trace.root.space[1] !== manager.trace_space.width)
  154. ) {
  155. manager.initializeTraceSpace([trace.root.space[0], 0, trace.root.space[1], 1]);
  156. scrollQueue.current = decodeScrollQueue(qs.parse(location.search).node);
  157. }
  158. const loadedRef = useRef(false);
  159. useEffect(() => {
  160. if (loadedRef.current) {
  161. return;
  162. }
  163. if (trace.type === 'loading' || !manager) {
  164. return;
  165. }
  166. loadedRef.current = true;
  167. if (!scrollQueue.current) {
  168. if (search_state.query) {
  169. onTraceSearch(search_state.query);
  170. }
  171. return;
  172. }
  173. manager
  174. .scrollToPath(trace, scrollQueue.current, () => setRender(a => (a + 1) % 2), {
  175. api,
  176. organization,
  177. })
  178. .then(maybeNode => {
  179. scrollQueue.current = null;
  180. if (!maybeNode) {
  181. return;
  182. }
  183. manager.onScrollEndOutOfBoundsCheck();
  184. setDetailNode(maybeNode.node);
  185. roving_dispatch({
  186. type: 'set index',
  187. index: maybeNode.index,
  188. node: maybeNode.node,
  189. });
  190. if (search_state.query) {
  191. onTraceSearch(search_state.query);
  192. }
  193. });
  194. }, [
  195. api,
  196. organization,
  197. trace,
  198. trace_id,
  199. manager,
  200. search_state.query,
  201. onTraceSearch,
  202. setDetailNode,
  203. roving_dispatch,
  204. ]);
  205. const handleZoomIn = useCallback(
  206. (
  207. event: React.MouseEvent,
  208. node: TraceTreeNode<TraceTree.NodeValue>,
  209. value: boolean
  210. ) => {
  211. if (!isTransactionNode(node) && !isSpanNode(node)) {
  212. throw new TypeError('Node must be a transaction or span');
  213. }
  214. event.stopPropagation();
  215. setRender(a => (a + 1) % 2);
  216. treeRef.current
  217. .zoomIn(node, value, {
  218. api,
  219. organization,
  220. })
  221. .then(() => {
  222. setRender(a => (a + 1) % 2);
  223. if (search_state.query) {
  224. onTraceSearch(search_state.query);
  225. }
  226. if (search_state.resultsLookup.has(node)) {
  227. const idx = search_state.resultsLookup.get(node)!;
  228. search_dispatch({
  229. type: 'set iterator index',
  230. resultIndex: search_state.results?.[idx]?.index!,
  231. resultIteratorIndex: idx,
  232. });
  233. } else {
  234. search_dispatch({type: 'clear iterator index'});
  235. }
  236. treePromiseStatusRef.current!.set(node, 'success');
  237. })
  238. .catch(_e => {
  239. treePromiseStatusRef.current!.set(node, 'error');
  240. });
  241. },
  242. [api, organization, search_state, search_dispatch, onTraceSearch]
  243. );
  244. const handleExpandNode = useCallback(
  245. (
  246. event: React.MouseEvent<Element>,
  247. node: TraceTreeNode<TraceTree.NodeValue>,
  248. value: boolean
  249. ) => {
  250. event.stopPropagation();
  251. treeRef.current.expand(node, value);
  252. setRender(a => (a + 1) % 2);
  253. if (search_state.query) {
  254. onTraceSearch(search_state.query);
  255. }
  256. if (search_state.resultsLookup.has(node)) {
  257. const idx = search_state.resultsLookup.get(node)!;
  258. search_dispatch({
  259. type: 'set iterator index',
  260. resultIndex: search_state.results?.[idx]?.index!,
  261. resultIteratorIndex: idx,
  262. });
  263. } else {
  264. search_dispatch({type: 'clear iterator index'});
  265. }
  266. },
  267. [search_state, search_dispatch, onTraceSearch]
  268. );
  269. const onRowClick = useCallback(
  270. (
  271. _event: React.MouseEvent,
  272. index: number,
  273. node: TraceTreeNode<TraceTree.NodeValue>
  274. ) => {
  275. previouslyFocusedIndexRef.current = index;
  276. browserHistory.push({
  277. pathname: location.pathname,
  278. query: {
  279. ...qs.parse(location.search),
  280. node: node.path,
  281. },
  282. });
  283. setDetailNode(node);
  284. roving_dispatch({type: 'set index', index, node});
  285. if (search_state.resultsLookup.has(node)) {
  286. const idx = search_state.resultsLookup.get(node)!;
  287. search_dispatch({
  288. type: 'set iterator index',
  289. resultIndex: index,
  290. resultIteratorIndex: idx,
  291. });
  292. } else {
  293. search_dispatch({type: 'clear iterator index'});
  294. }
  295. },
  296. [
  297. roving_dispatch,
  298. setDetailNode,
  299. search_state,
  300. search_dispatch,
  301. previouslyFocusedIndexRef,
  302. ]
  303. );
  304. const onRowKeyDown = useCallback(
  305. (
  306. event: React.KeyboardEvent,
  307. index: number,
  308. node: TraceTreeNode<TraceTree.NodeValue>
  309. ) => {
  310. if (!manager.list) {
  311. return;
  312. }
  313. const action = getRovingIndexActionFromEvent(event);
  314. if (action) {
  315. event.preventDefault();
  316. const nextIndex = computeNextIndexFromAction(
  317. index,
  318. action,
  319. treeRef.current.list.length - 1
  320. );
  321. manager.list.scrollToRow(nextIndex);
  322. roving_dispatch({type: 'set index', index: nextIndex, node});
  323. if (search_state.resultsLookup.has(trace.list[nextIndex])) {
  324. const idx = search_state.resultsLookup.get(trace.list[nextIndex])!;
  325. search_dispatch({
  326. type: 'set iterator index',
  327. resultIndex: nextIndex,
  328. resultIteratorIndex: idx,
  329. });
  330. } else {
  331. search_dispatch({type: 'clear iterator index'});
  332. }
  333. }
  334. },
  335. [manager.list, roving_dispatch, search_state, search_dispatch, trace.list]
  336. );
  337. // @TODO this is the implementation of infinite scroll. Once the user
  338. // reaches the end of the list, we fetch more data. The data is not yet
  339. // being appended to the tree as we need to figure out UX for this.
  340. // onRowsRendered callback should be passed to the List component
  341. // const limitRef = useRef<number | null>(null);
  342. // if (limitRef.current === null) {
  343. // let decodedLimit = getTraceQueryParams(qs.parse(location.search)).limit;
  344. // if (typeof decodedLimit === 'string') {
  345. // decodedLimit = parseInt(decodedLimit, 2);
  346. // }
  347. // limitRef.current = decodedLimit;
  348. // }
  349. // const loadMoreRequestRef =
  350. // useRef<Promise<TraceSplitResults<TraceFullDetailed> | null> | null>(null);
  351. // const onRowsRendered = useCallback((rows: RenderedRows) => {
  352. // if (loadMoreRequestRef.current) {
  353. // // in flight request
  354. // return;
  355. // }
  356. // if (rows.stopIndex !== treeRef.current.list.length - 1) {
  357. // // not at the end
  358. // return;
  359. // }
  360. // if (
  361. // !loadMoreRequestRef.current &&
  362. // limitRef.current &&
  363. // rows.stopIndex === treeRef.current.list.length - 1
  364. // ) {
  365. // limitRef.current = limitRef.current + 500;
  366. // const promise = fetchTrace(api, {
  367. // traceId: trace_id,
  368. // orgSlug: organization.slug,
  369. // query: qs.stringify(getTraceQueryParams(location, {limit: limitRef.current})),
  370. // })
  371. // .then(data => {
  372. // return data;
  373. // })
  374. // .catch(e => {
  375. // return e;
  376. // });
  377. // loadMoreRequestRef.current = promise;
  378. // }
  379. // }, []);
  380. const projectLookup: Record<string, PlatformKey | undefined> = useMemo(() => {
  381. return projects.reduce<Record<Project['slug'], Project['platform']>>(
  382. (acc, project) => {
  383. acc[project.slug] = project.platform;
  384. return acc;
  385. },
  386. {}
  387. );
  388. }, [projects]);
  389. const render = useCallback(
  390. (n: VirtualizedRow) => {
  391. return trace.type === 'loading' ? (
  392. <RenderPlaceholderRow
  393. key={n.key}
  394. index={n.index}
  395. style={n.style}
  396. node={n.item}
  397. theme={theme}
  398. projects={projectLookup}
  399. manager={manager}
  400. />
  401. ) : (
  402. <RenderRow
  403. key={n.key}
  404. index={n.index}
  405. organization={organization}
  406. previouslyFocusedIndexRef={previouslyFocusedIndexRef}
  407. tabIndex={roving_state.index ?? -1}
  408. isSearchResult={searchResultsMap.has(n.item)}
  409. searchResultsIteratorIndex={searchResultsIteratorIndex}
  410. style={n.style}
  411. trace_id={trace_id}
  412. projects={projectLookup}
  413. node={n.item}
  414. manager={manager}
  415. theme={theme}
  416. onExpand={handleExpandNode}
  417. onZoomIn={handleZoomIn}
  418. onRowClick={onRowClick}
  419. onRowKeyDown={onRowKeyDown}
  420. />
  421. );
  422. },
  423. // we add _rerender as a dependency to trigger the virtualized list rerender
  424. // eslint-disable-next-line react-hooks/exhaustive-deps
  425. [
  426. handleExpandNode,
  427. handleZoomIn,
  428. manager,
  429. onRowClick,
  430. onRowKeyDown,
  431. organization,
  432. projectLookup,
  433. roving_state.index,
  434. searchResultsIteratorIndex,
  435. searchResultsMap,
  436. theme,
  437. trace_id,
  438. trace.type,
  439. _rerender,
  440. ]
  441. );
  442. const [scrollContainer, setScrollContainer] = useState<HTMLElement | null>(null);
  443. const virtualizedList = useVirtualizedList({
  444. manager,
  445. items: trace.list,
  446. container: scrollContainer,
  447. render,
  448. });
  449. return (
  450. <TraceStylingWrapper
  451. ref={r => {
  452. containerRef.current = r;
  453. manager.onContainerRef(r);
  454. }}
  455. className={`${trace.indicators.length > 0 ? 'WithIndicators' : ''} ${trace.type === 'loading' ? 'Loading' : ''}`}
  456. >
  457. <div className="TraceDivider" ref={r => manager?.registerDividerRef(r)} />
  458. {trace.type === 'loading' ? <TraceLoading /> : null}
  459. <div
  460. className="TraceIndicatorContainer"
  461. ref={r => manager.registerIndicatorContainerRef(r)}
  462. >
  463. {trace.indicators.length > 0
  464. ? trace.indicators.map((indicator, i) => {
  465. return (
  466. <div
  467. key={i}
  468. ref={r => manager.registerIndicatorRef(r, i, indicator)}
  469. className="TraceIndicator"
  470. >
  471. <div className="TraceIndicatorLabel">{indicator.label}</div>
  472. <div className="TraceIndicatorLine" />
  473. </div>
  474. );
  475. })
  476. : null}
  477. {manager.interval_bars.map((_, i) => {
  478. const indicatorTimestamp = manager.intervals[i];
  479. const timestamp = manager.to_origin + indicatorTimestamp ?? 0;
  480. if (trace.type === 'loading') {
  481. return null;
  482. }
  483. return (
  484. <div
  485. key={i}
  486. ref={r => manager.registerTimelineIndicatorRef(r, i)}
  487. className="TraceIndicator Timeline"
  488. style={{
  489. transform: `translate(${manager.computeTransformXFromTimestamp(timestamp)}px, 0)`,
  490. }}
  491. >
  492. <div className="TraceIndicatorLabel">
  493. {indicatorTimestamp > 0
  494. ? getDuration(
  495. (manager.trace_view.x + indicatorTimestamp) / 1000,
  496. 2,
  497. true
  498. )
  499. : '0s'}
  500. </div>
  501. <div className="TraceIndicatorLine" />
  502. </div>
  503. );
  504. })}
  505. </div>
  506. <div ref={r => setScrollContainer(r)}>
  507. <div>{virtualizedList.rendered}</div>
  508. </div>
  509. </TraceStylingWrapper>
  510. );
  511. }
  512. export default Trace;
  513. function RenderRow(props: {
  514. index: number;
  515. isSearchResult: boolean;
  516. manager: VirtualizedViewManager;
  517. node: TraceTreeNode<TraceTree.NodeValue>;
  518. onExpand: (
  519. event: React.MouseEvent<Element>,
  520. node: TraceTreeNode<TraceTree.NodeValue>,
  521. value: boolean
  522. ) => void;
  523. onRowClick: (
  524. event: React.MouseEvent<Element>,
  525. index: number,
  526. node: TraceTreeNode<TraceTree.NodeValue>
  527. ) => void;
  528. onRowKeyDown: (
  529. event: React.KeyboardEvent,
  530. index: number,
  531. node: TraceTreeNode<TraceTree.NodeValue>
  532. ) => void;
  533. onZoomIn: (
  534. event: React.MouseEvent<Element>,
  535. node: TraceTreeNode<TraceTree.NodeValue>,
  536. value: boolean
  537. ) => void;
  538. organization: Organization;
  539. previouslyFocusedIndexRef: React.MutableRefObject<number | null>;
  540. projects: Record<Project['slug'], Project['platform']>;
  541. searchResultsIteratorIndex: number | undefined;
  542. style: React.CSSProperties;
  543. tabIndex: number;
  544. theme: Theme;
  545. trace_id: string;
  546. }) {
  547. const virtualized_index = props.index - props.manager.start_virtualized_index;
  548. if (!props.node.value) {
  549. return null;
  550. }
  551. const rowSearchClassName = `${props.isSearchResult ? 'SearchResult' : ''} ${props.searchResultsIteratorIndex === props.index ? 'Highlight' : ''}`;
  552. if (isAutogroupedNode(props.node)) {
  553. return (
  554. <div
  555. key={props.index}
  556. ref={r =>
  557. props.tabIndex === props.index
  558. ? maybeFocusRow(r, props.index, props.previouslyFocusedIndexRef)
  559. : null
  560. }
  561. tabIndex={props.tabIndex === props.index ? 0 : -1}
  562. className={`Autogrouped TraceRow ${rowSearchClassName}`}
  563. onClick={e => props.onRowClick(e, props.index, props.node)}
  564. onKeyDown={event => props.onRowKeyDown(event, props.index, props.node)}
  565. style={{
  566. top: props.style.top,
  567. height: props.style.height,
  568. }}
  569. >
  570. <div
  571. className="TraceLeftColumn"
  572. ref={r =>
  573. props.manager.registerColumnRef('list', r, virtualized_index, props.node)
  574. }
  575. style={{
  576. width: props.manager.columns.list.width * 100 + '%',
  577. }}
  578. >
  579. <div
  580. className={`TraceLeftColumnInner ${props.node.has_error ? 'Errored' : ''}`}
  581. style={{
  582. paddingLeft: props.node.depth * props.manager.row_depth_padding,
  583. }}
  584. >
  585. <div className="TraceChildrenCountWrapper">
  586. <Connectors node={props.node} manager={props.manager} />
  587. <ChildrenButton
  588. icon={
  589. props.node.expanded ? (
  590. <Chevron direction="up" />
  591. ) : (
  592. <Chevron direction="down" />
  593. )
  594. }
  595. status={props.node.fetchStatus}
  596. expanded={!props.node.expanded}
  597. onClick={e => props.onExpand(e, props.node, !props.node.expanded)}
  598. errored={props.node.has_error}
  599. >
  600. {COUNT_FORMATTER.format(props.node.groupCount)}
  601. </ChildrenButton>
  602. </div>
  603. <span className="TraceOperation">{t('Autogrouped')}</span>
  604. <strong className="TraceEmDash"> — </strong>
  605. <span className="TraceDescription">{props.node.value.autogrouped_by.op}</span>
  606. </div>
  607. </div>
  608. <div
  609. className={`TraceRightColumn ${props.index % 2 === 0 ? 0 : 'Odd'}`}
  610. ref={r =>
  611. props.manager.registerColumnRef('span_list', r, virtualized_index, props.node)
  612. }
  613. style={{
  614. width: props.manager.columns.span_list.width * 100 + '%',
  615. }}
  616. onDoubleClick={e => {
  617. e.stopPropagation();
  618. props.manager.onZoomIntoSpace(props.node.space!);
  619. }}
  620. >
  621. <AutogroupedTraceBar
  622. virtualized_index={virtualized_index}
  623. manager={props.manager}
  624. color={props.theme.blue300}
  625. entire_space={props.node.space}
  626. node_spaces={props.node.autogroupedSegments}
  627. />
  628. <button
  629. ref={ref =>
  630. props.manager.registerArrowRef(ref, props.node.space!, virtualized_index)
  631. }
  632. className="TraceArrow"
  633. onClick={_e => {
  634. props.manager.onBringRowIntoView(props.node.space!);
  635. }}
  636. >
  637. <Chevron direction="left" />
  638. </button>
  639. </div>
  640. </div>
  641. );
  642. }
  643. if (isTransactionNode(props.node)) {
  644. const errored =
  645. props.node.value.errors.length > 0 ||
  646. props.node.value.performance_issues.length > 0;
  647. return (
  648. <div
  649. key={props.index}
  650. ref={r =>
  651. props.tabIndex === props.index
  652. ? maybeFocusRow(r, props.index, props.previouslyFocusedIndexRef)
  653. : null
  654. }
  655. tabIndex={props.tabIndex === props.index ? 0 : -1}
  656. className={`TraceRow ${rowSearchClassName}`}
  657. onClick={e => props.onRowClick(e, props.index, props.node)}
  658. onKeyDown={event => props.onRowKeyDown(event, props.index, props.node)}
  659. style={{
  660. top: props.style.top,
  661. height: props.style.height,
  662. }}
  663. >
  664. <div
  665. className="TraceLeftColumn"
  666. ref={r =>
  667. props.manager.registerColumnRef('list', r, virtualized_index, props.node)
  668. }
  669. style={{
  670. width: props.manager.columns.list.width * 100 + '%',
  671. }}
  672. >
  673. <div
  674. className={`TraceLeftColumnInner ${errored ? 'Errored' : ''}`}
  675. style={{
  676. paddingLeft: props.node.depth * props.manager.row_depth_padding,
  677. }}
  678. >
  679. <div
  680. className={`TraceChildrenCountWrapper ${
  681. props.node.isOrphaned ? 'Orphaned' : ''
  682. }
  683. `}
  684. >
  685. <Connectors node={props.node} manager={props.manager} />
  686. {props.node.children.length > 0 || props.node.canFetch ? (
  687. <ChildrenButton
  688. icon={
  689. props.node.canFetch && props.node.fetchStatus === 'idle' ? (
  690. '+'
  691. ) : props.node.canFetch && props.node.zoomedIn ? (
  692. <Chevron direction="down" />
  693. ) : (
  694. '+'
  695. )
  696. }
  697. status={props.node.fetchStatus}
  698. expanded={props.node.expanded || props.node.zoomedIn}
  699. onClick={e =>
  700. props.node.canFetch
  701. ? props.onZoomIn(e, props.node, !props.node.zoomedIn)
  702. : props.onExpand(e, props.node, !props.node.expanded)
  703. }
  704. errored={errored}
  705. >
  706. {props.node.children.length > 0
  707. ? COUNT_FORMATTER.format(props.node.children.length)
  708. : null}
  709. </ChildrenButton>
  710. ) : null}
  711. </div>
  712. <PlatformIcon
  713. platform={props.projects[props.node.value.project_slug] ?? 'default'}
  714. />
  715. <span className="TraceOperation">{props.node.value['transaction.op']}</span>
  716. <strong className="TraceEmDash"> — </strong>
  717. <span>{props.node.value.transaction}</span>
  718. </div>
  719. </div>
  720. <div
  721. ref={r =>
  722. props.manager.registerColumnRef('span_list', r, virtualized_index, props.node)
  723. }
  724. className={`TraceRightColumn ${props.index % 2 === 0 ? 0 : 'Odd'}`}
  725. style={{
  726. width: props.manager.columns.span_list.width * 100 + '%',
  727. }}
  728. onDoubleClick={e => {
  729. e.stopPropagation();
  730. props.manager.onZoomIntoSpace(props.node.space!);
  731. }}
  732. >
  733. <TraceBar
  734. virtualized_index={virtualized_index}
  735. manager={props.manager}
  736. color={pickBarColor(props.node.value['transaction.op'])}
  737. node_space={props.node.space}
  738. />
  739. <button
  740. ref={ref =>
  741. props.manager.registerArrowRef(ref, props.node.space!, virtualized_index)
  742. }
  743. className="TraceArrow"
  744. onClick={_e => {
  745. props.manager.onBringRowIntoView(props.node.space!);
  746. }}
  747. >
  748. <Chevron direction="left" />
  749. </button>
  750. </div>
  751. </div>
  752. );
  753. }
  754. if (isSpanNode(props.node)) {
  755. const errored = props.node.value.relatedErrors.length > 0;
  756. return (
  757. <div
  758. key={props.index}
  759. ref={r =>
  760. props.tabIndex === props.index
  761. ? maybeFocusRow(r, props.index, props.previouslyFocusedIndexRef)
  762. : null
  763. }
  764. tabIndex={props.tabIndex === props.index ? 0 : -1}
  765. className={`TraceRow ${rowSearchClassName}`}
  766. onClick={e => props.onRowClick(e, props.index, props.node)}
  767. onKeyDown={event => props.onRowKeyDown(event, props.index, props.node)}
  768. style={{
  769. top: props.style.top,
  770. height: props.style.height,
  771. }}
  772. >
  773. <div
  774. className="TraceLeftColumn"
  775. ref={r =>
  776. props.manager.registerColumnRef('list', r, virtualized_index, props.node)
  777. }
  778. style={{
  779. width: props.manager.columns.list.width * 100 + '%',
  780. }}
  781. >
  782. <div
  783. className={`TraceLeftColumnInner ${errored ? 'Errored' : ''}`}
  784. style={{
  785. paddingLeft: props.node.depth * props.manager.row_depth_padding,
  786. }}
  787. >
  788. <div
  789. className={`TraceChildrenCountWrapper ${
  790. props.node.isOrphaned ? 'Orphaned' : ''
  791. }`}
  792. >
  793. <Connectors node={props.node} manager={props.manager} />
  794. {props.node.children.length > 0 || props.node.canFetch ? (
  795. <ChildrenButton
  796. icon={
  797. props.node.canFetch ? (
  798. '+'
  799. ) : props.node.expanded ? (
  800. <Chevron direction="up" />
  801. ) : (
  802. <Chevron direction="down" />
  803. )
  804. }
  805. status={props.node.fetchStatus}
  806. expanded={props.node.expanded || props.node.zoomedIn}
  807. onClick={e =>
  808. props.node.canFetch
  809. ? props.onZoomIn(e, props.node, !props.node.zoomedIn)
  810. : props.onExpand(e, props.node, !props.node.expanded)
  811. }
  812. errored={errored}
  813. >
  814. {props.node.children.length > 0
  815. ? COUNT_FORMATTER.format(props.node.children.length)
  816. : null}
  817. </ChildrenButton>
  818. ) : null}
  819. </div>
  820. <span className="TraceOperation">{props.node.value.op ?? '<unknown>'}</span>
  821. <strong className="TraceEmDash"> — </strong>
  822. <span className="TraceDescription" title={props.node.value.description}>
  823. {!props.node.value.description
  824. ? 'unknown'
  825. : props.node.value.description.length > 100
  826. ? props.node.value.description.slice(0, 100).trim() + '\u2026'
  827. : props.node.value.description}
  828. </span>
  829. </div>
  830. </div>
  831. <div
  832. ref={r =>
  833. props.manager.registerColumnRef('span_list', r, virtualized_index, props.node)
  834. }
  835. className={`TraceRightColumn ${props.index % 2 === 0 ? 0 : 'Odd'}`}
  836. style={{
  837. width: props.manager.columns.span_list.width * 100 + '%',
  838. }}
  839. onDoubleClick={e => {
  840. e.stopPropagation();
  841. props.manager.onZoomIntoSpace(props.node.space!);
  842. }}
  843. >
  844. <TraceBar
  845. virtualized_index={virtualized_index}
  846. manager={props.manager}
  847. color={pickBarColor(props.node.value.op)}
  848. node_space={props.node.space}
  849. />
  850. <button
  851. ref={ref =>
  852. props.manager.registerArrowRef(ref, props.node.space!, virtualized_index)
  853. }
  854. className="TraceArrow"
  855. onClick={_e => {
  856. props.manager.onBringRowIntoView(props.node.space!);
  857. }}
  858. >
  859. <Chevron direction="left" />
  860. </button>
  861. </div>
  862. </div>
  863. );
  864. }
  865. if (isMissingInstrumentationNode(props.node)) {
  866. return (
  867. <div
  868. key={props.index}
  869. ref={r =>
  870. props.tabIndex === props.index
  871. ? maybeFocusRow(r, props.index, props.previouslyFocusedIndexRef)
  872. : null
  873. }
  874. tabIndex={props.tabIndex === props.index ? 0 : -1}
  875. className={`TraceRow ${rowSearchClassName}`}
  876. onClick={e => props.onRowClick(e, props.index, props.node)}
  877. onKeyDown={event => props.onRowKeyDown(event, props.index, props.node)}
  878. style={{
  879. top: props.style.top,
  880. height: props.style.height,
  881. }}
  882. >
  883. <div
  884. className="TraceLeftColumn"
  885. ref={r =>
  886. props.manager.registerColumnRef('list', r, virtualized_index, props.node)
  887. }
  888. style={{
  889. width: props.manager.columns.list.width * 100 + '%',
  890. }}
  891. >
  892. <div
  893. className="TraceLeftColumnInner"
  894. style={{
  895. paddingLeft: props.node.depth * props.manager.row_depth_padding,
  896. }}
  897. >
  898. <div className="TraceChildrenCountWrapper">
  899. <Connectors node={props.node} manager={props.manager} />
  900. </div>
  901. <span className="TraceOperation">{t('Missing instrumentation')}</span>
  902. </div>
  903. </div>
  904. <div
  905. ref={r =>
  906. props.manager.registerColumnRef('span_list', r, virtualized_index, props.node)
  907. }
  908. className={`TraceRightColumn ${props.index % 2 === 0 ? 0 : 'Odd'}`}
  909. style={{
  910. width: props.manager.columns.span_list.width * 100 + '%',
  911. }}
  912. onDoubleClick={e => {
  913. e.stopPropagation();
  914. props.manager.onZoomIntoSpace(props.node.space!);
  915. }}
  916. >
  917. <TraceBar
  918. virtualized_index={virtualized_index}
  919. manager={props.manager}
  920. color={pickBarColor('missing-instrumentation')}
  921. node_space={props.node.space}
  922. />
  923. <button
  924. ref={ref =>
  925. props.manager.registerArrowRef(ref, props.node.space!, virtualized_index)
  926. }
  927. className="TraceArrow"
  928. onClick={_e => {
  929. props.manager.onBringRowIntoView(props.node.space!);
  930. }}
  931. >
  932. <Chevron direction="left" />
  933. </button>
  934. </div>
  935. </div>
  936. );
  937. }
  938. if (isTraceNode(props.node)) {
  939. return (
  940. <div
  941. key={props.index}
  942. ref={r =>
  943. props.tabIndex === props.index
  944. ? maybeFocusRow(r, props.index, props.previouslyFocusedIndexRef)
  945. : null
  946. }
  947. tabIndex={props.tabIndex === props.index ? 0 : -1}
  948. className={`TraceRow ${rowSearchClassName}`}
  949. onClick={e => props.onRowClick(e, props.index, props.node)}
  950. onKeyDown={event => props.onRowKeyDown(event, props.index, props.node)}
  951. style={{
  952. top: props.style.top,
  953. height: props.style.height,
  954. }}
  955. >
  956. <div
  957. className="TraceLeftColumn"
  958. ref={r =>
  959. props.manager.registerColumnRef('list', r, virtualized_index, props.node)
  960. }
  961. style={{
  962. width: props.manager.columns.list.width * 100 + '%',
  963. }}
  964. >
  965. <div
  966. className="TraceLeftColumnInner"
  967. style={{
  968. paddingLeft: props.node.depth * props.manager.row_depth_padding,
  969. }}
  970. >
  971. <div className="TraceChildrenCountWrapper Root">
  972. <Connectors node={props.node} manager={props.manager} />
  973. {props.node.children.length > 0 || props.node.canFetch ? (
  974. <ChildrenButton icon={''} status={'idle'} expanded onClick={() => void 0}>
  975. {props.node.children.length > 0
  976. ? COUNT_FORMATTER.format(props.node.children.length)
  977. : null}
  978. </ChildrenButton>
  979. ) : null}
  980. </div>
  981. <span className="TraceOperation">{t('Trace')}</span>
  982. <strong className="TraceEmDash"> — </strong>
  983. <span className="TraceDescription">{props.trace_id}</span>
  984. </div>
  985. </div>
  986. <div
  987. ref={r =>
  988. props.manager.registerColumnRef('span_list', r, virtualized_index, props.node)
  989. }
  990. className={`TraceRightColumn ${props.index % 2 === 0 ? 0 : 'Odd'}`}
  991. style={{
  992. width: props.manager.columns.span_list.width * 100 + '%',
  993. }}
  994. onDoubleClick={e => {
  995. e.stopPropagation();
  996. props.manager.onZoomIntoSpace(props.node.space!);
  997. }}
  998. >
  999. <TraceBar
  1000. virtualized_index={virtualized_index}
  1001. manager={props.manager}
  1002. color={pickBarColor('missing-instrumentation')}
  1003. node_space={props.node.space}
  1004. />
  1005. <button
  1006. ref={ref =>
  1007. props.manager.registerArrowRef(ref, props.node.space!, virtualized_index)
  1008. }
  1009. className="TraceArrow"
  1010. onClick={_e => {
  1011. props.manager.onBringRowIntoView(props.node.space!);
  1012. }}
  1013. >
  1014. <Chevron direction="left" />
  1015. </button>
  1016. </div>
  1017. </div>
  1018. );
  1019. }
  1020. if (isTraceErrorNode(props.node)) {
  1021. return (
  1022. <div
  1023. key={props.index}
  1024. ref={r =>
  1025. props.tabIndex === props.index
  1026. ? maybeFocusRow(r, props.index, props.previouslyFocusedIndexRef)
  1027. : null
  1028. }
  1029. tabIndex={props.tabIndex === props.index ? 0 : -1}
  1030. className={`TraceRow ${rowSearchClassName}`}
  1031. onClick={e => props.onRowClick(e, props.index, props.node)}
  1032. onKeyDown={event => props.onRowKeyDown(event, props.index, props.node)}
  1033. style={{
  1034. top: props.style.top,
  1035. height: props.style.height,
  1036. }}
  1037. >
  1038. <div
  1039. className="TraceLeftColumn"
  1040. ref={r =>
  1041. props.manager.registerColumnRef('list', r, virtualized_index, props.node)
  1042. }
  1043. style={{
  1044. width: props.manager.columns.list.width * 100 + '%',
  1045. }}
  1046. >
  1047. <div
  1048. className="TraceLeftColumnInner"
  1049. style={{
  1050. paddingLeft: props.node.depth * props.manager.row_depth_padding,
  1051. }}
  1052. >
  1053. <div className="TraceChildrenCountWrapper">
  1054. <Connectors node={props.node} manager={props.manager} />
  1055. </div>
  1056. <PlatformIcon
  1057. platform={props.projects[props.node.value.project_slug] ?? 'default'}
  1058. />
  1059. ;
  1060. <Link
  1061. className="Errored Link"
  1062. onClick={e => e.stopPropagation()}
  1063. to={generateIssueEventTarget(props.node.value, props.organization)}
  1064. >
  1065. <span className="TraceOperation">{t('Error')}</span>
  1066. <strong className="TraceEmDash"> — </strong>
  1067. <span className="TraceDescription">{props.node.value.title}</span>
  1068. </Link>
  1069. </div>
  1070. </div>
  1071. <div
  1072. ref={r =>
  1073. props.manager.registerColumnRef('span_list', r, virtualized_index, props.node)
  1074. }
  1075. className={`TraceRightColumn ${props.index % 2 === 0 ? 0 : 'Odd'}`}
  1076. style={{
  1077. width: props.manager.columns.span_list.width * 100 + '%',
  1078. }}
  1079. onDoubleClick={e => {
  1080. e.stopPropagation();
  1081. props.manager.onZoomIntoSpace(props.node.space!);
  1082. }}
  1083. >
  1084. <InvisibleTraceBar
  1085. node_space={props.node.space}
  1086. manager={props.manager}
  1087. virtualizedIndex={virtualized_index}
  1088. >
  1089. {typeof props.node.value.timestamp === 'number' ? (
  1090. <div className="TraceError">
  1091. <IconFire color="errorText" size="xs" />
  1092. </div>
  1093. ) : null}
  1094. </InvisibleTraceBar>
  1095. </div>
  1096. </div>
  1097. );
  1098. }
  1099. return null;
  1100. }
  1101. function RenderPlaceholderRow(props: {
  1102. index: number;
  1103. manager: VirtualizedViewManager;
  1104. node: TraceTreeNode<TraceTree.NodeValue>;
  1105. projects: Record<Project['slug'], Project['platform']>;
  1106. style: React.CSSProperties;
  1107. theme: Theme;
  1108. }) {
  1109. return (
  1110. <div
  1111. key={props.index}
  1112. className="TraceRow"
  1113. style={{
  1114. top: props.style.top,
  1115. height: props.style.height,
  1116. pointerEvents: 'none',
  1117. color: props.theme.subText,
  1118. paddingLeft: 8,
  1119. }}
  1120. >
  1121. <div
  1122. className="TraceLeftColumn"
  1123. style={{width: props.manager.columns.list.width * 100 + '%'}}
  1124. >
  1125. <div
  1126. className="TraceLeftColumnInner"
  1127. style={{
  1128. paddingLeft: props.node.depth * props.manager.row_depth_padding,
  1129. }}
  1130. >
  1131. <div
  1132. className={`TraceChildrenCountWrapper ${isTraceNode(props.node) ? 'Root' : ''}`}
  1133. >
  1134. <Connectors node={props.node} manager={props.manager} />
  1135. {props.node.children.length > 0 || props.node.canFetch ? (
  1136. <ChildrenButton
  1137. icon="+"
  1138. status={props.node.fetchStatus}
  1139. expanded={props.node.expanded || props.node.zoomedIn}
  1140. onClick={() => void 0}
  1141. >
  1142. {props.node.children.length > 0
  1143. ? COUNT_FORMATTER.format(props.node.children.length)
  1144. : null}
  1145. </ChildrenButton>
  1146. ) : null}
  1147. </div>
  1148. <Placeholder
  1149. className="Placeholder"
  1150. height="12px"
  1151. width={randomBetween(20, 80) + '%'}
  1152. style={{
  1153. transition: 'all 30s ease-out',
  1154. }}
  1155. />
  1156. </div>
  1157. </div>
  1158. <div
  1159. className={`TraceRightColumn ${props.index % 2 === 0 ? 0 : 'Odd'}`}
  1160. style={{
  1161. width: props.manager.columns.span_list.width * 100 + '%',
  1162. backgroundColor:
  1163. props.index % 2 === 0 ? props.theme.backgroundSecondary : undefined,
  1164. }}
  1165. >
  1166. <Placeholder
  1167. className="Placeholder"
  1168. height="12px"
  1169. width={randomBetween(20, 80) + '%'}
  1170. style={{
  1171. transition: 'all 30s ease-out',
  1172. transform: `translate(${randomBetween(0, 200) + 'px'}, 0)`,
  1173. }}
  1174. />
  1175. </div>
  1176. </div>
  1177. );
  1178. }
  1179. function randomBetween(min: number, max: number) {
  1180. return Math.floor(Math.random() * (max - min + 1) + min);
  1181. }
  1182. function Connectors(props: {
  1183. manager: VirtualizedViewManager;
  1184. node: TraceTreeNode<TraceTree.NodeValue>;
  1185. }) {
  1186. const showVerticalConnector =
  1187. ((props.node.expanded || props.node.zoomedIn) && props.node.children.length > 0) ||
  1188. (props.node.value && isParentAutogroupedNode(props.node));
  1189. // If the tail node of the collapsed node has no children,
  1190. // we don't want to render the vertical connector as no children
  1191. // are being rendered as the chain is entirely collapsed
  1192. const hideVerticalConnector =
  1193. showVerticalConnector &&
  1194. props.node.value &&
  1195. props.node instanceof ParentAutogroupNode &&
  1196. !props.node.tail.children.length;
  1197. return (
  1198. <Fragment>
  1199. {props.node.connectors.map((c, i) => {
  1200. return (
  1201. <div
  1202. key={i}
  1203. style={{
  1204. left: -(
  1205. Math.abs(Math.abs(c) - props.node.depth) * props.manager.row_depth_padding
  1206. ),
  1207. }}
  1208. className={`TraceVerticalConnector ${c < 0 ? 'Orphaned' : ''}`}
  1209. />
  1210. );
  1211. })}
  1212. {showVerticalConnector && !hideVerticalConnector ? (
  1213. <div className="TraceExpandedVerticalConnector" />
  1214. ) : null}
  1215. {props.node.isLastChild ? (
  1216. <div className="TraceVerticalLastChildConnector" />
  1217. ) : (
  1218. <div className="TraceVerticalConnector" />
  1219. )}
  1220. </Fragment>
  1221. );
  1222. }
  1223. function ChildrenButton(props: {
  1224. children: React.ReactNode;
  1225. expanded: boolean;
  1226. icon: React.ReactNode;
  1227. onClick: (e: React.MouseEvent) => void;
  1228. status: TraceTreeNode<any>['fetchStatus'] | undefined;
  1229. errored?: boolean;
  1230. }) {
  1231. return (
  1232. <button
  1233. className={`TraceChildrenCount ${props.errored ? 'Errored' : ''}`}
  1234. onClick={props.onClick}
  1235. >
  1236. <div className="TraceChildrenCountContent">{props.children}</div>
  1237. <div className="TraceChildrenCountAction">
  1238. {props.icon}
  1239. {props.status === 'loading' ? (
  1240. <LoadingIndicator className="TraceActionsLoadingIndicator" size={8} />
  1241. ) : null}
  1242. </div>
  1243. </button>
  1244. );
  1245. }
  1246. interface TraceBarProps {
  1247. color: string;
  1248. manager: VirtualizedViewManager;
  1249. node_space: [number, number] | null;
  1250. virtualized_index: number;
  1251. duration?: number;
  1252. }
  1253. function TraceBar(props: TraceBarProps) {
  1254. if (!props.node_space) {
  1255. return null;
  1256. }
  1257. const duration = getDuration(props.node_space[1] / 1000, 2, true);
  1258. const spanTransform = props.manager.computeSpanCSSMatrixTransform(props.node_space);
  1259. const [inside, textTransform] = props.manager.computeSpanTextPlacement(
  1260. props.node_space,
  1261. duration
  1262. );
  1263. return (
  1264. <Fragment>
  1265. <div
  1266. ref={r =>
  1267. props.manager.registerSpanBarRef(r, props.node_space!, props.virtualized_index)
  1268. }
  1269. className="TraceBar"
  1270. style={{
  1271. transform: `matrix(${spanTransform.join(',')})`,
  1272. backgroundColor: props.color,
  1273. }}
  1274. />
  1275. <div
  1276. ref={r =>
  1277. props.manager.registerSpanBarTextRef(
  1278. r,
  1279. duration,
  1280. props.node_space!,
  1281. props.virtualized_index
  1282. )
  1283. }
  1284. className="TraceBarDuration"
  1285. style={{
  1286. color: inside ? 'white' : '',
  1287. transform: `translate(${textTransform ?? 0}px, 0)`,
  1288. }}
  1289. >
  1290. {duration}
  1291. </div>
  1292. </Fragment>
  1293. );
  1294. }
  1295. interface InvisibleTraceBarProps {
  1296. children: React.ReactNode;
  1297. manager: VirtualizedViewManager;
  1298. node_space: [number, number] | null;
  1299. virtualizedIndex: number;
  1300. }
  1301. function InvisibleTraceBar(props: InvisibleTraceBarProps) {
  1302. if (!props.node_space || !props.children) {
  1303. return null;
  1304. }
  1305. const transform = `translateX(${props.manager.computeTransformXFromTimestamp(props.node_space[0])}px)`;
  1306. return (
  1307. <div
  1308. ref={r =>
  1309. props.manager.registerInvisibleBarRef(
  1310. r,
  1311. props.node_space!,
  1312. props.virtualizedIndex
  1313. )
  1314. }
  1315. className="TraceBar Invisible"
  1316. style={{
  1317. transform,
  1318. }}
  1319. onDoubleClick={e => {
  1320. e.stopPropagation();
  1321. props.manager.onZoomIntoSpace(props.node_space!);
  1322. }}
  1323. >
  1324. {props.children}
  1325. </div>
  1326. );
  1327. }
  1328. interface AutogroupedTraceBarProps {
  1329. color: string;
  1330. entire_space: [number, number] | null;
  1331. manager: VirtualizedViewManager;
  1332. node_spaces: [number, number][];
  1333. virtualized_index: number;
  1334. duration?: number;
  1335. }
  1336. function AutogroupedTraceBar(props: AutogroupedTraceBarProps) {
  1337. if (props.node_spaces && props.node_spaces.length <= 1) {
  1338. return (
  1339. <TraceBar
  1340. color={props.color}
  1341. node_space={props.entire_space}
  1342. manager={props.manager}
  1343. virtualized_index={props.virtualized_index}
  1344. duration={props.duration}
  1345. />
  1346. );
  1347. }
  1348. if (!props.node_spaces || !props.entire_space) {
  1349. return null;
  1350. }
  1351. const duration = getDuration(props.entire_space[1] / 1000, 2, true);
  1352. const spanTransform = props.manager.computeSpanCSSMatrixTransform(props.entire_space);
  1353. const [inside, textTransform] = props.manager.computeSpanTextPlacement(
  1354. props.entire_space,
  1355. duration
  1356. );
  1357. return (
  1358. <Fragment>
  1359. <div
  1360. ref={r =>
  1361. props.manager.registerSpanBarRef(
  1362. r,
  1363. props.entire_space!,
  1364. props.virtualized_index
  1365. )
  1366. }
  1367. className="TraceBar Invisible"
  1368. style={{
  1369. transform: `matrix(${spanTransform.join(',')})`,
  1370. backgroundColor: props.color,
  1371. }}
  1372. >
  1373. {props.node_spaces.map((node_space, i) => {
  1374. const width = node_space[1] / props.entire_space![1];
  1375. const left = (node_space[0] - props.entire_space![0]) / props.entire_space![1];
  1376. return (
  1377. <div
  1378. key={i}
  1379. className="TraceBar"
  1380. style={{
  1381. left: `${left * 1000}%`,
  1382. width: `${width * 100}%`,
  1383. backgroundColor: props.color,
  1384. }}
  1385. />
  1386. );
  1387. })}
  1388. </div>
  1389. <div
  1390. ref={r =>
  1391. props.manager.registerSpanBarTextRef(
  1392. r,
  1393. duration,
  1394. props.entire_space!,
  1395. props.virtualized_index
  1396. )
  1397. }
  1398. className="TraceBarDuration"
  1399. style={{
  1400. color: inside ? 'white' : '',
  1401. transform: `translate(${textTransform ?? 0}px, 0)`,
  1402. }}
  1403. >
  1404. {duration}
  1405. </div>
  1406. </Fragment>
  1407. );
  1408. }
  1409. /**
  1410. * This is a wrapper around the Trace component to apply styles
  1411. * to the trace tree. It exists because we _do not_ want to trigger
  1412. * emotion's css parsing logic as it is very slow and will cause
  1413. * the scrolling to flicker.
  1414. */
  1415. const TraceStylingWrapper = styled('div')`
  1416. flex-grow: 1;
  1417. width: 100%;
  1418. margin: auto;
  1419. overflow: hidden;
  1420. position: relative;
  1421. box-shadow: 0 0 0 1px ${p => p.theme.border};
  1422. border-radius: 4px;
  1423. padding-top: 22px;
  1424. &.WithIndicators {
  1425. padding-top: 44px;
  1426. &:before {
  1427. height: 44px;
  1428. }
  1429. .TraceIndicator.Timeline {
  1430. .TraceIndicatorLabel {
  1431. top: 26px;
  1432. }
  1433. .TraceIndicatorLine {
  1434. top: 30px;
  1435. }
  1436. }
  1437. }
  1438. &:before {
  1439. content: '';
  1440. position: absolute;
  1441. left: 0;
  1442. top: 0;
  1443. width: 100%;
  1444. height: 22px;
  1445. background-color: ${p => p.theme.backgroundSecondary};
  1446. border-bottom: 1px solid ${p => p.theme.border};
  1447. }
  1448. &.Loading {
  1449. .TraceRow {
  1450. .TraceLeftColumnInner {
  1451. width: 100%;
  1452. }
  1453. }
  1454. .TraceRightColumn {
  1455. background-color: transparent !important;
  1456. }
  1457. .TraceDivider {
  1458. pointer-events: none;
  1459. }
  1460. }
  1461. .TraceDivider {
  1462. position: absolute;
  1463. height: 100%;
  1464. background-color: transparent;
  1465. top: 0;
  1466. cursor: col-resize;
  1467. z-index: 10;
  1468. &:before {
  1469. content: '';
  1470. position: absolute;
  1471. width: 1px;
  1472. height: 100%;
  1473. background-color: ${p => p.theme.border};
  1474. left: 50%;
  1475. }
  1476. &:hover {
  1477. &:before {
  1478. background-color: ${p => p.theme.purple300};
  1479. }
  1480. }
  1481. }
  1482. .TraceIndicatorContainer {
  1483. overflow: hidden;
  1484. width: 100%;
  1485. height: 100%;
  1486. position: absolute;
  1487. right: 0;
  1488. top: 0;
  1489. }
  1490. .TraceIndicator {
  1491. z-index: 1;
  1492. width: 3px;
  1493. height: 100%;
  1494. top: 0;
  1495. position: absolute;
  1496. .TraceIndicatorLabel {
  1497. min-width: 34px;
  1498. text-align: center;
  1499. position: absolute;
  1500. font-size: ${p => p.theme.fontSizeExtraSmall};
  1501. font-weight: bold;
  1502. color: ${p => p.theme.textColor};
  1503. background-color: ${p => p.theme.background};
  1504. border-radius: ${p => p.theme.borderRadius};
  1505. border: 1px solid ${p => p.theme.border};
  1506. padding: 2px;
  1507. display: inline-block;
  1508. line-height: 1;
  1509. margin-top: 2px;
  1510. white-space: nowrap;
  1511. }
  1512. .TraceIndicatorLine {
  1513. width: 1px;
  1514. height: 100%;
  1515. top: 20px;
  1516. position: absolute;
  1517. left: 50%;
  1518. transform: translateX(-2px);
  1519. background: repeating-linear-gradient(
  1520. to bottom,
  1521. transparent 0 4px,
  1522. ${p => p.theme.textColor} 4px 8px
  1523. )
  1524. 80%/2px 100% no-repeat;
  1525. }
  1526. &.Timeline {
  1527. opacity: 1;
  1528. z-index: 1;
  1529. pointer-events: none;
  1530. .TraceIndicatorLabel {
  1531. font-weight: normal;
  1532. min-width: 0;
  1533. top: 2px;
  1534. width: auto;
  1535. border: none;
  1536. background-color: transparent;
  1537. color: ${p => p.theme.subText};
  1538. }
  1539. .TraceIndicatorLine {
  1540. background: ${p => p.theme.translucentGray100};
  1541. top: 4px;
  1542. }
  1543. }
  1544. }
  1545. .TraceRow {
  1546. display: flex;
  1547. align-items: center;
  1548. position: absolute;
  1549. height: 24px;
  1550. width: 100%;
  1551. transition: none;
  1552. font-size: ${p => p.theme.fontSizeSmall};
  1553. .Errored {
  1554. color: ${p => p.theme.error};
  1555. }
  1556. .TraceError {
  1557. position: absolute;
  1558. transform: translate(-50%, 0);
  1559. background: ${p => p.theme.background};
  1560. width: 16px !important;
  1561. height: 16px !important;
  1562. background-color: ${p => p.theme.error};
  1563. border-radius: 50%;
  1564. display: flex;
  1565. align-items: center;
  1566. justify-content: center;
  1567. svg {
  1568. fill: ${p => p.theme.white};
  1569. }
  1570. }
  1571. .TraceRightColumn.Odd {
  1572. background-color: ${p => p.theme.backgroundSecondary};
  1573. }
  1574. &:hover {
  1575. background-color: ${p => p.theme.backgroundSecondary};
  1576. }
  1577. &.Highlight,
  1578. &:focus {
  1579. outline: none;
  1580. background-color: ${p => p.theme.backgroundTertiary};
  1581. .TraceRightColumn.Odd {
  1582. background-color: transparent !important;
  1583. }
  1584. }
  1585. &:focus {
  1586. box-shadow: inset 0 0 0 1px ${p => p.theme.blue300} !important;
  1587. .TraceLeftColumn {
  1588. box-shadow: inset 0px 0 0px 1px ${p => p.theme.blue300} !important;
  1589. }
  1590. }
  1591. &.Highlight {
  1592. box-shadow: inset 0 0 0 1px ${p => p.theme.blue200} !important;
  1593. .TraceLeftColumn {
  1594. box-shadow: inset 0px 0 0px 1px ${p => p.theme.blue200} !important;
  1595. }
  1596. }
  1597. &.SearchResult {
  1598. background-color: ${p => p.theme.yellow100};
  1599. .TraceRightColumn {
  1600. background-color: transparent;
  1601. }
  1602. }
  1603. &.Autogrouped {
  1604. color: ${p => p.theme.blue300};
  1605. .TraceDescription {
  1606. font-weight: bold;
  1607. }
  1608. .TraceChildrenCountWrapper {
  1609. button {
  1610. color: ${p => p.theme.white};
  1611. background-color: ${p => p.theme.blue300};
  1612. }
  1613. }
  1614. }
  1615. }
  1616. .TraceLeftColumn {
  1617. height: 100%;
  1618. white-space: nowrap;
  1619. display: flex;
  1620. align-items: center;
  1621. overflow: hidden;
  1622. will-change: width;
  1623. box-shadow: inset 1px 0 0px 0px transparent;
  1624. .TraceLeftColumnInner {
  1625. height: 100%;
  1626. white-space: nowrap;
  1627. display: flex;
  1628. align-items: center;
  1629. will-change: transform;
  1630. transform-origin: left center;
  1631. img {
  1632. width: 16px;
  1633. height: 16px;
  1634. }
  1635. }
  1636. }
  1637. .TraceRightColumn {
  1638. height: 100%;
  1639. overflow: hidden;
  1640. position: relative;
  1641. display: flex;
  1642. align-items: center;
  1643. will-change: width;
  1644. z-index: 1;
  1645. &:hover {
  1646. .TraceArrow.Visible {
  1647. opacity: 1;
  1648. transition: 300ms 300ms ease-out;
  1649. pointer-events: auto;
  1650. }
  1651. }
  1652. }
  1653. .TraceBar {
  1654. position: absolute;
  1655. height: 64%;
  1656. width: 100%;
  1657. background-color: black;
  1658. transform-origin: left center;
  1659. &.Invisible {
  1660. background-color: transparent !important;
  1661. > div {
  1662. height: 100%;
  1663. }
  1664. }
  1665. }
  1666. .TraceArrow {
  1667. position: absolute;
  1668. pointer-events: none;
  1669. top: 0;
  1670. width: 14px;
  1671. height: 24px;
  1672. opacity: 0;
  1673. background-color: transparent;
  1674. border: none;
  1675. transition: 60ms ease-out;
  1676. font-size: ${p => p.theme.fontSizeMedium};
  1677. color: ${p => p.theme.subText};
  1678. padding: 0 2px;
  1679. display: flex;
  1680. align-items: center;
  1681. &.Left {
  1682. left: 0;
  1683. }
  1684. &.Right {
  1685. right: 0;
  1686. transform: rotate(180deg);
  1687. }
  1688. }
  1689. .TraceBarDuration {
  1690. display: inline-block;
  1691. transform-origin: left center;
  1692. font-size: ${p => p.theme.fontSizeExtraSmall};
  1693. color: ${p => p.theme.gray300};
  1694. white-space: nowrap;
  1695. font-variant-numeric: tabular-nums;
  1696. position: absolute;
  1697. transition: color 0.1s ease-in-out;
  1698. }
  1699. .TraceChildrenCount {
  1700. height: 16px;
  1701. white-space: nowrap;
  1702. min-width: 30px;
  1703. display: flex;
  1704. align-items: center;
  1705. justify-content: center;
  1706. border-radius: 99px;
  1707. padding: 0px 4px;
  1708. transition: all 0.15s ease-in-out;
  1709. background: ${p => p.theme.background};
  1710. border: 2px solid ${p => p.theme.border};
  1711. line-height: 0;
  1712. z-index: 1;
  1713. font-size: 10px;
  1714. box-shadow: ${p => p.theme.dropShadowLight};
  1715. margin-right: 8px;
  1716. &.Errored {
  1717. border: 2px solid ${p => p.theme.error};
  1718. }
  1719. .TraceChildrenCountContent {
  1720. + .TraceChildrenCountAction {
  1721. margin-left: 2px;
  1722. }
  1723. }
  1724. .TraceChildrenCountAction {
  1725. position: relative;
  1726. display: flex;
  1727. align-items: center;
  1728. justify-content: center;
  1729. }
  1730. .TraceActionsLoadingIndicator {
  1731. margin: 0;
  1732. position: absolute;
  1733. top: 50%;
  1734. left: 50%;
  1735. transform: translate(-50%, -50%);
  1736. background-color: ${p => p.theme.background};
  1737. animation: show 0.1s ease-in-out forwards;
  1738. @keyframes show {
  1739. from {
  1740. opacity: 0;
  1741. transform: translate(-50%, -50%) scale(0.86);
  1742. }
  1743. to {
  1744. opacity: 1;
  1745. transform: translate(-50%, -50%) scale(1);
  1746. }
  1747. }
  1748. .loading-indicator {
  1749. border-width: 2px;
  1750. }
  1751. .loading-message {
  1752. display: none;
  1753. }
  1754. }
  1755. svg {
  1756. width: 7px;
  1757. transition: none;
  1758. }
  1759. }
  1760. .TraceChildrenCountWrapper {
  1761. display: flex;
  1762. justify-content: flex-end;
  1763. align-items: center;
  1764. min-width: 44px;
  1765. height: 100%;
  1766. position: relative;
  1767. button {
  1768. transition: none;
  1769. }
  1770. &.Orphaned {
  1771. .TraceVerticalConnector,
  1772. .TraceVerticalLastChildConnector,
  1773. .TraceExpandedVerticalConnector {
  1774. border-left: 2px dashed ${p => p.theme.border};
  1775. }
  1776. &::before {
  1777. border-bottom: 2px dashed ${p => p.theme.border};
  1778. }
  1779. }
  1780. &.Root {
  1781. &:before,
  1782. .TraceVerticalLastChildConnector {
  1783. visibility: hidden;
  1784. }
  1785. }
  1786. &::before {
  1787. content: '';
  1788. display: block;
  1789. width: 50%;
  1790. height: 2px;
  1791. border-bottom: 2px solid ${p => p.theme.border};
  1792. position: absolute;
  1793. left: 0;
  1794. top: 50%;
  1795. transform: translateY(-50%);
  1796. }
  1797. &::after {
  1798. content: '';
  1799. background-color: ${p => p.theme.border};
  1800. border-radius: 50%;
  1801. height: 6px;
  1802. width: 6px;
  1803. position: absolute;
  1804. left: 50%;
  1805. top: 50%;
  1806. transform: translateY(-50%);
  1807. }
  1808. }
  1809. .TraceVerticalConnector {
  1810. position: absolute;
  1811. left: 0;
  1812. top: 0;
  1813. bottom: 0;
  1814. height: 100%;
  1815. width: 2px;
  1816. border-left: 2px solid ${p => p.theme.border};
  1817. &.Orphaned {
  1818. border-left: 2px dashed ${p => p.theme.border};
  1819. }
  1820. }
  1821. .TraceVerticalLastChildConnector {
  1822. position: absolute;
  1823. left: 0;
  1824. top: 0;
  1825. bottom: 0;
  1826. height: 50%;
  1827. width: 2px;
  1828. border-left: 2px solid ${p => p.theme.border};
  1829. border-bottom-left-radius: 4px;
  1830. }
  1831. .TraceExpandedVerticalConnector {
  1832. position: absolute;
  1833. bottom: 0;
  1834. height: 50%;
  1835. left: 50%;
  1836. width: 2px;
  1837. border-left: 2px solid ${p => p.theme.border};
  1838. }
  1839. .TraceOperation {
  1840. margin-left: 4px;
  1841. text-overflow: ellipsis;
  1842. white-space: nowrap;
  1843. font-weight: bold;
  1844. }
  1845. .TraceEmDash {
  1846. margin-left: 4px;
  1847. margin-right: 4px;
  1848. }
  1849. .TraceDescription {
  1850. white-space: nowrap;
  1851. }
  1852. `;
  1853. const LoadingContainer = styled('div')`
  1854. display: flex;
  1855. justify-content: center;
  1856. align-items: center;
  1857. flex-direction: column;
  1858. left: 50%;
  1859. top: 50%;
  1860. transform: translate(-50%, -50%);
  1861. position: absolute;
  1862. height: auto;
  1863. font-size: ${p => p.theme.fontSizeMedium};
  1864. color: ${p => p.theme.gray300};
  1865. z-index: 30;
  1866. padding: 24px;
  1867. background-color: ${p => p.theme.background};
  1868. border-radius: ${p => p.theme.borderRadius};
  1869. border: 1px solid ${p => p.theme.border};
  1870. `;
  1871. function TraceLoading() {
  1872. return (
  1873. <LoadingContainer>
  1874. <NoMarginIndicator size={24}>
  1875. <div>{t('Assembling the trace')}</div>
  1876. </NoMarginIndicator>
  1877. </LoadingContainer>
  1878. );
  1879. }
  1880. const NoMarginIndicator = styled(LoadingIndicator)`
  1881. margin: 0;
  1882. `;