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
10 changes: 7 additions & 3 deletions src/sentry/integrations/slack/message_builder/issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,9 +424,13 @@ def __init__(
self.has_mentions_read_scope = has_mentions_read_scope
self.workflow_id = workflow_id
self.link_decorator = link_decorator
self._has_autofix = SeerAutofixOperator.has_access(
organization=self.group.organization, entrypoint_key=SeerEntrypointKey.SLACK
) and SeerAutofixOperator.can_trigger_autofix(group=self.group)
self._has_autofix = (
not is_unfurl

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.

this is to block unfurl requests (generating preview for the graph) from checking autofix quota (which can take long)

and SeerAutofixOperator.has_access(
organization=self.group.organization, entrypoint_key=SeerEntrypointKey.SLACK
)
and SeerAutofixOperator.can_trigger_autofix(group=self.group)
)

def get_title_block(
self,
Expand Down
40 changes: 23 additions & 17 deletions src/sentry/seer/entrypoints/operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from taskbroker_client.retry import Retry

from sentry import features, options
from sentry.constants import ENABLE_SEER_CODING_DEFAULT, DataCategory
from sentry.constants import ENABLE_SEER_CODING_DEFAULT
from sentry.inbox.issue import record_issue_pr_ready_for_review
from sentry.inbox.types import SYSTEM_ACTOR as INBOX_SYSTEM_ACTOR
from sentry.inbox.types import InboxActor
Expand All @@ -27,7 +27,14 @@
from sentry.seer.agent.on_completion_hook import AgentOnCompletionHook
from sentry.seer.autofix.commit_author import commit_author_for_user
from sentry.seer.autofix.constants import AutofixReferrer
from sentry.seer.autofix.utils import AutofixStoppingPoint, get_automation_handoff
from sentry.seer.autofix.exceptions import NoSeerQuotaException, SeerOutOfCreditsException
from sentry.seer.autofix.feature.dispatch import check_autofix_billing
from sentry.seer.autofix.utils import (
AutofixStoppingPoint,
get_automation_handoff,
is_issue_category_eligible,
)
from sentry.seer.credits import SeerOperation
from sentry.seer.entrypoints.cache import (
SeerOperatorAgentCache,
SeerOperatorAutofixCache,
Expand Down Expand Up @@ -161,22 +168,22 @@ def has_access(
def can_trigger_autofix(cls, *, group: Group) -> bool:
"""
Checks if a group is permitted to trigger autofix.
Validates Seer access for the organization, the issue category, and autofix quota.
Validates Seer access for the organization, the issue category, and that the
organization can pay for the root cause step.
"""
from sentry import quotas
from sentry.seer.autofix.utils import is_free_cohort_org, is_issue_category_eligible

return (
has_seer_access(group.organization)
and is_issue_category_eligible(group)
and (
is_free_cohort_org(group.organization)
or quotas.backend.check_seer_quota(
org_id=group.organization.id,
data_category=DataCategory.SEER_AUTOFIX,
)
if not has_seer_access(group.organization) or not is_issue_category_eligible(group):
return False
try:
check_autofix_billing(
group.organization,
SeerOperation.AUTOFIX_ROOT_CAUSE,
is_new_run=True,
# Free cohort orgs have no subscription, so the quota check always fails for them.
skip_quota_for_free_cohort=True,
)
)
except (NoSeerQuotaException, SeerOutOfCreditsException):
return False
return True

def trigger_autofix(
self,
Expand Down Expand Up @@ -209,7 +216,6 @@ def trigger_autofix_agent(
trigger_autofix_agent,
trigger_push_changes,
)
from sentry.seer.autofix.exceptions import NoSeerQuotaException, SeerOutOfCreditsException
from sentry.seer.autofix.feature.models import FEATURE_ID as AUTOFIX_FEATURE_ID
from sentry.seer.autofix.steps import AutofixStep

Expand Down
30 changes: 30 additions & 0 deletions tests/sentry/integrations/slack/test_message_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

import orjson
from django.test import override_settings
from sentry_protos.billing.v1.services.seer_feature_billing.v1.endpoint_check_seer_credits_pb2 import (
CheckSeerCreditsResponse,
)

from sentry.grouping.grouptype import ErrorGroupType
from sentry.integrations.messaging.message_builder import (
Expand Down Expand Up @@ -45,6 +48,7 @@
from sentry.testutils.cases import PerformanceIssueTestCase, TestCase
from sentry.testutils.factories import EventType
from sentry.testutils.helpers.datetime import before_now, freeze_time
from sentry.testutils.helpers.features import with_feature
from sentry.testutils.helpers.notifications import create_workflow_notification_origin_for_rule
from sentry.testutils.outbox import outbox_runner
from sentry.testutils.silo import assume_test_silo_mode
Expand Down Expand Up @@ -1027,6 +1031,32 @@ def test_autofix_button_hidden_when_no_other_actions(self, mock_quota: MagicMock
blocks = SlackIssuesMessageBuilder(group, issue_details=True).build()
assert not self._has_autofix_button(blocks)

@override_settings(SENTRY_SELF_HOSTED=False)
@with_feature(["organizations:seer-credit-checks", "organizations:token-billed-seer-enabled"])
@patch(
"sentry.seer_credit.backend.check_seer_credits",
return_value=CheckSeerCreditsResponse(has_enough_credits=False),
)
@patch("sentry.quotas.backend.check_seer_quota", return_value=True)
def test_autofix_button_hidden_without_seer_credits(
self, _mock_quota: MagicMock, mock_check_seer_credits: MagicMock
) -> None:
group = self.create_group(project=self.project)
blocks = SlackIssuesMessageBuilder(group).build()
assert not self._has_autofix_button(blocks)
mock_check_seer_credits.assert_called_once()

@override_settings(SENTRY_SELF_HOSTED=False)
@with_feature(["organizations:seer-credit-checks", "organizations:token-billed-seer-enabled"])
@patch("sentry.seer_credit.backend.check_seer_credits")
@patch("sentry.quotas.backend.check_seer_quota", return_value=True)
def test_unfurl_skips_seer_credit_check(
self, _mock_quota: MagicMock, mock_check_seer_credits: MagicMock
) -> None:
group = self.create_group(project=self.project)
SlackIssuesMessageBuilder(group, is_unfurl=True).build()
mock_check_seer_credits.assert_not_called()


class BuildGroupAttachmentReplaysTest(TestCase):
@patch("sentry.models.group.Group.has_replays")
Expand Down
45 changes: 45 additions & 0 deletions tests/sentry/seer/entrypoints/test_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import pytest
from django.test import override_settings
from django.utils import timezone
from sentry_protos.billing.v1.services.seer_feature_billing.v1.endpoint_check_seer_credits_pb2 import (
CheckSeerCreditsResponse,
)

from fixtures.seer.webhooks import MOCK_RUN_ID
from sentry.integrations.types import EventLifecycleOutcome, ExternalProviders
Expand Down Expand Up @@ -59,6 +62,7 @@
from sentry.testutils.asserts import assert_failure_metric, assert_halt_metric
from sentry.testutils.cases import TestCase
from sentry.testutils.helpers.action_log import capture_action_log
from sentry.testutils.helpers.features import with_feature
from sentry.testutils.helpers.options import override_options
from sentry.types.activity import ActivityType
from sentry.utils.locking import UnableToAcquireLock
Expand Down Expand Up @@ -619,6 +623,47 @@ def test_can_trigger_autofix_returns_false_for_ineligible_category(
def test_can_trigger_autofix_returns_false_without_quota(self, mock_quota: MagicMock) -> None:
assert SeerAutofixOperator.can_trigger_autofix(group=self.group) is False

@with_feature(["organizations:seer-credit-checks", "organizations:token-billed-seer-enabled"])
@patch(
"sentry.seer_credit.backend.check_seer_credits",
return_value=CheckSeerCreditsResponse(has_enough_credits=False),
)
@patch("sentry.quotas.backend.check_seer_quota", return_value=True)
def test_can_trigger_autofix_returns_false_without_root_cause_credits(
self, _mock_quota: MagicMock, mock_check_seer_credits: MagicMock
) -> None:
assert SeerAutofixOperator.can_trigger_autofix(group=self.group) is False
mock_check_seer_credits.assert_called_once()

@patch("sentry.seer_credit.backend.check_seer_credits")
@patch("sentry.quotas.backend.check_seer_quota", return_value=True)
def test_can_trigger_autofix_skips_credit_check_when_flag_off(
self, _mock_quota: MagicMock, mock_check_seer_credits: MagicMock
) -> None:
assert SeerAutofixOperator.can_trigger_autofix(group=self.group) is True
mock_check_seer_credits.assert_not_called()

@with_feature(["organizations:seer-credit-checks", "organizations:token-billed-seer-enabled"])
@patch(
"sentry.seer_credit.backend.check_seer_credits",
return_value=CheckSeerCreditsResponse(has_enough_credits=True),
)
@patch("sentry.quotas.backend.check_seer_quota", return_value=False)
def test_can_trigger_autofix_skips_quota_check_for_token_billed_orgs(
self, mock_quota: MagicMock, mock_check_seer_credits: MagicMock
) -> None:
assert SeerAutofixOperator.can_trigger_autofix(group=self.group) is True
mock_check_seer_credits.assert_called_once()
mock_quota.assert_not_called()

@patch("sentry.seer.autofix.feature.dispatch.is_free_cohort_org", return_value=True)
@patch("sentry.quotas.backend.check_seer_quota", return_value=False)
def test_can_trigger_autofix_skips_quota_check_for_free_cohort_orgs(
self, mock_quota: MagicMock, _mock_free_cohort: MagicMock
) -> None:
assert SeerAutofixOperator.can_trigger_autofix(group=self.group) is True
mock_quota.assert_not_called()

@patch.object(SeerAutofixOperator, "has_access", return_value=True)
def test_seer_event_creates_activity_rca_completed(self, _mock_has_access: MagicMock) -> None:
event_payload = {
Expand Down
Loading