@@ -8,93 +8,28 @@ import DropdownButton from 'sentry/components/dropdownButton';
88import FormContext from 'sentry/components/forms/formContext' ;
99import { Container } from 'sentry/components/workflowEngine/ui/container' ;
1010import { t } from 'sentry/locale' ;
11- import { SessionsAggregate } from 'sentry/views/alerts/rules/metric/types' ;
1211import type { MetricAlertType } from 'sentry/views/alerts/wizard/options' ;
1312import {
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' ;
1717import { useDatasetChoices } from 'sentry/views/detectors/components/forms/metric/useDatasetChoices' ;
1818import { getDatasetConfig } from 'sentry/views/detectors/datasetConfig/getDatasetConfig' ;
1919import { 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
9934export 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 >
0 commit comments