Skip to content
Merged
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
7 changes: 7 additions & 0 deletions src/sentry/options/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -3655,3 +3655,10 @@
default=0.0,
flags=FLAG_AUTOMATOR_MODIFIABLE,
)

register(
"seer.scanner_no_consent.rollout_rate",
Copy link
Member

Choose a reason for hiding this comment

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

nit but i believe we prefer to use hyphens rather than underscores in options

Copy link
Member Author

Choose a reason for hiding this comment

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

really? i see both in the file.

Copy link
Member

Choose a reason for hiding this comment

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

ah yeah, that's not surprising. kind of a convention that's hard to stick to/enforce. we document it here but i couldn't tell you why 🤷‍♂️

type=Float,
default=0.0,
flags=FLAG_MODIFIABLE_RATE | FLAG_AUTOMATOR_MODIFIABLE,
)
4 changes: 2 additions & 2 deletions src/sentry/seer/autofix/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,9 @@ def is_issue_eligible_for_seer_automation(group: Group) -> bool:
):
return False

from sentry.seer.seer_setup import get_seer_org_acknowledgement
from sentry.seer.seer_setup import get_seer_org_acknowledgement_for_scanner

seer_enabled = get_seer_org_acknowledgement(group.organization)
seer_enabled = get_seer_org_acknowledgement_for_scanner(group.organization)
if not seer_enabled:
return False

Expand Down
12 changes: 12 additions & 0 deletions src/sentry/seer/seer_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from sentry import features
from sentry.models.organization import Organization
from sentry.models.promptsactivity import PromptsActivity
from sentry.options.rollout import in_rollout_group
from sentry.users.models.user import User
from sentry.users.services.user.model import RpcUser

Expand Down Expand Up @@ -36,6 +37,17 @@ def get_seer_org_acknowledgement(organization: Organization) -> bool:
).exists()


def get_seer_org_acknowledgement_for_scanner(organization: Organization) -> bool:
return PromptsActivity.objects.filter(
feature=feature_name,
organization_id=organization.id,
project_id=0,
).exists() or (
features.has("organizations:gen-ai-consent-flow-removal", organization)
and in_rollout_group("seer.scanner_no_consent.rollout_rate", organization.id)
)


def has_seer_access(
organization: Organization, actor: User | AnonymousUser | RpcUser | None = None
) -> bool:
Expand Down
123 changes: 123 additions & 0 deletions tests/sentry/seer/autofix/test_autofix_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import orjson
import pytest

from sentry.constants import DataCategory
from sentry.seer.autofix.constants import AutofixStatus
from sentry.seer.autofix.utils import (
AutofixState,
AutofixTriggerSource,
CodingAgentStatus,
get_autofix_prompt,
get_coding_agent_prompt,
is_issue_eligible_for_seer_automation,
)
from sentry.seer.models import SeerApiError
from sentry.testutils.cases import TestCase
Expand Down Expand Up @@ -189,3 +191,124 @@ def test_autofix_state_validate_parses_nested_structures(self):

# Top-level coding_agents map is parsed with enum status
assert state.coding_agents["agent-1"].status == CodingAgentStatus.COMPLETED


class TestIsIssueEligibleForSeerAutomation(TestCase):
"""Test the is_issue_eligible_for_seer_automation function."""

def setUp(self):
super().setUp()
self.organization = self.create_organization(name="test-org")
self.project = self.create_project(organization=self.organization)
self.group = self.create_group(project=self.project)

def test_returns_false_for_unsupported_issue_categories(self):
"""Test returns False for unsupported issue categories like REPLAY and FEEDBACK."""
from sentry.issues.grouptype import FeedbackGroup, ReplayRageClickType

# Create groups with unsupported categories
replay_group = self.create_group(project=self.project, type=ReplayRageClickType.type_id)
feedback_group = self.create_group(project=self.project, type=FeedbackGroup.type_id)

assert is_issue_eligible_for_seer_automation(replay_group) is False
assert is_issue_eligible_for_seer_automation(feedback_group) is False

def test_returns_true_for_supported_issue_categories(self):
"""Test returns True for supported issue categories when all conditions are met."""
with self.feature("organizations:gen-ai-features"):
with patch(
"sentry.seer.seer_setup.get_seer_org_acknowledgement_for_scanner"
) as mock_ack:
with patch("sentry.quotas.backend.has_available_reserved_budget") as mock_budget:
mock_ack.return_value = True
mock_budget.return_value = True
self.project.update_option("sentry:seer_scanner_automation", True)

# Test supported categories - using default error group
result = is_issue_eligible_for_seer_automation(self.group)

assert result is True

def test_returns_false_when_gen_ai_features_not_enabled(self):
"""Test returns False when organizations:gen-ai-features feature flag is not enabled."""
result = is_issue_eligible_for_seer_automation(self.group)
assert result is False

def test_returns_false_when_ai_features_hidden(self):
"""Test returns False when sentry:hide_ai_features option is enabled."""
with self.feature("organizations:gen-ai-features"):
self.organization.update_option("sentry:hide_ai_features", True)
result = is_issue_eligible_for_seer_automation(self.group)
assert result is False

def test_returns_false_when_scanner_automation_disabled_and_not_always_trigger(self):
"""Test returns False when scanner automation is disabled and issue type doesn't always trigger."""
with self.feature("organizations:gen-ai-features"):
self.project.update_option("sentry:seer_scanner_automation", False)
result = is_issue_eligible_for_seer_automation(self.group)
assert result is False

@patch("sentry.seer.seer_setup.get_seer_org_acknowledgement_for_scanner")
def test_returns_false_when_org_not_acknowledged(self, mock_get_acknowledgement):
"""Test returns False when organization has not acknowledged Seer for scanner."""
with self.feature("organizations:gen-ai-features"):
self.project.update_option("sentry:seer_scanner_automation", True)
mock_get_acknowledgement.return_value = False

result = is_issue_eligible_for_seer_automation(self.group)

assert result is False
mock_get_acknowledgement.assert_called_once_with(self.organization)

@patch("sentry.seer.seer_setup.get_seer_org_acknowledgement_for_scanner")
@patch("sentry.quotas.backend.has_available_reserved_budget")
def test_returns_false_when_no_budget_available(
self, mock_has_budget, mock_get_acknowledgement
):
"""Test returns False when organization has no available budget for scanner."""
with self.feature("organizations:gen-ai-features"):
self.project.update_option("sentry:seer_scanner_automation", True)
mock_get_acknowledgement.return_value = True
mock_has_budget.return_value = False

result = is_issue_eligible_for_seer_automation(self.group)

assert result is False
mock_has_budget.assert_called_once_with(
org_id=self.organization.id, data_category=DataCategory.SEER_SCANNER
)

@patch("sentry.seer.seer_setup.get_seer_org_acknowledgement_for_scanner")
@patch("sentry.quotas.backend.has_available_reserved_budget")
def test_returns_true_when_all_conditions_met(self, mock_has_budget, mock_get_acknowledgement):
"""Test returns True when all eligibility conditions are met."""
with self.feature("organizations:gen-ai-features"):
self.project.update_option("sentry:seer_scanner_automation", True)
mock_get_acknowledgement.return_value = True
mock_has_budget.return_value = True

result = is_issue_eligible_for_seer_automation(self.group)

assert result is True
mock_get_acknowledgement.assert_called_once_with(self.organization)
mock_has_budget.assert_called_once_with(
org_id=self.organization.id, data_category=DataCategory.SEER_SCANNER
)

@patch("sentry.seer.seer_setup.get_seer_org_acknowledgement_for_scanner")
@patch("sentry.quotas.backend.has_available_reserved_budget")
def test_returns_true_when_issue_type_always_triggers(
self, mock_has_budget, mock_get_acknowledgement
):
"""Test returns True when issue type has always_trigger_seer_automation even if scanner automation is disabled."""
with self.feature("organizations:gen-ai-features"):
# Disable scanner automation
self.project.update_option("sentry:seer_scanner_automation", False)
mock_get_acknowledgement.return_value = True
mock_has_budget.return_value = True

# Mock the group's issue_type to always trigger
with patch.object(self.group.issue_type, "always_trigger_seer_automation", True):
result = is_issue_eligible_for_seer_automation(self.group)

assert result is True
Loading
Loading