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
1 change: 1 addition & 0 deletions src/sentry/conf/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,7 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str:
"sentry.seer.code_review.webhooks.task",
"sentry.seer.entrypoints.operator",
"sentry.seer.entrypoints.slack.messaging",
"sentry.seer.entrypoints.slack.tasks",
"sentry.snuba.tasks",
"sentry.tasks.activity",
"sentry.tasks.assemble",
Expand Down
61 changes: 61 additions & 0 deletions src/sentry/integrations/slack/webhooks/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,64 @@ def on_link_shared(self, request: Request, slack_request: SlackDMRequest) -> boo

return True

def on_app_mention(self, slack_request: SlackDMRequest) -> Response:
"""Handle @mention events for Seer Explorer."""
data = slack_request.data.get("event", {})

ois = integration_service.get_organization_integrations(
integration_id=slack_request.integration.id, limit=1
)
if not ois:
_logger.info(
"on_app_mention.no-organization",
extra={"integration_id": slack_request.integration.id},
)
return self.respond()

organization_id = ois[0].organization_id
organization_context = organization_service.get_organization_by_id(
id=organization_id,
user_id=None,
include_projects=False,
include_teams=False,
)
if not organization_context:
_logger.info(
"on_app_mention.organization-not-found",
extra={
"integration_id": slack_request.integration.id,
"organization_id": organization_id,
},
)
return self.respond()

if not features.has("organizations:seer-slack-explorer", organization_context.organization):
return self.respond()

channel_id = slack_request.channel_id
text = data.get("text", "")
thread_ts = data.get("thread_ts")
message_ts = data.get("ts", "")

if not channel_id or not text:
return self.respond()

from sentry.seer.entrypoints.slack.tasks import process_mention_for_slack

process_mention_for_slack.apply_async(
kwargs={
"integration_id": slack_request.integration.id,
"organization_id": organization_id,
"channel_id": channel_id,
"thread_ts": thread_ts,
"message_ts": message_ts,
"text": text,
"slack_user_id": slack_request.user_id,
}
)

return self.respond()

# TODO(dcramer): implement app_uninstalled and tokens_revoked
def post(self, request: Request) -> Response:
try:
Expand All @@ -318,6 +376,9 @@ def post(self, request: Request) -> Response:
if self.on_link_shared(request, slack_request):
return self.respond()

if slack_request.type == "app_mention":
return self.on_app_mention(slack_request)

if slack_request.type == "message":
if slack_request.is_bot():
return self.respond()
Expand Down
46 changes: 46 additions & 0 deletions src/sentry/seer/entrypoints/slack/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from __future__ import annotations

import logging

from sentry.tasks.base import instrumented_task
from sentry.taskworker.namespaces import integrations_tasks
from sentry.taskworker.retry import Retry

logger = logging.getLogger(__name__)


@instrumented_task(
name="sentry.seer.entrypoints.slack.process_mention_for_slack",
namespace=integrations_tasks,
processing_deadline_duration=300,
retry=Retry(times=2, delay=30),
)
def process_mention_for_slack(
*,
integration_id: int,
organization_id: int,
channel_id: str,
thread_ts: str | None,
message_ts: str,
text: str,
slack_user_id: str,
) -> None:
"""
Process a Slack @mention for Seer Explorer.

Parses the mention, extracts issue links and thread context,
and triggers an Explorer run.

TODO(ISWF-2023): Implement full processing logic.
"""
logger.info(
"seer.explorer.slack.process_mention",
extra={
"integration_id": integration_id,
"organization_id": organization_id,
"channel_id": channel_id,
"thread_ts": thread_ts,
"message_ts": message_ts,
"slack_user_id": slack_user_id,
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from unittest.mock import patch

from . import BaseEventTest

APP_MENTION_EVENT = {
"type": "app_mention",
"channel": "C1234567890",
"user": "U1234567890",
"text": "<@U0BOT> What is causing this issue? https://testserver/organizations/test-org/issues/123/",
"ts": "1234567890.123456",
"event_ts": "1234567890.123456",
}

THREADED_APP_MENTION_EVENT = {
**APP_MENTION_EVENT,
"thread_ts": "1234567890.000001",
}


class AppMentionEventTest(BaseEventTest):
@patch("sentry.seer.entrypoints.slack.tasks.process_mention_for_slack.apply_async")
def test_app_mention_dispatches_task(self, mock_apply_async):
with self.feature("organizations:seer-slack-explorer"):
resp = self.post_webhook(event_data=APP_MENTION_EVENT)

assert resp.status_code == 200
mock_apply_async.assert_called_once()
kwargs = mock_apply_async.call_args[1]["kwargs"]
assert kwargs["integration_id"] == self.integration.id
assert kwargs["organization_id"] == self.organization.id
assert kwargs["channel_id"] == "C1234567890"
assert kwargs["thread_ts"] is None
assert kwargs["message_ts"] == "1234567890.123456"
assert kwargs["text"] == APP_MENTION_EVENT["text"]
assert kwargs["slack_user_id"] == "U1234567890"

@patch("sentry.seer.entrypoints.slack.tasks.process_mention_for_slack.apply_async")
def test_app_mention_threaded(self, mock_apply_async):
with self.feature("organizations:seer-slack-explorer"):
resp = self.post_webhook(event_data=THREADED_APP_MENTION_EVENT)

assert resp.status_code == 200
mock_apply_async.assert_called_once()
kwargs = mock_apply_async.call_args[1]["kwargs"]
assert kwargs["thread_ts"] == "1234567890.000001"

@patch("sentry.seer.entrypoints.slack.tasks.process_mention_for_slack.apply_async")
def test_app_mention_feature_flag_disabled(self, mock_apply_async):
resp = self.post_webhook(event_data=APP_MENTION_EVENT)

assert resp.status_code == 200
mock_apply_async.assert_not_called()

@patch("sentry.seer.entrypoints.slack.tasks.process_mention_for_slack.apply_async")
def test_app_mention_empty_text(self, mock_apply_async):
event_data = {**APP_MENTION_EVENT, "text": ""}
with self.feature("organizations:seer-slack-explorer"):
resp = self.post_webhook(event_data=event_data)

assert resp.status_code == 200
mock_apply_async.assert_not_called()

@patch("sentry.seer.entrypoints.slack.tasks.process_mention_for_slack.apply_async")
def test_app_mention_no_organization(self, mock_apply_async):
"""When the integration has no org integrations, we should not dispatch."""
with patch(
"sentry.integrations.slack.webhooks.event.integration_service.get_organization_integrations",
return_value=[],
):
with self.feature("organizations:seer-slack-explorer"):
resp = self.post_webhook(event_data=APP_MENTION_EVENT)

assert resp.status_code == 200
mock_apply_async.assert_not_called()
Loading