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
2 changes: 2 additions & 0 deletions UPDATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ assists people when migrating to a new version.

## Next

- [39914](https://github.com/apache/superset/pull/39914) `ALERT_REPORT_SLACK_V2` now defaults to `True` and the legacy Slack v1 integration (`Slack` recipient type, `files.upload` API) is deprecated for removal in the next major. Slack retired `files.upload` in 2025, so v1 file-bearing sends already fail at the API level — only text-only `chat_postMessage` still works via the legacy path. Grant your Slack bot the `channels:read` scope (and `groups:read` if you use private channels) so existing `Slack` recipients can be auto-upgraded to `SlackV2` on next send. Operators who explicitly override the flag to `False` will see a one-shot `DeprecationWarning` plus a `logger.warning`; remove the override or grant the scopes to clear it.

### Granular Export Controls

A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission:
Expand Down
4 changes: 2 additions & 2 deletions docs/static/feature-flags.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,9 @@
},
{
"name": "ALERT_REPORT_SLACK_V2",
"default": false,
"default": true,
"lifecycle": "testing",
"description": "Enables Slack V2 integration for Alerts and Reports"
"description": "Enables Slack V2 integration for Alerts and Reports. Defaults to True; the legacy Slack v1 path is deprecated and will be removed in the next major release. Operators must grant the Slack bot the `channels:read` scope (and `groups:read` for private channels) so existing v1 recipients can be auto-upgraded on their next send. Without those scopes, file uploads fail (Slack retired the `files.upload` endpoint in 2025) and only text-only `chat_postMessage` sends will continue to work via the legacy path."
},
{
"name": "ALERT_REPORT_WEBHOOK",
Expand Down
11 changes: 9 additions & 2 deletions superset/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,9 +611,16 @@ class D3TimeFormat(TypedDict, total=False):
# @lifecycle: testing
# @docs: https://superset.apache.org/docs/configuration/alerts-reports
"ALERT_REPORTS": False,
# Enables Slack V2 integration for Alerts and Reports
# Enables Slack V2 integration for Alerts and Reports.
# Defaults to True; the legacy Slack v1 path is deprecated and will be removed
# in the next major release. Operators must grant the Slack bot the
# `channels:read` scope (and `groups:read` for private channels) so existing
# v1 recipients can be auto-upgraded on their next send. Without those
# scopes, file uploads fail (Slack retired the `files.upload` endpoint in
# 2025) and only text-only `chat_postMessage` sends will continue to work
# via the legacy path.
# @lifecycle: testing
"ALERT_REPORT_SLACK_V2": False,
"ALERT_REPORT_SLACK_V2": True,
# Enables webhook integration for Alerts and Reports
# @lifecycle: testing
"ALERT_REPORT_WEBHOOK": False,
Expand Down
6 changes: 5 additions & 1 deletion superset/reports/notifications/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@
logger = logging.getLogger(__name__)


# TODO: Deprecated: Remove this class in Superset 6.0.0
# Deprecated: Slack v1 will be removed in the next major release. The Slack
# `files.upload` endpoint was retired in 2025, so file-bearing sends already
# fail at the API level; only text-only `chat_postMessage` sends still work
# here. Recipients with the `channels:read` scope are auto-upgraded to
# SlackV2 on first send via `update_report_schedule_slack_v2`.
class SlackNotification(SlackMixin, BaseNotification): # pylint: disable=too-few-public-methods
"""
Sends a slack notification for a report recipient
Expand Down
13 changes: 12 additions & 1 deletion superset/reports/notifications/slackv2.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,18 @@ def _get_inline_files(
return ("pdf", [self._content.pdf])
return (None, [])

@backoff.on_exception(backoff.expo, SlackApiError, factor=10, base=2, max_tries=5)
# Retry on NotificationUnprocessableException (the wrapper that send()
# raises for transient Slack failures: SlackApiError, connection errors,
# and the SlackClientError catch-all). Retrying on SlackApiError directly
# would never fire because the try/except below converts it before the
# decorator can see it. Mirrors the pattern in webhook.py.
@backoff.on_exception(
backoff.expo,
NotificationUnprocessableException,
factor=10,
base=2,
max_tries=5,
)
@statsd_gauge("reports.slack.send")
def send(self) -> None:
global_logs_context = getattr(g, "logs_context", {}) or {}
Expand Down
41 changes: 37 additions & 4 deletions superset/utils/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
# under the License.


import functools
import logging
import warnings
from typing import Callable, Optional

from flask import current_app as app
Expand All @@ -34,6 +36,33 @@

logger = logging.getLogger(__name__)

_SLACK_V1_DEPRECATION_MESSAGE = (
"Slack v1 (the legacy `Slack` recipient type and `files.upload` API) is "
"deprecated and will be removed in the next major release. Slack retired "
"the `files.upload` endpoint in 2025, so v1 file uploads no longer work; "
"only text-only `chat_postMessage` sends still succeed. Grant your Slack "
"bot the `channels:read` (and `groups:read` if you use private channels) "
"scopes so existing v1 recipients can be auto-upgraded to SlackV2 on "
"their next send."
)


# functools.cache gives us a process-lifetime, thread-safe one-shot guard
# without the read-then-write race that bare module globals would have under
# multi-threaded WSGI workers. The cached return value (None) is irrelevant —
# we only care that the body executes at most once per process.
@functools.cache
def _emit_v1_flag_off_deprecation() -> None:
warnings.warn(_SLACK_V1_DEPRECATION_MESSAGE, DeprecationWarning, stacklevel=3)
logger.warning(
"ALERT_REPORT_SLACK_V2 is disabled; %s", _SLACK_V1_DEPRECATION_MESSAGE
)


@functools.cache
def _emit_v1_scope_missing_deprecation() -> None:
warnings.warn(_SLACK_V1_DEPRECATION_MESSAGE, DeprecationWarning, stacklevel=3)


class SlackChannelTypes(StrEnum):
PUBLIC = "public_channel"
Expand Down Expand Up @@ -181,18 +210,22 @@ def get_channels_with_search(

def should_use_v2_api() -> bool:
if not feature_flag_manager.is_feature_enabled("ALERT_REPORT_SLACK_V2"):
_emit_v1_flag_off_deprecation()
return False
try:
client = get_slack_client()
client.conversations_list()
logger.info("Slack API v2 is available")
return True
except SlackApiError:
# use the v1 api but warn with a deprecation message
# The DeprecationWarning fires once per process, but the actionable
# log line fires every send so operators see it in their report logs.
_emit_v1_scope_missing_deprecation()
logger.warning(
"""Your current Slack scopes are missing `channels:read`. Please add
this to your Slack app in order to continue using the v1 API. Support
for the old Slack API will be removed in Superset version 6.0.0."""
"Slack bot is missing the `channels:read` (and `groups:read` for "
"private channels) scope; falling back to the deprecated v1 API. "
"%s",
_SLACK_V1_DEPRECATION_MESSAGE,
)
return False

Expand Down
13 changes: 13 additions & 0 deletions tests/integration_tests/reports/commands_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1975,11 +1975,13 @@ def test_slack_chart_alert_no_attachment(email_mock, create_alert_email_chart):
"load_birth_names_dashboard_with_slices",
"create_report_slack_chart",
)
@patch("superset.commands.report.execute.get_channels_with_search")
@patch("superset.utils.slack.WebClient")
@patch("superset.utils.screenshots.ChartScreenshot.get_screenshot")
def test_slack_token_callable_chart_report(
screenshot_mock,
slack_client_mock_class,
get_channels_with_search_mock,
create_report_slack_chart,
):
"""
Expand All @@ -1990,9 +1992,20 @@ def test_slack_token_callable_chart_report(
channel_name = notification_targets[0]
channel_id = "channel_id"
slack_client_mock_class.return_value = Mock()
# should_use_v2_api() probes via conversations_list(); a non-erroring return
# is enough — it doesn't read the response body. The v2 upgrade then resolves
# channel names through get_channels_with_search, which we mock directly.
slack_client_mock_class.return_value.conversations_list.return_value = {
"channels": [{"id": channel_id, "name": channel_name}]
}
get_channels_with_search_mock.return_value = [
{
"id": channel_id,
"name": channel_name,
"is_member": True,
"is_private": False,
}
]

slack_token_mock = Mock(return_value="cool_code")
with patch.dict("flask.current_app.config", {"SLACK_API_TOKEN": slack_token_mock}):
Expand Down
Loading
Loading