Skip to content

fix(dashboard): make screenshot completion truthful - #44144

Merged
msyavuz merged 7 commits into
masterfrom
geido/screenshot-task-status-signalling
Sep 21, 2026
Merged

msyavuz merged 7 commits into
masterfrom
geido/screenshot-task-status-signalling

Conversation

@geido

@geido geido commented Sep 10, 2026 •

Copy link
Copy Markdown
Member

SUMMARY

Make API/UI dashboard screenshot completion truthful: Updated is persisted only with a successfully captured artifact after the selected dashboard state reaches a stable terminal render state.

  • Publish a fresh Pending generation before broker I/O, preserve prior artifact URLs, and coalesce concurrent forced requests.
  • Keep producer coordination bounded: observers poll only the small generation pointer for at most one second, while an abandoned two-second producer lease can be replaced by a later forced request.
  • Fence pointer publication with an atomic metadata-backed compare-and-set, so a producer whose lease expires during a slow cache write cannot overwrite its completed successor. Thumbnail backends, including custom/S3 caches, need no conditional-write support.
  • Opt only API/UI dashboard exports into the existing report-grade readiness, stability, virtualization, and blank-image checks; scheduled reports and ordinary thumbnails retain their existing behavior.
  • Persist Error for incomplete captures and setup/cache failures without overwriting output completed by another worker.
  • Preserve the existing request fields, Celery task signature, status values, and 202 producer / 200 observer convention.
  • Let the dashboard download hook consume the additive server task timeout so it does not abandon a valid long-running export.
  • Change no visualization-plugin code and add no plugin readiness interface.

The signal covers Superset's chart-holder lifecycle plus the existing ECharts/AG Grid paint signals and blank-image validation. Built-in or third-party plugins that finish asynchronously in canvas/WebGL without one of those signals, including map visualizations, retain their existing readiness behavior; defining a plugin-wide completion contract is separate work.

Other deliberate scope boundaries:

  • The existing download hook's retry accounting for generic non-404 GET failures is unchanged from master; this PR only consumes terminal task Error metadata returned with the screenshot endpoint's 404 response.
  • THUMBNAIL_COMPUTING_CACHE_TTL retains its documented requirement to exceed the Celery task's 300-second soft limit (default: 360 seconds). Validation of invalid custom configurations is separate from this fix.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

On exact master, a below-fold chart backed by a 45-second query and a 30-second screenshot wait was cached as Updated; GET returned an 11,337-byte PNG containing a loading spinner. With this change, the same fixture transitions Pending → Computing → Error and GET returns 404 in both standard and tiled modes.

A browser UI export from a non-first top-level tab completed as Updated and downloaded the selected chart.

TESTING INSTRUCTIONS

Automated and local verification on final commit c4d477abce:

  • The producer-lease, slow-Pending-write, atomic pointer-publication, forced-retry, cache-read, and contention regressions fail against their respective prior implementations and pass with the fixes.
  • 224 focused backend screenshot unit tests, 199 dashboard API/metadata-cache integration tests, and 7 download-hook tests pass.
  • Staged pre-commit gates pass (MyPy, Ruff, formatting, and every other applicable hook); the exact Pylint hook was run in the repository image because the host pre-commit environment did not contain the pylint executable, and rated the changed production files 10/10.
  • A production-style Docker image with Superset, PostgreSQL, Redis, Celery, Chromium, and Mailpit passed 11/11 cold API cases plus standard/tiled success and failure flows: valid PNG/PDF for non-first, nested, chart-free, tall, and multi-tile dashboards, and Error plus artifact GET 404 for a chart exceeding SCREENSHOT_LOAD_WAIT.
  • The broader visualization matrix passed for Word Cloud, DeckGL single/multi, Point Cluster, Cartodiagram, and multi-tile capture; this PR changes no readiness predicate or plugin code.
  • Twelve simultaneous forced POSTs coalesced to one producer and eleven observers on one generation, exactly one Celery task, and no 503; the task reached Updated and its PNG remained downloadable. A forced request after the coalescing grace created a fresh generation, while non-forced callers retained the normal computing TTL.
  • Real Celery schedules delivered a selected-second-tab PNG report, a selected-second-tab PDF report, and a triggered alert with valid screenshot artifacts; a false alert was recorded as not triggered and sent no email. The same cold 45-second chart rejected by the bounded API path completed successfully through the scheduled-report path in 47.36 seconds.

Manual verification:

  1. Enable THUMBNAILS and ENABLE_DASHBOARD_SCREENSHOT_ENDPOINTS, configure the screenshot cache/Celery, and start Superset plus a worker.
  2. POST /api/v1/dashboard/<id>/cache_dashboard_screenshot/ and record cache_key, dashboard_url, and image_url.
  3. Extract the permalink key from the last path segment of dashboard_url, then poll the same POST endpoint with {"permalinkKey": "<key>"} until task_status is terminal.
  4. Confirm successful work becomes Updated and the returned PNG/PDF URL is HTTP 200.
  5. Delay a selected-tab chart beyond SCREENSHOT_LOAD_WAIT; confirm terminal Error and artifact GET 404.
  6. Force-refresh a successful request; confirm the new generation is Pending while the prior generation URL remains downloadable.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags: THUMBNAILS, ENABLE_DASHBOARD_SCREENSHOT_ENDPOINTS
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • Migration is atomic, supports rollback & is backwards-compatible
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided
  • Introduces new feature or API: additive task status/timeout response metadata; no endpoint, request field, or status-code removal
  • Removes existing feature or API

The atomic request pointer reuses the existing metadata key-value table and requires no schema migration.

@github-actions github-actions Bot added api Related to the REST API plugins labels Sep 10, 2026
@codecov

codecov Bot commented Sep 10, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.80299% with 85 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.65%. Comparing base (fc01645) to head (c4d477a).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
superset/utils/screenshots.py 74.45% 29 Missing and 6 partials ⚠️
superset/dashboards/api.py 77.35% 21 Missing and 3 partials ⚠️
superset/extensions/metastore_cache.py 70.00% 9 Missing ⚠️
superset/utils/screenshot_utils.py 73.07% 5 Missing and 2 partials ⚠️
superset/utils/webdriver.py 90.74% 2 Missing and 3 partials ⚠️
superset/tasks/thumbnails.py 81.25% 1 Missing and 2 partials ⚠️
...ntend/src/dashboard/hooks/useDownloadScreenshot.ts 93.54% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #44144      +/-   ##
==========================================
- Coverage   80.65%   80.65%   -0.01%     
==========================================
  Files        2942     2942              
  Lines      175691   176029     +338     
  Branches    40788    40840      +52     
==========================================
+ Hits       141704   141971     +267     
- Misses      31325    31381      +56     
- Partials     2662     2677      +15     
Flag Coverage Δ
hive 37.07% <14.05%> (-0.08%) ⬇️
javascript 76.10% <93.54%> (-0.01%) ⬇️
mysql 56.37% <47.83%> (-0.01%) ⬇️
postgres 56.39% <47.83%> (-0.01%) ⬇️
presto 38.95% <14.05%> (-0.09%) ⬇️
python 84.94% <77.29%> (-0.02%) ⬇️
sqlite 56.10% <47.83%> (+<0.01%) ⬆️
unit 76.68% <49.72%> (-0.09%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@msyavuz msyavuz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Core fix looks right: persisting Pending before enqueue and coalescing repeat/force POSTs closes both the spinner-as-Updated case and the per-poll task storm (every poll on a cache miss enqueued a new task on master). Two asks: fix the openapi drift check, and consider splitting the report/deck.gl readiness changes into their own PR so the API state-machine fix can land and be backported alone. Note nothing here stops an Updated entry from being evicted between the POST and a later GET, so 404-after-Updated can still happen on eviction-heavy caches.

Comment thread superset/dashboards/api.py
Comment thread superset/utils/screenshot_utils.py Outdated
Comment thread superset-frontend/plugins/preset-chart-deckgl/src/DeckGLContainer.tsx Outdated
Comment thread superset/dashboards/api.py Outdated
Comment thread superset/dashboards/api.py Outdated
Comment thread superset/utils/screenshot_utils.py Outdated
Comment thread superset/tasks/thumbnails.py Outdated
Comment thread superset/utils/cache.py Outdated
@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Sep 11, 2026
@geido
geido force-pushed the geido/screenshot-task-status-signalling branch from 4a90704 to 3044ae8 Compare September 11, 2026 11:10
@github-actions github-actions Bot added the doc Namespace | Anything related to documentation label Sep 11, 2026
@netlify

netlify Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit c4d477a
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6ab132daca26e20008b3bd5f
😎 Deploy Preview https://deploy-preview-44144--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Sep 11, 2026

@msyavuz msyavuz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prior threads check out against the head: default readiness predicate is logic-equivalent to master and the report dwell path is unchanged, locks and the setup-failure fallback are ordered correctly, markers match the backend selectors, openapi matches the schemas. pending() still nulls a good artifact on force, but since the UI now GETs only after Updated that's no longer observable.

One recurring gap in the new code: every readiness marker only has loading | rendered, with no terminal failed value. So a map/style/tile/registry failure doesn't produce a prompt Error, it burns the full readiness budget and then errors. The four inline comments below are instances of that; a failed marker value the backend selector treats as terminal would fix all of them.

Comment thread superset-frontend/plugins/preset-chart-deckgl/src/DeckGLContainer.tsx Outdated
Comment thread superset-frontend/plugins/plugin-chart-point-cluster-map/src/MapLibre.tsx Outdated
Comment thread superset-frontend/plugins/plugin-chart-cartodiagram/src/components/OlChartMap.tsx Outdated
Comment thread superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts Outdated
Comment thread superset/utils/screenshots.py Outdated
Comment thread superset/utils/screenshot_utils.py
@geido
geido force-pushed the geido/screenshot-task-status-signalling branch from 45a1d09 to 6058626 Compare September 14, 2026 11:55
@github-actions github-actions Bot removed doc Namespace | Anything related to documentation plugins packages labels Sep 14, 2026
@geido
geido force-pushed the geido/screenshot-task-status-signalling branch from 217b79e to 25ec992 Compare September 14, 2026 15:29
@github-actions github-actions Bot added the doc Namespace | Anything related to documentation label Sep 14, 2026
@geido geido changed the title fix(dashboard): harden screenshot completion status fix(dashboard): make screenshot completion truthful Sep 14, 2026
@geido
geido marked this pull request as ready for review September 14, 2026 16:28
Comment thread superset/utils/screenshot_utils.py
Comment thread superset/utils/screenshot_utils.py Outdated
Comment thread superset/dashboards/api.py Outdated
Comment thread superset/utils/webdriver.py
@bito-code-review

bito-code-review Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #6d771c

Actionable Suggestions - 0
Additional Suggestions - 5
  • superset/utils/webdriver.py - 1
    • Readiness gate always fires · Line 355-356
      `page.evaluate(capture_readiness_predicate)` passes an arrow-function *expression* string (e.g. `() => {...}`). Playwright evaluates it as an expression, yielding a function object that cannot be serialized, so it returns `undefined`. `not bool(undefined)` is always `True`, so this gate always discards the capture and raises `ScreenshotCaptureReadinessChangedError` when `require_complete_capture` is set. Wrap in an IIFE: `page.evaluate(f"({capture_readiness_predicate})()")`.
  • superset/dashboards/api.py - 1
    • Blocking lock poll loop · Line 2153-2153
      The `while True` loop blocks the request worker for up to `SCREENSHOT_API_LOCK_WAIT_SECONDS` (1s) via `time.sleep` while polling `DistributedLock`. Under concurrent screenshot requests this can exhaust synchronous gunicorn workers. Consider returning 202/503 immediately on `LockAlreadyHeldException` and letting clients poll the status endpoint instead of sleeping in the handler.
  • superset/utils/screenshots.py - 2
    • Cache error now 500 · Line 356-361
      `get_from_cache_key` now raises `ScreenshotCacheError` on cache read failure instead of returning None. The producer path (`get_current_generation`, api.py:2021) handles this with a 503, but the `thumbnail` trigger path (api.py:2335-2336) is unwrapped, so a transient cache backend error now surfaces as a generic 500 via `@safe` instead of the previous graceful async fallback (202). Consider handling it consistently.
    • Inconsistent write check · Line 497-500
      When `require_complete_capture` is False, the write uses raw `self.cache.set(...)` (line 500) without the `stored is False` failure check that `store_cache_payload` applies. This leaves the non-complete-capture path (charts) unable to detect a rejected write, diverging from the new guarded path.
  • superset/utils/screenshot_utils.py - 1
    • Fail-closed gate skips reports · Line 1361-1361
      This fail-closed check is gated on `require_complete_capture`, which is `False` for report execution (`execute.py` builds `DashboardScreenshot` without it), even though reports set `strict_capture=True` (line 851). So report tiles captured during a render transition are never discarded here. Consider gating on `strict_capture` to match the pre-capture stability intent.
Review Details
  • Files reviewed - 11 · Commit Range: 7eaaa95..25ec992
    • superset/dashboards/api.py
    • superset/tasks/thumbnails.py
    • superset/utils/screenshot_utils.py
    • superset/utils/screenshots.py
    • superset/utils/webdriver.py
    • tests/integration_tests/dashboards/api_tests.py
    • tests/unit_tests/tasks/test_thumbnails.py
    • tests/unit_tests/utils/screenshot_test.py
    • tests/unit_tests/utils/test_screenshot_cache_fix.py
    • tests/unit_tests/utils/test_screenshot_utils.py
    • tests/unit_tests/utils/webdriver_test.py
  • Files skipped - 1
    • docs/static/resources/openapi.json - Reason: Filter setting
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@EnxDev EnxDev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I traced the generation lifecycle and capture changes at 25ec992 and found one cache-expiry case worth fixing below. I reproduced it with the PR's cache helpers and a controlled SimpleCache clock. The focused pytest run couldn't start because the configured SQLite test database was unavailable, so I haven't validated the full suite locally.

Comment thread superset/utils/screenshots.py
@geido
geido force-pushed the geido/screenshot-task-status-signalling branch from 25ec992 to a55d5eb Compare September 15, 2026 08:29
@geido
geido marked this pull request as draft September 15, 2026 08:29
Comment thread superset/dashboards/api.py
Comment thread superset/dashboards/api.py Outdated
Comment thread superset/utils/webdriver.py Outdated

@EnxDev EnxDev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing the generation-key reuse. I checked the UUID change and the expiry regression passes. This pass found one more case in the worker's cache-error handling, reproduced below. Focused validation: 204 tests passed; 10 failed because the isolated SQLite test database has no key_value table, so those lock-dependent checks remain unverified.

Comment thread superset/utils/screenshots.py Outdated
Comment thread superset/dashboards/api.py Outdated
@geido
geido marked this pull request as ready for review September 17, 2026 14:56

@bito-code-review bito-code-review Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Agent Run #20778e

Actionable Suggestions - 1
  • superset-frontend/src/dashboard/hooks/useDownloadScreenshot.test.ts - 1
Additional Suggestions - 17
  • superset/dashboards/api.py - 3
    • Long blocking retry loop · Line 2209-2220
      The `LockAlreadyHeldException` path sleeps `SCREENSHOT_API_LOCK_RETRY_SECONDS` and loops until `lock_deadline`, which is `producer_lock_ttl` (default 30s) ahead. Concurrent requests for the same dashboard can each block a web thread for up to ~30s and issue hundreds of lock polls, risking thread-pool exhaustion under load. Verify this is acceptable.
    • Magic timeout multiplier · Line 2010-2010
      `task_timeout_seconds = 2 * current_app.config["THUMBNAIL_COMPUTING_CACHE_TTL"]` hardcodes the business rule that the task timeout is twice the computing TTL. A named constant would document intent and avoid silent divergence if the TTL or multiplier changes.
    • Unreachable cache-key guard · Line 2077-2086
      The `if cache_key is None:` branch inside `if cached_payload is not None:` is unreachable: `get_current_generation()` returns `get_from_cache_key(cache_key)` only when `cache_key` is truthy, so a non-None `cached_payload` implies `cache_key` is set. Dead defensive code adds confusion about the invariant. Remove the block.
  • superset-frontend/src/dashboard/hooks/useDownloadScreenshot.test.ts - 1
    • Magic retry-count coupling · Line 254-254
      The mock resolves only when `getCalls > 31` (line 254) while the driver loop runs exactly 31 ticks (line 268); the bare 31 in both places encodes 'initial GET + 31 retries = 32 calls' against the 120 s budget (`Math.ceil(120000 / RETRY_INTERVAL) = 40`). Bumping either the mock threshold or `task_timeout_seconds` silently breaks this tightly coupled pair — name the shared bound in a constant.
  • tests/unit_tests/utils/test_screenshot_utils.py - 3
    • Duplicated evaluate stub across tests · Line 568-575
      This `evaluate` dispatch stub (scrollWidth / `CONTENTFUL_CHART_HOLDERS_IN_CLIP_JS` / requestAnimationFrame branches) is duplicated verbatim in the sibling new test at line 647 and in pre-existing `test_repeated_blank_tiles_fail_closed_for_reports` and `test_blank_combined_image_fails_closed_after_contentful_tiles_pass`. Extract a shared helper so a dispatch-contract change updates one place.
    • Missing docstring and type hints · Line 565-565
      BITO.md adaptive rule 12148 requires every newly added test function to carry a docstring documenting purpose and expected behavior, and rule 12898 requires complete type annotations including `-> None` on test methods. `test_repeated_blank_tiles_fail_closed_for_api_exports` has neither, unlike documented siblings such as `test_unready_holder_state_classification_embedded_in_js`. Add a docstring and `-> None`.
    • Missing docstring and type hints · Line 644-644
      Same org-mandated gaps as the sibling at line 565: BITO.md adaptive rule 12148 requires a docstring and rule 12898 requires full type annotations including `-> None` on every newly added test function. `test_blank_combined_image_fails_closed_for_api_exports` has neither. Add a docstring and `-> None`.
  • superset/utils/screenshots.py - 4
    • Duplicated strict-write branching · Line 515-518
      The strict/lenient write branch (`if self.require_complete_capture: store_cache_payload(...) else: cache.set(...)`) is duplicated at the Computing write (lines 515-518) and the terminal write (lines 583-601). A shared persist helper would keep both sites in sync as capture modes or backends evolve.
    • Inconsistent status check style · Line 587-587
      This diff introduces `is_updated()` and uses it in `mark_cache_error_if_incomplete` (line 443), but the fallback path in `compute_and_cache` still compares `cache_payload.status == StatusValues.UPDATED` directly. Using the helper here keeps terminal-state checks consistent in one place.
    • Too many arguments · Line 466-466
      The `compute_and_cache` method has too many arguments (6 > 5). Consider grouping related parameters into a configuration object or using keyword-only arguments to reduce the count.
    • Too many branches · Line 466-466
      The `compute_and_cache` method has too many branches (16 > 12). Consider extracting sub-logic into separate methods to improve readability.
  • tests/unit_tests/utils/test_screenshot_cache_fix.py - 2
    • Missing test docstrings · Line 360-388
      The two new test methods lack docstrings, while every sibling test in this class (e.g. `test_no_trigger_on_fresh_error`) documents its scenario and expected outcome. Per BITO.md adaptive rule 12148, add a docstring to each new test method.
    • Misplaced test class · Line 360-388
      These two tests exercise `should_enqueue_task`, but they are placed inside `TestShouldTriggerTask` (whose docstring says it tests `should_trigger_task`). Grouping tests by the method under test keeps the suite navigable; consider a dedicated `TestShouldEnqueueTask` class.
  • tests/unit_tests/utils/screenshot_test.py - 4
    • Missing test docstrings · Line 159-200
      Org rule 12148 requires every newly added test function to include a docstring documenting purpose and expected behavior. The three new `mark_cache_error_if_incomplete` tests have none, while older tests here (e.g. `test_get_cache_key`) carry them. One-line docstrings keep the suite consistent with the standard.
    • Unannotated mock variables · Line 162-162
      Org rules 12787/13153 require explicit annotations on mock and local variables in test files, even when inferable. `cache = MagicMock()` (lines 162, 178, 193) and `stored_payload = cache.set.call_args.args[1]` (line 185) are unannotated; `cache: MagicMock` and `stored_payload: ScreenshotCachePayloadType` satisfy the rules.
    • Missing test docstrings · Line 229-248
      Org rule 12148 requires every newly added test function to include a docstring. The two new `get_from_cache_key` failure-path tests (`test_cache_read_failure_preserves_legacy_exception_behavior`, `test_cache_read_failure_can_be_distinguished_by_api`) have none; one-line docstrings documenting the default-vs-`raise_on_error` contract keep them consistent with the standard.
    • Unannotated mock variables · Line 230-230
      Org rule 12787 requires explicit type annotations on mock variables. `cache = MagicMock()` (lines 232 and 243) is unannotated; `cache: MagicMock = MagicMock()` satisfies the rule and matches the typed-parameter style these new tests already use for `monkeypatch`.
Review Details
  • Files reviewed - 14 · Commit Range: 18dadf2..fe43f35
    • superset-frontend/src/dashboard/hooks/useDownloadScreenshot.test.ts
    • superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts
    • superset/dashboards/api.py
    • superset/dashboards/schemas.py
    • superset/tasks/thumbnails.py
    • superset/utils/screenshot_utils.py
    • superset/utils/screenshots.py
    • superset/utils/webdriver.py
    • tests/integration_tests/dashboards/api_tests.py
    • tests/unit_tests/tasks/test_thumbnails.py
    • tests/unit_tests/utils/screenshot_test.py
    • tests/unit_tests/utils/test_screenshot_cache_fix.py
    • tests/unit_tests/utils/test_screenshot_utils.py
    • tests/unit_tests/utils/webdriver_test.py
  • Files skipped - 1
    • docs/static/resources/openapi.json - Reason: Filter setting
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Comment thread superset-frontend/src/dashboard/hooks/useDownloadScreenshot.test.ts
@geido
geido force-pushed the geido/screenshot-task-status-signalling branch from fe43f35 to 242940d Compare September 18, 2026 09:16

@EnxDev EnxDev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The longer contention wait helps callers join a slow producer. I found one remaining lease-expiry race in pointer publication, detailed inline. I reproduced it with the actual handler body and cache helpers, using a controlled clock and a lock double that models TTL expiry and ownership-safe release.

Validation: all 221 focused backend tests passed with the missing key_value table supplied in isolated SQLite, and all 7 download-hook tests passed on Node 24. API integration tests and live browser exports remain unverified locally.

Comment thread superset/dashboards/api.py Outdated
Comment thread superset/dashboards/api.py Outdated
Comment thread superset/dashboards/api.py Outdated
Comment thread superset/utils/screenshots.py Outdated
@bito-code-review

bito-code-review Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #d97c30

Actionable Suggestions - 0
Additional Suggestions - 5
  • superset-frontend/src/dashboard/hooks/useDownloadScreenshot.test.ts - 1
    • Duplicated URL/click stub setup · Line 290-296
      This test repeats the URL/click stub setup verbatim from the 'triggers only one download' test (`Object.assign(window.URL, ...)` plus the `HTMLAnchorElement.prototype.click` spy at lines 205-211). Extract a shared helper (e.g. `stubUrlAndClick()`) so future changes to the stub shape update one place; divergent copies could make tests stub download plumbing differently and mask regressions.
  • tests/unit_tests/utils/screenshot_test.py - 1
    • Duplicated test setup · Line 159-200
      The three `test_mark_cache_error_if_incomplete_*` tests (159-200) repeat the identical setup: `cache = MagicMock()`, `monkeypatch.setattr(BaseScreenshot, "cache", cache)`, and `with patch("...DistributedLock")`. Consider a shared helper/fixture to avoid divergence when the setup changes.
  • tests/unit_tests/utils/test_screenshot_utils.py - 1
    • Duplicated evaluate stub · Line 568-575
      The `evaluate` stub (scrollWidth/CONTENTFUL_CHART_HOLDERS_IN_CLIP_JS/rAF branches returning the same values) is duplicated verbatim between `test_repeated_blank_tiles_fail_closed_for_api_exports` and `test_blank_combined_image_fails_closed_for_api_exports`, mirroring the same copy pattern in `test_blank_tile_forces_repaint_and_retries`. Extract one shared helper so a future change to the stubbed JS contract updates a single place instead of four.
  • superset/dashboards/api.py - 1
    • Function too complex with many returns · Line 1905-1905
      The `cache_dashboard_screenshot` method exceeds complexity limits: 17 return statements (PLR0911), 22 branches (PLR0912), and 81 statements (PLR0915). Consider extracting helper methods for lock acquisition, cache generation, and response building to reduce complexity.
  • superset/utils/webdriver.py - 1
    • Too many arguments in function · Line 272-272
      `_get_validated_screenshot` has 10 arguments (PLR0913), too many branches (PLR0912), and too many statements (PLR0915). Consider refactoring into smaller functions or using a configuration object.
Review Details
  • Files reviewed - 14 · Commit Range: 07fd55a..242940d
    • superset-frontend/src/dashboard/hooks/useDownloadScreenshot.test.ts
    • superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts
    • superset/dashboards/api.py
    • superset/dashboards/schemas.py
    • superset/tasks/thumbnails.py
    • superset/utils/screenshot_utils.py
    • superset/utils/screenshots.py
    • superset/utils/webdriver.py
    • tests/integration_tests/dashboards/api_tests.py
    • tests/unit_tests/tasks/test_thumbnails.py
    • tests/unit_tests/utils/screenshot_test.py
    • tests/unit_tests/utils/test_screenshot_cache_fix.py
    • tests/unit_tests/utils/test_screenshot_utils.py
    • tests/unit_tests/utils/webdriver_test.py
  • Files skipped - 1
    • docs/static/resources/openapi.json - Reason: Filter setting
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@geido
geido force-pushed the geido/screenshot-task-status-signalling branch from 242940d to a843e1e Compare September 18, 2026 14:30
@bito-code-review

bito-code-review Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #447698

Actionable Suggestions - 0
Additional Suggestions - 2
  • superset-frontend/src/dashboard/hooks/useDownloadScreenshot.test.ts - 1
    • Magic timeout encodes retry count · Line 230-230
      `mockPostSuccess(6)` feeds `task_timeout_seconds: 6` into `useDownloadScreenshot`, where `maxRetries = Math.ceil((6 * 1000) / RETRY_INTERVAL)` = 2; the loop at line 237 runs 3 ticks so the 3rd tick trips `retries >= maxRetries`. The `6` silently encodes the retry count — a reader must recompute the hook's formula to see why 3 iterations exhaust retries. A one-line comment or named constant preserves the fast test while making the coupling explicit.
  • tests/unit_tests/utils/test_screenshot_utils.py - 1
    • duplicated test setup · Line 565-598
      The two new tests copy the `evaluate` mock and setup verbatim from the sibling report-path tests (`test_repeated_blank_tiles_fail_closed_for_reports`, `test_blank_combined_image_fails_closed_after_contentful_tiles_pass`); the file already holds 16 copies of `def evaluate(script`. A shared helper/fixture would make the API-export vs report-path difference (just `require_complete_capture=True`) explicit and reduce divergence risk.
Review Details
  • Files reviewed - 14 · Commit Range: a829d42..a843e1e
    • superset-frontend/src/dashboard/hooks/useDownloadScreenshot.test.ts
    • superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts
    • superset/dashboards/api.py
    • superset/dashboards/schemas.py
    • superset/tasks/thumbnails.py
    • superset/utils/screenshot_utils.py
    • superset/utils/screenshots.py
    • superset/utils/webdriver.py
    • tests/integration_tests/dashboards/api_tests.py
    • tests/unit_tests/tasks/test_thumbnails.py
    • tests/unit_tests/utils/screenshot_test.py
    • tests/unit_tests/utils/test_screenshot_cache_fix.py
    • tests/unit_tests/utils/test_screenshot_utils.py
    • tests/unit_tests/utils/webdriver_test.py
  • Files skipped - 1
    • docs/static/resources/openapi.json - Reason: Filter setting
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@EnxDev EnxDev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rechecked the latest revision and found one remaining lease-expiry case during cache I/O, detailed inline with a controlled reproduction.

Validation: all 222 focused backend screenshot tests and all 7 download-hook tests passed locally. API integration tests and live browser exports weren't run.

Comment thread superset/dashboards/api.py Outdated
@geido
geido force-pushed the geido/screenshot-task-status-signalling branch from a843e1e to c4d477a Compare September 21, 2026 13:36

@bito-code-review bito-code-review Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Agent Run #83ad27

Actionable Suggestions - 3
  • superset/extensions/metastore_cache.py - 1
  • tests/unit_tests/utils/screenshot_test.py - 1
  • superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts - 1
Additional Suggestions - 9
  • superset/utils/screenshots.py - 1
    • Pointer cache on base class · Line 306-306
      `api_pointer_cache` is consumed only by `DashboardScreenshot`'s pointer methods (`_get_api_pointer_cache`, `set_current_api_generation_cache_key`), yet it is declared on shared base `BaseScreenshot` next to `cache`/`cache_scope`, which both chart and dashboard flows use. Declaring it on `DashboardScreenshot` keeps this dashboard-only test seam beside its only consumers.
  • superset-frontend/src/dashboard/hooks/useDownloadScreenshot.test.ts - 1
    • Weak test assertion · Line 286-286
      This test drives 31 retries (93s), below both the server budget (120s -> 40 retries) and the default (360s -> 120 retries), so it passes even if `task_timeout_seconds` is ignored. It doesn't verify the server budget overrides the default. Drive >120 retries or assert exhaustion at a server-budget count smaller than default to actually test the feature.
  • superset/extensions/metastore_cache.py - 3
    • Redundant duplicate except clause · Line 181-187
      `except (SQLAlchemyError, KeyValueCreateFailedError)` and the following `except Exception` have identical bodies (rollback + raise), so the first clause is behaviorally redundant — codec and DAO failures alike fall through to the broad handler. Collapse to a single `except Exception:` handler to remove the duplicated rollback/raise block.
    • Disallow dynamic type in value · Line 117-117
      The `value` parameter is typed as `Any`, which is disallowed by `ANN401`. Consider using a more specific type or `object`. Note that similar issues exist for `expected` (line 118) and the return type of `get` (line 189).
    • Disallow dynamic type in expected · Line 118-118
      The `expected` parameter is typed as `Any`, which is disallowed by `ANN401`. This is similar to the issue on line 117; apply the same fix (e.g., use `object`).
  • tests/unit_tests/utils/test_screenshot_cache_fix.py - 1
    • Flaky timing-sensitive test · Line 399-406
      `fresh_payload` is built with `timestamp=now` (line 389) then asserted `should_enqueue_task(force=True, force_retry_after_seconds=1) is False`. `is_in_progress_stale(1)` returns `elapsed >= 1`, so if >1s elapses between construction and the assertion (slow/loaded CI), the result flips to True and the test fails. Widen the window or mock `datetime.now()`.
  • tests/unit_tests/utils/screenshot_test.py - 1
    • Test depends on ambient config · Line 110-124
      Unlike sibling driver tests (`TestBaseScreenshotDriverFallback`, `TestScreenshotSubclassesDriverBehavior`), this test never patches `PLAYWRIGHT_AVAILABLE` or the feature flag, so `BaseScreenshot.driver()` takes the fallback path and `isinstance(driver, WebDriverPlaywright)` depends on ambient config. Under the default fallback the assertion can fail; pin both to make the Playwright branch deterministic.
  • superset/dashboards/api.py - 2
    • Excessive return statements in function · Line 1913-1913
      The function `cache_dashboard_screenshot` has 22 return statements, exceeding the limit of 6. Consider refactoring into smaller helper functions to reduce complexity.
    • Disallowed dynamic type in kwargs · Line 1914-1914
      The `**kwargs` parameter uses `typing.Any`, which is disallowed by ANN401. Consider using a more specific type or adding a type ignore comment if the dynamic nature is intentional.
Filtered by Review Rules

Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.

  • tests/unit_tests/utils/test_screenshot_utils.py - 1
Review Details
  • Files reviewed - 16 · Commit Range: e96d9e1..c4d477a
    • superset-frontend/src/dashboard/hooks/useDownloadScreenshot.test.ts
    • superset-frontend/src/dashboard/hooks/useDownloadScreenshot.ts
    • superset/dashboards/api.py
    • superset/dashboards/schemas.py
    • superset/extensions/metastore_cache.py
    • superset/tasks/thumbnails.py
    • superset/utils/screenshot_utils.py
    • superset/utils/screenshots.py
    • superset/utils/webdriver.py
    • tests/integration_tests/dashboards/api_tests.py
    • tests/integration_tests/extensions/metastore_cache_test.py
    • tests/unit_tests/tasks/test_thumbnails.py
    • tests/unit_tests/utils/screenshot_test.py
    • tests/unit_tests/utils/test_screenshot_cache_fix.py
    • tests/unit_tests/utils/test_screenshot_utils.py
    • tests/unit_tests/utils/webdriver_test.py
  • Files skipped - 1
    • docs/static/resources/openapi.json - Reason: Filter setting
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Comment on lines +167 to +173
KeyValueDAO.create_entry(
resource=RESOURCE,
key=cache_key,
value=value,
codec=self.codec,
expires_on=expires_on,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CAS create skips audit fields

KeyValueDAO.create_entry sets only created_on/created_by_fk — never changed_on/changed_by_fk — so rows created by the expected is None branch get NULL audit fields, while the update branches (lines 137-142) populate both. Assign entry.changed_on = now and entry.changed_by_fk = get_user_id() on the returned entry before db.session.commit().

Code Review Run #83ad27


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

driver = screenshot.driver()

assert isinstance(driver, WebDriverPlaywright)
assert driver._require_complete_capture is True # pylint: disable=protected-access

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Private attribute coupling

Reaching into driver._require_complete_capture couples the test to a private attribute of WebDriverPlaywright (webdriver.py:198). A rename or refactor of the private field breaks this test even when behavior is unchanged; asserting the public contract (e.g. via patched constructor kwargs) is more robust.

Code Review Run #83ad27


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

Comment on lines +107 to +109
let maxRetries = Math.ceil(
(DEFAULT_SCREENSHOT_TASK_TIMEOUT_SECONDS * 1000) / RETRY_INTERVAL,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated retry math

The Math.ceil((timeoutSeconds * 1000) / RETRY_INTERVAL) computation is duplicated at lines 107-109 (default) and 231-233 (backend task_timeout_seconds). If the formula changes (e.g. adding a buffer or rounding policy), the two can diverge. Extract a small helper computeMaxRetries(timeoutSeconds) and call it from both sites.

Code Review Run #83ad27


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

@msyavuz
msyavuz merged commit fe2ed7b into master Sep 21, 2026
85 checks passed
@msyavuz
msyavuz deleted the geido/screenshot-task-status-signalling branch September 21, 2026 21:33
villebro pushed a commit that referenced this pull request Sep 22, 2026
villebro added a commit that referenced this pull request Sep 22, 2026
Native run of scripts/translations/babel_update.sh, superseding the
cherry-pick of #44509 (pure master-source regeneration, skipped) and
reconciling the auto-merged .po diffs from #44467/#44144/#44513/
#44286/#44508 with 7.0's actual translatable strings.
villebro added a commit that referenced this pull request Sep 23, 2026
Resolving #44144's import conflict took master's whole import block, but
DashboardDAO is only used there by #44184 (soft-delete), which isn't on 7.0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Related to the REST API doc Namespace | Anything related to documentation size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants