Skip to content

Commit 2464008

Browse files
srest2021claudecursoragent
authored
feat(seer): Read Seer project preferences from Sentry DB in Sentry endpoints (#111594)
Fixes AIML-2611 Depends on #111591 Behind `organizations:seer-project-settings-read-from-sentry`, read Seer project preferences directly from ProjectOption + SeerProjectRepository instead of proxying to the Seer API. Part of [Phase 3 of the Seer project preferences migration](https://www.notion.so/sentry/Tech-Spec-Migrate-Seer-Settings-to-Sentry-Database-3208b10e4b5d80f58ea0d7b77a301e2a). Updated call sites: - autofix_agent.py — trigger_coding_agent_handoff - coding_agent.py — _launch_agents_for_repos - issue_summary.py — get_automation_stopping_point - on_completion_hook.py — _get_handoff_config_if_applicable, _clear_handoff_preference - utils.py — has_project_connected_repos - project_seer_preferences.py — GET - organization_autofix_automation_settings.py — _serialize_projects_with_settings (GET), POST - tasks/seer/autofix.py — configure_seer_for_existing_org - tasks/seer/context_engine_index.py — index_repos - seer_rpc.py — trigger_coding_agent_launch - autofix.py — _resolve_project_preference Note: Generally I chose not to catch DB errors. We shouldn't be getting any. We'll roll out gradually and if something happens we should get a Sentry exception so I can fix it. --------- Co-authored-by: Claude Sonnet 4 <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent bb7a185 commit 2464008

24 files changed

+734
-244
lines changed

src/sentry/seer/autofix/autofix.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,12 @@
4040
get_project_seer_preferences,
4141
make_autofix_start_request,
4242
make_autofix_update_request,
43+
read_preference_from_sentry_db,
4344
set_project_seer_preference,
4445
write_preference_to_sentry_db,
4546
)
4647
from sentry.seer.explorer.utils import _convert_profile_to_execution_tree, fetch_profile_data
47-
from sentry.seer.models import SeerProjectPreference
48+
from sentry.seer.models import SeerApiError, SeerApiResponseValidationError, SeerProjectPreference
4849
from sentry.seer.signed_seer_api import SeerViewerContext
4950
from sentry.seer.utils import get_github_username_for_user
5051
from sentry.services import eventstore
@@ -638,16 +639,27 @@ def get_all_tags_overview(
638639

639640
def _resolve_project_preference(
640641
organization: Organization, project: Project, fallback_repos: list[dict]
641-
) -> SeerProjectPreference:
642+
) -> SeerProjectPreference | None:
642643
"""
643644
Resolve the Seer project preference for a project before triggering autofix.
644645
645646
If an existing preference is found in Seer, returns it.
646647
If not, creates one from fallback_repos.
647648
"""
648-
preference_response = get_project_seer_preferences(project.id)
649-
if preference_response.preference:
650-
return preference_response.preference
649+
if features.has("organizations:seer-project-settings-read-from-sentry", organization):
650+
preference = read_preference_from_sentry_db(project)
651+
else:
652+
try:
653+
preference = get_project_seer_preferences(project.id).preference
654+
except (SeerApiError, SeerApiResponseValidationError):
655+
logger.exception(
656+
"seer.resolve_project_preference.get_failed",
657+
extra={"project_id": project.id, "organization_id": organization.id},
658+
)
659+
return None
660+
661+
if preference:
662+
return preference
651663

652664
default_stopping_point, default_handoff = get_org_default_seer_automation_handoff(organization)
653665
preference = SeerProjectPreference(
@@ -658,7 +670,14 @@ def _resolve_project_preference(
658670
automation_handoff=default_handoff,
659671
)
660672

661-
set_project_seer_preference(preference)
673+
try:
674+
set_project_seer_preference(preference)
675+
except (SeerApiError, SeerApiResponseValidationError):
676+
logger.exception(
677+
"seer.resolve_project_preference.set_failed",
678+
extra={"project_id": project.id, "organization_id": organization.id},
679+
)
680+
return None
662681

663682
if features.has("organizations:seer-project-settings-dual-write", organization):
664683
try:
@@ -719,22 +738,19 @@ def trigger_autofix(
719738
return _respond_with_error("Cannot fix issues without an event.", 400)
720739

721740
code_mappings = get_sorted_code_mapping_configs(group.project)
722-
repos = get_autofix_repos_from_project_code_mappings(group.project, code_mappings=code_mappings)
741+
code_mappings_repos = get_autofix_repos_from_project_code_mappings(
742+
group.project, code_mappings=code_mappings
743+
)
723744

724745
# Resolve the project preference from Seer, or bootstrap one from code mapping repos.
725746
# On success, preference.repositories becomes the source of truth for repos
726747
# (even if empty — matching Seer's behavior of unconditionally using preference repos).
727748
# On failure, we fall back to the original code mapping repos above.
728-
preference: SeerProjectPreference | None = None
729-
try:
730-
preference = _resolve_project_preference(group.organization, group.project, repos)
749+
preference = _resolve_project_preference(group.organization, group.project, code_mappings_repos)
750+
if preference:
731751
repos = [repo.dict() for repo in preference.repositories]
732-
except Exception:
733-
logger.exception(
734-
"seer.write_preferences.resolve_project_preference.failed",
735-
extra={"project_id": group.project.id, "organization_id": group.organization.id},
736-
exc_info=True,
737-
)
752+
else:
753+
repos = code_mappings_repos
738754

739755
# Pre-resolve stacktrace frame paths using code mappings so Seer can skip
740756
# expensive git tree fetches for large repos.

src/sentry/seer/autofix/autofix_agent.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pydantic import BaseModel
99
from rest_framework.exceptions import PermissionDenied
1010

11-
from sentry import analytics
11+
from sentry import analytics, features
1212
from sentry.analytics.events.autofix_events import (
1313
AiAutofixAgentHandoffEvent,
1414
AiAutofixCodeChangesCompletedEvent,
@@ -44,6 +44,7 @@
4444
AutofixStoppingPoint,
4545
get_autofix_state,
4646
get_project_seer_preferences,
47+
read_preference_from_sentry_db,
4748
)
4849
from sentry.seer.entrypoints.operator import SeerAutofixOperator, process_autofix_updates
4950
from sentry.seer.explorer.client import SeerExplorerClient
@@ -504,21 +505,28 @@ def trigger_coding_agent_handoff(
504505

505506
auto_create_pr = False
506507
repo_definitions: list[SeerRepoDefinition] = []
507-
try:
508-
preference_response = get_project_seer_preferences(group.project_id)
509-
if preference_response and preference_response.preference:
510-
repo_definitions = list(preference_response.preference.repositories)
511-
if preference_response.preference.automation_handoff:
512-
auto_create_pr = preference_response.preference.automation_handoff.auto_create_pr
513-
except Exception:
514-
logger.exception(
515-
"autofix.coding_agent_handoff.get_preferences_error",
516-
extra={
517-
"organization_id": group.organization.id,
518-
"run_id": run_id,
519-
"project_id": group.project_id,
520-
},
521-
)
508+
if features.has("organizations:seer-project-settings-read-from-sentry", group.organization):
509+
preference = read_preference_from_sentry_db(group.project)
510+
if preference:
511+
repo_definitions = preference.repositories
512+
if preference.automation_handoff:
513+
auto_create_pr = preference.automation_handoff.auto_create_pr
514+
else:
515+
try:
516+
preference = get_project_seer_preferences(group.project_id).preference
517+
if preference:
518+
repo_definitions = preference.repositories
519+
if preference.automation_handoff:
520+
auto_create_pr = preference.automation_handoff.auto_create_pr
521+
except Exception:
522+
logger.exception(
523+
"autofix.coding_agent_handoff.get_preferences_error",
524+
extra={
525+
"organization_id": group.organization.id,
526+
"run_id": run_id,
527+
"project_id": group.project_id,
528+
},
529+
)
522530

523531
if not repo_definitions:
524532
return {

src/sentry/seer/autofix/coding_agent.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from rest_framework.exceptions import APIException, NotFound, PermissionDenied, ValidationError
1313

1414
from sentry import analytics, features
15+
from sentry.models.project import Project
1516

1617

1718
class IntegrationNotFound(NotFound):
@@ -57,6 +58,7 @@ class StateReposNotFound(NotFound):
5758
get_coding_agent_prompt,
5859
get_project_seer_preferences,
5960
make_store_coding_agent_states_request,
61+
read_preference_from_sentry_db,
6062
update_coding_agent_state,
6163
)
6264
from sentry.seer.models import SeerApiError, SeerApiResponseValidationError
@@ -231,20 +233,35 @@ def _launch_agents_for_repos(
231233

232234
# Fetch project preferences to get auto_create_pr setting from automation_handoff
233235
auto_create_pr = False
234-
try:
235-
preference_response = get_project_seer_preferences(autofix_state.request.project_id)
236-
if preference_response and preference_response.preference:
237-
if preference_response.preference.automation_handoff:
238-
auto_create_pr = preference_response.preference.automation_handoff.auto_create_pr
239-
except (SeerApiError, SeerApiResponseValidationError):
240-
logger.exception(
241-
"coding_agent.get_project_seer_preferences_error",
242-
extra={
243-
"organization_id": organization.id,
244-
"run_id": run_id,
245-
"project_id": autofix_state.request.project_id,
246-
},
247-
)
236+
if features.has("organizations:seer-project-settings-read-from-sentry", organization):
237+
try:
238+
project = Project.objects.get_from_cache(id=autofix_state.request.project_id)
239+
preference = read_preference_from_sentry_db(project)
240+
if preference and preference.automation_handoff:
241+
auto_create_pr = preference.automation_handoff.auto_create_pr
242+
except Project.DoesNotExist:
243+
logger.exception(
244+
"coding_agent.project_not_found",
245+
extra={
246+
"organization_id": organization.id,
247+
"run_id": run_id,
248+
"project_id": autofix_state.request.project_id,
249+
},
250+
)
251+
else:
252+
try:
253+
preference = get_project_seer_preferences(autofix_state.request.project_id).preference
254+
if preference and preference.automation_handoff:
255+
auto_create_pr = preference.automation_handoff.auto_create_pr
256+
except (SeerApiError, SeerApiResponseValidationError):
257+
logger.exception(
258+
"coding_agent.get_project_seer_preferences_error",
259+
extra={
260+
"organization_id": organization.id,
261+
"run_id": run_id,
262+
"project_id": autofix_state.request.project_id,
263+
},
264+
)
248265

249266
repos = set(
250267
_extract_repos_from_root_cause(autofix_state)

src/sentry/seer/autofix/issue_summary.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
is_seer_autotriggered_autofix_rate_limited_and_increment,
3636
is_seer_seat_based_tier_enabled,
3737
make_get_project_preference_request,
38+
read_preference_from_sentry_db,
3839
)
3940
from sentry.seer.entrypoints.cache import SeerOperatorAutofixCache
4041
from sentry.seer.entrypoints.operator import SeerAutofixOperator
@@ -511,7 +512,13 @@ def get_automation_stopping_point(group: Group) -> AutofixStoppingPoint:
511512
"""
512513
fixability_score = get_and_update_group_fixability_score(group)
513514
fixability_stopping_point = _get_stopping_point_from_fixability(fixability_score)
514-
user_preference = _fetch_user_preference(group.project.id)
515+
516+
if features.has("organizations:seer-project-settings-read-from-sentry", group.organization):
517+
preference = read_preference_from_sentry_db(group.project)
518+
user_preference = preference.automated_run_stopping_point if preference else None
519+
else:
520+
user_preference = _fetch_user_preference(group.project.id)
521+
515522
return _apply_user_preference_upper_bound(fixability_stopping_point, user_preference)
516523

517524

src/sentry/seer/autofix/on_completion_hook.py

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from sentry.seer.autofix.utils import (
2323
AutofixStoppingPoint,
2424
get_project_seer_preferences,
25+
read_preference_from_sentry_db,
2526
resolve_repository_ids,
2627
set_project_seer_preference,
2728
write_preference_to_sentry_db,
@@ -488,50 +489,64 @@ def _get_handoff_config_if_applicable(
488489
return None
489490

490491
# Check project preferences
491-
try:
492-
preference_response = get_project_seer_preferences(group.project_id)
493-
except (SeerApiError, SeerApiResponseValidationError):
494-
logger.exception(
495-
"autofix.on_completion_hook.get_preferences_failed",
496-
extra={"group_id": group.id, "project_id": group.project_id},
497-
)
498-
return None
499-
if not preference_response or not preference_response.preference:
500-
return None
501-
handoff_config = preference_response.preference.automation_handoff
502-
if not handoff_config:
503-
return None
492+
if features.has("organizations:seer-project-settings-read-from-sentry", group.organization):
493+
preference = read_preference_from_sentry_db(group.project)
494+
else:
495+
try:
496+
preference = get_project_seer_preferences(group.project_id).preference
497+
except (SeerApiError, SeerApiResponseValidationError):
498+
logger.exception(
499+
"autofix.on_completion_hook.get_preferences_failed",
500+
extra={"group_id": group.id, "project_id": group.project_id},
501+
)
502+
return None
504503

505-
return handoff_config
504+
if not preference:
505+
return None
506+
return preference.automation_handoff
506507

507508
@classmethod
508509
def _clear_handoff_preference(
509510
cls, project: Project, run_id: int, organization: Organization
510511
) -> None:
511512
"""Clear automation_handoff from project preferences after integration is not found."""
512-
try:
513-
preference_response = get_project_seer_preferences(project.id)
514-
if preference_response and preference_response.preference:
515-
updated_preference = preference_response.preference.copy(
516-
update={"automation_handoff": None}
513+
if features.has("organizations:seer-project-settings-read-from-sentry", organization):
514+
preference = read_preference_from_sentry_db(project)
515+
else:
516+
try:
517+
preference = get_project_seer_preferences(project.id).preference
518+
except (SeerApiError, SeerApiResponseValidationError):
519+
logger.exception(
520+
"autofix.on_completion_hook.clear_handoff_preference_failed",
521+
extra={"run_id": run_id, "organization_id": organization.id},
517522
)
518-
set_project_seer_preference(updated_preference)
523+
return
519524

520-
if features.has("organizations:seer-project-settings-dual-write", organization):
521-
try:
522-
validated_pref = SeerProjectPreference.validate(updated_preference)
523-
resolved_pref = resolve_repository_ids(organization.id, [validated_pref])
524-
write_preference_to_sentry_db(project, resolved_pref[0])
525-
except Exception:
526-
logger.exception(
527-
"seer.write_preferences.failed",
528-
extra={"project_id": project.id, "organization_id": organization.id},
529-
)
525+
if not preference or preference.automation_handoff is None:
526+
return
527+
528+
updated_preference = preference.copy(update={"automation_handoff": None})
529+
530+
try:
531+
set_project_seer_preference(updated_preference)
530532
except (SeerApiError, SeerApiResponseValidationError):
531533
logger.exception(
532534
"autofix.on_completion_hook.clear_handoff_preference_failed",
533535
extra={"run_id": run_id, "organization_id": organization.id},
534536
)
537+
return
538+
539+
if features.has("organizations:seer-project-settings-dual-write", organization):
540+
try:
541+
resolved_preference = resolve_repository_ids(
542+
organization.id, [SeerProjectPreference.validate(updated_preference)]
543+
)[0]
544+
write_preference_to_sentry_db(project, resolved_preference)
545+
except Exception:
546+
logger.exception(
547+
"seer.write_preferences.failed",
548+
extra={"project_id": project.id, "organization_id": organization.id},
549+
)
535550

536551
@classmethod
537552
def _trigger_coding_agent_handoff(

src/sentry/seer/autofix/trigger.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ def get_seat_based_seer_automation_skip_reason(
182182
return "automation_already_dispatched" # Another process already dispatched automation
183183

184184
# Check if project has connected repositories - requirement for new pricing
185-
if not has_project_connected_repos(group.organization.id, group.project.id):
185+
if not has_project_connected_repos(group.organization, group.project):
186186
return "no_connected_repos"
187187

188188
return None

0 commit comments

Comments
 (0)