Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -346,7 +350,7 @@ export function ContinuousFlamegraph(): ReactElement {
}, [segment]);

const spanChart = useMemo(() => {
if (!profile) {
if (!profile || !spanTree) {
return null;
}

Expand Down
3 changes: 3 additions & 0 deletions static/app/types/core.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ export type PageFilters = {
projects: number[];
};

type EmptyState = {type: 'empty'};

type InitialState = {type: 'initial'};

type LoadingState = {type: 'loading'};
Expand All @@ -204,6 +206,7 @@ type ErroredState = {
};

export type RequestState<T> =
| EmptyState
| InitialState
| LoadingState
| ResolvedState<T>
Expand Down
9 changes: 5 additions & 4 deletions static/app/utils/profiling/hooks/useSentryEvent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@ function fetchSentryEvent<T extends Event>(
export function useSentryEvent<T extends Event>(
organizationSlug: string,
projectSlug: string,
eventId: string | null
eventId: string | null,
disabled?: boolean
): RequestState<T> {
const api = useApi();
const [requestState, setRequestState] = useState<RequestState<T>>({
type: 'initial',
});

useLayoutEffect(() => {
if (eventId === null || !projectSlug || !organizationSlug) {
if (disabled || !eventId || !projectSlug || !organizationSlug) {
return undefined;
}

Expand All @@ -47,7 +48,7 @@ export function useSentryEvent<T extends Event>(
return () => {
api.clear();
};
}, [api, organizationSlug, projectSlug, eventId]);
}, [api, organizationSlug, projectSlug, eventId, disabled]);

return requestState;
return disabled ? {type: 'empty'} : requestState;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -158,6 +144,23 @@ export function TraceProfiles({tree}: {tree: TraceTree}) {
);
}

function getProfileRouteQueryFromNode(node: TraceTreeNode<TraceTree.NodeValue>) {
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 {};
}
Comment on lines +152 to +162
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: getProfileRouteQueryFromNode can return undefined eventId, leading to ?eventId=undefined in URL for regular profiles due to missing dropUndefinedKeys.
Severity: HIGH | Confidence: High

🔍 Detailed Analysis

The getProfileRouteQueryFromNode function can return {eventId: undefined} for regular span nodes or an empty object for unknown node types. When a regular profile is associated with a span node, this {eventId: undefined} is passed to generateProfileFlamechartRouteWithQuery(), which does not use dropUndefinedKeys. This results in a URL parameter like ?eventId=undefined, creating an invalid query parameter instead of omitting it, breaking the intent of not showing the transaction if no transaction is present.

💡 Suggested Fix

Modify generateProfileFlamechartRouteWithQuery to use dropUndefinedKeys on its query object, similar to generateContinuousProfileFlamechartRouteWithQuery, to prevent undefined eventId values from being included in the URL.

🤖 Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location:
static/app/views/performance/newTraceDetails/traceDrawer/tabs/traceProfiles.tsx#L147-L162

Potential issue: The `getProfileRouteQueryFromNode` function can return `{eventId:
undefined}` for regular span nodes or an empty object for unknown node types. When a
regular profile is associated with a span node, this `{eventId: undefined}` is passed to
`generateProfileFlamechartRouteWithQuery()`, which does not use `dropUndefinedKeys`.
This results in a URL parameter like `?eventId=undefined`, creating an invalid query
parameter instead of omitting it, breaking the intent of not showing the transaction if
no transaction is present.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 3294777


const ProfilesTable = styled('div')`
display: grid !important;
grid-template-columns: 1fr min-content;
Expand Down
7 changes: 5 additions & 2 deletions static/app/views/profiling/continuousProfileProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<RequestState<Profiling.ProfileInput>>({
type: 'initial',
type: eventId ? 'initial' : 'empty',
});

const profileTransaction = useSentryEvent<EventTransaction>(
organization.slug,
projectSlug,
decodeScalar(location.query.eventId) || null
eventId,
!eventId // disable if no event id
);

return (
Expand Down
Loading