import {createRef, Fragment, useCallback, useEffect, useState} from 'react'; import {browserHistory} from 'react-router'; import styled from '@emotion/styled'; import type {Location} from 'history'; import GuideAnchor from 'sentry/components/assistant/guideAnchor'; import Count from 'sentry/components/count'; import * as DividerHandlerManager from 'sentry/components/events/interfaces/spans/dividerHandlerManager'; import * as ScrollbarManager from 'sentry/components/events/interfaces/spans/scrollbarManager'; import {MeasurementMarker} from 'sentry/components/events/interfaces/spans/styles'; import type { SpanBoundsType, SpanGeneratedBoundsType, VerticalMark, } from 'sentry/components/events/interfaces/spans/utils'; import { getMeasurementBounds, transactionTargetHash, } from 'sentry/components/events/interfaces/spans/utils'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import Link from 'sentry/components/links/link'; import {ROW_HEIGHT, SpanBarType} from 'sentry/components/performance/waterfall/constants'; import { Row, RowCell, RowCellContainer, RowReplayTimeIndicators, } from 'sentry/components/performance/waterfall/row'; import {DurationPill, RowRectangle} from 'sentry/components/performance/waterfall/rowBar'; import { DividerContainer, DividerLine, DividerLineGhostContainer, ErrorBadge, } from 'sentry/components/performance/waterfall/rowDivider'; import { RowTitle, RowTitleContainer, RowTitleContent, } from 'sentry/components/performance/waterfall/rowTitle'; import { ConnectorBar, TOGGLE_BORDER_BOX, TreeConnector, TreeToggle, TreeToggleContainer, TreeToggleIcon, } from 'sentry/components/performance/waterfall/treeConnector'; import { getDurationDisplay, getHumanDuration, } from 'sentry/components/performance/waterfall/utils'; import {generateIssueEventTarget} from 'sentry/components/quickTrace/utils'; import {Tooltip} from 'sentry/components/tooltip'; import type {Organization} from 'sentry/types'; import {defined} from 'sentry/utils'; import toPercent from 'sentry/utils/number/toPercent'; import type { TraceError, TraceFullDetailed, } from 'sentry/utils/performance/quickTrace/types'; import { isTraceError, isTraceRoot, isTraceTransaction, } from 'sentry/utils/performance/quickTrace/utils'; import Projects from 'sentry/utils/projects'; import {ProjectBadgeContainer} from './styles'; import TransactionDetail from './transactionDetail'; import type {TraceInfo, TraceRoot, TreeDepth} from './types'; import {shortenErrorTitle} from './utils'; const MARGIN_LEFT = 0; type Props = { addContentSpanBarRef: (instance: HTMLDivElement | null) => void; continuingDepths: TreeDepth[]; generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType; hasGuideAnchor: boolean; index: number; isExpanded: boolean; isLast: boolean; isOrphan: boolean; isVisible: boolean; location: Location; onWheel: (deltaX: number) => void; organization: Organization; removeContentSpanBarRef: (instance: HTMLDivElement | null) => void; toggleExpandedState: () => void; traceInfo: TraceInfo; transaction: TraceRoot | TraceFullDetailed | TraceError; barColor?: string; isOrphanError?: boolean; measurements?: Map; numOfOrphanErrors?: number; onlyOrphanErrors?: boolean; }; function TransactionBar(props: Props) { const [showDetail, setShowDetail] = useState(false); const transactionRowDOMRef = createRef(); const transactionTitleRef = createRef(); let spanContentRef: HTMLDivElement | null = null; const handleWheel = useCallback( (event: WheelEvent) => { // https://stackoverflow.com/q/57358640 // https://github.com/facebook/react/issues/14856 if (Math.abs(event.deltaY) > Math.abs(event.deltaX)) { return; } event.preventDefault(); event.stopPropagation(); if (Math.abs(event.deltaY) === Math.abs(event.deltaX)) { return; } const {onWheel} = props; onWheel(event.deltaX); }, [props] ); const scrollIntoView = useCallback(() => { const element = transactionRowDOMRef.current; if (!element) { return; } const boundingRect = element.getBoundingClientRect(); const offset = boundingRect.top + window.scrollY; setShowDetail(true); window.scrollTo(0, offset); }, [transactionRowDOMRef]); useEffect(() => { const {location, transaction} = props; const transactionTitleRefCurrentCopy = transactionTitleRef.current; if ( 'event_id' in transaction && transactionTargetHash(transaction.event_id) === location.hash ) { scrollIntoView(); } if (transactionTitleRefCurrentCopy) { transactionTitleRefCurrentCopy.addEventListener('wheel', handleWheel, { passive: false, }); } return () => { if (transactionTitleRefCurrentCopy) { transactionTitleRefCurrentCopy.removeEventListener('wheel', handleWheel); } }; }, [handleWheel, props, scrollIntoView, transactionTitleRef]); const handleRowCellClick = () => { const {transaction, organization} = props; if (isTraceError(transaction)) { browserHistory.push(generateIssueEventTarget(transaction, organization)); } if (isTraceTransaction(transaction)) { setShowDetail(prev => !prev); } }; const getCurrentOffset = () => { const {transaction} = props; const {generation} = transaction; return getOffset(generation); }; const renderMeasurements = () => { const {measurements, generateBounds} = props; if (!measurements) { return null; } return ( {Array.from(measurements.values()).map(verticalMark => { const mark = Object.values(verticalMark.marks)[0]; const {timestamp} = mark; const bounds = getMeasurementBounds(timestamp, generateBounds); const shouldDisplay = defined(bounds.left) && defined(bounds.width); if (!shouldDisplay || !bounds.isSpanVisibleInView) { return null; } return ( ); })} ); }; const renderConnector = (hasToggle: boolean) => { const {continuingDepths, isExpanded, isOrphan, isLast, transaction} = props; const {generation = 0} = transaction; const eventId = isTraceTransaction(transaction) || isTraceError(transaction) ? transaction.event_id : transaction.traceSlug; if (generation === 0) { if (hasToggle) { return ( ); } return null; } const connectorBars: Array = continuingDepths.map( ({depth, isOrphanDepth}) => { if (generation - depth <= 1) { // If the difference is less than or equal to 1, then it means that the continued // bar is from its direct parent. In this case, do not render a connector bar // because the tree connector below will suffice. return null; } const left = -1 * getOffset(generation - depth - 1) - 2; return ( ); } ); if (hasToggle && isExpanded) { connectorBars.push( ); } return ( {connectorBars} ); }; const renderToggle = (errored: boolean) => { const {isExpanded, transaction, toggleExpandedState, numOfOrphanErrors} = props; const left = getCurrentOffset(); const hasOrphanErrors = numOfOrphanErrors && numOfOrphanErrors > 0; const childrenLength = (!isTraceError(transaction) && transaction.children?.length) || 0; const generation = transaction.generation || 0; if (childrenLength <= 0 && !hasOrphanErrors) { return ( {renderConnector(false)} ); } const isRoot = generation === 0; return ( {renderConnector(true)} { event.stopPropagation(); if (isRoot) { return; } toggleExpandedState(); }} > {!isRoot && (
)}
); }; const renderTitle = (_: ScrollbarManager.ScrollbarManagerChildrenProps) => { const {organization, transaction, addContentSpanBarRef, removeContentSpanBarRef} = props; const left = getCurrentOffset(); const errored = isTraceTransaction(transaction) ? transaction.errors && transaction.errors.length + transaction.performance_issues.length > 0 : false; const projectBadge = (isTraceTransaction(transaction) || isTraceError(transaction)) && ( {({projects}) => { const project = projects.find(p => p.slug === transaction.project_slug); return ( ); }} ); const content = isTraceError(transaction) ? ( {projectBadge} {'Unknown \u2014 '} {shortenErrorTitle(transaction.title)} ) : isTraceTransaction(transaction) ? ( {projectBadge} {transaction['transaction.op']} {' \u2014 '} {transaction.transaction} ) : ( {'Trace \u2014 '} {transaction.traceSlug} ); return ( { if (!ref) { removeContentSpanBarRef(spanContentRef); return; } addContentSpanBarRef(ref); spanContentRef = ref; }} > {renderToggle(errored)} {content} ); }; const renderDivider = ( dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps ) => { if (showDetail) { // Mock component to preserve layout spacing return ( ); } const {addDividerLineRef} = dividerHandlerChildrenProps; return ( { dividerHandlerChildrenProps.setHover(true); }} onMouseLeave={() => { dividerHandlerChildrenProps.setHover(false); }} onMouseOver={() => { dividerHandlerChildrenProps.setHover(true); }} onMouseDown={dividerHandlerChildrenProps.onDragStart} onClick={event => { // we prevent the propagation of the clicks from this component to prevent // the span detail from being opened. event.stopPropagation(); }} /> ); }; const renderGhostDivider = ( dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps ) => { const {dividerPosition, addGhostDividerLineRef} = dividerHandlerChildrenProps; return ( { // 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. event.stopPropagation(); }} /> ); }; const renderErrorBadge = () => { const {transaction} = props; if ( isTraceRoot(transaction) || isTraceError(transaction) || !(transaction.errors.length + transaction.performance_issues.length) ) { return null; } return ; }; const renderRectangle = () => { const {transaction, traceInfo, barColor} = props; // Use 1 as the difference in the case that startTimestamp === endTimestamp const delta = Math.abs(traceInfo.endTimestamp - traceInfo.startTimestamp) || 1; const start_timestamp = isTraceError(transaction) ? transaction.timestamp : transaction.start_timestamp; if (!(start_timestamp && transaction.timestamp)) { return null; } const startPosition = Math.abs(start_timestamp - traceInfo.startTimestamp); const startPercentage = startPosition / delta; const duration = Math.abs(transaction.timestamp - start_timestamp); const widthPercentage = duration / delta; return ( {renderPerformanceIssues()} {isTraceError(transaction) ? ( ) : ( {renderErrorBadge()} {getHumanDuration(duration)} )} ); }; const renderPerformanceIssues = () => { const {transaction, barColor} = props; if (isTraceError(transaction) || isTraceRoot(transaction)) { return null; } const rows: React.ReactElement[] = []; // Use 1 as the difference in the case that startTimestamp === endTimestamp const delta = Math.abs(transaction.timestamp - transaction.start_timestamp) || 1; for (let i = 0; i < transaction.performance_issues.length; i++) { const issue = transaction.performance_issues[i]; const startPosition = Math.abs(issue.start - transaction.start_timestamp); const startPercentage = startPosition / delta; const duration = Math.abs(issue.end - issue.start); const widthPercentage = duration / delta; rows.push( ); } return rows; }; const renderHeader = ({ dividerHandlerChildrenProps, scrollbarManagerChildrenProps, }: { dividerHandlerChildrenProps: DividerHandlerManager.DividerHandlerManagerChildrenProps; scrollbarManagerChildrenProps: ScrollbarManager.ScrollbarManagerChildrenProps; }) => { const {hasGuideAnchor, index, transaction, onlyOrphanErrors = false} = props; const {dividerPosition} = dividerHandlerChildrenProps; const hideDurationRectangle = isTraceRoot(transaction) && onlyOrphanErrors; return ( {renderTitle(scrollbarManagerChildrenProps)} {renderDivider(dividerHandlerChildrenProps)} {!hideDurationRectangle && renderRectangle()} {renderMeasurements()} {!showDetail && renderGhostDivider(dividerHandlerChildrenProps)} ); }; const renderDetail = () => { const {location, organization, isVisible, transaction} = props; if (isTraceError(transaction) || isTraceRoot(transaction)) { return null; } if (!isVisible || !showDetail) { return null; } return ( ); }; const {isVisible, transaction} = props; return ( (transaction) ? 'pointer' : 'default'} > {scrollbarManagerChildrenProps => ( {dividerHandlerChildrenProps => renderHeader({ dividerHandlerChildrenProps, scrollbarManagerChildrenProps, }) } )} {renderDetail()} ); } function getOffset(generation) { return generation * (TOGGLE_BORDER_BOX / 2) + MARGIN_LEFT; } export default TransactionBar; const StyledRow = styled(Row)` &, ${RowCellContainer} { overflow: visible; } `; const ErrorLink = styled(Link)` color: ${p => p.theme.error}; `; const StyledRowRectangle = styled(RowRectangle)` display: flex; align-items: center; `;