You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by el...@apache.org on 2025/10/15 00:45:25 UTC
(superset) 01/01: feat(alerts): Include screenshots in alert/report failure emails
This is an automated email from the ASF dual-hosted git repository.
elizabeth pushed a commit to branch log-alert-screenshot
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 5dee4e2209a6bac4526d522d547f2be9f7c0395c
Author: Elizabeth Thompson <es...@gmail.com>
AuthorDate: Tue Oct 14 17:44:47 2025 -0700
feat(alerts): Include screenshots in alert/report failure emails
When an alert or report fails, capture a screenshot of the current state
and include it in the error notification email sent to owners. This helps
admins/owners quickly identify issues without needing to navigate to the
dashboard or chart.
Key changes:
- Modified send_error() to attempt screenshot capture before sending error notifications
- Updated email notification templates to include inline screenshot images
- Added comprehensive tests for error emails with and without screenshots
- Implemented best-effort approach: screenshots are captured if possible, but failures don't prevent error notifications from being sent
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <no...@anthropic.com>
---
superset/commands/report/execute.py | 29 +++++++-
superset/reports/notifications/email.py | 25 ++++++-
.../reports/notifications/email_tests.py | 78 ++++++++++++++++++++++
3 files changed, 128 insertions(+), 4 deletions(-)
diff --git a/superset/commands/report/execute.py b/superset/commands/report/execute.py
index e51b9684dd..b45d6c60f5 100644
--- a/superset/commands/report/execute.py
+++ b/superset/commands/report/execute.py
@@ -664,8 +664,35 @@ class BaseReportState:
header_data,
self._execution_id,
)
+
+ # Try to capture screenshot even on failure - best effort
+ screenshot_data = []
+ try:
+ screenshot_data = self._get_screenshots()
+ logger.info(
+ "Successfully captured screenshot for error notification: %s",
+ self._execution_id,
+ )
+ except (
+ ReportScheduleScreenshotTimeout,
+ ReportScheduleScreenshotFailedError,
+ ) as ex:
+ logger.warning(
+ "Could not capture screenshot for error notification: %s", str(ex)
+ )
+ except Exception as ex: # pylint: disable=broad-except
+ logger.warning(
+ "Unexpected error while capturing screenshot for error "
+ "notification: %s",
+ str(ex),
+ )
+
notification_content = NotificationContent(
- name=name, text=message, header_data=header_data, url=url
+ name=name,
+ text=message,
+ header_data=header_data,
+ url=url,
+ screenshots=screenshot_data,
)
# filter recipients to recipients who are also owners
diff --git a/superset/reports/notifications/email.py b/superset/reports/notifications/email.py
index 22a0d38bdd..fc85b45ab6 100644
--- a/superset/reports/notifications/email.py
+++ b/superset/reports/notifications/email.py
@@ -97,22 +97,41 @@ class EmailNotification(BaseNotification): # pylint: disable=too-few-public-met
def _get_smtp_domain() -> str:
return parseaddr(current_app.config["SMTP_MAIL_FROM"])[1].split("@")[1]
- def _error_template(self, text: str) -> str:
+ def _error_template(self, text: str, img_tag: str = "") -> str:
call_to_action = self._get_call_to_action()
return __(
"""
<p>Your report/alert was unable to be generated because of the following error: %(text)s</p>
<p>Please check your dashboard/chart for errors.</p>
<p><b><a href="%(url)s">%(call_to_action)s</a></b></p>
+ %(screenshots)s
""", # noqa: E501
text=text,
url=self._content.url,
call_to_action=call_to_action,
+ screenshots=img_tag,
)
def _get_content(self) -> EmailContent:
if self._content.text:
- return EmailContent(body=self._error_template(self._content.text))
+ # Error case - include screenshots if available
+ images = {}
+ img_tag_str = ""
+ if self._content.screenshots:
+ domain = self._get_smtp_domain()
+ images = {
+ make_msgid(domain)[1:-1]: screenshot
+ for screenshot in self._content.screenshots
+ }
+ for msgid in images.keys():
+ img_tag_str += (
+ f'<div class="image"><img width="1000" src="cid:{msgid}"></div>'
+ )
+
+ return EmailContent(
+ body=self._error_template(self._content.text, img_tag_str),
+ images=images,
+ )
# Get the domain from the 'From' address ..
# and make a message id without the < > in the end
@@ -147,7 +166,7 @@ class EmailNotification(BaseNotification): # pylint: disable=too-few-public-met
else:
html_table = ""
- img_tags = []
+ img_tags: list[str] = []
for msgid in images.keys():
img_tags.append(
f"""<div class="image">
diff --git a/tests/unit_tests/reports/notifications/email_tests.py b/tests/unit_tests/reports/notifications/email_tests.py
index d8872d4d34..e063eb925d 100644
--- a/tests/unit_tests/reports/notifications/email_tests.py
+++ b/tests/unit_tests/reports/notifications/email_tests.py
@@ -98,3 +98,81 @@ def test_email_subject_with_datetime() -> None:
)._get_subject()
assert datetime_pattern not in subject
assert now.strftime(datetime_pattern) in subject
+
+
+def test_error_email_with_screenshot() -> None:
+ # `superset.models.helpers`, a dependency of following imports,
+ # requires app context
+ from superset.reports.models import ReportRecipients, ReportRecipientType
+ from superset.reports.notifications.base import NotificationContent
+ from superset.reports.notifications.email import EmailNotification
+
+ # Create mock screenshot data
+ screenshot_data = [b"fake_screenshot_data_1", b"fake_screenshot_data_2"]
+
+ content = NotificationContent(
+ name="test alert",
+ text="Error occurred while generating report",
+ url="http://localhost:8088/superset/dashboard/1",
+ screenshots=screenshot_data,
+ header_data={
+ "notification_format": "PNG",
+ "notification_type": "Alert",
+ "owners": [1],
+ "notification_source": None,
+ "chart_id": None,
+ "dashboard_id": None,
+ "slack_channels": None,
+ },
+ )
+ email_content = EmailNotification(
+ recipient=ReportRecipients(type=ReportRecipientType.EMAIL), content=content
+ )._get_content()
+
+ # Check that error message is in the body
+ assert "Error occurred while generating report" in email_content.body
+ assert "unable to be generated" in email_content.body
+
+ # Check that images are included
+ assert email_content.images is not None
+ assert len(email_content.images) == 2
+
+ # Check that image tags are in the body
+ assert '<img width="1000" src="cid:' in email_content.body
+ assert 'class="image"' in email_content.body
+
+
+def test_error_email_without_screenshot() -> None:
+ # `superset.models.helpers`, a dependency of following imports,
+ # requires app context
+ from superset.reports.models import ReportRecipients, ReportRecipientType
+ from superset.reports.notifications.base import NotificationContent
+ from superset.reports.notifications.email import EmailNotification
+
+ content = NotificationContent(
+ name="test alert",
+ text="Error occurred while generating report",
+ url="http://localhost:8088/superset/dashboard/1",
+ header_data={
+ "notification_format": "PNG",
+ "notification_type": "Alert",
+ "owners": [1],
+ "notification_source": None,
+ "chart_id": None,
+ "dashboard_id": None,
+ "slack_channels": None,
+ },
+ )
+ email_content = EmailNotification(
+ recipient=ReportRecipients(type=ReportRecipientType.EMAIL), content=content
+ )._get_content()
+
+ # Check that error message is in the body
+ assert "Error occurred while generating report" in email_content.body
+ assert "unable to be generated" in email_content.body
+
+ # Check that no images are included
+ assert email_content.images == {}
+
+ # Check that no image tags are in the body
+ assert "<img" not in email_content.body