Browse Source

feat(spans): Support queries on span fields autocomplete (#70984)

The frontend can pass a substring that we want to autocomplete for them.
This adds support for it behind a feature flag.
Tony Xiao 10 months ago
parent
commit
7acaa6fcd6

+ 31 - 2
src/sentry/api/endpoints/organization_spans_fields.py

@@ -88,12 +88,37 @@ class OrganizationSpansFieldValuesEndpoint(OrganizationEventsV2EndpointBase):
     }
     owner = ApiOwner.PERFORMANCE
 
+    ID_KEYS = {
+        "id",
+        "span_id",
+        "parent_span",
+        "parent_span_id",
+        "trace",
+        "trace_id",
+        "transaction.id",
+        "transaction_id",
+        "segment.id",
+        "segment_id",
+        "profile.id",
+        "profile_id",
+        "replay.id",
+        "replay_id",
+    }
+    NUMERIC_KEYS = {"span.duration", "span.self_time"}
+    TIMESTAMP_KEYS = {"timestamp"}
+
     def get(self, request: Request, organization, key: str) -> Response:
         if not features.has(
             "organizations:performance-trace-explorer", organization, actor=request.user
         ):
             return Response(status=404)
 
+        if key in self.NUMERIC_KEYS or key in self.ID_KEYS or key in self.TIMESTAMP_KEYS:
+            return self.paginate(
+                request=request,
+                paginator=SequencePaginator([]),
+            )
+
         try:
             snuba_params, params = self.get_snuba_dataclass(request, organization)
         except NoProjects:
@@ -104,6 +129,11 @@ class OrganizationSpansFieldValuesEndpoint(OrganizationEventsV2EndpointBase):
 
         sentry_sdk.set_tag("query.tag_key", key)
 
+        if options.get("performance.spans-tags-values.search"):
+            user_query = request.GET.get("query")
+        else:
+            user_query = None
+
         max_span_tags = options.get("performance.spans-tags-values.max")
 
         with handle_query_errors():
@@ -111,7 +141,7 @@ class OrganizationSpansFieldValuesEndpoint(OrganizationEventsV2EndpointBase):
                 Dataset.SpansIndexed,
                 params=cast(ParamsType, params),
                 snuba_params=snuba_params,
-                query=None,
+                query=f"{key}:*{user_query}*" if user_query else None,
                 selected_columns=[key, "count()", "min(timestamp)", "max(timestamp)"],
                 orderby="-count()",
                 limit=max_span_tags,
@@ -120,7 +150,6 @@ class OrganizationSpansFieldValuesEndpoint(OrganizationEventsV2EndpointBase):
                     transform_alias_to_input_format=True,
                 ),
             )
-
             results = builder.process_results(builder.run_query(Referrer.API_SPANS_TAG_KEYS.value))
 
         paginator = SequencePaginator(

+ 6 - 0
src/sentry/options/defaults.py

@@ -1768,6 +1768,12 @@ register(
     default=1000,
     flags=FLAG_AUTOMATOR_MODIFIABLE,
 )
+register(
+    "performance.spans-tags-values.search",
+    type=Bool,
+    default=False,
+    flags=FLAG_AUTOMATOR_MODIFIABLE,
+)
 
 # Dynamic Sampling system-wide options
 # Size of the sliding window used for dynamic sampling. It is defaulted to 24 hours.

+ 88 - 1
tests/sentry/api/endpoints/test_organization_spans_fields.py

@@ -3,6 +3,7 @@ from uuid import uuid4
 from django.urls import reverse
 
 from sentry.testutils.cases import APITestCase, BaseSpansTestCase
+from sentry.testutils.helpers import override_options
 from sentry.testutils.helpers.datetime import before_now
 
 
@@ -65,7 +66,7 @@ class OrganizationSpansTagKeyValuesEndpointTest(BaseSpansTestCase, APITestCase):
         super().setUp()
         self.login_as(user=self.user)
 
-    def do_request(self, key: str, features=None, **kwargs):
+    def do_request(self, key: str, query=None, features=None, **kwargs):
         if features is None:
             features = ["organizations:performance-trace-explorer"]
         with self.feature(features):
@@ -74,6 +75,7 @@ class OrganizationSpansTagKeyValuesEndpointTest(BaseSpansTestCase, APITestCase):
                     self.view,
                     kwargs={"organization_id_or_slug": self.organization.slug, "key": key},
                 ),
+                query,
                 format="json",
                 **kwargs,
             )
@@ -134,3 +136,88 @@ class OrganizationSpansTagKeyValuesEndpointTest(BaseSpansTestCase, APITestCase):
                 "lastSeen": timestamp.strftime("%Y-%m-%dT%H:%M:%S+00:00"),
             },
         ]
+
+    def test_tags_keys_autocomplete(self):
+        timestamp = before_now(days=0, minutes=10).replace(microsecond=0)
+        for tag in ["foo", "bar", "baz"]:
+            self.store_segment(
+                self.project.id,
+                uuid4().hex,
+                uuid4().hex,
+                span_id=uuid4().hex[:15],
+                parent_span_id=None,
+                timestamp=timestamp,
+                transaction=tag,
+                duration=100,
+                exclusive_time=100,
+                tags={"tag": tag},
+            )
+
+        with override_options({"performance.spans-tags-values.search": True}):
+            for key in ["tag", "transaction"]:
+                query = {
+                    "project": [self.project.id],
+                    "query": "b",
+                }
+                response = self.do_request(key, query=query)
+                assert response.status_code == 200, response.data
+                assert response.data == [
+                    {
+                        "count": 1,
+                        "key": key,
+                        "value": "bar",
+                        "name": "bar",
+                        "firstSeen": timestamp.strftime("%Y-%m-%dT%H:%M:%S+00:00"),
+                        "lastSeen": timestamp.strftime("%Y-%m-%dT%H:%M:%S+00:00"),
+                    },
+                    {
+                        "count": 1,
+                        "key": key,
+                        "value": "baz",
+                        "name": "baz",
+                        "firstSeen": timestamp.strftime("%Y-%m-%dT%H:%M:%S+00:00"),
+                        "lastSeen": timestamp.strftime("%Y-%m-%dT%H:%M:%S+00:00"),
+                    },
+                ]
+
+    def test_non_string_fields(self):
+        timestamp = before_now(days=0, minutes=10).replace(microsecond=0)
+        for tag in ["foo", "bar", "baz"]:
+            self.store_segment(
+                self.project.id,
+                uuid4().hex,
+                uuid4().hex,
+                span_id=uuid4().hex[:15],
+                parent_span_id=None,
+                timestamp=timestamp,
+                transaction=tag,
+                duration=100,
+                exclusive_time=100,
+                tags={"tag": tag},
+            )
+
+        for key in [
+            "span.duration",
+            "span.self_time",
+            "timestamp",
+            "id",
+            "span_id",
+            "parent_span",
+            "parent_span_id",
+            "trace",
+            "trace_id",
+            "transaction.id",
+            "transaction_id",
+            "segment.id",
+            "segment_id",
+            "profile.id",
+            "profile_id",
+            "replay.id",
+            "replay_id",
+        ]:
+            query = {
+                "project": [self.project.id],
+            }
+            response = self.do_request(key, query=query)
+            assert response.status_code == 200, response.data
+            assert response.data == [], key