feat(page-filters): Use fzf for project search in ProjectPageFilter#108725
feat(page-filters): Use fzf for project search in ProjectPageFilter#108725
Conversation
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>
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>
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>
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>
0d7131d to
7474bcf
Compare
…roject-fzf-search
…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>
static/app/components/pageFilters/project/projectPageFilter.tsx
Outdated
Show resolved
Hide resolved
static/app/components/pageFilters/project/projectPageFilter.tsx
Outdated
Show resolved
Hide resolved
static/app/components/pageFilters/project/projectPageFilter.tsx
Outdated
Show resolved
Hide resolved
| 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'; |
There was a problem hiding this comment.
Out of scope for now, but this seems to have propagate enough to graduate from utils/profiling
There was a problem hiding this comment.
Let me move it out and update the search interface as well
There was a problem hiding this comment.
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; | ||
| }); |
There was a problem hiding this comment.
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.
| // (more relevant) items are kept visible when the limit is reached. | ||
| // | ||
| const orderedRemainingItems = | ||
| scores.size > 0 ? getSortedItems(remainingItems, scores) : remainingItems; |
There was a problem hiding this comment.
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.
…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>
…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>


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 thesearchMatcherpropintroduced in the base branch.
With fzf, users can find projects using fuzzy/subsequence queries — e.g. typing
frdwill matchfrontend— and results are ranked by match quality so the mostrelevant projects float to the top.
The matcher is defined as a module-level function (no
useCallbackneeded) sinceit has no dependency on component state or props.
Stacked on top of: master...jb/compactselect/search-result