feat(seer): Add new bulk+single project Seer settings endpoints#114899
feat(seer): Add new bulk+single project Seer settings endpoints#114899
Conversation
|
🚨 Warning: This pull request contains Frontend and Backend changes! It's discouraged to make changes to Sentry's Frontend and Backend in a single pull request. The Frontend and Backend are not atomically deployed. If the changes are interdependent of each other, they must be separated into two pull requests and be made forward or backwards compatible, such that the Backend or Frontend can be safely deployed independently. Have questions? Please ask in the |
📊 Type Coverage Diff✅ No new type safety issues introduced. Coverage: 93.45% |
| def _annotate_queryset(queryset): | ||
| # ProjectOption.value is a LegacyTextJSONField — a text column storing JSON. | ||
| # Use LegacyTextJSONField as output_field. Coalesce fallback values must also | ||
| # be JSON-encoded to match what the DB stores. |
There was a problem hiding this comment.
This method is kind of ugly with the JSON stuff, but not sure how else to write this--we need the annotations for ordering (otherwise I would just filter in-memory using projectoptions get_option or get_value_bulk).
Basically the problem I ran into was that LegacyTextJSONField output has quotes, but setting a Coalesce default to Value(...) doesn't work for comparison because that wouldn't have quotes. So I am json.dumps'ing the hardcoded defaults.
|
@sentry review |
|
@sentry review |
f50de0a to
8829ef6
Compare
|
@sentry review |
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 e7fc0b6. Configure here.
| data = serializer.validated_data | ||
| search_query = data.pop("query") | ||
|
|
||
| accessible_projects = self.get_projects(request, organization) |
There was a problem hiding this comment.
PUT handler misses projects visible in GET response
Medium Severity
The get handler calls self.get_projects(request, organization, include_all_accessible=True) but the put handler calls self.get_projects(request, organization) without include_all_accessible=True. This means the PUT handler operates on a narrower set of projects (only team-scoped) than the GET handler shows. A user could see a project in the paginated GET list, craft a bulk update targeting it, and the update would silently skip it because get_projects without include_all_accessible doesn't return it.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit e7fc0b6. Configure here.
Relates to CW-1285 Tldr, add an unused helper and prep stuff for #114899. Adds `update_seer_project_settings`, a helper that translates high-level Seer settings (agent, stopping point, scanner automation) into the underlying project options. This centralizes the write logic so the upcoming bulk and single-project settings endpoints can share it instead of duplicating option-level writes. - **agent=seer**: clears required handoff options (point, target, integration_id) - **agent=cursor/claude**: sets handoff config, requires an integration ID - **stoppingPoint (non-off)**: sets tuning to MEDIUM and stores the stopping point value - **stoppingPoint=off**: sets tuning to OFF; leaves stopping point and auto_create_pr alone so that reenabling tuning restores the prior state - **stoppingPoint=open_pr**: sets auto_create_pr=True; any other stopping point clears it. Safe to set unconditionally since [auto_create_pr is only read when all other handoff options are non-null.](https://github.com/getsentry/sentry/blob/d254733b163432ca88ae796c05d415f97064cbbc/src/sentry/seer/autofix/utils.py#L622) - **stoppingPoint omitted**: tuning and auto_create_pr are left untouched - Options set to their registered default are deleted rather than stored
| | Q(_handoff_target=Value(json.dumps(None), output_field=LegacyTextJSONField())), | ||
| then=Value(json.dumps(AutomationCodingAgent.SEER)), | ||
| ), | ||
| default="_handoff_target", |
There was a problem hiding this comment.
Bug: The Case expression for the agent annotation uses a literal string default="_handoff_target" instead of a column reference, which will cause filtering and sorting by agent to fail.
Severity: HIGH
Suggested Fix
Wrap the default value in an F() expression to correctly reference the _handoff_target column value. Change default="_handoff_target" to default=F("_handoff_target") and import F from django.db.models.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: src/sentry/seer/endpoints/project_settings.py#L209
Potential issue: In `_annotate_queryset`, the `agent` annotation is defined using a
Django `Case` expression. The `default` is set to the literal string
`"_handoff_target"`. Because the `output_field` is `LegacyTextJSONField`, Django does
not automatically convert this string to an `F()` expression. As a result, when a
project has an external agent, the annotated `agent` field will contain the literal
string `"_handoff_target"` instead of the actual value from the `_handoff_target`
column. This breaks filtering and sorting by the `agent` field in the
`OrganizationSeerProjectSettingsEndpoint` for both GET and PUT requests, as they will
operate on the incorrect literal string.
There was a problem hiding this comment.
Covered in test_get_sort_by_agent
Relates to CW-1285 Tldr, add an unused helper and prep stuff for #114899. Adds `update_seer_project_settings`, a helper that translates high-level Seer settings (agent, stopping point, scanner automation) into the underlying project options. This centralizes the write logic so the upcoming bulk and single-project settings endpoints can share it instead of duplicating option-level writes. - **agent=seer**: clears required handoff options (point, target, integration_id) - **agent=cursor/claude**: sets handoff config, requires an integration ID - **stoppingPoint (non-off)**: sets tuning to MEDIUM and stores the stopping point value - **stoppingPoint=off**: sets tuning to OFF; leaves stopping point and auto_create_pr alone so that reenabling tuning restores the prior state - **stoppingPoint=open_pr**: sets auto_create_pr=True; any other stopping point clears it. Safe to set unconditionally since [auto_create_pr is only read when all other handoff options are non-null.](https://github.com/getsentry/sentry/blob/d254733b163432ca88ae796c05d415f97064cbbc/src/sentry/seer/autofix/utils.py#L622) - **stoppingPoint omitted**: tuning and auto_create_pr are left untouched - Options set to their registered default are deleted rather than stored
| "sentry:seer_automation_handoff_integration_id", | ||
| "sentry:seer_automation_handoff_auto_create_pr", | ||
| "sentry:autofix_automation_tuning", | ||
| "sentry:seer_scanner_automation", |
There was a problem hiding this comment.
It's not part of the SeerProjectPreference model (I wrote this list so I could use it in the original SeerProjectRepository read/write helpers which use SeerProjectPreference), but since it's an autofix-specific setting I figured I'd add it to try and centralize all of our Seer settings into one place.
JoshFerge
left a comment
There was a problem hiding this comment.
there is a lot in this PR, and it looks good, but i'd like to take my time with reviewing it. is it possible to:
- split the PR up into first an endpoint which contains the seer project endpont, and a second one for the bulk endpoint? that might make it easier to review


Fixes CW-1285
Depends on #115037
Adds endpoints for managing per-project Seer settings:
GET /api/0/projects/{org}/{project}/seer/settings/— single-project settings readPUT /api/0/projects/{org}/{project}/seer/settings/— single-project settings updateGET /api/0/organizations/{org}/seer/projects/— paginated list with search/sort/filterPUT /api/0/organizations/{org}/seer/projects/— bulk update across multiple projectsBoth endpoints use the
update_seer_project_settingshelper from #115037 to translate high-level fields (agent, integrationId, stoppingPoint, scannerAutomation) into project options.Supported filters (via
queryparameter)id=,!=,IN,NOT INid:1,id:[1,2,3]name(free text)=,!=my-projectreposCount=,!=,>,<,>=,<=reposCount:>0stoppingPoint=,!=,IN,NOT INstoppingPoint:offagent=,!=,IN,NOT INagent:seer,!agent:cursor_background_agentSupported sort fields (via
sortByparameter)name,-name,reposCount,-reposCount,agent,-agent,stoppingPoint,-stoppingPointExample: GET single project
{ "projectId": 2, "projectSlug": "test-seer-settings", "agent": "seer", "integrationId": null, "stoppingPoint": "code_changes", "scannerAutomation": true, "reposCount": 1 }Example: PUT single project
{ "projectId": 2, "projectSlug": "test-seer-settings", "agent": "seer", "integrationId": null, "stoppingPoint": "open_pr", "scannerAutomation": true, "reposCount": 1 }Example: PUT bulk update
Returns
204 No Content.