Skip to content

Commit 2faee57

Browse files
authored
Reapply "feat(seer): Attribute autofix referrer to MCP for MCP requests (#117521)" (#117735)
Relands #117521 ("feat(seer): Attribute autofix referrer to MCP for MCP requests"), which was reverted in 3a75f61. Attributes the autofix referrer to MCP for requests originating from the official Sentry MCP server, and centralizes MCP request detection in a shared `is_mcp_request` helper in `sentry.utils.http`. Agent transcript: https://claudescope.sentry.dev/share/YnvnJme51BPsRo02v1xZr1dl1u0_fTENx2hxgIcuo5A
1 parent 15557f2 commit 2faee57

5 files changed

Lines changed: 63 additions & 9 deletions

File tree

src/sentry/issues/action_log/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from sentry.users.models.user import User
1919
from sentry.users.services.user import RpcUser
2020
from sentry.utils import metrics
21+
from sentry.utils.http import is_mcp_request
2122

2223
logger = logging.getLogger(__name__)
2324

@@ -31,7 +32,6 @@
3132
# If you're adding a new caller to an instrumented function (e.g. GroupAssignee.objects.assign),
3233
# wrap it with action_context_scope() so the action gets proper source attribution.
3334

34-
MCP_USER_AGENT_PREFIX = "sentry-mcp/"
3535
MCP_CLIENT_FAMILY_HEADER = "HTTP_X_SENTRY_MCP_CLIENT_FAMILY"
3636
SEER_REFERRER_HEADER = "HTTP_X_SEER_REFERRER"
3737

@@ -77,7 +77,7 @@ def resolve_action_source(request: Request) -> str:
7777
"""
7878
user_agent = request.META.get("HTTP_USER_AGENT", "")
7979

80-
if user_agent.startswith(MCP_USER_AGENT_PREFIX):
80+
if is_mcp_request(request):
8181
family = request.META.get(MCP_CLIENT_FAMILY_HEADER, "").strip().lower()
8282
if family in KNOWN_MCP_CLIENT_FAMILIES:
8383
return f"{ActionSource.MCP}:{family}"

src/sentry/issues/endpoints/group_details.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
resolve_action_actor,
4949
resolve_action_source,
5050
)
51-
from sentry.issues.action_log.base import MCP_USER_AGENT_PREFIX
5251
from sentry.issues.action_log.types import ViewAction
5352
from sentry.issues.constants import (
5453
ISSUE_VIEW_CACHE_KEY_TTL,
@@ -76,6 +75,7 @@
7675
from sentry.types.ratelimit import RateLimit, RateLimitCategory
7776
from sentry.users.services.user.service import user_service
7877
from sentry.utils import metrics
78+
from sentry.utils.http import is_mcp_request
7979

8080
logger = logging.getLogger(__name__)
8181

@@ -506,8 +506,7 @@ def send_issue_view_attribution(request: Request, response: Response, group: Any
506506
if not isinstance(group, Group):
507507
return
508508

509-
user_agent = request.META.get("HTTP_USER_AGENT", "")
510-
if isinstance(user_agent, str) and user_agent.startswith(MCP_USER_AGENT_PREFIX):
509+
if is_mcp_request(request):
511510
client_family = request.headers.get("x-sentry-mcp-client-family") or "unknown"
512511
analytics.record(
513512
IssueViewedEvent(

src/sentry/seer/endpoints/group_ai_autofix.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
from sentry.seer.models import SeerPermissionError
7171
from sentry.types.ratelimit import RateLimit, RateLimitCategory
7272
from sentry.users.services.user.service import user_service
73+
from sentry.utils.http import is_mcp_request
7374

7475
logger = logging.getLogger(__name__)
7576

@@ -80,8 +81,12 @@ def _is_unknown_run_id_error(error: SeerPermissionError) -> bool:
8081
return getattr(error, "message", None) == UNKNOWN_RUN_ID_FOR_GROUP
8182

8283

83-
def _parse_autofix_referrer(raw: str | None) -> AutofixReferrer:
84+
def _parse_autofix_referrer(raw: str | None, request: Request) -> AutofixReferrer:
8485
if raw is None:
86+
# Fall back to the request origin: requests from the Sentry MCP server are
87+
# attributed to MCP, everything else to the generic endpoint referrer.
88+
if is_mcp_request(request):
89+
return AutofixReferrer.MCP
8590
return AutofixReferrer.GROUP_AUTOFIX_ENDPOINT
8691
try:
8792
return AutofixReferrer(raw)
@@ -268,7 +273,7 @@ def post(
268273
handoff_result: AutofixHandoffResponse = trigger_coding_agent_handoff(
269274
group=group,
270275
run_id=resolved_run_id,
271-
referrer=_parse_autofix_referrer(data.get("referrer")),
276+
referrer=_parse_autofix_referrer(data.get("referrer"), request),
272277
integration_id=integration_id,
273278
provider=provider,
274279
user_id=request.user.id if request.user else None,
@@ -290,7 +295,7 @@ def post(
290295
trigger_push_changes(
291296
group,
292297
resolved_run_id,
293-
referrer=_parse_autofix_referrer(data.get("referrer")),
298+
referrer=_parse_autofix_referrer(data.get("referrer"), request),
294299
repo_name=repo_name,
295300
)
296301
except SeerPermissionError:
@@ -341,7 +346,7 @@ def post(
341346
run_id = trigger_autofix_agent(
342347
group=group,
343348
step=AutofixStep(step),
344-
referrer=_parse_autofix_referrer(data.get("referrer")),
349+
referrer=_parse_autofix_referrer(data.get("referrer"), request),
345350
stopping_point=AutofixStoppingPoint(stopping_point) if stopping_point else None,
346351
run_id=resolved_run_id,
347352
user_context=user_context,

src/sentry/utils/http.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,17 @@
77
from asgiref.sync import sync_to_async
88
from django.conf import settings
99
from django.http import HttpRequest
10+
from rest_framework.request import Request
1011

1112
from sentry import options
1213

1314
if TYPE_CHECKING:
1415
from sentry.models.project import Project
1516

17+
# User-agent prefix set by the official Sentry MCP server (source of truth:
18+
# getsentry/sentry-mcp). Used to attribute requests originating from the MCP.
19+
MCP_USER_AGENT_PREFIX = "sentry-mcp/"
20+
1621

1722
class ParsedUriMatch(NamedTuple):
1823
scheme: str
@@ -212,6 +217,14 @@ def origin_from_request(request: HttpRequest) -> str | None:
212217
return rv
213218

214219

220+
def is_mcp_request(request: HttpRequest | Request) -> bool:
221+
"""
222+
Whether the request originated from the official Sentry MCP server, identified
223+
by its `sentry-mcp/` user-agent prefix.
224+
"""
225+
return request.META.get("HTTP_USER_AGENT", "").startswith(MCP_USER_AGENT_PREFIX)
226+
227+
215228
def percent_encode(val: str) -> str:
216229
# see https://en.wikipedia.org/wiki/Percent-encoding
217230
return quote(val).replace("%7E", "~").replace("/", "%2F")

tests/sentry/seer/endpoints/test_group_ai_autofix.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,43 @@ def test_post_continue_with_garbage_sentry_run_id_returns_400(self, mock_trigger
177177
assert response.status_code == 400, response.data
178178
mock_trigger_explorer.assert_not_called()
179179

180+
@patch("sentry.seer.endpoints.group_ai_autofix.trigger_autofix_agent")
181+
def test_post_from_mcp_defaults_referrer_to_mcp(self, mock_trigger_explorer):
182+
"""A request from the Sentry MCP server defaults the referrer to api.mcp."""
183+
group = self.create_group()
184+
mock_trigger_explorer.return_value = 123
185+
186+
self.login_as(user=self.user)
187+
response = self.client.post(
188+
self._get_url(group.id),
189+
data={"step": "root_cause"},
190+
format="json",
191+
headers={
192+
"user-agent": "sentry-mcp/0.35.0 (https://mcp.sentry.dev)",
193+
"X-Sentry-MCP-Client-Family": "cursor",
194+
},
195+
)
196+
197+
assert response.status_code == 202, response.data
198+
assert mock_trigger_explorer.call_args.kwargs["referrer"] == AutofixReferrer.MCP
199+
200+
@patch("sentry.seer.endpoints.group_ai_autofix.trigger_autofix_agent")
201+
def test_post_explicit_referrer_overrides_mcp_default(self, mock_trigger_explorer):
202+
"""An explicitly supplied referrer takes precedence over the MCP default."""
203+
group = self.create_group()
204+
mock_trigger_explorer.return_value = 123
205+
206+
self.login_as(user=self.user)
207+
response = self.client.post(
208+
self._get_url(group.id),
209+
data={"step": "root_cause", "referrer": AutofixReferrer.WEB.value},
210+
format="json",
211+
headers={"user-agent": "sentry-mcp/0.35.0 (https://mcp.sentry.dev)"},
212+
)
213+
214+
assert response.status_code == 202, response.data
215+
assert mock_trigger_explorer.call_args.kwargs["referrer"] == AutofixReferrer.WEB
216+
180217
@patch("sentry.seer.endpoints.group_ai_autofix.trigger_autofix_agent")
181218
def test_stopping_point(self, mock_trigger_explorer):
182219
"""Stopping point forces the step to be root_cause"""

0 commit comments

Comments
 (0)