From 890c5bbb4e4c3d6d980031f24585592d7ed7222f Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Mon, 24 Nov 2025 15:41:48 -0500 Subject: [PATCH] fix(profiling): Link event id for profiles in trace view This ensures that we link with the appropriate event id in the trace view and do not show the transaction in the flamechart view if there's no transaction present. --- .../flamegraph/continuousFlamegraph.tsx | 8 +++-- static/app/types/core.tsx | 3 ++ .../utils/profiling/hooks/useSentryEvent.tsx | 9 ++--- .../traceDrawer/tabs/traceProfiles.tsx | 35 ++++++++++--------- .../profiling/continuousProfileProvider.tsx | 7 ++-- 5 files changed, 38 insertions(+), 24 deletions(-) diff --git a/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx b/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx index 09128b671bbc95..a52084c3a8ec01 100644 --- a/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx +++ b/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx @@ -334,7 +334,11 @@ export function ContinuousFlamegraph(): ReactElement { return profileGroup.profiles.find(p => p.threadId === flamegraphProfiles.threadId); }, [profileGroup, flamegraphProfiles.threadId]); - const spanTree: SpanTree = useMemo(() => { + const spanTree: SpanTree | null = useMemo(() => { + if (segment.type === 'empty') { + return null; + } + if (segment.type === 'resolved' && segment.data) { return new SpanTree( segment.data, @@ -346,7 +350,7 @@ export function ContinuousFlamegraph(): ReactElement { }, [segment]); const spanChart = useMemo(() => { - if (!profile) { + if (!profile || !spanTree) { return null; } diff --git a/static/app/types/core.tsx b/static/app/types/core.tsx index e0ee2d9c2715d2..999c4ebc56fd68 100644 --- a/static/app/types/core.tsx +++ b/static/app/types/core.tsx @@ -189,6 +189,8 @@ export type PageFilters = { projects: number[]; }; +type EmptyState = {type: 'empty'}; + type InitialState = {type: 'initial'}; type LoadingState = {type: 'loading'}; @@ -204,6 +206,7 @@ type ErroredState = { }; export type RequestState = + | EmptyState | InitialState | LoadingState | ResolvedState diff --git a/static/app/utils/profiling/hooks/useSentryEvent.tsx b/static/app/utils/profiling/hooks/useSentryEvent.tsx index 0f517470430ec7..cd05b4b6bb9cf6 100644 --- a/static/app/utils/profiling/hooks/useSentryEvent.tsx +++ b/static/app/utils/profiling/hooks/useSentryEvent.tsx @@ -19,7 +19,8 @@ function fetchSentryEvent( export function useSentryEvent( organizationSlug: string, projectSlug: string, - eventId: string | null + eventId: string | null, + disabled?: boolean ): RequestState { const api = useApi(); const [requestState, setRequestState] = useState>({ @@ -27,7 +28,7 @@ export function useSentryEvent( }); useLayoutEffect(() => { - if (eventId === null || !projectSlug || !organizationSlug) { + if (disabled || !eventId || !projectSlug || !organizationSlug) { return undefined; } @@ -47,7 +48,7 @@ export function useSentryEvent( return () => { api.clear(); }; - }, [api, organizationSlug, projectSlug, eventId]); + }, [api, organizationSlug, projectSlug, eventId, disabled]); - return requestState; + return disabled ? {type: 'empty'} : requestState; } diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/tabs/traceProfiles.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/tabs/traceProfiles.tsx index 1a6066d029f788..ebea731772e811 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/tabs/traceProfiles.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/tabs/traceProfiles.tsx @@ -20,6 +20,7 @@ import { isTransactionNode, } from 'sentry/views/performance/newTraceDetails/traceGuards'; import {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree'; +import type {TraceTreeNode} from 'sentry/views/performance/newTraceDetails/traceModels/traceTreeNode'; export function TraceProfiles({tree}: {tree: TraceTree}) { const {projects} = useProjects(); @@ -65,22 +66,7 @@ export function TraceProfiles({tree}: {tree: TraceTree}) { return null; } - const threadId = isEAPSpanNode(node) - ? (node.value.additional_attributes?.['thread.id'] ?? undefined) - : undefined; - const tid = typeof threadId === 'string' ? threadId : undefined; - - const query = isTransactionNode(node) - ? { - eventId: node.value.event_id, - tid, - } - : isSpanNode(node) - ? { - eventId: TraceTree.ParentTransaction(node)?.value?.event_id, - tid, - } - : {tid}; + const query = getProfileRouteQueryFromNode(node); const link = 'profiler_id' in profile @@ -158,6 +144,23 @@ export function TraceProfiles({tree}: {tree: TraceTree}) { ); } +function getProfileRouteQueryFromNode(node: TraceTreeNode) { + if (isTransactionNode(node)) { + return {eventId: node.value.event_id}; + } + if (isSpanNode(node)) { + return {eventId: TraceTree.ParentTransaction(node)?.value?.event_id}; + } + if (isEAPSpanNode(node)) { + const threadId = node.value.additional_attributes?.['thread.id'] ?? undefined; + return { + eventId: node.value.transaction_id, + tid: typeof threadId === 'string' ? threadId : undefined, + }; + } + return {}; +} + const ProfilesTable = styled('div')` display: grid !important; grid-template-columns: 1fr min-content; diff --git a/static/app/views/profiling/continuousProfileProvider.tsx b/static/app/views/profiling/continuousProfileProvider.tsx index 0cd65b12b0068b..abe795aa86712a 100644 --- a/static/app/views/profiling/continuousProfileProvider.tsx +++ b/static/app/views/profiling/continuousProfileProvider.tsx @@ -43,14 +43,17 @@ export default function ProfileAndTransactionProvider(): React.ReactElement { }; }, [location.query.start, location.query.end, location.query.profilerId]); + const eventId = decodeScalar(location.query.eventId) || null; + const [profile, setProfile] = useState>({ - type: 'initial', + type: eventId ? 'initial' : 'empty', }); const profileTransaction = useSentryEvent( organization.slug, projectSlug, - decodeScalar(location.query.eventId) || null + eventId, + !eventId // disable if no event id ); return (