-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
feat(seer): Add deliver_feature_result RPC for Seer agent features #116734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+608
−0
Merged
Changes from 2 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
6cddc46
feat(seer): Add deliver_feature_result RPC for Seer agent features
trevor-e 3130d25
feat(seer): Add night_shift delivery handler
trevor-e 2e44223
fix(seer): Look up night shift run by SeerRun UUID
trevor-e 1c51a11
ref(seer): Make delivery handler registry explicit
trevor-e c07574c
fix(seer): Add seer prefix to feature_delivery log key
trevor-e 129553a
ref(seer): Use Protocol for FeatureDeliveryFn type
trevor-e 0338b84
ref(seer): Simplify feature delivery params
trevor-e ccc50ca
fix(seer): Reorder params and add org_id to query
trevor-e 0c0baaa
fix(night_shift): Address review feedback for delivery handler
trevor-e 7dfbadf
fix(night_shift): Use fixable_verdicts count for candidates_selected …
trevor-e e1d5fa8
ref(night_shift): Remove duplicate FeatureRunStatus definition
trevor-e 1ef7231
ref(night_shift): Move FeatureRunStatus to shared types module
trevor-e 0061638
test(night_shift): Add tests for deliver_night_shift_result
trevor-e e853a2c
fix(night_shift): Add type annotations and re-export FeatureRunStatus
trevor-e 7054125
ref(night_shift): Reuse _run_autofix_for_candidates from cron
trevor-e bb03aea
fix(night_shift): Avoid N+1 queries by caching org on each group's pr…
trevor-e 33763d9
fix(night_shift): Use bulk query for project preferences
trevor-e 75de6cd
fix(night_shift): Filter out inactive projects in groups query
trevor-e 9bab14b
fix(night_shift): Handle missing project preferences defensively
trevor-e File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| """Registry for Seer feature result delivery handlers.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Callable | ||
| from typing import Any, Literal | ||
|
|
||
| FeatureRunStatus = Literal["completed", "error"] | ||
|
|
||
| FeatureDeliveryFn = Callable[ | ||
| [int | str, FeatureRunStatus, dict[str, Any] | None, int, str | None, int], None | ||
| ] | ||
|
|
||
| DELIVERY_HANDLERS: dict[str, FeatureDeliveryFn] = {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| """Night shift feature delivery handler registration.""" | ||
|
|
||
| from sentry.seer.night_shift import delivery # noqa: F401 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,239 @@ | ||
| """Delivery handler for night_shift feature results from Seer.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| from collections.abc import Mapping | ||
| from typing import Any | ||
|
|
||
| import sentry_sdk | ||
|
|
||
| from sentry.constants import SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT | ||
| from sentry.models.group import Group | ||
| from sentry.models.organization import Organization | ||
| from sentry.seer.agent.feature_delivery import DELIVERY_HANDLERS, FeatureRunStatus | ||
| from sentry.seer.autofix.autofix_agent import AutofixStep, trigger_autofix_agent | ||
| from sentry.seer.autofix.constants import SeerAutomationSource | ||
| from sentry.seer.autofix.issue_summary import referrer_map | ||
| from sentry.seer.autofix.utils import AutofixStoppingPoint, read_preference_from_sentry_db | ||
| from sentry.seer.models.night_shift import SeerNightShiftRun, SeerNightShiftRunResult | ||
| from sentry.seer.models.run import SeerRun | ||
| from sentry.seer.models.workflow import SeerWorkflowStrategy | ||
| from sentry.seer.night_shift.models import TriageResponse, TriageVerdict | ||
| from sentry.tasks.seer.night_shift.models import TriageAction | ||
| from sentry.tasks.seer.night_shift.skip_cache import mark_skipped | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| _FEATURE_ID = "night_shift" | ||
|
|
||
|
|
||
| def deliver_night_shift_result( | ||
| ref: int | str, | ||
| status: FeatureRunStatus, | ||
| result: dict[str, Any] | None, | ||
| seer_run_id: int, | ||
| error: str | None, | ||
| organization_id: int, | ||
| ) -> None: | ||
| """Process a night_shift result from Seer.""" | ||
| try: | ||
| run = SeerNightShiftRun.objects.select_related("organization").get(id=int(ref)) | ||
| except SeerNightShiftRun.DoesNotExist: | ||
| logger.warning( | ||
| "night_shift.delivery.missing_run", | ||
| extra={"ref": ref, "seer_run_id": seer_run_id}, | ||
| ) | ||
| return | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| if run.organization_id != organization_id: | ||
| logger.warning( | ||
| "night_shift.delivery.org_mismatch", | ||
| extra={ | ||
| "ref": ref, | ||
| "expected_org_id": run.organization_id, | ||
| "actual_org_id": organization_id, | ||
| }, | ||
| ) | ||
| return | ||
|
|
||
| extras_update: dict[str, object] = { | ||
| **(run.extras or {}), | ||
| "agent_run_id": seer_run_id, | ||
| } | ||
| if error: | ||
| extras_update["error_message"] = error | ||
| run.update(extras=extras_update) | ||
|
|
||
| log_extra: dict[str, object] = { | ||
| "organization_id": run.organization_id, | ||
| "run_id": run.id, | ||
| "agent_run_id": seer_run_id, | ||
| } | ||
|
|
||
| if status == "error" or result is None: | ||
| sentry_sdk.metrics.incr( | ||
| "night_shift.triage_error", | ||
| tags={"error_type": "delivery_error" if status == "error" else "no_artifact"}, | ||
| ) | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| logger.warning("night_shift.delivery.no_result", extra={**log_extra, "status": status}) | ||
| return | ||
|
|
||
| try: | ||
| triage_response = TriageResponse.parse_obj(result) | ||
|
sentry-warden[bot] marked this conversation as resolved.
|
||
| except Exception: | ||
| sentry_sdk.metrics.incr("night_shift.triage_error", tags={"error_type": "invalid_artifact"}) | ||
| logger.exception("night_shift.delivery.invalid_result", extra=log_extra) | ||
| return | ||
|
|
||
| options = (run.extras or {}).get("options") or {} | ||
| dry_run = bool(options.get("dry_run", False)) | ||
|
|
||
| _process_verdicts( | ||
| run=run, | ||
| organization=run.organization, | ||
| triage_response=triage_response, | ||
| dry_run=dry_run, | ||
| log_extra=log_extra, | ||
| ) | ||
|
trevor-e marked this conversation as resolved.
|
||
|
|
||
|
|
||
| def _process_verdicts( | ||
| *, | ||
| run: SeerNightShiftRun, | ||
| organization: Organization, | ||
| triage_response: TriageResponse, | ||
| dry_run: bool, | ||
| log_extra: Mapping[str, object], | ||
| ) -> None: | ||
| """Mark SKIPs, fire autofix for fixable verdicts, persist result rows.""" | ||
| group_ids = [v.group_id for v in triage_response.verdicts] | ||
| groups_by_id: dict[int, Group] = { | ||
| g.id: g | ||
| for g in Group.objects.filter( | ||
| id__in=group_ids, project__organization_id=organization.id | ||
| ).select_related("project") | ||
| } | ||
|
|
||
| unknown_group_ids = [gid for gid in group_ids if gid not in groups_by_id] | ||
| if unknown_group_ids: | ||
| logger.warning( | ||
| "night_shift.delivery.unknown_group_ids", | ||
| extra={**log_extra, "unknown_group_ids": unknown_group_ids}, | ||
| ) | ||
|
|
||
| for v in triage_response.verdicts: | ||
| if v.action == TriageAction.SKIP and v.group_id in groups_by_id: | ||
| mark_skipped(v.group_id) | ||
|
|
||
| fixable_verdicts = [ | ||
| v | ||
| for v in triage_response.verdicts | ||
| if v.action in (TriageAction.AUTOFIX, TriageAction.ROOT_CAUSE_ONLY) | ||
| and v.group_id in groups_by_id | ||
| ] | ||
|
cursor[bot] marked this conversation as resolved.
|
||
|
|
||
| sentry_sdk.metrics.distribution( | ||
| "night_shift.candidates_selected", len(triage_response.verdicts) | ||
| ) | ||
|
cursor[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| results: list[SeerNightShiftRunResult] = [] | ||
| if not dry_run: | ||
| results = _trigger_autofix_for_fixable( | ||
| run=run, | ||
| organization=organization, | ||
| verdicts=fixable_verdicts, | ||
| groups_by_id=groups_by_id, | ||
| log_extra=log_extra, | ||
| ) | ||
|
|
||
| seer_run_id_by_group = {r.group_id: r.seer_run_id for r in results} | ||
| logger.info( | ||
| "night_shift.candidates_selected", | ||
| extra={ | ||
| **log_extra, | ||
| "num_verdicts": len(triage_response.verdicts), | ||
| "dry_run": dry_run, | ||
| "candidates": [ | ||
| { | ||
| "group_id": v.group_id, | ||
| "action": v.action, | ||
| "seer_run_id": seer_run_id_by_group.get(v.group_id), | ||
| } | ||
| for v in triage_response.verdicts | ||
| ], | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
| def _trigger_autofix_for_fixable( | ||
| *, | ||
| run: SeerNightShiftRun, | ||
| organization: Organization, | ||
| verdicts: list[TriageVerdict], | ||
| groups_by_id: dict[int, Group], | ||
| log_extra: Mapping[str, object], | ||
| ) -> list[SeerNightShiftRunResult]: | ||
| if not verdicts: | ||
| return [] | ||
|
|
||
| referrer = referrer_map[SeerAutomationSource.NIGHT_SHIFT] | ||
| project_ids = {groups_by_id[v.group_id].project_id for v in verdicts} | ||
| project_by_id = {g.project_id: g.project for g in groups_by_id.values()} | ||
|
|
||
| for project in project_by_id.values(): | ||
| project.organization = organization | ||
|
|
||
| stopping_point_by_project_id = { | ||
| pid: AutofixStoppingPoint( | ||
| read_preference_from_sentry_db(project_by_id[pid]).automated_run_stopping_point | ||
| or SEER_AUTOMATED_RUN_STOPPING_POINT_DEFAULT | ||
| ) | ||
| for pid in project_ids | ||
| } | ||
|
|
||
| results: list[SeerNightShiftRunResult] = [] | ||
| for v in verdicts: | ||
| group = groups_by_id[v.group_id] | ||
| stopping_point = ( | ||
| AutofixStoppingPoint.ROOT_CAUSE | ||
| if v.action == TriageAction.ROOT_CAUSE_ONLY | ||
| else stopping_point_by_project_id[group.project_id] | ||
| ) | ||
| user_context = ( | ||
| f"Night-shift triage already investigated this issue and concluded:\n{v.reason}" | ||
| if v.reason | ||
| else None | ||
| ) | ||
| try: | ||
| seer_run_id = trigger_autofix_agent( | ||
| group=group, | ||
| step=AutofixStep.ROOT_CAUSE, | ||
| referrer=referrer, | ||
| stopping_point=stopping_point, | ||
| user_context=user_context, | ||
| ) | ||
| except Exception: | ||
| logger.exception( | ||
| "night_shift.autofix_trigger_failed", | ||
| extra={**log_extra, "group_id": group.id}, | ||
| ) | ||
| continue | ||
|
|
||
| result_seer_run = SeerRun.objects.filter(seer_run_state_id=seer_run_id).first() | ||
| results.append( | ||
| SeerNightShiftRunResult( | ||
| run=run, | ||
| kind=SeerWorkflowStrategy.AGENTIC_TRIAGE, | ||
| group=group, | ||
| seer_run_id=str(seer_run_id), | ||
| result_seer_run=result_seer_run, | ||
| ) | ||
|
cursor[bot] marked this conversation as resolved.
Outdated
|
||
| ) | ||
|
|
||
| SeerNightShiftRunResult.objects.bulk_create(results) | ||
| sentry_sdk.metrics.incr("night_shift.autofix_triggered", amount=len(results)) | ||
| return results | ||
|
|
||
|
|
||
| DELIVERY_HANDLERS[_FEATURE_ID] = deliver_night_shift_result | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| """Wire types for the night_shift feature result payload from Seer.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from pydantic import BaseModel | ||
|
|
||
| from sentry.tasks.seer.night_shift.models import TriageAction | ||
|
|
||
|
|
||
| class _Base(BaseModel): | ||
| class Config: | ||
| extra = "ignore" | ||
|
|
||
|
|
||
| class TriageVerdict(_Base): | ||
| group_id: int | ||
| action: TriageAction | ||
| reason: str | ||
|
|
||
|
|
||
| class TriageResponse(_Base): | ||
| verdicts: list[TriageVerdict] |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.