Skip to content
Open
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", "")
Comment on lines +74 to +78
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.

Bug: The thread_ts property returns an empty string "" for top-level messages instead of None, contradicting its type hint and causing silent failures.
Severity: HIGH

Suggested Fix

Modify the thread_ts property to return None instead of an empty string "" when a thread_ts is not found in self.dm_data. This aligns the implementation with the type hint str | None and the documented intent. Specifically, change return self.dm_data.get("thread_ts", "") to return self.dm_data.get("thread_ts").

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: src/sentry/integrations/slack/requests/event.py#L74-L78

Potential issue: The `SlackEventRequest.thread_ts` property is annotated to return `str
| None`, but its implementation returns an empty string `""` for top-level
(non-threaded) messages. This contradicts the commit's intent and test expectations.
When an unlinked user sends a top-level `@mention`, the empty `thread_ts` is passed to
the Slack API, which fails. The resulting exception is silently caught, preventing the
user from receiving the crucial identity link prompt and breaking the user onboarding
flow for the Seer Explorer feature.

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.

thread_ts property returns empty string instead of None

Medium Severity

The thread_ts property defaults to "" (empty string) when the key is absent from the event data, but the old code used data.get("thread_ts") which returned None for non-threaded messages. The downstream process_mention_for_slack task parameter is typed str | None = None and documents that thread_ts is "None for top-level messages." Passing "" instead of None changes the semantics and breaks test assertions (assert kwargs["thread_ts"] is None) in both test_app_mention_non_threaded_dispatches_task and test_dm_dispatches_task. The non-assistant branch of thread_ts needs to return None when the key is missing rather than "".

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 84a7438. Configure here.


@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