Repository: getsentry/sentry

Issue:
Update digests to handle generic issue types
Email digests use `group.title`, but generic issues will need to use the `occurrence.issue_title`. 

The HTML template will need to be updated: https://github.com/getsentry/sentry/blob/master/src/sentry/templates/sentry/emails/_group.html#L51-L53

See https://github.com/getsentry/sentry/pull/41463 for performance issue updates.


Discussion:

Resolving patch:
diff --git a/src/sentry/web/frontend/debug/debug_generic_issue.py b/src/sentry/web/frontend/debug/debug_generic_issue.py
--- a/src/sentry/web/frontend/debug/debug_generic_issue.py
+++ b/src/sentry/web/frontend/debug/debug_generic_issue.py
@@ -1,49 +1,20 @@
-import uuid
-from datetime import datetime
-
 import pytz
 from django.utils.safestring import mark_safe
 from django.views.generic import View
 
-from sentry.issues.issue_occurrence import IssueEvidence, IssueOccurrence
 from sentry.models import Organization, Project, Rule
 from sentry.notifications.utils import get_generic_data, get_group_settings_link, get_rules
-from sentry.types.issues import GroupType
 from sentry.utils import json
-from sentry.utils.dates import ensure_aware
 
-from .mail import COMMIT_EXAMPLE, MailPreview, make_error_event
+from .mail import COMMIT_EXAMPLE, MailPreview, make_generic_event
 
 
 class DebugGenericIssueEmailView(View):
     def get(self, request):
-        platform = request.GET.get("platform", "python")
         org = Organization(id=1, slug="example", name="Example")
         project = Project(id=1, slug="example", name="Example", organization=org)
 
-        event = make_error_event(request, project, platform)
-        event = event.for_group(event.groups[0])
-
-        occurrence = IssueOccurrence(
-            uuid.uuid4().hex,
-            uuid.uuid4().hex,
-            ["some-fingerprint"],
-            "something bad happened",
-            "it was bad",
-            "1234",
-            {"Test": 123},
-            [
-                IssueEvidence("Name 1", "Value 1", True),
-                IssueEvidence("Name 2", "Value 2", False),
-                IssueEvidence("Name 3", "Value 3", False),
-            ],
-            GroupType.PROFILE_BLOCKED_THREAD,
-            ensure_aware(datetime.now()),
-        )
-        occurrence.save()
-        event.occurrence = occurrence
-        event.group.type = GroupType.PROFILE_BLOCKED_THREAD
-
+        event = make_generic_event(project)
         group = event.group
 
         rule = Rule(id=1, label="An example rule")
diff --git a/src/sentry/web/frontend/debug/mail.py b/src/sentry/web/frontend/debug/mail.py
--- a/src/sentry/web/frontend/debug/mail.py
+++ b/src/sentry/web/frontend/debug/mail.py
@@ -27,6 +27,7 @@
 from sentry.digests.utils import get_digest_metadata
 from sentry.event_manager import EventManager, get_event_type
 from sentry.http import get_server_hostname
+from sentry.issues.occurrence_consumer import process_event_and_issue_occurrence
 from sentry.mail.notifications import get_builder_args
 from sentry.models import (
     Activity,
@@ -45,6 +46,8 @@
 from sentry.notifications.types import GroupSubscriptionReason
 from sentry.notifications.utils import get_group_settings_link, get_interface_list, get_rules
 from sentry.testutils.helpers import override_options
+from sentry.testutils.helpers.datetime import before_now
+from sentry.testutils.helpers.notifications import TEST_ISSUE_OCCURRENCE
 from sentry.types.issues import GROUP_TYPE_TO_TEXT
 from sentry.utils import json, loremipsum
 from sentry.utils.dates import to_datetime, to_timestamp
@@ -173,7 +176,6 @@ def make_error_event(request, project, platform):
         ("level", "error"),
         ("device", "Other"),
     ]
-
     event_manager = EventManager(data)
     event_manager.normalize()
     data = event_manager.get_data()
@@ -208,6 +210,19 @@ def make_performance_event(project):
     return perf_event
 
 
+def make_generic_event(project):
+    occurrence, group_info = process_event_and_issue_occurrence(
+        TEST_ISSUE_OCCURRENCE.to_dict(),
+        {
+            "event_id": uuid.uuid4().hex,
+            "project_id": project.id,
+            "timestamp": before_now(minutes=1).isoformat(),
+        },
+    )
+    generic_group = group_info.group
+    return generic_group.get_latest_event()
+
+
 def get_shared_context(rule, org, project, group, event):
     return {
         "rule": rule,
@@ -490,7 +505,6 @@ def digest(request):
             event = eventstore.create_event(
                 event_id=uuid.uuid4().hex, group_id=group.id, project_id=project.id, data=data.data
             )
-
             records.append(
                 Record(
                     event.event_id,
@@ -531,6 +545,29 @@ def digest(request):
         state["event_counts"][perf_group.id] = random.randint(10, 1e4)
         state["user_counts"][perf_group.id] = random.randint(10, 1e4)
 
+    # add in generic issues
+    for i in range(random.randint(1, 3)):
+        generic_event = make_generic_event(project)
+        generic_group = generic_event.group
+        generic_group.id = i + 200  # don't clobber other issue ids
+
+        records.append(
+            Record(
+                generic_event.event_id,
+                Notification(
+                    generic_event,
+                    random.sample(
+                        list(state["rules"].keys()), random.randint(1, len(state["rules"]))
+                    ),
+                ),
+                # this is required for acceptance tests to pass as the EventManager won't accept a timestamp in the past
+                to_timestamp(datetime(2016, 6, 22, 16, 16, 0, tzinfo=timezone.utc)),
+            )
+        )
+        state["groups"][generic_group.id] = generic_group
+        state["event_counts"][generic_group.id] = random.randint(10, 1e4)
+        state["user_counts"][generic_group.id] = random.randint(10, 1e4)
+
     digest = build_digest(project, records, state)[0]
     start, end, counts = get_digest_metadata(digest)
 


Test patch:
diff --git a/tests/sentry/notifications/notifications/test_digests.py b/tests/sentry/notifications/notifications/test_digests.py
--- a/tests/sentry/notifications/notifications/test_digests.py
+++ b/tests/sentry/notifications/notifications/test_digests.py
@@ -1,3 +1,4 @@
+import uuid
 from unittest import mock
 from unittest.mock import patch
 from urllib.parse import parse_qs
@@ -9,40 +10,36 @@
 from sentry.digests.backends.base import Backend
 from sentry.digests.backends.redis import RedisBackend
 from sentry.digests.notifications import event_to_record
-from sentry.event_manager import EventManager
+from sentry.issues.occurrence_consumer import process_event_and_issue_occurrence
 from sentry.models import ProjectOwnership, Rule
 from sentry.tasks.digests import deliver_digest
 from sentry.testutils import TestCase
-from sentry.testutils.cases import SlackActivityNotificationTest
-from sentry.testutils.helpers import override_options
+from sentry.testutils.cases import PerformanceIssueTestCase, SlackActivityNotificationTest
 from sentry.testutils.helpers.datetime import before_now, iso_format
 from sentry.testutils.helpers.slack import send_notification
 from sentry.utils import json
-from sentry.utils.samples import load_data
+from tests.sentry.issues.test_utils import OccurrenceTestMixin
 
 USER_COUNT = 2
 
 
-class DigestNotificationTest(TestCase):
+class DigestNotificationTest(TestCase, OccurrenceTestMixin, PerformanceIssueTestCase):
     def add_event(self, fingerprint: str, backend: Backend, event_type: str = "error") -> None:
         if event_type == "performance":
-            with override_options(
+            event = self.create_performance_issue()
+        elif event_type == "generic":
+            occurrence_data = self.build_occurrence_data()
+            event_id = uuid.uuid4().hex
+            occurrence, group_info = process_event_and_issue_occurrence(
+                occurrence_data,
                 {
-                    "performance.issues.n_plus_one_db.problem-creation": 1.0,
-                }
-            ):
-                data = dict(
-                    load_data(
-                        "transaction-n-plus-one",
-                        timestamp=before_now(days=1),
-                    )
-                )
-                event_manager = EventManager(data)
-                event_manager.normalize()
-                data = event_manager.get_data()
-                event = event_manager.save(self.project.id)
-
-            event = event.for_group(event.groups[0])
+                    "event_id": event_id,
+                    "project_id": self.project.id,
+                    "timestamp": before_now(minutes=1).isoformat(),
+                },
+            )
+            group = group_info.group
+            event = group.get_latest_event()
         else:
             event = self.store_event(
                 data={
@@ -52,12 +49,19 @@ def add_event(self, fingerprint: str, backend: Backend, event_type: str = "error
                 },
                 project_id=self.project.id,
             )
+
         backend.add(
             self.key, event_to_record(event, [self.rule]), increment_delay=0, maximum_delay=0
         )
 
     @patch.object(sentry, "digests")
-    def run_test(self, digests, event_count: int, performance_issues: bool = False):
+    def run_test(
+        self,
+        digests,
+        event_count: int,
+        performance_issues: bool = False,
+        generic_issues: bool = False,
+    ):
         backend = RedisBackend()
         digests.digest = backend.digest
 
@@ -65,7 +69,10 @@ def run_test(self, digests, event_count: int, performance_issues: bool = False):
             self.add_event(f"group-{i}", backend, "error")
 
         if performance_issues:
-            self.add_event(f"group-{i}", backend, "performance")
+            self.add_event(f"group-{event_count+1}", backend, "performance")
+
+        if generic_issues:
+            self.add_event(f"group-{event_count+2}", backend, "generic")
 
         with self.tasks():
             deliver_digest(self.key)
@@ -89,10 +96,11 @@ def setUp(self):
     def test_sends_digest_to_every_member(self):
         """Test that each member of the project the events are created in receive a digest email notification"""
         event_count = 4
-        self.run_test(event_count=event_count, performance_issues=True)
-        assert f"{event_count + 1} new alerts since" in mail.outbox[0].subject
+        self.run_test(event_count=event_count, performance_issues=True, generic_issues=True)
+        assert f"{event_count + 2} new alerts since" in mail.outbox[0].subject
         assert "N+1 Query" in mail.outbox[0].body
         assert "oh no" in mail.outbox[0].body
+        assert self.build_occurrence_data()["issue_title"] in mail.outbox[0].body
 
     def test_sends_alert_rule_notification_to_each_member(self):
         """Test that if there is only one event it is sent as a regular alert rule notification"""