-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
feat(seer): Add dual-read helpers for Seer project preferences from Sentry DB #111591
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
db1575c
3be1c6d
241b8bd
e6ca4d1
24873f1
bd5959d
14f573d
03f8fce
770c187
061dc64
35bbeff
c47e758
92dd7e4
bdb89e2
a0b46d0
cdcff9e
46f2d14
4b6c36a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| import logging | ||
| from collections.abc import Iterable | ||
| from collections import defaultdict | ||
| from collections.abc import Callable, Iterable, Mapping | ||
| from datetime import UTC, datetime | ||
| from enum import StrEnum | ||
| from typing import Any, NotRequired, TypedDict | ||
|
|
@@ -13,7 +14,7 @@ | |
| from urllib3 import BaseHTTPResponse, HTTPConnectionPool | ||
| from urllib3.util.retry import Retry | ||
|
|
||
| from sentry import features, options, ratelimits | ||
| from sentry import features, options, projectoptions, ratelimits | ||
| from sentry.constants import ( | ||
| AUTO_OPEN_PRS_DEFAULT, | ||
| SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT, | ||
|
|
@@ -25,16 +26,20 @@ | |
| get_sorted_code_mapping_configs, | ||
| ) | ||
| from sentry.models.group import Group | ||
| from sentry.models.options.project_option import ProjectOption | ||
| from sentry.models.organization import Organization | ||
| from sentry.models.project import Project | ||
| from sentry.models.repository import Repository | ||
| from sentry.net.http import connection_from_url | ||
| from sentry.projectoptions.defaults import SEER_PROJECT_PREFERENCE_OPTION_KEYS | ||
| from sentry.seer.autofix.constants import AutofixAutomationTuningSettings, AutofixStatus | ||
| from sentry.seer.constants import SEER_SUPPORTED_SCM_PROVIDERS | ||
| from sentry.seer.models import ( | ||
| AutofixHandoffPoint, | ||
| BranchOverride, | ||
| SeerApiError, | ||
| SeerApiResponseValidationError, | ||
| SeerAutomationHandoffConfiguration, | ||
| SeerPermissionError, | ||
| SeerProjectPreference, | ||
| SeerRawPreferenceResponse, | ||
|
|
@@ -44,10 +49,6 @@ | |
| SeerProjectRepository, | ||
| SeerProjectRepositoryBranchOverride, | ||
| ) | ||
| from sentry.seer.models.seer_api_models import ( | ||
| AutofixHandoffPoint, | ||
| SeerAutomationHandoffConfiguration, | ||
| ) | ||
| from sentry.seer.signed_seer_api import SeerViewerContext, make_signed_seer_api_request | ||
| from sentry.utils.cache import cache | ||
| from sentry.utils.outcomes import Outcome, track_outcome | ||
|
|
@@ -674,6 +675,141 @@ | |
| _write_preferences_to_sentry_db(project_preferences) | ||
|
|
||
|
|
||
| def build_repo_definition_from_project_repo( | ||
| seer_project_repo: SeerProjectRepository, | ||
| ) -> SeerRepoDefinition: | ||
| """Build a SeerRepoDefinition from a SeerProjectRepository with its joined Repository.""" | ||
| repo = seer_project_repo.repository | ||
| repo_name_sections = repo.name.split("/") | ||
| if len(repo_name_sections) < 2: | ||
| raise ValueError(f"Invalid repository name format: {repo.name}") | ||
|
Check warning on line 685 in src/sentry/seer/autofix/utils.py
|
||
|
|
||
| return SeerRepoDefinition( | ||
| repository_id=repo.id, | ||
| organization_id=repo.organization_id, | ||
| integration_id=str(repo.integration_id) if repo.integration_id is not None else None, | ||
| provider=repo.provider or "", | ||
| owner=repo_name_sections[0], | ||
|
sentry[bot] marked this conversation as resolved.
|
||
| name="/".join(repo_name_sections[1:]), | ||
| external_id=repo.external_id or "", | ||
| branch_name=seer_project_repo.branch_name, | ||
| instructions=seer_project_repo.instructions, | ||
| branch_overrides=[ | ||
| BranchOverride( | ||
| tag_name=bo.tag_name, | ||
| tag_value=bo.tag_value, | ||
| branch_name=bo.branch_name, | ||
| ) | ||
| for bo in seer_project_repo.branch_overrides.all() | ||
| ], | ||
| ) | ||
|
|
||
|
|
||
| def _build_automation_handoff( | ||
| get_option: Callable[[str], Any], | ||
| ) -> SeerAutomationHandoffConfiguration | None: | ||
| """Build a SeerAutomationHandoffConfiguration from option values, or None if incomplete.""" | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Private for now since idk if we'll have any other use cases where this callable arg will be helpful or relevant) |
||
| handoff_point = get_option("sentry:seer_automation_handoff_point") | ||
| handoff_target = get_option("sentry:seer_automation_handoff_target") | ||
| handoff_integration_id = get_option("sentry:seer_automation_handoff_integration_id") | ||
|
|
||
| if handoff_point is None or handoff_target is None or handoff_integration_id is None: | ||
| return None | ||
|
|
||
| return SeerAutomationHandoffConfiguration( | ||
| handoff_point=handoff_point, | ||
| target=handoff_target, | ||
| integration_id=handoff_integration_id, | ||
| auto_create_pr=get_option("sentry:seer_automation_handoff_auto_create_pr"), | ||
| ) | ||
|
|
||
|
|
||
| def read_preference_from_sentry_db(project: Project) -> SeerProjectPreference | None: | ||
| """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`.""" | ||
| seer_project_repo_qs = ( | ||
| SeerProjectRepository.objects.filter(project=project) | ||
| .select_related("repository") | ||
| .prefetch_related("branch_overrides") | ||
| ) | ||
| repo_definitions = [ | ||
| build_repo_definition_from_project_repo(project_repo) | ||
| for project_repo in seer_project_repo_qs | ||
| ] | ||
|
|
||
| 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, | ||
| repositories=repo_definitions, | ||
| automated_run_stopping_point=project.get_option("sentry:seer_automated_run_stopping_point"), | ||
| automation_handoff=_build_automation_handoff(project.get_option), | ||
| ) | ||
|
|
||
|
|
||
| def bulk_read_preferences_from_sentry_db( | ||
| organization_id: int, project_ids: list[int] | ||
| ) -> dict[int, SeerProjectPreference | None]: | ||
| """Bulk read Seer preferences from Sentry DB. | ||
|
|
||
| For now, should only be used under feature flag `organizations:seer-project-settings-read-from-sentry`.""" | ||
| if not project_ids: | ||
| return {} | ||
|
|
||
| projects = list(Project.objects.filter(id__in=project_ids, organization_id=organization_id)) | ||
|
|
||
| repo_definitions_by_project: defaultdict[int, list[SeerRepoDefinition]] = defaultdict(list) | ||
| for project_repo in ( | ||
| SeerProjectRepository.objects.filter(project_id__in=project_ids) | ||
| .select_related("repository") | ||
| .prefetch_related("branch_overrides") | ||
| ): | ||
| repo_definitions_by_project[project_repo.project_id].append( | ||
| build_repo_definition_from_project_repo(project_repo) | ||
| ) | ||
|
|
||
| # get_value_bulk_id returns None for missing options, unlike project.get_option | ||
| # which automatically falls back to the registered well-known key default. | ||
| project_options: dict[str, Mapping[int, Any]] = { | ||
| key: ProjectOption.objects.get_value_bulk_id(project_ids, key) | ||
| for key in SEER_PROJECT_PREFERENCE_OPTION_KEYS | ||
| } | ||
|
|
||
| result: dict[int, SeerProjectPreference | None] = {} | ||
| for project in projects: | ||
| has_configured_options = any( | ||
| project_options[key][project.id] is not None | ||
| for key in SEER_PROJECT_PREFERENCE_OPTION_KEYS | ||
| ) | ||
|
srest2021 marked this conversation as resolved.
|
||
| 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] | ||
| if value is None: | ||
| return projectoptions.get_well_known_default(key, project=project) | ||
| return value | ||
|
srest2021 marked this conversation as resolved.
|
||
|
|
||
| result[project.id] = SeerProjectPreference( | ||
| organization_id=project.organization_id, | ||
| project_id=project.id, | ||
| repositories=repo_definitions_by_project.get(project.id, []), | ||
| automated_run_stopping_point=_get_project_option( | ||
| "sentry:seer_automated_run_stopping_point" | ||
| ), | ||
| automation_handoff=_build_automation_handoff(_get_project_option), | ||
| ) | ||
|
|
||
| return result | ||
|
|
||
|
|
||
| def set_project_seer_preference(preference: SeerProjectPreference) -> None: | ||
| """Set Seer project preference for a single project via Seer API.""" | ||
| response = make_set_project_preference_request( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.