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
2 changes: 2 additions & 0 deletions src/sentry/integrations/slack/message_builder/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class SlackAction(StrEnum):
SEER_AGENT_WRITE_APPROVE = "seer_agent_write_approve"
SEER_AGENT_WRITE_REJECT = "seer_agent_write_reject"
SEER_INVESTIGATION_START = "seer_investigation_start"
SEER_VIEW_BILLING = "seer_view_billing"
SEER_REQUEST_CREDITS = "seer_request_credits"


INCIDENT_COLOR_MAPPING = {
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/integrations/slack/webhooks/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,8 @@ def post(self, request: Request) -> Response:
SlackAction.SEER_AUTOFIX_VIEW_IN_SENTRY.value,
SlackAction.SEER_AUTOFIX_VIEW_PR.value,
SlackAction.SEER_INVESTIGATION_START.value,
SlackAction.SEER_VIEW_BILLING.value,
SlackAction.SEER_REQUEST_CREDITS.value,
}:
return self.respond()

Expand Down
60 changes: 60 additions & 0 deletions src/sentry/notifications/platform/slack/renderers/seer.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@
from sentry.notifications.platform.slack.provider import SlackRenderable
from sentry.notifications.platform.templates.seer import (
SeerAgentError,
SeerAgentOutOfCredits,
SeerAgentResponse,
SeerAutofixError,
SeerAutofixOutOfCredits,
SeerAutofixStopped,
SeerAutofixTrigger,
SeerAutofixUpdate,
Expand All @@ -48,6 +50,7 @@
)
from sentry.seer.agent.client_models import RepoPRState
from sentry.seer.autofix.utils import AutofixStoppingPoint, CodingAgentProviderType
from sentry.seer.credits import OUT_OF_CREDITS

if TYPE_CHECKING:
from sentry.models.group import Group
Expand Down Expand Up @@ -106,8 +109,10 @@ class AutofixStageConfig(TypedDict):
NotificationSource.SEER_AUTOFIX_STOPPED,
NotificationSource.SEER_AUTOFIX_SUCCESS,
NotificationSource.SEER_AUTOFIX_UPDATE,
NotificationSource.SEER_AUTOFIX_OUT_OF_CREDITS,
NotificationSource.SEER_AGENT_RESPONSE,
NotificationSource.SEER_AGENT_ERROR,
NotificationSource.SEER_AGENT_OUT_OF_CREDITS,
NotificationSource.SEER_INVESTIGATION_STARTED,
NotificationSource.SEER_INVESTIGATION_ERROR,
NotificationSource.SEER_INVESTIGATION_STATUS,
Expand Down Expand Up @@ -142,6 +147,8 @@ def render[DataT: NotificationData](
)
elif isinstance(data, SeerAutofixUpdate):
return cls._render_autofix_update(data, link_decorator)
elif isinstance(data, (SeerAutofixOutOfCredits, SeerAgentOutOfCredits)):
return cls._render_out_of_credits(data, link_decorator)
elif isinstance(data, SeerAgentError):
return cls._render_agent_error(data)
elif isinstance(data, SeerAgentResponse):
Expand Down Expand Up @@ -200,6 +207,57 @@ def _render_handoff_button(
),
)

@classmethod
def _render_out_of_credits(
cls,
data: SeerAutofixOutOfCredits | SeerAgentOutOfCredits,
link_decorator: NotificationLinkDecorator,
) -> SlackRenderable:
from sentry.integrations.slack.message_builder.routing import encode_action_id
from sentry.integrations.slack.message_builder.types import SlackAction
from sentry.models.organization import Organization

message = data.message
blocks: list[Block] = [SectionBlock(text=message)]

style: str | None
url: str | None
if data.has_billing_access:
try:
organization = Organization.objects.get_from_cache(id=data.organization_id)
except Organization.DoesNotExist:
return SlackRenderable(blocks=blocks, text=message)
text = "Configure auto-reload"
action = SlackAction.SEER_VIEW_BILLING
style = "primary"
url = link_decorator.decorate_url(
organization.absolute_url(f"/settings/{organization.slug}/billing/overview/")
)

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.

will update this once the request top-up/request credits page has been implemented, as well as the button text below.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would this be for people with or without billing permission? For billing permission users, the top up modal can be opened from the billing overview you've linked; for regular members, we can call this auto-reload request endpoint: https://github.com/getsentry/getsentry/blob/master/getsentry/api/seer_top_up_request.py
and maybe change the button copy accordingly

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.

Ah right, I'll need to make some changes in the template so we can render different copies based on who triggered the action. Thanks!

else:
# TODO(alexsohn): this click does nothing until the handler emails the admins.
text = "Contact Admin"
action = SlackAction.SEER_REQUEST_CREDITS
style = None
url = None

blocks.append(
ActionsBlock(
elements=[
ButtonElement(
text=text,
style=style,
url=url,
action_id=encode_action_id(
action=action.value,
organization_id=data.organization_id,
project_id=None,
),
)
]
)
)
return SlackRenderable(blocks=blocks, text=message)

@classmethod
def _render_autofix_error(cls, data: SeerAutofixError) -> SlackRenderable:
return SlackRenderable(
Expand Down Expand Up @@ -463,6 +521,8 @@ def _render_agent_pr_state(pr: RepoPRState) -> str:
return (
f"Updating {pr_text}..." if pr.pr_number else f"Creating a PR in {repo_text}..."
)
case "error" if pr.pr_creation_error_code == OUT_OF_CREDITS:
return f"Couldn't push to {pr_text}: your organization is out of Seer credits"
case "error":
return f"Latest push failed for {pr_text}"
case "completed":
Expand Down
68 changes: 68 additions & 0 deletions src/sentry/notifications/platform/templates/seer.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,42 @@ def render(self, data: SeerAutofixStopped) -> NotificationRenderedTemplate:
)


# NO_RELOADS is for billing members whose auto-reload is already on.
AUTOFIX_OUT_OF_CREDITS_MEMBER_MESSAGE = "Your organization does not have enough credits to continue your Autofix run. Ask your admin to increase your daily token limit or set up auto-reload."
AUTOFIX_OUT_OF_CREDITS_BILLING_MESSAGE = "Your organization does not have enough credits to continue your Autofix run. Set up auto-reload to keep your flow uninterrupted."
AUTOFIX_OUT_OF_CREDITS_NO_RELOADS_MESSAGE = "Your organization does not have enough credits to continue your Autofix run, and you have no auto-reloads remaining. Adjust your spend and turn on auto-reload."


class SeerAutofixOutOfCredits(NotificationData):
source: NotificationSource = NotificationSource.SEER_AUTOFIX_OUT_OF_CREDITS
has_billing_access: bool = False
auto_reload_enabled: bool = False

@property
def message(self) -> str:
if not self.has_billing_access:
return AUTOFIX_OUT_OF_CREDITS_MEMBER_MESSAGE
if self.auto_reload_enabled:
return AUTOFIX_OUT_OF_CREDITS_NO_RELOADS_MESSAGE
return AUTOFIX_OUT_OF_CREDITS_BILLING_MESSAGE


@template_registry.register(NotificationSource.SEER_AUTOFIX_OUT_OF_CREDITS)
class SeerAutofixOutOfCreditsTemplate(NotificationTemplate[SeerAutofixOutOfCredits]):
category = NotificationCategory.SEER
example_data = SeerAutofixOutOfCredits(
source=NotificationSource.SEER_AUTOFIX_OUT_OF_CREDITS,
organization_id=1,
)
hide_from_debugger = True

def render(self, data: SeerAutofixOutOfCredits) -> NotificationRenderedTemplate:
return NotificationRenderedTemplate(
subject=data.message,
body=[ParagraphSection(blocks=[PlainTextBlock(text=data.message)])],
)


class SeerAutofixCodeChange(TypedDict):
repo_name: str
diff: str
Expand Down Expand Up @@ -216,6 +252,38 @@ def render(self, data: SeerAgentError) -> NotificationRenderedTemplate:
)


AGENT_OUT_OF_CREDITS_MEMBER_MESSAGE = "Your organization does not have enough credits left for you to message Seer. Ask your admin to set up auto-reload."
AGENT_OUT_OF_CREDITS_BILLING_MESSAGE = "Your organization does not have enough credits left for you to message Seer. Adjust your spend and turn on auto-reload."
AGENT_OUT_OF_CREDITS_NO_RELOADS_MESSAGE = "Your organization does not have enough credits left for you to message Seer, and you have no auto-reloads remaining. Adjust your spend and turn on auto-reload."


class SeerAgentOutOfCredits(NotificationData):
source: NotificationSource = NotificationSource.SEER_AGENT_OUT_OF_CREDITS
has_billing_access: bool = False
auto_reload_enabled: bool = False

@property
def message(self) -> str:
if not self.has_billing_access:
return AGENT_OUT_OF_CREDITS_MEMBER_MESSAGE
if self.auto_reload_enabled:
return AGENT_OUT_OF_CREDITS_NO_RELOADS_MESSAGE
return AGENT_OUT_OF_CREDITS_BILLING_MESSAGE


@template_registry.register(NotificationSource.SEER_AGENT_OUT_OF_CREDITS)
class SeerAgentOutOfCreditsTemplate(NotificationTemplate[SeerAgentOutOfCredits]):
category = NotificationCategory.SEER
example_data = SeerAgentOutOfCredits(organization_id=1)
hide_from_debugger = True

def render(self, data: SeerAgentOutOfCredits) -> NotificationRenderedTemplate:
return NotificationRenderedTemplate(
subject=data.message,
body=[ParagraphSection(blocks=[PlainTextBlock(text=data.message)])],
)


class SeerAgentResponse(NotificationData):
"""Notification data for Agent completion response in Slack."""

Expand Down
4 changes: 4 additions & 0 deletions src/sentry/notifications/platform/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,11 @@ class NotificationSource(StrEnum):
SEER_AUTOFIX_TRIGGER = "seer-autofix-trigger"
SEER_AUTOFIX_FOOTER = "seer-autofix-footer"
SEER_AUTOFIX_SUCCESS = "seer-autofix-success"
SEER_AUTOFIX_OUT_OF_CREDITS = "seer-autofix-out-of-credits"
SEER_AGENT_RESPONSE = "seer-agent-response"
SEER_AGENT_ERROR = "seer-agent-error"
SEER_AGENT_WRITE_APPROVAL = "seer-agent-write-approval"
SEER_AGENT_OUT_OF_CREDITS = "seer-agent-out-of-credits"
SEER_INVESTIGATION_STARTED = "seer-investigation-started"
SEER_INVESTIGATION_ERROR = "seer-investigation-error"
SEER_INVESTIGATION_STATUS = "seer-investigation-status"
Expand Down Expand Up @@ -139,9 +141,11 @@ class NotificationSource(StrEnum):
NotificationSource.SEER_AUTOFIX_STOPPED,
NotificationSource.SEER_AUTOFIX_SUCCESS,
NotificationSource.SEER_AUTOFIX_UPDATE,
NotificationSource.SEER_AUTOFIX_OUT_OF_CREDITS,
NotificationSource.SEER_AGENT_RESPONSE,
NotificationSource.SEER_AGENT_ERROR,
NotificationSource.SEER_AGENT_WRITE_APPROVAL,
NotificationSource.SEER_AGENT_OUT_OF_CREDITS,
NotificationSource.SEER_INVESTIGATION_STARTED,
NotificationSource.SEER_INVESTIGATION_ERROR,
NotificationSource.SEER_INVESTIGATION_STATUS,
Expand Down
3 changes: 3 additions & 0 deletions src/sentry/seer/credits.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
from sentry import features, seer_credit
from sentry.models.organization import Organization

# Seer's failure_reason and pr_creation_error_code when the org ran out of credits.
OUT_OF_CREDITS = "out_of_credits"


class SeerOperation(StrEnum):
AGENT_CHAT_MESSAGE = "agent_chat_message"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from sentry.integrations.slack.message_builder.routing import encode_action_id
from sentry.integrations.slack.message_builder.types import SlackAction
from tests.sentry.integrations.slack.webhooks.actions import BaseEventTest


class SeerRequestCreditsTest(BaseEventTest):
def test_click_is_acknowledged(self) -> None:
resp = self.post_webhook_block_kit(
action_data=[
{
"type": "button",
"action_id": encode_action_id(
action=SlackAction.SEER_REQUEST_CREDITS,
organization_id=self.organization.id,
project_id=None,
),
"text": {"type": "plain_text", "text": "Contact Admin"},
}
]
)

assert resp.status_code == 200, resp.content
assert resp.content == b""
Loading
Loading