Skip to content

feat(api): tighten Response annotation on 6 endpoints (no-cast subset)#116717

Merged
azulus merged 1 commit into
masterfrom
jeremy/tighten-response-annotations
Jun 2, 2026
Merged

feat(api): tighten Response annotation on 6 endpoints (no-cast subset)#116717
azulus merged 1 commit into
masterfrom
jeremy/tighten-response-annotations

Conversation

@azulus
Copy link
Copy Markdown
Member

@azulus azulus commented Jun 2, 2026

Summary

Tightens 6 endpoint return annotations from -> Response to -> Response[T], where T is the type already named in the same method's inline_sentry_response_serializer(...) decorator. Continues the Response[T] rollout from #116335 / #116433 / #116496 / #116538 / #116659.

Changes

  • organization_events_timeseries.py-> Response[StatsResponse]. EMPTY_STATS_RESPONSE in timeseries.py retyped from dict[str, Any] to StatsResponse (its value {"timeSeries": []} is already a valid StatsResponse since meta is NotRequired).
  • event_attachments.py-> Response[list[EventAttachmentSerializerResponse]].
  • group_hashes.py-> Response[list[GroupHashesResult]].
  • project_releases.py-> Response[list[ReleaseSerializerResponse]].
  • discover_saved_queries.py-> Response[list[DiscoverSavedQueryResponse]]. Pass the (already-generic) DiscoverSavedQueryModelSerializer() instance explicitly so the call site is typed end-to-end.
  • source_map_debug.py-> Response[SourceMapDebugResponse]. Type the locally-built processed_exceptions: list[SourceMapDebugException] and processed_frames: list[SourceMapDebugFrame] lists.

Verification

  • mypy: 0 errors across the 7 touched files
  • prek: clean
  • sentry django spectacular: byte-identical OpenAPI output vs master

Test plan

  • CI typing job passes (mypy)
  • CI prek hooks pass
  • Existing endpoint tests pass unchanged (no runtime behavior change)

🤖 Generated with Claude Code

@github-actions github-actions Bot added the Scope: Backend Automatically applied to PRs that change backend components label Jun 2, 2026
Comment thread src/sentry/releases/endpoints/organization_release_details.py Fixed
Continues the Response[T] rollout (#116335 / #116433 / #116496 / #116538
/ #116659). Tightens 6 endpoints where the `T` already named in the
`@extend_schema(responses=...)` decorator can flow into the return
annotation with zero `cast()` calls — either the body already matches
structurally, or a one-line source-typing fix unblocks it:

- organization_events_timeseries.py: tighten to `Response[StatsResponse]`.
  EMPTY_STATS_RESPONSE in `timeseries.py` retyped from `dict[str, Any]`
  to `StatsResponse` (its value `{"timeSeries": []}` is already a valid
  StatsResponse since `meta` is NotRequired).
- event_attachments.py: tighten to `Response[list[EventAttachmentSerializerResponse]]`.
- group_hashes.py: tighten to `Response[list[GroupHashesResult]]`.
- project_releases.py: tighten to `Response[list[ReleaseSerializerResponse]]`.
- discover_saved_queries.py: tighten to `Response[list[DiscoverSavedQueryResponse]]`,
  pass the (already-generic) `DiscoverSavedQueryModelSerializer()` instance
  explicitly so the call site is typed end-to-end.
- source_map_debug.py: tighten to `Response[SourceMapDebugResponse]`, type
  the locally-built `processed_exceptions: list[SourceMapDebugException]`
  and `processed_frames: list[SourceMapDebugFrame]` lists.

The remaining 23 endpoints from the original Surface A audit hit drift
that requires real source-typing work (Serializer[T] migrations for
EventSerializer / RuleSerializer / ReleaseSerializer / etc., typed return
annotations on `_handle_results` / `massage_outcomes_result`, richer
union arms for validation-error paths). Those land in follow-up PRs that
do the source typing properly — no cast() escape valves. The linter flip
to make bare-Response + typed-decorator a violation also waits until all
29 endpoints are tightened.

Verification:
- mypy: 0 errors across the 7 touched files
- prek: clean
- `sentry django spectacular`: byte-identical OpenAPI output vs master

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@azulus azulus force-pushed the jeremy/tighten-response-annotations branch from fb55c2e to ae487b8 Compare June 2, 2026 20:58
@azulus azulus changed the title feat(api): tighten 29 bare Response annotations + harden structural linter feat(api): tighten Response annotation on 6 endpoints (no-cast subset) Jun 2, 2026
@azulus azulus marked this pull request as ready for review June 2, 2026 21:07
@azulus azulus requested review from a team as code owners June 2, 2026 21:07
@azulus azulus merged commit c79609a into master Jun 2, 2026
64 of 65 checks passed
@azulus azulus deleted the jeremy/tighten-response-annotations branch June 2, 2026 21:48
azulus added a commit that referenced this pull request Jun 2, 2026
…ct helper

Continues the Response[T] rollout (#116717 was round 1). Three more
endpoints tightened via source typing — no `cast()` calls.

Two patterns demonstrated:

1. **Typed local variable** for locally-built dicts:
   - `project_filters.py`: `results: list[ProjectFilterResponse] = []`
   - `project_replay_jobs_delete.py` POST success path:
     `response: ReplayDeletionJobDetailResponse = {"data": response_data}`

2. **Validation-error union arm** for DRF `serializer.errors` paths.
   Added a shared `ValidationErrorResponse` type alias and
   `validation_errors_dict(serializer)` helper to `apidocs/response_types.py`,
   alongside the existing `DetailResponse`. The helper projects DRF's
   `ReturnDict[Any, Any]` into a plain `dict[str, list[str]]` so a
   `Response[ValidationErrorResponse]` union arm is structurally satisfied
   without `cast()`:

       def post(...) -> Response[FooResponse] | Response[ValidationErrorResponse]:
           if not serializer.is_valid():
               return Response(validation_errors_dict(serializer), status=400)

   Applied to `organization_trace_item_attributes.py` and
   `project_replay_jobs_delete.py` POST.

The `project_replay_jobs_delete.py` GET endpoints are tightened to
`Response[ReplayDeletionJobListResponse]` and
`Response[ReplayDeletionJobDetailResponse]` — clean tightenings.

Verification:
- mypy: 0 errors across all touched files
- prek: clean
- `sentry django spectacular`: byte-identical OpenAPI vs master

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
azulus added a commit that referenced this pull request Jun 2, 2026
…ct helper

Continues the Response[T] rollout (#116717 was round 1). Three more
endpoints tightened via source typing — no `cast()` calls.

Two patterns demonstrated:

1. **Typed local variable** for locally-built dicts:
   - `project_filters.py`: `results: list[ProjectFilterResponse] = []`
   - `project_replay_jobs_delete.py` POST success path:
     `response: ReplayDeletionJobDetailResponse = {"data": response_data}`

2. **Validation-error union arm** for DRF `serializer.errors` paths.
   Added a shared `ValidationErrorResponse` type alias and
   `validation_errors_dict(serializer)` helper to `apidocs/response_types.py`,
   alongside the existing `DetailResponse`. The helper projects DRF's
   `ReturnDict[Any, Any]` into a plain `dict[str, list[str]]` so a
   `Response[ValidationErrorResponse]` union arm is structurally satisfied
   without `cast()`:

       def post(...) -> Response[FooResponse] | Response[ValidationErrorResponse]:
           if not serializer.is_valid():
               return Response(validation_errors_dict(serializer), status=400)

   Applied to `organization_trace_item_attributes.py` and
   `project_replay_jobs_delete.py` POST.

The `project_replay_jobs_delete.py` GET endpoints are tightened to
`Response[ReplayDeletionJobListResponse]` and
`Response[ReplayDeletionJobDetailResponse]` — clean tightenings.

Verification:
- mypy: 0 errors across all touched files
- prek: clean
- `sentry django spectacular`: byte-identical OpenAPI vs master

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
azulus added a commit that referenced this pull request Jun 2, 2026
… helper

Continues the Response[T] rollout (#116717 was round 1). Three more
endpoints tightened via source typing — no `cast()` calls.

Two patterns demonstrated:

1. **Typed local variable** for locally-built dicts:
   - `project_filters.py`: `results: list[ProjectFilterResponse] = []`
   - `project_replay_jobs_delete.py` POST success path:
     `response: ReplayDeletionJobDetailResponse = {"data": response_data}`

2. **Validation-error union arm** for DRF `serializer.errors` paths.
   Added a shared `ValidationErrorResponse` type alias and
   `as_validation_errors(serializer)` helper to `apidocs/response_types.py`,
   alongside the existing `DetailResponse`. The helper projects DRF's
   `ReturnDict[Any, Any]` into a plain `dict[str, list[str]]` so a
   `Response[ValidationErrorResponse]` union arm is structurally satisfied
   without `cast()`:

       def post(...) -> Response[FooResponse] | Response[ValidationErrorResponse]:
           if not serializer.is_valid():
               return Response(as_validation_errors(serializer), status=400)

   Applied to `organization_trace_item_attributes.py` and
   `project_replay_jobs_delete.py` POST.

The `project_replay_jobs_delete.py` GET endpoints are tightened to
`Response[ReplayDeletionJobListResponse]` and
`Response[ReplayDeletionJobDetailResponse]` — clean tightenings.

Verification:
- mypy: 0 errors across all touched files
- prek: clean
- `sentry django spectacular`: byte-identical OpenAPI vs master

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants