From 66a14c5a1dde0151e9c71830f4d57178e231b483 Mon Sep 17 00:00:00 2001 From: Warren Lee <5959690+wrn14897@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:52:49 -0700 Subject: [PATCH] fix(app): show not-found state when the side panel row lookup is empty A successful lookup with zero rows fell through to the tabs with an Invalid Date timestamp, so the trace panel's autocomplete and useSessionId queried with a NaN time range and ClickHouse rejected it (code 457). Render an EmptyState with the breadcrumb controls instead. Refs HDX-5832 --- .changeset/row-side-panel-not-found.md | 5 + .../app/src/components/DBRowSidePanel.tsx | 27 +++- .../DBRowSidePanel.rowNotFound.test.tsx | 145 ++++++++++++++++++ 3 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 .changeset/row-side-panel-not-found.md create mode 100644 packages/app/src/components/__tests__/DBRowSidePanel.rowNotFound.test.tsx diff --git a/.changeset/row-side-panel-not-found.md b/.changeset/row-side-panel-not-found.md new file mode 100644 index 0000000000..6f1602bb31 --- /dev/null +++ b/.changeset/row-side-panel-not-found.md @@ -0,0 +1,5 @@ +--- +'@hyperdx/app': patch +--- + +fix: Show "Event not found" in the row side panel when the row lookup returns nothing, instead of rendering the tabs with an invalid time range that made ClickHouse reject follow-up queries diff --git a/packages/app/src/components/DBRowSidePanel.tsx b/packages/app/src/components/DBRowSidePanel.tsx index 30e4b718d8..808fea0d05 100644 --- a/packages/app/src/components/DBRowSidePanel.tsx +++ b/packages/app/src/components/DBRowSidePanel.tsx @@ -35,7 +35,13 @@ import { Text, Tooltip, } from '@mantine/core'; -import { IconCopy, IconKeyboard, IconShare, IconX } from '@tabler/icons-react'; +import { + IconCopy, + IconKeyboard, + IconSearchOff, + IconShare, + IconX, +} from '@tabler/icons-react'; import { useCloseOnClickOutside } from '@/hooks/useCloseOnClickOutside'; import { useOpenedRowSpanSelection } from '@/hooks/useOpenedRowSpanSelection'; @@ -79,6 +85,7 @@ import { DrawerFullWidthToggle, INITIAL_DRAWER_WIDTH_PERCENT, } from './DrawerUtils'; +import EmptyState from './EmptyState'; import { eventRowWhereParser } from './eventRowWhere'; import LogLevel from './LogLevel'; import SidePanelBreadcrumbs, { BreadcrumbItem } from './SidePanelBreadcrumbs'; @@ -933,6 +940,24 @@ export const DBRowSidePanelInner = ({ ); } + // Without a row the timestamp is an Invalid Date, and the tabs would query + // with a NaN time range. + if (normalizedRow == null) { + return ( + <> + + {controls} + + } + title="Event not found" + description="It may have been deleted or aged out of retention. Use the back button or the breadcrumbs above to return." + /> + + ); + } + const showLogTraceActions = !sourceIsTrace && traceId && traceSourceId; return ( diff --git a/packages/app/src/components/__tests__/DBRowSidePanel.rowNotFound.test.tsx b/packages/app/src/components/__tests__/DBRowSidePanel.rowNotFound.test.tsx new file mode 100644 index 0000000000..599bcd027d --- /dev/null +++ b/packages/app/src/components/__tests__/DBRowSidePanel.rowNotFound.test.tsx @@ -0,0 +1,145 @@ +import React from 'react'; +import { TSource } from '@hyperdx/common-utils/dist/types'; +import { MantineProvider } from '@mantine/core'; +import { fireEvent, render, screen } from '@testing-library/react'; + +jest.mock('nuqs', () => { + const actual = jest.requireActual('nuqs'); + return { + ...actual, + useQueryState: (_key: string, parser?: { defaultValue?: unknown }) => [ + parser && 'defaultValue' in parser ? parser.defaultValue : null, + jest.fn(), + ], + }; +}); + +const mockUseRowData = jest.fn(); +jest.mock('../DBRowDataPanel', () => ({ + __esModule: true, + useRowData: (args: unknown) => mockUseRowData(args), + ROW_DATA_ALIASES: { + DURATION_MS: '__hdx_duration', + SPAN_KIND: '__hdx_span_kind', + SERVICE_NAME: '__hdx_service_name', + SEVERITY_TEXT: '__hdx_severity_text', + }, + rowHasK8sContext: () => false, + RowDataPanel: () => null, +})); + +jest.mock('@/source', () => ({ + __esModule: true, + getEventBody: () => '__hdx_body', + useSource: () => ({ data: undefined, isLoading: false, isSuccess: false }), +})); + +jest.mock('../DBSessionPanel', () => ({ + __esModule: true, + useSessionId: () => ({ rumSessionId: undefined, rumServiceName: undefined }), + DBSessionPanel: () => null, +})); + +jest.mock('@/utils/highlightedAttributes', () => ({ + __esModule: true, + getHighlightedAttributesFromData: () => [], +})); + +jest.mock('../DBTracePanel', () => ({ + __esModule: true, + default: () =>
, +})); +jest.mock('@/TabBar', () => ({ + __esModule: true, + default: () =>
, +})); +jest.mock('../ContextSidePanel', () => ({ + __esModule: true, + default: () => null, +})); +jest.mock('../DBInfraPanel', () => ({ __esModule: true, default: () => null })); +jest.mock('../DBRowOverviewPanel', () => ({ + __esModule: true, + RowOverviewPanel: () => null, +})); +jest.mock('../DBRowSidePanelErrorState', () => ({ + __esModule: true, + DBRowSidePanelErrorState: () => null, +})); +jest.mock('../DBRowSidePanelHeader', () => ({ + __esModule: true, + default: () => null, +})); +jest.mock('../LogLevel', () => ({ __esModule: true, default: () => null })); +jest.mock('../ServiceMap/ServiceMapSidePanel', () => ({ + __esModule: true, + default: () => null, +})); +jest.mock('../TimelineChart/utils', () => ({ + __esModule: true, + renderMs: () => '', +})); +jest.mock('../DrawerUtils', () => ({ + __esModule: true, + DrawerFullWidthToggle: () => null, + INITIAL_DRAWER_WIDTH_PERCENT: 50, +})); +jest.mock('@/LogSidePanelElements', () => ({ + __esModule: true, + KeyboardShortcutsModal: () => null, +})); +jest.mock('@/useFormatTime', () => ({ + __esModule: true, + FormatTime: () => null, +})); + +// NOTE: imported after the mock factories above. +import { DBRowSidePanelInner } from '@/components/DBRowSidePanel'; +import useSidePanelStack from '@/hooks/useSidePanelStack'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion +const TRACE_SOURCE = { id: 'trace-src', kind: 'trace' } as TSource; + +function Harness({ onClose }: { onClose: () => void }) { + const sidePanelStack = useSidePanelStack({ initialRowId: 'row' }); + return ( + + ); +} + +describe('DBRowSidePanelInner — row lookup returns no rows', () => { + beforeEach(() => { + mockUseRowData.mockReset().mockReturnValue({ + data: { data: [] }, + isLoading: false, + isSuccess: true, + isError: false, + error: null, + }); + }); + + it('shows a not-found state instead of the tabs', () => { + const onClose = jest.fn(); + render( + + + , + ); + + expect(screen.getByText('Event not found')).toBeInTheDocument(); + expect(screen.queryByTestId('tab-bar')).not.toBeInTheDocument(); + expect(screen.queryByTestId('trace-panel')).not.toBeInTheDocument(); + + fireEvent.click(screen.getByLabelText('Back')); + expect(onClose).toHaveBeenCalledTimes(1); + + fireEvent.click(screen.getByLabelText('Close')); + expect(onClose).toHaveBeenCalledTimes(2); + }); +});