From 297b1649b82d30b273cc48b994b37221a98b1e35 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Thu, 9 Apr 2026 14:03:11 -0700 Subject: [PATCH 1/4] write rpc methods --- src/sentry/seer/endpoints/seer_rpc.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/sentry/seer/endpoints/seer_rpc.py b/src/sentry/seer/endpoints/seer_rpc.py index 30e9085a9297b4..0d4eb6ce381ed1 100644 --- a/src/sentry/seer/endpoints/seer_rpc.py +++ b/src/sentry/seer/endpoints/seer_rpc.py @@ -82,7 +82,9 @@ ) from sentry.seer.autofix.utils import ( AutofixTriggerSource, + bulk_read_preferences_from_sentry_db, get_project_seer_preferences, + read_preference_from_sentry_db, resolve_repository_ids, write_preference_to_sentry_db, ) @@ -861,6 +863,23 @@ def check_repository_integrations_status(*, repository_integrations: list[dict[s return {"integration_ids": integration_ids} +def get_project_preferences(*, organization_id: int, project_id: int) -> dict | None: + try: + project = Project.objects.get(id=project_id, organization_id=organization_id) + except Project.DoesNotExist: + return None + + pref = read_preference_from_sentry_db(project) + if pref is None: + return None + return pref.dict() + + +def bulk_get_project_preferences(*, organization_id: int, project_ids: list[int]) -> dict: + prefs = bulk_read_preferences_from_sentry_db(organization_id, project_ids) + return {str(project_id): pref.dict() if pref else None for project_id, pref in prefs.items()} + + seer_method_registry: dict[str, Callable] = { # return type must be serialized # Common to Seer features "get_github_enterprise_integration_config": get_github_enterprise_integration_config, @@ -877,6 +896,8 @@ def check_repository_integrations_status(*, repository_integrations: list[dict[s "send_seer_webhook": send_seer_webhook, "get_attributes_for_span": get_attributes_for_span, "trigger_coding_agent_launch": trigger_coding_agent_launch, + "get_project_preferences": get_project_preferences, + "bulk_get_project_preferences": bulk_get_project_preferences, # # Bug prediction "has_repo_code_mappings": has_repo_code_mappings, From b5e5fde1a22acbdef8a6e07af7cdbf742192b92e Mon Sep 17 00:00:00 2001 From: srest2021 Date: Thu, 9 Apr 2026 14:21:59 -0700 Subject: [PATCH 2/4] add tests --- src/sentry/seer/endpoints/seer_rpc.py | 13 ++-- tests/sentry/seer/endpoints/test_seer_rpc.py | 65 +++++++++++++++++++- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/sentry/seer/endpoints/seer_rpc.py b/src/sentry/seer/endpoints/seer_rpc.py index 0d4eb6ce381ed1..51a095f298b587 100644 --- a/src/sentry/seer/endpoints/seer_rpc.py +++ b/src/sentry/seer/endpoints/seer_rpc.py @@ -869,15 +869,18 @@ def get_project_preferences(*, organization_id: int, project_id: int) -> dict | except Project.DoesNotExist: return None - pref = read_preference_from_sentry_db(project) - if pref is None: + preference = read_preference_from_sentry_db(project) + if preference is None: return None - return pref.dict() + return preference.dict() def bulk_get_project_preferences(*, organization_id: int, project_ids: list[int]) -> dict: - prefs = bulk_read_preferences_from_sentry_db(organization_id, project_ids) - return {str(project_id): pref.dict() if pref else None for project_id, pref in prefs.items()} + preferences = bulk_read_preferences_from_sentry_db(organization_id, project_ids) + return { + str(project_id): preference.dict() if preference else None + for project_id, preference in preferences.items() + } seer_method_registry: dict[str, Callable] = { # return type must be serialized diff --git a/tests/sentry/seer/endpoints/test_seer_rpc.py b/tests/sentry/seer/endpoints/test_seer_rpc.py index b7a52c6ac6f49c..ffccb376daedae 100644 --- a/tests/sentry/seer/endpoints/test_seer_rpc.py +++ b/tests/sentry/seer/endpoints/test_seer_rpc.py @@ -1,7 +1,7 @@ import logging from datetime import datetime, timezone from typing import Any -from unittest.mock import patch +from unittest.mock import MagicMock, patch import orjson import pytest @@ -16,10 +16,12 @@ from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig from sentry.models.repository import Repository from sentry.seer.endpoints.seer_rpc import ( + bulk_get_project_preferences, check_repository_integrations_status, generate_request_signature, get_attributes_for_span, get_github_enterprise_integration_config, + get_project_preferences, get_repo_installation_id, has_repo_code_mappings, trigger_coding_agent_launch, @@ -1484,6 +1486,67 @@ def test_get_repo_installation_id_integration_not_found(self) -> None: assert result == {"error": "integration_not_found"} + @patch("sentry.seer.endpoints.seer_rpc.read_preference_from_sentry_db") + def test_get_project_preferences_returns_preference(self, mock_read: Any) -> None: + project = self.create_project(organization=self.organization) + mock_read.return_value = MagicMock( + dict=MagicMock(return_value={"project_id": project.id, "repositories": []}) + ) + result = get_project_preferences( + organization_id=self.organization.id, + project_id=project.id, + ) + assert result == {"project_id": project.id, "repositories": []} + mock_read.assert_called_once() + + @patch("sentry.seer.endpoints.seer_rpc.read_preference_from_sentry_db") + def test_get_project_preferences_returns_none_when_no_preference(self, mock_read: Any) -> None: + project = self.create_project(organization=self.organization) + mock_read.return_value = None + result = get_project_preferences( + organization_id=self.organization.id, + project_id=project.id, + ) + assert result is None + + def test_get_project_preferences_returns_none_for_nonexistent_project(self) -> None: + result = get_project_preferences( + organization_id=self.organization.id, + project_id=999999, + ) + assert result is None + + @patch("sentry.seer.endpoints.seer_rpc.bulk_read_preferences_from_sentry_db") + def test_bulk_get_project_preferences_returns_preferences(self, mock_bulk_read: Any) -> None: + project1 = self.create_project(organization=self.organization) + project2 = self.create_project(organization=self.organization) + mock_bulk_read.return_value = { + project1.id: MagicMock( + dict=MagicMock(return_value={"project_id": project1.id, "repositories": []}) + ), + project2.id: None, + } + result = bulk_get_project_preferences( + organization_id=self.organization.id, + project_ids=[project1.id, project2.id], + ) + assert result == { + str(project1.id): {"project_id": project1.id, "repositories": []}, + str(project2.id): None, + } + mock_bulk_read.assert_called_once_with(self.organization.id, [project1.id, project2.id]) + + @patch("sentry.seer.endpoints.seer_rpc.bulk_read_preferences_from_sentry_db") + def test_bulk_get_project_preferences_returns_empty_for_no_projects( + self, mock_bulk_read: Any + ) -> None: + mock_bulk_read.return_value = {} + result = bulk_get_project_preferences( + organization_id=self.organization.id, + project_ids=[], + ) + assert result == {} + class TestTriggerCodingAgentLaunch: @patch("sentry.seer.endpoints.seer_rpc.launch_coding_agents_for_run") From 2df9a367eda329fd22714fdb4202a8ddbcfd1498 Mon Sep 17 00:00:00 2001 From: srest2021 Date: Thu, 9 Apr 2026 14:30:54 -0700 Subject: [PATCH 3/4] raise project dne for wrong org id or no project --- src/sentry/seer/endpoints/seer_rpc.py | 17 +++++++++++---- tests/sentry/seer/endpoints/test_seer_rpc.py | 22 ++++++++++++++------ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/sentry/seer/endpoints/seer_rpc.py b/src/sentry/seer/endpoints/seer_rpc.py index 51a095f298b587..8fc6f40e3656d3 100644 --- a/src/sentry/seer/endpoints/seer_rpc.py +++ b/src/sentry/seer/endpoints/seer_rpc.py @@ -864,10 +864,14 @@ def check_repository_integrations_status(*, repository_integrations: list[dict[s def get_project_preferences(*, organization_id: int, project_id: int) -> dict | None: - try: - project = Project.objects.get(id=project_id, organization_id=organization_id) - except Project.DoesNotExist: - return None + """Get Seer project preferences for a single project from Sentry DB. + + Raises Project.DoesNotExist if the project is not found or doesn't belong to the org. + Returns None if the project has no configured preferences. + """ + project = Project.objects.get_from_cache(id=project_id) + if project.organization_id != organization_id: + raise Project.DoesNotExist preference = read_preference_from_sentry_db(project) if preference is None: @@ -876,6 +880,11 @@ def get_project_preferences(*, organization_id: int, project_id: int) -> dict | def bulk_get_project_preferences(*, organization_id: int, project_ids: list[int]) -> dict: + """Bulk get Seer project preferences from Sentry DB. + + Returns a dict keyed by stringified project ID. Values are preference dicts or None + for projects with no configured preferences. + """ preferences = bulk_read_preferences_from_sentry_db(organization_id, project_ids) return { str(project_id): preference.dict() if preference else None diff --git a/tests/sentry/seer/endpoints/test_seer_rpc.py b/tests/sentry/seer/endpoints/test_seer_rpc.py index ffccb376daedae..086d3e14ef5594 100644 --- a/tests/sentry/seer/endpoints/test_seer_rpc.py +++ b/tests/sentry/seer/endpoints/test_seer_rpc.py @@ -14,6 +14,7 @@ from sentry.constants import ObjectStatus from sentry.integrations.models.integration import Integration from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig +from sentry.models.project import Project from sentry.models.repository import Repository from sentry.seer.endpoints.seer_rpc import ( bulk_get_project_preferences, @@ -1509,12 +1510,21 @@ def test_get_project_preferences_returns_none_when_no_preference(self, mock_read ) assert result is None - def test_get_project_preferences_returns_none_for_nonexistent_project(self) -> None: - result = get_project_preferences( - organization_id=self.organization.id, - project_id=999999, - ) - assert result is None + def test_get_project_preferences_raises_for_nonexistent_project(self) -> None: + with pytest.raises(Project.DoesNotExist): + get_project_preferences( + organization_id=self.organization.id, + project_id=999999, + ) + + def test_get_project_preferences_raises_for_wrong_org(self) -> None: + project = self.create_project(organization=self.organization) + other_org = self.create_organization(owner=self.user) + with pytest.raises(Project.DoesNotExist): + get_project_preferences( + organization_id=other_org.id, + project_id=project.id, + ) @patch("sentry.seer.endpoints.seer_rpc.bulk_read_preferences_from_sentry_db") def test_bulk_get_project_preferences_returns_preferences(self, mock_bulk_read: Any) -> None: From 1178cc741ce3333436603783cabe7a97ecf21a9d Mon Sep 17 00:00:00 2001 From: srest2021 Date: Thu, 9 Apr 2026 14:33:09 -0700 Subject: [PATCH 4/4] update docstring --- src/sentry/seer/endpoints/seer_rpc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sentry/seer/endpoints/seer_rpc.py b/src/sentry/seer/endpoints/seer_rpc.py index 8fc6f40e3656d3..21f82bbd7f7752 100644 --- a/src/sentry/seer/endpoints/seer_rpc.py +++ b/src/sentry/seer/endpoints/seer_rpc.py @@ -883,7 +883,8 @@ def bulk_get_project_preferences(*, organization_id: int, project_ids: list[int] """Bulk get Seer project preferences from Sentry DB. Returns a dict keyed by stringified project ID. Values are preference dicts or None - for projects with no configured preferences. + for projects with no configured preferences. Projects that don't belong to the + given organization are silently excluded from the result. """ preferences = bulk_read_preferences_from_sentry_db(organization_id, project_ids) return {