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
8 changes: 8 additions & 0 deletions .changeset/bright-patterns-neab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@hyperdx/app": minor
"@hyperdx/api": minor
---

feat: add event patterns as a first-class dashboard tile type

Event patterns can now be created, edited, and saved as dashboard tiles with a dedicated "Pattern Expression" editor. Supported across the UI, MCP server, and External API v2.
47 changes: 46 additions & 1 deletion packages/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1983,6 +1983,47 @@
}
}
},
"EventPatternsChartConfig": {
"type": "object",
"required": [
"displayType",
"sourceId"
],
"description": "Configuration for an event pattern mining tile. Clusters log or trace events by recurring message shapes.",
"properties": {
"displayType": {
"type": "string",
"enum": [
"event_patterns"
],
"description": "Display type discriminator. Must be \"event_patterns\" for pattern mining tiles.",
"example": "event_patterns"
},
"sourceId": {
"type": "string",
"description": "ID of the data source to mine patterns from.",
"example": "65f5e4a3b9e77c001a111111"
},
"select": {
"type": "string",
"maxLength": 10000,
"description": "Column or expression to mine patterns from. Leave empty to use the source default (Body for logs, SpanName for traces).",
"default": "",
"example": "Body"
},
"where": {
"type": "string",
"maxLength": 10000,
"description": "Filter condition for the pattern mining query (syntax depends on whereLanguage).",
"default": "",
"example": "level:error"
},
"whereLanguage": {
"$ref": "#/components/schemas/QueryLanguage",
"description": "Query language for the where clause."
}
}
},
"MarkdownChartConfig": {
"type": "object",
"required": [
Expand Down Expand Up @@ -2495,7 +2536,7 @@
}
},
"TileConfig": {
"description": "Tile chart configuration. displayType is the primary discriminant and determines which variant group applies. For displayTypes that support both builder and Raw SQL modes (line, stacked_bar, table, number, pie), configType is the secondary discriminant: omit it for the builder variant or set it to \"sql\" for the Raw SQL variant. The heatmap, search, and markdown displayTypes only have a builder variant.\n",
"description": "Tile chart configuration. displayType is the primary discriminant and determines which variant group applies. For displayTypes that support both builder and Raw SQL modes (line, stacked_bar, table, number, pie), configType is the secondary discriminant: omit it for the builder variant or set it to \"sql\" for the Raw SQL variant. The heatmap, search, event_patterns, and markdown displayTypes only have a builder variant.\n",
"oneOf": [
{
"$ref": "#/components/schemas/LineChartConfig"
Expand All @@ -2518,6 +2559,9 @@
{
"$ref": "#/components/schemas/SearchChartConfig"
},
{
"$ref": "#/components/schemas/EventPatternsChartConfig"
},
{
"$ref": "#/components/schemas/MarkdownChartConfig"
}
Expand All @@ -2532,6 +2576,7 @@
"pie": "#/components/schemas/PieChartConfig",
"heatmap": "#/components/schemas/HeatmapChartConfig",
"search": "#/components/schemas/SearchChartConfig",
"event_patterns": "#/components/schemas/EventPatternsChartConfig",
"markdown": "#/components/schemas/MarkdownChartConfig"
}
}
Expand Down
29 changes: 29 additions & 0 deletions packages/api/src/mcp/tools/dashboards/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,31 @@ const mcpSearchTileSchema = mcpTileLayoutSchema.extend({
}),
});

const mcpEventPatternsTileSchema = mcpTileLayoutSchema.extend({
config: z.object({
displayType: z
.literal('event_patterns')
.describe('Event pattern mining tile'),
sourceId: z.string().describe('Source ID – call clickstack_list_sources'),
where: z
.string()
.optional()
.default('')
.describe('Filter in Lucene syntax. Example: "level:error"'),
whereLanguage:
SearchConditionTrimmedLanguageSchema.optional().default('lucene'),
select: z
.string()
.optional()
.default('')
.describe(
'Pattern expression — column or expression to mine patterns from. ' +
'Leave empty to use the source default (Body for logs, SpanName for traces). ' +
'Example: "Body", "SpanName", "SpanAttributes[\'http.url\']"',
),
}),
});

const mcpMarkdownTileSchema = mcpTileLayoutSchema.extend({
config: z.object({
displayType: z.literal('markdown').describe('Free-form Markdown text tile'),
Expand Down Expand Up @@ -811,6 +836,7 @@ const mcpTileSchema = z.union([
mcpPieTileSchema,
mcpHeatmapTileSchema,
mcpSearchTileSchema,
mcpEventPatternsTileSchema,
mcpMarkdownTileSchema,
mcpSqlTileSchema,
]);
Expand Down Expand Up @@ -849,6 +875,9 @@ const mcpPatchTileSchema = z.union([
mcpPatchTileLayoutSchema.extend({
config: mcpSearchTileSchema.shape.config,
}),
mcpPatchTileLayoutSchema.extend({
config: mcpEventPatternsTileSchema.shape.config,
}),
mcpPatchTileLayoutSchema.extend({
config: mcpMarkdownTileSchema.shape.config,
}),
Expand Down
22 changes: 22 additions & 0 deletions packages/api/src/mcp/tools/query/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { trimToolResponse } from '@/utils/trimToolResponse';
import type { ExternalDashboardTileWithId } from '@/utils/zod';
import { externalDashboardTileSchemaWithId } from '@/utils/zod';

import { runEventPatterns } from './runEventPatterns';

// ─── Source body expression helpers ──────────────────────────────────────────

export interface SourceBodyFields {
Expand Down Expand Up @@ -404,6 +406,26 @@ export async function runConfigTile(
};
}

// Event-patterns tiles need the Drain pattern-mining pipeline, not the
// generic chart-config SQL renderer. Delegate to the same function the
// standalone clickstack_event_patterns tool uses.
if (builderConfig.displayType === DisplayType.EventPatterns) {
const selectStr =
typeof builderConfig.select === 'string' ? builderConfig.select : '';
return runEventPatterns(
teamId,
builderConfig.source,
startDate,
endDate,
{
where: builderConfig.where ?? '',
whereLanguage:
(builderConfig.whereLanguage as 'lucene' | 'sql') ?? 'lucene',
bodyExpression: selectStr || undefined,
},
);
}

const source = await getSource(teamId, builderConfig.source);
if (!source) {
return mcpUserError(
Expand Down
39 changes: 39 additions & 0 deletions packages/api/src/routers/external-api/__tests__/dashboards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2413,6 +2413,21 @@ describe('External API v2 Dashboards - new format', () => {
},
};

const eventPatternsChart: ExternalDashboardTile = {
name: 'Event Patterns',
x: 18,
y: 3,
w: 6,
h: 3,
config: {
displayType: 'event_patterns',
sourceId: traceSource._id.toString(),
select: 'SpanName',
where: 'level:error',
whereLanguage: 'lucene',
},
};

// Act
const response = await authRequest('post', BASE_URL)
.send({
Expand All @@ -2425,6 +2440,7 @@ describe('External API v2 Dashboards - new format', () => {
markdownChart,
pieChart,
heatmapChart,
eventPatternsChart,
],
tags: ['round-trip-test'],
})
Expand All @@ -2438,6 +2454,9 @@ describe('External API v2 Dashboards - new format', () => {
expect(omit(response.body.data.tiles[4], ['id'])).toEqual(markdownChart);
expect(omit(response.body.data.tiles[5], ['id'])).toEqual(pieChart);
expect(omit(response.body.data.tiles[6], ['id'])).toEqual(heatmapChart);
expect(omit(response.body.data.tiles[7], ['id'])).toEqual(
eventPatternsChart,
);
});

// Schema-level rejections that exercise pure Zod constraints
Expand Down Expand Up @@ -3976,6 +3995,22 @@ describe('External API v2 Dashboards - new format', () => {
},
};

const eventPatternsChart: ExternalDashboardTileWithId = {
id: new ObjectId().toString(),
name: 'Event Patterns',
x: 12,
y: 3,
w: 6,
h: 3,
config: {
displayType: 'event_patterns',
sourceId: traceSource._id.toString(),
select: 'SpanName',
where: 'level:error',
whereLanguage: 'lucene',
},
};

// Create an initial dashboard to update
const initialDashboard = await createTestDashboard();

Expand All @@ -3993,6 +4028,7 @@ describe('External API v2 Dashboards - new format', () => {
numberChart,
markdownChart,
heatmapChart,
eventPatternsChart,
],
tags: ['round-trip-test'],
})
Expand All @@ -4017,6 +4053,9 @@ describe('External API v2 Dashboards - new format', () => {
expect(omit(response.body.data.tiles[5], ['id'])).toEqual(
omit(heatmapChart, ['id']),
);
expect(omit(response.body.data.tiles[6], ['id'])).toEqual(
omit(eventPatternsChart, ['id']),
);
});

it('can round-trip all raw SQL chart config types', async () => {
Expand Down
36 changes: 35 additions & 1 deletion packages/api/src/routers/external-api/v2/dashboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,38 @@ const EXTERNAL_DASHBOARD_PROJECTION = {
* $ref: '#/components/schemas/QueryLanguage'
* description: Query language for the where clause.
*
* EventPatternsChartConfig:
* type: object
* required:
* - displayType
* - sourceId
* description: Configuration for an event pattern mining tile. Clusters log or trace events by recurring message shapes.
* properties:
* displayType:
* type: string
* enum: [event_patterns]
* description: Display type discriminator. Must be "event_patterns" for pattern mining tiles.
* example: "event_patterns"
* sourceId:
* type: string
* description: ID of the data source to mine patterns from.
* example: "65f5e4a3b9e77c001a111111"
* select:
* type: string
* maxLength: 10000
* description: Column or expression to mine patterns from. Leave empty to use the source default (Body for logs, SpanName for traces).
* default: ""
* example: "Body"
* where:
* type: string
* maxLength: 10000
* description: Filter condition for the pattern mining query (syntax depends on whereLanguage).
* default: ""
* example: "level:error"
* whereLanguage:
* $ref: '#/components/schemas/QueryLanguage'
* description: Query language for the where clause.
*
* MarkdownChartConfig:
* type: object
* required:
Expand Down Expand Up @@ -1299,7 +1331,7 @@ const EXTERNAL_DASHBOARD_PROJECTION = {
* both builder and Raw SQL modes (line, stacked_bar, table, number, pie),
* configType is the secondary discriminant: omit it for the builder
* variant or set it to "sql" for the Raw SQL variant. The heatmap,
* search, and markdown displayTypes only have a builder variant.
* search, event_patterns, and markdown displayTypes only have a builder variant.
* oneOf:
* - $ref: '#/components/schemas/LineChartConfig'
* - $ref: '#/components/schemas/BarChartConfig'
Expand All @@ -1308,6 +1340,7 @@ const EXTERNAL_DASHBOARD_PROJECTION = {
* - $ref: '#/components/schemas/PieChartConfig'
* - $ref: '#/components/schemas/HeatmapChartConfig'
* - $ref: '#/components/schemas/SearchChartConfig'
* - $ref: '#/components/schemas/EventPatternsChartConfig'
* - $ref: '#/components/schemas/MarkdownChartConfig'
* discriminator:
* propertyName: displayType
Expand All @@ -1319,6 +1352,7 @@ const EXTERNAL_DASHBOARD_PROJECTION = {
* pie: '#/components/schemas/PieChartConfig'
* heatmap: '#/components/schemas/HeatmapChartConfig'
* search: '#/components/schemas/SearchChartConfig'
* event_patterns: '#/components/schemas/EventPatternsChartConfig'
* markdown: '#/components/schemas/MarkdownChartConfig'
*
* DashboardContainerTab:
Expand Down
18 changes: 18 additions & 0 deletions packages/api/src/routers/external-api/v2/utils/dashboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ const convertToExternalTileChartConfig = (
case DisplayType.Search:
case DisplayType.Markdown:
case DisplayType.Heatmap:
case DisplayType.EventPatterns:
logger.error(
{ config },
'Error converting chart config to external chart - unsupported display type for raw SQL config',
Expand Down Expand Up @@ -420,6 +421,14 @@ const convertToExternalTileChartConfig = (
numberFormat: config.numberFormat,
};
}
case DisplayType.EventPatterns:
Comment thread
brandon-pereira marked this conversation as resolved.
return {
displayType: config.displayType,
sourceId,
select: stringValueOrDefault(config.select, ''),
where: stringValueOrDefault(config.where, ''),
whereLanguage: config.whereLanguage ?? 'lucene',
};
Comment thread
greptile-apps[bot] marked this conversation as resolved.
case undefined:
logger.error(
{ config },
Expand Down Expand Up @@ -788,6 +797,15 @@ export function convertToInternalTileConfig(
whereLanguage: externalConfig.whereLanguage ?? 'lucene',
} satisfies BuilderSavedChartConfig;
break;
case 'event_patterns':
internalConfig = {
...pick(externalConfig, ['select', 'where']),
displayType: DisplayType.EventPatterns,
source: externalConfig.sourceId,
name,
whereLanguage: externalConfig.whereLanguage ?? 'lucene',
} satisfies BuilderSavedChartConfig;
break;
case 'markdown':
internalConfig = {
displayType: DisplayType.Markdown,
Expand Down
9 changes: 9 additions & 0 deletions packages/api/src/utils/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,14 @@ const externalDashboardMarkdownChartConfigSchema = z.object({
markdown: z.string().max(50000).optional(),
});

const externalDashboardEventPatternsChartConfigSchema = z.object({
displayType: z.literal('event_patterns'),
sourceId: objectIdSchema,
select: z.string().max(10000).optional().default(''),
where: z.string().max(10000).optional().default(''),
whereLanguage: whereLanguageSchema,
});

const externalDashboardBuilderTileConfigSchema = z.discriminatedUnion(
'displayType',
[
Expand All @@ -435,6 +443,7 @@ const externalDashboardBuilderTileConfigSchema = z.discriminatedUnion(
externalDashboardHeatmapChartConfigSchema,
externalDashboardMarkdownChartConfigSchema,
externalDashboardSearchChartConfigSchema,
externalDashboardEventPatternsChartConfigSchema,
],
);

Expand Down
Loading
Loading