import {useState} from 'react'; import styled from '@emotion/styled'; import Alert from 'sentry/components/alert'; import Button from 'sentry/components/button'; import Link from 'sentry/components/links/link'; import {IconClose} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import type EventView from 'sentry/utils/discover/eventView'; import { Aggregation, AGGREGATIONS, explodeFieldString, } from 'sentry/utils/discover/fields'; import { errorFieldConfig, transactionFieldConfig, } from 'sentry/views/alerts/rules/metric/constants'; import {getQueryDatasource} from 'sentry/views/alerts/utils'; /** * Discover query supports more features than alert rules * To create an alert rule from a discover query, some parameters need to be adjusted */ type IncompatibleQueryProperties = { /** * Must have zero or one environments */ hasEnvironmentError: boolean; /** * event.type must be error or transaction */ hasEventTypeError: boolean; /** * Must have exactly one project selected and not -1 (all projects) */ hasProjectError: boolean; hasYAxisError: boolean; }; function incompatibleYAxis(eventView: EventView): boolean { const column = explodeFieldString(eventView.getYAxis()); if ( column.kind === 'field' || column.kind === 'equation' || column.kind === 'calculatedField' ) { return true; } const eventTypeMatch = eventView.query.match(/event\.type:(transaction|error)/); if (!eventTypeMatch) { return false; } const dataset = eventTypeMatch[1]; const yAxisConfig = dataset === 'error' ? errorFieldConfig : transactionFieldConfig; const invalidFunction = !yAxisConfig.aggregations.includes(column.function[0]); // Allow empty parameters, allow all numeric parameters - eg. apdex(300) const aggregation: Aggregation | undefined = AGGREGATIONS[column.function[0]]; if (!aggregation) { return false; } const isNumericParameter = aggregation.parameters.some( param => param.kind === 'value' && param.dataType === 'number' ); // There are other measurements possible, but for the time being, only allow alerting // on the predefined set of measurements for alerts. const allowedParameters = [ '', ...yAxisConfig.fields, ...(yAxisConfig.measurementKeys ?? []), ]; const invalidParameter = !isNumericParameter && !allowedParameters.includes(column.function[1]); return invalidFunction || invalidParameter; } export function checkMetricAlertCompatiablity( eventView: EventView ): IncompatibleQueryProperties { // Must have exactly one project selected and not -1 (all projects) const hasProjectError = eventView.project.length !== 1 || eventView.project[0] === -1; // Must have one or zero environments const hasEnvironmentError = eventView.environment.length > 1; // Must have event.type of error or transaction const hasEventTypeError = getQueryDatasource(eventView.query) === null; // yAxis must be a function and enabled on alerts const hasYAxisError = incompatibleYAxis(eventView); return { hasProjectError, hasEnvironmentError, hasEventTypeError, hasYAxisError, }; } interface IncompatibleAlertQueryProps { eventView: EventView; orgSlug: string; } /** * Displays messages to the user on what needs to change in their query */ export function IncompatibleAlertQuery(props: IncompatibleAlertQueryProps) { const [isOpen, setIsOpen] = useState(true); const incompatibleQuery = checkMetricAlertCompatiablity(props.eventView); const totalErrors = Object.values(incompatibleQuery).filter(val => val).length; if (!totalErrors || !isOpen) { return null; } const eventTypeError = props.eventView.clone(); eventTypeError.query += ' event.type:error'; const eventTypeTransaction = props.eventView.clone(); eventTypeTransaction.query += ' event.type:transaction'; const eventTypeDefault = props.eventView.clone(); eventTypeDefault.query += ' event.type:default'; const eventTypeErrorDefault = props.eventView.clone(); eventTypeErrorDefault.query += ' event.type:error or event.type:default'; const pathname = `/organizations/${props.orgSlug}/discover/results/`; const eventTypeLinks = { error: ( ), default: ( ), transaction: ( ), errorDefault: ( ), }; return ( } aria-label={t('Close')} size="zero" onClick={() => setIsOpen(false)} borderless /> } > {t('The following problems occurred while creating your alert:')} {incompatibleQuery.hasProjectError &&
  • {t('No project was selected')}
  • } {incompatibleQuery.hasEnvironmentError && (
  • {t('Too many environments were selected')}
  • )} {incompatibleQuery.hasEventTypeError && (
  • {tct("An event type wasn't selected", eventTypeLinks)}
  • )} {incompatibleQuery.hasYAxisError && (
  • {tct('An alert can’t use the metric [yAxis] just yet.', { yAxis: {props.eventView.getYAxis()}, })}
  • )}
    ); } const StyledAlert = styled(Alert)` color: ${p => p.theme.textColor}; `; const StyledUnorderedList = styled('ul')` margin-bottom: 0; `; const StyledCode = styled('code')` background-color: transparent; padding: 0; `;