Skip to content

Commit d5ac7a2

Browse files
scttcperclaude
authored andcommitted
fix(supergroups): Filter resolved groups from Seer response (#112403)
The status filter from #112216 was pre-filtering group IDs sent to Seer, but Seer returns all group_ids per supergroup regardless of what was requested - so the response still contained resolved groups. Moved filtering to the response side instead. Also fixes the response key from `"supergroups"` to `"data"` to match the actual Seer response model (`GetSupergroupsByGroupIdsResponse`). The old code was referencing the wrong key so the filtering was silently a no-op. fixes https://linear.app/getsentry/issue/ID-1440/hide-resolved-issues-from-counts-and-lists --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 08844fa commit d5ac7a2

File tree

3 files changed

+87
-30
lines changed

3 files changed

+87
-30
lines changed

src/sentry/seer/signed_seer_api.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,22 @@ class SupergroupsGetByGroupIdsRequest(TypedDict):
386386
group_ids: list[int]
387387

388388

389+
class SupergroupDetailData(TypedDict):
390+
id: int
391+
title: str
392+
summary: str
393+
error_type: str
394+
code_area: str
395+
group_ids: list[int]
396+
project_ids: list[int]
397+
created_at: str
398+
updated_at: str
399+
400+
401+
class SupergroupsByGroupIdsResponse(TypedDict):
402+
data: list[SupergroupDetailData]
403+
404+
389405
class ServiceMapUpdateRequest(TypedDict):
390406
organization_id: int
391407
nodes: list[dict[str, Any]]

src/sentry/seer/supergroups/endpoints/organization_supergroups_by_group.py

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from sentry.models.organization import Organization
1717
from sentry.seer.signed_seer_api import (
1818
SeerViewerContext,
19+
SupergroupsByGroupIdsResponse,
1920
make_supergroups_get_by_group_ids_request,
2021
)
2122

@@ -55,21 +56,19 @@ def get(self, request: Request, organization: Organization) -> Response:
5556
status=status_codes.HTTP_400_BAD_REQUEST,
5657
)
5758

58-
group_qs = Group.objects.filter(
59-
id__in=group_ids,
60-
project__organization=organization,
61-
)
62-
6359
status_param = request.GET.get("status")
64-
if status_param is not None:
65-
if status_param not in STATUS_QUERY_CHOICES:
66-
return Response(
67-
{"detail": "Invalid status parameter"},
68-
status=status_codes.HTTP_400_BAD_REQUEST,
69-
)
70-
group_qs = group_qs.filter(status=STATUS_QUERY_CHOICES[status_param])
71-
72-
valid_group_ids = set(group_qs.values_list("id", flat=True))
60+
if status_param is not None and status_param not in STATUS_QUERY_CHOICES:
61+
return Response(
62+
{"detail": "Invalid status parameter"},
63+
status=status_codes.HTTP_400_BAD_REQUEST,
64+
)
65+
66+
valid_group_ids = set(
67+
Group.objects.filter(
68+
id__in=group_ids,
69+
project__organization=organization,
70+
).values_list("id", flat=True)
71+
)
7372
group_ids = [gid for gid in group_ids if gid in valid_group_ids]
7473

7574
if not group_ids:
@@ -90,4 +89,31 @@ def get(self, request: Request, organization: Organization) -> Response:
9089
status=response.status,
9190
)
9291

93-
return Response(orjson.loads(response.data))
92+
data: SupergroupsByGroupIdsResponse = orjson.loads(response.data)
93+
94+
if not status_param:
95+
return Response(data)
96+
97+
# Seer returns all group_ids per supergroup regardless of status.
98+
# We can't filter before the Seer call because Seer expands group_ids
99+
# to include the full supergroup membership, not just the requested IDs.
100+
# Instead, collect every group_id from the response, check status in
101+
# bulk, and strip out non-matching ones.
102+
all_response_group_ids: list[int] = []
103+
for sg in data["data"]:
104+
all_response_group_ids.extend(sg["group_ids"])
105+
106+
matching_ids = set(
107+
Group.objects.filter(
108+
id__in=all_response_group_ids,
109+
project__organization=organization,
110+
status=STATUS_QUERY_CHOICES[status_param],
111+
).values_list("id", flat=True)
112+
)
113+
114+
for sg in data["data"]:
115+
sg["group_ids"] = [gid for gid in sg["group_ids"] if gid in matching_ids]
116+
# Drop supergroups that have no matching groups after filtering
117+
data["data"] = [sg for sg in data["data"] if sg["group_ids"]]
118+
119+
return Response(data)

tests/sentry/seer/supergroups/endpoints/test_organization_supergroups_by_group.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from typing import Any
34
from unittest.mock import MagicMock, patch
45

56
import orjson
@@ -8,7 +9,7 @@
89
from sentry.testutils.cases import APITestCase
910

1011

11-
def mock_seer_response(data):
12+
def mock_seer_response(data: dict[str, Any]) -> MagicMock:
1213
response = MagicMock()
1314
response.status = 200
1415
response.data = orjson.dumps(data)
@@ -29,18 +30,41 @@ def setUp(self):
2930
@patch(
3031
"sentry.seer.supergroups.endpoints.organization_supergroups_by_group.make_supergroups_get_by_group_ids_request"
3132
)
32-
def test_status_filter(self, mock_seer):
33-
mock_seer.return_value = mock_seer_response({"supergroups": []})
33+
def test_status_filter_strips_resolved_from_response(self, mock_seer):
34+
extra_unresolved = self.create_group(project=self.project, status=GroupStatus.UNRESOLVED)
35+
mock_seer.return_value = mock_seer_response(
36+
{
37+
"data": [
38+
{
39+
"id": 1,
40+
"group_ids": [
41+
self.unresolved_group.id,
42+
self.resolved_group.id,
43+
extra_unresolved.id,
44+
],
45+
"title": "kept",
46+
},
47+
{
48+
"id": 2,
49+
"group_ids": [self.resolved_group.id],
50+
"title": "dropped",
51+
},
52+
]
53+
}
54+
)
3455

3556
with self.feature("organizations:top-issues-ui"):
36-
self.get_success_response(
57+
response = self.get_success_response(
3758
self.organization.slug,
3859
group_id=[self.unresolved_group.id, self.resolved_group.id],
3960
status="unresolved",
4061
)
4162

42-
body = mock_seer.call_args[0][0]
43-
assert body["group_ids"] == [self.unresolved_group.id]
63+
assert len(response.data["data"]) == 1
64+
assert response.data["data"][0]["group_ids"] == [
65+
self.unresolved_group.id,
66+
extra_unresolved.id,
67+
]
4468

4569
def test_status_filter_invalid(self):
4670
with self.feature("organizations:top-issues-ui"):
@@ -50,12 +74,3 @@ def test_status_filter_invalid(self):
5074
status="bogus",
5175
status_code=400,
5276
)
53-
54-
def test_status_filter_all_filtered_out(self):
55-
with self.feature("organizations:top-issues-ui"):
56-
self.get_error_response(
57-
self.organization.slug,
58-
group_id=[self.resolved_group.id],
59-
status="unresolved",
60-
status_code=404,
61-
)

0 commit comments

Comments
 (0)