From 1383512e90c8d2330cb89c0db7102b62dbe5d454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Thu, 18 Jun 2026 10:53:00 -0400 Subject: [PATCH 1/2] fix(search): Render escaped asterisks as escaped When a filter token value contains an escaped asterisk (\*), formatFilterValue previously stripped the escape via unescapeAsteriskSearchValue before display, so a literal \* rendered identically to an active wildcard *. Stop stripping the escape in the VALUE_TEXT branch so escaped asterisks render as escaped, letting users distinguish a literal star from a wildcard. This is in the shared SearchQueryBuilder component, so it applies everywhere the component (and FormattedQuery) is used. Fixes LOGS-880 --- .../formattedQuery.spec.tsx | 12 +++++ .../searchQueryBuilder/index.spec.tsx | 18 ++++++-- .../tokens/filter/utils.spec.tsx | 46 +++++++++++++++++-- .../tokens/filter/utils.tsx | 4 +- 4 files changed, 69 insertions(+), 11 deletions(-) diff --git a/static/app/components/searchQueryBuilder/formattedQuery.spec.tsx b/static/app/components/searchQueryBuilder/formattedQuery.spec.tsx index d15a41aaa434..7a297959df42 100644 --- a/static/app/components/searchQueryBuilder/formattedQuery.spec.tsx +++ b/static/app/components/searchQueryBuilder/formattedQuery.spec.tsx @@ -102,4 +102,16 @@ describe('FormattedQuery', () => { expect(screen.getByText(textWithMarkupMatcher('has foo'))).toBeInTheDocument(); }); + + it('renders an escaped asterisk with the escape visible', () => { + render(); + + expect(screen.getByText('foo\\*bar')).toBeInTheDocument(); + }); + + it('renders a wildcard asterisk without an escape', () => { + render(); + + expect(screen.getByText('foo*bar')).toBeInTheDocument(); + }); }); diff --git a/static/app/components/searchQueryBuilder/index.spec.tsx b/static/app/components/searchQueryBuilder/index.spec.tsx index 8560319086a6..af99364d480a 100644 --- a/static/app/components/searchQueryBuilder/index.spec.tsx +++ b/static/app/components/searchQueryBuilder/index.spec.tsx @@ -2748,11 +2748,23 @@ describe('SearchQueryBuilder', () => { }); }); - it('renders escaped asterisks as a bare asterisk in the filter chip', async () => { + it('renders an escaped asterisk with the escape visible in the filter chip', async () => { render( ); + expect( + await within( + screen.getByRole('button', {name: 'Edit value for filter: browser.name'}) + ).findByText('foo\\*') + ).toBeInTheDocument(); + }); + + it('renders a wildcard asterisk without an escape in the filter chip', async () => { + render( + + ); + expect( await within( screen.getByRole('button', {name: 'Edit value for filter: browser.name'}) @@ -5621,7 +5633,7 @@ describe('SearchQueryBuilder', () => { ).toBeInTheDocument(); }); - it('escapes * for is op but not contains op', async () => { + it('renders the escaped asterisk for the contains suggestion but a wildcard for the is suggestion', async () => { render( { const options = within(screen.getByRole('listbox')).getAllByRole('option'); expect(options).toHaveLength(2); - expect(options[0]).toHaveTextContent('span.description contains test*'); + expect(options[0]).toHaveTextContent('span.description contains test\\*'); expect(options[1]).toHaveTextContent('span.description is test*'); }); diff --git a/static/app/components/searchQueryBuilder/tokens/filter/utils.spec.tsx b/static/app/components/searchQueryBuilder/tokens/filter/utils.spec.tsx index e95d5e1ca073..dd0114407727 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/utils.spec.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/utils.spec.tsx @@ -196,7 +196,7 @@ describe('unescapeAsteriskSearchValue', () => { }); describe('formatFilterValue', () => { - it('unescapes asterisks for unquoted values', () => { + it('preserves escaped asterisks for unquoted values', () => { expect( formatFilterValue({ token: { @@ -206,19 +206,55 @@ describe('formatFilterValue', () => { quoted: false, } as any, }) - ).toBe('****'); + ).toBe('\\*\\*\\*\\*'); }); - it('unescapes asterisks for quoted values', () => { + it('preserves escaped asterisks for quoted values', () => { expect( formatFilterValue({ token: { type: Token.VALUE_TEXT, - text: '"foo\\\\*bar"', + text: '"foo\\*bar"', value: 'foo\\*bar', quoted: true, } as any, }) - ).toBe('foo*bar'); + ).toBe('foo\\*bar'); + }); + + it('renders an escaped asterisk differently from a wildcard asterisk', () => { + const escaped = formatFilterValue({ + token: { + type: Token.VALUE_TEXT, + text: 'foo\\*bar', + value: 'foo\\*bar', + quoted: false, + } as any, + }); + const wildcard = formatFilterValue({ + token: { + type: Token.VALUE_TEXT, + text: 'foo*bar', + value: 'foo*bar', + quoted: false, + } as any, + }); + + expect(escaped).toBe('foo\\*bar'); + expect(wildcard).toBe('foo*bar'); + expect(escaped).not.toBe(wildcard); + }); + + it('preserves a literal backslash before a wildcard asterisk', () => { + expect( + formatFilterValue({ + token: { + type: Token.VALUE_TEXT, + text: 'foo\\\\*bar', + value: 'foo\\\\*bar', + quoted: false, + } as any, + }) + ).toBe('foo\\\\*bar'); }); }); diff --git a/static/app/components/searchQueryBuilder/tokens/filter/utils.tsx b/static/app/components/searchQueryBuilder/tokens/filter/utils.tsx index 9443019a7ca0..ab501dba92e4 100644 --- a/static/app/components/searchQueryBuilder/tokens/filter/utils.tsx +++ b/static/app/components/searchQueryBuilder/tokens/filter/utils.tsx @@ -209,9 +209,7 @@ export function formatFilterValue({ return content; } - return unescapeAsteriskSearchValue( - token.quoted ? unescapeTagValue(content) : content - ); + return token.quoted ? unescapeTagValue(content) : content; } case Token.VALUE_RELATIVE_DATE: return t('%s', `${token.value}${token.unit} ago`); From 188563badf44881c82d76a25b2e23120639872a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Thu, 18 Jun 2026 11:27:03 -0400 Subject: [PATCH 2/2] pnpm fix:format --- static/app/components/searchQueryBuilder/index.spec.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/static/app/components/searchQueryBuilder/index.spec.tsx b/static/app/components/searchQueryBuilder/index.spec.tsx index af99364d480a..a24b28879285 100644 --- a/static/app/components/searchQueryBuilder/index.spec.tsx +++ b/static/app/components/searchQueryBuilder/index.spec.tsx @@ -2761,9 +2761,7 @@ describe('SearchQueryBuilder', () => { }); it('renders a wildcard asterisk without an escape in the filter chip', async () => { - render( - - ); + render(); expect( await within(