diff --git a/src/sentry/integrations/vsts/issues.py b/src/sentry/integrations/vsts/issues.py index 774fd55fc09230..eb30a11fa53de3 100644 --- a/src/sentry/integrations/vsts/issues.py +++ b/src/sentry/integrations/vsts/issues.py @@ -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 @@ -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." @@ -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( diff --git a/src/sentry/rules/actions/integrations/create_ticket/utils.py b/src/sentry/rules/actions/integrations/create_ticket/utils.py index c47679f66fe4f2..4d9683620345f2 100644 --- a/src/sentry/rules/actions/integrations/create_ticket/utils.py +++ b/src/sentry/rules/actions/integrations/create_ticket/utils.py @@ -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, ) @@ -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. diff --git a/src/sentry/shared_integrations/exceptions/__init__.py b/src/sentry/shared_integrations/exceptions/__init__.py index 89174495efc5f2..3100d5b038308d 100644 --- a/src/sentry/shared_integrations/exceptions/__init__.py +++ b/src/sentry/shared_integrations/exceptions/__init__.py @@ -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) diff --git a/tests/sentry/integrations/vsts/test_issues.py b/tests/sentry/integrations/vsts/test_issues.py index 37380ad2f0287c..6042dc3a82e63c 100644 --- a/tests/sentry/integrations/vsts/test_issues.py +++ b/tests/sentry/integrations/vsts/test_issues.py @@ -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 @@ -193,6 +198,23 @@ def test_create_issue(self): {"op": "add", "path": "/fields/System.History", "value": "
Fix this.
\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(