123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 |
- import {css} from '@emotion/react';
- import styled from '@emotion/styled';
- import SelectControl from 'sentry/components/forms/controls/selectControl';
- import type {FormFieldProps} from 'sentry/components/forms/formField';
- import FormField from 'sentry/components/forms/formField';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import type {Organization} from 'sentry/types/organization';
- import type {Project} from 'sentry/types/project';
- import type {QueryFieldValue} from 'sentry/utils/discover/fields';
- import {explodeFieldString, generateFieldAsString} from 'sentry/utils/discover/fields';
- import {hasCustomMetrics} from 'sentry/utils/metrics/features';
- import MriField from 'sentry/views/alerts/rules/metric/mriField';
- import type {Dataset} from 'sentry/views/alerts/rules/metric/types';
- import type {AlertType} from 'sentry/views/alerts/wizard/options';
- import {
- AlertWizardAlertNames,
- AlertWizardRuleTemplates,
- } from 'sentry/views/alerts/wizard/options';
- import {QueryField} from 'sentry/views/discover/table/queryField';
- import {FieldValueKind} from 'sentry/views/discover/table/types';
- import {generateFieldOptions} from 'sentry/views/discover/utils';
- import {getFieldOptionConfig} from './metricField';
- type MenuOption = {label: string; value: AlertType};
- type GroupedMenuOption = {label: string; options: Array<MenuOption>};
- type Props = Omit<FormFieldProps, 'children'> & {
- organization: Organization;
- project: Project;
- alertType?: AlertType;
- /**
- * Optionally set a width for each column of selector
- */
- columnWidth?: number;
- inFieldLabels?: boolean;
- };
- export default function WizardField({
- organization,
- columnWidth,
- inFieldLabels,
- alertType,
- project,
- ...fieldProps
- }: Props) {
- const menuOptions: GroupedMenuOption[] = [
- {
- label: t('ERRORS'),
- options: [
- {
- label: AlertWizardAlertNames.num_errors,
- value: 'num_errors',
- },
- {
- label: AlertWizardAlertNames.users_experiencing_errors,
- value: 'users_experiencing_errors',
- },
- ],
- },
- ...((organization.features.includes('crash-rate-alerts')
- ? [
- {
- label: t('SESSIONS'),
- options: [
- {
- label: AlertWizardAlertNames.crash_free_sessions,
- value: 'crash_free_sessions',
- },
- {
- label: AlertWizardAlertNames.crash_free_users,
- value: 'crash_free_users',
- },
- ],
- },
- ]
- : []) as GroupedMenuOption[]),
- {
- label: t('PERFORMANCE'),
- options: [
- {
- label: AlertWizardAlertNames.throughput,
- value: 'throughput',
- },
- {
- label: AlertWizardAlertNames.trans_duration,
- value: 'trans_duration',
- },
- {
- label: AlertWizardAlertNames.apdex,
- value: 'apdex',
- },
- {
- label: AlertWizardAlertNames.failure_rate,
- value: 'failure_rate',
- },
- {
- label: AlertWizardAlertNames.lcp,
- value: 'lcp',
- },
- {
- label: AlertWizardAlertNames.fid,
- value: 'fid',
- },
- {
- label: AlertWizardAlertNames.cls,
- value: 'cls',
- },
- ...(hasCustomMetrics(organization)
- ? [
- {
- label: AlertWizardAlertNames.custom_transactions,
- value: 'custom_transactions' as const,
- },
- ]
- : []),
- ],
- },
- {
- label: hasCustomMetrics(organization) ? t('METRICS') : t('CUSTOM'),
- options: [
- hasCustomMetrics(organization)
- ? {
- label: AlertWizardAlertNames.custom_metrics,
- value: 'custom_metrics',
- }
- : {
- label: AlertWizardAlertNames.custom_transactions,
- value: 'custom_transactions',
- },
- ],
- },
- ];
- return (
- <FormField {...fieldProps}>
- {({onChange, model, disabled}) => {
- const aggregate = model.getValue('aggregate');
- const dataset: Dataset = model.getValue('dataset');
- const selectedTemplate: AlertType = alertType || 'custom_metrics';
- const {fieldOptionsConfig, hidePrimarySelector, hideParameterSelector} =
- getFieldOptionConfig({
- dataset: dataset as Dataset,
- alertType,
- });
- const fieldOptions = generateFieldOptions({organization, ...fieldOptionsConfig});
- const fieldValue = getFieldValue(aggregate ?? '', model);
- const fieldKey =
- fieldValue?.kind === FieldValueKind.FUNCTION
- ? `function:${fieldValue.function[0]}`
- : '';
- const selectedField = fieldOptions[fieldKey]?.value;
- const numParameters: number =
- selectedField?.kind === FieldValueKind.FUNCTION
- ? selectedField.meta.parameters.length
- : 0;
- const gridColumns =
- 1 +
- numParameters -
- (hideParameterSelector ? 1 : 0) -
- (hidePrimarySelector ? 1 : 0);
- return (
- <Container hideGap={gridColumns < 1}>
- <SelectControl
- value={selectedTemplate}
- options={menuOptions}
- disabled={disabled}
- onChange={(option: MenuOption) => {
- const template = AlertWizardRuleTemplates[option.value];
- model.setValue('aggregate', template.aggregate);
- model.setValue('dataset', template.dataset);
- model.setValue('eventTypes', [template.eventTypes]);
- // Keep alertType last
- model.setValue('alertType', option.value);
- }}
- />
- {hasCustomMetrics(organization) && alertType === 'custom_metrics' ? (
- <MriField
- project={project}
- aggregate={aggregate}
- onChange={newAggregate => onChange(newAggregate, {})}
- />
- ) : (
- <StyledQueryField
- filterPrimaryOptions={option =>
- option.value.kind === FieldValueKind.FUNCTION
- }
- fieldOptions={fieldOptions}
- fieldValue={fieldValue}
- onChange={v => onChange(generateFieldAsString(v), {})}
- columnWidth={columnWidth}
- gridColumns={gridColumns}
- inFieldLabels={inFieldLabels}
- shouldRenderTag={false}
- disabled={disabled}
- hideParameterSelector={hideParameterSelector}
- hidePrimarySelector={hidePrimarySelector}
- />
- )}
- </Container>
- );
- }}
- </FormField>
- );
- }
- // swaps out custom percentile values for known percentiles, used while we fade out custom percentiles in metric alerts
- // TODO(telemetry-experience): remove once we migrate all custom percentile alerts
- const getFieldValue = (aggregate: string | undefined, model) => {
- const fieldValue = explodeFieldString(aggregate ?? '');
- if (fieldValue?.kind !== FieldValueKind.FUNCTION) {
- return fieldValue;
- }
- if (fieldValue.function[0] !== 'percentile') {
- return fieldValue;
- }
- const newFieldValue: QueryFieldValue = {
- kind: FieldValueKind.FUNCTION,
- function: [
- getApproximateKnownPercentile(fieldValue.function[2] as string),
- fieldValue.function[1],
- undefined,
- undefined,
- ],
- alias: fieldValue.alias,
- };
- model.setValue('aggregate', generateFieldAsString(newFieldValue));
- return newFieldValue;
- };
- const getApproximateKnownPercentile = (customPercentile: string) => {
- const percentile = parseFloat(customPercentile);
- if (percentile <= 0.5) {
- return 'p50';
- }
- if (percentile <= 0.75) {
- return 'p75';
- }
- if (percentile <= 0.9) {
- return 'p90';
- }
- if (percentile <= 0.95) {
- return 'p95';
- }
- if (percentile <= 0.99) {
- return 'p99';
- }
- return 'p100';
- };
- const Container = styled('div')<{hideGap: boolean}>`
- display: grid;
- grid-template-columns: 1fr auto;
- gap: ${p => (p.hideGap ? space(0) : space(1))};
- `;
- const StyledQueryField = styled(QueryField)<{gridColumns: number; columnWidth?: number}>`
- ${p =>
- p.columnWidth &&
- css`
- width: ${p.gridColumns * p.columnWidth}px;
- `}
- `;
|