Compare commits

...

1 Commits

Author SHA1 Message Date
Elizabeth Thompson
b5eca9ad48 chore(reports): thread cache-key/execution-id log context through screenshot capture logs
The screenshot capture code is reached by two call paths that identify
their runs differently: scheduled reports carry an execution_id (already
threaded end-to-end via log_context since #42253), while thumbnails and
direct PDF/screenshot downloads are identified by their cache_key -- which
was never passed down. BaseScreenshot.compute_and_cache had the cache_key
in hand and get_screenshot already accepted a log_context parameter, but
the two were never connected, so every capture-layer log line produced by
a thumbnail or direct-download run is anonymous: there is no way to join
"trying to generate screenshot" / webdriver navigation / readiness /
capture-result log lines to the cached digest they were computing.

This threads the existing optional log_context through the remaining
capture-layer log lines, and populates it on the thumbnail path:

- screenshots.py: compute_and_cache passes
  log_context=f"cache_key={cache_key}" into get_screenshot and
  resize_image; the thumbnail lifecycle log lines (generate/fail/resize/
  cache-updated) now include the cache_key; driver() accepts log_context
  for its Playwright-unavailable fallback notice.
- webdriver.py: the non-tiled Playwright log lines (navigation, headstart,
  element/chart-container waits, screenshot result), the entire
  WebDriverSelenium.get_screenshot path, and find_unexpected_errors (both
  engines) now append the context suffix.
- screenshot_utils.py: the non-budget tiled log lines (dimensions, tile
  count, scroll, capture, skip, combine) and combine_screenshot_tiles gain
  the same suffix.

Log-line/plumbing only -- no behavior change. Split out of #42118 per its
scope reduction to tiled-path budgeting; the readiness log lines added by

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-31 22:10:36 +00:00
6 changed files with 219 additions and 74 deletions

View File

@@ -207,12 +207,16 @@ CHART_CONTAINER_READY_JS = f"""
"""
def combine_screenshot_tiles(screenshot_tiles: list[bytes]) -> bytes:
def combine_screenshot_tiles(
screenshot_tiles: list[bytes], log_context: str | None = None
) -> bytes:
"""
Combine multiple screenshot tiles into a single vertical image.
Args:
screenshot_tiles: List of screenshot bytes in PNG format
log_context: Optional identifier (e.g. report execution id, or a
thumbnail cache key) appended to log lines for tracing.
Returns:
Combined screenshot as bytes
@@ -223,6 +227,7 @@ def combine_screenshot_tiles(screenshot_tiles: list[bytes]) -> bytes:
if len(screenshot_tiles) == 1:
return screenshot_tiles[0]
context_suffix = f" [{log_context}]" if log_context else ""
try:
# Open all images
images = [Image.open(io.BytesIO(tile)) for tile in screenshot_tiles]
@@ -246,7 +251,7 @@ def combine_screenshot_tiles(screenshot_tiles: list[bytes]) -> bytes:
return output.getvalue()
except Exception as e:
logger.exception("Failed to combine screenshot tiles: %s", e)
logger.exception("Failed to combine screenshot tiles: %s%s", e, context_suffix)
# Return the first tile as fallback
return screenshot_tiles[0]
@@ -309,16 +314,17 @@ def take_tiled_screenshot(
dashboard_top = element_info["top"]
logger.info(
"Dashboard: %sx%spx at (%s, %s)",
"Dashboard: %sx%spx at (%s, %s)%s",
dashboard_width,
dashboard_height,
dashboard_left,
dashboard_top,
context_suffix,
)
# Calculate number of tiles needed
num_tiles = max(1, (dashboard_height + tile_height - 1) // tile_height)
logger.info("Taking %s screenshot tiles", num_tiles)
logger.info("Taking %s screenshot tiles%s", num_tiles, context_suffix)
screenshot_tiles = []
@@ -328,7 +334,11 @@ def take_tiled_screenshot(
page.evaluate(f"window.scrollTo(0, {scroll_y})")
logger.debug(
"Scrolled window to %s for tile %s/%s", scroll_y, i + 1, num_tiles
"Scrolled window to %s for tile %s/%s%s",
scroll_y,
i + 1,
num_tiles,
context_suffix,
)
# Wait for scroll to settle and content to load
page.wait_for_timeout(SCROLL_SETTLE_TIMEOUT_MS)
@@ -401,13 +411,14 @@ def take_tiled_screenshot(
logger.warning(
"Skipping tile %s/%s due to invalid clip dimensions: "
"x=%s, y=%s, width=%s, height=%s "
"(element may be scrolled out of viewport)",
"(element may be scrolled out of viewport).%s",
i + 1,
num_tiles,
clip_x,
clip_y,
dashboard_width,
clip_height,
context_suffix,
)
continue
@@ -423,11 +434,19 @@ def take_tiled_screenshot(
tile_screenshot = page.screenshot(type="png", clip=clip)
screenshot_tiles.append(tile_screenshot)
logger.debug("Captured tile %s/%s with clip %s", i + 1, num_tiles, clip)
logger.debug(
"Captured tile %s/%s with clip %s%s",
i + 1,
num_tiles,
clip,
context_suffix,
)
# Combine all tiles
logger.info("Combining screenshot tiles...")
combined_screenshot = combine_screenshot_tiles(screenshot_tiles)
logger.info("Combining screenshot tiles...%s", context_suffix)
combined_screenshot = combine_screenshot_tiles(
screenshot_tiles, log_context=log_context
)
return combined_screenshot

View File

@@ -190,7 +190,10 @@ class BaseScreenshot:
self.screenshot = None
def driver(
self, window_size: WindowSize | None = None, user: User | None = None
self,
window_size: WindowSize | None = None,
user: User | None = None,
log_context: str | None = None,
) -> WebDriverProxy:
window_size = window_size or self.window_size
if feature_flag_manager.is_feature_enabled("PLAYWRIGHT_REPORTS_AND_THUMBNAILS"):
@@ -199,11 +202,13 @@ class BaseScreenshot:
return WebDriverPlaywright(self.driver_type, window_size)
# Playwright not available, falling back to Selenium
context_suffix = f" [{log_context}]" if log_context else ""
logger.info(
"PLAYWRIGHT_REPORTS_AND_THUMBNAILS enabled but Playwright not "
"installed. Falling back to Selenium (WebGL/Canvas charts may "
"not render correctly). %s",
"not render correctly). %s%s",
PLAYWRIGHT_INSTALL_MESSAGE,
context_suffix,
)
# Use Selenium as default/fallback
@@ -215,7 +220,7 @@ class BaseScreenshot:
window_size: WindowSize | None = None,
log_context: str | None = None,
) -> bytes | None:
driver = self.driver(window_size, user)
driver = self.driver(window_size, user, log_context=log_context)
try:
self.screenshot = driver.get_screenshot(
self.url, self.element, user, log_context=log_context
@@ -310,22 +315,38 @@ class BaseScreenshot:
image = None
# Assuming all sorts of things can go wrong with Selenium
try:
logger.info("trying to generate screenshot")
logger.info(
"trying to generate screenshot for cache_key=%s", cache_key
)
with event_logger.log_context(
f"screenshot.compute.{self.thumbnail_type}"
):
image = self.get_screenshot(user=user, window_size=window_size)
image = self.get_screenshot(
user=user,
window_size=window_size,
log_context=f"cache_key={cache_key}",
)
except Exception as ex: # pylint: disable=broad-except
logger.warning(
"Failed at generating thumbnail %s", ex, exc_info=True
"Failed at generating thumbnail for cache_key=%s: %s",
cache_key,
ex,
exc_info=True,
)
cache_payload.error()
if image and window_size != thumb_size:
try:
image = self.resize_image(image, thumb_size=thumb_size)
image = self.resize_image(
image,
thumb_size=thumb_size,
log_context=f"cache_key={cache_key}",
)
except Exception as ex: # pylint: disable=broad-except
logger.warning(
"Failed at resizing thumbnail %s", ex, exc_info=True
"Failed at resizing thumbnail for cache_key=%s: %s",
cache_key,
ex,
exc_info=True,
)
cache_payload.error()
image = None
@@ -344,7 +365,9 @@ class BaseScreenshot:
logger.info("Caching thumbnail: %s", cache_key)
self.cache.set(cache_key, cache_payload.to_dict())
logger.info(
"Updated thumbnail cache; Status: %s", cache_payload.get_status()
"Updated thumbnail cache for %s; Status: %s",
cache_key,
cache_payload.get_status(),
)
except LockAlreadyHeldException:
logger.info(
@@ -359,16 +382,23 @@ class BaseScreenshot:
output: str = "png",
thumb_size: WindowSize | None = None,
crop: bool = True,
log_context: str | None = None,
) -> bytes:
context_suffix = f" [{log_context}]" if log_context else ""
thumb_size = thumb_size or cls.thumb_size
img = Image.open(BytesIO(img_bytes))
logger.debug("Selenium image size: %s", str(img.size))
logger.debug("Selenium image size: %s%s", str(img.size), context_suffix)
if crop and img.size[1] != cls.window_size[1]:
desired_ratio = float(cls.window_size[1]) / cls.window_size[0]
desired_width = int(img.size[0] * desired_ratio)
logger.debug("Cropping to: %s*%s", str(img.size[0]), str(desired_width))
logger.debug(
"Cropping to: %s*%s%s",
str(img.size[0]),
str(desired_width),
context_suffix,
)
img = img.crop((0, 0, img.size[0], desired_width))
logger.debug("Resizing to %s", str(thumb_size))
logger.debug("Resizing to %s%s", str(thumb_size), context_suffix)
img = img.resize(thumb_size, Image.Resampling.LANCZOS)
new_img = BytesIO()
if output != "png":

View File

@@ -237,14 +237,17 @@ class WebDriverPlaywright(WebDriverProxy):
)
@staticmethod
def find_unexpected_errors(page: Page) -> list[str]:
def find_unexpected_errors(page: Page, log_context: str | None = None) -> list[str]:
error_messages = []
context_suffix = f" [{log_context}]" if log_context else ""
try:
alert_divs = page.get_by_role("alert").all()
logger.debug(
"%i alert elements have been found in the screenshot", len(alert_divs)
"%i alert elements have been found in the screenshot%s",
len(alert_divs),
context_suffix,
)
for alert_div in alert_divs:
@@ -274,9 +277,12 @@ class WebDriverPlaywright(WebDriverProxy):
[error_as_html],
)
except PlaywrightError:
logger.exception("Failed to update error messages using alert_div")
logger.exception(
"Failed to update error messages using alert_div%s",
context_suffix,
)
except PlaywrightError:
logger.exception("Failed to capture unexpected errors")
logger.exception("Failed to capture unexpected errors%s", context_suffix)
return error_messages
@@ -426,12 +432,14 @@ class WebDriverPlaywright(WebDriverProxy):
log_context: str | None = None,
) -> bytes | None:
screenshot_started_at = time.monotonic()
context_suffix = f" [{log_context}]" if log_context else ""
if not PLAYWRIGHT_AVAILABLE:
logger.info(
"Playwright not available - falling back to Selenium. "
"Note: WebGL/Canvas charts may not render correctly with Selenium. "
"%s",
"%s%s",
PLAYWRIGHT_INSTALL_MESSAGE,
context_suffix,
)
return None
@@ -461,32 +469,44 @@ class WebDriverPlaywright(WebDriverProxy):
)
except PlaywrightTimeout:
logger.exception(
"Web event %s not detected. Page %s might not have been fully loaded", # noqa: E501
"Web event %s not detected. Page %s might not have been fully loaded%s", # noqa: E501
app.config["SCREENSHOT_PLAYWRIGHT_WAIT_EVENT"],
url,
context_suffix,
)
selenium_headstart = app.config["SCREENSHOT_SELENIUM_HEADSTART"]
logger.debug("Sleeping for %i seconds", selenium_headstart)
logger.debug(
"Sleeping for %i seconds%s", selenium_headstart, context_suffix
)
page.wait_for_timeout(selenium_headstart * 1000)
element: Locator
try:
try:
# page didn't load
logger.debug(
"Wait for the presence of %s at url: %s", element_name, url
"Wait for the presence of %s at url: %s%s",
element_name,
url,
context_suffix,
)
element = page.locator(f".{element_name}")
element.wait_for()
except PlaywrightTimeout:
logger.exception("Timed out requesting url %s", url)
logger.exception(
"Timed out requesting url %s%s", url, context_suffix
)
raise
slice_container_elems: list[Locator] = []
rendered_chart_count = 0
try:
# chart containers didn't render
logger.debug("Wait for chart containers to draw at url: %s", url)
logger.debug(
"Wait for chart containers to draw at url: %s%s",
url,
context_suffix,
)
slice_container_locator = page.locator(".chart-container")
# One-time snapshot: containers mounting after this point
# are neither waited on nor counted, so the progress
@@ -502,10 +522,11 @@ class WebDriverPlaywright(WebDriverProxy):
# it still fails the screenshot; see the `raise` below.
logger.warning(
"Timed out waiting for chart containers to draw at url %s "
"(%s of %s chart containers rendered before the timeout)",
"(%s of %s chart containers rendered before the timeout)%s",
url,
rendered_chart_count,
len(slice_container_elems),
context_suffix,
exc_info=True,
)
raise
@@ -513,13 +534,16 @@ class WebDriverPlaywright(WebDriverProxy):
"SCREENSHOT_SELENIUM_ANIMATION_WAIT"
]
if app.config["SCREENSHOT_REPLACE_UNEXPECTED_ERRORS"]:
unexpected_errors = WebDriverPlaywright.find_unexpected_errors(page)
unexpected_errors = WebDriverPlaywright.find_unexpected_errors(
page, log_context=log_context
)
if unexpected_errors:
logger.warning(
"%i errors found in the screenshot. URL: %s. Errors are: %s", # noqa: E501
"%i errors found in the screenshot. URL: %s. Errors are: %s%s", # noqa: E501
len(unexpected_errors),
url,
unexpected_errors,
context_suffix,
)
# Detect large dashboards and use tiled screenshots if enabled
tiled_enabled = app.config.get("SCREENSHOT_TILED_ENABLED", False)
@@ -563,13 +587,14 @@ class WebDriverPlaywright(WebDriverProxy):
)
log_fn(
"Could not determine dashboard height for element %s "
"at url %s (%s chart containers found); %s",
"at url %s (%s chart containers found); %s%s",
element_name,
url,
chart_count,
"attempting tiled screenshot anyway"
if likely_large_dashboard
else "falling back to standard screenshot behavior",
context_suffix,
)
# Use tiled screenshots for large dashboards
@@ -580,9 +605,10 @@ class WebDriverPlaywright(WebDriverProxy):
if use_tiled:
logger.info(
"Large dashboard detected: %s charts, %spx height. "
"Using tiled screenshots.",
"Using tiled screenshots.%s",
chart_count,
dashboard_height,
context_suffix,
)
# set viewport height to tile height for easier calculations
page.set_viewport_size(
@@ -605,25 +631,28 @@ class WebDriverPlaywright(WebDriverProxy):
# guessing at a "safer" fallback.
logger.warning(
"Tiled screenshot failed for url %s and no "
"safe fallback exists; failing the capture",
"safe fallback exists; failing the capture%s",
url,
context_suffix,
)
raise PlaywrightTimeout(
f"Tiled screenshot failed for url {url}"
)
logger.debug(
"Tiled screenshot result: %d bytes for url: %s",
"Tiled screenshot result: %d bytes for url: %s%s",
len(img),
url,
context_suffix,
)
else:
logger.debug(
"Dashboard below tiling threshold "
"(%s charts, %spx height); using standard screenshot "
"for url: %s",
"for url: %s%s",
chart_count,
dashboard_height,
url,
context_suffix,
)
# Standard screenshot captures the full element including
# below-the-fold content, so wait for all viewport-visible
@@ -638,28 +667,32 @@ class WebDriverPlaywright(WebDriverProxy):
)
if selenium_animation_wait > 0:
logger.debug(
"Wait %i seconds for chart animation",
"Wait %i seconds for chart animation%s",
selenium_animation_wait,
context_suffix,
)
page.wait_for_timeout(selenium_animation_wait * 1000)
logger.debug(
"Taking screenshot of url %s as user %s",
"Taking screenshot of url %s as user %s%s",
url,
user.username if user else "None",
context_suffix,
)
img = WebDriverPlaywright._get_screenshot(
page, element, element_name
)
logger.debug(
"Screenshot result: %d bytes for url: %s",
"Screenshot result: %d bytes for url: %s%s",
len(img) if img else 0,
url,
context_suffix,
)
else:
logger.debug(
"Tiled screenshots disabled; using standard screenshot "
"for url: %s",
"for url: %s%s",
url,
context_suffix,
)
# Standard screenshot captures the full element including
# below-the-fold content, so wait for all viewport-visible
@@ -674,29 +707,34 @@ class WebDriverPlaywright(WebDriverProxy):
)
if selenium_animation_wait > 0:
logger.debug(
"Wait %i seconds for chart animation",
"Wait %i seconds for chart animation%s",
selenium_animation_wait,
context_suffix,
)
page.wait_for_timeout(selenium_animation_wait * 1000)
logger.debug(
"Taking screenshot of url %s as user %s",
"Taking screenshot of url %s as user %s%s",
url,
user.username if user else "None",
context_suffix,
)
img = WebDriverPlaywright._get_screenshot(
page, element, element_name
)
logger.debug(
"Screenshot result: %d bytes for url: %s",
"Screenshot result: %d bytes for url: %s%s",
len(img) if img else 0,
url,
context_suffix,
)
except PlaywrightTimeout:
raise
except PlaywrightError:
logger.exception(
"Encountered an unexpected error when requesting url %s", url
"Encountered an unexpected error when requesting url %s%s",
url,
context_suffix,
)
finally:
context.close()
@@ -900,13 +938,18 @@ class WebDriverSelenium(WebDriverProxy):
self._driver = None
@staticmethod
def find_unexpected_errors(driver: WebDriver) -> list[str]:
def find_unexpected_errors(
driver: WebDriver, log_context: str | None = None
) -> list[str]:
error_messages = []
context_suffix = f" [{log_context}]" if log_context else ""
try:
alert_divs = driver.find_elements(By.XPATH, "//div[@role = 'alert']")
logger.debug(
"%i alert elements have been found in the screenshot", len(alert_divs)
"%i alert elements have been found in the screenshot%s",
len(alert_divs),
context_suffix,
)
for alert_div in alert_divs:
@@ -949,9 +992,12 @@ class WebDriverSelenium(WebDriverProxy):
f"arguments[0].innerHTML = '{error_as_html}'", alert_div
)
except WebDriverException:
logger.exception("Failed to update error messages using alert_div")
logger.exception(
"Failed to update error messages using alert_div%s",
context_suffix,
)
except WebDriverException:
logger.exception("Failed to capture unexpected errors")
logger.exception("Failed to capture unexpected errors%s", context_suffix)
return error_messages
@@ -962,6 +1008,7 @@ class WebDriverSelenium(WebDriverProxy):
user: User | None = None,
log_context: str | None = None,
) -> bytes | None:
context_suffix = f" [{log_context}]" if log_context else ""
# If a user is passed explicitly and differs from the stored user,
# update and re-authenticate
if user and user != self._user:
@@ -972,7 +1019,7 @@ class WebDriverSelenium(WebDriverProxy):
driver.get(url)
img: bytes | None = None
selenium_headstart = app.config["SCREENSHOT_SELENIUM_HEADSTART"]
logger.debug("Sleeping for %i seconds", selenium_headstart)
logger.debug("Sleeping for %i seconds%s", selenium_headstart, context_suffix)
sleep(selenium_headstart)
# WebDriver cleanup is intentionally not performed in this method. When the
@@ -983,27 +1030,37 @@ class WebDriverSelenium(WebDriverProxy):
try:
# page didn't load
logger.debug(
"Wait for the presence of %s at url: %s", element_name, url
"Wait for the presence of %s at url: %s%s",
element_name,
url,
context_suffix,
)
element = WebDriverWait(driver, self._screenshot_locate_wait).until(
EC.presence_of_element_located((By.CLASS_NAME, element_name))
)
except TimeoutException:
logger.warning(
"Selenium timed out requesting url %s", url, exc_info=True
"Selenium timed out requesting url %s%s",
url,
context_suffix,
exc_info=True,
)
raise
try:
# chart containers didn't render
logger.debug("Wait for chart containers to draw at url: %s", url)
logger.debug(
"Wait for chart containers to draw at url: %s%s",
url,
context_suffix,
)
WebDriverWait(driver, self._screenshot_locate_wait).until(
EC.visibility_of_all_elements_located(
(By.CLASS_NAME, "chart-container")
)
)
except TimeoutException:
logger.info("Timeout Exception caught")
logger.info("Timeout Exception caught%s", context_suffix)
# Fallback to allow a screenshot of an empty dashboard
try:
WebDriverWait(driver, 0).until(
@@ -1013,8 +1070,9 @@ class WebDriverSelenium(WebDriverProxy):
)
except Exception:
logger.warning(
"Selenium timed out waiting for dashboard to draw at url %s",
"Selenium timed out waiting for dashboard to draw at url %s%s",
url,
context_suffix,
exc_info=True,
)
raise
@@ -1022,36 +1080,47 @@ class WebDriverSelenium(WebDriverProxy):
try:
# charts took too long to load
logger.debug(
"Wait for loading element of charts to be gone at url: %s", url
"Wait for loading element of charts to be gone at url: %s%s",
url,
context_suffix,
)
WebDriverWait(driver, self._screenshot_load_wait).until_not(
EC.presence_of_all_elements_located((By.CLASS_NAME, "loading"))
)
except TimeoutException:
logger.warning(
"Selenium timed out waiting for charts to load at url %s",
"Selenium timed out waiting for charts to load at url %s%s",
url,
context_suffix,
exc_info=True,
)
raise
selenium_animation_wait = app.config["SCREENSHOT_SELENIUM_ANIMATION_WAIT"]
logger.debug("Wait %i seconds for chart animation", selenium_animation_wait)
logger.debug(
"Wait %i seconds for chart animation%s",
selenium_animation_wait,
context_suffix,
)
sleep(selenium_animation_wait)
logger.debug(
"Taking a PNG screenshot of url %s as user %s",
"Taking a PNG screenshot of url %s as user %s%s",
url,
self._user.username if self._user else "None",
context_suffix,
)
if app.config["SCREENSHOT_REPLACE_UNEXPECTED_ERRORS"]:
unexpected_errors = WebDriverSelenium.find_unexpected_errors(driver)
unexpected_errors = WebDriverSelenium.find_unexpected_errors(
driver, log_context=log_context
)
if unexpected_errors:
logger.warning(
"%i errors found in the screenshot. URL: %s. Errors are: %s",
"%i errors found in the screenshot. URL: %s. Errors are: %s%s",
len(unexpected_errors),
url,
unexpected_errors,
context_suffix,
)
img = element.screenshot_as_png
@@ -1060,19 +1129,21 @@ class WebDriverSelenium(WebDriverProxy):
raise
except StaleElementReferenceException:
logger.warning(
"Selenium got a stale element while requesting url %s",
"Selenium got a stale element while requesting url %s%s",
url,
context_suffix,
exc_info=True,
)
raise
except WebDriverException:
logger.warning(
"Encountered an unexpected error when requesting url %s",
"Encountered an unexpected error when requesting url %s%s",
url,
context_suffix,
exc_info=True,
)
raise
except Exception as ex:
logger.warning("exception in webdriver", exc_info=ex)
logger.warning("exception in webdriver%s", context_suffix, exc_info=ex)
raise
return img

View File

@@ -126,6 +126,27 @@ class TestComputeAndCache:
cache_payload: ScreenshotCachePayloadType = screenshot_obj.cache.get("key")
assert cache_payload["status"] == "Updated"
def test_passes_cache_key_log_context_to_capture(
self, mocker: MockerFixture, screenshot_obj
):
"""compute_and_cache must thread its cache_key into the capture layer
as log_context, so every webdriver/screenshot log line produced by a
thumbnail or direct-download run can be traced back to the exact
cached entry it was computing (reports already do this with their
execution_id)."""
mocks = self._setup_compute_and_cache(mocker, screenshot_obj)
cache_key = screenshot_obj.get_cache_key()
screenshot_obj.compute_and_cache(force=False)
get_screenshot: MagicMock = mocks.get("get_screenshot")
get_screenshot.assert_called_once()
assert (
get_screenshot.call_args.kwargs["log_context"] == f"cache_key={cache_key}"
)
resize_image: MagicMock = mocks.get("resize_image")
resize_image.assert_called_once()
assert resize_image.call_args.kwargs["log_context"] == f"cache_key={cache_key}"
def test_screenshot_error(self, mocker: MockerFixture, screenshot_obj):
mocks = self._setup_compute_and_cache(mocker, screenshot_obj)
get_screenshot: MagicMock = mocks.get("get_screenshot")

View File

@@ -231,10 +231,10 @@ class TestTakeTiledScreenshot:
# Should log dashboard dimensions with lazy logging format
mock_logger.info.assert_any_call(
"Dashboard: %sx%spx at (%s, %s)", 800, 5000, 50, 100
"Dashboard: %sx%spx at (%s, %s)%s", 800, 5000, 50, 100, ""
)
# Should log number of tiles with lazy logging format
mock_logger.info.assert_any_call("Taking %s screenshot tiles", 3)
mock_logger.info.assert_any_call("Taking %s screenshot tiles%s", 3, "")
def test_exception_handling_returns_none(self):
"""Test that exceptions are handled and None is returned."""

View File

@@ -627,7 +627,7 @@ class TestWebDriverPlaywrightErrorHandling:
assert result == []
mock_logger.exception.assert_called_once_with(
"Failed to capture unexpected errors"
"Failed to capture unexpected errors%s", ""
)
@patch("superset.utils.webdriver.PLAYWRIGHT_AVAILABLE", True)
@@ -838,7 +838,7 @@ class TestWebDriverPlaywrightErrorHandling:
# not installed) accepting unrelated exceptions.
assert exc_info.value is timeout
mock_logger.exception.assert_any_call(
"Timed out requesting url %s", "http://example.com"
"Timed out requesting url %s%s", "http://example.com", ""
)
@patch("superset.utils.webdriver.PLAYWRIGHT_AVAILABLE", True)
@@ -923,11 +923,12 @@ class TestWebDriverPlaywrightErrorHandling:
# the benign/expected case and must not be logged as a WARNING.
mock_logger.debug.assert_any_call(
"Could not determine dashboard height for element %s "
"at url %s (%s chart containers found); %s",
"at url %s (%s chart containers found); %s%s",
"dashboard",
"http://example.com",
1,
"falling back to standard screenshot behavior",
"",
)
assert not any(
call.args and "Could not determine dashboard height" in call.args[0]
@@ -1013,11 +1014,12 @@ class TestWebDriverPlaywrightErrorHandling:
mock_take_tiled.assert_called_once()
mock_logger.warning.assert_any_call(
"Could not determine dashboard height for element %s "
"at url %s (%s chart containers found); %s",
"at url %s (%s chart containers found); %s%s",
"dashboard",
"http://example.com",
25,
"attempting tiled screenshot anyway",
"",
)
@patch("superset.utils.webdriver.PLAYWRIGHT_AVAILABLE", True)
@@ -1091,10 +1093,11 @@ class TestWebDriverPlaywrightErrorHandling:
assert exc_info.value is timeout
mock_logger.warning.assert_any_call(
"Timed out waiting for chart containers to draw at url %s "
"(%s of %s chart containers rendered before the timeout)",
"(%s of %s chart containers rendered before the timeout)%s",
"http://example.com",
1,
2,
"",
exc_info=True,
)
mock_logger.exception.assert_not_called()
@@ -1172,8 +1175,9 @@ class TestWebDriverPlaywrightErrorHandling:
mock_element.screenshot.assert_not_called()
mock_logger.warning.assert_any_call(
"Tiled screenshot failed for url %s and no safe fallback "
"exists; failing the capture",
"exists; failing the capture%s",
"http://example.com",
"",
)