Skip to content
Draft
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
13 changes: 7 additions & 6 deletions src/sentry/integrations/messaging/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class MessagingInteractionType(StrEnum):
VIEW_SUBMISSION = "VIEW_SUBMISSION"
SEER_AUTOFIX_START = "SEER_AUTOFIX_START"
APP_MENTION = "APP_MENTION"
DIRECT_MESSAGE = "DIRECT_MESSAGE"
ASSISTANT_THREAD_STARTED = "ASSISTANT_THREAD_STARTED"

# Automatic behaviors
PROCESS_SHARED_LINK = "PROCESS_SHARED_LINK"
Expand Down Expand Up @@ -112,11 +114,10 @@ class MessageInteractionFailureReason(StrEnum):
MISSING_ACTION = "missing_action"


class AppMentionHaltReason(StrEnum):
"""Reasons why an app mention event may halt without processing."""
class SeerSlackHaltReason(StrEnum):
"""Reasons why a Seer Slack event (app mention, DM, assistant thread) may halt."""

NO_ORGANIZATION = "no_organization"
ORGANIZATION_NOT_FOUND = "organization_not_found"
ORGANIZATION_NOT_ACTIVE = "organization_not_active"
FEATURE_NOT_ENABLED = "feature_not_enabled"
NO_VALID_INTEGRATION = "no_valid_integration"
NO_VALID_ORGANIZATION = "no_valid_organization"
IDENTITY_NOT_LINKED = "identity_not_linked"
MISSING_EVENT_DATA = "missing_event_data"
59 changes: 59 additions & 0 deletions src/sentry/integrations/slack/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,36 @@ def send_threaded_ephemeral_message(
except SlackApiError as e:
translate_slack_api_error(e)

@staticmethod
def send_threaded_ephemeral_message_static(
*,
integration_id: int,
channel_id: str,
thread_ts: str,
renderable: SlackRenderable,
slack_user_id: str,
) -> None:
"""
In most cases, you should use the instance method instead, so an organization is associated
with the message.

In rare cases where we cannot infer an organization, but need to invoke a Slack API, use this.
For example, when linking a Slack identity to a Sentry user, there could be multiple organizations
attached to the Slack Workspace. We cannot infer which the user may link to.
"""
client = SlackSdkClient(integration_id=integration_id)
try:
client.chat_postEphemeral(
channel=channel_id,
blocks=renderable["blocks"] if len(renderable["blocks"]) > 0 else None,
attachments=renderable.get("attachments"),
text=renderable["text"],
thread_ts=thread_ts,
user=slack_user_id,
)
except SlackApiError as e:
translate_slack_api_error(e)

def update_message(
self,
*,
Expand Down Expand Up @@ -286,6 +316,34 @@ def set_thread_status(
extra={"channel_id": channel_id, "thread_ts": thread_ts},
)

def set_suggested_prompts(
self,
*,
channel_id: str,
thread_ts: str,
prompts: Sequence[dict[str, str]],
title: str = "",
) -> None:
"""
Set suggested prompts in a Slack assistant thread.

Each prompt is a dict with ``title`` (display label) and ``message``
(the text sent when clicked). Slack allows a maximum of 4 prompts.
"""
client = self.get_client()
try:
client.assistant_threads_setSuggestedPrompts(
channel_id=channel_id,
thread_ts=thread_ts,
title=title,
prompts=list(prompts),
)
except SlackApiError:
_logger.warning(
"slack.set_suggested_prompts.error",
extra={"channel_id": channel_id, "thread_ts": thread_ts},
)


class SlackIntegrationProvider(IntegrationProvider):
key = IntegrationProviderSlug.SLACK.value
Expand Down Expand Up @@ -319,6 +377,7 @@ class SlackIntegrationProvider(IntegrationProvider):
SlackScope.CHANNELS_HISTORY,
SlackScope.GROUPS_HISTORY,
SlackScope.APP_MENTIONS_READ,
SlackScope.ASSISTANT_WRITE,
]
)
user_scopes = frozenset(
Expand Down
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 @@ -20,6 +20,8 @@ class SlackAction(StrEnum):
RESOLVE_DIALOG = "resolve_dialog"
ARCHIVE_DIALOG = "archive_dialog"
ASSIGN = "assign"
# Older, /sentry link workflows send a hyperlink. Newer ones use a button block.
LINK_IDENTITY = "link_identity"
SEER_AUTOFIX_START = "seer_autofix_start"
SEER_AUTOFIX_VIEW_IN_SENTRY = "seer_autofix_view_in_sentry"
SEER_AUTOFIX_VIEW_PR = "seer_autofix_view_pr"
Expand Down
19 changes: 19 additions & 0 deletions src/sentry/integrations/slack/requests/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from sentry.integrations.slack.requests.base import SlackDMRequest, SlackRequestError
from sentry.integrations.slack.unfurl.handlers import match_link
from sentry.integrations.slack.unfurl.types import LinkType
from sentry.integrations.slack.utils.constants import SlackScope

COMMANDS = ["link", "unlink", "link team", "unlink team"]

Expand Down Expand Up @@ -60,12 +61,30 @@ def dm_data(self) -> Mapping[str, Any]:

@property
def channel_id(self) -> str:
if self.is_assistant_thread_event:
return self.dm_data.get("assistant_thread", {}).get("channel_id", "")
return self.dm_data.get("channel", "")

@property
def user_id(self) -> str:
if self.is_assistant_thread_event:
return self.dm_data.get("assistant_thread", {}).get("user_id", "")
return self.dm_data.get("user", "")

@property
def thread_ts(self) -> str | None:
if self.is_assistant_thread_event:
return self.dm_data.get("assistant_thread", {}).get("thread_ts", "")
return self.dm_data.get("thread_ts", "")

@property
def has_assistant_scope(self) -> bool:
return SlackScope.ASSISTANT_WRITE in self.integration.metadata.get("scopes", [])

@property
def is_assistant_thread_event(self) -> bool:
return self.dm_data.get("type") == "assistant_thread_started"

@property
def links(self) -> list[str]:
return [link["url"] for link in self.dm_data.get("links", []) if "url" in link]
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/integrations/slack/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ class SlackScope(StrEnum):
"""Allows the bot to read message history in private groups."""
APP_MENTIONS_READ = "app_mentions:read"
"""Allows the bot to read mentions in app messages."""
ASSISTANT_WRITE = "assistant:write"
"""Allows the bot to act as a Slack Agent."""
1 change: 1 addition & 0 deletions src/sentry/integrations/slack/webhooks/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,7 @@ def post(self, request: Request) -> Response:
if action_id in {
SlackAction.SEER_AUTOFIX_VIEW_IN_SENTRY.value,
SlackAction.SEER_AUTOFIX_VIEW_PR.value,
SlackAction.LINK_IDENTITY.value,
}:
return self.respond()

Expand Down
Loading
Loading