镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

fix(mcp): make event_patterns drill-down snippets executable - #3426

Open
brandon-pereira wants to merge 8 commits into
mainfrom
brandon/brandon-mcp-event-pattern-tweaks
Open

brandon-pereira wants to merge 8 commits into
mainfrom
brandon/brandon-mcp-event-pattern-tweaks

Conversation

@brandon-pereira

Copy link
Copy Markdown
Member

Summary

clickstack_event_patterns returned a whereSnippet per pattern that agents were told to pass to clickstack_search, but it often matched nothing. A template like Sending <*> rows became Body:"Sending rows" (a phrase that never occurs), and map-field body expressions produced Lucene that failed to parse. Agents hit dead ends on the main drill-down path.

Changes:

  • Executable drill-down. whereSnippet is now a SQL match() predicate (whereLanguage: "sql") built from the template: literal tokens in order, one non-whitespace token per <*>, whitespace matching Drain's JS \s tokenization. It already ANDs in the caller's where (rendered to SQL, dashboard variables expanded) and the source's table filter. If the caller filter can't be rendered as SQL, summary.drilldown.includesFilters is false with a warning. Templates over 4000 chars get drilldownUnavailable instead of an empty snippet that would match everything.
  • Source table filter. Pattern mining (event_patterns, emerging_signals, dashboard pattern tiles) now applies the log source's tableFilterExpression. Counts can drop for sources that set one.
  • bodyExpression also accepts JSONExtractString(<column or map ref>, '<key>', ...). It is rebuilt from validated args, so the injection allowlist still holds.
  • Sampling transparency. Response reports sampleCoverage, and summary.trend gives the real bucket interval and boundaries (granularity rounding means fewer or wider buckets than requested). Tool description and usage text explain that counts are extrapolated and a 0 bucket doesn't mean no events.
  • minePatterns template bug. Each cluster now reports its own template. Before, it re-matched the first member's body and could pick up a different same-length cluster's template that other members didn't fit, so the drill-down missed some of the pattern's own events.

How to reproduce

Starting point: yarn dev, MCP client connected to the local API, a log source with data.

On main:

  1. Call clickstack_event_patterns on a log source.
  2. Pass a pattern containing <*> as where to clickstack_search. It returns no rows, or a parse error when bodyExpression is a map field.

On this branch:

  1. Same call. Each pattern has a SQL whereSnippet and whereLanguage: "sql".
  2. Pass it to clickstack_search with whereLanguage: "sql". It returns that pattern's events.

Covered by new unit tests (patternDrilldown.test.ts, mine-patterns.test.ts, query.test.ts) and integration tests in queryTool.int.test.ts / queryTile.int.test.ts that run each snippet through search.

References

  • Related PRs:

…icates

whereSnippet is now a SQL match() predicate that keeps token order and
wildcard positions and includes the caller filter and source table filter.
Also: JSONExtractString bodyExpression support, sample coverage and trend
bucket metadata, tableFilterExpression applied to pattern mining, and
minePatterns reports each cluster's own template.
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hyperdx-oss Ready Ready Preview Oct 22, 2026 8:56pm UTC
hyperdx-storybook Ready Ready Preview Oct 22, 2026 8:56pm UTC

Request Review

@changeset-bot

changeset-bot Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e261c8d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@hyperdx/api Patch
@hyperdx/common-utils Patch
@hyperdx/app Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Oct 20, 2026
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 645 production lines changed (Tier 2 max: < 250)
  • Cross-layer change: touches backend (packages/api) + shared utils (packages/common-utils)

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 9
  • Production lines changed: 645 (+ 877 in test files, excluded from tier calculation)
  • Branch: brandon/brandon-mcp-event-pattern-tweaks
  • Author: brandon-pereira

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Duplicate code analysis

2 new clones introduced by this PR.

  • packages/api/src/mcp/tools/query/denoise.ts:85-114 ↔ packages/api/src/mcp/tools/query/runEventPatterns.ts:158-188 — 30 lines
  • packages/api/src/mcp/tools/query/denoise.ts:121-134 ↔ packages/api/src/mcp/tools/query/runEventPatterns.ts:197-209 — 14 lines

If one of these duplicates a helper that already exists, import it instead
of redefining it. If the duplication is deliberate, ignore this comment —
the check is advisory and never fails the build.


jscpd compared this branch against origin/main
across app, api and common-utils. Only new clones are listed; the codebase
currently holds 116 in total (1.43%).
Run yarn dupes locally to reproduce, or yarn dupes:all for the full picture.

Comment thread packages/common-utils/src/drain/mine-patterns.ts
};
}

const tableFilterSql =

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 — Re-implements the table-filter rule that buildSearchChartConfig already owns

buildSearchChartConfig in packages/common-utils/src/core/searchChartConfig.ts:134-137 already holds this rule: for a Log source with tableFilterExpression set, add [{ type: 'sql', condition }] to filters. Its doc says it exists so every path applies the filter the same way. Here, and in andSql(tableFilterSql, …), the rule is restated by hand with kind === SourceKind.Log && 'tableFilterExpression' in source. Pull a shared getSourceTableFilters(source) helper out of searchChartConfig.ts and use it in both places (or at least reuse isLogSource), so the two copies can't drift apart.

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

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

PR Review

If you are a coding agent acting for the author, read this first.
Nothing below blocks merge automatically; a maintainer decides. What they will expect:

  • 🔴 critical or 🟠 major in code this PR adds or changes: fix before asking for review.
  • 🔵 minor in code this PR changes: your call. Fix if small, otherwise reply.
  • Anything about surrounding code, or asking you to widen the change (hoist a helper,
    dedupe with another file, fix other call sites): never fix here, whatever the
    severity. Reply with one sentence; if it is critical, say so plainly so a human sees it.

One commit per review round. After two rounds, stop and ask a maintainer to review
scope rather than addressing more automated findings. Full rule: AGENTS.md.

5 finding(s): 🔴 0 critical · 🟠 0 major · 🔵 5 minor

1 posted as inline comment(s) on the changed lines. 4 listed below.

Findings outside the changed lines

4 minor
  • 🔵 packages/cli/src/cli.tsx:1667 — CLI pattern mining still has the template bug this PR fixes in minePatterns → Around line 1667, cli.tsx still picks each group's template with miner.match(g.sample, 'fallback'). That can land on a different same-length cluster, which is the bug mine-patterns.ts now fixes. Use miner.drain.getCluster(clusterId)?.getTemplate() (the new accessor) here too, or call the shared minePatterns.
  • 🔵 packages/api/src/mcp/tools/query/patternDrilldown.ts:1265 — andSql re-implements the existing joinConditions helper → packages/api/src/tasks/checkAlerts/template.ts:121 already has joinConditions, with the same behaviour: drop blank parts, leave a lone part unwrapped, and wrap each part in parens when joining with AND. Move that helper to a shared module (e.g. packages/api/src/utils/ or common-utils) and import it, rather than adding a second copy.
  • 🔵 packages/api/src/mcp/tools/query/patternDrilldown.ts:1304 — userWhereAsSql copies the Lucene branch of renderWhereExpressionStr → The CustomSchemaSQLSerializerV2 + SearchQueryBuilder(...).build() sequence duplicates renderWhereExpressionStr in packages/common-utils/src/core/renderChartConfig.ts:1277, which renders the mining query's where. Export a small renderLuceneWhereAsSql from common-utils and use it in both places. Otherwise any future serializer option added there (e.g. bodyExpression) will make the drill-down filter differ from the filter the counts were mined with.
  • 🔵 packages/api/src/mcp/tools/query/denoise.ts:990 — Template-matching rules now live in two modules with separate <*> constants → buildNoisyBodyMatcher/tokenize in denoise.ts and templateToRegex in patternDrilldown.ts each define PATTERN_WILDCARD = '<*>' and each implement "same token count, same literal tokens, one token per <*>". Drain's own default paramStr (drain.ts:26) is a third copy of the wildcard. Put the wildcard constant and the token-fit check in one module, ideally next to minePatterns in common-utils, and import them in both places so denoise and drill-down cannot drift apart.

Severity is the reviewer's own estimate and is used for ordering, not filtering. No finding blocks merge automatically; a maintainer decides.

@greptile-apps

greptile-apps Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge, with a non-blocking drill-down issue for long templates.

Fix All in Claude CodeFindings

  1. P2 Long patterns lose drill-down filters ▶

Summary

This PR makes event-pattern drill-down filters executable SQL.

  • Pattern snippets now lead to SQL searches for matching raw events.
  • Pattern mining honors source filters and can read fields inside JSON bodies.
  • Pattern results show how sampling shaped their counts and trends.
  • Each mined pattern now keeps the template from its own cluster.
  • Denoising now matches rows to the most specific pattern they fit.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Events[Sample matching events] --> Clusters[Mine pattern templates]
  Clusters --> Predicate[Build SQL match filter]
  Filters[Caller and source filters] --> Predicate
  Predicate --> Response[Return pattern and whereSnippet]
  Response --> Search[clickstack_search]
Loading

Reviews (7) · Last reviewed commit: "Merge branch 'main' into brandon/brandon..." · Reviewed by Greptile

Comment thread packages/api/src/mcp/tools/query/helpers.ts Outdated
Comment thread packages/api/src/mcp/tools/query/runEventPatterns.ts
Comment thread packages/api/src/mcp/__tests__/queryTool.int.test.ts Outdated
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

E2E Test Results

✅ All tests passed • 344 passed • 1 skipped • 1359s

Status Count
✅ Passed 344
❌ Failed 0
⚠️ Flaky 0
⏭️ Skipped 1

Tests ran across 3 shards in parallel.

View full report →

};
}

const tableFilterSql =

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 — Table-filter resolution duplicates buildSearchChartConfig's tableFilterExpression logic

packages/common-utils/src/core/searchChartConfig.ts:134 already turns a Log source's tableFilterExpression into [{ type: 'sql', condition }] behind the isLogSource guard. It is described as the single source of truth so that every path sees the same row set. Pull that logic into a shared exported helper, for example getSourceTableFilters(source), and call it here, instead of the hand-rolled source.kind === SourceKind.Log && 'tableFilterExpression' in source check. The two copies already differ slightly: one checks != null, the other checks truthiness. (REQUIRED DRY rule.)

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

…plate change

denoise compared minePatterns templates with the template match() returns,
which now differ when match() lands a member on another same-length
cluster. Build the noisy set from the clusters match() returns for noisy
members instead. Also accept doubled-quote JSON keys, count drill-down
filter render failures, and type the event pattern int-test helpers.
Comment thread packages/api/src/mcp/tools/query/runEventPatterns.ts
implicitColumnExpression?: string;
useTextIndexForImplicitColumn?: UseTextIndex;
}): Promise<string> {
if (!where.trim()) return '';

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 — userWhereAsSql re-implements renderWhereExpressionStr, and the two can drift

renderWhereExpressionStr in packages/common-utils/src/core/renderChartConfig.ts:1277 already turns a Lucene where into SQL with CustomSchemaSQLSerializerV2 + SearchQueryBuilder. It also passes bodyExpression and applies the materialized-column rewrite. Mining goes through that function, but the drill-down goes through this copy, so the two SQL renderings of the same filter can differ. Export the common-utils function (or a thin wrapper around it) and call it here instead of building the serializer a second time.

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

Comment thread packages/api/src/mcp/tools/query/denoise.ts Outdated
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found. No P0/P1 ship-blockers. The injection surface (the main risk in this diff) was traced safe: normalizeBodyExpression rebuilds JSONExtractString(...) from per-argument-validated parts so the greedy JSON_EXTRACT_STRING_CALL capture cannot leak extra SQL, and Drain templates are RE2-escaped then escapeSqlString-escaped before embedding in match(), so log content cannot break out of the SQL string literal. The four prior review comments (doubled-apostrophe keys, missing drill-down counter, any test helpers, denoise over-removal) are all addressed in the current diff.

🟡 P2 — recommended

  • packages/api/src/mcp/tools/query/patternDrilldown.ts:43 — templateToRegex and denoise's tokenize re-implement Drain's tokenization (trim + split(/\s+/)) and re-hardcode the <*> wildcard, but nothing couples them to Drain; they are correct only while drainExtraDelimiters stays [], and a future delimiter or glyph change would make drill-down snippets and the noisy-body matcher silently stop matching what mining clustered.
    • Fix: Export a shared wildcard constant and tokenizer from the drain package and import them in patternDrilldown.ts and denoise.ts so the predicate builder and matcher track the real tokenizer.
    • maintainability, project-standards
🔵 P3 nitpicks (7)
  • packages/api/src/mcp/tools/query/runEventPatterns.ts:261 — userWhereSql overloads one string | null | undefined union with three meanings (not requested / render failed / rendered), decodable only by cross-referencing three separate comment blocks.
    • Fix: Model the outcome as a discriminated union so includesUserFilter cannot drift from the where value.
    • maintainability, kieran-typescript
  • packages/api/src/mcp/tools/query/runEventPatterns.ts:414 — the name drilldown denotes two different shapes in one file: the internal { where, includesUserFilter } plumbing and the user-facing summary.drilldown instruction block.
    • Fix: Rename the internal value (e.g. drilldownWhere) to separate plumbing from the response field.
  • packages/api/src/mcp/tools/query/denoise.ts:319 — this PR pushes the file past the project's 300-line limit (283→319) by adding buildNoisyBodyMatcher and tokenize here.
    • Fix: Move the matcher and tokenizer into a sibling module to stay under the limit.
  • packages/api/src/mcp/tools/query/denoise.ts:85 — the ClickHouse client setup and sampleConfig/countConfig construction are near-duplicates of runEventPatterns.ts:158-209 (the two clones the duplication check flagged).
    • Fix: Extract a shared config builder so the pattern-mining query shape lives in one place.
    • maintainability
  • packages/api/src/mcp/tools/query/runEventPatterns.ts:258 — the new if (queriesFailed || abortSignal?.aborted) return null branch that suppresses drill-down telemetry after a query failure has no test; existing tests only exercise the reporting path with successful queries.
    • Fix: Add a case where the sample/count query throws while includeDrilldownWhere is true and assert no drilldown_where_failed telemetry is emitted.
    • testing
  • packages/api/src/mcp/__tests__/dashboards/queryTile.int.test.ts:897 — the new helper destructures JSON.parse(text) untyped, so p.whereSnippet is implicitly any while the sibling helper in queryTool.int.test.ts was upgraded to typed MinedOutput/MinedPattern in this same PR.
    • Fix: Annotate the parsed shape to catch drift, matching the sibling helper.
    • kieran-typescript
  • packages/api/src/mcp/__tests__/queryTool.int.test.ts:685 — the seeded-data trend assertion still uses any for patterns/p, so a retype of pattern.trend[].count would not be caught by the type checker (pre-existing residual of the earlier type-check concern).
    • Fix: Reuse the MinedPattern/trend-bucket type already defined in this file.
    • testing

Reviewers (7): security, testing, maintainability, project-standards, performance, kieran-typescript, previous-comments. The correctness and adversarial reviewers did not return before synthesis; their surface was covered by direct verification (injection payload tracing, Drain-tokenizer consistency, templateToRegex whitespace handling, and the denoise most-specific-fit fix), which surfaced no additional defects.

Testing gaps:

  • No end-to-end drill-down test for an all-wildcard template (e.g. <*> <*>), a behavior change from the old Lucene path that emitted an empty snippet.
  • No unit test asserting denoise.tokenize / templateToRegex agree with Drain's getContentAsTokens on the same input (the implicit coupling above is only exercised indirectly).
  • No unit test asserting buildPatternPredicate output escaping for templates containing a literal ' or \ at the SQL-literal level.

No finding blocks merge automatically. A maintainer will expect P0/P1 findings in code this PR changes to be fixed; P2/P3 are your call -- fix or reply. Never fix findings about surrounding code here; reply instead. Do not widen the PR. How to respond

… reporting after query errors

Matching rows against the clusters that noisy members re-match removed
whole rare clusters (greptile P1). A row is now noisy when its body fits a
noisy template token by token. Also stop counting drill-down render
failures caused by the client closing after the queries failed.
Comment thread packages/api/src/mcp/tools/query/denoise.ts
}

// Drain tokenizes with trim + split(/\s+/).
function tokenize(text: string): string[] {

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 — The 'body fits template' logic now exists in three places, with no shared helper

The rule "same token count, literal tokens equal, any token at <*>" is now implemented three times: tokenize/buildNoisyBodyMatcher in denoise.ts, templateToRegex in patternDrilldown.ts, and the fits() helper in mine-patterns.test.ts. PATTERN_WILDCARD is also declared separately in denoise.ts:19 and patternDrilldown.ts:13. tokenize copies Drain.getContentAsTokens (packages/common-utils/src/drain/drain.ts:104), and '<*>' copies Drain's paramStr. Fix: export a templateFits(template, body) helper and the wildcard constant from @hyperdx/common-utils/dist/drain, built on Drain's tokenizer. Use it in denoise and the test, and take the constant in patternDrilldown, so the three cannot drift apart.

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

};
}

const tableFilterSql =

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 — Rebuilds the table-filter rule that buildSearchChartConfig documents as its single source of truth

buildSearchChartConfig (packages/common-utils/src/core/searchChartConfig.ts:120-137) is documented as the one place that turns source.tableFilterExpression into a SQL Filter, so that every path sees the same rows. This PR writes a second copy, and it differs slightly: it uses a truthiness check where the original uses != null. Pull the table-filter construction out of searchChartConfig.ts into an exported helper (e.g. getSourceTableFilters(source)) and call it here, or build the sample and count configs with buildSearchChartConfig.

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

A broad noisy template like '<*> <*>' removed every same-length row,
including rows of rare specific clusters. Pick the fitting template with
the fewest wildcards and drop the row only if it is noisy. Also test the
drilldownUnavailable branch and the predicate length boundary.
};
}

const tableFilterSql =

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 — Table-filter assembly re-implements buildSearchChartConfig's tableFilterExpression logic

buildSearchChartConfig in packages/common-utils/src/core/searchChartConfig.ts:134-137 already turns a Log source's tableFilterExpression into the [{ type: 'sql', condition }] filter, and its doc comment calls itself the single source of truth so every path sees the same rows. This copy differs already: it uses || undefined where the original uses != null, so an empty string is handled differently. Move the tableFilter computation into an exported helper (e.g. getSourceTableFilters(source)) beside buildSearchChartConfig and call it from both places.

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

};
}

const tableFilterSql =

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 — Table-filter rule copied by hand from buildSearchChartConfig, with a slightly different condition

buildSearchChartConfig (packages/common-utils/src/core/searchChartConfig.ts:134) already defines how tableFilterExpression becomes a SQL filter. Its check is != null, while this copy uses truthiness. Extract a shared getSourceTableFilters(source) from searchChartConfig.ts and use it here, so the deprecated filter is applied the same way on every path.

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

* argument list (which may span lines, hence the `s` flag) for
* `normalizeBodyExpression` to split and validate argument by argument.
*/
const JSON_EXTRACT_STRING_CALL = /^JSONExtractString\((.*)\)$/s;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this stuff feels a bit complex - open to ideas

Comment on lines +58 to +59
if (template.length > MAX_PREDICATE_TEMPLATE_LENGTH) return null;
return `match(${valueExpression}, '${escapeSqlString(templateToRegex(template))}')`;

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.

P2 Long patterns lose drill-down filters

The 4000-character limit checks the template, not the generated SQL. A template with 1500 short tokens can be about 3000 characters long, but repeating the whitespace expression between tokens makes its filter much larger. Once the response exceeds 50 KB, trimToolResponse can replace whereSnippet with an object. The pattern has no drilldownUnavailable warning, but passing its filter to clickstack_search fails.

Bound the generated filter size or preserve executable fields when trimming.

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

This branch was successfully deployed

2 active deployments
Preview – hyperdx-oss — e261c8dc Deployed Oct 22, 2026 by vercel[bot]
Preview – hyperdx-storybook — e261c8dc Deployed Oct 22, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant