Skip to content

Commit 0124e08

Browse files
Adds initial issue service method for linked summaries
1 parent 228cc1a commit 0124e08

File tree

4 files changed

+296
-2
lines changed

4 files changed

+296
-2
lines changed

src/sentry/issues/services/issue/impl.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
# defined, because we want to reflect on type annotations and avoid forward references.
55

66

7-
from sentry.issues.services.issue.model import RpcGroupShareMetadata
7+
from sentry.integrations.models.external_issue import ExternalIssue
8+
from sentry.issues.services.issue.model import RpcGroupShareMetadata, RpcLinkedIssueSummary
89
from sentry.issues.services.issue.service import IssueService
910
from sentry.models.group import Group
1011
from sentry.models.organization import Organization
@@ -45,3 +46,25 @@ def upsert_issue_email_reply(
4546
# Call the task synchronously so that the outbox retry works
4647
# correctly should this fail.
4748
process_inbound_email(from_email, group_id, text)
49+
50+
def get_linked_issues(
51+
self,
52+
*,
53+
region_name: str,
54+
integration_id: int,
55+
organization_ids: list[int],
56+
external_issue_key: str,
57+
) -> list[RpcLinkedIssueSummary]:
58+
59+
# Filtering on organization_id and integration_id is analogous to
60+
# querying by integration installations.
61+
external_issues = ExternalIssue.objects.filter(
62+
key=external_issue_key,
63+
organization_id__in=organization_ids,
64+
integration_id=integration_id,
65+
).order_by("date_added")
66+
67+
return [
68+
RpcLinkedIssueSummary.from_external_issue(external_issue)
69+
for external_issue in external_issues
70+
]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,30 @@
1+
from __future__ import annotations
2+
13
from sentry.hybridcloud.rpc import RpcModel
4+
from sentry.integrations.models.external_issue import ExternalIssue
5+
from sentry.models.grouplink import GroupLink
26

37

48
class RpcGroupShareMetadata(RpcModel):
59
title: str
610
message: str
11+
12+
13+
class RpcLinkedIssueSummary(RpcModel):
14+
title: str
15+
issue_link: str
16+
17+
@classmethod
18+
def from_external_issue(cls, external_issue: ExternalIssue) -> RpcLinkedIssueSummary:
19+
group_link = GroupLink.objects.get(
20+
linked_id=external_issue.id,
21+
linked_type=GroupLink.LinkedType.issue,
22+
relationship=GroupLink.Relationship.references,
23+
)
24+
25+
group_url = group_link.group.get_absolute_url()
26+
27+
return RpcLinkedIssueSummary(
28+
title=external_issue.title or "",
29+
issue_link=group_url,
30+
)

src/sentry/issues/services/issue/service.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
from sentry.hybridcloud.rpc.resolvers import ByOrganizationId, ByOrganizationSlug, ByRegionName
1010
from sentry.hybridcloud.rpc.service import RpcService, regional_rpc_method
11-
from sentry.issues.services.issue.model import RpcGroupShareMetadata
11+
from sentry.issues.services.issue.model import RpcGroupShareMetadata, RpcLinkedIssueSummary
1212
from sentry.silo.base import SiloMode
1313

1414

@@ -54,5 +54,29 @@ def upsert_issue_email_reply(
5454
) -> None:
5555
pass
5656

57+
@regional_rpc_method(resolve=ByRegionName(), return_none_if_mapping_not_found=True)
58+
@abstractmethod
59+
def get_linked_issues(
60+
self,
61+
*,
62+
region_name: str,
63+
integration_id: int,
64+
organization_ids: list[int],
65+
external_issue_key: str,
66+
) -> list[RpcLinkedIssueSummary]:
67+
"""
68+
Returns a list of linked issue summaries for a given integration ID +
69+
org ID combination.
70+
71+
This is intended to be used for control to fan out to individual regions
72+
in order to surface linked issue data related to a given set of
73+
integration installations.
74+
75+
`organization_ids` may be a little superfluous here, but allows us to
76+
filter, if necessary, and validate that the issue data we're surfacing
77+
_does_ belong to the integration installation.
78+
"""
79+
pass
80+
5781

5882
issue_service = IssueService.create_delegation()
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
from dataclasses import dataclass
2+
3+
from sentry.integrations.models.external_issue import ExternalIssue
4+
from sentry.integrations.models.integration import Integration
5+
from sentry.integrations.types import IntegrationProviderSlug
6+
from sentry.interfaces.user import User
7+
from sentry.issues.services.issue.model import RpcLinkedIssueSummary
8+
from sentry.issues.services.issue.service import issue_service
9+
from sentry.models.group import Group
10+
from sentry.models.organization import Organization
11+
from sentry.models.project import Project
12+
from sentry.testutils.cases import TestCase
13+
from sentry.testutils.silo import all_silo_test, create_test_regions
14+
15+
16+
@dataclass
17+
class HcExternalIssueContext:
18+
org_owner: User
19+
region_name: str
20+
organization: Organization
21+
project: Project
22+
23+
24+
@all_silo_test(regions=create_test_regions("de", "us"))
25+
class TestIssueService(TestCase):
26+
def setUp(self):
27+
super().setUp()
28+
self.default_jira_integration = self.create_integration(
29+
provider=IntegrationProviderSlug.JIRA.value,
30+
organization=self.organization,
31+
external_id="jira-123",
32+
name="Jira",
33+
)
34+
self.us_region_context = self.create_region_context(
35+
"us",
36+
org_owner=self.create_user(email="us_test@example.com"),
37+
integration=self.default_jira_integration,
38+
)
39+
self.de_region_context = self.create_region_context(
40+
"de",
41+
org_owner=self.create_user(email="de_test@example.com"),
42+
integration=self.default_jira_integration,
43+
)
44+
45+
def create_region_context(
46+
self,
47+
region_name: str,
48+
org_owner: User | None = None,
49+
integration: Integration | None = None,
50+
) -> HcExternalIssueContext:
51+
organization = self.create_organization(
52+
name="test_org",
53+
slug="test",
54+
owner=org_owner,
55+
region=region_name,
56+
)
57+
58+
project = self.create_project(organization=organization)
59+
60+
return HcExternalIssueContext(
61+
region_name=region_name,
62+
organization=organization,
63+
project=project,
64+
org_owner=org_owner,
65+
)
66+
67+
def create_linked_issue(
68+
self,
69+
key: str,
70+
region_context: HcExternalIssueContext,
71+
group: Group,
72+
integration: Integration,
73+
title: str | None = None,
74+
) -> ExternalIssue:
75+
76+
external_issue = self.create_integration_external_issue(
77+
organization=region_context.organization,
78+
group=group,
79+
integration=integration,
80+
key=key,
81+
title=title,
82+
)
83+
84+
return external_issue
85+
86+
def test_get_linked_issues(self):
87+
group = self.create_group(project=self.us_region_context.project)
88+
self.create_linked_issue(
89+
key="TEST-123",
90+
region_context=self.us_region_context,
91+
group=group,
92+
integration=self.default_jira_integration,
93+
title="US Group Link",
94+
)
95+
96+
response = issue_service.get_linked_issues(
97+
region_name=self.us_region_context.region_name,
98+
integration_id=self.default_jira_integration.id,
99+
organization_ids=[self.us_region_context.organization.id],
100+
external_issue_key="TEST-123",
101+
)
102+
103+
assert response == [
104+
RpcLinkedIssueSummary(
105+
issue_link=group.get_absolute_url(),
106+
title=group.title,
107+
)
108+
]
109+
110+
def test_get_linked_issues_with_multiple_organizations_in_multiple_regions(self):
111+
us_group = self.create_group(project=self.us_region_context.project)
112+
us_linked_issue = self.create_linked_issue(
113+
key="TEST-123",
114+
region_context=self.us_region_context,
115+
group=us_group,
116+
integration=self.default_jira_integration,
117+
title="US Group Link",
118+
)
119+
120+
de_group = self.create_group(project=self.de_region_context.project)
121+
de_linked_issue = self.create_linked_issue(
122+
key="TEST-123",
123+
region_context=self.de_region_context,
124+
group=de_group,
125+
integration=self.default_jira_integration,
126+
title="DE Group Link",
127+
)
128+
129+
response = issue_service.get_linked_issues(
130+
region_name=self.us_region_context.region_name,
131+
integration_id=self.default_jira_integration.id,
132+
organization_ids=[
133+
self.us_region_context.organization.id,
134+
self.de_region_context.organization.id,
135+
],
136+
external_issue_key="TEST-123",
137+
)
138+
139+
assert response == [
140+
RpcLinkedIssueSummary(
141+
issue_link=us_group.get_absolute_url(),
142+
title=us_linked_issue.title,
143+
),
144+
RpcLinkedIssueSummary(
145+
issue_link=de_group.get_absolute_url(),
146+
title=de_linked_issue.title,
147+
),
148+
]
149+
150+
def test_get_empty_response_when_no_linked_issues(self):
151+
response = issue_service.get_linked_issues(
152+
region_name=self.us_region_context.region_name,
153+
integration_id=self.default_jira_integration.id,
154+
organization_ids=[self.us_region_context.organization.id],
155+
external_issue_key="TEST-123",
156+
)
157+
158+
assert response == []
159+
160+
def test_get_single_linked_issue_when_multiple_organizations_share_integration(self):
161+
us_group = self.create_group(project=self.us_region_context.project)
162+
us_linked_issue = self.create_linked_issue(
163+
key="TEST-123",
164+
region_context=self.us_region_context,
165+
group=us_group,
166+
integration=self.default_jira_integration,
167+
title="US Group Link",
168+
)
169+
170+
response = issue_service.get_linked_issues(
171+
region_name=self.us_region_context.region_name,
172+
integration_id=self.default_jira_integration.id,
173+
organization_ids=[self.us_region_context.organization.id],
174+
external_issue_key="TEST-123",
175+
)
176+
177+
assert response == [
178+
RpcLinkedIssueSummary(
179+
issue_link=us_group.get_absolute_url(),
180+
title=us_linked_issue.title,
181+
)
182+
]
183+
184+
def test_filters_out_issues_from_other_organizations(self):
185+
us_group = self.create_group(project=self.us_region_context.project)
186+
us_linked_issue = self.create_linked_issue(
187+
key="TEST-123",
188+
region_context=self.us_region_context,
189+
group=us_group,
190+
integration=self.default_jira_integration,
191+
title="US Group Link",
192+
)
193+
194+
other_integration = self.create_integration(
195+
provider=IntegrationProviderSlug.JIRA.value,
196+
organization=self.organization,
197+
external_id="jira-456",
198+
name="Other Jira",
199+
)
200+
201+
unrelated_us_group = self.create_group(project=self.us_region_context.project)
202+
# Create another linked issue with the same key but different integration.
203+
self.create_linked_issue(
204+
key="TEST-123",
205+
region_context=self.us_region_context,
206+
group=unrelated_us_group,
207+
integration=other_integration,
208+
title="Unrelated US Group Link",
209+
)
210+
211+
response = issue_service.get_linked_issues(
212+
region_name=self.us_region_context.region_name,
213+
integration_id=self.default_jira_integration.id,
214+
organization_ids=[self.us_region_context.organization.id],
215+
external_issue_key="TEST-123",
216+
)
217+
218+
assert response == [
219+
RpcLinkedIssueSummary(
220+
issue_link=us_group.get_absolute_url(),
221+
title=us_linked_issue.title,
222+
)
223+
]

0 commit comments

Comments
 (0)