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);
+ });
+});