From 827e0d24eb8edde2faecfd82a9ec179ee1e41ff5 Mon Sep 17 00:00:00 2001 From: Michelle Fu Date: Tue, 4 Nov 2025 13:39:36 -0800 Subject: [PATCH 1/5] exclude groups that shouldn't be manually altered --- src/sentry/api/helpers/group_index/update.py | 6 ++++++ .../api/endpoints/test_project_group_index.py | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/sentry/api/helpers/group_index/update.py b/src/sentry/api/helpers/group_index/update.py index 359c3cf275dce5..ae0d5da55712e0 100644 --- a/src/sentry/api/helpers/group_index/update.py +++ b/src/sentry/api/helpers/group_index/update.py @@ -354,6 +354,8 @@ def handle_resolve_in_release( acting_user: RpcUser | User | None, result: MutableMapping[str, Any], ) -> tuple[dict[str, Any], int | None]: + from sentry.grouping.grouptype import ErrorGroupType + res_type = None release = None commit = None @@ -447,6 +449,10 @@ def handle_resolve_in_release( if group.status == GroupStatus.RESOLVED: continue + # Users should only be able to manually resolve error issues + if group.type != ErrorGroupType.type_id: + continue + with transaction.atomic(router.db_for_write(Group)): process_group_resolution( group, diff --git a/tests/snuba/api/endpoints/test_project_group_index.py b/tests/snuba/api/endpoints/test_project_group_index.py index 69b965ea5126b6..f2f19c5e4ac9af 100644 --- a/tests/snuba/api/endpoints/test_project_group_index.py +++ b/tests/snuba/api/endpoints/test_project_group_index.py @@ -12,6 +12,7 @@ from django.conf import settings from django.utils import timezone +from sentry.incidents.grouptype import MetricIssue from sentry.integrations.models.external_issue import ExternalIssue from sentry.integrations.models.organization_integration import OrganizationIntegration from sentry.issues.grouptype import PerformanceSlowDBQueryGroupType @@ -486,6 +487,25 @@ def test_bulk_resolve(self) -> None: assert len(response.data) == 0 + def test_exclude_metric_issue(self) -> None: + self.login_as(user=self.user) + + error_group = self.create_group() + metric_issue_group = self.create_group(type=MetricIssue.type_id) + + response = self.client.put( + f"{self.path}?status=unresolved&query=is:unresolved", + data={"status": "resolved"}, + format="json", + ) + assert response.status_code == 200, response.data + + error_group.refresh_from_db() + metric_issue_group.refresh_from_db() + + assert error_group.status == GroupStatus.RESOLVED + assert metric_issue_group.status == GroupStatus.UNRESOLVED + @patch("sentry.integrations.example.integration.ExampleIntegration.sync_status_outbound") def test_resolve_with_integration(self, mock_sync_status_outbound: MagicMock) -> None: self.login_as(user=self.user) From e7fee12284968404e8d12206292ff125308c49f9 Mon Sep 17 00:00:00 2001 From: Michelle Fu Date: Tue, 4 Nov 2025 13:48:00 -0800 Subject: [PATCH 2/5] use grouptype attribute check instead --- src/sentry/api/helpers/group_index/update.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/sentry/api/helpers/group_index/update.py b/src/sentry/api/helpers/group_index/update.py index ae0d5da55712e0..01f5b2fa68e23d 100644 --- a/src/sentry/api/helpers/group_index/update.py +++ b/src/sentry/api/helpers/group_index/update.py @@ -354,8 +354,6 @@ def handle_resolve_in_release( acting_user: RpcUser | User | None, result: MutableMapping[str, Any], ) -> tuple[dict[str, Any], int | None]: - from sentry.grouping.grouptype import ErrorGroupType - res_type = None release = None commit = None @@ -450,7 +448,7 @@ def handle_resolve_in_release( continue # Users should only be able to manually resolve error issues - if group.type != ErrorGroupType.type_id: + if not get_group_type_by_type_id(group.type).enable_user_priority_changes: continue with transaction.atomic(router.db_for_write(Group)): From 7244805e8c6e8105a7ac78c636d9aa3c32fe451b Mon Sep 17 00:00:00 2001 From: Michelle Fu Date: Tue, 4 Nov 2025 14:28:45 -0800 Subject: [PATCH 3/5] rename field --- src/sentry/api/helpers/group_index/update.py | 6 +++--- src/sentry/incidents/grouptype.py | 2 +- src/sentry/issues/grouptype.py | 2 +- src/sentry/workflow_engine/typings/grouptype.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sentry/api/helpers/group_index/update.py b/src/sentry/api/helpers/group_index/update.py index 01f5b2fa68e23d..d94211a9f22581 100644 --- a/src/sentry/api/helpers/group_index/update.py +++ b/src/sentry/api/helpers/group_index/update.py @@ -207,11 +207,11 @@ def update_groups( res_type = None if "priority" in result: if any( - not get_group_type_by_type_id(group.type).enable_user_priority_changes + not get_group_type_by_type_id(group.type).enable_user_status_and_priority_changes for group in groups ): return Response( - {"detail": "Cannot manually set priority of a metric issue."}, + {"detail": "Cannot manually set priority of one or more issues."}, status=HTTPStatus.BAD_REQUEST, ) @@ -448,7 +448,7 @@ def handle_resolve_in_release( continue # Users should only be able to manually resolve error issues - if not get_group_type_by_type_id(group.type).enable_user_priority_changes: + if not get_group_type_by_type_id(group.type).enable_user_status_and_priority_changes: continue with transaction.atomic(router.db_for_write(Group)): diff --git a/src/sentry/incidents/grouptype.py b/src/sentry/incidents/grouptype.py index 67f70fcc8ffd20..560099f4760afc 100644 --- a/src/sentry/incidents/grouptype.py +++ b/src/sentry/incidents/grouptype.py @@ -333,7 +333,7 @@ class MetricIssue(GroupType): enable_escalation_detection = False enable_status_change_workflow_notifications = False enable_workflow_notifications = False - enable_user_priority_changes = False + enable_user_status_and_priority_changes = False detector_settings = DetectorSettings( handler=MetricIssueDetectorHandler, validator=MetricIssueDetectorValidator, diff --git a/src/sentry/issues/grouptype.py b/src/sentry/issues/grouptype.py index 16f9ff7a9f972a..0b4d4880521793 100644 --- a/src/sentry/issues/grouptype.py +++ b/src/sentry/issues/grouptype.py @@ -219,7 +219,7 @@ class GroupType: enable_workflow_notifications = True # Controls whether users are able to manually update the group's priority. - enable_user_priority_changes = True + enable_user_status_and_priority_changes = True # Controls whether Seer automation is always triggered for this group type. always_trigger_seer_automation = False diff --git a/src/sentry/workflow_engine/typings/grouptype.py b/src/sentry/workflow_engine/typings/grouptype.py index d7857a50b2e763..dd4f2107243384 100644 --- a/src/sentry/workflow_engine/typings/grouptype.py +++ b/src/sentry/workflow_engine/typings/grouptype.py @@ -17,4 +17,4 @@ class IssueStreamGroupType(GroupType): enable_escalation_detection = False enable_status_change_workflow_notifications = False enable_workflow_notifications = False - enable_user_priority_changes = False + enable_user_status_and_priority_changes = False From bc29d18f0b834a9869937277a165b5ffa624d962 Mon Sep 17 00:00:00 2001 From: Michelle Fu <83109586+mifu67@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:07:45 -0800 Subject: [PATCH 4/5] Update src/sentry/api/helpers/group_index/update.py Co-authored-by: Kyle Consalus --- src/sentry/api/helpers/group_index/update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry/api/helpers/group_index/update.py b/src/sentry/api/helpers/group_index/update.py index d94211a9f22581..9595b9c69c1f5b 100644 --- a/src/sentry/api/helpers/group_index/update.py +++ b/src/sentry/api/helpers/group_index/update.py @@ -448,7 +448,7 @@ def handle_resolve_in_release( continue # Users should only be able to manually resolve error issues - if not get_group_type_by_type_id(group.type).enable_user_status_and_priority_changes: + if not group.issue_type.enable_user_status_and_priority_changes: continue with transaction.atomic(router.db_for_write(Group)): From 66ed336b89b9c9850e44cd053a1742a872abef0b Mon Sep 17 00:00:00 2001 From: Michelle Fu Date: Tue, 4 Nov 2025 16:57:04 -0800 Subject: [PATCH 5/5] comments --- src/sentry/api/helpers/group_index/update.py | 17 ++++++++--------- .../endpoints/test_organization_group_index.py | 2 +- .../api/endpoints/test_project_group_index.py | 13 ++++--------- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/src/sentry/api/helpers/group_index/update.py b/src/sentry/api/helpers/group_index/update.py index 9595b9c69c1f5b..632ccd9d79cfd9 100644 --- a/src/sentry/api/helpers/group_index/update.py +++ b/src/sentry/api/helpers/group_index/update.py @@ -25,7 +25,7 @@ from sentry.db.models.query import create_or_update from sentry.hybridcloud.rpc import coerce_id_from from sentry.integrations.tasks.kick_off_status_syncs import kick_off_status_syncs -from sentry.issues.grouptype import GroupCategory, get_group_type_by_type_id +from sentry.issues.grouptype import GroupCategory from sentry.issues.ignored import handle_archived_until_escalating, handle_ignored from sentry.issues.merge import MergedGroup, handle_merge from sentry.issues.priority import update_priority @@ -206,10 +206,7 @@ def update_groups( status = result.get("status") res_type = None if "priority" in result: - if any( - not get_group_type_by_type_id(group.type).enable_user_status_and_priority_changes - for group in groups - ): + if any(not group.issue_type.enable_user_status_and_priority_changes for group in groups): return Response( {"detail": "Cannot manually set priority of one or more issues."}, status=HTTPStatus.BAD_REQUEST, @@ -222,6 +219,12 @@ def update_groups( project_lookup=project_lookup, ) if status in ("resolved", "resolvedInNextRelease"): + if any(not group.issue_type.enable_user_status_and_priority_changes for group in groups): + return Response( + {"detail": "Cannot manually resolve one or more issues."}, + status=HTTPStatus.BAD_REQUEST, + ) + try: result, res_type = handle_resolve_in_release( status, @@ -447,10 +450,6 @@ def handle_resolve_in_release( if group.status == GroupStatus.RESOLVED: continue - # Users should only be able to manually resolve error issues - if not group.issue_type.enable_user_status_and_priority_changes: - continue - with transaction.atomic(router.db_for_write(Group)): process_group_resolution( group, diff --git a/tests/sentry/issues/endpoints/test_organization_group_index.py b/tests/sentry/issues/endpoints/test_organization_group_index.py index c4ab1117ff272e..531eb35a3a2b11 100644 --- a/tests/sentry/issues/endpoints/test_organization_group_index.py +++ b/tests/sentry/issues/endpoints/test_organization_group_index.py @@ -4230,7 +4230,7 @@ def test_cannot_update_metric_issue_priority(self) -> None: qs_params={"id": [group.id]}, priority=PriorityLevel.MEDIUM.to_str() ) assert response.status_code == 400 - assert response.data["detail"] == "Cannot manually set priority of a metric issue." + assert response.data["detail"] == "Cannot manually set priority of one or more issues." class GroupDeleteTest(APITestCase, SnubaTestCase): diff --git a/tests/snuba/api/endpoints/test_project_group_index.py b/tests/snuba/api/endpoints/test_project_group_index.py index f2f19c5e4ac9af..97aa85077b4766 100644 --- a/tests/snuba/api/endpoints/test_project_group_index.py +++ b/tests/snuba/api/endpoints/test_project_group_index.py @@ -490,21 +490,16 @@ def test_bulk_resolve(self) -> None: def test_exclude_metric_issue(self) -> None: self.login_as(user=self.user) - error_group = self.create_group() - metric_issue_group = self.create_group(type=MetricIssue.type_id) + self.create_group() + self.create_group(type=MetricIssue.type_id) response = self.client.put( f"{self.path}?status=unresolved&query=is:unresolved", data={"status": "resolved"}, format="json", ) - assert response.status_code == 200, response.data - - error_group.refresh_from_db() - metric_issue_group.refresh_from_db() - - assert error_group.status == GroupStatus.RESOLVED - assert metric_issue_group.status == GroupStatus.UNRESOLVED + assert response.status_code == 400, response.data + assert response.data["detail"] == "Cannot manually resolve one or more issues." @patch("sentry.integrations.example.integration.ExampleIntegration.sync_status_outbound") def test_resolve_with_integration(self, mock_sync_status_outbound: MagicMock) -> None: