Repository navigation
docs(seer): Document Autofix 402/409 error responses - #131067
Conversation
Add typed 402 and 409 bodies to the startOrganizationIssueAutofix schema, with each code as an enum, so generated clients like @sentry/api can type them. The Literal types back the existing code constants, so mypy rejects a code that the schema doesn't list. No runtime behavior change. Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
BYK
left a comment
There was a problem hiding this comment.
Would it be possible to prevent this kind of an omission/drift with a lint rule?
|
@BYK Partly. A lint rule can catch most of the omission, but not all of it. The body drift needs a type-checker or test change. Missing statuses: a flake8 rule, similar to S021. In a method with Body drift: the mypy plugin is the better place. Complete coverage: a runtime check in tests. Validate each response from endpoint tests against If you want one of these, the flake8 status check is the smallest. I'd start with it. |
…rm (#131074) Requested by @BYK, following up on #131067 ([comment](#131067 (comment))). The new `AutofixConflictResponse` / `AutofixPaymentRequiredResponse` TypedDicts in #131067 don't actually constrain the 409/402 bodies. `GroupAutofixEndpoint.post` also returns `Response[ValidationErrorResponse]`, which is `dict[str, Any]`. When a dict literal matches no TypedDict arm, the plugin's union-narrowing hook returns mypy's default `Response[dict[str, str]]`, and that type-checks through the `dict[str, Any]` arm. So `{"detail": ..., "code": "bogus"}` in the 409 branch passed mypy. **Change.** In `tools/mypy_helpers/plugin.py`, a non-empty dict literal that matches none of the TypedDict arms in the declared union now gets a `[response-body]` error, but only when a loose (non-TypedDict) arm would have let it through. Without a loose arm, mypy already reports the incompatible return, so the plugin adds nothing. Two kinds of body are still accepted: literals that match an arm, and non-literal bodies like `as_validation_errors(serializer)` or a variable annotated `ValidationErrorResponse`. I also changed how the plugin checks literal values. Each one is now inferred against its declared field type, silently, the way mypy probes overloads. Before, a nested literal like `{"detail": {"message": ..., "code": ...}}` could never match a nested TypedDict, and with the new check that would have been a false positive. **Why not narrow `ValidationErrorResponse`?** Its docstring explains that `dict[str, list[str]]` loses DRF's nested error shapes. It's also the arm for real `serializer.errors` bodies, so the fix belongs in the literal check, not the alias. **Blast radius.** With the check on, a full `mypy` run reported 21 errors in 9 files. The value-inference change cleared one of those, a false positive on the nested `_CodedErrorResponse` in `organization_auth_tokens.py`. The other 20 were inline DRF-style field-error bodies such as `{"non_field_errors": [...]}`, `{"refs": [...]}` and `{"role": "..."}`. Each one now has a TypedDict arm, with no runtime change. I added `NonFieldErrorsResponse` to `sentry.apidocs.response_types` because two endpoints share that shape; the others are local `_...ErrorResponse` types. In `project_team_details.py`, the `ValidationErrorResponse` arm only covered one `{"detail": [...]}` literal, so I replaced it. getsentry uses this same plugin through its `tools` symlink, and its full `mypy` run is still clean. With this branch, putting `"code": "bogus"` in the `run_in_flight` 409 in `group_ai_autofix.py` fails with `[response-body]`. The unmodified file passes. On master, the bogus version passes. The regression tests in `tests/tools/mypy_helpers/test_plugin.py` cover: - a wrong `Literal` value - a typo'd key - correct literals, both raw and from a `Final` constant - the annotated-variable opt-out - nested TypedDicts - no duplicate error when there's no loose arm <!-- junior-request-attribution:start --> via @BYK. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:agent%3Ab8a5ce8078be0c8781dcf80f7ae2a39e --> -- [View Session](https://junior-prod.sentry.dev/conversations/agent%3Ab8a5ce8078be0c8781dcf80f7ae2a39e) [[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3Ab8a5ce8078be0c8781dcf80f7ae2a39e/?project=4510944073809921) <!-- junior-session-footer:end --> Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: BYK <126780+BYK@users.noreply.github.com>
…131072) #131067 had to add the 402 and 409 responses that the Seer Autofix start endpoint already returned but its `@extend_schema(responses=...)` never listed. Until then, `@sentry/api` typed those errors as `unknown`. This adds a flake8 rule, S034, that catches that kind of omission when the endpoint is written. Requested by @BYK after the review question on #131067. **What S034 checks:** a class method with `@extend_schema(responses={...})` that writes `return Response(..., status=...)` with a 4xx status, either a literal or a `status.HTTP_4xx_*` / `HTTP_4xx_*` name. If that status isn't a key in `responses=`, the rule fires. `(status, media_type)` tuple keys and digit-string keys count as documented. Run on the pre-#131067 version of `group_ai_autofix.py`, it flags all six of the 402/409 returns that PR documented. **What it doesn't check**, as the rule's docstring says, so passing doesn't mean every 4xx is documented: - a 4xx that comes back from a helper, e.g. `return handle_ui_feedback(...)`; - a response assigned to a variable before it's returned; - raised exceptions, which DRF renders as 4xx; - `responses=` values it can't read statically (a name, a `**` spread, a non-literal key). The method is skipped rather than guessed at. Two follow-ups would close the gaps the rule leaves: tightening the mypy plugin so error bodies are checked against their TypedDict (handled separately), and a test-time check of responses against the generated schema (#131073). **Existing violations:** 95 inline returns in 43 methods across 30 files, all in `src/`. getsentry has no `extend_schema` endpoints. Every one returns a status its schema didn't list, so I documented each with the existing `RESPONSE_*` constant instead of baselining them: 400, 401, 403, 404, 409 and 429. This adds `RESPONSE_GONE` for the one 410. Like S021–S023, the rule then runs with no baseline, and `tests/*` is exempt the same way. These are schema-only additions, with no runtime behavior change. Where a status has a known body shape, a typed response like the ones in #131067 can replace the generic constant later. The optional prod-vs-dev schema diff check fails by design here: the schema gains those response entries, and nothing is removed. <!-- junior-request-attribution:start --> via @BYK. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:agent%3A255009b50ca40a74d79e4c03aef8af92 --> -- [View Session](https://junior-prod.sentry.dev/conversations/agent%3A255009b50ca40a74d79e4c03aef8af92) [[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3A255009b50ca40a74d79e4c03aef8af92/?project=4510944073809921) <!-- junior-session-footer:end --> Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: BYK <126780+BYK@users.noreply.github.com>
The OpenAPI schema for
POST /organizations/{org}/issues/{issue_id}/autofix/(startOrganizationIssueAutofix) listed only 202, 400, 401, 403 and 404, so@sentry/apitypes every error asunknown. The Sentry CLI and MCP in getsentry/toolkit map the 402 and 409codevalues to user-facing messages, and they hardcode those codes today because the schema doesn't have them (see getsentry/toolkit#2207). Requested by @BYK.This adds typed bodies for both statuses, with
codeas an enum:AutofixPaymentRequiredResponse:detailand a requiredcode, eitherseer_out_of_creditsorseer_out_of_quota. Every 402 this endpoint returns sets a code: theopen_prand default-step credit and quota checks, and thepr_iterationcredit check inhandle_ui_feedback.AutofixConflictResponse:detail, plus an optionalcode(scm_integration_required,repos_not_linked, orrun_in_flight), and the optionalrun_idandsentry_run_idthat come withrun_in_flight.codeis optional because some 409s carry onlydetail: re-running a step after a PR or coding agent started, paused PR iteration, and a run that is still being created.The codes come from
Literaltypes that now back the existing constants (SEER_OUT_OF_CREDITS_CODE,SEER_OUT_OF_QUOTA_CODE,RUN_IN_FLIGHT_CODE, and the keys ofAUTOFIX_SETUP_REQUIRED_DETAIL). If a constant holds a code that theLiteraldoesn't list, mypy fails, so the enum and the runtime stay in sync. This is a typing and schema change only, with no change to runtime behavior.The 422 that
resolve_seer_runreturns when a run failed to start is still undocumented. I left it out to keep this change to 402 and 409.via @BYK.
--
View Session [Sentry]