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
34 changes: 34 additions & 0 deletions src/sentry/seer/endpoints/seer_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -861,6 +863,36 @@ 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:
"""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:
return None
return preference.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. 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 {
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
# Common to Seer features
"get_github_enterprise_integration_config": get_github_enterprise_integration_config,
Expand All @@ -877,6 +909,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,
Expand Down
75 changes: 74 additions & 1 deletion tests/sentry/seer/endpoints/test_seer_rpc.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -14,12 +14,15 @@
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,
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,
Expand Down Expand Up @@ -1484,6 +1487,76 @@ 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_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:
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")
Expand Down
Loading