Browse Source

feat(issue_search): Add `regressed_in_release` search term to issue search (#29890)

This adds `regressed_in_release` to issue search. This search term will return all groups that
regressed in the given release, regardless of their current status.
Dan Fuller 3 years ago
parent
commit
d7cc9b622b

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

@@ -101,6 +101,7 @@ value_converters = {
     "first_release": convert_first_release_value,
     "release": convert_release_value,
     "status": convert_status_value,
+    "regressed_in_release": convert_first_release_value,
 }
 
 

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

@@ -2,6 +2,7 @@ import functools
 from abc import ABCMeta, abstractmethod
 from collections import defaultdict
 from datetime import timedelta
+from typing import Sequence
 
 from django.db.models import Q
 from django.utils import timezone
@@ -13,6 +14,8 @@ from sentry.models import (
     Group,
     GroupAssignee,
     GroupEnvironment,
+    GroupHistory,
+    GroupHistoryStatus,
     GroupLink,
     GroupOwner,
     GroupStatus,
@@ -20,6 +23,7 @@ from sentry.models import (
     OrganizationMember,
     OrganizationMemberTeam,
     PlatformExternalIssue,
+    Project,
     Release,
     Team,
     User,
@@ -246,6 +250,19 @@ def assigned_or_suggested_filter(owners, projects, field_filter="id"):
     return query
 
 
+def regressed_in_release_filter(versions: Sequence[str], projects: Sequence[Project]) -> Q:
+    release_ids = Release.objects.filter(
+        organization_id=projects[0].organization_id, version__in=versions
+    ).values_list("id", flat=True)
+    return Q(
+        id__in=GroupHistory.objects.filter(
+            release_id__in=release_ids,
+            status=GroupHistoryStatus.REGRESSED,
+            project__in=projects,
+        ).values_list("group_id", flat=True),
+    )
+
+
 class Condition:
     """\
     Adds a single filter to a ``QuerySet`` object. Used with
@@ -466,6 +483,9 @@ class EventsDatasetSnubaSearchBackend(SnubaSearchBackendBase):
             "assigned_or_suggested": QCallbackCondition(
                 functools.partial(assigned_or_suggested_filter, projects=projects)
             ),
+            "regressed_in_release": QCallbackCondition(
+                functools.partial(regressed_in_release_filter, projects=projects)
+            ),
         }
 
         if environments is not None:

+ 2 - 0
src/sentry/search/snuba/executors.py

@@ -271,6 +271,7 @@ class PostgresSnubaQueryExecutor(AbstractQueryExecutor):
         "subscribed_by",
         "first_release",
         "first_seen",
+        "regressed_in_release",
     }
     sort_strategies = {
         "date": "last_seen",
@@ -431,6 +432,7 @@ class PostgresSnubaQueryExecutor(AbstractQueryExecutor):
 
         max_time = options.get("snuba.search.max-total-chunk-time-seconds")
         time_start = time.time()
+        more_results = False
 
         # Do smaller searches in chunks until we have enough results
         # to answer the query (or hit the end of possible results). We do

+ 17 - 1
tests/snuba/api/endpoints/test_organization_group_index.py

@@ -37,7 +37,7 @@ from sentry.models import (
     add_group_to_inbox,
     remove_group_from_inbox,
 )
-from sentry.models.grouphistory import GroupHistoryStatus
+from sentry.models.grouphistory import GroupHistoryStatus, record_group_history
 from sentry.search.events.constants import (
     RELEASE_STAGE_ALIAS,
     SEMVER_ALIAS,
@@ -628,6 +628,22 @@ class GroupListTest(APITestCase, SnubaTestCase):
         assert len(issues) == 1
         assert int(issues[0]["id"]) == event.group.id
 
+    def test_lookup_by_regressed_in_release(self):
+        self.login_as(self.user)
+        project = self.project
+        release = self.create_release()
+        event = self.store_event(
+            data={
+                "timestamp": iso_format(before_now(seconds=1)),
+                "tags": {"sentry:release": release.version},
+            },
+            project_id=project.id,
+        )
+        record_group_history(event.group, GroupHistoryStatus.REGRESSED, release=release)
+        response = self.get_valid_response(query=f"regressed_in_release:{release.version}")
+        issues = json.loads(response.content)
+        assert [int(issue["id"]) for issue in issues] == [event.group.id]
+
     def test_pending_delete_pending_merge_excluded(self):
         events = []
         for i in "abcd":

+ 46 - 0
tests/snuba/search/test_backend.py

@@ -16,9 +16,11 @@ from sentry.models import (
     GroupAssignee,
     GroupBookmark,
     GroupEnvironment,
+    GroupHistoryStatus,
     GroupStatus,
     GroupSubscription,
     Integration,
+    record_group_history,
 )
 from sentry.models.groupowner import GroupOwner
 from sentry.search.snuba.backend import (
@@ -1393,6 +1395,50 @@ class EventsSnubaSearchTest(TestCase, SnubaTestCase):
             assert third_results.hits > 10
             assert third_results.results != second_results.results
 
+    def test_regressed_in_release(self):
+        # expect no groups within the results since there are no releases
+        results = self.make_query(search_filter_query="regressed_in_release:fake")
+        assert set(results) == set()
+
+        # expect no groups even though there is a release; since no group regressed in this release
+        release_1 = self.create_release()
+
+        results = self.make_query(search_filter_query="regressed_in_release:%s" % release_1.version)
+        assert set(results) == set()
+
+        # Create a new event so that we get a group in this release
+        group = self.store_event(
+            data={
+                "release": release_1.version,
+            },
+            project_id=self.project.id,
+        ).group
+
+        # # Should still be no group since we didn't regress in this release
+        results = self.make_query(search_filter_query="regressed_in_release:%s" % release_1.version)
+        assert set(results) == set()
+
+        record_group_history(group, GroupHistoryStatus.REGRESSED, release=release_1)
+        results = self.make_query(search_filter_query="regressed_in_release:%s" % release_1.version)
+        assert set(results) == {group}
+
+        # Make sure this works correctly with multiple releases
+        release_2 = self.create_release()
+        group_2 = self.store_event(
+            data={
+                "fingerprint": ["put-me-in-group9001"],
+                "event_id": "a" * 32,
+                "release": release_2.version,
+            },
+            project_id=self.project.id,
+        ).group
+        record_group_history(group_2, GroupHistoryStatus.REGRESSED, release=release_2)
+
+        results = self.make_query(search_filter_query="regressed_in_release:%s" % release_1.version)
+        assert set(results) == {group}
+        results = self.make_query(search_filter_query="regressed_in_release:%s" % release_2.version)
+        assert set(results) == {group_2}
+
     def test_first_release(self):
 
         # expect no groups within the results since there are no releases