Skip to content

Commit d33ede4

Browse files
fix(search): Match regex keys before plain keys and bound the key scan
Colon and uppercase bracket keys now take the regex path, wildcard rewrites are caught on every regex key shape, a regex may follow ) or a quote, and key detection no longer rescans the whole query prefix. Refs LOGS-1015 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2a2335b commit d33ede4

2 files changed

Lines changed: 44 additions & 12 deletions

File tree

‎packages/mcp-core/src/tools/support/search-events/utils.test.ts‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,13 +315,19 @@ describe("search query helpers", () => {
315315
it("should detect regex filters inside parentheses and on Sentry's other key shapes", () => {
316316
expect(looksLikeSentrySearchSyntax("(message://a b//)", "logs")).toBe(true);
317317
expect(
318-
looksLikeSentrySearchSyntax("tags[sentry:user]://^id \\d+//", "logs"),
318+
looksLikeSentrySearchSyntax("tags[App:Region]://^us-//", "logs"),
319319
).toBe(true);
320+
expect(looksLikeSentrySearchSyntax("flags[Feature:X]://y//", "logs")).toBe(
321+
true,
322+
);
320323
expect(
321324
looksLikeSentrySearchSyntax("tags[foo, string]://a b//", "logs"),
322325
).toBe(true);
323326
expect(looksLikeSentrySearchSyntax('"mykey"://a b//', "logs")).toBe(true);
324327
expect(looksLikeSentrySearchSyntax("arr[*]://a b//", "logs")).toBe(true);
328+
expect(
329+
looksLikeSentrySearchSyntax('"Note"message://a b//', "logs"),
330+
).toBe(true);
325331
expect(looksLikeSentrySearchSyntax("(level:error)", "logs")).toBe(false);
326332
});
327333

@@ -392,7 +398,23 @@ describe("search query helpers", () => {
392398
expect(
393399
isSemanticFilterDowngrade(
394400
"tags[sentry:user]://^id \\d+//",
395-
'message:"*id*"',
401+
"tags[sentry:user]://^id//",
402+
"logs",
403+
),
404+
).toBe(true);
405+
});
406+
407+
it("detects regex filters rewritten into wildcards on array, quoted, and spaced keys", () => {
408+
expect(
409+
isSemanticFilterDowngrade("arr[*]://^a b//", "arr[*]:*a*", "logs"),
410+
).toBe(true);
411+
expect(
412+
isSemanticFilterDowngrade('"mykey"://^a b//', '"mykey":*a*', "logs"),
413+
).toBe(true);
414+
expect(
415+
isSemanticFilterDowngrade(
416+
"tags[x, string]://^a b//",
417+
'tags[x, string]:"*a*"',
396418
"logs",
397419
),
398420
).toBe(true);

‎packages/mcp-core/src/tools/support/search-events/utils.ts‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,18 @@ const DEFAULT_MAX_VALUE_LENGTH = 200;
2929
const DEFAULT_MAX_ARRAY_ITEMS = 20;
3030
const REGEX_FILTER_VALUE_SOURCE = String.raw`\/\/(?!\/\/(?:[\t\n )]|$))[^\n]{1,1024}?\/\/(?=[\t\n )]|$)`;
3131
const REGEX_FILTER_VALUE_PATTERN = new RegExp(`^${REGEX_FILTER_VALUE_SOURCE}`);
32-
const SEARCH_FILTER_KEY_SOURCE = String.raw`(^|\s)!?([A-Za-z_][A-Za-z0-9_.[\],-]*):`;
33-
const REGEX_FILTER_KEY_SOURCE = String.raw`(^|[\s(])!?((?:tags|flags)\[[\w.:-]+(?: *, *(?:string|number|boolean|array))?\](?:\[\*\])?|"[\w.:-]+"(?:\[\*\])?|[A-Za-z_][A-Za-z0-9_.[\],-]*(?:\[\*\])?):`;
32+
const SEARCH_FILTER_KEY_SOURCE = String.raw`(^|\s)!?(?<key>[A-Za-z_][A-Za-z0-9_.[\],-]*):`;
33+
const REGEX_FILTER_KEY_SOURCE = String.raw`(^|[\s()"])!?(?<regexKey>(?:tags|flags)\[[\w.:-]+(?: *, *(?:string|number|boolean|array))?\](?:\[\*\])?|"[\w.:-]+"(?:\[\*\])?|[A-Za-z_][A-Za-z0-9_.[\],-]*(?:\[\*\])?):`;
3434
const REGEX_FILTER_KEY_BEFORE_PATTERN = new RegExp(
3535
`${REGEX_FILTER_KEY_SOURCE}$`,
3636
);
37+
const REGEX_FILTER_KEY_SCAN_LIMIT = 256;
3738
const SENTRY_SEARCH_TOKEN_PATTERN = new RegExp(
3839
String.raw`${SEARCH_FILTER_KEY_SOURCE}(?=\S)(?!\/\/)`,
3940
"g",
4041
);
4142
const SENTRY_SEARCH_TOKEN_WITH_REGEX_PATTERN = new RegExp(
42-
String.raw`${SEARCH_FILTER_KEY_SOURCE}(?=\S)(?!\/\/)|${REGEX_FILTER_KEY_SOURCE}(?=${REGEX_FILTER_VALUE_SOURCE})`,
43+
String.raw`${REGEX_FILTER_KEY_SOURCE}(?=${REGEX_FILTER_VALUE_SOURCE})|${SEARCH_FILTER_KEY_SOURCE}(?=\S)(?!\/\/)`,
4344
"g",
4445
);
4546
const KNOWN_SENTRY_SEARCH_KEYS = new Set([
@@ -117,11 +118,11 @@ export function looksLikeSentrySearchSyntax(
117118
}
118119

119120
for (const match of trimmedQuery.matchAll(searchTokenPattern(dataset))) {
120-
if (match[4]) {
121+
if (match.groups?.regexKey) {
121122
return true;
122123
}
123124

124-
const key = match[2];
125+
const key = match.groups?.key;
125126
if (!key) {
126127
continue;
127128
}
@@ -150,10 +151,14 @@ export function readRegexFilterValue(
150151
query: string,
151152
index: number,
152153
): string | undefined {
153-
if (
154-
!query.startsWith("//", index) ||
155-
!REGEX_FILTER_KEY_BEFORE_PATTERN.test(query.slice(0, index))
156-
) {
154+
if (!query.startsWith("//", index) || query[index - 1] !== ":") {
155+
return undefined;
156+
}
157+
// Callers probe every index, so only scan a bounded key-sized prefix; the
158+
// NUL stands in for truncated text so `^` can't match mid-query.
159+
const start = Math.max(0, index - REGEX_FILTER_KEY_SCAN_LIMIT);
160+
const prefix = `${start > 0 ? "\0" : ""}${query.slice(start, index)}`;
161+
if (!REGEX_FILTER_KEY_BEFORE_PATTERN.test(prefix)) {
157162
return undefined;
158163
}
159164
return REGEX_FILTER_VALUE_PATTERN.exec(query.slice(index))?.[0];
@@ -275,7 +280,7 @@ function searchFilterOccurrences(
275280
const masked = maskQuotedRegions(query);
276281

277282
for (const match of masked.matchAll(searchTokenPattern(dataset))) {
278-
const key = (match[2] ?? match[4])?.toLowerCase();
283+
const key = (match.groups?.regexKey ?? match.groups?.key)?.toLowerCase();
279284
if (!key || match.index === undefined) {
280285
continue;
281286
}
@@ -401,8 +406,13 @@ function isRegexFilterDowngrade(
401406
}
402407

403408
const repairedFilters = searchFilterOccurrences(repairedQuery, dataset);
409+
const maskedRepairedQuery = maskQuotedRegions(repairedQuery);
404410
return unmatchedStructuredFilters(originalRegexFilters, repairedFilters).some(
405411
(filter) =>
412+
new RegExp(
413+
String.raw`(?:^|[\s()"])!?${escapeRegExp(filter.key)}:(?!\/\/)`,
414+
"i",
415+
).test(maskedRepairedQuery) ||
406416
repairedFilters.some(
407417
(repaired) =>
408418
repaired.key === filter.key ||

0 commit comments

Comments
 (0)