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
84 changes: 84 additions & 0 deletions src/sentry/discover/endpoints/discover_saved_query_starred.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from django.db.models import Q
from rest_framework import serializers, status
from rest_framework.request import Request
from rest_framework.response import Response

from sentry import features
from sentry.api.api_owners import ApiOwner
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.discover.models import DiscoverSavedQuery, DiscoverSavedQueryStarred
from sentry.models.organization import Organization


class StarQuerySerializer(serializers.Serializer):
starred = serializers.BooleanField(required=True)
position = serializers.IntegerField(required=False)

def validate(self, data):
if not data["starred"] and "position" in data:
raise serializers.ValidationError("Position is only allowed when starring a query.")
return data


class MemberPermission(OrganizationPermission):
scope_map = {
"POST": ["member:read", "member:write"],
}


@cell_silo_endpoint
class DiscoverSavedQueryStarredEndpoint(OrganizationEndpoint):
"""
Star or unstar a single saved Discover query.
"""

publish_status = {
"POST": ApiPublishStatus.EXPERIMENTAL,
}
owner = ApiOwner.DATA_BROWSING
permission_classes = (MemberPermission,)

def has_feature(self, organization, request):
return features.has(
"organizations:visibility-explore-view", organization, actor=request.user
) and features.has(
"organizations:discover-queries-in-all-queries", organization, actor=request.user
)
Comment thread
sentry[bot] marked this conversation as resolved.

def post(self, request: Request, organization: Organization, id: int) -> Response:
"""
Update the starred status of a saved Discover query for the current organization member.
"""
if not request.user.is_authenticated:
return Response(status=status.HTTP_400_BAD_REQUEST)

if not self.has_feature(organization, request):
return self.respond(status=404)

serializer = StarQuerySerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

is_starred = serializer.validated_data["starred"]

try:
query = DiscoverSavedQuery.objects.get(
Q(is_homepage=False) | Q(is_homepage__isnull=True), id=id, organization=organization
)
except DiscoverSavedQuery.DoesNotExist:
Comment on lines +66 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 position field is accepted by the StarQuerySerializer but is silently ignored by the endpoint, which always appends the starred query to the end.
Severity: MEDIUM

Suggested Fix

Modify the endpoint to read the position value from the validated serializer data. Update DiscoverSavedQueryStarredManager.insert_starred_query to accept an optional position parameter. If a position is provided, use it to insert the starred query at the specified location instead of always appending it.

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/discover/endpoints/discover_saved_query_starred.py#L66-L70

Potential issue: The `StarQuerySerializer` accepts an optional `position` field, but the
endpoint logic silently ignores it. The code only reads the `is_starred` value from the
validated serializer data and never extracts or uses the `position`. The call to
`DiscoverSavedQueryStarredManager.insert_starred_query` does not pass a position;
instead, the underlying method always calculates a new position by calling
`next_starred_position`. This results in any client-provided `position` being
disregarded, and the starred query is always appended to the end of the list, leading to
unexpected ordering behavior.

@lzhao-sentry lzhao-sentry Sep 9, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not an issue because clients arent ordering the queries on creation, it's done automatically by insert_starred_query

return Response(status=status.HTTP_404_NOT_FOUND)
Comment thread
cursor[bot] marked this conversation as resolved.

if is_starred:
if DiscoverSavedQueryStarred.objects.insert_starred_query(
organization, request.user.id, query
):
return Response(status=status.HTTP_200_OK)
else:
if DiscoverSavedQueryStarred.objects.delete_starred_query(
organization, request.user.id, query
):
return Response(status=status.HTTP_200_OK)

return Response(status=status.HTTP_204_NO_CONTENT)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import pytest
from django.urls import reverse

from sentry.discover.models import DiscoverSavedQuery, DiscoverSavedQueryStarred
from sentry.testutils.cases import APITestCase


@pytest.mark.skip(reason="API not public yet, this line will be removed in future")
class DiscoverSavedQueryStarredTest(APITestCase):
feature_flags = {
"organizations:visibility-explore-view": True,
"organizations:discover-queries-in-all-queries": True,
}

def setUp(self) -> None:
super().setUp()
self.login_as(user=self.user)
self.org = self.create_organization(owner=self.user)
self.project_ids = [
self.create_project(organization=self.org).id,
self.create_project(organization=self.org).id,
]
query = {"fields": ["title"], "conditions": "", "limit": 10}

model = DiscoverSavedQuery.objects.create(
organization=self.org, created_by_id=self.user.id, name="Test query", query=query
)

model.set_projects(self.project_ids)

self.query_id = model.id

self.url = reverse(
"sentry-api-0-discover-saved-query-starred", args=[self.org.slug, self.query_id]
)

def test_post(self) -> None:
with self.feature(self.feature_flags):
assert not DiscoverSavedQuery.objects.filter(
id__in=DiscoverSavedQueryStarred.objects.filter(
organization=self.org, user_id=self.user.id
).values_list("discover_saved_query_id", flat=True)
).exists()
response = self.client.post(self.url, data={"starred": "1"})
assert response.status_code == 200, response.content
assert DiscoverSavedQuery.objects.filter(
id__in=DiscoverSavedQueryStarred.objects.filter(
organization=self.org, user_id=self.user.id
).values_list("discover_saved_query_id", flat=True)
).exists()
response = self.client.post(self.url, data={"starred": "0"})
assert response.status_code == 200, response.content
assert not DiscoverSavedQuery.objects.filter(
id__in=DiscoverSavedQueryStarred.objects.filter(
organization=self.org, user_id=self.user.id
).values_list("discover_saved_query_id", flat=True)
).exists()
Comment thread
cursor[bot] marked this conversation as resolved.
Loading