diff --git a/static/app/views/performance/newTraceDetails/traceApi/utils.tsx b/static/app/views/performance/newTraceDetails/traceApi/utils.tsx index c2b0e412d65530..7c79a0dc5481c3 100644 --- a/static/app/views/performance/newTraceDetails/traceApi/utils.tsx +++ b/static/app/views/performance/newTraceDetails/traceApi/utils.tsx @@ -56,7 +56,8 @@ export const getRepresentativeTraceEvent = ( return {type: 'uptime_check', event: traceChild.value}; } - let rootEvent: TraceTree.TraceEvent | null = null; + let preferredRootEvent: TraceTree.TraceEvent | null = null; + let firstRootEvent: TraceTree.TraceEvent | null = null; let candidateEvent: TraceTree.TraceEvent | null = null; let firstEvent: TraceTree.TraceEvent | null = null; @@ -66,26 +67,22 @@ export const getRepresentativeTraceEvent = ( : [...traceNode.value.transactions, ...traceNode.value.orphan_errors]; for (const event of events) { if (isRootEvent(event)) { - rootEvent = event; + if (!firstRootEvent) { + firstRootEvent = event; + } - if (!isEAP) { - // For non-EAP traces, we return the first root event. + if (hasPreferredOp(event)) { + preferredRootEvent = event; break; } - + // Otherwise we keep looking for a root eap transaction. If we don't find one, we use other roots, like standalone spans. continue; } else if ( // If we haven't found a root transaction, but we found a candidate transaction // with an op that we care about, we can use it for the title. We keep looking for // a root. !candidateEvent && - CANDIDATE_TRACE_TITLE_OPS.includes( - 'transaction.op' in event - ? event['transaction.op'] - : 'op' in event - ? event.op - : '' - ) + hasPreferredOp(event) ) { candidateEvent = event; continue; @@ -97,7 +94,7 @@ export const getRepresentativeTraceEvent = ( } return { - event: rootEvent ?? candidateEvent ?? firstEvent, + event: preferredRootEvent ?? firstRootEvent ?? candidateEvent ?? firstEvent, type: 'span', }; }; @@ -113,3 +110,18 @@ export const isValidEventUUID = (id: string): boolean => { /^[0-9a-f]{8}[0-9a-f]{4}[1-5][0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}$/i; return uuidRegex.test(id); }; + +/** + * Prefer "special" root events over generic root events when generating a title + * for the waterfall view. Picking these improves contextual navigation for linked + * traces, resulting in more meaningful waterfall titles. + */ +function hasPreferredOp(event: TraceTree.TraceEvent): boolean { + const op = + 'op' in event + ? event.op + : 'transaction.op' in event + ? event['transaction.op'] + : undefined; + return !!op && CANDIDATE_TRACE_TITLE_OPS.includes(op); +} diff --git a/static/app/views/performance/newTraceDetails/traceHeader/index.tsx b/static/app/views/performance/newTraceDetails/traceHeader/index.tsx index 0104abe455efe7..97778ac59edae2 100644 --- a/static/app/views/performance/newTraceDetails/traceHeader/index.tsx +++ b/static/app/views/performance/newTraceDetails/traceHeader/index.tsx @@ -92,7 +92,11 @@ export function TraceMetaDataHeader(props: TraceMetadataHeaderProps) { - + <Title + representativeEvent={rep} + rootEventResults={props.rootEventResults} + tree={props.tree} + /> <Meta organization={props.organization} tree={props.tree} diff --git a/static/app/views/performance/newTraceDetails/traceHeader/title.tsx b/static/app/views/performance/newTraceDetails/traceHeader/title.tsx index 90375f515401a4..927012ccbd3580 100644 --- a/static/app/views/performance/newTraceDetails/traceHeader/title.tsx +++ b/static/app/views/performance/newTraceDetails/traceHeader/title.tsx @@ -16,21 +16,21 @@ import { type RepresentativeTraceEvent, } from 'sentry/views/performance/newTraceDetails/traceApi/utils'; import {findSpanAttributeValue} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/utils'; -import { - isEAPError, - isTraceError, - isUptimeCheck, -} from 'sentry/views/performance/newTraceDetails/traceGuards'; +import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree'; import {makeReplaysPathname} from 'sentry/views/replays/pathnames'; interface TitleProps { representativeEvent: RepresentativeTraceEvent; rootEventResults: TraceRootEventQueryResults; + tree: TraceTree; } -function getTitle(representativeEvent: RepresentativeTraceEvent): { - subtitle: string | undefined; +function getTitle( + tree: TraceTree, + representativeEvent: RepresentativeTraceEvent +): { title: string; + subtitle?: string; } | null { const {event} = representativeEvent; if (!event) { @@ -45,36 +45,8 @@ function getTitle(representativeEvent: RepresentativeTraceEvent): { }; } - // Handle error events - if (isEAPError(event) || isTraceError(event)) { - const subtitle = isEAPError(event) ? event.description : event.title || event.message; - - return { - title: t('Trace'), - subtitle, - }; - } - - // Handle uptime check traces - if (isUptimeCheck(event)) { - return { - title: t('Uptime Monitor Check'), - subtitle: `${event.additional_attributes?.method} ${event.additional_attributes?.request_url}`, - }; - } - - if (!('transaction' in event)) { - return null; - } - - // Normalize operation field access across event types - const op = - 'transaction.op' in event ? event['transaction.op'] : 'op' in event ? event.op : ''; - - return { - title: op || t('Trace'), - subtitle: event.transaction, - }; + const node = tree.root.findChild(n => n.value === event); + return node?.traceHeaderTitle ?? null; } function ContextBadges({rootEventResults}: Pick<TitleProps, 'rootEventResults'>) { @@ -119,8 +91,8 @@ const ReplayButton = styled(LinkButton)` text-decoration-style: dotted; `; -export function Title({representativeEvent, rootEventResults}: TitleProps) { - const traceTitle = getTitle(representativeEvent); +export function Title({representativeEvent, rootEventResults, tree}: TitleProps) { + const traceTitle = getTitle(tree, representativeEvent); return ( <div>