feat(issues): Add issue.autofix_state search key for autofix overview#120293
Conversation
Each section is now its own issue-stream query against the new search key with true counts from X-Hits, replacing the client-side classification that required two Seer round trips per issue. Card content (prose, diffs, PR state) loads viewport-lazily per card. Removes the Outcome and Needs-attention filters and page-level pagination; sections fetch up to 100 issues each. Requires the issue.autofix_state backend PR (#120293) to land first.
Classifies issues into mutually exclusive autofix pipeline states from SEER_* activity rows and the seer-run pull request join, for the upcoming issue.autofix_state search key.
Postgres-only search key over the autofix state filter so the autofix overview can fetch each section as a filtered issue-stream query with true counts.
Exercises milestone exclusivity, merged-PR precedence, recency-window fallback, multi-value union, and invalid-value rejection through the search backend.
A single null group_id in the milestone subquery turns the negated states' NOT IN into an always-unknown predicate, silently emptying every section that negates it. Unreachable today (Seer activities always carry a group) but guarded like the merged-PR join already is.
cfea846 to
1366e5c
Compare
Verifies the search backend excludes matching groups when the autofix state filter is negated, guarding the QCallbackCondition exclude path end to end.
A previously merged Seer PR permanently classified a group as merged, hiding newer runs whose PRs still await review. Restrict the merged check to each group's most recent autofix run so re-runs after a regression surface again.
Non-autofix agent runs (chat, explorer) on the same group could become the latest run and knock a merged autofix PR out of the merged state.
trevor-e
left a comment
There was a problem hiding this comment.
Approving to unblock you since this isn't wired up anywhere but I would confirm with the issues team before merging. I don't know the downstream ramifications of registering a new search key. I would look into revising the query before merging as well.
| coding = _milestone_q(ActivityType.SEER_CODING_COMPLETED, projects) | ||
| solution = _milestone_q(ActivityType.SEER_SOLUTION_COMPLETED, projects) | ||
|
|
||
| conditions: dict[str, Q] = { |
There was a problem hiding this comment.
I think this is going to result in a pretty inefficient query that might not work in prod. I tested compiling the SQL for this and it repeats the JOIN and IN parts several times. You may want to pull some of this into a CTE first and go from there.
There was a problem hiding this comment.
I've now collapsed the repeated milestone queries into a single GROUP BY group_id HAVING scan per state (a95ed1d), since Django's ORM can't express CTEs.
Worst-case single-state query went from 5 subqueries (3 duplicate Activity scans) to 3, and each compiles to hashed subplans over the (project_id, type) index so they execute once per query, not per row. The UI only ever queries one state at a time, so that bounded shape is the hot path.
FWIW though the subplans are hashed and run once per query, not per row, each over the (project_id, type) index with the candidate set capped at 5k, so even without the consolidation, it was already bounded and should still be fast from what i can tell
Remove the constant-equality frozenset test and the e2e precedence test that duplicated unit-level coverage.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 58a914f. Configure here.
Convert the loop-based converter test to pytest parametrize driven by AUTOFIX_STATE_VALUES, and widen the converter user annotation to match sibling converters.
Each autofix state compiled up to three separate Activity subqueries scanning the same rows. Aggregate milestone flags with GROUP BY group_id HAVING so every state needs at most one Activity scan, cutting the worst single-state query from five subqueries to three.
An empty list compiled to a blank Q() that matched every group. Return a match-nothing filter instead so callers outside the search pipeline cannot select everything by accident.

Adds a Postgres-only issue-search key,
issue.autofix_state, that classifies issues into five mutually exclusive autofix pipeline states:merged— the group's latest autofix run has a merged PR (via theSeerAgentRun → SeerRun → SeerRunPullRequest → PullRequest.statejoin, webhook-fresh and flag-free)review_pr—SEER_PR_CREATEDactivity exists, PR not mergedcode_changes_ready—SEER_CODING_COMPLETED, no PR yetsolution_ready—SEER_SOLUTION_COMPLETED, no code yetneeds_investigation— a Seer run in the last 30 days with none of the aboveThe autofix overview page currently classifies issues client-side by fetching each issue's full run state from Seer (two Seer round trips per issue per page load), which makes true per-section counts and server-side filtering impossible. With this key, each overview section becomes a plain filtered issue-stream query with real counts from
X-Hits. Milestone signals come from theSEER_*activity rows Sentry already records when Seer pushes step-completion events; precedence is baked into the conditions so every seer-touched issue lands in exactly one state.The filter logic lives in a Seer-owned module (
sentry/seer/autofix/issue_search.py); registration mirrorsissue.progressat the standard three points (value converter,QCallbackCondition,SKIP_SNUBA_FIELDS). No migration (the join is already indexed), no new flags, no behavior change for any existing query.Performance notes (verified against compiled SQL + prod indexes): milestone subqueries hit Activity's
(project_id, type)index and plan as hashed anti-joins (NULL-guarded); the Postgres candidate query is capped at 5k. Two known plan-shape assumptions: the merged join relies on the planner anchoring onSeerAgentRun.project_id(PullRequest.stateis unindexed), andneeds_investigationhas no positive anchor — its scan is bounded by the standard issue-search indexes, same cost model asissue.progress.The frontend consumer (per-section fetches on the autofix overview) follows in a separate PR.