Skip to content

Commit 42b9053

Browse files
srest2021claude
andcommitted
feat(seer): Add org-level default stopping point and wire coding agent defaults into project creation
Register new org option `sentry:default_stopping_point` and wire all org-level Seer defaults (stopping point, coding agent, auto_open_prs) into project creation and existing-org migration. When auto_open_prs is true, stopping point is forced to open_pr. External agents (Cursor/Claude) now get automation_handoff set on new projects. Also adds input validation for defaultCodingAgent with alias mapping (cursor -> cursor_background_agent, claude_code -> claude_code_agent) and ChoiceField validation for defaultAutomatedRunStoppingPoint. Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com> Made-with: Cursor
1 parent 9c8c155 commit 42b9053

File tree

6 files changed

+113
-10
lines changed

6 files changed

+113
-10
lines changed

src/sentry/api/serializers/models/organization.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
ROLLBACK_ENABLED_DEFAULT,
5959
SAMPLING_MODE_DEFAULT,
6060
SCRAPE_JAVASCRIPT_DEFAULT,
61+
SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
6162
SEER_DEFAULT_CODING_AGENT_DEFAULT,
6263
TARGET_SAMPLE_RATE_DEFAULT,
6364
ObjectStatus,
@@ -560,6 +561,7 @@ class DetailedOrganizationSerializerResponse(_DetailedOrganizationSerializerResp
560561
enableSeerCoding: bool
561562
defaultCodingAgent: str | None
562563
defaultCodingAgentIntegrationId: int | None
564+
defaultAutomatedRunStoppingPoint: str
563565
autoEnableCodeReview: bool
564566
autoOpenPrs: bool
565567
defaultCodeReviewTriggers: list[str]
@@ -741,6 +743,10 @@ def serialize( # type: ignore[override]
741743
"sentry:seer_default_coding_agent_integration_id",
742744
None,
743745
),
746+
"defaultAutomatedRunStoppingPoint": obj.get_option(
747+
"sentry:default_automated_run_stopping_point",
748+
SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
749+
),
744750
"autoOpenPrs": bool(
745751
obj.get_option(
746752
"sentry:auto_open_prs",

src/sentry/apidocs/examples/organization_examples.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ class OrganizationExamples:
306306
"autoOpenPrs": False,
307307
"defaultCodingAgent": None,
308308
"defaultCodingAgentIntegrationId": None,
309+
"defaultAutomatedRunStoppingPoint": "open_pr",
309310
"issueAlertsThreadFlag": True,
310311
"metricAlertsThreadFlag": True,
311312
"trustedRelays": [],

src/sentry/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,7 @@ class InsightModules(Enum):
730730
"on_new_commit",
731731
]
732732
SEER_DEFAULT_CODING_AGENT_DEFAULT = "seer"
733+
SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT = "code_changes"
733734
SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT = "code_changes"
734735
ENABLED_CONSOLE_PLATFORMS_DEFAULT: list[str] = []
735736
CONSOLE_SDK_INVITE_QUOTA_DEFAULT = 0

src/sentry/core/endpoints/organization_details.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
ROLLBACK_ENABLED_DEFAULT,
6969
SAMPLING_MODE_DEFAULT,
7070
SCRAPE_JAVASCRIPT_DEFAULT,
71+
SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
7172
SEER_DEFAULT_CODING_AGENT_DEFAULT,
7273
TARGET_SAMPLE_RATE_DEFAULT,
7374
ObjectStatus,
@@ -260,6 +261,12 @@
260261
bool,
261262
AUTO_OPEN_PRS_DEFAULT,
262263
),
264+
(
265+
"defaultAutomatedRunStoppingPoint",
266+
"sentry:default_automated_run_stopping_point",
267+
str,
268+
SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
269+
),
263270
(
264271
"autoEnableCodeReview",
265272
"sentry:auto_enable_code_review",
@@ -371,8 +378,16 @@ class OrganizationSerializer(BaseOrganizationSerializer):
371378
dashboardsAsyncQueueParallelLimit = serializers.IntegerField(required=False, min_value=1)
372379
enableSeerEnhancedAlerts = serializers.BooleanField(required=False)
373380
enableSeerCoding = serializers.BooleanField(required=False)
374-
defaultCodingAgent = serializers.CharField(required=False, allow_null=True)
381+
defaultCodingAgent = serializers.ChoiceField(
382+
choices=["seer", "cursor", "claude_code", "cursor_background_agent", "claude_code_agent"],
383+
required=False,
384+
allow_null=True,
385+
)
375386
defaultCodingAgentIntegrationId = serializers.IntegerField(required=False, allow_null=True)
387+
defaultAutomatedRunStoppingPoint = serializers.ChoiceField(
388+
choices=["root_cause", "solution", "code_changes", "open_pr"],
389+
required=False,
390+
)
376391
autoOpenPrs = serializers.BooleanField(required=False)
377392
autoEnableCodeReview = serializers.BooleanField(required=False)
378393
defaultCodeReviewTriggers = serializers.ListField(
@@ -401,6 +416,15 @@ def validate_relayPiiConfig(self, value):
401416
organization = self.context["organization"]
402417
return validate_pii_config_update(organization, value)
403418

419+
def validate_defaultCodingAgent(self, value: str | None) -> str:
420+
coding_agent_aliases: dict[str, str] = {
421+
"cursor": "cursor_background_agent",
422+
"claude_code": "claude_code_agent",
423+
}
424+
if value is None:
425+
return SEER_DEFAULT_CODING_AGENT_DEFAULT
426+
return coding_agent_aliases.get(value, value)
427+
404428
def validate_defaultCodingAgentIntegrationId(self, value: int | None) -> int | None:
405429
if value is None:
406430
return None

src/sentry/seer/similarity/utils.py

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
from tokenizers import Tokenizer
1010

1111
from sentry import features, options
12-
from sentry.constants import DATA_ROOT
12+
from sentry.constants import (
13+
AUTO_OPEN_PRS_DEFAULT,
14+
DATA_ROOT,
15+
SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
16+
SEER_DEFAULT_CODING_AGENT_DEFAULT,
17+
)
1318
from sentry.grouping.api import get_contributing_variant_and_component
1419
from sentry.grouping.grouping_info import get_grouping_info_from_variants_legacy
1520
from sentry.grouping.variants import BaseVariant
@@ -23,7 +28,11 @@
2328
set_project_seer_preference,
2429
write_preference_to_sentry_db,
2530
)
26-
from sentry.seer.models import SeerProjectPreference
31+
from sentry.seer.models import (
32+
AutofixHandoffPoint,
33+
SeerAutomationHandoffConfiguration,
34+
SeerProjectPreference,
35+
)
2736
from sentry.seer.similarity.types import GroupingVersion
2837
from sentry.services.eventstore.models import Event, GroupEvent
2938
from sentry.utils import metrics
@@ -564,21 +573,51 @@ def set_default_project_seer_scanner_automation(
564573

565574

566575
def set_default_project_auto_open_prs(organization: Organization, project: Project) -> None:
567-
"""Called once at project creation time to set the initial auto open PRs."""
576+
"""Called once at project creation time to set the initial automated run stopping
577+
point and automation handoff.
578+
579+
Reads org options (default_automated_run_stopping_point, auto_open_prs, default_coding_agent,
580+
default_coding_agent_integration_id) and writes the corresponding project-level
581+
options (stopping point, handoff config).
582+
583+
When auto_open_prs is True, stopping_point is forced to open_pr regardless of
584+
default_stopping_point.
585+
"""
568586
if not is_seer_seat_based_tier_enabled(organization):
569587
return
570588

571-
stopping_point = AutofixStoppingPoint.CODE_CHANGES
572-
if organization.get_option("sentry:auto_open_prs"):
589+
auto_open_prs = bool(organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT))
590+
if auto_open_prs:
573591
stopping_point = AutofixStoppingPoint.OPEN_PR
592+
else:
593+
stopping_point = organization.get_option(
594+
"sentry:default_stopping_point", SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT
595+
)
596+
597+
coding_agent = organization.get_option(
598+
"sentry:seer_default_coding_agent", SEER_DEFAULT_CODING_AGENT_DEFAULT
599+
)
600+
coding_agent_integration_id = organization.get_option(
601+
"sentry:seer_default_coding_agent_integration_id", None
602+
)
603+
604+
automation_handoff: SeerAutomationHandoffConfiguration | None = None
605+
if coding_agent and coding_agent != "seer" and coding_agent_integration_id is not None:
606+
automation_handoff = SeerAutomationHandoffConfiguration(
607+
handoff_point=AutofixHandoffPoint.ROOT_CAUSE,
608+
target=coding_agent,
609+
integration_id=coding_agent_integration_id,
610+
auto_create_pr=auto_open_prs,
611+
)
574612

575-
# We need to make an API call to Seer to set this preference
576613
preference = SeerProjectPreference(
577614
organization_id=organization.id,
578615
project_id=project.id,
579616
repositories=[],
580617
automated_run_stopping_point=stopping_point,
618+
automation_handoff=automation_handoff,
581619
)
620+
582621
try:
583622
set_project_seer_preference(preference)
584623
except Exception as e:

src/sentry/tasks/seer/autofix.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
from datetime import datetime, timedelta
3+
from typing import Any
34

45
import sentry_sdk
56
from django.utils import timezone
@@ -8,7 +9,12 @@
89

910
from sentry import analytics, features
1011
from sentry.analytics.events.autofix_automation_events import AiAutofixAutomationEvent
11-
from sentry.constants import ObjectStatus
12+
from sentry.constants import (
13+
AUTO_OPEN_PRS_DEFAULT,
14+
SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
15+
SEER_DEFAULT_CODING_AGENT_DEFAULT,
16+
ObjectStatus,
17+
)
1218
from sentry.models.group import Group
1319
from sentry.models.organization import Organization
1420
from sentry.models.project import Project
@@ -238,6 +244,30 @@ def configure_seer_for_existing_org(organization_id: int) -> None:
238244
"sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.MEDIUM
239245
)
240246

247+
auto_open_prs = bool(organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT))
248+
if auto_open_prs:
249+
default_stopping_point = "open_pr"
250+
else:
251+
default_stopping_point = organization.get_option(
252+
"sentry:default_stopping_point", SEER_DEFAULT_AUTOMATED_RUN_STOPPING_POINT_DEFAULT
253+
)
254+
255+
coding_agent = organization.get_option(
256+
"sentry:seer_default_coding_agent", SEER_DEFAULT_CODING_AGENT_DEFAULT
257+
)
258+
coding_agent_integration_id = organization.get_option(
259+
"sentry:seer_default_coding_agent_integration_id", None
260+
)
261+
262+
default_handoff: dict[str, Any] | None = None
263+
if coding_agent and coding_agent != "seer" and coding_agent_integration_id is not None:
264+
default_handoff = {
265+
"handoff_point": "root_cause",
266+
"target": coding_agent,
267+
"integration_id": coding_agent_integration_id,
268+
"auto_create_pr": auto_open_prs,
269+
}
270+
241271
preferences_by_id = bulk_get_project_preferences(organization_id, project_ids)
242272

243273
# Determine which projects need updates
@@ -262,9 +292,11 @@ def configure_seer_for_existing_org(organization_id: int) -> None:
262292
"organization_id": organization_id,
263293
"project_id": project_id,
264294
"repositories": repositories or [],
265-
"automated_run_stopping_point": "code_changes",
295+
"automated_run_stopping_point": default_stopping_point,
266296
"automation_handoff": (
267-
existing_pref.get("automation_handoff") if existing_pref else None
297+
existing_pref.get("automation_handoff")
298+
if existing_pref and existing_pref.get("automation_handoff")
299+
else default_handoff
268300
),
269301
}
270302
)

0 commit comments

Comments
 (0)