diff --git a/api-docs/paths/events/project-issues.json b/api-docs/paths/events/project-issues.json index 8c824e90893ea..fc67d3fcb6cd7 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 3c847b9863c63..e8d09888f84f6 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 daaf66c02c55b..01557094cf9d7 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):