From 22109bf3181a33b8cbcdf7dbbfc20807eaa05670 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 14:25:41 +0000 Subject: [PATCH] feat(search-events): Require dataset so Seer runs for natural language queries Seer's search agent translates into a single strategy, so search_events only calls it when a supported dataset is passed explicitly. Because `dataset` was optional, client models often omitted it, the handler fell back to errors, and Seer was skipped even for plain natural language queries like "slowest api calls in the last 24 hours". Make `dataset` required so the calling model picks it up front, and add a short description hint that routes API/HTTP calls, DB queries, and latency questions to spans. The embedded agent can still correct the dataset when configured. Co-Authored-By: Shaun Kaasten --- docs/architecture/overview.md | 2 +- docs/specs/search-events.md | 6 ++-- docs/testing/stdio.md | 5 +-- packages/mcp-core/src/skillDefinitions.json | 6 ++-- packages/mcp-core/src/toolDefinitions.json | 8 ++--- .../src/tools/catalog/search-events.test.ts | 31 +++++++++++++++++++ .../src/tools/catalog/search-events.ts | 18 +++++------ 7 files changed, 53 insertions(+), 23 deletions(-) diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 3283d4bb2..ab84ffb7b 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -304,7 +304,7 @@ Some tools (`search_events` and `search_issues`) implement a two-tier agent patt ``` 1. User: "Show me errors from yesterday" ↓ -2. Claude: Calls search_events(query="errors from yesterday") +2. Claude: Calls search_events(dataset="errors", query="errors from yesterday") ↓ 3. MCP Tool Handler: Receives request ↓ diff --git a/docs/specs/search-events.md b/docs/specs/search-events.md index 6a9868c13..34a93ed98 100644 --- a/docs/specs/search-events.md +++ b/docs/specs/search-events.md @@ -16,7 +16,7 @@ A unified search tool that accepts natural language queries and translates them interface SearchEventsParams { organizationSlug: string; // Required query: string; // Natural language search description - dataset?: "spans" | "errors" | "logs" | "metrics"; // Dataset to search (default: "errors") + dataset: "spans" | "errors" | "logs" | "metrics" | "profiles" | "replays"; // Required; the agent may correct it projectSlug?: string; // Optional - limit to specific project regionUrl?: string; limit?: number; // Default: 10, Max: 100 @@ -27,9 +27,10 @@ interface SearchEventsParams { ### Examples ```typescript -// Find errors (errors dataset is default) +// Find errors search_events({ organizationSlug: "my-org", + dataset: "errors", query: "database timeouts in checkout flow from last hour" }) @@ -152,6 +153,7 @@ find_errors({ // After search_events({ organizationSlug: "sentry", + dataset: "errors", query: "unresolved errors in checkout.js" }) ``` diff --git a/docs/testing/stdio.md b/docs/testing/stdio.md index 8a1aa0076..1c6e51ea3 100644 --- a/docs/testing/stdio.md +++ b/docs/testing/stdio.md @@ -198,7 +198,7 @@ This opens the MCP Inspector at `http://localhost:6274` 1. **List Tools** - Verify expected tools appear 2. **Call a tool** - Start with `execute_sentry_tool` using `name="whoami"` and `arguments={}` 3. **Test with parameters** - Try `find_organizations()` -4. **Test complex operations** - Try `search_events(query="errors in the last hour")` +4. **Test complex operations** - Try `search_events(dataset="errors", query="errors in the last hour")` **Example test sequence:** ``` @@ -207,6 +207,7 @@ This opens the MCP Inspector at `http://localhost:6274` 3. find_projects(organizationSlug="your-org") 4. search_events( organizationSlug="your-org", + dataset="errors", query="errors from yesterday" ) ``` @@ -483,7 +484,7 @@ OPENAI_API_KEY=your-key pnpm start --access-token=TOKEN # Test search_events and search_issues work # In MCP Inspector: -# - Call search_events(query="errors in production") +# - Call search_events(dataset="errors", query="errors in production") # - Call search_issues(query="unresolved crashes") ``` diff --git a/packages/mcp-core/src/skillDefinitions.json b/packages/mcp-core/src/skillDefinitions.json index 11cba76f5..216ff0272 100644 --- a/packages/mcp-core/src/skillDefinitions.json +++ b/packages/mcp-core/src/skillDefinitions.json @@ -194,7 +194,7 @@ }, { "name": "search_events", - "description": "Search Sentry events and replays. Use for event counts/statistics.\n\n`query` is natural language or Sentry search syntax; a configured agent fixes dataset, query, fields, and sort.\n\nSupports THREE query types:\n1. AGGREGATIONS (counts, sums, averages): 'how many errors', 'total tokens'\n2. Individual events with timestamps: 'error logs from last hour'\n3. TIME SERIES (metric over time): 'errors per hour', 'error trend over time'\n\nDatasets:\n- errors: Exception/crash events with stack traces, usually grouped into issues\n- logs: Application log entries, including error-severity log messages\n- spans: Raw trace/span events for performance, AI/LLM calls, requests, and operations\n- metrics: Metric rows and aggregates: counters, gauges, distributions, values\n- profiles: Transaction/continuous profile results, profile IDs, profiled transactions\n- replays: Session replay results: rage clicks, dead clicks, visited pages, replay users\nIf the user says logs, log messages, error logs, or warning logs, choose logs instead of errors.\n\nReplay searches return replay lists only; replay count()/avg()/sum() are not supported.\n\nNOT for grouped issue lists (use search_issues) or app screenshots/images (use get_latest_base_snapshot).\n\n\nsearch_events(organizationSlug='my-org', dataset='errors', query='how many errors today')\nsearch_events(organizationSlug='my-org', dataset='errors', fields=['issue', 'count()'], sort='-count()')\nsearch_events(organizationSlug='my-org', dataset='errors', query='errors per hour last 24h')\nsearch_events(organizationSlug='my-org', dataset='spans', query='span.op:db', sort='-span.duration')\nsearch_events(organizationSlug='my-org', dataset='replays', query='count_errors:>0', sort='-count_errors')\n\n\n\n- name/otherName notation means /; parse it directly, don't call find_organizations/find_projects.\n- Use fields with aggregate functions like count(), avg(), sum() for statistics\n- Sort by -count() for most common, -timestamp for newest\n", + "description": "Search Sentry events and replays. Use for event counts/statistics.\n\n`query` is natural language or Sentry search syntax; a configured agent fixes dataset, query, fields, and sort.\n\nSupports THREE query types:\n1. AGGREGATIONS (counts, sums, averages): 'how many errors', 'total tokens'\n2. Individual events with timestamps: 'error logs from last hour'\n3. TIME SERIES (metric over time): 'errors per hour', 'error trend over time'\n\nDatasets:\n- errors: Exception/crash events with stack traces, usually grouped into issues\n- logs: Application log entries, including error-severity log messages\n- spans: Raw trace/span events for performance, AI/LLM calls, requests, and operations\n- metrics: Metric rows and aggregates: counters, gauges, distributions, values\n- profiles: Transaction/continuous profile results, profile IDs, profiled transactions\n- replays: Session replay results: rage clicks, dead clicks, visited pages, replay users\nPick logs for log messages (incl. error/warning logs); spans for API/HTTP calls, DB queries, latency.\n\nReplay searches return replay lists only; replay count()/avg()/sum() are not supported.\n\nNOT for grouped issue lists (use search_issues) or app screenshots/images (use get_latest_base_snapshot).\n\n\nsearch_events(organizationSlug='my-org', dataset='errors', query='how many errors today')\nsearch_events(organizationSlug='my-org', dataset='errors', fields=['issue', 'count()'], sort='-count()')\nsearch_events(organizationSlug='my-org', dataset='errors', query='errors per hour last 24h')\nsearch_events(organizationSlug='my-org', dataset='spans', query='span.op:db', sort='-span.duration')\nsearch_events(organizationSlug='my-org', dataset='replays', query='count_errors:>0', sort='-count_errors')\n\n\n\n- name/otherName notation means /; parse it directly, don't call find_organizations/find_projects.\n- Use fields with aggregate functions like count(), avg(), sum() for statistics\n- Sort by -count() for most common, -timestamp for newest\n", "requiredScopes": ["event:read"] }, { @@ -269,7 +269,7 @@ }, { "name": "search_events", - "description": "Search Sentry events and replays. Use for event counts/statistics.\n\n`query` is natural language or Sentry search syntax; a configured agent fixes dataset, query, fields, and sort.\n\nSupports THREE query types:\n1. AGGREGATIONS (counts, sums, averages): 'how many errors', 'total tokens'\n2. Individual events with timestamps: 'error logs from last hour'\n3. TIME SERIES (metric over time): 'errors per hour', 'error trend over time'\n\nDatasets:\n- errors: Exception/crash events with stack traces, usually grouped into issues\n- logs: Application log entries, including error-severity log messages\n- spans: Raw trace/span events for performance, AI/LLM calls, requests, and operations\n- metrics: Metric rows and aggregates: counters, gauges, distributions, values\n- profiles: Transaction/continuous profile results, profile IDs, profiled transactions\n- replays: Session replay results: rage clicks, dead clicks, visited pages, replay users\nIf the user says logs, log messages, error logs, or warning logs, choose logs instead of errors.\n\nReplay searches return replay lists only; replay count()/avg()/sum() are not supported.\n\nNOT for grouped issue lists (use search_issues) or app screenshots/images (use get_latest_base_snapshot).\n\n\nsearch_events(organizationSlug='my-org', dataset='errors', query='how many errors today')\nsearch_events(organizationSlug='my-org', dataset='errors', fields=['issue', 'count()'], sort='-count()')\nsearch_events(organizationSlug='my-org', dataset='errors', query='errors per hour last 24h')\nsearch_events(organizationSlug='my-org', dataset='spans', query='span.op:db', sort='-span.duration')\nsearch_events(organizationSlug='my-org', dataset='replays', query='count_errors:>0', sort='-count_errors')\n\n\n\n- name/otherName notation means /; parse it directly, don't call find_organizations/find_projects.\n- Use fields with aggregate functions like count(), avg(), sum() for statistics\n- Sort by -count() for most common, -timestamp for newest\n", + "description": "Search Sentry events and replays. Use for event counts/statistics.\n\n`query` is natural language or Sentry search syntax; a configured agent fixes dataset, query, fields, and sort.\n\nSupports THREE query types:\n1. AGGREGATIONS (counts, sums, averages): 'how many errors', 'total tokens'\n2. Individual events with timestamps: 'error logs from last hour'\n3. TIME SERIES (metric over time): 'errors per hour', 'error trend over time'\n\nDatasets:\n- errors: Exception/crash events with stack traces, usually grouped into issues\n- logs: Application log entries, including error-severity log messages\n- spans: Raw trace/span events for performance, AI/LLM calls, requests, and operations\n- metrics: Metric rows and aggregates: counters, gauges, distributions, values\n- profiles: Transaction/continuous profile results, profile IDs, profiled transactions\n- replays: Session replay results: rage clicks, dead clicks, visited pages, replay users\nPick logs for log messages (incl. error/warning logs); spans for API/HTTP calls, DB queries, latency.\n\nReplay searches return replay lists only; replay count()/avg()/sum() are not supported.\n\nNOT for grouped issue lists (use search_issues) or app screenshots/images (use get_latest_base_snapshot).\n\n\nsearch_events(organizationSlug='my-org', dataset='errors', query='how many errors today')\nsearch_events(organizationSlug='my-org', dataset='errors', fields=['issue', 'count()'], sort='-count()')\nsearch_events(organizationSlug='my-org', dataset='errors', query='errors per hour last 24h')\nsearch_events(organizationSlug='my-org', dataset='spans', query='span.op:db', sort='-span.duration')\nsearch_events(organizationSlug='my-org', dataset='replays', query='count_errors:>0', sort='-count_errors')\n\n\n\n- name/otherName notation means /; parse it directly, don't call find_organizations/find_projects.\n- Use fields with aggregate functions like count(), avg(), sum() for statistics\n- Sort by -count() for most common, -timestamp for newest\n", "requiredScopes": ["event:read"] }, { @@ -405,7 +405,7 @@ }, { "name": "search_events", - "description": "Search Sentry events and replays. Use for event counts/statistics.\n\n`query` is natural language or Sentry search syntax; a configured agent fixes dataset, query, fields, and sort.\n\nSupports THREE query types:\n1. AGGREGATIONS (counts, sums, averages): 'how many errors', 'total tokens'\n2. Individual events with timestamps: 'error logs from last hour'\n3. TIME SERIES (metric over time): 'errors per hour', 'error trend over time'\n\nDatasets:\n- errors: Exception/crash events with stack traces, usually grouped into issues\n- logs: Application log entries, including error-severity log messages\n- spans: Raw trace/span events for performance, AI/LLM calls, requests, and operations\n- metrics: Metric rows and aggregates: counters, gauges, distributions, values\n- profiles: Transaction/continuous profile results, profile IDs, profiled transactions\n- replays: Session replay results: rage clicks, dead clicks, visited pages, replay users\nIf the user says logs, log messages, error logs, or warning logs, choose logs instead of errors.\n\nReplay searches return replay lists only; replay count()/avg()/sum() are not supported.\n\nNOT for grouped issue lists (use search_issues) or app screenshots/images (use get_latest_base_snapshot).\n\n\nsearch_events(organizationSlug='my-org', dataset='errors', query='how many errors today')\nsearch_events(organizationSlug='my-org', dataset='errors', fields=['issue', 'count()'], sort='-count()')\nsearch_events(organizationSlug='my-org', dataset='errors', query='errors per hour last 24h')\nsearch_events(organizationSlug='my-org', dataset='spans', query='span.op:db', sort='-span.duration')\nsearch_events(organizationSlug='my-org', dataset='replays', query='count_errors:>0', sort='-count_errors')\n\n\n\n- name/otherName notation means /; parse it directly, don't call find_organizations/find_projects.\n- Use fields with aggregate functions like count(), avg(), sum() for statistics\n- Sort by -count() for most common, -timestamp for newest\n", + "description": "Search Sentry events and replays. Use for event counts/statistics.\n\n`query` is natural language or Sentry search syntax; a configured agent fixes dataset, query, fields, and sort.\n\nSupports THREE query types:\n1. AGGREGATIONS (counts, sums, averages): 'how many errors', 'total tokens'\n2. Individual events with timestamps: 'error logs from last hour'\n3. TIME SERIES (metric over time): 'errors per hour', 'error trend over time'\n\nDatasets:\n- errors: Exception/crash events with stack traces, usually grouped into issues\n- logs: Application log entries, including error-severity log messages\n- spans: Raw trace/span events for performance, AI/LLM calls, requests, and operations\n- metrics: Metric rows and aggregates: counters, gauges, distributions, values\n- profiles: Transaction/continuous profile results, profile IDs, profiled transactions\n- replays: Session replay results: rage clicks, dead clicks, visited pages, replay users\nPick logs for log messages (incl. error/warning logs); spans for API/HTTP calls, DB queries, latency.\n\nReplay searches return replay lists only; replay count()/avg()/sum() are not supported.\n\nNOT for grouped issue lists (use search_issues) or app screenshots/images (use get_latest_base_snapshot).\n\n\nsearch_events(organizationSlug='my-org', dataset='errors', query='how many errors today')\nsearch_events(organizationSlug='my-org', dataset='errors', fields=['issue', 'count()'], sort='-count()')\nsearch_events(organizationSlug='my-org', dataset='errors', query='errors per hour last 24h')\nsearch_events(organizationSlug='my-org', dataset='spans', query='span.op:db', sort='-span.duration')\nsearch_events(organizationSlug='my-org', dataset='replays', query='count_errors:>0', sort='-count_errors')\n\n\n\n- name/otherName notation means /; parse it directly, don't call find_organizations/find_projects.\n- Use fields with aggregate functions like count(), avg(), sum() for statistics\n- Sort by -count() for most common, -timestamp for newest\n", "requiredScopes": ["event:read"] }, { diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index 534c64d3c..67159e0fc 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -7203,7 +7203,7 @@ }, { "name": "search_events", - "description": "Search Sentry events and replays. Use for event counts/statistics.\n\n`query` is natural language or Sentry search syntax; a configured agent fixes dataset, query, fields, and sort.\n\nSupports THREE query types:\n1. AGGREGATIONS (counts, sums, averages): 'how many errors', 'total tokens'\n2. Individual events with timestamps: 'error logs from last hour'\n3. TIME SERIES (metric over time): 'errors per hour', 'error trend over time'\n\nDatasets:\n- errors: Exception/crash events with stack traces, usually grouped into issues\n- logs: Application log entries, including error-severity log messages\n- spans: Raw trace/span events for performance, AI/LLM calls, requests, and operations\n- metrics: Metric rows and aggregates: counters, gauges, distributions, values\n- profiles: Transaction/continuous profile results, profile IDs, profiled transactions\n- replays: Session replay results: rage clicks, dead clicks, visited pages, replay users\nIf the user says logs, log messages, error logs, or warning logs, choose logs instead of errors.\n\nReplay searches return replay lists only; replay count()/avg()/sum() are not supported.\n\nNOT for grouped issue lists (use search_issues) or app screenshots/images (use get_latest_base_snapshot).\n\n\nsearch_events(organizationSlug='my-org', dataset='errors', query='how many errors today')\nsearch_events(organizationSlug='my-org', dataset='errors', fields=['issue', 'count()'], sort='-count()')\nsearch_events(organizationSlug='my-org', dataset='errors', query='errors per hour last 24h')\nsearch_events(organizationSlug='my-org', dataset='spans', query='span.op:db', sort='-span.duration')\nsearch_events(organizationSlug='my-org', dataset='replays', query='count_errors:>0', sort='-count_errors')\n\n\n\n- name/otherName notation means /; parse it directly, don't call find_organizations/find_projects.\n- Use fields with aggregate functions like count(), avg(), sum() for statistics\n- Sort by -count() for most common, -timestamp for newest\n", + "description": "Search Sentry events and replays. Use for event counts/statistics.\n\n`query` is natural language or Sentry search syntax; a configured agent fixes dataset, query, fields, and sort.\n\nSupports THREE query types:\n1. AGGREGATIONS (counts, sums, averages): 'how many errors', 'total tokens'\n2. Individual events with timestamps: 'error logs from last hour'\n3. TIME SERIES (metric over time): 'errors per hour', 'error trend over time'\n\nDatasets:\n- errors: Exception/crash events with stack traces, usually grouped into issues\n- logs: Application log entries, including error-severity log messages\n- spans: Raw trace/span events for performance, AI/LLM calls, requests, and operations\n- metrics: Metric rows and aggregates: counters, gauges, distributions, values\n- profiles: Transaction/continuous profile results, profile IDs, profiled transactions\n- replays: Session replay results: rage clicks, dead clicks, visited pages, replay users\nPick logs for log messages (incl. error/warning logs); spans for API/HTTP calls, DB queries, latency.\n\nReplay searches return replay lists only; replay count()/avg()/sum() are not supported.\n\nNOT for grouped issue lists (use search_issues) or app screenshots/images (use get_latest_base_snapshot).\n\n\nsearch_events(organizationSlug='my-org', dataset='errors', query='how many errors today')\nsearch_events(organizationSlug='my-org', dataset='errors', fields=['issue', 'count()'], sort='-count()')\nsearch_events(organizationSlug='my-org', dataset='errors', query='errors per hour last 24h')\nsearch_events(organizationSlug='my-org', dataset='spans', query='span.op:db', sort='-span.duration')\nsearch_events(organizationSlug='my-org', dataset='replays', query='count_errors:>0', sort='-count_errors')\n\n\n\n- name/otherName notation means /; parse it directly, don't call find_organizations/find_projects.\n- Use fields with aggregate functions like count(), avg(), sum() for statistics\n- Sort by -count() for most common, -timestamp for newest\n", "inputSchema": { "type": "object", "properties": { @@ -7212,9 +7212,9 @@ "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool." }, "dataset": { - "description": "Initial dataset hint: errors, logs, spans, metrics, profiles, or replays. Always pass it, including for natural language queries. The agent may correct it when configured.", "type": "string", - "enum": ["spans", "errors", "logs", "metrics", "profiles", "replays"] + "enum": ["spans", "errors", "logs", "metrics", "profiles", "replays"], + "description": "Dataset to search: errors, logs, spans, metrics, profiles, or replays. Pick the one that matches the question, including for natural language queries. The agent may correct it when configured." }, "query": { "description": "Natural language or Sentry event search query syntax.", @@ -7304,7 +7304,7 @@ "type": "boolean" } }, - "required": ["organizationSlug"] + "required": ["organizationSlug", "dataset"] }, "requiredScopes": ["event:read"], "skills": ["inspect", "triage", "seer"], diff --git a/packages/mcp-core/src/tools/catalog/search-events.test.ts b/packages/mcp-core/src/tools/catalog/search-events.test.ts index b3cfb3f40..bb3a8ebca 100644 --- a/packages/mcp-core/src/tools/catalog/search-events.test.ts +++ b/packages/mcp-core/src/tools/catalog/search-events.test.ts @@ -2,6 +2,7 @@ import { mswServer } from "@sentry/mcp-server-mocks"; import { APICallError, generateText } from "ai"; import { HttpResponse, http } from "msw"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { z } from "zod"; import { UserInputError } from "../../errors"; import searchEvents from "./search-events"; @@ -126,6 +127,24 @@ describe("search_events", () => { mockValidEventsValidation(); }); + it("requires a dataset so callers pick one up front", () => { + const schema = z.object(searchEvents.inputSchema); + + expect( + schema.safeParse({ + organizationSlug: "test-org", + query: "slowest api calls in the last 24 hours", + }).success, + ).toBe(false); + expect( + schema.safeParse({ + organizationSlug: "test-org", + dataset: "spans", + query: "slowest api calls in the last 24 hours", + }).success, + ).toBe(true); + }); + it("falls back to the original query when the AI provider is unavailable", async () => { mockGenerateText.mockRejectedValue( new APICallError({ @@ -1183,6 +1202,7 @@ describe("search_events", () => { const result = await searchEvents.handler( { organizationSlug: "test-org", + dataset: "errors", regionUrl: null, projectSlug: null, query: "slow request duration metrics", @@ -1283,6 +1303,7 @@ describe("search_events", () => { const result = await searchEvents.handler( { organizationSlug: "test-org", + dataset: "errors", regionUrl: null, projectSlug: null, query: "recent metrics", @@ -1370,6 +1391,7 @@ describe("search_events", () => { const result = await searchEvents.handler( { organizationSlug: "test-org", + dataset: "errors", regionUrl: null, projectSlug: null, query: "recent profiles for /api/users", @@ -1441,6 +1463,7 @@ describe("search_events", () => { const result = await searchEvents.handler( { organizationSlug: "test-org", + dataset: "errors", regionUrl: null, projectSlug: null, query: "recent profiles for /api/users", @@ -1511,6 +1534,7 @@ describe("search_events", () => { const result = await searchEvents.handler( { organizationSlug: "test-org", + dataset: "errors", regionUrl: null, projectSlug: null, query: "recent profiles for /api/users", @@ -1566,6 +1590,7 @@ describe("search_events", () => { const result = await searchEvents.handler( { organizationSlug: "test-org", + dataset: "errors", regionUrl: null, projectSlug: null, query: "count profiles by release", @@ -1639,6 +1664,7 @@ describe("search_events", () => { const result = await searchEvents.handler( { organizationSlug: "test-org", + dataset: "errors", regionUrl: null, projectSlug: null, query: "recent profiles for /api/users", @@ -1713,6 +1739,7 @@ describe("search_events", () => { const result = await searchEvents.handler( { organizationSlug: "test-org", + dataset: "errors", regionUrl: null, projectSlug: null, query: "production checkout replays with errors in the last day", @@ -1770,6 +1797,7 @@ describe("search_events", () => { const result = await searchEvents.handler( { organizationSlug: "test-org", + dataset: "errors", regionUrl: null, projectSlug: null, query: "checkout replays yesterday", @@ -2016,6 +2044,7 @@ describe("search_events", () => { const result = await searchEvents.handler( { organizationSlug: "test-org", + dataset: "errors", regionUrl: null, projectSlug: null, query: "recent errors with geo-only user data", @@ -2072,6 +2101,7 @@ describe("search_events", () => { const result = await searchEvents.handler( { organizationSlug: "test-org", + dataset: "errors", regionUrl: null, projectSlug: null, query: "logs with user geo", @@ -2131,6 +2161,7 @@ describe("search_events", () => { const result = await searchEvents.handler( { organizationSlug: "test-org", + dataset: "errors", regionUrl: null, projectSlug: null, query: "spans with user geo", diff --git a/packages/mcp-core/src/tools/catalog/search-events.ts b/packages/mcp-core/src/tools/catalog/search-events.ts index 180c53d84..f97cdb36d 100644 --- a/packages/mcp-core/src/tools/catalog/search-events.ts +++ b/packages/mcp-core/src/tools/catalog/search-events.ts @@ -1,6 +1,5 @@ import { getActiveSpan, setTag } from "@sentry/core"; import { z } from "zod"; -import { setOrganizationContext } from "../../telem/organization"; import { UserInputError } from "../../errors"; import { hasAgentProvider } from "../../internal/agents/provider-factory"; import { withProviderFallback } from "../../internal/agents/provider-fallback"; @@ -13,6 +12,7 @@ import { ParamRegionUrl, } from "../../schema"; import { logWarn } from "../../telem/logging"; +import { setOrganizationContext } from "../../telem/organization"; import { scrubSensitiveText } from "../../telem/sentry"; import type { ServerContext } from "../../types"; import { @@ -395,7 +395,7 @@ export default defineTool({ "- metrics: Metric rows and aggregates: counters, gauges, distributions, values", "- profiles: Transaction/continuous profile results, profile IDs, profiled transactions", "- replays: Session replay results: rage clicks, dead clicks, visited pages, replay users", - "If the user says logs, log messages, error logs, or warning logs, choose logs instead of errors.", + "Pick logs for log messages (incl. error/warning logs); spans for API/HTTP calls, DB queries, latency.", "", "Replay searches return replay lists only; replay count()/avg()/sum() are not supported.", "", @@ -419,9 +419,8 @@ export default defineTool({ organizationSlug: ParamOrganizationSlug, dataset: z .enum(SEARCH_EVENTS_DATASETS) - .optional() .describe( - "Initial dataset hint: errors, logs, spans, metrics, profiles, or replays. Always pass it, including for natural language queries. The agent may correct it when configured.", + "Dataset to search: errors, logs, spans, metrics, profiles, or replays. Pick the one that matches the question, including for natural language queries. The agent may correct it when configured.", ), query: z .string() @@ -506,7 +505,7 @@ export default defineTool({ setOrganizationContext(organizationSlug); if (params.projectSlug) setTag("project.slug", params.projectSlug); - const inputDataset = params.dataset ?? "errors"; + const inputDataset = params.dataset; const hasStructuredQuery = looksLikeSentrySearchSyntax(params.query); const canApplyEnvironmentFilter = inputDataset !== "replays" && @@ -532,14 +531,11 @@ export default defineTool({ let timeSeries: { yAxis: string; interval: string | null } | null = null; const explicitSort = params.sort?.trim() || undefined; - const hasExplicitDataset = params.dataset !== undefined; const hasExplicitFields = hasFields(params.fields); const hasExplicitSort = explicitSort !== undefined; const hasExplicitPeriod = params.period !== undefined; - const hasExplicitTraceItemDataset = - hasExplicitDataset && isTraceItemDataset(inputDataset); const shouldTrustStructuredTraceSearch = - hasStructuredQuery && hasExplicitTraceItemDataset; + hasStructuredQuery && isTraceItemDataset(inputDataset); const environmentFilter = formatEnvironmentFilter(params.environment); const explicitStructuredTraceQuery = shouldTrustStructuredTraceSearch ? appendSearchFilter(params.query ?? "", environmentFilter) @@ -551,8 +547,8 @@ export default defineTool({ // (below) and to flag any requested environment that doesn't exist. Skipped // only when nothing references an environment — including a structured query // that skips the agent but puts `environment:` in the query string. - // Seer only translates into the dataset it is given, so it runs only when - // one is explicit. It only sees the natural language query, so skip it for + // Seer only translates into the dataset it is given, so it runs only for + // the datasets it has a strategy for. It only sees the natural language query, so skip it for // structured queries and explicit fields or sort, which the embedded agent // preserves. Like the UI, an explicit environment is added to Seer's query // afterwards.