Skip to content
Open
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
10 changes: 10 additions & 0 deletions .changeset/adhoc-filters-external-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@hyperdx/api': minor
---

feat: Create, read, and update ad hoc dashboard filters through the external API

`/api/v2/dashboards` now accepts and returns `ADHOC` filters, with SQL and
PromQL source types, and their `adhoc` entries in `savedFilterValues`. Each
filter's sources must exist and be a kind its `sourceType` supports. MCP still
hides ad hoc filters.
178 changes: 176 additions & 2 deletions packages/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -959,13 +959,16 @@
"description": "Query language for the where clause."
},
"SavedFilterValue": {
"description": "A single saved dashboard filter selection. Either a rendered SQL condition, or a selection addressed by the name of the dashboard variable it belongs to.\n",
"description": "A single saved dashboard filter selection. Either a rendered SQL condition, a selection addressed by the name of the dashboard variable it belongs to, or the conditions of an ADHOC filter.\n",
"oneOf": [
{
"$ref": "#/components/schemas/SqlSavedFilterValue"
},
{
"$ref": "#/components/schemas/VariableSavedFilterValue"
},
{
"$ref": "#/components/schemas/AdhocSavedFilterValue"
}
]
},
Expand Down Expand Up @@ -1029,6 +1032,78 @@
}
}
},
"AdhocSavedFilterValue": {
"type": "object",
"required": [
"type",
"name",
"conditions"
],
"properties": {
"type": {
"type": "string",
"enum": [
"adhoc"
],
"description": "Filter type.",
"example": "adhoc"
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 1024,
"description": "The variableName of the ADHOC filter these conditions belong to.",
"example": "conditions"
},
"conditions": {
"type": "array",
"maxItems": 100,
"description": "The filter's conditions.",
"items": {
"$ref": "#/components/schemas/AdhocFilterCondition"
}
}
}
},
"AdhocFilterCondition": {
"type": "object",
"description": "One \"key operator value\" condition. Conditions are AND'd, except that IN (sql) and =| (promql) conditions sharing a key are OR'd into one.\n",
"required": [
"key",
"operator",
"value"
],
"properties": {
"key": {
"type": "string",
"minLength": 1,
"maxLength": 10000,
"description": "A SQL expression (column or map access) for sql filters, or a label name for promql filters.",
"example": "ServiceName"
},
"operator": {
"type": "string",
"enum": [
"=",
"!=",
"=~",
"!~",
"LIKE",
"NOT LIKE",
"IN",
"=|"
],
"description": "Comparison operator. =~ and !~ match a regular expression. LIKE, NOT LIKE, and IN apply to sql filters only, and =| to promql filters only; a condition its filter's sourceType can't express is not applied.\n",
"example": "="
},
"value": {
"type": "string",
"maxLength": 10000,
"description": "Value, pattern, or regular expression to compare against.",
"example": "api"
}
}
},
"MetricDataType": {
"type": "string",
"enum": [
Expand Down Expand Up @@ -3951,14 +4026,18 @@
},
{
"$ref": "#/components/schemas/PrometheusLabelFilterInput"
},
{
"$ref": "#/components/schemas/AdhocFilterInput"
}
],
"discriminator": {
"propertyName": "type",
"mapping": {
"QUERY_EXPRESSION": "#/components/schemas/QueryExpressionFilterInput",
"STATIC_LIST": "#/components/schemas/StaticListFilterInput",
"PROMETHEUS_LABEL": "#/components/schemas/PrometheusLabelFilterInput"
"PROMETHEUS_LABEL": "#/components/schemas/PrometheusLabelFilterInput",
"ADHOC": "#/components/schemas/AdhocFilterInput"
}
}
},
Expand Down Expand Up @@ -4262,6 +4341,101 @@
}
}
},
"AdhocFilterInput": {
"type": "object",
"description": "A filter holding free-form \"key operator value\" conditions, with keys\nand values suggested from its sources: columns and map keys for sql\nsources, labels for promql sources. Its conditions are always\nbroadcast to the tiles it applies to: as WHERE conditions on builder\ntiles and raw sql tiles using the $__filters macro, or as label\nmatchers on PromQL tiles. Its conditions are saved in\nsavedFilterValues as an \"adhoc\" entry. Ad hoc filters are a preview\nfeature.\n",
"required": [
"type",
"name",
"sourceType",
"sources"
],
"properties": {
"type": {
"type": "string",
"enum": [
"ADHOC"
],
"description": "Discriminator. Must be \"ADHOC\".",
"example": "ADHOC"
},
"name": {
"type": "string",
"minLength": 1,
"description": "Display name for the dashboard filter",
"example": "Conditions"
},
"sourceType": {
"type": "string",
"enum": [
"sql",
"promql"
],
"description": "The kind of sources the filter reads keys from and applies to: Log,\nTrace, or Session sources for sql, PromQL sources for promql.\n",
"example": "sql"
},
"sources": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
},
"description": "IDs of the sources whose keys and values the filter suggests, all of sourceType.",
"example": [
"65f5e4a3b9e77c001a111111"
]
},
"where": {
"type": "string",
"description": "Optional condition narrowing the rows keys and values are suggested\nfrom. sql filters only; never applied to tiles. May reference the\ndashboard's variables.\n",
"example": "ServiceName:api"
},
"whereLanguage": {
"type": "string",
"enum": [
"sql",
"lucene"
],
"description": "Language of the where condition",
"example": "lucene"
},
"appliesToSourceIds": {
"type": "array",
"items": {
"type": "string"
},
"description": "IDs of the sources whose tiles the conditions apply to, all of\nsourceType. Omit or send an empty array to apply to the tiles of\nthe filter's sources.\n",
"example": [
"65f5e4a3b9e77c001a111111"
]
},
"isBroadcastEnabled": {
"type": "boolean",
"enum": [
true
],
"default": true,
"description": "Must be true or omitted.",
"example": true
},
"isVariableEnabled": {
"type": "boolean",
"enum": [
true
],
"default": true,
"description": "Must be true or omitted.",
"example": true
},
"variableName": {
"type": "string",
"maxLength": 64,
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
"description": "Unique name the filter's conditions are saved under, in\nsavedFilterValues and the dashboard URL. Tiles can't reference it\nas a variable. Defaults to the display name with whitespace\nreplaced by underscores and remaining illegal characters removed.\nMust be unique across a dashboard's variable-enabled filters.\n",
"example": "conditions"
}
}
},
"Filter": {
"allOf": [
{
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/mcp/tools/dashboards/getDashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export function registerGetDashboard({
}
const externalDashboard = convertToExternalDashboard(dashboard, {
hidePromql: !isPromqlEnabled(context),
hideAdhoc: true,
});
return {
content: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export function registerGetDashboardTile({

const externalDashboard = convertToExternalDashboard(dashboard, {
hidePromql: !isPromqlEnabled(context),
hideAdhoc: true,
});
const tile = externalDashboard.tiles.find(t => t.id === tileId);
if (!tile) {
Expand Down
6 changes: 3 additions & 3 deletions packages/api/src/mcp/tools/dashboards/patchDashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
isConfigTile,
validateDashboardTiles,
} from '@/routers/external-api/v2/utils/dashboards';
import { getExternalCompatibleFilterValues } from '@/utils/externalApi';
import { getMcpCompatibleFilterValues } from '@/utils/externalApi';
import type { ExternalDashboardTileWithId } from '@/utils/zod';
import { externalDashboardTileSchemaWithId } from '@/utils/zod';

Expand Down Expand Up @@ -127,7 +127,7 @@ export function registerPatchDashboard({
// Convert only for the error message (read-only, no write-back).
const externalDashboard = convertToExternalDashboard(
existingDashboard,
{ hidePromql: !promqlEnabled },
{ hidePromql: !promqlEnabled, hideAdhoc: true },
);
return mcpUserError(
`Tile not found: ${tileId}. Available tile IDs: ${externalDashboard.tiles.map(t => `${t.id} (${t.name})`).join(', ')}`,
Expand Down Expand Up @@ -271,7 +271,7 @@ export function registerPatchDashboard({
tags: updatedDashboard.tags,
...(savedFilterValues !== undefined
? {
savedFilterValues: getExternalCompatibleFilterValues(
savedFilterValues: getMcpCompatibleFilterValues(
updatedDashboard.savedFilterValues,
),
}
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/mcp/tools/dashboards/queryTile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export function registerQueryTile({

const externalDashboard = convertToExternalDashboard(dashboard, {
hidePromql: !isPromqlEnabled(context),
hideAdhoc: true,
});
const tile = externalDashboard.tiles.find(t => t.id === tileId);
if (!tile) {
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/mcp/tools/dashboards/queryTiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export function registerQueryTiles({

const externalDashboard = convertToExternalDashboard(dashboard, {
hidePromql: !isPromqlEnabled(context),
hideAdhoc: true,
});
const allTiles = externalDashboard.tiles;

Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/mcp/tools/dashboards/saveDashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ async function createDashboard({

const externalDashboard = convertToExternalDashboard(newDashboard, {
hidePromql: !promqlEnabled,
hideAdhoc: true,
});
return {
content: [
Expand Down Expand Up @@ -452,6 +453,7 @@ async function updateDashboard({

const externalDashboard = convertToExternalDashboard(updatedDashboard, {
hidePromql: !promqlEnabled,
hideAdhoc: true,
});
return {
content: [
Expand Down
5 changes: 2 additions & 3 deletions packages/api/src/mcp/tools/dashboards/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
TIME_RANGE_MACROS,
} from '@hyperdx/common-utils/dist/macros';
import {
DashboardFilterValue,
DisplayType,
QueryExpressionDashboardFilter,
SavedChartConfig,
Expand All @@ -36,8 +37,6 @@ import {
} from '@/routers/external-api/v2/utils/dashboards';
import type { ExternalDashboardTileWithId } from '@/utils/zod';

import type { McpSavedFilterValues } from './schemas';

/**
* Returns one entry per raw SQL tile that uses a source-dependent macro but
* does not set a `sourceId`, recording which macro(s) triggered it so callers
Expand Down Expand Up @@ -253,7 +252,7 @@ export function getFilterVariableWarnings(
* applies. Warn so the agent can fix the name or add the filter.
*/
export function getSavedFilterValueWarnings(
savedFilterValues: McpSavedFilterValues | undefined,
savedFilterValues: DashboardFilterValue[] | undefined,
filters: FilterForVariableDeclaration[] | undefined,
): string[] {
if (!savedFilterValues?.length) return [];
Expand Down
Loading
Loading