Skip to content

Commit 59422a1

Browse files
authored
feat: Add custom attributes for individual rows (#1379)
Closes HDX-2776 Closes HDX-2752 # Summary This PR extends the custom attributes feature from #1356 to support custom attributes on individual rows (rather than entire traces). Further, attributes which are URLs are now rendered as links, supporting use-cases which require building custom external links from queried data. ## Demo Source Configuration: <img width="972" height="649" alt="Screenshot 2025-11-18 at 6 01 55 PM" src="https://github.com/user-attachments/assets/42e9cc95-d9e3-4155-8e25-58e4dcf0b787" /> Side panel: <img width="1587" height="511" alt="Screenshot 2025-11-18 at 6 02 12 PM" src="https://github.com/user-attachments/assets/3a1d7683-eb79-4925-99fe-4e2d7cdad78f" /> Inferred link: <img width="727" height="173" alt="Screenshot 2025-11-18 at 6 02 32 PM" src="https://github.com/user-attachments/assets/68f42f95-598c-4b30-b616-9556e32945bf" /> <details> <summary>logview attribute definition</summary> ```sql if( NOT empty(TraceId) AND NOT empty(SpanId) AND NOT empty(ServiceName), concat('https://logview.com?q=', 'trace_id=', TraceId, '&span_id=', SpanId, '&service=', ServiceName, '&start_time=', formatDateTime(Timestamp - INTERVAL 1 SECOND, '%FT%T.%fZ'), '&end_time=', formatDateTime(Timestamp + Duration/1e9 + INTERVAL 1 SECOND, '%FT%T.%fZ') ), '' ) ``` </details>
1 parent 770276a commit 59422a1

File tree

12 files changed

+431
-65
lines changed

12 files changed

+431
-65
lines changed

.changeset/slow-eyes-attack.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@hyperdx/common-utils": patch
3+
"@hyperdx/app": patch
4+
---
5+
6+
feat: Add custom attributes for individual rows

packages/api/src/models/source.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ export const Source = mongoose.model<ISource>(
6969
highlightedTraceAttributeExpressions: {
7070
type: mongoose.Schema.Types.Array,
7171
},
72+
highlightedRowAttributeExpressions: {
73+
type: mongoose.Schema.Types.Array,
74+
},
7275

7376
metricTables: {
7477
type: {

packages/app/src/components/DBHighlightedAttributesList.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function DBHighlightedAttributesList({
3939
displayedKey={displayedKey}
4040
name={lucene ? lucene : sql}
4141
nameLanguage={lucene ? 'lucene' : 'sql'}
42-
value={value as string}
42+
value={value}
4343
key={`${displayedKey}-${value}-${source.id}`}
4444
{...(onPropertyAddClick && contextSource?.id === source.id
4545
? {

packages/app/src/components/DBRowDataPanel.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Box } from '@mantine/core';
66

77
import { useQueriedChartConfig } from '@/hooks/useChartConfig';
88
import { getDisplayedTimestampValueExpression, getEventBody } from '@/source';
9+
import { getSelectExpressionsForHighlightedAttributes } from '@/utils/highlightedAttributes';
910

1011
import { DBRowJsonViewer } from './DBRowJsonViewer';
1112

@@ -37,6 +38,13 @@ export function useRowData({
3738
const severityTextExpr =
3839
source.severityTextExpression || source.statusCodeExpression;
3940

41+
const selectHighlightedRowAttributes =
42+
source.kind === SourceKind.Trace || source.kind === SourceKind.Log
43+
? getSelectExpressionsForHighlightedAttributes(
44+
source.highlightedRowAttributeExpressions,
45+
)
46+
: [];
47+
4048
const queryResult = useQueriedChartConfig(
4149
{
4250
connection: source.connection,
@@ -116,6 +124,7 @@ export function useRowData({
116124
},
117125
]
118126
: []),
127+
...selectHighlightedRowAttributes,
119128
],
120129
where: rowId ?? '0=1',
121130
from: source.from,

packages/app/src/components/DBRowOverviewPanel.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,6 @@ export function RowOverviewPanel({
165165
<Box px="32px" pt="md">
166166
<DBRowSidePanelHeader
167167
date={new Date(firstRow?.__hdx_timestamp ?? 0)}
168-
tags={{}}
169168
mainContent={mainContent}
170169
mainContentHeader={mainContentColumn}
171170
severityText={firstRow?.__hdx_severity_text}

packages/app/src/components/DBRowSidePanel.tsx

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { isString } from 'lodash';
1212
import { parseAsStringEnum, useQueryState } from 'nuqs';
1313
import { ErrorBoundary } from 'react-error-boundary';
1414
import { useHotkeys } from 'react-hotkeys-hook';
15-
import { TSource } from '@hyperdx/common-utils/dist/types';
15+
import { SourceKind, TSource } from '@hyperdx/common-utils/dist/types';
1616
import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types';
1717
import { Box, Drawer, Flex, Stack } from '@mantine/core';
1818

@@ -26,6 +26,7 @@ import { LogSidePanelKbdShortcuts } from '@/LogSidePanelElements';
2626
import { getEventBody } from '@/source';
2727
import TabBar from '@/TabBar';
2828
import { SearchConfig } from '@/types';
29+
import { getHighlightedAttributesFromData } from '@/utils/highlightedAttributes';
2930
import { useZIndex, ZIndexContext } from '@/zIndex';
3031

3132
import ServiceMapSidePanel from './ServiceMap/ServiceMapSidePanel';
@@ -188,15 +189,34 @@ const DBRowSidePanel = ({
188189
: undefined;
189190
const severityText: string | undefined =
190191
normalizedRow?.['__hdx_severity_text'];
191-
const serviceName = normalizedRow?.['__hdx_service_name'];
192192

193-
const tags = useMemo(() => {
194-
const tags: Record<string, string> = {};
195-
if (serviceName && source.serviceNameExpression) {
196-
tags[source.serviceNameExpression] = serviceName;
193+
const highlightedAttributeValues = useMemo(() => {
194+
const attributeExpressions: TSource['highlightedRowAttributeExpressions'] =
195+
[];
196+
if (
197+
(source.kind === SourceKind.Trace || source.kind === SourceKind.Log) &&
198+
source.highlightedRowAttributeExpressions
199+
) {
200+
attributeExpressions.push(...source.highlightedRowAttributeExpressions);
197201
}
198-
return tags;
199-
}, [serviceName, source.serviceNameExpression]);
202+
203+
// Add service name expression to all sources, to maintain compatibility with
204+
// the behavior prior to the addition of highlightedRowAttributeExpressions
205+
if (source.serviceNameExpression) {
206+
attributeExpressions.push({
207+
sqlExpression: source.serviceNameExpression,
208+
});
209+
}
210+
211+
return rowData
212+
? getHighlightedAttributesFromData(
213+
source,
214+
attributeExpressions,
215+
rowData.data || [],
216+
rowData.meta || [],
217+
)
218+
: [];
219+
}, [source, rowData]);
200220

201221
const oneHourRange = useMemo(() => {
202222
return [
@@ -275,7 +295,7 @@ const DBRowSidePanel = ({
275295
<Box p="sm">
276296
<DBRowSidePanelHeader
277297
date={timestampDate}
278-
tags={tags}
298+
attributes={highlightedAttributeValues}
279299
mainContent={mainContent}
280300
mainContentHeader={mainContentColumn}
281301
severityText={severityText}

packages/app/src/components/DBRowSidePanelHeader.tsx

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@ import {
1717
UnstyledButton,
1818
} from '@mantine/core';
1919

20-
import EventTag from '@/components/EventTag';
2120
import { FormatTime } from '@/useFormatTime';
2221
import { useUserPreferences } from '@/useUserPreferences';
2322
import { formatDistanceToNowStrictShort } from '@/utils';
2423

24+
import {
25+
DBHighlightedAttributesList,
26+
HighlightedAttribute,
27+
} from './DBHighlightedAttributesList';
2528
import { RowSidePanelContext } from './DBRowSidePanel';
2629
import LogLevel from './LogLevel';
2730

@@ -119,7 +122,7 @@ function BreadcrumbNavigation({
119122
}
120123

121124
export default function DBRowSidePanelHeader({
122-
tags,
125+
attributes = [],
123126
mainContent = '',
124127
mainContentHeader,
125128
date,
@@ -130,7 +133,7 @@ export default function DBRowSidePanelHeader({
130133
date: Date;
131134
mainContent?: string;
132135
mainContentHeader?: string;
133-
tags: Record<string, string>;
136+
attributes?: HighlightedAttribute[];
134137
severityText?: string;
135138
breadcrumbPath?: BreadcrumbPath;
136139
onBreadcrumbClick?: BreadcrumbNavigationCallback;
@@ -264,35 +267,9 @@ export default function DBRowSidePanelHeader({
264267
</Text>
265268
</Paper>
266269
)}
267-
<Flex mt="sm">
268-
{Object.entries(tags).map(([sqlKey, value]) => {
269-
// Convert SQL syntax to Lucene syntax
270-
// SQL: column['property.foo'] -> Lucene: column.property.foo
271-
// or SQL: column -> Lucene: column
272-
const luceneKey = sqlKey.replace(/\['([^']+)'\]/g, '.$1');
273-
274-
return (
275-
<EventTag
276-
{...(onPropertyAddClick
277-
? {
278-
onPropertyAddClick,
279-
sqlExpression: sqlKey,
280-
}
281-
: {
282-
onPropertyAddClick: undefined,
283-
sqlExpression: undefined,
284-
})}
285-
generateSearchUrl={
286-
generateSearchUrl ? _generateSearchUrl : undefined
287-
}
288-
displayedKey={luceneKey}
289-
name={luceneKey}
290-
value={value}
291-
key={sqlKey}
292-
/>
293-
);
294-
})}
295-
</Flex>
270+
<Box mt="xs">
271+
<DBHighlightedAttributesList attributes={attributes} />
272+
</Box>
296273
</>
297274
);
298275
}

packages/app/src/components/EventTag.tsx

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { useState } from 'react';
22
import Link from 'next/link';
33
import SqlString from 'sqlstring';
44
import { SearchConditionLanguage } from '@hyperdx/common-utils/dist/types';
5-
import { Button, Popover, Stack } from '@mantine/core';
5+
import { Button, Popover, Stack, Tooltip } from '@mantine/core';
6+
import { IconLink } from '@tabler/icons-react';
7+
8+
import { isLinkableUrl } from '@/utils/highlightedAttributes';
69

710
export default function EventTag({
811
displayedKey,
@@ -44,6 +47,8 @@ export default function EventTag({
4447
);
4548
}
4649

50+
const isLink = isLinkableUrl(value);
51+
4752
const searchCondition =
4853
nameLanguage === 'sql'
4954
? SqlString.format('? = ?', [SqlString.raw(name), value])
@@ -58,13 +63,32 @@ export default function EventTag({
5863
onChange={setOpened}
5964
>
6065
<Popover.Target>
61-
<div
62-
key={name}
63-
className="bg-highlighted px-2 py-0.5 me-1 my-1 cursor-pointer"
64-
onClick={() => setOpened(!opened)}
65-
>
66-
{displayedKey || name}: {value}
67-
</div>
66+
{isLink ? (
67+
<Tooltip
68+
label={value}
69+
withArrow
70+
maw={400}
71+
multiline
72+
style={{ wordBreak: 'break-word' }}
73+
>
74+
<a
75+
href={encodeURI(value)}
76+
target="_blank"
77+
rel="noopener noreferrer"
78+
className="d-flex flex-row align-items-center bg-highlighted px-2 py-0.5 me-1 my-1 cursor-pointer"
79+
>
80+
{displayedKey || name}
81+
<IconLink size={14} className="ms-1" />
82+
</a>
83+
</Tooltip>
84+
) : (
85+
<div
86+
className="bg-highlighted px-2 py-0.5 me-1 my-1 cursor-pointer"
87+
onClick={() => setOpened(!opened)}
88+
>
89+
{displayedKey || name}: {value}
90+
</div>
91+
)}
6892
</Popover.Target>
6993
<Popover.Dropdown p={2}>
7094
<Stack gap={0} justify="stretch">

packages/app/src/components/SourceForm.tsx

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,16 @@ function FormRow({
151151
function HighlightedAttributeExpressionsFormRow({
152152
control,
153153
watch,
154-
}: TableModelProps) {
154+
name,
155+
label,
156+
helpText,
157+
}: TableModelProps & {
158+
name:
159+
| 'highlightedTraceAttributeExpressions'
160+
| 'highlightedRowAttributeExpressions';
161+
label: string;
162+
helpText?: string;
163+
}) {
155164
const databaseName = watch(`from.databaseName`, DEFAULT_DATABASE);
156165
const tableName = watch(`from.tableName`);
157166
const connectionId = watch(`connection`);
@@ -162,14 +171,11 @@ function HighlightedAttributeExpressionsFormRow({
162171
remove: removeHighlightedAttribute,
163172
} = useFieldArray({
164173
control,
165-
name: 'highlightedTraceAttributeExpressions',
174+
name,
166175
});
167176

168177
return (
169-
<FormRow
170-
label={'Highlighted Attributes'}
171-
helpText="Expressions defining trace-level attributes which are displayed in the search side panel."
172-
>
178+
<FormRow label={label} helpText={helpText}>
173179
<Grid columns={5}>
174180
{highlightedAttributes.map((field, index) => (
175181
<React.Fragment key={field.id}>
@@ -181,7 +187,7 @@ function HighlightedAttributeExpressionsFormRow({
181187
connectionId,
182188
}}
183189
control={control}
184-
name={`highlightedTraceAttributeExpressions.${index}.sqlExpression`}
190+
name={`${name}.${index}.sqlExpression`}
185191
disableKeywordAutocomplete
186192
placeholder="ResourceAttributes['http.host']"
187193
/>
@@ -191,7 +197,7 @@ function HighlightedAttributeExpressionsFormRow({
191197
<Text c="gray">AS</Text>
192198
<SQLInlineEditorControlled
193199
control={control}
194-
name={`highlightedTraceAttributeExpressions.${index}.alias`}
200+
name={`${name}.${index}.alias`}
195201
placeholder="Optional Alias"
196202
disableKeywordAutocomplete
197203
/>
@@ -208,7 +214,7 @@ function HighlightedAttributeExpressionsFormRow({
208214
<Grid.Col span={3} pe={0}>
209215
<InputControlled
210216
control={control}
211-
name={`highlightedTraceAttributeExpressions.${index}.luceneExpression`}
217+
name={`${name}.${index}.luceneExpression`}
212218
placeholder="ResourceAttributes.http.host (Optional) "
213219
/>
214220
</Grid.Col>
@@ -489,7 +495,18 @@ export function LogTableModelForm(props: TableModelProps) {
489495
/>
490496
</FormRow>
491497
<Divider />
492-
<HighlightedAttributeExpressionsFormRow {...props} />
498+
<HighlightedAttributeExpressionsFormRow
499+
{...props}
500+
name="highlightedRowAttributeExpressions"
501+
label="Highlighted Attributes"
502+
helpText="Expressions defining row-level attributes which are displayed in the search side panel."
503+
/>
504+
<HighlightedAttributeExpressionsFormRow
505+
{...props}
506+
name="highlightedTraceAttributeExpressions"
507+
label="Highlighted Trace Attributes"
508+
helpText="Expressions defining trace-level attributes which are displayed in the search side panel."
509+
/>
493510
</Stack>
494511
</>
495512
);
@@ -758,7 +775,18 @@ export function TraceTableModelForm(props: TableModelProps) {
758775
/>
759776
</FormRow>
760777
<Divider />
761-
<HighlightedAttributeExpressionsFormRow {...props} />
778+
<HighlightedAttributeExpressionsFormRow
779+
{...props}
780+
name="highlightedRowAttributeExpressions"
781+
label="Highlighted Attributes"
782+
helpText="Expressions defining row-level attributes which are displayed in the search side panel."
783+
/>
784+
<HighlightedAttributeExpressionsFormRow
785+
{...props}
786+
name="highlightedTraceAttributeExpressions"
787+
label="Highlighted Trace Attributes"
788+
helpText="Expressions defining trace-level attributes which are displayed in the search side panel."
789+
/>
762790
</Stack>
763791
);
764792
}

0 commit comments

Comments
 (0)