fix(dashboards): Don't 500 when validating with an empty project list - #120998
Open
skaasten wants to merge 2 commits into
Open
fix(dashboards): Don't 500 when validating with an empty project list#120998skaasten wants to merge 2 commits into
skaasten wants to merge 2 commits into
Conversation
Widget query validation built the query params from the serializer's `projects` context. That list is empty whenever a request reaches the endpoint without a `project` query param and `get_projects` resolves to nothing, since it then filters by team membership. `resolve_params` raised `UnqualifiedQueryError`, which subclasses `SnubaError` rather than `InvalidSearchQuery`, so none of the existing `except` clauses caught it and it surfaced as a 500. Widget queries are validated for syntactic correctness, which doesn't depend on which projects are in scope — the project list only exists to satisfy the query builder. Fall back to any active project in the organization when the context list is empty, mirroring the workaround already used in `dashboards/on_completion_hook.py`, and catch `UnqualifiedQueryError` so a request can't 500 even when no fallback project exists. The new handler catches `UnqualifiedQueryError` specifically rather than `SnubaError`, and reports a fixed message instead of the exception text. Sibling `SnubaError` types (`SchemaValidationError`, the ClickHouse code map, `QueryConnectionFailed`) carry Snuba and ClickHouse internals that should not reach API consumers, and infrastructure failures such as `RateLimitExceeded` should not be reported as field validation errors. Observed on both the create and details endpoints, across several organizations. The exact reason the resolved list was empty varies per request and isn't recorded in the events, so this fixes the failure mode rather than any single precondition. Fixes SENTRY-44CF Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_get_validation_project_ids runs once per widget query, so an empty project context triggered the same fallback lookup up to MAX_WIDGETS times per request. Cache it in the serializer context, which DRF shares across the serializer tree for the life of the request. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes SENTRY-44CF, and the frontend error it surfaces as, JAVASCRIPT-39NM (DAIN-1803).
Problem
POST /organizations/{org}/dashboards/returned a 500 whenever the resolved project list was empty:Widget query validation builds its params from
context["projects"], which comes fromget_projects(request, organization). With noprojectquery param that filters by team membership (auth/access.py:181) — buthas_project_accessalso grants access viahas_global_access(allow_joinleave OR role.is_global). So a teamless member of an open-membership org resolves to[]despite being entitled to every project.resolve_paramsthen raisedUnqualifiedQueryError, which subclassesSnubaErrorrather thanInvalidSearchQuery, so none of the existingexceptclauses caught it.This isn't specific to the Seer flow in the linked issue — five call sites share this serializer, and both the create and edit endpoints were affected.
Fix
Two layers in
DashboardWidgetQuerySerializer.validate:_get_validation_project_ids()— when the context list is empty, borrow any single active project in the organization. Widget queries are validated for syntactic correctness, which doesn't depend on project scope; the list only exists to satisfy the query builder. Mirrors the existing workaround indashboards/on_completion_hook.py:83-87.except UnqualifiedQueryError— if no project exists at all, return a validation error instead of a 500.resolve_paramsitself is untouched — it's widely shared and its empty-project guard is intentional.Notes for review
UnqualifiedQueryError, notSnubaError, with a fixed message. Sibling types carry Snuba/ClickHouse internals that shouldn't reach API consumers, and infrastructure failures likeRateLimitExceededshouldn't surface as field validation errors.Testing
Three regression tests, all confirmed to fail against the parent commit:
resolve_paramsonly raises outside the test environment (base.py:592), so these patchin_test_environment. Without that patch they pass against unfixed code — worth knowing if they're ever edited.Verification
🤖 Generated with Claude Code