diff --git a/docs/specs/search-events.md b/docs/specs/search-events.md index 4ca2a6c99..ff31c8ca9 100644 --- a/docs/specs/search-events.md +++ b/docs/specs/search-events.md @@ -144,7 +144,7 @@ Requests for a metric over time ("per hour", "per day", "trend", "over time") re - **Logs timestamp handling**: Logs don't support query-based timestamp filters like `timestamp:-1h`. Instead, use `statsPeriod=24h` parameter - **Project ID mapping**: API requires numeric project IDs, not slugs. Tool automatically converts project slugs to IDs -- **Seer translation**: Seer translates natural-language queries when the organization has the required Seer features and AI features are enabled. If Seer is unavailable or cannot translate the query, the tool falls back to the configured embedded agent. +- **Seer translation**: Seer translates natural-language queries when the organization has the `mcp-search-events-seer-translate` rollout flag and has not hidden AI features. If Seer is unavailable or cannot translate the query, the tool falls back to the configured embedded agent. - **Seer cross-event filters**: Time series results do not apply cross-event filters. When Seer returns those filters for a time series, the response always begins with a warning identifying the omitted filters and the broader results, even when `includeExplanation` is false. - **Seer project scope**: For a successful Seer translation without `projectSlug`, search and Explorer links use `project=-1` to match the all-accessible-project scope sent to Seer. Other unscoped searches retain their existing default scope. - **Parallel attribute fetching**: For spans/logs/metrics, fetches both string and number attribute types in parallel for better performance 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 04fcced9f..ab030f013 100644 --- a/packages/mcp-core/src/tools/catalog/search-events.test.ts +++ b/packages/mcp-core/src/tools/catalog/search-events.test.ts @@ -3611,7 +3611,10 @@ describe("search_events", () => { metric_query: null, }; - const mockOrganization = (features: string[]) => + const mockOrganization = ( + features: string[], + { hideAiFeatures = false } = {}, + ) => http.get( "https://sentry.io/api/0/organizations/test-org/", ({ request }) => @@ -3625,7 +3628,7 @@ describe("search_events", () => { ) === "1" ? { features } : {}), - hideAiFeatures: false, + hideAiFeatures, }), ); const mockProject = http.get( @@ -3659,7 +3662,7 @@ describe("search_events", () => { it("should translate natural language queries with Seer", async () => { mswServer.use( - mockOrganization(["gen-ai-search-agent-translate"]), + mockOrganization(["mcp-search-events-seer-translate"]), mockSeerState({ status: "completed", final_response: { responses: [seerQuery], unsupported_reason: null }, @@ -3698,7 +3701,7 @@ describe("search_events", () => { it("should return a time series when Seer sets an interval", async () => { mswServer.use( - mockOrganization(["gen-ai-search-agent-translate"]), + mockOrganization(["mcp-search-events-seer-translate"]), mockSeerState({ status: "completed", final_response: { @@ -3752,7 +3755,7 @@ describe("search_events", () => { it("should apply Seer's cross-event filters", async () => { mswServer.use( - mockOrganization(["gen-ai-search-agent-translate"]), + mockOrganization(["mcp-search-events-seer-translate"]), mockSeerState({ status: "completed", final_response: { @@ -3790,7 +3793,7 @@ describe("search_events", () => { "warns about unapplied time-series filters with includeExplanation=%s", async (includeExplanation) => { mswServer.use( - mockOrganization(["gen-ai-search-agent-translate"]), + mockOrganization(["mcp-search-events-seer-translate"]), mockSeerState({ status: "completed", final_response: { @@ -3883,7 +3886,7 @@ describe("search_events", () => { it("should keep a grouped Seer query with an interval as a table", async () => { mswServer.use( - mockOrganization(["gen-ai-search-agent-translate"]), + mockOrganization(["mcp-search-events-seer-translate"]), mockSeerState({ status: "completed", final_response: { @@ -3914,7 +3917,7 @@ describe("search_events", () => { it("should add an explicit environment to Seer's query", async () => { mswServer.use( - mockOrganization(["gen-ai-search-agent-translate"]), + mockOrganization(["mcp-search-events-seer-translate"]), mockSeerState({ status: "completed", final_response: { responses: [seerQuery], unsupported_reason: null }, @@ -3958,7 +3961,7 @@ describe("search_events", () => { "should keep the requested project and %s Seer's wider scope", async (_, handlerContext, expectNote) => { mswServer.use( - mockOrganization(["gen-ai-search-agent-translate"]), + mockOrganization(["mcp-search-events-seer-translate"]), mockSeerState({ status: "completed", final_response: { @@ -3988,7 +3991,7 @@ describe("search_events", () => { it("should keep the requested project when Seer does not broaden it", async () => { mswServer.use( - mockOrganization(["gen-ai-search-agent-translate"]), + mockOrganization(["mcp-search-events-seer-translate"]), mockSeerState({ status: "completed", final_response: { @@ -4021,7 +4024,7 @@ describe("search_events", () => { }, ); mswServer.use( - mockOrganization(["gen-ai-search-agent-translate"]), + mockOrganization(["mcp-search-events-seer-translate"]), http.post( "https://sentry.io/api/0/organizations/test-org/search-agent/start/", mockAllProjectsStart, @@ -4068,7 +4071,7 @@ describe("search_events", () => { it("should keep Seer's all-project scope for time series", async () => { mswServer.use( - mockOrganization(["gen-ai-search-agent-translate"]), + mockOrganization(["mcp-search-events-seer-translate"]), http.post( "https://sentry.io/api/0/organizations/test-org/search-agent/start/", async ({ request }) => { @@ -4109,7 +4112,7 @@ describe("search_events", () => { it("should prefer an explicit period over Seer's time range", async () => { mswServer.use( - mockOrganization(["gen-ai-search-agent-translate"]), + mockOrganization(["mcp-search-events-seer-translate"]), mockSeerState({ status: "completed", final_response: { responses: [seerQuery], unsupported_reason: null }, @@ -4131,7 +4134,7 @@ describe("search_events", () => { it("should not group by a non-aggregate Seer sort", async () => { mswServer.use( - mockOrganization(["gen-ai-search-agent-translate"]), + mockOrganization(["mcp-search-events-seer-translate"]), mockSeerState({ status: "completed", final_response: { @@ -4167,7 +4170,7 @@ describe("search_events", () => { mockAIResponse("spans", "span.op:http.client"), ); mswServer.use( - mockOrganization(["gen-ai-search-agent-translate"]), + mockOrganization(["mcp-search-events-seer-translate"]), http.get("https://sentry.io/api/0/organizations/test-org/events/", () => HttpResponse.json({ data: [] }), ), @@ -4223,12 +4226,31 @@ describe("search_events", () => { expect(mockGenerateText).toHaveBeenCalled(); }); + it("should fall back to the agent when AI features are hidden", async () => { + mockGenerateText.mockResolvedValueOnce( + mockAIResponse("spans", "span.op:http.client"), + ); + mswServer.use( + mockOrganization(["mcp-search-events-seer-translate"], { + hideAiFeatures: true, + }), + http.get("https://sentry.io/api/0/organizations/test-org/events/", () => + HttpResponse.json({ data: [] }), + ), + ); + + await searchEvents.handler(seerParams, context); + + expect(mockSeerStart).not.toHaveBeenCalled(); + expect(mockGenerateText).toHaveBeenCalled(); + }); + it("should fall back to the agent when Seer cannot translate", async () => { mockGenerateText.mockResolvedValueOnce( mockAIResponse("spans", "span.op:http.client"), ); mswServer.use( - mockOrganization(["gen-ai-search-agent-translate"]), + mockOrganization(["mcp-search-events-seer-translate"]), mockSeerState({ status: "error", unsupported_reason: "Unsupported" }), http.get("https://sentry.io/api/0/organizations/test-org/events/", () => HttpResponse.json({ data: [] }), @@ -4246,7 +4268,7 @@ describe("search_events", () => { mockAIResponse("spans", "span.op:http.client"), ); mswServer.use( - mockOrganization(["gen-ai-search-agent-translate"]), + mockOrganization(["mcp-search-events-seer-translate"]), http.post( "https://sentry.io/api/0/organizations/test-org/search-agent/start/", () => diff --git a/packages/mcp-core/src/tools/support/search-events/seer.ts b/packages/mcp-core/src/tools/support/search-events/seer.ts index 5ba7408d0..b146a6742 100644 --- a/packages/mcp-core/src/tools/support/search-events/seer.ts +++ b/packages/mcp-core/src/tools/support/search-events/seer.ts @@ -17,8 +17,9 @@ export const SEER_SEARCH_AGENT_TIMEOUT = 60 * 1000; // 1 minute // Sentry's sentinel for all projects the user can access. const ALL_ACCESSIBLE_PROJECTS = -1; -// The search agent endpoints require this feature. `hideAiFeatures` is checked separately. -const SEARCH_AGENT_FEATURE = "gen-ai-search-agent-translate"; +// Gradual rollout flag for Seer translation in search_events. The search agent +// endpoints themselves are ungated; `hideAiFeatures` is checked separately. +const SEER_TRANSLATE_FEATURE = "mcp-search-events-seer-translate"; const SEER_STRATEGIES = { errors: "Errors", @@ -62,7 +63,7 @@ async function hasSeerSearchAgentAccess( return false; } const features = organization.features ?? []; - return features.includes(SEARCH_AGENT_FEATURE); + return features.includes(SEER_TRANSLATE_FEATURE); } function toSearchTranslation(