Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mcp-promql-shared-chart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperdx/api': patch
---

fix: MCP `query_tile` returns PromQL table and histogram heatmap tiles in the rows the dashboard draws
17 changes: 6 additions & 11 deletions packages/api/src/controllers/promql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
buildQueryId,
} from '@hyperdx/common-utils/dist/clickhouse';
import { getCurrentQueryAttribution } from '@hyperdx/common-utils/dist/clickhouse/node';
import type { PromqlQuery } from '@hyperdx/common-utils/dist/core/promqlChart';
import { SourceKind, type SQLInterval } from '@hyperdx/common-utils/dist/types';
import { z } from 'zod';

Expand Down Expand Up @@ -520,16 +521,6 @@ export async function resolvePromqlQueryBackend(
return resolveClickhouseQueryBackend(target);
}

export type PromqlQuery =
| {
kind: 'range';
expr: string;
startSec: number;
endSec: number;
stepSec: number;
}
| { kind: 'instant'; expr: string; timeSec: number };

const sampleSchema = z.tuple([z.number(), z.string()]);
const metricSchema = z.record(z.string());

Expand Down Expand Up @@ -559,7 +550,11 @@ function promqlQueryParams(query: PromqlQuery): Record<string, string> {
end: String(query.endSec),
step: String(query.stepSec),
}
: { query: query.expr, time: String(query.timeSec) };
: {
query: query.expr,
time: String(query.timeSec),
...(query.limit != null ? { limit: String(query.limit) } : {}),
};
}

async function evaluatePromqlQuery(
Expand Down
60 changes: 56 additions & 4 deletions packages/api/src/mcp/tools/query/__tests__/promqlTile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,30 @@ describe('buildPromqlTileQueries', () => {
]);
});

it("caps an instant table query's series at the table's row cap", () => {
const [{ query }] = buildPromqlTileQueries({
config: {
displayType: DisplayType.Table,
promqlExpression: [{ expression: 'up', queryType: 'instant' }],
},
dateRange,
});

expect(query).toMatchObject({ kind: 'instant', limit: 10_000 });
});

it("sizes a heatmap's auto step for the heatmap's finer buckets", () => {
const [{ query }] = buildPromqlTileQueries({
config: {
displayType: DisplayType.Heatmap,
promqlExpression: [{ expression: 'up' }],
},
dateRange,
});

expect(query).toMatchObject({ kind: 'range', stepSec: 15 });
});

it('runs an instant expression on a time chart as a range query', () => {
const [{ query }] = buildPromqlTileQueries({
config: {
Expand Down Expand Up @@ -237,7 +261,7 @@ describe('toPromqlTileResult', () => {
expect(result.data).toEqual([{ series_name: 'up', value: 3 }]);
});

it('leaves out the time bucket for an instant table', () => {
it('gives an instant table a column per label and no time column', () => {
const result = toPromqlTileResult(
{
displayType: DisplayType.Table,
Expand All @@ -256,9 +280,37 @@ describe('toPromqlTileResult', () => {
);

expect(result.data).toEqual([
{ series_name: '{instance="checkout-0"}', value: 1 },
{ series_name: '{instance="checkout-1"}', value: 0 },
{ instance: 'checkout-0', job: 'checkout', Value: 1 },
{ instance: 'checkout-1', job: 'checkout', Value: 0 },
]);
expect(result.meta.map(m => m.name)).toEqual(['instance', 'job', 'Value']);
// An undefined key would break trimToolResponse on large results.
expect(result).not.toHaveProperty('seriesLabels');
});

it('keys a histogram heatmap by bucket', () => {
const result = toPromqlTileResult(
{
displayType: DisplayType.Heatmap,
promqlExpression: 'sum by (le) (rate(x_bucket[5m]))',
heatmap: { mode: 'histogram' },
},
[
{
expression: 'sum by (le) (rate(x_bucket[5m]))',
isBucketed: true,
result: [
{ metric: { le: '0.5' }, values: [[t0, 2]] },
{ metric: { le: '+Inf' }, values: [[t0, 3]] },
],
},
],
);

expect(result.data).toEqual([
{ __hdx_time_bucket: '2026-11-02T10:00:00.000Z', le: '0.5', value: 2 },
{ __hdx_time_bucket: '2026-11-02T10:00:00.000Z', le: '+Inf', value: 3 },
]);
expect(result.meta.map(m => m.name)).toEqual(['series_name', 'value']);
expect(result).not.toHaveProperty('seriesLabels');
});
});
4 changes: 4 additions & 0 deletions packages/api/src/mcp/tools/query/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
} from '@clickhouse/client-common';
import { getHeatmapMode } from '@hyperdx/common-utils/dist/core/heatmap';
import { getMetadata } from '@hyperdx/common-utils/dist/core/metadata';
import { PromqlChartError } from '@hyperdx/common-utils/dist/core/promqlChart';
import {
convertToCategoricalChartConfig,
getFirstTimestampValueExpression,
Expand Down Expand Up @@ -727,6 +728,9 @@ export async function runConfigTile(
? mcpUserError(e.message)
: mcpServerError(e.message);
}
if (e instanceof PromqlChartError) {
Comment thread
karl-power marked this conversation as resolved.
Resolved
return mcpUserError(e.message);
}
// The table-function backend throws ClickHouse errors as they come.
return clickHouseErrorResult(e);
}
Expand Down
Loading
Loading