mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
fix: Fix Services Dashboard bugs (#1484)
Closes HDX-3033 # Summary This PR fixes three bugs in the Services Dashboard 1. When using CTEs in chart configs, as we do on the HTTP and Databases tabs, there were frequent console errors as we tried to `DESCRIBE` the CTE names, to support the materialized columns optimization. With this PR, we no longer try to DESCRIBE CTEs, by skipping the materialized column optimization for configs without a `from.databaseName`. 2. Previously, the Request Throughput chart would reload whenever switching the Request Error Rate chart from `Overall` to `By Endpoint`. This was because the `displayType` in the Request Throughput chart was based on the toggle state, despite being unrelated. Now, the displayType of the Request Throughput chart is constant, eliminating the extra refetch. 3. Previously, when switching to the Services dashboard with a non-Trace Source ID in the URL params, the Services dashboard would initially be empty, then after toggling to a Trace Source, queries would briefly be issued against the non-Trace source (they would fail and/or be cancelled a moment later). Now, non-Trace sources are filtered out so that a Trace source is chosen as the default, and non-Trace sources are not queried. 4. Previously, we were spreading the entirety of `...source` into each config, which resulted in `metricTables` being in the config under particular circumstances (HDX-3035), which in turn caused errors from renderChartConfig. This has been fixed by `pick`ing only the fields we need from source.
This commit is contained in:
parent
8241ffea89
commit
b58c52eb41
8 changed files with 169 additions and 68 deletions
7
.changeset/popular-coins-pretend.md
Normal file
7
.changeset/popular-coins-pretend.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
"@hyperdx/common-utils": patch
|
||||
"@hyperdx/api": patch
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
fix: Fix bugs in the Services dashboard
|
||||
|
|
@ -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: '',
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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({
|
|||
) : (
|
||||
<DBHistogramChart
|
||||
config={{
|
||||
...source,
|
||||
...pick(source, [
|
||||
'timestampValueExpression',
|
||||
'connection',
|
||||
'from',
|
||||
]),
|
||||
where: appliedConfig.where || '',
|
||||
whereLanguage: appliedConfig.whereLanguage || 'sql',
|
||||
select: [
|
||||
|
|
@ -343,7 +351,7 @@ function HttpTab({
|
|||
if (!source || !expressions) return null;
|
||||
if (reqChartType === 'overall') {
|
||||
return {
|
||||
...source,
|
||||
...pick(source, ['timestampValueExpression', 'connection', 'from']),
|
||||
where: appliedConfig.where || '',
|
||||
whereLanguage: appliedConfig.whereLanguage || 'sql',
|
||||
displayType: DisplayType.Line,
|
||||
|
|
@ -539,13 +547,14 @@ function HttpTab({
|
|||
<DBTimeChart
|
||||
sourceId={source.id}
|
||||
config={{
|
||||
...source,
|
||||
...pick(source, [
|
||||
'timestampValueExpression',
|
||||
'connection',
|
||||
'from',
|
||||
]),
|
||||
where: appliedConfig.where || '',
|
||||
whereLanguage: appliedConfig.whereLanguage || 'sql',
|
||||
displayType:
|
||||
reqChartType === 'overall'
|
||||
? DisplayType.Line
|
||||
: DisplayType.StackedBar,
|
||||
displayType: DisplayType.Line,
|
||||
select: [
|
||||
{
|
||||
aggFn: 'count' as const,
|
||||
|
|
@ -582,7 +591,11 @@ function HttpTab({
|
|||
'error_requests',
|
||||
]}
|
||||
config={{
|
||||
...source,
|
||||
...pick(source, [
|
||||
'timestampValueExpression',
|
||||
'connection',
|
||||
'from',
|
||||
]),
|
||||
where: appliedConfig.where || '',
|
||||
whereLanguage: appliedConfig.whereLanguage || 'sql',
|
||||
select: [
|
||||
|
|
@ -703,7 +716,11 @@ function HttpTab({
|
|||
'error_count',
|
||||
]}
|
||||
config={{
|
||||
...source,
|
||||
...pick(source, [
|
||||
'timestampValueExpression',
|
||||
'connection',
|
||||
'from',
|
||||
]),
|
||||
where: appliedConfig.where || '',
|
||||
whereLanguage: appliedConfig.whereLanguage || 'sql',
|
||||
select: [
|
||||
|
|
@ -1122,7 +1139,11 @@ function DatabaseTab({
|
|||
'p50_duration_ns',
|
||||
]}
|
||||
config={{
|
||||
...source,
|
||||
...pick(source, [
|
||||
'timestampValueExpression',
|
||||
'connection',
|
||||
'from',
|
||||
]),
|
||||
where: appliedConfig.where || '',
|
||||
whereLanguage: appliedConfig.whereLanguage || 'sql',
|
||||
dateRange: searchedTimeRange,
|
||||
|
|
@ -1198,7 +1219,11 @@ function DatabaseTab({
|
|||
'p50_duration_ns',
|
||||
]}
|
||||
config={{
|
||||
...source,
|
||||
...pick(source, [
|
||||
'timestampValueExpression',
|
||||
'connection',
|
||||
'from',
|
||||
]),
|
||||
where: appliedConfig.where || '',
|
||||
whereLanguage: appliedConfig.whereLanguage || 'sql',
|
||||
dateRange: searchedTimeRange,
|
||||
|
|
@ -1292,7 +1317,11 @@ function ErrorsTab({
|
|||
<DBTimeChart
|
||||
sourceId={source.id}
|
||||
config={{
|
||||
...source,
|
||||
...pick(source, [
|
||||
'timestampValueExpression',
|
||||
'connection',
|
||||
'from',
|
||||
]),
|
||||
where: appliedConfig.where || '',
|
||||
whereLanguage: appliedConfig.whereLanguage || 'sql',
|
||||
displayType: DisplayType.StackedBar,
|
||||
|
|
@ -1341,13 +1370,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 ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -1357,11 +1407,19 @@ function ServicesDashboardPage() {
|
|||
id: watch('source'),
|
||||
});
|
||||
|
||||
// Update the `source` query parameter if the appliedConfig source changes
|
||||
useEffect(() => {
|
||||
if (sourceId && !appliedConfig.source) {
|
||||
setAppliedConfig({ source: sourceId });
|
||||
if (
|
||||
appliedConfig.source &&
|
||||
appliedConfig.source !== appliedConfigParams.source
|
||||
) {
|
||||
setAppliedConfigParams({ source: appliedConfig.source });
|
||||
}
|
||||
}, [appliedConfig.source, setAppliedConfig, sourceId]);
|
||||
}, [
|
||||
appliedConfig.source,
|
||||
appliedConfigParams.source,
|
||||
setAppliedConfigParams,
|
||||
]);
|
||||
|
||||
const DEFAULT_INTERVAL = 'Past 1h';
|
||||
const [displayedTimeInputValue, setDisplayedTimeInputValue] =
|
||||
|
|
@ -1374,7 +1432,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,
|
||||
|
|
@ -1385,30 +1443,38 @@ 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
|
||||
// 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 (
|
||||
<Box p="sm">
|
||||
|
|
@ -1554,7 +1620,7 @@ const ServicesDashboardPageDynamic = dynamic(
|
|||
},
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
// @ts-expect-error Next.js layout typing
|
||||
ServicesDashboardPageDynamic.getLayout = withAppNav;
|
||||
|
||||
export default ServicesDashboardPageDynamic;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useCallback, useMemo } from 'react';
|
||||
import { pick } from 'lodash';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import type { Filter } from '@hyperdx/common-utils/dist/types';
|
||||
import { Drawer, Grid, Group, Text } from '@mantine/core';
|
||||
|
|
@ -97,7 +98,11 @@ export default function ServiceDashboardDbQuerySidePanel({
|
|||
sourceId={sourceId}
|
||||
hiddenSeries={['total_duration_ns']}
|
||||
config={{
|
||||
...source,
|
||||
...pick(source, [
|
||||
'timestampValueExpression',
|
||||
'connection',
|
||||
'from',
|
||||
]),
|
||||
where: '',
|
||||
whereLanguage: 'sql',
|
||||
select: [
|
||||
|
|
@ -130,7 +135,11 @@ export default function ServiceDashboardDbQuerySidePanel({
|
|||
<DBTimeChart
|
||||
sourceId={sourceId}
|
||||
config={{
|
||||
...source,
|
||||
...pick(source, [
|
||||
'timestampValueExpression',
|
||||
'connection',
|
||||
'from',
|
||||
]),
|
||||
where: '',
|
||||
whereLanguage: 'sql',
|
||||
select: [
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { pick } from 'lodash';
|
||||
import { TSource } from '@hyperdx/common-utils/dist/types';
|
||||
import { Group, Text } from '@mantine/core';
|
||||
|
||||
|
|
@ -95,7 +96,7 @@ export default function ServiceDashboardEndpointPerformanceChart({
|
|||
groupColumn="group"
|
||||
valueColumn="Total Time Spent"
|
||||
config={{
|
||||
...source,
|
||||
...pick(source, ['timestampValueExpression', 'connection', 'from']),
|
||||
where: '',
|
||||
whereLanguage: 'sql',
|
||||
select: [
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useCallback, useMemo } from 'react';
|
||||
import { pick } from 'lodash';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import type { Filter } from '@hyperdx/common-utils/dist/types';
|
||||
import { Drawer, Grid, Group, Text } from '@mantine/core';
|
||||
|
|
@ -104,7 +105,11 @@ export default function ServiceDashboardEndpointSidePanel({
|
|||
sourceId={source.id}
|
||||
hiddenSeries={['total_count', 'error_count']}
|
||||
config={{
|
||||
...source,
|
||||
...pick(source, [
|
||||
'timestampValueExpression',
|
||||
'connection',
|
||||
'from',
|
||||
]),
|
||||
where: '',
|
||||
whereLanguage: 'sql',
|
||||
select: [
|
||||
|
|
@ -144,7 +149,11 @@ export default function ServiceDashboardEndpointSidePanel({
|
|||
<DBTimeChart
|
||||
sourceId={source.id}
|
||||
config={{
|
||||
...source,
|
||||
...pick(source, [
|
||||
'timestampValueExpression',
|
||||
'connection',
|
||||
'from',
|
||||
]),
|
||||
where: '',
|
||||
whereLanguage: 'sql',
|
||||
select: [
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { pick } from 'lodash';
|
||||
import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse';
|
||||
import type { Filter, TSource } from '@hyperdx/common-utils/dist/types';
|
||||
import { Box, Code, Group, Text } from '@mantine/core';
|
||||
|
|
@ -30,7 +31,7 @@ export default function SlowestEventsTile({
|
|||
|
||||
const { data, isLoading, isError, error } = useQueriedChartConfig(
|
||||
{
|
||||
...source,
|
||||
...pick(source, ['timestampValueExpression', 'connection', 'from']),
|
||||
where: '',
|
||||
whereLanguage: 'sql',
|
||||
select: [
|
||||
|
|
@ -109,7 +110,11 @@ export default function SlowestEventsTile({
|
|||
breadcrumbPath={[{ label: 'Endpoint' }]}
|
||||
sourceId={source.id}
|
||||
config={{
|
||||
...source,
|
||||
...pick(source, [
|
||||
'timestampValueExpression',
|
||||
'connection',
|
||||
'from',
|
||||
]),
|
||||
where: '',
|
||||
whereLanguage: 'sql',
|
||||
select: [
|
||||
|
|
|
|||
|
|
@ -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<string, string> | 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<string, string> | 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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue