Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
42b9053
feat(seer): Add org-level default stopping point and wire coding agen…
srest2021 Mar 26, 2026
007a027
test(seer): Add tests for set_default_project_auto_open_prs
srest2021 Mar 26, 2026
52fb592
preserve original stopping point
srest2021 Mar 27, 2026
91b53a8
fixes
srest2021 Mar 27, 2026
83f31ca
org details tests
srest2021 Mar 27, 2026
3564251
coding agent none default
srest2021 Mar 27, 2026
b14d154
handle seer coding agent
srest2021 Mar 27, 2026
22f98fe
fix typing
srest2021 Mar 27, 2026
7a0759f
set default coding agent to seer, and cast null to seer
srest2021 Mar 30, 2026
684ac29
other spots
srest2021 Mar 30, 2026
88d4106
fix failing test
srest2021 Mar 30, 2026
df1f39e
fix bug for projects with existing stopping point
srest2021 Mar 30, 2026
99b0f5f
more tests
srest2021 Mar 30, 2026
33bc6b2
clean up tests
srest2021 Mar 30, 2026
f745023
only allow code changes and open pr as valid default stopping points
srest2021 Mar 30, 2026
5bb23fd
ref(seer): Clean up project preferences and restrict stopping point c…
srest2021 Mar 30, 2026
e5508b9
small fix
srest2021 Mar 30, 2026
e9a9725
consolidate two stopping point defaults
srest2021 Mar 30, 2026
82b453a
test(seer): Add tests for configure_seer_for_existing_org skip logic
srest2021 Mar 30, 2026
8138f6c
remove obsolete comment
srest2021 Mar 31, 2026
1150293
feat(seer): Add feature flag for root_cause stopping point and fix au…
srest2021 Mar 31, 2026
2e5a937
allow root cause default stopping point if feature flag
srest2021 Mar 31, 2026
ab215d0
use existing feature flag
srest2021 Mar 31, 2026
53c106c
fix auto_open_prs logic in configure_seer_for_existing_org
srest2021 Mar 31, 2026
d7e7df5
small fix
srest2021 Mar 31, 2026
adeb0c8
fall back to default stoppin gpont if root cause and not feature flagged
srest2021 Mar 31, 2026
e236a23
make helper and move tests around
srest2021 Mar 31, 2026
6c3c62d
fix mypy
srest2021 Mar 31, 2026
ac7e0da
rm root cause feature flag
srest2021 Mar 31, 2026
e210c94
fix test
srest2021 Mar 31, 2026
cb71151
Merge branch 'master' into srest2021/CW-1120
srest2021 Mar 31, 2026
d6bddbe
update default seer pref
srest2021 Mar 31, 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: 9 additions & 5 deletions src/sentry/api/serializers/models/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
ROLLBACK_ENABLED_DEFAULT,
SAMPLING_MODE_DEFAULT,
SCRAPE_JAVASCRIPT_DEFAULT,
SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
SEER_DEFAULT_CODING_AGENT_DEFAULT,
TARGET_SAMPLE_RATE_DEFAULT,
ObjectStatus,
Expand Down Expand Up @@ -558,8 +559,9 @@ class DetailedOrganizationSerializerResponse(_DetailedOrganizationSerializerResp
defaultSeerScannerAutomation: bool
enableSeerEnhancedAlerts: bool
enableSeerCoding: bool
defaultCodingAgent: str | None
defaultCodingAgent: str
defaultCodingAgentIntegrationId: int | None
defaultAutomatedRunStoppingPoint: str
autoEnableCodeReview: bool
autoOpenPrs: bool
defaultCodeReviewTriggers: list[str]
Expand Down Expand Up @@ -734,12 +736,14 @@ def serialize( # type: ignore[override]
)
),
"defaultCodingAgent": obj.get_option(
"sentry:seer_default_coding_agent",
SEER_DEFAULT_CODING_AGENT_DEFAULT,
"sentry:seer_default_coding_agent", SEER_DEFAULT_CODING_AGENT_DEFAULT
),
"defaultCodingAgentIntegrationId": obj.get_option(
"sentry:seer_default_coding_agent_integration_id",
None,
"sentry:seer_default_coding_agent_integration_id", None
),
"defaultAutomatedRunStoppingPoint": obj.get_option(
"sentry:default_automated_run_stopping_point",
SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
),
"autoOpenPrs": bool(
obj.get_option(
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/apidocs/examples/organization_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,9 @@ class OrganizationExamples:
"enableSeerCoding": True,
"enableSeerEnhancedAlerts": True,
"autoOpenPrs": False,
"defaultCodingAgent": None,
"defaultCodingAgent": "seer",
"defaultCodingAgentIntegrationId": None,
"defaultAutomatedRunStoppingPoint": "code_changes",
"issueAlertsThreadFlag": True,
"metricAlertsThreadFlag": True,
"trustedRelays": [],
Expand Down
26 changes: 24 additions & 2 deletions src/sentry/core/endpoints/organization_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
ROLLBACK_ENABLED_DEFAULT,
SAMPLING_MODE_DEFAULT,
SCRAPE_JAVASCRIPT_DEFAULT,
SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
SEER_DEFAULT_CODING_AGENT_DEFAULT,
TARGET_SAMPLE_RATE_DEFAULT,
ObjectStatus,
Expand Down Expand Up @@ -254,12 +255,17 @@
None,
),
(
# Informs UI default for automated_run_stopping_point in project preferences
"autoOpenPrs",
"sentry:auto_open_prs",
bool,
AUTO_OPEN_PRS_DEFAULT,
),
(
"defaultAutomatedRunStoppingPoint",
"sentry:default_automated_run_stopping_point",
str,
SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
),
(
"autoEnableCodeReview",
"sentry:auto_enable_code_review",
Expand Down Expand Up @@ -371,8 +377,15 @@ class OrganizationSerializer(BaseOrganizationSerializer):
dashboardsAsyncQueueParallelLimit = serializers.IntegerField(required=False, min_value=1)
enableSeerEnhancedAlerts = serializers.BooleanField(required=False)
enableSeerCoding = serializers.BooleanField(required=False)
defaultCodingAgent = serializers.CharField(required=False, allow_null=True)
defaultCodingAgent = serializers.ChoiceField(
choices=["seer", "cursor", "claude_code", "cursor_background_agent", "claude_code_agent"],
required=False,
allow_null=True,
)
defaultCodingAgentIntegrationId = serializers.IntegerField(required=False, allow_null=True)
defaultAutomatedRunStoppingPoint = serializers.ChoiceField(
choices=["code_changes", "open_pr"], required=False
)
autoOpenPrs = serializers.BooleanField(required=False)
autoEnableCodeReview = serializers.BooleanField(required=False)
defaultCodeReviewTriggers = serializers.ListField(
Expand Down Expand Up @@ -401,6 +414,15 @@ def validate_relayPiiConfig(self, value):
organization = self.context["organization"]
return validate_pii_config_update(organization, value)

def validate_defaultCodingAgent(self, value: str | None) -> str:
if value is None:
return SEER_DEFAULT_CODING_AGENT_DEFAULT
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Normalize None to "seer" so that we always have an explicit default coding agent

coding_agent_aliases: dict[str, str] = {
"cursor": "cursor_background_agent",
"claude_code": "claude_code_agent",
}
Comment on lines +420 to +423
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.

👍

return coding_agent_aliases.get(value, value)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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


def validate_defaultCodingAgentIntegrationId(self, value: int | None) -> int | None:
if value is None:
return None
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/core/endpoints/team_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
from sentry.models.team import Team
from sentry.seer.similarity.utils import (
project_is_seer_eligible,
set_default_project_auto_open_prs,
set_default_project_autofix_automation_tuning,
set_default_project_seer_preferences,
set_default_project_seer_scanner_automation,
)
from sentry.signals import project_created
Expand All @@ -56,7 +56,7 @@ def apply_default_project_settings(organization: Organization, project: Project)

set_default_project_autofix_automation_tuning(organization, project)
set_default_project_seer_scanner_automation(organization, project)
set_default_project_auto_open_prs(organization, project)
set_default_project_seer_preferences(organization, project)


class ProjectPostSerializer(serializers.Serializer):
Expand Down
42 changes: 40 additions & 2 deletions src/sentry/seer/autofix/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from sentry import features, options, ratelimits
from sentry.constants import (
AUTO_OPEN_PRS_DEFAULT,
SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
DataCategory,
ObjectStatus,
Expand Down Expand Up @@ -42,6 +43,10 @@
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
Expand Down Expand Up @@ -384,14 +389,47 @@


def default_seer_project_preference(project: Project) -> SeerProjectPreference:
stopping_point, handoff = get_org_default_seer_automation_handoff(project.organization)
return SeerProjectPreference(
organization_id=project.organization.id,
project_id=project.id,
repositories=[],
automated_run_stopping_point=AutofixStoppingPoint.CODE_CHANGES.value,
automation_handoff=None,
automated_run_stopping_point=stopping_point,
automation_handoff=handoff,
)


def get_org_default_seer_automation_handoff(
organization: Organization,
) -> tuple[str, SeerAutomationHandoffConfiguration | None]:
"""Get the default stopping point and automation handoff for an organization."""
stopping_point = organization.get_option(
"sentry:default_automated_run_stopping_point", SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT
)

auto_open_prs = organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT)

automation_handoff: SeerAutomationHandoffConfiguration | None = None
coding_agent = organization.get_option("sentry:seer_default_coding_agent")
coding_agent_integration_id = organization.get_option(
"sentry:seer_default_coding_agent_integration_id"
)
if coding_agent and coding_agent != "seer" and coding_agent_integration_id is not None:
automation_handoff = SeerAutomationHandoffConfiguration(
handoff_point=AutofixHandoffPoint.ROOT_CAUSE,
target=coding_agent,

Check failure on line 420 in src/sentry/seer/autofix/utils.py

View check run for this annotation

@sentry/warden / warden: sentry-backend-bugs

Pydantic ValidationError when reading legacy coding_agent values

The `coding_agent` value from `organization.get_option("sentry:seer_default_coding_agent")` can be `"cursor"` or `"claude_code"` if stored before alias mapping was added. However, `SeerAutomationHandoffConfiguration.target` is a `Literal["cursor_background_agent", "claude_code_agent"]` which will raise a Pydantic `ValidationError` for unaliased values. The alias mapping in `validate_defaultCodingAgent` only applies when setting the value, not when reading it. This will cause project creation and org migration tasks to crash for organizations with legacy stored values.
integration_id=coding_agent_integration_id,
auto_create_pr=auto_open_prs,
)
# If Seer agent and auto open PRs, we can run up to open_pr.
elif auto_open_prs:
stopping_point = "open_pr"
# If Seer agent and no auto open PRs, we shouldn't go past code_changes.
elif stopping_point == "open_pr":
stopping_point = "code_changes"

return stopping_point, automation_handoff


def get_project_seer_preferences(project_id: int) -> SeerRawPreferenceResponse:
"""
Expand Down
22 changes: 14 additions & 8 deletions src/sentry/seer/similarity/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
from tokenizers import Tokenizer

from sentry import features, options
from sentry.constants import DATA_ROOT
from sentry.constants import (
DATA_ROOT,
)
from sentry.grouping.api import get_contributing_variant_and_component
from sentry.grouping.grouping_info import get_grouping_info_from_variants_legacy
from sentry.grouping.variants import BaseVariant
Expand All @@ -18,12 +20,14 @@
from sentry.models.project import Project
from sentry.seer.autofix.constants import AutofixAutomationTuningSettings
from sentry.seer.autofix.utils import (
AutofixStoppingPoint,
get_org_default_seer_automation_handoff,
is_seer_seat_based_tier_enabled,
set_project_seer_preference,
write_preference_to_sentry_db,
)
from sentry.seer.models import SeerProjectPreference
from sentry.seer.models import (
SeerProjectPreference,
)
from sentry.seer.similarity.types import GroupingVersion
from sentry.services.eventstore.models import Event, GroupEvent
from sentry.utils import metrics
Expand Down Expand Up @@ -563,22 +567,24 @@ def set_default_project_seer_scanner_automation(
project.update_option("sentry:seer_scanner_automation", org_default)


def set_default_project_auto_open_prs(organization: Organization, project: Project) -> None:
"""Called once at project creation time to set the initial auto open PRs."""
def set_default_project_seer_preferences(organization: Organization, project: Project) -> None:
"""Called once at project creation time to set the initial automated run stopping
point and automation handoff.
"""
if not is_seer_seat_based_tier_enabled(organization):
return

stopping_point = AutofixStoppingPoint.CODE_CHANGES
if organization.get_option("sentry:auto_open_prs"):
stopping_point = AutofixStoppingPoint.OPEN_PR
stopping_point, automation_handoff = get_org_default_seer_automation_handoff(organization)

# We need to make an API call to Seer to set this preference
preference = SeerProjectPreference(
organization_id=organization.id,
project_id=project.id,
repositories=[],
automated_run_stopping_point=stopping_point,
automation_handoff=automation_handoff,
)

try:
set_project_seer_preference(preference)
except Exception as e:
Expand Down
40 changes: 29 additions & 11 deletions src/sentry/tasks/seer/autofix.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@

from sentry import analytics, features
from sentry.analytics.events.autofix_automation_events import AiAutofixAutomationEvent
from sentry.constants import ObjectStatus
from sentry.constants import (
ObjectStatus,
)
from sentry.models.group import Group
from sentry.models.organization import Organization
from sentry.models.project import Project
Expand All @@ -24,6 +26,7 @@
deduplicate_repositories,
get_autofix_repos_from_project_code_mappings,
get_autofix_state,
get_org_default_seer_automation_handoff,
get_seer_seat_based_tier_cache_key,
resolve_repository_ids,
)
Expand Down Expand Up @@ -238,34 +241,49 @@ def configure_seer_for_existing_org(organization_id: int) -> None:
"sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.MEDIUM
)

default_stopping_point, default_handoff = get_org_default_seer_automation_handoff(organization)
default_handoff_dict = default_handoff.dict() if default_handoff else None

valid_stopping_points = {"open_pr", "code_changes"}

preferences_by_id = bulk_get_project_preferences(organization_id, project_ids)

# Determine which projects need updates
preferences_to_set = []
projects_by_id = {p.id: p for p in projects}
for project_id in project_ids:
stopping_point = default_stopping_point
handoff = default_handoff_dict

existing_pref = preferences_by_id.get(str(project_id))
if not existing_pref:
# No existing preferences, get repositories from code mappings
repositories = get_autofix_repos_from_project_code_mappings(projects_by_id[project_id])
else:
# Skip projects that already have an acceptable stopping point configured
if existing_pref.get("automated_run_stopping_point") in ("open_pr", "code_changes"):
continue
repositories = existing_pref.get("repositories") or []

repositories = deduplicate_repositories(repositories)
existing_stopping_point = existing_pref.get("automated_run_stopping_point")
existing_handoff = existing_pref.get("automation_handoff")

# Skip projects that a) already have an acceptable stopping point configured
# AND b) already have a handoff configured or no org default handoff.
if existing_stopping_point in valid_stopping_points and (
existing_handoff or default_handoff_dict is None
):
continue
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@Mihir-Mavalankar Fixed the continue stuff here. We'll continue if we have a valid stopping point and we either already have a handoff configured or the org default is None (= noop).


if existing_stopping_point in valid_stopping_points:
stopping_point = existing_stopping_point
if existing_handoff:
handoff = existing_handoff

# Preserve existing repositories and automation_handoff, only update the stopping point
preferences_to_set.append(
{
"organization_id": organization_id,
"project_id": project_id,
"repositories": repositories or [],
"automated_run_stopping_point": "code_changes",
"automation_handoff": (
existing_pref.get("automation_handoff") if existing_pref else None
),
"repositories": deduplicate_repositories(repositories) or [],
"automated_run_stopping_point": stopping_point,
"automation_handoff": handoff,
}
)

Expand Down
Loading
Loading