From 8e38d835a1142358c49b854cbf863c4bb46613a8 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Mon, 6 Oct 2025 15:16:51 -0600 Subject: [PATCH 01/16] initial wip for heatmap customization --- packages/app/src/DBSearchPage.tsx | 64 ++---- .../app/src/components/DBHeatmapChart.tsx | 195 ++++++++++++++---- .../Search/DBSearchHeatmapChart.tsx | 138 +++++++++++++ 3 files changed, 302 insertions(+), 95 deletions(-) create mode 100644 packages/app/src/components/Search/DBSearchHeatmapChart.tsx diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx index d8e3708cb5..5c2d5c61b8 100644 --- a/packages/app/src/DBSearchPage.tsx +++ b/packages/app/src/DBSearchPage.tsx @@ -43,6 +43,7 @@ import { Flex, Grid, Group, + Input, Menu, Modal, Paper, @@ -55,6 +56,7 @@ import { useDocumentVisibility, } from '@mantine/hooks'; import { notifications } from '@mantine/notifications'; +import { IconPlayCard, IconPlayerPlay } from '@tabler/icons-react'; import { useIsFetching } from '@tanstack/react-query'; import CodeMirror from '@uiw/react-codemirror'; @@ -101,6 +103,7 @@ import { QUERY_LOCAL_STORAGE, useLocalStorage, usePrevious } from '@/utils'; import { SQLPreview } from './components/ChartSQLPreview'; import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar'; import PatternTable from './components/PatternTable'; +import { DBSearchHeatmapChart } from './components/Search/DBSearchHeatmapChart'; import SourceSchemaPreview from './components/SourceSchemaPreview'; import { useTableMetadata } from './hooks/useMetadata'; import { useSqlSuggestions } from './hooks/useSqlSuggestions'; @@ -1567,57 +1570,16 @@ function DBSearchPage() { )} {analysisMode === 'delta' && searchedSource != null && ( - -
- { - setOutlierSqlCondition( - [ - `${searchedSource.durationExpression} >= ${yMin} * 1e${(searchedSource.durationPrecision ?? 9) - 3}`, - `${searchedSource.durationExpression} <= ${yMax} * 1e${(searchedSource.durationPrecision ?? 9) - 3}`, - `${getFirstTimestampValueExpression(chartConfig.timestampValueExpression)} >= ${xMin}`, - `${getFirstTimestampValueExpression(chartConfig.timestampValueExpression)} <= ${xMax}`, - ].join(' AND '), - ); - }} - /> -
- {outlierSqlCondition ? ( - - ) : ( - -
- - Please highlight an outlier range in the heatmap to - view the delta chart. - -
-
- )} -
+ )}
{analysisMode === 'results' && diff --git a/packages/app/src/components/DBHeatmapChart.tsx b/packages/app/src/components/DBHeatmapChart.tsx index 8b594aaa7d..1ae8a95365 100644 --- a/packages/app/src/components/DBHeatmapChart.tsx +++ b/packages/app/src/components/DBHeatmapChart.tsx @@ -259,7 +259,14 @@ const opt: uPlot.Options = { type HeatmapChartConfig = { displayType: DisplayType.Heatmap; - select: [{ aggFn: 'heatmap'; valueExpression: string }]; + select: [ + { + aggFn: 'heatmap'; + valueExpression: string; + countExpression: string; + groupExpression: string; + }, + ]; from: ChartConfigWithDateRange['from']; where: ChartConfigWithDateRange['where']; dateRange: ChartConfigWithDateRange['dateRange']; @@ -286,27 +293,73 @@ function HeatmapContainer({ const nBuckets = 80; const valueExpression = config.select[0].valueExpression; + const countExpression = config.select[0].countExpression; + const groupExpression = config.select[0].groupExpression; + + // When valueExpression is an aggregate like count(), we need to use a CTE + // to calculate it first before finding min/max + const isAggregateExpression = + valueExpression.includes('count(') || + valueExpression.includes('countIf(') || + valueExpression.includes('countDistinct(') || + valueExpression.includes('sum(') || + valueExpression.includes('avg(') || + valueExpression.includes('distinct'); + + const minMaxConfig: ChartConfigWithDateRange = isAggregateExpression + ? { + ...config, + orderBy: undefined, + granularity: undefined, + select: [ + { + aggFn: 'min', + // TODO: Select if we can be negative + aggCondition: `value_calc >= 0`, + aggConditionLanguage: 'sql', + valueExpression: 'value_calc', + alias: 'min', + }, + { + aggFn: 'max', + valueExpression: 'value_calc', + alias: 'max', + }, + ], + with: [ + { + name: 'pre_calc', + chartConfig: { + ...config, + select: [{ valueExpression, alias: 'value_calc' }], + orderBy: undefined, + }, + }, + ], + timestampValueExpression: '__hdx_time_bucket', + from: { databaseName: '', tableName: 'pre_calc' }, + } + : { + ...config, + orderBy: undefined, + granularity: undefined, + select: [ + { + aggFn: 'min', + valueExpression, + // TODO: Select if we can be negative + aggCondition: `${valueExpression} >= 0`, + aggConditionLanguage: 'sql', + alias: 'min', + }, + { aggFn: 'max', valueExpression, aggCondition: '', alias: 'max' }, + ], + }; - const minMaxConfig: ChartConfigWithDateRange = { - ...config, - orderBy: undefined, - granularity: undefined, - select: [ - { - aggFn: 'min', - valueExpression, - // TODO: Select if we can be negative - aggCondition: `${valueExpression} >= 0`, - aggConditionLanguage: 'sql', - alias: 'min', - }, - { aggFn: 'max', valueExpression, aggCondition: '', alias: 'max' }, - ], - }; const { data: minMaxData, isLoading: isMinMaxLoading, - isError: isMinMaxError, + error: minMaxError, } = useQueriedChartConfig(minMaxConfig, { queryKey: ['heatmap', minMaxConfig], enabled: enabled, @@ -316,25 +369,68 @@ function HeatmapContainer({ const min = Number.parseInt(minMaxData?.data?.[0]?.['min'] ?? '0', 10); const max = Number.parseInt(minMaxData?.data?.[0]?.['max'] ?? '0', 10); - const bucketConfig: ChartConfigWithDateRange = { - ...config, - select: [ - { aggFn: 'count', valueExpression: '', aggCondition: '', alias: 'count' }, - ], - groupBy: [ - { - // TODO: ChSQL? - valueExpression: `widthBucket(${valueExpression}, ${min}, ${max}, ${nBuckets})`, - alias: 'bucket', - }, - ], - orderBy: [{ valueExpression: 'bucket', ordering: 'ASC' }], - granularity, - }; + const bucketConfig: ChartConfigWithDateRange = isAggregateExpression + ? { + ...config, + select: [ + { + valueExpression: 'sum(value_count)', + alias: 'count', + }, + ], + groupBy: [ + { + valueExpression: `widthBucket(value_calc, ${min}, ${max}, ${nBuckets})`, + alias: 'x_bucket', + }, + ], + with: [ + { + name: 'pre_calc', + chartConfig: { + ...config, + select: [ + { valueExpression, alias: 'value_calc' }, + { + valueExpression: countExpression, + alias: 'value_count', + }, + { valueExpression: groupExpression, alias: 'group_by' }, + ], + groupBy: [ + { valueExpression: groupExpression, alias: 'group_by' }, + ], + granularity, + orderBy: undefined, + }, + }, + ], + timestampValueExpression: '__hdx_time_bucket', + from: { databaseName: '', tableName: 'pre_calc' }, + orderBy: [{ valueExpression: 'x_bucket', ordering: 'ASC' }], + granularity, + } + : { + ...config, + select: [ + { + valueExpression: countExpression, + alias: 'count', // error rate per user (errors/users) + }, + ], + groupBy: [ + { + valueExpression: `widthBucket(${valueExpression}, ${min}, ${max}, ${nBuckets})`, + alias: 'x_bucket', + }, + ], + orderBy: [{ valueExpression: 'x_bucket', ordering: 'ASC' }], + granularity, + }; - const { data, isLoading, isError } = useQueriedChartConfig(bucketConfig, { + const { data, isLoading, error } = useQueriedChartConfig(bucketConfig, { queryKey: ['heatmap_bucket', bucketConfig], - enabled: !!minMaxData && bucketConfig != null, + enabled: min != null && max != null && bucketConfig != null, }); const generatedTsBuckets = timeBucketByGranularity( @@ -342,12 +438,12 @@ function HeatmapContainer({ dateRange[1], granularity, ); - const timestampColumn = inferTimestampColumn(data?.meta ?? []); - const time: number[] = []; - const bucket: number[] = []; - const count: number[] = []; + const timestampColumn = inferTimestampColumn(data?.meta ?? []); + const time: number[] = []; // x values + const bucket: number[] = []; // y value series 1 + const count: number[] = []; // y value series 2 if (data != null && timestampColumn != null) { let dataIndex = 0; @@ -362,10 +458,10 @@ function HeatmapContainer({ if ( row != null && new Date(row[timestampColumn.name]).getTime() == generatedTs && - row['bucket'] == j + row['x_bucket'] == j ) { time.push(new Date(row[timestampColumn.name]).getTime()); - bucket.push(min + row['bucket'] * ((max - min) / nBuckets)); + bucket.push(min + row['x_bucket'] * ((max - min) / nBuckets)); count.push(Number.parseInt(row['count'], 10)); // UInt64 returns as string dataIndex++; @@ -387,6 +483,16 @@ function HeatmapContainer({ ); } + if (error || minMaxError) { + return ( + + + Error loading heatmap. Try expanding your search criteria. + + + ); + } + if (time.length < 2 || generatedTsBuckets?.length < 2) { return ( @@ -397,6 +503,7 @@ function HeatmapContainer({ ); } + return ( @@ -721,10 +828,10 @@ function Heatmap({
- Duration: {tickFormatter(highlightedPoint.yVal)} + Y Value: {tickFormatter(highlightedPoint.yVal)}
- Count:{' '} + Count Value:{' '} {new Intl.NumberFormat('en-US', { notation: 'standard', compactDisplay: 'short', diff --git a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx new file mode 100644 index 0000000000..31644487f0 --- /dev/null +++ b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx @@ -0,0 +1,138 @@ +import { parseAsString, useQueryState, useQueryStates } from 'nuqs'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { + ChartConfigWithDateRange, + DisplayType, + SourceKind, + TSource, +} from '@hyperdx/common-utils/dist/types'; +import { Flex, InputLabel } from '@mantine/core'; +import { Input } from '@mantine/core'; +import { ActionIcon } from '@mantine/core'; +import { Paper } from '@mantine/core'; +import { Center } from '@mantine/core'; +import { Text } from '@mantine/core'; +import { IconPlayerPlay } from '@tabler/icons-react'; + +import { + getDurationMsExpression, + getFirstTimestampValueExpression, +} from '@/source'; + +import DBDeltaChart from '../DBDeltaChart'; +import DBHeatmapChart from '../DBHeatmapChart'; + +// countExpression: "countDistinct(SpanAttributes['userId'])", +// valueExpression: "countIf(StatusCode = 'Error')", +// groupExpression: "SpanAttributes['userId']", + +const Schema = z.object({ + groupBy: z.string(), + value: z.string(), + count: z.string(), +}); +export function DBSearchHeatmapChart({ + chartConfig, + source, +}: { + chartConfig: ChartConfigWithDateRange; + source: TSource; +}) { + const [fields, setFields] = useQueryStates({ + groupBy: parseAsString.withDefault(''), + value: parseAsString.withDefault(getDurationMsExpression(source)), + count: parseAsString.withDefault('count()'), + outlierSqlCondition: parseAsString, + }); + + const form = useForm({ + resolver: zodResolver(Schema), + reValidateMode: 'onSubmit', + defaultValues: { + groupBy: fields.groupBy, + value: fields.value, + count: fields.count, + }, + }); + + const onSubmit = (data: z.infer) => { + setFields({ + groupBy: data.groupBy, + value: data.value, + count: data.count, + }); + }; + + // return
Hi
; + return ( + // + +
+ + Value + + Count + + Group by + + + + + +
+
+ { + setFields({ + outlierSqlCondition: [ + `${source.durationExpression} >= ${yMin} * 1e${(source.durationPrecision ?? 9) - 3}`, + `${source.durationExpression} <= ${yMax} * 1e${(source.durationPrecision ?? 9) - 3}`, + `${getFirstTimestampValueExpression(chartConfig.timestampValueExpression)} >= ${xMin}`, + `${getFirstTimestampValueExpression(chartConfig.timestampValueExpression)} <= ${xMax}`, + ].join(' AND '), + }); + }} + /> +
+ + {fields.outlierSqlCondition ? ( + + ) : ( + +
+ + Please highlight an outlier range in the heatmap to view the delta + chart. + +
+
+ )} +
+ ); +} From cf93991f18b50d5a0ee8c8b4e6eadfe4d5e6578a Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Tue, 7 Oct 2025 10:59:27 -0600 Subject: [PATCH 02/16] improve error ui --- .../app/src/components/DBHeatmapChart.tsx | 78 ++++++++++++++++--- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/packages/app/src/components/DBHeatmapChart.tsx b/packages/app/src/components/DBHeatmapChart.tsx index 1ae8a95365..043c1bbaca 100644 --- a/packages/app/src/components/DBHeatmapChart.tsx +++ b/packages/app/src/components/DBHeatmapChart.tsx @@ -3,11 +3,24 @@ import dynamic from 'next/dynamic'; import type { Plugin } from 'uplot'; import uPlot from 'uplot'; import UplotReact from 'uplot-react'; -import { inferTimestampColumn } from '@hyperdx/common-utils/dist/clickhouse'; +import { + ClickHouseQueryError, + inferTimestampColumn, +} from '@hyperdx/common-utils/dist/clickhouse'; import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types'; import { DisplayType } from '@hyperdx/common-utils/dist/types'; -import { Divider, Paper, Text } from '@mantine/core'; -import { useElementSize } from '@mantine/hooks'; +import { + Box, + Button, + Code, + Divider, + Group, + Modal, + Paper, + Text, +} from '@mantine/core'; +import { useDisclosure, useElementSize } from '@mantine/hooks'; +import { IconArrowsDiagonal } from '@tabler/icons-react'; import { convertDateRangeToGranularityString, @@ -18,6 +31,8 @@ import { Chart, NumberFormat } from '@/types'; import { FormatTime } from '@/useFormatTime'; import { formatNumber } from '@/utils'; +import { SQLPreview } from './ChartSQLPreview'; + type Mode2DataArray = [number[], number[], number[]]; // From: https://github.com/leeoniya/uPlot/blob/a4edb297a9b80baf781f4d05a40fb52fae737bff/demos/latency-heatmap.html#L436 @@ -328,7 +343,7 @@ function HeatmapContainer({ ], with: [ { - name: 'pre_calc', + name: 'min_max_calc', chartConfig: { ...config, select: [{ valueExpression, alias: 'value_calc' }], @@ -337,7 +352,7 @@ function HeatmapContainer({ }, ], timestampValueExpression: '__hdx_time_bucket', - from: { databaseName: '', tableName: 'pre_calc' }, + from: { databaseName: '', tableName: 'min_max_calc' }, } : { ...config, @@ -365,6 +380,8 @@ function HeatmapContainer({ enabled: enabled, }); + const [errorModal, errorModalControls] = useDisclosure(); + // UInt64 are returned as strings const min = Number.parseInt(minMaxData?.data?.[0]?.['min'] ?? '0', 10); const max = Number.parseInt(minMaxData?.data?.[0]?.['max'] ?? '0', 10); @@ -386,7 +403,7 @@ function HeatmapContainer({ ], with: [ { - name: 'pre_calc', + name: 'bucket_calc', chartConfig: { ...config, select: [ @@ -406,7 +423,7 @@ function HeatmapContainer({ }, ], timestampValueExpression: '__hdx_time_bucket', - from: { databaseName: '', tableName: 'pre_calc' }, + from: { databaseName: '', tableName: 'bucket_calc' }, orderBy: [{ valueExpression: 'x_bucket', ordering: 'ASC' }], granularity, } @@ -430,7 +447,7 @@ function HeatmapContainer({ const { data, isLoading, error } = useQueriedChartConfig(bucketConfig, { queryKey: ['heatmap_bucket', bucketConfig], - enabled: min != null && max != null && bucketConfig != null, + enabled: minMaxData != null && bucketConfig != null, }); const generatedTsBuckets = timeBucketByGranularity( @@ -484,11 +501,50 @@ function HeatmapContainer({ ); } if (error || minMaxError) { + const _error: Error = error || minMaxError!; return ( - - - Error loading heatmap. Try expanding your search criteria. + + + Error loading chart, please check your query or try again later. + + errorModalControls.close()} + title="Error Details" + > + + + Error Message: + + + {_error.message} + + {_error instanceof ClickHouseQueryError && ( + <> + + Sent Query: + + + + )} + + ); } From 73097b6ec35d83678c4415a6cd84b8d70cc3e945 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Tue, 7 Oct 2025 12:40:53 -0600 Subject: [PATCH 03/16] make group by optional, fix issue when where is added --- packages/app/src/components/DBHeatmapChart.tsx | 10 ++++++++-- .../app/src/components/Search/DBSearchHeatmapChart.tsx | 10 ++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/app/src/components/DBHeatmapChart.tsx b/packages/app/src/components/DBHeatmapChart.tsx index 043c1bbaca..f4ddd67949 100644 --- a/packages/app/src/components/DBHeatmapChart.tsx +++ b/packages/app/src/components/DBHeatmapChart.tsx @@ -324,6 +324,7 @@ function HeatmapContainer({ const minMaxConfig: ChartConfigWithDateRange = isAggregateExpression ? { ...config, + where: '', orderBy: undefined, granularity: undefined, select: [ @@ -389,6 +390,7 @@ function HeatmapContainer({ const bucketConfig: ChartConfigWithDateRange = isAggregateExpression ? { ...config, + where: '', select: [ { valueExpression: 'sum(value_count)', @@ -412,10 +414,14 @@ function HeatmapContainer({ valueExpression: countExpression, alias: 'value_count', }, - { valueExpression: groupExpression, alias: 'group_by' }, + ...(groupExpression + ? [{ valueExpression: groupExpression, alias: 'group_by' }] + : []), ], groupBy: [ - { valueExpression: groupExpression, alias: 'group_by' }, + ...(groupExpression + ? [{ valueExpression: groupExpression, alias: 'group_by' }] + : []), ], granularity, orderBy: undefined, diff --git a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx index 31644487f0..2c62c41151 100644 --- a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx +++ b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx @@ -1,11 +1,10 @@ -import { parseAsString, useQueryState, useQueryStates } from 'nuqs'; +import { parseAsString, useQueryStates } from 'nuqs'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { ChartConfigWithDateRange, DisplayType, - SourceKind, TSource, } from '@hyperdx/common-utils/dist/types'; import { Flex, InputLabel } from '@mantine/core'; @@ -24,15 +23,12 @@ import { import DBDeltaChart from '../DBDeltaChart'; import DBHeatmapChart from '../DBHeatmapChart'; -// countExpression: "countDistinct(SpanAttributes['userId'])", -// valueExpression: "countIf(StatusCode = 'Error')", -// groupExpression: "SpanAttributes['userId']", - const Schema = z.object({ groupBy: z.string(), value: z.string(), count: z.string(), }); + export function DBSearchHeatmapChart({ chartConfig, source, @@ -65,9 +61,7 @@ export function DBSearchHeatmapChart({ }); }; - // return
Hi
; return ( - //
From 8e287c210be59acf04e8cb5c50b91d56d259f905 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Tue, 7 Oct 2025 12:51:04 -0600 Subject: [PATCH 04/16] improve form validation --- .../Search/DBSearchHeatmapChart.tsx | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx index 2c62c41151..99c356e7d9 100644 --- a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx +++ b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx @@ -7,8 +7,7 @@ import { DisplayType, TSource, } from '@hyperdx/common-utils/dist/types'; -import { Flex, InputLabel } from '@mantine/core'; -import { Input } from '@mantine/core'; +import { Flex, TextInput } from '@mantine/core'; import { ActionIcon } from '@mantine/core'; import { Paper } from '@mantine/core'; import { Center } from '@mantine/core'; @@ -24,9 +23,9 @@ import DBDeltaChart from '../DBDeltaChart'; import DBHeatmapChart from '../DBHeatmapChart'; const Schema = z.object({ - groupBy: z.string(), - value: z.string(), - count: z.string(), + groupBy: z.string().trim().optional(), + value: z.string().trim().min(1), + count: z.string().trim().min(1), }); export function DBSearchHeatmapChart({ @@ -65,16 +64,31 @@ export function DBSearchHeatmapChart({ - Value - - Count - - Group by - + + + + + From c839915d8c9052c50281424678acebdbf679e2b9 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Tue, 7 Oct 2025 13:29:54 -0600 Subject: [PATCH 05/16] hide form in collapse section --- .../Search/DBSearchHeatmapChart.tsx | 157 ++++++++++++------ 1 file changed, 104 insertions(+), 53 deletions(-) diff --git a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx index 99c356e7d9..a37f1de0e2 100644 --- a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx +++ b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx @@ -7,12 +7,13 @@ import { DisplayType, TSource, } from '@hyperdx/common-utils/dist/types'; -import { Flex, TextInput } from '@mantine/core'; +import { Box, Button, Collapse, Flex, TextInput } from '@mantine/core'; import { ActionIcon } from '@mantine/core'; import { Paper } from '@mantine/core'; import { Center } from '@mantine/core'; import { Text } from '@mantine/core'; -import { IconPlayerPlay } from '@tabler/icons-react'; +import { useDisclosure } from '@mantine/hooks'; +import { IconPlayerPlay, IconSettings } from '@tabler/icons-react'; import { getDurationMsExpression, @@ -35,6 +36,8 @@ export function DBSearchHeatmapChart({ chartConfig: ChartConfigWithDateRange; source: TSource; }) { + const [opened, { toggle }] = useDisclosure(false); + const [fields, setFields] = useQueryStates({ groupBy: parseAsString.withDefault(''), value: parseAsString.withDefault(getDurationMsExpression(source)), @@ -42,59 +45,53 @@ export function DBSearchHeatmapChart({ outlierSqlCondition: parseAsString, }); - const form = useForm({ - resolver: zodResolver(Schema), - reValidateMode: 'onSubmit', - defaultValues: { - groupBy: fields.groupBy, - value: fields.value, - count: fields.count, - }, - }); - - const onSubmit = (data: z.infer) => { - setFields({ - groupBy: data.groupBy, - value: data.value, - count: data.count, - }); - }; - return ( - - - - - - - - - - + + + { + setFields({ + groupBy: data.groupBy, + value: data.value, + count: data.count, + }); + }} + /> + + + - -
+ +
+ {/*
+ +
*/}
- {fields.outlierSqlCondition ? ( ); } + +function DBSearchHeatmapForm({ + defaultValues, + onSubmit, +}: { + defaultValues: z.infer; + onSubmit: (v: z.infer) => void; +}) { + const form = useForm({ + resolver: zodResolver(Schema), + defaultValues, + }); + + return ( +
+ + + + + + + {/* mb below is to align the play button with the text inputs */} + + + + +
+ ); +} From 5d453341b527562454704cf1ade932e9c2ed322b Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Tue, 7 Oct 2025 14:00:49 -0600 Subject: [PATCH 06/16] use inline sql editor --- .../Search/DBSearchHeatmapChart.tsx | 80 ++++++++++++------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx index a37f1de0e2..7e9862c991 100644 --- a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx +++ b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx @@ -2,12 +2,16 @@ import { parseAsString, useQueryStates } from 'nuqs'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; +import { + TableConnection, + tcFromSource, +} from '@hyperdx/common-utils/dist/metadata'; import { ChartConfigWithDateRange, DisplayType, TSource, } from '@hyperdx/common-utils/dist/types'; -import { Box, Button, Collapse, Flex, TextInput } from '@mantine/core'; +import { Box, Button, Collapse, Flex, Grid } from '@mantine/core'; import { ActionIcon } from '@mantine/core'; import { Paper } from '@mantine/core'; import { Center } from '@mantine/core'; @@ -22,6 +26,7 @@ import { import DBDeltaChart from '../DBDeltaChart'; import DBHeatmapChart from '../DBHeatmapChart'; +import { SQLInlineEditorControlled } from '../SQLInlineEditor'; const Schema = z.object({ groupBy: z.string().trim().optional(), @@ -50,6 +55,7 @@ export function DBSearchHeatmapChart({ ; onSubmit: (v: z.infer) => void; }) { @@ -155,43 +163,57 @@ function DBSearchHeatmapForm({ return (
- - - + + - + + - + + - - {/* mb below is to align the play button with the text inputs */} - - - - + + + + + + +
); } From 46c333a0ada7848bcc02bad0bf2f6bc2b37bf73c Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Tue, 7 Oct 2025 14:05:21 -0600 Subject: [PATCH 07/16] allow onsubmit via enter --- packages/app/src/components/Search/DBSearchHeatmapChart.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx index 7e9862c991..57111bdac9 100644 --- a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx +++ b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx @@ -171,7 +171,7 @@ function DBSearchHeatmapForm({ name="value" placeholder="SQL expression" language="sql" - enableHotkey + onSubmit={form.handleSubmit(onSubmit)} label="Value" error={form.formState.errors.value?.message} rules={{ required: true }} @@ -184,7 +184,7 @@ function DBSearchHeatmapForm({ name="count" placeholder="SQL expression" language="sql" - enableHotkey + onSubmit={form.handleSubmit(onSubmit)} label="Count" error={form.formState.errors.count?.message} rules={{ required: true }} @@ -197,7 +197,7 @@ function DBSearchHeatmapForm({ name="groupBy" placeholder="SQL expression" language="sql" - enableHotkey + onSubmit={form.handleSubmit(onSubmit)} label="Group by" error={form.formState.errors.groupBy?.message} /> From 6c9060a257867b8f016790e3b0a5d9b95c653aa3 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Tue, 7 Oct 2025 14:12:23 -0600 Subject: [PATCH 08/16] small cleanup --- packages/app/src/components/DBHeatmapChart.tsx | 4 ++-- packages/app/src/components/Search/DBSearchHeatmapChart.tsx | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/app/src/components/DBHeatmapChart.tsx b/packages/app/src/components/DBHeatmapChart.tsx index f4ddd67949..a545407c43 100644 --- a/packages/app/src/components/DBHeatmapChart.tsx +++ b/packages/app/src/components/DBHeatmapChart.tsx @@ -438,7 +438,7 @@ function HeatmapContainer({ select: [ { valueExpression: countExpression, - alias: 'count', // error rate per user (errors/users) + alias: 'count', }, ], groupBy: [ @@ -453,7 +453,7 @@ function HeatmapContainer({ const { data, isLoading, error } = useQueriedChartConfig(bucketConfig, { queryKey: ['heatmap_bucket', bucketConfig], - enabled: minMaxData != null && bucketConfig != null, + enabled: !!minMaxData && bucketConfig != null, }); const generatedTsBuckets = timeBucketByGranularity( diff --git a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx index 57111bdac9..92221bbca8 100644 --- a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx +++ b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx @@ -95,9 +95,6 @@ export function DBSearchHeatmapChart({ position: 'relative', }} > - {/*
- -
*/} Date: Tue, 7 Oct 2025 14:29:00 -0600 Subject: [PATCH 09/16] cleanup config object --- packages/app/src/DBSearchPage.tsx | 9 ++------- .../app/src/components/Search/DBSearchHeatmapChart.tsx | 6 ++++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx index 5812fbd75e..db4dc3afdc 100644 --- a/packages/app/src/DBSearchPage.tsx +++ b/packages/app/src/DBSearchPage.tsx @@ -56,14 +56,11 @@ import { useDocumentVisibility, } from '@mantine/hooks'; import { notifications } from '@mantine/notifications'; -import { IconPlayCard, IconPlayerPlay } from '@tabler/icons-react'; import { useIsFetching } from '@tanstack/react-query'; import { SortingState } from '@tanstack/react-table'; import CodeMirror from '@uiw/react-codemirror'; import { ContactSupportText } from '@/components/ContactSupportText'; -import DBDeltaChart from '@/components/DBDeltaChart'; -import DBHeatmapChart from '@/components/DBHeatmapChart'; import { DBSearchPageFilters } from '@/components/DBSearchPageFilters'; import { DBTimeChart } from '@/components/DBTimeChart'; import { ErrorBoundary } from '@/components/ErrorBoundary'; @@ -90,7 +87,6 @@ import { import { useSearchPageFilterState } from '@/searchFilters'; import SearchInputV2 from '@/SearchInputV2'; import { - getDurationMsExpression, getFirstTimestampValueExpression, useSource, useSources, @@ -1593,10 +1589,9 @@ function DBSearchPage() { chartConfig={{ ...chartConfig, dateRange: searchedTimeRange, - // displayType: DisplayType.Heatmap, - // granularity: 'auto', - // with: aliasWith, + with: aliasWith, }} + isReady={isReady} source={searchedSource} /> )} diff --git a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx index 92221bbca8..fdbae2db79 100644 --- a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx +++ b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx @@ -37,9 +37,11 @@ const Schema = z.object({ export function DBSearchHeatmapChart({ chartConfig, source, + isReady, }: { chartConfig: ChartConfigWithDateRange; source: TSource; + isReady: boolean; }) { const [opened, { toggle }] = useDisclosure(false); @@ -109,7 +111,7 @@ export function DBSearchHeatmapChart({ granularity: 'auto', displayType: DisplayType.Heatmap, }} - enabled={true} + enabled={isReady} onFilter={(xMin, xMax, yMin, yMax) => { setFields({ outlierSqlCondition: [ @@ -126,7 +128,7 @@ export function DBSearchHeatmapChart({ From be14aabb3a73d4407df22d342328df9d50046d40 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Tue, 7 Oct 2025 14:36:32 -0600 Subject: [PATCH 10/16] clean up function call --- .../app/src/components/DBHeatmapChart.tsx | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/app/src/components/DBHeatmapChart.tsx b/packages/app/src/components/DBHeatmapChart.tsx index a545407c43..31125e2ecc 100644 --- a/packages/app/src/components/DBHeatmapChart.tsx +++ b/packages/app/src/components/DBHeatmapChart.tsx @@ -10,7 +10,6 @@ import { import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types'; import { DisplayType } from '@hyperdx/common-utils/dist/types'; import { - Box, Button, Code, Divider, @@ -270,8 +269,6 @@ const opt: uPlot.Options = { ], }; -// const d = genData(); - type HeatmapChartConfig = { displayType: DisplayType.Heatmap; select: [ @@ -293,6 +290,23 @@ type HeatmapChartConfig = { with?: ChartConfigWithDateRange['with']; }; +const isAggregateFunction = (value: string) => { + const fns = [ + 'count', + 'countIf', + 'countDistinct', + 'sum', + 'avg', + 'distinct', + 'min', + 'max', + 'quantile', + 'any', + 'none', + ]; + return fns.some(fn => value.includes(fn + '(')); +}; + function HeatmapContainer({ config, enabled = true, @@ -311,15 +325,8 @@ function HeatmapContainer({ const countExpression = config.select[0].countExpression; const groupExpression = config.select[0].groupExpression; - // When valueExpression is an aggregate like count(), we need to use a CTE - // to calculate it first before finding min/max - const isAggregateExpression = - valueExpression.includes('count(') || - valueExpression.includes('countIf(') || - valueExpression.includes('countDistinct(') || - valueExpression.includes('sum(') || - valueExpression.includes('avg(') || - valueExpression.includes('distinct'); + // When valueExpression is an aggregate like count(), we need to use a CTE to calculate the heatmap + const isAggregateExpression = isAggregateFunction(valueExpression); const minMaxConfig: ChartConfigWithDateRange = isAggregateExpression ? { From e5b7077714d2ee500d1903634ce4336e0f4977c0 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Tue, 7 Oct 2025 14:40:13 -0600 Subject: [PATCH 11/16] make count optional --- packages/app/src/components/DBHeatmapChart.tsx | 6 +++--- packages/app/src/components/Search/DBSearchHeatmapChart.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/app/src/components/DBHeatmapChart.tsx b/packages/app/src/components/DBHeatmapChart.tsx index 31125e2ecc..64bcfc8cb1 100644 --- a/packages/app/src/components/DBHeatmapChart.tsx +++ b/packages/app/src/components/DBHeatmapChart.tsx @@ -275,8 +275,8 @@ type HeatmapChartConfig = { { aggFn: 'heatmap'; valueExpression: string; - countExpression: string; - groupExpression: string; + countExpression?: string; + groupExpression?: string; }, ]; from: ChartConfigWithDateRange['from']; @@ -322,7 +322,7 @@ function HeatmapContainer({ const nBuckets = 80; const valueExpression = config.select[0].valueExpression; - const countExpression = config.select[0].countExpression; + const countExpression = config.select[0].countExpression ?? 'count()'; const groupExpression = config.select[0].groupExpression; // When valueExpression is an aggregate like count(), we need to use a CTE to calculate the heatmap diff --git a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx index fdbae2db79..1794a27efe 100644 --- a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx +++ b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx @@ -30,7 +30,7 @@ import { SQLInlineEditorControlled } from '../SQLInlineEditor'; const Schema = z.object({ groupBy: z.string().trim().optional(), - value: z.string().trim().min(1), + value: z.string().trim().optional(), count: z.string().trim().min(1), }); From ee8ef9638c0785bfdfa79adbc041ad04ab528c15 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Tue, 7 Oct 2025 15:13:26 -0600 Subject: [PATCH 12/16] add changeset --- .changeset/many-donkeys-help.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/many-donkeys-help.md diff --git a/.changeset/many-donkeys-help.md b/.changeset/many-donkeys-help.md new file mode 100644 index 0000000000..7d0c91f5e6 --- /dev/null +++ b/.changeset/many-donkeys-help.md @@ -0,0 +1,5 @@ +--- +"@hyperdx/app": patch +--- + +Add ability to customize event deltas heat map y, count, and grouping attributes From 0d73be69c19cce0faafdccba57fa691047c30034 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Fri, 10 Oct 2025 11:26:08 -0600 Subject: [PATCH 13/16] add helperText, improve spacing/button size --- .../app/src/components/SQLInlineEditor.tsx | 16 ++++-- .../Search/DBSearchHeatmapChart.tsx | 54 ++++++++++--------- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/packages/app/src/components/SQLInlineEditor.tsx b/packages/app/src/components/SQLInlineEditor.tsx index c1956d0407..1d6bb3728d 100644 --- a/packages/app/src/components/SQLInlineEditor.tsx +++ b/packages/app/src/components/SQLInlineEditor.tsx @@ -13,7 +13,8 @@ import { Field, TableConnectionChoice, } from '@hyperdx/common-utils/dist/metadata'; -import { Paper, Text } from '@mantine/core'; +import { Flex, Paper, Text, Tooltip } from '@mantine/core'; +import { IconInfoCircle } from '@tabler/icons-react'; import CodeMirror, { Compartment, EditorView, @@ -113,6 +114,7 @@ type SQLInlineEditorProps = { label?: React.ReactNode; disableKeywordAutocomplete?: boolean; enableHotkey?: boolean; + tooltipText?: string; additionalSuggestions?: string[]; queryHistoryType?: string; parentRef?: HTMLElement | null; @@ -159,6 +161,7 @@ export default function SQLInlineEditor({ label, disableKeywordAutocomplete, enableHotkey, + tooltipText, additionalSuggestions = [], queryHistoryType, parentRef, @@ -320,9 +323,16 @@ export default function SQLInlineEditor({ mx="4px" size="xs" fw="bold" - style={{ whiteSpace: 'nowrap' }} + style={{ + whiteSpace: 'nowrap', + }} > - {label} + + + {label} + {tooltipText && } + + )}
diff --git a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx index 1794a27efe..5178f09afc 100644 --- a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx +++ b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx @@ -11,7 +11,7 @@ import { DisplayType, TSource, } from '@hyperdx/common-utils/dist/types'; -import { Box, Button, Collapse, Flex, Grid } from '@mantine/core'; +import { Box, Button, Collapse, Flex } from '@mantine/core'; import { ActionIcon } from '@mantine/core'; import { Paper } from '@mantine/core'; import { Center } from '@mantine/core'; @@ -30,8 +30,8 @@ import { SQLInlineEditorControlled } from '../SQLInlineEditor'; const Schema = z.object({ groupBy: z.string().trim().optional(), - value: z.string().trim().optional(), - count: z.string().trim().min(1), + value: z.string().trim().min(1), + count: z.string().trim().optional(), }); export function DBSearchHeatmapChart({ @@ -53,7 +53,7 @@ export function DBSearchHeatmapChart({ }); return ( - + - - + +
- - +
+
- - +
+
- - - - - - - +
+ + + +
); } From e5014e3d21c249f961c4399f3b4152219b34a3ec Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Fri, 10 Oct 2025 14:16:04 -0600 Subject: [PATCH 14/16] move aggregate function to utility file --- packages/app/src/ChartUtils.tsx | 17 ++++++++++++++++ .../app/src/components/DBHeatmapChart.tsx | 20 ++----------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/app/src/ChartUtils.tsx b/packages/app/src/ChartUtils.tsx index 1f6d64926e..005ce51f4a 100644 --- a/packages/app/src/ChartUtils.tsx +++ b/packages/app/src/ChartUtils.tsx @@ -385,6 +385,23 @@ export function timeBucketByGranularity( return buckets; } +export const isAggregateFunction = (value: string) => { + const fns = [ + 'count', + 'countIf', + 'countDistinct', + 'sum', + 'avg', + 'distinct', + 'min', + 'max', + 'quantile', + 'any', + 'none', + ]; + return fns.some(fn => value.includes(fn + '(')); +}; + export const INTEGER_NUMBER_FORMAT: NumberFormat = { factor: 1, output: 'number', diff --git a/packages/app/src/components/DBHeatmapChart.tsx b/packages/app/src/components/DBHeatmapChart.tsx index 64bcfc8cb1..2862a86768 100644 --- a/packages/app/src/components/DBHeatmapChart.tsx +++ b/packages/app/src/components/DBHeatmapChart.tsx @@ -23,10 +23,11 @@ import { IconArrowsDiagonal } from '@tabler/icons-react'; import { convertDateRangeToGranularityString, + isAggregateFunction, timeBucketByGranularity, } from '@/ChartUtils'; import { useQueriedChartConfig } from '@/hooks/useChartConfig'; -import { Chart, NumberFormat } from '@/types'; +import { NumberFormat } from '@/types'; import { FormatTime } from '@/useFormatTime'; import { formatNumber } from '@/utils'; @@ -290,23 +291,6 @@ type HeatmapChartConfig = { with?: ChartConfigWithDateRange['with']; }; -const isAggregateFunction = (value: string) => { - const fns = [ - 'count', - 'countIf', - 'countDistinct', - 'sum', - 'avg', - 'distinct', - 'min', - 'max', - 'quantile', - 'any', - 'none', - ]; - return fns.some(fn => value.includes(fn + '(')); -}; - function HeatmapContainer({ config, enabled = true, From 978f0410b9523043fed920042dc6921d351fa3e5 Mon Sep 17 00:00:00 2001 From: Brandon Pereira Date: Fri, 10 Oct 2025 15:33:48 -0600 Subject: [PATCH 15/16] feedback pt 1 --- .../app/src/components/DBHeatmapChart.tsx | 10 --- .../Search/DBSearchHeatmapChart.tsx | 65 ++++--------------- 2 files changed, 13 insertions(+), 62 deletions(-) diff --git a/packages/app/src/components/DBHeatmapChart.tsx b/packages/app/src/components/DBHeatmapChart.tsx index 2862a86768..ee0ee1c578 100644 --- a/packages/app/src/components/DBHeatmapChart.tsx +++ b/packages/app/src/components/DBHeatmapChart.tsx @@ -277,7 +277,6 @@ type HeatmapChartConfig = { aggFn: 'heatmap'; valueExpression: string; countExpression?: string; - groupExpression?: string; }, ]; from: ChartConfigWithDateRange['from']; @@ -307,7 +306,6 @@ function HeatmapContainer({ const valueExpression = config.select[0].valueExpression; const countExpression = config.select[0].countExpression ?? 'count()'; - const groupExpression = config.select[0].groupExpression; // When valueExpression is an aggregate like count(), we need to use a CTE to calculate the heatmap const isAggregateExpression = isAggregateFunction(valueExpression); @@ -405,14 +403,6 @@ function HeatmapContainer({ valueExpression: countExpression, alias: 'value_count', }, - ...(groupExpression - ? [{ valueExpression: groupExpression, alias: 'group_by' }] - : []), - ], - groupBy: [ - ...(groupExpression - ? [{ valueExpression: groupExpression, alias: 'group_by' }] - : []), ], granularity, orderBy: undefined, diff --git a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx index 5178f09afc..406802bcb6 100644 --- a/packages/app/src/components/Search/DBSearchHeatmapChart.tsx +++ b/packages/app/src/components/Search/DBSearchHeatmapChart.tsx @@ -29,7 +29,6 @@ import DBHeatmapChart from '../DBHeatmapChart'; import { SQLInlineEditorControlled } from '../SQLInlineEditor'; const Schema = z.object({ - groupBy: z.string().trim().optional(), value: z.string().trim().min(1), count: z.string().trim().optional(), }); @@ -43,10 +42,7 @@ export function DBSearchHeatmapChart({ source: TSource; isReady: boolean; }) { - const [opened, { toggle }] = useDisclosure(false); - const [fields, setFields] = useQueryStates({ - groupBy: parseAsString.withDefault(''), value: parseAsString.withDefault(getDurationMsExpression(source)), count: parseAsString.withDefault('count()'), outlierSqlCondition: parseAsString, @@ -55,39 +51,19 @@ export function DBSearchHeatmapChart({ return ( - - { - setFields({ - groupBy: data.groupBy, - value: data.value, - count: data.count, - }); - }} - /> - - - - + { + setFields({ + value: data.value, + count: data.count, + }); + }} + />
-
- -
Date: Fri, 10 Oct 2025 15:42:18 -0600 Subject: [PATCH 16/16] fix bug where delta chart not shown if only inliers exist --- packages/app/src/components/DBDeltaChart.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/DBDeltaChart.tsx b/packages/app/src/components/DBDeltaChart.tsx index cd6966f9ba..3e1f9adb72 100644 --- a/packages/app/src/components/DBDeltaChart.tsx +++ b/packages/app/src/components/DBDeltaChart.tsx @@ -334,7 +334,12 @@ export default function DBDeltaChart({ const { percentageOccurences: inlierValueOccurences } = getPropertyStatistics(inlierData?.data ?? []); - const sortedProperties = Array.from(outlierValueOccurences.keys()) + // Get all the unique keys from both maps, and process them to get the merged arrays + const uniqueKeys = new Set([ + ...outlierValueOccurences.keys(), + ...inlierValueOccurences.keys(), + ]); + const sortedProperties = Array.from(uniqueKeys) .map(key => { const inlierCount = inlierValueOccurences.get(key) ?? new Map();