Repository: getsentry/sentry

Issue:
Update Jira integration to support generic issue types
Allow for the `IssueEvidence` to populate a Jira ticket created from a Sentry issue.

See https://github.com/getsentry/sentry/pull/40525 and https://github.com/getsentry/sentry/pull/40535 for the performance issue updates, it'll likely be very similar. 


Discussion:

Resolving patch:
diff --git a/src/sentry/integrations/jira/integration.py b/src/sentry/integrations/jira/integration.py
--- a/src/sentry/integrations/jira/integration.py
+++ b/src/sentry/integrations/jira/integration.py
@@ -10,6 +10,7 @@
 from django.utils.translation import ugettext as _
 
 from sentry import features
+from sentry.eventstore.models import GroupEvent
 from sentry.integrations import (
     FeatureDescription,
     IntegrationFeatures,
@@ -337,6 +338,16 @@ def get_performance_issue_body(self, event):
         body += f"| *Repeating Spans ({num_repeating_spans})* | {truncatechars(repeating_spans, MAX_CHAR)} |"
         return body
 
+    def get_generic_issue_body(self, event):
+        body = ""
+        important = event.occurrence.important_evidence_display
+        if important:
+            body = f"| *{important.name}* | {truncatechars(important.value, MAX_CHAR)} |\n"
+        for evidence in event.occurrence.evidence_display:
+            if evidence.important is False:
+                body += f"| *{evidence.name}* | {truncatechars(evidence.value, MAX_CHAR)} |\n"
+        return body[:-2]  # chop off final newline
+
     def get_group_description(self, group, event, **kwargs):
         output = [
             "Sentry Issue: [{}|{}]".format(
@@ -348,7 +359,9 @@ def get_group_description(self, group, event, **kwargs):
         if group.issue_category == GroupCategory.PERFORMANCE:
             body = self.get_performance_issue_body(event)
             output.extend([body])
-
+        elif isinstance(event, GroupEvent) and event.occurrence is not None:
+            body = self.get_generic_issue_body(event)
+            output.extend([body])
         else:
             body = self.get_group_body(group, event)
             if body:
diff --git a/src/sentry/notifications/utils/__init__.py b/src/sentry/notifications/utils/__init__.py
--- a/src/sentry/notifications/utils/__init__.py
+++ b/src/sentry/notifications/utils/__init__.py
@@ -23,6 +23,7 @@
 
 from sentry import integrations
 from sentry.api.serializers.models.event import get_entries, get_problems
+from sentry.eventstore.models import Event, GroupEvent
 from sentry.incidents.models import AlertRuleTriggerAction
 from sentry.integrations import IntegrationFeatures, IntegrationProvider
 from sentry.models import (
@@ -55,7 +56,6 @@
 from sentry.web.helpers import render_to_string
 
 if TYPE_CHECKING:
-    from sentry.eventstore.models import Event, GroupEvent
     from sentry.notifications.notifications.activity.base import ActivityNotification
     from sentry.notifications.notifications.user_report import UserReportNotification
 
@@ -436,13 +436,16 @@ def get_performance_issue_alert_subtitle(event: Event) -> str:
 
 
 def get_notification_group_title(
-    group: Group, event: Event, max_length: int = 255, **kwargs: str
+    group: Group, event: Event | GroupEvent, max_length: int = 255, **kwargs: str
 ) -> str:
     if group.issue_category == GroupCategory.PERFORMANCE:
         issue_type = GROUP_TYPE_TO_TEXT.get(group.issue_type, "Issue")
         transaction = get_performance_issue_alert_subtitle(event)
         title = f"{issue_type}: {transaction}"
         return (title[: max_length - 2] + "..") if len(title) > max_length else title
+    elif isinstance(event, GroupEvent) and event.occurrence is not None:
+        issue_title: str = event.occurrence.issue_title
+        return issue_title
     else:
         event_title: str = event.title
         return event_title


Test patch:
diff --git a/tests/sentry/integrations/jira/test_notify_action.py b/tests/sentry/integrations/jira/test_notify_action.py
--- a/tests/sentry/integrations/jira/test_notify_action.py
+++ b/tests/sentry/integrations/jira/test_notify_action.py
@@ -1,19 +1,15 @@
 import responses
 
 from fixtures.integrations.mock_service import StubService
-from sentry.event_manager import EventManager
 from sentry.integrations.jira import JiraCreateTicketAction
 from sentry.models import ExternalIssue, GroupLink, Integration, Rule
-from sentry.testutils.cases import RuleTestCase
-from sentry.testutils.helpers import override_options
-from sentry.testutils.helpers.datetime import before_now
-from sentry.types.issues import GroupType
+from sentry.testutils.cases import PerformanceIssueTestCase, RuleTestCase
+from sentry.testutils.helpers.notifications import TEST_ISSUE_OCCURRENCE
 from sentry.types.rules import RuleFuture
 from sentry.utils import json
-from sentry.utils.samples import load_data
 
 
-class JiraCreateTicketActionTest(RuleTestCase):
+class JiraCreateTicketActionTest(RuleTestCase, PerformanceIssueTestCase):
     rule_cls = JiraCreateTicketAction
 
     def setUp(self):
@@ -30,23 +26,7 @@ def setUp(self):
         self.integration.add_organization(self.organization, self.user)
         self.installation = self.integration.get_installation(self.organization.id)
 
-    @responses.activate
-    def test_creates_issue(self):
-        event = self.get_event()
-        responses.add(
-            responses.GET,
-            "https://example.atlassian.net/rest/api/2/project",
-            body=StubService.get_stub_json("jira", "project_list_response.json"),
-            content_type="application/json",
-        )
-
-        responses.add(
-            responses.GET,
-            "https://example.atlassian.net/rest/api/2/issue/createmeta",
-            body=StubService.get_stub_json("jira", "createmeta_response.json"),
-            content_type="json",
-        )
-        jira_rule = self.get_rule(
+        self.jira_rule = self.get_rule(
             data={
                 "issuetype": "1",
                 "labels": "bunnies",
@@ -59,69 +39,14 @@ def test_creates_issue(self):
                 "fixVersions": "[10000]",
             }
         )
-        jira_rule.rule = Rule.objects.create(
+        self.jira_rule.rule = Rule.objects.create(
             project=self.project,
             label="test rule",
         )
 
-        jira_rule.data["key"] = "APP-123"
-
-        # Add two mocks: one for POSTing the issue and a GET to confirm it's there.
-        responses.add(
-            method=responses.POST,
-            url="https://example.atlassian.net/rest/api/2/issue",
-            json=jira_rule.data,
-            status=202,
-            content_type="application/json",
-        )
-        responses.add(
-            responses.GET,
-            "https://example.atlassian.net/rest/api/2/issue/APP-123",
-            body=StubService.get_stub_json("jira", "get_issue_response.json"),
-            content_type="application/json",
-        )
-
-        results = list(jira_rule.after(event=event, state=self.get_state()))
-        assert len(results) == 1
-
-        # Trigger rule callback
-        rule_future = RuleFuture(rule=jira_rule, kwargs=results[0].kwargs)
-        results[0].callback(event, futures=[rule_future])
-
-        # Make assertions about what would be POSTed to api/2/issue.
-        data = json.loads(responses.calls[1].request.body)
-        assert data["fields"]["summary"] == event.title
-        assert event.message in data["fields"]["description"]
-        assert data["fields"]["issuetype"]["id"] == "1"
-
-        external_issue = ExternalIssue.objects.get(key="APP-123")
-        assert external_issue
-
-    @responses.activate
-    def test_creates_performance_issue(self):
-        """Test that a performance issue properly creates a Jira ticket"""
-        event_data = load_data(
-            "transaction-n-plus-one",
-            timestamp=before_now(minutes=10),
-            fingerprint=[f"{GroupType.PERFORMANCE_N_PLUS_ONE_DB_QUERIES.value}-group1"],
-        )
-        perf_event_manager = EventManager(event_data)
-        perf_event_manager.normalize()
-        with override_options(
-            {
-                "performance.issues.all.problem-creation": 1.0,
-                "performance.issues.all.problem-detection": 1.0,
-                "performance.issues.n_plus_one_db.problem-creation": 1.0,
-            }
-        ), self.feature(
-            [
-                "organizations:performance-issues-ingest",
-                "projects:performance-suspect-spans-ingestion",
-            ]
-        ):
-            event = perf_event_manager.save(self.project.id)
-        event = event.for_group(event.groups[0])
+        self.jira_rule.data["key"] = "APP-123"
 
+    def create_issue_base(self, event):
         responses.add(
             responses.GET,
             "https://example.atlassian.net/rest/api/2/project",
@@ -135,31 +60,12 @@ def test_creates_performance_issue(self):
             body=StubService.get_stub_json("jira", "createmeta_response.json"),
             content_type="json",
         )
-        jira_rule = self.get_rule(
-            data={
-                "issuetype": "1",
-                "labels": "bunnies",
-                "customfield_10200": "sad",
-                "customfield_10300": ["Feature 1", "Feature 2"],
-                "project": "10000",
-                "integration": self.integration.id,
-                "jira_project": "10000",
-                "issue_type": "Bug",
-                "fixVersions": "[10000]",
-            }
-        )
-        jira_rule.rule = Rule.objects.create(
-            project=self.project,
-            label="test rule",
-        )
-
-        jira_rule.data["key"] = "APP-123"
 
         # Add two mocks: one for POSTing the issue and a GET to confirm it's there.
         responses.add(
             method=responses.POST,
             url="https://example.atlassian.net/rest/api/2/issue",
-            json=jira_rule.data,
+            json=self.jira_rule.data,
             status=202,
             content_type="application/json",
         )
@@ -170,15 +76,34 @@ def test_creates_performance_issue(self):
             content_type="application/json",
         )
 
-        results = list(jira_rule.after(event=event, state=self.get_state()))
+        results = list(self.jira_rule.after(event=event, state=self.get_state()))
         assert len(results) == 1
 
         # Trigger rule callback
-        rule_future = RuleFuture(rule=jira_rule, kwargs=results[0].kwargs)
+        rule_future = RuleFuture(rule=self.jira_rule, kwargs=results[0].kwargs)
         results[0].callback(event, futures=[rule_future])
+        return json.loads(responses.calls[1].request.body)
+
+    @responses.activate
+    def test_creates_issue(self):
+        event = self.get_event()
+        data = self.create_issue_base(event)
+
+        # Make assertions about what would be POSTed to api/2/issue.
+        assert data["fields"]["summary"] == event.title
+        assert event.message in data["fields"]["description"]
+        assert data["fields"]["issuetype"]["id"] == "1"
+
+        external_issue = ExternalIssue.objects.get(key="APP-123")
+        assert external_issue
+
+    @responses.activate
+    def test_creates_performance_issue(self):
+        """Test that a performance issue properly creates a Jira ticket"""
+        event = self.create_performance_issue()
+        data = self.create_issue_base(event)
 
         # Make assertions about what would be POSTed to api/2/issue.
-        data = json.loads(responses.calls[1].request.body)
         assert (
             data["fields"]["summary"]
             == 'N+1 Query: SELECT "books_author"."id", "books_author"."name" FROM "books_author" WHERE "books_author"."id" = %s LIMIT 21'
@@ -192,6 +117,24 @@ def test_creates_performance_issue(self):
         external_issue = ExternalIssue.objects.get(key="APP-123")
         assert external_issue
 
+    @responses.activate
+    def test_creates_generic_issue(self):
+        """Test that a generic issue properly creates a Jira ticket"""
+
+        occurrence = TEST_ISSUE_OCCURRENCE
+        event = self.get_event()
+        event = event.for_group(event.groups[0])
+        event.occurrence = occurrence
+        data = self.create_issue_base(event)
+
+        # Make assertions about what would be POSTed to api/2/issue.
+        assert data["fields"]["summary"] == event.occurrence.issue_title
+        assert event.occurrence.evidence_display[0].value in data["fields"]["description"]
+        assert data["fields"]["issuetype"]["id"] == "1"
+
+        external_issue = ExternalIssue.objects.get(key="APP-123")
+        assert external_issue
+
     @responses.activate
     def test_doesnt_create_issue(self):
         """Don't create an issue if one already exists on the event for the given integration"""
@@ -212,27 +155,8 @@ def test_doesnt_create_issue(self):
             relationship=GroupLink.Relationship.references,
             data={"provider": self.integration.provider},
         )
-        jira_rule = self.get_rule(
-            data={
-                "title": "example summary",
-                "description": "example bug report",
-                "issuetype": "1",
-                "project": "10000",
-                "customfield_10200": "sad",
-                "customfield_10300": ["Feature 1", "Feature 2"],
-                "labels": "bunnies",
-                "integration": self.integration.id,
-                "jira_project": "10000",
-                "issue_type": "Bug",
-                "fixVersions": "[10000]",
-            }
-        )
-        jira_rule.rule = Rule.objects.create(
-            project=self.project,
-            label="test rule",
-        )
 
-        results = list(jira_rule.after(event=event, state=self.get_state()))
+        results = list(self.jira_rule.after(event=event, state=self.get_state()))
         assert len(results) == 1
         results[0].callback(event, futures=[])