From e438ddcd9de1f235e1d340fdc61160d857864c91 Mon Sep 17 00:00:00 2001 From: Alex Fedotyev <61838744+alex-fedotyev@users.noreply.github.com> Date: Fri, 13 Nov 2026 22:36:17 +0000 Subject: [PATCH] refactor(mcp): use the shared pattern predicate for drill-downs clickstack_event_patterns now imports templateToRegex, buildPatternPredicate and MAX_PREDICATE_TEMPLATE_LENGTH from @hyperdx/common-utils/dist/drain, and its own copy is deleted. MCP mines without extra delimiters, so the whereSnippet it returns is unchanged. The existing patternDrilldown tests import from common-utils and pass as they are. Part of FR-385. Co-Authored-By: Claude Opus 5.5 --- .changeset/mcp-shared-pattern-predicate.md | 7 +++ .../query/__tests__/patternDrilldown.test.ts | 8 ++-- .../src/mcp/tools/query/patternDrilldown.ts | 43 ------------------- .../src/mcp/tools/query/runEventPatterns.ts | 8 ++-- 4 files changed, 16 insertions(+), 50 deletions(-) create mode 100644 .changeset/mcp-shared-pattern-predicate.md diff --git a/.changeset/mcp-shared-pattern-predicate.md b/.changeset/mcp-shared-pattern-predicate.md new file mode 100644 index 0000000000..4905bd3ff4 --- /dev/null +++ b/.changeset/mcp-shared-pattern-predicate.md @@ -0,0 +1,7 @@ +--- +'@hyperdx/api': patch +--- + +refactor: the MCP `clickstack_event_patterns` tool builds its drill-down +`whereSnippet` with the shared pattern predicate from `@hyperdx/common-utils` +instead of its own copy. The snippet is unchanged. diff --git a/packages/api/src/mcp/tools/query/__tests__/patternDrilldown.test.ts b/packages/api/src/mcp/tools/query/__tests__/patternDrilldown.test.ts index ce69e41d69..490264e45f 100644 --- a/packages/api/src/mcp/tools/query/__tests__/patternDrilldown.test.ts +++ b/packages/api/src/mcp/tools/query/__tests__/patternDrilldown.test.ts @@ -1,11 +1,11 @@ -import { minePatterns } from '@hyperdx/common-utils/dist/drain'; - import { - andSql, buildPatternPredicate, MAX_PREDICATE_TEMPLATE_LENGTH, + minePatterns, templateToRegex, -} from '@/mcp/tools/query/patternDrilldown'; +} from '@hyperdx/common-utils/dist/drain'; + +import { andSql } from '@/mcp/tools/query/patternDrilldown'; // RE2 spells code points \x{HHHH} and categories \pZ; JS (with the u flag) // spells them \u{HHHH} and \p{Z}. ClickHouse behaviour is covered by diff --git a/packages/api/src/mcp/tools/query/patternDrilldown.ts b/packages/api/src/mcp/tools/query/patternDrilldown.ts index 5e85cb6c00..75716db368 100644 --- a/packages/api/src/mcp/tools/query/patternDrilldown.ts +++ b/packages/api/src/mcp/tools/query/patternDrilldown.ts @@ -1,21 +1,11 @@ import type { Metadata } from '@hyperdx/common-utils/dist/core/metadata'; import { renderWhereExpressionStr } from '@hyperdx/common-utils/dist/core/renderChartConfig'; -import { escapeSqlString } from '@hyperdx/common-utils/dist/core/utils'; import type { ChartVariable, UseTextIndex, } from '@hyperdx/common-utils/dist/types'; import { substituteVariables } from '@hyperdx/common-utils/dist/variables'; -const PATTERN_WILDCARD = '<*>'; - -/** - * Templates longer than this get no predicate: the regex (and the SQL that - * embeds it) would be unwieldy, and very long templates are usually a - * single unparameterized payload rather than a reusable shape. - */ -export const MAX_PREDICATE_TEMPLATE_LENGTH = 4000; - export const PATTERN_MATCH_SEMANTICS = 'whereSnippet matches values with the same whitespace-separated token count as the ' + 'template, the same literal tokens (case-sensitive) in the same positions, and exactly ' + @@ -23,39 +13,6 @@ export const PATTERN_MATCH_SEMANTICS = 'It can also match unsampled events whose shape fits, including ones Drain would have ' + 'put in a more specific cluster, so the drill-down is a superset of cluster membership.'; -// Drain tokenizes with JS `\s`, which includes Unicode spaces and BOM; RE2's -// `\s` is ASCII-only. JS `\s` = RE2 `\s` + \v + \pZ (Zs/Zl/Zp) + BOM. `\pZ` -// keeps the snippet short; spelling out the code points repeats ~100 chars -// between every token. -const JS_WHITESPACE = '\\s\\x{0B}\\pZ\\x{FEFF}'; -const WS = `[${JS_WHITESPACE}]`; -const NON_WS = `[^${JS_WHITESPACE}]`; - -function escapeRe2Literal(token: string): string { - return token.replace(/[\\^$.|?*+()[\]{}]/g, '\\$&'); -} - -/** Anchored RE2 regex that a value matches when it fits the Drain template. */ -export function templateToRegex(template: string): string { - const tokens = template.split(/\s+/).filter(t => t.length > 0); - const body = tokens - .map(t => (t === PATTERN_WILDCARD ? `${NON_WS}+` : escapeRe2Literal(t))) - .join(`${WS}+`); - return `^${WS}*${body}${WS}*$`; -} - -/** - * SQL predicate selecting events whose `valueExpression` fits the template, - * or null when the template is too long to turn into a predicate. - */ -export function buildPatternPredicate( - valueExpression: string, - template: string, -): string | null { - if (template.length > MAX_PREDICATE_TEMPLATE_LENGTH) return null; - return `match(${valueExpression}, '${escapeSqlString(templateToRegex(template))}')`; -} - /** AND together SQL conditions, skipping blanks. */ export function andSql(...conditions: Array): string { const parts = conditions.filter( diff --git a/packages/api/src/mcp/tools/query/runEventPatterns.ts b/packages/api/src/mcp/tools/query/runEventPatterns.ts index f92e423956..460192a2ad 100644 --- a/packages/api/src/mcp/tools/query/runEventPatterns.ts +++ b/packages/api/src/mcp/tools/query/runEventPatterns.ts @@ -6,7 +6,11 @@ import { type Granularity, splitAndTrimWithBracket, } from '@hyperdx/common-utils/dist/core/utils'; -import { minePatterns } from '@hyperdx/common-utils/dist/drain'; +import { + buildPatternPredicate, + MAX_PREDICATE_TEMPLATE_LENGTH, + minePatterns, +} from '@hyperdx/common-utils/dist/drain'; import type { ChartConfigWithDateRange, ChartVariable, @@ -34,8 +38,6 @@ import { } from './helpers'; import { andSql, - buildPatternPredicate, - MAX_PREDICATE_TEMPLATE_LENGTH, PATTERN_MATCH_SEMANTICS, userWhereAsSql, } from './patternDrilldown';