diff --git a/superset/utils/screenshot_utils.py b/superset/utils/screenshot_utils.py index 6b87f7312202..121fc6b04439 100644 --- a/superset/utils/screenshot_utils.py +++ b/superset/utils/screenshot_utils.py @@ -261,12 +261,15 @@ def combine_screenshot_tiles( screenshot_tiles: list[bytes], *, allow_partial_fallback: bool = True, + 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 @@ -277,6 +280,7 @@ def combine_screenshot_tiles( 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] @@ -300,7 +304,7 @@ def combine_screenshot_tiles( 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) if allow_partial_fallback: # Preserve the historical thumbnail behavior. Scheduled reports # opt out because delivering only the first tile is incomplete. @@ -475,16 +479,17 @@ def _timeout_seconds( 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: list[bytes] = [] @@ -525,7 +530,11 @@ def _raise_if_budget_exhausted() -> None: 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) @@ -688,13 +697,14 @@ def _raise_if_budget_exhausted() -> None: 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 @@ -730,7 +740,13 @@ def _raise_if_budget_exhausted() -> None: ) 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 try: @@ -761,10 +777,11 @@ def _raise_if_budget_exhausted() -> None: f"{remaining:.2f}" if remaining is not None else None, context_suffix, ) - logger.info("Combining screenshot tiles...") + logger.info("Combining screenshot tiles...%s", context_suffix) combined_screenshot = combine_screenshot_tiles( screenshot_tiles, allow_partial_fallback=report_execution_context is None, + log_context=log_context, ) return combined_screenshot diff --git a/superset/utils/screenshots.py b/superset/utils/screenshots.py index 168816135ca2..5ebfb85125d8 100644 --- a/superset/utils/screenshots.py +++ b/superset/utils/screenshots.py @@ -218,7 +218,10 @@ def __init__(self, url: str, digest: str | None): 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"): @@ -227,11 +230,13 @@ def driver( 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 @@ -244,7 +249,7 @@ def get_screenshot( log_context: str | None = None, report_execution_context: ReportExecutionContext | 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, @@ -351,22 +356,38 @@ def compute_and_cache( # pylint: disable=too-many-arguments 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 @@ -398,7 +419,9 @@ def compute_and_cache( # pylint: disable=too-many-arguments 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( @@ -413,16 +436,23 @@ def resize_image( 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": diff --git a/superset/utils/webdriver.py b/superset/utils/webdriver.py index d624a8dba2c0..c16e71f256bd 100644 --- a/superset/utils/webdriver.py +++ b/superset/utils/webdriver.py @@ -240,14 +240,17 @@ def auth(user: User, context: BrowserContext) -> BrowserContext: ) @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: @@ -277,9 +280,12 @@ def find_unexpected_errors(page: Page) -> list[str]: [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 @@ -576,12 +582,14 @@ def get_screenshot( # pylint: disable=too-many-locals, too-many-statements # n "browser_setup", reserve_seconds=report_execution_context.readiness_reserve_seconds, ) + 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 @@ -626,9 +634,10 @@ def get_screenshot( # pylint: disable=too-many-locals, too-many-statements # n ) 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"] @@ -642,14 +651,19 @@ def get_screenshot( # pylint: disable=too-many-locals, too-many-statements # n ), ), ) - 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_timeout = ( @@ -670,14 +684,20 @@ def get_screenshot( # pylint: disable=too-many-locals, too-many-statements # n ) ) 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 @@ -709,10 +729,11 @@ def get_screenshot( # pylint: disable=too-many-locals, too-many-statements # n # 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 @@ -720,13 +741,16 @@ def get_screenshot( # pylint: disable=too-many-locals, too-many-statements # n "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) @@ -779,13 +803,14 @@ def get_screenshot( # pylint: disable=too-many-locals, too-many-statements # n ) 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 @@ -841,18 +866,20 @@ def get_screenshot( # pylint: disable=too-many-locals, too-many-statements # n 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 @@ -878,14 +905,16 @@ def get_screenshot( # pylint: disable=too-many-locals, too-many-statements # n ), ) 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, ) capture_timeout = ( report_execution_context.deadline.timeout_seconds( @@ -904,15 +933,17 @@ def get_screenshot( # pylint: disable=too-many-locals, too-many-statements # n timeout_seconds=capture_timeout, ) 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 @@ -938,14 +969,16 @@ def get_screenshot( # pylint: disable=too-many-locals, too-many-statements # n ), ) 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, ) capture_timeout = ( report_execution_context.deadline.timeout_seconds( @@ -964,16 +997,19 @@ def get_screenshot( # pylint: disable=too-many-locals, too-many-statements # n timeout_seconds=capture_timeout, ) 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() @@ -1177,13 +1213,18 @@ def _destroy(self, tries: int = 2) -> None: 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: @@ -1226,9 +1267,12 @@ def find_unexpected_errors(driver: WebDriver) -> list[str]: 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 @@ -1256,6 +1300,8 @@ def phase_timeout( ) return float(requested_seconds or self._screenshot_load_wait) + 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: @@ -1283,7 +1329,7 @@ def phase_timeout( report_execution_context.readiness_reserve_seconds, ), ) - 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 @@ -1294,7 +1340,10 @@ def phase_timeout( 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, @@ -1310,7 +1359,10 @@ def phase_timeout( ).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 @@ -1467,8 +1519,9 @@ def phase_timeout( ) 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 @@ -1483,22 +1536,30 @@ def phase_timeout( report_execution_context.readiness_reserve_seconds, ), ) - 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, ) if report_execution_context: @@ -1513,19 +1574,21 @@ def phase_timeout( 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 diff --git a/tests/unit_tests/utils/screenshot_test.py b/tests/unit_tests/utils/screenshot_test.py index c57df50484c8..c9445d2165e4 100644 --- a/tests/unit_tests/utils/screenshot_test.py +++ b/tests/unit_tests/utils/screenshot_test.py @@ -130,6 +130,27 @@ def test_happy_path(self, mocker: MockerFixture, screenshot_obj): 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") diff --git a/tests/unit_tests/utils/test_screenshot_utils.py b/tests/unit_tests/utils/test_screenshot_utils.py index 1e5c602cc8a0..cb772e45d78a 100644 --- a/tests/unit_tests/utils/test_screenshot_utils.py +++ b/tests/unit_tests/utils/test_screenshot_utils.py @@ -333,10 +333,10 @@ def test_logs_dashboard_info(self, mock_page): # 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.""" diff --git a/tests/unit_tests/utils/webdriver_test.py b/tests/unit_tests/utils/webdriver_test.py index 1a4cdf8d6de7..dbfd676935bc 100644 --- a/tests/unit_tests/utils/webdriver_test.py +++ b/tests/unit_tests/utils/webdriver_test.py @@ -772,7 +772,7 @@ def test_find_unexpected_errors_handles_playwright_error( 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) @@ -979,7 +979,7 @@ def test_get_screenshot_raises_on_element_wait_timeout( # 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) @@ -1064,11 +1064,12 @@ def evaluate_side_effect(script): # 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] @@ -1154,11 +1155,12 @@ def evaluate_side_effect(script): 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) @@ -1232,10 +1234,11 @@ def locator_side_effect(selector): 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()