fix: bypass aliasWith so that useRowWhere works correctly (#1623)

Revisit the bug fix for https://github.com/hyperdxio/hyperdx/pull/1614.

The alias map should be used in useRowWhere hook

Ref: HDX-3196

Example:
For select
```
Timestamp,ServiceName,SeverityText,Body AS b, concat(b, 'blabla')
```
The generated query from useRowWhere is
```
WITH (Body) AS b
SELECT
    *,
    Timestamp AS "__hdx_timestamp",
    Body AS "__hdx_body",
    TraceId AS "__hdx_trace_id",
    SpanId AS "__hdx_span_id",
    SeverityText AS "__hdx_severity_text",
    ServiceName AS "__hdx_service_name",
    ResourceAttributes AS "__hdx_resource_attributes",
    LogAttributes AS "__hdx_event_attributes"
FROM
    DEFAULT.otel_logs
WHERE
    (
        Timestamp = parseDateTime64BestEffort('2026-01-20T06:11:00.170000000Z', 9)
        AND ServiceName = 'hdx-oss-dev-api'
        AND SeverityText = 'info'
        AND Body = 'Received alert metric [saved_search source]'
        AND concat(b, 'blabla') = 'Received alert metric [saved_search source]blabla'
        AND TimestampTime = parseDateTime64BestEffort('2026-01-20T06:11:00Z', 9)
    )
LIMIT
    1
```
This commit is contained in:
Warren Lee 2026-01-21 23:21:40 +01:00 committed by GitHub
parent ddc54e43f0
commit db845604a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 273 additions and 106 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---
fix: bypass aliasWith so that useRowWhere works correctly

View file

@ -92,6 +92,7 @@ import WhereLanguageControlled from '@/components/WhereLanguageControlled';
import { IS_LOCAL_MODE } from '@/config';
import { useAliasMapFromChartConfig } from '@/hooks/useChartConfig';
import { useExplainQuery } from '@/hooks/useExplainQuery';
import { aliasMapToWithClauses } from '@/hooks/useRowWhere';
import { withAppNav } from '@/layout';
import {
useCreateSavedSearch,
@ -1348,18 +1349,7 @@ function DBSearchPage() {
const { data: aliasMap } = useAliasMapFromChartConfig(dbSqlRowTableConfig);
const aliasWith = useMemo(
() =>
Object.entries(aliasMap ?? {}).map(([key, value]) => ({
name: key,
sql: {
sql: value,
params: {},
},
isSubquery: false,
})),
[aliasMap],
);
const aliasWith = useMemo(() => aliasMapToWithClauses(aliasMap), [aliasMap]);
const histogramTimeChartConfig = useMemo(() => {
if (chartConfig == null) {

View file

@ -23,6 +23,7 @@ import DBRowSidePanel from '@/components/DBRowSidePanel';
import { DBTimeChart } from '@/components/DBTimeChart';
import { DrawerBody, DrawerHeader } from '@/components/DrawerUtils';
import { KubeTimeline, useV2LogBatch } from '@/components/KubeComponents';
import { RowWhereResult, WithClause } from '@/hooks/useRowWhere';
import { parseTimeQuery, useTimeQuery } from '@/timeQuery';
import { useZIndex, ZIndexContext } from '@/zIndex';
@ -136,7 +137,7 @@ function PodLogs({
logSource: TSource;
where: string;
rowId: string | null;
onRowClick: (rowId: string) => void;
onRowClick: (rowWhere: RowWhereResult) => void;
}) {
const [resultType, setResultType] = React.useState<'all' | 'error'>('all');
@ -231,8 +232,10 @@ export default function PodDetailsSidePanel({
);
const [rowId, setRowId] = React.useState<string | null>(null);
const handleRowClick = React.useCallback((rowWhere: string) => {
setRowId(rowWhere);
const [aliasWith, setAliasWith] = React.useState<WithClause[]>([]);
const handleRowClick = React.useCallback((rowWhere: RowWhereResult) => {
setRowId(rowWhere.where);
setAliasWith(rowWhere.aliasWith);
}, []);
const handleCloseRowSidePanel = React.useCallback(() => {
setRowId(null);
@ -466,6 +469,7 @@ export default function PodDetailsSidePanel({
<DBRowSidePanel
source={logSource}
rowId={rowId}
aliasWith={aliasWith}
onClose={handleCloseRowSidePanel}
isNestedPanel={true}
/>

View file

@ -13,7 +13,7 @@ import {
} from '@tabler/icons-react';
import { useVirtualizer } from '@tanstack/react-virtual';
import useRowWhere from '@/hooks/useRowWhere';
import useRowWhere, { RowWhereResult } from '@/hooks/useRowWhere';
import { useQueriedChartConfig } from './hooks/useChartConfig';
import { useFormatTime } from './useFormatTime';
@ -108,7 +108,7 @@ export const SessionEventList = ({
focus: { ts: number; setBy: string } | undefined;
minTs: number;
showRelativeTime: boolean;
onClick: (rowId: string) => void;
onClick: (rowWhere: RowWhereResult) => void;
onTimeClick: (ts: number) => void;
eventsFollowPlayerPosition: boolean;
}) => {

View file

@ -31,6 +31,7 @@ import {
} from '@tabler/icons-react';
import DBRowSidePanel from '@/components/DBRowSidePanel';
import { RowWhereResult, WithClause } from '@/hooks/useRowWhere';
import { SQLInlineEditorControlled } from './components/SQLInlineEditor';
import DOMPlayer from './DOMPlayer';
@ -79,6 +80,7 @@ export default function SessionSubpanel({
whereLanguage?: SearchConditionLanguage;
}) {
const [rowId, setRowId] = useState<string | undefined>(undefined);
const [aliasWith, setAliasWith] = useState<WithClause[]>([]);
// Without portaling the nested drawer close overlay will not render properly
const containerRef = useRef<HTMLDivElement | null>(null);
@ -103,6 +105,7 @@ export default function SessionSubpanel({
<DBRowSidePanel
source={traceSource}
rowId={rowId}
aliasWith={aliasWith}
onClose={() => {
setDrawerOpen(false);
setRowId(undefined);
@ -551,11 +554,12 @@ export default function SessionSubpanel({
aliasMap={aliasMap}
queriedConfig={sessionEventListConfig}
onClick={useCallback(
(id: string) => {
(rowWhere: RowWhereResult) => {
setDrawerOpen(true);
setRowId(id);
setRowId(rowWhere.where);
setAliasWith(rowWhere.aliasWith);
},
[setDrawerOpen, setRowId],
[setDrawerOpen, setRowId, setAliasWith],
)}
focus={focus}
onTimeClick={useCallback(

View file

@ -9,6 +9,7 @@ import { Paper } from '@mantine/core';
import { DBTimeChart } from '@/components/DBTimeChart';
import { useAliasMapFromChartConfig } from '@/hooks/useChartConfig';
import { aliasMapToWithClauses } from '@/hooks/useRowWhere';
import { intervalToDateRange, intervalToGranularity } from '@/utils/alerts';
import { getAlertReferenceLines } from './Alerts';
@ -46,14 +47,7 @@ export const AlertPreviewChart = ({
from: source.from,
whereLanguage: whereLanguage || undefined,
});
const aliasWith = Object.entries(aliasMap ?? {}).map(([key, value]) => ({
name: key,
sql: {
sql: value,
params: {},
},
isSubquery: false,
}));
const aliasWith = aliasMapToWithClauses(aliasMap);
return (
<Paper w="100%" h={200}>

View file

@ -13,6 +13,7 @@ import { useDebouncedValue } from '@mantine/hooks';
import { SQLInlineEditorControlled } from '@/components/SQLInlineEditor';
import WhereLanguageControlled from '@/components/WhereLanguageControlled';
import { RowWhereResult, WithClause } from '@/hooks/useRowWhere';
import SearchInputV2 from '@/SearchInputV2';
import { useSource } from '@/source';
import { formatAttributeClause } from '@/utils';
@ -106,6 +107,8 @@ export default function ContextSubpanel({
id: contextRowSource || '',
});
const [contextAliasWith, setContextAliasWith] = useState<WithClause[]>([]);
const handleContextSidePanelClose = useCallback(() => {
setContextRowId(null);
setContextRowSource(null);
@ -114,8 +117,9 @@ export default function ContextSubpanel({
const { setChildModalOpen } = useContext(RowSidePanelContext);
const handleRowExpandClick = useCallback(
(rowWhere: string) => {
setContextRowId(rowWhere);
(rowWhere: RowWhereResult) => {
setContextRowId(rowWhere.where);
setContextAliasWith(rowWhere.aliasWith);
setContextRowSource(source.id);
},
[source.id, setContextRowId, setContextRowSource],
@ -334,6 +338,7 @@ export default function ContextSubpanel({
<DBRowSidePanel
source={contextRowSidePanelSource}
rowId={contextRowId}
aliasWith={contextAliasWith}
onClose={handleContextSidePanelClose}
isNestedPanel={true}
breadcrumbPath={[

View file

@ -5,6 +5,7 @@ import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types';
import { Box } from '@mantine/core';
import { useQueriedChartConfig } from '@/hooks/useChartConfig';
import { WithClause } from '@/hooks/useRowWhere';
import { getDisplayedTimestampValueExpression, getEventBody } from '@/source';
import { getSelectExpressionsForHighlightedAttributes } from '@/utils/highlightedAttributes';
@ -26,9 +27,11 @@ export enum ROW_DATA_ALIASES {
export function useRowData({
source,
rowId,
aliasWith,
}: {
source: TSource;
rowId: string | undefined | null;
aliasWith?: WithClause[];
}) {
const eventBodyExpr = getEventBody(source);
@ -129,9 +132,10 @@ export function useRowData({
where: rowId ?? '0=1',
from: source.from,
limit: { limit: 1 },
...(aliasWith && aliasWith.length > 0 ? { with: aliasWith } : {}),
},
{
queryKey: ['row_side_panel', rowId, source],
queryKey: ['row_side_panel', rowId, aliasWith, source],
enabled: rowId != null,
},
);
@ -182,13 +186,15 @@ export function getJSONColumnNames(meta: ResponseJSON['meta'] | undefined) {
export function RowDataPanel({
source,
rowId,
aliasWith,
'data-testid': dataTestId,
}: {
source: TSource;
rowId: string | undefined | null;
aliasWith?: WithClause[];
'data-testid'?: string;
}) {
const { data, isLoading, isError } = useRowData({ source, rowId });
const { data, isLoading, isError } = useRowData({ source, rowId, aliasWith });
const firstRow = useMemo(() => {
const firstRow = { ...(data?.data?.[0] ?? {}) };

View file

@ -4,6 +4,7 @@ import pickBy from 'lodash/pickBy';
import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types';
import { Accordion, Box, Flex, Text } from '@mantine/core';
import { WithClause } from '@/hooks/useRowWhere';
import { getEventBody } from '@/source';
import { getHighlightedAttributesFromData } from '@/utils/highlightedAttributes';
@ -20,15 +21,17 @@ const EMPTY_OBJ = {};
export function RowOverviewPanel({
source,
rowId,
aliasWith,
hideHeader = false,
'data-testid': dataTestId,
}: {
source: TSource;
rowId: string | undefined | null;
aliasWith?: WithClause[];
hideHeader?: boolean;
'data-testid'?: string;
}) {
const { data } = useRowData({ source, rowId });
const { data } = useRowData({ source, rowId, aliasWith });
const { onPropertyAddClick, generateSearchUrl } =
useContext(RowSidePanelContext);

View file

@ -21,6 +21,7 @@ import DBRowSidePanelHeader, {
BreadcrumbPath,
} from '@/components/DBRowSidePanelHeader';
import useResizable from '@/hooks/useResizable';
import { WithClause } from '@/hooks/useRowWhere';
import useWaterfallSearchState from '@/hooks/useWaterfallSearchState';
import { LogSidePanelKbdShortcuts } from '@/LogSidePanelElements';
import { getEventBody } from '@/source';
@ -84,6 +85,7 @@ enum Tab {
type DBRowSidePanelProps = {
source: TSource;
rowId: string | undefined;
aliasWith?: WithClause[];
onClose: () => void;
isNestedPanel?: boolean;
breadcrumbPath?: BreadcrumbPath;
@ -92,6 +94,7 @@ type DBRowSidePanelProps = {
const DBRowSidePanel = ({
rowId: rowId,
aliasWith,
source,
isNestedPanel = false,
setSubDrawerOpen,
@ -108,6 +111,7 @@ const DBRowSidePanel = ({
} = useRowData({
source,
rowId,
aliasWith,
});
const { dbSqlRowTableConfig } = useContext(RowSidePanelContext);
@ -377,6 +381,7 @@ const DBRowSidePanel = ({
data-testid="side-panel-tab-overview"
source={source}
rowId={rowId}
aliasWith={aliasWith}
hideHeader={true}
/>
</ErrorBoundary>
@ -440,6 +445,7 @@ const DBRowSidePanel = ({
data-testid="side-panel-tab-parsed"
source={source}
rowId={rowId}
aliasWith={aliasWith}
/>
</ErrorBoundary>
)}
@ -518,6 +524,7 @@ const DBRowSidePanel = ({
export default function DBRowSidePanelErrorBoundary({
onClose,
rowId,
aliasWith,
source,
isNestedPanel,
breadcrumbPath = [],
@ -594,6 +601,7 @@ export default function DBRowSidePanelErrorBoundary({
<DBRowSidePanel
source={source}
rowId={rowId}
aliasWith={aliasWith}
onClose={_onClose}
isNestedPanel={isNestedPanel}
breadcrumbPath={breadcrumbPath}

View file

@ -78,7 +78,11 @@ import { useCsvExport } from '@/hooks/useCsvExport';
import { useTableMetadata } from '@/hooks/useMetadata';
import useOffsetPaginatedQuery from '@/hooks/useOffsetPaginatedQuery';
import { useGroupedPatterns } from '@/hooks/usePatterns';
import useRowWhere from '@/hooks/useRowWhere';
import useRowWhere, {
INTERNAL_ROW_FIELDS,
RowWhereResult,
WithClause,
} from '@/hooks/useRowWhere';
import { useSource } from '@/source';
import { UNDEFINED_WIDTH } from '@/tableUtils';
import { FormatTime } from '@/useFormatTime';
@ -120,7 +124,8 @@ const ACCESSOR_MAP: Record<string, AccessorFn> = {
const MAX_SCROLL_FETCH_LINES = 1000;
const MAX_CELL_LENGTH = 500;
const getRowId = (row: Record<string, any>): string => row.__hyperdx_id;
const getRowId = (row: Record<string, any>): string =>
row[INTERNAL_ROW_FIELDS.ID];
function retrieveColumnValue(column: string, row: Row): any {
const accessor = ACCESSOR_MAP[column] ?? ACCESSOR_MAP.default;
@ -334,7 +339,7 @@ export const RawLogTable = memo(
isLoading?: boolean;
fetchNextPage?: (options?: FetchNextPageOptions | undefined) => any;
onRowDetailsClick: (row: Record<string, any>) => void;
generateRowId: (row: Record<string, any>) => string;
generateRowId: (row: Record<string, any>) => RowWhereResult;
// onPropertySearchClick: (
// name: string,
// value: string | number | boolean,
@ -360,33 +365,39 @@ export const RawLogTable = memo(
onExpandedRowsChange?: (hasExpandedRows: boolean) => void;
collapseAllRows?: boolean;
showExpandButton?: boolean;
renderRowDetails?: (row: Record<string, any>) => React.ReactNode;
renderRowDetails?: (row: {
id: string;
aliasWith?: WithClause[];
[key: string]: any;
}) => React.ReactNode;
enableSorting?: boolean;
sortOrder?: SortingState;
onSortingChange?: (v: SortingState | null) => void;
getRowWhere?: (row: Record<string, any>) => string;
getRowWhere?: (row: Record<string, any>) => RowWhereResult;
variant?: DBRowTableVariant;
}) => {
const generateRowMatcher = generateRowId;
const dedupedRows = useMemo(() => {
const lIds = new Set();
const returnedRows = dedupRows
? rows.filter(l => {
const matcher = generateRowMatcher(l);
if (lIds.has(matcher)) {
const rowWhereResult = generateRowId(l);
if (lIds.has(rowWhereResult.where)) {
return false;
}
lIds.add(matcher);
lIds.add(rowWhereResult.where);
return true;
})
: rows;
return returnedRows.map(r => ({
...r,
__hyperdx_id: generateRowMatcher(r),
}));
}, [rows, dedupRows, generateRowMatcher]);
return returnedRows.map(r => {
const rowWhereResult = generateRowId(r);
return {
...r,
[INTERNAL_ROW_FIELDS.ID]: rowWhereResult.where,
[INTERNAL_ROW_FIELDS.ALIAS_WITH]: rowWhereResult.aliasWith,
};
});
}, [rows, dedupRows, generateRowId]);
const _onRowExpandClick = useCallback(
(row: Record<string, any>) => {
@ -964,6 +975,7 @@ export const RawLogTable = memo(
>
{renderRowDetails?.({
id: rowId,
aliasWith: row.original[INTERNAL_ROW_FIELDS.ALIAS_WITH],
...row.original,
})}
</ExpandedLogRow>
@ -1197,12 +1209,16 @@ function DBSqlRowTableComponent({
}: {
config: ChartConfigWithDateRange;
sourceId?: string;
onRowDetailsClick?: (where: string) => void;
onRowDetailsClick?: (rowWhere: RowWhereResult) => void;
highlightedLineId?: string;
queryKeyPrefix?: string;
enabled?: boolean;
isLive?: boolean;
renderRowDetails?: (r: { [key: string]: unknown }) => React.ReactNode;
renderRowDetails?: (r: {
id: string;
aliasWith?: WithClause[];
[key: string]: unknown;
}) => React.ReactNode;
onScroll?: (scrollTop: number) => void;
onError?: (error: Error | ClickHouseQueryError) => void;
denoiseResults?: boolean;

View file

@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useState } from 'react';
import { useQueryState } from 'nuqs';
import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse';
import {
@ -7,6 +7,7 @@ import {
} from '@hyperdx/common-utils/dist/types';
import { SortingState } from '@tanstack/react-table';
import { RowWhereResult, WithClause } from '@/hooks/useRowWhere';
import { useSource } from '@/source';
import TabBar from '@/TabBar';
import { useLocalStorage } from '@/utils';
@ -63,15 +64,17 @@ export default function DBSqlRowTableWithSideBar({
const { data: sourceData } = useSource({ id: sourceId });
const [rowId, setRowId] = useQueryState('rowWhere');
const [rowSource, setRowSource] = useQueryState('rowSource');
const [aliasWith, setAliasWith] = useState<WithClause[]>([]);
const { setContextRowId, setContextRowSource } = useNestedPanelState();
const onOpenSidebar = useCallback(
(rowWhere: string) => {
setRowId(rowWhere);
(rowWhere: RowWhereResult) => {
setRowId(rowWhere.where);
setAliasWith(rowWhere.aliasWith);
setRowSource(sourceId);
onSidebarOpen?.(rowWhere);
onSidebarOpen?.(rowWhere.where);
},
[setRowId, setRowSource, sourceId, onSidebarOpen],
[setRowId, setAliasWith, setRowSource, sourceId, onSidebarOpen],
);
const onCloseSidebar = useCallback(() => {
@ -91,12 +94,16 @@ export default function DBSqlRowTableWithSideBar({
setContextRowSource,
]);
const renderRowDetails = useCallback(
(r: { [key: string]: unknown }) => {
(r: { id: string; aliasWith?: WithClause[]; [key: string]: unknown }) => {
if (!sourceData) {
return <div className="p-3 text-muted">Loading...</div>;
}
return (
<RowOverviewPanelWrapper source={sourceData} rowId={r.id as string} />
<RowOverviewPanelWrapper
source={sourceData}
rowId={r.id}
aliasWith={r.aliasWith}
/>
);
},
[sourceData],
@ -108,6 +115,7 @@ export default function DBSqlRowTableWithSideBar({
<DBRowSidePanel
source={sourceData}
rowId={rowId ?? undefined}
aliasWith={aliasWith}
isNestedPanel={isNestedPanel}
breadcrumbPath={breadcrumbPath}
onClose={onCloseSidebar}
@ -143,9 +151,11 @@ enum InlineTab {
function RowOverviewPanelWrapper({
source,
rowId,
aliasWith,
}: {
source: TSource;
rowId: string;
aliasWith?: WithClause[];
}) {
// Use localStorage to persist the selected tab
const [activeTab, setActiveTab] = useLocalStorage<InlineTab>(
@ -175,11 +185,15 @@ function RowOverviewPanelWrapper({
<div>
{activeTab === InlineTab.Overview && (
<div className="inline-overview-panel">
<RowOverviewPanel source={source} rowId={rowId} />
<RowOverviewPanel
source={source}
rowId={rowId}
aliasWith={aliasWith}
/>
</div>
)}
{activeTab === InlineTab.ColumnValues && (
<RowDataPanel source={source} rowId={rowId} />
<RowDataPanel source={source} rowId={rowId} aliasWith={aliasWith} />
)}
</div>
</div>

View file

@ -1,13 +1,15 @@
import React, { useState } from 'react';
import { IconCopy, IconLink, IconTextWrap } from '@tabler/icons-react';
import { INTERNAL_ROW_FIELDS, RowWhereResult } from '@/hooks/useRowWhere';
import DBRowTableIconButton from './DBRowTableIconButton';
import styles from '../../../styles/LogTable.module.scss';
export interface DBRowTableRowButtonsProps {
row: Record<string, any>;
getRowWhere: (row: Record<string, any>) => string;
getRowWhere: (row: Record<string, any>) => RowWhereResult;
sourceId?: string;
isWrapped: boolean;
onToggleWrap: () => void;
@ -27,7 +29,7 @@ export const DBRowTableRowButtons: React.FC<DBRowTableRowButtonsProps> = ({
try {
// Filter out internal metadata fields that start with __ or are generated IDs
const { __hyperdx_id, ...cleanRow } = row;
const { [INTERNAL_ROW_FIELDS.ID]: _id, ...cleanRow } = row;
// Parse JSON string fields to make them proper JSON objects
const parsedRow = Object.entries(cleanRow).reduce(
@ -62,10 +64,10 @@ export const DBRowTableRowButtons: React.FC<DBRowTableRowButtonsProps> = ({
const copyRowUrl = async () => {
try {
const rowWhere = getRowWhere(row);
const rowWhereResult = getRowWhere(row);
const currentUrl = new URL(window.location.href);
// Add the row identifier as query parameters
currentUrl.searchParams.set('rowWhere', rowWhere);
currentUrl.searchParams.set('rowWhere', rowWhereResult.where);
if (sourceId) {
currentUrl.searchParams.set('rowSource', sourceId);
}

View file

@ -16,6 +16,7 @@ import {
import { IconPencil } from '@tabler/icons-react';
import { DBTraceWaterfallChartContainer } from '@/components/DBTraceWaterfallChart';
import { WithClause } from '@/hooks/useRowWhere';
import { useSource, useUpdateSource } from '@/source';
import TabBar from '@/TabBar';
@ -95,7 +96,7 @@ export default function DBTracePanel({
const [eventRowWhere, setEventRowWhere] = useQueryState(
'eventRowWhere',
parseAsJson<{ id: string; type: string }>(),
parseAsJson<{ id: string; type: string; aliasWith: WithClause[] }>(),
);
const {
@ -231,6 +232,7 @@ export default function DBTracePanel({
: traceSourceData
}
rowId={eventRowWhere?.id}
aliasWith={eventRowWhere?.aliasWith}
/>
)}
{displayedTab === Tab.Parsed && (
@ -241,6 +243,7 @@ export default function DBTracePanel({
: traceSourceData
}
rowId={eventRowWhere?.id}
aliasWith={eventRowWhere?.aliasWith}
/>
)}
</>

View file

@ -29,7 +29,7 @@ import {
import { ContactSupportText } from '@/components/ContactSupportText';
import useOffsetPaginatedQuery from '@/hooks/useOffsetPaginatedQuery';
import useResizable from '@/hooks/useResizable';
import useRowWhere from '@/hooks/useRowWhere';
import useRowWhere, { RowWhereResult, WithClause } from '@/hooks/useRowWhere';
import useWaterfallSearchState from '@/hooks/useWaterfallSearchState';
import SearchInputV2 from '@/SearchInputV2';
import {
@ -313,7 +313,7 @@ export function useEventsAroundFocus({
const meta = beforeSpanData?.meta ?? afterSpanData?.meta;
const error = beforeSpanError || afterSpanError;
const rowWhere = useRowWhere({ meta, aliasMap: alias });
const getRowWhere = useRowWhere({ meta, aliasMap: alias });
const rows = useMemo(() => {
// Sometimes meta has not loaded yet
// DO NOT REMOVE, useRowWhere will error if no meta
@ -322,6 +322,11 @@ export function useEventsAroundFocus({
...(beforeSpanData?.data ?? []),
...(afterSpanData?.data ?? []),
].map(cd => {
// Omit SpanAttributes, SpanEvents and __hdx_hidden from rowWhere id generation.
// SpanAttributes and SpanEvents can be large objects, and __hdx_hidden may be a lucene expression.
const rowWhereResult = getRowWhere(
omit(cd, ['SpanAttributes', 'SpanEvents', '__hdx_hidden']),
);
return {
// Keep all fields available for display
...cd,
@ -329,14 +334,14 @@ export function useEventsAroundFocus({
SpanId: cd?.SpanId,
__hdx_hidden: cd?.__hdx_hidden,
type,
// Omit SpanAttributes, SpanEvents and __hdx_hidden from rowWhere id generation.
// SpanAttributes and SpanEvents can be large objects, and __hdx_hidden may be a lucene expression.
id: rowWhere(
omit(cd, ['SpanAttributes', 'SpanEvents', '__hdx_hidden']),
),
id: rowWhereResult.where,
// Don't pass aliasWith for trace waterfall chart - the WHERE clause already uses
// raw column expressions (e.g., SpanName='value'), and the aliasMap creates
// redundant WITH clauses like (Timestamp) AS Timestamp that interfere with queries.
aliasWith: [],
};
});
}, [afterSpanData, beforeSpanData, meta, rowWhere, type]);
}, [afterSpanData, beforeSpanData, meta, getRowWhere, type]);
return {
rows,
@ -362,7 +367,11 @@ export function DBTraceWaterfallChartContainer({
traceId: string;
dateRange: [Date, Date];
focusDate: Date;
onClick?: (rowWhere: { id: string; type: string }) => void;
onClick?: (rowWhere: {
id: string;
type: string;
aliasWith: WithClause[];
}) => void;
highlightedRowWhere?: string | null;
initialRowHighlightHint?: {
timestamp: string;
@ -499,6 +508,7 @@ export function DBTraceWaterfallChartContainer({
onClick?.({
id: rows[initialRowHighlightIndex].id,
type: rows[initialRowHighlightIndex].type ?? '',
aliasWith: rows[initialRowHighlightIndex].aliasWith,
});
}
}
@ -510,7 +520,12 @@ export function DBTraceWaterfallChartContainer({
// 3. Spans, with multiple root nodes (ex. somehow disjoint traces fe/be)
// Parse out a DAG of spans
type Node = SpanRow & { id: string; parentId: string; children: SpanRow[] };
type Node = SpanRow & {
id: string;
parentId: string;
children: SpanRow[];
aliasWith: WithClause[];
};
const validSpanIDs = useMemo(() => {
return new Set(
traceRowsData // only spans in traces can define valid span ids
@ -653,7 +668,13 @@ export function DBTraceWaterfallChartContainer({
const start = startOffset - minOffset;
const end = start + tookMs;
const { Body: _body, ServiceName: serviceName, id, type } = result;
const {
Body: _body,
ServiceName: serviceName,
id,
type,
aliasWith,
} = result;
let body = `${_body}`;
try {
body = typeof _body === 'string' ? _body : JSON.stringify(_body);
@ -692,6 +713,7 @@ export function DBTraceWaterfallChartContainer({
return {
id,
type,
aliasWith,
label: (
<div
className={`${textColor({ isError, isWarn })} ${
@ -699,7 +721,7 @@ export function DBTraceWaterfallChartContainer({
} text-truncate cursor-pointer ps-2 ${styles.traceTimelineLabel}`}
role="button"
onClick={() => {
onClick?.({ id, type: type ?? '' });
onClick?.({ id, type: type ?? '', aliasWith });
}}
>
<div className="d-flex align-items-center" style={{ height: 24 }}>
@ -774,6 +796,8 @@ export function DBTraceWaterfallChartContainer({
events: [
{
id,
type,
aliasWith,
start,
end,
tooltip: `${displayText} ${tookMs >= 0 ? `took ${tookMs.toFixed(4)}ms` : ''} ${status ? `| Status: ${status}` : ''}${!isNaN(startOffset) ? ` | Started at ${formatTime(new Date(startOffset), { format: 'withMs' })}` : ''}`,
@ -921,8 +945,16 @@ export function DBTraceWaterfallChartContainer({
onClick={ts => {
// onTimeClick(ts + startedAt);
}}
onEventClick={event => {
onClick?.({ id: event.id, type: event.type ?? '' });
onEventClick={(event: {
id: string;
type?: string;
aliasWith?: WithClause[];
}) => {
onClick?.({
id: event.id,
type: event.type ?? '',
aliasWith: event.aliasWith ?? [],
});
}}
cursors={[]}
rows={timelineRows}

View file

@ -4,6 +4,8 @@ import { useQueryState } from 'nuqs';
import { TSource } from '@hyperdx/common-utils/dist/types';
import { IconArrowsMaximize, IconChevronRight } from '@tabler/icons-react';
import { INTERNAL_ROW_FIELDS } from '@/hooks/useRowWhere';
import styles from '../../styles/LogTable.module.scss';
// Hook that provides a function to open the sidebar with specific row details
@ -179,7 +181,7 @@ export const createExpandButtonColumn = (
highlightedLineId?: string,
) => ({
id: 'expand-btn',
accessorKey: '__hyperdx_id',
accessorKey: INTERNAL_ROW_FIELDS.ID,
header: () => '',
cell: (info: any) => {
const rowId = info.getValue() as string;

View file

@ -12,7 +12,7 @@ import {
SEVERITY_TEXT_COLUMN_ALIAS,
TIMESTAMP_COLUMN_ALIAS,
} from '@/hooks/usePatterns';
import useRowWhere from '@/hooks/useRowWhere';
import useRowWhere, { RowWhereResult } from '@/hooks/useRowWhere';
import { getFirstTimestampValueExpression } from '@/source';
import { useZIndex, ZIndexContext } from '@/zIndex';
@ -34,9 +34,8 @@ export default function PatternSidePanel({
const contextZIndex = useZIndex();
const drawerZIndex = contextZIndex + 100;
const [selectedRowWhere, setSelectedRowWhere] = React.useState<string | null>(
null,
);
const [selectedRowWhere, setSelectedRowWhere] =
React.useState<RowWhereResult | null>(null);
const serviceNameExpression = source?.serviceNameExpression || 'Service';
@ -81,11 +80,11 @@ export default function PatternSidePanel({
const handleRowClick = React.useCallback(
(row: Record<string, any>) => {
const whereClause = getRowWhere({
const rowWhereResult = getRowWhere({
body: row[PATTERN_COLUMN_ALIAS],
ts: row[TIMESTAMP_COLUMN_ALIAS],
});
setSelectedRowWhere(whereClause);
setSelectedRowWhere(rowWhereResult);
},
[getRowWhere],
);
@ -125,7 +124,7 @@ export default function PatternSidePanel({
</Card.Section>
<RawLogTable
rows={pattern.samples}
generateRowId={row => row.id}
generateRowId={row => ({ where: row.id, aliasWith: [] })}
displayedColumns={displayedColumns}
columnTypeMap={columnTypeMap}
columnNameMap={columnNameMap}
@ -140,7 +139,8 @@ export default function PatternSidePanel({
{selectedRowWhere && (
<DBRowSidePanel
source={source}
rowId={selectedRowWhere}
rowId={selectedRowWhere.where}
aliasWith={selectedRowWhere.aliasWith}
onClose={handleCloseRowSidePanel}
isNestedPanel={true}
breadcrumbPath={[{ label: 'Pattern Overview' }]}

View file

@ -74,7 +74,7 @@ export default function PatternTable({
fetchNextPage={() => {}}
highlightedLineId={''}
columnTypeMap={emptyMap}
generateRowId={row => row.id}
generateRowId={row => ({ where: row.id, aliasWith: [] })}
columnNameMap={{
__hdx_pattern_trend: 'Trend',
countStr: 'Count',

View file

@ -5,9 +5,12 @@ import {
appendSelectWithPrimaryAndPartitionKey,
RawLogTable,
} from '@/components/DBRowTable';
import { RowWhereResult } from '@/hooks/useRowWhere';
import * as useChartConfigModule from '../../hooks/useChartConfig';
const mockRowWhereResult: RowWhereResult = { where: '', aliasWith: [] };
describe('RawLogTable', () => {
beforeEach(() => {
jest.clearAllMocks();
@ -34,7 +37,7 @@ describe('RawLogTable', () => {
dedupRows={false}
hasNextPage={false}
onRowDetailsClick={() => {}}
generateRowId={() => ''}
generateRowId={() => mockRowWhereResult}
columnTypeMap={new Map()}
/>,
);
@ -55,7 +58,7 @@ describe('RawLogTable', () => {
dedupRows: false,
hasNextPage: false,
onRowDetailsClick: () => {},
generateRowId: () => '',
generateRowId: () => mockRowWhereResult,
columnTypeMap: new Map(),
};
it('Should not allow changing sort if disabled', () => {

View file

@ -120,7 +120,7 @@ describe('DBTraceWaterfallChartContainer', () => {
// Reset mocks before each test
beforeEach(() => {
jest.clearAllMocks();
mockUseRowWhere.mockReturnValue(() => 'row-id');
mockUseRowWhere.mockReturnValue(() => ({ where: 'row-id', aliasWith: [] }));
MockTimelineChart.latestProps = {};
});
@ -294,7 +294,7 @@ describe('useEventsAroundFocus', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseRowWhere.mockReturnValue(() => 'row-id');
mockUseRowWhere.mockReturnValue(() => ({ where: 'row-id', aliasWith: [] }));
});
const testEventsAroundFocus = (options: {

View file

@ -426,9 +426,10 @@ describe('useRowWhere', () => {
const { result } = renderHook(() => useRowWhere({ meta }));
const row = { id: '123', status: 'active' };
const whereClause = result.current(row);
const rowWhereResult = result.current(row);
expect(whereClause).toBe("id='123' AND status='active'");
expect(rowWhereResult.where).toBe("id='123' AND status='active'");
expect(rowWhereResult.aliasWith).toEqual([]);
});
it('should handle aliasMap correctly', () => {
@ -445,9 +446,23 @@ describe('useRowWhere', () => {
const { result } = renderHook(() => useRowWhere({ meta, aliasMap }));
const row = { user_id: '123', user_status: 'active' };
const whereClause = result.current(row);
const rowWhereResult = result.current(row);
expect(whereClause).toBe("users.id='123' AND users.status='active'");
expect(rowWhereResult.where).toBe(
"users.id='123' AND users.status='active'",
);
expect(rowWhereResult.aliasWith).toEqual([
{
name: 'user_id',
sql: { sql: 'users.id', params: {} },
isSubquery: false,
},
{
name: 'user_status',
sql: { sql: 'users.status', params: {} },
isSubquery: false,
},
]);
});
it('should use column name when alias not found in aliasMap', () => {
@ -464,9 +479,12 @@ describe('useRowWhere', () => {
const { result } = renderHook(() => useRowWhere({ meta, aliasMap }));
const row = { id: '123', status: 'active' };
const whereClause = result.current(row);
const rowWhereResult = result.current(row);
expect(whereClause).toBe("users.id='123' AND status='active'");
expect(rowWhereResult.where).toBe("users.id='123' AND status='active'");
expect(rowWhereResult.aliasWith).toEqual([
{ name: 'id', sql: { sql: 'users.id', params: {} }, isSubquery: false },
]);
});
it('should handle undefined alias values in aliasMap', () => {
@ -483,9 +501,12 @@ describe('useRowWhere', () => {
const { result } = renderHook(() => useRowWhere({ meta, aliasMap }));
const row = { id: '123', status: 'active' };
const whereClause = result.current(row);
const rowWhereResult = result.current(row);
expect(whereClause).toBe("users.id='123' AND status='active'");
expect(rowWhereResult.where).toBe("users.id='123' AND status='active'");
expect(rowWhereResult.aliasWith).toEqual([
{ name: 'id', sql: { sql: 'users.id', params: {} }, isSubquery: false },
]);
});
it('should memoize the column map', () => {

View file

@ -9,6 +9,28 @@ import {
const MAX_STRING_LENGTH = 512;
// Internal row field names used by the table component for row tracking
export const INTERNAL_ROW_FIELDS = {
ID: '__hyperdx_id',
ALIAS_WITH: '__hyperdx_alias_with',
} as const;
// Type for WITH clause entries, matching ChartConfig's with property
export type WithClause = {
name: string;
sql: {
sql: string;
params: Record<string, unknown>;
};
isSubquery: boolean;
};
// Result type for row WHERE clause with alias support
export type RowWhereResult = {
where: string;
aliasWith: WithClause[];
};
type ColumnWithMeta = ColumnMetaType & {
valueExpr: string;
jsType: JSDataType | null;
@ -111,6 +133,29 @@ export function processRowToWhereClause(
return res;
}
/**
* Converts an aliasMap to an array of WITH clause entries.
* This allows aliases to be properly defined when querying for a specific row.
*/
export function aliasMapToWithClauses(
aliasMap: Record<string, string | undefined> | undefined,
): WithClause[] {
if (!aliasMap) {
return [];
}
return Object.entries(aliasMap)
.filter(([, value]) => value != null && value.trim() !== '')
.map(([name, value]) => ({
name,
sql: {
sql: value as string,
params: {},
},
isSubquery: false,
}));
}
export default function useRowWhere({
meta,
aliasMap,
@ -126,6 +171,7 @@ export default function useRowWhere({
// but if the alias is not found, use the column name as the valueExpr
const valueExpr =
aliasMap != null ? (aliasMap[c.name] ?? c.name) : c.name;
return [
c.name,
{
@ -139,13 +185,22 @@ export default function useRowWhere({
[meta, aliasMap],
);
return useCallback(
(row: Record<string, any>) => {
// Filter out synthetic columns that aren't in the database schema
// Memoize the aliasWith array since it only depends on aliasMap
const aliasWith = useMemo(() => aliasMapToWithClauses(aliasMap), [aliasMap]);
const { __hyperdx_id, ...dbRow } = row;
return processRowToWhereClause(dbRow, columnMap);
return useCallback(
(row: Record<string, any>): RowWhereResult => {
// Filter out synthetic columns that aren't in the database schema
const {
[INTERNAL_ROW_FIELDS.ID]: _id,
[INTERNAL_ROW_FIELDS.ALIAS_WITH]: _aliasWith,
...dbRow
} = row;
return {
where: processRowToWhereClause(dbRow, columnMap),
aliasWith,
};
},
[columnMap],
[columnMap, aliasWith],
);
}