Skip to content

Commit 8d8cc29

Browse files
committed
cleanup and prevent overwriting of filters
1 parent 86d3c27 commit 8d8cc29

File tree

3 files changed

+127
-183
lines changed

3 files changed

+127
-183
lines changed
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+
export 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/templateSection.tsx

Lines changed: 51 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -8,93 +8,28 @@ import DropdownButton from 'sentry/components/dropdownButton';
88
import FormContext from 'sentry/components/forms/formContext';
99
import {Container} from 'sentry/components/workflowEngine/ui/container';
1010
import {t} from 'sentry/locale';
11-
import {SessionsAggregate} from 'sentry/views/alerts/rules/metric/types';
1211
import type {MetricAlertType} from 'sentry/views/alerts/wizard/options';
1312
import {
1413
METRIC_DETECTOR_FORM_FIELDS,
1514
useMetricDetectorFormField,
1615
} from 'sentry/views/detectors/components/forms/metric/metricFormData';
16+
import {METRIC_TEMPLATE_OPTIONS} from 'sentry/views/detectors/components/forms/metric/metricTemplateOptions';
1717
import {useDatasetChoices} from 'sentry/views/detectors/components/forms/metric/useDatasetChoices';
1818
import {getDatasetConfig} from 'sentry/views/detectors/datasetConfig/getDatasetConfig';
1919
import {DetectorDataset} from 'sentry/views/detectors/datasetConfig/types';
2020

21-
interface TemplateOption {
22-
aggregate: string;
23-
detectorDataset: DetectorDataset;
24-
key: MetricAlertType;
25-
label: string;
26-
query: string;
27-
}
21+
const DATASET_LABELS: Record<DetectorDataset, string> = {
22+
[DetectorDataset.ERRORS]: t('Errors'),
23+
[DetectorDataset.SPANS]: t('Spans'),
24+
[DetectorDataset.LOGS]: t('Logs'),
25+
[DetectorDataset.RELEASES]: t('Releases'),
26+
[DetectorDataset.TRANSACTIONS]: t('Transactions'),
27+
};
2828

2929
/**
30-
* Template options for metric detectors.
31-
* These define the available metric templates that users can select.
30+
* Value used to indicate a custom template (not matching any predefined template)
3231
*/
33-
const METRIC_TEMPLATE_OPTIONS: TemplateOption[] = [
34-
{
35-
key: 'num_errors',
36-
label: t('Number of Errors'),
37-
detectorDataset: DetectorDataset.ERRORS,
38-
aggregate: 'count()',
39-
query: '',
40-
},
41-
{
42-
key: 'users_experiencing_errors',
43-
label: t('Users Experiencing Errors'),
44-
detectorDataset: DetectorDataset.ERRORS,
45-
aggregate: 'count_unique(user)',
46-
query: '',
47-
},
48-
{
49-
key: 'trace_item_throughput',
50-
label: t('Throughput'),
51-
detectorDataset: DetectorDataset.SPANS,
52-
aggregate: 'count(span.duration)',
53-
query: '',
54-
},
55-
{
56-
key: 'trace_item_duration',
57-
label: t('Duration'),
58-
detectorDataset: DetectorDataset.SPANS,
59-
aggregate: 'p95(span.duration)',
60-
query: '',
61-
},
62-
{
63-
key: 'trace_item_failure_rate',
64-
label: t('Failure Rate'),
65-
detectorDataset: DetectorDataset.SPANS,
66-
aggregate: 'failure_rate()',
67-
query: '',
68-
},
69-
{
70-
key: 'trace_item_lcp',
71-
label: t('Largest Contentful Paint'),
72-
detectorDataset: DetectorDataset.SPANS,
73-
aggregate: 'p95(measurements.lcp)',
74-
query: '',
75-
},
76-
{
77-
key: 'trace_item_logs',
78-
label: t('Logs'),
79-
detectorDataset: DetectorDataset.LOGS,
80-
aggregate: 'count(message)',
81-
query: '',
82-
},
83-
{
84-
key: 'crash_free_sessions',
85-
label: t('Crash Free Session Rate'),
86-
detectorDataset: DetectorDataset.RELEASES,
87-
aggregate: SessionsAggregate.CRASH_FREE_SESSIONS,
88-
query: '',
89-
},
90-
{
91-
key: 'crash_free_users',
92-
label: t('Crash Free User Rate'),
93-
detectorDataset: DetectorDataset.RELEASES,
94-
aggregate: SessionsAggregate.CRASH_FREE_USERS,
95-
query: '',
96-
},
97-
];
32+
const CUSTOM_TEMPLATE_VALUE = '__custom__' as const;
9833

9934
export function TemplateSection() {
10035
const formContext = useContext(FormContext);
@@ -110,136 +45,68 @@ export function TemplateSection() {
11045
);
11146
const currentQuery = useMetricDetectorFormField(METRIC_DETECTOR_FORM_FIELDS.query);
11247

113-
// Build template options grouped by dataset into sections
114-
const templateOptions = useMemo(() => {
115-
// Group templates by dataset
116-
const templatesByDataset = METRIC_TEMPLATE_OPTIONS.reduce(
117-
(acc, opt) => {
118-
if (!allowedDatasets.has(opt.detectorDataset)) {
119-
return acc;
120-
}
121-
if (!acc[opt.detectorDataset]) {
122-
acc[opt.detectorDataset] = [];
123-
}
124-
acc[opt.detectorDataset].push(opt);
125-
return acc;
126-
},
127-
{} as Record<DetectorDataset, TemplateOption[]>
128-
);
129-
130-
// Convert to sections
131-
const sections: Array<{
132-
key: DetectorDataset;
133-
label: string;
134-
options: Array<{label: string; value: MetricAlertType}>;
135-
}> = [];
136-
137-
// Dataset labels mapping
138-
const datasetLabels: Record<DetectorDataset, string> = {
139-
[DetectorDataset.ERRORS]: t('Errors'),
140-
[DetectorDataset.SPANS]: t('Spans'),
141-
[DetectorDataset.LOGS]: t('Logs'),
142-
[DetectorDataset.RELEASES]: t('Releases'),
143-
[DetectorDataset.TRANSACTIONS]: t('Transactions'),
144-
};
145-
146-
// Create sections for each dataset that has templates
147-
for (const [dataset, templates] of Object.entries(templatesByDataset)) {
148-
if (templates.length > 0) {
149-
sections.push({
150-
key: dataset as DetectorDataset,
151-
label: datasetLabels[dataset as DetectorDataset] ?? dataset,
152-
options: templates.map(opt => ({
153-
label: opt.label,
154-
value: opt.key,
155-
})),
156-
});
157-
}
158-
}
159-
160-
return sections;
161-
}, [allowedDatasets]);
162-
48+
// Filter templates to allowed datasets
16349
const templateMetaByKey = useMemo(() => {
16450
const filtered = METRIC_TEMPLATE_OPTIONS.filter(opt =>
16551
allowedDatasets.has(opt.detectorDataset)
16652
);
16753
return Object.fromEntries(filtered.map(m => [m.key, m]));
16854
}, [allowedDatasets]);
16955

56+
// Build template options grouped by dataset into sections
57+
const templateOptions = useMemo(() => {
58+
// Group templates by dataset
59+
const templatesByDataset = Object.groupBy(
60+
Object.values(templateMetaByKey),
61+
opt => opt.detectorDataset
62+
);
63+
64+
// Convert to sections
65+
return Object.entries(templatesByDataset)
66+
.filter(([, templates]) => templates && templates.length > 0)
67+
.map(([dataset, templates]) => ({
68+
key: dataset as DetectorDataset,
69+
label: DATASET_LABELS[dataset as DetectorDataset] ?? dataset,
70+
options: templates.map(opt => ({
71+
label: opt.label,
72+
value: opt.key,
73+
})),
74+
}));
75+
}, [templateMetaByKey]);
76+
17077
// Derive current template value based on form state
17178
const currentTemplateValue = useMemo(() => {
17279
if (!currentDataset || !currentAggregateFunction) {
173-
return '__custom__' as const;
80+
return CUSTOM_TEMPLATE_VALUE;
17481
}
17582

176-
// Find all matching templates
177-
const matchingTemplates: Array<
178-
[MetricAlertType, (typeof templateMetaByKey)[string]]
179-
> = [];
180-
181-
for (const [key, meta] of Object.entries(templateMetaByKey)) {
83+
// Find first matching template
84+
const matchingTemplate = Object.entries(templateMetaByKey).find(([, meta]) => {
18285
// Match dataset
18386
if (meta.detectorDataset !== currentDataset) {
184-
continue;
87+
return false;
18588
}
18689

18790
// Match aggregate - convert template's API aggregate to UI format for comparison
18891
const datasetConfig = getDatasetConfig(meta.detectorDataset);
18992
const templateUiAggregate = datasetConfig.fromApiAggregate(meta.aggregate);
190-
if (templateUiAggregate !== currentAggregateFunction) {
191-
continue;
192-
}
193-
194-
// Match query (normalize empty strings and undefined)
195-
const templateQuery = meta.query ?? '';
196-
const formQuery = currentQuery ?? '';
197-
if (templateQuery !== formQuery) {
198-
continue;
199-
}
200-
201-
matchingTemplates.push([key as MetricAlertType, meta]);
202-
}
93+
return templateUiAggregate === currentAggregateFunction;
94+
});
20395

204-
// If multiple templates match (e.g., eap_metrics and trace_item_throughput),
205-
// prefer trace_item_throughput over eap_metrics
206-
if (matchingTemplates.length > 0) {
207-
// Sort to prefer trace_item_* templates over eap_metrics
208-
matchingTemplates.sort(([keyA], [keyB]) => {
209-
// Prefer trace_item_* templates
210-
const aIsTraceItem = keyA.startsWith('trace_item_');
211-
const bIsTraceItem = keyB.startsWith('trace_item_');
212-
if (aIsTraceItem && !bIsTraceItem) return -1;
213-
if (!aIsTraceItem && bIsTraceItem) return 1;
214-
215-
// If both are trace_item_* or both are not, prefer the one that appears first in options
216-
// This ensures deterministic selection
217-
return 0;
218-
});
219-
220-
return matchingTemplates[0]![0];
221-
}
222-
223-
// No template matches, return "custom"
224-
return '__custom__' as const;
225-
}, [currentDataset, currentAggregateFunction, currentQuery, templateMetaByKey]);
96+
return matchingTemplate
97+
? (matchingTemplate[0] as MetricAlertType)
98+
: CUSTOM_TEMPLATE_VALUE;
99+
}, [currentDataset, currentAggregateFunction, templateMetaByKey]);
226100

227101
// Get the label for the current selected template
228102
const selectedOptionLabel = useMemo(() => {
229-
if (currentTemplateValue === '__custom__') {
103+
if (currentTemplateValue === CUSTOM_TEMPLATE_VALUE) {
230104
return t('Custom');
231105
}
232-
// Search through sections to find the selected option
233-
for (const section of templateOptions) {
234-
const selectedOption = section.options.find(
235-
opt => opt.value === currentTemplateValue
236-
);
237-
if (selectedOption) {
238-
return selectedOption.label;
239-
}
240-
}
241-
return t('Choose a template (optional)');
242-
}, [currentTemplateValue, templateOptions]);
106+
return (
107+
templateMetaByKey[currentTemplateValue]?.label ?? t('Choose a template (optional)')
108+
);
109+
}, [currentTemplateValue, templateMetaByKey]);
243110

244111
// No templates available, skip rendering
245112
if (!templateOptions.length) {
@@ -288,7 +155,10 @@ export function TemplateSection() {
288155
METRIC_DETECTOR_FORM_FIELDS.aggregateFunction,
289156
uiAggregate
290157
);
291-
formContext.form?.setValue(METRIC_DETECTOR_FORM_FIELDS.query, meta.query);
158+
// Only set query if template has one and user hasn't customized the filter
159+
if (meta.query !== undefined && !currentQuery) {
160+
formContext.form?.setValue(METRIC_DETECTOR_FORM_FIELDS.query, meta.query);
161+
}
292162
}}
293163
/>
294164
</Flex>

static/app/views/detectors/new-setting.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,8 @@ describe('DetectorEdit', () => {
266266
aggregate: 'count()',
267267
dataset: 'events',
268268
environment: null,
269-
eventTypes: ['error', 'default'],
270-
query: '',
269+
eventTypes: expect.arrayContaining(['error', 'default']),
270+
query: 'is:unresolved',
271271
queryType: 0,
272272
timeWindow: 3600,
273273
}),

0 commit comments

Comments
 (0)