From 7bff424afd685ce97c81ce00b4ba20761dc51cdb Mon Sep 17 00:00:00 2001 From: JonasBa Date: Mon, 23 Sep 2024 11:41:49 -0400 Subject: [PATCH 1/8] ref(trace) break up the rendering logic ref(trace) break up the rendering logic --- .../performance/newTraceDetails/trace.tsx | 1128 +---------------- .../traceRow/traceAutogroupedRow.tsx | 92 ++ .../traceRow/traceBackgroundPatterns.tsx | 98 ++ .../newTraceDetails/traceRow/traceBar.tsx | 281 ++++ .../traceRow/traceErrorRow.tsx | 91 ++ .../newTraceDetails/traceRow/traceIcons.tsx | 99 ++ .../traceRow/traceLoadingRow.tsx | 101 ++ .../traceMissingInstrumentationRow.tsx | 72 ++ .../traceRow/traceRootNode.tsx | 101 ++ .../newTraceDetails/traceRow/traceRow.tsx | 140 ++ .../newTraceDetails/traceRow/traceSpanRow.tsx | 103 ++ .../traceRow/traceTransactionRow.tsx | 110 ++ 12 files changed, 1313 insertions(+), 1103 deletions(-) create mode 100644 static/app/views/performance/newTraceDetails/traceRow/traceAutogroupedRow.tsx create mode 100644 static/app/views/performance/newTraceDetails/traceRow/traceBackgroundPatterns.tsx create mode 100644 static/app/views/performance/newTraceDetails/traceRow/traceBar.tsx create mode 100644 static/app/views/performance/newTraceDetails/traceRow/traceErrorRow.tsx create mode 100644 static/app/views/performance/newTraceDetails/traceRow/traceIcons.tsx create mode 100644 static/app/views/performance/newTraceDetails/traceRow/traceLoadingRow.tsx create mode 100644 static/app/views/performance/newTraceDetails/traceRow/traceMissingInstrumentationRow.tsx create mode 100644 static/app/views/performance/newTraceDetails/traceRow/traceRootNode.tsx create mode 100644 static/app/views/performance/newTraceDetails/traceRow/traceRow.tsx create mode 100644 static/app/views/performance/newTraceDetails/traceRow/traceSpanRow.tsx create mode 100644 static/app/views/performance/newTraceDetails/traceRow/traceTransactionRow.tsx diff --git a/static/app/views/performance/newTraceDetails/trace.tsx b/static/app/views/performance/newTraceDetails/trace.tsx index d9764b9af140e6..369b2770fc1cd0 100644 --- a/static/app/views/performance/newTraceDetails/trace.tsx +++ b/static/app/views/performance/newTraceDetails/trace.tsx @@ -10,23 +10,14 @@ import { } from 'react'; import {type Theme, useTheme} from '@emotion/react'; import styled from '@emotion/styled'; -import * as Sentry from '@sentry/react'; -import {PlatformIcon} from 'platformicons'; +import Sentry from '@sentry/react'; -import LoadingIndicator from 'sentry/components/loadingIndicator'; -import Placeholder from 'sentry/components/placeholder'; -import {t} from 'sentry/locale'; import ConfigStore from 'sentry/stores/configStore'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types/organization'; import type {PlatformKey, Project} from 'sentry/types/project'; import {trackAnalytics} from 'sentry/utils/analytics'; import {formatTraceDuration} from 'sentry/utils/duration/formatTraceDuration'; -import type { - TraceError, - TracePerformanceIssue, -} from 'sentry/utils/performance/quickTrace/types'; -import {clamp} from 'sentry/utils/profiling/colors/utils'; import {replayPlayerTimestampEmitter} from 'sentry/utils/replays/replayPlayerTimestampEmitter'; import useApi from 'sentry/utils/useApi'; import useOrganization from 'sentry/utils/useOrganization'; @@ -40,34 +31,22 @@ import { type VirtualizedRow, } from 'sentry/views/performance/newTraceDetails/traceRenderers/traceVirtualizedList'; import type {VirtualizedViewManager} from 'sentry/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager'; +import {TraceLoadingRow} from 'sentry/views/performance/newTraceDetails/traceRow/traceLoadingRow'; +import { + TRACE_CHILDREN_COUNT_WRAPPER_CLASSNAME, + TRACE_CHILDREN_COUNT_WRAPPER_ORPHANED_CLASSNAME, + TRACE_RIGHT_COLUMN_EVEN_CLASSNAME, + TRACE_RIGHT_COLUMN_ODD_CLASSNAME, +} from 'sentry/views/performance/newTraceDetails/traceRow/traceRow'; import type {TraceReducerState} from 'sentry/views/performance/newTraceDetails/traceState'; import { getRovingIndexActionFromDOMEvent, type RovingTabIndexUserActions, } from 'sentry/views/performance/newTraceDetails/traceState/traceRovingTabIndex'; -import { - makeTraceNodeBarColor, - ParentAutogroupNode, - TraceTree, - type TraceTreeNode, -} from './traceModels/traceTree'; +import {TraceTree, type TraceTreeNode} from './traceModels/traceTree'; import {useTraceState, useTraceStateDispatch} from './traceState/traceStateProvider'; -import { - isAutogroupedNode, - isMissingInstrumentationNode, - isParentAutogroupedNode, - isSpanNode, - isTraceErrorNode, - isTraceNode, - isTransactionNode, -} from './guards'; -import {TraceIcons} from './icons'; - -const COUNT_FORMATTER = Intl.NumberFormat(undefined, {notation: 'compact'}); -const NO_ERRORS = new Set(); -const NO_PERFORMANCE_ISSUES = new Set(); -const NO_PROFILES = []; +import {isSpanNode, isTransactionNode} from './guards'; function computeNextIndexFromAction( current_index: number, @@ -94,53 +73,6 @@ function computeNextIndexFromAction( } } -function getMaxErrorSeverity(errors: TraceTree.TraceError[]) { - return errors.reduce((acc, error) => { - if (error.level === 'fatal') { - return 'fatal'; - } - if (error.level === 'error') { - return acc === 'fatal' ? 'fatal' : 'error'; - } - if (error.level === 'warning') { - return acc === 'fatal' || acc === 'error' ? acc : 'warning'; - } - return acc; - }, 'default'); -} - -const RIGHT_COLUMN_EVEN_CLASSNAME = `TraceRightColumn`; -const RIGHT_COLUMN_ODD_CLASSNAME = [RIGHT_COLUMN_EVEN_CLASSNAME, 'Odd'].join(' '); -const CHILDREN_COUNT_WRAPPER_CLASSNAME = `TraceChildrenCountWrapper`; -const CHILDREN_COUNT_WRAPPER_ORPHANED_CLASSNAME = [ - CHILDREN_COUNT_WRAPPER_CLASSNAME, - 'Orphaned', -].join(' '); - -const ERROR_LEVEL_LABELS: Record = { - sample: t('Sample'), - info: t('Info'), - warning: t('Warning'), - // Hardcoded legacy color (orange400). We no longer use orange anywhere - // else in the app (except for the chart palette). This needs to be harcoded - // here because existing users may still associate orange with the "error" level. - error: t('Error'), - fatal: t('Fatal'), - default: t('Default'), - unknown: t('Unknown'), -}; - -function maybeFocusRow( - ref: HTMLDivElement | null, - node: TraceTreeNode, - previouslyFocusedNodeRef: React.MutableRefObject | null> -) { - if (!ref) return; - if (node === previouslyFocusedNodeRef.current) return; - previouslyFocusedNodeRef.current = node; - ref.focus(); -} - interface TraceProps { forceRerender: number; initializedRef: React.MutableRefObject; @@ -422,7 +354,7 @@ export function Trace({ const renderLoadingRow = useCallback( (n: VirtualizedRow) => { return ( - { return ( - ) => { - onRowClickProp(props.node, event, props.index); + props.onRowClick(props.node, event, props.index); }, - [props.index, props.node, onRowClickProp] + [props.index, props.node, props.onRowClick] ); - const onKeyDownProp = props.onRowKeyDown; const onRowKeyDown = useCallback( - event => onKeyDownProp(event, props.index, props.node), - [props.index, props.node, onKeyDownProp] + event => props.onRowKeyDown(event, props.index, props.node), + [props.index, props.node, props.onRowKeyDown] ); const onRowDoubleClick = useCallback( @@ -677,12 +607,11 @@ function RenderRow(props: { [props.node.space, props.manager] ); - const onExpandProp = props.onExpand; - const onExpandClick = useCallback( + const onExpand = useCallback( (e: React.MouseEvent) => { - onExpandProp(e, props.node, !props.node.expanded); + props.onExpand(e, props.node, !props.node.expanded); }, - [props.node, onExpandProp] + [props.node, props.onExpand] ); const onExpandDoubleClick = useCallback((e: React.MouseEvent) => { @@ -690,1028 +619,21 @@ function RenderRow(props: { }, []); const spanColumnClassName = - props.index % 2 === 1 ? RIGHT_COLUMN_ODD_CLASSNAME : RIGHT_COLUMN_EVEN_CLASSNAME; + props.index % 2 === 1 + ? TRACE_RIGHT_COLUMN_ODD_CLASSNAME + : TRACE_RIGHT_COLUMN_EVEN_CLASSNAME; const listColumnClassName = props.node.isOrphaned - ? CHILDREN_COUNT_WRAPPER_ORPHANED_CLASSNAME - : CHILDREN_COUNT_WRAPPER_CLASSNAME; + ? TRACE_CHILDREN_COUNT_WRAPPER_ORPHANED_CLASSNAME + : TRACE_CHILDREN_COUNT_WRAPPER_CLASSNAME; const listColumnStyle: React.CSSProperties = { paddingLeft: props.node.depth * props.manager.row_depth_padding, }; - if (isAutogroupedNode(props.node)) { - return ( -
- props.tabIndex === 0 && !props.isEmbedded - ? maybeFocusRow(r, props.node, props.previouslyFocusedNodeRef) - : null - } - tabIndex={props.tabIndex} - className={`Autogrouped TraceRow ${rowSearchClassName} ${props.node.has_errors ? props.node.max_severity : ''}`} - onClick={onRowClick} - onKeyDown={onRowKeyDown} - style={props.style} - > -
-
-
- - - } - status={props.node.fetchStatus} - expanded={!props.node.expanded} - onClick={onExpandClick} - onDoubleClick={onExpandDoubleClick} - > - {COUNT_FORMATTER.format(props.node.groupCount)} - -
- - {t('Autogrouped')} - - {props.node.value.autogrouped_by.op} -
-
-
- - -
-
- ); - } - - if (isTransactionNode(props.node)) { - return ( -
- props.tabIndex === 0 && !props.isEmbedded - ? maybeFocusRow(r, props.node, props.previouslyFocusedNodeRef) - : null - } - tabIndex={props.tabIndex} - className={`TraceRow ${rowSearchClassName} ${props.node.has_errors ? props.node.max_severity : ''}`} - onKeyDown={onRowKeyDown} - onClick={onRowClick} - style={props.style} - > -
-
-
- - {props.node.children.length > 0 || props.node.canFetch ? ( - - ) : ( - '+' - ) - ) : ( - - ) - } - status={props.node.fetchStatus} - expanded={props.node.expanded || props.node.zoomedIn} - onDoubleClick={onExpandDoubleClick} - onClick={e => { - props.node.canFetch - ? props.onZoomIn(e, props.node, !props.node.zoomedIn) - : props.onExpand(e, props.node, !props.node.expanded); - }} - > - {props.node.children.length > 0 - ? COUNT_FORMATTER.format(props.node.children.length) - : null} - - ) : null} -
- - {props.node.value['transaction.op']} - - {props.node.value.transaction} -
-
-
- - -
-
- ); - } - - if (isSpanNode(props.node)) { - return ( -
- props.tabIndex === 0 && !props.isEmbedded - ? maybeFocusRow(r, props.node, props.previouslyFocusedNodeRef) - : null - } - tabIndex={props.tabIndex} - className={`TraceRow ${rowSearchClassName} ${props.node.has_errors ? props.node.max_severity : ''}`} - onClick={onRowClick} - onKeyDown={onRowKeyDown} - style={props.style} - > -
-
-
- - {props.node.children.length > 0 || props.node.canFetch ? ( - - ) - } - status={props.node.fetchStatus} - expanded={props.node.expanded || props.node.zoomedIn} - onDoubleClick={onExpandDoubleClick} - onClick={e => - props.node.canFetch - ? props.onZoomIn(e, props.node, !props.node.zoomedIn) - : props.onExpand(e, props.node, !props.node.expanded) - } - > - {props.node.children.length > 0 - ? COUNT_FORMATTER.format(props.node.children.length) - : null} - - ) : null} -
- {props.node.value.op ?? ''} - - - {!props.node.value.description - ? props.node.value.span_id ?? 'unknown' - : props.node.value.description.length > 100 - ? props.node.value.description.slice(0, 100).trim() + '\u2026' - : props.node.value.description} - -
-
-
- - -
-
- ); - } - - if (isMissingInstrumentationNode(props.node)) { - return ( -
- props.tabIndex === 0 && !props.isEmbedded - ? maybeFocusRow(r, props.node, props.previouslyFocusedNodeRef) - : null - } - tabIndex={props.tabIndex} - className={`TraceRow ${rowSearchClassName}`} - onClick={onRowClick} - onKeyDown={onRowKeyDown} - style={props.style} - > -
-
-
- -
- {t('Missing instrumentation')} -
-
-
- - -
-
- ); - } - - if (isTraceNode(props.node)) { - return ( -
- props.tabIndex === 0 && !props.isEmbedded - ? maybeFocusRow(r, props.node, props.previouslyFocusedNodeRef) - : null - } - tabIndex={props.tabIndex} - className={`TraceRow ${rowSearchClassName} ${props.node.has_errors ? props.node.max_severity : ''}`} - onClick={onRowClick} - onKeyDown={onRowKeyDown} - style={props.style} - > -
-
- {' '} -
- - {props.node.children.length > 0 || props.node.canFetch ? ( - void 0} - onDoubleClick={onExpandDoubleClick} - > - {props.node.fetchStatus === 'loading' - ? null - : props.node.children.length > 0 - ? COUNT_FORMATTER.format(props.node.children.length) - : null} - - ) : null} -
- {t('Trace')} - {props.trace_id ? ( - - - {props.trace_id} - - ) : null} -
-
-
- - -
-
- ); - } - - if (isTraceErrorNode(props.node)) { - return ( -
- props.tabIndex === 0 && !props.isEmbedded - ? maybeFocusRow(r, props.node, props.previouslyFocusedNodeRef) - : null - } - tabIndex={props.tabIndex} - className={`TraceRow ${rowSearchClassName} ${props.node.max_severity}`} - onClick={onRowClick} - onKeyDown={onRowKeyDown} - style={props.style} - > -
-
-
- {' '} -
- - - {ERROR_LEVEL_LABELS[props.node.value.level ?? 'error']} - - - - {props.node.value.message ?? props.node.value.title} - -
-
-
- - {typeof props.node.value.timestamp === 'number' ? ( -
- -
- ) : null} -
-
-
- ); - } - return null; } -function RenderPlaceholderRow(props: { - index: number; - manager: VirtualizedViewManager; - node: TraceTreeNode; - style: React.CSSProperties; - theme: Theme; -}) { - return ( -
-
-
-
- - {props.node.children.length > 0 || props.node.canFetch ? ( - void 0} - onDoubleClick={() => void 0} - > - {props.node.children.length > 0 - ? COUNT_FORMATTER.format(props.node.children.length) - : null} - - ) : null} -
- -
-
-
- -
-
- ); -} - -function randomBetween(min: number, max: number) { - return Math.floor(Math.random() * (max - min + 1) + min); -} - -function Connectors(props: { - manager: VirtualizedViewManager; - node: TraceTreeNode; -}) { - const hasChildren = - (props.node.expanded || props.node.zoomedIn) && props.node.children.length > 0; - const showVerticalConnector = - hasChildren || (props.node.value && isParentAutogroupedNode(props.node)); - - // If the tail node of the collapsed node has no children, - // we don't want to render the vertical connector as no children - // are being rendered as the chain is entirely collapsed - const hideVerticalConnector = - showVerticalConnector && - props.node.value && - props.node instanceof ParentAutogroupNode && - (!props.node.tail.children.length || - (!props.node.tail.expanded && !props.node.expanded)); - - return ( - - {props.node.connectors.map((c, i) => { - return ( - - ); - })} - {showVerticalConnector && !hideVerticalConnector ? ( - - ) : null} - {props.node.isLastChild ? ( - - ) : ( - - )} - - ); -} - -function ChildrenButton(props: { - children: React.ReactNode; - expanded: boolean; - icon: React.ReactNode; - onClick: (e: React.MouseEvent) => void; - onDoubleClick: (e: React.MouseEvent) => void; - status: TraceTreeNode['fetchStatus'] | undefined; -}) { - return ( - - ); -} - -interface TraceBarProps { - color: string; - errors: TraceTreeNode['errors']; - manager: VirtualizedViewManager; - node_space: [number, number] | null; - performance_issues: TraceTreeNode['performance_issues']; - profiles: TraceTreeNode['profiles']; - virtualized_index: number; -} - -function TraceBar(props: TraceBarProps) { - const duration = props.node_space ? formatTraceDuration(props.node_space[1]) : null; - - const registerSpanBarRef = useCallback( - (ref: HTMLDivElement | null) => { - props.manager.registerSpanBarRef( - ref, - props.node_space!, - props.color, - props.virtualized_index - ); - }, - [props.manager, props.node_space, props.color, props.virtualized_index] - ); - - const registerSpanBarTextRef = useCallback( - (ref: HTMLDivElement | null) => { - props.manager.registerSpanBarTextRef( - ref, - duration!, - props.node_space!, - props.virtualized_index - ); - }, - [props.manager, props.node_space, props.virtualized_index, duration] - ); - - if (!props.node_space) { - return null; - } - - return ( - -
- {props.errors.size > 0 ? ( - - ) : null} - {props.performance_issues.size > 0 ? ( - - ) : null} - {props.performance_issues.size > 0 || - props.errors.size > 0 || - props.profiles.length > 0 ? ( - - ) : null} -
-
- {duration} -
-
- ); -} - -interface MissingInstrumentationTraceBarProps { - color: string; - manager: VirtualizedViewManager; - node_space: [number, number] | null; - virtualized_index: number; -} -function MissingInstrumentationTraceBar(props: MissingInstrumentationTraceBarProps) { - const duration = props.node_space ? formatTraceDuration(props.node_space[1]) : null; - - const registerSpanBarRef = useCallback( - (ref: HTMLDivElement | null) => { - props.manager.registerSpanBarRef( - ref, - props.node_space!, - props.color, - props.virtualized_index - ); - }, - [props.manager, props.node_space, props.color, props.virtualized_index] - ); - - const registerSpanBarTextRef = useCallback( - (ref: HTMLDivElement | null) => { - props.manager.registerSpanBarTextRef( - ref, - duration!, - props.node_space!, - props.virtualized_index - ); - }, - [props.manager, props.node_space, props.virtualized_index, duration] - ); - - return ( - -
-
-
-
-
-
- {duration} -
- - ); -} - -interface InvisibleTraceBarProps { - children: React.ReactNode; - manager: VirtualizedViewManager; - node_space: [number, number] | null; - virtualizedIndex: number; -} - -function InvisibleTraceBar(props: InvisibleTraceBarProps) { - const registerInvisibleBarRef = useCallback( - (ref: HTMLDivElement | null) => { - props.manager.registerInvisibleBarRef( - ref, - props.node_space!, - props.virtualizedIndex - ); - }, - [props.manager, props.node_space, props.virtualizedIndex] - ); - - const onDoubleClick = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation(); - props.manager.onZoomIntoSpace(props.node_space!); - }, - [props.manager, props.node_space] - ); - - if (!props.node_space || !props.children) { - return null; - } - - return ( -
- {props.children} -
- ); -} - -interface BackgroundPatternsProps { - errors: TraceTreeNode['errors']; - manager: VirtualizedViewManager; - node_space: [number, number] | null; - performance_issues: TraceTreeNode['performance_issues']; -} - -function BackgroundPatterns(props: BackgroundPatternsProps) { - const performance_issues = useMemo(() => { - if (!props.performance_issues.size) return []; - return [...props.performance_issues]; - }, [props.performance_issues]); - - const errors = useMemo(() => { - if (!props.errors.size) return []; - return [...props.errors]; - }, [props.errors]); - - const severity = useMemo(() => { - return getMaxErrorSeverity(errors); - }, [errors]); - - if (!props.performance_issues.size && !props.errors.size) { - return null; - } - - // If there is an error, render the error pattern across the entire width. - // Else if there is a performance issue, render the performance issue pattern - // for the duration of the performance issue. If there is a profile, render - // the profile pattern for entire duration (we do not have profile durations here) - return ( - - {errors.length > 0 ? ( -
-
-
- ) : performance_issues.length > 0 ? ( - - {performance_issues.map((issue, i) => { - const timestamp = issue.start * 1e3; - // Clamp the issue timestamp to the span's timestamp - const left = props.manager.computeRelativeLeftPositionFromOrigin( - clamp( - timestamp, - props.node_space![0], - props.node_space![0] + props.node_space![1] - ), - props.node_space! - ); - - return ( -
-
-
- ); - })} - - ) : null} - - ); -} - -interface ErrorIconsProps { - errors: TraceTreeNode['errors']; - manager: VirtualizedViewManager; - node_space: [number, number] | null; -} - -function ErrorIcons(props: ErrorIconsProps) { - const errors = useMemo(() => { - return [...props.errors]; - }, [props.errors]); - - if (!props.errors.size) { - return null; - } - - return ( - - {errors.map((error, i) => { - const timestamp = error.timestamp ? error.timestamp * 1e3 : props.node_space![0]; - // Clamp the error timestamp to the span's timestamp - const left = props.manager.computeRelativeLeftPositionFromOrigin( - clamp( - timestamp, - props.node_space![0], - props.node_space![0] + props.node_space![1] - ), - props.node_space! - ); - - return ( -
- -
- ); - })} -
- ); -} - -interface PerformanceIssueIconsProps { - manager: VirtualizedViewManager; - node_space: [number, number] | null; - performance_issues: TraceTreeNode['performance_issues']; -} - -function PerformanceIssueIcons(props: PerformanceIssueIconsProps) { - const performance_issues = useMemo(() => { - return [...props.performance_issues]; - }, [props.performance_issues]); - - if (!props.performance_issues.size) { - return null; - } - - return ( - - {performance_issues.map((issue, i) => { - const timestamp = issue.timestamp - ? issue.timestamp * 1e3 - : issue.start - ? issue.start * 1e3 - : props.node_space![0]; - // Clamp the issue timestamp to the span's timestamp - const left = props.manager.computeRelativeLeftPositionFromOrigin( - clamp( - timestamp, - props.node_space![0], - props.node_space![0] + props.node_space![1] - ), - props.node_space! - ); - - return ( -
- -
- ); - })} -
- ); -} - -interface AutogroupedTraceBarProps { - color: string; - entire_space: [number, number] | null; - errors: TraceTreeNode['errors']; - manager: VirtualizedViewManager; - node_spaces: [number, number][]; - performance_issues: TraceTreeNode['performance_issues']; - profiles: TraceTreeNode['profiles']; - virtualized_index: number; -} - -function AutogroupedTraceBar(props: AutogroupedTraceBarProps) { - const duration = props.entire_space ? formatTraceDuration(props.entire_space[1]) : null; - - const registerInvisibleBarRef = useCallback( - (ref: HTMLDivElement | null) => { - props.manager.registerInvisibleBarRef( - ref, - props.entire_space!, - props.virtualized_index - ); - }, - [props.manager, props.entire_space, props.virtualized_index] - ); - - const registerAutogroupedSpanBarTextRef = useCallback( - (ref: HTMLDivElement | null) => { - props.manager.registerSpanBarTextRef( - ref, - duration!, - props.entire_space!, - props.virtualized_index - ); - }, - [props.manager, props.entire_space, props.virtualized_index, duration] - ); - - if (props.node_spaces && props.node_spaces.length <= 1) { - return ( - - ); - } - - if (!props.node_spaces || !props.entire_space) { - return null; - } - - return ( - -
- {props.node_spaces.map((node_space, i) => { - const width = node_space[1] / props.entire_space![1]; - const left = props.manager.computeRelativeLeftPositionFromOrigin( - node_space[0], - props.entire_space! - ); - return ( -
- ); - })} - {/* Autogrouped bars only render icons. That is because in the case of multiple bars - with tiny gaps, the background pattern looks broken as it does not repeat nicely */} - {props.errors.size > 0 ? ( - - ) : null} - {props.performance_issues.size > 0 ? ( - - ) : null} -
-
- {duration} -
- - ); -} - function VerticalTimestampIndicators({ viewmanager, traceStartTimestamp, diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceAutogroupedRow.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceAutogroupedRow.tsx new file mode 100644 index 00000000000000..7b3f35f045da89 --- /dev/null +++ b/static/app/views/performance/newTraceDetails/traceRow/traceAutogroupedRow.tsx @@ -0,0 +1,92 @@ +import {t} from 'sentry/locale'; +import {isAutogroupedNode} from 'sentry/views/performance/newTraceDetails/guards'; +import {TraceIcons} from 'sentry/views/performance/newTraceDetails/icons'; +import { + makeTraceNodeBarColor, + type ParentAutogroupNode, + type SiblingAutogroupNode, +} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree'; +import {AutogroupedTraceBar} from 'sentry/views/performance/newTraceDetails/traceRow/traceBar'; +import { + maybeFocusTraceRow, + TRACE_COUNT_FORMATTER, + TraceChildrenButton, + TraceRowConnectors, + type TraceRowProps, +} from 'sentry/views/performance/newTraceDetails/traceRow/traceRow'; + +export function TraceAutogroupedRow( + props: TraceRowProps +) { + if (!isAutogroupedNode(props.node)) { + throw new Error( + 'Called autogrouped renderer for a node that is not of type autogroped' + ); + } + + return ( +
+ props.tabIndex === 0 && !props.isEmbedded + ? maybeFocusTraceRow(r, props.node, props.previouslyFocusedNodeRef) + : null + } + tabIndex={props.tabIndex} + className={`Autogrouped TraceRow ${props.rowSearchClassName} ${props.node.has_errors ? props.node.max_severity : ''}`} + onClick={props.onRowClick} + onKeyDown={props.onRowKeyDown} + style={props.style} + > +
+
+
+ + + } + status={props.node.fetchStatus} + expanded={!props.node.expanded} + onClick={props.onExpand} + onDoubleClick={props.onExpandDoubleClick} + > + {TRACE_COUNT_FORMATTER.format(props.node.groupCount)} + +
+ + {t('Autogrouped')} + + {props.node.value.autogrouped_by.op} +
+
+
+ + +
+
+ ); +} diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceBackgroundPatterns.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceBackgroundPatterns.tsx new file mode 100644 index 00000000000000..fc5fe509aac090 --- /dev/null +++ b/static/app/views/performance/newTraceDetails/traceRow/traceBackgroundPatterns.tsx @@ -0,0 +1,98 @@ +import {Fragment, useMemo} from 'react'; +import clamp from 'lodash/clamp'; + +import type { + TraceTree, + TraceTreeNode, +} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree'; +import type {VirtualizedViewManager} from 'sentry/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager'; + +function getMaxErrorSeverity(errors: TraceTree.TraceError[]) { + return errors.reduce((acc, error) => { + if (error.level === 'fatal') { + return 'fatal'; + } + if (error.level === 'error') { + return acc === 'fatal' ? 'fatal' : 'error'; + } + if (error.level === 'warning') { + return acc === 'fatal' || acc === 'error' ? acc : 'warning'; + } + return acc; + }, 'default'); +} + +interface BackgroundPatternsProps { + errors: TraceTreeNode['errors']; + manager: VirtualizedViewManager; + node_space: [number, number] | null; + performance_issues: TraceTreeNode['performance_issues']; +} + +export function TraceBackgroundPatterns(props: BackgroundPatternsProps) { + const performance_issues = useMemo(() => { + if (!props.performance_issues.size) return []; + return [...props.performance_issues]; + }, [props.performance_issues]); + + const errors = useMemo(() => { + if (!props.errors.size) return []; + return [...props.errors]; + }, [props.errors]); + + const severity = useMemo(() => { + return getMaxErrorSeverity(errors); + }, [errors]); + + if (!props.performance_issues.size && !props.errors.size) { + return null; + } + + // If there is an error, render the error pattern across the entire width. + // Else if there is a performance issue, render the performance issue pattern + // for the duration of the performance issue. If there is a profile, render + // the profile pattern for entire duration (we do not have profile durations here) + return ( + + {errors.length > 0 ? ( +
+
+
+ ) : performance_issues.length > 0 ? ( + + {performance_issues.map((issue, i) => { + const timestamp = issue.start * 1e3; + // Clamp the issue timestamp to the span's timestamp + const left = props.manager.computeRelativeLeftPositionFromOrigin( + clamp( + timestamp, + props.node_space![0], + props.node_space![0] + props.node_space![1] + ), + props.node_space! + ); + + return ( +
+
+
+ ); + })} + + ) : null} + + ); +} diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceBar.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceBar.tsx new file mode 100644 index 00000000000000..54ff94bd8b0773 --- /dev/null +++ b/static/app/views/performance/newTraceDetails/traceRow/traceBar.tsx @@ -0,0 +1,281 @@ +import {Fragment, useCallback} from 'react'; + +import {formatTraceDuration} from 'sentry/utils/duration/formatTraceDuration'; +import type { + TraceTree, + TraceTreeNode, +} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree'; +import type {VirtualizedViewManager} from 'sentry/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager'; +import {TraceBackgroundPatterns} from 'sentry/views/performance/newTraceDetails/traceRow/traceBackgroundPatterns'; +import { + TraceErrorIcons, + TracePerformanceIssueIcons, +} from 'sentry/views/performance/newTraceDetails/traceRow/traceIcons'; + +interface InvisibleTraceBarProps { + children: React.ReactNode; + manager: VirtualizedViewManager; + node_space: [number, number] | null; + virtualizedIndex: number; +} + +export function InvisibleTraceBar(props: InvisibleTraceBarProps) { + const registerInvisibleBarRef = useCallback( + (ref: HTMLDivElement | null) => { + props.manager.registerInvisibleBarRef( + ref, + props.node_space!, + props.virtualizedIndex + ); + }, + [props.manager, props.node_space, props.virtualizedIndex] + ); + + const onDoubleClick = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + props.manager.onZoomIntoSpace(props.node_space!); + }, + [props.manager, props.node_space] + ); + + if (!props.node_space || !props.children) { + return null; + } + + return ( +
+ {props.children} +
+ ); +} + +interface MissingInstrumentationTraceBarProps { + color: string; + manager: VirtualizedViewManager; + node_space: [number, number] | null; + virtualized_index: number; +} + +export function MissingInstrumentationTraceBar( + props: MissingInstrumentationTraceBarProps +) { + const duration = props.node_space ? formatTraceDuration(props.node_space[1]) : null; + + const registerSpanBarRef = useCallback( + (ref: HTMLDivElement | null) => { + props.manager.registerSpanBarRef( + ref, + props.node_space!, + props.color, + props.virtualized_index + ); + }, + [props.manager, props.node_space, props.color, props.virtualized_index] + ); + + const registerSpanBarTextRef = useCallback( + (ref: HTMLDivElement | null) => { + props.manager.registerSpanBarTextRef( + ref, + duration!, + props.node_space!, + props.virtualized_index + ); + }, + [props.manager, props.node_space, props.virtualized_index, duration] + ); + + return ( + +
+
+
+
+
+
+ {duration} +
+ + ); +} + +interface TraceBarProps { + color: string; + errors: TraceTreeNode['errors']; + manager: VirtualizedViewManager; + node_space: [number, number] | null; + performance_issues: TraceTreeNode['performance_issues']; + profiles: TraceTreeNode['profiles']; + virtualized_index: number; +} + +export function TraceBar(props: TraceBarProps) { + const duration = props.node_space ? formatTraceDuration(props.node_space[1]) : null; + + const registerSpanBarRef = useCallback( + (ref: HTMLDivElement | null) => { + props.manager.registerSpanBarRef( + ref, + props.node_space!, + props.color, + props.virtualized_index + ); + }, + [props.manager, props.node_space, props.color, props.virtualized_index] + ); + + const registerSpanBarTextRef = useCallback( + (ref: HTMLDivElement | null) => { + props.manager.registerSpanBarTextRef( + ref, + duration!, + props.node_space!, + props.virtualized_index + ); + }, + [props.manager, props.node_space, props.virtualized_index, duration] + ); + + if (!props.node_space) { + return null; + } + + return ( + +
+ {props.errors.size > 0 ? ( + + ) : null} + {props.performance_issues.size > 0 ? ( + + ) : null} + {props.performance_issues.size > 0 || + props.errors.size > 0 || + props.profiles.length > 0 ? ( + + ) : null} +
+
+ {duration} +
+
+ ); +} + +interface AutogroupedTraceBarProps { + color: string; + entire_space: [number, number] | null; + errors: TraceTreeNode['errors']; + manager: VirtualizedViewManager; + node_spaces: [number, number][]; + performance_issues: TraceTreeNode['performance_issues']; + profiles: TraceTreeNode['profiles']; + virtualized_index: number; +} + +export function AutogroupedTraceBar(props: AutogroupedTraceBarProps) { + const duration = props.entire_space ? formatTraceDuration(props.entire_space[1]) : null; + + const registerInvisibleBarRef = useCallback( + (ref: HTMLDivElement | null) => { + props.manager.registerInvisibleBarRef( + ref, + props.entire_space!, + props.virtualized_index + ); + }, + [props.manager, props.entire_space, props.virtualized_index] + ); + + const registerAutogroupedSpanBarTextRef = useCallback( + (ref: HTMLDivElement | null) => { + props.manager.registerSpanBarTextRef( + ref, + duration!, + props.entire_space!, + props.virtualized_index + ); + }, + [props.manager, props.entire_space, props.virtualized_index, duration] + ); + + if (props.node_spaces && props.node_spaces.length <= 1) { + return ( + + ); + } + + if (!props.node_spaces || !props.entire_space) { + return null; + } + + return ( + +
+ {props.node_spaces.map((node_space, i) => { + const width = node_space[1] / props.entire_space![1]; + const left = props.manager.computeRelativeLeftPositionFromOrigin( + node_space[0], + props.entire_space! + ); + return ( +
+ ); + })} + {/* Autogrouped bars only render icons. That is because in the case of multiple bars + with tiny gaps, the background pattern looks broken as it does not repeat nicely */} + {props.errors.size > 0 ? ( + + ) : null} + {props.performance_issues.size > 0 ? ( + + ) : null} +
+
+ {duration} +
+ + ); +} diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceErrorRow.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceErrorRow.tsx new file mode 100644 index 00000000000000..5e7c55aca49848 --- /dev/null +++ b/static/app/views/performance/newTraceDetails/traceRow/traceErrorRow.tsx @@ -0,0 +1,91 @@ +import type {Theme} from '@emotion/react'; +import {PlatformIcon} from 'platformicons'; + +import {t} from 'sentry/locale'; +import {isTraceErrorNode} from 'sentry/views/performance/newTraceDetails/guards'; +import {TraceIcons} from 'sentry/views/performance/newTraceDetails/icons'; +import type { + TraceTree, + TraceTreeNode, +} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree'; +import {InvisibleTraceBar} from 'sentry/views/performance/newTraceDetails/traceRow/traceBar'; +import { + maybeFocusTraceRow, + TraceRowConnectors, + type TraceRowProps, +} from 'sentry/views/performance/newTraceDetails/traceRow/traceRow'; + +const ERROR_LEVEL_LABELS: Record = { + sample: t('Sample'), + info: t('Info'), + warning: t('Warning'), + // Hardcoded legacy color (orange400). We no longer use orange anywhere + // else in the app (except for the chart palette). This needs to be harcoded + // here because existing users may still associate orange with the "error" level. + error: t('Error'), + fatal: t('Fatal'), + default: t('Default'), + unknown: t('Unknown'), +}; + +export function TraceErrorRow(props: TraceRowProps>) { + if (!isTraceErrorNode(props.node)) { + throw new Error( + 'Trace error row renderer called on a row that is not trace error type' + ); + } + return ( +
+ props.tabIndex === 0 && !props.isEmbedded + ? maybeFocusTraceRow(r, props.node, props.previouslyFocusedNodeRef) + : null + } + tabIndex={props.tabIndex} + className={`TraceRow ${props.rowSearchClassName} ${props.node.max_severity}`} + onClick={props.onRowClick} + onKeyDown={props.onRowKeyDown} + style={props.style} + > +
+
+
+ {' '} +
+ + + {ERROR_LEVEL_LABELS[props.node.value.level ?? 'error']} + + + + {props.node.value.message ?? props.node.value.title} + +
+
+
+ + {typeof props.node.value.timestamp === 'number' ? ( +
+ +
+ ) : null} +
+
+
+ ); +} diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceIcons.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceIcons.tsx new file mode 100644 index 00000000000000..e9ce12304ddea3 --- /dev/null +++ b/static/app/views/performance/newTraceDetails/traceRow/traceIcons.tsx @@ -0,0 +1,99 @@ +import {Fragment, useMemo} from 'react'; +import clamp from 'lodash/clamp'; + +import {TraceIcons} from 'sentry/views/performance/newTraceDetails/icons'; +import type { + TraceTree, + TraceTreeNode, +} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree'; +import type {VirtualizedViewManager} from 'sentry/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager'; + +interface ErrorIconsProps { + errors: TraceTreeNode['errors']; + manager: VirtualizedViewManager; + node_space: [number, number] | null; +} + +export function TraceErrorIcons(props: ErrorIconsProps) { + const errors = useMemo(() => { + return [...props.errors]; + }, [props.errors]); + + if (!props.errors.size) { + return null; + } + + return ( + + {errors.map((error, i) => { + const timestamp = error.timestamp ? error.timestamp * 1e3 : props.node_space![0]; + // Clamp the error timestamp to the span's timestamp + const left = props.manager.computeRelativeLeftPositionFromOrigin( + clamp( + timestamp, + props.node_space![0], + props.node_space![0] + props.node_space![1] + ), + props.node_space! + ); + + return ( +
+ +
+ ); + })} +
+ ); +} + +interface TracePerformanceIssueIconsProps { + manager: VirtualizedViewManager; + node_space: [number, number] | null; + performance_issues: TraceTreeNode['performance_issues']; +} + +export function TracePerformanceIssueIcons(props: TracePerformanceIssueIconsProps) { + const performance_issues = useMemo(() => { + return [...props.performance_issues]; + }, [props.performance_issues]); + + if (!props.performance_issues.size) { + return null; + } + + return ( + + {performance_issues.map((issue, i) => { + const timestamp = issue.timestamp + ? issue.timestamp * 1e3 + : issue.start + ? issue.start * 1e3 + : props.node_space![0]; + // Clamp the issue timestamp to the span's timestamp + const left = props.manager.computeRelativeLeftPositionFromOrigin( + clamp( + timestamp, + props.node_space![0], + props.node_space![0] + props.node_space![1] + ), + props.node_space! + ); + + return ( +
+ +
+ ); + })} +
+ ); +} diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceLoadingRow.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceLoadingRow.tsx new file mode 100644 index 00000000000000..475e9806b1c867 --- /dev/null +++ b/static/app/views/performance/newTraceDetails/traceRow/traceLoadingRow.tsx @@ -0,0 +1,101 @@ +import type {Theme} from '@emotion/react'; + +import Placeholder from 'sentry/components/placeholder'; +import {isTraceNode} from 'sentry/views/performance/newTraceDetails/guards'; +import type { + TraceTree, + TraceTreeNode, +} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree'; +import type {VirtualizedViewManager} from 'sentry/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager'; +import { + TRACE_COUNT_FORMATTER, + TRACE_RIGHT_COLUMN_EVEN_CLASSNAME, + TRACE_RIGHT_COLUMN_ODD_CLASSNAME, + TraceChildrenButton, + TraceRowConnectors, +} from 'sentry/views/performance/newTraceDetails/traceRow/traceRow'; + +function randomBetween(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1) + min); +} + +export function TraceLoadingRow(props: { + index: number; + manager: VirtualizedViewManager; + node: TraceTreeNode; + style: React.CSSProperties; + theme: Theme; +}) { + return ( +
+
+
+
+ + {props.node.children.length > 0 || props.node.canFetch ? ( + void 0} + onDoubleClick={() => void 0} + > + {props.node.children.length > 0 + ? TRACE_COUNT_FORMATTER.format(props.node.children.length) + : null} + + ) : null} +
+ +
+
+
+ +
+
+ ); +} diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceMissingInstrumentationRow.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceMissingInstrumentationRow.tsx new file mode 100644 index 00000000000000..7d72ce289e9d18 --- /dev/null +++ b/static/app/views/performance/newTraceDetails/traceRow/traceMissingInstrumentationRow.tsx @@ -0,0 +1,72 @@ +import {t} from 'sentry/locale'; +import {isMissingInstrumentationNode} from 'sentry/views/performance/newTraceDetails/guards'; +import {TraceIcons} from 'sentry/views/performance/newTraceDetails/icons'; +import { + makeTraceNodeBarColor, + type TraceTree, + type TraceTreeNode, +} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree'; +import {MissingInstrumentationTraceBar} from 'sentry/views/performance/newTraceDetails/traceRow/traceBar'; +import { + maybeFocusTraceRow, + TraceRowConnectors, + type TraceRowProps, +} from 'sentry/views/performance/newTraceDetails/traceRow/traceRow'; + +export function TraceMissingInstrumentationRow( + props: TraceRowProps> +) { + if (!isMissingInstrumentationNode(props.node)) { + throw new Error( + 'Missing instrumentation rendered called for a node that is not missing instrumentation' + ); + } + + return ( +
+ props.tabIndex === 0 && !props.isEmbedded + ? maybeFocusTraceRow(r, props.node, props.previouslyFocusedNodeRef) + : null + } + tabIndex={props.tabIndex} + className={`TraceRow ${props.rowSearchClassName}`} + onClick={props.onRowClick} + onKeyDown={props.onRowKeyDown} + style={props.style} + > +
+
+
+ +
+ {t('Missing instrumentation')} +
+
+
+ + +
+
+ ); +} diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceRootNode.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceRootNode.tsx new file mode 100644 index 00000000000000..e979e3833aeb87 --- /dev/null +++ b/static/app/views/performance/newTraceDetails/traceRow/traceRootNode.tsx @@ -0,0 +1,101 @@ +import {Fragment} from 'react'; + +import {t} from 'sentry/locale'; +import {isTraceNode} from 'sentry/views/performance/newTraceDetails/guards'; +import {TraceIcons} from 'sentry/views/performance/newTraceDetails/icons'; +import { + makeTraceNodeBarColor, + type TraceTree, + type TraceTreeNode, +} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree'; +import {TraceBar} from 'sentry/views/performance/newTraceDetails/traceRow/traceBar'; +import { + maybeFocusTraceRow, + TRACE_COUNT_FORMATTER, + TraceChildrenButton, + TraceRowConnectors, + type TraceRowProps, +} from 'sentry/views/performance/newTraceDetails/traceRow/traceRow'; + +const NO_ERRORS = new Set(); +const NO_PERFORMANCE_ISSUES = new Set(); +const NO_PROFILES = []; + +export function TraceRootRow(props: TraceRowProps>) { + if (!isTraceNode(props.node)) { + throw new Error('Trace row rendered called on row that is not root'); + } + + return ( +
+ props.tabIndex === 0 && !props.isEmbedded + ? maybeFocusTraceRow(r, props.node, props.previouslyFocusedNodeRef) + : null + } + tabIndex={props.tabIndex} + className={`TraceRow ${props.rowSearchClassName} ${props.node.has_errors ? props.node.max_severity : ''}`} + onClick={props.onRowClick} + onKeyDown={props.onRowKeyDown} + style={props.style} + > +
+
+ {' '} +
+ + {props.node.children.length > 0 || props.node.canFetch ? ( + void 0} + onDoubleClick={props.onExpandDoubleClick} + > + {props.node.fetchStatus === 'loading' + ? null + : props.node.children.length > 0 + ? TRACE_COUNT_FORMATTER.format(props.node.children.length) + : null} + + ) : null} +
+ {t('Trace')} + {props.trace_id ? ( + + + {props.trace_id} + + ) : null} +
+
+
+ + +
+
+ ); +} diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceRow.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceRow.tsx new file mode 100644 index 00000000000000..fb05493e66e406 --- /dev/null +++ b/static/app/views/performance/newTraceDetails/traceRow/traceRow.tsx @@ -0,0 +1,140 @@ +import {Fragment} from 'react'; +import type {Theme} from '@emotion/react'; + +import LoadingIndicator from 'sentry/components/loadingIndicator'; +import type {PlatformKey} from 'sentry/types/project'; +import {isParentAutogroupedNode} from 'sentry/views/performance/newTraceDetails/guards'; +import type {VirtualizedViewManager} from 'sentry/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager'; + +import { + ParentAutogroupNode, + type TraceTree, + type TraceTreeNode, +} from '../traceModels/traceTree'; + +export const TRACE_COUNT_FORMATTER = Intl.NumberFormat(undefined, {notation: 'compact'}); + +export const TRACE_RIGHT_COLUMN_EVEN_CLASSNAME = `TraceRightColumn`; +export const TRACE_RIGHT_COLUMN_ODD_CLASSNAME = [ + TRACE_RIGHT_COLUMN_EVEN_CLASSNAME, + 'Odd', +].join(' '); +export const TRACE_CHILDREN_COUNT_WRAPPER_CLASSNAME = `TraceChildrenCountWrapper`; +export const TRACE_CHILDREN_COUNT_WRAPPER_ORPHANED_CLASSNAME = [ + TRACE_CHILDREN_COUNT_WRAPPER_CLASSNAME, + 'Orphaned', +].join(' '); + +export interface TraceRowProps { + index: number; + isEmbedded: boolean; + listColumnClassName: string; + listColumnStyle: React.CSSProperties; + manager: VirtualizedViewManager; + node: T; + onExpand: (e: React.MouseEvent) => void; + onExpandDoubleClick: () => void; + onRowClick: () => void; + onRowDoubleClick: () => void; + onRowKeyDown: () => void; + onSpanArrowClick: number; + onSpanRowArrowClick: () => void; + onZoomIn: (e: React.MouseEvent) => void; + previouslyFocusedNodeRef: React.MutableRefObject< + TraceTreeNode + >; + projects: Record; + registerListColumnRef: () => void; + registerSpanArrowRef: () => void; + registerSpanColumnRef: () => void; + rowSearchClassName: string; + spanColumnClassName: string; + style: React.CSSProperties; + tabIndex: number; + theme: Theme; + trace_id: string; + virtualized_index: number; +} + +export function maybeFocusTraceRow( + ref: HTMLDivElement | null, + node: TraceTreeNode, + previouslyFocusedNodeRef: React.MutableRefObject | null> +) { + if (!ref) return; + if (node === previouslyFocusedNodeRef.current) return; + + previouslyFocusedNodeRef.current = node; + ref.focus(); +} + +export function TraceRowConnectors(props: { + manager: VirtualizedViewManager; + node: TraceTreeNode; +}) { + const hasChildren = + (props.node.expanded || props.node.zoomedIn) && props.node.children.length > 0; + const showVerticalConnector = + hasChildren || (props.node.value && isParentAutogroupedNode(props.node)); + + // If the tail node of the collapsed node has no children, + // we don't want to render the vertical connector as no children + // are being rendered as the chain is entirely collapsed + const hideVerticalConnector = + showVerticalConnector && + props.node.value && + props.node instanceof ParentAutogroupNode && + (!props.node.tail.children.length || + (!props.node.tail.expanded && !props.node.expanded)); + + return ( + + {props.node.connectors.map((c, i) => { + return ( + + ); + })} + {showVerticalConnector && !hideVerticalConnector ? ( + + ) : null} + {props.node.isLastChild ? ( + + ) : ( + + )} + + ); +} + +export function TraceChildrenButton(props: { + children: React.ReactNode; + expanded: boolean; + icon: React.ReactNode; + onClick: (e: React.MouseEvent) => void; + onDoubleClick: (e: React.MouseEvent) => void; + status: TraceTreeNode['fetchStatus'] | undefined; +}) { + return ( + + ); +} diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceSpanRow.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceSpanRow.tsx new file mode 100644 index 00000000000000..010588c2143c26 --- /dev/null +++ b/static/app/views/performance/newTraceDetails/traceRow/traceSpanRow.tsx @@ -0,0 +1,103 @@ +import {isSpanNode} from 'sentry/views/performance/newTraceDetails/guards'; +import {TraceIcons} from 'sentry/views/performance/newTraceDetails/icons'; +import { + makeTraceNodeBarColor, + type TraceTree, + type TraceTreeNode, +} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree'; +import {TraceBar} from 'sentry/views/performance/newTraceDetails/traceRow/traceBar'; +import { + maybeFocusTraceRow, + TRACE_COUNT_FORMATTER, + TraceChildrenButton, + TraceRowConnectors, + type TraceRowProps, +} from 'sentry/views/performance/newTraceDetails/traceRow/traceRow'; + +const NO_PROFILES = []; + +export function TraceSpanRow(props: TraceRowProps>) { + if (!isSpanNode(props.node)) { + throw new Error('Trace row span renderer called for trace that is not of span type'); + } + + return ( +
+ props.tabIndex === 0 && !props.isEmbedded + ? maybeFocusTraceRow(r, props.node, props.previouslyFocusedNodeRef) + : null + } + tabIndex={props.tabIndex} + className={`TraceRow ${props.rowSearchClassName} ${props.node.has_errors ? props.node.max_severity : ''}`} + onClick={props.onRowClick} + onKeyDown={props.onRowKeyDown} + style={props.style} + > +
+
+
+ + {props.node.children.length > 0 || props.node.canFetch ? ( + + ) + } + status={props.node.fetchStatus} + expanded={props.node.expanded || props.node.zoomedIn} + onDoubleClick={props.onExpandDoubleClick} + onClick={e => + props.node.canFetch ? props.onZoomIn(e) : props.onExpand(e) + } + > + {props.node.children.length > 0 + ? TRACE_COUNT_FORMATTER.format(props.node.children.length) + : null} + + ) : null} +
+ {props.node.value.op ?? ''} + + + {!props.node.value.description + ? props.node.value.span_id ?? 'unknown' + : props.node.value.description.length > 100 + ? props.node.value.description.slice(0, 100).trim() + '\u2026' + : props.node.value.description} + +
+
+
+ + +
+
+ ); +} diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceTransactionRow.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceTransactionRow.tsx new file mode 100644 index 00000000000000..9472d4d29652f0 --- /dev/null +++ b/static/app/views/performance/newTraceDetails/traceRow/traceTransactionRow.tsx @@ -0,0 +1,110 @@ +import {PlatformIcon} from 'platformicons'; + +import {isTransactionNode} from 'sentry/views/performance/newTraceDetails/guards'; +import {TraceIcons} from 'sentry/views/performance/newTraceDetails/icons'; +import { + makeTraceNodeBarColor, + type TraceTree, + type TraceTreeNode, +} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree'; +import {TraceBar} from 'sentry/views/performance/newTraceDetails/traceRow/traceBar'; +import { + maybeFocusTraceRow, + TRACE_COUNT_FORMATTER, + TraceChildrenButton, + TraceRowConnectors, + type TraceRowProps, +} from 'sentry/views/performance/newTraceDetails/traceRow/traceRow'; + +export function TraceTransactionRow( + props: TraceRowProps> +) { + if (!isTransactionNode(props.node)) { + throw new Error( + 'Called transaction renderer for a node that is not of type transaction' + ); + } + + return ( +
+ props.tabIndex === 0 && !props.isEmbedded + ? maybeFocusTraceRow(r, props.node, props.previouslyFocusedNodeRef) + : null + } + tabIndex={props.tabIndex} + className={`TraceRow ${props.rowSearchClassName} ${props.node.has_errors ? props.node.max_severity : ''}`} + onKeyDown={props.onRowKeyDown} + onClick={props.onRowClick} + style={props.style} + > +
+
+
+ + {props.node.children.length > 0 || props.node.canFetch ? ( + + ) : ( + '+' + ) + ) : ( + + ) + } + status={props.node.fetchStatus} + expanded={props.node.expanded || props.node.zoomedIn} + onDoubleClick={props.onExpandDoubleClick} + onClick={e => { + props.node.canFetch ? props.onZoomIn(e) : props.onExpand(e); + }} + > + {props.node.children.length > 0 + ? TRACE_COUNT_FORMATTER.format(props.node.children.length) + : null} + + ) : null} +
+ + {props.node.value['transaction.op']} + + {props.node.value.transaction} +
+
+
+ + +
+
+ ); +} From f12cd9159b8712cfce76fef66c79713ff836a3b1 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Mon, 23 Sep 2024 16:05:41 -0400 Subject: [PATCH 2/8] ref(trace) move to separate render --- .../performance/newTraceDetails/trace.tsx | 109 +++++++++++++++--- .../traceRow/traceAutogroupedRow.tsx | 9 +- .../traceRow/traceErrorRow.tsx | 8 +- .../traceMissingInstrumentationRow.tsx | 11 +- .../traceRow/traceRootNode.tsx | 2 +- .../newTraceDetails/traceRow/traceRow.tsx | 23 ++-- .../newTraceDetails/traceRow/traceSpanRow.tsx | 7 +- .../traceRow/traceTransactionRow.tsx | 9 +- 8 files changed, 107 insertions(+), 71 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/trace.tsx b/static/app/views/performance/newTraceDetails/trace.tsx index 369b2770fc1cd0..eac06e09326594 100644 --- a/static/app/views/performance/newTraceDetails/trace.tsx +++ b/static/app/views/performance/newTraceDetails/trace.tsx @@ -31,13 +31,19 @@ import { type VirtualizedRow, } from 'sentry/views/performance/newTraceDetails/traceRenderers/traceVirtualizedList'; import type {VirtualizedViewManager} from 'sentry/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager'; +import {TraceErrorRow} from 'sentry/views/performance/newTraceDetails/traceRow/traceErrorRow'; import {TraceLoadingRow} from 'sentry/views/performance/newTraceDetails/traceRow/traceLoadingRow'; +import {TraceMissingInstrumentationRow} from 'sentry/views/performance/newTraceDetails/traceRow/traceMissingInstrumentationRow'; +import {TraceRootRow} from 'sentry/views/performance/newTraceDetails/traceRow/traceRootNode'; import { TRACE_CHILDREN_COUNT_WRAPPER_CLASSNAME, TRACE_CHILDREN_COUNT_WRAPPER_ORPHANED_CLASSNAME, TRACE_RIGHT_COLUMN_EVEN_CLASSNAME, TRACE_RIGHT_COLUMN_ODD_CLASSNAME, + type TraceRowProps, } from 'sentry/views/performance/newTraceDetails/traceRow/traceRow'; +import {TraceSpanRow} from 'sentry/views/performance/newTraceDetails/traceRow/traceSpanRow'; +import {TraceTransactionRow} from 'sentry/views/performance/newTraceDetails/traceRow/traceTransactionRow'; import type {TraceReducerState} from 'sentry/views/performance/newTraceDetails/traceState'; import { getRovingIndexActionFromDOMEvent, @@ -46,7 +52,13 @@ import { import {TraceTree, type TraceTreeNode} from './traceModels/traceTree'; import {useTraceState, useTraceStateDispatch} from './traceState/traceStateProvider'; -import {isSpanNode, isTransactionNode} from './guards'; +import { + isMissingInstrumentationNode, + isSpanNode, + isTraceErrorNode, + isTraceNode, + isTransactionNode, +} from './guards'; function computeNextIndexFromAction( current_index: number, @@ -553,40 +565,43 @@ function RenderTraceRow(props: { trace_id: string | undefined; tree: TraceTree; }) { + const node = props.node; const virtualized_index = props.index - props.manager.start_virtualized_index; const rowSearchClassName = `${props.isSearchResult ? 'SearchResult' : ''} ${props.searchResultsIteratorIndex === props.index ? 'Highlight' : ''}`; const registerListColumnRef = useCallback( (ref: HTMLDivElement | null) => { - props.manager.registerColumnRef('list', ref, virtualized_index, props.node); + props.manager.registerColumnRef('list', ref, virtualized_index, node); }, - [props.manager, props.node, virtualized_index] + [props.manager, node, virtualized_index] ); const registerSpanColumnRef = useCallback( (ref: HTMLDivElement | null) => { - props.manager.registerColumnRef('span_list', ref, virtualized_index, props.node); + props.manager.registerColumnRef('span_list', ref, virtualized_index, node); }, - [props.manager, props.node, virtualized_index] + [props.manager, node, virtualized_index] ); const registerSpanArrowRef = useCallback( ref => { - props.manager.registerArrowRef(ref, props.node.space!, virtualized_index); + props.manager.registerArrowRef(ref, node.space!, virtualized_index); }, - [props.manager, props.node, virtualized_index] + [props.manager, node, virtualized_index] ); + const onRowClickProp = props.onRowClick; const onRowClick = useCallback( (event: React.MouseEvent) => { - props.onRowClick(props.node, event, props.index); + onRowClickProp(node, event, props.index); }, - [props.index, props.node, props.onRowClick] + [props.index, node, onRowClickProp] ); + const onRowKeyDownProp = props.onRowKeyDown; const onRowKeyDown = useCallback( - event => props.onRowKeyDown(event, props.index, props.node), - [props.index, props.node, props.onRowKeyDown] + event => onRowKeyDownProp(event, props.index, node), + [props.index, node, onRowKeyDownProp] ); const onRowDoubleClick = useCallback( @@ -595,25 +610,33 @@ function RenderTraceRow(props: { organization: props.organization, }); e.stopPropagation(); - props.manager.onZoomIntoSpace(props.node.space!); + props.manager.onZoomIntoSpace(node.space!); }, - [props.node, props.manager, props.organization] + [node, props.manager, props.organization] ); const onSpanRowArrowClick = useCallback( (_e: React.MouseEvent) => { - props.manager.onBringRowIntoView(props.node.space!); + props.manager.onBringRowIntoView(node.space!); }, - [props.node.space, props.manager] + [node.space, props.manager] ); + const onExpandProp = props.onExpand; const onExpand = useCallback( (e: React.MouseEvent) => { - props.onExpand(e, props.node, !props.node.expanded); + onExpandProp(e, node, !node.expanded); }, - [props.node, props.onExpand] + [node, onExpandProp] ); + const onZoomInProp = props.onExpand; + const onZoomIn = useCallback( + (e: React.MouseEvent) => { + onZoomInProp(e, node, !node.zoomedIn); + }, + [node, onZoomInProp] + ); const onExpandDoubleClick = useCallback((e: React.MouseEvent) => { e.stopPropagation(); }, []); @@ -623,14 +646,62 @@ function RenderTraceRow(props: { ? TRACE_RIGHT_COLUMN_ODD_CLASSNAME : TRACE_RIGHT_COLUMN_EVEN_CLASSNAME; - const listColumnClassName = props.node.isOrphaned + const listColumnClassName = node.isOrphaned ? TRACE_CHILDREN_COUNT_WRAPPER_ORPHANED_CLASSNAME : TRACE_CHILDREN_COUNT_WRAPPER_CLASSNAME; const listColumnStyle: React.CSSProperties = { - paddingLeft: props.node.depth * props.manager.row_depth_padding, + paddingLeft: node.depth * props.manager.row_depth_padding, }; + const rowProps: TraceRowProps> = { + onExpand, + onZoomIn, + onRowClick, + onRowKeyDown, + previouslyFocusedNodeRef: props.previouslyFocusedNodeRef, + isEmbedded: props.isEmbedded, + onSpanArrowClick: onSpanRowArrowClick, + manager: props.manager, + index: props.index, + theme: props.theme, + style: props.style, + projects: props.projects, + tabIndex: props.tabIndex, + onRowDoubleClick, + trace_id: props.trace_id, + node: props.node, + virtualized_index, + listColumnStyle, + listColumnClassName, + spanColumnClassName, + onExpandDoubleClick, + rowSearchClassName, + registerListColumnRef, + registerSpanColumnRef, + registerSpanArrowRef, + }; + + if (isTransactionNode(node)) { + return ; + } + + if (isSpanNode(node)) { + return ; + } + + if (isMissingInstrumentationNode(node)) { + return ; + } + + if (isTraceErrorNode(node)) { + return ; + } + + if (isTraceNode(node)) { + return ; + } + return null; } diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceAutogroupedRow.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceAutogroupedRow.tsx index 7b3f35f045da89..3e7252d5e4b1b3 100644 --- a/static/app/views/performance/newTraceDetails/traceRow/traceAutogroupedRow.tsx +++ b/static/app/views/performance/newTraceDetails/traceRow/traceAutogroupedRow.tsx @@ -1,5 +1,4 @@ import {t} from 'sentry/locale'; -import {isAutogroupedNode} from 'sentry/views/performance/newTraceDetails/guards'; import {TraceIcons} from 'sentry/views/performance/newTraceDetails/icons'; import { makeTraceNodeBarColor, @@ -18,12 +17,6 @@ import { export function TraceAutogroupedRow( props: TraceRowProps ) { - if (!isAutogroupedNode(props.node)) { - throw new Error( - 'Called autogrouped renderer for a node that is not of type autogroped' - ); - } - return (
diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceErrorRow.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceErrorRow.tsx index 5e7c55aca49848..37dd4be42bd2f2 100644 --- a/static/app/views/performance/newTraceDetails/traceRow/traceErrorRow.tsx +++ b/static/app/views/performance/newTraceDetails/traceRow/traceErrorRow.tsx @@ -2,7 +2,6 @@ import type {Theme} from '@emotion/react'; import {PlatformIcon} from 'platformicons'; import {t} from 'sentry/locale'; -import {isTraceErrorNode} from 'sentry/views/performance/newTraceDetails/guards'; import {TraceIcons} from 'sentry/views/performance/newTraceDetails/icons'; import type { TraceTree, @@ -28,12 +27,7 @@ const ERROR_LEVEL_LABELS: Record = { unknown: t('Unknown'), }; -export function TraceErrorRow(props: TraceRowProps>) { - if (!isTraceErrorNode(props.node)) { - throw new Error( - 'Trace error row renderer called on a row that is not trace error type' - ); - } +export function TraceErrorRow(props: TraceRowProps>) { return (
> + props: TraceRowProps> ) { - if (!isMissingInstrumentationNode(props.node)) { - throw new Error( - 'Missing instrumentation rendered called for a node that is not missing instrumentation' - ); - } - return (
diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceRootNode.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceRootNode.tsx index e979e3833aeb87..40fb2e1789a0f4 100644 --- a/static/app/views/performance/newTraceDetails/traceRow/traceRootNode.tsx +++ b/static/app/views/performance/newTraceDetails/traceRow/traceRootNode.tsx @@ -91,7 +91,7 @@ export function TraceRootRow(props: TraceRowProps diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceRow.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceRow.tsx index fb05493e66e406..6bb7d46e19e27d 100644 --- a/static/app/views/performance/newTraceDetails/traceRow/traceRow.tsx +++ b/static/app/views/performance/newTraceDetails/traceRow/traceRow.tsx @@ -33,26 +33,23 @@ export interface TraceRowProps { manager: VirtualizedViewManager; node: T; onExpand: (e: React.MouseEvent) => void; - onExpandDoubleClick: () => void; - onRowClick: () => void; - onRowDoubleClick: () => void; - onRowKeyDown: () => void; - onSpanArrowClick: number; - onSpanRowArrowClick: () => void; + onExpandDoubleClick: (e: React.MouseEvent) => void; + onRowClick: (e: React.MouseEvent) => void; + onRowDoubleClick: (e: React.MouseEvent) => void; + onRowKeyDown: (e: React.KeyboardEvent) => void; + onSpanArrowClick: (e: React.MouseEvent) => void; onZoomIn: (e: React.MouseEvent) => void; - previouslyFocusedNodeRef: React.MutableRefObject< - TraceTreeNode - >; + previouslyFocusedNodeRef: React.MutableRefObject | null>; projects: Record; - registerListColumnRef: () => void; - registerSpanArrowRef: () => void; - registerSpanColumnRef: () => void; + registerListColumnRef: (e: HTMLDivElement | null) => void; + registerSpanArrowRef: (e: HTMLButtonElement | null) => void; + registerSpanColumnRef: (e: HTMLDivElement | null) => void; rowSearchClassName: string; spanColumnClassName: string; style: React.CSSProperties; tabIndex: number; theme: Theme; - trace_id: string; + trace_id: string | undefined; virtualized_index: number; } diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceSpanRow.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceSpanRow.tsx index 010588c2143c26..5bcc1bc7d1d7d7 100644 --- a/static/app/views/performance/newTraceDetails/traceRow/traceSpanRow.tsx +++ b/static/app/views/performance/newTraceDetails/traceRow/traceSpanRow.tsx @@ -1,4 +1,3 @@ -import {isSpanNode} from 'sentry/views/performance/newTraceDetails/guards'; import {TraceIcons} from 'sentry/views/performance/newTraceDetails/icons'; import { makeTraceNodeBarColor, @@ -17,10 +16,6 @@ import { const NO_PROFILES = []; export function TraceSpanRow(props: TraceRowProps>) { - if (!isSpanNode(props.node)) { - throw new Error('Trace row span renderer called for trace that is not of span type'); - } - return (
> diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceTransactionRow.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceTransactionRow.tsx index 9472d4d29652f0..0ecb176536fd55 100644 --- a/static/app/views/performance/newTraceDetails/traceRow/traceTransactionRow.tsx +++ b/static/app/views/performance/newTraceDetails/traceRow/traceTransactionRow.tsx @@ -1,6 +1,5 @@ import {PlatformIcon} from 'platformicons'; -import {isTransactionNode} from 'sentry/views/performance/newTraceDetails/guards'; import {TraceIcons} from 'sentry/views/performance/newTraceDetails/icons'; import { makeTraceNodeBarColor, @@ -19,12 +18,6 @@ import { export function TraceTransactionRow( props: TraceRowProps> ) { - if (!isTransactionNode(props.node)) { - throw new Error( - 'Called transaction renderer for a node that is not of type transaction' - ); - } - return (
From 19e0876ac6408ad7ab8571c749a1197dba9afaca Mon Sep 17 00:00:00 2001 From: JonasBa Date: Mon, 23 Sep 2024 16:22:59 -0400 Subject: [PATCH 3/8] ref(trace): bad ref --- static/app/views/performance/newTraceDetails/trace.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/trace.tsx b/static/app/views/performance/newTraceDetails/trace.tsx index eac06e09326594..e7088f0e2d5075 100644 --- a/static/app/views/performance/newTraceDetails/trace.tsx +++ b/static/app/views/performance/newTraceDetails/trace.tsx @@ -31,6 +31,7 @@ import { type VirtualizedRow, } from 'sentry/views/performance/newTraceDetails/traceRenderers/traceVirtualizedList'; import type {VirtualizedViewManager} from 'sentry/views/performance/newTraceDetails/traceRenderers/virtualizedViewManager'; +import {TraceAutogroupedRow} from 'sentry/views/performance/newTraceDetails/traceRow/traceAutogroupedRow'; import {TraceErrorRow} from 'sentry/views/performance/newTraceDetails/traceRow/traceErrorRow'; import {TraceLoadingRow} from 'sentry/views/performance/newTraceDetails/traceRow/traceLoadingRow'; import {TraceMissingInstrumentationRow} from 'sentry/views/performance/newTraceDetails/traceRow/traceMissingInstrumentationRow'; @@ -53,6 +54,7 @@ import { import {TraceTree, type TraceTreeNode} from './traceModels/traceTree'; import {useTraceState, useTraceStateDispatch} from './traceState/traceStateProvider'; import { + isAutogroupedNode, isMissingInstrumentationNode, isSpanNode, isTraceErrorNode, @@ -600,7 +602,7 @@ function RenderTraceRow(props: { const onRowKeyDownProp = props.onRowKeyDown; const onRowKeyDown = useCallback( - event => onRowKeyDownProp(event, props.index, node), + (event: React.KeyboardEvent) => onRowKeyDownProp(event, props.index, node), [props.index, node, onRowKeyDownProp] ); @@ -630,7 +632,7 @@ function RenderTraceRow(props: { [node, onExpandProp] ); - const onZoomInProp = props.onExpand; + const onZoomInProp = props.onZoomIn; const onZoomIn = useCallback( (e: React.MouseEvent) => { onZoomInProp(e, node, !node.zoomedIn); @@ -694,6 +696,10 @@ function RenderTraceRow(props: { return ; } + if (isAutogroupedNode(node)) { + return ; + } + if (isTraceErrorNode(node)) { return ; } From 6cbece37b19cdb9a2d1125b95f6eeb6284cca2cb Mon Sep 17 00:00:00 2001 From: JonasBa Date: Mon, 23 Sep 2024 21:37:23 -0400 Subject: [PATCH 4/8] ref: query the fresh for ref --- .../app/views/performance/newTraceDetails/trace.spec.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/static/app/views/performance/newTraceDetails/trace.spec.tsx b/static/app/views/performance/newTraceDetails/trace.spec.tsx index 86de16b1b7515d..19f80688ac831d 100644 --- a/static/app/views/performance/newTraceDetails/trace.spec.tsx +++ b/static/app/views/performance/newTraceDetails/trace.spec.tsx @@ -790,7 +790,12 @@ describe('trace view', () => { await userEvent.keyboard('{arrowright}'); expect(await screen.findByText('special-span')).toBeInTheDocument(); await userEvent.keyboard('{arrowdown}'); - await waitFor(() => expect(rows[2]).toHaveFocus()); + await waitFor(() => { + const updatedRows = virtualizedContainer.querySelectorAll( + VISIBLE_TRACE_ROW_SELECTOR + ); + expect(updatedRows[2]).toHaveFocus(); + }); expect(await screen.findByTestId('trace-drawer-title')).toHaveTextContent( 'special-span' From 7108b5cccdd335236d18e8e571be5faa8fe420ca Mon Sep 17 00:00:00 2001 From: JonasBa Date: Mon, 23 Sep 2024 21:49:40 -0400 Subject: [PATCH 5/8] add returns --- .../newTraceDetails/traceRow/traceBackgroundPatterns.tsx | 9 +++++++-- .../performance/newTraceDetails/traceRow/traceRow.tsx | 8 ++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceBackgroundPatterns.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceBackgroundPatterns.tsx index fc5fe509aac090..e952b9f7a699e6 100644 --- a/static/app/views/performance/newTraceDetails/traceRow/traceBackgroundPatterns.tsx +++ b/static/app/views/performance/newTraceDetails/traceRow/traceBackgroundPatterns.tsx @@ -31,12 +31,17 @@ interface BackgroundPatternsProps { export function TraceBackgroundPatterns(props: BackgroundPatternsProps) { const performance_issues = useMemo(() => { - if (!props.performance_issues.size) return []; + if (!props.performance_issues.size) { + return []; + } + return [...props.performance_issues]; }, [props.performance_issues]); const errors = useMemo(() => { - if (!props.errors.size) return []; + if (!props.errors.size) { + return []; + } return [...props.errors]; }, [props.errors]); diff --git a/static/app/views/performance/newTraceDetails/traceRow/traceRow.tsx b/static/app/views/performance/newTraceDetails/traceRow/traceRow.tsx index 6bb7d46e19e27d..fa268f35686734 100644 --- a/static/app/views/performance/newTraceDetails/traceRow/traceRow.tsx +++ b/static/app/views/performance/newTraceDetails/traceRow/traceRow.tsx @@ -58,8 +58,12 @@ export function maybeFocusTraceRow( node: TraceTreeNode, previouslyFocusedNodeRef: React.MutableRefObject | null> ) { - if (!ref) return; - if (node === previouslyFocusedNodeRef.current) return; + if (!ref) { + return; + } + if (node === previouslyFocusedNodeRef.current) { + return; + } previouslyFocusedNodeRef.current = node; ref.focus(); From 8b4673174d16dff7fe6537611d3421d3440ea8c9 Mon Sep 17 00:00:00 2001 From: Jonas Date: Wed, 25 Sep 2024 14:07:54 -0400 Subject: [PATCH 6/8] ref(trace) break up trace loading logic (#77986) Split loading and path logic to separate hooks --- .../performance/newTraceDetails/index.tsx | 72 ++++----------- .../performance/newTraceDetails/trace.tsx | 92 +------------------ .../useTraceScrollToEventOnLoad.tsx | 92 +++++++++++++++++++ .../newTraceDetails/useTraceScrollToPath.tsx | 59 ++++++++++++ 4 files changed, 175 insertions(+), 140 deletions(-) create mode 100644 static/app/views/performance/newTraceDetails/useTraceScrollToEventOnLoad.tsx create mode 100644 static/app/views/performance/newTraceDetails/useTraceScrollToPath.tsx diff --git a/static/app/views/performance/newTraceDetails/index.tsx b/static/app/views/performance/newTraceDetails/index.tsx index 24df36c7cfd588..47a92f20cb9bab 100644 --- a/static/app/views/performance/newTraceDetails/index.tsx +++ b/static/app/views/performance/newTraceDetails/index.tsx @@ -67,6 +67,8 @@ import { useTraceStateDispatch, useTraceStateEmitter, } from 'sentry/views/performance/newTraceDetails/traceState/traceStateProvider'; +import {useTraceScrollToEventOnLoad} from 'sentry/views/performance/newTraceDetails/useTraceScrollToEventOnLoad'; +import {useTraceScrollToPath} from 'sentry/views/performance/newTraceDetails/useTraceScrollToPath'; import type {ReplayTrace} from 'sentry/views/replays/detail/trace/useReplayTraces'; import type {ReplayRecord} from 'sentry/views/replays/types'; @@ -93,18 +95,6 @@ import {TraceType} from './traceType'; import TraceTypeWarnings from './traceTypeWarnings'; import {useTraceQueryParamStateSync} from './useTraceQueryParamStateSync'; -function decodeScrollQueue(maybePath: unknown): TraceTree.NodePath[] | null { - if (Array.isArray(maybePath)) { - return maybePath; - } - - if (typeof maybePath === 'string') { - return [maybePath as TraceTree.NodePath]; - } - - return null; -} - function logTraceMetadata( tree: TraceTree, projects: Project[], @@ -303,33 +293,6 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) { }); }, [props.organization, props.source]); - const initializedRef = useRef(false); - const scrollQueueRef = useRef< - TraceViewWaterfallProps['scrollToNode'] | null | undefined - >(undefined); - - if (scrollQueueRef.current === undefined) { - let scrollToNode: TraceViewWaterfallProps['scrollToNode'] = props.scrollToNode; - if (!props.scrollToNode) { - const queryParams = qs.parse(location.search); - scrollToNode = { - eventId: queryParams.eventId as string | undefined, - path: decodeScrollQueue( - queryParams.node - ) as TraceTreeNode['path'], - }; - } - - if (scrollToNode && (scrollToNode.path || scrollToNode.eventId)) { - scrollQueueRef.current = { - eventId: scrollToNode.eventId as string, - path: scrollToNode.path, - }; - } else { - scrollQueueRef.current = null; - } - } - const previouslyFocusedNodeRef = useRef | null>( null ); @@ -818,7 +781,6 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) { nodeToScrollTo: TraceTreeNode | null, indexOfNodeToScrollTo: number | null ) => { - scrollQueueRef.current = null; const query = qs.parse(location.search); if (query.fov && typeof query.fov === 'string') { @@ -952,13 +914,6 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) { }; }, [traceScheduler, traceDispatch]); - // Sync part of the state with the URL - const traceQueryStateSync = useMemo(() => { - return {search: traceState.search.query}; - }, [traceState.search.query]); - - useTraceQueryParamStateSync(traceQueryStateSync); - const [traceGridRef, setTraceGridRef] = useState(null); // Memoized because it requires tree traversal @@ -996,6 +951,23 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) { }; }, [viewManager, traceScheduler, tree]); + // Sync part of the state with the URL + const traceQueryStateSync = useMemo(() => { + return {search: traceState.search.query}; + }, [traceState.search.query]); + useTraceQueryParamStateSync(traceQueryStateSync); + + const scrollQueueRef = useTraceScrollToPath(props.scrollToNode); + + useTraceScrollToEventOnLoad({ + rerender, + onTraceLoad, + scrollQueueRef, + manager: viewManager, + scheduler: traceScheduler, + trace: tree, + }); + return ( ) : tree.type === 'empty' ? ( - ) : tree.type === 'loading' || - (scrollQueueRef.current && tree.type !== 'trace') ? ( + ) : tree.type === 'loading' || tree.type !== 'trace' ? ( ) : null} diff --git a/static/app/views/performance/newTraceDetails/trace.tsx b/static/app/views/performance/newTraceDetails/trace.tsx index bce7a10d143244..497c44236917fe 100644 --- a/static/app/views/performance/newTraceDetails/trace.tsx +++ b/static/app/views/performance/newTraceDetails/trace.tsx @@ -10,7 +10,6 @@ import { } from 'react'; import {type Theme, useTheme} from '@emotion/react'; import styled from '@emotion/styled'; -import Sentry from '@sentry/react'; import ConfigStore from 'sentry/stores/configStore'; import {space} from 'sentry/styles/space'; @@ -51,7 +50,7 @@ import { type RovingTabIndexUserActions, } from 'sentry/views/performance/newTraceDetails/traceState/traceRovingTabIndex'; -import {TraceTree, type TraceTreeNode} from './traceModels/traceTree'; +import type {TraceTree, TraceTreeNode} from './traceModels/traceTree'; import {useTraceState, useTraceStateDispatch} from './traceState/traceStateProvider'; import { isAutogroupedNode, @@ -89,7 +88,6 @@ function computeNextIndexFromAction( interface TraceProps { forceRerender: number; - initializedRef: React.MutableRefObject; isEmbedded: boolean; manager: VirtualizedViewManager; onRowClick: ( @@ -97,11 +95,6 @@ interface TraceProps { event: React.MouseEvent, index: number ) => void; - onTraceLoad: ( - trace: TraceTree, - node: TraceTreeNode | null, - index: number | null - ) => void; onTraceSearch: ( query: string, node: TraceTreeNode, @@ -110,14 +103,6 @@ interface TraceProps { previouslyFocusedNodeRef: React.MutableRefObject | null>; rerender: () => void; scheduler: TraceScheduler; - scrollQueueRef: React.MutableRefObject< - | { - eventId?: string; - path?: TraceTree.NodePath[]; - } - | null - | undefined - >; trace: TraceTree; trace_id: string | undefined; } @@ -126,13 +111,10 @@ export function Trace({ trace, onRowClick, manager, - scrollQueueRef, previouslyFocusedNodeRef, onTraceSearch, - onTraceLoad, rerender, scheduler, - initializedRef, forceRerender, trace_id, isEmbedded, @@ -198,71 +180,6 @@ export function Trace({ }; }, [manager, scheduler]); - useLayoutEffect(() => { - if (initializedRef.current) { - return; - } - if (trace.type !== 'trace' || !manager) { - return; - } - - initializedRef.current = true; - - if (!scrollQueueRef.current) { - onTraceLoad(trace, null, null); - return; - } - - // Node path has higher specificity than eventId - const promise = scrollQueueRef.current?.path - ? TraceTree.ExpandToPath(trace, scrollQueueRef.current.path, rerenderRef.current, { - api, - organization, - }) - : scrollQueueRef.current.eventId - ? TraceTree.ExpandToEventID( - scrollQueueRef?.current?.eventId, - trace, - rerenderRef.current, - { - api, - organization, - } - ) - : Promise.resolve(null); - - promise - .then(maybeNode => { - onTraceLoad(trace, maybeNode?.node ?? null, maybeNode?.index ?? null); - - if (!maybeNode) { - Sentry.captureMessage('Failed to find and scroll to node in tree'); - return; - } - }) - .finally(() => { - // Important to set scrollQueueRef.current to null and trigger a rerender - // after the promise resolves as we show a loading state during scroll, - // else the screen could jump around while we fetch span data - scrollQueueRef.current = null; - rerenderRef.current(); - // Allow react to rerender before dispatching the init event - requestAnimationFrame(() => { - scheduler.dispatch('initialize virtualized list'); - }); - }); - }, [ - api, - trace, - manager, - onTraceLoad, - scheduler, - traceDispatch, - scrollQueueRef, - initializedRef, - organization, - ]); - const onNodeZoomIn = useCallback( ( event: React.MouseEvent | React.KeyboardEvent, @@ -419,7 +336,6 @@ export function Trace({ onNodeExpand, onNodeZoomIn, manager, - scrollQueueRef, previouslyFocusedNodeRef, onRowKeyDown, onRowClick, @@ -436,10 +352,10 @@ export function Trace({ ); const render = useMemo(() => { - return trace.type !== 'trace' || scrollQueueRef.current + return trace.type !== 'trace' ? r => renderLoadingRow(r) : r => renderVirtualizedRow(r); - }, [renderLoadingRow, renderVirtualizedRow, trace.type, scrollQueueRef]); + }, [renderLoadingRow, renderVirtualizedRow, trace.type]); const traceNode = trace.root.children[0]; const traceStartTimestamp = traceNode?.space?.[0]; @@ -459,7 +375,7 @@ export function Trace({ className={` ${trace.root.space[1] === 0 ? 'Empty' : ''} ${trace.indicators.length > 0 ? 'WithIndicators' : ''} - ${trace.type !== 'trace' || scrollQueueRef.current ? 'Loading' : ''} + ${trace.type !== 'trace' ? 'Loading' : ''} ${ConfigStore.get('theme')}`} >
| null, + index: number | null + ) => void; + rerender: () => void; + scheduler: TraceScheduler; + scrollQueueRef: ReturnType; + trace: TraceTree; +}; + +export function useTraceScrollToEventOnLoad(options: UseTraceScrollToEventOnLoadProps) { + const api = useApi(); + const organization = useOrganization(); + const initializedRef = useRef(false); + const {trace, manager, onTraceLoad, scheduler, scrollQueueRef, rerender} = options; + + useLayoutEffect(() => { + if (initializedRef.current) { + return; + } + if (trace.type !== 'trace' || !manager) { + return; + } + + initializedRef.current = true; + + if (!scrollQueueRef.current) { + onTraceLoad(trace, null, null); + return; + } + + // Node path has higher specificity than eventId + const promise = scrollQueueRef.current?.path + ? TraceTree.ExpandToPath(trace, scrollQueueRef.current.path, rerender, { + api, + organization, + }) + : scrollQueueRef.current.eventId + ? TraceTree.ExpandToEventID(scrollQueueRef?.current?.eventId, trace, rerender, { + api, + organization, + }) + : Promise.resolve(null); + + promise + .then(maybeNode => { + onTraceLoad(trace, maybeNode?.node ?? null, maybeNode?.index ?? null); + + if (!maybeNode) { + Sentry.captureMessage('Failed to find and scroll to node in tree'); + return; + } + }) + .finally(() => { + // Important to set scrollQueueRef.current to null and trigger a rerender + // after the promise resolves as we show a loading state during scroll, + // else the screen could jump around while we fetch span data + scrollQueueRef.current = null; + rerender(); + // Allow react to rerender before dispatching the init event + requestAnimationFrame(() => { + scheduler.dispatch('initialize virtualized list'); + }); + }); + }, [ + api, + trace, + manager, + onTraceLoad, + scheduler, + scrollQueueRef, + rerender, + initializedRef, + organization, + ]); +} diff --git a/static/app/views/performance/newTraceDetails/useTraceScrollToPath.tsx b/static/app/views/performance/newTraceDetails/useTraceScrollToPath.tsx new file mode 100644 index 00000000000000..c373112cb5da90 --- /dev/null +++ b/static/app/views/performance/newTraceDetails/useTraceScrollToPath.tsx @@ -0,0 +1,59 @@ +import {useRef} from 'react'; +import * as qs from 'query-string'; + +import type { + TraceTree, + TraceTreeNode, +} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree'; + +function decodeScrollQueue(maybePath: unknown): TraceTree.NodePath[] | null { + if (Array.isArray(maybePath)) { + return maybePath; + } + + if (typeof maybePath === 'string') { + return [maybePath as TraceTree.NodePath]; + } + + return null; +} + +type UseTraceScrollToPath = + | {eventId?: string; path?: TraceTree.NodePath[]} + | null + | undefined; + +export function useTraceScrollToPath( + path: UseTraceScrollToPath +): React.MutableRefObject { + const scrollQueueRef = useRef< + {eventId?: string; path?: TraceTree.NodePath[]} | null | undefined + >(undefined); + + // If we havent decoded anything yet, then decode the path + if (scrollQueueRef.current === undefined) { + let scrollToNode: UseTraceScrollToPath = path; + + if (!path) { + const queryParams = qs.parse(location.search); + + scrollToNode = { + eventId: queryParams.eventId as string | undefined, + path: decodeScrollQueue( + queryParams.node + ) as TraceTreeNode['path'], + }; + } + + if (scrollToNode && (scrollToNode.path || scrollToNode.eventId)) { + scrollQueueRef.current = { + eventId: scrollToNode.eventId as string, + path: scrollToNode.path, + }; + } else { + scrollQueueRef.current = null; + } + } + + return scrollQueueRef; +} From bebdd31a39c690ef259be0a68b5ffdbc6cd5b509 Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 26 Sep 2024 17:19:45 -0400 Subject: [PATCH 7/8] fix merge --- .../useTraceScrollToEventOnLoad.tsx | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/useTraceScrollToEventOnLoad.tsx b/static/app/views/performance/newTraceDetails/useTraceScrollToEventOnLoad.tsx index 86e70301e3c401..72bc60611578df 100644 --- a/static/app/views/performance/newTraceDetails/useTraceScrollToEventOnLoad.tsx +++ b/static/app/views/performance/newTraceDetails/useTraceScrollToEventOnLoad.tsx @@ -3,6 +3,10 @@ import Sentry from '@sentry/react'; import useApi from 'sentry/utils/useApi'; import useOrganization from 'sentry/utils/useOrganization'; +import { + isAutogroupedNode, + isTransactionNode, +} from 'sentry/views/performance/newTraceDetails/guards'; import { TraceTree, type TraceTreeNode, @@ -59,13 +63,53 @@ export function useTraceScrollToEventOnLoad(options: UseTraceScrollToEventOnLoad : Promise.resolve(null); promise - .then(maybeNode => { - onTraceLoad(trace, maybeNode?.node ?? null, maybeNode?.index ?? null); + .then(async node => { + if (!scrollQueueRef.current?.path && !scrollQueueRef.current?.eventId) { + return; + } - if (!maybeNode) { + if (!node) { Sentry.captureMessage('Failed to find and scroll to node in tree'); return; } + + // When users are coming off an eventID link, we want to fetch the children + // of the node that the eventID points to. This is because the eventID link + // only points to the transaction, but we want to fetch the children of the + // transaction to show the user the list of spans in that transaction + if (scrollQueueRef.current.eventId && node?.canFetch) { + await trace.zoomIn(node, true, {api, organization}).catch(_e => { + Sentry.captureMessage('Failed to fetch children of eventId on mount'); + }); + } + + let index = trace.list.indexOf(node); + // We have found the node, yet it is somehow not in the visible tree. + // This means that the path we were given did not match the current tree. + // This sometimes happens when we receive external links like span-x, txn-y + // however the resulting tree looks like span-x, autogroup, txn-y. In this case, + // we should expand the autogroup node and try to find the node again. + if (node && index === -1) { + let parent_node = node.parent; + while (parent_node) { + // Transactions break autogrouping chains, so we can stop here + if (isTransactionNode(parent_node)) { + break; + } + if (isAutogroupedNode(parent_node)) { + trace.expand(parent_node, true); + // This is very wasteful as it performs O(n^2) search each time we expand a node... + // In most cases though, we should be operating on a tree with sub 10k elements and hopefully + // a low autogrouped node count. + index = node ? trace.list.findIndex(n => n === node) : -1; + if (index !== -1) { + break; + } + } + parent_node = parent_node.parent; + } + } + onTraceLoad(trace, node, index === -1 ? null : index); }) .finally(() => { // Important to set scrollQueueRef.current to null and trigger a rerender From c368f9b318013e7af0097c6315e96fb013cb002e Mon Sep 17 00:00:00 2001 From: JonasBa Date: Thu, 26 Sep 2024 17:40:59 -0400 Subject: [PATCH 8/8] ref: ensure loading until everything laod --- static/app/views/performance/newTraceDetails/index.tsx | 8 +++++++- static/app/views/performance/newTraceDetails/trace.tsx | 6 ++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/index.tsx b/static/app/views/performance/newTraceDetails/index.tsx index c4237d90928336..a079083e51e424 100644 --- a/static/app/views/performance/newTraceDetails/index.tsx +++ b/static/app/views/performance/newTraceDetails/index.tsx @@ -976,6 +976,11 @@ export function TraceViewWaterfall(props: TraceViewWaterfallProps) { trace: tree, }); + const isLoading = !!( + tree.type === 'loading' || + tree.type !== 'trace' || + scrollQueueRef.current + ); return ( {tree.type === 'error' ? ( ) : tree.type === 'empty' ? ( - ) : tree.type === 'loading' || tree.type !== 'trace' ? ( + ) : isLoading ? ( ) : null} diff --git a/static/app/views/performance/newTraceDetails/trace.tsx b/static/app/views/performance/newTraceDetails/trace.tsx index 497c44236917fe..64313d17e2938a 100644 --- a/static/app/views/performance/newTraceDetails/trace.tsx +++ b/static/app/views/performance/newTraceDetails/trace.tsx @@ -89,6 +89,7 @@ function computeNextIndexFromAction( interface TraceProps { forceRerender: number; isEmbedded: boolean; + isLoading: boolean; manager: VirtualizedViewManager; onRowClick: ( node: TraceTreeNode, @@ -118,6 +119,7 @@ export function Trace({ forceRerender, trace_id, isEmbedded, + isLoading, }: TraceProps) { const theme = useTheme(); const api = useApi(); @@ -352,10 +354,10 @@ export function Trace({ ); const render = useMemo(() => { - return trace.type !== 'trace' + return trace.type !== 'trace' || isLoading ? r => renderLoadingRow(r) : r => renderVirtualizedRow(r); - }, [renderLoadingRow, renderVirtualizedRow, trace.type]); + }, [isLoading, renderLoadingRow, renderVirtualizedRow, trace.type]); const traceNode = trace.root.children[0]; const traceStartTimestamp = traceNode?.space?.[0];