From 87c8fc770895a5a94ddab4e5460679045d7db994 Mon Sep 17 00:00:00 2001 From: "sentry[bot]" <39604003+sentry[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:48:08 +0000 Subject: [PATCH 1/6] fix(api-conventions): Remove callback API requests from group action creators --- static/app/actionCreators/group.tsx | 40 ++----- .../app/views/issueDetails/actions/index.tsx | 47 ++++---- .../views/issueDetails/actions/shareModal.tsx | 47 ++++---- .../app/views/issueDetails/groupPriority.tsx | 49 ++++---- .../similarStackTrace/index.tsx | 20 +--- static/app/views/issueDetails/utils.tsx | 18 ++- static/app/views/issueList/actions/index.tsx | 20 ++-- static/app/views/issueList/actions/utils.tsx | 41 +++---- .../issuePreview/issuePreviewActions.tsx | 108 +++++++++--------- 9 files changed, 161 insertions(+), 229 deletions(-) diff --git a/static/app/actionCreators/group.tsx b/static/app/actionCreators/group.tsx index 43c3e3737f50..dcfb4afadb82 100644 --- a/static/app/actionCreators/group.tsx +++ b/static/app/actionCreators/group.tsx @@ -1,6 +1,5 @@ import {queryOptions} from '@tanstack/react-query'; -import type {RequestCallbacks} from 'sentry/api'; import {Client} from 'sentry/api'; import {GroupStore} from 'sentry/stores/groupStore'; import type {Group, Tag as GroupTag, TagValue} from 'sentry/types/group'; @@ -121,11 +120,7 @@ type BulkUpdateParams = UpdateParams & { failSilently?: boolean; }; -export async function bulkUpdate( - api: Client, - params: BulkUpdateParams, - options: RequestCallbacks = {} -) { +export async function bulkUpdate(api: Client, params: BulkUpdateParams) { const {itemIds, failSilently, data} = params; const path = getUpdateUrl(params); @@ -138,35 +133,22 @@ export async function bulkUpdate( : data; GroupStore.onUpdate(id, itemIds, optimisticData); - let responseMeta: any; - let statusText: string | undefined; - try { - const [response, status, meta] = await api.requestPromise(path, { + const response = await api.requestPromise(path, { query, method: 'PUT', data, - includeAllArgs: true, }); - statusText = status; - responseMeta = meta; GroupStore.onUpdateSuccess(id, itemIds, response); - options?.success?.(response, statusText, responseMeta); } catch (error) { GroupStore.onUpdateError(id, itemIds, !!failSilently); - options?.error?.(error); - } finally { - options?.complete?.(responseMeta, statusText ?? ''); + throw error; } } type MergeGroupsParams = UpdateParams; -export async function mergeGroups( - api: Client, - params: MergeGroupsParams, - options: RequestCallbacks = {} -) { +export async function mergeGroups(api: Client, params: MergeGroupsParams) { const {itemIds} = params; const path = getUpdateUrl(params); @@ -175,25 +157,17 @@ export async function mergeGroups( GroupStore.onMerge(id, itemIds); - let responseMeta: any; - let statusText: string | undefined; - try { - const [response, status, meta] = await api.requestPromise(path, { + const response = await api.requestPromise(path, { query, method: 'PUT', data: {merge: 1}, - includeAllArgs: true, }); - statusText = status; - responseMeta = meta; GroupStore.onMergeSuccess(id, itemIds, response); - options?.success?.(response, statusText, responseMeta); + return response; } catch (error) { GroupStore.onMergeError(id, itemIds, error); - options?.error?.(error); - } finally { - options?.complete?.(responseMeta, statusText ?? ''); + throw error; } } diff --git a/static/app/views/issueDetails/actions/index.tsx b/static/app/views/issueDetails/actions/index.tsx index 9024595ae706..354549f0e800 100644 --- a/static/app/views/issueDetails/actions/index.tsx +++ b/static/app/views/issueDetails/actions/index.tsx @@ -288,32 +288,27 @@ export function GroupActions({group, project, disabled, event}: GroupActionsProp const onUpdate = (data: UpdateData, onComplete?: () => void) => { const successMessage = getUpdateSuccessMessage(group, data); - bulkUpdate( - api, - { - orgId: organization.slug, - projectId: project.slug, - itemIds: [group.id], - data, - }, - { - success: () => { - clearIndicators(); - if (successMessage) { - addSuccessMessage(successMessage); - } - onComplete?.(); - }, - complete: () => { - queryClient.invalidateQueries({ - queryKey: groupQueryKey({ - organizationSlug: organization.slug, - groupId: group.id, - }), - }); - }, - } - ); + bulkUpdate(api, { + orgId: organization.slug, + projectId: project.slug, + itemIds: [group.id], + data, + }) + .then(() => { + clearIndicators(); + if (successMessage) { + addSuccessMessage(successMessage); + } + onComplete?.(); + }) + .finally(() => { + queryClient.invalidateQueries({ + queryKey: groupQueryKey({ + organizationSlug: organization.slug, + groupId: group.id, + }), + }); + }); if (isResolutionStatus(data)) { trackIssueAction( diff --git a/static/app/views/issueDetails/actions/shareModal.tsx b/static/app/views/issueDetails/actions/shareModal.tsx index 48d9ed461f97..5aae3c4054cb 100644 --- a/static/app/views/issueDetails/actions/shareModal.tsx +++ b/static/app/views/issueDetails/actions/shareModal.tsx @@ -101,33 +101,28 @@ export function ShareIssueModal({ e?.preventDefault(); setLoading(true); onToggle(); - bulkUpdate( - api, - { - orgId: organization.slug, - projectId: projectSlug, - itemIds: [groupId], - data: { - isPublic: reshare ?? !isPublished, - }, + bulkUpdate(api, { + orgId: organization.slug, + projectId: projectSlug, + itemIds: [groupId], + data: { + isPublic: reshare ?? !isPublished, }, - { - success: () => { - queryClient.invalidateQueries({ - queryKey: groupQueryKey({ - organizationSlug: organization.slug, - groupId, - }), - }); - }, - error: () => { - addErrorMessage(t('Error sharing')); - }, - complete: () => { - setLoading(false); - }, - } - ); + }) + .then(() => { + queryClient.invalidateQueries({ + queryKey: groupQueryKey({ + organizationSlug: organization.slug, + groupId, + }), + }); + }) + .catch(() => { + addErrorMessage(t('Error sharing')); + }) + .finally(() => { + setLoading(false); + }); }; const shareUrl = group?.shareId ? getShareUrl(organization, group) : null; diff --git a/static/app/views/issueDetails/groupPriority.tsx b/static/app/views/issueDetails/groupPriority.tsx index 24e33ba124b8..b13bd587de29 100644 --- a/static/app/views/issueDetails/groupPriority.tsx +++ b/static/app/views/issueDetails/groupPriority.tsx @@ -53,33 +53,28 @@ function useChangePriority(group: Group, onChange?: (priority: PriorityLevel) => addLoadingMessage(t('Saving changes\u2026')); IssueListCacheStore.reset(); - bulkUpdate( - api, - { - orgId: organization.slug, - itemIds: [group.id], - data: {priority: nextPriority}, - failSilently: true, - project: [group.project.id], - }, - { - success: () => { - queryClient.invalidateQueries({ - queryKey: groupQueryKey({ - organizationSlug: organization.slug, - groupId: group.id, - }), - }); - clearIndicators(); - addSuccessMessage(getPriorityUpdateSuccessMessage(nextPriority)); - onChange?.(nextPriority); - }, - error: () => { - clearIndicators(); - addErrorMessage(t('Unable to update issue priority')); - }, - } - ); + bulkUpdate(api, { + orgId: organization.slug, + itemIds: [group.id], + data: {priority: nextPriority}, + failSilently: true, + project: [group.project.id], + }) + .then(() => { + queryClient.invalidateQueries({ + queryKey: groupQueryKey({ + organizationSlug: organization.slug, + groupId: group.id, + }), + }); + clearIndicators(); + addSuccessMessage(getPriorityUpdateSuccessMessage(nextPriority)); + onChange?.(nextPriority); + }) + .catch(() => { + clearIndicators(); + addErrorMessage(t('Unable to update issue priority')); + }); }; } diff --git a/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/index.tsx b/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/index.tsx index 3d6849ccb40c..0c42bfecf512 100644 --- a/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/index.tsx +++ b/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/index.tsx @@ -94,21 +94,11 @@ export function SimilarStackTrace({project}: Props) { {ids: string[]; projectSlug: string; query?: string} >({ mutationFn: ({ids, projectSlug, query}) => - new Promise((resolve, reject) => { - mergeGroups( - api, - { - orgId: organization.slug, - projectId: projectSlug, - itemIds: [...ids, params.groupId], - query, - }, - { - success: (data: any) => resolve(data), - error: (err: any) => - reject(err instanceof Error ? err : new Error('Failed to merge issues')), - } - ); + mergeGroups(api, { + orgId: organization.slug, + projectId: projectSlug, + itemIds: [...ids, params.groupId], + query, }), onSuccess: data => { if (data?.merge?.parent && data.merge.parent !== params.groupId) { diff --git a/static/app/views/issueDetails/utils.tsx b/static/app/views/issueDetails/utils.tsx index c463db0de628..c79624aaac55 100644 --- a/static/app/views/issueDetails/utils.tsx +++ b/static/app/views/issueDetails/utils.tsx @@ -28,17 +28,13 @@ export function markEventSeen( projectId: string, groupId: string ) { - bulkUpdate( - api, - { - orgId, - projectId, - itemIds: [groupId], - failSilently: true, - data: {hasSeen: true}, - }, - {} - ); + bulkUpdate(api, { + orgId, + projectId, + itemIds: [groupId], + failSilently: true, + data: {hasSeen: true}, + }).catch(() => {}); IssueListCacheStore.markGroupAsSeen(groupId); } diff --git a/static/app/views/issueList/actions/index.tsx b/static/app/views/issueList/actions/index.tsx index aa7343436264..b58d0496e47c 100644 --- a/static/app/views/issueList/actions/index.tsx +++ b/static/app/views/issueList/actions/index.tsx @@ -223,18 +223,14 @@ export function IssueListActions({ function handleMerge() { actionSelectedGroups(itemIds => { - mergeGroups( - api, - { - orgId: organization.slug, - itemIds, - query: queryExcludingPerformanceIssues, - project: selection.projects, - environment: selection.environments, - ...selection.datetime, - }, - {} - ); + mergeGroups(api, { + orgId: organization.slug, + itemIds, + query: queryExcludingPerformanceIssues, + project: selection.projects, + environment: selection.environments, + ...selection.datetime, + }).catch(() => {}); if (selection.projects[0]) { const trackProject = ProjectsStore.getById(`${selection.projects[0]}`); trackAnalytics('issues_stream.merged', { diff --git a/static/app/views/issueList/actions/utils.tsx b/static/app/views/issueList/actions/utils.tsx index 6b646db31357..02c4a393dfac 100644 --- a/static/app/views/issueList/actions/utils.tsx +++ b/static/app/views/issueList/actions/utils.tsx @@ -257,27 +257,22 @@ export function performBulkUpdate({ addLoadingMessage(t('Saving changes…')); - bulkUpdate( - api, - { - orgId: organizationSlug, - itemIds, - data, - query, - environment: selection.environments, - failSilently: true, - ...projectConstraints, - ...selection.datetime, - }, - { - success: () => { - clearIndicators(); - onSuccess?.(itemIds); - }, - error: () => { - clearIndicators(); - addErrorMessage(t('Unable to update issues')); - }, - } - ); + bulkUpdate(api, { + orgId: organizationSlug, + itemIds, + data, + query, + environment: selection.environments, + failSilently: true, + ...projectConstraints, + ...selection.datetime, + }) + .then(() => { + clearIndicators(); + onSuccess?.(itemIds); + }) + .catch(() => { + clearIndicators(); + addErrorMessage(t('Unable to update issues')); + }); } diff --git a/static/app/views/issueList/pages/inbox/issuePreview/issuePreviewActions.tsx b/static/app/views/issueList/pages/inbox/issuePreview/issuePreviewActions.tsx index bc8f1e118891..ef8c899c5dbf 100644 --- a/static/app/views/issueList/pages/inbox/issuePreview/issuePreviewActions.tsx +++ b/static/app/views/issueList/pages/inbox/issuePreview/issuePreviewActions.tsx @@ -149,63 +149,59 @@ function IssueResolutionActions({ const location = useLocation(); const queryClient = useQueryClient(); function handleUpdate(data: GroupStatusResolution) { - bulkUpdate( - api, - { - orgId: organization.slug, - projectId: project.slug, - itemIds: [group.id], - data, - }, - { - success: () => { - clearIndicators(); - addSuccessMessage( - data.status === GroupStatus.UNRESOLVED - ? t('Issue marked unresolved') - : t('Issue resolved') - ); - IssueListCacheStore.reset(); - const issueListUrl = getApiUrl('/organizations/$organizationIdOrSlug/issues/', { - path: {organizationIdOrSlug: organization.slug}, - }); - const issueCountUrl = getApiUrl( - '/organizations/$organizationIdOrSlug/issues-count/', - {path: {organizationIdOrSlug: organization.slug}} - ); - const issueUrl = getApiUrl( - '/organizations/$organizationIdOrSlug/issues/$issueId/', - { - path: { - organizationIdOrSlug: organization.slug, - issueId: group.id, - }, - } - ); - const issueActivitiesUrl = getApiUrl( - '/organizations/$organizationIdOrSlug/issues/$issueId/activities/', - { - path: { - organizationIdOrSlug: organization.slug, - issueId: group.id, - }, - } - ); - void queryClient.invalidateQueries({ - predicate: query => { - const url = safeParseQueryKey(query.queryKey)?.url; - - return ( - url === issueListUrl || - url === issueCountUrl || - url === issueUrl || - url === issueActivitiesUrl - ); + bulkUpdate(api, { + orgId: organization.slug, + projectId: project.slug, + itemIds: [group.id], + data, + }) + .then(() => { + clearIndicators(); + addSuccessMessage( + data.status === GroupStatus.UNRESOLVED + ? t('Issue marked unresolved') + : t('Issue resolved') + ); + IssueListCacheStore.reset(); + const issueListUrl = getApiUrl('/organizations/$organizationIdOrSlug/issues/', { + path: {organizationIdOrSlug: organization.slug}, + }); + const issueCountUrl = getApiUrl( + '/organizations/$organizationIdOrSlug/issues-count/', + {path: {organizationIdOrSlug: organization.slug}} + ); + const issueUrl = getApiUrl( + '/organizations/$organizationIdOrSlug/issues/$issueId/', + { + path: { + organizationIdOrSlug: organization.slug, + issueId: group.id, }, - }); - }, - } - ); + } + ); + const issueActivitiesUrl = getApiUrl( + '/organizations/$organizationIdOrSlug/issues/$issueId/activities/', + { + path: { + organizationIdOrSlug: organization.slug, + issueId: group.id, + }, + } + ); + void queryClient.invalidateQueries({ + predicate: query => { + const url = safeParseQueryKey(query.queryKey)?.url; + + return ( + url === issueListUrl || + url === issueCountUrl || + url === issueUrl || + url === issueActivitiesUrl + ); + }, + }); + }) + .catch(() => {}); const {alert_date, alert_rule_id, alert_type} = location.query; trackAnalytics('issue_inbox.resolve_clicked', { From b20ee866207de7ea39cc8e36dbd3dd0f203ff34f Mon Sep 17 00:00:00 2001 From: "sentry[bot]" <39604003+sentry[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:52:04 +0000 Subject: [PATCH 2/6] fix(api-callbacks): Migrate group action creators to promises --- static/app/views/issueDetails/actions/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/static/app/views/issueDetails/actions/index.tsx b/static/app/views/issueDetails/actions/index.tsx index 354549f0e800..6a1e0fe7b3e4 100644 --- a/static/app/views/issueDetails/actions/index.tsx +++ b/static/app/views/issueDetails/actions/index.tsx @@ -301,6 +301,7 @@ export function GroupActions({group, project, disabled, event}: GroupActionsProp } onComplete?.(); }) + .catch(() => {}) .finally(() => { queryClient.invalidateQueries({ queryKey: groupQueryKey({ From 92e46c1f244973302fe9a936aacc5a3faece8344 Mon Sep 17 00:00:00 2001 From: "sentry[bot]" <39604003+sentry[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2026 23:02:46 +0000 Subject: [PATCH 3/6] ref(group): Migrate bulkUpdate and mergeGroups to async/await --- static/app/actionCreators/group.spec.tsx | 148 +++++++++-------------- static/app/actionCreators/group.tsx | 1 + 2 files changed, 57 insertions(+), 92 deletions(-) diff --git a/static/app/actionCreators/group.spec.tsx b/static/app/actionCreators/group.spec.tsx index 7d0f7fe2c982..0601ac42103e 100644 --- a/static/app/actionCreators/group.spec.tsx +++ b/static/app/actionCreators/group.spec.tsx @@ -84,17 +84,13 @@ describe('group', () => { method: 'PUT', }); - bulkUpdate( - new MockApiClient(), - { - orgId: '1337', - projectId: '1337', - itemIds: ['1', '2', '3'], - data: {status: 'unresolved'}, - query: 'is:resolved', - }, - {} - ); + bulkUpdate(new MockApiClient(), { + orgId: '1337', + projectId: '1337', + itemIds: ['1', '2', '3'], + data: {status: 'unresolved'}, + query: 'is:resolved', + }); expect(request).toHaveBeenCalledTimes(1); expect(request).toHaveBeenCalledWith( @@ -109,17 +105,13 @@ describe('group', () => { method: 'PUT', }); - bulkUpdate( - new MockApiClient(), - { - orgId: '1337', - projectId: '1337', - itemIds: undefined, - data: {status: 'unresolved'}, - query: 'is:resolved', - }, - {} - ); + bulkUpdate(new MockApiClient(), { + orgId: '1337', + projectId: '1337', + itemIds: undefined, + data: {status: 'unresolved'}, + query: 'is:resolved', + }); expect(request).toHaveBeenCalledTimes(1); expect(request).toHaveBeenCalledWith( @@ -134,16 +126,12 @@ describe('group', () => { method: 'PUT', }); - bulkUpdate( - new MockApiClient(), - { - orgId: '1337', - project: [99], - itemIds: ['1', '2', '3'], - data: {status: 'unresolved'}, - }, - {} - ); + bulkUpdate(new MockApiClient(), { + orgId: '1337', + project: [99], + itemIds: ['1', '2', '3'], + data: {status: 'unresolved'}, + }); expect(request).toHaveBeenCalledTimes(1); expect(request).toHaveBeenCalledWith( @@ -158,16 +146,12 @@ describe('group', () => { method: 'PUT', }); - bulkUpdate( - new MockApiClient(), - { - orgId: '1337', - projectId: '1337', - itemIds: ['1'], - data: {assignedTo: 'user:123'}, - }, - {} - ); + bulkUpdate(new MockApiClient(), { + orgId: '1337', + projectId: '1337', + itemIds: ['1'], + data: {assignedTo: 'user:123'}, + }); expect(GroupStore.onUpdate).toHaveBeenCalledWith(expect.any(String), ['1'], { assignedTo: {type: 'user', id: '123', name: ''}, @@ -180,16 +164,12 @@ describe('group', () => { method: 'PUT', }); - bulkUpdate( - new MockApiClient(), - { - orgId: '1337', - projectId: '1337', - itemIds: ['1'], - data: {assignedTo: ''}, - }, - {} - ); + bulkUpdate(new MockApiClient(), { + orgId: '1337', + projectId: '1337', + itemIds: ['1'], + data: {assignedTo: ''}, + }); expect(GroupStore.onUpdate).toHaveBeenCalledWith(expect.any(String), ['1'], { assignedTo: null, @@ -202,16 +182,12 @@ describe('group', () => { method: 'PUT', }); - bulkUpdate( - new MockApiClient(), - { - orgId: '1337', - projectId: '1337', - itemIds: ['1'], - data: {assignedTo: 'team:456'}, - }, - {} - ); + bulkUpdate(new MockApiClient(), { + orgId: '1337', + projectId: '1337', + itemIds: ['1'], + data: {assignedTo: 'team:456'}, + }); expect(GroupStore.onUpdate).toHaveBeenCalledWith(expect.any(String), ['1'], { assignedTo: {type: 'team', id: '456', name: ''}, @@ -224,16 +200,12 @@ describe('group', () => { method: 'PUT', }); - bulkUpdate( - new MockApiClient(), - { - orgId: '1337', - projectId: '1337', - itemIds: ['1'], - data: {assignedTo: 'user:123'}, - }, - {} - ); + bulkUpdate(new MockApiClient(), { + orgId: '1337', + projectId: '1337', + itemIds: ['1'], + data: {assignedTo: 'user:123'}, + }); expect(request).toHaveBeenCalledWith( '/projects/1337/1337/issues/', @@ -255,16 +227,12 @@ describe('group', () => { method: 'PUT', }); - mergeGroups( - new MockApiClient(), - { - orgId: '1337', - projectId: '1337', - itemIds: ['1', '2', '3'], - query: 'is:resolved', - }, - {} - ); + mergeGroups(new MockApiClient(), { + orgId: '1337', + projectId: '1337', + itemIds: ['1', '2', '3'], + query: 'is:resolved', + }); expect(request).toHaveBeenCalledTimes(1); expect(request).toHaveBeenCalledWith( @@ -279,16 +247,12 @@ describe('group', () => { method: 'PUT', }); - mergeGroups( - new MockApiClient(), - { - orgId: '1337', - projectId: '1337', - itemIds: undefined, - query: 'is:resolved', - }, - {} - ); + mergeGroups(new MockApiClient(), { + orgId: '1337', + projectId: '1337', + itemIds: undefined, + query: 'is:resolved', + }); expect(request).toHaveBeenCalledTimes(1); expect(request).toHaveBeenCalledWith( diff --git a/static/app/actionCreators/group.tsx b/static/app/actionCreators/group.tsx index dcfb4afadb82..8f9b00eab86a 100644 --- a/static/app/actionCreators/group.tsx +++ b/static/app/actionCreators/group.tsx @@ -1,5 +1,6 @@ import {queryOptions} from '@tanstack/react-query'; +import type {RequestCallbacks} from 'sentry/api'; import {Client} from 'sentry/api'; import {GroupStore} from 'sentry/stores/groupStore'; import type {Group, Tag as GroupTag, TagValue} from 'sentry/types/group'; From e652159b29f80c809a420f20148741f079fd89ee Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 1 Oct 2026 19:55:04 -0700 Subject: [PATCH 4/6] ref(group): Await bulkUpdate and mergeGroups at every callsite Replace the .then/.catch/.finally chains with try/catch/finally around an awaited call. Analytics and cache resets that used to run right after the request was fired now run before the await, so they still happen immediately instead of waiting on the response. --- static/app/actionCreators/group.spec.tsx | 36 +++--- .../app/views/issueDetails/actions/index.tsx | 48 ++++---- .../views/issueDetails/actions/shareModal.tsx | 42 ++++--- .../app/views/issueDetails/groupPriority.tsx | 43 ++++--- static/app/views/issueDetails/utils.tsx | 22 ++-- static/app/views/issueList/actions/index.tsx | 23 ++-- static/app/views/issueList/actions/utils.tsx | 35 +++--- .../issuePreview/issuePreviewActions.tsx | 111 +++++++++--------- 8 files changed, 183 insertions(+), 177 deletions(-) diff --git a/static/app/actionCreators/group.spec.tsx b/static/app/actionCreators/group.spec.tsx index 0601ac42103e..798afbafbb92 100644 --- a/static/app/actionCreators/group.spec.tsx +++ b/static/app/actionCreators/group.spec.tsx @@ -78,13 +78,13 @@ describe('group', () => { jest.spyOn(GroupStore, 'onUpdate'); // stub GroupStore.onUpdate call from update }); - it('should use itemIds as query if provided', () => { + it('should use itemIds as query if provided', async () => { const request = MockApiClient.addMockResponse({ url: '/projects/1337/1337/issues/', method: 'PUT', }); - bulkUpdate(new MockApiClient(), { + await bulkUpdate(new MockApiClient(), { orgId: '1337', projectId: '1337', itemIds: ['1', '2', '3'], @@ -99,13 +99,13 @@ describe('group', () => { ); }); - it('should use query as query if itemIds are absent', () => { + it('should use query as query if itemIds are absent', async () => { const request = MockApiClient.addMockResponse({ url: '/projects/1337/1337/issues/', method: 'PUT', }); - bulkUpdate(new MockApiClient(), { + await bulkUpdate(new MockApiClient(), { orgId: '1337', projectId: '1337', itemIds: undefined, @@ -120,13 +120,13 @@ describe('group', () => { ); }); - it('should apply project option', () => { + it('should apply project option', async () => { const request = MockApiClient.addMockResponse({ url: '/organizations/1337/issues/', method: 'PUT', }); - bulkUpdate(new MockApiClient(), { + await bulkUpdate(new MockApiClient(), { orgId: '1337', project: [99], itemIds: ['1', '2', '3'], @@ -140,13 +140,13 @@ describe('group', () => { ); }); - it('should normalize string assignedTo to Actor object for optimistic update', () => { + it('should normalize string assignedTo to Actor object for optimistic update', async () => { MockApiClient.addMockResponse({ url: '/projects/1337/1337/issues/', method: 'PUT', }); - bulkUpdate(new MockApiClient(), { + await bulkUpdate(new MockApiClient(), { orgId: '1337', projectId: '1337', itemIds: ['1'], @@ -158,13 +158,13 @@ describe('group', () => { }); }); - it('should normalize empty assignedTo string to null for optimistic update', () => { + it('should normalize empty assignedTo string to null for optimistic update', async () => { MockApiClient.addMockResponse({ url: '/projects/1337/1337/issues/', method: 'PUT', }); - bulkUpdate(new MockApiClient(), { + await bulkUpdate(new MockApiClient(), { orgId: '1337', projectId: '1337', itemIds: ['1'], @@ -176,13 +176,13 @@ describe('group', () => { }); }); - it('should normalize team assignedTo string for optimistic update', () => { + it('should normalize team assignedTo string for optimistic update', async () => { MockApiClient.addMockResponse({ url: '/projects/1337/1337/issues/', method: 'PUT', }); - bulkUpdate(new MockApiClient(), { + await bulkUpdate(new MockApiClient(), { orgId: '1337', projectId: '1337', itemIds: ['1'], @@ -194,13 +194,13 @@ describe('group', () => { }); }); - it('should send raw string assignedTo to the API', () => { + it('should send raw string assignedTo to the API', async () => { const request = MockApiClient.addMockResponse({ url: '/projects/1337/1337/issues/', method: 'PUT', }); - bulkUpdate(new MockApiClient(), { + await bulkUpdate(new MockApiClient(), { orgId: '1337', projectId: '1337', itemIds: ['1'], @@ -221,13 +221,13 @@ describe('group', () => { jest.spyOn(GroupStore, 'onMerge'); // stub GroupStore.onMerge call from mergeGroups }); - it('should use itemIds as query if provided', () => { + it('should use itemIds as query if provided', async () => { const request = MockApiClient.addMockResponse({ url: '/projects/1337/1337/issues/', method: 'PUT', }); - mergeGroups(new MockApiClient(), { + await mergeGroups(new MockApiClient(), { orgId: '1337', projectId: '1337', itemIds: ['1', '2', '3'], @@ -241,13 +241,13 @@ describe('group', () => { ); }); - it('should use query as query if itemIds are absent', () => { + it('should use query as query if itemIds are absent', async () => { const request = MockApiClient.addMockResponse({ url: '/projects/1337/1337/issues/', method: 'PUT', }); - mergeGroups(new MockApiClient(), { + await mergeGroups(new MockApiClient(), { orgId: '1337', projectId: '1337', itemIds: undefined, diff --git a/static/app/views/issueDetails/actions/index.tsx b/static/app/views/issueDetails/actions/index.tsx index 6a1e0fe7b3e4..904716c89059 100644 --- a/static/app/views/issueDetails/actions/index.tsx +++ b/static/app/views/issueDetails/actions/index.tsx @@ -285,32 +285,9 @@ export function GroupActions({group, project, disabled, event}: GroupActionsProp IssueListCacheStore.reset(); }; - const onUpdate = (data: UpdateData, onComplete?: () => void) => { + const onUpdate = async (data: UpdateData, onComplete?: () => void) => { const successMessage = getUpdateSuccessMessage(group, data); - bulkUpdate(api, { - orgId: organization.slug, - projectId: project.slug, - itemIds: [group.id], - data, - }) - .then(() => { - clearIndicators(); - if (successMessage) { - addSuccessMessage(successMessage); - } - onComplete?.(); - }) - .catch(() => {}) - .finally(() => { - queryClient.invalidateQueries({ - queryKey: groupQueryKey({ - organizationSlug: organization.slug, - groupId: group.id, - }), - }); - }); - if (isResolutionStatus(data)) { trackIssueAction( data.status, @@ -322,6 +299,29 @@ export function GroupActions({group, project, disabled, event}: GroupActionsProp trackIssueAction('mark_reviewed'); } IssueListCacheStore.reset(); + + try { + await bulkUpdate(api, { + orgId: organization.slug, + projectId: project.slug, + itemIds: [group.id], + data, + }); + clearIndicators(); + if (successMessage) { + addSuccessMessage(successMessage); + } + onComplete?.(); + } catch { + // GroupStore already shows the error + } finally { + queryClient.invalidateQueries({ + queryKey: groupQueryKey({ + organizationSlug: organization.slug, + groupId: group.id, + }), + }); + } }; const onReprocessEvent = () => { diff --git a/static/app/views/issueDetails/actions/shareModal.tsx b/static/app/views/issueDetails/actions/shareModal.tsx index 5aae3c4054cb..921e51a0dd20 100644 --- a/static/app/views/issueDetails/actions/shareModal.tsx +++ b/static/app/views/issueDetails/actions/shareModal.tsx @@ -94,35 +94,33 @@ export function ShareIssueModal({ ); }; - const handlePublicShare = ( + const handlePublicShare = async ( e: React.ChangeEvent | null, reshare?: boolean ) => { e?.preventDefault(); setLoading(true); onToggle(); - bulkUpdate(api, { - orgId: organization.slug, - projectId: projectSlug, - itemIds: [groupId], - data: { - isPublic: reshare ?? !isPublished, - }, - }) - .then(() => { - queryClient.invalidateQueries({ - queryKey: groupQueryKey({ - organizationSlug: organization.slug, - groupId, - }), - }); - }) - .catch(() => { - addErrorMessage(t('Error sharing')); - }) - .finally(() => { - setLoading(false); + try { + await bulkUpdate(api, { + orgId: organization.slug, + projectId: projectSlug, + itemIds: [groupId], + data: { + isPublic: reshare ?? !isPublished, + }, }); + queryClient.invalidateQueries({ + queryKey: groupQueryKey({ + organizationSlug: organization.slug, + groupId, + }), + }); + } catch { + addErrorMessage(t('Error sharing')); + } finally { + setLoading(false); + } }; const shareUrl = group?.shareId ? getShareUrl(organization, group) : null; diff --git a/static/app/views/issueDetails/groupPriority.tsx b/static/app/views/issueDetails/groupPriority.tsx index b13bd587de29..850e47115a1b 100644 --- a/static/app/views/issueDetails/groupPriority.tsx +++ b/static/app/views/issueDetails/groupPriority.tsx @@ -38,7 +38,7 @@ function useChangePriority(group: Group, onChange?: (priority: PriorityLevel) => const organization = useOrganization(); const queryClient = useQueryClient(); - return (nextPriority: PriorityLevel) => { + return async (nextPriority: PriorityLevel) => { if (nextPriority === group.priority) { return; } @@ -53,28 +53,27 @@ function useChangePriority(group: Group, onChange?: (priority: PriorityLevel) => addLoadingMessage(t('Saving changes\u2026')); IssueListCacheStore.reset(); - bulkUpdate(api, { - orgId: organization.slug, - itemIds: [group.id], - data: {priority: nextPriority}, - failSilently: true, - project: [group.project.id], - }) - .then(() => { - queryClient.invalidateQueries({ - queryKey: groupQueryKey({ - organizationSlug: organization.slug, - groupId: group.id, - }), - }); - clearIndicators(); - addSuccessMessage(getPriorityUpdateSuccessMessage(nextPriority)); - onChange?.(nextPriority); - }) - .catch(() => { - clearIndicators(); - addErrorMessage(t('Unable to update issue priority')); + try { + await bulkUpdate(api, { + orgId: organization.slug, + itemIds: [group.id], + data: {priority: nextPriority}, + failSilently: true, + project: [group.project.id], }); + queryClient.invalidateQueries({ + queryKey: groupQueryKey({ + organizationSlug: organization.slug, + groupId: group.id, + }), + }); + clearIndicators(); + addSuccessMessage(getPriorityUpdateSuccessMessage(nextPriority)); + onChange?.(nextPriority); + } catch { + clearIndicators(); + addErrorMessage(t('Unable to update issue priority')); + } }; } diff --git a/static/app/views/issueDetails/utils.tsx b/static/app/views/issueDetails/utils.tsx index c79624aaac55..b54fe5e91496 100644 --- a/static/app/views/issueDetails/utils.tsx +++ b/static/app/views/issueDetails/utils.tsx @@ -22,21 +22,25 @@ import {useGroupData} from 'sentry/views/issueDetails/groupDataContext'; import {useGroupId} from 'sentry/views/issueDetails/groupIdContext'; import {useGroupTags} from 'sentry/views/issueDetails/groupTags/useGroupTags'; -export function markEventSeen( +export async function markEventSeen( api: Client, orgId: string, projectId: string, groupId: string ) { - bulkUpdate(api, { - orgId, - projectId, - itemIds: [groupId], - failSilently: true, - data: {hasSeen: true}, - }).catch(() => {}); - IssueListCacheStore.markGroupAsSeen(groupId); + + try { + await bulkUpdate(api, { + orgId, + projectId, + itemIds: [groupId], + failSilently: true, + data: {hasSeen: true}, + }); + } catch { + // Marking an issue as seen is best-effort + } } export function useDefaultIssueEvent() { diff --git a/static/app/views/issueList/actions/index.tsx b/static/app/views/issueList/actions/index.tsx index b58d0496e47c..6092ea4c63d0 100644 --- a/static/app/views/issueList/actions/index.tsx +++ b/static/app/views/issueList/actions/index.tsx @@ -222,15 +222,7 @@ export function IssueListActions({ } function handleMerge() { - actionSelectedGroups(itemIds => { - mergeGroups(api, { - orgId: organization.slug, - itemIds, - query: queryExcludingPerformanceIssues, - project: selection.projects, - environment: selection.environments, - ...selection.datetime, - }).catch(() => {}); + actionSelectedGroups(async itemIds => { if (selection.projects[0]) { const trackProject = ProjectsStore.getById(`${selection.projects[0]}`); trackAnalytics('issues_stream.merged', { @@ -241,6 +233,19 @@ export function IssueListActions({ area, }); } + + try { + await mergeGroups(api, { + orgId: organization.slug, + itemIds, + query: queryExcludingPerformanceIssues, + project: selection.projects, + environment: selection.environments, + ...selection.datetime, + }); + } catch { + // GroupStore already shows the error + } }); } diff --git a/static/app/views/issueList/actions/utils.tsx b/static/app/views/issueList/actions/utils.tsx index 02c4a393dfac..c0022e9e3739 100644 --- a/static/app/views/issueList/actions/utils.tsx +++ b/static/app/views/issueList/actions/utils.tsx @@ -234,7 +234,7 @@ export function invalidateIssueQueries({ }); } -export function performBulkUpdate({ +export async function performBulkUpdate({ api, data, itemIds, @@ -257,22 +257,21 @@ export function performBulkUpdate({ addLoadingMessage(t('Saving changes…')); - bulkUpdate(api, { - orgId: organizationSlug, - itemIds, - data, - query, - environment: selection.environments, - failSilently: true, - ...projectConstraints, - ...selection.datetime, - }) - .then(() => { - clearIndicators(); - onSuccess?.(itemIds); - }) - .catch(() => { - clearIndicators(); - addErrorMessage(t('Unable to update issues')); + try { + await bulkUpdate(api, { + orgId: organizationSlug, + itemIds, + data, + query, + environment: selection.environments, + failSilently: true, + ...projectConstraints, + ...selection.datetime, }); + clearIndicators(); + onSuccess?.(itemIds); + } catch { + clearIndicators(); + addErrorMessage(t('Unable to update issues')); + } } diff --git a/static/app/views/issueList/pages/inbox/issuePreview/issuePreviewActions.tsx b/static/app/views/issueList/pages/inbox/issuePreview/issuePreviewActions.tsx index ef8c899c5dbf..da3cfb631afb 100644 --- a/static/app/views/issueList/pages/inbox/issuePreview/issuePreviewActions.tsx +++ b/static/app/views/issueList/pages/inbox/issuePreview/issuePreviewActions.tsx @@ -148,61 +148,7 @@ function IssueResolutionActions({ const organization = useOrganization(); const location = useLocation(); const queryClient = useQueryClient(); - function handleUpdate(data: GroupStatusResolution) { - bulkUpdate(api, { - orgId: organization.slug, - projectId: project.slug, - itemIds: [group.id], - data, - }) - .then(() => { - clearIndicators(); - addSuccessMessage( - data.status === GroupStatus.UNRESOLVED - ? t('Issue marked unresolved') - : t('Issue resolved') - ); - IssueListCacheStore.reset(); - const issueListUrl = getApiUrl('/organizations/$organizationIdOrSlug/issues/', { - path: {organizationIdOrSlug: organization.slug}, - }); - const issueCountUrl = getApiUrl( - '/organizations/$organizationIdOrSlug/issues-count/', - {path: {organizationIdOrSlug: organization.slug}} - ); - const issueUrl = getApiUrl( - '/organizations/$organizationIdOrSlug/issues/$issueId/', - { - path: { - organizationIdOrSlug: organization.slug, - issueId: group.id, - }, - } - ); - const issueActivitiesUrl = getApiUrl( - '/organizations/$organizationIdOrSlug/issues/$issueId/activities/', - { - path: { - organizationIdOrSlug: organization.slug, - issueId: group.id, - }, - } - ); - void queryClient.invalidateQueries({ - predicate: query => { - const url = safeParseQueryKey(query.queryKey)?.url; - - return ( - url === issueListUrl || - url === issueCountUrl || - url === issueUrl || - url === issueActivitiesUrl - ); - }, - }); - }) - .catch(() => {}); - + async function handleUpdate(data: GroupStatusResolution) { const {alert_date, alert_rule_id, alert_type} = location.query; trackAnalytics('issue_inbox.resolve_clicked', { organization, @@ -217,6 +163,61 @@ function IssueResolutionActions({ ...getAnalyicsDataForProject(project), org_streamline_only: organization.streamlineOnly ?? undefined, }); + + try { + await bulkUpdate(api, { + orgId: organization.slug, + projectId: project.slug, + itemIds: [group.id], + data, + }); + clearIndicators(); + addSuccessMessage( + data.status === GroupStatus.UNRESOLVED + ? t('Issue marked unresolved') + : t('Issue resolved') + ); + IssueListCacheStore.reset(); + const issueListUrl = getApiUrl('/organizations/$organizationIdOrSlug/issues/', { + path: {organizationIdOrSlug: organization.slug}, + }); + const issueCountUrl = getApiUrl( + '/organizations/$organizationIdOrSlug/issues-count/', + {path: {organizationIdOrSlug: organization.slug}} + ); + const issueUrl = getApiUrl( + '/organizations/$organizationIdOrSlug/issues/$issueId/', + { + path: { + organizationIdOrSlug: organization.slug, + issueId: group.id, + }, + } + ); + const issueActivitiesUrl = getApiUrl( + '/organizations/$organizationIdOrSlug/issues/$issueId/activities/', + { + path: { + organizationIdOrSlug: organization.slug, + issueId: group.id, + }, + } + ); + void queryClient.invalidateQueries({ + predicate: query => { + const url = safeParseQueryKey(query.queryKey)?.url; + + return ( + url === issueListUrl || + url === issueCountUrl || + url === issueUrl || + url === issueActivitiesUrl + ); + }, + }); + } catch { + // GroupStore already shows the error + } } return ( From 5f611718dd6aee4d40ed65bbdcd1fae3ba499f2b Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 1 Oct 2026 20:50:51 -0700 Subject: [PATCH 5/6] ref(group): Pass includeAllArgs to requestPromise in bulkUpdate and mergeGroups Every requestPromise call in the group action creators now passes `includeAllArgs: true` and destructures the body as `const [data]`, the same shape bulkDelete uses, so the calls read alike. bulkUpdate's request body is renamed from `data` to `changes`, so the response can be `data` without shadowing the body it sends. --- static/app/actionCreators/group.tsx | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/static/app/actionCreators/group.tsx b/static/app/actionCreators/group.tsx index 8f9b00eab86a..07c300ea8d41 100644 --- a/static/app/actionCreators/group.tsx +++ b/static/app/actionCreators/group.tsx @@ -122,25 +122,26 @@ type BulkUpdateParams = UpdateParams & { }; export async function bulkUpdate(api: Client, params: BulkUpdateParams) { - const {itemIds, failSilently, data} = params; + const {itemIds, failSilently, data: changes} = params; const path = getUpdateUrl(params); const query = paramsToQueryArgs(params); const id = uniqueId(); const optimisticData: Partial = - typeof data.assignedTo === 'string' - ? {...data, assignedTo: parseActorString(data.assignedTo) ?? null} - : data; + typeof changes.assignedTo === 'string' + ? {...changes, assignedTo: parseActorString(changes.assignedTo) ?? null} + : changes; GroupStore.onUpdate(id, itemIds, optimisticData); try { - const response = await api.requestPromise(path, { + const [data] = await api.requestPromise(path, { query, method: 'PUT', - data, + data: changes, + includeAllArgs: true, }); - GroupStore.onUpdateSuccess(id, itemIds, response); + GroupStore.onUpdateSuccess(id, itemIds, data); } catch (error) { GroupStore.onUpdateError(id, itemIds, !!failSilently); throw error; @@ -159,13 +160,14 @@ export async function mergeGroups(api: Client, params: MergeGroupsParams) { GroupStore.onMerge(id, itemIds); try { - const response = await api.requestPromise(path, { + const [data] = await api.requestPromise(path, { query, method: 'PUT', data: {merge: 1}, + includeAllArgs: true, }); - GroupStore.onMergeSuccess(id, itemIds, response); - return response; + GroupStore.onMergeSuccess(id, itemIds, data); + return data; } catch (error) { GroupStore.onMergeError(id, itemIds, error); throw error; From 43a5973ca772fcc8571cb87bb8c32bebac69132c Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 1 Oct 2026 20:55:53 -0700 Subject: [PATCH 6/6] ref(group): Keep the response name in bulkUpdate and mergeGroups `const [response]` reads fine, so bulkUpdate's request body keeps its `data` name and the only change from the async migration is the destructuring and `includeAllArgs: true`. --- static/app/actionCreators/group.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/static/app/actionCreators/group.tsx b/static/app/actionCreators/group.tsx index 07c300ea8d41..30279224b135 100644 --- a/static/app/actionCreators/group.tsx +++ b/static/app/actionCreators/group.tsx @@ -122,26 +122,26 @@ type BulkUpdateParams = UpdateParams & { }; export async function bulkUpdate(api: Client, params: BulkUpdateParams) { - const {itemIds, failSilently, data: changes} = params; + const {itemIds, failSilently, data} = params; const path = getUpdateUrl(params); const query = paramsToQueryArgs(params); const id = uniqueId(); const optimisticData: Partial = - typeof changes.assignedTo === 'string' - ? {...changes, assignedTo: parseActorString(changes.assignedTo) ?? null} - : changes; + typeof data.assignedTo === 'string' + ? {...data, assignedTo: parseActorString(data.assignedTo) ?? null} + : data; GroupStore.onUpdate(id, itemIds, optimisticData); try { - const [data] = await api.requestPromise(path, { + const [response] = await api.requestPromise(path, { query, method: 'PUT', - data: changes, + data, includeAllArgs: true, }); - GroupStore.onUpdateSuccess(id, itemIds, data); + GroupStore.onUpdateSuccess(id, itemIds, response); } catch (error) { GroupStore.onUpdateError(id, itemIds, !!failSilently); throw error; @@ -160,14 +160,14 @@ export async function mergeGroups(api: Client, params: MergeGroupsParams) { GroupStore.onMerge(id, itemIds); try { - const [data] = await api.requestPromise(path, { + const [response] = await api.requestPromise(path, { query, method: 'PUT', data: {merge: 1}, includeAllArgs: true, }); - GroupStore.onMergeSuccess(id, itemIds, data); - return data; + GroupStore.onMergeSuccess(id, itemIds, response); + return response; } catch (error) { GroupStore.onMergeError(id, itemIds, error); throw error;