import {createRef, Fragment, useEffect} from 'react'; import type {RouteComponentProps} from 'react-router'; import styled from '@emotion/styled'; import * as Sentry from '@sentry/react'; import * as DividerHandlerManager from 'sentry/components/events/interfaces/spans/dividerHandlerManager'; import MeasurementsPanel from 'sentry/components/events/interfaces/spans/measurementsPanel'; import * as ScrollbarManager from 'sentry/components/events/interfaces/spans/scrollbarManager'; import { boundsGenerator, getMeasurements, } from 'sentry/components/events/interfaces/spans/utils'; import Panel from 'sentry/components/panels/panel'; import {MessageRow} from 'sentry/components/performance/waterfall/messageRow'; import { DividerSpacer, ScrollbarContainer, VirtualScrollbar, VirtualScrollbarGrip, } from 'sentry/components/performance/waterfall/miniHeader'; import {pickBarColor} from 'sentry/components/performance/waterfall/utils'; import {tct} from 'sentry/locale'; import type {Organization} from 'sentry/types/organization'; import {trackAnalytics} from 'sentry/utils/analytics'; import type EventView from 'sentry/utils/discover/eventView'; import toPercent from 'sentry/utils/number/toPercent'; import type { TraceError, TraceFullDetailed, TraceMeta, } from 'sentry/utils/performance/quickTrace/types'; import { TraceDetailBody, TraceViewContainer, TraceViewHeaderContainer, } from 'sentry/views/performance/traceDetails/styles'; import TransactionGroup from 'sentry/views/performance/traceDetails/transactionGroup'; import type {TraceInfo, TreeDepth} from 'sentry/views/performance/traceDetails/types'; import { getTraceInfo, hasTraceData, isRootTransaction, } from 'sentry/views/performance/traceDetails/utils'; import LimitExceededMessage from './limitExceededMessage'; import TraceNotFound from './traceNotFound'; type AccType = { lastIndex: number; numberOfHiddenTransactionsAbove: number; renderedChildren: React.ReactNode[]; }; type Props = Pick, 'location'> & { meta: TraceMeta | null; organization: Organization; traceEventView: EventView; traceSlug: string; traces: TraceFullDetailed[]; filteredEventIds?: Set; handleLimitChange?: (newLimit: number) => void; orphanErrors?: TraceError[]; traceInfo?: TraceInfo; }; function TraceHiddenMessage({ isVisible, numberOfHiddenTransactionsAbove, numberOfHiddenErrorsAbove, }: { isVisible: boolean; numberOfHiddenErrorsAbove: number; numberOfHiddenTransactionsAbove: number; }) { if ( !isVisible || (numberOfHiddenTransactionsAbove < 1 && numberOfHiddenErrorsAbove < 1) ) { return null; } const numOfTransaction = {numberOfHiddenTransactionsAbove}; const numOfErrors = {numberOfHiddenErrorsAbove}; const hiddenTransactionsMessage = numberOfHiddenTransactionsAbove < 1 ? '' : numberOfHiddenTransactionsAbove === 1 ? tct('[numOfTransaction] hidden transaction', { numOfTransaction, }) : tct('[numOfTransaction] hidden transactions', { numOfTransaction, }); const hiddenErrorsMessage = numberOfHiddenErrorsAbove < 1 ? '' : numberOfHiddenErrorsAbove === 1 ? tct('[numOfErrors] hidden error', { numOfErrors, }) : tct('[numOfErrors] hidden errors', { numOfErrors, }); return ( {hiddenTransactionsMessage} {hiddenErrorsMessage && hiddenTransactionsMessage && ', '} {hiddenErrorsMessage} ); } function isRowVisible( row: TraceFullDetailed | TraceError, filteredEventIds?: Set ): boolean { return filteredEventIds ? filteredEventIds.has(row.event_id) : true; } function generateBounds(traceInfo: TraceInfo) { return boundsGenerator({ traceStartTimestamp: traceInfo.startTimestamp, traceEndTimestamp: traceInfo.endTimestamp, viewStart: 0, viewEnd: 1, }); } export default function TraceView({ location, meta, organization, traces, traceSlug, traceEventView, filteredEventIds, orphanErrors, handleLimitChange, ...props }: Props) { const sentrySpan = Sentry.startInactiveSpan({ op: 'trace.render', name: 'trace-view-content', onlyIfParent: true, }); const hasOrphanErrors = orphanErrors && orphanErrors.length > 0; const onlyOrphanErrors = hasOrphanErrors && (!traces || traces.length === 0); useEffect(() => { trackAnalytics('performance_views.trace_view.view', { organization, }); }, [organization]); function renderTransaction( transaction: TraceFullDetailed, { continuingDepths, isOrphan, isLast, index, numberOfHiddenTransactionsAbove, traceInfo, hasGuideAnchor, }: { continuingDepths: TreeDepth[]; hasGuideAnchor: boolean; index: number; isLast: boolean; isOrphan: boolean; numberOfHiddenTransactionsAbove: number; traceInfo: TraceInfo; } ) { const {children, event_id: eventId} = transaction; // Add 1 to the generation to make room for the "root trace" const generation = transaction.generation + 1; const isVisible = isRowVisible(transaction, filteredEventIds); const accumulated: AccType = children.reduce( (acc: AccType, child: TraceFullDetailed, idx: number) => { const isLastChild = idx === children.length - 1; const hasChildren = child.children.length > 0; const result = renderTransaction(child, { continuingDepths: !isLastChild && hasChildren ? [...continuingDepths, {depth: generation, isOrphanDepth: isOrphan}] : continuingDepths, isOrphan, isLast: isLastChild, index: acc.lastIndex + 1, numberOfHiddenTransactionsAbove: acc.numberOfHiddenTransactionsAbove, traceInfo, hasGuideAnchor: false, }); acc.lastIndex = result.lastIndex; acc.numberOfHiddenTransactionsAbove = result.numberOfHiddenTransactionsAbove; acc.renderedChildren.push(result.transactionGroup); return acc; }, { renderedChildren: [], lastIndex: index, numberOfHiddenTransactionsAbove: isVisible ? 0 : numberOfHiddenTransactionsAbove + 1, } ); return { transactionGroup: ( 0 ? getMeasurements(traces[0], generateBounds(traceInfo)) : undefined } generateBounds={generateBounds(traceInfo)} continuingDepths={continuingDepths} isOrphan={isOrphan} isLast={isLast} index={index} isVisible={isVisible} hasGuideAnchor={hasGuideAnchor} renderedChildren={accumulated.renderedChildren} barColor={pickBarColor(transaction['transaction.op'])} /> ), lastIndex: accumulated.lastIndex, numberOfHiddenTransactionsAbove: accumulated.numberOfHiddenTransactionsAbove, }; } const traceViewRef = createRef(); const virtualScrollbarContainerRef = createRef(); if (!hasTraceData(traces, orphanErrors)) { return ( ); } const traceInfo = props.traceInfo || getTraceInfo(traces); const accumulator: { index: number; numberOfHiddenTransactionsAbove: number; traceInfo: TraceInfo; transactionGroups: React.ReactNode[]; } = { index: 1, numberOfHiddenTransactionsAbove: 0, traceInfo, transactionGroups: [], }; let lastIndex: number = 0; const {transactionGroups, numberOfHiddenTransactionsAbove} = traces.reduce( (acc, trace, index) => { const isLastTransaction = index === traces.length - 1; const hasChildren = trace.children.length > 0; const isNextChildOrphaned = !isLastTransaction && traces[index + 1].parent_span_id !== null; const result = renderTransaction(trace, { ...acc, // if the root of a subtrace has a parent_span_id, then it must be an orphan isOrphan: !isRootTransaction(trace), isLast: isLastTransaction && !hasOrphanErrors, continuingDepths: (!isLastTransaction && hasChildren) || hasOrphanErrors ? [{depth: 0, isOrphanDepth: isNextChildOrphaned || Boolean(hasOrphanErrors)}] : [], hasGuideAnchor: index === 0, }); acc.index = result.lastIndex + 1; lastIndex = Math.max(lastIndex, result.lastIndex); acc.numberOfHiddenTransactionsAbove = result.numberOfHiddenTransactionsAbove; acc.transactionGroups.push(result.transactionGroup); return acc; }, accumulator ); // Build transaction groups for orphan errors let numOfHiddenErrorsAbove = 0; let totalNumOfHiddenErrors = 0; if (hasOrphanErrors) { orphanErrors.forEach((error, index) => { const isLastError = index === orphanErrors.length - 1; const isVisible = isRowVisible(error, filteredEventIds); const currentHiddenCount = numOfHiddenErrorsAbove; if (!isVisible) { numOfHiddenErrorsAbove += 1; totalNumOfHiddenErrors += 1; } else { numOfHiddenErrorsAbove = 0; } transactionGroups.push( 0 ? currentHiddenCount : 0} /> 0 ? getMeasurements(traces[0], generateBounds(traceInfo)) : undefined } continuingDepths={[]} isOrphan isLast={isLastError} index={lastIndex + index + 1} isVisible={isVisible} hasGuideAnchor={index === 0 && transactionGroups.length === 0} renderedChildren={[]} /> ); }); } const bounds = generateBounds(traceInfo); const measurements = traces.length > 0 && Object.keys(traces[0].measurements ?? {}).length > 0 ? getMeasurements(traces[0], bounds) : undefined; const traceView = ( {({dividerPosition}) => ( {({virtualScrollbarRef, scrollBarAreaRef, onDragStart, onScroll}) => { return (
); }} {measurements ? ( ) : null} )} ); sentrySpan?.end(); return traceView; } export const StyledTracePanel = styled(Panel)` height: 100%; overflow-x: visible; ${TraceViewContainer} { overflow-x: visible; } `;