Skip to content
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
878d082
add update_seer_project_settings helper, tests, and constants prep
srest2021 May 6, 2026
8829ef6
add bulk and single-project Seer settings endpoints
srest2021 May 7, 2026
dbaab6f
:hammer_and_wrench: Sync API Urls to TypeScript
getsantry[bot] May 7, 2026
4e3acca
cleaning up
srest2021 May 7, 2026
3499981
cleaning up
srest2021 May 7, 2026
7aad2cb
Merge branch 'srest2021/CW-1285-utils-prep' into srest2021/CW-1285
srest2021 May 7, 2026
b30614a
use CODING_AGENT_ALIAS_TO_HANDOFF_TARGET from constants in organizati…
srest2021 May 7, 2026
5df803c
Merge branch 'srest2021/CW-1285-utils-prep' into srest2021/CW-1285
srest2021 May 7, 2026
d661040
remove unnecessary delete
srest2021 May 7, 2026
b77074e
fix test
srest2021 May 7, 2026
486991f
Merge branch 'srest2021/CW-1285-utils-prep' into srest2021/CW-1285
srest2021 May 7, 2026
1e3b611
address review comment; dont clear auto_create_pr if stopping point off
srest2021 May 7, 2026
8f1ccc2
fix missing alias types
srest2021 May 7, 2026
18562b2
fix typing
srest2021 May 7, 2026
3a2290f
Merge branch 'srest2021/CW-1285-utils-prep' into srest2021/CW-1285
srest2021 May 7, 2026
7922e49
add alias enum
srest2021 May 7, 2026
aff51bc
add project lock
srest2021 May 7, 2026
5b0413e
Merge branch 'srest2021/CW-1285-utils-prep' into srest2021/CW-1285
srest2021 May 7, 2026
28acf0f
sort by stopping point
srest2021 May 7, 2026
372f4a1
use coding agent enum and remove dict
srest2021 May 7, 2026
10e8c4d
Merge branch 'srest2021/CW-1285-utils-prep' into srest2021/CW-1285
srest2021 May 7, 2026
696039d
remove alias conversion
srest2021 May 7, 2026
8e1043e
move to utils and use codingagentprovidertype
srest2021 May 7, 2026
b22f7ed
minimize diff
srest2021 May 7, 2026
e2aa4e4
minimize diff
srest2021 May 7, 2026
b6291e5
Merge branch 'srest2021/CW-1285-utils-prep' into srest2021/CW-1285
srest2021 May 7, 2026
e7fc0b6
update enum name and throw invalidsearchquery if op not supported
srest2021 May 7, 2026
0572992
resolve merge conflicts
srest2021 May 7, 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
14 changes: 14 additions & 0 deletions src/sentry/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,10 @@
from sentry.seer.endpoints.organization_seer_workflows import OrganizationSeerWorkflowsEndpoint
from sentry.seer.endpoints.project_seer_night_shift import ProjectSeerNightShiftEndpoint
from sentry.seer.endpoints.project_seer_preferences import ProjectSeerPreferencesEndpoint
from sentry.seer.endpoints.project_settings import (
OrganizationSeerProjectSettingsEndpoint,
ProjectSeerSettingsEndpoint,
)
from sentry.seer.endpoints.search_agent_start import SearchAgentStartEndpoint
from sentry.seer.endpoints.search_agent_state import SearchAgentStateEndpoint
from sentry.seer.endpoints.seer_rpc import SeerRpcServiceEndpoint
Expand Down Expand Up @@ -2491,6 +2495,11 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]:
OrganizationAutofixAutomationSettingsEndpoint.as_view(),
name="sentry-api-0-organization-autofix-automation-settings",
),
re_path(
r"^(?P<organization_id_or_slug>[^/]+)/seer/projects/$",
OrganizationSeerProjectSettingsEndpoint.as_view(),
name="sentry-api-0-organization-seer-project-settings",
),
re_path(
r"^(?P<organization_id_or_slug>[^/]+)/seer-rpc/(?P<method_name>\w+)/$",
OrganizationSeerRpcEndpoint.as_view(),
Expand Down Expand Up @@ -3396,6 +3405,11 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]:
name="sentry-api-0-project-tempest-credentials-details",
),
# Seer
re_path(
r"^(?P<organization_id_or_slug>[^/]+)/(?P<project_id_or_slug>[^/]+)/seer/settings/$",
ProjectSeerSettingsEndpoint.as_view(),
name="sentry-api-0-project-seer-settings",
),
re_path(
r"^(?P<organization_id_or_slug>[^/]+)/(?P<project_id_or_slug>[^/]+)/seer/preferences/$",
ProjectSeerPreferencesEndpoint.as_view(),
Expand Down
11 changes: 5 additions & 6 deletions src/sentry/core/endpoints/organization_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,10 @@
)
from sentry.relay.datascrubbing import validate_pii_config_update, validate_pii_selectors
from sentry.replays.models import OrganizationMemberReplayAccess
from sentry.seer.autofix.constants import AutofixAutomationTuningSettings
from sentry.seer.autofix.constants import (
CODING_AGENT_ALIAS_TO_HANDOFF_TARGET,
AutofixAutomationTuningSettings,
)
from sentry.seer.autofix.utils import get_valid_automated_run_stopping_points
from sentry.services.organization.provisioning import organization_provisioning_service
from sentry.tasks.console_platform_cleanup import remove_inaccessible_console_platform_sources
Expand Down Expand Up @@ -386,11 +389,7 @@ def validate_relayPiiConfig(self, value):
def validate_defaultCodingAgent(self, value: str | None) -> str:
if value is None:
return SEER_DEFAULT_CODING_AGENT_DEFAULT
coding_agent_aliases: dict[str, str] = {
"cursor": "cursor_background_agent",
"claude_code": "claude_code_agent",
}
return coding_agent_aliases.get(value, value)
return CODING_AGENT_ALIAS_TO_HANDOFF_TARGET.get(value, value)

def validate_defaultCodingAgentIntegrationId(self, value: int | None) -> int | None:
if value is None:
Expand Down
1 change: 1 addition & 0 deletions src/sentry/projectoptions/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@
"sentry:seer_automation_handoff_integration_id",
"sentry:seer_automation_handoff_auto_create_pr",
"sentry:autofix_automation_tuning",
"sentry:seer_scanner_automation",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this didn't exist before?

Copy link
Copy Markdown
Member Author

@srest2021 srest2021 May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not part of the SeerProjectPreference model (I wrote this list so I could use it in the original SeerProjectRepository read/write helpers which use SeerProjectPreference), but since it's an autofix-specific setting I figured I'd add it to try and centralize all of our Seer settings into one place.

]

# Boolean to enable/disable preprod size analysis for this project.
Expand Down
13 changes: 13 additions & 0 deletions src/sentry/seer/autofix/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@
AUTOFIX_AUTOMATION_OCCURRENCE_THRESHOLD = 10


class CodingAgentAlias(enum.StrEnum):
CURSOR = "cursor"
CLAUDE = "claude"


# Map from possible user-facing coding agent aliases to their handoff target strs.
CODING_AGENT_ALIAS_TO_HANDOFF_TARGET: dict[str | CodingAgentAlias, str] = {
CodingAgentAlias.CURSOR: "cursor_background_agent",
CodingAgentAlias.CLAUDE: "claude_code_agent",
"claude_code": "claude_code_agent",
}


class FixabilityScoreThresholds(enum.Enum):
SUPER_HIGH = 0.76
HIGH = 0.66
Expand Down
96 changes: 90 additions & 6 deletions src/sentry/seer/autofix/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from collections.abc import Callable, Iterable, Mapping
from datetime import UTC, datetime
from enum import StrEnum
from typing import Any, NotRequired, TypedDict
from typing import Any, Literal, NotRequired, TypedDict

import orjson
import sentry_sdk
Expand All @@ -16,6 +16,7 @@
from sentry import features, options, projectoptions, ratelimits
from sentry.constants import (
AUTO_OPEN_PRS_DEFAULT,
AUTOFIX_AUTOMATION_TUNING_DEFAULT,
SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
DataCategory,
ObjectStatus,
Expand All @@ -31,7 +32,12 @@
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.autofix.constants import (
CODING_AGENT_ALIAS_TO_HANDOFF_TARGET,
AutofixAutomationTuningSettings,
AutofixStatus,
CodingAgentAlias,
)
from sentry.seer.constants import SEER_SUPPORTED_SCM_PROVIDERS
from sentry.seer.models import (
AutofixHandoffPoint,
Expand Down Expand Up @@ -611,10 +617,10 @@ def build_repo_definition_from_project_repo(
)


def _build_automation_handoff(
def build_automation_handoff(
get_option: Callable[[str], Any],
) -> SeerAutomationHandoffConfiguration | None:
"""Build a SeerAutomationHandoffConfiguration from option values, or None if incomplete."""
"""Build a SeerAutomationHandoffConfiguration from option key/value pairs, or None if incomplete."""
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")
Expand Down Expand Up @@ -650,7 +656,7 @@ def read_preference_from_sentry_db(project: Project) -> SeerProjectPreference:
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),
automation_handoff=build_automation_handoff(project.get_option),
autofix_automation_tuning=project.get_option("sentry:autofix_automation_tuning"),
)

Expand Down Expand Up @@ -699,13 +705,91 @@ def _get_project_option(key: str) -> Any:
automated_run_stopping_point=_get_project_option(
"sentry:seer_automated_run_stopping_point"
),
automation_handoff=_build_automation_handoff(_get_project_option),
automation_handoff=build_automation_handoff(_get_project_option),
autofix_automation_tuning=_get_project_option("sentry:autofix_automation_tuning"),
)

return result


class SeerProjectSettingsUpdate(TypedDict, total=False):
agent: CodingAgentAlias | Literal["seer"]
integrationId: int
stoppingPoint: AutofixStoppingPoint | Literal["off"]
scannerAutomation: bool


def update_seer_project_settings(project: Project, data: SeerProjectSettingsUpdate) -> None:
"""Apply high-level Seer settings to a project. Only update a setting if it's present in data."""

def _set_if_not_default(key: str, value: Any, default: Any) -> None:
"""If we're trying to set a default, delete the option. Otherwise, set it."""
if value == default:
project.delete_option(key)
else:
project.update_option(key, value)

with transaction.atomic(using=router.db_for_write(ProjectOption)):
list(Project.objects.select_for_update().filter(id=project.id))

stopping_point: str | None = data.get("stoppingPoint")

if "agent" in data:
agent: str = data["agent"]
if agent == "seer":
project.delete_option("sentry:seer_automation_handoff_point")
project.delete_option("sentry:seer_automation_handoff_target")
project.delete_option("sentry:seer_automation_handoff_integration_id")
else:
integration_id = data.get("integrationId")
if integration_id is None:
Comment thread
srest2021 marked this conversation as resolved.
raise ValueError("integrationId is required for external coding agents")

project.update_option(
"sentry:seer_automation_handoff_point", AutofixHandoffPoint.ROOT_CAUSE
)
project.update_option(
"sentry:seer_automation_handoff_target",
CODING_AGENT_ALIAS_TO_HANDOFF_TARGET.get(agent, agent),
)
project.update_option(
"sentry:seer_automation_handoff_integration_id", integration_id
)

if stopping_point is not None:
if stopping_point == "off":
# Turn off tuning and leave stopping point and handoff_auto_create_pr unchanged
# so that reenabling restores the prior state.
_set_if_not_default(
"sentry:autofix_automation_tuning",
AutofixAutomationTuningSettings.OFF,
default=AUTOFIX_AUTOMATION_TUNING_DEFAULT,
)
else:
_set_if_not_default(
"sentry:autofix_automation_tuning",
AutofixAutomationTuningSettings.MEDIUM,
default=AUTOFIX_AUTOMATION_TUNING_DEFAULT,
)
_set_if_not_default(
"sentry:seer_automated_run_stopping_point",
stopping_point,
default=SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
)

if stopping_point == AutofixStoppingPoint.OPEN_PR:
# Safe to set even if no external handoff is configured
# since we'll only read it if the other handoff options are all non-null.
project.update_option("sentry:seer_automation_handoff_auto_create_pr", True)
else:
project.delete_option("sentry:seer_automation_handoff_auto_create_pr")

if "scannerAutomation" in data:
_set_if_not_default(
"sentry:seer_scanner_automation", data["scannerAutomation"], default=True
)


def has_project_connected_repos(organization: Organization, project: Project) -> bool:
"""Check if a project has connected repositories for Seer automation."""
return SeerProjectRepository.objects.filter(
Expand Down
Loading
Loading