mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
fix: filter values for json casted to string (#1184)
Fixes HDX-2425 Was able to make improvements to the filters appearance, but cast to string for the actual query. Fixes the query and improves the UI! <img width="420" height="204" alt="image" src="https://github.com/user-attachments/assets/0c30cc9a-ccea-499e-8c5f-51c8c38871e5" />
This commit is contained in:
parent
825452fe86
commit
21f1aa7567
3 changed files with 225 additions and 6 deletions
5
.changeset/hungry-onions-buy.md
Normal file
5
.changeset/hungry-onions-buy.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
fix: filter values for json casted to string
|
||||
|
|
@ -42,6 +42,21 @@ import { mergePath } from '@/utils';
|
|||
import resizeStyles from '../../styles/ResizablePanel.module.scss';
|
||||
import classes from '../../styles/SearchPage.module.scss';
|
||||
|
||||
// This function will clean json string attributes specifically. It will turn a string like
|
||||
// 'toString(ResourceAttributes.`hdx`.`sdk`.`version`)' into 'ResourceAttributes.hdx.sdk.verion'.
|
||||
export function cleanedFacetName(key: string): string {
|
||||
if (key.startsWith('toString')) {
|
||||
return key
|
||||
.slice('toString('.length, key.length - 1)
|
||||
.split('.')
|
||||
.map(str =>
|
||||
str.startsWith('`') && str.endsWith('`') ? str.slice(1, -1) : str,
|
||||
)
|
||||
.join('.');
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
type FilterCheckboxProps = {
|
||||
label: string;
|
||||
value?: 'included' | 'excluded' | false;
|
||||
|
|
@ -455,7 +470,7 @@ const DBSearchPageFiltersComponent = ({
|
|||
filters: filterState,
|
||||
clearAllFilters,
|
||||
clearFilter,
|
||||
setFilterValue,
|
||||
setFilterValue: _setFilterValue,
|
||||
isLive,
|
||||
chartConfig,
|
||||
analysisMode,
|
||||
|
|
@ -474,6 +489,13 @@ const DBSearchPageFiltersComponent = ({
|
|||
denoiseResults: boolean;
|
||||
setDenoiseResults: (denoiseResults: boolean) => void;
|
||||
} & FilterStateHook) => {
|
||||
const setFilterValue: typeof _setFilterValue = (
|
||||
property: string,
|
||||
value: string,
|
||||
action?: 'only' | 'exclude' | 'include' | undefined,
|
||||
) => {
|
||||
return _setFilterValue(property, value, action);
|
||||
};
|
||||
const {
|
||||
toggleFilterPin,
|
||||
toggleFieldPin,
|
||||
|
|
@ -612,7 +634,12 @@ const DBSearchPageFiltersComponent = ({
|
|||
|
||||
const shownFacets = useMemo(() => {
|
||||
const _facets: { key: string; value: string[] }[] = [];
|
||||
for (const facet of facets ?? []) {
|
||||
for (const _facet of facets ?? []) {
|
||||
const facet = structuredClone(_facet);
|
||||
if (jsonColumns?.some(col => facet.key.startsWith(col))) {
|
||||
facet.key = `toString(${facet.key})`;
|
||||
}
|
||||
|
||||
// don't include empty facets, unless they are already selected
|
||||
const filter = filterState[facet.key];
|
||||
const hasSelectedValues =
|
||||
|
|
@ -665,7 +692,14 @@ const DBSearchPageFiltersComponent = ({
|
|||
});
|
||||
|
||||
return _facets;
|
||||
}, [facets, filterState, tableMetadata, extraFacets, isFieldPinned]);
|
||||
}, [
|
||||
facets,
|
||||
filterState,
|
||||
tableMetadata,
|
||||
extraFacets,
|
||||
isFieldPinned,
|
||||
jsonColumns,
|
||||
]);
|
||||
|
||||
const showClearAllButton = useMemo(
|
||||
() =>
|
||||
|
|
@ -778,7 +812,7 @@ const DBSearchPageFiltersComponent = ({
|
|||
{shownFacets.map(facet => (
|
||||
<FilterGroup
|
||||
key={facet.key}
|
||||
name={facet.key}
|
||||
name={cleanedFacetName(facet.key)}
|
||||
options={facet.value.map(value => ({
|
||||
value,
|
||||
label: value,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,187 @@
|
|||
import { render, screen, within } from '@testing-library/react';
|
||||
import { screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { FilterGroup, type FilterGroupProps } from '../DBSearchPageFilters';
|
||||
import {
|
||||
cleanedFacetName,
|
||||
FilterGroup,
|
||||
type FilterGroupProps,
|
||||
} from '../DBSearchPageFilters';
|
||||
|
||||
describe('cleanedFacetName', () => {
|
||||
describe('basic functionality', () => {
|
||||
it('should return non-toString strings unchanged', () => {
|
||||
expect(cleanedFacetName('simple.field')).toBe('simple.field');
|
||||
expect(cleanedFacetName('column_name')).toBe('column_name');
|
||||
expect(cleanedFacetName('ResourceAttributes.service.name')).toBe(
|
||||
'ResourceAttributes.service.name',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty strings', () => {
|
||||
expect(cleanedFacetName('')).toBe('');
|
||||
});
|
||||
|
||||
it('should handle strings that do not start with toString', () => {
|
||||
expect(cleanedFacetName('notToString(field)')).toBe('notToString(field)');
|
||||
expect(cleanedFacetName("JSONExtractString(data, 'field')")).toBe(
|
||||
"JSONExtractString(data, 'field')",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('JSON column cleaning', () => {
|
||||
it('should clean basic ResourceAttributes paths', () => {
|
||||
expect(
|
||||
cleanedFacetName('toString(ResourceAttributes.`service`.`name`)'),
|
||||
).toBe('ResourceAttributes.service.name');
|
||||
|
||||
expect(
|
||||
cleanedFacetName('toString(ResourceAttributes.`hdx`.`sdk`.`version`)'),
|
||||
).toBe('ResourceAttributes.hdx.sdk.version');
|
||||
});
|
||||
|
||||
it('should handle mixed quoted and unquoted ResourceAttributes', () => {
|
||||
expect(
|
||||
cleanedFacetName('toString(ResourceAttributes.`service`.name)'),
|
||||
).toBe('ResourceAttributes.service.name');
|
||||
|
||||
expect(
|
||||
cleanedFacetName('toString(ResourceAttributes.service.`name`)'),
|
||||
).toBe('ResourceAttributes.service.name');
|
||||
});
|
||||
|
||||
it('should handle deeply nested ResourceAttributes', () => {
|
||||
expect(
|
||||
cleanedFacetName(
|
||||
'toString(ResourceAttributes.`telemetry`.`sdk`.`language`)',
|
||||
),
|
||||
).toBe('ResourceAttributes.telemetry.sdk.language');
|
||||
|
||||
expect(
|
||||
cleanedFacetName(
|
||||
'toString(ResourceAttributes.`cloud`.`provider`.`account`.`id`)',
|
||||
),
|
||||
).toBe('ResourceAttributes.cloud.provider.account.id');
|
||||
});
|
||||
|
||||
it('should handle ResourceAttributes with special characters', () => {
|
||||
expect(
|
||||
cleanedFacetName('toString(ResourceAttributes.`service-name`)'),
|
||||
).toBe('ResourceAttributes.service-name');
|
||||
|
||||
expect(
|
||||
cleanedFacetName('toString(ResourceAttributes.`service`.`version`)'),
|
||||
).toBe('ResourceAttributes.service.version');
|
||||
|
||||
expect(
|
||||
cleanedFacetName('toString(ResourceAttributes.`k8s`.`pod`.`name`)'),
|
||||
).toBe('ResourceAttributes.k8s.pod.name');
|
||||
});
|
||||
|
||||
it('should clean basic LogAttributes paths', () => {
|
||||
expect(
|
||||
cleanedFacetName('toString(LogAttributes.`severity`.`text`)'),
|
||||
).toBe('LogAttributes.severity.text');
|
||||
|
||||
expect(cleanedFacetName('toString(LogAttributes.`level`)')).toBe(
|
||||
'LogAttributes.level',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle deeply nested LogAttributes', () => {
|
||||
expect(
|
||||
cleanedFacetName('toString(LogAttributes.`context`.`user`.`id`)'),
|
||||
).toBe('LogAttributes.context.user.id');
|
||||
|
||||
expect(
|
||||
cleanedFacetName('toString(LogAttributes.`http`.`request`.`method`)'),
|
||||
).toBe('LogAttributes.http.request.method');
|
||||
});
|
||||
|
||||
it('should handle Map access in ResourceAttributes', () => {
|
||||
expect(
|
||||
cleanedFacetName(
|
||||
"ResourceAttributes['http.request.headers.user-agent']",
|
||||
),
|
||||
).toBe("ResourceAttributes['http.request.headers.user-agent']");
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases with Attributes', () => {
|
||||
it('should handle attributes with spaces', () => {
|
||||
expect(
|
||||
cleanedFacetName('toString(ResourceAttributes.`service name`)'),
|
||||
).toBe('ResourceAttributes.service name');
|
||||
|
||||
expect(cleanedFacetName('toString(LogAttributes.`error message`)')).toBe(
|
||||
'LogAttributes.error message',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle attributes with numbers', () => {
|
||||
expect(
|
||||
cleanedFacetName('toString(ResourceAttributes.`service`.`v2`)'),
|
||||
).toBe('ResourceAttributes.service.v2');
|
||||
|
||||
expect(
|
||||
cleanedFacetName('toString(LogAttributes.`error`.`code`.`404`)'),
|
||||
).toBe('LogAttributes.error.code.404');
|
||||
});
|
||||
});
|
||||
|
||||
describe('real-world OpenTelemetry patterns', () => {
|
||||
// Common OTEL semantic conventions
|
||||
it('should handle service attributes', () => {
|
||||
expect(
|
||||
cleanedFacetName('toString(ResourceAttributes.`service`.`name`)'),
|
||||
).toBe('ResourceAttributes.service.name');
|
||||
|
||||
expect(
|
||||
cleanedFacetName('toString(ResourceAttributes.`service`.`version`)'),
|
||||
).toBe('ResourceAttributes.service.version');
|
||||
|
||||
expect(
|
||||
cleanedFacetName(
|
||||
'toString(ResourceAttributes.`service`.`instance`.`id`)',
|
||||
),
|
||||
).toBe('ResourceAttributes.service.instance.id');
|
||||
});
|
||||
|
||||
it('should handle telemetry SDK attributes', () => {
|
||||
expect(
|
||||
cleanedFacetName(
|
||||
'toString(ResourceAttributes.`telemetry`.`sdk`.`name`)',
|
||||
),
|
||||
).toBe('ResourceAttributes.telemetry.sdk.name');
|
||||
|
||||
expect(
|
||||
cleanedFacetName(
|
||||
'toString(ResourceAttributes.`telemetry`.`sdk`.`language`)',
|
||||
),
|
||||
).toBe('ResourceAttributes.telemetry.sdk.language');
|
||||
|
||||
expect(
|
||||
cleanedFacetName(
|
||||
'toString(ResourceAttributes.`telemetry`.`sdk`.`version`)',
|
||||
),
|
||||
).toBe('ResourceAttributes.telemetry.sdk.version');
|
||||
});
|
||||
|
||||
it('should handle HTTP attributes', () => {
|
||||
expect(cleanedFacetName('toString(LogAttributes.`http`.`method`)')).toBe(
|
||||
'LogAttributes.http.method',
|
||||
);
|
||||
|
||||
expect(
|
||||
cleanedFacetName('toString(LogAttributes.`http`.`status_code`)'),
|
||||
).toBe('LogAttributes.http.status_code');
|
||||
|
||||
expect(cleanedFacetName('toString(LogAttributes.`http`.`url`)')).toBe(
|
||||
'LogAttributes.http.url',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('FilterGroup', () => {
|
||||
const defaultProps: FilterGroupProps = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue