Skip to content

feat(page-filters): Use fzf for project search in ProjectPageFilter#108725

Merged
JonasBa merged 21 commits intomasterfrom
jb/compactselect/project-fzf-search
Feb 23, 2026
Merged

feat(page-filters): Use fzf for project search in ProjectPageFilter#108725
JonasBa merged 21 commits intomasterfrom
jb/compactselect/project-fzf-search

Conversation

@JonasBa
Copy link
Member

@JonasBa JonasBa commented Feb 20, 2026

Use fzf for project search in ProjectPageFilter

The project dropdown previously used a simple case-insensitive substring match
on the project slug. This replaces it with the fzf v1 algorithm (already present
in the codebase at sentry/utils/profiling/fzf/fzf) via the searchMatcher prop
introduced in the base branch.

With fzf, users can find projects using fuzzy/subsequence queries — e.g. typing
frd will match frontend — and results are ranked by match quality so the most
relevant projects float to the top.

The matcher is defined as a module-level function (no useCallback needed) since
it has no dependency on component state or props.

Stacked on top of: master...jb/compactselect/search-result

JonasBa and others added 6 commits February 20, 2026 10:53
Allow callers to provide a custom match function via the new
searchMatcher prop. When provided, it replaces the built-in
case-insensitive substring matching so callers can implement
arbitrary logic (e.g. suffix matching, fuzzy search, matching on
non-label fields).

The function receives the full option object and the current search
string, returning true when the option should be visible.

Co-Authored-By: Claude <noreply@anthropic.com>
Extend the searchMatcher prop so it can return a SearchMatchResult
object instead of a plain boolean. The result's score field is used to
sort matching options — higher scores appear first.

Returning a SearchMatchResult always means the option is a match.
Returning false still hides the option. Returning true shows it with
no ordering influence.

Sorting is applied within sections for sectioned lists, and globally
for flat lists. Options without a score maintain their original
relative order.

Co-Authored-By: Claude <noreply@anthropic.com>
Remove the boolean return path from searchMatcher. The function must now
always return a SearchMatchResult. A score > 0 means the option matches;
score <= 0 hides it. Higher scores sort first.

The default implementation returns {score: 1} for a substring match and
{score: 0} for no match, preserving the existing behaviour. Sorting is
only triggered when a custom searchMatcher is provided, so the default
path pays no sorting cost.

Co-Authored-By: Claude <noreply@anthropic.com>
…rn type

getHiddenOptions was changed to return {hidden, scores} instead of a
plain Set, but two callers in searchQueryBuilder and tokenizedInput
were not updated, causing TypeScript errors.

Co-Authored-By: Claude <noreply@anthropic.com>
Replace the default substring search with the fzf v1 algorithm so that
project slugs are matched by subsequence with proper scoring. This means
users can find projects with fuzzy/abbreviated queries (e.g. "frd" matches
"frontend") and results are ranked by match quality.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions bot added the Scope: Frontend Automatically applied to PRs that change frontend components label Feb 20, 2026
Project slugs are always lowercase, so case-sensitive matching avoids
unnecessary case folding and ensures input casing is preserved as-is.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
JonasBa and others added 7 commits February 20, 2026 12:04
Add tests verifying that:
- clearing the search input restores the full option list
- closing and reopening the menu resets the search query and shows all options

Both ListBox and GridList variants are covered.

Co-Authored-By: Claude <noreply@anthropic.com>
Extend the searchMatcher prop so it can return a SearchMatchResult
object instead of a plain boolean. The result's score field is used to
sort matching options — higher scores appear first.

Returning a SearchMatchResult always means the option is a match.
Returning false still hides the option. Returning true shows it with
no ordering influence.

Sorting is applied within sections for sectioned lists, and globally
for flat lists. Options without a score maintain their original
relative order.

Co-Authored-By: Claude <noreply@anthropic.com>
Remove the boolean return path from searchMatcher. The function must now
always return a SearchMatchResult. A score > 0 means the option matches;
score <= 0 hides it. Higher scores sort first.

The default implementation returns {score: 1} for a substring match and
{score: 0} for no match, preserving the existing behaviour. Sorting is
only triggered when a custom searchMatcher is provided, so the default
path pays no sorting cost.

Co-Authored-By: Claude <noreply@anthropic.com>
…rn type

getHiddenOptions was changed to return {hidden, scores} instead of a
plain Set, but two callers in searchQueryBuilder and tokenizedInput
were not updated, causing TypeScript errors.

Co-Authored-By: Claude <noreply@anthropic.com>
@JonasBa JonasBa force-pushed the jb/compactselect/search-result branch from 0d7131d to 7474bcf Compare February 20, 2026 20:24
…nt options visible

When both sizeLimit and a custom searchMatcher with varying scores are
active, the size limit was applied based on original item order before
score sorting. Lower-scored items appearing early in the list would
stay visible while higher-scored items beyond the limit were hidden,
defeating the purpose of score-based ranking.

Sort remaining items by score before applying the size limit so the
most relevant results are always the ones kept visible.

Co-Authored-By: Claude <noreply@anthropic.com>
@JonasBa JonasBa marked this pull request as ready for review February 20, 2026 21:12
@JonasBa JonasBa requested a review from a team as a code owner February 20, 2026 21:12
import type {Project} from 'sentry/types/project';
import {trackAnalytics} from 'sentry/utils/analytics';
import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
import {fzf} from 'sentry/utils/profiling/fzf/fzf';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of scope for now, but this seems to have propagate enough to graduate from utils/profiling

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me move it out and update the search interface as well

Base automatically changed from jb/compactselect/search-result to master February 23, 2026 21:15
@JonasBa JonasBa enabled auto-merge (squash) February 23, 2026 21:18
@JonasBa JonasBa merged commit 280cdd2 into master Feb 23, 2026
63 checks passed
@JonasBa JonasBa deleted the jb/compactselect/project-fzf-search branch February 23, 2026 21:24
Copy link
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

};
}
return item;
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mixed section sorting fails for top-level options

Low Severity

In getSortedItems, when hasSections is true, the function sorts options within sections but returns non-section items unchanged. If an array contains both sections and top-level options, those top-level options won't be sorted by score despite having custom matcher scores. The type SelectOptionOrSectionWithKey allows mixed arrays, but the sorting logic assumes items are either all sections or all flat options.

Fix in Cursor Fix in Web

// (more relevant) items are kept visible when the limit is reached.
//
const orderedRemainingItems =
scores.size > 0 ? getSortedItems(remainingItems, scores) : remainingItems;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sizeLimit doesn't prioritize highest scores across sections

Medium Severity

When getSortedItems processes sectioned lists, it only sorts options within each section, not across sections. This means sizeLimit can hide high-scoring options in later sections while showing low-scoring options in earlier sections. For example, with sections A (3 items, scores 1-3) and B (2 items, scores 50-100), a sizeLimit of 4 would show all of A (scores 1-3) and one from B (score 100), hiding the item with score 50 despite it being more relevant than items with scores 1-3.

Fix in Cursor Fix in Web

mchen-sentry pushed a commit that referenced this pull request Feb 24, 2026
…108725)

Use fzf for project search in ProjectPageFilter

The project dropdown previously used a simple case-insensitive substring
match
on the project slug. This replaces it with the fzf v1 algorithm (already
present
in the codebase at `sentry/utils/profiling/fzf/fzf`) via the
`searchMatcher` prop
introduced in the base branch.

With fzf, users can find projects using fuzzy/subsequence queries — e.g.
typing
`frd` will match `frontend` — and results are ranked by match quality so
the most
relevant projects float to the top.

The matcher is defined as a module-level function (no `useCallback`
needed) since
it has no dependency on component state or props.

Stacked on top of:
master...jb/compactselect/search-result

---------

Co-authored-by: Claude <noreply@anthropic.com>
wedamija pushed a commit that referenced this pull request Feb 24, 2026
…108725)

Use fzf for project search in ProjectPageFilter

The project dropdown previously used a simple case-insensitive substring
match
on the project slug. This replaces it with the fzf v1 algorithm (already
present
in the codebase at `sentry/utils/profiling/fzf/fzf`) via the
`searchMatcher` prop
introduced in the base branch.

With fzf, users can find projects using fuzzy/subsequence queries — e.g.
typing
`frd` will match `frontend` — and results are ranked by match quality so
the most
relevant projects float to the top.

The matcher is defined as a module-level function (no `useCallback`
needed) since
it has no dependency on component state or props.

Stacked on top of:
master...jb/compactselect/search-result

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude-code-assisted Scope: Frontend Automatically applied to PRs that change frontend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants