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
7 changes: 7 additions & 0 deletions .changeset/mcp-shared-pattern-predicate.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { minePatterns } from '@hyperdx/common-utils/dist/drain';

import {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 minor — Tests for the common-utils predicate now sit in the api package, so common-utils' own suite doesn't cover them

Move the templateToRegex and buildPatternPredicate cases into packages/common-utils/src/__tests__/pattern-predicate.test.ts and keep only the andSql tests here. Today that file only tests extra delimiters and parsePatternPredicate. These cases cover escaping, quotes and backslashes, Unicode whitespace, empty templates and MAX_PREDICATE_TEMPLATE_LENGTH. Because they import the built @hyperdx/common-utils/dist/drain, changing pattern-predicate.ts and running the common-utils tests can't catch a regression in any of them. They also only run against whatever dist was last built.

Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping these here: they pin the exact whereSnippet this tool returns, and the common-utils suite now also covers the length limit and the no-option round trip (04c8a76).

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
Expand Down
43 changes: 0 additions & 43 deletions packages/api/src/mcp/tools/query/patternDrilldown.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,18 @@
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 ' +
'one non-whitespace token at each <*>. Every sampled member of the cluster matches. ' +
'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 | undefined>): string {
const parts = conditions.filter(
Expand Down
8 changes: 5 additions & 3 deletions packages/api/src/mcp/tools/query/runEventPatterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -34,8 +38,6 @@ import {
} from './helpers';
import {
andSql,
buildPatternPredicate,
MAX_PREDICATE_TEMPLATE_LENGTH,
PATTERN_MATCH_SEMANTICS,
userWhereAsSql,
} from './patternDrilldown';
Expand Down
Loading