Browse Source

feat(metric-stats): Add cardinality limited outcome id (#68422)

This adds a new outcome id for metric cardinality limits. A cardinality
limited is similar to rate limits but instead of being enforced on
volume (rate) it enforces cardinality. As well as quotas a metric can be
subject to multiple cardinality limits, the limit id is noted in the
reason field.


The new id will only be emitted for the `METRIC_BUCKET` data category
for now, which is unused throughout the product except for
(abuse-)quotas.
David Herberth 11 months ago
parent
commit
93655dcfad

+ 2 - 0
src/sentry/apidocs/examples/organization_examples.py

@@ -89,6 +89,7 @@ class OrganizationExamples:
                                     "invalid": 0,
                                     "abuse": 1938113,
                                     "client_discard": 1942414,
+                                    "cardinality_limited": 0,
                                 },
                                 "totals": {"dropped": 2506132, "sum(quantity)": 10252111},
                             },
@@ -101,6 +102,7 @@ class OrganizationExamples:
                                     "invalid": 0,
                                     "abuse": 1927179,
                                     "client_discard": 1931595,
+                                    "cardinality_limited": 0,
                                 },
                                 "totals": {"dropped": 2458946, "sum(quantity)": 10174711},
                             },

+ 6 - 1
src/sentry/snuba/sessions_v2.py

@@ -21,7 +21,11 @@ from sentry.utils.outcomes import Outcome
 
 logger = logging.getLogger(__name__)
 
-dropped_outcomes = [Outcome.INVALID.api_name(), Outcome.RATE_LIMITED.api_name()]
+dropped_outcomes = [
+    Outcome.INVALID.api_name(),
+    Outcome.RATE_LIMITED.api_name(),
+    Outcome.CARDINALITY_LIMITED.api_name(),
+]
 
 
 """
@@ -639,6 +643,7 @@ def massage_sessions_result_summary(
                 "invalid": 0,
                 "abuse": 0,
                 "client_discard": 0,
+                "cardinality_limited": 0,
               },
               "totals": {
                 "dropped": 1,

+ 1 - 0
src/sentry/utils/outcomes.py

@@ -20,6 +20,7 @@ class Outcome(IntEnum):
     INVALID = 3
     ABUSE = 4
     CLIENT_DISCARD = 5
+    CARDINALITY_LIMITED = 6
 
     def api_name(self) -> str:
         return self.name.lower()

+ 1 - 0
static/app/types/core.tsx

@@ -117,6 +117,7 @@ export enum Outcome {
   DROPPED = 'dropped', // this is not a real outcome coming from the server
   RATE_LIMITED = 'rate_limited',
   CLIENT_DISCARD = 'client_discard',
+  CARDINALITY_LIMITED = 'cardinality_limited',
 }
 
 export type IntervalPeriod = ReturnType<typeof getInterval>;

+ 3 - 0
static/app/views/organizationStats/usageStatsOrg.tsx

@@ -363,6 +363,7 @@ class UsageStatsOrganization<
         [Outcome.INVALID]: 0, // Combined with dropped later
         [Outcome.RATE_LIMITED]: 0, // Combined with dropped later
         [Outcome.CLIENT_DISCARD]: 0, // Not exposed yet
+        [Outcome.CARDINALITY_LIMITED]: 0, // Combined with dropped later
       };
 
       orgStats.groups.forEach(group => {
@@ -386,6 +387,7 @@ class UsageStatsOrganization<
               return;
             case Outcome.DROPPED:
             case Outcome.RATE_LIMITED:
+            case Outcome.CARDINALITY_LIMITED:
             case Outcome.INVALID:
               usageStats[i].dropped.total += stat;
               // TODO: add client discards to dropped?
@@ -399,6 +401,7 @@ class UsageStatsOrganization<
       // Invalid and rate_limited data is combined with dropped
       count[Outcome.DROPPED] += count[Outcome.INVALID];
       count[Outcome.DROPPED] += count[Outcome.RATE_LIMITED];
+      count[Outcome.DROPPED] += count[Outcome.CARDINALITY_LIMITED];
 
       usageStats.forEach(stat => {
         stat.total = stat.accepted + stat.filtered + stat.dropped.total;

+ 1 - 0
static/app/views/organizationStats/usageStatsProjects.tsx

@@ -372,6 +372,7 @@ class UsageStatsProjects extends DeprecatedAsyncComponent<Props, State> {
           stats[projectId][outcome] += group.totals['sum(quantity)'];
         } else if (
           outcome === Outcome.RATE_LIMITED ||
+          outcome === Outcome.CARDINALITY_LIMITED ||
           outcome === Outcome.INVALID ||
           outcome === Outcome.DROPPED
         ) {

+ 1 - 0
tests/sentry/utils/test_outcomes.py

@@ -31,6 +31,7 @@ def setup():
         (Outcome.INVALID, False),
         (Outcome.ABUSE, False),
         (Outcome.CLIENT_DISCARD, False),
+        (Outcome.CARDINALITY_LIMITED, False),
     ],
 )
 def test_outcome_is_billing(outcome: Outcome, is_billing: bool):

+ 14 - 0
tests/snuba/api/endpoints/test_organization_stats_summary.py

@@ -308,6 +308,7 @@ class OrganizationStatsSummaryTest(APITestCase, OutcomesSnubaTest):
                             "outcomes": {
                                 "abuse": 0,
                                 "accepted": 6,
+                                "cardinality_limited": 0,
                                 "client_discard": 0,
                                 "filtered": 0,
                                 "invalid": 0,
@@ -326,6 +327,7 @@ class OrganizationStatsSummaryTest(APITestCase, OutcomesSnubaTest):
                             "outcomes": {
                                 "abuse": 0,
                                 "accepted": 0,
+                                "cardinality_limited": 0,
                                 "client_discard": 0,
                                 "filtered": 0,
                                 "invalid": 0,
@@ -370,6 +372,7 @@ class OrganizationStatsSummaryTest(APITestCase, OutcomesSnubaTest):
                                 "invalid": 0,
                                 "abuse": 0,
                                 "client_discard": 0,
+                                "cardinality_limited": 0,
                             },
                             "totals": {"dropped": 1024, "sum(quantity)": 1024},
                         },
@@ -382,6 +385,7 @@ class OrganizationStatsSummaryTest(APITestCase, OutcomesSnubaTest):
                                 "invalid": 0,
                                 "abuse": 0,
                                 "client_discard": 0,
+                                "cardinality_limited": 0,
                             },
                             "totals": {"dropped": 0, "sum(quantity)": 6},
                         },
@@ -400,6 +404,7 @@ class OrganizationStatsSummaryTest(APITestCase, OutcomesSnubaTest):
                                 "invalid": 0,
                                 "abuse": 0,
                                 "client_discard": 0,
+                                "cardinality_limited": 0,
                             },
                             "totals": {"dropped": 1, "sum(quantity)": 1},
                         }
@@ -440,6 +445,7 @@ class OrganizationStatsSummaryTest(APITestCase, OutcomesSnubaTest):
                                 "invalid": 0,
                                 "abuse": 0,
                                 "client_discard": 0,
+                                "cardinality_limited": 0,
                             },
                             "totals": {
                                 "dropped": 1025,
@@ -456,6 +462,7 @@ class OrganizationStatsSummaryTest(APITestCase, OutcomesSnubaTest):
                                 "invalid": 0,
                                 "abuse": 0,
                                 "client_discard": 0,
+                                "cardinality_limited": 0,
                             },
                             "totals": {"dropped": 0, "sum(quantity)": 6, "sum(times_seen)": 6},
                         },
@@ -474,6 +481,7 @@ class OrganizationStatsSummaryTest(APITestCase, OutcomesSnubaTest):
                                 "invalid": 0,
                                 "abuse": 0,
                                 "client_discard": 0,
+                                "cardinality_limited": 0,
                             },
                             "totals": {"dropped": 2, "sum(quantity)": 1, "sum(times_seen)": 1},
                         }
@@ -511,6 +519,7 @@ class OrganizationStatsSummaryTest(APITestCase, OutcomesSnubaTest):
                             "outcomes": {
                                 "abuse": 0,
                                 "accepted": 6,
+                                "cardinality_limited": 0,
                                 "client_discard": 0,
                                 "filtered": 0,
                                 "invalid": 0,
@@ -529,6 +538,7 @@ class OrganizationStatsSummaryTest(APITestCase, OutcomesSnubaTest):
                             "outcomes": {
                                 "abuse": 0,
                                 "accepted": 0,
+                                "cardinality_limited": 0,
                                 "client_discard": 0,
                                 "filtered": 0,
                                 "invalid": 0,
@@ -571,6 +581,7 @@ class OrganizationStatsSummaryTest(APITestCase, OutcomesSnubaTest):
                             "outcomes": {
                                 "abuse": 0,
                                 "accepted": 6,
+                                "cardinality_limited": 0,
                                 "client_discard": 0,
                                 "filtered": 0,
                                 "invalid": 0,
@@ -618,6 +629,7 @@ class OrganizationStatsSummaryTest(APITestCase, OutcomesSnubaTest):
                                 "invalid": 0,
                                 "abuse": 0,
                                 "client_discard": 0,
+                                "cardinality_limited": 0,
                             },
                             "totals": {"dropped": 1, "sum(times_seen)": 1},
                         }
@@ -637,6 +649,7 @@ class OrganizationStatsSummaryTest(APITestCase, OutcomesSnubaTest):
                                 "invalid": 0,
                                 "abuse": 0,
                                 "client_discard": 0,
+                                "cardinality_limited": 0,
                             },
                             "totals": {"dropped": 1, "sum(times_seen)": 1},
                         }
@@ -713,6 +726,7 @@ class OrganizationStatsSummaryTest(APITestCase, OutcomesSnubaTest):
                                 "invalid": 0,
                                 "abuse": 0,
                                 "client_discard": 0,
+                                "cardinality_limited": 0,
                             },
                             "totals": {"dropped": 0, "sum(quantity)": 6},
                         }