Browse Source

feat(issue-stream): Find search from backend (#59127)

this pr allows the main issues request from the issue stream to be made
without waiting for the saved searches request. 2 additional parameters
are optionally passed to the backend: one if there is a specific saved
search and one if saved searches have not loaded from the front end. If
saved searched are not loaded from the front end, it will check if there
is a specific saved search requested. If there is, it will be used. If
not, it will check if the user has a default search set, and use it if
it is found. If not, it will used the default "is:unresolved" query.

Closes https://github.com/getsentry/sentry/issues/57853
Richard Roggenkemper 1 year ago
parent
commit
aeea69263d

+ 3 - 0
src/sentry/api/endpoints/organization_group_index.py

@@ -224,6 +224,9 @@ class OrganizationGroupIndexEndpoint(OrganizationEventsEndpointBase):
         :qparam querystring query: an optional Sentry structured search
                                    query.  If not provided an implied
                                    ``"is:unresolved"`` is assumed.)
+        :qparam bool savedSearch:  if this is set to False, then we are making the request without
+                                   a saved search and will look for the default search from this endpoint.
+        :qparam string searchId:   if passed in, this is the selected search
         :pparam string organization_slug: the slug of the organization the
                                           issues belong to.
         :auth: required

+ 29 - 0
src/sentry/api/helpers/group_index/index.py

@@ -2,6 +2,7 @@ from datetime import datetime
 from typing import Any, Callable, Mapping, MutableMapping, Optional, Sequence, Tuple
 
 import sentry_sdk
+from django.db.models import Q
 from rest_framework.exceptions import ParseError
 from rest_framework.request import Request
 from rest_framework.response import Response
@@ -17,6 +18,7 @@ from sentry.models.group import Group, looks_like_short_id
 from sentry.models.organization import Organization
 from sentry.models.project import Project
 from sentry.models.release import Release
+from sentry.models.savedsearch import SavedSearch, Visibility
 from sentry.models.user import User
 from sentry.signals import advanced_search_feature_gated
 from sentry.utils import metrics
@@ -77,6 +79,33 @@ def build_query_params_from_request(
         except ValueError:
             raise ParseError(detail="Invalid cursor parameter.")
     query = request.GET.get("query", "is:unresolved").strip()
+    if request.GET.get("savedSearch") == "0" and request.user:
+        saved_searches = (
+            SavedSearch.objects
+            # Do not include pinned or personal searches from other users in
+            # the same organization. DOES include the requesting users pinned
+            # search
+            .exclude(
+                ~Q(owner_id=request.user.id),
+                visibility__in=(Visibility.OWNER, Visibility.OWNER_PINNED),
+            )
+            .filter(
+                Q(organization=organization) | Q(is_global=True),
+            )
+            .extra(order_by=["name"])
+        )
+        selected_search_id = request.GET.get("searchId", None)
+        if selected_search_id:
+            # saved search requested by the id
+            saved_search = saved_searches.filter(id=int(selected_search_id)).first()
+        else:
+            # pinned saved search
+            saved_search = saved_searches.filter(visibility=Visibility.OWNER_PINNED).first()
+
+        if saved_search:
+            query_kwargs["sort_by"] = saved_search.sort
+            query = saved_search.query
+
     sentry_sdk.set_tag("search.query", query)
     sentry_sdk.set_tag("search.sort", query)
     if projects:

+ 77 - 0
tests/snuba/api/endpoints/test_organization_group_index.py

@@ -38,6 +38,7 @@ from sentry.models.integrations.organization_integration import OrganizationInte
 from sentry.models.options.user_option import UserOption
 from sentry.models.release import Release
 from sentry.models.releaseprojectenvironment import ReleaseStages
+from sentry.models.savedsearch import SavedSearch, Visibility
 from sentry.search.events.constants import (
     RELEASE_STAGE_ALIAS,
     SEMVER_ALIAS,
@@ -1932,6 +1933,82 @@ class GroupListTest(APITestCase, SnubaTestCase):
         assert int(response.data[0]["id"]) == event.group.id
         assert "isUnhandled" not in response.data[0]
 
+    def test_selected_saved_search(self):
+        saved_search = SavedSearch.objects.create(
+            name="Saved Search",
+            query="ZeroDivisionError",
+            organization=self.organization,
+            owner_id=self.user.id,
+        )
+        event = self.store_event(
+            data={
+                "timestamp": iso_format(before_now(seconds=500)),
+                "fingerprint": ["group-1"],
+                "message": "ZeroDivisionError",
+            },
+            project_id=self.project.id,
+        )
+
+        self.store_event(
+            data={
+                "timestamp": iso_format(before_now(seconds=500)),
+                "fingerprint": ["group-2"],
+                "message": "TypeError",
+            },
+            project_id=self.project.id,
+        )
+
+        self.login_as(user=self.user)
+        response = self.get_response(
+            sort_by="date",
+            limit=10,
+            query="is:unresolved",
+            collapse=["unhandled"],
+            savedSearch=0,
+            searchId=saved_search.id,
+        )
+        assert response.status_code == 200
+        assert len(response.data) == 1
+        assert int(response.data[0]["id"]) == event.group.id
+
+    def test_default_saved_search(self):
+        SavedSearch.objects.create(
+            name="Saved Search",
+            query="ZeroDivisionError",
+            organization=self.organization,
+            owner_id=self.user.id,
+            visibility=Visibility.OWNER_PINNED,
+        )
+        event = self.store_event(
+            data={
+                "timestamp": iso_format(before_now(seconds=500)),
+                "fingerprint": ["group-1"],
+                "message": "ZeroDivisionError",
+            },
+            project_id=self.project.id,
+        )
+
+        self.store_event(
+            data={
+                "timestamp": iso_format(before_now(seconds=500)),
+                "fingerprint": ["group-2"],
+                "message": "TypeError",
+            },
+            project_id=self.project.id,
+        )
+
+        self.login_as(user=self.user)
+        response = self.get_response(
+            sort_by="date",
+            limit=10,
+            query="is:unresolved",
+            collapse=["unhandled"],
+            savedSearch=0,
+        )
+        assert response.status_code == 200
+        assert len(response.data) == 1
+        assert int(response.data[0]["id"]) == event.group.id
+
     def test_query_status_and_substatus_overlapping(self):
         event = self.store_event(
             data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]},