Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c0cf38d
fix: Stale waterfall selection when opening another span from the sam…
Oct 5, 2026
d29dc33
fix: comparing two different row-id formats
Oct 5, 2026
00fae35
fix in keep check
Oct 5, 2026
30a6b97
fix: changes in drawer span selection
Oct 9, 2026
7c5d48f
fix: changes in trace view mount
Oct 9, 2026
5c7646c
fix: stale span selection is fixed in both drawers
Oct 9, 2026
a0ce39b
fix: clearing span when session name cliked
Oct 12, 2026
4602d88
fix: lint issue
Oct 14, 2026
cb5dde5
revert changes of ratchet test file.
Oct 14, 2026
0c3862e
Merge branch 'main' into fix_stale_waterfall_selection
pulpdrew Oct 14, 2026
010f01e
Merge branch 'main' into fix_stale_waterfall_selection
bahltushar Oct 15, 2026
a14431e
Merge branch 'main' into fix_stale_waterfall_selection
pulpdrew Oct 15, 2026
85d723f
chore: Fix lint warning
pulpdrew Oct 15, 2026
b74f028
Merge branch 'main' into fix_stale_waterfall_selection
pulpdrew Oct 15, 2026
2a7f725
feat: Add UI for configuring adhoc dashboard filters (#3341)
pulpdrew Oct 15, 2026
0d800d6
feat: Render adhoc dashboard filter inputs (#3343)
pulpdrew Oct 15, 2026
8b8ae5a
fix(app): keep dashboard tile menu on screen with many groups (#3360)
jordan-simonovski Oct 15, 2026
3491880
fix: spans details assertion to saw span body.
Oct 16, 2026
62829cc
Merge branch 'main' into fix_stale_waterfall_selection
bahltushar Oct 16, 2026
a75720e
Merge branch 'main' into fix_stale_waterfall_selection
pulpdrew Oct 16, 2026
c7a6898
Merge branch 'main' into fix_stale_waterfall_selection
bahltushar Oct 21, 2026
22833ce
fix: lint error and e2e test fix.
Oct 21, 2026
2cf87e4
Merge branch 'main' into fix_stale_waterfall_selection
bahltushar Oct 21, 2026
5b7cb58
Merge branch 'main' into fix_stale_waterfall_selection
bahltushar Oct 22, 2026
011d4c3
chore: Revert changes to unrelated files
pulpdrew Oct 22, 2026
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
5 changes: 5 additions & 0 deletions .changeset/trace-span-follows-opened-result.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperdx/app': patch
---

Fix the trace waterfall sticking on the previously opened span when another span of the same trace is opened, including from a session replay. Closing the result clears the selected span. Opening a different result selects that result's span, including when the waterfall stays mounted on cached data.
9 changes: 7 additions & 2 deletions packages/app/src/SessionSidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import SidePanelBreadcrumbs, {
BreadcrumbItem,
} from '@/components/SidePanelBreadcrumbs';
import { useCloseOnClickOutside } from '@/hooks/useCloseOnClickOutside';
import { useClearSpanSelection } from '@/hooks/useOpenedRowSpanSelection';
import useResizable from '@/hooks/useResizable';
import { WithClause } from '@/hooks/useRowWhere';
import {
Expand Down Expand Up @@ -129,25 +130,29 @@ export default function SessionSidePanel({
initialRowId: selectedEvent?.rowId,
});

const clearSpanSelection = useClearSpanSelection();

const handleBackToSession = useCallback(() => {
setSelectedEvent(null);
sidePanelStack.clearTrail();
}, [setSelectedEvent, sidePanelStack]);

const handleEventNavigate = useCallback(
(rowId: string, aliasWith: WithClause[]) => {
clearSpanSelection();
sidePanelStack.clearTrail();
setSelectedEvent({ rowId, aliasWith, sessionId });
},
[setSelectedEvent, sidePanelStack, sessionId],
[clearSpanSelection, setSelectedEvent, sidePanelStack, sessionId],
);

// X / Esc-at-root closes the whole panel and clears the session-panel params.
const handleClose = useCallback(() => {
clearSpanSelection();
setSelectedEvent(null);
sidePanelStack.clearTrail();
onClose();
}, [setSelectedEvent, sidePanelStack, onClose]);
}, [clearSpanSelection, setSelectedEvent, sidePanelStack, onClose]);

useHotkeys(['esc'], handleClose, { enabled: !selectedEvent });

Expand Down
45 changes: 41 additions & 4 deletions packages/app/src/components/DBRowSidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from 'react';
import { add } from 'date-fns';
import { isString } from 'lodash';
import { useQueryState } from 'nuqs';
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';
import { useHotkeys } from 'react-hotkeys-hook';
import SqlString from 'sqlstring';
Expand Down Expand Up @@ -37,6 +38,7 @@ import {
import { IconCopy, IconKeyboard, IconShare, IconX } from '@tabler/icons-react';

import { useCloseOnClickOutside } from '@/hooks/useCloseOnClickOutside';
import { useOpenedRowSpanSelection } from '@/hooks/useOpenedRowSpanSelection';
import useResizable from '@/hooks/useResizable';
import { WithClause } from '@/hooks/useRowWhere';
import useSidePanelStack, {
Expand Down Expand Up @@ -77,6 +79,7 @@ import {
DrawerFullWidthToggle,
INITIAL_DRAWER_WIDTH_PERCENT,
} from './DrawerUtils';
import { eventRowWhereParser } from './eventRowWhere';
import LogLevel from './LogLevel';
import SidePanelBreadcrumbs, { BreadcrumbItem } from './SidePanelBreadcrumbs';
import { SpanLinkData } from './SpanLinksSubpanel';
Expand Down Expand Up @@ -203,6 +206,7 @@ function SidePanelHeaderActions({
size="sm"
onClick={onClose}
aria-label="Close"
data-testid="side-panel-close"
>
<IconX size={16} />
</ActionIcon>
Expand Down Expand Up @@ -407,18 +411,22 @@ export const DBRowSidePanelInner = ({
[pushSource],
);

const clearSpanSelection = useOpenedRowSpanSelection(initialRowId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 major — Clicking the session breadcrumb leaves the old span selected, so the session replay bug still happens

The span is only cleared in handlePanelBack (Esc and the Back button) and in closePanel. In packages/app/src/SessionSidePanel.tsx:236-241, the session breadcrumb passes onClick: handleBackToSession straight through to parentBreadcrumbs, and that path never calls clearSpanSelection. Here is how it breaks. Open event A from a replay; the waterfall selects span A. Click the session breadcrumb: selectedEvent becomes null and DBRowSidePanelInner unmounts. Open event B in the same trace. DBRowSidePanelInner mounts again, and openedRowIdRef starts out as B, so useOpenedRowSpanSelection never clears. useRowHighlightHint then treats span A as a restored selection on its first hint and keeps it. Fix: clear eventRowWhere in SessionSidePanel's handleBackToSession (and handleEventNavigate). Another option is to wrap the onClick of each parentBreadcrumbs item in DBRowSidePanelInner with clearSpanSelection. Then add a test for the breadcrumb path.

Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 major — Span selection is reset only when the root rowId changes, so stack navigation still shows a stale span

useOpenedRowSpanSelection(initialRowId) only fires when the root row changes. pushNav / pushSource (for example "View Trace" from a log, or picking a neighbour in surrounding context) change resolvedRowId but leave initialRowId the same. If the Trace tab remounts for the new row, useRowHighlightHint treats the leftover eventRowWhere from the same trace as a restored selection and keeps it, which is the stale-highlight bug this PR fixes. Pass resolvedRowId (the row actually shown) to the hook, or clear inside pushNav/pushSource/popOne.

Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 major — Span reset is keyed on the root row id, so in-panel navigation to another row in the same trace still shows the old span

The hint is built from normalizedRow (line 736), which follows the leaf row (resolvedRowId = nav/source-stack leaf), but useOpenedRowSpanSelection(initialRowId) only watches the root rowId prop. Repro: open span A of a trace (the waterfall auto-selects A). Switch to the Context tab, which unmounts the waterfall (conditional render at line 1148). Click a neighbouring span B from the same trace, which runs pushNav and leaves initialRowId unchanged. Switch back to Trace. The waterfall remounts and its first hint finds eventRowWhere = A for the same traceId, so useRowHighlightHint keeps A while the header shows B. That is the bug this PR fixes, reached through a different path. Fix: pass the row the hint is built from (resolvedRowId or activeRowId) to useOpenedRowSpanSelection, so pushing or popping a nav or source frame also clears the span.

Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Component size limit exceeded This change adds span-selection behavior to DBRowSidePanel.tsx, which is already over 1,400 lines. The repository requires component files to stay under 300 lines and large components to be broken down; the changed DBTracePanel.tsx and DBTraceWaterfallChart.tsx also exceed that limit. This requirement must be satisfied before merging.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 major — Span selection is cleared only when the root row changes, so drilldowns inside the panel still show the previous span

useOpenedRowSpanSelection(initialRowId) watches only the root row. A row pushed with pushNav, pushSource ("View Trace"), or reached by Back through popOne changes resolvedRowId but leaves initialRowId the same. For those rows isRowLoading renders "Loading...", which unmounts DBTracePanel. The waterfall then remounts, and useRowHighlightHint treats the old eventRowWhere (same trace) as a restored selection and keeps it. This is the same stale-span bug the PR fixes, reached through in-panel navigation. Example: open log A, click View Trace (span X is selected), go Back, open neighbour log B from the same trace in Context, click View Trace: X is still highlighted. Fix: key the hook on resolvedRowId, plus the active source id, instead of initialRowId.

Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 major — Navigating to another row inside the panel doesn't clear the span, so the stale-span bug comes back

useOpenedRowSpanSelection(initialRowId) only watches the root row, but the waterfall's hint follows the active row (resolvedRowId, from the nav/source stacks via normalizedRow, ~line 736). To reproduce: open trace span A, so the Trace tab selects A. Switch to Surrounding Context and click a neighbour span B from the same trace (this goes through pushNav, and initialRowId doesn't change). Return to the Trace tab. The waterfall remounts, the first hint gives way to the existing selection A (its traceId still matches), and the header shows B while the highlight and details show A. Back (popOne) has the same problem in reverse. Fix: pass resolvedRowId (or activeRowId) to the hook, and add a nav-stack case to DBRowSidePanel.spanSelection.test.tsx.

Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 major — Span selection is cleared only when the root row changes, not when in-panel navigation changes the row on screen

The hook is keyed on initialRowId, but the panel renders resolvedRowId (leafNav?.rowId ?? activeSourceFrame?.rowId ?? initialRowId, line 304-308). Reproduce: select span X in a row's trace tab, then use pushNav (a surrounding-context neighbour), pushSource (View trace) or Back/breadcrumb (popOne/truncateTo) to reach another row in the same trace. Each of these loads a new row, so the trace panel unmounts and remounts (isRowLoading at line 887). On remount useRowHighlightHint yields to the old eventRowWhere, so span X stays highlighted, which is the bug this PR fixes. Fix: pass resolvedRowId (or a source+row key) to useOpenedRowSpanSelection.

Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond


const handlePanelBack = useCallback(() => {
// Pop one level (nav → source), restoring the tab active before that level
// was entered. When the trail is empty, leave the panel: hand off to an
// embedding parent (session) or close.
// embedding parent (session) or close. The search drawer clears the span
// in its own close handler; a parent (the session) does not.
if (popOne() === 'none') {
if (onNavigateToParent) {
clearSpanSelection();
onNavigateToParent();
} else {
onClose();
}
}
}, [popOne, onNavigateToParent, onClose]);
}, [popOne, onNavigateToParent, onClose, clearSpanSelection]);

useHotkeys(['esc'], handlePanelBack);

Expand Down Expand Up @@ -760,7 +768,23 @@ export const DBRowSidePanelInner = ({
const items: BreadcrumbItem[] = [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — The inner panel wraps the parent's callbacks to clear the span on the session's behalf

Both handlePanelBack (via onNavigateToParent) and the parentBreadcrumbs wrapper end up calling SessionSidePanel's handleBackToSession, and that file already has clearSpanSelection. Call clearSpanSelection() inside handleBackToSession in SessionSidePanel.tsx. Then drop the breadcrumb .map wrapper and the extra call in handlePanelBack, so one layer owns this instead of two.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Clearing on return to the session is done in the child by wrapping the parent's callbacks

The only parent that passes onNavigateToParent and parentBreadcrumbs is SessionSidePanel, and it passes handleBackToSession for both. Call clearSpanSelection() once inside handleBackToSession (SessionSidePanel.tsx:135), the way handleClose and handleEventNavigate already do. Then remove the breadcrumb .map wrapper and the extra branch in handlePanelBack, so this flow has one owner instead of two.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond


if (parentBreadcrumbs) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Session span clearing is spread across Inner instead of living in SessionSidePanel.handleBackToSession

The new handlePanelBack branch (clearSpanSelection(); onNavigateToParent()) and the parentBreadcrumbs.map wrapper both exist only to clear the span before SessionSidePanel.handleBackToSession runs. That is the only onNavigateToParent/parent-crumb provider (packages/app/src/SessionSidePanel.tsx:135,238,245), and it already has useClearSpanSelection in scope. Call clearSpanSelection() inside handleBackToSession and drop the crumb-wrapping map and the extra branch in Inner. Back and the crumb then go through one place, and Inner no longer rewrites callbacks it was handed.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Session-only cleanup is spread into the generic inner panel instead of handleBackToSession

SessionSidePanel is the only caller that passes onNavigateToParent and parentBreadcrumbs, and both point at handleBackToSession (SessionSidePanel.tsx:135). That component already calls clearSpanSelection in handleEventNavigate and handleClose. Call it in handleBackToSession too, then remove the breadcrumb-wrapping map here and the extra clearSpanSelection() in handlePanelBack. That keeps all of the session's clear-on-leave logic in one place.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

items.push(...parentBreadcrumbs);
// A parent crumb (the session name) leaves this row without going

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Inner panel wraps the parent's breadcrumb and back callbacks to clear the span for the parent

SessionSidePanel already calls useClearSpanSelection in its own handlers. Put clearSpanSelection() in its handleBackToSession (packages/app/src/SessionSidePanel.tsx:135) and drop both the parentBreadcrumbs.map wrapper and the onNavigateToParent special case here. That way the code that owns the transition clears the span in one place, and future parent crumbs don't depend on this wrapper.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Span clearing for 'leave to session' is added in DBRowSidePanelInner by wrapping callbacks the parent passes in, when the parent already has a single handler for it

Only SessionSidePanel passes onNavigateToParent and parentBreadcrumbs, and both point at handleBackToSession (SessionSidePanel.tsx:135, 238, 245). Call clearSpanSelection() inside handleBackToSession, then drop the parentBreadcrumbs.map wrapper and the extra branch in handlePanelBack. That leaves one clear per exit path, owned by the parent that controls the exit, rather than Inner rewriting another component's callbacks.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

// through Back or Close. Clear the span on that click too.
items.push(
...parentBreadcrumbs.map(item => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Wrapping parent breadcrumb / onNavigateToParent to clear the span adds nothing and puts session logic in the shared inner panel

In SessionSidePanel.tsx, the only way to open an event is handleEventNavigate, and it already calls clearSpanSelection() before setSelectedEvent. handleClose also clears it. So clearing again when the user goes back to the session can't change what is shown next. Delete the parentBreadcrumbs.map(...) wrapper and the clearSpanSelection() call in handlePanelBack's onNavigateToParent branch. If you still want the URL cleaned up at the session root, add one clearSpanSelection() call inside handleBackToSession in SessionSidePanel.tsx. That function already backs both the Back button and the breadcrumb, so DBRowSidePanelInner wouldn't need to rewrite every parent crumb's onClick.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Clearing the span when leaving a session event lives in the wrong component

onNavigateToParent and the parent breadcrumb onClick both point at SessionSidePanel.handleBackToSession (packages/app/src/SessionSidePanel.tsx:135), and that handler is the only one either path calls. Put clearSpanSelection() in handleBackToSession, next to the clears already in handleEventNavigate and handleClose. Then drop the parentBreadcrumbs.map wrapper and the onNavigateToParent-only clear in handlePanelBack. As written, the session's "leave this event" rule is split between the parent and a child that rewrites the parent's callbacks, and a new parent crumb or back path added later will miss it.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

const onClick = item.onClick;
if (onClick == null) {
return item;
}
return {
...item,
onClick: () => {
clearSpanSelection();
onClick();
},
};
}),
);
}

const crumbSourceStack = sourceStack;
Expand Down Expand Up @@ -822,6 +846,7 @@ export const DBRowSidePanelInner = ({
source.kind,
handleBreadcrumbNavigation,
parentBreadcrumbs,
clearSpanSelection,
]);

const availableTabs = useMemo<Tab[]>(() => {
Expand Down Expand Up @@ -1327,6 +1352,10 @@ export default function DBRowSidePanelErrorBoundary({
}, [isFullWidth, setSize]);

const { clear: clearTraceWaterfallSearchState } = useWaterfallSearchState({});
const [, setEventRowWhere] = useQueryState(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — _onClose sets up its own eventRowWhere setter instead of using the new useClearSpanSelection

This PR adds useClearSpanSelection (packages/app/src/hooks/useOpenedRowSpanSelection.ts:14), which wraps exactly useQueryState('eventRowWhere', eventRowWhereParser) + set(null). SessionSidePanel uses it, but DBRowSidePanelErrorBoundary sets up its own useQueryState and calls setEventRowWhere(null) directly. Use const clearSpanSelection = useClearSpanSelection() here and drop the nuqs/eventRowWhereParser imports from this file's boundary component. That leaves one definition of how a span selection is cleared.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Drawer close re-implements the span-clearing hook this PR adds

DBRowSidePanelErrorBoundary creates its own useQueryState('eventRowWhere', eventRowWhereParser) just to call setEventRowWhere(null). That is exactly what useClearSpanSelection in hooks/useOpenedRowSpanSelection.ts does, and SessionSidePanel already uses that hook. Call const clearSpanSelection = useClearSpanSelection() here and use it in _onClose, so every clear goes through one place and this file no longer needs the nuqs/eventRowWhereParser imports.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — The close handler sets up its own eventRowWhere setter instead of using the new useClearSpanSelection

Replace the local useQueryState('eventRowWhere', eventRowWhereParser) with const clearSpanSelection = useClearSpanSelection() from @/hooks/useOpenedRowSpanSelection. Then every clear path goes through one helper, and the query key/parser are no longer repeated here.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Close handler clears the span with its own useQueryState instead of the new useClearSpanSelection hook

DBRowSidePanelErrorBoundary opens a second useQueryState('eventRowWhere', eventRowWhereParser) just to call setEventRowWhere(null). That is exactly what useClearSpanSelection in hooks/useOpenedRowSpanSelection.ts does, and SessionSidePanel already uses that hook. Call const clearSpanSelection = useClearSpanSelection() here so 'how to clear the selected span' lives in one place.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Close handler re-implements useClearSpanSelection with its own useQueryState

DBRowSidePanelErrorBoundary opens a second useQueryState('eventRowWhere', eventRowWhereParser) just to call setEventRowWhere(null). The new useClearSpanSelection in hooks/useOpenedRowSpanSelection.ts does exactly this, and SessionSidePanel already uses it. Use const clearSpanSelection = useClearSpanSelection() here, and drop the nuqs and eventRowWhereParser imports from this file.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — The drawer close handler repeats useClearSpanSelection inline

DBRowSidePanelErrorBoundary sets up its own useQueryState('eventRowWhere', eventRowWhereParser) just to call setEventRowWhere(null). That is exactly what the new useClearSpanSelection in hooks/useOpenedRowSpanSelection.ts does. Use const clearSpanSelection = useClearSpanSelection() and remove the direct nuqs and eventRowWhereParser imports here.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Second hand-rolled eventRowWhere setter instead of the new useClearSpanSelection hook

DBRowSidePanelErrorBoundary creates its own useQueryState('eventRowWhere', eventRowWhereParser) only to call setEventRowWhere(null). That is exactly what useClearSpanSelection (hooks/useOpenedRowSpanSelection.ts) does, and SessionSidePanel already uses it. Use const clearSpanSelection = useClearSpanSelection() here and drop the nuqs and parser imports.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — The close handler sets up its own eventRowWhere setter instead of using the new useClearSpanSelection hook

This PR adds useClearSpanSelection in packages/app/src/hooks/useOpenedRowSpanSelection.ts for exactly this operation, and SessionSidePanel already uses it. Call const clearSpanSelection = useClearSpanSelection() here and use it in _onClose. That removes the extra useQueryState('eventRowWhere', eventRowWhereParser) and the nuqs/eventRowWhereParser imports from this file, so clearing the span happens in one place.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Drawer close sets up its own eventRowWhere query state instead of reusing the new useClearSpanSelection hook

Replace the local useQueryState('eventRowWhere', eventRowWhereParser) and setEventRowWhere(null) with const clearSpanSelection = useClearSpanSelection() from @/hooks/useOpenedRowSpanSelection, which this PR adds and already uses in SessionSidePanel and DBRowSidePanelInner. Then every place that clears the span goes through one helper.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

'eventRowWhere',
eventRowWhereParser,
);

const sidePanelStack = useSidePanelStack({ initialRowId: rowId });

Expand All @@ -1336,8 +1365,16 @@ export default function DBRowSidePanelErrorBoundary({
// Clear waterfall search state on close, so that filters don't
// persist when reopening another trace.
clearTraceWaterfallSearchState();
// The selected span is in the URL. Leaving it there makes the next open,
// and any shared row link, show the previous span.
setEventRowWhere(null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 minor — Span-selection clearing is spread across three call sites and runs twice on normal close

With the boundary, the X button runs Inner's closePanel (clear) and then _onClose (clear again). Outside click, Drawer onClose and the error fallback only reach _onClose, and the session panel only reaches Inner's clear. Every new way out of the panel has to remember to clear, which is how the session breadcrumb path above got missed. Move the clear into one place that owns leaving the panel, e.g. have sidePanelStack.clearTrail() or a shared leavePanel helper clear eventRowWhere, and drop the per-call-site setEventRowWhere(null)/clearSpanSelection() calls.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

onClose();
}, [sidePanelStack, onClose, clearTraceWaterfallSearchState]);
}, [
sidePanelStack,
onClose,
clearTraceWaterfallSearchState,
setEventRowWhere,
]);

useCloseOnClickOutside({
// Only close on outside click at the root level. When the user has
Expand Down
33 changes: 3 additions & 30 deletions packages/app/src/components/DBTracePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ import { useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { useQueryState } from 'nuqs';
import { useForm, useWatch } from 'react-hook-form';
import { z } from 'zod';
import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata';
import {
isLogSource,
isTraceSource,
SourceKind,
TSource,
WithClauseSchema,
} from '@hyperdx/common-utils/dist/types';
import {
ActionIcon,
Expand All @@ -38,10 +36,10 @@ import {
import { DBTraceWaterfallChartContainer } from '@/components/DBTraceWaterfallChart';
import { SQLInlineEditorControlled } from '@/components/SQLEditor/SQLInlineEditor';
import useResizable from '@/hooks/useResizable';
import { RowHighlightHint } from '@/hooks/useRowHighlightHint';
import { WithClause } from '@/hooks/useRowWhere';
import { useSource, useUpdateSource } from '@/source';
import TabBar from '@/TabBar';
import { parseAsJsonEncoded } from '@/utils/queryParsers';

import DBInfraPanel from './DBInfraPanel';
import { RowDataPanel, rowHasK8sContext, useRowData } from './DBRowDataPanel';
Expand All @@ -50,35 +48,14 @@ import {
deriveRowSidePanelContextForSource,
RowSidePanelContext,
} from './DBRowSidePanel';
import { eventRowWhereParser } from './eventRowWhere';
import SourceSchemaPreview, {
isSourceSchemaPreviewEnabled,
} from './SourceSchemaPreview';
import { SourceSelectControlled } from './SourceSelect';

import resizeStyles from '@/../styles/ResizablePanel.module.scss';

type EventRowWhere = {
id: string;
type: string;
aliasWith: WithClause[];
// The trace this span selection was made in. Used to gate a selection left in
// the URL from a previous trace so it can't render against a different one.
traceId?: string;
};

// Validate the persisted span selection so a stale / hand-edited `eventRowWhere`
// (valid JSON but wrong shape) resolves to null instead of feeding a malformed
// row id into the span detail query.
const eventRowWhereSchema = z.object({
id: z.string(),
type: z.string(),
aliasWith: z.array(WithClauseSchema),
traceId: z.string().optional(),
});
const eventRowWhereParser = parseAsJsonEncoded<EventRowWhere>(
eventRowWhereSchema.parse,
);

enum SpanDetailTab {
Overview = 'overview',
Parsed = 'parsed',
Expand Down Expand Up @@ -255,11 +232,7 @@ export default function DBTracePanel({
// Passed in from side panel to try to identify which
// span in the chart to highlight first without constructing
// a full row where clause
initialRowHighlightHint?: {
timestamp: string;
spanId: string;
body: string;
};
initialRowHighlightHint?: RowHighlightHint;
emptyState?: ReactNode;
'data-testid'?: string;
}) {
Expand Down
73 changes: 13 additions & 60 deletions packages/app/src/components/DBTraceWaterfallChart.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import {
ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { ReactNode, useCallback, useMemo, useState } from 'react';
import _, { omit } from 'lodash';
import { useForm } from 'react-hook-form';
import SqlString from 'sqlstring';
Expand Down Expand Up @@ -59,6 +52,10 @@ import {
} from '@/components/traceWaterfallLimits';
import { useColumns } from '@/hooks/useMetadata';
import useOffsetPaginatedQuery from '@/hooks/useOffsetPaginatedQuery';
import {
RowHighlightHint,
useRowHighlightHint,
} from '@/hooks/useRowHighlightHint';
import useRowWhere, { WithClause } from '@/hooks/useRowWhere';
import useWaterfallSearchState from '@/hooks/useWaterfallSearchState';
import {
Expand Down Expand Up @@ -686,11 +683,7 @@ export function DBTraceWaterfallChartContainer({
aliasWith: WithClause[];
}) => void;
highlightedRowWhere?: string | null;
initialRowHighlightHint?: {
timestamp: string;
spanId: string;
body: string;
};
initialRowHighlightHint?: RowHighlightHint;
emptyState?: ReactNode;
/** Extra controls rendered in the waterfall controls bar (e.g. the correlated logs source selector). */
controlsExtra?: ReactNode;
Expand Down Expand Up @@ -967,53 +960,13 @@ export function DBTraceWaterfallChartContainer({
logRowsMeta,
]);

// Auto-select the originating span once when the panel opens — but only if
// nothing is already selected. Two cases must NOT trigger a (re-)select:
// - the user explicitly cleared the selection (closing the span detail), and
// - a selection was restored from the URL on load (deep link / reload).
// We mark the hint applied as soon as a selection exists or we apply it
// ourselves, so it never fires twice for the same hint; it re-fires only when
// the hint genuinely changes (different originating span / trace).
const appliedHighlightHintRef = useRef<string | null>(null);

useEffect(() => {
appliedHighlightHintRef.current = null;
}, [traceId]);

useEffect(() => {
if (!initialRowHighlightHint || !onClick) {
return;
}

const hintKey = `${initialRowHighlightHint.timestamp}|${initialRowHighlightHint.spanId}|${initialRowHighlightHint.body}`;
if (appliedHighlightHintRef.current === hintKey) {
return;
}

// A selection already exists (restored from the URL, or user-chosen). Honor
// it and record the hint so we never override it — now or later.
if (highlightedRowWhere != null) {
appliedHighlightHintRef.current = hintKey;
return;
}

const initialRowHighlightIndex = rows.findIndex(row => {
return (
row.Timestamp === initialRowHighlightHint.timestamp &&
row.SpanId === initialRowHighlightHint.spanId &&
row.Body === initialRowHighlightHint.body
);
});

if (initialRowHighlightIndex !== -1) {
appliedHighlightHintRef.current = hintKey;
onClick({
id: rows[initialRowHighlightIndex].id,
type: rows[initialRowHighlightIndex].type ?? '',
aliasWith: rows[initialRowHighlightIndex].aliasWith,
});
}
}, [initialRowHighlightHint, rows, onClick, highlightedRowWhere]);
useRowHighlightHint({
traceId,
initialRowHighlightHint,
highlightedRowWhere,
rows,
onClick,
});

// 3 Edge-cases
// 1. No spans, just logs (ex. sampling)
Expand Down
Loading
Loading