From 2d4a54046e1e90c843e6f537cacac0c864b42c7c Mon Sep 17 00:00:00 2001 From: Richard Roggenkemper <46740234+roggenkemper@users.noreply.github.com> Date: Thu, 29 Aug 2024 12:04:05 -0700 Subject: [PATCH] feat(issues): Add ability to query issues by hash (#76701) this pr adds the ability to query by hash in the `project_group_index` endpoint. If a user passes in any number of hashes, the endpoint will only look for those hashes and return matching groups it finds for those hashes. closes https://github.com/getsentry/sentry/issues/72753 --- api-docs/paths/events/project-issues.json | 8 +++++ .../api/endpoints/project_group_index.py | 23 ++++++++++++ .../api/endpoints/test_project_group_index.py | 35 +++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/api-docs/paths/events/project-issues.json b/api-docs/paths/events/project-issues.json index 8c824e90893ea8..fc67d3fcb6cd71 100644 --- a/api-docs/paths/events/project-issues.json +++ b/api-docs/paths/events/project-issues.json @@ -46,6 +46,14 @@ "type": "string" } }, + { + "name": "hash", + "in": "query", + "description": "A list of hashes of groups to return. Is not compatible with 'query' parameter. The maximum number of hashes that can be sent is 100. If more are sent, only the first 100 will be used.", + "schema": { + "type": "string" + } + }, { "$ref": "../../components/parameters/pagination-cursor.json#/PaginationCursor" } diff --git a/src/sentry/api/endpoints/project_group_index.py b/src/sentry/api/endpoints/project_group_index.py index 3c847b9863c63e..e8d09888f84f63 100644 --- a/src/sentry/api/endpoints/project_group_index.py +++ b/src/sentry/api/endpoints/project_group_index.py @@ -20,12 +20,14 @@ from sentry.api.serializers.models.group_stream import StreamGroupSerializer from sentry.models.environment import Environment from sentry.models.group import QUERY_STATUS_LOOKUP, Group, GroupStatus +from sentry.models.grouphash import GroupHash from sentry.search.events.constants import EQUALITY_OPERATORS from sentry.signals import advanced_search from sentry.types.ratelimit import RateLimit, RateLimitCategory from sentry.utils.validators import normalize_event_id ERR_INVALID_STATS_PERIOD = "Invalid stats_period. Valid choices are '', '24h', and '14d'" +ERR_HASHES_AND_OTHER_QUERY = "Cannot use 'hash' with 'query'" @region_silo_endpoint @@ -77,6 +79,7 @@ def get(self, request: Request, project) -> Response: ``"is:unresolved"`` is assumed.) :qparam string environment: this restricts the issues to ones containing events from this environment + :qparam list hashes: hashes of groups to return, overrides 'query' parameter, only returning list of groups found from hashes. The maximum number of hashes that can be sent is 100. If more are sent, only the first 100 will be used. :pparam string organization_id_or_slug: the id or slug of the organization the issues belong to. :pparam string project_id_or_slug: the id or slug of the project the issues @@ -99,7 +102,27 @@ def get(self, request: Request, project) -> Response: stats_period=stats_period, ) + hashes = request.GET.getlist("hashes", []) query = request.GET.get("query", "").strip() + + if hashes: + if query: + return Response({"detail": ERR_HASHES_AND_OTHER_QUERY}, status=400) + + # limit to 100 hashes + hashes = hashes[:100] + groups_from_hashes = GroupHash.objects.filter( + hash__in=hashes, project=project + ).values_list("group_id", flat=True) + groups = list(Group.objects.filter(id__in=groups_from_hashes)) + + serialized_groups = serialize( + groups, + request.user, + serializer(), + ) + return Response(serialized_groups) + if query: matching_group = None matching_event = None diff --git a/tests/snuba/api/endpoints/test_project_group_index.py b/tests/snuba/api/endpoints/test_project_group_index.py index daaf66c02c55bc..01557094cf9d75 100644 --- a/tests/snuba/api/endpoints/test_project_group_index.py +++ b/tests/snuba/api/endpoints/test_project_group_index.py @@ -352,6 +352,41 @@ def test_filter_not_unresolved(self): assert response.status_code == 200 assert [int(r["id"]) for r in response.data] == [event.group.id] + def test_single_group_by_hash(self): + event = self.store_event( + data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + project_id=self.project.id, + ) + + self.login_as(user=self.user) + + response = self.client.get(f"{self.path}?hashes={event.get_primary_hash()}") + assert response.status_code == 200 + assert len(response.data) == 1 + assert int(response.data[0]["id"]) == event.group.id + + def test_multiple_groups_by_hashes(self): + event = self.store_event( + data={"timestamp": iso_format(before_now(seconds=500)), "fingerprint": ["group-1"]}, + project_id=self.project.id, + ) + + event2 = self.store_event( + data={"timestamp": iso_format(before_now(seconds=400)), "fingerprint": ["group-2"]}, + project_id=self.project.id, + ) + self.login_as(user=self.user) + + response = self.client.get( + f"{self.path}?hashes={event.get_primary_hash()}&hashes={event2.get_primary_hash()}" + ) + assert response.status_code == 200 + assert len(response.data) == 2 + + response_group_ids = [int(group["id"]) for group in response.data] + assert event.group.id in response_group_ids + assert event2.group.id in response_group_ids + class GroupUpdateTest(APITestCase, SnubaTestCase): def setUp(self):