Browse Source

feat(issue-platform): Integrate policy layer into issue search (#46107)

This adds in the policy layer to the issue search, so that issue types
will only appear in search when they're been released.

We'll need to change how issue search works once performance issues are
moved over. Issue types that use the same dataset should be grouped into
the same query, but at the moment we'll make a separate query for each
category.
Dan Fuller 1 year ago
parent
commit
34a6dde71e

+ 20 - 0
src/sentry/issues/grouptype.py

@@ -48,6 +48,26 @@ class GroupTypeRegistry:
     def all(self) -> List[Type[GroupType]]:
         return list(self._registry.values())
 
+    def get_visible(
+        self, organization: Organization, actor: Optional[Any] = None
+    ) -> List[Type[GroupType]]:
+        released = [gt for gt in self.all() if gt.released]
+        feature_to_grouptype = {
+            gt.build_visible_feature_name(): gt for gt in self.all() if not gt.released
+        }
+        batch_features = features.batch_has(
+            list(feature_to_grouptype.keys()), actor=actor, organization=organization
+        )
+        enabled = []
+        if batch_features:
+            feature_results = batch_features.get(f"organization:{organization.id}", {})
+            enabled = [
+                feature_to_grouptype[feature]
+                for feature, active in feature_results.items()
+                if active
+            ]
+        return released + enabled
+
     def get_all_group_type_ids(self) -> Set[int]:
         return {type.type_id for type in self._registry.values()}
 

+ 31 - 20
src/sentry/issues/search.py

@@ -4,14 +4,10 @@ import functools
 from copy import deepcopy
 from typing import Any, Callable, Mapping, Optional, Protocol, Sequence, Set, TypedDict
 
-from sentry import features
+from sentry import features, options
 from sentry.api.event_search import SearchFilter, SearchKey, SearchValue
-from sentry.issues.grouptype import (
-    GroupCategory,
-    get_all_group_type_ids,
-    get_group_type_by_type_id,
-    get_group_types_by_category,
-)
+from sentry.issues import grouptype
+from sentry.issues.grouptype import GroupCategory, get_all_group_type_ids, get_group_type_by_type_id
 from sentry.models import Environment, Organization
 from sentry.search.events.filter import convert_search_filter_to_snuba_query
 from sentry.utils import snuba
@@ -211,19 +207,22 @@ def _query_params_for_generic(
     filters: Mapping[str, Sequence[int]],
     conditions: Sequence[Any],
     actor: Optional[Any] = None,
+    categories: Optional[Sequence[GroupCategory]] = None,
 ) -> Optional[SnubaQueryParams]:
     organization = Organization.objects.filter(id=organization_id).first()
     if organization and features.has(
         "organizations:issue-platform", organization=organization, actor=actor
     ):
+        category_ids = {gc.value for gc in categories} if categories else None
+        group_types = {
+            gt.type_id
+            for gt in grouptype.registry.get_visible(organization, actor)
+            if not category_ids or gt.category in category_ids
+        }
+
+        filters = {"occurrence_type_id": list(group_types), **filters}
         if group_ids:
-            filters = {
-                "group_id": sorted(group_ids),
-                "occurrence_type_id": list(
-                    get_group_types_by_category(GroupCategory.PROFILE.value)
-                ),
-                **filters,
-            }
+            filters["group_id"] = sorted(group_ids)
 
         params = query_partial(
             dataset=snuba.Dataset.IssuePlatform,
@@ -240,12 +239,24 @@ def _query_params_for_generic(
     return None
 
 
-# TODO: We need to add a way to make this dynamic for additional generic types
-SEARCH_STRATEGIES: Mapping[int, GroupSearchStrategy] = {
-    GroupCategory.ERROR.value: _query_params_for_error,
-    GroupCategory.PERFORMANCE.value: _query_params_for_perf,
-    GroupCategory.PROFILE.value: _query_params_for_generic,
-}
+def get_search_strategies() -> Mapping[int, GroupSearchStrategy]:
+    strategies = {}
+    for group_category in GroupCategory:
+        if group_category == GroupCategory.ERROR:
+            strategy = _query_params_for_error
+        elif group_category == GroupCategory.PERFORMANCE:
+            if not options.get("performance.issues.create_issues_through_platform", False):
+                strategy = _query_params_for_perf
+            else:
+                strategy = functools.partial(
+                    _query_params_for_generic, categories=[GroupCategory.PERFORMANCE]
+                )
+        else:
+            strategy = functools.partial(
+                _query_params_for_generic, categories=[GroupCategory.PROFILE]
+            )
+        strategies[group_category.value] = strategy
+    return strategies
 
 
 def _update_profiling_search_filters(

+ 3 - 4
src/sentry/search/snuba/executors.py

@@ -37,12 +37,11 @@ from sentry.db.models.manager.base_query_set import BaseQuerySet
 from sentry.issues.grouptype import ErrorGroupType, GroupCategory, get_group_types_by_category
 from sentry.issues.search import (
     SEARCH_FILTER_UPDATERS,
-    SEARCH_STRATEGIES,
     IntermediateSearchQueryPartial,
     MergeableRow,
     SearchQueryPartial,
     UnsupportedSearchQuery,
-    _query_params_for_generic,
+    get_search_strategies,
     group_categories_from,
 )
 from sentry.models import Environment, Group, Organization, Project
@@ -277,7 +276,7 @@ class AbstractQueryExecutor(metaclass=ABCMeta):
             ),
         )
 
-        strategy = SEARCH_STRATEGIES.get(group_category, _query_params_for_generic)
+        strategy = get_search_strategies()[group_category]
         snuba_query_params = strategy(
             pinned_query_partial,
             selected_columns,
@@ -359,7 +358,7 @@ class AbstractQueryExecutor(metaclass=ABCMeta):
         if not group_categories:
             group_categories = {
                 gc
-                for gc in SEARCH_STRATEGIES.keys()
+                for gc in get_search_strategies().keys()
                 if gc != GroupCategory.PROFILE.value
                 or features.has("organizations:issue-platform", organization, actor=actor)
             }

+ 19 - 0
tests/sentry/issues/test_grouptype.py

@@ -5,11 +5,14 @@ from unittest.mock import patch
 from sentry.issues.grouptype import (
     DEFAULT_EXPIRY_TIME,
     DEFAULT_IGNORE_LIMIT,
+    ErrorGroupType,
     GroupCategory,
     GroupType,
     GroupTypeRegistry,
     NoiseConfig,
     PerformanceGroupTypeDefaults,
+    PerformanceNPlusOneGroupType,
+    ProfileJSONDecodeType,
     get_group_type_by_slug,
     get_group_types_by_category,
 )
@@ -158,3 +161,19 @@ class GroupTypeReleasedTest(BaseGroupTypeTest):
             assert TestGroupType.allow_post_process_group(self.organization)
         with self.feature(TestGroupType.build_ingest_feature_name()):
             assert TestGroupType.allow_ingest(self.organization)
+
+
+class GroupRegistryTest(BaseGroupTypeTest):
+    def test_get_visible(self) -> None:
+        registry = GroupTypeRegistry()
+        registry.add(PerformanceNPlusOneGroupType)
+        registry.add(ProfileJSONDecodeType)
+        assert registry.get_visible(self.organization) == []
+        with self.feature(PerformanceNPlusOneGroupType.build_visible_feature_name()):
+            assert registry.get_visible(self.organization) == [PerformanceNPlusOneGroupType]
+        registry.add(ErrorGroupType)
+        with self.feature(PerformanceNPlusOneGroupType.build_visible_feature_name()):
+            assert set(registry.get_visible(self.organization)) == {
+                PerformanceNPlusOneGroupType,
+                ErrorGroupType,
+            }

+ 40 - 1
tests/snuba/search/test_backend.py

@@ -11,6 +11,7 @@ from sentry.api.issue_search import convert_query_values, issue_search_config, p
 from sentry.exceptions import InvalidSearchQuery
 from sentry.issues.grouptype import (
     ErrorGroupType,
+    NoiseConfig,
     PerformanceNPlusOneGroupType,
     PerformanceRenderBlockingAssetSpanGroupType,
 )
@@ -2362,6 +2363,44 @@ class EventsGenericSnubaSearchTest(SharedSnubaTest, OccurrenceTestMixin):
             )
         assert list(results) == [self.profile_group_1, self.profile_group_2]
 
+    def test_generic_query_perf(self):
+        event_id = uuid.uuid4().hex
+        group_type = PerformanceNPlusOneGroupType
+        self.project.update_option("sentry:performance_issue_create_issue_through_platform", True)
+
+        with self.options(
+            {"performance.issues.create_issues_through_platform": True}
+        ), mock.patch.object(
+            PerformanceNPlusOneGroupType, "noise_config", new=NoiseConfig(0, timedelta(minutes=1))
+        ):
+            with self.feature(group_type.build_ingest_feature_name()):
+                _, group_info = process_event_and_issue_occurrence(
+                    self.build_occurrence_data(
+                        event_id=event_id, type=group_type.type_id, fingerprint=["some perf issue"]
+                    ),
+                    {
+                        "event_id": event_id,
+                        "project_id": self.project.id,
+                        "title": "some problem",
+                        "platform": "python",
+                        "tags": {"my_tag": "2"},
+                        "timestamp": before_now(minutes=1).isoformat(),
+                        "received": before_now(minutes=1).isoformat(),
+                    },
+                )
+
+            results = self.make_query(search_filter_query="issue.category:performance my_tag:2")
+            assert list(results) == []
+            with self.feature(
+                [
+                    "organizations:issue-platform",
+                    group_type.build_visible_feature_name(),
+                    "organizations:performance-issues-search",
+                ]
+            ):
+                results = self.make_query(search_filter_query="issue.category:performance my_tag:2")
+        assert list(results) == [group_info.group]
+
     def test_error_generic_query(self):
         with self.feature("organizations:issue-platform"):
             results = self.make_query(search_filter_query="my_tag:1")
@@ -2393,7 +2432,7 @@ class EventsGenericSnubaSearchTest(SharedSnubaTest, OccurrenceTestMixin):
             self.error_group_1,
         ]
 
-    def test_cursor_performance_issues(self):
+    def test_cursor_profile_issues(self):
         with self.feature("organizations:issue-platform"):
             results = self.make_query(
                 projects=[self.project],