Skip to content
Open
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
143 changes: 50 additions & 93 deletions src/sentry/seer/autofix/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,29 +331,20 @@ def deduplicate_repositories(
return deduplicated


def _write_preferences_to_sentry_db(
project_preferences: list[tuple[Project, SeerProjectPreference]],
) -> None:
"""Write project preferences to ProjectOption + SeerProjectRepository.

Replaces all existing SeerProjectRepository rows for the given projects.
"""
if not project_preferences:
return

def write_preference_to_sentry_db(preference: SeerProjectPreference) -> None:
"""Write a single Seer project preference to ProjectOption and SeerProjectRepository.
Replaces the project's existing SeerProjectRepository rows.
TODO(AIML-2753): Add support for writing autofix_automation_tuning"""
with transaction.atomic(using=router.db_for_write(SeerProjectRepository)):
project_ids = {project.id for project, _ in project_preferences}

# Only delete SeerProjectRepository for active repos.
SeerProjectRepository.objects.filter(
project_repository__project_id__in=project_ids,
project_repository__project_id=preference.project_id,
project_repository__repository__status=ObjectStatus.ACTIVE,
).delete()

all_repo_ids = {
repo_def.repository_id
for _, pref in project_preferences
for repo_def in pref.repositories
for repo_def in preference.repositories
if repo_def.repository_id is not None
}
active_repo_ids = (
Expand All @@ -367,60 +358,55 @@ def _write_preferences_to_sentry_db(
)

# Collect project repos to create.
project_repos_to_create: list[tuple[Project, int, SeerProjectRepository]] = []
overrides_by_key: dict[tuple[int, int], list[BranchOverride]] = {}
for project, pref in project_preferences:
for repo_def in pref.repositories:
if repo_def.repository_id is None:
sentry_sdk.capture_message(
"SeerRepoDefinition missing repository_id at write time",
level="error",
extras={
"project_id": project.id,
"organization_id": project.organization_id,
"external_id": repo_def.external_id,
},
)
continue

# Only create new project repos for active repos.
if repo_def.repository_id not in active_repo_ids:
continue

project_repos_to_create.append(
(
project,
repo_def.repository_id,
SeerProjectRepository(
branch_name=repo_def.branch_name,
instructions=repo_def.instructions,
),
)
project_repos_to_create: list[tuple[int, SeerProjectRepository]] = []
overrides_by_repo_id: dict[int, list[BranchOverride]] = {}
for repo_def in preference.repositories:
if repo_def.repository_id is None:
sentry_sdk.capture_message(
"SeerRepoDefinition missing repository_id at write time",
level="error",
extras={
"project_id": preference.project_id,
"organization_id": preference.organization_id,
"external_id": repo_def.external_id,
},
)
continue

# Only create new project repos for active repos.
if repo_def.repository_id not in active_repo_ids:
continue

project_repos_to_create.append(
(
repo_def.repository_id,
SeerProjectRepository(
branch_name=repo_def.branch_name,
instructions=repo_def.instructions,
),
)
)

if repo_def.branch_overrides:
overrides_by_key[(project.id, repo_def.repository_id)] = (
repo_def.branch_overrides
)
if repo_def.branch_overrides:
overrides_by_repo_id[repo_def.repository_id] = repo_def.branch_overrides

if project_repos_to_create:
for project, repository_id, spr in project_repos_to_create:
for repository_id, spr in project_repos_to_create:
spr.project_repository, _ = ProjectRepository.objects.get_or_create_with_source(
project_id=project.id,
project_id=preference.project_id,
repository_id=repository_id,
source=ProjectRepositorySource.SEER_PREFERENCE,
)

created_project_repos = SeerProjectRepository.objects.bulk_create(
[spr for _, _, spr in project_repos_to_create]
[spr for _, spr in project_repos_to_create]
)

# Create branch overrides using the created project repos.
overrides_to_create: list[SeerProjectRepositoryBranchOverride] = []
for seer_project_repo in created_project_repos:
pr = seer_project_repo.project_repository
key = (pr.project_id, pr.repository_id)
for override in overrides_by_key.get(key, []):
repository_id = seer_project_repo.project_repository.repository_id
for override in overrides_by_repo_id.get(repository_id, []):
overrides_to_create.append(
SeerProjectRepositoryBranchOverride(
seer_project_repository=seer_project_repo,
Expand All @@ -433,48 +419,19 @@ def _write_preferences_to_sentry_db(

# Write ProjectOptions last so cache updates happen after repo DB writes succeed
# (cache cannot be rolled back by the transaction).
for project, pref in project_preferences:
update = SeerProjectSettingsUpdate()

if pref.automated_run_stopping_point is not None:
update["stopping_point"] = pref.automated_run_stopping_point

if pref.automation_handoff is not None:
update["agent"] = AutomationCodingAgent(pref.automation_handoff.target)
update["integration_id"] = pref.automation_handoff.integration_id
update["auto_create_pr"] = pref.automation_handoff.auto_create_pr
else:
update["agent"] = AutomationCodingAgent.SEER

update_seer_project_settings([project.id], update)
update = SeerProjectSettingsUpdate()

if preference.automated_run_stopping_point is not None:
update["stopping_point"] = preference.automated_run_stopping_point

def write_preference_to_sentry_db(project: Project, preference: SeerProjectPreference) -> None:
"""Write a single Seer project preference to ProjectOption and SeerProjectRepository.
TODO(AIML-2753): Add support for writing autofix_automation_tuning"""
_write_preferences_to_sentry_db([(project, preference)])


def bulk_write_preferences_to_sentry_db(
projects: list[Project], preferences: list[SeerProjectPreference]
) -> None:
"""Write multiple Seer project preferences using bulk operations.
TODO(AIML-2753): Add support for writing autofix_automation_tuning
"""
projects_by_id = {p.id: p for p in projects}

project_preferences: list[tuple[Project, SeerProjectPreference]] = []
for pref in preferences:
project = projects_by_id.get(pref.project_id)
if project is None:
logger.warning(
"seer.write_preferences.project_not_found",
extra={"project_id": pref.project_id, "organization_id": pref.organization_id},
)
continue
project_preferences.append((project, pref))
if preference.automation_handoff is not None:
update["agent"] = AutomationCodingAgent(preference.automation_handoff.target)
update["integration_id"] = preference.automation_handoff.integration_id
update["auto_create_pr"] = preference.automation_handoff.auto_create_pr
else:
update["agent"] = AutomationCodingAgent.SEER

_write_preferences_to_sentry_db(project_preferences)
update_seer_project_settings([preference.project_id], update)


def clear_preference_automation_handoff(project: Project) -> None:
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/seer/endpoints/project_seer_preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def post(self, request: Request, project: Project) -> Response:
"project_id": project.id,
}
)
write_preference_to_sentry_db(project, preference)
write_preference_to_sentry_db(preference)

return Response(status=204)

Expand Down
119 changes: 8 additions & 111 deletions tests/sentry/seer/autofix/test_autofix_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
CodingAgentProviderType,
add_seer_project_repos,
bulk_read_preferences_from_sentry_db,
bulk_write_preferences_to_sentry_db,
clear_preference_automation_handoff,
deduplicate_repositories,
extract_api_error_message,
Expand Down Expand Up @@ -436,9 +435,6 @@ def test_normalizes_provider_alias_in_key(self) -> None:


class TestWritePreferencesToSentryDb(TestCase):
"""Tests for _write_preferences_to_sentry_db via write_preference_to_sentry_db
and bulk_write_preferences_to_sentry_db."""

def setUp(self) -> None:
super().setUp()
self.organization = self.create_organization()
Expand All @@ -464,7 +460,7 @@ def test_writes_project_options(self) -> None:
),
)

write_preference_to_sentry_db(self.project, preference)
write_preference_to_sentry_db(preference)

assert self.project.get_option("sentry:seer_automated_run_stopping_point") == "open_pr"
assert self.project.get_option("sentry:seer_automation_handoff_point") == "root_cause"
Expand All @@ -484,7 +480,7 @@ def test_deletes_project_options_when_defaults(self) -> None:
organization_id=self.organization.id, project_id=self.project.id, repositories=[]
)

write_preference_to_sentry_db(self.project, preference)
write_preference_to_sentry_db(preference)

assert self.project.get_option("sentry:seer_automated_run_stopping_point") == "code_changes"
assert self.project.get_option("sentry:seer_automation_handoff_point") is None
Expand Down Expand Up @@ -521,7 +517,7 @@ def test_creates_seer_project_repository_with_branch_overrides(self) -> None:
],
)

write_preference_to_sentry_db(self.project, preference)
write_preference_to_sentry_db(preference)

seer_repo = SeerProjectRepository.objects.get(project_repository__project=self.project)
assert seer_repo.project_repository.repository_id == self.repo.id
Expand Down Expand Up @@ -562,7 +558,7 @@ def test_replaces_existing_preference_on_write(self) -> None:
),
],
)
write_preference_to_sentry_db(self.project, preference_to_replace)
write_preference_to_sentry_db(preference_to_replace)

assert (
SeerProjectRepository.objects.filter(project_repository__project=self.project).count()
Expand Down Expand Up @@ -595,7 +591,7 @@ def test_replaces_existing_preference_on_write(self) -> None:
),
],
)
write_preference_to_sentry_db(self.project, new_preference)
write_preference_to_sentry_db(new_preference)

repos = SeerProjectRepository.objects.filter(project_repository__project=self.project)
assert len(repos) == 1
Expand Down Expand Up @@ -638,7 +634,7 @@ def test_multiple_repos_for_one_project(self) -> None:
],
)

write_preference_to_sentry_db(self.project, preference)
write_preference_to_sentry_db(preference)

repos = SeerProjectRepository.objects.filter(
project_repository__project=self.project
Expand Down Expand Up @@ -681,7 +677,7 @@ def test_preserves_existing_seer_project_repository_for_inactive_repo(self) -> N
),
],
)
write_preference_to_sentry_db(self.project, new_preference)
write_preference_to_sentry_db(new_preference)

project_repos = SeerProjectRepository.objects.filter(
project_repository__project=self.project
Expand Down Expand Up @@ -719,113 +715,14 @@ def test_skips_new_seer_project_repository_for_inactive_repo(self) -> None:
),
],
)
write_preference_to_sentry_db(self.project, new_preference)
write_preference_to_sentry_db(new_preference)

repos = list(SeerProjectRepository.objects.filter(project_repository__project=self.project))
assert len(repos) == 1
assert repos[0].project_repository.repository_id == self.repo.id
assert repos[0].branch_name == "original"
assert repos[0].instructions == "original instructions"

def test_bulk_write_multiple_projects(self) -> None:
project2 = self.create_project(organization=self.organization)
repo2 = self.create_repo(
project=project2,
provider="integrations:github",
external_id="ext456",
name="test-org/other-repo",
)

preferences = [
SeerProjectPreference(
organization_id=self.organization.id,
project_id=self.project.id,
repositories=[
SeerRepoDefinition(
repository_id=self.repo.id,
provider="github",
owner="test-org",
name="test-repo",
external_id="ext123",
branch_name="develop",
),
],
automated_run_stopping_point="open_pr",
),
SeerProjectPreference(
organization_id=self.organization.id,
project_id=project2.id,
repositories=[
SeerRepoDefinition(
repository_id=repo2.id,
provider="github",
owner="test-org",
name="other-repo",
external_id="ext456",
instructions="Be careful",
),
],
automated_run_stopping_point="code_changes",
),
]

bulk_write_preferences_to_sentry_db([self.project, project2], preferences)

p1_repos = SeerProjectRepository.objects.filter(project_repository__project=self.project)
assert len(p1_repos) == 1
assert p1_repos[0].project_repository.repository_id == self.repo.id
assert p1_repos[0].branch_name == "develop"
assert self.project.get_option("sentry:seer_automated_run_stopping_point") == "open_pr"

p2_repos = SeerProjectRepository.objects.filter(project_repository__project=project2)
assert len(p2_repos) == 1
assert p2_repos[0].project_repository.repository_id == repo2.id
assert p2_repos[0].instructions == "Be careful"
assert project2.get_option("sentry:seer_automated_run_stopping_point") == "code_changes"

def test_bulk_write_replaces_per_project(self) -> None:
project2 = self.create_project(organization=self.organization)
repo2 = self.create_repo(
project=project2,
provider="integrations:github",
external_id="ext456",
name="test-org/other-repo",
)

self.create_seer_project_repository(
project=self.project, repository_id=self.repo.id, branch_name="project-1-branch"
)
self.create_seer_project_repository(
project=project2, repository_id=repo2.id, branch_name="project-2-branch"
)

bulk_write_preferences_to_sentry_db(
[self.project, project2],
[
SeerProjectPreference(
organization_id=self.organization.id,
project_id=self.project.id,
repositories=[
SeerRepoDefinition(
repository_id=self.repo.id,
provider="github",
owner="test-org",
name="test-repo",
external_id="ext123",
branch_name="new-branch",
),
],
),
],
)

# When bulk writing, existing repos for included projects are replaced,
# but repos for projects NOT in the preferences list are untouched.
p1_repo = SeerProjectRepository.objects.get(project_repository__project=self.project)
assert p1_repo.branch_name == "new-branch"
p2_repo = SeerProjectRepository.objects.get(project_repository__project=project2)
assert p2_repo.branch_name == "project-2-branch"


class TestClearPreferenceAutomationHandoff(TestCase):
def setUp(self) -> None:
Expand Down
Loading