Browse Source

feat(chartcuterie): Charts for Function Regression (#70815)

Raj Joshi 10 months ago
parent
commit
cd846307df

+ 71 - 1
src/sentry/integrations/slack/message_builder/image_block_builder.py

@@ -12,7 +12,10 @@ from sentry.integrations.slack.message_builder.time_utils import (
     get_approx_start_time,
     get_approx_start_time,
     get_relative_time,
     get_relative_time,
 )
 )
-from sentry.issues.grouptype import PerformanceP95EndpointRegressionGroupType
+from sentry.issues.grouptype import (
+    PerformanceP95EndpointRegressionGroupType,
+    ProfileFunctionRegressionType,
+)
 from sentry.models.apikey import ApiKey
 from sentry.models.apikey import ApiKey
 from sentry.models.group import Group
 from sentry.models.group import Group
 from sentry.snuba.referrer import Referrer
 from sentry.snuba.referrer import Referrer
@@ -33,6 +36,12 @@ class ImageBlockBuilder(BlockSlackMessageBuilder):
         ):
         ):
             return self._build_endpoint_regression_image_block()
             return self._build_endpoint_regression_image_block()
 
 
+        if (
+            features.has("organizations:slack-function-regression-image", self.group.organization)
+            and self.group.issue_type == ProfileFunctionRegressionType
+        ):
+            return self._build_function_regression_image_block()
+
         # TODO: Add support for other issue alerts
         # TODO: Add support for other issue alerts
         return None
         return None
 
 
@@ -89,3 +98,64 @@ class ImageBlockBuilder(BlockSlackMessageBuilder):
             )
             )
             sentry_sdk.capture_exception()
             sentry_sdk.capture_exception()
             return None
             return None
+
+    def _build_function_regression_image_block(self) -> SlackBlock | None:
+        logger.info(
+            "build_function_regression_image.attempt",
+            extra={
+                "group_id": self.group.id,
+            },
+        )
+
+        try:
+            organization = self.group.organization
+            event = self.group.get_latest_event_for_environments()
+            if event is None or event.occurrence is None:
+                return None
+
+            period = get_relative_time(anchor=get_approx_start_time(self.group), relative_days=14)
+            resp = client.get(
+                auth=ApiKey(organization_id=organization.id, scope_list=["org:read"]),
+                user=None,
+                path=f"/organizations/{organization.slug}/events-stats/",
+                data={
+                    "dataset": "profileFunctions",
+                    "referrer": Referrer.API_ALERTS_CHARTCUTERIE,
+                    "project": self.group.project.id,
+                    "start": period["start"].strftime("%Y-%m-%d %H:%M:%S"),
+                    "end": period["end"].strftime("%Y-%m-%d %H:%M:%S"),
+                    "yAxis": ["p95()"],
+                    "query": f"fingerprint:{event.occurrence.evidence_data['fingerprint']}",
+                },
+            )
+
+            # Convert the aggregate range from nanoseconds to milliseconds
+            evidence_data = {
+                "aggregate_range_1": event.occurrence.evidence_data["aggregate_range_1"] / 1e6,
+                "aggregate_range_2": event.occurrence.evidence_data["aggregate_range_2"] / 1e6,
+                "breakpoint": event.occurrence.evidence_data["breakpoint"],
+            }
+
+            url = charts.generate_chart(
+                ChartType.SLACK_PERFORMANCE_FUNCTION_REGRESSION,
+                data={
+                    "evidenceData": evidence_data,
+                    "rawResponse": resp.data,
+                },
+            )
+
+            return self.get_image_block(
+                url=url,
+                title=self.group.title,
+                alt="P95(function.duration)",
+            )
+
+        except Exception as e:
+            logger.exception(
+                "build_function_regression_image.failed",
+                extra={
+                    "exception": e,
+                },
+            )
+            sentry_sdk.capture_exception()
+            return None

+ 64 - 4
tests/acceptance/chartcuterie/test_image_block_builder.py

@@ -1,11 +1,19 @@
 import uuid
 import uuid
+from datetime import timedelta
 
 
 import pytest
 import pytest
 
 
 from sentry.integrations.slack.message_builder.image_block_builder import ImageBlockBuilder
 from sentry.integrations.slack.message_builder.image_block_builder import ImageBlockBuilder
-from sentry.issues.grouptype import PerformanceP95EndpointRegressionGroupType
+from sentry.issues.grouptype import (
+    PerformanceP95EndpointRegressionGroupType,
+    ProfileFunctionRegressionType,
+)
 from sentry.models.group import Group
 from sentry.models.group import Group
-from sentry.testutils.cases import AcceptanceTestCase, MetricsEnhancedPerformanceTestCase
+from sentry.testutils.cases import (
+    AcceptanceTestCase,
+    MetricsEnhancedPerformanceTestCase,
+    ProfilesSnubaTestCase,
+)
 from sentry.testutils.helpers.datetime import before_now
 from sentry.testutils.helpers.datetime import before_now
 from sentry.testutils.helpers.features import with_feature
 from sentry.testutils.helpers.features import with_feature
 from tests.sentry.issues.test_utils import OccurrenceTestMixin
 from tests.sentry.issues.test_utils import OccurrenceTestMixin
@@ -14,17 +22,21 @@ pytestmark = pytest.mark.sentry_metrics
 
 
 
 
 class TestSlackImageBlockBuilder(
 class TestSlackImageBlockBuilder(
-    AcceptanceTestCase, MetricsEnhancedPerformanceTestCase, OccurrenceTestMixin
+    AcceptanceTestCase,
+    MetricsEnhancedPerformanceTestCase,
+    ProfilesSnubaTestCase,
+    OccurrenceTestMixin,
 ):
 ):
     def setUp(self):
     def setUp(self):
         super().setUp()
         super().setUp()
         self.features = {
         self.features = {
             "organizations:performance-use-metrics": True,
             "organizations:performance-use-metrics": True,
             "organizations:slack-endpoint-regression-image": True,
             "organizations:slack-endpoint-regression-image": True,
+            "organizations:slack-function-regression-image": True,
         }
         }
 
 
     @with_feature("organizations:slack-endpoint-regression-image")
     @with_feature("organizations:slack-endpoint-regression-image")
-    def test_image_block(self):
+    def test_image_block_for_endpoint_regression(self):
         for i in range(10):
         for i in range(10):
             event_id = uuid.uuid4().hex
             event_id = uuid.uuid4().hex
             _ = self.process_occurrence(
             _ = self.process_occurrence(
@@ -55,3 +67,51 @@ class TestSlackImageBlockBuilder(
 
 
         assert image_block and "type" in image_block and image_block["type"] == "image"
         assert image_block and "type" in image_block and image_block["type"] == "image"
         assert "_media/" in image_block["image_url"]
         assert "_media/" in image_block["image_url"]
+
+    @with_feature("organizations:slack-function-regression-image")
+    def test_image_block_for_function_regression(self):
+        hour_ago = (before_now(minutes=10) - timedelta(hours=1)).replace(
+            minute=0, second=0, microsecond=0
+        )
+
+        for i in range(10):
+            event_id = uuid.uuid4().hex
+            _ = self.process_occurrence(
+                project_id=self.project.id,
+                event_id=event_id,
+                type=ProfileFunctionRegressionType.type_id,
+                event_data={
+                    "fingerprint": ["group-1"],
+                    "timestamp": before_now(minutes=i + 10).isoformat(),
+                    "function": "foo",
+                },
+                evidence_data={
+                    "breakpoint": before_now(minutes=i + 10).timestamp(),
+                    "fingerprint": self.function_fingerprint({"package": "foo", "function": "foo"}),
+                    "aggregate_range_1": 51588984.199999996,
+                    "aggregate_range_2": 839118611.8535699,
+                },
+            )
+
+            self.store_functions(
+                [
+                    {
+                        "self_times_ns": [100 for _ in range(100)],
+                        "package": "foo",
+                        "function": "foo",
+                        # only in app functions should
+                        # appear in the results
+                        "in_app": True,
+                    },
+                ],
+                project=self.project,
+                timestamp=hour_ago,
+            )
+
+        group = Group.objects.first()
+
+        with self.feature(self.features):
+            image_block = ImageBlockBuilder(group=group).build_image_block()
+
+        assert image_block and "type" in image_block and image_block["type"] == "image"
+        assert "_media/" in image_block["image_url"]