From 83fd7f20b4f325428d4a1f67640083972b384898 Mon Sep 17 00:00:00 2001 From: Drew Davis Date: Fri, 12 Dec 2025 16:16:32 -0500 Subject: [PATCH 1/7] fix: Prevent errors from DESCRIBEing CTEs --- .../src/core/renderChartConfig.ts | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/common-utils/src/core/renderChartConfig.ts b/packages/common-utils/src/core/renderChartConfig.ts index fb7c903828..ade3148438 100644 --- a/packages/common-utils/src/core/renderChartConfig.ts +++ b/packages/common-utils/src/core/renderChartConfig.ts @@ -381,14 +381,16 @@ async function renderSelectList( // supported for queries using CTEs so skip the metadata fetch if there are CTE objects in the config. let materializedFields: Map | undefined; try { - // This will likely error for a CTE - materializedFields = chartConfig.with?.length - ? undefined - : await metadata.getMaterializedColumnsLookupTable({ - connectionId: chartConfig.connection, - databaseName: chartConfig.from.databaseName, - tableName: chartConfig.from.tableName, - }); + // This will likely error when referencing a CTE, which is assumed + // to be the case when chartConfig.from.databaseName is not set. + materializedFields = + chartConfig.with?.length || !chartConfig.from.databaseName + ? undefined + : await metadata.getMaterializedColumnsLookupTable({ + connectionId: chartConfig.connection, + databaseName: chartConfig.from.databaseName, + tableName: chartConfig.from.tableName, + }); } catch { // ignore } @@ -689,14 +691,16 @@ async function renderWhereExpression({ let materializedFields: Map | undefined; try { - // This will likely error for a CTE - materializedFields = withClauses?.length - ? undefined - : await metadata.getMaterializedColumnsLookupTable({ - connectionId, - databaseName: from.databaseName, - tableName: from.tableName, - }); + // This will likely error when referencing a CTE, which is assumed + // to be the case when from.databaseName is not set. + materializedFields = + withClauses?.length || !from.databaseName + ? undefined + : await metadata.getMaterializedColumnsLookupTable({ + connectionId, + databaseName: from.databaseName, + tableName: from.tableName, + }); } catch { // ignore } From 3a71b3e8f182181a46a91306dfd5e77dbb973574 Mon Sep 17 00:00:00 2001 From: Drew Davis Date: Mon, 15 Dec 2025 09:33:21 -0500 Subject: [PATCH 2/7] fix: Prevent extra Request Throughput refetches --- packages/app/src/ServicesDashboardPage.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/app/src/ServicesDashboardPage.tsx b/packages/app/src/ServicesDashboardPage.tsx index 9fb3eafd19..1a7cf262f6 100644 --- a/packages/app/src/ServicesDashboardPage.tsx +++ b/packages/app/src/ServicesDashboardPage.tsx @@ -542,10 +542,7 @@ function HttpTab({ ...source, where: appliedConfig.where || '', whereLanguage: appliedConfig.whereLanguage || 'sql', - displayType: - reqChartType === 'overall' - ? DisplayType.Line - : DisplayType.StackedBar, + displayType: DisplayType.Line, select: [ { aggFn: 'count' as const, From 17b05565617c8336b6c03e01a6e95eff3e601d53 Mon Sep 17 00:00:00 2001 From: Drew Davis Date: Mon, 15 Dec 2025 10:54:19 -0500 Subject: [PATCH 3/7] fix: Prevent extra Services Dashboard requests when switching sources --- .changeset/popular-coins-pretend.md | 7 ++++ packages/app/src/ServicesDashboardPage.tsx | 49 +++++++++++++++++----- 2 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 .changeset/popular-coins-pretend.md diff --git a/.changeset/popular-coins-pretend.md b/.changeset/popular-coins-pretend.md new file mode 100644 index 0000000000..60dd22f6b7 --- /dev/null +++ b/.changeset/popular-coins-pretend.md @@ -0,0 +1,7 @@ +--- +"@hyperdx/common-utils": patch +"@hyperdx/api": patch +"@hyperdx/app": patch +--- + +fix: Fix bugs in the Services dashboard diff --git a/packages/app/src/ServicesDashboardPage.tsx b/packages/app/src/ServicesDashboardPage.tsx index 1a7cf262f6..c47d3ae9aa 100644 --- a/packages/app/src/ServicesDashboardPage.tsx +++ b/packages/app/src/ServicesDashboardPage.tsx @@ -1338,13 +1338,34 @@ function ServicesDashboardPage() { const { data: sources } = useSources(); - const [appliedConfig, setAppliedConfig] = useQueryStates(appliedConfigMap); + const [appliedConfigParams, setAppliedConfigParams] = + useQueryStates(appliedConfigMap); + + // Only use the source from the URL params if it is a trace source + const appliedConfig = useMemo(() => { + if (!sources?.length) return appliedConfigParams; + + const traceSources = sources?.filter(s => s.kind === SourceKind.Trace); + const paramsSourceIdIsTraceSource = traceSources?.find( + s => s.id === appliedConfigParams.source, + ); + + const effectiveSourceId = paramsSourceIdIsTraceSource + ? appliedConfigParams.source + : traceSources?.[0]?.id || ''; + + return { + ...appliedConfigParams, + source: effectiveSourceId, + }; + }, [appliedConfigParams, sources]); + const { control, watch, setValue, handleSubmit } = useForm({ - values: { + defaultValues: { where: '', whereLanguage: 'sql' as 'sql' | 'lucene', service: appliedConfig?.service || '', - source: appliedConfig?.source || sources?.[0]?.id, + source: appliedConfig?.source ?? '', }, }); @@ -1355,10 +1376,18 @@ function ServicesDashboardPage() { }); useEffect(() => { - if (sourceId && !appliedConfig.source) { - setAppliedConfig({ source: sourceId }); + if ( + appliedConfig.source && + appliedConfig.source !== appliedConfigParams.source + ) { + setAppliedConfigParams({ source: sourceId }); } - }, [appliedConfig.source, setAppliedConfig, sourceId]); + }, [ + appliedConfig.source, + appliedConfigParams.source, + setAppliedConfigParams, + sourceId, + ]); const DEFAULT_INTERVAL = 'Past 1h'; const [displayedTimeInputValue, setDisplayedTimeInputValue] = @@ -1371,7 +1400,7 @@ function ServicesDashboardPage() { }); // For future use if Live button is added - const [isLive, setIsLive] = useState(false); + const [isLive, _setIsLive] = useState(false); const { manualRefreshCooloff, refresh } = useDashboardRefresh({ searchedTimeRange, @@ -1382,9 +1411,9 @@ function ServicesDashboardPage() { const onSubmit = useCallback(() => { onSearch(displayedTimeInputValue); handleSubmit(values => { - setAppliedConfig(values); + setAppliedConfigParams(values); })(); - }, [handleSubmit, setAppliedConfig, onSearch, displayedTimeInputValue]); + }, [handleSubmit, setAppliedConfigParams, onSearch, displayedTimeInputValue]); // Auto submit when service or source changes useEffect(() => { @@ -1551,7 +1580,7 @@ const ServicesDashboardPageDynamic = dynamic( }, ); -// @ts-ignore +// @ts-expect-error Next.js layout typing ServicesDashboardPageDynamic.getLayout = withAppNav; export default ServicesDashboardPageDynamic; From 209c5b246455cc38008102afc8e98d899bd14ee1 Mon Sep 17 00:00:00 2001 From: Drew Davis Date: Mon, 15 Dec 2025 15:20:38 -0500 Subject: [PATCH 4/7] test: Fix integration test after renderChartConfig update --- .../api/src/clickhouse/__tests__/renderChartConfig.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts b/packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts index 36d8cc366c..228cbe035f 100644 --- a/packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts +++ b/packages/api/src/clickhouse/__tests__/renderChartConfig.test.ts @@ -145,7 +145,7 @@ describe('renderChartConfig', () => { { aggFn: 'sum', valueExpression: 'strVal' }, ], from: { - databaseName: '', + databaseName: DEFAULT_DATABASE, tableName: `agg_fn_str_test`, }, where: '', @@ -183,7 +183,7 @@ describe('renderChartConfig', () => { { aggFn: 'sum', valueExpression: 'strVal' }, ], from: { - databaseName: '', + databaseName: DEFAULT_DATABASE, tableName: `agg_fn_default_test`, }, where: '', From 4bf27bc9ad458767f60a847e9cb3af66b4d93cde Mon Sep 17 00:00:00 2001 From: Drew Davis Date: Tue, 16 Dec 2025 08:57:40 -0500 Subject: [PATCH 5/7] fix: Prevent errors on services dashboard when source has metricTables defined --- packages/app/src/ServicesDashboardPage.tsx | 52 +++++++++++++++---- .../ServiceDashboardDbQuerySidePanel.tsx | 13 ++++- ...rviceDashboardEndpointPerformanceChart.tsx | 3 +- .../ServiceDashboardEndpointSidePanel.tsx | 13 ++++- .../ServiceDashboardSlowestEventsTile.tsx | 9 +++- 5 files changed, 73 insertions(+), 17 deletions(-) diff --git a/packages/app/src/ServicesDashboardPage.tsx b/packages/app/src/ServicesDashboardPage.tsx index c47d3ae9aa..ef5c0fb298 100644 --- a/packages/app/src/ServicesDashboardPage.tsx +++ b/packages/app/src/ServicesDashboardPage.tsx @@ -128,7 +128,7 @@ function ServiceSelectControlled({ const { expressions } = useServiceDashboardExpressions({ source }); const queriedConfig = { - ...source, + timestampValueExpression: source?.timestampValueExpression || '', from: { databaseName: source?.from.databaseName || '', tableName: source?.from.tableName || '', @@ -240,7 +240,11 @@ export function EndpointLatencyChart({ 'avg_duration_ns', ]} config={{ - ...source, + ...pick(source, [ + 'timestampValueExpression', + 'connection', + 'from', + ]), where: appliedConfig.where || '', whereLanguage: appliedConfig.whereLanguage || 'sql', select: [ @@ -289,7 +293,11 @@ export function EndpointLatencyChart({ ) : ( Date: Tue, 16 Dec 2025 10:31:36 -0500 Subject: [PATCH 6/7] fix: Fix render loop --- packages/app/src/ServicesDashboardPage.tsx | 47 +++++++++++++--------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/packages/app/src/ServicesDashboardPage.tsx b/packages/app/src/ServicesDashboardPage.tsx index ef5c0fb298..b84b8fa560 100644 --- a/packages/app/src/ServicesDashboardPage.tsx +++ b/packages/app/src/ServicesDashboardPage.tsx @@ -7,7 +7,7 @@ import { useQueryState, useQueryStates, } from 'nuqs'; -import { UseControllerProps, useForm } from 'react-hook-form'; +import { UseControllerProps, useForm, useWatch } from 'react-hook-form'; import { tcFromSource } from '@hyperdx/common-utils/dist/core/metadata'; import { DEFAULT_AUTO_GRANULARITY_MAX_BUCKETS } from '@hyperdx/common-utils/dist/core/renderChartConfig'; import { @@ -1407,6 +1407,7 @@ function ServicesDashboardPage() { id: watch('source'), }); + // Update the `source` query parameter if the appliedConfig source changes useEffect(() => { if ( appliedConfig.source && @@ -1447,26 +1448,34 @@ function ServicesDashboardPage() { })(); }, [handleSubmit, setAppliedConfigParams, onSearch, displayedTimeInputValue]); - // Auto submit when service or source changes + // Auto-submit when source changes useEffect(() => { - const normalizedService = service ?? ''; - const appliedService = appliedConfig.service ?? ''; - const normalizedSource = sourceId ?? ''; - const appliedSource = appliedConfig.source ?? ''; + const { unsubscribe } = watch((data, { name, type }) => { + if ( + name === 'source' && + type === 'change' && + data.source && + data.source !== appliedConfig.source + ) { + onSubmit(); + } + }); + return () => unsubscribe(); + }, [appliedConfig.source, onSubmit, watch]); - if ( - normalizedService !== appliedService || - (normalizedSource && normalizedSource !== appliedSource) - ) { - onSubmit(); - } - }, [ - service, - sourceId, - appliedConfig.service, - appliedConfig.source, - onSubmit, - ]); + // Auto-submit when service changes + useEffect(() => { + const { unsubscribe } = watch((data, { name, type }) => { + if ( + name === 'service' && + type === 'change' && + data.service !== appliedConfig.service + ) { + onSubmit(); + } + }); + return () => unsubscribe(); + }, [appliedConfig.service, onSubmit, watch]); return ( From 58da5eed8afc58f0a635eebf93a38b6ca8761d7e Mon Sep 17 00:00:00 2001 From: Drew Davis Date: Tue, 16 Dec 2025 10:39:50 -0500 Subject: [PATCH 7/7] fix: Use the right source value in effect --- packages/app/src/ServicesDashboardPage.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/app/src/ServicesDashboardPage.tsx b/packages/app/src/ServicesDashboardPage.tsx index b84b8fa560..43db4794e3 100644 --- a/packages/app/src/ServicesDashboardPage.tsx +++ b/packages/app/src/ServicesDashboardPage.tsx @@ -1413,13 +1413,12 @@ function ServicesDashboardPage() { appliedConfig.source && appliedConfig.source !== appliedConfigParams.source ) { - setAppliedConfigParams({ source: sourceId }); + setAppliedConfigParams({ source: appliedConfig.source }); } }, [ appliedConfig.source, appliedConfigParams.source, setAppliedConfigParams, - sourceId, ]); const DEFAULT_INTERVAL = 'Past 1h';