Skip to content

Commit 70c8e92

Browse files
chore(integrations): Changes exception type for missing org integrations, updates assignment sync to halt on this exception (#91542)
1 parent 8d4f71c commit 70c8e92

File tree

5 files changed

+58
-22
lines changed

5 files changed

+58
-22
lines changed

src/sentry/integrations/base.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from sentry.exceptions import InvalidIdentity
1717
from sentry.identity.services.identity import identity_service
1818
from sentry.identity.services.identity.model import RpcIdentity
19+
from sentry.integrations.errors import OrganizationIntegrationNotFound
1920
from sentry.integrations.models.external_actor import ExternalActor
2021
from sentry.integrations.models.integration import Integration
2122
from sentry.integrations.notify_disable import notify_disable
@@ -372,7 +373,7 @@ def org_integration(self) -> RpcOrganizationIntegration:
372373
organization_id=self.organization_id,
373374
)
374375
if integration is None:
375-
raise NotFound("missing org_integration")
376+
raise OrganizationIntegrationNotFound("missing org_integration")
376377
return integration
377378

378379
@cached_property
@@ -442,7 +443,7 @@ def default_identity(self) -> RpcIdentity:
442443
"""For Integrations that rely solely on user auth for authentication."""
443444
try:
444445
org_integration = self.org_integration
445-
except NotFound:
446+
except OrganizationIntegrationNotFound:
446447
raise Identity.DoesNotExist
447448
else:
448449
if org_integration.default_auth_id is None:

src/sentry/integrations/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ class InvalidProviderException(Exception):
44
"""
55

66
pass
7+
8+
9+
class OrganizationIntegrationNotFound(Exception):
10+
pass

src/sentry/integrations/tasks/sync_assignee_outbound.py

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

33
from sentry import analytics, features
44
from sentry.constants import ObjectStatus
5+
from sentry.integrations.errors import OrganizationIntegrationNotFound
56
from sentry.integrations.models.external_issue import ExternalIssue
67
from sentry.integrations.models.integration import Integration
78
from sentry.integrations.project_management.metrics import (
@@ -69,23 +70,31 @@ def sync_assignee_outbound(
6970
action_type=ProjectManagementActionType.OUTBOUND_ASSIGNMENT_SYNC, integration=integration
7071
).capture() as lifecycle:
7172
lifecycle.add_extra("sync_task", "sync_assignee_outbound")
73+
lifecycle.add_extra("organization_id", external_issue.organization_id)
74+
lifecycle.add_extra("integration_id", integration.id)
75+
7276
if not (
7377
hasattr(installation, "should_sync") and hasattr(installation, "sync_assignee_outbound")
7478
):
7579
return
7680

77-
parsed_assignment_source = (
78-
AssignmentSource.from_dict(assignment_source_dict) if assignment_source_dict else None
79-
)
80-
if installation.should_sync("outbound_assignee", parsed_assignment_source):
81-
# Assume unassign if None.
82-
user = user_service.get_user(user_id) if user_id else None
83-
installation.sync_assignee_outbound(
84-
external_issue, user, assign=assign, assignment_source=parsed_assignment_source
85-
)
86-
analytics.record(
87-
"integration.issue.assignee.synced",
88-
provider=integration.provider,
89-
id=integration.id,
90-
organization_id=external_issue.organization_id,
81+
try:
82+
parsed_assignment_source = (
83+
AssignmentSource.from_dict(assignment_source_dict)
84+
if assignment_source_dict
85+
else None
9186
)
87+
if installation.should_sync("outbound_assignee", parsed_assignment_source):
88+
# Assume unassign if None.
89+
user = user_service.get_user(user_id) if user_id else None
90+
installation.sync_assignee_outbound(
91+
external_issue, user, assign=assign, assignment_source=parsed_assignment_source
92+
)
93+
analytics.record(
94+
"integration.issue.assignee.synced",
95+
provider=integration.provider,
96+
id=integration.id,
97+
organization_id=external_issue.organization_id,
98+
)
99+
except OrganizationIntegrationNotFound:
100+
lifecycle.record_halt("organization_integration_not_found")

tests/sentry/integrations/tasks/test_sync_assignee_outbound.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
from sentry.integrations.example import ExampleIntegration
66
from sentry.integrations.models import ExternalIssue, Integration
7+
from sentry.integrations.models.organization_integration import OrganizationIntegration
78
from sentry.integrations.tasks import sync_assignee_outbound
89
from sentry.integrations.types import EventLifecycleOutcome
10+
from sentry.testutils.asserts import assert_halt_metric, assert_success_metric
911
from sentry.testutils.cases import TestCase
1012
from sentry.testutils.silo import assume_test_silo_mode_of
1113
from sentry.users.services.user import RpcUser
@@ -93,9 +95,7 @@ def test_skips_syncing_if_should_sync_false(
9395
mock_sync_assignee.assert_not_called()
9496

9597
assert mock_record_event.call_count == 2
96-
start, success = mock_record_event.mock_calls
97-
assert start.args == (EventLifecycleOutcome.STARTED,)
98-
assert success.args == (EventLifecycleOutcome.SUCCESS, None)
98+
assert_success_metric(mock_record_event)
9999

100100
@mock.patch("sentry.integrations.utils.metrics.EventLifecycle.record_event")
101101
@mock.patch.object(ExampleIntegration, "sync_assignee_outbound")
@@ -115,9 +115,7 @@ def test_missing_issue_sync(self, mock_sync_assignee, mock_record_event):
115115
# We don't want to log halt/failure metrics for these as it will taint
116116
# all non-sync integrations' metrics.
117117
assert mock_record_event.call_count == 2
118-
start, success = mock_record_event.mock_calls
119-
assert start.args == (EventLifecycleOutcome.STARTED,)
120-
assert success.args == (EventLifecycleOutcome.SUCCESS, None)
118+
assert_success_metric(mock_record_event)
121119

122120
@mock.patch.object(ExampleIntegration, "sync_assignee_outbound")
123121
def test_missing_integration_installation(self, mock_sync_assignee):
@@ -134,3 +132,23 @@ def test_missing_integration_installation(self, mock_sync_assignee):
134132
assert ExternalIssue.objects.filter(id=external_issue.id).exists()
135133
sync_assignee_outbound(external_issue.id, self.user.id, True, None)
136134
mock_sync_assignee.assert_not_called()
135+
136+
@mock.patch("sentry.integrations.utils.metrics.EventLifecycle.record_event")
137+
@mock.patch.object(ExampleIntegration, "sync_assignee_outbound")
138+
def test_missing_organization_integration(self, mock_sync_assignee, mock_record_event):
139+
external_issue = self.create_integration_external_issue(
140+
group=self.group,
141+
key="foo-1234",
142+
integration=self.example_integration,
143+
)
144+
145+
# Delete all organization integrations, but ensure we still have an external issue
146+
with assume_test_silo_mode_of(OrganizationIntegration):
147+
OrganizationIntegration.objects.filter().delete()
148+
149+
assert ExternalIssue.objects.filter(id=external_issue.id).exists()
150+
sync_assignee_outbound(external_issue.id, self.user.id, True, None)
151+
mock_sync_assignee.assert_not_called()
152+
153+
assert mock_record_event.call_count == 2
154+
assert_halt_metric(mock_record_event, "organization_integration_not_found")

tests/sentry/integrations/test_base.py

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

33
from sentry.identity.services.identity.serial import serialize_identity
44
from sentry.integrations.base import IntegrationInstallation
5+
from sentry.integrations.errors import OrganizationIntegrationNotFound
56
from sentry.silo.base import SiloMode
67
from sentry.testutils.cases import TestCase
78
from sentry.testutils.silo import all_silo_test, assume_test_silo_mode
@@ -43,6 +44,9 @@ def test_with_context(self):
4344
assert integration.default_identity == serialize_identity(self.identity)
4445

4546
def test_missing_org_integration(self):
47+
with pytest.raises(OrganizationIntegrationNotFound):
48+
ExampleIntegration(self.model, -1).org_integration
49+
4650
with pytest.raises(Identity.DoesNotExist):
4751
ExampleIntegration(self.model, -1).default_identity
4852

0 commit comments

Comments
 (0)