Skip to content

🔧 chore(vsts): mark user config and api unauthorized errors as halts #92358

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 27, 2025
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
21 changes: 20 additions & 1 deletion src/sentry/integrations/vsts/issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@
from sentry.integrations.services.integration import integration_service
from sentry.integrations.source_code_management.issues import SourceCodeIssueIntegration
from sentry.models.activity import Activity
from sentry.shared_integrations.exceptions import ApiError, ApiUnauthorized, IntegrationError
from sentry.shared_integrations.exceptions import (
ApiError,
ApiUnauthorized,
IntegrationError,
IntegrationFormError,
)
from sentry.silo.base import all_silo_function
from sentry.users.models.identity import Identity
from sentry.users.models.user import User
Expand All @@ -34,6 +39,10 @@

VSTS_ISSUE_TITLE_MAX_LENGTH = 128

# Represents error codes caused by misconfiguration when creating a ticket
# Example: Error Communicating with Azure DevOps (HTTP 400): TF401320: Rule Error for field xxx. Error code: Required, HasValues, LimitedToValues, AllowsOldValue, InvalidEmpty.
VSTS_INTEGRATION_FORM_ERROR_CODES_SUBSTRINGS = ["TF401320"]


class VstsIssuesSpec(IssueSyncIntegration, SourceCodeIssueIntegration, ABC):
description = "Integrate Azure DevOps work items by linking a project."
Expand Down Expand Up @@ -61,6 +70,16 @@ def raise_error(self, exc: Exception, identity: Identity | None = None) -> NoRet
substring in str(exc) for substring in VSTS_IDENTITY_DELETED_ERROR_SUBSTRING
):
raise ApiUnauthorized(text=str(exc))
elif isinstance(exc, ApiError) and any(
substring in str(exc) for substring in VSTS_INTEGRATION_FORM_ERROR_CODES_SUBSTRINGS
):
# Parse the field name from the error message
# Example: TF401320: Rule Error for field Empowered Team. Error code: Required, HasValues, LimitedToValues, AllowsOldValue, InvalidEmpty.
try:
field_name = str(exc).split("Error for field ")[1].split(".")[0]
raise IntegrationFormError(field_errors={field_name: f"{field_name} is required."})
except IndexError:
raise IntegrationFormError()
raise super().raise_error(exc, identity)

def get_project_choices(
Expand Down
2 changes: 2 additions & 0 deletions src/sentry/rules/actions/integrations/create_ticket/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from sentry.models.grouplink import GroupLink
from sentry.notifications.utils.links import create_link_to_workflow
from sentry.shared_integrations.exceptions import (
ApiUnauthorized,
IntegrationFormError,
IntegrationInstallationConfigurationError,
)
Expand Down Expand Up @@ -179,6 +180,7 @@ def create_issue(event: GroupEvent, futures: Sequence[RuleFuture]) -> None:
IntegrationInstallationConfigurationError,
IntegrationFormError,
InvalidIdentity,
ApiUnauthorized,
) as e:
# Most of the time, these aren't explicit failures, they're
# some misconfiguration of an issue field - typically Jira.
Expand Down
2 changes: 1 addition & 1 deletion src/sentry/shared_integrations/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ class DuplicateDisplayNameError(IntegrationError):


class IntegrationFormError(IntegrationError):
def __init__(self, field_errors: Mapping[str, Any]) -> None:
def __init__(self, field_errors: Mapping[str, Any] | None = None) -> None:
error = "Invalid integration action"
if field_errors:
error = str(field_errors)
Expand Down
24 changes: 23 additions & 1 deletion tests/sentry/integrations/vsts/test_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
from sentry.integrations.models.integration_external_project import IntegrationExternalProject
from sentry.integrations.services.integration import integration_service
from sentry.integrations.vsts.integration import VstsIntegration
from sentry.shared_integrations.exceptions import ApiError, ApiUnauthorized, IntegrationError
from sentry.shared_integrations.exceptions import (
ApiError,
ApiUnauthorized,
IntegrationError,
IntegrationFormError,
)
from sentry.silo.base import SiloMode
from sentry.silo.util import PROXY_PATH
from sentry.testutils.cases import TestCase
Expand Down Expand Up @@ -193,6 +198,23 @@ def test_create_issue(self):
{"op": "add", "path": "/fields/System.History", "value": "<p>Fix this.</p>\n"},
]

@patch(
"sentry.integrations.vsts.client.VstsApiClient.create_work_item",
side_effect=ApiError(
"Error Communicating with Azure DevOps (HTTP 400): TF401320: Rule Error for field xxx. Error code: Required, HasValues, LimitedToValues, AllowsOldValue, InvalidEmpty."
),
)
@responses.activate
def test_create_issue_integration_form_error(self, create_work_item):
form_data = {
"title": "Hello",
"description": "Fix this.",
"project": "0987654321",
"work_item_type": "Microsoft.VSTS.WorkItemTypes.Task",
}
with pytest.raises(IntegrationFormError):
self.integration.create_issue(form_data)

@responses.activate
def test_create_issue_title_too_long(self):
responses.add(
Expand Down
Loading