Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/sentry/seer/agent/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@

import sentry_sdk
from django.contrib.auth.models import AnonymousUser
from django.db import router, transaction
from django.utils import timezone as django_timezone
from django.utils.timezone import now
from pydantic import BaseModel
from rest_framework.request import Request

from sentry import features, options
from sentry.constants import ENABLE_SEER_CODING_DEFAULT, ObjectStatus
from sentry.hybridcloud.models.outbox import CellOutbox, outbox_context
from sentry.hybridcloud.outbox.category import OutboxCategory, OutboxScope
from sentry.models.organization import Organization
from sentry.models.project import Project
from sentry.seer.agent.client_models import AgentRun, AgentRunWithPrs, SeerRunState
Expand All @@ -36,6 +40,7 @@
extract_hook_definition,
)
from sentry.seer.models import SeerApiError, SeerPermissionError, SeerRepoDefinition
from sentry.seer.models.run import SeerRun, SeerRunMirrorStatus, SeerRunType
from sentry.seer.seer_setup import has_seer_access_with_detail
from sentry.seer.signed_seer_api import SeerViewerContext
from sentry.tasks.seer.context_engine_index import build_service_map, index_org_project_knowledge
Expand Down Expand Up @@ -254,6 +259,7 @@ def start_run(
request: Request | None = None,
override_ce_enable: bool = True,
ui_tools: str | None = None,
run_type: SeerRunType | None = None,
) -> int:
"""
Start a new Seer Agent session.
Expand Down Expand Up @@ -345,6 +351,34 @@ def start_run(
):
chat_body["is_context_engine_enabled"] = override_ce_enable

if run_type and features.has("organizations:seer-run-mirror", self.organization):
user_id = (
self.user.id
if self.user and hasattr(self.user, "id") and self.user.id is not None
else None
)
with outbox_context(transaction.atomic(using=router.db_for_write(SeerRun)), flush=True):
run = SeerRun.objects.create(
organization=self.organization,
user_id=user_id,
type=run_type,
last_triggered_at=now(),
)
CellOutbox(
shard_scope=OutboxScope.ORGANIZATION_SCOPE,
shard_identifier=self.organization.id,
category=OutboxCategory.SEER_RUN_CREATE,
object_identifier=run.id,
payload={
"body": dict(chat_body),
"viewer_context": dict(self.viewer_context),
},
).save()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing OutboxFlushError handling in outbox path

High Severity

The outbox context with flush=True can raise OutboxFlushError if the signal receiver fails (e.g., Seer returns a 500, triggering a RuntimeError in handle_seer_run_create). This exception is not caught here, unlike the analogous code in search_agent_start.py which wraps the block in a try/except OutboxFlushError. Since OutboxFlushError does not inherit from SeerApiError, the endpoint's exception handler in organization_seer_agent_chat.py won't catch it either, resulting in an unhandled crash instead of a graceful error response.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9e2060d. Configure here.

run.refresh_from_db()
if run.mirror_status != SeerRunMirrorStatus.LIVE or run.seer_run_state_id is None:
raise SeerApiError("Seer run mirror failed to materialize", 500)
return run.seer_run_state_id
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Outbox path skips explorer index triggering

Medium Severity

The outbox path returns early at line 380, completely bypassing _maybe_trigger_explorer_index_for_new_run. Since the explorer chat endpoint is the only caller passing run_type, all explorer runs routed through the outbox will never trigger project indexing or context-engine indexing, even when Seer's response indicates missing indexes. The response data containing has_explorer_index and has_org_project_context is consumed only in the receiver, which discards those fields.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9e2060d. Configure here.


response = make_agent_chat_request(chat_body, viewer_context=self.viewer_context)

if response.status >= 400:
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/seer/endpoints/organization_seer_agent_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
snapshot_to_markdown,
)
from sentry.seer.models import SeerApiError, SeerPermissionError
from sentry.seer.models.run import SeerRunType
from sentry.seer.seer_setup import has_seer_access_with_detail
from sentry.types.ratelimit import RateLimit, RateLimitCategory
from sentry.utils import json
Expand Down Expand Up @@ -274,6 +275,7 @@ def post(
ui_tools=ui_tools,
override_ce_enable=override_ce_enable,
request=request,
run_type=SeerRunType.EXPLORER,
)

return Response({"run_id": result_run_id})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from typing import Any
from unittest.mock import ANY, MagicMock, patch
from unittest.mock import ANY, MagicMock, Mock, patch

import pytest

from sentry.seer.endpoints.organization_seer_agent_chat import SeerAgentChatSerializer
from sentry.seer.models.run import SeerRunType
from sentry.testutils.cases import APITestCase
from sentry.testutils.helpers.features import with_feature
from sentry.utils import json
Expand Down Expand Up @@ -126,6 +127,7 @@ def test_post_new_conversation_calls_client(self, mock_client_class: MagicMock):
ui_tools=None,
override_ce_enable=True,
request=ANY,
run_type=SeerRunType.EXPLORER,
)

@patch("sentry.seer.endpoints.organization_seer_agent_chat.SeerAgentClient")
Expand Down Expand Up @@ -330,6 +332,44 @@ def test_post_ascii_on_page_context_passed_through(self, mock_client_class: Magi
call_kwargs = mock_client.start_run.call_args[1]
assert call_kwargs["on_page_context"] == ascii_screenshot

@patch("sentry.receivers.outbox.cell.make_agent_chat_request")
@patch("sentry.seer.agent.client.has_seer_access_with_detail")
@patch("sentry.seer.agent.client.collect_user_org_context")
def test_outbox_path_creates_run_and_returns_run_id(
self, mock_collect_context: MagicMock, mock_access: MagicMock, mock_request: Mock
) -> None:
mock_access.return_value = (True, None)
mock_collect_context.return_value = {}
mock_request.return_value = Mock(status=200, json=Mock(return_value={"run_id": 99}))

from sentry.seer.agent.client import SeerAgentClient

client = SeerAgentClient(
self.organization, self.user, is_interactive=True, reasoning_effort="medium"
)

with self.feature("organizations:seer-run-mirror"):
run_id = client.start_run(
prompt="What happened?",
run_type=SeerRunType.EXPLORER,
)

assert run_id == 99

from sentry.seer.models.run import SeerRun, SeerRunMirrorStatus

run = SeerRun.objects.get(organization_id=self.organization.id)
assert run.type == SeerRunType.EXPLORER
assert run.mirror_status == SeerRunMirrorStatus.LIVE
assert run.seer_run_state_id == 99
assert run.user_id == self.user.id

sent_body = mock_request.call_args[0][0]
assert sent_body["query"] == "What happened?"
assert sent_body["organization_id"] == self.organization.id
assert "external_idempotency_key" in sent_body
assert sent_body["external_idempotency_key"] == str(run.uuid)


@with_feature("organizations:seer-explorer")
@with_feature("organizations:gen-ai-features")
Expand Down
Loading