diff --git a/migrations_lockfile.txt b/migrations_lockfile.txt index 40f2a27e2d3155..0adb8c8eb52e95 100644 --- a/migrations_lockfile.txt +++ b/migrations_lockfile.txt @@ -29,7 +29,7 @@ releases: 0004_cleanup_failed_safe_deletes replays: 0007_organizationmember_replay_access -seer: 0014_add_new_unique_constraints +seer: 0015_add_seer_run_fks sentry: 1097_add_new_unique_constraints diff --git a/src/sentry/seer/migrations/0015_add_seer_run_fks.py b/src/sentry/seer/migrations/0015_add_seer_run_fks.py new file mode 100644 index 00000000000000..3d840bb1178df5 --- /dev/null +++ b/src/sentry/seer/migrations/0015_add_seer_run_fks.py @@ -0,0 +1,48 @@ +# Generated by Django 5.2.14 on 2026-05-15 23:33 + +import django.db.models.deletion +import sentry.db.models.fields.foreignkey +from django.db import migrations + +from sentry.new_migrations.migrations import CheckedMigration + + +class Migration(CheckedMigration): + # This flag is used to mark that a migration shouldn't be automatically run in production. + # This should only be used for operations where it's safe to run the migration after your + # code has deployed. So this should not be used for most operations that alter the schema + # of a table. + # Here are some things that make sense to mark as post deployment: + # - Large data migrations. Typically we want these to be run manually so that they can be + # monitored and not block the deploy for a long period of time while they run. + # - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to + # run this outside deployments so that we don't block them. Note that while adding an index + # is a schema change, it's completely safe to run the operation after the code has deployed. + # Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment + + is_post_deployment = False + + dependencies = [ + ("seer", "0014_add_new_unique_constraints"), + ] + + operations = [ + migrations.AddField( + model_name="seernightshiftrun", + name="seer_run", + field=sentry.db.models.fields.foreignkey.FlexibleForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="seer.seerrun", + ), + ), + migrations.AddField( + model_name="seernightshiftrunresult", + name="result_seer_run", + field=sentry.db.models.fields.foreignkey.FlexibleForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="seer.seerrun", + ), + ), + ] diff --git a/src/sentry/seer/models/night_shift.py b/src/sentry/seer/models/night_shift.py index dae3117f72a7a2..bbf2babb5911d6 100644 --- a/src/sentry/seer/models/night_shift.py +++ b/src/sentry/seer/models/night_shift.py @@ -21,6 +21,8 @@ class SeerNightShiftRun(DefaultFieldsModel): workflow_config = FlexibleForeignKey( "seer.SeerWorkflowConfig", on_delete=models.SET_NULL, null=True ) + # TODO: make required once backfilled + seer_run = FlexibleForeignKey("seer.SeerRun", on_delete=models.SET_NULL, null=True) extras = models.JSONField(db_default={}, default=dict) class Meta: @@ -48,7 +50,9 @@ class SeerNightShiftRunResult(DefaultFieldsModel): group = FlexibleForeignKey( "sentry.Group", on_delete=models.CASCADE, db_constraint=False, null=True ) - seer_run_id = models.TextField(null=True) + seer_run_id = models.TextField(null=True) # TODO: remove once result_seer_run is backfilled + # TODO: make required once backfilled + result_seer_run = FlexibleForeignKey("seer.SeerRun", on_delete=models.SET_NULL, null=True) extras = models.JSONField(db_default={}, default=dict) class Meta: diff --git a/src/sentry/tasks/seer/night_shift/agentic_triage.py b/src/sentry/tasks/seer/night_shift/agentic_triage.py index 4973c4aaf87882..0930721e73afe3 100644 --- a/src/sentry/tasks/seer/night_shift/agentic_triage.py +++ b/src/sentry/tasks/seer/night_shift/agentic_triage.py @@ -13,6 +13,7 @@ from sentry.seer.agent.client import SeerAgentClient from sentry.seer.agent.client_models import SeerRunState from sentry.seer.models.night_shift import SeerNightShiftRun +from sentry.seer.models.run import SeerRun from sentry.tasks.seer.night_shift.models import TriageAction, TriageResult from sentry.tasks.seer.night_shift.simple_triage import ( ScoredCandidate, @@ -114,7 +115,9 @@ def _triage_candidates( artifact_schema=_TriageResponse, ) - run.update(extras={**run.extras, "agent_run_id": agent_run_id}) + # TODO: have start_run return the SeerRun directly to avoid this lookup. + seer_run = SeerRun.objects.filter(seer_run_state_id=agent_run_id).first() + run.update(seer_run=seer_run, extras={**run.extras, "agent_run_id": agent_run_id}) logger.info( "night_shift.explorer_run_started", diff --git a/src/sentry/tasks/seer/night_shift/cron.py b/src/sentry/tasks/seer/night_shift/cron.py index 82101d6f62ebed..d7a7636749ba59 100644 --- a/src/sentry/tasks/seer/night_shift/cron.py +++ b/src/sentry/tasks/seer/night_shift/cron.py @@ -29,6 +29,7 @@ SeerNightShiftRunResult, ) from sentry.seer.models.project_repository import SeerProjectRepository +from sentry.seer.models.run import SeerRun from sentry.seer.models.workflow import SeerWorkflowConfig, SeerWorkflowStrategy from sentry.tasks.base import instrumented_task from sentry.tasks.seer.night_shift.agentic_triage import agentic_triage_strategy @@ -544,12 +545,15 @@ def _run_autofix_for_candidates( ) continue + # TODO: have trigger_autofix_agent return the SeerRun directly to avoid this lookup. + result_seer_run = SeerRun.objects.filter(seer_run_state_id=seer_run_id).first() results.append( SeerNightShiftRunResult( run=run, kind=SeerWorkflowStrategy.AGENTIC_TRIAGE, group=c.group, seer_run_id=str(seer_run_id), + result_seer_run=result_seer_run, extras={"action": str(c.action)}, ) )