Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3202993
feat(search-events): Teach the logs query agent regex message search
JoshuaKGoldberg Oct 5, 2026
5623a65
fix(search-events): Keep the regex prompt from favoring regex over wi…
JoshuaKGoldberg Oct 5, 2026
c6fc093
fix(search-events): Point the logs agent at attributes before message…
JoshuaKGoldberg Oct 5, 2026
37ffbe2
fix(search-events): Teach regex for ignoring case and drop the trace-…
JoshuaKGoldberg Oct 5, 2026
ef38959
fix(search-events): Say regex works on string attributes only
JoshuaKGoldberg Oct 5, 2026
5138386
fix(search-events): Ask for short regex patterns near the 64-characte…
JoshuaKGoldberg Oct 5, 2026
b3ce402
fix(search-events): Forbid regex filters outside the logs dataset
JoshuaKGoldberg Oct 5, 2026
ea918c5
fix(search-events): Prefer (?i) over mixed-case classes to fit the re…
JoshuaKGoldberg Oct 5, 2026
c2b44ae
fix(search): Pass regex log queries through as written
JoshuaKGoldberg Oct 5, 2026
4d1aaf4
fix(search): Only treat regex filters as search syntax for logs
JoshuaKGoldberg Oct 5, 2026
ef63fa3
fix(search): Recognize regex filters in groups and on all Sentry key …
JoshuaKGoldberg Oct 5, 2026
ceee205
fix(search): Match regex keys before plain keys and bound the key scan
JoshuaKGoldberg Oct 5, 2026
b08ce67
test(search): Format the regex helper tests
JoshuaKGoldberg Oct 5, 2026
4788b82
fix(search): Don't read URLs as regex log filters
JoshuaKGoldberg Oct 7, 2026
d85afd6
fix(search): Ignore URL schemes in any case when reading regex filters
JoshuaKGoldberg Oct 7, 2026
55756fd
Merge branch 'main' into joshgoldberg/logs-1015-pass-regex-log-querie…
JoshuaKGoldberg Oct 13, 2026
e6779a3
fix(search): Catch regex downgrades on every dataset and in any case
JoshuaKGoldberg Oct 13, 2026
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
6 changes: 3 additions & 3 deletions packages/mcp-core/src/skillDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@
},
{
"name": "search_logs",
"description": "Search Sentry logs: application log entries, including error- and warning-severity log messages. Use for log counts, statistics, trends, and individual log lines.\n\n`query` is natural language (preferred) or Sentry search syntax; a configured agent translates it into query, fields, and sort.\n\nSupports aggregations ('warning logs by service'), individual entries ('error logs from the last hour'), and time series ('error logs per hour').\n\nNOT for exceptions/crashes (use search_errors). For requests or spans whose trace also has a matching log, use search_traces.\n\n<examples>\nsearch_logs(organizationSlug='my-org', query='error logs from the last hour')\nsearch_logs(organizationSlug='my-org', query='logs mentioning payment timeout in production')\nsearch_logs(organizationSlug='my-org', query='count warning logs by service over 7 days')\n</examples>\n\n<hints>\n- name/otherName notation means <organizationSlug>/<projectSlug>; parse it directly, don't call find_organizations/find_projects.\n- Natural language is usually enough. Only pass fields/sort when you need exact columns or ordering.\n</hints>",
"description": "Search Sentry logs: application log entries, including error- and warning-severity log messages. Use for log counts, statistics, trends, and individual log lines.\n\n`query` is natural language (preferred) or Sentry search syntax; a configured agent translates it into query, fields, and sort.\n\nSupports aggregations ('warning logs by service'), individual entries ('error logs from the last hour'), and time series ('error logs per hour').\n\nNOT for exceptions/crashes (use search_errors). For requests or spans whose trace also has a matching log, use search_traces.\n\n<examples>\nsearch_logs(organizationSlug='my-org', query='error logs from the last hour')\nsearch_logs(organizationSlug='my-org', query='logs mentioning payment timeout in production')\nsearch_logs(organizationSlug='my-org', query='count warning logs by service over 7 days')\n</examples>\n\n<hints>\n- name/otherName notation means <organizationSlug>/<projectSlug>; parse it directly, don't call find_organizations/find_projects.\n- Natural language is usually enough. Only pass fields/sort when you need exact columns or ordering.\n- Sentry search syntax for logs includes RE2 regex filters, never quoted: `message://^Timeout after \\d+ms//`.\n</hints>",
"requiredScopes": ["event:read"]
},
{
Expand Down Expand Up @@ -309,7 +309,7 @@
},
{
"name": "search_logs",
"description": "Search Sentry logs: application log entries, including error- and warning-severity log messages. Use for log counts, statistics, trends, and individual log lines.\n\n`query` is natural language (preferred) or Sentry search syntax; a configured agent translates it into query, fields, and sort.\n\nSupports aggregations ('warning logs by service'), individual entries ('error logs from the last hour'), and time series ('error logs per hour').\n\nNOT for exceptions/crashes (use search_errors). For requests or spans whose trace also has a matching log, use search_traces.\n\n<examples>\nsearch_logs(organizationSlug='my-org', query='error logs from the last hour')\nsearch_logs(organizationSlug='my-org', query='logs mentioning payment timeout in production')\nsearch_logs(organizationSlug='my-org', query='count warning logs by service over 7 days')\n</examples>\n\n<hints>\n- name/otherName notation means <organizationSlug>/<projectSlug>; parse it directly, don't call find_organizations/find_projects.\n- Natural language is usually enough. Only pass fields/sort when you need exact columns or ordering.\n</hints>",
"description": "Search Sentry logs: application log entries, including error- and warning-severity log messages. Use for log counts, statistics, trends, and individual log lines.\n\n`query` is natural language (preferred) or Sentry search syntax; a configured agent translates it into query, fields, and sort.\n\nSupports aggregations ('warning logs by service'), individual entries ('error logs from the last hour'), and time series ('error logs per hour').\n\nNOT for exceptions/crashes (use search_errors). For requests or spans whose trace also has a matching log, use search_traces.\n\n<examples>\nsearch_logs(organizationSlug='my-org', query='error logs from the last hour')\nsearch_logs(organizationSlug='my-org', query='logs mentioning payment timeout in production')\nsearch_logs(organizationSlug='my-org', query='count warning logs by service over 7 days')\n</examples>\n\n<hints>\n- name/otherName notation means <organizationSlug>/<projectSlug>; parse it directly, don't call find_organizations/find_projects.\n- Natural language is usually enough. Only pass fields/sort when you need exact columns or ordering.\n- Sentry search syntax for logs includes RE2 regex filters, never quoted: `message://^Timeout after \\d+ms//`.\n</hints>",
"requiredScopes": ["event:read"]
},
{
Expand Down Expand Up @@ -475,7 +475,7 @@
},
{
"name": "search_logs",
"description": "Search Sentry logs: application log entries, including error- and warning-severity log messages. Use for log counts, statistics, trends, and individual log lines.\n\n`query` is natural language (preferred) or Sentry search syntax; a configured agent translates it into query, fields, and sort.\n\nSupports aggregations ('warning logs by service'), individual entries ('error logs from the last hour'), and time series ('error logs per hour').\n\nNOT for exceptions/crashes (use search_errors). For requests or spans whose trace also has a matching log, use search_traces.\n\n<examples>\nsearch_logs(organizationSlug='my-org', query='error logs from the last hour')\nsearch_logs(organizationSlug='my-org', query='logs mentioning payment timeout in production')\nsearch_logs(organizationSlug='my-org', query='count warning logs by service over 7 days')\n</examples>\n\n<hints>\n- name/otherName notation means <organizationSlug>/<projectSlug>; parse it directly, don't call find_organizations/find_projects.\n- Natural language is usually enough. Only pass fields/sort when you need exact columns or ordering.\n</hints>",
"description": "Search Sentry logs: application log entries, including error- and warning-severity log messages. Use for log counts, statistics, trends, and individual log lines.\n\n`query` is natural language (preferred) or Sentry search syntax; a configured agent translates it into query, fields, and sort.\n\nSupports aggregations ('warning logs by service'), individual entries ('error logs from the last hour'), and time series ('error logs per hour').\n\nNOT for exceptions/crashes (use search_errors). For requests or spans whose trace also has a matching log, use search_traces.\n\n<examples>\nsearch_logs(organizationSlug='my-org', query='error logs from the last hour')\nsearch_logs(organizationSlug='my-org', query='logs mentioning payment timeout in production')\nsearch_logs(organizationSlug='my-org', query='count warning logs by service over 7 days')\n</examples>\n\n<hints>\n- name/otherName notation means <organizationSlug>/<projectSlug>; parse it directly, don't call find_organizations/find_projects.\n- Natural language is usually enough. Only pass fields/sort when you need exact columns or ordering.\n- Sentry search syntax for logs includes RE2 regex filters, never quoted: `message://^Timeout after \\d+ms//`.\n</hints>",
"requiredScopes": ["event:read"]
},
{
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-core/src/toolDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -7832,7 +7832,7 @@
},
{
"name": "search_logs",
"description": "Search Sentry logs: application log entries, including error- and warning-severity log messages. Use for log counts, statistics, trends, and individual log lines.\n\n`query` is natural language (preferred) or Sentry search syntax; a configured agent translates it into query, fields, and sort.\n\nSupports aggregations ('warning logs by service'), individual entries ('error logs from the last hour'), and time series ('error logs per hour').\n\nNOT for exceptions/crashes (use search_errors). For requests or spans whose trace also has a matching log, use search_traces.\n\n<examples>\nsearch_logs(organizationSlug='my-org', query='error logs from the last hour')\nsearch_logs(organizationSlug='my-org', query='logs mentioning payment timeout in production')\nsearch_logs(organizationSlug='my-org', query='count warning logs by service over 7 days')\n</examples>\n\n<hints>\n- name/otherName notation means <organizationSlug>/<projectSlug>; parse it directly, don't call find_organizations/find_projects.\n- Natural language is usually enough. Only pass fields/sort when you need exact columns or ordering.\n</hints>",
"description": "Search Sentry logs: application log entries, including error- and warning-severity log messages. Use for log counts, statistics, trends, and individual log lines.\n\n`query` is natural language (preferred) or Sentry search syntax; a configured agent translates it into query, fields, and sort.\n\nSupports aggregations ('warning logs by service'), individual entries ('error logs from the last hour'), and time series ('error logs per hour').\n\nNOT for exceptions/crashes (use search_errors). For requests or spans whose trace also has a matching log, use search_traces.\n\n<examples>\nsearch_logs(organizationSlug='my-org', query='error logs from the last hour')\nsearch_logs(organizationSlug='my-org', query='logs mentioning payment timeout in production')\nsearch_logs(organizationSlug='my-org', query='count warning logs by service over 7 days')\n</examples>\n\n<hints>\n- name/otherName notation means <organizationSlug>/<projectSlug>; parse it directly, don't call find_organizations/find_projects.\n- Natural language is usually enough. Only pass fields/sort when you need exact columns or ordering.\n- Sentry search syntax for logs includes RE2 regex filters, never quoted: `message://^Timeout after \\d+ms//`.\n</hints>",
"inputSchema": {
"type": "object",
"properties": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ describe("collectRequestedEnvironments", () => {
),
).toEqual(["qa"]);
});

it("keeps regex values with spaces and apostrophes as one token", () => {
expect(
collectRequestedEnvironments(
null,
"message://can't connect// environment:qa",
),
).toEqual(["qa"]);
expect(
collectRequestedEnvironments(null, "message://a environment:foo b//"),
).toEqual([]);
expect(
collectRequestedEnvironments(null, "(message://a environment:foo b//)"),
).toEqual([]);
});
});

describe("formatUnknownEnvironmentNote", () => {
Expand Down
113 changes: 113 additions & 0 deletions packages/mcp-core/src/tools/catalog/search-events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3260,6 +3260,115 @@ describe("search_events", () => {
expect(result).toContain("TimeoutError");
});

describe("with regex log queries", () => {
const regexSearchParams = {
organizationSlug: "test-org",
regionUrl: null,
projectSlug: null,
dataset: "logs" as const,
fields: null,
sort: null,
period: "24h",
limit: 10,
includeExplanation: false,
};
const regexSearchContext = {
constraints: {
organizationSlug: null,
regionUrl: null,
projectSlug: null,
},
accessToken: "test-token",
userId: "1",
};

const captureEventsQueries = () => {
const queries: Array<string | null> = [];
mswServer.use(
http.get(
"https://sentry.io/api/0/organizations/test-org/events/",
({ request }) => {
queries.push(new URL(request.url).searchParams.get("query"));
return HttpResponse.json({ data: [] });
},
),
);
return queries;
};

it("runs a regex-only query as written without the agent when fields and sort are explicit", async () => {
const queries = captureEventsQueries();

await searchEvents.handler(
{
...regexSearchParams,
query: "message://^Timeout after \\d+ms//",
fields: ["timestamp", "message"],
sort: "-timestamp",
},
regexSearchContext,
);

expect(mockGenerateText).not.toHaveBeenCalled();
expect(queries).toEqual(["message://^Timeout after \\d+ms//"]);
});

it("keeps the regex filter when the agent rewrites it into a wildcard", async () => {
mockGenerateText.mockResolvedValueOnce(
mockAIResponse("logs", 'message:"*Timeout after*"'),
);
const queries = captureEventsQueries();

await searchEvents.handler(
{ ...regexSearchParams, query: "message://^Timeout after \\d+ms//" },
regexSearchContext,
);

expect(mockGenerateText).toHaveBeenCalledTimes(1);
expect(queries).toEqual(["message://^Timeout after \\d+ms//"]);
});

it("accepts agent repairs that keep a regex value with spaces and apostrophes", async () => {
mockGenerateText.mockResolvedValueOnce(
mockAIResponse(
"logs",
"severity:error message://can't connect to \\w+// has:trace",
),
);
const queries = captureEventsQueries();

await searchEvents.handler(
{
...regexSearchParams,
query: "message://can't connect to \\w+// severity:error",
},
regexSearchContext,
);

expect(queries).toEqual([
"severity:error message://can't connect to \\w+// has:trace",
]);
});

it("uses the agent's rewrite of regex syntax outside logs", async () => {
mockGenerateText.mockResolvedValueOnce(
mockAIResponse("spans", 'span.description:"GET /api/*"'),
);
const queries = captureEventsQueries();

await searchEvents.handler(
{
...regexSearchParams,
dataset: "spans",
query: "span.description://^GET \\/api\\/\\d+//",
},
regexSearchContext,
);

expect(queries).toEqual(['span.description:"GET /api/*"']);
});
});

it("keeps caller fields when the agent returns an empty fields array", async () => {
let eventsRequestUrl: URL | undefined;

Expand Down Expand Up @@ -4163,6 +4272,10 @@ describe("search_events", () => {

it.each([
["a structured query", { query: "span.op:http.client" }],
[
"a regex-only logs query",
{ dataset: "logs" as const, query: "message://^Timeout//" },
],
["explicit fields", { fields: ["span.description", "count()"] }],
["an explicit sort", { sort: "-count()" }],
])("should skip Seer for %s", async (_, overrides) => {
Expand Down
1 change: 1 addition & 0 deletions packages/mcp-core/src/tools/catalog/search-logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default defineTool({
"<hints>",
"- name/otherName notation means <organizationSlug>/<projectSlug>; parse it directly, don't call find_organizations/find_projects.",
"- Natural language is usually enough. Only pass fields/sort when you need exact columns or ordering.",
"- Sentry search syntax for logs includes RE2 regex filters, never quoted: `message://^Timeout after \\d+ms//`.",
"</hints>",
].join("\n"),
inputSchema: buildDatasetSearchInputSchema(),
Expand Down
28 changes: 23 additions & 5 deletions packages/mcp-core/src/tools/support/search-events/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
isAggregateQuery,
isSemanticFilterDowngrade,
looksLikeSentrySearchSyntax,
readRegexFilterValue,
recordEventsSearchValidationTelemetry,
validateEventsSearch,
} from "./utils";
Expand Down Expand Up @@ -262,7 +263,8 @@ function tokenizeSearchQuery(query: string): string[] {
let quote: '"' | "'" | null = null;
let escaped = false;

for (const char of query) {
for (let i = 0; i < query.length; i += 1) {
const char = query[i];
if (escaped) {
currentToken += char;
escaped = false;
Expand All @@ -283,6 +285,13 @@ function tokenizeSearchQuery(query: string): string[] {
continue;
}

const regexValue = readRegexFilterValue(query, i);
if (regexValue) {
currentToken += regexValue;
i += regexValue.length - 1;
continue;
}

if (char === '"' || char === "'") {
currentToken += char;
quote = char;
Expand Down Expand Up @@ -322,14 +331,15 @@ function choosePreservingRepairedQuery(params: {
originalQuery: string;
repairedQuery?: string | null;
filter?: string;
dataset: PublicEventsDataset | "replays";
}): string {
const originalQuery = params.originalQuery.trim();
const repairedQuery = params.repairedQuery?.trim();
if (!repairedQuery) {
return appendSearchFilter(originalQuery, params.filter);
}

if (isSemanticFilterDowngrade(originalQuery, repairedQuery)) {
if (isSemanticFilterDowngrade(originalQuery, repairedQuery, params.dataset)) {
return appendSearchFilter(originalQuery, params.filter);
}

Expand Down Expand Up @@ -611,7 +621,10 @@ export async function runSearchEvents(
setTargetTagsAndAttributes(params);

const inputDataset = params.dataset ?? "errors";
const hasStructuredQuery = looksLikeSentrySearchSyntax(params.query);
const hasStructuredQuery = looksLikeSentrySearchSyntax(
params.query,
inputDataset,
);
const canApplyEnvironmentFilter =
inputDataset !== "replays" &&
isTraceItemDataset(inputDataset) &&
Expand Down Expand Up @@ -780,9 +793,14 @@ export async function runSearchEvents(
originalQuery: params.query ?? "",
repairedQuery: parsed.query,
filter: environmentFilter,
dataset,
})
: looksLikeSentrySearchSyntax(params.query) &&
isSemanticFilterDowngrade(params.query ?? "", parsed.query || "")
: looksLikeSentrySearchSyntax(params.query, dataset) &&
isSemanticFilterDowngrade(
params.query ?? "",
parsed.query || "",
dataset,
)
? (params.query ?? "")
: parsed.query || "";
sortParam =
Expand Down
Loading
Loading