fix(reports): don't let PlaywrightTimeout=Exception fallback swallow unrelated errors

CI failures on this branch traced to two issues:

1. take_tiled_screenshot()'s outer `except PlaywrightTimeout: raise` /
   `except Exception: return None` pair assumed PlaywrightTimeout is always
   distinguishable from a generic Exception. It isn't: when the playwright
   package isn't installed, `PlaywrightTimeout = Exception` (see the
   try/except ImportError above the function). In that case *any* exception
   -- not just our own deliberate per-tile readiness-timeout raise --
   matched `except PlaywrightTimeout` first and incorrectly propagated
   instead of degrading to `None` like every other unexpected error (e.g.
   the initial dashboard element never appearing). This is exactly what
   unit-tests hit in CI (no playwright installed there), turning
   `test_element_not_found_returns_none` and `test_exception_handling_*`
   into uncaught-exception failures.

   Fixed by tracking the "this is our own readiness timeout" case with an
   explicit boolean flag set right before the inner `raise`, instead of
   relying on exception-type dispatch that can't tell the two cases apart
   when the import fallback is active.

2. `execute.py` now always passes `log_context=...` to
   `screenshot.get_screenshot(...)`. One integration test
   (`test_email_chart_report_schedule_alpha_owner`) mocks
   `ChartScreenshot.get_screenshot` with a side_effect function whose
   signature only accepted `user`, so the new kwarg raised a TypeError,
   caught by execute.py's own error handling and reported as a
   ReportScheduleScreenshotFailedError in test-mysql/postgres/sqlite. Fixed
   by accepting the new `log_context` keyword in that test's side_effect
   signature. Confirmed this was the only such call site in the test suite
   (all others use plain `.return_value`/exception-instance side effects,
   which don't care about the new kwarg).

Verified locally with and without playwright installed, matching both
conditions CI exercises.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Elizabeth Thompson
2026-07-20 23:45:11 +00:00
parent f6feb70eca
commit 97d15485ad
2 changed files with 23 additions and 8 deletions

View File

@@ -184,6 +184,16 @@ def take_tiled_screenshot(
Combined screenshot bytes or None if failed
"""
context_suffix = f" [{log_context}]" if log_context else ""
# Set right before re-raising the per-tile readiness timeout below, and
# checked in the except block at the bottom of this function. Deciding
# whether to propagate via `isinstance(e, PlaywrightTimeout)` would be
# unreliable: when the playwright package isn't installed,
# `PlaywrightTimeout` is aliased to the bare `Exception` class (see the
# try/except ImportError above this function), which would make *any*
# exception -- not just our own deliberate readiness-timeout raise --
# match `except PlaywrightTimeout` and incorrectly propagate instead of
# degrading to `None` like every other unexpected error in this function.
readiness_timeout = False
try:
# Get the target element
element = page.locator(f".{element_name}")
@@ -263,6 +273,7 @@ def take_tiled_screenshot(
context_suffix,
unready_chart_holders,
)
readiness_timeout = True
raise
else:
elapsed = time.monotonic() - tile_wait_start
@@ -328,13 +339,15 @@ def take_tiled_screenshot(
return combined_screenshot
except PlaywrightTimeout:
# Let per-tile readiness timeouts propagate so the caller fails the
# report instead of silently falling back to a degraded screenshot.
raise
except Exception as e:
# Unlike the readiness-timeout warning above, this is a genuine
# system-level fault (unexpected error, not a customer chart taking
# too long to load), so it stays at ERROR/exception level.
if readiness_timeout:
# Let the per-tile readiness timeout propagate so the caller
# fails the report instead of silently falling back to a
# degraded screenshot -- already logged as a WARNING above.
raise
# Any other exception is a genuine system-level fault (or a setup
# failure unrelated to chart readiness, e.g. the dashboard element
# itself never appearing), not a customer chart taking too long to
# load, so it stays at ERROR/exception level.
logger.exception("Tiled screenshot failed: %s%s", e, context_suffix)
return None

View File

@@ -826,7 +826,9 @@ def test_email_chart_report_schedule_alpha_owner(
# setup screenshot mock
username = ""
def _screenshot_side_effect(user: User) -> Optional[bytes]:
def _screenshot_side_effect(
user: User, log_context: Optional[str] = None
) -> Optional[bytes]:
nonlocal username
username = user.username