From ba1722566ef33b5cd888d0249a8d7689f8c99b1a Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Tue, 16 Jun 2026 12:14:28 -0400 Subject: [PATCH 1/2] feat(seer): Return sentry_run_id from runs list and dashboard generate Surface the run's UUID on the two remaining endpoints that only returned the numeric run id: - GET /seer/explorer-runs/ adds sentry_run_id per run (single bulk SeerRun lookup; null for legacy runs with no mirror row). - POST /dashboards/generate/ returns sentry_run_id alongside run_id. Additive: existing clients ignore the new field. --- .../organization_dashboard_generate.py | 2 +- .../endpoints/organization_seer_agent_runs.py | 15 ++++++++++++- .../test_organization_dashboard_generate.py | 11 ++++++---- .../test_organization_seer_agent_runs.py | 22 +++++++++++++++++++ 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/sentry/dashboards/endpoints/organization_dashboard_generate.py b/src/sentry/dashboards/endpoints/organization_dashboard_generate.py index 8213c94ceb554d..1af65432e899e7 100644 --- a/src/sentry/dashboards/endpoints/organization_dashboard_generate.py +++ b/src/sentry/dashboards/endpoints/organization_dashboard_generate.py @@ -198,7 +198,7 @@ def post(self, request: Request, organization: Organization) -> Response: artifact_schema=GeneratedDashboard, request=request, ) - return Response({"run_id": run.seer_run_state_id}) + return Response({"run_id": run.seer_run_state_id, "sentry_run_id": str(run.uuid)}) except SeerPermissionError as e: raise PermissionDenied(e.message) from e except SeerApiError: diff --git a/src/sentry/seer/endpoints/organization_seer_agent_runs.py b/src/sentry/seer/endpoints/organization_seer_agent_runs.py index 5883ed05fb0d27..e624fc9ee10ce5 100644 --- a/src/sentry/seer/endpoints/organization_seer_agent_runs.py +++ b/src/sentry/seer/endpoints/organization_seer_agent_runs.py @@ -16,6 +16,7 @@ from sentry.seer.agent.client import SeerAgentClient from sentry.seer.agent.client_utils import has_seer_agent_access_with_detail from sentry.seer.models import SeerPermissionError +from sentry.seer.models.run import SeerRun logger = logging.getLogger(__name__) @@ -72,7 +73,19 @@ def _make_seer_runs_request(offset: int, limit: int) -> dict[str, Any]: except SeerPermissionError as e: raise PermissionDenied(e.message) from e - return {"data": [run.dict() for run in runs]} + uuid_by_state_id = { + seer_run_state_id: str(run_uuid) + for seer_run_state_id, run_uuid in SeerRun.objects.filter( + organization=organization, + seer_run_state_id__in=[run.run_id for run in runs], + ).values_list("seer_run_state_id", "uuid") + } + return { + "data": [ + {**run.dict(), "sentry_run_id": uuid_by_state_id.get(run.run_id)} + for run in runs + ] + } return self.paginate( request=request, diff --git a/tests/sentry/dashboards/endpoints/test_organization_dashboard_generate.py b/tests/sentry/dashboards/endpoints/test_organization_dashboard_generate.py index 45a142d0af3bd0..56213cef783161 100644 --- a/tests/sentry/dashboards/endpoints/test_organization_dashboard_generate.py +++ b/tests/sentry/dashboards/endpoints/test_organization_dashboard_generate.py @@ -1,5 +1,6 @@ from __future__ import annotations +import uuid from typing import Any from unittest.mock import ANY, MagicMock, patch @@ -31,15 +32,16 @@ def test_post_with_empty_prompt_returns_400(self) -> None: @patch("sentry.dashboards.endpoints.organization_dashboard_generate.SeerAgentClient") def test_post_starts_run_and_returns_run_id(self, mock_client_class: MagicMock) -> None: + run_uuid = uuid.uuid4() mock_client = MagicMock() - mock_client.start_run.return_value = MagicMock(seer_run_state_id=789) + mock_client.start_run.return_value = MagicMock(seer_run_state_id=789, uuid=run_uuid) mock_client_class.return_value = mock_client data = {"prompt": "Show me error rates by project"} response = self.client.post(self.url, data, format="json") assert response.status_code == 200 - assert response.data == {"run_id": 789} + assert response.data == {"run_id": 789, "sentry_run_id": str(run_uuid)} mock_client_class.assert_called_once_with( self.organization, @@ -104,8 +106,9 @@ def test_post_with_invalid_current_dashboard_returns_400(self) -> None: def test_post_with_current_dashboard_uses_edit_context( self, mock_client_class: MagicMock ) -> None: + run_uuid = uuid.uuid4() mock_client = MagicMock() - mock_client.start_run.return_value = MagicMock(seer_run_state_id=123) + mock_client.start_run.return_value = MagicMock(seer_run_state_id=123, uuid=run_uuid) mock_client_class.return_value = mock_client data = { @@ -137,7 +140,7 @@ def test_post_with_current_dashboard_uses_edit_context( response = self.client.post(self.url, data, format="json") assert response.status_code == 200 - assert response.data == {"run_id": 123} + assert response.data == {"run_id": 123, "sentry_run_id": str(run_uuid)} mock_client_class.assert_called_once_with( self.organization, diff --git a/tests/sentry/seer/endpoints/test_organization_seer_agent_runs.py b/tests/sentry/seer/endpoints/test_organization_seer_agent_runs.py index 45af8ec4ce9aaa..835c0c3eeea688 100644 --- a/tests/sentry/seer/endpoints/test_organization_seer_agent_runs.py +++ b/tests/sentry/seer/endpoints/test_organization_seer_agent_runs.py @@ -67,6 +67,28 @@ def test_get_simple(self) -> None: query=None, ) + def test_get_includes_sentry_run_id(self) -> None: + run = self.create_seer_run(organization=self.organization, seer_run_state_id=1) + self.mock_client.get_runs.return_value = [ + AgentRun( + run_id=1, + title="Mirrored", + last_triggered_at=datetime.now(), + created_at=datetime.now(), + ), + AgentRun( + run_id=2, + title="Legacy (no mirror)", + last_triggered_at=datetime.now(), + created_at=datetime.now(), + ), + ] + response = self.client.get(self.url) + assert response.status_code == 200 + data = response.json()["data"] + assert data[0]["sentry_run_id"] == str(run.uuid) + assert data[1]["sentry_run_id"] is None + def test_get_cursor_pagination(self) -> None: # Mock seer response for offset 0, limit 3. self.mock_client.get_runs.return_value = [ From 710e7e13c77d7776a1884dd102c1b2096bd0db8c Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Tue, 16 Jun 2026 18:05:48 -0400 Subject: [PATCH 2/2] ref(seer): Type the runs-list and dashboard-generate responses Build the runs-list items via an AgentRunWithUuid(AgentRun) subclass instead of spreading into a raw dict, keeping AgentRun pure (it models Seer's response) and the sentry_run_id enrichment at the API layer. Type the dashboard-generate response with a DashboardGenerateResponse model. Same wire shape; mypy now knows both response shapes. --- .../endpoints/organization_dashboard_generate.py | 12 +++++++++++- .../seer/endpoints/organization_seer_agent_runs.py | 9 ++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/sentry/dashboards/endpoints/organization_dashboard_generate.py b/src/sentry/dashboards/endpoints/organization_dashboard_generate.py index 1af65432e899e7..aab80348d077a2 100644 --- a/src/sentry/dashboards/endpoints/organization_dashboard_generate.py +++ b/src/sentry/dashboards/endpoints/organization_dashboard_generate.py @@ -3,6 +3,7 @@ import logging from typing import Any +from pydantic import BaseModel from rest_framework import serializers from rest_framework.exceptions import PermissionDenied from rest_framework.request import Request @@ -108,6 +109,11 @@ """ +class DashboardGenerateResponse(BaseModel): + run_id: int + sentry_run_id: str + + class DashboardGenerateSerializer(serializers.Serializer[dict[str, Any]]): prompt = serializers.CharField( required=True, @@ -198,7 +204,11 @@ def post(self, request: Request, organization: Organization) -> Response: artifact_schema=GeneratedDashboard, request=request, ) - return Response({"run_id": run.seer_run_state_id, "sentry_run_id": str(run.uuid)}) + return Response( + DashboardGenerateResponse( + run_id=run.seer_run_state_id, sentry_run_id=str(run.uuid) + ).dict() + ) except SeerPermissionError as e: raise PermissionDenied(e.message) from e except SeerApiError: diff --git a/src/sentry/seer/endpoints/organization_seer_agent_runs.py b/src/sentry/seer/endpoints/organization_seer_agent_runs.py index e624fc9ee10ce5..fd5ed001d968b0 100644 --- a/src/sentry/seer/endpoints/organization_seer_agent_runs.py +++ b/src/sentry/seer/endpoints/organization_seer_agent_runs.py @@ -14,6 +14,7 @@ from sentry.api.paginator import GenericOffsetPaginator from sentry.models.organization import Organization from sentry.seer.agent.client import SeerAgentClient +from sentry.seer.agent.client_models import AgentRun from sentry.seer.agent.client_utils import has_seer_agent_access_with_detail from sentry.seer.models import SeerPermissionError from sentry.seer.models.run import SeerRun @@ -21,6 +22,10 @@ logger = logging.getLogger(__name__) +class AgentRunWithUuid(AgentRun): + sentry_run_id: str | None + + class OrganizationSeerAgentRunsPermission(OrganizationPermission): scope_map = { "GET": ["org:read"], @@ -82,7 +87,9 @@ def _make_seer_runs_request(offset: int, limit: int) -> dict[str, Any]: } return { "data": [ - {**run.dict(), "sentry_run_id": uuid_by_state_id.get(run.run_id)} + AgentRunWithUuid( + **run.dict(), sentry_run_id=uuid_by_state_id.get(run.run_id) + ).dict() for run in runs ] }