Skip to content
Closed
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: 9 additions & 1 deletion src/sentry/notifications/platform/slack/renderers/seer.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@
from sentry.notifications.platform.renderer import NotificationRenderer
from sentry.notifications.platform.slack.provider import SlackRenderable
from sentry.notifications.platform.templates.seer import (
AUTOFIX_OUT_OF_CREDITS_MESSAGE,
SeerAgentError,
SeerAgentResponse,
SeerAutofixError,
SeerAutofixOutOfCredits,
SeerAutofixTrigger,
SeerAutofixUpdate,
SeerInvestigationConclusion,
Expand Down Expand Up @@ -104,6 +106,7 @@ class AutofixStageConfig(TypedDict):
NotificationSource.SEER_AUTOFIX_ERROR,
NotificationSource.SEER_AUTOFIX_SUCCESS,
NotificationSource.SEER_AUTOFIX_UPDATE,
NotificationSource.SEER_AUTOFIX_OUT_OF_CREDITS,
NotificationSource.SEER_AGENT_RESPONSE,
NotificationSource.SEER_AGENT_ERROR,
NotificationSource.SEER_INVESTIGATION_STARTED,
Expand Down Expand Up @@ -131,6 +134,11 @@ def render[DataT: NotificationData](
return cls._render_autofix_error(data)
elif isinstance(data, SeerAutofixUpdate):
return cls._render_autofix_update(data, link_decorator)
elif isinstance(data, SeerAutofixOutOfCredits):
return SlackRenderable(
blocks=[SectionBlock(text=AUTOFIX_OUT_OF_CREDITS_MESSAGE)],
text=AUTOFIX_OUT_OF_CREDITS_MESSAGE,
)
elif isinstance(data, SeerAgentError):
return cls._render_agent_error(data)
elif isinstance(data, SeerAgentResponse):
Expand Down Expand Up @@ -221,7 +229,7 @@ def _render_autofix_update(
project_id=data.project_id,
)
)
elif data.has_next_trigger:
elif data.has_next_trigger and data.has_credits_for_next_step:
action_elements.append(
cls._render_autofix_button(data=SeerAutofixTrigger.from_update(data))
)
Expand Down
28 changes: 28 additions & 0 deletions src/sentry/notifications/platform/templates/seer.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,33 @@ def render(self, data: SeerAutofixError) -> NotificationRenderedTemplate:
)


AUTOFIX_OUT_OF_CREDITS_MESSAGE = "Your org does not have enough credits to continue your Autofix run. Set up auto top up to keep your flow uninterrupted."


class SeerAutofixOutOfCredits(NotificationData):
# False: the org tried to start a Seer step without credits.
# True: the org ran out of credits partway through a run.
started: bool
source: NotificationSource = NotificationSource.SEER_AUTOFIX_OUT_OF_CREDITS


@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,
started=False,
)
hide_from_debugger = True

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


class SeerAutofixCodeChange(TypedDict):
repo_name: str
diff: str
Expand All @@ -82,6 +109,7 @@ class SeerAutofixUpdate(NotificationData):
pull_requests: list[SeerAutofixPullRequest] = Field(default_factory=list)
summary: str | None = None
handoff_target: CodingAgentProviderType | None = None
has_credits_for_next_step: bool = True
source: NotificationSource = NotificationSource.SEER_AUTOFIX_UPDATE

@property
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/notifications/platform/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ 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"
Expand Down Expand Up @@ -137,6 +138,7 @@ class NotificationSource(StrEnum):
NotificationSource.SEER_AUTOFIX_ERROR,
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@
SeerAgentWriteApprovalSlackRenderer,
)
from sentry.notifications.platform.templates.seer import (
AUTOFIX_OUT_OF_CREDITS_MESSAGE,
SeerAgentError,
SeerAgentResponse,
SeerAgentWriteApproval,
SeerAutofixCodeChange,
SeerAutofixOutOfCredits,
SeerAutofixOutOfCreditsTemplate,
SeerAutofixPullRequest,
SeerAutofixUpdate,
SeerInvestigationConclusionTemplate,
Expand Down Expand Up @@ -230,6 +233,32 @@ def test_render_autofix_update_no_progress_with_next_trigger(
assert isinstance(actions_block.elements[1], ButtonElement)
assert actions_block.elements[1].value == AutofixStoppingPoint.SOLUTION.value

def test_render_autofix_update_without_credits_has_no_next_trigger(self) -> None:
data = self._create_update(
current_point=AutofixStoppingPoint.ROOT_CAUSE,
summary="Test summary",
).copy(update={"has_credits_for_next_step": False})
renderable = SeerSlackRenderer._render_autofix_update(data, link_decorator(data))
actions_blocks = [b for b in renderable["blocks"] if isinstance(b, ActionsBlock)]
assert len(actions_blocks) == 1
# Only the link to Sentry is left
assert len(actions_blocks[0].elements) == 1
assert isinstance(actions_blocks[0].elements[0], LinkButtonElement)

@patch(
"sentry.notifications.platform.templates.seer.organization_service.get_option",
return_value=False,
)
def test_render_autofix_update_solution_coding_disabled_has_no_next_trigger(
self, _mock_get_option: Mock
) -> None:
data = self._create_update(current_point=AutofixStoppingPoint.SOLUTION)
renderable = SeerSlackRenderer._render_autofix_update(data, link_decorator(data))
actions_blocks = [b for b in renderable["blocks"] if isinstance(b, ActionsBlock)]
assert len(actions_blocks) == 1
assert len(actions_blocks[0].elements) == 1
assert isinstance(actions_blocks[0].elements[0], LinkButtonElement)

@patch(
"sentry.notifications.platform.templates.seer.organization_service.get_option",
return_value=True,
Expand Down Expand Up @@ -291,6 +320,21 @@ def test_render_autofix_update_root_cause_with_handoff_target(self) -> None:
assert handoff_button.action_id is not None
assert handoff_button.action_id.startswith("seer_autofix_handoff::")

def test_render_autofix_update_handoff_shown_without_credits(self) -> None:
data = self._create_update(
current_point=AutofixStoppingPoint.ROOT_CAUSE,
handoff_target="cursor_background_agent",
).copy(update={"has_credits_for_next_step": False})
renderable = SeerSlackRenderer._render_autofix_update(data, link_decorator(data))

actions_block = next((b for b in renderable["blocks"] if isinstance(b, ActionsBlock)), None)
assert actions_block is not None
assert len(actions_block.elements) == 2
handoff_button = actions_block.elements[1]
assert isinstance(handoff_button, ButtonElement)
assert handoff_button.action_id is not None
assert handoff_button.action_id.startswith("seer_autofix_handoff::")

def test_render_autofix_update_root_cause_with_reasoning(self) -> None:
data = self._create_update(
current_point=AutofixStoppingPoint.ROOT_CAUSE,
Expand Down Expand Up @@ -325,6 +369,22 @@ def test_render_autofix_update_solution_no_next_trigger_when_coding_disabled(
assert isinstance(actions_block.elements[0], LinkButtonElement)


class SeerSlackRendererAutofixOutOfCreditsTest(TestCase):
def test_render_out_of_credits(self) -> None:
for started in (False, True):
data = SeerAutofixOutOfCredits(organization_id=1, started=started)
renderable = render_seer(
data=data, rendered_template=SeerAutofixOutOfCreditsTemplate().render(data)
)

assert renderable["text"] == AUTOFIX_OUT_OF_CREDITS_MESSAGE
blocks = renderable["blocks"]
assert len(blocks) == 1
assert isinstance(blocks[0], SectionBlock)
assert blocks[0].text is not None
assert blocks[0].text.text == AUTOFIX_OUT_OF_CREDITS_MESSAGE


class SeerSlackRendererAgentErrorTest(TestCase):
def test_render_agent_error(self) -> None:
data = SeerAgentError(
Expand Down
16 changes: 16 additions & 0 deletions tests/sentry/notifications/platform/templates/test_seer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
from fixtures.seer.webhooks import MOCK_GROUP_ID, MOCK_RUN_ID
from sentry.constants import ENABLE_SEER_CODING_DEFAULT
from sentry.notifications.platform.templates.seer import (
AUTOFIX_OUT_OF_CREDITS_MESSAGE,
SeerAgentError,
SeerAutofixOutOfCredits,
SeerAutofixOutOfCreditsTemplate,
SeerAutofixTrigger,
SeerAutofixUpdate,
SeerInvestigationConclusionTemplate,
Expand Down Expand Up @@ -142,6 +145,19 @@ def test_render_custom_title(self) -> None:
assert rendered.body[0].blocks[0].text == "Timeout."


class SeerAutofixOutOfCreditsTemplateTest(TestCase):
def test_render(self) -> None:
for started in (False, True):
rendered = SeerAutofixOutOfCreditsTemplate().render(
SeerAutofixOutOfCredits(organization_id=1, started=started)
)

assert rendered.subject == AUTOFIX_OUT_OF_CREDITS_MESSAGE
assert len(rendered.body) == 1
assert isinstance(rendered.body[0], ParagraphSection)
assert rendered.body[0].blocks[0].text == AUTOFIX_OUT_OF_CREDITS_MESSAGE


class SeerInvestigationStartedTemplateTest(TestCase):
def test_render(self) -> None:
rendered = SeerInvestigationStartedTemplate().render(
Expand Down
1 change: 1 addition & 0 deletions tests/sentry/notifications/test_apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,5 @@ def test_registers_platform_renderers(self) -> None:
(NotificationProviderKey.SLACK, NotificationSource.SEER_INVESTIGATION_STARTED),
(NotificationProviderKey.SLACK, NotificationSource.SEER_INVESTIGATION_STATUS),
(NotificationProviderKey.SLACK, NotificationSource.SEER_INVESTIGATION_CONCLUSION),
(NotificationProviderKey.SLACK, NotificationSource.SEER_AUTOFIX_OUT_OF_CREDITS),
}
Loading