Skip to content

feat(eco): Adds new RPC method for surfacing externally linked issue summaries #96699

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/sentry/issues/services/issue/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
# defined, because we want to reflect on type annotations and avoid forward references.


from sentry.issues.services.issue.model import RpcGroupShareMetadata
from sentry.integrations.models.external_issue import ExternalIssue
from sentry.issues.services.issue.model import RpcGroupShareMetadata, RpcLinkedIssueSummary
from sentry.issues.services.issue.serial import serialize_linked_issue_summary
from sentry.issues.services.issue.service import IssueService
from sentry.models.group import Group
from sentry.models.organization import Organization
Expand Down Expand Up @@ -45,3 +47,24 @@ def upsert_issue_email_reply(
# Call the task synchronously so that the outbox retry works
# correctly should this fail.
process_inbound_email(from_email, group_id, text)

def get_integration_linked_issue_summaries(
self,
*,
region_name: str,
integration_id: int,
organization_ids: list[int],
external_issue_key: str,
) -> list[RpcLinkedIssueSummary]:

# Filtering on organization_id and integration_id is analogous to
# querying by integration installations.
external_issues = ExternalIssue.objects.filter(
key=external_issue_key,
organization_id__in=organization_ids,
integration_id=integration_id,
).order_by("date_added")

return [
serialize_linked_issue_summary(external_issue) for external_issue in external_issues
]
9 changes: 9 additions & 0 deletions src/sentry/issues/services/issue/model.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
from __future__ import annotations

from datetime import datetime

from sentry.hybridcloud.rpc import RpcModel


class RpcGroupShareMetadata(RpcModel):
title: str
message: str


class RpcLinkedIssueSummary(RpcModel):
issue_link: str
date_added: datetime
18 changes: 18 additions & 0 deletions src/sentry/issues/services/issue/serial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from sentry.integrations.models.external_issue import ExternalIssue
from sentry.issues.services.issue.model import RpcLinkedIssueSummary
from sentry.models.grouplink import GroupLink


def serialize_linked_issue_summary(external_issue: ExternalIssue) -> RpcLinkedIssueSummary:
group_link = GroupLink.objects.get(
linked_id=external_issue.id,
linked_type=GroupLink.LinkedType.issue,
relationship=GroupLink.Relationship.references,
)

group_url = group_link.group.get_absolute_url()

return RpcLinkedIssueSummary(
issue_link=group_url,
date_added=external_issue.date_added,
)
26 changes: 25 additions & 1 deletion src/sentry/issues/services/issue/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from sentry.hybridcloud.rpc.resolvers import ByOrganizationId, ByOrganizationSlug, ByRegionName
from sentry.hybridcloud.rpc.service import RpcService, regional_rpc_method
from sentry.issues.services.issue.model import RpcGroupShareMetadata
from sentry.issues.services.issue.model import RpcGroupShareMetadata, RpcLinkedIssueSummary
from sentry.silo.base import SiloMode


Expand Down Expand Up @@ -54,5 +54,29 @@ def upsert_issue_email_reply(
) -> None:
pass

@regional_rpc_method(resolve=ByRegionName(), return_none_if_mapping_not_found=True)
@abstractmethod
def get_integration_linked_issue_summaries(
self,
*,
region_name: str,
integration_id: int,
organization_ids: list[int],
external_issue_key: str,
) -> list[RpcLinkedIssueSummary]:
"""
Returns a list of linked issue summaries for a given integration ID +
org ID combination.

This is intended to be used for control to fan out to individual regions
in order to surface linked issue data related to a given set of
integration installations.

`organization_ids` may be a little superfluous here, but allows us to
filter, if necessary, and validate that the issue data we're surfacing
_does_ belong to the integration installation.
"""
pass


issue_service = IssueService.create_delegation()
214 changes: 214 additions & 0 deletions tests/sentry/issues/services/test_issue_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
from dataclasses import dataclass

from sentry.integrations.models.external_issue import ExternalIssue
from sentry.integrations.models.integration import Integration
from sentry.integrations.types import IntegrationProviderSlug
from sentry.issues.services.issue.model import RpcLinkedIssueSummary
from sentry.issues.services.issue.service import issue_service
from sentry.models.group import Group
from sentry.models.organization import Organization
from sentry.models.project import Project
from sentry.testutils.cases import TestCase
from sentry.testutils.silo import all_silo_test, create_test_regions
from sentry.users.models import User


@dataclass
class HcExternalIssueContext:
org_owner: User | None
region_name: str
organization: Organization
project: Project


@all_silo_test(regions=create_test_regions("de", "us"))
class TestIssueService(TestCase):
def setUp(self):
super().setUp()
self.default_jira_integration = self.create_integration(
provider=IntegrationProviderSlug.JIRA.value,
organization=self.organization,
external_id="jira-123",
name="Jira",
)
self.us_region_context = self.create_region_context(
"us",
org_owner=self.create_user(email="us_test@example.com"),
)
self.de_region_context = self.create_region_context(
"de",
org_owner=self.create_user(email="de_test@example.com"),
)

def create_region_context(
self,
region_name: str,
org_owner: User | None = None,
) -> HcExternalIssueContext:
organization = self.create_organization(
name="test_org",
slug="test",
owner=org_owner,
region=region_name,
)

project = self.create_project(organization=organization)

return HcExternalIssueContext(
region_name=region_name,
organization=organization,
project=project,
org_owner=org_owner,
)

def create_linked_issue(
self,
key: str,
region_context: HcExternalIssueContext,
group: Group,
integration: Integration,
title: str | None = None,
) -> ExternalIssue:

external_issue = self.create_integration_external_issue(
organization=region_context.organization,
group=group,
integration=integration,
key=key,
title=title,
)

return external_issue

def test_get_linked_issues(self):
group = self.create_group(project=self.us_region_context.project)
linked_issue = self.create_linked_issue(
key="TEST-123",
region_context=self.us_region_context,
group=group,
integration=self.default_jira_integration,
)

response = issue_service.get_integration_linked_issue_summaries(
region_name=self.us_region_context.region_name,
integration_id=self.default_jira_integration.id,
organization_ids=[self.us_region_context.organization.id],
external_issue_key="TEST-123",
)

assert response == [
RpcLinkedIssueSummary(
issue_link=group.get_absolute_url(),
date_added=linked_issue.date_added,
)
]

def test_get_linked_issues_with_multiple_organizations_in_multiple_regions(self):
us_group = self.create_group(project=self.us_region_context.project)
us_linked_issue = self.create_linked_issue(
key="TEST-123",
region_context=self.us_region_context,
group=us_group,
integration=self.default_jira_integration,
)

de_group = self.create_group(project=self.de_region_context.project)
de_linked_issue = self.create_linked_issue(
key="TEST-123",
region_context=self.de_region_context,
group=de_group,
integration=self.default_jira_integration,
)

response = issue_service.get_integration_linked_issue_summaries(
region_name=self.us_region_context.region_name,
integration_id=self.default_jira_integration.id,
organization_ids=[
self.us_region_context.organization.id,
self.de_region_context.organization.id,
],
external_issue_key="TEST-123",
)

assert response == [
RpcLinkedIssueSummary(
issue_link=us_group.get_absolute_url(),
date_added=us_linked_issue.date_added,
),
RpcLinkedIssueSummary(
issue_link=de_group.get_absolute_url(),
date_added=de_linked_issue.date_added,
),
]

def test_get_empty_response_when_no_linked_issues(self):
response = issue_service.get_integration_linked_issue_summaries(
region_name=self.us_region_context.region_name,
integration_id=self.default_jira_integration.id,
organization_ids=[self.us_region_context.organization.id],
external_issue_key="TEST-123",
)

assert response == []

def test_get_single_linked_issue_when_multiple_organizations_share_integration(self):
us_group = self.create_group(project=self.us_region_context.project)
linked_issue = self.create_linked_issue(
key="TEST-123",
region_context=self.us_region_context,
group=us_group,
integration=self.default_jira_integration,
)

response = issue_service.get_integration_linked_issue_summaries(
region_name=self.us_region_context.region_name,
integration_id=self.default_jira_integration.id,
organization_ids=[self.us_region_context.organization.id],
external_issue_key="TEST-123",
)

assert response == [
RpcLinkedIssueSummary(
issue_link=us_group.get_absolute_url(),
date_added=linked_issue.date_added,
)
]

def test_filters_out_issues_from_other_organizations(self):
us_group = self.create_group(project=self.us_region_context.project)
us_linked_issue = self.create_linked_issue(
key="TEST-123",
region_context=self.us_region_context,
group=us_group,
integration=self.default_jira_integration,
)

other_integration = self.create_integration(
provider=IntegrationProviderSlug.JIRA.value,
organization=self.organization,
external_id="jira-456",
name="Other Jira",
)

unrelated_us_group = self.create_group(project=self.us_region_context.project)
# Create another linked issue with the same key but different integration.
self.create_linked_issue(
key="TEST-123",
region_context=self.us_region_context,
group=unrelated_us_group,
integration=other_integration,
)

response = issue_service.get_integration_linked_issue_summaries(
region_name=self.us_region_context.region_name,
integration_id=self.default_jira_integration.id,
organization_ids=[self.us_region_context.organization.id],
external_issue_key="TEST-123",
)

assert response == [
RpcLinkedIssueSummary(
issue_link=us_group.get_absolute_url(),
date_added=us_linked_issue.date_added,
)
]
Loading