import type {LegacyRef, MutableRefObject} from 'react'; import {Fragment, useCallback, useEffect, useMemo, useRef} from 'react'; import {useTheme} from '@emotion/react'; import maxBy from 'lodash/maxBy'; import Count from 'sentry/components/count'; import { FREQUENCY_BOX_WIDTH, SpanFrequencyBox, } from 'sentry/components/events/interfaces/spans/spanFrequencyBox'; import type {SpanBarType} from 'sentry/components/performance/waterfall/constants'; import { getSpanBarColours, ROW_HEIGHT, } from 'sentry/components/performance/waterfall/constants'; import { Row, RowCell, RowCellContainer, } from 'sentry/components/performance/waterfall/row'; import { DividerContainer, DividerLine, DividerLineGhostContainer, } from 'sentry/components/performance/waterfall/rowDivider'; import { RowTitle, RowTitleContainer, SpanGroupRowTitleContent, } from 'sentry/components/performance/waterfall/rowTitle'; import { TOGGLE_BORDER_BOX, TreeToggle, TreeToggleContainer, } from 'sentry/components/performance/waterfall/treeConnector'; import type {AggregateEventTransaction, EventTransaction} from 'sentry/types/event'; import {EventOrGroupType} from 'sentry/types/event'; import {defined} from 'sentry/utils'; import toPercent from 'sentry/utils/number/toPercent'; import {PerformanceInteraction} from 'sentry/utils/performanceForSentry'; import * as DividerHandlerManager from './dividerHandlerManager'; import SpanBarCursorGuide from './spanBarCursorGuide'; import {MeasurementMarker} from './styles'; import type {AggregateSpanType, EnhancedSpan, ProcessedSpanType} from './types'; import type {SpanBoundsType, SpanGeneratedBoundsType, VerticalMark} from './utils'; import {getMeasurementBounds, getMeasurements, spanTargetHash} from './utils'; const MARGIN_LEFT = 0; type Props = { addContentSpanBarRef: (instance: HTMLDivElement | null) => void; didAnchoredSpanMount: () => boolean; event: Readonly; generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType; getCurrentLeftPos: () => number; onWheel: (deltaX: number) => void; removeContentSpanBarRef: (instance: HTMLDivElement | null) => void; renderGroupSpansTitle: () => React.ReactNode; renderSpanRectangles: () => React.ReactNode; renderSpanTreeConnector: () => React.ReactNode; span: Readonly; spanGrouping: EnhancedSpan[]; spanNumber: number; toggleSpanGroup: () => void; treeDepth: number; measurements?: Map; spanBarType?: SpanBarType; }; function renderGroupedSpansToggler(props: Props) { const { treeDepth, spanGrouping, renderSpanTreeConnector, toggleSpanGroup, spanBarType, event, } = props; const isAggregateEvent = event.type === EventOrGroupType.AGGREGATE_TRANSACTION; const left = treeDepth * (TOGGLE_BORDER_BOX / 2) + MARGIN_LEFT + (isAggregateEvent ? FREQUENCY_BOX_WIDTH : 0); return ( {renderSpanTreeConnector()} { e.stopPropagation(); toggleSpanGroup(); }} spanBarType={spanBarType} > ); } function renderDivider( dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps ) { const {addDividerLineRef} = dividerHandlerChildrenProps; return ( { dividerHandlerChildrenProps.setHover(true); }} onMouseLeave={() => { dividerHandlerChildrenProps.setHover(false); }} onMouseOver={() => { dividerHandlerChildrenProps.setHover(true); }} onMouseDown={dividerHandlerChildrenProps.onDragStart} onClick={e => { // we prevent the propagation of the clicks from this component to prevent // the span detail from being opened. e.stopPropagation(); }} /> ); } function renderMeasurements( event: Readonly, generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType, measurements: Map | undefined ) { const barMeasurements = measurements ?? getMeasurements(event, generateBounds); return ( {Array.from(barMeasurements).map(([timestamp, verticalMark]) => { const bounds = getMeasurementBounds(timestamp, generateBounds); const shouldDisplay = defined(bounds.left) && defined(bounds.width); if (!shouldDisplay || !bounds.isSpanVisibleInView) { return null; } return ( ); })} ); } export function SpanGroupBar(props: Props) { const spanTitleRef: LegacyRef | null = useRef(null); const spanContentRef: MutableRefObject = useRef(null); const { onWheel, addContentSpanBarRef, removeContentSpanBarRef, didAnchoredSpanMount, spanGrouping, toggleSpanGroup, getCurrentLeftPos, spanBarType, event, measurements, } = props; const theme = useTheme(); // On mount, it is necessary to set the left styling of the content here due to the span tree being virtualized. // If we rely on the scrollBarManager to set the styling, it happens too late and awkwardly applies an animation. const setTransformCallback = useCallback( (ref: HTMLDivElement | null) => { if (ref) { spanContentRef.current = ref; addContentSpanBarRef(ref); const left = -getCurrentLeftPos(); ref.style.transform = `translateX(${left}px)`; ref.style.transformOrigin = 'left'; return; } // If ref is null, this means the component is about to unmount removeContentSpanBarRef(spanContentRef.current); }, [addContentSpanBarRef, removeContentSpanBarRef, getCurrentLeftPos] ); useEffect(() => { if (location.hash && !didAnchoredSpanMount()) { const anchoredSpanIndex = spanGrouping.findIndex( span => spanTargetHash(span.span.span_id) === location.hash ); // TODO: This doesn't always work. // A potential fix is to just scroll to the Autogroup without expanding it if a span within it is anchored. if (anchoredSpanIndex > -1) { toggleSpanGroup(); window.scrollTo(0, window.scrollY + anchoredSpanIndex * ROW_HEIGHT); } } }, [didAnchoredSpanMount, spanGrouping, toggleSpanGroup]); useEffect(() => { const currentRef = spanTitleRef.current; const handleWheel = (e: WheelEvent) => { if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) { return; } e.preventDefault(); e.stopPropagation(); if (Math.abs(e.deltaY) === Math.abs(e.deltaX)) { return; } onWheel(e.deltaX); }; if (currentRef) { currentRef.addEventListener('wheel', handleWheel, { passive: false, }); } return () => { if (currentRef) { currentRef.removeEventListener('wheel', handleWheel); } }; }, [onWheel]); // If this is an aggregate span waterfall, we will use the span with the highest frequency in the grouping to represent // the value shown in the frequency box. Using a memo because otherwise this operation will fire on every vertical scroll tick const mostFrequentSpanInGroup = useMemo(() => { if (event.type !== EventOrGroupType.AGGREGATE_TRANSACTION) { return null; } const spanObjects = spanGrouping.map(({span}) => span); return maxBy(spanObjects, 'frequency'); }, [event, spanGrouping]); return ( {( dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps ) => { const {generateBounds, span, treeDepth, spanNumber} = props; const {isSpanVisibleInView: isSpanVisible} = generateBounds({ startTimestamp: span.start_timestamp, endTimestamp: span.timestamp, }); const isAggregateEvent = event.type === EventOrGroupType.AGGREGATE_TRANSACTION && mostFrequentSpanInGroup; const {dividerPosition, addGhostDividerLineRef} = dividerHandlerChildrenProps; const left = treeDepth * (TOGGLE_BORDER_BOX / 2) + MARGIN_LEFT + (isAggregateEvent ? FREQUENCY_BOX_WIDTH : 0); return ( { PerformanceInteraction.startInteraction('SpanTreeToggle', 1000 * 10); props.toggleSpanGroup(); }} ref={spanTitleRef} > {isAggregateEvent && ( )} {renderGroupedSpansToggler(props)} {props.renderGroupSpansTitle()} {renderDivider(dividerHandlerChildrenProps)} toggleSpanGroup()} > {props.renderSpanRectangles()} {renderMeasurements(event, generateBounds, measurements)} { // the ghost divider line should not be interactive. // we prevent the propagation of the clicks from this component to prevent // the span detail from being opened. e.stopPropagation(); }} /> ); }} ); }