Skip to content

Commit 679f70b

Browse files
authored
chore(seer): add short id to cursor prompt (#106344)
Addresses AIML-2267 Problem: When seer RCA triggers the cursor integration to make a PR, we want it to include the Sentry short-id so it links back. Solution: Tell cursor in the prompt to include the short id if auto_create_pr is True Testing: Added unit tests to make sure the prompt is correct, but can only complete E2E testing once it is live for proper github/cursor integration.
1 parent a3c63c3 commit 679f70b

File tree

5 files changed

+179
-13
lines changed

5 files changed

+179
-13
lines changed

src/sentry/seer/autofix/coding_agent.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,11 @@ def _launch_agents_for_repos(
248248
"There are no repos in the Seer state to launch coding agents with, make sure you have repos connected to Seer and rerun this Issue Fix."
249249
)
250250

251-
prompt = get_coding_agent_prompt(run_id, trigger_source, instruction)
251+
short_id = None
252+
if autofix_state and auto_create_pr:
253+
short_id = autofix_state.request.issue.get("short_id")
254+
255+
prompt = get_coding_agent_prompt(run_id, trigger_source, instruction, short_id)
252256

253257
if not prompt:
254258
raise APIException("Issue fetching prompt to send to coding agents.")

src/sentry/seer/autofix/utils.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22
from datetime import UTC, datetime
33
from enum import StrEnum
4-
from typing import TypedDict
4+
from typing import NotRequired, TypedDict
55

66
import orjson
77
import pydantic
@@ -39,6 +39,7 @@
3939
class AutofixIssue(TypedDict):
4040
id: int
4141
title: str
42+
short_id: NotRequired[str | None]
4243

4344

4445
class AutofixStoppingPoint(StrEnum):
@@ -629,7 +630,10 @@ def get_autofix_prompt(run_id: int, include_root_cause: bool, include_solution:
629630

630631

631632
def get_coding_agent_prompt(
632-
run_id: int, trigger_source: AutofixTriggerSource, instruction: str | None = None
633+
run_id: int,
634+
trigger_source: AutofixTriggerSource,
635+
instruction: str | None = None,
636+
short_id: str | None = None,
633637
) -> str:
634638
"""Get the coding agent prompt with prefix from Seer API."""
635639
include_root_cause = trigger_source in [
@@ -642,6 +646,11 @@ def get_coding_agent_prompt(
642646

643647
base_prompt = "Please fix the following issue. Ensure that your fix is fully working."
644648

649+
if short_id:
650+
base_prompt = (
651+
f"{base_prompt}\n\nInclude 'Fixes {short_id}' in the pull request description."
652+
)
653+
645654
if instruction and instruction.strip():
646655
base_prompt = f"{base_prompt}\n\n{instruction.strip()}"
647656

tests/sentry/integrations/api/endpoints/test_organization_coding_agents.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,7 @@ def test_launches_coding_agent(
593593
assert response.data["failed_count"] >= 0
594594

595595
# Verify prompt was called with default trigger_source and no instruction
596-
mock_get_prompt.assert_called_with(123, AutofixTriggerSource.SOLUTION, None)
596+
mock_get_prompt.assert_called_with(123, AutofixTriggerSource.SOLUTION, None, None)
597597

598598
@patch("sentry.seer.autofix.coding_agent.get_coding_agent_providers")
599599
@patch("sentry.seer.autofix.coding_agent.get_autofix_state")
@@ -1012,7 +1012,7 @@ def test_root_cause_trigger_source(
10121012
assert response.data["failed_count"] >= 0
10131013

10141014
# Verify prompt was called with root_cause trigger_source and no instruction
1015-
mock_get_prompt.assert_called_with(123, AutofixTriggerSource.ROOT_CAUSE, None)
1015+
mock_get_prompt.assert_called_with(123, AutofixTriggerSource.ROOT_CAUSE, None, None)
10161016

10171017
@patch("sentry.seer.autofix.coding_agent.get_coding_agent_providers")
10181018
@patch("sentry.seer.autofix.coding_agent.get_autofix_state")
@@ -1090,7 +1090,7 @@ def test_root_cause_repos_extracted_and_deduped(
10901090
):
10911091
response = self.get_success_response(self.organization.slug, method="post", **data)
10921092
assert response.data["success"] is True
1093-
mock_get_prompt.assert_called_with(123, AutofixTriggerSource.ROOT_CAUSE, None)
1093+
mock_get_prompt.assert_called_with(123, AutofixTriggerSource.ROOT_CAUSE, None, None)
10941094

10951095
@patch("sentry.seer.autofix.coding_agent.get_coding_agent_providers")
10961096
@patch("sentry.seer.autofix.coding_agent.get_autofix_state")
@@ -1162,7 +1162,7 @@ def test_root_cause_without_relevant_repos_falls_back_to_request_repos(
11621162
assert response.data["success"] is True
11631163
assert response.data["launched_count"] >= 0
11641164
assert response.data["failed_count"] >= 0
1165-
mock_get_prompt.assert_called_with(123, AutofixTriggerSource.ROOT_CAUSE, None)
1165+
mock_get_prompt.assert_called_with(123, AutofixTriggerSource.ROOT_CAUSE, None, None)
11661166

11671167
@patch("sentry.seer.autofix.coding_agent.get_coding_agent_providers")
11681168
@patch("sentry.seer.autofix.coding_agent.get_autofix_state")
@@ -1211,7 +1211,7 @@ def test_solution_trigger_source(
12111211
assert response.data["failed_count"] >= 0
12121212

12131213
# Verify prompt was called with solution trigger_source and no instruction
1214-
mock_get_prompt.assert_called_with(123, AutofixTriggerSource.SOLUTION, None)
1214+
mock_get_prompt.assert_called_with(123, AutofixTriggerSource.SOLUTION, None, None)
12151215

12161216
@patch("sentry.seer.autofix.coding_agent.get_coding_agent_providers")
12171217
@patch("sentry.seer.autofix.coding_agent.get_autofix_state")
@@ -1346,7 +1346,7 @@ def test_launch_with_custom_instruction(
13461346

13471347
# Verify prompt was called with the instruction
13481348
mock_get_prompt.assert_called_with(
1349-
123, AutofixTriggerSource.SOLUTION, "Use TypeScript instead of JavaScript"
1349+
123, AutofixTriggerSource.SOLUTION, "Use TypeScript instead of JavaScript", None
13501350
)
13511351

13521352
@patch("sentry.seer.autofix.coding_agent.get_coding_agent_providers")
@@ -1394,7 +1394,7 @@ def test_launch_with_blank_instruction(
13941394
assert response.data["success"] is True
13951395

13961396
# CharField trims whitespace by default, so blank instruction becomes empty string
1397-
mock_get_prompt.assert_called_with(123, AutofixTriggerSource.SOLUTION, "")
1397+
mock_get_prompt.assert_called_with(123, AutofixTriggerSource.SOLUTION, "", None)
13981398

13991399
@patch("sentry.seer.autofix.coding_agent.get_coding_agent_providers")
14001400
@patch("sentry.seer.autofix.coding_agent.get_autofix_state")
@@ -1441,7 +1441,7 @@ def test_launch_with_empty_instruction(
14411441
assert response.data["success"] is True
14421442

14431443
# Verify prompt was called with empty string instruction
1444-
mock_get_prompt.assert_called_with(123, AutofixTriggerSource.SOLUTION, "")
1444+
mock_get_prompt.assert_called_with(123, AutofixTriggerSource.SOLUTION, "", None)
14451445

14461446
@patch("sentry.seer.autofix.coding_agent.get_coding_agent_providers")
14471447
@patch("sentry.seer.autofix.coding_agent.get_autofix_state")
@@ -1491,7 +1491,9 @@ def test_launch_with_max_length_instruction(
14911491
assert response.data["success"] is True
14921492

14931493
# Verify prompt was called with the long instruction
1494-
mock_get_prompt.assert_called_with(123, AutofixTriggerSource.SOLUTION, long_instruction)
1494+
mock_get_prompt.assert_called_with(
1495+
123, AutofixTriggerSource.SOLUTION, long_instruction, None
1496+
)
14951497

14961498
@patch("sentry.seer.autofix.coding_agent.get_coding_agent_providers")
14971499
@patch(
@@ -1574,5 +1576,5 @@ def test_launch_with_instruction_and_root_cause_trigger(
15741576

15751577
# Verify prompt was called with both trigger_source and instruction
15761578
mock_get_prompt.assert_called_with(
1577-
123, AutofixTriggerSource.ROOT_CAUSE, "Focus on the database queries"
1579+
123, AutofixTriggerSource.ROOT_CAUSE, "Focus on the database queries", None
15781580
)

tests/sentry/seer/autofix/test_autofix_utils.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,58 @@ def test_get_coding_agent_prompt_with_empty_instruction(self, mock_get_autofix_p
178178
assert result == expected
179179
mock_get_autofix_prompt.assert_called_once_with(12345, True, True)
180180

181+
@patch("sentry.seer.autofix.utils.get_autofix_prompt")
182+
def test_get_coding_agent_prompt_with_short_id(self, mock_get_autofix_prompt):
183+
"""Test get_coding_agent_prompt includes Fixes line when short_id is provided."""
184+
mock_get_autofix_prompt.return_value = "This is the autofix prompt"
185+
186+
result = get_coding_agent_prompt(
187+
12345, AutofixTriggerSource.SOLUTION, None, short_id="AIML-2301"
188+
)
189+
190+
assert "Fixes AIML-2301" in result
191+
assert "Include 'Fixes AIML-2301' in the pull request description" in result
192+
assert "Please fix the following issue" in result
193+
assert "This is the autofix prompt" in result
194+
195+
@patch("sentry.seer.autofix.utils.get_autofix_prompt")
196+
def test_get_coding_agent_prompt_without_short_id(self, mock_get_autofix_prompt):
197+
"""Test get_coding_agent_prompt does not include Fixes line when short_id is None."""
198+
mock_get_autofix_prompt.return_value = "This is the autofix prompt"
199+
200+
result = get_coding_agent_prompt(12345, AutofixTriggerSource.SOLUTION, None, short_id=None)
201+
202+
assert "Fixes" not in result
203+
assert "Please fix the following issue" in result
204+
assert "This is the autofix prompt" in result
205+
206+
@patch("sentry.seer.autofix.utils.get_autofix_prompt")
207+
def test_get_coding_agent_prompt_with_short_id_and_instruction(self, mock_get_autofix_prompt):
208+
"""Test get_coding_agent_prompt includes both Fixes line and instruction."""
209+
mock_get_autofix_prompt.return_value = "This is the autofix prompt"
210+
211+
result = get_coding_agent_prompt(
212+
12345,
213+
AutofixTriggerSource.SOLUTION,
214+
"Be careful with backwards compatibility",
215+
short_id="PROJ-1234",
216+
)
217+
218+
assert "Fixes PROJ-1234" in result
219+
assert "Be careful with backwards compatibility" in result
220+
assert "Please fix the following issue" in result
221+
assert "This is the autofix prompt" in result
222+
223+
@patch("sentry.seer.autofix.utils.get_autofix_prompt")
224+
def test_get_coding_agent_prompt_with_empty_short_id(self, mock_get_autofix_prompt):
225+
"""Test get_coding_agent_prompt does not include Fixes line when short_id is empty string."""
226+
mock_get_autofix_prompt.return_value = "This is the autofix prompt"
227+
228+
result = get_coding_agent_prompt(12345, AutofixTriggerSource.SOLUTION, None, short_id="")
229+
230+
assert "Fixes" not in result
231+
assert "Please fix the following issue" in result
232+
181233

182234
class TestAutofixStateParsing(TestCase):
183235
def test_autofix_state_validate_parses_nested_structures(self):

tests/sentry/seer/autofix/test_coding_agent.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,102 @@ def test_api_error_non_401_includes_status_code(
298298
assert (
299299
error_message == "Failed to make request to coding agent. 500 Error: Some error message"
300300
)
301+
302+
@patch("sentry.seer.autofix.coding_agent.store_coding_agent_states_to_seer")
303+
@patch("sentry.seer.autofix.coding_agent.get_coding_agent_prompt")
304+
@patch("sentry.seer.autofix.coding_agent.get_project_seer_preferences")
305+
def test_short_id_passed_to_prompt_when_auto_create_pr_enabled(
306+
self, mock_get_preferences, mock_get_prompt, mock_store_states
307+
):
308+
"""Test that short_id is passed to get_coding_agent_prompt when auto_create_pr is True."""
309+
from sentry.seer.models import (
310+
AutofixHandoffPoint,
311+
PreferenceResponse,
312+
SeerAutomationHandoffConfiguration,
313+
SeerProjectPreference,
314+
)
315+
316+
# Add short_id to the autofix state
317+
self.autofix_state.request.issue["short_id"] = "AIML-2301"
318+
319+
# Setup: Mock preferences with auto_create_pr=True
320+
preference = SeerProjectPreference(
321+
organization_id=self.organization.id,
322+
project_id=self.project.id,
323+
repositories=[
324+
SeerRepoDefinition(
325+
provider="github",
326+
owner="getsentry",
327+
name="sentry",
328+
external_id="123456",
329+
)
330+
],
331+
automation_handoff=SeerAutomationHandoffConfiguration(
332+
handoff_point=AutofixHandoffPoint.ROOT_CAUSE,
333+
target="cursor_background_agent",
334+
integration_id=123,
335+
auto_create_pr=True,
336+
),
337+
)
338+
mock_get_preferences.return_value = PreferenceResponse(
339+
preference=preference, code_mapping_repos=[]
340+
)
341+
342+
mock_get_prompt.return_value = "Test prompt with Fixes AIML-2301"
343+
344+
mock_installation = MagicMock()
345+
mock_installation.launch.return_value = {
346+
"url": "https://example.com/agent",
347+
"id": "agent-123",
348+
}
349+
350+
_launch_agents_for_repos(
351+
installation=mock_installation,
352+
autofix_state=self.autofix_state,
353+
run_id=self.run_id,
354+
organization=self.organization,
355+
trigger_source=AutofixTriggerSource.SOLUTION,
356+
)
357+
358+
# Assert: Verify get_coding_agent_prompt was called with the short_id
359+
mock_get_prompt.assert_called_once()
360+
call_args = mock_get_prompt.call_args
361+
assert call_args[0][3] == "AIML-2301"
362+
363+
@patch("sentry.seer.autofix.coding_agent.store_coding_agent_states_to_seer")
364+
@patch("sentry.seer.autofix.coding_agent.get_coding_agent_prompt")
365+
@patch("sentry.seer.autofix.coding_agent.get_project_seer_preferences")
366+
def test_short_id_not_passed_when_auto_create_pr_disabled(
367+
self, mock_get_preferences, mock_get_prompt, mock_store_states
368+
):
369+
"""Test that short_id is None when auto_create_pr is False."""
370+
from sentry.seer.models import PreferenceResponse
371+
372+
# Add short_id to the autofix state
373+
self.autofix_state.request.issue["short_id"] = "AIML-2301"
374+
375+
# Setup: Mock preferences with auto_create_pr=False (default)
376+
mock_get_preferences.return_value = PreferenceResponse(
377+
preference=None, code_mapping_repos=[]
378+
)
379+
380+
mock_get_prompt.return_value = "Test prompt"
381+
382+
mock_installation = MagicMock()
383+
mock_installation.launch.return_value = {
384+
"url": "https://example.com/agent",
385+
"id": "agent-123",
386+
}
387+
388+
_launch_agents_for_repos(
389+
installation=mock_installation,
390+
autofix_state=self.autofix_state,
391+
run_id=self.run_id,
392+
organization=self.organization,
393+
trigger_source=AutofixTriggerSource.SOLUTION,
394+
)
395+
396+
# Assert: Verify get_coding_agent_prompt was called with short_id=None
397+
mock_get_prompt.assert_called_once()
398+
call_args = mock_get_prompt.call_args
399+
assert call_args[0][3] is None # Fourth positional arg is short_id

0 commit comments

Comments
 (0)