From ba08dfed138342f0261a02977b65bf28339259d0 Mon Sep 17 00:00:00 2001 From: Harshul Rathod Date: Mon, 28 Sep 2026 22:08:57 +0530 Subject: [PATCH 1/3] fix(common-utils): count distinct attribute values on gauge metric charts --- .../gauge-count-distinct-value-expression.md | 12 +++++ .../src/__tests__/renderChartConfig.test.ts | 48 +++++++++++++++++++ .../src/core/renderChartConfig.ts | 8 +++- 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 .changeset/gauge-count-distinct-value-expression.md diff --git a/.changeset/gauge-count-distinct-value-expression.md b/.changeset/gauge-count-distinct-value-expression.md new file mode 100644 index 0000000000..98ec43087f --- /dev/null +++ b/.changeset/gauge-count-distinct-value-expression.md @@ -0,0 +1,12 @@ +--- +'@hyperdx/common-utils': patch +'@hyperdx/app': patch +'@hyperdx/api': patch +--- + +fix: count distinct attribute values on gauge metric charts + +`count_distinct` on a gauge metric ignored its value expression and counted the +distinct metric values instead, so counting `Attributes['name']` on an info +metric returned 1 or 2. It now counts the given expression. The default `Value` +expression and every other aggregation still use the bucketed metric value. diff --git a/packages/common-utils/src/__tests__/renderChartConfig.test.ts b/packages/common-utils/src/__tests__/renderChartConfig.test.ts index 2264838536..c24f054638 100644 --- a/packages/common-utils/src/__tests__/renderChartConfig.test.ts +++ b/packages/common-utils/src/__tests__/renderChartConfig.test.ts @@ -273,6 +273,54 @@ describe('renderChartConfig', () => { expect(actual).toMatchSnapshot(); }); + describe('count_distinct on a gauge metric', () => { + const renderGaugeSelect = async (select: { + aggFn?: string; + valueExpression: string; + }) => + parameterizedQueryToSql( + await renderChartConfig( + { + ...gaugeConfiguration, + select: [ + { + aggFn: 'count_distinct', + metricName: 'argocd_app_info', + metricType: MetricsDataType.Gauge, + ...select, + }, + ], + }, + mockMetadata, + querySettings, + ), + ); + + it('counts the distinct values of an attribute expression', async () => { + const sql = await renderGaugeSelect({ + valueExpression: "Attributes['name']", + }); + expect(sql).toContain("count(DISTINCT Attributes['name'])"); + expect(sql).not.toContain('count(DISTINCT LastValue)'); + }); + + it('still counts the bucketed metric value for the default Value expression', async () => { + const sql = await renderGaugeSelect({ valueExpression: 'Value' }); + expect(sql).toContain('count(DISTINCT LastValue)'); + }); + + it('keeps other aggregations on the bucketed metric value', async () => { + const sql = await renderGaugeSelect({ + aggFn: 'max', + valueExpression: "Attributes['name']", + }); + expect(sql).toMatch( + /SELECT max\(\s*toFloat64OrDefault\(toString\(LastValue\)\)/, + ); + expect(sql).not.toContain("Attributes['name']"); + }); + }); + it('should generate sql for a single sum metric', async () => { const config: ChartConfigWithOptDateRange = { displayType: DisplayType.Line, diff --git a/packages/common-utils/src/core/renderChartConfig.ts b/packages/common-utils/src/core/renderChartConfig.ts index 70dab887c4..5168bb737c 100644 --- a/packages/common-utils/src/core/renderChartConfig.ts +++ b/packages/common-utils/src/core/renderChartConfig.ts @@ -2101,7 +2101,13 @@ async function translateMetricChartConfig( select: [ { ..._select, - valueExpression: 'LastValue', + // count_distinct may count an attribute, which Bucketed still carries + valueExpression: + _select.aggFn === 'count_distinct' && + _select.valueExpression && + _select.valueExpression !== 'Value' + ? _select.valueExpression + : 'LastValue', aggCondition: '', // clear up the condition since the where clause is already applied at the upstream CTE }, ], From 45a02749574c7e0133a706c8317b671e762c481d Mon Sep 17 00:00:00 2001 From: Harshul Rathod Date: Thu, 1 Oct 2026 13:29:43 +0530 Subject: [PATCH 2/3] fix(common-utils): keep LastValue for count_distinct expressions over Value --- .../common-utils/src/__tests__/renderChartConfig.test.ts | 6 ++++++ packages/common-utils/src/core/renderChartConfig.ts | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/common-utils/src/__tests__/renderChartConfig.test.ts b/packages/common-utils/src/__tests__/renderChartConfig.test.ts index c24f054638..7d69fad1a9 100644 --- a/packages/common-utils/src/__tests__/renderChartConfig.test.ts +++ b/packages/common-utils/src/__tests__/renderChartConfig.test.ts @@ -309,6 +309,12 @@ describe('renderChartConfig', () => { expect(sql).toContain('count(DISTINCT LastValue)'); }); + it('still counts the bucketed metric value for an expression over Value', async () => { + const sql = await renderGaugeSelect({ valueExpression: 'Value * 100' }); + expect(sql).toContain('count(DISTINCT LastValue)'); + expect(sql).not.toContain('Value * 100'); + }); + it('keeps other aggregations on the bucketed metric value', async () => { const sql = await renderGaugeSelect({ aggFn: 'max', diff --git a/packages/common-utils/src/core/renderChartConfig.ts b/packages/common-utils/src/core/renderChartConfig.ts index 5168bb737c..2b76eab533 100644 --- a/packages/common-utils/src/core/renderChartConfig.ts +++ b/packages/common-utils/src/core/renderChartConfig.ts @@ -2101,11 +2101,12 @@ async function translateMetricChartConfig( select: [ { ..._select, - // count_distinct may count an attribute, which Bucketed still carries + // count_distinct may count an attribute, which Bucketed still carries; + // Bucketed has no Value column, only LastValue valueExpression: _select.aggFn === 'count_distinct' && _select.valueExpression && - _select.valueExpression !== 'Value' + !/\bValue\b/.test(_select.valueExpression) ? _select.valueExpression : 'LastValue', aggCondition: '', // clear up the condition since the where clause is already applied at the upstream CTE From 71d977c1ae9a3ec6d168d4f5f7ebfdc921aae4f2 Mon Sep 17 00:00:00 2001 From: Harshul Rathod Date: Thu, 1 Oct 2026 13:39:54 +0530 Subject: [PATCH 3/3] fix(common-utils): ignore quoted text when checking for the Value column --- .../common-utils/src/__tests__/renderChartConfig.test.ts | 7 +++++++ packages/common-utils/src/core/renderChartConfig.ts | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/common-utils/src/__tests__/renderChartConfig.test.ts b/packages/common-utils/src/__tests__/renderChartConfig.test.ts index 7d69fad1a9..ed1b95fdfc 100644 --- a/packages/common-utils/src/__tests__/renderChartConfig.test.ts +++ b/packages/common-utils/src/__tests__/renderChartConfig.test.ts @@ -315,6 +315,13 @@ describe('renderChartConfig', () => { expect(sql).not.toContain('Value * 100'); }); + it('counts an attribute whose key is Value', async () => { + const sql = await renderGaugeSelect({ + valueExpression: "Attributes['Value']", + }); + expect(sql).toContain("count(DISTINCT Attributes['Value'])"); + }); + it('keeps other aggregations on the bucketed metric value', async () => { const sql = await renderGaugeSelect({ aggFn: 'max', diff --git a/packages/common-utils/src/core/renderChartConfig.ts b/packages/common-utils/src/core/renderChartConfig.ts index 2b76eab533..39529a4416 100644 --- a/packages/common-utils/src/core/renderChartConfig.ts +++ b/packages/common-utils/src/core/renderChartConfig.ts @@ -2106,7 +2106,9 @@ async function translateMetricChartConfig( valueExpression: _select.aggFn === 'count_distinct' && _select.valueExpression && - !/\bValue\b/.test(_select.valueExpression) + !/\bValue\b/.test( + _select.valueExpression.replace(/'(?:[^'\\]|\\.)*'/g, "''"), + ) ? _select.valueExpression : 'LastValue', aggCondition: '', // clear up the condition since the where clause is already applied at the upstream CTE