Browse Source

feat(arithmetic): adding support for count_if (#26927)

- This adds count_if as a column to the discover column editor
- Most of these changes were from adding a 4th item to the function
  array, and having to update everywhere that references it
- count_if is hidden behind the arithmetic feature flag
William Mak 3 years ago
parent
commit
24eea1604a

+ 65 - 3
static/app/utils/discover/fields.tsx

@@ -1,4 +1,4 @@
-import {LightWeightOrganization} from 'app/types';
+import {LightWeightOrganization, SelectValue} from 'app/types';
 import {assert} from 'app/types/utils';
 
 export type Sort = {
@@ -46,6 +46,14 @@ export type AggregateParameter =
       defaultValue?: string;
       required: boolean;
       placeholder?: string;
+    }
+  | {
+      kind: 'dropdown';
+      options: SelectValue<string>[];
+      dataType: string;
+      defaultValue?: string;
+      required: boolean;
+      placeholder?: string;
     };
 
 export type AggregationRefinement = string | undefined;
@@ -65,7 +73,7 @@ export type QueryFieldValue =
     }
   | {
       kind: 'function';
-      function: [AggregationKey, string, AggregationRefinement];
+      function: [AggregationKey, string, AggregationRefinement, AggregationRefinement];
     };
 
 // Column is just an alias of a Query value
@@ -73,7 +81,34 @@ export type Column = QueryFieldValue;
 
 export type Alignments = 'left' | 'right';
 
-// Refer to src/sentry/api/event_search.py
+const CONDITIONS_ARGUMENTS: SelectValue<string>[] = [
+  {
+    label: 'equal =',
+    value: 'equals',
+  },
+  {
+    label: 'not equal !=',
+    value: 'notEquals',
+  },
+  {
+    label: 'less <',
+    value: 'less',
+  },
+  {
+    label: 'greater >',
+    value: 'greater',
+  },
+  {
+    label: 'less or equals <=',
+    value: 'lessOrEquals',
+  },
+  {
+    label: 'greater or equals >=',
+    value: 'greaterOrEquals',
+  },
+];
+
+// Refer to src/sentry/search/events/fields.py
 export const AGGREGATIONS = {
   count: {
     parameters: [],
@@ -351,6 +386,32 @@ export const AGGREGATIONS = {
     isSortable: true,
     multiPlotType: 'area',
   },
+  count_if: {
+    parameters: [
+      {
+        kind: 'column',
+        columnTypes: ['string', 'duration'],
+        defaultValue: 'transaction.duration',
+        required: true,
+      },
+      {
+        kind: 'dropdown',
+        options: CONDITIONS_ARGUMENTS,
+        dataType: 'string',
+        defaultValue: CONDITIONS_ARGUMENTS[0].value,
+        required: true,
+      },
+      {
+        kind: 'value',
+        dataType: 'string',
+        defaultValue: '300',
+        required: true,
+      },
+    ],
+    outputType: 'number',
+    isSortable: true,
+    multiPlotType: 'area',
+  },
 } as const;
 
 // TPM and TPS are aliases that are only used in Performance
@@ -781,6 +842,7 @@ export function explodeFieldString(field: string): Column {
         results.name as AggregationKey,
         results.arguments[0] ?? '',
         results.arguments[1] as AggregationRefinement,
+        results.arguments[2] as AggregationRefinement,
       ],
     };
   }

+ 65 - 23
static/app/views/eventsV2/table/queryField.tsx

@@ -41,6 +41,14 @@ type ParameterDescription =
       value: FieldValue | null;
       options: FieldValueOption[];
       required: boolean;
+    }
+  | {
+      kind: 'dropdown';
+      value: string;
+      options: SelectValue<string>[];
+      dataType: string;
+      required: boolean;
+      placeholder?: string;
     };
 
 type Props = {
@@ -107,7 +115,7 @@ class QueryField extends React.Component<Props> {
         if (current.kind === 'field') {
           fieldValue = {
             kind: 'function',
-            function: [value.meta.name as AggregationKey, '', undefined],
+            function: [value.meta.name as AggregationKey, '', undefined, undefined],
           };
         } else if (current.kind === 'function') {
           fieldValue = {
@@ -116,6 +124,7 @@ class QueryField extends React.Component<Props> {
               value.meta.name as AggregationKey,
               current.function[1],
               current.function[2],
+              current.function[3],
             ],
           };
         }
@@ -147,14 +156,14 @@ class QueryField extends React.Component<Props> {
             fieldValue.function[i + 1] = param.defaultValue || '';
             fieldValue.function[i + 2] = undefined;
           }
-        } else if (param.kind === 'value') {
+        } else {
           fieldValue.function[i + 1] = param.defaultValue || '';
         }
       });
 
       if (fieldValue.kind === 'function') {
         if (value.meta.parameters.length === 0) {
-          fieldValue.function = [fieldValue.function[0], '', undefined];
+          fieldValue.function = [fieldValue.function[0], '', undefined, undefined];
         } else if (value.meta.parameters.length === 1) {
           fieldValue.function[2] = undefined;
         }
@@ -180,20 +189,24 @@ class QueryField extends React.Component<Props> {
     this.triggerChange(newColumn);
   };
 
-  handleScalarParameterChange = (value: string) => {
-    const newColumn = cloneDeep(this.props.fieldValue);
-    if (newColumn.kind === 'function') {
-      newColumn.function[1] = value;
-    }
-    this.triggerChange(newColumn);
+  handleDropdownParameterChange = (index: number) => {
+    return (value: SelectValue<string>) => {
+      const newColumn = cloneDeep(this.props.fieldValue);
+      if (newColumn.kind === 'function') {
+        newColumn.function[index] = value.value;
+      }
+      this.triggerChange(newColumn);
+    };
   };
 
-  handleRefinementChange = (value: string) => {
-    const newColumn = cloneDeep(this.props.fieldValue);
-    if (newColumn.kind === 'function') {
-      newColumn.function[2] = value;
-    }
-    this.triggerChange(newColumn);
+  handleScalarParameterChange = (index: number) => {
+    return (value: string) => {
+      const newColumn = cloneDeep(this.props.fieldValue);
+      if (newColumn.kind === 'function') {
+        newColumn.function[index] = value;
+      }
+      this.triggerChange(newColumn);
+    };
   };
 
   triggerChange(fieldValue: QueryFieldValue) {
@@ -291,6 +304,17 @@ class QueryField extends React.Component<Props> {
                   validateColumnTypes(param.columnTypes as ValidateColumnTypes, value)
               ),
             };
+          } else if (param.kind === 'dropdown') {
+            return {
+              kind: 'dropdown',
+              options: param.options,
+              dataType: param.dataType,
+              required: param.required,
+              value:
+                (fieldValue.kind === 'function' && fieldValue.function[index + 1]) ||
+                param.defaultValue ||
+                '',
+            };
           }
 
           return {
@@ -353,13 +377,10 @@ class QueryField extends React.Component<Props> {
         );
       }
       if (descriptor.kind === 'value') {
-        const handler =
-          index === 0 ? this.handleScalarParameterChange : this.handleRefinementChange;
-
         const inputProps = {
           required: descriptor.required,
           value: descriptor.value,
-          onUpdate: handler,
+          onUpdate: this.handleScalarParameterChange(index + 1),
           placeholder: descriptor.placeholder,
           disabled,
         };
@@ -397,6 +418,20 @@ class QueryField extends React.Component<Props> {
             );
         }
       }
+      if (descriptor.kind === 'dropdown') {
+        return (
+          <SelectControl
+            name="dropdown"
+            placeholder={t('Select value')}
+            options={descriptor.options}
+            value={descriptor.value}
+            required={descriptor.required}
+            onChange={this.handleDropdownParameterChange(index + 1)}
+            inFieldLabel={inFieldLabels ? t('Parameter: ') : undefined}
+            disabled={disabled}
+          />
+        );
+      }
       throw new Error(`Unknown parameter type encountered for ${this.props.fieldValue}`);
     });
 
@@ -502,7 +537,7 @@ class QueryField extends React.Component<Props> {
 
     if (fieldValue.kind === FieldValueKind.EQUATION) {
       return (
-        <Container className={className} gridColumns={1}>
+        <Container className={className} gridColumns={1} tripleLayout={false}>
           <ArithmeticInput
             name="arithmetic"
             key="parameter:text"
@@ -516,10 +551,14 @@ class QueryField extends React.Component<Props> {
       );
     }
 
+    // if there's more than 2 parameters, set gridColumns to 2 so they go onto the next line instead
+    const containerColumns =
+      parameters.length > 2 ? 2 : gridColumns ? gridColumns : parameters.length + 1;
     return (
       <Container
         className={className}
-        gridColumns={gridColumns ? gridColumns : parameters.length + 1}
+        gridColumns={containerColumns}
+        tripleLayout={gridColumns === 3 && parameters.length > 2}
       >
         {!hidePrimarySelector && (
           <SelectControl
@@ -558,9 +597,12 @@ function validateColumnTypes(
   return columnTypes.includes(input.meta.dataType);
 }
 
-const Container = styled('div')<{gridColumns: number}>`
+const Container = styled('div')<{gridColumns: number; tripleLayout: boolean}>`
   display: grid;
-  grid-template-columns: repeat(${p => p.gridColumns}, 1fr);
+  ${p =>
+    p.tripleLayout
+      ? `grid-template-columns: 1fr 2fr;`
+      : `grid-template-columns: repeat(${p.gridColumns}, 1fr);`}
   grid-column-gap: ${space(1)};
   align-items: center;
 

+ 1 - 1
static/app/views/eventsV2/table/tableView.tsx

@@ -410,7 +410,7 @@ class TableView extends React.Component<TableViewProps> {
           // Drilldown into each distinct value and get a count() for each value.
           nextView = getExpandedResults(nextView, {}, dataRow).withNewColumn({
             kind: 'function',
-            function: ['count', '', undefined],
+            function: ['count', '', undefined, undefined],
           });
 
           browserHistory.push(nextView.getResultsViewUrlTarget(organization.slug));

+ 4 - 0
static/app/views/eventsV2/utils.tsx

@@ -444,6 +444,10 @@ export function generateFieldOptions({
     fieldKeys = fieldKeys.filter(item => !TRACING_FIELDS.includes(item));
     functions = functions.filter(item => !TRACING_FIELDS.includes(item));
   }
+  // Feature flagged by arithmetic for now
+  if (!organization.features.includes('discover-arithmetic')) {
+    functions = functions.filter(item => item !== 'count_if');
+  }
   const fieldOptions: Record<string, SelectValue<FieldValue>> = {};
 
   // Index items by prefixed keys as custom tags can overlap both fields and

+ 8 - 5
static/app/views/performance/landing/utils.tsx

@@ -84,21 +84,24 @@ export function getBackendFunction(
 ): Column {
   switch (functionName) {
     case 'p75':
-      return {kind: 'function', function: ['p75', 'transaction.duration', undefined]};
+      return {
+        kind: 'function',
+        function: ['p75', 'transaction.duration', undefined, undefined],
+      };
     case 'tpm':
-      return {kind: 'function', function: ['tpm', '', undefined]};
+      return {kind: 'function', function: ['tpm', '', undefined, undefined]};
     case 'failure_rate':
-      return {kind: 'function', function: ['failure_rate', '', undefined]};
+      return {kind: 'function', function: ['failure_rate', '', undefined, undefined]};
     case 'apdex':
       if (organization.features.includes('project-transaction-threshold')) {
         return {
           kind: 'function',
-          function: ['apdex' as AggregationKey, '', undefined],
+          function: ['apdex' as AggregationKey, '', undefined, undefined],
         };
       }
       return {
         kind: 'function',
-        function: ['apdex', `${organization.apdexThreshold}`, undefined],
+        function: ['apdex', `${organization.apdexThreshold}`, undefined, undefined],
       };
     default:
       throw new Error(`Unsupported backend function: ${functionName}`);

+ 12 - 12
static/app/views/performance/transactionSummary/index.tsx

@@ -151,23 +151,23 @@ class TransactionSummary extends Component<Props, State> {
     const totalsColumns: QueryFieldValue[] = [
       {
         kind: 'function',
-        function: ['p95', '', undefined],
+        function: ['p95', '', undefined, undefined],
       },
       {
         kind: 'function',
-        function: ['count', '', undefined],
+        function: ['count', '', undefined, undefined],
       },
       {
         kind: 'function',
-        function: ['count_unique', 'user', undefined],
+        function: ['count_unique', 'user', undefined, undefined],
       },
       {
         kind: 'function',
-        function: ['failure_rate', '', undefined],
+        function: ['failure_rate', '', undefined, undefined],
       },
       {
         kind: 'function',
-        function: ['tpm', '', undefined],
+        function: ['tpm', '', undefined, undefined],
       },
     ];
 
@@ -177,29 +177,29 @@ class TransactionSummary extends Component<Props, State> {
       ? [
           {
             kind: 'function',
-            function: ['count_miserable', 'user', undefined],
+            function: ['count_miserable', 'user', undefined, undefined],
           },
           {
             kind: 'function',
-            function: ['user_misery', '', undefined],
+            function: ['user_misery', '', undefined, undefined],
           },
           {
             kind: 'function',
-            function: ['apdex', '', undefined],
+            function: ['apdex', '', undefined, undefined],
           },
         ]
       : [
           {
             kind: 'function',
-            function: ['count_miserable', 'user', threshold],
+            function: ['count_miserable', 'user', threshold, undefined],
           },
           {
             kind: 'function',
-            function: ['user_misery', threshold, undefined],
+            function: ['user_misery', threshold, undefined, undefined],
           },
           {
             kind: 'function',
-            function: ['apdex', threshold, undefined],
+            function: ['apdex', threshold, undefined, undefined],
           },
         ];
 
@@ -210,7 +210,7 @@ class TransactionSummary extends Component<Props, State> {
         vital =>
           ({
             kind: 'function',
-            function: ['percentile', vital, VITAL_PERCENTILE.toString()],
+            function: ['percentile', vital, VITAL_PERCENTILE.toString(), undefined],
           } as Column)
       ),
     ]);

+ 1 - 1
static/app/views/performance/transactionSummary/statusBreakdown.tsx

@@ -26,7 +26,7 @@ type Props = {
 function StatusBreakdown({eventView, location, organization}: Props) {
   const breakdownView = eventView
     .withColumns([
-      {kind: 'function', function: ['count', '', '']},
+      {kind: 'function', function: ['count', '', '', undefined]},
       {kind: 'field', field: 'transaction.status'},
     ])
     .withSorts([{kind: 'desc', field: 'count'}]);

+ 5 - 2
static/app/views/performance/transactionSummary/transactionVitals/vitalCard.tsx

@@ -155,8 +155,11 @@ class VitalCard extends Component<Props, State> {
     const newEventView = eventView
       .withColumns([
         {kind: 'field', field: 'transaction'},
-        {kind: 'function', function: ['percentile', column, PERCENTILE.toString()]},
-        {kind: 'function', function: ['count', '', '']},
+        {
+          kind: 'function',
+          function: ['percentile', column, PERCENTILE.toString(), undefined],
+        },
+        {kind: 'function', function: ['count', '', '', undefined]},
       ])
       .withSorts([
         {

+ 1 - 1
static/app/views/performance/trends/utils.tsx

@@ -169,7 +169,7 @@ export function generateTrendFunctionAsString(
 ): string {
   return generateFieldAsString({
     kind: 'function',
-    function: [trendFunction as AggregationKey, trendParameter, undefined],
+    function: [trendFunction as AggregationKey, trendParameter, undefined, undefined],
   });
 }
 

+ 2 - 2
tests/js/spec/utils/discover/fields.spec.jsx

@@ -180,7 +180,7 @@ describe('explodeField', function () {
     // has aggregation
     expect(explodeField({field: 'count(foobar)', width: 123})).toEqual({
       kind: 'function',
-      function: ['count', 'foobar', undefined],
+      function: ['count', 'foobar', undefined, undefined],
     });
 
     // custom tag
@@ -192,7 +192,7 @@ describe('explodeField', function () {
     // custom tag with aggregation
     expect(explodeField({field: 'count(foo.bar.is-Enterprise_42)', width: 123})).toEqual({
       kind: 'function',
-      function: ['count', 'foo.bar.is-Enterprise_42', undefined],
+      function: ['count', 'foo.bar.is-Enterprise_42', undefined, undefined],
     });
   });
 });

Some files were not shown because too many files changed in this diff