Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
db1575c
feat(seer): Add read helpers for Seer project preferences from Sentry DB
srest2021 Mar 25, 2026
3be1c6d
test(seer): Add tests for Sentry DB read preference helpers
srest2021 Mar 25, 2026
241b8bd
fixes
srest2021 Apr 3, 2026
e6ca4d1
remove handoff helper
srest2021 Apr 4, 2026
24873f1
ref(seer): Simplify preference reads using cached project options
srest2021 Apr 7, 2026
bd5959d
resolve conflicts
srest2021 Apr 7, 2026
14f573d
ref(seer): Restore batched queries in bulk_read_preferences
srest2021 Apr 7, 2026
03f8fce
fix typing
srest2021 Apr 7, 2026
770c187
more test coverage
srest2021 Apr 7, 2026
061dc64
add build handoff helper
srest2021 Apr 7, 2026
35bbeff
move comment
srest2021 Apr 7, 2026
c47e758
Use epoch-aware defaults in bulk preference reads
cursoragent Apr 7, 2026
8718314
update endpoints and other spots
srest2021 Apr 7, 2026
c1cad19
test coverage
srest2021 Apr 7, 2026
7b1f1da
more test coverage
srest2021 Apr 7, 2026
259533a
more test coverage
srest2021 Apr 7, 2026
aaf382f
fixes
srest2021 Apr 7, 2026
7aef4d3
cover trigger_coding_agent_launch
srest2021 Apr 7, 2026
fc797cc
renaming
srest2021 Apr 7, 2026
5cdd4d7
simplify some stuff
srest2021 Apr 9, 2026
92dd7e4
Merge branch 'master' into srest2021/AIML-2611
srest2021 Apr 9, 2026
bdb89e2
fix defualt
srest2021 Apr 9, 2026
a0b46d0
cleaning up
srest2021 Apr 9, 2026
cdcff9e
raise valueerror on malformed repo name explicitly
srest2021 Apr 9, 2026
46f2d14
capture sentry exception
srest2021 Apr 9, 2026
4b6c36a
add test
srest2021 Apr 9, 2026
610558e
resolve conflicts
srest2021 Apr 9, 2026
e890305
fix tests
srest2021 Apr 9, 2026
2ff3f57
Merge branch 'master' into srest2021/AIML-2611-endpoints
srest2021 Apr 9, 2026
da22697
cover _clear_handoff_preference
srest2021 Apr 9, 2026
0009575
cover _resolve_project_preference
srest2021 Apr 9, 2026
d7c0890
cover index_repos
srest2021 Apr 9, 2026
dda19ac
Merge branch 'master' into srest2021/AIML-2611-endpoints
srest2021 Apr 9, 2026
e2d9ecb
add null/empty dict check to write fn
srest2021 Apr 10, 2026
520655a
early returns for none handoff
srest2021 Apr 10, 2026
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
48 changes: 32 additions & 16 deletions src/sentry/seer/autofix/autofix.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@
get_project_seer_preferences,
make_autofix_start_request,
make_autofix_update_request,
read_preference_from_sentry_db,
set_project_seer_preference,
write_preference_to_sentry_db,
)
from sentry.seer.explorer.utils import _convert_profile_to_execution_tree, fetch_profile_data
from sentry.seer.models import SeerProjectPreference
from sentry.seer.models import SeerApiError, SeerApiResponseValidationError, SeerProjectPreference
from sentry.seer.signed_seer_api import SeerViewerContext
from sentry.services import eventstore
from sentry.services.eventstore.models import Event, GroupEvent
Expand Down Expand Up @@ -700,16 +701,27 @@ def get_all_tags_overview(

def _resolve_project_preference(
organization: Organization, project: Project, fallback_repos: list[dict]
) -> SeerProjectPreference:
) -> 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.
"""
preference_response = get_project_seer_preferences(project.id)
if preference_response.preference:
return preference_response.preference
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

if preference:
return preference

default_stopping_point, default_handoff = get_org_default_seer_automation_handoff(organization)
preference = SeerProjectPreference(
Expand All @@ -720,7 +732,14 @@ def _resolve_project_preference(
automation_handoff=default_handoff,
)

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

if features.has("organizations:seer-project-settings-dual-write", organization):
try:
Expand Down Expand Up @@ -781,22 +800,19 @@ def trigger_autofix(
return _respond_with_error("Cannot fix issues without an event.", 400)

code_mappings = get_sorted_code_mapping_configs(group.project)
repos = get_autofix_repos_from_project_code_mappings(group.project, code_mappings=code_mappings)
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: SeerProjectPreference | None = None
try:
preference = _resolve_project_preference(group.organization, group.project, repos)
preference = _resolve_project_preference(group.organization, group.project, code_mappings_repos)
if preference:
repos = [repo.dict() for repo in preference.repositories]
except Exception:
logger.exception(
"seer.write_preferences.resolve_project_preference.failed",
extra={"project_id": group.project.id, "organization_id": group.organization.id},
exc_info=True,
)
else:
repos = code_mappings_repos

# Pre-resolve stacktrace frame paths using code mappings so Seer can skip
# expensive git tree fetches for large repos.
Expand Down
40 changes: 24 additions & 16 deletions src/sentry/seer/autofix/autofix_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pydantic import BaseModel
from rest_framework.exceptions import PermissionDenied

from sentry import analytics
from sentry import analytics, features
from sentry.analytics.events.autofix_events import (
AiAutofixAgentHandoffEvent,
AiAutofixCodeChangesCompletedEvent,
Expand Down Expand Up @@ -44,6 +44,7 @@
AutofixStoppingPoint,
get_autofix_state,
get_project_seer_preferences,
read_preference_from_sentry_db,
)
from sentry.seer.entrypoints.operator import SeerAutofixOperator, process_autofix_updates
from sentry.seer.explorer.client import SeerExplorerClient
Expand Down Expand Up @@ -504,21 +505,28 @@ def trigger_coding_agent_handoff(

auto_create_pr = False
repo_definitions: list[SeerRepoDefinition] = []
try:
preference_response = get_project_seer_preferences(group.project_id)
if preference_response and preference_response.preference:
repo_definitions = list(preference_response.preference.repositories)
if preference_response.preference.automation_handoff:
auto_create_pr = preference_response.preference.automation_handoff.auto_create_pr
except Exception:
logger.exception(
"autofix.coding_agent_handoff.get_preferences_error",
extra={
"organization_id": group.organization.id,
"run_id": run_id,
"project_id": group.project_id,
},
)
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
Comment thread
srest2021 marked this conversation as resolved.
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
except Exception:
logger.exception(
"autofix.coding_agent_handoff.get_preferences_error",
extra={
"organization_id": group.organization.id,
"run_id": run_id,
"project_id": group.project_id,
},
)

if not repo_definitions:
return {
Expand Down
45 changes: 31 additions & 14 deletions src/sentry/seer/autofix/coding_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from rest_framework.exceptions import APIException, NotFound, PermissionDenied, ValidationError

from sentry import analytics, features
from sentry.models.project import Project


class IntegrationNotFound(NotFound):
Expand Down Expand Up @@ -57,6 +58,7 @@ class StateReposNotFound(NotFound):
get_coding_agent_prompt,
get_project_seer_preferences,
make_store_coding_agent_states_request,
read_preference_from_sentry_db,
update_coding_agent_state,
)
from sentry.seer.models import SeerApiError, SeerApiResponseValidationError
Expand Down Expand Up @@ -231,20 +233,35 @@ def _launch_agents_for_repos(

# Fetch project preferences to get auto_create_pr setting from automation_handoff
auto_create_pr = False
try:
preference_response = get_project_seer_preferences(autofix_state.request.project_id)
if preference_response and preference_response.preference:
if preference_response.preference.automation_handoff:
auto_create_pr = preference_response.preference.automation_handoff.auto_create_pr
except (SeerApiError, SeerApiResponseValidationError):
logger.exception(
"coding_agent.get_project_seer_preferences_error",
extra={
"organization_id": organization.id,
"run_id": run_id,
"project_id": autofix_state.request.project_id,
},
)
if features.has("organizations:seer-project-settings-read-from-sentry", organization):
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:
auto_create_pr = preference.automation_handoff.auto_create_pr
except Project.DoesNotExist:
logger.exception(
"coding_agent.project_not_found",
extra={
"organization_id": organization.id,
"run_id": run_id,
"project_id": autofix_state.request.project_id,
},
)
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
except (SeerApiError, SeerApiResponseValidationError):
logger.exception(
"coding_agent.get_project_seer_preferences_error",
extra={
"organization_id": organization.id,
"run_id": run_id,
"project_id": autofix_state.request.project_id,
},
)

repos = set(
_extract_repos_from_root_cause(autofix_state)
Expand Down
9 changes: 8 additions & 1 deletion src/sentry/seer/autofix/issue_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
is_seer_autotriggered_autofix_rate_limited_and_increment,
is_seer_seat_based_tier_enabled,
make_get_project_preference_request,
read_preference_from_sentry_db,
)
from sentry.seer.entrypoints.cache import SeerOperatorAutofixCache
from sentry.seer.entrypoints.operator import SeerAutofixOperator
Expand Down Expand Up @@ -511,7 +512,13 @@ def get_automation_stopping_point(group: Group) -> AutofixStoppingPoint:
"""
fixability_score = get_and_update_group_fixability_score(group)
fixability_stopping_point = _get_stopping_point_from_fixability(fixability_score)
user_preference = _fetch_user_preference(group.project.id)

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
else:
user_preference = _fetch_user_preference(group.project.id)

return _apply_user_preference_upper_bound(fixability_stopping_point, user_preference)


Expand Down
75 changes: 45 additions & 30 deletions src/sentry/seer/autofix/on_completion_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from sentry.seer.autofix.utils import (
AutofixStoppingPoint,
get_project_seer_preferences,
read_preference_from_sentry_db,
resolve_repository_ids,
set_project_seer_preference,
write_preference_to_sentry_db,
Expand Down Expand Up @@ -488,50 +489,64 @@ def _get_handoff_config_if_applicable(
return None

# Check project preferences
try:
preference_response = get_project_seer_preferences(group.project_id)
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_response or not preference_response.preference:
return None
handoff_config = preference_response.preference.automation_handoff
if not handoff_config:
return None
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 handoff_config
if not preference:
return None
return preference.automation_handoff

@classmethod
def _clear_handoff_preference(
cls, project: Project, run_id: int, organization: Organization
) -> None:
"""Clear automation_handoff from project preferences after integration is not found."""
try:
preference_response = get_project_seer_preferences(project.id)
if preference_response and preference_response.preference:
updated_preference = preference_response.preference.copy(
update={"automation_handoff": None}
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(
"autofix.on_completion_hook.clear_handoff_preference_failed",
extra={"run_id": run_id, "organization_id": organization.id},
)
set_project_seer_preference(updated_preference)
return

if features.has("organizations:seer-project-settings-dual-write", organization):
try:
validated_pref = SeerProjectPreference.validate(updated_preference)
resolved_pref = resolve_repository_ids(organization.id, [validated_pref])
write_preference_to_sentry_db(project, resolved_pref[0])
except Exception:
logger.exception(
"seer.write_preferences.failed",
extra={"project_id": project.id, "organization_id": organization.id},
)
if not preference or preference.automation_handoff is None:
return

updated_preference = preference.copy(update={"automation_handoff": None})

try:
set_project_seer_preference(updated_preference)
except (SeerApiError, SeerApiResponseValidationError):
logger.exception(
"autofix.on_completion_hook.clear_handoff_preference_failed",
extra={"run_id": run_id, "organization_id": organization.id},
)
return
Comment thread
srest2021 marked this conversation as resolved.

if features.has("organizations:seer-project-settings-dual-write", organization):
try:
resolved_preference = resolve_repository_ids(
organization.id, [SeerProjectPreference.validate(updated_preference)]
)[0]
write_preference_to_sentry_db(project, resolved_preference)
except Exception:
logger.exception(
"seer.write_preferences.failed",
extra={"project_id": project.id, "organization_id": organization.id},
)
Comment thread
sentry[bot] marked this conversation as resolved.
Comment thread
srest2021 marked this conversation as resolved.

@classmethod
def _trigger_coding_agent_handoff(
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/seer/autofix/trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def get_seat_based_seer_automation_skip_reason(
return "automation_already_dispatched" # Another process already dispatched automation

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

return None
Loading
Loading