Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
693fb1c
🚧 initial pass
leeandher Apr 7, 2026
727e078
🚧 unreviewed claude output for thread start, need to review
leeandher Apr 7, 2026
db819cf
feat(slack): Add assistant:write scope for Slack Agent support
leeandher Apr 8, 2026
58aa620
ref(slack): Consolidate duplicate Seer event handlers and halt enums
leeandher Apr 8, 2026
6a28028
test(slack): Update tests for Seer org resolution and halt reason cha…
leeandher Apr 8, 2026
6f4a9d9
fix(slack): Validate user org membership in Seer org resolution
leeandher Apr 10, 2026
08e693a
feat(slack): Require identity linking before Seer processes Slack req…
leeandher Apr 10, 2026
d9bca7a
ref(slack): Tighten Seer handler types and improve identity link UX
leeandher Apr 10, 2026
e72039e
✨ revamp linking prompts
leeandher Apr 10, 2026
86ee72e
✏️ review fixes
leeandher Apr 10, 2026
b7049b3
✏️ more review fixes
leeandher Apr 10, 2026
5211e7d
ref(slack): Fix thread_ts handling and add missing type annotations
leeandher Apr 10, 2026
84a7438
fix(slack): Fix thread_ts return type to satisfy mypy
leeandher Apr 10, 2026
a463758
ref(slack): Clean up Seer event handler tests
leeandher Apr 10, 2026
fa5a3e7
fix(slack): Resolve mypy errors in assistant thread test
leeandher Apr 10, 2026
45456f6
test(slack): Simplify link shared and event test setup
leeandher Apr 10, 2026
2c86e69
ref(slack): Allow thread_ts to be None in identity link prompt
leeandher Apr 10, 2026
ec363ba
fix(slack): Resolve mypy errors in link shared tests
leeandher Apr 10, 2026
1354089
✅ mypy
leeandher Apr 10, 2026
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:
if self.is_assistant_thread_event:
return self.dm_data.get("assistant_thread", {}).get("thread_ts", "")
return self.dm_data.get("thread_ts") or self.dm_data.get("ts", "")

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

@property
def is_assistant_thread_event(self):
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