Browse Source

feat(spike-protection): Enable project stats for spike protection organizations (#43925)

Refactor + rollout, this modifies the flag checking code to allow for
one option to set multiple flags (and changes the tests associated with
it).

I needed to make this change to allow the `quotas:new-spike-protection`
option to set both `spike-projections` and `project-stats`

Doing so will enable the spike protections UI for the customers with
spike protection available to their organization

Requires https://github.com/getsentry/getsentry/pull/9407
Leander Rodrigues 2 years ago
parent
commit
42fc6f84bd

+ 22 - 10
src/sentry/api/serializers/models/organization.py

@@ -1,8 +1,8 @@
 from __future__ import annotations
 
-from collections.abc import Callable, Mapping, MutableMapping, Sequence
+from collections.abc import Mapping, MutableMapping, Sequence
 from datetime import datetime
-from typing import TYPE_CHECKING, Any, List, Optional, Union, cast
+from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Union, cast
 
 from rest_framework import serializers
 from sentry_relay.auth import PublicKey
@@ -28,7 +28,6 @@ from sentry.constants import (
     DEBUG_FILES_ROLE_DEFAULT,
     EVENTS_MEMBER_ADMIN_DEFAULT,
     JOIN_REQUESTS_DEFAULT,
-    ORGANIZATION_OPTIONS_AS_FEATURES,
     PROJECT_RATE_LIMIT_DEFAULT,
     REQUIRE_SCRUB_DATA_DEFAULT,
     REQUIRE_SCRUB_DEFAULTS_DEFAULT,
@@ -61,6 +60,23 @@ _ORGANIZATION_SCOPE_PREFIX = "organizations:"
 if TYPE_CHECKING:
     from sentry.api.serializers import UserSerializerResponse, UserSerializerResponseSelf
 
+# A mapping of OrganizationOption keys to a list of frontend features, and functions to apply the feature.
+# Enabling feature-flagging frontend components without an extra API call/endpoint to verify
+# the OrganizationOption.
+OptionFeature = Tuple[str, Callable[[OrganizationOption], bool]]
+ORGANIZATION_OPTIONS_AS_FEATURES: Mapping[str, List[OptionFeature]] = {
+    "sentry:project-rate-limit": [
+        ("legacy-rate-limits", lambda opt: True),
+    ],
+    "sentry:account-rate-limit": [
+        ("legacy-rate-limits", lambda opt: True),
+    ],
+    "quotas:new-spike-protection": [
+        ("spike-projections", lambda opt: bool(opt.value)),
+        ("project-stats", lambda opt: bool(opt.value)),
+    ],
+}
+
 
 class BaseOrganizationSerializer(serializers.Serializer):  # type: ignore
     name = serializers.CharField(max_length=64)
@@ -269,13 +285,9 @@ class OrganizationSerializer(Serializer):  # type: ignore
             organization=obj, key__in=ORGANIZATION_OPTIONS_AS_FEATURES.keys()
         )
         for option in options_as_features:
-            option_feature = ORGANIZATION_OPTIONS_AS_FEATURES.get(option.key)
-            if not option_feature:
-                continue
-            feature: str = option_feature[0]  # feature flag string
-            func: Callable[[OrganizationOption], bool] | None = option_feature[1]  # flag validator
-            if not callable(func) or func(option):
-                feature_list.add(feature)
+            for option_feature, option_function in ORGANIZATION_OPTIONS_AS_FEATURES[option.key]:
+                if option_function(option):
+                    feature_list.add(option_feature)
 
         if getattr(obj.flags, "allow_joinleave"):
             feature_list.add("open-membership")

+ 0 - 10
src/sentry/constants.py

@@ -577,16 +577,6 @@ StatsPeriod = namedtuple("StatsPeriod", ("segments", "interval"))
 
 LEGACY_RATE_LIMIT_OPTIONS = frozenset(("sentry:project-rate-limit", "sentry:account-rate-limit"))
 
-# A mapping of OrganizationOption keys to frontend features, and functions to apply the feature.
-# Enabling feature-flagging frontend components without an extra API call/endpoint to verify
-# the OrganizationOption.
-# If the function is None, the feature will be added regardless of the option value (if present)
-ORGANIZATION_OPTIONS_AS_FEATURES = {
-    "sentry:project-rate-limit": ("legacy-rate-limits", None),
-    "sentry:account-rate-limit": ("legacy-rate-limits", None),
-    "quotas:new-spike-protection": ("spike-projections", lambda opt: bool(opt.value)),
-}
-
 
 # We need to limit the range of valid timestamps of an event because that
 # timestamp is used to control data retention.

+ 26 - 10
tests/sentry/api/serializers/test_organization.py

@@ -10,8 +10,8 @@ from sentry.api.serializers import (
     OnboardingTasksSerializer,
     serialize,
 )
+from sentry.api.serializers.models.organization import ORGANIZATION_OPTIONS_AS_FEATURES
 from sentry.auth import access
-from sentry.constants import ORGANIZATION_OPTIONS_AS_FEATURES
 from sentry.features.base import OrganizationFeature
 from sentry.models import OrganizationOnboardingTask
 from sentry.models.options.organization_option import OrganizationOption
@@ -20,10 +20,22 @@ from sentry.testutils import TestCase
 from sentry.testutils.silo import region_silo_test
 
 mock_options_as_features = {
-    "sentry:set_no_func": ("frontend-flag-one", None),
-    "sentry:unset_no_func": ("frontend-flag-two", None),
-    "sentry:set_with_func_pass": ("frontend-flag-three", lambda opt: bool(opt.value)),
-    "sentry:set_with_func_fail": ("frontend-flag-four", lambda opt: bool(opt.value)),
+    "sentry:set_no_value": [
+        ("frontend-flag-1-1", lambda opt: True),
+        ("frontend-flag-1-2", lambda opt: True),
+    ],
+    "sentry:unset_no_value": [
+        ("frontend-flag-2-1", lambda opt: True),
+        ("frontend-flag-2-2", lambda opt: True),
+    ],
+    "sentry:set_with_func_pass": [
+        ("frontend-flag-3-1", lambda opt: bool(opt.value)),
+        ("frontend-flag-3-2", lambda opt: bool(opt.value)),
+    ],
+    "sentry:set_with_func_fail": [
+        ("frontend-flag-4-1", lambda opt: bool(opt.value)),
+        ("frontend-flag-4-2", lambda opt: bool(opt.value)),
+    ],
 }
 
 
@@ -93,20 +105,24 @@ class OrganizationSerializerTest(TestCase):
         user = self.create_user()
         organization = self.create_organization(owner=user)
 
-        OrganizationOption.objects.set_value(organization, "sentry:set_no_func", {})
+        OrganizationOption.objects.set_value(organization, "sentry:set_no_value", {})
         OrganizationOption.objects.set_value(organization, "sentry:set_with_func_pass", 1)
         OrganizationOption.objects.set_value(organization, "sentry:set_with_func_fail", 0)
 
         features = serialize(organization, user)["features"]
 
         # Setting a flag with no function checks for option, regardless of value
-        assert mock_options_as_features["sentry:set_no_func"][0] in features
+        for feature, _func in mock_options_as_features["sentry:set_no_value"]:
+            assert feature in features
         # If the option isn't set, it doesn't appear in features
-        assert mock_options_as_features["sentry:unset_no_func"][0] not in features
+        for feature, _func in mock_options_as_features["sentry:unset_no_value"]:
+            assert feature not in features
         # With a function, run it against the value
-        assert mock_options_as_features["sentry:set_with_func_pass"][0] in features
+        for feature, _func in mock_options_as_features["sentry:set_with_func_pass"]:
+            assert feature in features
         # If it returns False, it doesn't appear in features
-        assert mock_options_as_features["sentry:set_with_func_fail"][0] not in features
+        for feature, _func in mock_options_as_features["sentry:set_with_func_fail"]:
+            assert feature not in features
 
 
 @region_silo_test