Skip to content

Commit 32f8d80

Browse files
authored
feat(aci): Add metric monitor template dropdown (#102992)
adds a template dropdown and moves customize section into a collapsible component
1 parent c0d6ba7 commit 32f8d80

File tree

8 files changed

+454
-146
lines changed

8 files changed

+454
-146
lines changed

static/app/views/detectors/components/forms/metric/metric.tsx

Lines changed: 83 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import {Fragment, useContext, useEffect, useMemo} from 'react';
1+
import {Fragment, useContext, useEffect} from 'react';
22
import styled from '@emotion/styled';
33
import toNumber from 'lodash/toNumber';
44

5-
import {FeatureBadge} from 'sentry/components/core/badge/featureBadge';
5+
import {Disclosure} from 'sentry/components/core/disclosure';
66
import {Flex} from 'sentry/components/core/layout';
77
import {Heading} from 'sentry/components/core/text/heading';
88
import {Text} from 'sentry/components/core/text/text';
@@ -15,29 +15,20 @@ import FormContext from 'sentry/components/forms/formContext';
1515
import {Container} from 'sentry/components/workflowEngine/ui/container';
1616
import {t} from 'sentry/locale';
1717
import {space} from 'sentry/styles/space';
18-
import type {SelectValue} from 'sentry/types/core';
1918
import {DataConditionType} from 'sentry/types/workflowEngine/dataConditions';
20-
import type {
21-
Detector,
22-
MetricDetector,
23-
MetricDetectorConfig,
24-
} from 'sentry/types/workflowEngine/detectors';
19+
import type {Detector, MetricDetectorConfig} from 'sentry/types/workflowEngine/detectors';
2520
import {generateFieldAsString} from 'sentry/utils/discover/fields';
26-
import useOrganization from 'sentry/utils/useOrganization';
2721
import {
2822
AlertRuleSensitivity,
2923
AlertRuleThresholdType,
30-
Dataset,
3124
} from 'sentry/views/alerts/rules/metric/types';
32-
import {hasLogAlerts} from 'sentry/views/alerts/wizard/utils';
3325
import {
3426
TRANSACTIONS_DATASET_DEPRECATION_MESSAGE,
3527
TransactionsDatasetWarning,
3628
} from 'sentry/views/detectors/components/details/metric/transactionsDatasetWarning';
3729
import {AutomateSection} from 'sentry/views/detectors/components/forms/automateSection';
3830
import {AssignSection} from 'sentry/views/detectors/components/forms/common/assignSection';
3931
import {DescribeSection} from 'sentry/views/detectors/components/forms/common/describeSection';
40-
import {useDetectorFormContext} from 'sentry/views/detectors/components/forms/context';
4132
import {EditDetectorLayout} from 'sentry/views/detectors/components/forms/editDetectorLayout';
4233
import type {MetricDetectorFormData} from 'sentry/views/detectors/components/forms/metric/metricFormData';
4334
import {
@@ -49,7 +40,9 @@ import {
4940
import {MetricDetectorPreviewChart} from 'sentry/views/detectors/components/forms/metric/previewChart';
5041
import {DetectorQueryFilterBuilder} from 'sentry/views/detectors/components/forms/metric/queryFilterBuilder';
5142
import {ResolveSection} from 'sentry/views/detectors/components/forms/metric/resolveSection';
43+
import {TemplateSection} from 'sentry/views/detectors/components/forms/metric/templateSection';
5244
import {useAutoMetricDetectorName} from 'sentry/views/detectors/components/forms/metric/useAutoMetricDetectorName';
45+
import {useDatasetChoices} from 'sentry/views/detectors/components/forms/metric/useDatasetChoices';
5346
import {useInitialMetricDetectorFormData} from 'sentry/views/detectors/components/forms/metric/useInitialMetricDetectorFormData';
5447
import {useIntervalChoices} from 'sentry/views/detectors/components/forms/metric/useIntervalChoices';
5548
import {Visualize} from 'sentry/views/detectors/components/forms/metric/visualize';
@@ -58,14 +51,14 @@ import {SectionLabel} from 'sentry/views/detectors/components/forms/sectionLabel
5851
import {getDatasetConfig} from 'sentry/views/detectors/datasetConfig/getDatasetConfig';
5952
import {DetectorDataset} from 'sentry/views/detectors/datasetConfig/types';
6053
import {getMetricDetectorSuffix} from 'sentry/views/detectors/utils/metricDetectorSuffix';
61-
import {deprecateTransactionAlerts} from 'sentry/views/insights/common/utils/hasEAPAlerts';
6254

6355
function MetricDetectorForm() {
6456
useAutoMetricDetectorName();
6557

6658
return (
6759
<FormStack>
6860
<TransactionsDatasetWarningListener />
61+
<TemplateSection />
6962
<CustomizeMetricSection />
7063
<DetectSection />
7164
<AssignSection />
@@ -343,6 +336,7 @@ function IntervalPicker() {
343336
placeholder={t('Interval')}
344337
flexibleControlStateSize
345338
inline={false}
339+
preserveOnUnmount
346340
label={
347341
<Tooltip
348342
title={t('The time period over which to evaluate your metric.')}
@@ -358,51 +352,6 @@ function IntervalPicker() {
358352
);
359353
}
360354

361-
function useDatasetChoices() {
362-
const organization = useOrganization();
363-
364-
const {detector} = useDetectorFormContext();
365-
const savedDataset = (detector as MetricDetector | undefined)?.dataSources[0]?.queryObj
366-
?.snubaQuery?.dataset;
367-
const isExistingTransactionsDetector =
368-
Boolean(detector) &&
369-
[Dataset.TRANSACTIONS, Dataset.GENERIC_METRICS].includes(savedDataset as Dataset);
370-
const shouldHideTransactionsDataset =
371-
!isExistingTransactionsDetector && deprecateTransactionAlerts(organization);
372-
373-
return useMemo(() => {
374-
const datasetChoices: Array<SelectValue<DetectorDataset>> = [
375-
{
376-
value: DetectorDataset.ERRORS,
377-
label: t('Errors'),
378-
},
379-
...(shouldHideTransactionsDataset
380-
? []
381-
: [
382-
{
383-
value: DetectorDataset.TRANSACTIONS,
384-
label: t('Transactions'),
385-
},
386-
]),
387-
...(organization.features.includes('visibility-explore-view')
388-
? [{value: DetectorDataset.SPANS, label: t('Spans')}]
389-
: []),
390-
...(hasLogAlerts(organization)
391-
? [
392-
{
393-
value: DetectorDataset.LOGS,
394-
label: t('Logs'),
395-
trailingItems: <FeatureBadge type="new" />,
396-
},
397-
]
398-
: []),
399-
{value: DetectorDataset.RELEASES, label: t('Releases')},
400-
];
401-
402-
return datasetChoices;
403-
}, [organization, shouldHideTransactionsDataset]);
404-
}
405-
406355
function CustomizeMetricSection() {
407356
const detectionType = useMetricDetectorFormField(
408357
METRIC_DETECTOR_FORM_FIELDS.detectionType
@@ -414,72 +363,81 @@ function CustomizeMetricSection() {
414363

415364
return (
416365
<Container>
417-
<Flex direction="column" gap="xs">
418-
<BorderBottomHeader>
419-
<Heading as="h3">{t('Customize Metric')}</Heading>
420-
</BorderBottomHeader>
421-
<DatasetRow>
422-
<DatasetField
423-
placeholder={t('Dataset')}
424-
flexibleControlStateSize
425-
inline={false}
426-
label={
427-
<Tooltip
428-
title={t('This reflects the type of information you want to use.')}
429-
showUnderline
430-
>
431-
<SectionLabel>{t('Dataset')}</SectionLabel>
432-
</Tooltip>
433-
}
434-
name={METRIC_DETECTOR_FORM_FIELDS.dataset}
435-
options={datasetChoices}
436-
onChange={newDataset => {
437-
// Reset aggregate function to dataset default when dataset changes
438-
const datasetConfig = getDatasetConfig(newDataset);
439-
const defaultAggregate = generateFieldAsString(datasetConfig.defaultField);
440-
formContext.form?.setValue(
441-
METRIC_DETECTOR_FORM_FIELDS.aggregateFunction,
442-
defaultAggregate
443-
);
444-
445-
const supportedDetectionTypes = datasetConfig.supportedDetectionTypes;
446-
if (!supportedDetectionTypes.includes(detectionType)) {
447-
formContext.form?.setValue(
448-
METRIC_DETECTOR_FORM_FIELDS.detectionType,
449-
supportedDetectionTypes[0]
450-
);
451-
}
452-
}}
453-
/>
454-
<Tooltip
455-
title={TRANSACTIONS_DATASET_DEPRECATION_MESSAGE}
456-
isHoverable
457-
disabled={!isTransactionsDataset}
458-
>
459-
<DisabledSection disabled={isTransactionsDataset}>
460-
<IntervalPicker />
461-
</DisabledSection>
462-
</Tooltip>
463-
</DatasetRow>
464-
</Flex>
465-
<Tooltip
466-
title={TRANSACTIONS_DATASET_DEPRECATION_MESSAGE}
467-
isHoverable
468-
disabled={!isTransactionsDataset}
469-
>
470-
<DisabledSection disabled={isTransactionsDataset}>
471-
<Visualize />
472-
</DisabledSection>
473-
</Tooltip>
474-
<Tooltip
475-
title={TRANSACTIONS_DATASET_DEPRECATION_MESSAGE}
476-
isHoverable
477-
disabled={!isTransactionsDataset}
478-
>
479-
<FilterRow disabled={isTransactionsDataset}>
480-
<DetectorQueryFilterBuilder />
481-
</FilterRow>
482-
</Tooltip>
366+
<Disclosure as="section" size="md" role="region" defaultExpanded>
367+
<Disclosure.Title aria-label={t('Customize Metric Section')}>
368+
<Text size="lg">{t('Customize Metric')}</Text>
369+
</Disclosure.Title>
370+
<Disclosure.Content>
371+
<Flex direction="column" gap="md">
372+
<Flex direction="column" gap="xs">
373+
<DatasetRow>
374+
<DatasetField
375+
placeholder={t('Dataset')}
376+
flexibleControlStateSize
377+
inline={false}
378+
preserveOnUnmount
379+
label={
380+
<Tooltip
381+
title={t('This reflects the type of information you want to use.')}
382+
showUnderline
383+
>
384+
<SectionLabel>{t('Dataset')}</SectionLabel>
385+
</Tooltip>
386+
}
387+
name={METRIC_DETECTOR_FORM_FIELDS.dataset}
388+
options={datasetChoices}
389+
onChange={newDataset => {
390+
// Reset aggregate function to dataset default when dataset changes
391+
const datasetConfig = getDatasetConfig(newDataset);
392+
const defaultAggregate = generateFieldAsString(
393+
datasetConfig.defaultField
394+
);
395+
formContext.form?.setValue(
396+
METRIC_DETECTOR_FORM_FIELDS.aggregateFunction,
397+
defaultAggregate
398+
);
399+
400+
const supportedDetectionTypes = datasetConfig.supportedDetectionTypes;
401+
if (!supportedDetectionTypes.includes(detectionType)) {
402+
formContext.form?.setValue(
403+
METRIC_DETECTOR_FORM_FIELDS.detectionType,
404+
supportedDetectionTypes[0]
405+
);
406+
}
407+
}}
408+
/>
409+
<Tooltip
410+
title={TRANSACTIONS_DATASET_DEPRECATION_MESSAGE}
411+
isHoverable
412+
disabled={!isTransactionsDataset}
413+
>
414+
<DisabledSection disabled={isTransactionsDataset}>
415+
<IntervalPicker />
416+
</DisabledSection>
417+
</Tooltip>
418+
</DatasetRow>
419+
</Flex>
420+
<Tooltip
421+
title={TRANSACTIONS_DATASET_DEPRECATION_MESSAGE}
422+
isHoverable
423+
disabled={!isTransactionsDataset}
424+
>
425+
<DisabledSection disabled={isTransactionsDataset}>
426+
<Visualize />
427+
</DisabledSection>
428+
</Tooltip>
429+
<Tooltip
430+
title={TRANSACTIONS_DATASET_DEPRECATION_MESSAGE}
431+
isHoverable
432+
disabled={!isTransactionsDataset}
433+
>
434+
<FilterRow disabled={isTransactionsDataset}>
435+
<DetectorQueryFilterBuilder />
436+
</FilterRow>
437+
</Tooltip>
438+
</Flex>
439+
</Disclosure.Content>
440+
</Disclosure>
483441
</Container>
484442
);
485443
}
@@ -496,9 +454,7 @@ function DetectSection() {
496454
<Container>
497455
<Flex direction="column" gap="lg">
498456
<div>
499-
<BorderBottomHeader>
500-
<Heading as="h3">{t('Issue Detection')}</Heading>
501-
</BorderBottomHeader>
457+
<Heading as="h3">{t('Issue Detection')}</Heading>
502458
<DetectionType />
503459
<Flex direction="column">
504460
{(!detectionType || detectionType === 'static') && (
@@ -620,12 +576,6 @@ const FilterRow = styled('div')<{disabled: boolean}>`
620576
${p => (p.disabled ? `opacity: 0.6;` : '')}
621577
`;
622578

623-
const BorderBottomHeader = styled('div')`
624-
padding-bottom: ${p => p.theme.space.sm};
625-
margin-bottom: ${p => p.theme.space.md};
626-
border-bottom: 1px solid ${p => p.theme.border};
627-
`;
628-
629579
const StyledSelectField = styled(SelectField)`
630580
width: 180px;
631581
padding: 0;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import {t} from 'sentry/locale';
2+
import {SessionsAggregate} from 'sentry/views/alerts/rules/metric/types';
3+
import type {MetricAlertType} from 'sentry/views/alerts/wizard/options';
4+
import {DetectorDataset} from 'sentry/views/detectors/datasetConfig/types';
5+
6+
interface TemplateOption {
7+
aggregate: string;
8+
detectorDataset: DetectorDataset;
9+
key: MetricAlertType;
10+
label: string;
11+
query?: string;
12+
}
13+
14+
/**
15+
* Template options for metric detectors.
16+
* These define the available metric templates that users can select.
17+
*/
18+
export const METRIC_TEMPLATE_OPTIONS: TemplateOption[] = [
19+
{
20+
key: 'num_errors',
21+
label: t('Number of Errors'),
22+
detectorDataset: DetectorDataset.ERRORS,
23+
aggregate: 'count()',
24+
query: 'is:unresolved',
25+
},
26+
{
27+
key: 'users_experiencing_errors',
28+
label: t('Users Experiencing Errors'),
29+
detectorDataset: DetectorDataset.ERRORS,
30+
aggregate: 'count_unique(user)',
31+
},
32+
{
33+
key: 'trace_item_throughput',
34+
label: t('Throughput'),
35+
detectorDataset: DetectorDataset.SPANS,
36+
aggregate: 'count(span.duration)',
37+
},
38+
{
39+
key: 'trace_item_duration',
40+
label: t('Duration'),
41+
detectorDataset: DetectorDataset.SPANS,
42+
aggregate: 'p95(span.duration)',
43+
},
44+
{
45+
key: 'trace_item_failure_rate',
46+
label: t('Failure Rate'),
47+
detectorDataset: DetectorDataset.SPANS,
48+
aggregate: 'failure_rate()',
49+
},
50+
{
51+
key: 'trace_item_lcp',
52+
label: t('Largest Contentful Paint'),
53+
detectorDataset: DetectorDataset.SPANS,
54+
aggregate: 'p95(measurements.lcp)',
55+
},
56+
{
57+
key: 'trace_item_logs',
58+
label: t('Logs'),
59+
detectorDataset: DetectorDataset.LOGS,
60+
aggregate: 'count(message)',
61+
},
62+
{
63+
key: 'crash_free_sessions',
64+
label: t('Crash Free Session Rate'),
65+
detectorDataset: DetectorDataset.RELEASES,
66+
aggregate: SessionsAggregate.CRASH_FREE_SESSIONS,
67+
},
68+
{
69+
key: 'crash_free_users',
70+
label: t('Crash Free User Rate'),
71+
detectorDataset: DetectorDataset.RELEASES,
72+
aggregate: SessionsAggregate.CRASH_FREE_USERS,
73+
},
74+
];

static/app/views/detectors/components/forms/metric/queryFilterBuilder.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export function DetectorQueryFilterBuilder() {
4444
name={METRIC_DETECTOR_FORM_FIELDS.query}
4545
inline={false}
4646
flexibleControlStateSize
47+
preserveOnUnmount
4748
label={t('Filter')}
4849
hideLabel
4950
disabled={dataset === DetectorDataset.TRANSACTIONS}

0 commit comments

Comments
 (0)