Repository: getsentry/sentry

Issue:
Investigate work needed to add issue platform to rule previews
Andrew's project with alert rule previews https://github.com/getsentry/sentry/blob/master/src/sentry/rules/history/preview.py has a mapping of group categories to datasets https://github.com/getsentry/sentry/blob/master/src/sentry/rules/history/preview_strategy.py#L18-L21 

We should spec out the work needed to add issue platform issues to this feature.


Discussion:

Resolving patch:
diff --git a/src/sentry/rules/history/preview.py b/src/sentry/rules/history/preview.py
--- a/src/sentry/rules/history/preview.py
+++ b/src/sentry/rules/history/preview.py
@@ -11,9 +11,9 @@
 from sentry.rules import RuleBase, rules
 from sentry.rules.history.preview_strategy import (
     DATASET_TO_COLUMN_NAME,
-    GROUP_CATEGORY_TO_DATASET,
-    UPDATE_KWARGS_FOR_GROUP,
-    UPDATE_KWARGS_FOR_GROUPS,
+    get_dataset_from_category,
+    get_update_kwargs_for_group,
+    get_update_kwargs_for_groups,
 )
 from sentry.rules.processor import get_match_function
 from sentry.snuba.dataset import Dataset
@@ -104,7 +104,6 @@ def preview(
         group_fires = get_fired_groups(
             group_activity, filter_objects, filter_func, start, frequency, event_map
         )
-
         return group_fires
     except PreviewException:
         return None
@@ -236,10 +235,8 @@ def get_top_groups(
     # queries each dataset for top x groups and then gets top x overall
     query_params = []
     for dataset in datasets:
-        if dataset not in UPDATE_KWARGS_FOR_GROUPS:
-            continue
-
-        kwargs = UPDATE_KWARGS_FOR_GROUPS[dataset](
+        kwargs = get_update_kwargs_for_groups(
+            dataset,
             group_ids,
             {
                 "dataset": dataset,
@@ -279,8 +276,8 @@ def get_group_dataset(group_ids: Sequence[int]) -> Dict[int, Dataset]:
     """
     group_categories = Group.objects.filter(id__in=group_ids).values_list("id", "type")
     return {
-        group[0]: GROUP_CATEGORY_TO_DATASET.get(get_group_type_by_type_id(group[1]).category)
-        for group in group_categories
+        group_id: get_dataset_from_category(get_group_type_by_type_id(group_type).category)
+        for group_id, group_type in group_categories
     }
 
 
@@ -318,14 +315,11 @@ def get_events(
     query_params = []
     # query events by group_id (first event for each group)
     for dataset, ids in group_ids.items():
-        if (
-            dataset not in columns
-            or dataset not in UPDATE_KWARGS_FOR_GROUP
-            or dataset == Dataset.Transactions
-        ):
+        if dataset not in columns or dataset == Dataset.Transactions:
             # transaction query cannot be made until https://getsentry.atlassian.net/browse/SNS-1891 is fixed
             continue
-        kwargs = UPDATE_KWARGS_FOR_GROUPS[dataset](
+        kwargs = get_update_kwargs_for_groups(
+            dataset,
             ids,
             {
                 "dataset": dataset,
@@ -503,10 +497,8 @@ def get_frequency_buckets(
     """
     Puts the events of a group into buckets, and returns the bucket counts.
     """
-    if dataset not in UPDATE_KWARGS_FOR_GROUP:
-        return {}
-
-    kwargs = UPDATE_KWARGS_FOR_GROUP[dataset](
+    kwargs = get_update_kwargs_for_group(
+        dataset,
         group_id,
         {
             "dataset": dataset,
diff --git a/src/sentry/rules/history/preview_strategy.py b/src/sentry/rules/history/preview_strategy.py
--- a/src/sentry/rules/history/preview_strategy.py
+++ b/src/sentry/rules/history/preview_strategy.py
@@ -8,22 +8,26 @@
 Issue category specific components to computing a preview for a set of rules.
 
 To add support for a new issue category/dataset:
-    1. Add mapping from GroupCategory to Dataset
+    1. Update get_dataset_from_category
     2. Add mapping from Dataset to snuba column name
         a. The column name should be a field in sentry.snuba.events.Column
-    3. Add category-specific query params for UPDATE_KWARGS_FOR_GROUPS and UPDATE_KWARGS_FOR_GROUP
+    3. Add category-specific query params for get_update_kwargs_for_groups and get_update_kwargs_for_group
 """
 
-# Maps group category to dataset
-GROUP_CATEGORY_TO_DATASET: Dict[int, Dataset] = {
-    GroupCategory.ERROR.value: Dataset.Events,
-    GroupCategory.PERFORMANCE.value: Dataset.Transactions,
-}
+
+def get_dataset_from_category(category: int) -> Dataset:
+    if category == GroupCategory.ERROR.value:
+        return Dataset.Events
+    elif category == GroupCategory.PERFORMANCE.value:
+        return Dataset.Transactions
+    return Dataset.IssuePlatform
+
 
 # Maps datasets to snuba column name
 DATASET_TO_COLUMN_NAME: Dict[Dataset, str] = {
     Dataset.Events: "event_name",
     Dataset.Transactions: "transaction_name",
+    Dataset.IssuePlatform: "issue_platform_name",
 }
 
 
@@ -65,10 +69,17 @@ def _transactions_from_groups_kwargs(
 If there's a many-to-many relationship, the group id column should be arrayjoined.
 If there are no issue state changes (causes no group ids), then do not filter by group ids.
 """
-UPDATE_KWARGS_FOR_GROUPS = {
-    Dataset.Events: _events_from_groups_kwargs,
-    Dataset.Transactions: _transactions_from_groups_kwargs,
-}
+
+
+def get_update_kwargs_for_groups(
+    dataset: Dataset,
+    group_ids: Sequence[int],
+    kwargs: Dict[str, Any],
+    has_issue_state_condition: bool = True,
+) -> Dict[str, Any]:
+    if dataset == Dataset.Transactions:
+        return _transactions_from_groups_kwargs(group_ids, kwargs, has_issue_state_condition)
+    return _events_from_groups_kwargs(group_ids, kwargs, has_issue_state_condition)
 
 
 def _events_from_group_kwargs(group_id: int, kwargs: Dict[str, Any]) -> Dict[str, Any]:
@@ -84,7 +95,11 @@ def _transactions_from_group_kwargs(group_id: int, kwargs: Dict[str, Any]) -> Di
 """
 Returns the rows that reference the group id without arrayjoining.
 """
-UPDATE_KWARGS_FOR_GROUP = {
-    Dataset.Events: _events_from_group_kwargs,
-    Dataset.Transactions: _transactions_from_group_kwargs,
-}
+
+
+def get_update_kwargs_for_group(
+    dataset: Dataset, group_id: int, kwargs: Dict[str, Any]
+) -> Dict[str, Any]:
+    if dataset == Dataset.Transactions:
+        return _transactions_from_group_kwargs(group_id, kwargs)
+    return _events_from_group_kwargs(group_id, kwargs)


Test patch:
diff --git a/tests/sentry/rules/history/test_preview.py b/tests/sentry/rules/history/test_preview.py
--- a/tests/sentry/rules/history/test_preview.py
+++ b/tests/sentry/rules/history/test_preview.py
@@ -3,7 +3,12 @@
 from django.utils import timezone
 from freezegun import freeze_time
 
-from sentry.issues.grouptype import ErrorGroupType, PerformanceNPlusOneGroupType
+from sentry.issues.grouptype import (
+    ErrorGroupType,
+    GroupCategory,
+    PerformanceNPlusOneGroupType,
+    ProfileBlockedThreadGroupType,
+)
 from sentry.models import Activity, Group, Project
 from sentry.rules.history.preview import (
     FREQUENCY_CONDITION_GROUP_LIMIT,
@@ -19,6 +24,7 @@
 from sentry.types.activity import ActivityType
 from sentry.types.condition_activity import ConditionActivity, ConditionActivityType
 from sentry.utils.samples import load_data
+from tests.sentry.issues.test_utils import OccurrenceTestMixin
 
 MATCH_ARGS = ("all", "all", 0)
 
@@ -186,13 +192,46 @@ def test_issue_category(self):
         filters = [
             {
                 "id": "sentry.rules.filters.issue_category.IssueCategoryFilter",
-                "value": ErrorGroupType.type_id,
+                "value": GroupCategory.ERROR.value,
             }
         ]
         result = preview(self.project, conditions, filters, *MATCH_ARGS)
         assert all(group.id in result for group in errors)
         assert all(group.id not in result for group in n_plus_one)
 
+    def test_issue_platform(self):
+        """Ensure that issues using the issue platform can be shown as a preview."""
+        hours = get_hours(PREVIEW_TIME_RANGE)
+        prev_hour = timezone.now() - timedelta(hours=1)
+        errors = []
+        profile_blocked_thread = []
+        for i in range(hours):
+            if i % 2:
+                errors.append(
+                    Group.objects.create(
+                        project=self.project, first_seen=prev_hour, type=ErrorGroupType.type_id
+                    )
+                )
+            else:
+                profile_blocked_thread.append(
+                    Group.objects.create(
+                        project=self.project,
+                        first_seen=prev_hour,
+                        type=ProfileBlockedThreadGroupType.type_id,
+                    )
+                )
+
+        conditions = [{"id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"}]
+        filters = [
+            {
+                "id": "sentry.rules.filters.issue_category.IssueCategoryFilter",
+                "value": GroupCategory.PROFILE.value,
+            }
+        ]
+        result = preview(self.project, conditions, filters, *MATCH_ARGS)
+        assert all(group.id not in result for group in errors)
+        assert all(group.id in result for group in profile_blocked_thread)
+
     def test_level(self):
         event = self._set_up_event({"tags": {"level": "error"}})
 
@@ -481,7 +520,7 @@ def test_triggered_times(self):
 
 @freeze_time()
 @region_silo_test
-class FrequencyConditionTest(TestCase, SnubaTestCase):
+class FrequencyConditionTest(TestCase, SnubaTestCase, OccurrenceTestMixin):
     def setUp(self):
         super().setUp()
         self.transaction_data = load_data(
@@ -675,6 +714,42 @@ def test_frequency_conditions(self):
         result = preview(self.project, conditions, [], "any", "all", 0)
         assert group.id in result
 
+    def test_frequency_conditions_issue_platform(self):
+        prev_hour = timezone.now() - timedelta(hours=1)
+        prev_two_hour = timezone.now() - timedelta(hours=2)
+        for time in (prev_hour, prev_two_hour):
+            for i in range(5):
+                event = self.store_event(
+                    project_id=self.project.id, data={"timestamp": iso_format(time)}
+                )
+                event = event.for_group(event.groups[0])
+                occurrence = self.build_occurrence(level="info")
+                occurrence.save()
+                event.occurrence = occurrence
+                event.group.type = ProfileBlockedThreadGroupType.type_id
+
+        conditions = [
+            {
+                "id": "sentry.rules.conditions.event_frequency.EventFrequencyCondition",
+                "value": 4,
+                "interval": "5m",
+            },
+            {
+                "id": "sentry.rules.conditions.event_frequency.EventFrequencyCondition",
+                "value": 9,
+                "interval": "1d",
+            },
+        ]
+        result = preview(self.project, conditions, [], *MATCH_ARGS)
+        assert event.group.id in result
+
+        conditions[0]["value"] = 5
+        result = preview(self.project, conditions, [], *MATCH_ARGS)
+        assert event.group.id not in result
+
+        result = preview(self.project, conditions, [], "any", "all", 0)
+        assert event.group.id in result
+
     def test_interval_comparison(self):
         prev_hour = timezone.now() - timedelta(hours=1)
         group = None