Browse Source

perf(projects): add collapse field for unused project features (#65987)

In large organizations with lots of projects, these feature flag
computations get expensive and take up to ~350ms each.

This PR aims to remove unused feature flags from the projects serialized
if a new collapse field, `unusedFeatures` is set.

Related: https://github.com/getsentry/sentry/issues/65947

Frontend PR to follow
Josh Ferge 1 year ago
parent
commit
3abd05fcb2

+ 28 - 2
src/sentry/api/serializers/models/project.py

@@ -60,6 +60,24 @@ STATS_PERIOD_CHOICES = {
 _PROJECT_SCOPE_PREFIX = "projects:"
 
 LATEST_DEPLOYS_KEY: Final = "latestDeploys"
+UNUSED_ON_FRONTEND_FEATURES: Final = "unusedFeatures"
+
+
+# These features are not used on the frontend,
+# and add a lot of latency ~100-300ms per flag for large organizations
+# so we exclude them from the response if the unusedFeatures collapse parameter is set
+PROJECT_FEATURES_NOT_USED_ON_FRONTEND = {
+    "profiling-ingest-unsampled-profiles",
+    "discard-transaction",
+    "span-metrics-extraction-resource",
+    "span-metrics-extraction-all-modules",
+    "race-free-group-creation",
+    "first-event-severity-new-escalation",
+    "first-event-severity-calculation",
+    "first-event-severity-alerting",
+    "alert-filters",
+    "servicehooks",
+}
 
 
 def _get_team_memberships(team_list: Sequence[Team], user: User) -> Iterable[int]:
@@ -132,7 +150,7 @@ def get_access_by_project(
 
 
 def get_features_for_projects(
-    all_projects: Sequence[Project], user: User
+    all_projects: Sequence[Project], user: User, filter_unused_on_frontend_features: bool = False
 ) -> MutableMapping[Project, list[str]]:
     # Arrange to call features.has_for_batch rather than features.has
     # for performance's sake
@@ -146,6 +164,12 @@ def get_features_for_projects(
         for feature in features.all(feature_type=ProjectFeature).keys()
         if feature.startswith(_PROJECT_SCOPE_PREFIX)
     ]
+    if filter_unused_on_frontend_features:
+        project_features = [
+            feature
+            for feature in project_features
+            if feature[len(_PROJECT_SCOPE_PREFIX) :] not in PROJECT_FEATURES_NOT_USED_ON_FRONTEND
+        ]
 
     batch_checked = set()
     for organization, projects in projects_by_org.items():
@@ -338,7 +362,9 @@ class ProjectSerializer(Serializer):
             result = get_access_by_project(item_list, user)
 
         with measure_span("features"):
-            features_by_project = get_features_for_projects(item_list, user)
+            features_by_project = get_features_for_projects(
+                item_list, user, self._collapse(UNUSED_ON_FRONTEND_FEATURES)
+            )
             for project, serialized in result.items():
                 serialized["features"] = features_by_project[project]
 

+ 24 - 0
tests/sentry/api/serializers/test_project.py

@@ -10,6 +10,8 @@ from django.utils import timezone as django_timezone
 from sentry import features
 from sentry.api.serializers import serialize
 from sentry.api.serializers.models.project import (
+    PROJECT_FEATURES_NOT_USED_ON_FRONTEND,
+    UNUSED_ON_FRONTEND_FEATURES,
     DetailedProjectSerializer,
     ProjectSummarySerializer,
     ProjectWithOrganizationSerializer,
@@ -651,6 +653,28 @@ class ProjectSummarySerializerTest(SnubaTestCase, TestCase):
 
         assert check_has_health_data.call_count == 1
 
+    def test_project_with_collapsed_unused_frontend_flags(self):
+        result_with_unused_flags = serialize(
+            self.project,
+            self.user,
+            ProjectSummarySerializer(collapse=[UNUSED_ON_FRONTEND_FEATURES]),
+        )
+
+        intersected_result = PROJECT_FEATURES_NOT_USED_ON_FRONTEND.intersection(
+            set(result_with_unused_flags["features"])
+        )
+        assert intersected_result == set()
+
+        unused_on_frontend_first_element = next(iter(PROJECT_FEATURES_NOT_USED_ON_FRONTEND))
+        with self.feature(f"projects:{unused_on_frontend_first_element}"):
+            result_with_all_flags = serialize(
+                self.project,
+                self.user,
+                ProjectSummarySerializer(),
+            )
+
+        assert unused_on_frontend_first_element in result_with_all_flags["features"]
+
 
 @region_silo_test
 class ProjectWithOrganizationSerializerTest(TestCase):