Browse Source

feat(explore): Supporting more aggregate functions and params in search. (#80034)

- Added definitions for supported aggregate functions
- Added value type to numeric tags, to be passed as function parameters
in search
- Using `allowAggregateConditions` param, to ignore aggregate function
in sample mode queries

---------

Co-authored-by: Abdullah Khan <abdullahkhan@PG9Y57YDXQ.local>
Abdullah Khan 4 months ago
parent
commit
ae1c16dc6b

+ 12 - 9
static/app/components/performance/spanSearchQueryBuilder.tsx

@@ -11,6 +11,7 @@ import {defined} from 'sentry/utils';
 import {isAggregateField, isMeasurement} from 'sentry/utils/discover/fields';
 import {
   type AggregationKey,
+  ALLOWED_EXPLORE_VISUALIZE_AGGREGATES,
   DEVICE_CLASS_TAG_VALUES,
   FieldKind,
   getFieldDefinition,
@@ -33,10 +34,9 @@ interface SpanSearchQueryBuilderProps {
   onSearch?: (query: string, state: CallbackSearchState) => void;
   placeholder?: string;
   projects?: PageFilters['projects'];
-  supportedAggregates?: AggregationKey[];
 }
 
-const getFunctionTags = (supportedAggregates: AggregationKey[] | undefined) => {
+const getFunctionTags = (supportedAggregates?: AggregationKey[]) => {
   if (!supportedAggregates?.length) {
     return {};
   }
@@ -51,8 +51,8 @@ const getFunctionTags = (supportedAggregates: AggregationKey[] | undefined) => {
   }, {});
 };
 
-const getSpanFieldDefinition = (key: string) => {
-  return getFieldDefinition(key, 'span');
+const getSpanFieldDefinition = (key: string, kind?: FieldKind) => {
+  return getFieldDefinition(key, 'span', kind);
 };
 
 export function SpanSearchQueryBuilder({
@@ -62,15 +62,14 @@ export function SpanSearchQueryBuilder({
   onSearch,
   placeholder,
   projects,
-  supportedAggregates,
 }: SpanSearchQueryBuilderProps) {
   const api = useApi();
   const organization = useOrganization();
   const {selection} = usePageFilters();
 
   const functionTags = useMemo(() => {
-    return getFunctionTags(supportedAggregates);
-  }, [supportedAggregates]);
+    return getFunctionTags();
+  }, []);
 
   const placeholderText = useMemo(() => {
     return placeholder ?? t('Search for spans, users, tags, and more');
@@ -166,9 +165,13 @@ export function EAPSpanSearchQueryBuilder({
 
   const placeholderText = placeholder ?? t('Search for spans, users, tags, and more');
 
+  const functionTags = useMemo(() => {
+    return getFunctionTags(ALLOWED_EXPLORE_VISUALIZE_AGGREGATES);
+  }, []);
+
   const tags = useMemo(() => {
-    return {...numberTags, ...stringTags};
-  }, [numberTags, stringTags]);
+    return {...functionTags, ...numberTags, ...stringTags};
+  }, [numberTags, stringTags, functionTags]);
 
   const filterKeySections = useMemo(() => {
     const predefined = new Set(

+ 2 - 2
static/app/components/searchQueryBuilder/context.tsx

@@ -7,7 +7,7 @@ import type {
 } from 'sentry/components/searchQueryBuilder/types';
 import type {ParseResult} from 'sentry/components/searchSyntax/parser';
 import type {SavedSearchType, Tag, TagCollection} from 'sentry/types/group';
-import type {FieldDefinition} from 'sentry/utils/fields';
+import type {FieldDefinition, FieldKind} from 'sentry/utils/fields';
 
 export interface SearchQueryBuilderContextData {
   disabled: boolean;
@@ -18,7 +18,7 @@ export interface SearchQueryBuilderContextData {
   filterKeySections: FilterKeySection[];
   filterKeys: TagCollection;
   focusOverride: FocusOverride | null;
-  getFieldDefinition: (key: string) => FieldDefinition | null;
+  getFieldDefinition: (key: string, kind?: FieldKind) => FieldDefinition | null;
   getTagValues: (tag: Tag, query: string) => Promise<string[]>;
   handleSearch: (query: string) => void;
   parsedQuery: ParseResult | null;

+ 2 - 1
static/app/components/searchQueryBuilder/tokens/filter/parametersCombobox.tsx

@@ -116,7 +116,8 @@ function useParameterSuggestions({
               columnTypes({
                 key: col.key,
                 valueType:
-                  getFieldDefinition(col.key)?.valueType ?? FieldValueType.STRING,
+                  getFieldDefinition(col.key, col.kind)?.valueType ??
+                  FieldValueType.STRING,
               })
             )
             .map(col => ({value: col.key, label: col.key}));

+ 162 - 15
static/app/utils/fields/index.ts

@@ -820,6 +820,7 @@ export const ALLOWED_EXPLORE_VISUALIZE_AGGREGATES: AggregationKey[] = [
   AggregationKey.MIN,
   AggregationKey.MAX,
   AggregationKey.AVG,
+  AggregationKey.SUM,
   AggregationKey.P50,
   AggregationKey.P75,
   AggregationKey.P90,
@@ -837,11 +838,13 @@ export const SPAN_AGGREGATION_FIELDS: Record<AggregationKey, FieldDefinition> =
       {
         name: 'column',
         kind: 'column',
-        columnTypes: function ({key}): boolean {
-          return ALLOWED_EXPLORE_VISUALIZE_FIELDS.includes(key as SpanIndexedField);
-        },
+        columnTypes: validateForNumericAggregate([
+          FieldValueType.DURATION,
+          FieldValueType.NUMBER,
+          FieldValueType.PERCENTAGE,
+        ]),
         defaultValue: 'span.duration',
-        required: true,
+        required: false,
       },
     ],
   },
@@ -851,9 +854,13 @@ export const SPAN_AGGREGATION_FIELDS: Record<AggregationKey, FieldDefinition> =
       {
         name: 'column',
         kind: 'column',
-        columnTypes: function ({key}): boolean {
-          return ALLOWED_EXPLORE_VISUALIZE_FIELDS.includes(key as SpanIndexedField);
-        },
+        columnTypes: validateForNumericAggregate([
+          FieldValueType.INTEGER,
+          FieldValueType.NUMBER,
+          FieldValueType.DURATION,
+          FieldValueType.DATE,
+          FieldValueType.PERCENTAGE,
+        ]),
         defaultValue: 'span.duration',
         required: true,
       },
@@ -865,23 +872,141 @@ export const SPAN_AGGREGATION_FIELDS: Record<AggregationKey, FieldDefinition> =
       {
         name: 'column',
         kind: 'column',
-        columnTypes: function ({key}): boolean {
-          return ALLOWED_EXPLORE_VISUALIZE_FIELDS.includes(key as SpanIndexedField);
-        },
+        columnTypes: validateForNumericAggregate([
+          FieldValueType.INTEGER,
+          FieldValueType.NUMBER,
+          FieldValueType.DURATION,
+          FieldValueType.DATE,
+          FieldValueType.PERCENTAGE,
+        ]),
         defaultValue: 'span.duration',
         required: true,
       },
     ],
   },
+  [AggregationKey.SUM]: {
+    ...AGGREGATION_FIELDS[AggregationKey.SUM],
+    parameters: [
+      {
+        name: 'column',
+        kind: 'column',
+        columnTypes: validateForNumericAggregate([
+          FieldValueType.DURATION,
+          FieldValueType.NUMBER,
+          FieldValueType.PERCENTAGE,
+        ]),
+        required: true,
+        defaultValue: 'span.duration',
+      },
+    ],
+  },
   [AggregationKey.AVG]: {
     ...AGGREGATION_FIELDS[AggregationKey.AVG],
     parameters: [
       {
         name: 'column',
         kind: 'column',
-        columnTypes: function ({key}): boolean {
-          return ALLOWED_EXPLORE_VISUALIZE_FIELDS.includes(key as SpanIndexedField);
-        },
+        columnTypes: validateForNumericAggregate([
+          FieldValueType.DURATION,
+          FieldValueType.NUMBER,
+          FieldValueType.PERCENTAGE,
+        ]),
+        defaultValue: 'span.duration',
+        required: true,
+      },
+    ],
+  },
+  [AggregationKey.P50]: {
+    ...AGGREGATION_FIELDS[AggregationKey.P50],
+    parameters: [
+      {
+        name: 'column',
+        kind: 'column',
+        columnTypes: validateForNumericAggregate([
+          FieldValueType.DURATION,
+          FieldValueType.NUMBER,
+          FieldValueType.PERCENTAGE,
+        ]),
+        defaultValue: 'span.duration',
+        required: true,
+      },
+    ],
+  },
+  [AggregationKey.P75]: {
+    ...AGGREGATION_FIELDS[AggregationKey.P75],
+    parameters: [
+      {
+        name: 'column',
+        kind: 'column',
+        columnTypes: validateForNumericAggregate([
+          FieldValueType.DURATION,
+          FieldValueType.NUMBER,
+          FieldValueType.PERCENTAGE,
+        ]),
+        defaultValue: 'span.duration',
+        required: true,
+      },
+    ],
+  },
+  [AggregationKey.P90]: {
+    ...AGGREGATION_FIELDS[AggregationKey.P90],
+    parameters: [
+      {
+        name: 'column',
+        kind: 'column',
+        columnTypes: validateForNumericAggregate([
+          FieldValueType.DURATION,
+          FieldValueType.NUMBER,
+          FieldValueType.PERCENTAGE,
+        ]),
+        defaultValue: 'span.duration',
+        required: true,
+      },
+    ],
+  },
+  [AggregationKey.P95]: {
+    ...AGGREGATION_FIELDS[AggregationKey.P95],
+    parameters: [
+      {
+        name: 'column',
+        kind: 'column',
+        columnTypes: validateForNumericAggregate([
+          FieldValueType.DURATION,
+          FieldValueType.NUMBER,
+          FieldValueType.PERCENTAGE,
+        ]),
+        defaultValue: 'span.duration',
+        required: true,
+      },
+    ],
+  },
+  [AggregationKey.P99]: {
+    ...AGGREGATION_FIELDS[AggregationKey.P99],
+    parameters: [
+      {
+        name: 'column',
+        kind: 'column',
+        columnTypes: validateForNumericAggregate([
+          FieldValueType.DURATION,
+          FieldValueType.NUMBER,
+          FieldValueType.PERCENTAGE,
+        ]),
+        defaultValue: 'span.duration',
+        required: true,
+      },
+    ],
+  },
+  [AggregationKey.P100]: {
+    ...AGGREGATION_FIELDS[AggregationKey.P100],
+    parameters: [
+      {
+        name: 'column',
+        kind: 'column',
+        columnTypes: validateForNumericAggregate([
+          FieldValueType.DURATION,
+          FieldValueType.NUMBER,
+          FieldValueType.PERCENTAGE,
+        ]),
         defaultValue: 'span.duration',
         required: true,
       },
@@ -2263,7 +2388,8 @@ const FEEDBACK_FIELD_DEFINITIONS: Record<FeedbackFieldKey, FieldDefinition> = {
 
 export const getFieldDefinition = (
   key: string,
-  type: 'event' | 'replay' | 'replay_click' | 'feedback' | 'span' = 'event'
+  type: 'event' | 'replay' | 'replay_click' | 'feedback' | 'span' = 'event',
+  kind?: FieldKind
 ): FieldDefinition | null => {
   switch (type) {
     case 'replay':
@@ -2286,7 +2412,28 @@ export const getFieldDefinition = (
       }
       return null;
     case 'span':
-      return SPAN_FIELD_DEFINITIONS[key] ?? null;
+      if (SPAN_FIELD_DEFINITIONS[key]) {
+        return SPAN_FIELD_DEFINITIONS[key];
+      }
+
+      // In EAP we have numeric tags that can be passed as parameters to
+      // aggregate functions. We assign value type based on kind, so that we can filter
+      // on them when suggesting function parameters.
+      if (kind === FieldKind.MEASUREMENT) {
+        return {
+          kind: FieldKind.FIELD,
+          valueType: FieldValueType.NUMBER,
+        };
+      }
+
+      if (kind === FieldKind.TAG) {
+        return {
+          kind: FieldKind.FIELD,
+          valueType: FieldValueType.STRING,
+        };
+      }
+
+      return null;
     case 'event':
     default:
       return EVENT_FIELD_DEFINITIONS[key] ?? null;

+ 1 - 10
static/app/views/explore/content.tsx

@@ -1,4 +1,4 @@
-import {useCallback, useMemo} from 'react';
+import {useCallback} from 'react';
 import styled from '@emotion/styled';
 import type {Location} from 'history';
 
@@ -19,7 +19,6 @@ import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
 import {DiscoverDatasets} from 'sentry/utils/discover/types';
-import {ALLOWED_EXPLORE_VISUALIZE_AGGREGATES} from 'sentry/utils/fields';
 import {useLocation} from 'sentry/utils/useLocation';
 import {useNavigate} from 'sentry/utils/useNavigate';
 import useOrganization from 'sentry/utils/useOrganization';
@@ -30,7 +29,6 @@ import {
   useSpanTags,
 } from 'sentry/views/explore/contexts/spanTagsContext';
 import {useDataset} from 'sentry/views/explore/hooks/useDataset';
-import {useResultMode} from 'sentry/views/explore/hooks/useResultsMode';
 import {useUserQuery} from 'sentry/views/explore/hooks/useUserQuery';
 import {ExploreTables} from 'sentry/views/explore/tables';
 import {ExploreToolbar} from 'sentry/views/explore/toolbar';
@@ -45,15 +43,10 @@ function ExploreContentImpl({}: ExploreContentProps) {
   const organization = useOrganization();
   const {selection} = usePageFilters();
   const [dataset] = useDataset();
-  const [resultsMode] = useResultMode();
 
   const numberTags = useSpanTags('number');
   const stringTags = useSpanTags('string');
 
-  const supportedAggregates = useMemo(() => {
-    return resultsMode === 'aggregate' ? ALLOWED_EXPLORE_VISUALIZE_AGGREGATES : [];
-  }, [resultsMode]);
-
   const [userQuery, setUserQuery] = useUserQuery();
 
   const toolbarExtras = organization.features.includes('visibility-explore-dataset')
@@ -96,7 +89,6 @@ function ExploreContentImpl({}: ExploreContentProps) {
               </StyledPageFilterBar>
               {dataset === DiscoverDatasets.SPANS_INDEXED ? (
                 <SpanSearchQueryBuilder
-                  supportedAggregates={supportedAggregates}
                   projects={selection.projects}
                   initialQuery={userQuery}
                   onSearch={setUserQuery}
@@ -104,7 +96,6 @@ function ExploreContentImpl({}: ExploreContentProps) {
                 />
               ) : (
                 <EAPSpanSearchQueryBuilder
-                  supportedAggregates={supportedAggregates}
                   projects={selection.projects}
                   initialQuery={userQuery}
                   onSearch={setUserQuery}

+ 1 - 0
static/app/views/explore/tables/spansTable.tsx

@@ -67,6 +67,7 @@ export function SpansTable({}: SpansTableProps) {
     eventView,
     initialData: [],
     referrer: 'api.explore.spans-samples-table',
+    allowAggregateConditions: false,
   });
 
   const {tableStyles} = useTableStyles({

+ 13 - 0
static/app/views/insights/common/queries/useSpansQuery.tsx

@@ -24,8 +24,10 @@ export function useSpansQuery<T = any[]>({
   limit,
   enabled,
   referrer = 'use-spans-query',
+  allowAggregateConditions,
   cursor,
 }: {
+  allowAggregateConditions?: boolean;
   cursor?: string;
   enabled?: boolean;
   eventView?: EventView;
@@ -50,6 +52,7 @@ export function useSpansQuery<T = any[]>({
       enabled: (enabled || enabled === undefined) && pageFiltersReady,
       referrer,
       cursor,
+      allowAggregateConditions,
     });
 
     TrackResponse(eventView, response);
@@ -134,8 +137,10 @@ export function useWrappedDiscoverQuery<T>({
   limit,
   cursor,
   noPagination,
+  allowAggregateConditions,
 }: {
   eventView: EventView;
+  allowAggregateConditions?: boolean;
   cursor?: string;
   enabled?: boolean;
   initialData?: T;
@@ -146,6 +151,13 @@ export function useWrappedDiscoverQuery<T>({
   const location = useLocation();
   const organization = useOrganization();
   const {isReady: pageFiltersReady} = usePageFilters();
+
+  const queryExtras: Record<string, string> = {};
+
+  if (allowAggregateConditions !== undefined) {
+    queryExtras.allowAggregateConditions = allowAggregateConditions ? '1' : '0';
+  }
+
   const result = useDiscoverQuery({
     eventView,
     orgSlug: organization.slug,
@@ -160,6 +172,7 @@ export function useWrappedDiscoverQuery<T>({
       retryDelay: getRetryDelay,
       staleTime: Infinity,
     },
+    queryExtras,
     noPagination,
   });