Skip to content

Commit 99c5a91

Browse files
committed
ref(night-shift): Use seer-project-settings helpers for project eligibility
Replace direct SeerProjectRepository queries in _get_eligible_projects with bulk_read_preferences_from_sentry_db / bulk_get_project_preferences behind the seer-project-settings-read-from-sentry feature flag, matching the pattern used in context_engine_index and autofix. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Agent transcript: https://claudescope.sentry.dev/share/TusCoPLrgCYHLxdSUeDTGIDPYU1f6BITAaO7rK6QDl0
1 parent 50f0b20 commit 99c5a91

File tree

6 files changed

+154
-29
lines changed

6 files changed

+154
-29
lines changed

src/sentry/projectoptions/defaults.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@
202202
"sentry:seer_automation_handoff_target",
203203
"sentry:seer_automation_handoff_integration_id",
204204
"sentry:seer_automation_handoff_auto_create_pr",
205+
"sentry:autofix_automation_tuning",
205206
]
206207

207208
# Boolean to enable/disable preprod size analysis for this project.

src/sentry/seer/autofix/utils.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,7 @@ def read_preference_from_sentry_db(project: Project) -> SeerProjectPreference |
758758
repositories=repo_definitions,
759759
automated_run_stopping_point=project.get_option("sentry:seer_automated_run_stopping_point"),
760760
automation_handoff=_build_automation_handoff(project.get_option),
761+
autofix_automation_tuning=project.get_option("sentry:autofix_automation_tuning"),
761762
)
762763

763764

@@ -813,11 +814,49 @@ def _get_project_option(key: str) -> Any:
813814
"sentry:seer_automated_run_stopping_point"
814815
),
815816
automation_handoff=_build_automation_handoff(_get_project_option),
817+
autofix_automation_tuning=_get_project_option("sentry:autofix_automation_tuning"),
816818
)
817819

818820
return result
819821

820822

823+
def bulk_read_preferences(
824+
organization: Organization, project_ids: list[int]
825+
) -> dict[int, SeerProjectPreference | None]:
826+
"""Read Seer project preferences in bulk, using the correct source based on feature flag.
827+
828+
Always returns ``dict[int, SeerProjectPreference | None]`` regardless of the
829+
underlying read path (Sentry DB or Seer API)."""
830+
if features.has("organizations:seer-project-settings-read-from-sentry", organization):
831+
return bulk_read_preferences_from_sentry_db(organization.id, project_ids)
832+
833+
raw = bulk_get_project_preferences(organization.id, project_ids)
834+
tuning_by_id = ProjectOption.objects.get_value_bulk_id(
835+
project_ids, "sentry:autofix_automation_tuning"
836+
)
837+
result: dict[int, SeerProjectPreference | None] = {}
838+
for pid, data in raw.items():
839+
int_pid = int(pid)
840+
if data is None:
841+
result[int_pid] = None
842+
continue
843+
try:
844+
pref = SeerProjectPreference.validate(data)
845+
except pydantic.ValidationError:
846+
logger.exception(
847+
"seer.bulk_read_preferences.validation_error",
848+
extra={"project_id": pid, "organization_id": organization.id},
849+
)
850+
result[int_pid] = None
851+
continue
852+
tuning = tuning_by_id.get(int_pid)
853+
if tuning is None:
854+
tuning = projectoptions.get_well_known_default("sentry:autofix_automation_tuning")
855+
pref.autofix_automation_tuning = tuning
856+
result[int_pid] = pref
857+
return result
858+
859+
821860
def set_project_seer_preference(preference: SeerProjectPreference) -> None:
822861
"""Set Seer project preference for a single project via Seer API."""
823862
response = make_set_project_preference_request(

src/sentry/seer/models/seer_api_models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from pydantic import BaseModel, Field
77

8+
from sentry.seer.autofix.constants import AutofixAutomationTuningSettings
9+
810

911
class BranchOverride(BaseModel):
1012
tag_name: str = Field(description="The tag key to match against")
@@ -96,6 +98,7 @@ class SeerProjectPreference(BaseModel):
9698
repositories: list[SeerRepoDefinition]
9799
automated_run_stopping_point: str | None = None
98100
automation_handoff: SeerAutomationHandoffConfiguration | None = None
101+
autofix_automation_tuning: AutofixAutomationTuningSettings | None = None
99102

100103

101104
class SeerRawPreferenceResponse(BaseModel):

src/sentry/tasks/seer/night_shift/cron.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
from sentry.models.organization import Organization, OrganizationStatus
1212
from sentry.models.project import Project
1313
from sentry.seer.autofix.constants import AutofixAutomationTuningSettings
14+
from sentry.seer.autofix.utils import bulk_read_preferences
1415
from sentry.seer.models.night_shift import SeerNightShiftRun, SeerNightShiftRunIssue
15-
from sentry.seer.models.project_repository import SeerProjectRepository
1616
from sentry.tasks.base import instrumented_task
1717
from sentry.tasks.seer.night_shift.agentic_triage import agentic_triage_strategy
1818
from sentry.taskworker.namespaces import seer_tasks
@@ -88,13 +88,22 @@ def run_night_shift_for_org(organization_id: int) -> None:
8888
}
8989
)
9090

91-
eligible_projects = _get_eligible_projects(organization)
92-
if not eligible_projects:
93-
logger.info(
94-
"night_shift.no_eligible_projects",
91+
try:
92+
eligible_projects = _get_eligible_projects(organization)
93+
if not eligible_projects:
94+
logger.info(
95+
"night_shift.no_eligible_projects",
96+
extra={
97+
"organization_id": organization_id,
98+
"organization_slug": organization.slug,
99+
},
100+
)
101+
return
102+
except Exception:
103+
logger.exception(
104+
"night_shift.failed_to_get_eligible_projects",
95105
extra={
96106
"organization_id": organization_id,
97-
"organization_slug": organization.slug,
98107
},
99108
)
100109
return
@@ -173,18 +182,19 @@ def _get_eligible_orgs_from_batch(
173182

174183
def _get_eligible_projects(organization: Organization) -> list[Project]:
175184
"""Return active projects that have automation enabled and connected repos."""
176-
projects_with_repos = set(
177-
SeerProjectRepository.objects.filter(
178-
project__organization=organization,
179-
project__status=ObjectStatus.ACTIVE,
180-
).values_list("project_id", flat=True)
181-
)
182-
if not projects_with_repos:
185+
project_map = {
186+
p.id: p
187+
for p in Project.objects.filter(organization=organization, status=ObjectStatus.ACTIVE)
188+
}
189+
if not project_map:
183190
return []
184191

185-
projects = Project.objects.filter(id__in=projects_with_repos)
192+
preferences = bulk_read_preferences(organization, list(project_map))
193+
186194
return [
187-
p
188-
for p in projects
189-
if p.get_option("sentry:autofix_automation_tuning") != AutofixAutomationTuningSettings.OFF
195+
project_map[pid]
196+
for pid, pref in preferences.items()
197+
if pref is not None
198+
and pref.repositories
199+
and pref.autofix_automation_tuning != AutofixAutomationTuningSettings.OFF
190200
]

tests/sentry/seer/autofix/test_autofix_utils.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66

77
from sentry.constants import SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, DataCategory
8-
from sentry.seer.autofix.constants import AutofixStatus
8+
from sentry.seer.autofix.constants import AutofixAutomationTuningSettings, AutofixStatus
99
from sentry.seer.autofix.trigger import is_issue_eligible_for_seer_automation
1010
from sentry.seer.autofix.utils import (
1111
AutofixState,
@@ -1311,6 +1311,37 @@ def test_project_with_repos_only(self):
13111311
assert result.automated_run_stopping_point == "code_changes"
13121312
assert result.automation_handoff is None
13131313

1314+
def test_autofix_automation_tuning_default(self):
1315+
SeerProjectRepository.objects.create(
1316+
project=self.project, repository=self.repo, branch_name="main"
1317+
)
1318+
1319+
result = read_preference_from_sentry_db(self.project)
1320+
assert result is not None
1321+
assert result.autofix_automation_tuning == AutofixAutomationTuningSettings.OFF
1322+
1323+
def test_autofix_automation_tuning_explicit(self):
1324+
SeerProjectRepository.objects.create(
1325+
project=self.project, repository=self.repo, branch_name="main"
1326+
)
1327+
self.project.update_option(
1328+
"sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.MEDIUM
1329+
)
1330+
1331+
result = read_preference_from_sentry_db(self.project)
1332+
assert result is not None
1333+
assert result.autofix_automation_tuning == AutofixAutomationTuningSettings.MEDIUM
1334+
1335+
def test_autofix_automation_tuning_alone_creates_preference(self):
1336+
self.project.update_option(
1337+
"sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.HIGH
1338+
)
1339+
1340+
result = read_preference_from_sentry_db(self.project)
1341+
assert result is not None
1342+
assert result.autofix_automation_tuning == AutofixAutomationTuningSettings.HIGH
1343+
assert result.repositories == []
1344+
13141345
def test_project_with_stopping_point_only(self):
13151346
self.project.update_option("sentry:seer_automated_run_stopping_point", "open_pr")
13161347

@@ -1479,6 +1510,35 @@ def test_bulk_returns_correct_preferences(self):
14791510
assert pref2.automation_handoff.integration_id == 99
14801511
assert pref2.automation_handoff.auto_create_pr is False
14811512

1513+
def test_autofix_automation_tuning_populated(self):
1514+
SeerProjectRepository.objects.create(
1515+
project=self.project1, repository=self.repo, branch_name="main"
1516+
)
1517+
self.project1.update_option(
1518+
"sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.HIGH
1519+
)
1520+
1521+
result = bulk_read_preferences_from_sentry_db(
1522+
self.organization.id, [self.project1.id, self.project2.id]
1523+
)
1524+
1525+
pref1 = result[self.project1.id]
1526+
assert pref1 is not None
1527+
assert pref1.autofix_automation_tuning == AutofixAutomationTuningSettings.HIGH
1528+
1529+
assert result[self.project2.id] is None
1530+
1531+
def test_autofix_automation_tuning_defaults_to_off(self):
1532+
SeerProjectRepository.objects.create(
1533+
project=self.project1, repository=self.repo, branch_name="main"
1534+
)
1535+
1536+
result = bulk_read_preferences_from_sentry_db(self.organization.id, [self.project1.id])
1537+
1538+
pref = result[self.project1.id]
1539+
assert pref is not None
1540+
assert pref.autofix_automation_tuning == AutofixAutomationTuningSettings.OFF
1541+
14821542
def test_wrong_organization_excluded(self):
14831543
other_org = self.create_organization()
14841544
SeerProjectRepository.objects.create(

tests/sentry/tasks/seer/test_night_shift.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,19 +92,20 @@ def test_filters_by_automation_and_repos(self) -> None:
9292
eligible.update_option(
9393
"sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.MEDIUM
9494
)
95-
repo = self.create_repo(project=eligible, provider="github")
95+
repo = self.create_repo(project=eligible, provider="github", name="owner/eligible-repo")
9696
SeerProjectRepository.objects.create(project=eligible, repository=repo)
9797

9898
# Automation off (even with repo)
9999
off = self.create_project(organization=org)
100100
off.update_option("sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.OFF)
101-
repo2 = self.create_repo(project=off, provider="github")
101+
repo2 = self.create_repo(project=off, provider="github", name="owner/off-repo")
102102
SeerProjectRepository.objects.create(project=off, repository=repo2)
103103

104104
# No connected repo
105105
self.create_project(organization=org)
106106

107-
assert _get_eligible_projects(org) == [eligible]
107+
with self.feature("organizations:seer-project-settings-read-from-sentry"):
108+
assert _get_eligible_projects(org) == [eligible]
108109

109110

110111
@django_db_all
@@ -115,7 +116,7 @@ def _make_eligible(self, project):
115116
project.update_option(
116117
"sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.MEDIUM
117118
)
118-
repo = self.create_repo(project=project, provider="github")
119+
repo = self.create_repo(project=project, provider="github", name=f"owner/{project.slug}")
119120
SeerProjectRepository.objects.create(project=project, repository=repo)
120121

121122
def _store_event_and_update_group(self, project, fingerprint, **group_attrs):
@@ -139,7 +140,10 @@ def test_no_eligible_projects(self) -> None:
139140
org = self.create_organization()
140141
self.create_project(organization=org)
141142

142-
with patch("sentry.tasks.seer.night_shift.cron.logger") as mock_logger:
143+
with (
144+
self.feature("organizations:seer-project-settings-read-from-sentry"),
145+
patch("sentry.tasks.seer.night_shift.cron.logger") as mock_logger,
146+
):
143147
run_night_shift_for_org(org.id)
144148
mock_logger.info.assert_called_once()
145149
assert mock_logger.info.call_args.args[0] == "night_shift.no_eligible_projects"
@@ -166,6 +170,7 @@ def test_selects_candidates_and_skips_triggered(self) -> None:
166170
)
167171

168172
with (
173+
self.feature("organizations:seer-project-settings-read-from-sentry"),
169174
patch(
170175
"sentry.tasks.seer.night_shift.agentic_triage.make_llm_generate_request",
171176
return_value=_mock_llm_response([high_fix.id, low_fix.id]),
@@ -204,6 +209,7 @@ def test_global_ranking_across_projects(self) -> None:
204209
)
205210

206211
with (
212+
self.feature("organizations:seer-project-settings-read-from-sentry"),
207213
patch(
208214
"sentry.tasks.seer.night_shift.agentic_triage.make_llm_generate_request",
209215
return_value=_mock_llm_response([high_group.id, low_group.id]),
@@ -225,9 +231,12 @@ def test_triage_error_records_error_message(self) -> None:
225231
project, "fixable", seer_fixability_score=0.9, times_seen=5
226232
)
227233

228-
with patch(
229-
"sentry.tasks.seer.night_shift.cron.agentic_triage_strategy",
230-
side_effect=RuntimeError("boom"),
234+
with (
235+
self.feature("organizations:seer-project-settings-read-from-sentry"),
236+
patch(
237+
"sentry.tasks.seer.night_shift.cron.agentic_triage_strategy",
238+
side_effect=RuntimeError("boom"),
239+
),
231240
):
232241
run_night_shift_for_org(org.id)
233242

@@ -244,9 +253,12 @@ def test_empty_candidates_creates_run_with_no_issues(self) -> None:
244253
project, "fixable", seer_fixability_score=0.9, times_seen=5
245254
)
246255

247-
with patch(
248-
"sentry.tasks.seer.night_shift.cron.agentic_triage_strategy",
249-
return_value=[],
256+
with (
257+
self.feature("organizations:seer-project-settings-read-from-sentry"),
258+
patch(
259+
"sentry.tasks.seer.night_shift.cron.agentic_triage_strategy",
260+
return_value=[],
261+
),
250262
):
251263
run_night_shift_for_org(org.id)
252264

0 commit comments

Comments
 (0)