Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions src/sentry/seer/agentic_triage/delivery.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
is_seer_autotriggered_autofix_rate_limited_and_increment,
is_seer_seat_based_tier_enabled,
)
from sentry.seer.credits import SeerFeature
from sentry.seer.models.agentic_triage import (
SeerAgenticTriageRunErrorType,
SeerAgenticTriageRunResult,
Expand Down Expand Up @@ -338,6 +339,7 @@ def _process_verdicts(
user_context=user_context,
allow_free_cohort=True,
single_run=single_run,
seer_feature=SeerFeature.AGENTIC_TRIAGE,
)
except Exception:
logger.exception(
Expand Down
24 changes: 18 additions & 6 deletions src/sentry/seer/autofix/autofix_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
AutofixFeatureArgs,
autofix_run_targets_other_issue,
check_autofix_billing,
get_run_seer_feature,
require_autofix_run_for_group,
trigger_autofix_feature,
)
Expand Down Expand Up @@ -64,7 +65,7 @@
is_free_cohort_org,
read_preference_from_sentry_db,
)
from sentry.seer.credits import SeerOperation
from sentry.seer.credits import SeerFeature, SeerOperation
from sentry.seer.entrypoints.operator import (
SeerActivityAttribution,
SeerAutofixOperator,
Expand Down Expand Up @@ -449,6 +450,7 @@ def trigger_autofix_agent(
iteration_id: int | None = None,
allow_free_cohort: bool = False,
single_run: bool = False,
seer_feature: SeerFeature = SeerFeature.AUTOFIX,
) -> SeerRun:
"""
Start or continue an agent-based autofix run.
Expand All @@ -460,6 +462,8 @@ def trigger_autofix_agent(
stopping_point: Where to stop the automated pipeline (only used for new runs)
allow_free_cohort: Internal-only flag set by agentic triage to bypass
quota for free cohort orgs. Not exposed via the API.
seer_feature: Internal-only. Agentic triage starts runs with AGENTIC_TRIAGE.
Not exposed via the API.
"""
outcome = "success"
try:
Expand Down Expand Up @@ -503,6 +507,7 @@ def trigger_autofix_agent(
user_context=user_context,
stopping_point=stopping_point,
allow_free_cohort=allow_free_cohort,
seer_feature=seer_feature,
user=user,
single_run=single_run,
)
Expand Down Expand Up @@ -832,7 +837,18 @@ def trigger_push_changes(
):
raise PermissionDenied("Code generation is disabled for this organization")

check_autofix_billing(group.organization, SeerOperation.AUTOFIX_OPEN_PR, is_new_run=False)
seer_run = (
SeerRun.objects.filter(organization_id=group.organization.id, seer_run_state_id=run_id)
.select_related("agent")
.first()
)
seer_agent_run = getattr(seer_run, "agent", None)
check_autofix_billing(
group.organization,
SeerOperation.AUTOFIX_OPEN_PR,
is_new_run=False,
seer_feature=get_run_seer_feature(seer_agent_run),
)

client = get_autofix_agent_client(group, user=user)

Expand All @@ -851,10 +867,6 @@ def trigger_push_changes(
)
)

seer_run = SeerRun.objects.filter(
organization_id=group.organization.id, seer_run_state_id=run_id
).first()

# Draft when review-request/CI-green is enabled; mark ready once green fires.
client.push_changes(
run_id,
Expand Down
34 changes: 26 additions & 8 deletions src/sentry/seer/autofix/feature/dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
get_automation_handoff,
is_free_cohort_org,
)
from sentry.seer.credits import SeerOperation, has_enough_seer_credits
from sentry.seer.credits import SeerFeature, SeerOperation, has_enough_seer_credits
from sentry.seer.models.run import SeerAgentRun, SeerRun
from sentry.seer.models.seer_api_models import UNKNOWN_RUN_ID_FOR_GROUP, SeerPermissionError
from sentry.users.models.user import User
Expand All @@ -65,6 +65,7 @@ class AutofixFeatureArgs:
user: User | RpcUser | AnonymousUser | None = None
flush: bool = True
single_run: bool = False
seer_feature: SeerFeature = SeerFeature.AUTOFIX


def _run_mirrors(group: Group, run_id: int) -> QuerySet[SeerAgentRun]:
Expand All @@ -78,6 +79,12 @@ def _mirror_matches_issue(group: Group) -> Q:
return Q(group_id=group.id, project_id=group.project_id)


def get_run_seer_feature(agent_run: SeerAgentRun | None) -> SeerFeature:
"""The feature the run was started under."""
raw = agent_run.extras.get("seer_feature") if agent_run is not None else None
return SeerFeature(raw) if isinstance(raw, str) and raw in SeerFeature else SeerFeature.AUTOFIX


def autofix_run_targets_other_issue(group: Group, run_id: int) -> bool:
"""True unless ``run_id`` is mirrored to this issue's group and project.

Expand Down Expand Up @@ -156,6 +163,7 @@ def check_autofix_billing(
*,
is_new_run: bool,
skip_quota_for_free_cohort: bool = False,
seer_feature: SeerFeature = SeerFeature.AUTOFIX,
) -> bool:
"""
Raise if the organization can't pay for the operation. Token-billed orgs are
Expand All @@ -164,9 +172,12 @@ def check_autofix_billing(
"""
# Check credits for token-billed orgs.
if features.has("organizations:token-billed-seer-enabled", organization):
if features.has(
"organizations:seer-credit-checks", organization
) and not has_enough_seer_credits(organization, operation):
if (
# Don't bill runs started by agentic triage (not exposed via the API).
seer_feature != SeerFeature.AGENTIC_TRIAGE
and features.has("organizations:seer-credit-checks", organization)
and not has_enough_seer_credits(organization, operation)
):
raise SeerOutOfCreditsException()
return False

Expand Down Expand Up @@ -198,6 +209,7 @@ def continue_single_run(
group.organization,
SeerOperation.AUTOFIX_SINGLE_RUN_REPLY,
is_new_run=False,
seer_feature=get_run_seer_feature(agent_run),
)

enable_coding = _org_allows_coding(group)
Expand Down Expand Up @@ -249,7 +261,13 @@ def trigger_autofix_feature(
# Avoid a circular import through the legacy Autofix dispatcher.
from sentry.seer.autofix.on_completion_hook import AutofixOnCompletionHook

is_new_run = args.existing_run_id is None
existing_agent_run: SeerAgentRun | None = None
seer_feature = args.seer_feature
if args.existing_run_id is not None:
existing_agent_run = require_autofix_run_for_group(group, args.existing_run_id)
seer_feature = get_run_seer_feature(existing_agent_run)
is_new_run = existing_agent_run is None

try:
uses_quota = check_autofix_billing(
group.organization,
Expand All @@ -258,6 +276,7 @@ def trigger_autofix_feature(
# Free cohort orgs bypass quota only when called from agentic triage
# (allow_free_cohort=True). Not exposed via the API.
skip_quota_for_free_cohort=args.allow_free_cohort,
seer_feature=seer_feature,
)
except NoSeerQuotaException:
logger.warning(
Expand Down Expand Up @@ -363,6 +382,7 @@ def trigger_autofix_feature(

extras: dict[str, Any] = {
"referrer": args.referrer.value,
"seer_feature": args.seer_feature.value,
}
# Store the stopping point here for delivery to use when advancing steps.
if stopping_point is not None:
Expand All @@ -381,9 +401,7 @@ def trigger_autofix_feature(
flush=args.flush,
extras=extras,
)
elif args.existing_run_id is not None:
existing_agent_run = require_autofix_run_for_group(group, args.existing_run_id)

elif existing_agent_run is not None:
run = client.continue_feature_run(
existing_agent_run=existing_agent_run,
payload=payload.dict(),
Expand Down
15 changes: 13 additions & 2 deletions src/sentry/seer/autofix/pr_iteration/manual_trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@
from sentry.models.group import Group
from sentry.seer.autofix.autofix_agent import get_autofix_run_state
from sentry.seer.autofix.constants import AutofixReferrer
from sentry.seer.autofix.feature.dispatch import get_run_seer_feature
from sentry.seer.autofix.pr_iteration.emit import bootstrap_iteration
from sentry.seer.autofix.pr_iteration.feedback import Feedback
from sentry.seer.autofix.pr_iteration.feedback_sources.base import ConsumeTriggerSource
from sentry.seer.autofix.pr_iteration.pause import PauseReason, get_pause_reason
from sentry.seer.autofix.pr_iteration.queue import enqueue_autofix_feedback
from sentry.seer.credits import SeerOperation, can_run_seer_operation
from sentry.seer.credits import SeerFeature, SeerOperation, can_run_seer_operation
from sentry.seer.endpoints.utils import SEER_PERMISSION_DENIED
from sentry.seer.models import SeerPermissionError
from sentry.seer.models.run import SeerAgentRun
from sentry.tasks.seer.pr_iteration import trigger_consume_pr_iteration_feedback
from sentry.users.services.user.service import user_service

Expand Down Expand Up @@ -83,7 +85,16 @@ def handle_ui_feedback(
)

# Check credits here, since the queued feedback is consumed after this request returns.
if not can_run_seer_operation(group.organization, SeerOperation.AUTOFIX_PR_ITERATION):
# Runs agentic triage starts aren't billed yet.
if (
not can_run_seer_operation(group.organization, SeerOperation.AUTOFIX_PR_ITERATION)
and get_run_seer_feature(
SeerAgentRun.objects.filter(
run__organization_id=group.organization.id, run__seer_run_state_id=run_id
).first()
)
!= SeerFeature.AGENTIC_TRIAGE
):
return Response(
{
"detail": "Your organization does not have enough credits to continue your Autofix run."
Expand Down
1 change: 1 addition & 0 deletions src/sentry/seer/credits.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class SeerOperation(StrEnum):
class SeerFeature(StrEnum):
"""Seer features as tagged on LLM proxy calls, which billing maps to data categories."""

AGENTIC_TRIAGE = "agentic_triage" # Not billed yet. Marks the Autofix runs it starts.
AUTOFIX = "autofix"
EXPLORER = "explorer"

Expand Down
6 changes: 4 additions & 2 deletions src/sentry/tasks/post_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -1652,15 +1652,17 @@ def kick_off_seer_automation(job: PostProcessJob) -> None:
event = job["event"]
group = event.group

if is_seer_seat_based_tier_enabled(group.organization):
if is_seer_seat_based_tier_enabled(group.organization) or features.has(
"organizations:token-billed-seer-enabled", group.organization
):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Token-billed orgs lose Autofix automation

High Severity

Token-billed orgs now skip post-process Autofix, but agentic triage still only enrolls seat-based or free-cohort orgs (seer.night_shift.enable_for_legacy_orgs defaults to false). Paying token-billed orgs can be left with an issue summary and no automated Autofix.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d15d1c0. Configure here.

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.

if group.seer_fixability_score is not None:
return
# Issues created in last 5 minutes only. This can be removed once this is live past 1 week.
# We don't want to backfill old issues since that will overwhelm Seer.
if (timezone.now() - group.first_seen).total_seconds() > 300:
return
# Generate issue summary and fixability score if not cached.
# Agentic Triage handles automations for seat-based orgs but still needs these.
# Agentic Triage handles automations for seat-based and token-billed orgs but still needs these.
cache_key = get_issue_summary_cache_key(group.id)
if cache.get(cache_key) is None:
lock_key, lock_name = get_issue_summary_lock_key(group.id)
Expand Down
6 changes: 6 additions & 0 deletions tests/sentry/seer/agentic_triage/test_delivery.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
deliver_agentic_triage_result,
)
from sentry.seer.autofix.utils import AutofixStoppingPoint
from sentry.seer.credits import SeerFeature
from sentry.seer.models.agentic_triage import (
SeerAgenticTriageRunErrorType,
SeerAgenticTriageRunResult,
Expand Down Expand Up @@ -219,6 +220,11 @@ def test_single_run_option_starts_single_runs(self) -> None:

assert mock_trigger.call_args.kwargs["single_run"] is True

def test_starts_autofix_as_agentic_triage(self) -> None:
mock_trigger = self._deliver_autofix_verdict()

assert mock_trigger.call_args.kwargs["seer_feature"] == SeerFeature.AGENTIC_TRIAGE

def test_dry_run_with_single_run_starts_nothing(self) -> None:
mock_trigger = self._deliver_autofix_verdict(single_run=True, dry_run=True)

Expand Down
Loading
Loading