feat(seer): Add dual-read helpers for Seer project preferences from Sentry DB#111591
feat(seer): Add dual-read helpers for Seer project preferences from Sentry DB#111591
Conversation
Add read_preference_from_sentry_db() and bulk_read_preferences_from_sentry_db() to read Seer project preferences directly from Sentry's database instead of proxying to the Seer API. This is the foundation for cutting over reads behind the organizations:seer-project-settings-read-from-sentry feature flag. - Register organizations:seer-project-settings-read-from-sentry feature flag - Add SEER_PREFERENCE_OPTION_KEYS constant in projectoptions/defaults.py - Add build_repo_definition_from_project_repo() and build_automation_handoff() shared helpers using a Callable interface for option getters - Single read uses project.get_option() with caching - Bulk read uses ProjectOption.objects.get_value_bulk_id() with fallback to registered defaults via projectoptions.lookup_well_known_key() Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com> Made-with: Cursor
Cover read_preference_from_sentry_db and bulk_read_preferences_from_sentry_db with tests for unconfigured projects, repos with branch overrides, options-only projects, full and partial handoff configs, org scoping, and configured-vs-absent filtering. Co-Authored-By: Claude Sonnet 4 <noreply@anthropic.com> Made-with: Cursor
read_preference_from_sentry_db now returns None for unconfigured projects (no explicit option keys set and no repos), matching the bulk function's behavior. bulk_read_preferences_from_sentry_db is simplified to delegate to the single-project reader. Uses ProjectOption.objects.isset() and project.get_option() which both go through get_all_values(), a cached lookup, instead of the previous get_value_bulk_id() approach that issued a separate DB query per option key bypassing the cache. This is efficient for the small-project-count callers (settings endpoints, org setup). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous commit simplified bulk_read to delegate to the single-project reader, but that trades O(1) batched queries for O(N) per-project queries (one repo query + one cache/DB hit per project). The bulk function is called from configure_seer_for_existing_org which operates on all projects in an org, so N can be large. Keep the batched approach (1 repo query + 5 option queries total) for the bulk path, while the single-project reader uses the cached isset()/get_option() path which is efficient for its callers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| def _build_automation_handoff( | ||
| get_option: Callable[[str], Any], | ||
| ) -> SeerAutomationHandoffConfiguration | None: | ||
| """Build a SeerAutomationHandoffConfiguration from option values, or None if incomplete.""" |
There was a problem hiding this comment.
(Private for now since idk if we'll have any other use cases where this callable arg will be helpful or relevant)
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Bulk read bypasses epoch-aware default resolution
- Updated bulk preference fallback to use
lookup_well_known_key(key).get_default(project)so bulk reads resolve epoch-aware defaults exactly likeproject.get_option.
- Updated bulk preference fallback to use
Or push these changes by commenting:
@cursor push 5cf12c1e45
Preview (5cf12c1e45)
diff --git a/src/sentry/seer/autofix/utils.py b/src/sentry/seer/autofix/utils.py
--- a/src/sentry/seer/autofix/utils.py
+++ b/src/sentry/seer/autofix/utils.py
@@ -787,7 +787,7 @@
def get_project_option(key: str) -> Any:
value = project_options[key][project.id]
if value is None:
- return projectoptions.lookup_well_known_key(key).default
+ return projectoptions.lookup_well_known_key(key).get_default(project)
return value
result[project.id] = SeerProjectPreference(This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
Co-authored-by: Armen Zambrano G. <armenzg@users.noreply.github.com> Applied via @cursor push command
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 92dd7e4. Configure here.
…entry DB (#111591) relates to AIML-2611, AIML-2610 Add `read_preference_from_sentry_db()` and `bulk_read_preferences_from_sentry_db()` helpers to read Seer project preferences directly from Sentry DB instead of Seer API. This is the foundation for cutting over reads behind the `organizations:seer-project-settings-read-from-sentry` feature flag. We don't actually use the helpers in this PR. WIP followup PR: #111594 ### Extra We return None if a project is unconfigured. Matching Seer's behavior ([single](https://github.com/getsentry/seer/blob/3cec03cac3abd9f1f26ac42296d6c870669c66f0/src/seer/automation/preferences.py#L54), [bulk](https://github.com/getsentry/seer/blob/3cec03cac3abd9f1f26ac42296d6c870669c66f0/src/seer/automation/preferences.py#L98)), "unconfigured" means we don't have any SeerProjectRepository rows _and_ none of the project options are set. We use `project.get_option` and `ProjectOptions.objects.isset` for single reads, and `ProjectOption.objects.get_value_bulk_id` for bulk read. Why: 1. `project.get_option` and `ProjectOptions.objects.isset` use cache. For a single project we want to check the cache first. 2. `ProjectOption.objects.get_value_bulk_id` is one DB query. For our bulk use cases (`OrganizationAutofixAutomationSettingsEndpoint` get/post, `configure_seer_for_existing_org`) where we may be querying many projects for a single org, we do max 5 option queries, one for each project option. It also returns None for missing options so we can easily tell which projects have configured options and which don't. --------- Co-authored-by: Claude Sonnet 4 <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
…dpoints (#111594) Fixes AIML-2611 Depends on #111591 Behind `organizations:seer-project-settings-read-from-sentry`, read Seer project preferences directly from ProjectOption + SeerProjectRepository instead of proxying to the Seer API. Part of [Phase 3 of the Seer project preferences migration](https://www.notion.so/sentry/Tech-Spec-Migrate-Seer-Settings-to-Sentry-Database-3208b10e4b5d80f58ea0d7b77a301e2a). Updated call sites: - autofix_agent.py — trigger_coding_agent_handoff - coding_agent.py — _launch_agents_for_repos - issue_summary.py — get_automation_stopping_point - on_completion_hook.py — _get_handoff_config_if_applicable, _clear_handoff_preference - utils.py — has_project_connected_repos - project_seer_preferences.py — GET - organization_autofix_automation_settings.py — _serialize_projects_with_settings (GET), POST - tasks/seer/autofix.py — configure_seer_for_existing_org - tasks/seer/context_engine_index.py — index_repos - seer_rpc.py — trigger_coding_agent_launch - autofix.py — _resolve_project_preference Note: Generally I chose not to catch DB errors. We shouldn't be getting any. We'll roll out gradually and if something happens we should get a Sentry exception so I can fix it. --------- Co-authored-by: Claude Sonnet 4 <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>


relates to AIML-2611, AIML-2610
Add
read_preference_from_sentry_db()andbulk_read_preferences_from_sentry_db()helpers to read Seer project preferences directly from Sentry DB instead of Seer API. This is the foundation for cutting over reads behind theorganizations:seer-project-settings-read-from-sentryfeature flag.We don't actually use the helpers in this PR. WIP followup PR: #111594
Extra
We return None if a project is unconfigured. Matching Seer's behavior (single, bulk), "unconfigured" means we don't have any SeerProjectRepository rows and none of the project options are set.
We use
project.get_optionandProjectOptions.objects.issetfor single reads, andProjectOption.objects.get_value_bulk_idfor bulk read. Why:project.get_optionandProjectOptions.objects.issetuse cache. For a single project we want to check the cache first.ProjectOption.objects.get_value_bulk_idis one DB query. For our bulk use cases (OrganizationAutofixAutomationSettingsEndpointget/post,configure_seer_for_existing_org) where we may be querying many projects for a single org, we do max 5 option queries, one for each project option. It also returns None for missing options so we can easily tell which projects have configured options and which don't.