Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def post(self, request: Request, organization: Organization) -> Response:
failures = results["failures"]

response_data: LaunchResponse = {
"success": True,
"success": len(successes) > 0,
"launched_count": len(successes),
"failed_count": len(failures),
}
Expand Down
25 changes: 14 additions & 11 deletions src/sentry/seer/autofix/coding_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,17 +329,20 @@ def _launch_agents_for_repos(
if isinstance(e, ApiError):
url_part = f" ({e.url})" if e.url else ""
if e.code == 403 and client is not None:
failure_type = "github_app_permissions"
error_message = f"The Sentry GitHub App installation does not have the required permissions for {repo_name}. Please update your GitHub App permissions to include 'contents:write'."
if repo and repo.integration_id:
try:
sentry_integration = integration_service.get_integration(
integration_id=int(repo.integration_id)
)
if sentry_integration:
github_installation_id = sentry_integration.external_id
except Exception:
sentry_sdk.capture_exception(level="warning")
if e.text and "not licensed" in e.text:
error_message = "Your GitHub account does not have an active Copilot license. Please check your GitHub Copilot subscription."
else:
failure_type = "github_app_permissions"
error_message = f"The Sentry GitHub App installation does not have the required permissions for {repo_name}. Please update your GitHub App permissions to include 'contents:write'."
if repo and repo.integration_id:
try:
sentry_integration = integration_service.get_integration(
integration_id=int(repo.integration_id)
)
if sentry_integration:
github_installation_id = sentry_integration.external_id
except Exception:
sentry_sdk.capture_exception(level="warning")
elif e.code == 401:
error_message = f"Failed to make request to coding agent{url_part}. Please check that your API credentials are correct: {e.code} Error: {e.text}"
else:
Expand Down
25 changes: 14 additions & 11 deletions src/sentry/seer/explorer/coding_agent_handoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,17 +149,20 @@ def launch_coding_agents(
error_message = "Failed to launch coding agent"
github_installation_id: str | None = None
if isinstance(e, ApiError) and e.code == 403 and is_github_copilot:
failure_type = "github_app_permissions"
error_message = f"The Sentry GitHub App installation does not have the required permissions for {repo_name}. Please update your GitHub App permissions to include 'contents:write'."
try:
github_integrations = integration_service.get_integrations(
organization_id=organization.id,
providers=["github"],
)
if github_integrations:
github_installation_id = github_integrations[0].external_id
except Exception:
sentry_sdk.capture_exception(level="warning")
if e.text and "not licensed" in e.text:
error_message = "Your GitHub account does not have an active Copilot license. Please check your GitHub Copilot subscription."
else:
failure_type = "github_app_permissions"
error_message = f"The Sentry GitHub App installation does not have the required permissions for {repo_name}. Please update your GitHub App permissions to include 'contents:write'."
try:
github_integrations = integration_service.get_integrations(
organization_id=organization.id,
providers=["github"],
)
if github_integrations:
github_installation_id = github_integrations[0].external_id
except Exception:
sentry_sdk.capture_exception(level="warning")

failure: dict = {
"repo_name": repo_name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1029,8 +1029,8 @@ def test_all_repos_fail_returns_failures(

with self.feature("organizations:seer-coding-agent-integrations"):
response = self.get_success_response(self.organization.slug, method="post", **data)
# Should succeed but with all failures
assert response.data["success"] is True
# All repos failed, so success is False (but HTTP 200 is still returned)
assert response.data["success"] is False
assert response.data["launched_count"] == 0
assert response.data["failed_count"] == 2
# Should have failure details
Expand Down Expand Up @@ -1136,7 +1136,7 @@ def test_org_installation_403_returns_generic_failure_type(

with self.feature("organizations:seer-coding-agent-integrations"):
response = self.get_success_response(self.organization.slug, method="post", **data)
assert response.data["success"] is True
assert response.data["success"] is False
assert response.data["failed_count"] >= 1
failure = response.data["failures"][0]
assert failure["failure_type"] == "generic"
Expand Down Expand Up @@ -1184,11 +1184,64 @@ def test_copilot_403_returns_github_app_permissions_failure_type(
patch("sentry.seer.autofix.coding_agent.store_coding_agent_states_to_seer"),
):
response = self.get_success_response(self.organization.slug, method="post", **data)
assert response.data["success"] is True
assert response.data["success"] is False
assert response.data["failed_count"] >= 1
failure = response.data["failures"][0]
assert failure["failure_type"] == "github_app_permissions"

@patch("sentry.seer.autofix.coding_agent.get_coding_agent_providers")
@patch("sentry.seer.autofix.coding_agent.get_autofix_state")
@patch("sentry.seer.autofix.coding_agent.get_coding_agent_prompt")
@patch("sentry.seer.autofix.coding_agent.get_project_seer_preferences")
@patch("sentry.seer.autofix.coding_agent.GithubCopilotAgentClient")
@patch("sentry.seer.autofix.coding_agent.github_copilot_identity_service")
def test_copilot_not_licensed_403_returns_generic_failure_type(
self,
mock_identity_service,
mock_copilot_client_class,
mock_get_preferences,
mock_get_prompt,
mock_get_autofix_state,
mock_get_providers,
):
"""Test POST endpoint returns failure_type=generic for Copilot 403s with a licensing error.

When GitHub Copilot returns a 403 with "not licensed to use Copilot", the user's
account lacks an active Copilot subscription. This is distinct from a GitHub App
permissions issue, so we should NOT show the permissions modal.
"""
mock_get_providers.return_value = ["github"]
mock_get_prompt.return_value = "Test prompt"
mock_get_preferences.return_value = PreferenceResponse(
preference=None, code_mapping_repos=[]
)
mock_identity_service.get_access_token_for_user.return_value = "test-copilot-token"

mock_client_instance = MagicMock()
mock_copilot_client_class.return_value = mock_client_instance
mock_client_instance.launch.side_effect = ApiError(
"unauthorized: not licensed to use Copilot", code=403
)

mock_get_autofix_state.return_value = self._create_mock_autofix_state()

data = {"provider": "github_copilot", "run_id": 123}

with (
self.feature("organizations:seer-coding-agent-integrations"),
self.feature("organizations:integrations-github-copilot-agent"),
patch("sentry.seer.autofix.coding_agent.store_coding_agent_states_to_seer"),
):
response = self.get_success_response(self.organization.slug, method="post", **data)
assert response.data["success"] is False
assert response.data["failed_count"] >= 1
failure = response.data["failures"][0]
assert failure["failure_type"] == "generic"
assert (
"not licensed" in failure["error_message"]
or "Copilot license" in failure["error_message"]
)

@patch("sentry.seer.autofix.coding_agent.get_coding_agent_providers")
@patch("sentry.seer.autofix.coding_agent.get_autofix_state")
@patch("sentry.seer.autofix.coding_agent.get_coding_agent_prompt")
Expand Down Expand Up @@ -1227,7 +1280,7 @@ def test_non_403_error_returns_generic_failure_type(

with self.feature("organizations:seer-coding-agent-integrations"):
response = self.get_success_response(self.organization.slug, method="post", **data)
assert response.data["success"] is True
assert response.data["success"] is False
assert response.data["failed_count"] >= 1
assert "failures" in response.data
failure = response.data["failures"][0]
Expand Down
46 changes: 46 additions & 0 deletions tests/sentry/seer/explorer/test_coding_agent_handoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rest_framework.exceptions import PermissionDenied

from sentry.seer.explorer.coding_agent_handoff import launch_coding_agents
from sentry.shared_integrations.exceptions import ApiError
from sentry.testutils.cases import TestCase


Expand Down Expand Up @@ -135,3 +136,48 @@ def test_branch_name_is_sanitized(self, mock_features, mock_validate, mock_store
# Verify launch was called with a sanitized branch name
launch_request = mock_installation.launch.call_args[0][0]
assert launch_request.branch_name.startswith("my-fix-")

@patch("sentry.seer.explorer.coding_agent_handoff.store_coding_agent_states_to_seer")
@patch("sentry.seer.explorer.coding_agent_handoff.GithubCopilotAgentClient")
@patch("sentry.seer.explorer.coding_agent_handoff.github_copilot_identity_service")
@patch("sentry.seer.explorer.coding_agent_handoff.features.has")
def test_copilot_not_licensed_403_returns_generic_failure_type(
self,
mock_features,
mock_identity_service,
mock_copilot_client_class,
mock_store,
):
"""Test that Copilot 403 'not licensed' errors return generic failure_type.

When GitHub Copilot returns a 403 with "not licensed to use Copilot", the user's
account lacks an active Copilot subscription. This is distinct from a GitHub App
permissions issue, so we should NOT show the permissions modal.
"""
mock_features.return_value = True
mock_identity_service.get_access_token_for_user.return_value = "test-token"

mock_client_instance = MagicMock()
mock_copilot_client_class.return_value = mock_client_instance
mock_client_instance.launch.side_effect = ApiError(
"unauthorized: not licensed to use Copilot", code=403
)

result = launch_coding_agents(
organization=self.organization,
integration_id=None,
run_id=self.run_id,
prompt="Fix the bug",
repos=["owner/repo"],
provider="github_copilot",
user_id=1,
)

assert len(result["successes"]) == 0
assert len(result["failures"]) == 1
failure = result["failures"][0]
assert failure["failure_type"] == "generic"
assert (
"not licensed" in failure["error_message"]
or "Copilot license" in failure["error_message"]
)
Loading