From a402c3128434a231ae1b3696bf8cfbc6ecdc5a3c Mon Sep 17 00:00:00 2001 From: Richard Roggenkemper Date: Tue, 26 May 2026 12:06:19 -0400 Subject: [PATCH 1/4] feat(search): Add recommended sort option to issue stream dropdown Gate the experimental recommended sort behind the issue-stream-recommended-sort feature flag. Also show the option when ?sort=recommended is already in the URL for manual testing. Co-Authored-By: Claude --- static/app/views/issueList/actions/sortOptions.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/static/app/views/issueList/actions/sortOptions.tsx b/static/app/views/issueList/actions/sortOptions.tsx index 719f9551040e..fdd2b026d9b9 100644 --- a/static/app/views/issueList/actions/sortOptions.tsx +++ b/static/app/views/issueList/actions/sortOptions.tsx @@ -4,6 +4,7 @@ import {OverlayTrigger} from '@sentry/scraps/overlayTrigger'; import type {DropdownButtonProps} from 'sentry/components/dropdownButton'; import {IconSort} from 'sentry/icons/iconSort'; import {t} from 'sentry/locale'; +import useOrganization from 'sentry/utils/useOrganization'; import { FOR_REVIEW_QUERIES, getSortLabel, @@ -47,6 +48,10 @@ export function IssueListSortOptions({ triggerSize = 'xs', showIcon = true, }: Props) { + const organization = useOrganization(); + const hasRecommendedSort = + organization.features.includes('issue-stream-recommended-sort') || + sort === IssueSortOptions.RECOMMENDED; const sortKey = sort || IssueSortOptions.DATE; const sortKeys = [ ...(FOR_REVIEW_QUERIES.includes(query || '') ? [IssueSortOptions.INBOX] : []), @@ -55,7 +60,7 @@ export function IssueListSortOptions({ IssueSortOptions.TRENDS, IssueSortOptions.FREQ, IssueSortOptions.USER, - ...(sort === IssueSortOptions.RECOMMENDED ? [IssueSortOptions.RECOMMENDED] : []), + ...(hasRecommendedSort ? [IssueSortOptions.RECOMMENDED] : []), ]; return ( From e5f909ffcf015275413b3de5711b68ce8360ce5e Mon Sep 17 00:00:00 2001 From: Richard Roggenkemper Date: Tue, 26 May 2026 13:40:09 -0400 Subject: [PATCH 2/4] fix(search): Use named import for useOrganization Co-Authored-By: Claude --- static/app/views/issueList/actions/sortOptions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/views/issueList/actions/sortOptions.tsx b/static/app/views/issueList/actions/sortOptions.tsx index fdd2b026d9b9..6b8d5538f544 100644 --- a/static/app/views/issueList/actions/sortOptions.tsx +++ b/static/app/views/issueList/actions/sortOptions.tsx @@ -4,7 +4,7 @@ import {OverlayTrigger} from '@sentry/scraps/overlayTrigger'; import type {DropdownButtonProps} from 'sentry/components/dropdownButton'; import {IconSort} from 'sentry/icons/iconSort'; import {t} from 'sentry/locale'; -import useOrganization from 'sentry/utils/useOrganization'; +import {useOrganization} from 'sentry/utils/useOrganization'; import { FOR_REVIEW_QUERIES, getSortLabel, From 8295861590a6f02a4762199feab9cbc63c2450f8 Mon Sep 17 00:00:00 2001 From: Richard Roggenkemper Date: Wed, 27 May 2026 13:23:13 -0400 Subject: [PATCH 3/4] feat(issues): Persist sort selection and default to recommended When the issue-stream-recommended-sort flag is enabled, default the sort to "recommended" instead of "last seen". Persist the user's sort choice in localStorage so it survives navigation. Co-Authored-By: Claude --- static/app/views/issueList/overview.tsx | 16 +++++++++++----- static/app/views/issueList/utils.tsx | 19 +++++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/static/app/views/issueList/overview.tsx b/static/app/views/issueList/overview.tsx index 655d1bc5256f..53b8a9e57d28 100644 --- a/static/app/views/issueList/overview.tsx +++ b/static/app/views/issueList/overview.tsx @@ -68,6 +68,8 @@ import { DEFAULT_ISSUE_STREAM_SORT, DEFAULT_QUERY, FOR_REVIEW_QUERIES, + getStoredIssueSort, + setStoredIssueSort, isForReviewQuery, IssueSortOptions, Query, @@ -213,10 +215,13 @@ function IssueListOverviewInner({ const query = defined(location.query.query) ? (location.query.query as string) : initialQuery; - const sort = decodeScalar( - location.query.sort, - DEFAULT_ISSUE_STREAM_SORT - ) as IssueSortOptions; + const hasRecommendedSort = organization.features.includes( + 'issue-stream-recommended-sort' + ); + const defaultSort = hasRecommendedSort + ? (getStoredIssueSort(organization.slug) ?? IssueSortOptions.RECOMMENDED) + : DEFAULT_ISSUE_STREAM_SORT; + const sort = decodeScalar(location.query.sort, defaultSort) as IssueSortOptions; const getGroupStatsPeriod = useCallback((): string => { let currentPeriod: string; @@ -250,7 +255,7 @@ function IssueListOverviewInner({ params.start = getUtcDateString(params.start); } - if (sort !== DEFAULT_ISSUE_STREAM_SORT) { + if (sort !== IssueSortOptions.DATE) { params.sort = sort; } @@ -675,6 +680,7 @@ function IssueListOverviewInner({ organization, sort: newSort, }); + setStoredIssueSort(organization.slug, newSort as IssueSortOptions); transitionTo({sort: newSort}); }; diff --git a/static/app/views/issueList/utils.tsx b/static/app/views/issueList/utils.tsx index 2655ad97f341..4ceb4c7e7f52 100644 --- a/static/app/views/issueList/utils.tsx +++ b/static/app/views/issueList/utils.tsx @@ -4,6 +4,7 @@ import {t} from 'sentry/locale'; import type {Event} from 'sentry/types/event'; import type {Group} from 'sentry/types/group'; import type {Organization} from 'sentry/types/organization'; +import {localStorageWrapper} from 'sentry/utils/localStorage'; export const DEFAULT_QUERY = 'is:unresolved issue.priority:[high, medium]'; @@ -81,6 +82,24 @@ export const FOR_REVIEW_QUERIES: string[] = [Query.FOR_REVIEW]; export const SAVED_SEARCHES_SIDEBAR_OPEN_LOCALSTORAGE_KEY = 'issue-stream-saved-searches-sidebar-open'; +const ISSUE_STREAM_SORT_LOCALSTORAGE_KEY = 'issue-stream-sort'; + +function makeSortStorageKey(orgSlug: string): string { + return `${ISSUE_STREAM_SORT_LOCALSTORAGE_KEY}:${orgSlug}`; +} + +export function getStoredIssueSort(orgSlug: string): IssueSortOptions | null { + const value = localStorageWrapper.getItem(makeSortStorageKey(orgSlug)); + if (value && Object.values(IssueSortOptions).includes(value as IssueSortOptions)) { + return value as IssueSortOptions; + } + return null; +} + +export function setStoredIssueSort(orgSlug: string, sort: IssueSortOptions): void { + localStorageWrapper.setItem(makeSortStorageKey(orgSlug), sort); +} + export function createIssueLink({ organization, data, From c8889853b978c1eff1bc3f5e2d42167d8334b3a4 Mon Sep 17 00:00:00 2001 From: Richard Roggenkemper Date: Tue, 16 Jun 2026 15:31:48 -0700 Subject: [PATCH 4/4] fix(issues): Only persist sort to localStorage when recommended sort is enabled onSortChange wrote the selected sort to localStorage unconditionally, but the default sort only reads that stored value when the issue-stream-recommended-sort flag is on. A user who changed sort before getting the flag would have a stale value persisted, and once the flag turned on it would override the intended Recommended default. Gate the write on the same flag as the read so the conditions stay symmetric. Co-Authored-By: Claude --- static/app/views/issueList/overview.spec.tsx | 32 +++++++++++++++++++- static/app/views/issueList/overview.tsx | 4 ++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/static/app/views/issueList/overview.spec.tsx b/static/app/views/issueList/overview.spec.tsx index e29641529cbf..79175b0f2d71 100644 --- a/static/app/views/issueList/overview.spec.tsx +++ b/static/app/views/issueList/overview.spec.tsx @@ -17,7 +17,11 @@ import {TagStore} from 'sentry/stores/tagStore'; import {localStorageWrapper} from 'sentry/utils/localStorage'; import * as parseLinkHeaderModule from 'sentry/utils/parseLinkHeader'; import IssueListOverview from 'sentry/views/issueList/overview'; -import {DEFAULT_QUERY} from 'sentry/views/issueList/utils'; +import { + DEFAULT_QUERY, + getStoredIssueSort, + IssueSortOptions, +} from 'sentry/views/issueList/utils'; const DEFAULT_LINKS_HEADER = '; rel="previous"; results="false"; cursor="1443575731:0:1", ' + @@ -350,6 +354,32 @@ describe('IssueList', () => { }); }); + describe('sort persistence', () => { + it('does not persist sort to localStorage without the recommended-sort feature', async () => { + render(, {organization, initialRouterConfig}); + + await userEvent.click(await screen.findByRole('button', {name: 'Last Seen'})); + await userEvent.click(screen.getByRole('option', {name: 'Events'})); + + // Writing while the feature is off would leave a stale value that overrides + // the Recommended default once the flag is enabled. + expect(getStoredIssueSort(organization.slug)).toBeNull(); + }); + + it('persists sort to localStorage with the recommended-sort feature', async () => { + const featureOrg = OrganizationFixture({ + ...organization, + features: ['issue-stream-recommended-sort'], + }); + render(, {organization: featureOrg, initialRouterConfig}); + + await userEvent.click(await screen.findByRole('button', {name: 'Recommended'})); + await userEvent.click(screen.getByRole('option', {name: 'Events'})); + + expect(getStoredIssueSort(featureOrg.slug)).toBe(IssueSortOptions.FREQ); + }); + }); + describe('transitionTo', () => { it('pushes to history when query is updated', async () => { MockApiClient.addMockResponse({ diff --git a/static/app/views/issueList/overview.tsx b/static/app/views/issueList/overview.tsx index 69d849cb419f..6d4f4816bd19 100644 --- a/static/app/views/issueList/overview.tsx +++ b/static/app/views/issueList/overview.tsx @@ -695,7 +695,9 @@ function IssueListOverviewInner({ organization, sort: newSort, }); - setStoredIssueSort(organization.slug, newSort as IssueSortOptions); + if (hasRecommendedSort) { + setStoredIssueSort(organization.slug, newSort as IssueSortOptions); + } transitionTo({sort: newSort}); };