feat(slack): Implement process_mention_for_slack task for Explorer#109733
Conversation
881c751 to
c96afad
Compare
9727f67 to
5896d0f
Compare
c96afad to
740d420
Compare
5896d0f to
c97eaa9
Compare
| try: | ||
| organization = Organization.objects.get_from_cache(id=organization_id) | ||
| except Organization.DoesNotExist: | ||
| lifecycle.record_halt(halt_reason="org_not_found") |
There was a problem hiding this comment.
We typically try to group these in an enum to contain all of the possible halt reasons. It's probably a good idea to do that here as it makes test assertions easier as well.
There was a problem hiding this comment.
Done:
sentry/src/sentry/seer/entrypoints/slack/metrics.py
Lines 6 to 9 in b4b9f72
| slack_user_id=slack_user_id, | ||
| ) | ||
| except ValueError: | ||
| lifecycle.record_halt(halt_reason="integration_not_found") |
There was a problem hiding this comment.
Should we be raising a more explicit exception from the Entrypoint code? ValueError on the surface doesn't necessarily imply to me that the integration is missing, so this grouping seems funny.
There was a problem hiding this comment.
sentry/src/sentry/seer/entrypoints/slack/entrypoint.py
Lines 358 to 359 in d381d13
| entrypoint.install.add_reaction( | ||
| channel_id=channel_id, | ||
| message_ts=message_ts, | ||
| emoji="thinking_face", | ||
| ) |
There was a problem hiding this comment.
If this is being processed in region (which it appears to be), then this won't be immediate. If you want to keep latency on this reaction to a minimum, you likely need to move this reaction logic to Control to occur before the processing here.
| operator = SeerOperator(entrypoint=entrypoint) | ||
| operator.trigger_explorer( | ||
| organization=organization, | ||
| user=None, |
There was a problem hiding this comment.
Is user here a sentry user? If so, do we have plans to fill this in later?
There was a problem hiding this comment.
Hm, good point, it seems like Seer explorer may use user's ID for context building. It also seems like we are working towards making the user info required.
sentry/src/sentry/seer/signed_seer_api.py
Lines 16 to 23 in 4f15cec
| self._run_task(organization_id=999999999) | ||
|
|
||
| mock_has_access.assert_not_called() | ||
| mock_from_mention.assert_not_called() |
There was a problem hiding this comment.
You may want to assert on halts here to ensure the code you're testing is actually being invoked. It also ensures we have the appropriate metrics logged when this occurs. See our testing helpers here:
sentry/src/sentry/testutils/asserts.py
Line 94 in 94485a3
Backend Test FailuresFailures on
|
Backend Test FailuresFailures on
|
Backend Test FailuresFailures on
|
Backend Test FailuresFailures on
|
Backend Test FailuresFailures on
|
0236280 to
9840cc4
Compare
5138896 to
5cc7997
Compare
The custom message parameter is no longer needed since the link identity prompt now uses SlackRenderable directly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend Test FailuresFailures on
|
Move the ephemeral link-identity prompt to fire before clearing the thread status indicator. Remove the thread_ts parameter from _send_link_identity_prompt so the prompt is sent as a top-level ephemeral message rather than threaded. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
leeandher
left a comment
There was a problem hiding this comment.
minor comments about lifecycles and comments, looks good!
| slack_user_id=slack_user_id, | ||
| ) | ||
| if not user: | ||
| lifecycle.record_failure(failure_reason=ProcessMentionFailureReason.IDENTITY_NOT_LINKED) |
There was a problem hiding this comment.
This could probably be a halt, since it's expected behavior. We don't want to trigger sentry errors if users haven't linked their accounts yet for example.
There was a problem hiding this comment.
Thanks! Changed:
… context Instead of relying solely on the plain `text` field, thread context parsing now prefers Block Kit `blocks` and extracts links with URLs, user mentions, channel refs, emojis, and broadcasts. Falls back to `text` when blocks are absent or empty.
…mention context Pass ts and thread_ts separately from the webhook handler so the task knows whether the mention was top-level or in a thread. The ephemeral identity-linking prompt now appears in the thread when mentioned in a thread, or at the channel level for top-level mentions.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Unlinked identity uses
record_failurecreating noisy Sentry issues- Changed record_failure to record_halt for unlinked identity case, which defaults to create_issue=False and prevents noisy Sentry alerts for expected behavior.
Or push these changes by commenting:
@cursor push 1f17feeb59
Preview (1f17feeb59)
diff --git a/src/sentry/seer/entrypoints/slack/tasks.py b/src/sentry/seer/entrypoints/slack/tasks.py
--- a/src/sentry/seer/entrypoints/slack/tasks.py
+++ b/src/sentry/seer/entrypoints/slack/tasks.py
@@ -99,7 +99,7 @@
slack_user_id=slack_user_id,
)
if not user:
- lifecycle.record_failure(failure_reason=ProcessMentionFailureReason.IDENTITY_NOT_LINKED)
+ lifecycle.record_halt(halt_reason=ProcessMentionFailureReason.IDENTITY_NOT_LINKED)
# In a thread, show the prompt in the thread; top-level, show in the channel.
_send_link_identity_prompt(
entrypoint=entrypoint,This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
…identity not linked
The test was incorrectly using assert_failure_metric with ProcessMentionFailureReason.IDENTITY_NOT_LINKED, but the production code calls lifecycle.record_halt() with ProcessMentionHaltReason.
…-processing-slack-explorer-mentions
…n-cleanup - Use SlackIntegration for org lookup in app_mention handler - Add Sentry Voice loading messages for thinking state - Set completion summary to None instead of error text on fetch failure - Pass loading_messages through set_thread_status
…-processing-slack-explorer-mentions
…-processing-slack-explorer-mentions
Backend Test FailuresFailures on
|
Remove test_init_defaults_thread_ts_to_message_ts_when_none since SlackExplorerEntrypoint does not use message_ts — the thread_ts fallback is handled at the call site in tasks.py instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The process_mention_for_slack task resolved a Sentry user from a Slack identity but never verified the user is a member of the target organization. This allowed a user who linked their Sentry identity to a Slack workspace to continue using Seer Explorer for an organization after losing membership. Add an organization.has_access(user) check after user resolution, consistent with other Slack action handlers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
The USER_NOT_ORG_MEMBER early return was missing the set_thread_status(status="") call to clear the "Thinking..." indicator, leaving it stuck for up to 2 minutes. Match the IDENTITY_NOT_LINKED branch which already clears it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show an ephemeral message including the org name so the user knows which Sentry organization they need to join. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add isinstance check before calling .get() on rich_text container elements parsed from Slack API data. This prevents AttributeError if the API returns malformed data with non-dict values, consistent with the defensive pattern already used in the context block handler. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| _extract_rich_text_element_text(el) for el in container.get("elements", []) | ||
| ] | ||
| parts.append("".join(container_parts)) |
There was a problem hiding this comment.
Bug: The _extract_block_text function lacks an isinstance check for inner rich_text elements, risking an AttributeError if the Slack API returns malformed data.
Severity: MEDIUM
Suggested Fix
Add an isinstance(el, dict) check inside the list comprehension that processes the inner elements of a rich_text container. This will filter out any non-dictionary elements, preventing the AttributeError and aligning the code with the defensive pattern used elsewhere in the file.
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/seer/entrypoints/slack/mention.py#L81-L83
Potential issue: In the `_extract_block_text` function, when processing a `rich_text`
block, the code iterates through nested elements. While it checks if the outer
`container` is a dictionary, it fails to perform the same check for the inner elements
(`el`) before passing them to `_extract_rich_text_element_text`. This helper function
immediately calls `.get()` on its argument. If the Slack API returns a malformed payload
where an inner element is not a dictionary (e.g., `None` or a string), the call will
raise an `AttributeError`. This would crash the task and prevent Explorer runs from
being triggered.
There was a problem hiding this comment.
this assumes that slack's api returns a malformed response. blockkits are pretty well-defined and i don't think we should assume it'll fail
| if not identity: | ||
| return None | ||
|
|
||
| return user_service.get_user(identity.user_id) |
There was a problem hiding this comment.
Future thought: Depending on the scale of requests we get, we may want to combine this with the identity RPC call to reduce latency here.


Summary
process_mention_for_slacktask body (replacing the previous TODO stub): fetch org, check explorer access, constructSlackExplorerEntrypoint, resolve the Sentry user from Slack identity, extract the user prompt, build thread context, and trigger an Explorer run viaSeerExplorerOperator_resolve_user()helper that maps a Slack user ID to anRpcUserthrough the linked identity provider,_send_link_identity_prompt()that sends an ephemeral Slack message prompting the user to link their identity when no link exists, and_send_not_org_member_message()that sends an ephemeral error when the user is not a member of the organizationbot_user_idfrom the Slackauthorizationspayload inon_app_mentionso the task can strip the bot mention from the user's prompt viaextract_prompt()thread_tsresolution inon_app_mentionto correctly distinguishts(message timestamp) fromthread_ts(parent thread timestamp, None for top-level messages), and pass both to the taskreturn self.respond()inside the lifecycle context manager so halts/failures are properly recordedSlackExplorerEntrypoint: removemessage_tsparameter (caller now resolvesthread_ts), makethread_tsrequired, raiseEntrypointSetupErrorinstead ofValueError, and storeself.integrationfor use by identity-linking helpers_extract_block_text,_extract_text_from_blocks,_extract_rich_text_element_text) for building thread context from rich_text, section, context, header, and markdown blocks — with defensiveisinstancechecks for external Slack API databuild_thread_contextto prefer block-based text extraction over plaintextfallback, preserving URLs and mentions from rich text blocksSlackActionRequest.get_action_listto use.get("value", "")instead of["value"]to avoidKeyErroron actions without a value fieldMISSING_CHANNEL_OR_TEXTtoMISSING_EVENT_DATAinAppMentionHaltReasonto reflect broader validationSlackEntrypointInteractionType.PROCESS_MENTIONandProcessMentionHaltReason/ProcessMentionFailureReasonenums for structured observabilitySeerExplorerOperator.executeto passsummary=Noneinstead of a hardcoded fallback string when no assistant content is found in Explorer resultsloading_messagesparameter toSlackIntegration.set_thread_statusfor rotating status messagestest_tasks.py) covering happy path, org-not-found, no-access, integration-not-found, identity-not-linked, user-not-org-member, and thread-context scenarios; add Block Kit extraction tests; update webhook and entrypoint tests accordinglyRefs ISWF-2023