Skip to content

Commit 321954f

Browse files
fix(Jira-Server): Adds halts, better exceptions for failed syncs (#95281)
1 parent 3deb0b6 commit 321954f

File tree

9 files changed

+165
-63
lines changed

9 files changed

+165
-63
lines changed

src/sentry/integrations/jira/integration.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
from sentry import features
1717
from sentry.eventstore.models import GroupEvent
18-
from sentry.exceptions import InvalidConfiguration
1918
from sentry.integrations.base import (
2019
FeatureDescription,
2120
IntegrationData,
@@ -25,7 +24,12 @@
2524
)
2625
from sentry.integrations.jira.models.create_issue_metadata import JiraIssueTypeMetadata
2726
from sentry.integrations.jira.tasks import migrate_issues
28-
from sentry.integrations.mixins.issues import MAX_CHAR, IssueSyncIntegration, ResolveSyncAction
27+
from sentry.integrations.mixins.issues import (
28+
MAX_CHAR,
29+
IntegrationSyncTargetNotFound,
30+
IssueSyncIntegration,
31+
ResolveSyncAction,
32+
)
2933
from sentry.integrations.models.external_issue import ExternalIssue
3034
from sentry.integrations.models.integration_external_project import IntegrationExternalProject
3135
from sentry.integrations.pipeline import IntegrationPipeline
@@ -1014,16 +1018,20 @@ def sync_assignee_outbound(
10141018
},
10151019
)
10161020
if not user.emails:
1017-
raise InvalidConfiguration(
1021+
raise IntegrationSyncTargetNotFound(
10181022
{
10191023
"email": "User must have a verified email on Sentry to sync assignee in Jira",
10201024
"help": "https://sentry.io/settings/account/emails",
10211025
}
10221026
)
1023-
raise InvalidConfiguration({"email": "Unable to find the requested user"})
1027+
raise IntegrationSyncTargetNotFound("No matching Jira user found.")
10241028
try:
10251029
id_field = client.user_id_field()
10261030
client.assign_issue(external_issue.key, jira_user and jira_user.get(id_field))
1031+
except ApiUnauthorized as e:
1032+
raise IntegrationInstallationConfigurationError(
1033+
"Insufficient permissions to assign user to the Jira issue."
1034+
) from e
10271035
except ApiError as e:
10281036
# TODO(jess): do we want to email people about these types of failures?
10291037
logger.info(
@@ -1036,7 +1044,7 @@ def sync_assignee_outbound(
10361044
"issue_key": external_issue.key,
10371045
},
10381046
)
1039-
raise
1047+
raise IntegrationError("There was an error assigning the issue.") from e
10401048

10411049
def sync_status_outbound(
10421050
self, external_issue: ExternalIssue, is_resolved: bool, project_id: int

src/sentry/integrations/jira_server/integration.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from sentry.integrations.jira.tasks import migrate_issues
3030
from sentry.integrations.jira_server.utils.choice import build_user_choice
3131
from sentry.integrations.mixins import ResolveSyncAction
32-
from sentry.integrations.mixins.issues import IssueSyncIntegration
32+
from sentry.integrations.mixins.issues import IntegrationSyncTargetNotFound, IssueSyncIntegration
3333
from sentry.integrations.models.external_actor import ExternalActor
3434
from sentry.integrations.models.external_issue import ExternalIssue
3535
from sentry.integrations.models.integration_external_project import IntegrationExternalProject
@@ -1272,30 +1272,32 @@ def sync_assignee_outbound(
12721272
if jira_user is None:
12731273
# TODO(jess): do we want to email people about these types of failures?
12741274
logger.info(
1275-
"jira.assignee-not-found",
1275+
"jira_server.assignee-not-found",
12761276
extra=logging_context,
12771277
)
1278-
raise IntegrationError("Failed to assign user to Jira Server issue")
1278+
raise IntegrationSyncTargetNotFound("No matching Jira Server user found")
12791279
try:
12801280
id_field = client.user_id_field()
12811281
client.assign_issue(external_issue.key, jira_user and jira_user.get(id_field))
1282-
except ApiUnauthorized:
1282+
except ApiUnauthorized as e:
12831283
logger.info(
1284-
"jira.user-assignment-unauthorized",
1284+
"jira_server.user-assignment-unauthorized",
12851285
extra={
12861286
**logging_context,
12871287
},
12881288
)
1289-
raise IntegrationError("Insufficient permissions to assign user to Jira Server issue")
1289+
raise IntegrationInstallationConfigurationError(
1290+
"Insufficient permissions to assign user to Jira Server issue"
1291+
) from e
12901292
except ApiError as e:
12911293
logger.info(
1292-
"jira.user-assignment-request-error",
1294+
"jira_server.user-assignment-request-error",
12931295
extra={
12941296
**logging_context,
12951297
"error": str(e),
12961298
},
12971299
)
1298-
raise IntegrationError("Failed to assign user to Jira Server issue")
1300+
raise IntegrationError("Failed to assign user to Jira Server issue") from e
12991301

13001302
def sync_status_outbound(
13011303
self, external_issue: ExternalIssue, is_resolved: bool, project_id: int

src/sentry/integrations/mixins/issues.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from sentry.models.grouplink import GroupLink
2525
from sentry.models.project import Project
2626
from sentry.notifications.utils import get_notification_group_title
27+
from sentry.shared_integrations.exceptions import IntegrationError
2728
from sentry.silo.base import all_silo_function
2829
from sentry.users.models.user import User
2930
from sentry.users.services.user import RpcUser
@@ -370,6 +371,10 @@ def update_comment(self, issue_id, user_id, group_note):
370371
pass
371372

372373

374+
class IntegrationSyncTargetNotFound(IntegrationError):
375+
pass
376+
377+
373378
class IssueSyncIntegration(IssueBasicIntegration, ABC):
374379
comment_key: ClassVar[str | None] = None
375380
outbound_status_key: ClassVar[str | None] = None

src/sentry/integrations/tasks/sync_assignee_outbound.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from sentry import analytics, features
44
from sentry.constants import ObjectStatus
5-
from sentry.exceptions import InvalidConfiguration
65
from sentry.integrations.errors import OrganizationIntegrationNotFound
76
from sentry.integrations.models.external_issue import ExternalIssue
87
from sentry.integrations.models.integration import Integration
@@ -13,7 +12,11 @@
1312
from sentry.integrations.services.assignment_source import AssignmentSource
1413
from sentry.integrations.services.integration import integration_service
1514
from sentry.models.organization import Organization
16-
from sentry.shared_integrations.exceptions import ApiUnauthorized, IntegrationError
15+
from sentry.shared_integrations.exceptions import (
16+
ApiUnauthorized,
17+
IntegrationError,
18+
IntegrationInstallationConfigurationError,
19+
)
1720
from sentry.silo.base import SiloMode
1821
from sentry.tasks.base import instrumented_task, retry
1922
from sentry.taskworker.config import TaskworkerConfig
@@ -53,6 +56,8 @@ def sync_assignee_outbound(
5356
assign: bool,
5457
assignment_source_dict: dict[str, Any] | None = None,
5558
) -> None:
59+
from sentry.integrations.mixins.issues import IntegrationSyncTargetNotFound
60+
5661
# Sync Sentry assignee to an external issue.
5762
external_issue = ExternalIssue.objects.get(id=external_issue_id)
5863

@@ -98,5 +103,10 @@ def sync_assignee_outbound(
98103
id=integration.id,
99104
organization_id=external_issue.organization_id,
100105
)
101-
except (OrganizationIntegrationNotFound, ApiUnauthorized, InvalidConfiguration) as e:
106+
except (
107+
OrganizationIntegrationNotFound,
108+
ApiUnauthorized,
109+
IntegrationSyncTargetNotFound,
110+
IntegrationInstallationConfigurationError,
111+
) as e:
102112
lifecycle.record_halt(halt_reason=e)

src/sentry/integrations/vsts/issues.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from sentry.constants import ObjectStatus
1313
from sentry.integrations.mixins import ResolveSyncAction
14-
from sentry.integrations.mixins.issues import IssueSyncIntegration
14+
from sentry.integrations.mixins.issues import IntegrationSyncTargetNotFound, IssueSyncIntegration
1515
from sentry.integrations.services.integration import integration_service
1616
from sentry.integrations.source_code_management.issues import SourceCodeIssueIntegration
1717
from sentry.integrations.types import IntegrationProviderSlug
@@ -21,6 +21,7 @@
2121
ApiUnauthorized,
2222
IntegrationError,
2323
IntegrationFormError,
24+
IntegrationInstallationConfigurationError,
2425
)
2526
from sentry.silo.base import all_silo_function
2627
from sentry.users.models.identity import Identity
@@ -295,11 +296,11 @@ def sync_assignee_outbound(
295296
"issue_key": external_issue.key,
296297
},
297298
)
298-
return
299+
raise IntegrationSyncTargetNotFound("No matching VSTS user found.")
299300

300301
try:
301302
client.update_work_item(external_issue.key, assigned_to=assignee)
302-
except (ApiUnauthorized, ApiError):
303+
except (ApiUnauthorized, ApiError) as e:
303304
self.logger.info(
304305
"vsts.failed-to-assign",
305306
extra={
@@ -308,6 +309,11 @@ def sync_assignee_outbound(
308309
"issue_key": external_issue.key,
309310
},
310311
)
312+
if isinstance(e, ApiUnauthorized):
313+
raise IntegrationInstallationConfigurationError(
314+
"Insufficient permissions to assign user to the VSTS issue."
315+
) from e
316+
raise IntegrationError("There was an error assigning the issue.") from e
311317
except Exception as e:
312318
self.raise_error(e)
313319

src/sentry/shared_integrations/exceptions/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ class IntegrationError(Exception):
169169
pass
170170

171171

172-
class IntegrationInstallationConfigurationError(Exception):
172+
class IntegrationInstallationConfigurationError(IntegrationError):
173173
"""
174174
Error when external API access is blocked due to configuration issues
175175
like permissions, visibility changes, or invalid project settings.

tests/sentry/integrations/jira/test_integration.py

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,20 @@
99

1010
from fixtures.integrations.jira.stub_client import StubJiraApiClient
1111
from fixtures.integrations.stub_service import StubService
12-
from sentry.exceptions import InvalidConfiguration
1312
from sentry.integrations.jira.integration import JiraIntegrationProvider
1413
from sentry.integrations.jira.views import SALT
14+
from sentry.integrations.mixins.issues import IntegrationSyncTargetNotFound
1515
from sentry.integrations.models.external_issue import ExternalIssue
1616
from sentry.integrations.models.integration import Integration
1717
from sentry.integrations.models.integration_external_project import IntegrationExternalProject
1818
from sentry.integrations.models.organization_integration import OrganizationIntegration
1919
from sentry.integrations.services.integration import integration_service
2020
from sentry.models.grouplink import GroupLink
2121
from sentry.models.groupmeta import GroupMeta
22-
from sentry.shared_integrations.exceptions import IntegrationError
22+
from sentry.shared_integrations.exceptions import (
23+
IntegrationError,
24+
IntegrationInstallationConfigurationError,
25+
)
2326
from sentry.silo.base import SiloMode
2427
from sentry.testutils.cases import APITestCase, IntegrationTestCase
2528
from sentry.testutils.factories import EventType
@@ -825,7 +828,7 @@ def test_sync_assignee_outbound_no_email(self):
825828
"https://example.atlassian.net/rest/api/2/user/assignable/search",
826829
json=[{"accountId": "deadbeef123", "displayName": "Dead Beef"}],
827830
)
828-
with pytest.raises(InvalidConfiguration):
831+
with pytest.raises(IntegrationSyncTargetNotFound):
829832
installation.sync_assignee_outbound(external_issue, user)
830833

831834
# No sync made as jira users don't have email addresses
@@ -866,6 +869,56 @@ def test_sync_assignee_outbound_use_email_api(self):
866869
assert assign_issue_response.status_code == 200
867870
assert assign_issue_response.request.body == b'{"accountId": "deadbeef123"}'
868871

872+
@responses.activate
873+
def test_sync_assignee_outbound_api_unauthorized(self):
874+
user = serialize_rpc_user(self.create_user(email="bob@example.com"))
875+
issue_id = "APP-123"
876+
installation = self.integration.get_installation(self.organization.id)
877+
assign_issue_url = "https://example.atlassian.net/rest/api/2/issue/%s/assignee" % issue_id
878+
879+
external_issue = ExternalIssue.objects.create(
880+
organization_id=self.organization.id, integration_id=installation.model.id, key=issue_id
881+
)
882+
883+
responses.add(
884+
responses.GET,
885+
"https://example.atlassian.net/rest/api/2/user/assignable/search",
886+
json=[{"accountId": "deadbeef123", "emailAddress": "bob@example.com"}],
887+
)
888+
889+
responses.add(responses.PUT, assign_issue_url, status=401, json={})
890+
891+
with pytest.raises(IntegrationInstallationConfigurationError) as excinfo:
892+
installation.sync_assignee_outbound(external_issue, user)
893+
894+
assert str(excinfo.value) == "Insufficient permissions to assign user to the Jira issue."
895+
assert len(responses.calls) == 2
896+
897+
@responses.activate
898+
def test_sync_assignee_outbound_api_error(self):
899+
user = serialize_rpc_user(self.create_user(email="bob@example.com"))
900+
issue_id = "APP-123"
901+
installation = self.integration.get_installation(self.organization.id)
902+
assign_issue_url = "https://example.atlassian.net/rest/api/2/issue/%s/assignee" % issue_id
903+
904+
external_issue = ExternalIssue.objects.create(
905+
organization_id=self.organization.id, integration_id=installation.model.id, key=issue_id
906+
)
907+
908+
responses.add(
909+
responses.GET,
910+
"https://example.atlassian.net/rest/api/2/user/assignable/search",
911+
json=[{"accountId": "deadbeef123", "emailAddress": "bob@example.com"}],
912+
)
913+
914+
responses.add(responses.PUT, assign_issue_url, status=400, json={})
915+
916+
with pytest.raises(IntegrationError) as excinfo:
917+
installation.sync_assignee_outbound(external_issue, user)
918+
919+
assert str(excinfo.value) == "There was an error assigning the issue."
920+
assert len(responses.calls) == 2
921+
869922

870923
@control_silo_test
871924
class JiraIntegrationTest(APITestCase):

tests/sentry/integrations/jira_server/test_integration.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from fixtures.integrations.jira.stub_client import StubJiraApiClient
1212
from fixtures.integrations.stub_service import StubService
1313
from sentry.integrations.jira_server.integration import JiraServerIntegration
14+
from sentry.integrations.mixins.issues import IntegrationSyncTargetNotFound
1415
from sentry.integrations.models.external_actor import ExternalActor
1516
from sentry.integrations.models.external_issue import ExternalIssue
1617
from sentry.integrations.models.integration_external_project import IntegrationExternalProject
@@ -19,7 +20,12 @@
1920
from sentry.integrations.types import ExternalProviders
2021
from sentry.models.grouplink import GroupLink
2122
from sentry.models.groupmeta import GroupMeta
22-
from sentry.shared_integrations.exceptions import ApiError, ApiUnauthorized, IntegrationError
23+
from sentry.shared_integrations.exceptions import (
24+
ApiError,
25+
ApiUnauthorized,
26+
IntegrationError,
27+
IntegrationInstallationConfigurationError,
28+
)
2329
from sentry.silo.base import SiloMode
2430
from sentry.silo.safety import unguarded_write
2531
from sentry.testutils.cases import APITestCase
@@ -819,9 +825,9 @@ def test_sync_assignee_outbound_no_emails_for_multiple_users(self):
819825
],
820826
)
821827

822-
with pytest.raises(IntegrationError) as e:
828+
with pytest.raises(IntegrationSyncTargetNotFound) as e:
823829
self.installation.sync_assignee_outbound(external_issue, user)
824-
assert str(e.value) == "Failed to assign user to Jira Server issue"
830+
assert str(e.value) == "No matching Jira Server user found"
825831

826832
# No sync made as jira users don't have email addresses
827833
assert len(responses.calls) == 1
@@ -877,7 +883,7 @@ def test_sync_assignee_outbound_unauthorized(self, mock_assign_issue):
877883
}
878884
],
879885
)
880-
with pytest.raises(IntegrationError) as exc_info:
886+
with pytest.raises(IntegrationInstallationConfigurationError) as exc_info:
881887
self.installation.sync_assignee_outbound(
882888
external_issue=external_issue, user=user, assign=True
883889
)
@@ -902,7 +908,7 @@ def test_sync_assignee_outbound_api_error(self, mock_assign_issue):
902908
{
903909
"accountId": "deadbeef123",
904910
"displayName": "Dead Beef",
905-
"username": "bob@example.com",
911+
"emailAddress": "bob@example.com",
906912
}
907913
],
908914
)

0 commit comments

Comments
 (0)