Skip to content
48 changes: 21 additions & 27 deletions src/sentry/seer/autofix/autofix.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
)
from sentry.seer.autofix.utils import (
AutofixStoppingPoint,
get_autofix_repos_from_project_code_mappings,
get_org_default_seer_automation_handoff,
get_project_seer_preferences,
make_autofix_start_request,
Expand Down Expand Up @@ -638,34 +637,34 @@ def get_all_tags_overview(


def _resolve_project_preference(
organization: Organization, project: Project, fallback_repos: list[dict]
organization: Organization, project: Project
) -> SeerProjectPreference | None:
"""
Resolve the Seer project preference for a project before triggering autofix.

If an existing preference is found in Seer, returns it.
If not, creates one from fallback_repos.
Returns the existing preference if one exists. If not, creates a new one
with empty repos and org default settings.
"""
if features.has("organizations:seer-project-settings-read-from-sentry", organization):
preference = read_preference_from_sentry_db(project)
else:
try:
preference = get_project_seer_preferences(project.id).preference
except (SeerApiError, SeerApiResponseValidationError):
logger.exception(
"seer.resolve_project_preference.get_failed",
extra={"project_id": project.id, "organization_id": organization.id},
)
return None
return read_preference_from_sentry_db(project)

if preference:
return preference
try:
preference = get_project_seer_preferences(project.id).preference
if preference:
return preference
except (SeerApiError, SeerApiResponseValidationError):
logger.exception(
"seer.resolve_project_preference.get_failed",
extra={"project_id": project.id, "organization_id": organization.id},
)
return None

# No preference exists — create one with org defaults.
default_stopping_point, default_handoff = get_org_default_seer_automation_handoff(organization)
preference = SeerProjectPreference(
organization_id=organization.id,
project_id=project.id,
repositories=fallback_repos,
repositories=[],
automated_run_stopping_point=default_stopping_point,
automation_handoff=default_handoff,
)
Expand All @@ -684,7 +683,7 @@ def _resolve_project_preference(
write_preference_to_sentry_db(project, preference)
except Exception:
logger.exception(
"seer.write_preferences.resolve_project_preference.sentry_db_write_failed",
"seer.resolve_project_preference.write_failed",
extra={"project_id": project.id, "organization_id": organization.id},
exc_info=True,
)
Expand Down Expand Up @@ -738,19 +737,14 @@ def trigger_autofix(
return _respond_with_error("Cannot fix issues without an event.", 400)

code_mappings = get_sorted_code_mapping_configs(group.project)
code_mappings_repos = get_autofix_repos_from_project_code_mappings(
group.project, code_mappings=code_mappings
)

# Resolve the project preference from Seer, or bootstrap one from code mapping repos.
# On success, preference.repositories becomes the source of truth for repos
# (even if empty — matching Seer's behavior of unconditionally using preference repos).
# On failure, we fall back to the original code mapping repos above.
preference = _resolve_project_preference(group.organization, group.project, code_mappings_repos)
# Resolve the project preference, or create a new one with org defaults.
# Preference repos are the source of truth (even if empty).
preference = _resolve_project_preference(group.organization, group.project)
if preference:
repos = [repo.dict() for repo in preference.repositories]
else:
repos = code_mappings_repos
repos = []

# Pre-resolve stacktrace frame paths using code mappings so Seer can skip
# expensive git tree fetches for large repos.
Expand Down
17 changes: 8 additions & 9 deletions src/sentry/seer/autofix/autofix_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,17 +513,16 @@ def trigger_coding_agent_handoff(
repo_definitions: list[SeerRepoDefinition] = []
if features.has("organizations:seer-project-settings-read-from-sentry", group.organization):
preference = read_preference_from_sentry_db(group.project)
if preference:
repo_definitions = preference.repositories
if preference.automation_handoff:
auto_create_pr = preference.automation_handoff.auto_create_pr
repo_definitions = preference.repositories
if preference.automation_handoff:
auto_create_pr = preference.automation_handoff.auto_create_pr
else:
try:
preference = get_project_seer_preferences(group.project_id).preference
if preference:
repo_definitions = preference.repositories
if preference.automation_handoff:
auto_create_pr = preference.automation_handoff.auto_create_pr
pref = get_project_seer_preferences(group.project_id).preference
if pref:
repo_definitions = pref.repositories
if pref.automation_handoff:
auto_create_pr = pref.automation_handoff.auto_create_pr
Copy link
Copy Markdown
Member Author

@srest2021 srest2021 Apr 15, 2026

Choose a reason for hiding this comment

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

This renaming stuff is due to mypy. read_preference_from_sentry_db guarantees SeerProjectPreference but get_project_seer_preferences can return None. I'll just rename the Seer API call result since it'll get removed soon.

except Exception:
logger.exception(
"autofix.coding_agent_handoff.get_preferences_error",
Expand Down
8 changes: 4 additions & 4 deletions src/sentry/seer/autofix/coding_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ def _launch_agents_for_repos(
try:
project = Project.objects.get_from_cache(id=autofix_state.request.project_id)
preference = read_preference_from_sentry_db(project)
if preference and preference.automation_handoff:
if preference.automation_handoff:
auto_create_pr = preference.automation_handoff.auto_create_pr
except Project.DoesNotExist:
logger.exception(
Expand All @@ -250,9 +250,9 @@ def _launch_agents_for_repos(
)
else:
try:
preference = get_project_seer_preferences(autofix_state.request.project_id).preference
if preference and preference.automation_handoff:
auto_create_pr = preference.automation_handoff.auto_create_pr
pref = get_project_seer_preferences(autofix_state.request.project_id).preference
if pref and pref.automation_handoff:
auto_create_pr = pref.automation_handoff.auto_create_pr
except (SeerApiError, SeerApiResponseValidationError):
logger.exception(
"coding_agent.get_project_seer_preferences_error",
Expand Down
3 changes: 1 addition & 2 deletions src/sentry/seer/autofix/issue_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,8 +506,7 @@ def get_automation_stopping_point(group: Group) -> AutofixStoppingPoint:
fixability_stopping_point = _get_stopping_point_from_fixability(fixability_score)

if features.has("organizations:seer-project-settings-read-from-sentry", group.organization):
preference = read_preference_from_sentry_db(group.project)
user_preference = preference.automated_run_stopping_point if preference else None
user_preference = read_preference_from_sentry_db(group.project).automated_run_stopping_point
else:
user_preference = _fetch_user_preference(group.project.id)

Expand Down
21 changes: 11 additions & 10 deletions src/sentry/seer/autofix/on_completion_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,16 +490,16 @@ def _get_handoff_config_if_applicable(

# Check project preferences
if features.has("organizations:seer-project-settings-read-from-sentry", group.organization):
preference = read_preference_from_sentry_db(group.project)
else:
try:
preference = get_project_seer_preferences(group.project_id).preference
except (SeerApiError, SeerApiResponseValidationError):
logger.exception(
"autofix.on_completion_hook.get_preferences_failed",
extra={"group_id": group.id, "project_id": group.project_id},
)
return None
return read_preference_from_sentry_db(group.project).automation_handoff

try:
preference = get_project_seer_preferences(group.project_id).preference
except (SeerApiError, SeerApiResponseValidationError):
logger.exception(
"autofix.on_completion_hook.get_preferences_failed",
extra={"group_id": group.id, "project_id": group.project_id},
)
return None

if not preference:
return None
Expand All @@ -510,6 +510,7 @@ def _clear_handoff_preference(
cls, project: Project, run_id: int, organization: Organization
) -> None:
"""Clear automation_handoff from project preferences after integration is not found."""
preference: SeerProjectPreference | None = None
if features.has("organizations:seer-project-settings-read-from-sentry", organization):
preference = read_preference_from_sentry_db(project)
else:
Expand Down
24 changes: 5 additions & 19 deletions src/sentry/seer/autofix/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,7 +734,7 @@ def _build_automation_handoff(
)


def read_preference_from_sentry_db(project: Project) -> SeerProjectPreference | None:
def read_preference_from_sentry_db(project: Project) -> SeerProjectPreference:
"""Read a single project's Seer preferences from Sentry DB.

For now, should only be used under feature flag `organizations:seer-project-settings-read-from-sentry`."""
Expand All @@ -749,12 +749,6 @@ def read_preference_from_sentry_db(project: Project) -> SeerProjectPreference |
if (repo_def := build_repo_definition_from_project_repo(project_repo)) is not None
]

has_configured_options = any(
ProjectOption.objects.isset(project, key) for key in SEER_PROJECT_PREFERENCE_OPTION_KEYS
)
if not repo_definitions and not has_configured_options:
return None

return SeerProjectPreference(
organization_id=project.organization_id,
project_id=project.id,
Expand All @@ -767,7 +761,7 @@ def read_preference_from_sentry_db(project: Project) -> SeerProjectPreference |

def bulk_read_preferences_from_sentry_db(
organization_id: int, project_ids: list[int]
) -> dict[int, SeerProjectPreference | None]:
) -> dict[int, SeerProjectPreference]:
"""Bulk read Seer preferences from Sentry DB.

For now, should only be used under feature flag `organizations:seer-project-settings-read-from-sentry`."""
Expand All @@ -793,15 +787,8 @@ def bulk_read_preferences_from_sentry_db(
for key in SEER_PROJECT_PREFERENCE_OPTION_KEYS
}

result: dict[int, SeerProjectPreference | None] = {}
result: dict[int, SeerProjectPreference] = {}
for project in projects:
has_configured_options = any(
project_options[key][project.id] is not None
for key in SEER_PROJECT_PREFERENCE_OPTION_KEYS
)
if project.id not in repo_definitions_by_project and not has_configured_options:
result[project.id] = None
continue

def _get_project_option(key: str) -> Any:
value = project_options[key][project.id]
Comment thread
sentry[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -831,7 +818,7 @@ def bulk_read_preferences(
Always returns ``dict[int, SeerProjectPreference | None]`` regardless of the
underlying read path (Sentry DB or Seer API)."""
if features.has("organizations:seer-project-settings-read-from-sentry", organization):
return bulk_read_preferences_from_sentry_db(organization.id, project_ids)
return bulk_read_preferences_from_sentry_db(organization.id, project_ids) # type: ignore[return-value]
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.

Again mypy. Return type here is dict[int, SeerProjectPreference] so I think it's ok to override mypy


raw = bulk_get_project_preferences(organization.id, project_ids)
tuning_by_id = ProjectOption.objects.get_value_bulk_id(
Expand Down Expand Up @@ -887,8 +874,7 @@ def has_project_connected_repos(

has_repos = False
if features.has("organizations:seer-project-settings-read-from-sentry", organization):
preference = read_preference_from_sentry_db(project)
has_repos = bool(preference and preference.repositories)
has_repos = bool(read_preference_from_sentry_db(project).repositories)
else:
try:
preference = get_project_seer_preferences(project.id).preference
Expand Down
16 changes: 7 additions & 9 deletions src/sentry/seer/endpoints/seer_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,7 @@ def trigger_coding_agent_launch(
if features.has("organizations:seer-project-settings-dual-write", organization):
project = Project.objects.get_from_cache(id=project_id)

preference: SeerProjectPreference | None = None
if features.has(
"organizations:seer-project-settings-read-from-sentry", organization
):
Expand Down Expand Up @@ -879,21 +880,18 @@ def get_project_preferences(*, organization_id: int, project_id: int) -> dict |
"""Get Seer project preferences for a single project.

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.
Returns None if the project has no preference row in Seer DB.
"""
project = Project.objects.get_from_cache(id=project_id)
if project.organization_id != organization_id:
raise Project.DoesNotExist

organization = Organization.objects.get_from_cache(id=organization_id)
if features.has("organizations:seer-project-settings-read-from-sentry", organization):
preference = read_preference_from_sentry_db(project)
else:
preference = get_project_seer_preferences(project_id).preference
return read_preference_from_sentry_db(project).dict()

if preference is None:
return None
return preference.dict()
preference = get_project_seer_preferences(project_id).preference
Comment thread
sentry[bot] marked this conversation as resolved.
return preference.dict() if preference else None


def bulk_get_project_preferences(*, organization_id: int, project_ids: list[int]) -> dict:
Expand All @@ -909,8 +907,8 @@ def bulk_get_project_preferences(*, organization_id: int, project_ids: list[int]
str(project_id): preference.dict() if preference else None
for project_id, preference in preferences.items()
}
else:
return bulk_get_project_seer_preferences(organization_id, project_ids)

return bulk_get_project_seer_preferences(organization_id, project_ids)


seer_method_registry: dict[str, Callable] = { # return type must be serialized
Expand Down
Loading
Loading