Browse Source

feat(rca): Add base endpoint for testing RCA (#54054)

Add an endpoint where we will add Root Cause Analysis logic to
run against a transaction and a breakpoint. In this first version it
does some light validation on whether transaction name and project ID
are passed in, as well as if there is transaction data to query
Nar Saynorath 1 year ago
parent
commit
a298f12705

+ 43 - 0
src/sentry/api/endpoints/organization_events_root_cause_analysis.py

@@ -0,0 +1,43 @@
+from rest_framework.response import Response
+
+from sentry import features
+from sentry.api.base import region_silo_endpoint
+from sentry.api.bases.organization_events import OrganizationEventsEndpointBase
+from sentry.snuba.metrics_performance import query as metrics_query
+
+
+@region_silo_endpoint
+class OrganizationEventsRootCauseAnalysisEndpoint(OrganizationEventsEndpointBase):
+    def get(self, request, organization):
+        if not features.has(
+            "organizations:statistical-detectors-root-cause-analysis",
+            organization,
+            actor=request.user,
+        ):
+            return Response(status=404)
+
+        root_cause_results = {}
+
+        transaction_name = request.GET.get("transaction")
+        project_id = request.GET.get("project")
+        if not transaction_name or not project_id:
+            # Project ID is required to ensure the events we query for are
+            # the same transaction
+            return Response(status=400)
+
+        params = self.get_snuba_params(request, organization)
+
+        with self.handle_query_errors():
+            transaction_count_query = metrics_query(
+                ["count()"],
+                f"event.type:transaction transaction:{transaction_name} project_id:{project_id}",
+                params,
+                referrer="api.organization-events-root-cause-analysis",
+            )
+
+        if transaction_count_query["data"][0]["count"] == 0:
+            return Response(status=400, data="Transaction not found")
+
+        # TODO: This is only a temporary stub for surfacing RCA data
+        root_cause_results["transaction_count"] = transaction_count_query["data"][0]["count"]
+        return Response(status=200, data=root_cause_results)

+ 8 - 0
src/sentry/api/urls.py

@@ -8,6 +8,9 @@ from sentry.api.endpoints.org_auth_tokens import OrgAuthTokensEndpoint
 from sentry.api.endpoints.organization_events_facets_stats_performance import (
     OrganizationEventsFacetsStatsPerformanceEndpoint,
 )
+from sentry.api.endpoints.organization_events_root_cause_analysis import (
+    OrganizationEventsRootCauseAnalysisEndpoint,
+)
 from sentry.api.endpoints.organization_events_starfish import OrganizationEventsStarfishEndpoint
 from sentry.api.endpoints.organization_missing_org_members import OrganizationMissingMembersEndpoint
 from sentry.api.endpoints.organization_projects_experiment import (
@@ -1210,6 +1213,11 @@ ORGANIZATION_URLS = [
         OrganizationEventsSpansStatsEndpoint.as_view(),
         name="sentry-api-0-organization-events-spans-stats",
     ),
+    re_path(
+        r"^(?P<organization_slug>[^\/]+)/events-root-cause-analysis/$",
+        OrganizationEventsRootCauseAnalysisEndpoint.as_view(),
+        name="sentry-api-0-organization-events-root-cause-analysis",
+    ),
     re_path(
         r"^(?P<organization_slug>[^\/]+)/events-meta/$",
         OrganizationEventsMetaEndpoint.as_view(),

+ 75 - 0
tests/sentry/api/endpoints/test_organization_root_cause_analysis.py

@@ -0,0 +1,75 @@
+import pytest
+from django.urls import reverse
+from freezegun import freeze_time
+
+from sentry.snuba.metrics.naming_layer import TransactionMRI
+from sentry.testutils.cases import MetricsAPIBaseTestCase
+from sentry.testutils.silo import region_silo_test
+
+ROOT_CAUSE_FEATURE_FLAG = "organizations:statistical-detectors-root-cause-analysis"
+
+FEATURES = [ROOT_CAUSE_FEATURE_FLAG]
+
+pytestmark = [pytest.mark.sentry_metrics]
+
+
+@region_silo_test(stable=True)
+@freeze_time(MetricsAPIBaseTestCase.MOCK_DATETIME)
+class OrganizationRootCauseAnalysisTest(MetricsAPIBaseTestCase):
+    def setUp(self):
+        super().setUp()
+        self.login_as(self.user)
+        self.org = self.create_organization(owner=self.user)
+        self.project = self.create_project(organization=self.org)
+        self.url = reverse(
+            "sentry-api-0-organization-events-root-cause-analysis", args=[self.org.slug]
+        )
+        self.store_performance_metric(
+            name=TransactionMRI.DURATION.value,
+            tags={"transaction": "foo"},
+            org_id=self.org.id,
+            project_id=self.project.id,
+            value=1,
+        )
+
+    @property
+    def now(self):
+        return MetricsAPIBaseTestCase.MOCK_DATETIME
+
+    def test_404s_without_feature_flag(self):
+        response = self.client.get(self.url, format="json")
+        assert response.status_code == 404, response.content
+
+    def test_transaction_name_required(self):
+        with self.feature(FEATURES):
+            response = self.client.get(self.url, format="json")
+
+        assert response.status_code == 400, response.content
+
+    def test_project_id_required(self):
+        with self.feature(FEATURES):
+            response = self.client.get(self.url, format="json", data={"transaction": "foo"})
+
+        assert response.status_code == 400, response.content
+
+    def test_transaction_must_exist(self):
+        with self.feature(FEATURES):
+            response = self.client.get(
+                self.url,
+                format="json",
+                data={"transaction": "foo", "project": self.project.id},
+            )
+
+        assert response.status_code == 200, response.content
+
+        with self.feature(FEATURES):
+            response = self.client.get(
+                self.url,
+                format="json",
+                data={
+                    "transaction": "does not exist",
+                    "project": self.project.id,
+                },
+            )
+
+        assert response.status_code == 400, response.content