newTraceDetailsSpanBar.tsx 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042
  1. import 'intersection-observer'; // this is a polyfill
  2. import {Component, createRef, Fragment} from 'react';
  3. import styled from '@emotion/styled';
  4. import {withProfiler} from '@sentry/react';
  5. import type {Location} from 'history';
  6. import Count from 'sentry/components/count';
  7. import {
  8. FREQUENCY_BOX_WIDTH,
  9. SpanFrequencyBox,
  10. } from 'sentry/components/events/interfaces/spans/spanFrequencyBox';
  11. import {ROW_HEIGHT} from 'sentry/components/performance/waterfall/constants';
  12. import {MessageRow} from 'sentry/components/performance/waterfall/messageRow';
  13. import {
  14. Row,
  15. RowCell,
  16. RowCellContainer,
  17. } from 'sentry/components/performance/waterfall/row';
  18. import {DurationPill, RowRectangle} from 'sentry/components/performance/waterfall/rowBar';
  19. import {
  20. DividerContainer,
  21. DividerLine,
  22. DividerLineGhostContainer,
  23. ErrorBadge,
  24. MetricsBadge,
  25. ProfileBadge,
  26. } from 'sentry/components/performance/waterfall/rowDivider';
  27. import {
  28. RowTitle,
  29. RowTitleContainer,
  30. RowTitleContent,
  31. } from 'sentry/components/performance/waterfall/rowTitle';
  32. import {
  33. ConnectorBar,
  34. TOGGLE_BORDER_BOX,
  35. TreeConnector,
  36. TreeToggle,
  37. TreeToggleContainer,
  38. TreeToggleIcon,
  39. } from 'sentry/components/performance/waterfall/treeConnector';
  40. import {
  41. getDurationDisplay,
  42. getHumanDuration,
  43. lightenBarColor,
  44. } from 'sentry/components/performance/waterfall/utils';
  45. import {Tooltip} from 'sentry/components/tooltip';
  46. import {IconWarning} from 'sentry/icons';
  47. import {t} from 'sentry/locale';
  48. import {space} from 'sentry/styles/space';
  49. import type {EventTransaction} from 'sentry/types/event';
  50. import {EventOrGroupType} from 'sentry/types/event';
  51. import {defined} from 'sentry/utils';
  52. import {trackAnalytics} from 'sentry/utils/analytics';
  53. import {browserHistory} from 'sentry/utils/browserHistory';
  54. import {generateEventSlug} from 'sentry/utils/discover/urls';
  55. import {hasMetricsExperimentalFeature} from 'sentry/utils/metrics/features';
  56. import toPercent from 'sentry/utils/number/toPercent';
  57. import type {QuickTraceContextChildrenProps} from 'sentry/utils/performance/quickTrace/quickTraceContext';
  58. import type {
  59. QuickTraceEvent,
  60. TraceErrorOrIssue,
  61. TraceFull,
  62. } from 'sentry/utils/performance/quickTrace/types';
  63. import {isTraceTransaction} from 'sentry/utils/performance/quickTrace/utils';
  64. import {PerformanceInteraction} from 'sentry/utils/performanceForSentry';
  65. import {decodeScalar} from 'sentry/utils/queryString';
  66. import {StyledZoomIcon} from 'sentry/views/performance/traceDetails/newTraceDetailsTransactionBar';
  67. import {ProfileContext} from 'sentry/views/profiling/profilesProvider';
  68. import * as DividerHandlerManager from './dividerHandlerManager';
  69. import type {SpanDetailProps} from './newTraceDetailsSpanDetails';
  70. import {withScrollbarManager} from './scrollbarManager';
  71. import type {SpanBarProps} from './spanBar';
  72. import SpanBarCursorGuide from './spanBarCursorGuide';
  73. import {MeasurementMarker} from './styles';
  74. import type {AggregateSpanType, GapSpanType, ProcessedSpanType} from './types';
  75. import {GroupType} from './types';
  76. import type {SpanGeneratedBoundsType, SpanViewBoundsType, VerticalMark} from './utils';
  77. import {
  78. durationlessBrowserOps,
  79. formatSpanTreeLabel,
  80. getMeasurementBounds,
  81. getMeasurements,
  82. getSpanID,
  83. getSpanOperation,
  84. getSpanSubTimings,
  85. isEventFromBrowserJavaScriptSDK,
  86. isGapSpan,
  87. isOrphanSpan,
  88. isOrphanTreeDepth,
  89. parseTraceDetailsURLHash,
  90. shouldLimitAffectedToTiming,
  91. spanTargetHash,
  92. transactionTargetHash,
  93. unwrapTreeDepth,
  94. } from './utils';
  95. export const MARGIN_LEFT = 0;
  96. const SPAN_BAR_HEIGHT = 24;
  97. export type NewTraceDetailsSpanBarProps = SpanBarProps & {
  98. location: Location;
  99. quickTrace: QuickTraceContextChildrenProps;
  100. measurements?: Map<number, VerticalMark>;
  101. onRowClick?: (detailKey: SpanDetailProps | undefined) => void;
  102. };
  103. type State = {
  104. isIntersecting: boolean;
  105. };
  106. export class NewTraceDetailsSpanBar extends Component<
  107. NewTraceDetailsSpanBarProps,
  108. State
  109. > {
  110. state = {
  111. isIntersecting: false,
  112. };
  113. componentDidMount() {
  114. const {didAnchoredSpanMount, markAnchoredSpanIsMounted} = this.props;
  115. this._mounted = true;
  116. this.updateHighlightedState();
  117. this.connectObservers();
  118. // If span is anchored scroll to span bar and open its detail panel
  119. if (this.isHighlighted && this.props.onRowClick) {
  120. this.props.onRowClick(undefined);
  121. // Needs a little delay after bar is rendered, to achieve
  122. // scrollto bar functionality for spans that exist much further down the
  123. // react virtualized list.
  124. if (!didAnchoredSpanMount()) {
  125. setTimeout(() => {
  126. this.scrollIntoView();
  127. }, 100);
  128. }
  129. markAnchoredSpanIsMounted?.();
  130. }
  131. if (this.spanRowDOMRef.current) {
  132. this.props.storeSpanBar(this);
  133. }
  134. if (this.spanTitleRef.current) {
  135. this.spanTitleRef.current.addEventListener('wheel', this.handleWheel, {
  136. passive: false,
  137. });
  138. }
  139. // On mount, it is necessary to set the left styling of the content here due to the span tree being virtualized.
  140. // If we rely on the scrollBarManager to set the styling, it happens too late and awkwardly applies an animation.
  141. if (this.spanContentRef) {
  142. this.props.addContentSpanBarRef(this.spanContentRef);
  143. const left = -this.props.getCurrentLeftPos();
  144. this.spanContentRef.style.transform = `translateX(${left}px)`;
  145. this.spanContentRef.style.transformOrigin = 'left';
  146. }
  147. const {span} = this.props;
  148. if (isGapSpan(span)) {
  149. return;
  150. }
  151. }
  152. componentDidUpdate(prevProps: Readonly<NewTraceDetailsSpanBarProps>): void {
  153. if (this.props.location.query !== prevProps.location.query) {
  154. this.updateHighlightedState();
  155. }
  156. if (this.props.quickTrace !== prevProps.quickTrace) {
  157. const relatedErrors = this.getRelatedErrors(this.props.quickTrace);
  158. if (
  159. this.isHighlighted &&
  160. this.props.onRowClick &&
  161. relatedErrors &&
  162. relatedErrors.length > 0
  163. ) {
  164. this.props.onRowClick(undefined);
  165. }
  166. }
  167. }
  168. componentWillUnmount() {
  169. this._mounted = false;
  170. this.disconnectObservers();
  171. if (this.spanTitleRef.current) {
  172. this.spanTitleRef.current.removeEventListener('wheel', this.handleWheel);
  173. }
  174. const {span} = this.props;
  175. if (isGapSpan(span)) {
  176. return;
  177. }
  178. this.props.removeContentSpanBarRef(this.spanContentRef);
  179. }
  180. spanRowDOMRef = createRef<HTMLDivElement>();
  181. spanTitleRef = createRef<HTMLDivElement>();
  182. spanContentRef: HTMLDivElement | null = null;
  183. intersectionObserver?: IntersectionObserver = void 0;
  184. zoomLevel: number = 1; // assume initial zoomLevel is 100%
  185. _mounted: boolean = false;
  186. hashSpanId: string | undefined = undefined;
  187. isHighlighted: boolean = false;
  188. updateHighlightedState = () => {
  189. const hashValues = parseTraceDetailsURLHash(this.props.location.hash);
  190. this.hashSpanId = hashValues?.spanId;
  191. this.isHighlighted = !!(
  192. !isGapSpan(this.props.span) &&
  193. this.hashSpanId &&
  194. this.hashSpanId === this.props.span.span_id
  195. );
  196. // TODO Abdullah Khan: Converting the component to a functional component will help us get rid
  197. // of the forcedUpdate.
  198. this.forceUpdate();
  199. };
  200. handleWheel = (event: WheelEvent) => {
  201. // https://stackoverflow.com/q/57358640
  202. // https://github.com/facebook/react/issues/14856
  203. if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) {
  204. return;
  205. }
  206. event.preventDefault();
  207. event.stopPropagation();
  208. if (Math.abs(event.deltaY) === Math.abs(event.deltaX)) {
  209. return;
  210. }
  211. const {onWheel} = this.props;
  212. onWheel(event.deltaX);
  213. };
  214. scrollIntoView = () => {
  215. const element = this.spanRowDOMRef.current;
  216. if (!element) {
  217. return;
  218. }
  219. const boundingRect = element.getBoundingClientRect();
  220. const offset = boundingRect.top + window.scrollY - 40;
  221. window.scrollTo(0, offset);
  222. };
  223. getBounds(bounds?: SpanGeneratedBoundsType): SpanViewBoundsType {
  224. const {event, span, generateBounds} = this.props;
  225. bounds ??= generateBounds({
  226. startTimestamp: span.start_timestamp,
  227. endTimestamp: span.timestamp,
  228. });
  229. const shouldHideSpanWarnings = isEventFromBrowserJavaScriptSDK(event);
  230. switch (bounds.type) {
  231. case 'TRACE_TIMESTAMPS_EQUAL': {
  232. return {
  233. warning: t('Trace times are equal'),
  234. left: void 0,
  235. width: void 0,
  236. isSpanVisibleInView: bounds.isSpanVisibleInView,
  237. };
  238. }
  239. case 'INVALID_VIEW_WINDOW': {
  240. return {
  241. warning: t('Invalid view window'),
  242. left: void 0,
  243. width: void 0,
  244. isSpanVisibleInView: bounds.isSpanVisibleInView,
  245. };
  246. }
  247. case 'TIMESTAMPS_EQUAL': {
  248. const warning =
  249. shouldHideSpanWarnings &&
  250. 'op' in span &&
  251. span.op &&
  252. durationlessBrowserOps.includes(span.op)
  253. ? void 0
  254. : t('Equal start and end times');
  255. return {
  256. warning,
  257. left: bounds.start,
  258. width: 0.00001,
  259. isSpanVisibleInView: bounds.isSpanVisibleInView,
  260. };
  261. }
  262. case 'TIMESTAMPS_REVERSED': {
  263. return {
  264. warning: t('Reversed start and end times'),
  265. left: bounds.start,
  266. width: bounds.end - bounds.start,
  267. isSpanVisibleInView: bounds.isSpanVisibleInView,
  268. };
  269. }
  270. case 'TIMESTAMPS_STABLE': {
  271. return {
  272. warning: void 0,
  273. left: bounds.start,
  274. width: bounds.end - bounds.start,
  275. isSpanVisibleInView: bounds.isSpanVisibleInView,
  276. };
  277. }
  278. default: {
  279. const _exhaustiveCheck: never = bounds;
  280. return _exhaustiveCheck;
  281. }
  282. }
  283. }
  284. renderMeasurements() {
  285. const {event, generateBounds, measurements} = this.props;
  286. if (this.isHighlighted) {
  287. return null;
  288. }
  289. const spanMeasurements = measurements ?? getMeasurements(event, generateBounds);
  290. return (
  291. <Fragment>
  292. {Array.from(spanMeasurements.values()).map(verticalMark => {
  293. const mark = Object.values(verticalMark.marks)[0];
  294. const {timestamp} = mark;
  295. const bounds = getMeasurementBounds(timestamp, generateBounds);
  296. const shouldDisplay = defined(bounds.left) && defined(bounds.width);
  297. if (!shouldDisplay || !bounds.isSpanVisibleInView) {
  298. return null;
  299. }
  300. return (
  301. <MeasurementMarker
  302. key={String(timestamp)}
  303. style={{
  304. left: `clamp(0%, ${toPercent(bounds.left || 0)}, calc(100% - 1px))`,
  305. }}
  306. failedThreshold={verticalMark.failedThreshold}
  307. />
  308. );
  309. })}
  310. </Fragment>
  311. );
  312. }
  313. renderSpanTreeConnector({hasToggler}: {hasToggler: boolean}) {
  314. const {
  315. isLast,
  316. isRoot,
  317. treeDepth: spanTreeDepth,
  318. continuingTreeDepths,
  319. span,
  320. showSpanTree,
  321. } = this.props;
  322. const spanID = getSpanID(span);
  323. if (isRoot) {
  324. if (hasToggler) {
  325. return (
  326. <ConnectorBar
  327. style={{right: '15px', height: '10px', bottom: '-5px', top: 'auto'}}
  328. key={`${spanID}-last`}
  329. orphanBranch={false}
  330. />
  331. );
  332. }
  333. return null;
  334. }
  335. const connectorBars: Array<React.ReactNode> = continuingTreeDepths.map(treeDepth => {
  336. const depth: number = unwrapTreeDepth(treeDepth);
  337. if (depth === 0) {
  338. // do not render a connector bar at depth 0,
  339. // if we did render a connector bar, this bar would be placed at depth -1
  340. // which does not exist.
  341. return null;
  342. }
  343. const left = ((spanTreeDepth - depth) * (TOGGLE_BORDER_BOX / 2) + 2) * -1;
  344. return (
  345. <ConnectorBar
  346. style={{left}}
  347. key={`${spanID}-${depth}`}
  348. orphanBranch={isOrphanTreeDepth(treeDepth)}
  349. />
  350. );
  351. });
  352. if (hasToggler && showSpanTree) {
  353. // if there is a toggle button, we add a connector bar to create an attachment
  354. // between the toggle button and any connector bars below the toggle button
  355. connectorBars.push(
  356. <ConnectorBar
  357. style={{
  358. right: '15px',
  359. height: `${ROW_HEIGHT / 2}px`,
  360. bottom: isLast ? `-${ROW_HEIGHT / 2 + 2}px` : '0',
  361. top: 'auto',
  362. }}
  363. key={`${spanID}-last-bottom`}
  364. orphanBranch={false}
  365. />
  366. );
  367. }
  368. return (
  369. <TreeConnector
  370. isLast={isLast}
  371. hasToggler={hasToggler}
  372. orphanBranch={isOrphanSpan(span)}
  373. >
  374. {connectorBars}
  375. </TreeConnector>
  376. );
  377. }
  378. renderSpanTreeToggler({left, errored}: {errored: boolean; left: number}) {
  379. const {numOfSpanChildren, isRoot, showSpanTree} = this.props;
  380. const chevron = <TreeToggleIcon direction={showSpanTree ? 'up' : 'down'} />;
  381. if (numOfSpanChildren <= 0) {
  382. return (
  383. <TreeToggleContainer style={{left: `${left}px`}}>
  384. {this.renderSpanTreeConnector({hasToggler: false})}
  385. </TreeToggleContainer>
  386. );
  387. }
  388. const chevronElement = !isRoot ? <div>{chevron}</div> : null;
  389. return (
  390. <TreeToggleContainer style={{left: `${left}px`}} hasToggler>
  391. {this.renderSpanTreeConnector({hasToggler: true})}
  392. <TreeToggle
  393. disabled={!!isRoot}
  394. isExpanded={showSpanTree}
  395. errored={errored}
  396. onClick={event => {
  397. event.stopPropagation();
  398. if (isRoot) {
  399. return;
  400. }
  401. PerformanceInteraction.startInteraction('SpanTreeToggle', 1000 * 10);
  402. this.props.toggleSpanTree();
  403. // TODO Abdullah Khan: A little bit hacky, but ensures that the toggled tree aligns
  404. // with the rest of the span tree, in the trace view.
  405. if (this.props.fromTraceView) {
  406. this.props.updateHorizontalScrollState(0.5);
  407. }
  408. }}
  409. >
  410. <Count value={numOfSpanChildren} />
  411. {chevronElement}
  412. </TreeToggle>
  413. </TreeToggleContainer>
  414. );
  415. }
  416. renderTitle(errors: TraceErrorOrIssue[] | null) {
  417. const {
  418. span,
  419. spanBarType,
  420. treeDepth,
  421. groupOccurrence,
  422. toggleSpanGroup,
  423. toggleSiblingSpanGroup,
  424. addContentSpanBarRef,
  425. removeContentSpanBarRef,
  426. groupType,
  427. event,
  428. } = this.props;
  429. let titleFragments: React.ReactNode[] = [];
  430. if (
  431. typeof toggleSpanGroup === 'function' ||
  432. typeof toggleSiblingSpanGroup === 'function'
  433. ) {
  434. titleFragments.push(
  435. <Regroup
  436. key={`regroup-${span.timestamp}`}
  437. onClick={e => {
  438. e.stopPropagation();
  439. e.preventDefault();
  440. if (groupType === GroupType.SIBLINGS && 'op' in span) {
  441. toggleSiblingSpanGroup?.(span, groupOccurrence ?? 0);
  442. } else {
  443. toggleSpanGroup?.();
  444. }
  445. }}
  446. >
  447. <a
  448. href="#regroup"
  449. onClick={e => {
  450. e.preventDefault();
  451. }}
  452. >
  453. {t('Regroup')}
  454. </a>
  455. </Regroup>
  456. );
  457. }
  458. const spanOperationName = getSpanOperation(span);
  459. if (spanOperationName) {
  460. titleFragments.push(spanOperationName);
  461. }
  462. titleFragments = titleFragments.flatMap(current => [current, ' \u2014 ']);
  463. const isAggregateEvent = event.type === EventOrGroupType.AGGREGATE_TRANSACTION;
  464. const left =
  465. treeDepth * (TOGGLE_BORDER_BOX / 2) +
  466. MARGIN_LEFT +
  467. (isAggregateEvent ? FREQUENCY_BOX_WIDTH : 0);
  468. const errored = Boolean(errors && errors.length > 0);
  469. return (
  470. <Fragment>
  471. {isAggregateEvent && (
  472. <SpanFrequencyBox span={span as AggregateSpanType | GapSpanType} />
  473. )}
  474. <RowTitleContainer
  475. data-debug-id="SpanBarTitleContainer"
  476. ref={ref => {
  477. if (!ref) {
  478. removeContentSpanBarRef(this.spanContentRef);
  479. return;
  480. }
  481. addContentSpanBarRef(ref);
  482. this.spanContentRef = ref;
  483. }}
  484. >
  485. {this.renderSpanTreeToggler({left, errored})}
  486. <RowTitle
  487. style={{
  488. left: `${left}px`,
  489. width: '100%',
  490. }}
  491. >
  492. <RowTitleContent
  493. errored={errored}
  494. data-test-id={`row-title-content${spanBarType ? `-${spanBarType}` : ''}`}
  495. >
  496. <strong>{titleFragments}</strong>
  497. {formatSpanTreeLabel(span)}
  498. </RowTitleContent>
  499. </RowTitle>
  500. </RowTitleContainer>
  501. </Fragment>
  502. );
  503. }
  504. connectObservers() {
  505. const observer = new IntersectionObserver(([entry]) =>
  506. this.setState({isIntersecting: entry.isIntersecting}, () => {
  507. // Scrolls the next(invisible) bar from the virtualized list,
  508. // by its height. Allows us to look for anchored span bars occuring
  509. // at the bottom of the span tree.
  510. if (
  511. this.hashSpanId &&
  512. !this.props.didAnchoredSpanMount() &&
  513. !this.state.isIntersecting
  514. ) {
  515. window.scrollBy(0, SPAN_BAR_HEIGHT);
  516. }
  517. })
  518. );
  519. this.intersectionObserver = observer;
  520. if (this.spanRowDOMRef.current) {
  521. observer.observe(this.spanRowDOMRef.current);
  522. }
  523. }
  524. disconnectObservers() {
  525. if (this.intersectionObserver) {
  526. this.intersectionObserver.disconnect();
  527. }
  528. }
  529. renderDivider(
  530. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
  531. ) {
  532. if (this.isHighlighted) {
  533. // Mock component to preserve layout spacing
  534. return (
  535. <DividerLine
  536. showDetail
  537. style={{
  538. position: 'absolute',
  539. }}
  540. />
  541. );
  542. }
  543. const {addDividerLineRef} = dividerHandlerChildrenProps;
  544. return (
  545. <DividerLine
  546. ref={addDividerLineRef()}
  547. style={{
  548. position: 'absolute',
  549. }}
  550. onMouseEnter={() => {
  551. dividerHandlerChildrenProps.setHover(true);
  552. }}
  553. onMouseLeave={() => {
  554. dividerHandlerChildrenProps.setHover(false);
  555. }}
  556. onMouseOver={() => {
  557. dividerHandlerChildrenProps.setHover(true);
  558. }}
  559. onMouseDown={dividerHandlerChildrenProps.onDragStart}
  560. onClick={event => {
  561. // we prevent the propagation of the clicks from this component to prevent
  562. // the span detail from being opened.
  563. event.stopPropagation();
  564. }}
  565. />
  566. );
  567. }
  568. getRelatedErrors(
  569. quickTrace: QuickTraceContextChildrenProps
  570. ): TraceErrorOrIssue[] | null {
  571. if (!quickTrace) {
  572. return null;
  573. }
  574. const {span, isSpanInEmbeddedTree} = this.props;
  575. const {currentEvent} = quickTrace;
  576. if (
  577. isGapSpan(span) ||
  578. !currentEvent ||
  579. !isTraceTransaction<TraceFull>(currentEvent)
  580. ) {
  581. return null;
  582. }
  583. const performanceIssues = currentEvent.performance_issues.filter(
  584. issue =>
  585. issue.span.some(id => id === span.span_id) ||
  586. issue.suspect_spans.some(suspectSpanId => suspectSpanId === span.span_id)
  587. );
  588. return [
  589. ...currentEvent.errors.filter(error => error.span === span.span_id),
  590. ...(isSpanInEmbeddedTree ? [] : performanceIssues), // Spans can be shown when embedded in performance issues
  591. ];
  592. }
  593. getChildTransactions(
  594. quickTrace: QuickTraceContextChildrenProps
  595. ): QuickTraceEvent[] | null {
  596. if (!quickTrace) {
  597. return null;
  598. }
  599. const {span} = this.props;
  600. const {trace} = quickTrace;
  601. if (isGapSpan(span) || !trace) {
  602. return null;
  603. }
  604. return trace.filter(({parent_span_id}) => parent_span_id === span.span_id);
  605. }
  606. renderErrorBadge(errors: TraceErrorOrIssue[] | null): React.ReactNode {
  607. return errors?.length ? <ErrorBadge /> : null;
  608. }
  609. renderMetricsBadge(span: NewTraceDetailsSpanBarProps['span']): React.ReactNode {
  610. const hasMetrics =
  611. '_metrics_summary' in span && Object.keys(span._metrics_summary ?? {}).length > 0;
  612. return hasMetrics && hasMetricsExperimentalFeature(this.props.organization) ? (
  613. <MetricsBadge />
  614. ) : null;
  615. }
  616. renderEmbeddedTransactionsBadge(
  617. transactions: QuickTraceEvent[] | null
  618. ): React.ReactNode {
  619. const {toggleEmbeddedChildren, organization, showEmbeddedChildren} = this.props;
  620. if (transactions && transactions.length >= 1) {
  621. return (
  622. <Tooltip
  623. title={
  624. <span>
  625. {showEmbeddedChildren
  626. ? t('This span is showing a direct child. Remove transaction to hide')
  627. : t('This span has a direct child. Add transaction to view')}
  628. </span>
  629. }
  630. position="top"
  631. containerDisplayMode="block"
  632. >
  633. <StyledZoomIcon
  634. isZoomIn={!showEmbeddedChildren}
  635. onClick={() => {
  636. if (toggleEmbeddedChildren) {
  637. const eventKey = showEmbeddedChildren
  638. ? 'span_view.embedded_child.hide'
  639. : 'span_view.embedded_child.show';
  640. trackAnalytics(eventKey, {organization});
  641. const eventSlugs = transactions.map(transaction =>
  642. generateEventSlug({
  643. id: transaction.event_id,
  644. project: transaction.project_slug,
  645. })
  646. );
  647. toggleEmbeddedChildren(organization.slug, eventSlugs);
  648. }
  649. }}
  650. />
  651. </Tooltip>
  652. );
  653. }
  654. return null;
  655. }
  656. renderMissingInstrumentationProfileBadge(): React.ReactNode {
  657. const {organization, span} = this.props;
  658. if (!organization.features.includes('profiling')) {
  659. return null;
  660. }
  661. if (!isGapSpan(span)) {
  662. return null;
  663. }
  664. return (
  665. <ProfileContext.Consumer>
  666. {profiles => {
  667. if (profiles?.type !== 'resolved') {
  668. return null;
  669. }
  670. return <ProfileBadge />;
  671. }}
  672. </ProfileContext.Consumer>
  673. );
  674. }
  675. renderWarningText() {
  676. let warningText = this.getBounds().warning;
  677. if (this.props.isEmbeddedTransactionTimeAdjusted) {
  678. const embeddedWarningText = t(
  679. 'All child span timestamps have been adjusted to account for mismatched client and server clocks.'
  680. );
  681. warningText = warningText
  682. ? `${warningText}. ${embeddedWarningText}`
  683. : embeddedWarningText;
  684. }
  685. if (!warningText) {
  686. return null;
  687. }
  688. return (
  689. <Tooltip containerDisplayMode="flex" title={warningText}>
  690. <StyledIconWarning size="xs" />
  691. </Tooltip>
  692. );
  693. }
  694. getSpanDetailsProps() {
  695. const {
  696. span,
  697. organization,
  698. event,
  699. isRoot,
  700. trace,
  701. resetCellMeasureCache,
  702. quickTrace,
  703. location,
  704. } = this.props;
  705. const openPanel = decodeScalar(location.query.openPanel);
  706. const errors = this.getRelatedErrors(quickTrace);
  707. const transactions = this.getChildTransactions(quickTrace);
  708. return {
  709. span: span as ProcessedSpanType,
  710. organization,
  711. event: event as EventTransaction,
  712. isRoot: !!isRoot,
  713. openPanel,
  714. trace,
  715. childTransactions: transactions,
  716. relatedErrors: errors,
  717. scrollToHash: this.scrollIntoView,
  718. resetCellMeasureCache,
  719. };
  720. }
  721. handleRowClick() {
  722. const {span, event, location, markAnchoredSpanIsMounted} = this.props;
  723. const spanDetailProps = this.getSpanDetailsProps();
  724. if (this.props.onRowClick && !isGapSpan(span)) {
  725. markAnchoredSpanIsMounted?.();
  726. const isTransactionEvent = event.type === EventOrGroupType.TRANSACTION;
  727. if (isTransactionEvent) {
  728. browserHistory.push({
  729. ...location,
  730. hash: `${transactionTargetHash(event.eventID)}${spanTargetHash(span.span_id)}`,
  731. query: {
  732. ...location.query,
  733. openPanel: 'open',
  734. },
  735. });
  736. spanDetailProps.openPanel = 'open';
  737. }
  738. this.props.onRowClick(undefined);
  739. }
  740. }
  741. renderHeader({
  742. dividerHandlerChildrenProps,
  743. }: {
  744. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps;
  745. }) {
  746. const {dividerPosition, addGhostDividerLineRef} = dividerHandlerChildrenProps;
  747. const {quickTrace} = this.props;
  748. const errors = this.getRelatedErrors(quickTrace);
  749. const transactions = this.getChildTransactions(quickTrace);
  750. return (
  751. <RowCellContainer showDetail={this.isHighlighted}>
  752. <RowCell
  753. data-type="span-row-cell"
  754. showDetail={this.isHighlighted}
  755. style={{
  756. width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
  757. paddingTop: 0,
  758. }}
  759. onClick={() => this.handleRowClick()}
  760. ref={this.spanTitleRef}
  761. >
  762. {this.renderTitle(errors)}
  763. </RowCell>
  764. <DividerContainer>
  765. {this.renderDivider(dividerHandlerChildrenProps)}
  766. {this.renderMetricsBadge(this.props.span)}
  767. {this.renderErrorBadge(errors)}
  768. {this.renderEmbeddedTransactionsBadge(transactions)}
  769. {this.renderMissingInstrumentationProfileBadge()}
  770. </DividerContainer>
  771. <RowCell
  772. data-type="span-row-cell"
  773. showDetail={this.isHighlighted}
  774. showStriping={this.props.spanNumber % 2 !== 0}
  775. style={{
  776. width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
  777. }}
  778. onClick={() => this.handleRowClick()}
  779. >
  780. {this.renderSpanBarRectangles()}
  781. {this.renderMeasurements()}
  782. <SpanBarCursorGuide />
  783. </RowCell>
  784. {!this.isHighlighted && (
  785. <DividerLineGhostContainer
  786. style={{
  787. width: `calc(${toPercent(dividerPosition)} + 0.5px)`,
  788. display: 'none',
  789. }}
  790. >
  791. <DividerLine
  792. ref={addGhostDividerLineRef()}
  793. style={{
  794. right: 0,
  795. }}
  796. className="hovering"
  797. onClick={event => {
  798. // the ghost divider line should not be interactive.
  799. // we prevent the propagation of the clicks from this component to prevent
  800. // the span detail from being opened.
  801. event.stopPropagation();
  802. }}
  803. />
  804. </DividerLineGhostContainer>
  805. )}
  806. </RowCellContainer>
  807. );
  808. }
  809. renderSpanBarRectangles() {
  810. const {span, spanBarColor, spanBarType, generateBounds} = this.props;
  811. const startTimestamp: number = span.start_timestamp;
  812. const endTimestamp: number = span.timestamp;
  813. const duration = Math.abs(endTimestamp - startTimestamp);
  814. const durationString = getHumanDuration(duration);
  815. const bounds = this.getBounds();
  816. const displaySpanBar = defined(bounds.left) && defined(bounds.width);
  817. if (!displaySpanBar) {
  818. return null;
  819. }
  820. const subTimings = getSpanSubTimings(span);
  821. const hasSubTimings = !!subTimings;
  822. const subSpans = hasSubTimings
  823. ? subTimings.map(timing => {
  824. const timingGeneratedBounds = generateBounds(timing);
  825. const timingBounds = this.getBounds(timingGeneratedBounds);
  826. const isAffectedSubTiming = shouldLimitAffectedToTiming(timing);
  827. return (
  828. <RowRectangle
  829. key={timing.name}
  830. spanBarType={isAffectedSubTiming ? spanBarType : undefined}
  831. style={{
  832. backgroundColor: lightenBarColor(
  833. getSpanOperation(span),
  834. timing.colorLighten
  835. ),
  836. left: `min(${toPercent(timingBounds.left || 0)}, calc(100% - 1px))`,
  837. width: toPercent(timingBounds.width || 0),
  838. }}
  839. />
  840. );
  841. })
  842. : null;
  843. const durationDisplay = getDurationDisplay(bounds);
  844. return (
  845. <Fragment>
  846. {subSpans}
  847. <RowRectangle
  848. spanBarType={spanBarType}
  849. style={{
  850. backgroundColor: hasSubTimings ? 'rgba(0,0,0,0.0)' : spanBarColor,
  851. left: `min(${toPercent(bounds.left || 0)}, calc(100% - 1px))`,
  852. width: toPercent(bounds.width || 0),
  853. }}
  854. isHidden={hasSubTimings}
  855. >
  856. <DurationPill
  857. durationDisplay={durationDisplay}
  858. showDetail={this.isHighlighted}
  859. spanBarType={spanBarType}
  860. >
  861. {durationString}
  862. {this.renderWarningText()}
  863. </DurationPill>
  864. </RowRectangle>
  865. </Fragment>
  866. );
  867. }
  868. renderEmbeddedChildrenState() {
  869. const {fetchEmbeddedChildrenState} = this.props;
  870. switch (fetchEmbeddedChildrenState) {
  871. case 'loading_embedded_transactions': {
  872. return (
  873. <MessageRow>
  874. <span>{t('Loading embedded transaction')}</span>
  875. </MessageRow>
  876. );
  877. }
  878. case 'error_fetching_embedded_transactions': {
  879. return (
  880. <MessageRow>
  881. <span>{t('Error loading embedded transaction')}</span>
  882. </MessageRow>
  883. );
  884. }
  885. default:
  886. return null;
  887. }
  888. }
  889. render() {
  890. const {spanNumber} = this.props;
  891. const bounds = this.getBounds();
  892. const {isSpanVisibleInView} = bounds;
  893. return (
  894. <Fragment>
  895. <Row
  896. ref={this.spanRowDOMRef}
  897. visible={isSpanVisibleInView}
  898. showBorder={this.isHighlighted}
  899. data-test-id={`span-row-${spanNumber}`}
  900. >
  901. <Fragment>
  902. <DividerHandlerManager.Consumer>
  903. {(
  904. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
  905. ) =>
  906. this.renderHeader({
  907. dividerHandlerChildrenProps,
  908. })
  909. }
  910. </DividerHandlerManager.Consumer>
  911. </Fragment>
  912. </Row>
  913. {this.renderEmbeddedChildrenState()}
  914. </Fragment>
  915. );
  916. }
  917. }
  918. const StyledIconWarning = styled(IconWarning)`
  919. margin-left: ${space(0.25)};
  920. margin-bottom: ${space(0.25)};
  921. `;
  922. const Regroup = styled('span')``;
  923. export const NewTraceDetailsProfiledSpanBar = withProfiler(
  924. withScrollbarManager(NewTraceDetailsSpanBar)
  925. );