spanGroupBar.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. import type {LegacyRef, MutableRefObject} from 'react';
  2. import {Fragment, useCallback, useEffect, useMemo, useRef} from 'react';
  3. import {useTheme} from '@emotion/react';
  4. import maxBy from 'lodash/maxBy';
  5. import Count from 'sentry/components/count';
  6. import {
  7. FREQUENCY_BOX_WIDTH,
  8. SpanFrequencyBox,
  9. } from 'sentry/components/events/interfaces/spans/spanFrequencyBox';
  10. import type {SpanBarType} from 'sentry/components/performance/waterfall/constants';
  11. import {
  12. getSpanBarColours,
  13. ROW_HEIGHT,
  14. } from 'sentry/components/performance/waterfall/constants';
  15. import {
  16. Row,
  17. RowCell,
  18. RowCellContainer,
  19. } from 'sentry/components/performance/waterfall/row';
  20. import {
  21. DividerContainer,
  22. DividerLine,
  23. DividerLineGhostContainer,
  24. } from 'sentry/components/performance/waterfall/rowDivider';
  25. import {
  26. RowTitle,
  27. RowTitleContainer,
  28. SpanGroupRowTitleContent,
  29. } from 'sentry/components/performance/waterfall/rowTitle';
  30. import {
  31. TOGGLE_BORDER_BOX,
  32. TreeToggle,
  33. TreeToggleContainer,
  34. } from 'sentry/components/performance/waterfall/treeConnector';
  35. import type {AggregateEventTransaction, EventTransaction} from 'sentry/types/event';
  36. import {EventOrGroupType} from 'sentry/types/event';
  37. import {defined} from 'sentry/utils';
  38. import toPercent from 'sentry/utils/number/toPercent';
  39. import {PerformanceInteraction} from 'sentry/utils/performanceForSentry';
  40. import * as DividerHandlerManager from './dividerHandlerManager';
  41. import SpanBarCursorGuide from './spanBarCursorGuide';
  42. import {MeasurementMarker} from './styles';
  43. import type {AggregateSpanType, EnhancedSpan, ProcessedSpanType} from './types';
  44. import type {SpanBoundsType, SpanGeneratedBoundsType, VerticalMark} from './utils';
  45. import {getMeasurementBounds, getMeasurements, spanTargetHash} from './utils';
  46. const MARGIN_LEFT = 0;
  47. type Props = {
  48. addContentSpanBarRef: (instance: HTMLDivElement | null) => void;
  49. didAnchoredSpanMount: () => boolean;
  50. event: Readonly<EventTransaction | AggregateEventTransaction>;
  51. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType;
  52. getCurrentLeftPos: () => number;
  53. onWheel: (deltaX: number) => void;
  54. removeContentSpanBarRef: (instance: HTMLDivElement | null) => void;
  55. renderGroupSpansTitle: () => React.ReactNode;
  56. renderSpanRectangles: () => React.ReactNode;
  57. renderSpanTreeConnector: () => React.ReactNode;
  58. span: Readonly<ProcessedSpanType>;
  59. spanGrouping: EnhancedSpan[];
  60. spanNumber: number;
  61. toggleSpanGroup: () => void;
  62. treeDepth: number;
  63. measurements?: Map<number, VerticalMark>;
  64. spanBarType?: SpanBarType;
  65. };
  66. function renderGroupedSpansToggler(props: Props) {
  67. const {
  68. treeDepth,
  69. spanGrouping,
  70. renderSpanTreeConnector,
  71. toggleSpanGroup,
  72. spanBarType,
  73. event,
  74. } = props;
  75. const isAggregateEvent = event.type === EventOrGroupType.AGGREGATE_TRANSACTION;
  76. const left =
  77. treeDepth * (TOGGLE_BORDER_BOX / 2) +
  78. MARGIN_LEFT +
  79. (isAggregateEvent ? FREQUENCY_BOX_WIDTH : 0);
  80. return (
  81. <TreeToggleContainer style={{left: `${left}px`}} hasToggler>
  82. {renderSpanTreeConnector()}
  83. <TreeToggle
  84. disabled={false}
  85. isExpanded={false}
  86. errored={false}
  87. isSpanGroupToggler
  88. onClick={e => {
  89. e.stopPropagation();
  90. toggleSpanGroup();
  91. }}
  92. spanBarType={spanBarType}
  93. >
  94. <Count value={spanGrouping.length} />
  95. </TreeToggle>
  96. </TreeToggleContainer>
  97. );
  98. }
  99. function renderDivider(
  100. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
  101. ) {
  102. const {addDividerLineRef} = dividerHandlerChildrenProps;
  103. return (
  104. <DividerLine
  105. ref={addDividerLineRef()}
  106. style={{
  107. position: 'absolute',
  108. }}
  109. onMouseEnter={() => {
  110. dividerHandlerChildrenProps.setHover(true);
  111. }}
  112. onMouseLeave={() => {
  113. dividerHandlerChildrenProps.setHover(false);
  114. }}
  115. onMouseOver={() => {
  116. dividerHandlerChildrenProps.setHover(true);
  117. }}
  118. onMouseDown={dividerHandlerChildrenProps.onDragStart}
  119. onClick={e => {
  120. // we prevent the propagation of the clicks from this component to prevent
  121. // the span detail from being opened.
  122. e.stopPropagation();
  123. }}
  124. />
  125. );
  126. }
  127. function renderMeasurements(
  128. event: Readonly<EventTransaction | AggregateEventTransaction>,
  129. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType,
  130. measurements: Map<number, VerticalMark> | undefined
  131. ) {
  132. const barMeasurements = measurements ?? getMeasurements(event, generateBounds);
  133. return (
  134. <Fragment>
  135. {Array.from(barMeasurements).map(([timestamp, verticalMark]) => {
  136. const bounds = getMeasurementBounds(timestamp, generateBounds);
  137. const shouldDisplay = defined(bounds.left) && defined(bounds.width);
  138. if (!shouldDisplay || !bounds.isSpanVisibleInView) {
  139. return null;
  140. }
  141. return (
  142. <MeasurementMarker
  143. key={String(timestamp)}
  144. style={{
  145. left: `clamp(0%, ${toPercent(bounds.left || 0)}, calc(100% - 1px))`,
  146. }}
  147. failedThreshold={verticalMark.failedThreshold}
  148. />
  149. );
  150. })}
  151. </Fragment>
  152. );
  153. }
  154. export function SpanGroupBar(props: Props) {
  155. const spanTitleRef: LegacyRef<HTMLDivElement> | null = useRef(null);
  156. const spanContentRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
  157. const {
  158. onWheel,
  159. addContentSpanBarRef,
  160. removeContentSpanBarRef,
  161. didAnchoredSpanMount,
  162. spanGrouping,
  163. toggleSpanGroup,
  164. getCurrentLeftPos,
  165. spanBarType,
  166. event,
  167. measurements,
  168. } = props;
  169. const theme = useTheme();
  170. // On mount, it is necessary to set the left styling of the content here due to the span tree being virtualized.
  171. // If we rely on the scrollBarManager to set the styling, it happens too late and awkwardly applies an animation.
  172. const setTransformCallback = useCallback(
  173. (ref: HTMLDivElement | null) => {
  174. if (ref) {
  175. spanContentRef.current = ref;
  176. addContentSpanBarRef(ref);
  177. const left = -getCurrentLeftPos();
  178. ref.style.transform = `translateX(${left}px)`;
  179. ref.style.transformOrigin = 'left';
  180. return;
  181. }
  182. // If ref is null, this means the component is about to unmount
  183. removeContentSpanBarRef(spanContentRef.current);
  184. },
  185. [addContentSpanBarRef, removeContentSpanBarRef, getCurrentLeftPos]
  186. );
  187. useEffect(() => {
  188. if (location.hash && !didAnchoredSpanMount()) {
  189. const anchoredSpanIndex = spanGrouping.findIndex(
  190. span => spanTargetHash(span.span.span_id) === location.hash
  191. );
  192. // TODO: This doesn't always work.
  193. // A potential fix is to just scroll to the Autogroup without expanding it if a span within it is anchored.
  194. if (anchoredSpanIndex > -1) {
  195. toggleSpanGroup();
  196. window.scrollTo(0, window.scrollY + anchoredSpanIndex * ROW_HEIGHT);
  197. }
  198. }
  199. }, [didAnchoredSpanMount, spanGrouping, toggleSpanGroup]);
  200. useEffect(() => {
  201. const currentRef = spanTitleRef.current;
  202. const handleWheel = (e: WheelEvent) => {
  203. if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
  204. return;
  205. }
  206. e.preventDefault();
  207. e.stopPropagation();
  208. if (Math.abs(e.deltaY) === Math.abs(e.deltaX)) {
  209. return;
  210. }
  211. onWheel(e.deltaX);
  212. };
  213. if (currentRef) {
  214. currentRef.addEventListener('wheel', handleWheel, {
  215. passive: false,
  216. });
  217. }
  218. return () => {
  219. if (currentRef) {
  220. currentRef.removeEventListener('wheel', handleWheel);
  221. }
  222. };
  223. }, [onWheel]);
  224. // If this is an aggregate span waterfall, we will use the span with the highest frequency in the grouping to represent
  225. // the value shown in the frequency box. Using a memo because otherwise this operation will fire on every vertical scroll tick
  226. const mostFrequentSpanInGroup = useMemo(() => {
  227. if (event.type !== EventOrGroupType.AGGREGATE_TRANSACTION) {
  228. return null;
  229. }
  230. const spanObjects = spanGrouping.map(({span}) => span);
  231. return maxBy(spanObjects, 'frequency');
  232. }, [event, spanGrouping]);
  233. return (
  234. <DividerHandlerManager.Consumer>
  235. {(
  236. dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps
  237. ) => {
  238. const {generateBounds, span, treeDepth, spanNumber} = props;
  239. const {isSpanVisibleInView: isSpanVisible} = generateBounds({
  240. startTimestamp: span.start_timestamp,
  241. endTimestamp: span.timestamp,
  242. });
  243. const isAggregateEvent =
  244. event.type === EventOrGroupType.AGGREGATE_TRANSACTION &&
  245. mostFrequentSpanInGroup;
  246. const {dividerPosition, addGhostDividerLineRef} = dividerHandlerChildrenProps;
  247. const left =
  248. treeDepth * (TOGGLE_BORDER_BOX / 2) +
  249. MARGIN_LEFT +
  250. (isAggregateEvent ? FREQUENCY_BOX_WIDTH : 0);
  251. return (
  252. <Row
  253. visible={isSpanVisible}
  254. showBorder={false}
  255. data-test-id={`span-row-${spanNumber}`}
  256. >
  257. <RowCellContainer>
  258. <RowCell
  259. data-type="span-row-cell"
  260. style={{
  261. width: `calc(${toPercent(dividerPosition)} - 0.5px)`,
  262. paddingTop: 0,
  263. }}
  264. onClick={() => {
  265. PerformanceInteraction.startInteraction('SpanTreeToggle', 1000 * 10);
  266. props.toggleSpanGroup();
  267. }}
  268. ref={spanTitleRef}
  269. >
  270. {isAggregateEvent && (
  271. <SpanFrequencyBox span={mostFrequentSpanInGroup as AggregateSpanType} />
  272. )}
  273. <RowTitleContainer ref={setTransformCallback}>
  274. {renderGroupedSpansToggler(props)}
  275. <RowTitle
  276. style={{
  277. left: `${left}px`,
  278. width: '100%',
  279. }}
  280. >
  281. <SpanGroupRowTitleContent
  282. color={getSpanBarColours(spanBarType, theme).primary}
  283. >
  284. {props.renderGroupSpansTitle()}
  285. </SpanGroupRowTitleContent>
  286. </RowTitle>
  287. </RowTitleContainer>
  288. </RowCell>
  289. <DividerContainer>
  290. {renderDivider(dividerHandlerChildrenProps)}
  291. </DividerContainer>
  292. <RowCell
  293. data-type="span-row-cell"
  294. showStriping={spanNumber % 2 !== 0}
  295. style={{
  296. width: `calc(${toPercent(1 - dividerPosition)} - 0.5px)`,
  297. }}
  298. onClick={() => toggleSpanGroup()}
  299. >
  300. {props.renderSpanRectangles()}
  301. {renderMeasurements(event, generateBounds, measurements)}
  302. <SpanBarCursorGuide />
  303. </RowCell>
  304. <DividerLineGhostContainer
  305. style={{
  306. width: `calc(${toPercent(dividerPosition)} + 0.5px)`,
  307. display: 'none',
  308. }}
  309. >
  310. <DividerLine
  311. ref={addGhostDividerLineRef()}
  312. style={{
  313. right: 0,
  314. }}
  315. className="hovering"
  316. onClick={e => {
  317. // the ghost divider line should not be interactive.
  318. // we prevent the propagation of the clicks from this component to prevent
  319. // the span detail from being opened.
  320. e.stopPropagation();
  321. }}
  322. />
  323. </DividerLineGhostContainer>
  324. </RowCellContainer>
  325. </Row>
  326. );
  327. }}
  328. </DividerHandlerManager.Consumer>
  329. );
  330. }