Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -209,7 +209,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 @@ -304,7 +304,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 @@ -470,7 +470,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 @@ -7554,7 +7554,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 @@ -3073,6 +3073,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 @@ -3977,6 +4086,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 @@ -781,9 +794,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