Skip to content

Commit 6f6d707

Browse files
committed
feat(seer-slack): Only show missing scope footer on first thread response
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.
1 parent 49ba41b commit 6f6d707

File tree

2 files changed

+76
-3
lines changed

2 files changed

+76
-3
lines changed

src/sentry/seer/entrypoints/slack/messaging.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from sentry.shared_integrations.exceptions import IntegrationConfigurationError, IntegrationError
3030
from sentry.tasks.base import instrumented_task
3131
from sentry.taskworker.namespaces import integrations_tasks
32+
from sentry.utils.cache import cache
3233
from sentry.utils.registry import NoRegistrationExistsError
3334

3435
if TYPE_CHECKING:
@@ -37,9 +38,16 @@
3738
from sentry.seer.entrypoints.slack.entrypoint import SlackThreadDetails
3839

3940

41+
# TTL for the "missing scope footer already shown" cache key (24 hours).
42+
MISSING_SCOPE_FOOTER_CACHE_TIMEOUT = 60 * 60 * 24
43+
4044
logger = logging.getLogger(__name__)
4145

4246

47+
def _missing_scope_footer_cache_key(integration_id: int, channel_id: str, thread_ts: str) -> str:
48+
return f"seer:explorer:scope_footer:{integration_id}:{channel_id}:{thread_ts}"
49+
50+
4351
def send_thread_update(
4452
*,
4553
install: SlackIntegration,
@@ -70,10 +78,17 @@ def send_thread_update(
7078
if isinstance(data, SeerExplorerResponse) and not install.has_history_scope(
7179
thread["channel_id"]
7280
):
73-
settings_url = install.organization.absolute_url(
74-
f"/settings/{install.organization.slug}/integrations/slack/"
81+
footer_key = _missing_scope_footer_cache_key(
82+
install.model.id, thread["channel_id"], thread["thread_ts"]
7583
)
76-
renderable["blocks"].extend(SeerSlackRenderer.render_missing_scope_footer(settings_url))
84+
# cache.add is atomic: returns True only the first time (key didn't exist).
85+
if cache.add(footer_key, True, timeout=MISSING_SCOPE_FOOTER_CACHE_TIMEOUT):
86+
settings_url = install.organization.absolute_url(
87+
f"/settings/{install.organization.slug}/integrations/slack/"
88+
)
89+
renderable["blocks"].extend(
90+
SeerSlackRenderer.render_missing_scope_footer(settings_url)
91+
)
7792

7893
try:
7994
if ephemeral_user_id:

tests/sentry/seer/entrypoints/slack/test_slack.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,64 @@ def test_send_thread_update_explorer_response_missing_scope_has_footer(
669669
assert "Reinstall" in footer_text
670670
assert "Thread context is unavailable" in footer_text
671671

672+
@patch("sentry.integrations.slack.integration.SlackIntegration.send_threaded_message")
673+
@patch(
674+
"sentry.integrations.slack.integration.SlackIntegration.has_history_scope",
675+
return_value=False,
676+
)
677+
def test_send_thread_update_explorer_response_missing_scope_no_footer_on_second_message(
678+
self, mock_has_history_scope, mock_send_threaded_message
679+
):
680+
"""Subsequent explorer responses in the same thread should not repeat the footer."""
681+
data = SeerExplorerResponse(
682+
run_id=12345,
683+
organization_id=self.organization.id,
684+
summary="Test summary",
685+
)
686+
install = self.integration.get_installation(organization_id=self.organization.id)
687+
thread = SlackThreadDetails(thread_ts=self.thread_ts, channel_id=self.channel_id)
688+
689+
# First call — footer should be present
690+
send_thread_update(install=install, thread=thread, data=data)
691+
renderable_first = mock_send_threaded_message.call_args.kwargs["renderable"]
692+
assert renderable_first["blocks"][-1].type == "context"
693+
694+
mock_send_threaded_message.reset_mock()
695+
696+
# Second call in same thread — footer should be absent
697+
send_thread_update(install=install, thread=thread, data=data)
698+
renderable_second = mock_send_threaded_message.call_args.kwargs["renderable"]
699+
for block in renderable_second["blocks"]:
700+
assert block.type != "context"
701+
702+
@patch("sentry.integrations.slack.integration.SlackIntegration.send_threaded_message")
703+
@patch(
704+
"sentry.integrations.slack.integration.SlackIntegration.has_history_scope",
705+
return_value=False,
706+
)
707+
def test_send_thread_update_explorer_response_missing_scope_different_thread_gets_footer(
708+
self, mock_has_history_scope, mock_send_threaded_message
709+
):
710+
"""Different threads should each get their own first-time footer."""
711+
data = SeerExplorerResponse(
712+
run_id=12345,
713+
organization_id=self.organization.id,
714+
summary="Test summary",
715+
)
716+
install = self.integration.get_installation(organization_id=self.organization.id)
717+
thread_a = SlackThreadDetails(thread_ts=self.thread_ts, channel_id=self.channel_id)
718+
thread_b = SlackThreadDetails(thread_ts="9999999999.999999", channel_id=self.channel_id)
719+
720+
send_thread_update(install=install, thread=thread_a, data=data)
721+
renderable_a = mock_send_threaded_message.call_args.kwargs["renderable"]
722+
assert renderable_a["blocks"][-1].type == "context"
723+
724+
mock_send_threaded_message.reset_mock()
725+
726+
send_thread_update(install=install, thread=thread_b, data=data)
727+
renderable_b = mock_send_threaded_message.call_args.kwargs["renderable"]
728+
assert renderable_b["blocks"][-1].type == "context"
729+
672730
@patch("sentry.integrations.slack.integration.SlackIntegration.send_threaded_message")
673731
@patch(
674732
"sentry.integrations.slack.integration.SlackIntegration.has_history_scope",

0 commit comments

Comments
 (0)