From 17655b1632919bda0b9881e92161bd08151e8cf2 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 06:17:32 +0000 Subject: [PATCH 1/2] fix(search-events): Keep explicit timestamp bounds the agent drops When a structured query has timestamp:>= / timestamp:< bounds and the caller also passes period, the embedded agent often moves the bounds into an absolute timeRange and drops them from the query. The explicit period then wins over that range, so the search silently runs over the whole period instead of the requested window. Put any dropped timestamp bounds back on the repaired query. They only narrow the period, so they are safe to send. Grouped and boolean queries are left alone. Co-Authored-By: Sergiy Dybskiy <3760543+sergical@users.noreply.github.com> --- .../src/tools/catalog/search-events.test.ts | 56 +++++++++++++++++++ .../src/tools/support/search-events/search.ts | 41 +++++++++++++- 2 files changed, 94 insertions(+), 3 deletions(-) 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 5f6a82246..23fff3ec6 100644 --- a/packages/mcp-core/src/tools/catalog/search-events.test.ts +++ b/packages/mcp-core/src/tools/catalog/search-events.test.ts @@ -3419,6 +3419,62 @@ describe("search_events", () => { expect(validatedQueries.at(-1)).not.toContain("message:"); }); + it("keeps explicit timestamp bounds the agent moved into an overridden time range", async () => { + // The agent moves the bounds into an absolute timeRange, but the explicit + // period wins over it, so dropping them would search the whole period. + mockGenerateText.mockResolvedValueOnce( + mockAIResponse( + "errors", + "environment:production is:unresolved", + ["count()", "count_unique(issue)"], + undefined, + "-count()", + { start: "2025-01-01T00:00:00Z", end: "2025-01-02T00:00:00Z" }, + ), + ); + const requests: URLSearchParams[] = []; + mswServer.use( + http.get( + "https://sentry.io/api/0/organizations/test-org/events/", + ({ request }) => { + requests.push(new URL(request.url).searchParams); + return HttpResponse.json({ data: [] }); + }, + ), + ); + + await runSearchEvents( + { + organizationSlug: "test-org", + regionUrl: null, + projectSlug: null, + dataset: "errors", + query: + "environment:production is:unresolved timestamp:>=2025-01-01T00:00:00Z timestamp:<2025-01-02T00:00:00Z", + fields: ["count()", "count_unique(issue)"], + period: "7d", + limit: 10, + includeExplanation: false, + }, + { + constraints: { + organizationSlug: null, + regionUrl: null, + projectSlug: null, + }, + accessToken: "test-token", + userId: "1", + }, + { lockDataset: true }, + ); + + expect(requests).toHaveLength(1); + expect(requests[0]!.get("statsPeriod")).toBe("7d"); + expect(requests[0]!.get("query")).toBe( + "environment:production is:unresolved timestamp:>=2025-01-01T00:00:00Z timestamp:<2025-01-02T00:00:00Z", + ); + }); + it("rejects partial full-text downgrades when a duplicate structured key remains", async () => { // Set-based key comparison would miss this: custom remains present after // custom:foo is dropped into message full-text. diff --git a/packages/mcp-core/src/tools/support/search-events/search.ts b/packages/mcp-core/src/tools/support/search-events/search.ts index 2f070b83f..f3de294c5 100644 --- a/packages/mcp-core/src/tools/support/search-events/search.ts +++ b/packages/mcp-core/src/tools/support/search-events/search.ts @@ -351,6 +351,40 @@ function preservesSearchTokens(originalQuery: string, repairedQuery: string) { ); } +const TIME_BOUND_FILTER_PATTERN = /^timestamp:\S/; + +/** + * Put back explicit `timestamp:` bounds that the agent dropped from a + * structured query. + * + * The agent often moves them into an absolute `timeRange`, but an explicit + * `period` wins over that range, so dropping them silently widens the search to + * the whole period. Kept bounds only narrow the period, so they are always + * safe to send. Grouped or boolean queries are left as they are, because a + * bound appended outside its group would change what it means. + */ +function restoreDroppedTimeBounds( + originalQuery: string, + repairedQuery: string, +): string { + const originalTokens = tokenizeSearchQuery(originalQuery); + if ( + originalTokens.some( + (token) => token === "OR" || token === "AND" || /[()]/.test(token), + ) + ) { + return repairedQuery; + } + + return originalTokens + .filter( + (token) => + TIME_BOUND_FILTER_PATTERN.test(token) && + !containsSearchToken(repairedQuery, token), + ) + .reduce((query, token) => appendSearchFilter(query, token), repairedQuery); +} + function choosePreservingRepairedQuery(params: { originalQuery: string; repairedQuery?: string | null; @@ -917,13 +951,14 @@ async function executeSearchEvents( filter: environmentFilter, dataset, }) - : looksLikeSentrySearchSyntax(params.query, dataset) && - isSemanticFilterDowngrade( + : looksLikeSentrySearchSyntax(params.query, dataset) + ? isSemanticFilterDowngrade( params.query ?? "", parsed.query || "", dataset, ) - ? (params.query ?? "") + ? (params.query ?? "") + : restoreDroppedTimeBounds(params.query ?? "", parsed.query || "") : parsed.query || ""; sortParam = shouldTrustExplicitSearchParams && explicitSort From 68cd2ca8faef72a336fdc2c0f56ed2e4d227194b Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 12:27:32 +0000 Subject: [PATCH 2/2] fix(search-events): Ignore parentheses inside quoted values The grouping guard skipped restoring dropped timestamp bounds whenever a token contained a parenthesis, including parentheses inside a quoted value such as message:"err (timeout)". Strip quoted values before the check so only real grouping skips the restore. --- .../src/tools/catalog/search-events.test.ts | 106 +++++++++--------- .../src/tools/support/search-events/search.ts | 8 +- 2 files changed, 63 insertions(+), 51 deletions(-) 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 23fff3ec6..8aa869ee0 100644 --- a/packages/mcp-core/src/tools/catalog/search-events.test.ts +++ b/packages/mcp-core/src/tools/catalog/search-events.test.ts @@ -3419,61 +3419,67 @@ describe("search_events", () => { expect(validatedQueries.at(-1)).not.toContain("message:"); }); - it("keeps explicit timestamp bounds the agent moved into an overridden time range", async () => { - // The agent moves the bounds into an absolute timeRange, but the explicit - // period wins over it, so dropping them would search the whole period. - mockGenerateText.mockResolvedValueOnce( - mockAIResponse( - "errors", - "environment:production is:unresolved", - ["count()", "count_unique(issue)"], - undefined, - "-count()", - { start: "2025-01-01T00:00:00Z", end: "2025-01-02T00:00:00Z" }, - ), - ); - const requests: URLSearchParams[] = []; - mswServer.use( - http.get( - "https://sentry.io/api/0/organizations/test-org/events/", - ({ request }) => { - requests.push(new URL(request.url).searchParams); - return HttpResponse.json({ data: [] }); - }, - ), - ); + it.each([ + "environment:production is:unresolved", + // Parentheses inside a quoted value are not grouping. + 'environment:production message:"err (timeout)"', + ])( + "keeps explicit timestamp bounds the agent moved into an overridden time range: %s", + async (filters) => { + // The agent moves the bounds into an absolute timeRange, but the explicit + // period wins over it, so dropping them would search the whole period. + mockGenerateText.mockResolvedValueOnce( + mockAIResponse( + "errors", + filters, + ["count()", "count_unique(issue)"], + undefined, + "-count()", + { start: "2025-01-01T00:00:00Z", end: "2025-01-02T00:00:00Z" }, + ), + ); + const bounds = + "timestamp:>=2025-01-01T00:00:00Z timestamp:<2025-01-02T00:00:00Z"; + const requests: URLSearchParams[] = []; + mswServer.use( + http.get( + "https://sentry.io/api/0/organizations/test-org/events/", + ({ request }) => { + requests.push(new URL(request.url).searchParams); + return HttpResponse.json({ data: [] }); + }, + ), + ); - await runSearchEvents( - { - organizationSlug: "test-org", - regionUrl: null, - projectSlug: null, - dataset: "errors", - query: - "environment:production is:unresolved timestamp:>=2025-01-01T00:00:00Z timestamp:<2025-01-02T00:00:00Z", - fields: ["count()", "count_unique(issue)"], - period: "7d", - limit: 10, - includeExplanation: false, - }, - { - constraints: { - organizationSlug: null, + await runSearchEvents( + { + organizationSlug: "test-org", regionUrl: null, projectSlug: null, + dataset: "errors", + query: `${filters} ${bounds}`, + fields: ["count()", "count_unique(issue)"], + period: "7d", + limit: 10, + includeExplanation: false, }, - accessToken: "test-token", - userId: "1", - }, - { lockDataset: true }, - ); + { + constraints: { + organizationSlug: null, + regionUrl: null, + projectSlug: null, + }, + accessToken: "test-token", + userId: "1", + }, + { lockDataset: true }, + ); - expect(requests).toHaveLength(1); - expect(requests[0]!.get("statsPeriod")).toBe("7d"); - expect(requests[0]!.get("query")).toBe( - "environment:production is:unresolved timestamp:>=2025-01-01T00:00:00Z timestamp:<2025-01-02T00:00:00Z", - ); - }); + expect(requests).toHaveLength(1); + expect(requests[0]!.get("statsPeriod")).toBe("7d"); + expect(requests[0]!.get("query")).toBe(`${filters} ${bounds}`); + }, + ); it("rejects partial full-text downgrades when a duplicate structured key remains", async () => { // Set-based key comparison would miss this: custom remains present after diff --git a/packages/mcp-core/src/tools/support/search-events/search.ts b/packages/mcp-core/src/tools/support/search-events/search.ts index f3de294c5..c13a0e768 100644 --- a/packages/mcp-core/src/tools/support/search-events/search.ts +++ b/packages/mcp-core/src/tools/support/search-events/search.ts @@ -352,6 +352,7 @@ function preservesSearchTokens(originalQuery: string, repairedQuery: string) { } const TIME_BOUND_FILTER_PATTERN = /^timestamp:\S/; +const QUOTED_VALUE_PATTERN = /(["'])(?:\\.|(?!\1).)*\1/g; /** * Put back explicit `timestamp:` bounds that the agent dropped from a @@ -370,7 +371,12 @@ function restoreDroppedTimeBounds( const originalTokens = tokenizeSearchQuery(originalQuery); if ( originalTokens.some( - (token) => token === "OR" || token === "AND" || /[()]/.test(token), + (token) => + token === "OR" || + token === "AND" || + // Parentheses inside a quoted value, e.g. message:"err (timeout)", + // are not grouping. + /[()]/.test(token.replace(QUOTED_VALUE_PATTERN, "")), ) ) { return repairedQuery;