hyperdx/packages/app/src/components/DBRowOverviewPanel.tsx
Warren Lee db845604a2
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
```
2026-01-21 22:21:40 +00:00

339 lines
10 KiB
TypeScript

import { useCallback, useContext, useMemo } from 'react';
import isString from 'lodash/isString';
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';
import { getJSONColumnNames, useRowData } from './DBRowDataPanel';
import { DBRowJsonViewer } from './DBRowJsonViewer';
import { RowSidePanelContext } from './DBRowSidePanel';
import DBRowSidePanelHeader from './DBRowSidePanelHeader';
import EventTag from './EventTag';
import { ExceptionSubpanel } from './ExceptionSubpanel';
import { NetworkPropertySubpanel } from './NetworkPropertyPanel';
import { SpanEventsSubpanel } from './SpanEventsSubpanel';
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, aliasWith });
const { onPropertyAddClick, generateSearchUrl } =
useContext(RowSidePanelContext);
const highlightedAttributeValues = useMemo(() => {
const attributeExpressions =
source.kind === SourceKind.Trace || source.kind === SourceKind.Log
? (source.highlightedRowAttributeExpressions ?? [])
: [];
return data
? getHighlightedAttributesFromData(
source,
attributeExpressions,
data.data || [],
data.meta || [],
)
: [];
}, [source, data]);
const jsonColumns = getJSONColumnNames(data?.meta);
const eventAttributesExpr = source.eventAttributesExpression;
const firstRow = useMemo(() => {
const firstRow = { ...(data?.data?.[0] ?? {}) };
if (!firstRow) {
return null;
}
return firstRow;
}, [data]);
// TODO: Use source config to select these in SQL, but we'll just
// assume OTel column names for now
const topLevelAttributeKeys = [
'ServiceName',
'SpanName',
'Duration',
'SeverityText',
'StatusCode',
'StatusMessage',
'SpanKind',
'TraceId',
'SpanId',
'ParentSpanId',
'ScopeName',
'ScopeVersion',
];
const topLevelAttributes = pickBy(firstRow, (value, key) => {
if (value === '') {
return false;
}
if (topLevelAttributeKeys.includes(key)) {
return true;
}
return false;
});
const resourceAttributes = firstRow?.__hdx_resource_attributes ?? EMPTY_OBJ;
const flattenedEventAttributes =
firstRow?.__hdx_event_attributes ?? EMPTY_OBJ;
const dataAttributes = useMemo(
() =>
eventAttributesExpr &&
firstRow?.[eventAttributesExpr] &&
Object.keys(firstRow[eventAttributesExpr]).length > 0
? { [eventAttributesExpr]: firstRow[eventAttributesExpr] }
: {},
[eventAttributesExpr, firstRow],
);
const _generateSearchUrl = useCallback(
(query?: string, queryLanguage?: 'sql' | 'lucene') => {
return (
generateSearchUrl?.({
where: query,
whereLanguage: queryLanguage,
}) ?? '/'
);
},
[generateSearchUrl],
);
const isHttpRequest = useMemo(() => {
const attributes =
eventAttributesExpr && dataAttributes?.[eventAttributesExpr];
return attributes?.['http.url'] != null;
}, [dataAttributes, eventAttributesExpr]);
const filteredEventAttributes = useMemo(() => {
if (!eventAttributesExpr) return dataAttributes;
const attributes = dataAttributes?.[eventAttributesExpr];
return isHttpRequest && attributes
? {
[eventAttributesExpr]: pickBy(
attributes,
(_, key) => !key.startsWith('http.'),
),
}
: dataAttributes;
}, [dataAttributes, isHttpRequest, eventAttributesExpr]);
const exceptionValues = useMemo(() => {
const parsedEvents =
firstRow?.__hdx_events_exception_attributes ?? EMPTY_OBJ;
const stacktrace =
parsedEvents?.['exception.stacktrace'] ||
parsedEvents?.['exception.parsed_stacktrace'];
let parsedStacktrace = stacktrace ?? '[]';
try {
parsedStacktrace = JSON.parse(stacktrace);
} catch (e) {
// do nothing
}
return [
{
stacktrace: parsedStacktrace,
type: parsedEvents?.['exception.type'],
value:
typeof parsedEvents?.['exception.message'] !== 'string'
? JSON.stringify(parsedEvents?.['exception.message'])
: parsedEvents?.['exception.message'],
mechanism: parsedEvents?.['exception.mechanism'],
},
];
}, [firstRow]);
const hasException = useMemo(() => {
return (
Object.keys(firstRow?.__hdx_events_exception_attributes ?? {}).length > 0
);
}, [firstRow?.__hdx_events_exception_attributes]);
const hasSpanEvents = useMemo(() => {
return (
Array.isArray(firstRow?.__hdx_span_events) &&
firstRow?.__hdx_span_events.length > 0
);
}, [firstRow?.__hdx_span_events]);
const mainContentColumn = getEventBody(source);
const mainContent = isString(firstRow?.['__hdx_body'])
? firstRow['__hdx_body']
: firstRow?.['__hdx_body'] !== undefined
? JSON.stringify(firstRow['__hdx_body'])
: undefined;
return (
<div className="flex-grow-1 overflow-auto" data-testid={dataTestId}>
{!hideHeader && (
<Box px="sm" pt="md">
<DBRowSidePanelHeader
attributes={highlightedAttributeValues}
date={new Date(firstRow?.__hdx_timestamp ?? 0)}
mainContent={mainContent}
mainContentHeader={mainContentColumn}
severityText={firstRow?.__hdx_severity_text}
/>
</Box>
)}
<Accordion
mt="sm"
defaultValue={[
'exception',
'spanEvents',
'network',
'resourceAttributes',
'eventAttributes',
'topLevelAttributes',
]}
multiple
variant="noPadding"
>
{isHttpRequest && (
<Accordion.Item value="network">
<Accordion.Control>
<Text size="sm" ps="md">
HTTP Request
</Text>
</Accordion.Control>
<Accordion.Panel>
<Box px="md">
<NetworkPropertySubpanel
eventAttributes={flattenedEventAttributes}
/>
</Box>
</Accordion.Panel>
</Accordion.Item>
)}
{hasException && (
<Accordion.Item value="exception">
<Accordion.Control>
<Text size="sm" ps="md">
Exception
</Text>
</Accordion.Control>
<Accordion.Panel>
<Box px="md">
<ExceptionSubpanel
exceptionValues={exceptionValues}
breadcrumbs={[]}
logData={{
timestamp: firstRow?.__hdx_timestamp,
}}
/>
</Box>
</Accordion.Panel>
</Accordion.Item>
)}
{hasSpanEvents && (
<Accordion.Item value="spanEvents">
<Accordion.Control>
<Text size="sm" ps="md">
Span Events
</Text>
</Accordion.Control>
<Accordion.Panel>
<Box px="md">
<SpanEventsSubpanel spanEvents={firstRow?.__hdx_span_events} />
</Box>
</Accordion.Panel>
</Accordion.Item>
)}
{Object.keys(topLevelAttributes).length > 0 && (
<Accordion.Item value="topLevelAttributes">
<Accordion.Control>
<Text size="sm" ps="md">
Top Level Attributes
</Text>
</Accordion.Control>
<Accordion.Panel>
<Box px="md">
<DBRowJsonViewer
data={topLevelAttributes}
jsonColumns={jsonColumns}
/>
</Box>
</Accordion.Panel>
</Accordion.Item>
)}
<Accordion.Item value="eventAttributes">
<Accordion.Control>
<Text size="sm" ps="md">
{source.kind === 'log' ? 'Log' : 'Span'} Attributes
</Text>
</Accordion.Control>
<Accordion.Panel>
<Box px="md">
<DBRowJsonViewer
data={filteredEventAttributes}
jsonColumns={jsonColumns}
/>
</Box>
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value="resourceAttributes">
<Accordion.Control>
<Text size="sm" ps="md">
Resource Attributes
</Text>
</Accordion.Control>
<Accordion.Panel>
<Flex wrap="wrap" gap="2px" mx="md" mb="lg">
{Object.entries(resourceAttributes).map(([key, value]) => (
<EventTag
{...(onPropertyAddClick
? {
onPropertyAddClick,
sqlExpression:
source.resourceAttributesExpression &&
jsonColumns?.includes(
source.resourceAttributesExpression,
)
? // If resource attributes is a JSON column, we need to cast the key to a string so we can run where X in Y queries
`toString(${source.resourceAttributesExpression}.${key})`
: `${source.resourceAttributesExpression}['${key}']`,
}
: {
onPropertyAddClick: undefined,
sqlExpression: undefined,
})}
generateSearchUrl={
generateSearchUrl ? _generateSearchUrl : undefined
}
displayedKey={key}
name={`${source.resourceAttributesExpression}.${key}`}
value={value as string}
key={key}
/>
))}
</Flex>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</div>
);
}