Skip to content

Commit 4b93a9c

Browse files
authored
feat(autofix): Add Seer project preference getters to Seer RPC (#112624)
fixes AIML-2610 Allow Seer to call Sentry for project preferences. Even though we pass the autofix preference in the payload, Seer still needs preferences for trace-connected project ids and other things in the future.
1 parent e3a1992 commit 4b93a9c

File tree

2 files changed

+107
-1
lines changed

2 files changed

+107
-1
lines changed

src/sentry/seer/endpoints/seer_rpc.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
)
8383
from sentry.seer.autofix.utils import (
8484
AutofixTriggerSource,
85+
bulk_read_preferences_from_sentry_db,
8586
get_project_seer_preferences,
8687
read_preference_from_sentry_db,
8788
resolve_repository_ids,
@@ -869,6 +870,36 @@ def check_repository_integrations_status(*, repository_integrations: list[dict[s
869870
return {"integration_ids": integration_ids}
870871

871872

873+
def get_project_preferences(*, organization_id: int, project_id: int) -> dict | None:
874+
"""Get Seer project preferences for a single project from Sentry DB.
875+
876+
Raises Project.DoesNotExist if the project is not found or doesn't belong to the org.
877+
Returns None if the project has no configured preferences.
878+
"""
879+
project = Project.objects.get_from_cache(id=project_id)
880+
if project.organization_id != organization_id:
881+
raise Project.DoesNotExist
882+
883+
preference = read_preference_from_sentry_db(project)
884+
if preference is None:
885+
return None
886+
return preference.dict()
887+
888+
889+
def bulk_get_project_preferences(*, organization_id: int, project_ids: list[int]) -> dict:
890+
"""Bulk get Seer project preferences from Sentry DB.
891+
892+
Returns a dict keyed by stringified project ID. Values are preference dicts or None
893+
for projects with no configured preferences. Projects that don't belong to the
894+
given organization are silently excluded from the result.
895+
"""
896+
preferences = bulk_read_preferences_from_sentry_db(organization_id, project_ids)
897+
return {
898+
str(project_id): preference.dict() if preference else None
899+
for project_id, preference in preferences.items()
900+
}
901+
902+
872903
seer_method_registry: dict[str, Callable] = { # return type must be serialized
873904
# Common to Seer features
874905
"get_github_enterprise_integration_config": get_github_enterprise_integration_config,
@@ -885,6 +916,8 @@ def check_repository_integrations_status(*, repository_integrations: list[dict[s
885916
"send_seer_webhook": send_seer_webhook,
886917
"get_attributes_for_span": get_attributes_for_span,
887918
"trigger_coding_agent_launch": trigger_coding_agent_launch,
919+
"get_project_preferences": get_project_preferences,
920+
"bulk_get_project_preferences": bulk_get_project_preferences,
888921
#
889922
# Bug prediction
890923
"has_repo_code_mappings": has_repo_code_mappings,

tests/sentry/seer/endpoints/test_seer_rpc.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22
from datetime import datetime, timezone
33
from typing import Any
4-
from unittest.mock import patch
4+
from unittest.mock import MagicMock, patch
55

66
import orjson
77
import pytest
@@ -14,12 +14,15 @@
1414
from sentry.constants import ObjectStatus
1515
from sentry.integrations.models.integration import Integration
1616
from sentry.integrations.models.repository_project_path_config import RepositoryProjectPathConfig
17+
from sentry.models.project import Project
1718
from sentry.models.repository import Repository
1819
from sentry.seer.endpoints.seer_rpc import (
20+
bulk_get_project_preferences,
1921
check_repository_integrations_status,
2022
generate_request_signature,
2123
get_attributes_for_span,
2224
get_github_enterprise_integration_config,
25+
get_project_preferences,
2326
get_repo_installation_id,
2427
has_repo_code_mappings,
2528
trigger_coding_agent_launch,
@@ -1484,6 +1487,76 @@ def test_get_repo_installation_id_integration_not_found(self) -> None:
14841487

14851488
assert result == {"error": "integration_not_found"}
14861489

1490+
@patch("sentry.seer.endpoints.seer_rpc.read_preference_from_sentry_db")
1491+
def test_get_project_preferences_returns_preference(self, mock_read: Any) -> None:
1492+
project = self.create_project(organization=self.organization)
1493+
mock_read.return_value = MagicMock(
1494+
dict=MagicMock(return_value={"project_id": project.id, "repositories": []})
1495+
)
1496+
result = get_project_preferences(
1497+
organization_id=self.organization.id,
1498+
project_id=project.id,
1499+
)
1500+
assert result == {"project_id": project.id, "repositories": []}
1501+
mock_read.assert_called_once()
1502+
1503+
@patch("sentry.seer.endpoints.seer_rpc.read_preference_from_sentry_db")
1504+
def test_get_project_preferences_returns_none_when_no_preference(self, mock_read: Any) -> None:
1505+
project = self.create_project(organization=self.organization)
1506+
mock_read.return_value = None
1507+
result = get_project_preferences(
1508+
organization_id=self.organization.id,
1509+
project_id=project.id,
1510+
)
1511+
assert result is None
1512+
1513+
def test_get_project_preferences_raises_for_nonexistent_project(self) -> None:
1514+
with pytest.raises(Project.DoesNotExist):
1515+
get_project_preferences(
1516+
organization_id=self.organization.id,
1517+
project_id=999999,
1518+
)
1519+
1520+
def test_get_project_preferences_raises_for_wrong_org(self) -> None:
1521+
project = self.create_project(organization=self.organization)
1522+
other_org = self.create_organization(owner=self.user)
1523+
with pytest.raises(Project.DoesNotExist):
1524+
get_project_preferences(
1525+
organization_id=other_org.id,
1526+
project_id=project.id,
1527+
)
1528+
1529+
@patch("sentry.seer.endpoints.seer_rpc.bulk_read_preferences_from_sentry_db")
1530+
def test_bulk_get_project_preferences_returns_preferences(self, mock_bulk_read: Any) -> None:
1531+
project1 = self.create_project(organization=self.organization)
1532+
project2 = self.create_project(organization=self.organization)
1533+
mock_bulk_read.return_value = {
1534+
project1.id: MagicMock(
1535+
dict=MagicMock(return_value={"project_id": project1.id, "repositories": []})
1536+
),
1537+
project2.id: None,
1538+
}
1539+
result = bulk_get_project_preferences(
1540+
organization_id=self.organization.id,
1541+
project_ids=[project1.id, project2.id],
1542+
)
1543+
assert result == {
1544+
str(project1.id): {"project_id": project1.id, "repositories": []},
1545+
str(project2.id): None,
1546+
}
1547+
mock_bulk_read.assert_called_once_with(self.organization.id, [project1.id, project2.id])
1548+
1549+
@patch("sentry.seer.endpoints.seer_rpc.bulk_read_preferences_from_sentry_db")
1550+
def test_bulk_get_project_preferences_returns_empty_for_no_projects(
1551+
self, mock_bulk_read: Any
1552+
) -> None:
1553+
mock_bulk_read.return_value = {}
1554+
result = bulk_get_project_preferences(
1555+
organization_id=self.organization.id,
1556+
project_ids=[],
1557+
)
1558+
assert result == {}
1559+
14871560

14881561
class TestTriggerCodingAgentLaunch:
14891562
@patch("sentry.seer.endpoints.seer_rpc.launch_coding_agents_for_run")

0 commit comments

Comments
 (0)