Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import cell_silo_endpoint
from sentry.api.bases.organization import OrganizationEndpoint, OrganizationPermission
from sentry.models.group import Group
from sentry.models.group import STATUS_QUERY_CHOICES, Group
from sentry.models.organization import Organization
from sentry.seer.signed_seer_api import (
SeerViewerContext,
Expand Down Expand Up @@ -55,12 +55,21 @@ def get(self, request: Request, organization: Organization) -> Response:
status=status_codes.HTTP_400_BAD_REQUEST,
)

valid_group_ids = set(
Group.objects.filter(
id__in=group_ids,
project__organization=organization,
).values_list("id", flat=True)
group_qs = Group.objects.filter(
id__in=group_ids,
project__organization=organization,
)

status_param = request.GET.get("status")
if status_param is not None:
if status_param not in STATUS_QUERY_CHOICES:
return Response(
{"detail": "Invalid status parameter"},
status=status_codes.HTTP_400_BAD_REQUEST,
)
group_qs = group_qs.filter(status=STATUS_QUERY_CHOICES[status_param])
Comment on lines +65 to +70
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The endpoint incorrectly filters for status="archived" by only checking status=IGNORED and not the required substatus=UNTIL_ESCALATING, returning all ignored groups instead of just archived ones.
Severity: MEDIUM

Suggested Fix

When the status parameter is "archived", the query should be modified to filter on both status=GroupStatus.IGNORED and substatus=GroupSubStatus.UNTIL_ESCALATING. This will correctly isolate only the groups that are truly archived.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location:
src/sentry/seer/supergroups/endpoints/organization_supergroups_by_group.py#L65-L70

Potential issue: The endpoint accepts `status="archived"` as a valid parameter because
it exists in `STATUS_QUERY_CHOICES`. However, the implementation only filters by
`Group.status` (`GroupStatus.IGNORED`), without also filtering by `Group.substatus`
(`GroupSubStatus.UNTIL_ESCALATING`). In Sentry's data model, an "archived" group must
satisfy both conditions. As a result, if a client calls this endpoint with
`status="archived"`, it will incorrectly receive all "ignored" groups (e.g., permanently
ignored) instead of only the "archived" (until escalating) ones, leading to incorrect
data being displayed.

Did we get this right? 👍 / 👎 to inform future reviews.


valid_group_ids = set(group_qs.values_list("id", flat=True))
group_ids = [gid for gid in group_ids if gid in valid_group_ids]

if not group_ids:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from __future__ import annotations

from unittest.mock import MagicMock, patch

import orjson

from sentry.models.group import GroupStatus
from sentry.testutils.cases import APITestCase


def mock_seer_response(data):
response = MagicMock()
response.status = 200
response.data = orjson.dumps(data)
return response


class OrganizationSupergroupsByGroupEndpointTest(APITestCase):
endpoint = "sentry-api-0-organization-supergroups-by-group"

def setUp(self):
super().setUp()
self.login_as(self.user)
self.unresolved_group = self.create_group(
project=self.project, status=GroupStatus.UNRESOLVED
)
self.resolved_group = self.create_group(project=self.project, status=GroupStatus.RESOLVED)

@patch(
"sentry.seer.supergroups.endpoints.organization_supergroups_by_group.make_supergroups_get_by_group_ids_request"
)
def test_status_filter(self, mock_seer):
mock_seer.return_value = mock_seer_response({"supergroups": []})

with self.feature("organizations:top-issues-ui"):
self.get_success_response(
self.organization.slug,
group_id=[self.unresolved_group.id, self.resolved_group.id],
status="unresolved",
)

body = mock_seer.call_args[0][0]
assert body["group_ids"] == [self.unresolved_group.id]

def test_status_filter_invalid(self):
with self.feature("organizations:top-issues-ui"):
self.get_error_response(
self.organization.slug,
group_id=[self.unresolved_group.id],
status="bogus",
status_code=400,
)

def test_status_filter_all_filtered_out(self):
with self.feature("organizations:top-issues-ui"):
self.get_error_response(
self.organization.slug,
group_id=[self.resolved_group.id],
status="unresolved",
status_code=404,
)
Loading