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 637a5fd6fea6e6..10a852ba07eb41 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):