Browse Source

feat(perf): Allow excluding span ops (#53075)

### Summary
We don't have a way currently to exclude span ops from "suspect spans"
in the spans requests,
  this means sometimes suspect spans tab shows root spans (eg.
  http.server, pageload). The arrays that are used by suspect spans have
  no parent information, which would be the preferred way of checking
  this, but users might also set custom transactions they do care about
  the self-time for.

  This api changes allows us to send a small list of known root
  transaction op types from the frontend.
Kev 1 year ago
parent
commit
ffb441dd9f

+ 16 - 1
src/sentry/api/endpoints/organization_events_spans_performance.py

@@ -115,7 +115,10 @@ class OrganizationEventsSpansEndpointBase(OrganizationEventsV2EndpointBase):
 class SpansPerformanceSerializer(serializers.Serializer):
     field = ListField(child=serializers.CharField(), required=False, allow_null=True)
     query = serializers.CharField(required=False, allow_null=True)
-    spanOp = ListField(child=serializers.CharField(), required=False, allow_null=True, max_length=4)
+    spanOp = ListField(child=serializers.CharField(), required=False, allow_null=True, max_length=5)
+    excludeSpanOp = ListField(
+        child=serializers.CharField(), required=False, allow_null=True, max_length=5
+    )
     spanGroup = ListField(
         child=serializers.CharField(), required=False, allow_null=True, max_length=4
     )
@@ -157,6 +160,7 @@ class OrganizationEventsSpansPerformanceEndpoint(OrganizationEventsSpansEndpoint
         fields = serialized.get("field", [])
         query = serialized.get("query")
         span_ops = serialized.get("spanOp")
+        exclude_span_ops = serialized.get("excludeSpanOp")
         span_groups = serialized.get("spanGroup")
         min_exclusive_time = serialized.get("min_exclusive_time")
         max_exclusive_time = serialized.get("max_exclusive_time")
@@ -169,6 +173,7 @@ class OrganizationEventsSpansPerformanceEndpoint(OrganizationEventsSpansEndpoint
                 fields,
                 query,
                 span_ops,
+                exclude_span_ops,
                 span_groups,
                 direction,
                 orderby_column,
@@ -467,6 +472,7 @@ def query_suspect_span_groups(
     fields: List[str],
     query: Optional[str],
     span_ops: Optional[List[str]],
+    exclude_span_ops: Optional[List[str]],
     span_groups: Optional[List[str]],
     direction: str,
     orderby: str,
@@ -519,6 +525,15 @@ def query_suspect_span_groups(
             )
         )
 
+    if exclude_span_ops:
+        extra_conditions.append(
+            Condition(
+                builder.resolve_function("array_join(spans_op)"),
+                Op.NOT_IN,
+                Function("tuple", exclude_span_ops),
+            )
+        )
+
     if span_groups:
         extra_conditions.append(
             Condition(

+ 39 - 0
tests/snuba/api/endpoints/test_organization_events_spans_performance.py

@@ -823,6 +823,45 @@ class OrganizationEventsSpansPerformanceEndpointTest(OrganizationEventsSpansEndp
             in mock_raw_snql_query.call_args_list[0][0][0].query.where
         )
 
+    @patch("sentry.api.endpoints.organization_events_spans_performance.raw_snql_query")
+    def test_exclude_op_filter(self, mock_raw_snql_query):
+        event = self.create_event()
+
+        mock_raw_snql_query.side_effect = [
+            {
+                "data": [
+                    self.suspect_span_group_snuba_results("django.middleware", event),
+                ],
+            },
+        ]
+
+        with self.feature(self.FEATURES):
+            response = self.client.get(
+                self.url,
+                data={
+                    "project": self.project.id,
+                    "excludeSpanOp": "http.server",
+                },
+                format="json",
+            )
+
+        assert response.status_code == 200, response.content
+        self.assert_suspect_span(
+            response.data,
+            [self.suspect_span_results("django.middleware", event)],
+        )
+
+        assert mock_raw_snql_query.call_count == 1
+        # the first call should also contain the additional condition on the span op
+        assert (
+            Condition(
+                lhs=Function("arrayJoin", [Column("spans.op")], "array_join_spans_op"),
+                op=Op.NOT_IN,
+                rhs=Function("tuple", ["http.server"]),
+            )
+            in mock_raw_snql_query.call_args_list[0][0][0].query.where
+        )
+
     def test_bad_group_filter(self):
         with self.feature(self.FEATURES):
             response = self.client.get(