Skip to content

Commit ac0ceb4

Browse files
authored
🔧 chore: classify vsts identity deleted errors as halt (#92078)
1 parent a8a30ea commit ac0ceb4

File tree

5 files changed

+61
-7
lines changed

5 files changed

+61
-7
lines changed

src/sentry/integrations/tasks/sync_status_outbound.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from sentry import analytics, features
22
from sentry.constants import ObjectStatus
3+
from sentry.integrations.base import IntegrationInstallation
34
from sentry.integrations.models.external_issue import ExternalIssue
45
from sentry.integrations.models.integration import Integration
56
from sentry.integrations.project_management.metrics import (
@@ -8,7 +9,7 @@
89
)
910
from sentry.integrations.services.integration import integration_service
1011
from sentry.models.group import Group, GroupStatus
11-
from sentry.shared_integrations.exceptions import IntegrationFormError
12+
from sentry.shared_integrations.exceptions import ApiUnauthorized, IntegrationFormError
1213
from sentry.silo.base import SiloMode
1314
from sentry.tasks.base import instrumented_task, retry, track_group_async_operation
1415
from sentry.taskworker.config import TaskworkerConfig
@@ -55,7 +56,9 @@ def sync_status_outbound(group_id: int, external_issue_id: int) -> bool | None:
5556
)
5657
if not integration:
5758
return None
58-
installation = integration.get_installation(organization_id=external_issue.organization_id)
59+
installation: IntegrationInstallation = integration.get_installation(
60+
organization_id=external_issue.organization_id
61+
)
5962
if not (hasattr(installation, "should_sync") and hasattr(installation, "sync_status_outbound")):
6063
return None
6164

@@ -76,7 +79,7 @@ def sync_status_outbound(group_id: int, external_issue_id: int) -> bool | None:
7679
installation.sync_status_outbound(
7780
external_issue, group.status == GroupStatus.RESOLVED, group.project_id
7881
)
79-
except IntegrationFormError as e:
82+
except (IntegrationFormError, ApiUnauthorized) as e:
8083
lifecycle.record_halt(halt_reason=e)
8184
return None
8285
analytics.record(

src/sentry/integrations/utils/metrics.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ def __init__(self, payload: EventLifecycleMetric, assume_success: bool = True) -
108108
self._state: EventLifecycleOutcome | None = None
109109
self._extra = dict(self.payload.get_extras())
110110

111+
def get_state(self) -> EventLifecycleOutcome | None:
112+
return self._state
113+
111114
def add_extra(self, name: str, value: Any) -> None:
112115
"""Add a value to logged "extra" data.
113116

src/sentry/integrations/vsts/issues.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from abc import ABC
44
from collections.abc import Mapping, MutableMapping, Sequence
5-
from typing import TYPE_CHECKING, Any
5+
from typing import TYPE_CHECKING, Any, NoReturn
66

77
from django.urls import reverse
88
from django.utils.translation import gettext as _
@@ -17,6 +17,7 @@
1717
from sentry.models.activity import Activity
1818
from sentry.shared_integrations.exceptions import ApiError, ApiUnauthorized, IntegrationError
1919
from sentry.silo.base import all_silo_function
20+
from sentry.users.models.identity import Identity
2021
from sentry.users.models.user import User
2122
from sentry.users.services.user import RpcUser
2223
from sentry.users.services.user.service import user_service
@@ -25,6 +26,12 @@
2526
from sentry.integrations.models.external_issue import ExternalIssue
2627
from sentry.models.group import Group
2728

29+
# Specific substring to identify Azure Entra ID "identity deleted" errors
30+
# Example: According to Microsoft Entra, your Identity xxx is currently Deleted within the following Microsoft Entra tenant: xxx Please contact your Microsoft Entra administrator to resolve this.
31+
VSTS_IDENTITY_DELETED_ERROR_SUBSTRING = [
32+
"is currently Deleted within the following Microsoft Entra tenant"
33+
]
34+
2835

2936
class VstsIssuesSpec(IssueSyncIntegration, SourceCodeIssueIntegration, ABC):
3037
description = "Integrate Azure DevOps work items by linking a project."
@@ -46,6 +53,14 @@ def create_default_repo_choice(self, default_repo: str) -> tuple[str, str]:
4653
project = self.get_client().get_project(default_repo)
4754
return (project["id"], project["name"])
4855

56+
def raise_error(self, exc: Exception, identity: Identity | None = None) -> NoReturn:
57+
# Reraise Azure Specific Errors correctly
58+
if isinstance(exc, ApiError) and any(
59+
substring in str(exc) for substring in VSTS_IDENTITY_DELETED_ERROR_SUBSTRING
60+
):
61+
raise ApiUnauthorized(text=str(exc))
62+
raise super().raise_error(exc, identity)
63+
4964
def get_project_choices(
5065
self, group: Group | None = None, **kwargs: Any
5166
) -> tuple[str | None, Sequence[tuple[str, str]]]:

tests/sentry/integrations/tasks/test_sync_status_outbound.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from sentry.integrations.models import ExternalIssue, Integration
77
from sentry.integrations.tasks import sync_status_outbound
88
from sentry.integrations.types import EventLifecycleOutcome
9-
from sentry.shared_integrations.exceptions import IntegrationFormError
9+
from sentry.shared_integrations.exceptions import ApiUnauthorized, IntegrationFormError
1010
from sentry.testutils.asserts import assert_count_of_metric, assert_halt_metric
1111
from sentry.testutils.cases import TestCase
1212
from sentry.testutils.silo import assume_test_silo_mode_of, region_silo_test
@@ -20,6 +20,10 @@ def raise_integration_form_error(*args, **kwargs):
2020
raise IntegrationFormError(field_errors={"foo": "Invalid foo provided"})
2121

2222

23+
def raise_api_unauthorized_error(*args, **kwargs):
24+
raise ApiUnauthorized(text="auth failed")
25+
26+
2327
@region_silo_test
2428
class TestSyncStatusOutbound(TestCase):
2529
def setUp(self):
@@ -129,5 +133,24 @@ def test_integration_form_error(self, mock_sync_status, mock_record):
129133
)
130134

131135
assert_halt_metric(
132-
mock_record=mock_record, error_msg=IntegrationFormError({"error": "bruh"})
136+
mock_record=mock_record, error_msg=IntegrationFormError({"foo": "Invalid foo provided"})
137+
)
138+
139+
@mock.patch("sentry.integrations.utils.metrics.EventLifecycle.record_event")
140+
@mock.patch.object(ExampleIntegration, "sync_status_outbound")
141+
def test_api_unauthorized_error_halts(self, mock_sync_status, mock_record):
142+
mock_sync_status.side_effect = raise_api_unauthorized_error
143+
external_issue: ExternalIssue = self.create_integration_external_issue(
144+
group=self.group, key="foo_integration", integration=self.example_integration
145+
)
146+
147+
sync_status_outbound(self.group.id, external_issue_id=external_issue.id)
148+
149+
assert_count_of_metric(
150+
mock_record=mock_record, outcome=EventLifecycleOutcome.STARTED, outcome_count=1
133151
)
152+
assert_count_of_metric(
153+
mock_record=mock_record, outcome=EventLifecycleOutcome.HALTED, outcome_count=1
154+
)
155+
156+
assert_halt_metric(mock_record=mock_record, error_msg=ApiUnauthorized("auth failed"))

tests/sentry/integrations/vsts/test_issues.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from sentry.integrations.models.integration_external_project import IntegrationExternalProject
2222
from sentry.integrations.services.integration import integration_service
2323
from sentry.integrations.vsts.integration import VstsIntegration
24-
from sentry.shared_integrations.exceptions import IntegrationError
24+
from sentry.shared_integrations.exceptions import ApiError, ApiUnauthorized, IntegrationError
2525
from sentry.silo.base import SiloMode
2626
from sentry.silo.util import PROXY_PATH
2727
from sentry.testutils.cases import TestCase
@@ -591,3 +591,13 @@ def test_default_project_no_projects(self):
591591
fields = self.integration.get_create_issue_config(self.group, self.user)
592592

593593
self.assert_project_field(fields, None, [])
594+
595+
596+
@region_silo_test
597+
class VstsIssueRaiseErrorTest(VstsIssueBase):
598+
@responses.activate
599+
def test_raise_error_api_unauthorized(self):
600+
error_message = "According to Microsoft Entra, your Identity xxx is currently Deleted within the following Microsoft Entra tenant: xxx Please contact your Microsoft Entra administrator to resolve this."
601+
api_error = ApiError(error_message)
602+
with pytest.raises(ApiUnauthorized):
603+
self.integration.raise_error(api_error)

0 commit comments

Comments
 (0)