Skip to content

feat(seer-slack): Check channel type for correct history scope before API call#112371

Merged
alexsohn1126 merged 38 commits intomasterfrom
alexsohn/iswf-2353-ensure-history-scopes-before-api-call
Apr 14, 2026
Merged

feat(seer-slack): Check channel type for correct history scope before API call#112371
alexsohn1126 merged 38 commits intomasterfrom
alexsohn/iswf-2353-ensure-history-scopes-before-api-call

Conversation

@alexsohn1126
Copy link
Copy Markdown
Member

@alexsohn1126 alexsohn1126 commented Apr 7, 2026

Motivation

Right now, we require every Sentry Slack app integration to require every scope that we request. However, with Seer Explorer requiring channels:history and groups:history (public and private channels history read scopes), some organization may not want to allow us to have access to those kinds of data.

Therefore we must check whether the bot mention happened in a public or a private channel, and whether we have the appropriate scopes to read the history from those channels.

Details

Use Slack's conversations.info API to determine channel type (public vs private) and check the appropriate OAuth scope (channels:history vs groups:history) before calling conversations.replies. Previously we only checked channels:history regardless of channel type, which meant private channel threads were always blocked.

Also adds a missing-scope footer to the first Seer Explorer response in a thread when the integration lacks the required history scope. Subsequent responses in the same thread do not repeat the warning.

If an installation does not have the correct permissions and is not able to fetch the whole thread context and pass it onto Seer, this footer will appear on the first response:

image

If the installation has the correct scopes and is able to grab the thread context, it won't show the footer:

image

Refs ISWF-2353

@linear-code
Copy link
Copy Markdown

linear-code bot commented Apr 7, 2026

@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Apr 7, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

Backend Test Failures

Failures on 9361c13 in this run:

tests/sentry/integrations/slack/test_integration.py::SlackIntegrationNotificationPlatformTest::test_get_thread_history_dm_always_allowedlog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/integrations/slack/test_integration.py:682: in test_get_thread_history_dm_always_allowed
    assert len(result) == 1
E   assert 0 == 1
E    +  where 0 = len([])
tests/sentry/integrations/slack/test_integration.py::SlackIntegrationNotificationPlatformTest::test_get_thread_history_private_channel_with_groups_historylog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/integrations/slack/test_integration.py:667: in test_get_thread_history_private_channel_with_groups_history
    assert len(result) == 1
E   assert 0 == 1
E    +  where 0 = len([])
tests/sentry/integrations/slack/test_integration.py::SlackIntegrationNotificationPlatformTest::test_get_thread_history_successlog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/integrations/slack/test_integration.py:639: in test_get_thread_history_success
    assert len(result) == 3
E   assert 0 == 3
E    +  where 0 = len([])
tests/sentry/integrations/slack/test_integration.py::SlackIntegrationNotificationPlatformTest::test_has_history_scope_dm_always_truelog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/integrations/slack/test_integration.py:694: in test_has_history_scope_dm_always_true
    assert self.installation.has_history_scope("D1234567890") is True
E   AssertionError: assert False is True
E    +  where False = <bound method SlackIntegration.has_history_scope of <sentry.integrations.slack.integration.SlackIntegration object at 0x7f952c2757f0>>('D1234567890')
E    +    where <bound method SlackIntegration.has_history_scope of <sentry.integrations.slack.integration.SlackIntegration object at 0x7f952c2757f0>> = <sentry.integrations.slack.integration.SlackIntegration object at 0x7f952c2757f0>.has_history_scope
E    +      where <sentry.integrations.slack.integration.SlackIntegration object at 0x7f952c2757f0> = <sentry.testutils.silo.SlackIntegrationNotificationPlatformTest testMethod=test_has_history_scope_dm_always_true>.installation
tests/sentry/integrations/slack/test_integration.py::SlackIntegrationNotificationPlatformTest::test_has_history_scope_public_channellog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/integrations/slack/test_integration.py:687: in test_has_history_scope_public_channel
    assert self.installation.has_history_scope("C1234567890") is True
E   AssertionError: assert False is True
E    +  where False = <bound method SlackIntegration.has_history_scope of <sentry.integrations.slack.integration.SlackIntegration object at 0x7f952c0d3e30>>('C1234567890')
E    +    where <bound method SlackIntegration.has_history_scope of <sentry.integrations.slack.integration.SlackIntegration object at 0x7f952c0d3e30>> = <sentry.integrations.slack.integration.SlackIntegration object at 0x7f952c0d3e30>.has_history_scope
E    +      where <sentry.integrations.slack.integration.SlackIntegration object at 0x7f952c0d3e30> = <sentry.testutils.silo.SlackIntegrationNotificationPlatformTest testMethod=test_has_history_scope_public_channel>.installation
tests/sentry/seer/entrypoints/slack/test_slack.py::SlackExplorerEntrypointTest::test_send_thread_update_explorer_response_missing_scope_has_footerlog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/entrypoints/slack/test_slack.py:659: in test_send_thread_update_explorer_response_missing_scope_has_footer
    send_thread_update(install=install, thread=thread, data=data)
src/sentry/seer/entrypoints/slack/messaging.py:70: in send_thread_update
    if isinstance(data, SeerExplorerResponse) and not install.has_history_scope(
src/sentry/integrations/slack/integration.py:231: in has_history_scope
    conversation_data = self.get_conversations_info(channel_id=channel_id)
src/sentry/integrations/slack/integration.py:260: in get_conversations_info
    client = self.get_client()
src/sentry/integrations/slack/integration.py:90: in get_client
    return SlackSdkClient(integration_id=self.model.id)
src/sentry/integrations/slack/sdk_client.py:119: in __init__
    raise ValueError(f"Missing token for integration with id {integration_id}")
E   ValueError: Missing token for integration with id 111
tests/sentry/seer/entrypoints/slack/test_slack.py::SlackExplorerEntrypointTest::test_send_thread_update_explorer_response_with_scope_no_footerlog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/entrypoints/slack/test_slack.py:684: in test_send_thread_update_explorer_response_with_scope_no_footer
    send_thread_update(install=install, thread=thread, data=data)
src/sentry/seer/entrypoints/slack/messaging.py:70: in send_thread_update
    if isinstance(data, SeerExplorerResponse) and not install.has_history_scope(
src/sentry/integrations/slack/integration.py:231: in has_history_scope
    conversation_data = self.get_conversations_info(channel_id=channel_id)
src/sentry/integrations/slack/integration.py:260: in get_conversations_info
    client = self.get_client()
src/sentry/integrations/slack/integration.py:90: in get_client
    return SlackSdkClient(integration_id=self.model.id)
src/sentry/integrations/slack/sdk_client.py:119: in __init__
    raise ValueError(f"Missing token for integration with id {integration_id}")
E   ValueError: Missing token for integration with id 117

Comment on lines +227 to +228
if all(s in installed_scope_set for s in history_scopes):
return True
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have both channels:history and groups:history, we are covered for both cases whether the mention is in a private or a public channel.

@alexsohn1126 alexsohn1126 marked this pull request as ready for review April 8, 2026 16:02
@alexsohn1126 alexsohn1126 requested review from a team as code owners April 8, 2026 16:02
@alexsohn1126 alexsohn1126 requested a review from leeandher April 8, 2026 16:02
Comment thread src/sentry/integrations/slack/integration.py
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: Missing early return causes unnecessary API call
    • Added early return when neither history scope is present to avoid unnecessary Slack API call before checking channel type.

Create PR

Or push these changes by commenting:

@cursor push 0cbe4af674
Preview (0cbe4af674)
diff --git a/src/sentry/integrations/slack/integration.py b/src/sentry/integrations/slack/integration.py
--- a/src/sentry/integrations/slack/integration.py
+++ b/src/sentry/integrations/slack/integration.py
@@ -227,6 +227,9 @@
         if all(s in installed_scope_set for s in history_scopes):
             return True
 
+        if not any(s in installed_scope_set for s in history_scopes):
+            return False
+
         conversation_data = self.get_conversations_info(channel_id=channel_id)
 
         channel_info = conversation_data.get("channel", {})

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Comment thread src/sentry/integrations/slack/integration.py
leeandher and others added 2 commits April 8, 2026 13:30
Add the assistant:write scope to the Slack integration to enable
the bot to act as a Slack Agent, supporting DM-based agent interfaces.

Refs ISWF-2388
Co-Authored-By: Claude Opus 4.6 <noreply@example.com>
Extract shared org-resolution logic into _resolve_seer_organization helper
and merge on_app_mention/on_dm into a single _handle_seer_mention method.
Replace three identical halt reason enums with unified SeerSlackHaltReason.
Extract duplicated loading messages list into a module-level constant.

Refs ISWF-2388
Co-Authored-By: Claude Opus 4.6 <noreply@example.com>
@alexsohn1126 alexsohn1126 marked this pull request as draft April 8, 2026 18:41
leeandher and others added 7 commits April 8, 2026 17:38
…nges

Update tests to match the refactored _resolve_seer_organization which
now iterates org integrations and uses SlackExplorerEntrypoint.has_access
instead of checking a single feature flag. Align halt reasons with the
consolidated enum values (NO_VALID_INTEGRATION, NO_VALID_ORGANIZATION)
and update the has_access test for the seer-slack-explorer flag rename.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…scope footer

Check the correct OAuth scope (channels:history vs groups:history)
based on channel type before calling conversations.replies, and append
a reinstall footer to Seer Explorer responses when the integration
lacks the required history scope.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ions_info

Remove debug _logger.info/warning calls from has_history_scope that
were left from local testing. Update all tests that go through
has_history_scope to mock the conversations_info API call, and remove
DM tests with incorrect expectations.
…onse

Use cache.add() to atomically track whether the missing-scope footer
has already been shown in a given thread. Subsequent Seer Explorer
responses in the same thread no longer repeat the warning.
@alexsohn1126 alexsohn1126 force-pushed the alexsohn/iswf-2353-ensure-history-scopes-before-api-call branch from 6f6d707 to 78f49b6 Compare April 9, 2026 15:50
@alexsohn1126 alexsohn1126 changed the base branch from master to leanderrodrigues/iswf-2388-explore-slack-dms-agent-interface-for-seer-explorer April 9, 2026 15:51
Comment thread src/sentry/integrations/slack/integration.py Outdated
Comment thread src/sentry/seer/entrypoints/slack/entrypoint.py
…helper

If Organization.objects.get raised DoesNotExist, the cache key was already
consumed by cache.add, preventing the footer on any future call for the
thread. More critically, the unhandled exception propagated into
on_explorer_update and prevented schedule_all_thread_updates from
executing, blocking delivery of the Seer Explorer response entirely.

Move the org lookup before cache.add so the key is never consumed on
failure, and wrap it in try/except to return None gracefully — consistent
with the integration lookup above.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/sentry/integrations/slack/integration.py
Comment thread src/sentry/integrations/slack/integration.py Outdated
Copy link
Copy Markdown
Member

@leeandher leeandher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Some small suggestions but logically sound 👏

Comment on lines +272 to +273
f"_Thread context is unavailable because optional scopes are disabled. "
f"<{settings_url}|Reinstall Sentry's Slack app> to enable this feature._"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be a bit more personal and less technical IMO, what do you think of:

I'm working off mentions only, I can't read the whole thread. <{settings_url}|Reinstall me> to change that.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, thanks!

bf83740

"""
Returns whether this integration is allowed to access the history in the channel.
"""
history_scopes = [SlackScope.CHANNELS_HISTORY, SlackScope.GROUPS_HISTORY]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the bots got a point, I think you need im:history here, and it will make that not any(... check redundant since all installs have im:history

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh it's handled below, then maybe just slot this all under the is_im check 👍

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bots were right, even if someone doesn't have neither SlackScope.CHANNELS_HISTORY and SlackScope.GROUPS_HISTORY, they should still be able to DM the bot.

Removed the early exit if neither of those history scopes are available

eb603bd

Comment on lines +268 to +296
history_scopes = [SlackScope.CHANNELS_HISTORY, SlackScope.GROUPS_HISTORY]
installed_scope_set = frozenset(self.model.metadata.get("scopes", []))

installed_history_scopes = [s in installed_scope_set for s in history_scopes]

if all(installed_history_scopes):
return True

if not any(installed_history_scopes):
return False

conversation_data = self.get_conversations_info(channel_id=channel_id)

channel_info = conversation_data.get("channel", {})
is_channel = channel_info.get("is_channel", False)
is_private = channel_info.get("is_private", False)
is_im = channel_info.get("is_im", False)

# DMs and assistant threads: the bot is a participant and can
# always read its own conversation history.
if is_im:
return True

if is_channel and is_private:
return SlackScope.GROUPS_HISTORY in installed_scope_set
if is_channel:
return SlackScope.CHANNELS_HISTORY in installed_scope_set

# Shouldn't reach here unless channel_info is empty (most likely
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's worthwhile to add a logger.warning or something similar just in case! we might catch users trying multi person DMs, and missing the mpim:history scope for example

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a logger warning:

f9d824d


def has_history_scope(self, channel_id: str) -> bool:
"""
Returns whether this integration is allowed to access the history in the channel.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth adding the detail here that the check is different depending on the type of channel. And that type of channel will be determined by an API call in the function.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, added more detail in:

48ab446

if not any(installed_history_scopes):
return False

conversation_data = self.get_conversations_info(channel_id=channel_id)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd try/catch this in case this ever gets used somewhere else where users control what channel_id we provide. If that's the case it's possible the channel doesn't exist 🤷

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It already is try/catched in get_conversations_info method:

def get_conversations_info(
self,
*,
channel_id: str,
) -> dict:
"""
Fetch conversations info from Slack API.
"""
client = self.get_client()
try:
conversations = client.conversations_info(
channel=channel_id,
)
assert isinstance(conversations.data, dict)
return conversations.data
except (SlackApiError, AssertionError) as e:
_logger.warning(
"slack.get_conversations_info.error",
extra={"channel_id": channel_id, "error": str(e)},
)
return {}

Copy link
Copy Markdown
Member

@leeandher leeandher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

meant to approve, small fixes shouldn't need re-review

…tory_scope

Log a warning when has_history_scope falls through to the default
return without matching any known channel type, making it easier to
diagnose unexpected conversation types from the Slack API.
Clarify which scope applies to which channel type and that
conversations.info is used to determine the channel type.
Comment thread src/sentry/seer/entrypoints/slack/entrypoint.py
@github-actions
Copy link
Copy Markdown
Contributor

Backend Test Failures

Failures on 3cb325d in this run:

tests/sentry/notifications/platform/slack/renderers/test_seer.py::SeerSlackRendererExplorerTest::test_render_explorer_response_with_missing_scope_footerlog
[gw0] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/notifications/platform/slack/renderers/test_seer.py:291: in test_render_explorer_response_with_missing_scope_footer
    assert "Thread context is unavailable" in footer_text
E   assert 'Thread context is unavailable' in "_I am only able to see the message with the mention. I can't read the whole thread. <https://sentry.io/settings/test/integrations/slack/|Reinstall me> to change that._"
tests/sentry/seer/entrypoints/slack/test_slack.py::SlackExplorerEntrypointTest::test_on_explorer_updatelog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/entrypoints/slack/test_slack.py:591: in test_on_explorer_update
    SlackExplorerEntrypoint.on_explorer_update(
src/sentry/seer/entrypoints/slack/entrypoint.py:463: in on_explorer_update
    missing_scope_url = _get_missing_scope_settings_url(
src/sentry/seer/entrypoints/slack/entrypoint.py:98: in _get_missing_scope_settings_url
    if install.has_history_scope(channel_id):
src/sentry/integrations/slack/integration.py:281: in has_history_scope
    conversation_data = self.get_conversations_info(channel_id=channel_id)
src/sentry/integrations/slack/integration.py:313: in get_conversations_info
    client = self.get_client()
src/sentry/integrations/slack/integration.py:91: in get_client
    return SlackSdkClient(integration_id=self.model.id)
src/sentry/integrations/slack/sdk_client.py:119: in __init__
    raise ValueError(f"Missing token for integration with id {integration_id}")
E   ValueError: Missing token for integration with id 58

Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 48ab446. Configure here.

Comment thread tests/sentry/notifications/platform/slack/renderers/test_seer.py Outdated
Update assertion to match new missing-scope footer copy and add
missing has_history_scope mock to test_on_explorer_update so it
does not hit the Slack API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
alexsohn1126 and others added 2 commits April 14, 2026 11:01
A transient Slack conversations.info failure returned an empty dict,
causing has_history_scope to incorrectly report missing scopes and
show a misleading "reinstall" footer. Return True on the fall-through
path so API errors don't trigger false prompts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@alexsohn1126 alexsohn1126 merged commit 407f7ac into master Apr 14, 2026
57 checks passed
@alexsohn1126 alexsohn1126 deleted the alexsohn/iswf-2353-ensure-history-scopes-before-api-call branch April 14, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants