Browse Source

feat(issue-priority): Add support for issue.priority search (#65205)

Adds support for searching for issue.priority with a single value or an
array of values.

<img width="1286" alt="image"
src="https://github.com/getsentry/sentry/assets/16563948/45d97b10-43b4-4f31-96d0-e4902bcb1e9d">

Fixes https://github.com/getsentry/sentry/issues/65097
Snigdha Sharma 1 year ago
parent
commit
b7a86364ba

+ 3 - 3
src/sentry/api/helpers/group_index/update.py

@@ -23,7 +23,7 @@ from sentry.db.models.query import create_or_update
 from sentry.issues.grouptype import GroupCategory
 from sentry.issues.ignored import handle_archived_until_escalating, handle_ignored
 from sentry.issues.merge import handle_merge
-from sentry.issues.priority import PRIORITY_UPDATE_CHOICES, update_priority
+from sentry.issues.priority import update_priority
 from sentry.issues.status_change import handle_status_update
 from sentry.issues.update_inbox import update_inbox
 from sentry.models.activity import Activity, ActivityIntegration
@@ -54,7 +54,7 @@ from sentry.signals import issue_resolved
 from sentry.tasks.auto_ongoing_issues import TRANSITION_AFTER_DAYS
 from sentry.tasks.integrations import kick_off_status_syncs
 from sentry.types.activity import ActivityType
-from sentry.types.group import SUBSTATUS_UPDATE_CHOICES, GroupSubStatus
+from sentry.types.group import SUBSTATUS_UPDATE_CHOICES, GroupSubStatus, PriorityLevel
 from sentry.utils import metrics
 
 from . import ACTIVITIES_COUNT, BULK_MUTATION_LIMIT, SearchFunction, delete_group_list
@@ -784,7 +784,7 @@ def handle_priority(priority: str, group_list: Sequence[Group], actor: User) ->
     for group in group_list:
         update_priority(
             group=group,
-            priority=PRIORITY_UPDATE_CHOICES[priority] if priority else None,
+            priority=PriorityLevel.from_str(priority) if priority else None,
             actor=actor,
         )
         group.update(priority_locked_at=django_timezone.now())

+ 7 - 3
src/sentry/api/helpers/group_index/validators/group.py

@@ -4,12 +4,11 @@ from typing import Any
 from rest_framework import serializers
 
 from sentry.api.fields import ActorField
-from sentry.issues.priority import PRIORITY_UPDATE_CHOICES
 from sentry.models.actor import Actor
 from sentry.models.group import STATUS_UPDATE_CHOICES
 from sentry.models.team import Team
 from sentry.models.user import User
-from sentry.types.group import SUBSTATUS_UPDATE_CHOICES
+from sentry.types.group import SUBSTATUS_UPDATE_CHOICES, PriorityLevel
 
 from . import InboxDetailsValidator, StatusDetailsValidator
 
@@ -40,7 +39,12 @@ class GroupValidator(serializers.Serializer):
     ignoreUserWindow = serializers.IntegerField(max_value=7 * 24 * 60)
     assignedTo = ActorField()
     priority = serializers.ChoiceField(
-        choices=list(zip(PRIORITY_UPDATE_CHOICES.keys(), PRIORITY_UPDATE_CHOICES.keys()))
+        choices=list(
+            zip(
+                [p.to_str() for p in PriorityLevel],
+                [p.to_str() for p in PriorityLevel],
+            )
+        )
     )
 
     # TODO(dcramer): remove in 9.0

+ 18 - 1
src/sentry/api/issue_search.py

@@ -38,7 +38,7 @@ from sentry.search.utils import (
     parse_user_value,
 )
 from sentry.services.hybrid_cloud.user import RpcUser
-from sentry.types.group import SUBSTATUS_UPDATE_CHOICES, GroupSubStatus
+from sentry.types.group import SUBSTATUS_UPDATE_CHOICES, GroupSubStatus, PriorityLevel
 
 is_filter_translation = {
     "assigned": ("unassigned", False),
@@ -192,6 +192,22 @@ def convert_category_value(
     return results
 
 
+def convert_priority_value(
+    value: Iterable[str],
+    projects: Sequence[Project],
+    user: User,
+    environments: Sequence[Environment] | None,
+) -> list[int]:
+    """Convert a value like 'high' or 'medium' to the Priority value for issue lookup"""
+    results: list[int] = []
+    for priority in value:
+        priority_value = PriorityLevel.from_str(priority)
+        if not priority_value:
+            raise InvalidSearchQuery(f"Invalid priority value of '{priority}'")
+        results.append(priority_value.value)
+    return results
+
+
 def convert_type_value(
     value: Iterable[str],
     projects: Sequence[Project],
@@ -234,6 +250,7 @@ value_converters: Mapping[str, ValueConverter] = {
     "status": convert_status_value,
     "regressed_in_release": convert_first_release_value,
     "issue.category": convert_category_value,
+    "issue.priority": convert_priority_value,
     "issue.type": convert_type_value,
     "device.class": convert_device_class_value,
     "substatus": convert_substatus_value,

+ 15 - 15
src/sentry/api/serializers/models/group.py

@@ -21,7 +21,6 @@ from sentry.app import env
 from sentry.auth.superuser import is_active_superuser
 from sentry.constants import LOG_LEVELS
 from sentry.issues.grouptype import GroupCategory
-from sentry.issues.priority import PRIORITY_LEVEL_TO_STR
 from sentry.models.apitoken import is_api_token_auth
 from sentry.models.commit import Commit
 from sentry.models.environment import Environment
@@ -54,7 +53,7 @@ from sentry.snuba.dataset import Dataset
 from sentry.tagstore.snuba.backend import fix_tag_value_data
 from sentry.tagstore.types import GroupTagValue
 from sentry.tsdb.snuba import SnubaTSDB
-from sentry.types.group import SUBSTATUS_TO_STR
+from sentry.types.group import SUBSTATUS_TO_STR, PriorityLevel
 from sentry.utils.cache import cache
 from sentry.utils.json import JSONData
 from sentry.utils.safe import safe_execute
@@ -352,7 +351,7 @@ class GroupSerializerBase(Serializer, ABC):
         }
 
         if features.has("projects:issue-priority", obj.project, actor=None):
-            priority_label = PRIORITY_LEVEL_TO_STR[obj.priority] if obj.priority else None
+            priority_label = PriorityLevel(obj.priority).to_str() if obj.priority else None
             group_dict["priority"] = priority_label
             group_dict["priorityLockedAt"] = obj.priority_locked_at
 
@@ -519,9 +518,9 @@ class GroupSerializerBase(Serializer, ABC):
                 start=start,
                 orderby="group_id",
                 referrer="group.unhandled-flag",
-                tenant_ids={"organization_id": item_list[0].project.organization_id}
-                if item_list
-                else None,
+                tenant_ids=(
+                    {"organization_id": item_list[0].project.organization_id} if item_list else None
+                ),
             )
             for x in rv["data"]:
                 unhandled[x["group_id"]] = x["unhandled"]
@@ -896,6 +895,7 @@ SKIP_SNUBA_FIELDS = frozenset(
         "first_release",
         "first_seen",
         "issue.category",
+        "issue.priority",
         "issue.type",
     )
 )
@@ -1048,9 +1048,9 @@ class GroupSerializerSnuba(GroupSerializerBase):
             filter_keys=filters,
             aggregations=aggregations,
             referrer="serializers.GroupSerializerSnuba._execute_error_seen_stats_query",
-            tenant_ids={"organization_id": item_list[0].project.organization_id}
-            if item_list
-            else None,
+            tenant_ids=(
+                {"organization_id": item_list[0].project.organization_id} if item_list else None
+            ),
         )
 
     @staticmethod
@@ -1081,9 +1081,9 @@ class GroupSerializerSnuba(GroupSerializerBase):
             filter_keys=filters,
             aggregations=aggregations,
             referrer="serializers.GroupSerializerSnuba._execute_perf_seen_stats_query",
-            tenant_ids={"organization_id": item_list[0].project.organization_id}
-            if item_list
-            else None,
+            tenant_ids=(
+                {"organization_id": item_list[0].project.organization_id} if item_list else None
+            ),
         )
 
     @staticmethod
@@ -1110,9 +1110,9 @@ class GroupSerializerSnuba(GroupSerializerBase):
             filter_keys=filters,
             aggregations=aggregations,
             referrer="serializers.GroupSerializerSnuba._execute_generic_seen_stats_query",
-            tenant_ids={"organization_id": item_list[0].project.organization_id}
-            if item_list
-            else None,
+            tenant_ids=(
+                {"organization_id": item_list[0].project.organization_id} if item_list else None
+            ),
         )
 
     @staticmethod

+ 1 - 14
src/sentry/issues/priority.py

@@ -16,19 +16,6 @@ if TYPE_CHECKING:
     from sentry.models.group import Group
 
 
-PRIORITY_LEVEL_TO_STR: dict[int, str] = {
-    PriorityLevel.LOW: "low",
-    PriorityLevel.MEDIUM: "medium",
-    PriorityLevel.HIGH: "high",
-}
-
-PRIORITY_UPDATE_CHOICES: dict[str, int] = {
-    "low": PriorityLevel.LOW,
-    "medium": PriorityLevel.MEDIUM,
-    "high": PriorityLevel.HIGH,
-}
-
-
 class PriorityChangeReason(Enum):
     ESCALATING = "escalating"
     ONGOING = "ongoing"
@@ -58,7 +45,7 @@ def update_priority(
         type=ActivityType.SET_PRIORITY,
         user=actor,
         data={
-            "priority": PRIORITY_LEVEL_TO_STR[priority],
+            "priority": priority.to_str(),
             "reason": reason,
         },
     )

+ 1 - 5
src/sentry/models/activity.py

@@ -33,8 +33,6 @@ if TYPE_CHECKING:
 
 class ActivityManager(BaseManager["Activity"]):
     def get_activities_for_group(self, group: Group, num: int) -> Sequence[Activity]:
-        from sentry.issues.priority import PRIORITY_LEVEL_TO_STR
-
         activities = []
         activity_qs = self.filter(group=group).order_by("-datetime")
         initial_priority = None
@@ -45,9 +43,7 @@ class ActivityManager(BaseManager["Activity"]):
             event_metadata = group.get_event_metadata()
             # Check if 'initial_priority' is available and the feature flag is on
             initial_priority_key = event_metadata.get("initial_priority")
-            initial_priority = (
-                PRIORITY_LEVEL_TO_STR[initial_priority_key] if initial_priority_key else None
-            )
+            initial_priority = initial_priority_key.to_str() if initial_priority_key else None
 
         prev_sig = None
         sig = None

+ 1 - 2
src/sentry/models/group.py

@@ -39,7 +39,6 @@ from sentry.db.models import (
 from sentry.eventstore.models import GroupEvent
 from sentry.issues.grouptype import ErrorGroupType, GroupCategory, get_group_type_by_type_id
 from sentry.issues.priority import (
-    PRIORITY_LEVEL_TO_STR,
     PRIORITY_TO_GROUP_HISTORY_STATUS,
     PriorityChangeReason,
     get_priority_for_ongoing_group,
@@ -480,7 +479,7 @@ class GroupManager(BaseManager["Group"]):
                     group=group,
                     type=ActivityType.SET_PRIORITY,
                     data={
-                        "priority": PRIORITY_LEVEL_TO_STR[new_priority],
+                        "priority": new_priority.to_str(),
                         "reason": PriorityChangeReason.ONGOING,
                     },
                 )

+ 1 - 0
src/sentry/search/snuba/backend.py

@@ -701,6 +701,7 @@ class EventsDatasetSnubaSearchBackend(SnubaSearchBackendBase):
             ),
             "issue.category": QCallbackCondition(lambda categories: Q(type__in=categories)),
             "issue.type": QCallbackCondition(lambda types: Q(type__in=types)),
+            "issue.priority": QCallbackCondition(lambda priorities: Q(priority__in=priorities)),
         }
 
         message_filter = next((sf for sf in search_filters or () if "message" == sf.key.name), None)

+ 13 - 0
src/sentry/types/group.py

@@ -72,3 +72,16 @@ class PriorityLevel(IntEnum):
     LOW = 25
     MEDIUM = 50
     HIGH = 75
+
+    def to_str(self) -> str:
+        """
+        Return the string representation of the priority level.
+        """
+        return self.name.lower()
+
+    @classmethod
+    def from_str(self, name: str) -> "PriorityLevel":
+        """
+        Return the priority level from a string representation.
+        """
+        return self[name.upper()]

+ 16 - 1
tests/sentry/api/test_issue_search.py

@@ -27,7 +27,7 @@ from sentry.models.group import GROUP_SUBSTATUS_TO_STATUS_MAP, STATUS_QUERY_CHOI
 from sentry.search.utils import get_teams_for_users
 from sentry.testutils.cases import TestCase
 from sentry.testutils.silo import region_silo_test
-from sentry.types.group import SUBSTATUS_UPDATE_CHOICES, GroupSubStatus
+from sentry.types.group import SUBSTATUS_UPDATE_CHOICES, GroupSubStatus, PriorityLevel
 
 
 class ParseSearchQueryTest(unittest.TestCase):
@@ -313,6 +313,21 @@ class ConvertSubStatusValueTest(TestCase):
         ]
 
 
+class ConvertPriorityValueTest(TestCase):
+    def test_valid(self):
+        for priority in PriorityLevel:
+            filters = [
+                SearchFilter(SearchKey("issue.priority"), "=", SearchValue([priority.to_str()]))
+            ]
+            result = convert_query_values(filters, [self.project], self.user, None)
+            assert result[0].value.raw_value == [priority]
+
+    def test_invalid(self):
+        filters = [SearchFilter(SearchKey("issue.priority"), "=", SearchValue("wrong"))]
+        with pytest.raises(KeyError):
+            convert_query_values(filters, [self.project], self.user, None)
+
+
 @region_silo_test
 class ConvertActorOrNoneValueTest(TestCase):
     def test_user(self):

Some files were not shown because too many files changed in this diff