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
1 change: 1 addition & 0 deletions src/sentry/seer/entrypoints/slack/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
class ProcessMentionHaltReason(StrEnum):
IDENTITY_NOT_LINKED = "identity_not_linked"
USER_NOT_ORG_MEMBER = "user_not_org_member"
OUT_OF_SEER_CREDITS = "out_of_seer_credits"


class ProcessMentionFailureReason(StrEnum):
Expand Down
14 changes: 14 additions & 0 deletions src/sentry/seer/entrypoints/slack/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from sentry.integrations.services.integration.service import integration_service
from sentry.models.organization import Organization
from sentry.notifications.platform.slack.provider import SlackRenderable
from sentry.seer.credits import SeerFeature, SeerOperation, can_run_seer_operation
from sentry.seer.entrypoints.metrics import (
SlackEntrypointEventLifecycleMetric,
SlackEntrypointInteractionType,
Expand Down Expand Up @@ -160,6 +161,19 @@ def process_mention_for_slack(
)
return

# Check credits before fetching context or starting a run.
if not can_run_seer_operation(
organization, SeerFeature.EXPLORER, SeerOperation.AGENT_CHAT_MESSAGE
):
lifecycle.record_halt(ProcessMentionHaltReason.OUT_OF_SEER_CREDITS)
# The webhook set "is thinking..." before queueing this task; clear it.
entrypoint.install.set_thread_status(
channel_id=channel_id,
thread_ts=thread_ts or ts,
status="",
)
return

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.

Out-of-credits halt sends no Slack message

Medium Severity

When the org is out of Seer credits, the mention handler records a halt and clears the thinking status, then returns without telling the user. Sibling halt paths (identity_not_linked, user_not_org_member) send an ephemeral Slack message, and the web chat equivalent returns a 402 with an explanation. Users will see is thinking... vanish with no reply and no indication they are out of credits.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ff2979f. 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.

this will be a follow-up


prompt = extract_prompt(text, bot_user_id)

linked_result = _resolve_linked_messages(
Expand Down
100 changes: 100 additions & 0 deletions tests/sentry/seer/entrypoints/slack/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.test import override_settings

from sentry.integrations.messaging.metrics import SeerSlackHaltReason
from sentry.seer.credits import SeerFeature, SeerOperation
from sentry.seer.entrypoints.slack.analytics import (
SlackSeerAgentConversation,
SlackSeerAgentFeedback,
Expand Down Expand Up @@ -401,6 +402,105 @@ def test_skips_analytics_when_trigger_agent_fails(
assert_not_analytics_event(mock_record, SlackSeerAgentResponded)


class ProcessMentionSeerCreditsTest(TestCase):
def setUp(self) -> None:
super().setUp()
self.mock_entrypoint = MagicMock()
self.mock_entrypoint.thread_ts = "1234567890.123456"
self.mock_entrypoint.install.get_thread_history.return_value = []

def _run_task(
self, *, user_id: int, mock_agent_cls: MagicMock, mock_resolve_user: MagicMock
) -> None:
mock_resolve_user.return_value = MagicMock(id=user_id, username="alice")
mock_agent_cls.return_value = self.mock_entrypoint
process_mention_for_slack(**{**TASK_KWARGS, "organization_id": self.organization.id})

@patch("sentry.integrations.utils.metrics.EventLifecycle.record_event")
@patch("sentry.seer.entrypoints.slack.tasks.can_run_seer_operation", return_value=False)
@patch("sentry.seer.entrypoints.slack.tasks.SeerAgentOperator")
@patch("sentry.seer.entrypoints.slack.tasks.SlackAgentEntrypoint")
@patch("sentry.seer.entrypoints.slack.tasks._resolve_user")
def test_out_of_credits_halts_without_running(
self,
mock_resolve_user: MagicMock,
mock_agent_cls: MagicMock,
mock_operator_cls: MagicMock,
mock_can_run: MagicMock,
mock_record: MagicMock,
) -> None:
mock_operator_cls.has_access.return_value = True

self._run_task(
user_id=self.user.id, mock_agent_cls=mock_agent_cls, mock_resolve_user=mock_resolve_user
)

mock_can_run.assert_called_once_with(
self.organization, SeerFeature.EXPLORER, SeerOperation.AGENT_CHAT_MESSAGE
)
mock_operator_cls.assert_not_called()
self.mock_entrypoint.install.get_thread_history.assert_not_called()
self.mock_entrypoint.install.set_thread_status.assert_called_once_with(
channel_id="C1234567890",
thread_ts="1234567890.123456",
status="",
)
assert_halt_metric(mock_record, ProcessMentionHaltReason.OUT_OF_SEER_CREDITS)

@patch("sentry.seer.credits.seer_credit.backend.check_seer_credits")
@patch("sentry.seer.entrypoints.slack.tasks.SeerAgentOperator")
@patch("sentry.seer.entrypoints.slack.tasks.SlackAgentEntrypoint")
@patch("sentry.seer.entrypoints.slack.tasks._resolve_user")
def test_credit_check_flag_on_with_enough_credits_runs(
self,
mock_resolve_user: MagicMock,
mock_agent_cls: MagicMock,
mock_operator_cls: MagicMock,
mock_check: MagicMock,
) -> None:
mock_operator_cls.has_access.return_value = True
mock_operator_cls.return_value.trigger_agent.return_value = None
mock_check.return_value = MagicMock(has_enough_credits=True)

with self.feature(
["organizations:seer-credit-checks", "organizations:token-billed-seer-enabled"]
):
self._run_task(
user_id=self.user.id,
mock_agent_cls=mock_agent_cls,
mock_resolve_user=mock_resolve_user,
)

mock_check.assert_called_once()
mock_operator_cls.return_value.trigger_agent.assert_called_once()

@patch("sentry.seer.credits.seer_credit.backend.check_seer_credits")
@patch("sentry.seer.entrypoints.slack.tasks.SeerAgentOperator")
@patch("sentry.seer.entrypoints.slack.tasks.SlackAgentEntrypoint")
@patch("sentry.seer.entrypoints.slack.tasks._resolve_user")
def test_credit_check_flag_on_without_enough_credits_halts(
self,
mock_resolve_user: MagicMock,
mock_agent_cls: MagicMock,
mock_operator_cls: MagicMock,
mock_check: MagicMock,
) -> None:
mock_operator_cls.has_access.return_value = True
mock_check.return_value = MagicMock(has_enough_credits=False)

with self.feature(
["organizations:seer-credit-checks", "organizations:token-billed-seer-enabled"]
):
self._run_task(
user_id=self.user.id,
mock_agent_cls=mock_agent_cls,
mock_resolve_user=mock_resolve_user,
)

mock_check.assert_called_once()
mock_operator_cls.assert_not_called()


class LinkedMessagesContextTest(TestCase):
"""Tests for the Slack-permalink → on_page_context wiring."""

Expand Down
Loading