Repository: getsentry/sentry

Issue:
Display issue assignee if available, suggested assignees if not
Add the issue assignee to issue alerts if available, or show the suggested assignees (if available). If there are no suggested assignees, just show "unassigned". Render the user's email / team name rather than doing `@colleen` for example, so as to not notify multiple suggested assignees in Slack.


Discussion:

Resolving patch:
diff --git a/src/sentry/issues/ongoing.py b/src/sentry/issues/ongoing.py
--- a/src/sentry/issues/ongoing.py
+++ b/src/sentry/issues/ongoing.py
@@ -32,11 +32,13 @@ def bulk_transition_group_to_ongoing(
             activity_type=ActivityType.AUTO_SET_ONGOING,
             activity_data=activity_data,
             send_activity_notification=False,
+            from_substatus=from_substatus,
         )
 
     for group in groups_to_transistion:
         group.status = GroupStatus.UNRESOLVED
         group.substatus = GroupSubStatus.ONGOING
+        # TODO(snigdha): do we need to update the priority here?
 
     with sentry_sdk.start_span(description="bulk_remove_groups_from_inbox"):
         bulk_remove_groups_from_inbox(groups_to_transistion)
diff --git a/src/sentry/issues/priority.py b/src/sentry/issues/priority.py
--- a/src/sentry/issues/priority.py
+++ b/src/sentry/issues/priority.py
@@ -2,15 +2,18 @@
 
 import logging
 from enum import Enum, IntEnum
+from typing import TYPE_CHECKING
 
 from sentry import features
 from sentry.models.activity import Activity
-from sentry.models.group import Group
-from sentry.models.grouphistory import GroupHistoryStatus, record_group_history
+from sentry.models.grouphistory import GroupHistory, GroupHistoryStatus, record_group_history
 from sentry.models.user import User
 from sentry.services.hybrid_cloud.user.model import RpcUser
 from sentry.types.activity import ActivityType
 
+if TYPE_CHECKING:
+    from sentry.models.group import Group
+
 
 class PriorityLevel(IntEnum):
     LOW = 25
@@ -68,23 +71,62 @@ def get_priority_for_escalating_group(group: Group) -> PriorityLevel:
     """
     Get the priority for a group that is escalating by incrementing it one level.
     """
-
     if group.priority and group.priority == PriorityLevel.LOW:
         return PriorityLevel.MEDIUM
 
     return PriorityLevel.HIGH
 
 
+def get_priority_for_ongoing_group(group: Group) -> PriorityLevel | None:
+    if not features.has("projects:issue-priority", group.project, actor=None):
+        return None
+
+    previous_priority_history = (
+        GroupHistory.objects.filter(
+            group_id=group.id, status__in=PRIORITY_TO_GROUP_HISTORY_STATUS.values()
+        )
+        .order_by("-date_added")
+        .first()
+    )
+
+    initial_priority = (
+        group.data.get("metadata", {}).get("initial_priority")
+        if not previous_priority_history
+        else None
+    )
+
+    new_priority = (
+        [
+            priority
+            for priority, status in PRIORITY_TO_GROUP_HISTORY_STATUS.items()
+            if status == previous_priority_history.status
+        ][0]
+        if previous_priority_history
+        else initial_priority
+    )
+
+    if not new_priority:
+        logger.error(
+            "Unable to determine previous priority value for group %s after transitioning to ongoing",
+            group.id,
+        )
+        return None
+
+    return new_priority
+
+
 def auto_update_priority(group: Group, reason: PriorityChangeReason) -> None:
     """
     Update the priority of a group due to state changes.
     """
     if not features.has("projects:issue-priority", group.project, actor=None):
-        return
+        return None
 
+    new_priority = None
     if reason == PriorityChangeReason.ESCALATING:
         new_priority = get_priority_for_escalating_group(group)
-        if not new_priority:
-            return
+    elif reason == PriorityChangeReason.ONGOING:
+        new_priority = get_priority_for_ongoing_group(group)
 
+    if new_priority is not None:
         update_priority(group, new_priority, reason)
diff --git a/src/sentry/models/group.py b/src/sentry/models/group.py
--- a/src/sentry/models/group.py
+++ b/src/sentry/models/group.py
@@ -37,7 +37,13 @@
 )
 from sentry.eventstore.models import GroupEvent
 from sentry.issues.grouptype import ErrorGroupType, GroupCategory, get_group_type_by_type_id
-from sentry.models.grouphistory import record_group_history_from_activity_type
+from sentry.issues.priority import (
+    PRIORITY_LEVEL_TO_STR,
+    PRIORITY_TO_GROUP_HISTORY_STATUS,
+    PriorityChangeReason,
+    get_priority_for_ongoing_group,
+)
+from sentry.models.grouphistory import record_group_history, record_group_history_from_activity_type
 from sentry.models.organization import Organization
 from sentry.services.hybrid_cloud.actor import RpcActor
 from sentry.snuba.dataset import Dataset
@@ -429,6 +435,7 @@ def update_group_status(
         activity_type: ActivityType,
         activity_data: Optional[Mapping[str, Any]] = None,
         send_activity_notification: bool = True,
+        from_substatus: int | None = None,
     ) -> None:
         """For each groups, update status to `status` and create an Activity."""
         from sentry.models.activity import Activity
@@ -438,12 +445,24 @@ def update_group_status(
             status=status, substatus=substatus
         )
 
+        should_update_priority = (
+            from_substatus == GroupSubStatus.ESCALATING
+            and activity_type == ActivityType.AUTO_SET_ONGOING
+        )
+
+        updated_priority = {}
         for group in selected_groups:
             group.status = status
             group.substatus = substatus
+            if should_update_priority:
+                priority = get_priority_for_ongoing_group(group)
+                if priority:
+                    group.priority = priority
+                    updated_priority[group.id] = priority
+
             modified_groups_list.append(group)
 
-        Group.objects.bulk_update(modified_groups_list, ["status", "substatus"])
+        Group.objects.bulk_update(modified_groups_list, ["status", "substatus", "priority"])
 
         for group in modified_groups_list:
             Activity.objects.create_group_activity(
@@ -452,9 +471,20 @@ def update_group_status(
                 data=activity_data,
                 send_notification=send_activity_notification,
             )
-
             record_group_history_from_activity_type(group, activity_type.value)
 
+            if group.id in updated_priority:
+                new_priority = updated_priority[group.id]
+                Activity.objects.create_group_activity(
+                    group=group,
+                    type=ActivityType.SET_PRIORITY,
+                    data={
+                        "priority": PRIORITY_LEVEL_TO_STR[new_priority],
+                        "reason": PriorityChangeReason.ONGOING,
+                    },
+                )
+                record_group_history(group, PRIORITY_TO_GROUP_HISTORY_STATUS[new_priority])
+
     def from_share_id(self, share_id: str) -> Group:
         if not share_id or len(share_id) != 32:
             raise Group.DoesNotExist


Test patch:
diff --git a/tests/sentry/issues/test_ongoing.py b/tests/sentry/issues/test_ongoing.py
--- a/tests/sentry/issues/test_ongoing.py
+++ b/tests/sentry/issues/test_ongoing.py
@@ -3,10 +3,12 @@
 from sentry.models.group import GroupStatus
 from sentry.models.grouphistory import GroupHistory, GroupHistoryStatus
 from sentry.testutils.cases import TestCase
+from sentry.testutils.helpers.features import apply_feature_flag_on_cls
 from sentry.types.activity import ActivityType
 from sentry.types.group import GroupSubStatus
 
 
+@apply_feature_flag_on_cls("projects:issue-priority")
 class TransitionNewToOngoingTest(TestCase):
     def test_new_to_ongoing(self) -> None:
         group = self.create_group(status=GroupStatus.UNRESOLVED, substatus=GroupSubStatus.NEW)
@@ -18,6 +20,9 @@ def test_new_to_ongoing(self) -> None:
         assert GroupHistory.objects.filter(
             group=group, status=GroupHistoryStatus.UNRESOLVED
         ).exists()
+        assert not Activity.objects.filter(
+            group=group, type=ActivityType.SET_PRIORITY.value
+        ).exists()
 
     def test_regressed_to_ongoing(self) -> None:
         group = self.create_group(status=GroupStatus.UNRESOLVED, substatus=GroupSubStatus.REGRESSED)
@@ -31,3 +36,29 @@ def test_regressed_to_ongoing(self) -> None:
         assert GroupHistory.objects.filter(
             group=group, status=GroupHistoryStatus.UNRESOLVED
         ).exists()
+        assert not Activity.objects.filter(
+            group=group, type=ActivityType.SET_PRIORITY.value
+        ).exists()
+
+    def test_escalating_to_ongoing(self) -> None:
+        group = self.create_group(
+            status=GroupStatus.UNRESOLVED, substatus=GroupSubStatus.ESCALATING
+        )
+        GroupHistory.objects.create(
+            group=group,
+            status=GroupHistoryStatus.PRIORITY_MEDIUM,
+            project=self.project,
+            organization=self.organization,
+        )
+
+        bulk_transition_group_to_ongoing(
+            GroupStatus.UNRESOLVED, GroupSubStatus.ESCALATING, [group.id]
+        )
+        assert Activity.objects.filter(
+            group=group, type=ActivityType.AUTO_SET_ONGOING.value
+        ).exists()
+
+        assert GroupHistory.objects.filter(
+            group=group, status=GroupHistoryStatus.PRIORITY_MEDIUM
+        ).exists()
+        assert Activity.objects.filter(group=group, type=ActivityType.SET_PRIORITY.value).exists()
diff --git a/tests/sentry/issues/test_priority.py b/tests/sentry/issues/test_priority.py
--- a/tests/sentry/issues/test_priority.py
+++ b/tests/sentry/issues/test_priority.py
@@ -1,3 +1,5 @@
+from unittest.mock import MagicMock, patch
+
 from sentry.issues.priority import (
     PRIORITY_LEVEL_TO_STR,
     PRIORITY_TO_GROUP_HISTORY_STATUS,
@@ -7,7 +9,7 @@
 )
 from sentry.models.activity import Activity
 from sentry.models.group import GroupStatus
-from sentry.models.grouphistory import GroupHistory
+from sentry.models.grouphistory import GroupHistory, GroupHistoryStatus
 from sentry.testutils.cases import TestCase
 from sentry.testutils.helpers.datetime import before_now
 from sentry.testutils.helpers.features import apply_feature_flag_on_cls
@@ -70,3 +72,81 @@ def test_skips_if_priority_locked(self) -> None:
 
         assert Activity.objects.filter(group=self.group).count() == 0
         assert GroupHistory.objects.filter(group=self.group).count() == 0
+
+    def test_updates_priority_ongoing(self) -> None:
+        self.group = self.create_group(
+            status=GroupStatus.RESOLVED,
+            priority=PriorityLevel.MEDIUM,
+        )
+        GroupHistory.objects.create(
+            group=self.group,
+            organization_id=self.group.project.organization_id,
+            project_id=self.group.project_id,
+            status=GroupHistoryStatus.PRIORITY_LOW,
+        )
+        auto_update_priority(self.group, PriorityChangeReason.ONGOING)
+        self.group.refresh_from_db()
+
+        assert self.group.priority == PriorityLevel.LOW
+        self.assert_activity_grouphistory_set(
+            self.group, PriorityLevel.LOW, PriorityChangeReason.ONGOING
+        )
+
+    def test_updates_priority_ongoing_multiple_histories(self) -> None:
+        self.group = self.create_group(
+            status=GroupStatus.RESOLVED,
+            priority=PriorityLevel.HIGH,
+        )
+        group_history_data = {
+            "group": self.group,
+            "organization_id": self.group.project.organization_id,
+            "project_id": self.group.project_id,
+        }
+        GroupHistory.objects.create(
+            **group_history_data,
+            status=GroupHistoryStatus.PRIORITY_LOW,
+        )
+        GroupHistory.objects.create(
+            **group_history_data,
+            status=GroupHistoryStatus.PRIORITY_MEDIUM,
+        )
+        GroupHistory.objects.create(
+            **group_history_data,
+            status=GroupHistoryStatus.PRIORITY_HIGH,
+        )
+        auto_update_priority(self.group, PriorityChangeReason.ONGOING)
+        assert self.group.priority == PriorityLevel.HIGH
+        self.assert_activity_grouphistory_set(
+            self.group, PriorityLevel.HIGH, PriorityChangeReason.ONGOING
+        )
+
+    def test_updates_priority_ongoing_no_history(self) -> None:
+        self.group = self.create_group(
+            status=GroupStatus.RESOLVED,
+        )
+        self.group.data.get("metadata", {})["initial_priority"] = PriorityLevel.MEDIUM
+        self.group.save()
+
+        auto_update_priority(self.group, PriorityChangeReason.ONGOING)
+        assert self.group.priority == PriorityLevel.MEDIUM
+        self.assert_activity_grouphistory_set(
+            self.group, PriorityLevel.MEDIUM, PriorityChangeReason.ONGOING
+        )
+
+    @patch("sentry.issues.priority.logger.error")
+    def test_updates_priority_ongoing_no_initial_priority(self, mock_logger: MagicMock) -> None:
+        self.group = self.create_group(
+            status=GroupStatus.RESOLVED,
+        )
+        self.group.data.get("metadata", {})["initial_priority"] = None
+        self.group.save()
+
+        auto_update_priority(self.group, PriorityChangeReason.ONGOING)
+        mock_logger.assert_called_with(
+            "Unable to determine previous priority value for group %s after transitioning to ongoing",
+            self.group.id,
+        )
+        assert not self.group.priority
+
+        assert Activity.objects.filter(group=self.group).count() == 0
+        assert GroupHistory.objects.filter(group=self.group).count() == 0