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/traceModels/traceTree.tsx b/static/app/views/performance/newTraceDetails/traceModels/traceTree.tsx index df2065dd0ba16f..af7e9ac237fd84 100644 --- a/static/app/views/performance/newTraceDetails/traceModels/traceTree.tsx +++ b/static/app/views/performance/newTraceDetails/traceModels/traceTree.tsx @@ -231,7 +231,6 @@ function isServerRequestHandlerTransactionNode( function isBrowserRequestSpan(value: TraceTree.Span): boolean { return ( - // Adjust for SDK changes in https://github.com/getsentry/sentry-javascript/pull/13527 value.op === 'browser.request' || (value.op === 'browser' && value.description === 'request') ); 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} +
+
+
+ + +
+
+ ); +}