Skip to content

Commit 525c06d

Browse files
srest2021claude
authored andcommitted
feat(seer): Add org-level default stopping point and wire coding agent defaults into project creation (#111697)
Fixes CW-1120 Register new org option `sentry:default_automated_run_stopping_point` and wire all org-level Seer defaults (stopping point, coding agent, auto_open_prs) into both project creation and the existing-org migration task. Project creation and the org migration task to new seat-based pricing now: - Read the org's `defaultAutomatedRunStoppingPoint` and `defaultCodingAgent` and `defaultCodingAgentIntegrationId` - For external agents, configure automation_handoff with `auto_create_pr` from `auto_open_prs` - For Seer agent, `auto_open_prs=true` forces `open_pr`; `auto_open_prs=false` caps `open_pr` down to `code_changes` - Also adds ChoiceField validation for `defaultCodingAgent` (with alias mapping for cursor → cursor_background_agent, claude_code → claude_code_agent) and `defaultAutomatedRunStoppingPoint`. --------- Co-authored-by: Claude Sonnet 4 <noreply@anthropic.com>
1 parent 8bad8a3 commit 525c06d

File tree

10 files changed

+417
-50
lines changed

10 files changed

+417
-50
lines changed

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

Lines changed: 9 additions & 5 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_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
6162
SEER_DEFAULT_CODING_AGENT_DEFAULT,
6263
TARGET_SAMPLE_RATE_DEFAULT,
6364
ObjectStatus,
@@ -558,8 +559,9 @@ class DetailedOrganizationSerializerResponse(_DetailedOrganizationSerializerResp
558559
defaultSeerScannerAutomation: bool
559560
enableSeerEnhancedAlerts: bool
560561
enableSeerCoding: bool
561-
defaultCodingAgent: str | None
562+
defaultCodingAgent: str
562563
defaultCodingAgentIntegrationId: int | None
564+
defaultAutomatedRunStoppingPoint: str
563565
autoEnableCodeReview: bool
564566
autoOpenPrs: bool
565567
defaultCodeReviewTriggers: list[str]
@@ -734,12 +736,14 @@ def serialize( # type: ignore[override]
734736
)
735737
),
736738
"defaultCodingAgent": obj.get_option(
737-
"sentry:seer_default_coding_agent",
738-
SEER_DEFAULT_CODING_AGENT_DEFAULT,
739+
"sentry:seer_default_coding_agent", SEER_DEFAULT_CODING_AGENT_DEFAULT
739740
),
740741
"defaultCodingAgentIntegrationId": obj.get_option(
741-
"sentry:seer_default_coding_agent_integration_id",
742-
None,
742+
"sentry:seer_default_coding_agent_integration_id", None
743+
),
744+
"defaultAutomatedRunStoppingPoint": obj.get_option(
745+
"sentry:default_automated_run_stopping_point",
746+
SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
743747
),
744748
"autoOpenPrs": bool(
745749
obj.get_option(

src/sentry/apidocs/examples/organization_examples.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,9 @@ class OrganizationExamples:
304304
"enableSeerCoding": True,
305305
"enableSeerEnhancedAlerts": True,
306306
"autoOpenPrs": False,
307-
"defaultCodingAgent": None,
307+
"defaultCodingAgent": "seer",
308308
"defaultCodingAgentIntegrationId": None,
309+
"defaultAutomatedRunStoppingPoint": "code_changes",
309310
"issueAlertsThreadFlag": True,
310311
"metricAlertsThreadFlag": True,
311312
"trustedRelays": [],

src/sentry/core/endpoints/organization_details.py

Lines changed: 24 additions & 2 deletions
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_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
7172
SEER_DEFAULT_CODING_AGENT_DEFAULT,
7273
TARGET_SAMPLE_RATE_DEFAULT,
7374
ObjectStatus,
@@ -254,12 +255,17 @@
254255
None,
255256
),
256257
(
257-
# Informs UI default for automated_run_stopping_point in project preferences
258258
"autoOpenPrs",
259259
"sentry:auto_open_prs",
260260
bool,
261261
AUTO_OPEN_PRS_DEFAULT,
262262
),
263+
(
264+
"defaultAutomatedRunStoppingPoint",
265+
"sentry:default_automated_run_stopping_point",
266+
str,
267+
SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
268+
),
263269
(
264270
"autoEnableCodeReview",
265271
"sentry:auto_enable_code_review",
@@ -371,8 +377,15 @@ class OrganizationSerializer(BaseOrganizationSerializer):
371377
dashboardsAsyncQueueParallelLimit = serializers.IntegerField(required=False, min_value=1)
372378
enableSeerEnhancedAlerts = serializers.BooleanField(required=False)
373379
enableSeerCoding = serializers.BooleanField(required=False)
374-
defaultCodingAgent = serializers.CharField(required=False, allow_null=True)
380+
defaultCodingAgent = serializers.ChoiceField(
381+
choices=["seer", "cursor", "claude_code", "cursor_background_agent", "claude_code_agent"],
382+
required=False,
383+
allow_null=True,
384+
)
375385
defaultCodingAgentIntegrationId = serializers.IntegerField(required=False, allow_null=True)
386+
defaultAutomatedRunStoppingPoint = serializers.ChoiceField(
387+
choices=["code_changes", "open_pr"], required=False
388+
)
376389
autoOpenPrs = serializers.BooleanField(required=False)
377390
autoEnableCodeReview = serializers.BooleanField(required=False)
378391
defaultCodeReviewTriggers = serializers.ListField(
@@ -401,6 +414,15 @@ def validate_relayPiiConfig(self, value):
401414
organization = self.context["organization"]
402415
return validate_pii_config_update(organization, value)
403416

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

src/sentry/core/endpoints/team_projects.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@
3131
from sentry.models.team import Team
3232
from sentry.seer.similarity.utils import (
3333
project_is_seer_eligible,
34-
set_default_project_auto_open_prs,
3534
set_default_project_autofix_automation_tuning,
35+
set_default_project_seer_preferences,
3636
set_default_project_seer_scanner_automation,
3737
)
3838
from sentry.signals import project_created
@@ -56,7 +56,7 @@ def apply_default_project_settings(organization: Organization, project: Project)
5656

5757
set_default_project_autofix_automation_tuning(organization, project)
5858
set_default_project_seer_scanner_automation(organization, project)
59-
set_default_project_auto_open_prs(organization, project)
59+
set_default_project_seer_preferences(organization, project)
6060

6161

6262
class ProjectPostSerializer(serializers.Serializer):

src/sentry/seer/autofix/utils.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from sentry import features, options, ratelimits
1717
from sentry.constants import (
18+
AUTO_OPEN_PRS_DEFAULT,
1819
SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT,
1920
DataCategory,
2021
ObjectStatus,
@@ -42,6 +43,10 @@
4243
SeerProjectRepository,
4344
SeerProjectRepositoryBranchOverride,
4445
)
46+
from sentry.seer.models.seer_api_models import (
47+
AutofixHandoffPoint,
48+
SeerAutomationHandoffConfiguration,
49+
)
4550
from sentry.seer.signed_seer_api import SeerViewerContext, make_signed_seer_api_request
4651
from sentry.utils.cache import cache
4752
from sentry.utils.outcomes import Outcome, track_outcome
@@ -384,14 +389,47 @@ def validate(self, data):
384389

385390

386391
def default_seer_project_preference(project: Project) -> SeerProjectPreference:
392+
stopping_point, handoff = get_org_default_seer_automation_handoff(project.organization)
387393
return SeerProjectPreference(
388394
organization_id=project.organization.id,
389395
project_id=project.id,
390396
repositories=[],
391-
automated_run_stopping_point=AutofixStoppingPoint.CODE_CHANGES.value,
392-
automation_handoff=None,
397+
automated_run_stopping_point=stopping_point,
398+
automation_handoff=handoff,
399+
)
400+
401+
402+
def get_org_default_seer_automation_handoff(
403+
organization: Organization,
404+
) -> tuple[str, SeerAutomationHandoffConfiguration | None]:
405+
"""Get the default stopping point and automation handoff for an organization."""
406+
stopping_point = organization.get_option(
407+
"sentry:default_automated_run_stopping_point", SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT
393408
)
394409

410+
auto_open_prs = organization.get_option("sentry:auto_open_prs", AUTO_OPEN_PRS_DEFAULT)
411+
412+
automation_handoff: SeerAutomationHandoffConfiguration | None = None
413+
coding_agent = organization.get_option("sentry:seer_default_coding_agent")
414+
coding_agent_integration_id = organization.get_option(
415+
"sentry:seer_default_coding_agent_integration_id"
416+
)
417+
if coding_agent and coding_agent != "seer" and coding_agent_integration_id is not None:
418+
automation_handoff = SeerAutomationHandoffConfiguration(
419+
handoff_point=AutofixHandoffPoint.ROOT_CAUSE,
420+
target=coding_agent,
421+
integration_id=coding_agent_integration_id,
422+
auto_create_pr=auto_open_prs,
423+
)
424+
# If Seer agent and auto open PRs, we can run up to open_pr.
425+
elif auto_open_prs:
426+
stopping_point = "open_pr"
427+
# If Seer agent and no auto open PRs, we shouldn't go past code_changes.
428+
elif stopping_point == "open_pr":
429+
stopping_point = "code_changes"
430+
431+
return stopping_point, automation_handoff
432+
395433

396434
def get_project_seer_preferences(project_id: int) -> SeerRawPreferenceResponse:
397435
"""

src/sentry/seer/similarity/utils.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
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+
DATA_ROOT,
14+
)
1315
from sentry.grouping.api import get_contributing_variant_and_component
1416
from sentry.grouping.grouping_info import get_grouping_info_from_variants_legacy
1517
from sentry.grouping.variants import BaseVariant
@@ -18,12 +20,14 @@
1820
from sentry.models.project import Project
1921
from sentry.seer.autofix.constants import AutofixAutomationTuningSettings
2022
from sentry.seer.autofix.utils import (
21-
AutofixStoppingPoint,
23+
get_org_default_seer_automation_handoff,
2224
is_seer_seat_based_tier_enabled,
2325
set_project_seer_preference,
2426
write_preference_to_sentry_db,
2527
)
26-
from sentry.seer.models import SeerProjectPreference
28+
from sentry.seer.models import (
29+
SeerProjectPreference,
30+
)
2731
from sentry.seer.similarity.types import GroupingVersion
2832
from sentry.services.eventstore.models import Event, GroupEvent
2933
from sentry.utils import metrics
@@ -563,22 +567,24 @@ def set_default_project_seer_scanner_automation(
563567
project.update_option("sentry:seer_scanner_automation", org_default)
564568

565569

566-
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."""
570+
def set_default_project_seer_preferences(organization: Organization, project: Project) -> None:
571+
"""Called once at project creation time to set the initial automated run stopping
572+
point and automation handoff.
573+
"""
568574
if not is_seer_seat_based_tier_enabled(organization):
569575
return
570576

571-
stopping_point = AutofixStoppingPoint.CODE_CHANGES
572-
if organization.get_option("sentry:auto_open_prs"):
573-
stopping_point = AutofixStoppingPoint.OPEN_PR
577+
stopping_point, automation_handoff = get_org_default_seer_automation_handoff(organization)
574578

575579
# We need to make an API call to Seer to set this preference
576580
preference = SeerProjectPreference(
577581
organization_id=organization.id,
578582
project_id=project.id,
579583
repositories=[],
580584
automated_run_stopping_point=stopping_point,
585+
automation_handoff=automation_handoff,
581586
)
587+
582588
try:
583589
set_project_seer_preference(preference)
584590
except Exception as e:

src/sentry/tasks/seer/autofix.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
from sentry import analytics, features
1010
from sentry.analytics.events.autofix_automation_events import AiAutofixAutomationEvent
11-
from sentry.constants import ObjectStatus
11+
from sentry.constants import (
12+
ObjectStatus,
13+
)
1214
from sentry.models.group import Group
1315
from sentry.models.organization import Organization
1416
from sentry.models.project import Project
@@ -24,6 +26,7 @@
2426
deduplicate_repositories,
2527
get_autofix_repos_from_project_code_mappings,
2628
get_autofix_state,
29+
get_org_default_seer_automation_handoff,
2730
get_seer_seat_based_tier_cache_key,
2831
resolve_repository_ids,
2932
)
@@ -238,34 +241,49 @@ def configure_seer_for_existing_org(organization_id: int) -> None:
238241
"sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.MEDIUM
239242
)
240243

244+
default_stopping_point, default_handoff = get_org_default_seer_automation_handoff(organization)
245+
default_handoff_dict = default_handoff.dict() if default_handoff else None
246+
247+
valid_stopping_points = {"open_pr", "code_changes"}
248+
241249
preferences_by_id = bulk_get_project_preferences(organization_id, project_ids)
242250

243251
# Determine which projects need updates
244252
preferences_to_set = []
245253
projects_by_id = {p.id: p for p in projects}
246254
for project_id in project_ids:
255+
stopping_point = default_stopping_point
256+
handoff = default_handoff_dict
257+
247258
existing_pref = preferences_by_id.get(str(project_id))
248259
if not existing_pref:
249260
# No existing preferences, get repositories from code mappings
250261
repositories = get_autofix_repos_from_project_code_mappings(projects_by_id[project_id])
251262
else:
252-
# Skip projects that already have an acceptable stopping point configured
253-
if existing_pref.get("automated_run_stopping_point") in ("open_pr", "code_changes"):
254-
continue
255263
repositories = existing_pref.get("repositories") or []
256264

257-
repositories = deduplicate_repositories(repositories)
265+
existing_stopping_point = existing_pref.get("automated_run_stopping_point")
266+
existing_handoff = existing_pref.get("automation_handoff")
267+
268+
# Skip projects that a) already have an acceptable stopping point configured
269+
# AND b) already have a handoff configured or no org default handoff.
270+
if existing_stopping_point in valid_stopping_points and (
271+
existing_handoff or default_handoff_dict is None
272+
):
273+
continue
274+
275+
if existing_stopping_point in valid_stopping_points:
276+
stopping_point = existing_stopping_point
277+
if existing_handoff:
278+
handoff = existing_handoff
258279

259-
# Preserve existing repositories and automation_handoff, only update the stopping point
260280
preferences_to_set.append(
261281
{
262282
"organization_id": organization_id,
263283
"project_id": project_id,
264-
"repositories": repositories or [],
265-
"automated_run_stopping_point": "code_changes",
266-
"automation_handoff": (
267-
existing_pref.get("automation_handoff") if existing_pref else None
268-
),
284+
"repositories": deduplicate_repositories(repositories) or [],
285+
"automated_run_stopping_point": stopping_point,
286+
"automation_handoff": handoff,
269287
}
270288
)
271289

0 commit comments

Comments
 (0)