trace.tsx 61 KB

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