spanBar.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995
  1. import 'intersection-observer'; // this is a polyfill
  2. import * as React from 'react';
  3. import styled from '@emotion/styled';
  4. import Count from 'app/components/count';
  5. import {ROW_HEIGHT} from 'app/components/performance/waterfall/constants';
  6. import {Row, RowCell, RowCellContainer} from 'app/components/performance/waterfall/row';
  7. import {DurationPill, RowRectangle} from 'app/components/performance/waterfall/rowBar';
  8. import {
  9. DividerContainer,
  10. DividerLine,
  11. DividerLineGhostContainer,
  12. ErrorBadge,
  13. } from 'app/components/performance/waterfall/rowDivider';
  14. import {
  15. RowTitle,
  16. RowTitleContainer,
  17. RowTitleContent,
  18. } from 'app/components/performance/waterfall/rowTitle';
  19. import {
  20. ConnectorBar,
  21. StyledIconChevron,
  22. TOGGLE_BORDER_BOX,
  23. TreeConnector,
  24. TreeToggle,
  25. TreeToggleContainer,
  26. } from 'app/components/performance/waterfall/treeConnector';
  27. import {
  28. getDurationDisplay,
  29. getHumanDuration,
  30. toPercent,
  31. } from 'app/components/performance/waterfall/utils';
  32. import Tooltip from 'app/components/tooltip';
  33. import {IconWarning} from 'app/icons';
  34. import {t} from 'app/locale';
  35. import space from 'app/styles/space';
  36. import {Organization} from 'app/types';
  37. import {EventTransaction} from 'app/types/event';
  38. import {defined} from 'app/utils';
  39. import * as QuickTraceContext from 'app/utils/performance/quickTrace/quickTraceContext';
  40. import {QuickTraceContextChildrenProps} from 'app/utils/performance/quickTrace/quickTraceContext';
  41. import {QuickTraceEvent, TraceError} from 'app/utils/performance/quickTrace/types';
  42. import {isTraceFull} from 'app/utils/performance/quickTrace/utils';
  43. import * as AnchorLinkManager from './anchorLinkManager';
  44. import {
  45. MINIMAP_CONTAINER_HEIGHT,
  46. MINIMAP_SPAN_BAR_HEIGHT,
  47. NUM_OF_SPANS_FIT_IN_MINI_MAP,
  48. } from './constants';
  49. import * as CursorGuideHandler from './cursorGuideHandler';
  50. import * as DividerHandlerManager from './dividerHandlerManager';
  51. import * as ScrollbarManager from './scrollbarManager';
  52. import SpanDetail from './spanDetail';
  53. import {ParsedTraceType, ProcessedSpanType, TreeDepthType} from './types';
  54. import {
  55. durationlessBrowserOps,
  56. getMeasurementBounds,
  57. getMeasurements,
  58. getSpanID,
  59. getSpanOperation,
  60. isEventFromBrowserJavaScriptSDK,
  61. isGapSpan,
  62. isOrphanSpan,
  63. isOrphanTreeDepth,
  64. SpanBoundsType,
  65. SpanGeneratedBoundsType,
  66. SpanViewBoundsType,
  67. unwrapTreeDepth,
  68. } from './utils';
  69. // TODO: maybe use babel-plugin-preval
  70. // for (let i = 0; i <= 1.0; i += 0.01) {
  71. // INTERSECTION_THRESHOLDS.push(i);
  72. // }
  73. const INTERSECTION_THRESHOLDS: Array<number> = [
  74. 0,
  75. 0.01,
  76. 0.02,
  77. 0.03,
  78. 0.04,
  79. 0.05,
  80. 0.06,
  81. 0.07,
  82. 0.08,
  83. 0.09,
  84. 0.1,
  85. 0.11,
  86. 0.12,
  87. 0.13,
  88. 0.14,
  89. 0.15,
  90. 0.16,
  91. 0.17,
  92. 0.18,
  93. 0.19,
  94. 0.2,
  95. 0.21,
  96. 0.22,
  97. 0.23,
  98. 0.24,
  99. 0.25,
  100. 0.26,
  101. 0.27,
  102. 0.28,
  103. 0.29,
  104. 0.3,
  105. 0.31,
  106. 0.32,
  107. 0.33,
  108. 0.34,
  109. 0.35,
  110. 0.36,
  111. 0.37,
  112. 0.38,
  113. 0.39,
  114. 0.4,
  115. 0.41,
  116. 0.42,
  117. 0.43,
  118. 0.44,
  119. 0.45,
  120. 0.46,
  121. 0.47,
  122. 0.48,
  123. 0.49,
  124. 0.5,
  125. 0.51,
  126. 0.52,
  127. 0.53,
  128. 0.54,
  129. 0.55,
  130. 0.56,
  131. 0.57,
  132. 0.58,
  133. 0.59,
  134. 0.6,
  135. 0.61,
  136. 0.62,
  137. 0.63,
  138. 0.64,
  139. 0.65,
  140. 0.66,
  141. 0.67,
  142. 0.68,
  143. 0.69,
  144. 0.7,
  145. 0.71,
  146. 0.72,
  147. 0.73,
  148. 0.74,
  149. 0.75,
  150. 0.76,
  151. 0.77,
  152. 0.78,
  153. 0.79,
  154. 0.8,
  155. 0.81,
  156. 0.82,
  157. 0.83,
  158. 0.84,
  159. 0.85,
  160. 0.86,
  161. 0.87,
  162. 0.88,
  163. 0.89,
  164. 0.9,
  165. 0.91,
  166. 0.92,
  167. 0.93,
  168. 0.94,
  169. 0.95,
  170. 0.96,
  171. 0.97,
  172. 0.98,
  173. 0.99,
  174. 1.0,
  175. ];
  176. const MARGIN_LEFT = 0;
  177. type SpanBarProps = {
  178. event: Readonly<EventTransaction>;
  179. organization: Organization;
  180. trace: Readonly<ParsedTraceType>;
  181. span: Readonly<ProcessedSpanType>;
  182. spanBarColour?: string;
  183. spanBarHatch?: boolean;
  184. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType;
  185. treeDepth: number;
  186. continuingTreeDepths: Array<TreeDepthType>;
  187. showSpanTree: boolean;
  188. numOfSpanChildren: number;
  189. spanNumber: number;
  190. isLast?: boolean;
  191. isRoot?: boolean;
  192. toggleSpanTree: () => void;
  193. isCurrentSpanFilteredOut: boolean;
  194. };
  195. type SpanBarState = {
  196. showDetail: boolean;
  197. };
  198. class SpanBar extends React.Component<SpanBarProps, SpanBarState> {
  199. state: SpanBarState = {
  200. showDetail: false,
  201. };
  202. componentDidMount() {
  203. this._mounted = true;
  204. if (this.spanRowDOMRef.current) {
  205. this.connectObservers();
  206. }
  207. }
  208. componentWillUnmount() {
  209. this._mounted = false;
  210. this.disconnectObservers();
  211. }
  212. spanRowDOMRef = React.createRef<HTMLDivElement>();
  213. intersectionObserver?: IntersectionObserver = void 0;
  214. zoomLevel: number = 1; // assume initial zoomLevel is 100%
  215. _mounted: boolean = false;
  216. toggleDisplayDetail = () => {
  217. this.setState(state => ({
  218. showDetail: !state.showDetail,
  219. }));
  220. };
  221. scrollIntoView = () => {
  222. const element = this.spanRowDOMRef.current;
  223. if (!element) {
  224. return;
  225. }
  226. const boundingRect = element.getBoundingClientRect();
  227. const offset = boundingRect.top + window.scrollY - MINIMAP_CONTAINER_HEIGHT;
  228. this.setState({showDetail: true}, () => window.scrollTo(0, offset));
  229. };
  230. renderDetail({
  231. isVisible,
  232. transactions,
  233. errors,
  234. }: {
  235. isVisible: boolean;
  236. transactions: QuickTraceEvent[] | null;
  237. errors: TraceError[] | null;
  238. }) {
  239. const {span, organization, isRoot, trace, event} = this.props;
  240. return (
  241. <AnchorLinkManager.Consumer>
  242. {({registerScrollFn, scrollToHash}) => {
  243. if (!isGapSpan(span)) {
  244. registerScrollFn(`#span-${span.span_id}`, this.scrollIntoView);
  245. }
  246. if (!this.state.showDetail || !isVisible) {
  247. return null;
  248. }
  249. return (
  250. <SpanDetail
  251. span={span}
  252. organization={organization}
  253. event={event}
  254. isRoot={!!isRoot}
  255. trace={trace}
  256. childTransactions={transactions}
  257. relatedErrors={errors}
  258. scrollToHash={scrollToHash}
  259. />
  260. );
  261. }}
  262. </AnchorLinkManager.Consumer>
  263. );
  264. }
  265. getBounds(): SpanViewBoundsType {
  266. const {event, span, generateBounds} = this.props;
  267. const bounds = generateBounds({
  268. startTimestamp: span.start_timestamp,
  269. endTimestamp: span.timestamp,
  270. });
  271. const shouldHideSpanWarnings = isEventFromBrowserJavaScriptSDK(event);
  272. switch (bounds.type) {
  273. case 'TRACE_TIMESTAMPS_EQUAL': {
  274. return {
  275. warning: t('Trace times are equal'),
  276. left: void 0,
  277. width: void 0,
  278. isSpanVisibleInView: bounds.isSpanVisibleInView,
  279. };
  280. }
  281. case 'INVALID_VIEW_WINDOW': {
  282. return {
  283. warning: t('Invalid view window'),
  284. left: void 0,
  285. width: void 0,
  286. isSpanVisibleInView: bounds.isSpanVisibleInView,
  287. };
  288. }
  289. case 'TIMESTAMPS_EQUAL': {
  290. const warning =
  291. shouldHideSpanWarnings &&
  292. 'op' in span &&
  293. span.op &&
  294. durationlessBrowserOps.includes(span.op)
  295. ? void 0
  296. : t('Equal start and end times');
  297. return {
  298. warning,
  299. left: bounds.start,
  300. width: 0.00001,
  301. isSpanVisibleInView: bounds.isSpanVisibleInView,
  302. };
  303. }
  304. case 'TIMESTAMPS_REVERSED': {
  305. return {
  306. warning: t('Reversed start and end times'),
  307. left: bounds.start,
  308. width: bounds.end - bounds.start,
  309. isSpanVisibleInView: bounds.isSpanVisibleInView,
  310. };
  311. }
  312. case 'TIMESTAMPS_STABLE': {
  313. return {
  314. warning: void 0,
  315. left: bounds.start,
  316. width: bounds.end - bounds.start,
  317. isSpanVisibleInView: bounds.isSpanVisibleInView,
  318. };
  319. }
  320. default: {
  321. const _exhaustiveCheck: never = bounds;
  322. return _exhaustiveCheck;
  323. }
  324. }
  325. }
  326. renderMeasurements() {
  327. const {event, generateBounds} = this.props;
  328. if (this.state.showDetail) {
  329. return null;
  330. }
  331. const measurements = getMeasurements(event);
  332. return (
  333. <React.Fragment>
  334. {Array.from(measurements).map(([timestamp, verticalMark]) => {
  335. const bounds = getMeasurementBounds(timestamp, generateBounds);
  336. const shouldDisplay = defined(bounds.left) && defined(bounds.width);
  337. if (!shouldDisplay || !bounds.isSpanVisibleInView) {
  338. return null;
  339. }
  340. return (
  341. <MeasurementMarker
  342. key={String(timestamp)}
  343. style={{
  344. left: `clamp(0%, ${toPercent(bounds.left || 0)}, calc(100% - 1px))`,
  345. }}
  346. failedThreshold={verticalMark.failedThreshold}
  347. />
  348. );
  349. })}
  350. </React.Fragment>
  351. );
  352. }
  353. renderSpanTreeConnector({hasToggler}: {hasToggler: boolean}) {
  354. const {
  355. isLast,
  356. isRoot,
  357. treeDepth: spanTreeDepth,
  358. continuingTreeDepths,
  359. span,
  360. showSpanTree,
  361. } = this.props;
  362. const spanID = getSpanID(span);
  363. if (isRoot) {
  364. if (hasToggler) {
  365. return (
  366. <ConnectorBar
  367. style={{right: '16px', height: '10px', bottom: '-5px', top: 'auto'}}
  368. key={`${spanID}-last`}
  369. orphanBranch={false}
  370. />
  371. );
  372. }
  373. return null;
  374. }
  375. const connectorBars: Array<React.ReactNode> = continuingTreeDepths.map(treeDepth => {
  376. const depth: number = unwrapTreeDepth(treeDepth);
  377. if (depth === 0) {
  378. // do not render a connector bar at depth 0,
  379. // if we did render a connector bar, this bar would be placed at depth -1
  380. // which does not exist.
  381. return null;
  382. }
  383. const left = ((spanTreeDepth - depth) * (TOGGLE_BORDER_BOX / 2) + 1) * -1;
  384. return (
  385. <ConnectorBar
  386. style={{left}}
  387. key={`${spanID}-${depth}`}
  388. orphanBranch={isOrphanTreeDepth(treeDepth)}
  389. />
  390. );
  391. });
  392. if (hasToggler && showSpanTree) {
  393. // if there is a toggle button, we add a connector bar to create an attachment
  394. // between the toggle button and any connector bars below the toggle button
  395. connectorBars.push(
  396. <ConnectorBar
  397. style={{
  398. right: '16px',
  399. height: '10px',
  400. bottom: isLast ? `-${ROW_HEIGHT / 2}px` : '0',
  401. top: 'auto',
  402. }}
  403. key={`${spanID}-last`}
  404. orphanBranch={false}
  405. />
  406. );
  407. }
  408. return (
  409. <TreeConnector
  410. isLast={isLast}
  411. hasToggler={hasToggler}
  412. orphanBranch={isOrphanSpan(span)}
  413. >
  414. {connectorBars}
  415. </TreeConnector>
  416. );
  417. }
  418. renderSpanTreeToggler({left, errored}: {left: number; errored: boolean}) {
  419. const {numOfSpanChildren, isRoot, showSpanTree} = this.props;
  420. const chevron = <StyledIconChevron direction={showSpanTree ? 'up' : 'down'} />;
  421. if (numOfSpanChildren <= 0) {
  422. return (
  423. <TreeToggleContainer style={{left: `${left}px`}}>
  424. {this.renderSpanTreeConnector({hasToggler: false})}
  425. </TreeToggleContainer>
  426. );
  427. }
  428. const chevronElement = !isRoot ? <div>{chevron}</div> : null;
  429. return (
  430. <TreeToggleContainer style={{left: `${left}px`}} hasToggler>
  431. {this.renderSpanTreeConnector({hasToggler: true})}
  432. <TreeToggle
  433. disabled={!!isRoot}
  434. isExpanded={showSpanTree}
  435. errored={errored}
  436. onClick={event => {
  437. event.stopPropagation();
  438. if (isRoot) {
  439. return;
  440. }
  441. this.props.toggleSpanTree();
  442. }}
  443. >
  444. <Count value={numOfSpanChildren} />
  445. {chevronElement}
  446. </TreeToggle>
  447. </TreeToggleContainer>
  448. );
  449. }
  450. renderTitle(
  451. scrollbarManagerChildrenProps: ScrollbarManager.ScrollbarManagerChildrenProps,
  452. errors: TraceError[] | null
  453. ) {
  454. const {generateContentSpanBarRef} = scrollbarManagerChildrenProps;
  455. const {span, treeDepth} = this.props;
  456. const operationName = getSpanOperation(span) ? (
  457. <strong>
  458. {getSpanOperation(span)}
  459. {' \u2014 '}
  460. </strong>
  461. ) : (
  462. ''
  463. );
  464. const description = span?.description ?? getSpanID(span);
  465. const left = treeDepth * (TOGGLE_BORDER_BOX / 2) + MARGIN_LEFT;
  466. const errored = Boolean(errors && errors.length > 0);
  467. return (
  468. <RowTitleContainer
  469. data-debug-id="SpanBarTitleContainer"
  470. ref={generateContentSpanBarRef()}
  471. >
  472. {this.renderSpanTreeToggler({left, errored})}
  473. <RowTitle
  474. style={{
  475. left: `${left}px`,
  476. width: '100%',
  477. }}
  478. >
  479. <RowTitleContent errored={errored}>
  480. {operationName}
  481. {description}
  482. </RowTitleContent>
  483. </RowTitle>
  484. </RowTitleContainer>
  485. );
  486. }
  487. connectObservers() {
  488. if (!this.spanRowDOMRef.current) {
  489. return;
  490. }
  491. this.disconnectObservers();
  492. /**
  493. We track intersections events between the span bar's DOM element
  494. and the viewport's (root) intersection area. the intersection area is sized to
  495. exclude the minimap. See below.
  496. By default, the intersection observer's root intersection is the viewport.
  497. We adjust the margins of this root intersection area to exclude the minimap's
  498. height. The minimap's height is always fixed.
  499. VIEWPORT (ancestor element used for the intersection events)
  500. +--+-------------------------+--+
  501. | | | |
  502. | | MINIMAP | |
  503. | | | |
  504. | +-------------------------+ | ^
  505. | | | | |
  506. | | SPANS | | | ROOT
  507. | | | | | INTERSECTION
  508. | | | | | OBSERVER
  509. | | | | | HEIGHT
  510. | | | | |
  511. | | | | |
  512. | | | | |
  513. | +-------------------------+ | |
  514. | | |
  515. +-------------------------------+ v
  516. */
  517. this.intersectionObserver = new IntersectionObserver(
  518. entries => {
  519. entries.forEach(entry => {
  520. if (!this._mounted) {
  521. return;
  522. }
  523. const shouldMoveMinimap =
  524. this.props.trace.numOfSpans > NUM_OF_SPANS_FIT_IN_MINI_MAP;
  525. if (!shouldMoveMinimap) {
  526. return;
  527. }
  528. const spanNumber = this.props.spanNumber;
  529. const minimapSlider = document.getElementById('minimap-background-slider');
  530. if (!minimapSlider) {
  531. return;
  532. }
  533. // NOTE: THIS IS HACKY.
  534. //
  535. // IntersectionObserver.rootMargin is un-affected by the browser's zoom level.
  536. // The margins of the intersection area needs to be adjusted.
  537. // Thus, IntersectionObserverEntry.rootBounds may not be what we expect.
  538. //
  539. // We address this below.
  540. //
  541. // Note that this function was called whenever an intersection event occurred wrt
  542. // the thresholds.
  543. //
  544. if (entry.rootBounds) {
  545. // After we create the IntersectionObserver instance with rootMargin set as:
  546. // -${MINIMAP_CONTAINER_HEIGHT * this.zoomLevel}px 0px 0px 0px
  547. //
  548. // we can introspect the rootBounds to infer the zoomlevel.
  549. //
  550. // we always expect entry.rootBounds.top to equal MINIMAP_CONTAINER_HEIGHT
  551. const actualRootTop = Math.ceil(entry.rootBounds.top);
  552. if (actualRootTop !== MINIMAP_CONTAINER_HEIGHT && actualRootTop > 0) {
  553. // we revert the actualRootTop value by the current zoomLevel factor
  554. const normalizedActualTop = actualRootTop / this.zoomLevel;
  555. const zoomLevel = MINIMAP_CONTAINER_HEIGHT / normalizedActualTop;
  556. this.zoomLevel = zoomLevel;
  557. // we reconnect the observers; the callback functions may be invoked
  558. this.connectObservers();
  559. // NOTE: since we cannot guarantee that the callback function is invoked on
  560. // the newly connected observers, we continue running this function.
  561. }
  562. }
  563. // root refers to the root intersection rectangle used for the IntersectionObserver
  564. const rectRelativeToRoot = entry.boundingClientRect as DOMRect;
  565. const bottomYCoord = rectRelativeToRoot.y + rectRelativeToRoot.height;
  566. // refers to if the rect is out of view from the viewport
  567. const isOutOfViewAbove = rectRelativeToRoot.y < 0 && bottomYCoord < 0;
  568. if (isOutOfViewAbove) {
  569. return;
  570. }
  571. const relativeToMinimap = {
  572. top: rectRelativeToRoot.y - MINIMAP_CONTAINER_HEIGHT,
  573. bottom: bottomYCoord - MINIMAP_CONTAINER_HEIGHT,
  574. };
  575. const rectBelowMinimap =
  576. relativeToMinimap.top > 0 && relativeToMinimap.bottom > 0;
  577. if (rectBelowMinimap) {
  578. // if the first span is below the minimap, we scroll the minimap
  579. // to the top. this addresses spurious scrolling to the top of the page
  580. if (spanNumber <= 1) {
  581. minimapSlider.style.top = '0px';
  582. return;
  583. }
  584. return;
  585. }
  586. const inAndAboveMinimap = relativeToMinimap.bottom <= 0;
  587. if (inAndAboveMinimap) {
  588. return;
  589. }
  590. // invariant: spanNumber >= 1
  591. const numberOfMovedSpans = spanNumber - 1;
  592. const totalHeightOfHiddenSpans = numberOfMovedSpans * MINIMAP_SPAN_BAR_HEIGHT;
  593. const currentSpanHiddenRatio = 1 - entry.intersectionRatio;
  594. const panYPixels =
  595. totalHeightOfHiddenSpans + currentSpanHiddenRatio * MINIMAP_SPAN_BAR_HEIGHT;
  596. // invariant: this.props.trace.numOfSpansend - spanNumberToStopMoving + 1 = NUM_OF_SPANS_FIT_IN_MINI_MAP
  597. const spanNumberToStopMoving =
  598. this.props.trace.numOfSpans + 1 - NUM_OF_SPANS_FIT_IN_MINI_MAP;
  599. if (spanNumber > spanNumberToStopMoving) {
  600. // if the last span bar appears on the minimap, we do not want the minimap
  601. // to keep panning upwards
  602. minimapSlider.style.top = `-${
  603. spanNumberToStopMoving * MINIMAP_SPAN_BAR_HEIGHT
  604. }px`;
  605. return;
  606. }
  607. minimapSlider.style.top = `-${panYPixels}px`;
  608. });
  609. },
  610. {
  611. threshold: INTERSECTION_THRESHOLDS,
  612. rootMargin: `-${MINIMAP_CONTAINER_HEIGHT * this.zoomLevel}px 0px 0px 0px`,
  613. }
  614. );
  615. this.intersectionObserver.observe(this.spanRowDOMRef.current);
  616. }
  617. disconnectObservers() {
  618. if (this.intersectionObserver) {
  619. this.intersectionObserver.disconnect();
  620. }
  621. }
  622. renderCursorGuide() {
  623. return (
  624. <CursorGuideHandler.Consumer>
  625. {({
  626. showCursorGuide,
  627. traceViewMouseLeft,
  628. }: {
  629. showCursorGuide: boolean;
  630. traceViewMouseLeft: number | undefined;
  631. }) => {
  632. if (!showCursorGuide || !traceViewMouseLeft) {
  633. return null;
  634. }
  635. return (
  636. <CursorGuide
  637. style={{
  638. left: toPercent(traceViewMouseLeft),
  639. }}
  640. />
  641. );
  642. }}
  643. </CursorGuideHandler.Consumer>
  644. );
  645. }
  646. renderDivider(
  647. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
  648. ) {
  649. if (this.state.showDetail) {
  650. // Mock component to preserve layout spacing
  651. return (
  652. <DividerLine
  653. showDetail
  654. style={{
  655. position: 'absolute',
  656. }}
  657. />
  658. );
  659. }
  660. const {addDividerLineRef} = dividerHandlerChildrenProps;
  661. return (
  662. <DividerLine
  663. ref={addDividerLineRef()}
  664. style={{
  665. position: 'absolute',
  666. }}
  667. onMouseEnter={() => {
  668. dividerHandlerChildrenProps.setHover(true);
  669. }}
  670. onMouseLeave={() => {
  671. dividerHandlerChildrenProps.setHover(false);
  672. }}
  673. onMouseOver={() => {
  674. dividerHandlerChildrenProps.setHover(true);
  675. }}
  676. onMouseDown={dividerHandlerChildrenProps.onDragStart}
  677. onClick={event => {
  678. // we prevent the propagation of the clicks from this component to prevent
  679. // the span detail from being opened.
  680. event.stopPropagation();
  681. }}
  682. />
  683. );
  684. }
  685. getRelatedErrors(quickTrace: QuickTraceContextChildrenProps): TraceError[] | null {
  686. if (!quickTrace) {
  687. return null;
  688. }
  689. const {span} = this.props;
  690. const {currentEvent} = quickTrace;
  691. if (isGapSpan(span) || !currentEvent || !isTraceFull(currentEvent)) {
  692. return null;
  693. }
  694. return currentEvent.errors.filter(error => error.span === span.span_id);
  695. }
  696. getChildTransactions(
  697. quickTrace: QuickTraceContextChildrenProps
  698. ): QuickTraceEvent[] | null {
  699. if (!quickTrace) {
  700. return null;
  701. }
  702. const {span} = this.props;
  703. const {trace} = quickTrace;
  704. if (isGapSpan(span) || !trace) {
  705. return null;
  706. }
  707. return trace.filter(({parent_span_id}) => parent_span_id === span.span_id);
  708. }
  709. renderErrorBadge(errors: TraceError[] | null): React.ReactNode {
  710. return errors?.length ? <ErrorBadge /> : null;
  711. }
  712. renderWarningText({warningText}: {warningText?: string} = {}) {
  713. if (!warningText) {
  714. return null;
  715. }
  716. return (
  717. <Tooltip containerDisplayMode="flex" title={warningText}>
  718. <StyledIconWarning size="xs" />
  719. </Tooltip>
  720. );
  721. }
  722. renderHeader({
  723. scrollbarManagerChildrenProps,
  724. dividerHandlerChildrenProps,
  725. errors,
  726. }: {
  727. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps;
  728. scrollbarManagerChildrenProps: ScrollbarManager.ScrollbarManagerChildrenProps;
  729. errors: TraceError[] | null;
  730. }) {
  731. const {span, spanBarColour, spanBarHatch, spanNumber} = this.props;
  732. const startTimestamp: number = span.start_timestamp;
  733. const endTimestamp: number = span.timestamp;
  734. const duration = Math.abs(endTimestamp - startTimestamp);
  735. const durationString = getHumanDuration(duration);
  736. const bounds = this.getBounds();
  737. const {dividerPosition, addGhostDividerLineRef} = dividerHandlerChildrenProps;
  738. const displaySpanBar = defined(bounds.left) && defined(bounds.width);
  739. const durationDisplay = getDurationDisplay(bounds);
  740. return (
  741. <RowCellContainer showDetail={this.state.showDetail}>
  742. <RowCell
  743. data-type="span-row-cell"
  744. showDetail={this.state.showDetail}
  745. style={{
  746. width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
  747. paddingTop: 0,
  748. }}
  749. onClick={() => {
  750. this.toggleDisplayDetail();
  751. }}
  752. >
  753. {this.renderTitle(scrollbarManagerChildrenProps, errors)}
  754. </RowCell>
  755. <DividerContainer>
  756. {this.renderDivider(dividerHandlerChildrenProps)}
  757. {this.renderErrorBadge(errors)}
  758. </DividerContainer>
  759. <RowCell
  760. data-type="span-row-cell"
  761. showDetail={this.state.showDetail}
  762. showStriping={spanNumber % 2 !== 0}
  763. style={{
  764. width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
  765. }}
  766. onClick={() => {
  767. this.toggleDisplayDetail();
  768. }}
  769. >
  770. {displaySpanBar && (
  771. <RowRectangle
  772. spanBarHatch={!!spanBarHatch}
  773. style={{
  774. backgroundColor: spanBarColour,
  775. left: `clamp(0%, ${toPercent(bounds.left || 0)}, calc(100% - 1px))`,
  776. width: toPercent(bounds.width || 0),
  777. }}
  778. >
  779. <DurationPill
  780. durationDisplay={durationDisplay}
  781. showDetail={this.state.showDetail}
  782. spanBarHatch={!!spanBarHatch}
  783. >
  784. {durationString}
  785. {this.renderWarningText({warningText: bounds.warning})}
  786. </DurationPill>
  787. </RowRectangle>
  788. )}
  789. {this.renderMeasurements()}
  790. {this.renderCursorGuide()}
  791. </RowCell>
  792. {!this.state.showDetail && (
  793. <DividerLineGhostContainer
  794. style={{
  795. width: `calc(${toPercent(dividerPosition)} + 0.5px)`,
  796. display: 'none',
  797. }}
  798. >
  799. <DividerLine
  800. ref={addGhostDividerLineRef()}
  801. style={{
  802. right: 0,
  803. }}
  804. className="hovering"
  805. onClick={event => {
  806. // the ghost divider line should not be interactive.
  807. // we prevent the propagation of the clicks from this component to prevent
  808. // the span detail from being opened.
  809. event.stopPropagation();
  810. }}
  811. />
  812. </DividerLineGhostContainer>
  813. )}
  814. </RowCellContainer>
  815. );
  816. }
  817. render() {
  818. const {isCurrentSpanFilteredOut} = this.props;
  819. const bounds = this.getBounds();
  820. const isSpanVisibleInView = bounds.isSpanVisibleInView;
  821. const isSpanVisible = isSpanVisibleInView && !isCurrentSpanFilteredOut;
  822. return (
  823. <Row
  824. ref={this.spanRowDOMRef}
  825. visible={isSpanVisible}
  826. showBorder={this.state.showDetail}
  827. data-test-id="span-row"
  828. >
  829. <QuickTraceContext.Consumer>
  830. {quickTrace => {
  831. const errors = this.getRelatedErrors(quickTrace);
  832. const transactions = this.getChildTransactions(quickTrace);
  833. return (
  834. <React.Fragment>
  835. <ScrollbarManager.Consumer>
  836. {scrollbarManagerChildrenProps => (
  837. <DividerHandlerManager.Consumer>
  838. {(
  839. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
  840. ) =>
  841. this.renderHeader({
  842. dividerHandlerChildrenProps,
  843. scrollbarManagerChildrenProps,
  844. errors,
  845. })
  846. }
  847. </DividerHandlerManager.Consumer>
  848. )}
  849. </ScrollbarManager.Consumer>
  850. {this.renderDetail({isVisible: isSpanVisible, transactions, errors})}
  851. </React.Fragment>
  852. );
  853. }}
  854. </QuickTraceContext.Consumer>
  855. </Row>
  856. );
  857. }
  858. }
  859. const CursorGuide = styled('div')`
  860. position: absolute;
  861. top: 0;
  862. width: 1px;
  863. background-color: ${p => p.theme.red300};
  864. transform: translateX(-50%);
  865. height: 100%;
  866. `;
  867. const MeasurementMarker = styled('div')<{failedThreshold: boolean}>`
  868. position: absolute;
  869. top: 0;
  870. height: ${ROW_HEIGHT}px;
  871. user-select: none;
  872. width: 1px;
  873. background: repeating-linear-gradient(
  874. to bottom,
  875. transparent 0 4px,
  876. ${p => (p.failedThreshold ? p.theme.red300 : 'black')} 4px 8px
  877. )
  878. 80%/2px 100% no-repeat;
  879. z-index: ${p => p.theme.zIndex.traceView.dividerLine};
  880. color: ${p => p.theme.textColor};
  881. `;
  882. const StyledIconWarning = styled(IconWarning)`
  883. margin-left: ${space(0.25)};
  884. margin-bottom: ${space(0.25)};
  885. `;
  886. export default SpanBar;