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

Reply via email to