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 <[email protected]> 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 <[email protected]> --- 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
