Просмотр исходного кода

feat(vitals): Support measurements in metric alerts (#21498)

Co-authored-by: Dan Fuller <dfuller@sentry.io>
Alberto Leal 4 лет назад
Родитель
Сommit
32933efb11

+ 7 - 2
src/sentry/incidents/logic.py

@@ -50,7 +50,7 @@ from sentry.snuba.subscriptions import (
 from sentry.snuba.tasks import build_snuba_filter
 from sentry.utils.compat import zip
 from sentry.utils.dates import to_timestamp
-from sentry.utils.snuba import bulk_raw_query, SnubaQueryParams, SnubaTSResult
+from sentry.utils.snuba import bulk_raw_query, is_measurement, SnubaQueryParams, SnubaTSResult
 from sentry.shared_integrations.exceptions import DuplicateDisplayNameError
 
 # We can return an incident as "windowed" which returns a range of points around the start of the incident
@@ -1350,7 +1350,12 @@ def get_column_from_aggregate(aggregate):
 
 def check_aggregate_column_support(aggregate):
     column = get_column_from_aggregate(aggregate)
-    return column is None or column in SUPPORTED_COLUMNS or column in TRANSLATABLE_COLUMNS
+    return (
+        column is None
+        or is_measurement(column)
+        or column in SUPPORTED_COLUMNS
+        or column in TRANSLATABLE_COLUMNS
+    )
 
 
 def translate_aggregate_field(aggregate, reverse=False):

+ 3 - 0
src/sentry/static/sentry/app/views/settings/incidentRules/constants.tsx

@@ -6,6 +6,7 @@ import {
 } from 'app/views/settings/incidentRules/types';
 import EventView from 'app/utils/discover/eventView';
 import {AggregationKey, LooseFieldKey} from 'app/utils/discover/fields';
+import {WEB_VITAL_DETAILS} from 'app/views/performance/transactionVitals/constants';
 
 export const DEFAULT_AGGREGATE = 'count()';
 
@@ -17,6 +18,7 @@ export const DATASET_EVENT_TYPE_FILTERS = {
 type OptionConfig = {
   aggregations: AggregationKey[];
   fields: LooseFieldKey[];
+  measurementKeys?: string[];
 };
 
 /**
@@ -44,6 +46,7 @@ export const transactionFieldConfig: OptionConfig = {
     'p100',
   ],
   fields: ['transaction.duration'],
+  measurementKeys: Object.keys(WEB_VITAL_DETAILS),
 };
 
 export function createDefaultTrigger(label: 'critical' | 'warning'): Trigger {

+ 12 - 7
src/sentry/static/sentry/app/views/settings/incidentRules/metricField.tsx

@@ -27,12 +27,13 @@ type Props = Omit<FormField['props'], 'children' | 'help'> & {
   organization: Organization;
 };
 
-const getFieldOptionConfig = (dataset: Dataset) => {
+const getFieldOptionConfig = (dataset: Dataset, organization: Organization) => {
   const config = dataset === Dataset.ERRORS ? errorFieldConfig : transactionFieldConfig;
 
   const aggregations = Object.fromEntries(
     config.aggregations.map(key => [key, AGGREGATIONS[key]])
   );
+
   const fields = Object.fromEntries(
     config.fields.map(key => {
       // XXX(epurkhiser): Temporary hack while we handle the translation of user ->
@@ -45,7 +46,11 @@ const getFieldOptionConfig = (dataset: Dataset) => {
     })
   );
 
-  return {aggregations, fields};
+  const measurementKeys = organization.features.includes('measurements')
+    ? config.measurementKeys
+    : undefined;
+
+  return {aggregations, fields, measurementKeys};
 };
 
 const help = ({name, model}: {name: string; model: FormModel}) => {
@@ -81,7 +86,7 @@ const MetricField = ({organization, ...props}: Props) => (
     {({onChange, value, model}) => {
       const dataset = model.getValue('dataset');
 
-      const fieldOptionsConfig = getFieldOptionConfig(dataset);
+      const fieldOptionsConfig = getFieldOptionConfig(dataset, organization);
       const fieldOptions = generateFieldOptions({organization, ...fieldOptionsConfig});
       const fieldValue = explodeFieldString(value ?? '');
 
@@ -91,10 +96,10 @@ const MetricField = ({organization, ...props}: Props) => (
           : '';
 
       const selectedField = fieldOptions[fieldKey]?.value;
-      const numParameters =
-        selectedField &&
-        selectedField.kind === FieldValueKind.FUNCTION &&
-        selectedField.meta.parameters.length;
+      const numParameters: number =
+        selectedField?.kind === FieldValueKind.FUNCTION
+          ? selectedField.meta.parameters.length
+          : 0;
 
       return (
         <React.Fragment>

+ 9 - 0
tests/sentry/incidents/endpoints/test_serializers.py

@@ -165,6 +165,15 @@ class TestAlertRuleSerializer(TestCase):
         alert_rule = serializer.save()
         assert alert_rule.snuba_query.aggregate == aggregate
 
+        aggregate = "sum(measurements.fp)"
+        base_params = self.valid_transaction_params.copy()
+        base_params["name"] = "measurement test"
+        base_params["aggregate"] = aggregate
+        serializer = AlertRuleSerializer(context=self.context, data=base_params)
+        assert serializer.is_valid(), serializer.errors
+        alert_rule = serializer.save()
+        assert alert_rule.snuba_query.aggregate == aggregate
+
     def test_alert_rule_resolved_invalid(self):
         self.run_fail_validation_test(
             {"resolve_threshold": 500},