1
1
import { useMemo } from 'react' ;
2
+ import type { YAXisComponentOption } from 'echarts' ;
2
3
3
4
import { AreaChart } from 'sentry/components/charts/areaChart' ;
4
5
import ErrorPanel from 'sentry/components/charts/errorPanel' ;
@@ -9,8 +10,14 @@ import {t} from 'sentry/locale';
9
10
import { space } from 'sentry/styles/space' ;
10
11
import type { DataCondition } from 'sentry/types/workflowEngine/dataConditions' ;
11
12
import type { MetricDetectorConfig } from 'sentry/types/workflowEngine/detectors' ;
12
- import { TimePeriod } from 'sentry/views/alerts/rules/metric/types' ;
13
+ import {
14
+ AlertRuleSensitivity ,
15
+ AlertRuleThresholdType ,
16
+ TimePeriod ,
17
+ } from 'sentry/views/alerts/rules/metric/types' ;
13
18
import type { DetectorDataset } from 'sentry/views/detectors/components/forms/metric/metricFormData' ;
19
+ import { useIncidentMarkers } from 'sentry/views/detectors/hooks/useIncidentMarkers' ;
20
+ import { useMetricDetectorAnomalyPeriods } from 'sentry/views/detectors/hooks/useMetricDetectorAnomalyPeriods' ;
14
21
import { useMetricDetectorSeries } from 'sentry/views/detectors/hooks/useMetricDetectorSeries' ;
15
22
import { useMetricDetectorThresholdSeries } from 'sentry/views/detectors/hooks/useMetricDetectorThresholdSeries' ;
16
23
@@ -50,10 +57,18 @@ interface MetricDetectorChartProps {
50
57
* The query filter string
51
58
*/
52
59
query : string ;
60
+ /**
61
+ * Used in anomaly detection
62
+ */
63
+ sensitivity : AlertRuleSensitivity | undefined ;
53
64
/**
54
65
* The time period for the chart data (optional, defaults to 7d)
55
66
*/
56
67
statsPeriod : TimePeriod ;
68
+ /**
69
+ * Used in anomaly detection
70
+ */
71
+ thresholdType : AlertRuleThresholdType | undefined ;
57
72
}
58
73
59
74
export function MetricDetectorChart ( {
@@ -67,8 +82,10 @@ export function MetricDetectorChart({
67
82
detectionType,
68
83
statsPeriod,
69
84
comparisonDelta,
85
+ sensitivity,
86
+ thresholdType,
70
87
} : MetricDetectorChartProps ) {
71
- const { series, comparisonSeries, isPending , isError} = useMetricDetectorSeries ( {
88
+ const { series, comparisonSeries, isLoading , isError} = useMetricDetectorSeries ( {
72
89
dataset,
73
90
aggregate,
74
91
interval,
@@ -79,13 +96,47 @@ export function MetricDetectorChart({
79
96
comparisonDelta,
80
97
} ) ;
81
98
82
- const { maxValue : thresholdMaxValue , additionalSeries} =
99
+ const { maxValue : thresholdMaxValue , additionalSeries : thresholdAdditionalSeries } =
83
100
useMetricDetectorThresholdSeries ( {
84
101
conditions,
85
102
detectionType,
86
103
comparisonSeries,
87
104
} ) ;
88
105
106
+ const isAnomalyDetection = detectionType === 'dynamic' ;
107
+ const shouldFetchAnomalies =
108
+ isAnomalyDetection && ! isLoading && ! isError && series . length > 0 ;
109
+
110
+ // Fetch anomaly data when detection type is dynamic and series data is ready
111
+ const {
112
+ anomalyPeriods,
113
+ isLoading : isLoadingAnomalies ,
114
+ error : anomalyErrorObject ,
115
+ } = useMetricDetectorAnomalyPeriods ( {
116
+ series : shouldFetchAnomalies ? series : [ ] ,
117
+ dataset,
118
+ aggregate,
119
+ query,
120
+ environment,
121
+ projectId,
122
+ statsPeriod,
123
+ timePeriod : interval ,
124
+ thresholdType,
125
+ sensitivity,
126
+ enabled : shouldFetchAnomalies ,
127
+ } ) ;
128
+
129
+ // Create anomaly marker rendering from pre-grouped anomaly periods
130
+ const anomalyMarkerResult = useIncidentMarkers ( {
131
+ incidents : anomalyPeriods ,
132
+ seriesName : t ( 'Anomalies' ) ,
133
+ seriesId : '__anomaly_marker__' ,
134
+ yAxisIndex : 1 , // Use index 1 to avoid conflict with main chart axis
135
+ } ) ;
136
+
137
+ const anomalyLoading = shouldFetchAnomalies ? isLoadingAnomalies : false ;
138
+ const anomalyError = shouldFetchAnomalies ? anomalyErrorObject : null ;
139
+
89
140
// Calculate y-axis bounds to ensure all thresholds are visible
90
141
const maxValue = useMemo ( ( ) => {
91
142
// Get max from series data
@@ -111,15 +162,72 @@ export function MetricDetectorChart({
111
162
return roundedMax + padding ;
112
163
} , [ series , thresholdMaxValue ] ) ;
113
164
114
- if ( isPending ) {
165
+ const additionalSeries = useMemo ( ( ) => {
166
+ const baseSeries = [ ...thresholdAdditionalSeries ] ;
167
+
168
+ if ( isAnomalyDetection && anomalyMarkerResult . incidentMarkerSeries ) {
169
+ // Line series not working well with the custom series type
170
+ baseSeries . push ( anomalyMarkerResult . incidentMarkerSeries as any ) ;
171
+ }
172
+
173
+ return baseSeries ;
174
+ } , [
175
+ isAnomalyDetection ,
176
+ thresholdAdditionalSeries ,
177
+ anomalyMarkerResult . incidentMarkerSeries ,
178
+ ] ) ;
179
+
180
+ const yAxes = useMemo ( ( ) => {
181
+ const mainYAxis : YAXisComponentOption = {
182
+ max : maxValue > 0 ? maxValue : undefined ,
183
+ min : 0 ,
184
+ axisLabel : {
185
+ // Hide the maximum y-axis label to avoid showing arbitrary threshold values
186
+ showMaxLabel : false ,
187
+ } ,
188
+ // Disable the y-axis grid lines
189
+ splitLine : { show : false } ,
190
+ } ;
191
+
192
+ const axes : YAXisComponentOption [ ] = [ mainYAxis ] ;
193
+
194
+ // Add anomaly marker Y-axis if available
195
+ if ( isAnomalyDetection && anomalyMarkerResult . incidentMarkerYAxis ) {
196
+ axes . push ( anomalyMarkerResult . incidentMarkerYAxis ) ;
197
+ }
198
+
199
+ return axes ;
200
+ } , [ maxValue , isAnomalyDetection , anomalyMarkerResult . incidentMarkerYAxis ] ) ;
201
+
202
+ // Prepare grid with anomaly marker adjustments
203
+ const grid = useMemo ( ( ) => {
204
+ const baseGrid = {
205
+ left : space ( 0.25 ) ,
206
+ right : space ( 0.25 ) ,
207
+ top : space ( 1.5 ) ,
208
+ bottom : space ( 1 ) ,
209
+ } ;
210
+
211
+ // Apply anomaly marker grid adjustments if available
212
+ if ( isAnomalyDetection && anomalyMarkerResult . incidentMarkerGrid ) {
213
+ return {
214
+ ...baseGrid ,
215
+ ...anomalyMarkerResult . incidentMarkerGrid ,
216
+ } ;
217
+ }
218
+
219
+ return baseGrid ;
220
+ } , [ isAnomalyDetection , anomalyMarkerResult . incidentMarkerGrid ] ) ;
221
+
222
+ if ( isLoading || anomalyLoading ) {
115
223
return (
116
224
< Flex style = { { height : CHART_HEIGHT } } justify = "center" align = "center" >
117
225
< Placeholder height = { `${ CHART_HEIGHT - 20 } px` } />
118
226
</ Flex >
119
227
) ;
120
228
}
121
229
122
- if ( isError ) {
230
+ if ( isError || anomalyError ) {
123
231
return (
124
232
< Flex style = { { height : CHART_HEIGHT } } justify = "center" align = "center" >
125
233
< ErrorPanel >
@@ -138,22 +246,13 @@ export function MetricDetectorChart({
138
246
stacked = { false }
139
247
series = { series }
140
248
additionalSeries = { additionalSeries }
141
- yAxis = { {
142
- max : maxValue > 0 ? maxValue : undefined ,
143
- min : 0 ,
144
- axisLabel : {
145
- // Hide the maximum y-axis label to avoid showing arbitrary threshold values
146
- showMaxLabel : false ,
147
- } ,
148
- // Disable the y-axis grid lines
149
- splitLine : { show : false } ,
150
- } }
151
- grid = { {
152
- left : space ( 0.25 ) ,
153
- right : space ( 0.25 ) ,
154
- top : space ( 1.5 ) ,
155
- bottom : space ( 1 ) ,
156
- } }
249
+ yAxes = { yAxes . length > 1 ? yAxes : undefined }
250
+ yAxis = { yAxes . length === 1 ? yAxes [ 0 ] : undefined }
251
+ grid = { grid }
252
+ xAxis = { isAnomalyDetection ? anomalyMarkerResult . incidentMarkerXAxis : undefined }
253
+ ref = {
254
+ isAnomalyDetection ? anomalyMarkerResult . connectIncidentMarkerChartRef : undefined
255
+ }
157
256
/>
158
257
) ;
159
258
}
0 commit comments