trace.tsx 62 KB

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