Browse Source

chore(billing): Add profile duration to org stats v2 endpoint (#80485)

Add profile duration to org stats v2 endpoint.
Alberto Leal 4 months ago
parent
commit
9f97c34a5a

+ 9 - 3
src/sentry/api/endpoints/organization_stats_v2.py

@@ -99,11 +99,17 @@ class OrgStatsQueryParamsSerializer(serializers.Serializer):
     )
 
     category = serializers.ChoiceField(
-        ("error", "transaction", "attachment", "replay", "profile", "monitor"),
+        ("error", "transaction", "attachment", "replay", "profile", "profile_duration", "monitor"),
         required=False,
         help_text=(
-            "If filtering by attachments, you cannot filter by any other category due to quantity values becoming nonsensical (combining bytes and event counts).\n\n"
-            "If filtering by `error`, it will automatically add `default` and `security` as we currently roll those two categories into `error` for displaying."
+            "Filter by data category. Each category represents a different type of data:\n\n"
+            "- `error`: Error events (includes `default` and `security` categories)\n"
+            "- `transaction`: Transaction events\n"
+            "- `attachment`: File attachments (note: cannot be combined with other categories since quantity represents bytes)\n"
+            "- `replay`: Session replay events\n"
+            "- `profile`: Performance profiles\n"
+            "- `profile_duration`: Profile duration data (note: cannot be combined with other categories since quantity represents milliseconds)\n"
+            "- `monitor`: Cron monitor events"
         ),
     )
     outcome = serializers.ChoiceField(

+ 33 - 0
tests/apidocs/endpoints/organizations/test_org_stats_v2.py

@@ -26,6 +26,18 @@ class OrganizationStatsDocs(APIDocsTestCase, OutcomesSnubaTest):
             },
             5,
         )
+        self.store_outcomes(
+            {
+                "org_id": self.organization.id,
+                "timestamp": self.now - timedelta(hours=1),
+                "project_id": self.project.id,
+                "outcome": Outcome.ACCEPTED,
+                "reason": "none",
+                "category": DataCategory.PROFILE_DURATION,
+                "quantity": 1000,  # Duration in milliseconds
+            },
+            3,
+        )
 
         self.url = reverse(
             "sentry-api-0-organization-stats-v2",
@@ -33,8 +45,29 @@ class OrganizationStatsDocs(APIDocsTestCase, OutcomesSnubaTest):
         )
 
     def test_get(self):
+        """
+        Test that the organization stats endpoint returns valid schema.
+        This test verifies that the endpoint correctly handles basic queries with interval, field and groupBy parameters.
+        """
         query = {"interval": "1d", "field": "sum(quantity)", "groupBy": "category"}
         response = self.client.get(self.url, query, format="json")
         request = RequestFactory().get(self.url)
 
         self.validate_schema(request, response)
+
+    def test_profile_duration_category(self):
+        """
+        Test that the organization stats endpoint correctly handles profile duration category.
+        This test verifies that the endpoint returns valid schema when filtering by profile_duration category
+        and aggregating quantity values.
+        """
+        query = {
+            "interval": "1d",
+            "field": "sum(quantity)",
+            "groupBy": "category",
+            "category": "profile_duration",
+        }
+        response = self.client.get(self.url, query, format="json")
+        request = RequestFactory().get(self.url)
+
+        self.validate_schema(request, response)

+ 89 - 10
tests/snuba/api/endpoints/test_organization_stats_v2.py

@@ -84,6 +84,20 @@ class OrganizationStatsTestV2(APITestCase, OutcomesSnubaTest):
             }
         )
 
+        # Add profile_duration outcome data
+        self.store_outcomes(
+            {
+                "org_id": self.org.id,
+                "timestamp": self.now - timedelta(hours=1),
+                "project_id": self.project.id,
+                "outcome": Outcome.ACCEPTED,
+                "reason": "none",
+                "category": DataCategory.PROFILE_DURATION,
+                "quantity": 1000,  # Duration in milliseconds
+            },
+            3,
+        )
+
     def do_request(self, query, user=None, org=None, status_code=200):
         self.login_as(user=user or self.user)
         org_slug = (org or self.organization).slug
@@ -439,34 +453,39 @@ class OrganizationStatsTestV2(APITestCase, OutcomesSnubaTest):
         )
 
         assert result_sorted(response.data) == {
-            "start": "2021-03-12T00:00:00Z",
             "end": "2021-03-15T00:00:00Z",
-            "intervals": ["2021-03-12T00:00:00Z", "2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
             "groups": [
                 {
                     "by": {
+                        "category": "attachment",
                         "outcome": "rate_limited",
                         "reason": "spike_protection",
-                        "category": "attachment",
                     },
-                    "totals": {"sum(quantity)": 1024},
                     "series": {"sum(quantity)": [0, 0, 1024]},
+                    "totals": {"sum(quantity)": 1024},
                 },
                 {
-                    "by": {"outcome": "accepted", "reason": "none", "category": "error"},
-                    "totals": {"sum(quantity)": 6},
+                    "by": {"category": "error", "outcome": "accepted", "reason": "none"},
                     "series": {"sum(quantity)": [0, 0, 6]},
+                    "totals": {"sum(quantity)": 6},
+                },
+                {
+                    "by": {"category": "profile_duration", "outcome": "accepted", "reason": "none"},
+                    "series": {"sum(quantity)": [0, 0, 3000]},
+                    "totals": {"sum(quantity)": 3000},
                 },
                 {
                     "by": {
                         "category": "transaction",
-                        "reason": "spike_protection",
                         "outcome": "rate_limited",
+                        "reason": "spike_protection",
                     },
-                    "totals": {"sum(quantity)": 1},
                     "series": {"sum(quantity)": [0, 0, 1]},
+                    "totals": {"sum(quantity)": 1},
                 },
             ],
+            "intervals": ["2021-03-12T00:00:00Z", "2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
+            "start": "2021-03-12T00:00:00Z",
         }
 
     @freeze_time("2021-03-14T12:27:28.303Z")
@@ -543,18 +562,23 @@ class OrganizationStatsTestV2(APITestCase, OutcomesSnubaTest):
             "groups": [
                 {
                     "by": {
+                        "reason": "spike_protection",
                         "outcome": "rate_limited",
                         "category": "attachment",
-                        "reason": "spike_protection",
                     },
                     "totals": {"sum(quantity)": 1024, "sum(times_seen)": 1},
                     "series": {"sum(quantity)": [0, 0, 1024], "sum(times_seen)": [0, 0, 1]},
                 },
                 {
-                    "by": {"outcome": "accepted", "reason": "none", "category": "error"},
+                    "by": {"category": "error", "reason": "none", "outcome": "accepted"},
                     "totals": {"sum(quantity)": 6, "sum(times_seen)": 6},
                     "series": {"sum(quantity)": [0, 0, 6], "sum(times_seen)": [0, 0, 6]},
                 },
+                {
+                    "by": {"category": "profile_duration", "reason": "none", "outcome": "accepted"},
+                    "totals": {"sum(quantity)": 3000, "sum(times_seen)": 3},
+                    "series": {"sum(quantity)": [0, 0, 3000], "sum(times_seen)": [0, 0, 3]},
+                },
                 {
                     "by": {
                         "category": "transaction",
@@ -851,6 +875,61 @@ class OrganizationStatsTestV2(APITestCase, OutcomesSnubaTest):
             ],
         }
 
+    @freeze_time("2021-03-14T12:27:28.303Z")
+    def test_profile_duration_filter(self):
+        """Test that profile_duration data is correctly filtered and returned"""
+        response = self.do_request(
+            {
+                "project": [-1],
+                "statsPeriod": "1d",
+                "interval": "1d",
+                "field": ["sum(quantity)"],
+                "category": ["profile_duration"],
+            },
+            status_code=200,
+        )
+
+        assert result_sorted(response.data) == {
+            "start": "2021-03-13T00:00:00Z",
+            "end": "2021-03-15T00:00:00Z",
+            "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
+            "groups": [
+                {
+                    "by": {},
+                    "series": {"sum(quantity)": [0, 3000]},  # 3 outcomes * 1000ms = 3000
+                    "totals": {"sum(quantity)": 3000},
+                }
+            ],
+        }
+
+    @freeze_time("2021-03-14T12:27:28.303Z")
+    def test_profile_duration_groupby(self):
+        """Test that profile_duration data is correctly grouped"""
+        response = self.do_request(
+            {
+                "project": [-1],
+                "statsPeriod": "1d",
+                "interval": "1d",
+                "field": ["sum(quantity)"],
+                "groupBy": ["category"],
+                "category": ["profile_duration"],
+            },
+            status_code=200,
+        )
+
+        assert result_sorted(response.data) == {
+            "start": "2021-03-13T00:00:00Z",
+            "end": "2021-03-15T00:00:00Z",
+            "intervals": ["2021-03-13T00:00:00Z", "2021-03-14T00:00:00Z"],
+            "groups": [
+                {
+                    "by": {"category": "profile_duration"},
+                    "series": {"sum(quantity)": [0, 3000]},
+                    "totals": {"sum(quantity)": 3000},
+                }
+            ],
+        }
+
 
 def result_sorted(result):
     """sort the groups of the results array by the `by` object, ensuring a stable order"""