[HDX-3277] Fix service filter quote escaping on Services page (#1931)

## Summary
- escape service name values when generating the Services page SQL filter to prevent malformed queries when names contain quotes
- switch from string interpolation to `SqlString.format` with a raw left-hand expression and escaped right-hand value

## Why
- service names containing apostrophes/single quotes broke ClickHouse query parsing, causing the Services page to error

Linear: https://linear.app/clickhouse/issue/HDX-3277/service-page-quote-escape-bug
This commit is contained in:
Warren Lee 2026-03-18 14:24:07 -07:00 committed by GitHub
parent 2b53b8e9ab
commit 134f1dca47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 44 additions and 1 deletions

View file

@ -0,0 +1,5 @@
---
'@hyperdx/app': patch
---
fix: escape service filter values on Services page to handle quoted names safely

View file

@ -8,6 +8,7 @@ import {
useQueryStates,
} from 'nuqs';
import { UseControllerProps, useForm, useWatch } from 'react-hook-form';
import SqlString from 'sqlstring';
import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata';
import { convertDateRangeToGranularityString } from '@hyperdx/common-utils/dist/core/utils';
import {
@ -90,6 +91,13 @@ type AppliedConfig = AppliedConfigParams & {
const MAX_NUM_SERIES = HARD_LINES_LIMIT;
export function buildInFilterCondition(
columnExpression: string,
value: string,
): string {
return SqlString.format('? IN (?)', [SqlString.raw(columnExpression), value]);
}
function getScopedFilters({
appliedConfig,
expressions,
@ -112,7 +120,10 @@ function getScopedFilters({
if (appliedConfig.service) {
filters.push({
type: 'sql',
condition: `${expressions.service} IN ('${appliedConfig.service}')`,
condition: buildInFilterCondition(
expressions.service,
appliedConfig.service,
),
});
}
if (includeNonEmptyEndpointFilter) {

View file

@ -0,0 +1,27 @@
import { buildInFilterCondition } from '../ServicesDashboardPage';
describe('buildInFilterCondition', () => {
it.each([
{
columnExpression: 'ServiceName',
value: 'checkout-service',
expected: "ServiceName IN ('checkout-service')",
},
{
columnExpression: "SpanAttributes['service.name']",
value: "O'Reilly API",
expected: "SpanAttributes['service.name'] IN ('O\\'Reilly API')",
},
{
columnExpression: "ResourceAttributes['service.namespace']",
value: 'payments "v2"',
expected:
"ResourceAttributes['service.namespace'] IN ('payments \\\"v2\\\"')",
},
])(
'escapes value and keeps column expression for $columnExpression',
({ columnExpression, value, expected }) => {
expect(buildInFilterCondition(columnExpression, value)).toBe(expected);
},
);
});