|
@@ -85,12 +85,14 @@ import {
|
|
|
DEFAULT_COUNT_TIME_WINDOW,
|
|
|
} from './constants';
|
|
|
import RuleConditionsForm from './ruleConditionsForm';
|
|
|
-import type {
|
|
|
- EventTypes,
|
|
|
- MetricActionTemplate,
|
|
|
- MetricRule,
|
|
|
- Trigger,
|
|
|
- UnsavedMetricRule,
|
|
|
+import {
|
|
|
+ AlertRuleSeasonality,
|
|
|
+ AlertRuleSensitivity,
|
|
|
+ type EventTypes,
|
|
|
+ type MetricActionTemplate,
|
|
|
+ type MetricRule,
|
|
|
+ type Trigger,
|
|
|
+ type UnsavedMetricRule,
|
|
|
} from './types';
|
|
|
import {
|
|
|
AlertRuleComparisonType,
|
|
@@ -142,6 +144,7 @@ type State = {
|
|
|
project: Project;
|
|
|
query: string;
|
|
|
resolveThreshold: UnsavedMetricRule['resolveThreshold'];
|
|
|
+ sensitivity: UnsavedMetricRule['sensitivity'];
|
|
|
thresholdPeriod: UnsavedMetricRule['thresholdPeriod'];
|
|
|
thresholdType: UnsavedMetricRule['thresholdType'];
|
|
|
timeWindow: number;
|
|
@@ -151,6 +154,7 @@ type State = {
|
|
|
comparisonDelta?: number;
|
|
|
isExtrapolatedChartData?: boolean;
|
|
|
monitorType?: MonitorType;
|
|
|
+ seasonality?: AlertRuleSeasonality;
|
|
|
} & DeprecatedAsyncComponent['state'];
|
|
|
|
|
|
const isEmpty = (str: unknown): boolean => str === '' || !defined(str);
|
|
@@ -233,6 +237,7 @@ class RuleFormContainer extends DeprecatedAsyncComponent<Props, State> {
|
|
|
metricExtractionRules: null,
|
|
|
triggers: triggersClone,
|
|
|
resolveThreshold: rule.resolveThreshold,
|
|
|
+ sensitivity: null,
|
|
|
thresholdType: rule.thresholdType,
|
|
|
thresholdPeriod: rule.thresholdPeriod ?? 1,
|
|
|
comparisonDelta: rule.comparisonDelta ?? undefined,
|
|
@@ -455,7 +460,25 @@ class RuleFormContainer extends DeprecatedAsyncComponent<Props, State> {
|
|
|
) {
|
|
|
const {comparisonType} = this.state;
|
|
|
const triggerErrors = new Map();
|
|
|
-
|
|
|
+ // If we have an anomaly detection alert, then we don't need to validate the thresholds, but we do need to set them to 0
|
|
|
+ if (comparisonType === AlertRuleComparisonType.DYNAMIC) {
|
|
|
+ // NOTE: we don't support warning triggers for anomaly detection alerts yet
|
|
|
+ // once we do, uncomment this code and delete 475-478:
|
|
|
+ // triggers.forEach(trigger => {
|
|
|
+ // trigger.alertThreshold = 0;
|
|
|
+ // });
|
|
|
+ const criticalTriggerIndex = triggers.findIndex(
|
|
|
+ ({label}) => label === AlertRuleTriggerType.CRITICAL
|
|
|
+ );
|
|
|
+ const warningTriggerIndex = criticalTriggerIndex ^ 1;
|
|
|
+ const triggersCopy = [...triggers];
|
|
|
+ const criticalTrigger = triggersCopy[criticalTriggerIndex];
|
|
|
+ const warningTrigger = triggersCopy[warningTriggerIndex];
|
|
|
+ criticalTrigger.alertThreshold = 0;
|
|
|
+ warningTrigger.alertThreshold = ''; // we need to set this to empty
|
|
|
+ this.setState({triggers: triggersCopy});
|
|
|
+ return triggerErrors; // return an empty map
|
|
|
+ }
|
|
|
const requiredFields = ['label', 'alertThreshold'];
|
|
|
triggers.forEach((trigger, triggerIndex) => {
|
|
|
requiredFields.forEach(field => {
|
|
@@ -725,6 +748,9 @@ class RuleFormContainer extends DeprecatedAsyncComponent<Props, State> {
|
|
|
eventTypes,
|
|
|
monitorType,
|
|
|
activationCondition,
|
|
|
+ sensitivity,
|
|
|
+ seasonality,
|
|
|
+ comparisonType,
|
|
|
} = this.state;
|
|
|
// Remove empty warning trigger
|
|
|
const sanitizedTriggers = triggers.filter(
|
|
@@ -761,7 +787,12 @@ class RuleFormContainer extends DeprecatedAsyncComponent<Props, State> {
|
|
|
activationCondition,
|
|
|
};
|
|
|
}
|
|
|
-
|
|
|
+ const detectionTypes = new Map([
|
|
|
+ [AlertRuleComparisonType.COUNT, 'static'],
|
|
|
+ [AlertRuleComparisonType.CHANGE, 'percent'],
|
|
|
+ [AlertRuleComparisonType.DYNAMIC, 'dynamic'],
|
|
|
+ ]);
|
|
|
+ const detectionType = detectionTypes.get(comparisonType) ?? '';
|
|
|
const dataset = this.determinePerformanceDataset();
|
|
|
this.setState({loading: true});
|
|
|
// Add or update is just the PUT/POST to the org alert-rules api
|
|
@@ -785,6 +816,9 @@ class RuleFormContainer extends DeprecatedAsyncComponent<Props, State> {
|
|
|
eventTypes: isCrashFreeAlert(rule.dataset) ? undefined : eventTypes,
|
|
|
dataset,
|
|
|
queryType: DatasetMEPAlertQueryTypes[dataset],
|
|
|
+ sensitivity: sensitivity ?? null,
|
|
|
+ seasonality: seasonality ?? null,
|
|
|
+ detectionType: detectionType,
|
|
|
},
|
|
|
{
|
|
|
duplicateRule: this.isDuplicateRule ? 'true' : 'false',
|
|
@@ -819,7 +853,13 @@ class RuleFormContainer extends DeprecatedAsyncComponent<Props, State> {
|
|
|
? err?.responseJSON
|
|
|
: Object.values(err?.responseJSON)
|
|
|
: [];
|
|
|
- const apiErrors = errors.length > 0 ? `: ${errors.join(', ')}` : '';
|
|
|
+ let apiErrors = '';
|
|
|
+ if (typeof errors[0] === 'object') {
|
|
|
+ // NOTE: this occurs if we get a TimeoutError when attempting to hit the Seer API
|
|
|
+ apiErrors = ': ' + errors[0].message;
|
|
|
+ } else {
|
|
|
+ apiErrors = errors.length > 0 ? `: ${errors.join(', ')}` : '';
|
|
|
+ }
|
|
|
this.handleRuleSaveFailure(t('Unable to save alert%s', apiErrors));
|
|
|
}
|
|
|
});
|
|
@@ -850,6 +890,10 @@ class RuleFormContainer extends DeprecatedAsyncComponent<Props, State> {
|
|
|
});
|
|
|
};
|
|
|
|
|
|
+ handleSensitivityChange = (sensitivity: AlertRuleSensitivity) => {
|
|
|
+ this.setState({sensitivity});
|
|
|
+ };
|
|
|
+
|
|
|
handleThresholdTypeChange = (thresholdType: AlertRuleThresholdType) => {
|
|
|
const {triggers} = this.state;
|
|
|
|
|
@@ -883,13 +927,25 @@ class RuleFormContainer extends DeprecatedAsyncComponent<Props, State> {
|
|
|
|
|
|
handleComparisonTypeChange = (value: AlertRuleComparisonType) => {
|
|
|
const comparisonDelta =
|
|
|
- value === AlertRuleComparisonType.COUNT
|
|
|
- ? undefined
|
|
|
- : this.state.comparisonDelta ?? DEFAULT_CHANGE_COMP_DELTA;
|
|
|
+ value === AlertRuleComparisonType.CHANGE
|
|
|
+ ? this.state.comparisonDelta ?? DEFAULT_CHANGE_COMP_DELTA
|
|
|
+ : undefined;
|
|
|
const timeWindow = this.state.comparisonDelta
|
|
|
? DEFAULT_COUNT_TIME_WINDOW
|
|
|
: DEFAULT_CHANGE_TIME_WINDOW;
|
|
|
- this.setState({comparisonType: value, comparisonDelta, timeWindow});
|
|
|
+ const sensitivity =
|
|
|
+ value === AlertRuleComparisonType.DYNAMIC
|
|
|
+ ? this.state.sensitivity || AlertRuleSensitivity.MEDIUM
|
|
|
+ : undefined;
|
|
|
+ const seasonality =
|
|
|
+ value === AlertRuleComparisonType.DYNAMIC ? AlertRuleSeasonality.AUTO : undefined; // TODO: replace "auto" with the correct constant
|
|
|
+ this.setState({
|
|
|
+ comparisonType: value,
|
|
|
+ comparisonDelta,
|
|
|
+ timeWindow,
|
|
|
+ sensitivity,
|
|
|
+ seasonality,
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
handleDeleteRule = async () => {
|
|
@@ -1096,6 +1152,7 @@ class RuleFormContainer extends DeprecatedAsyncComponent<Props, State> {
|
|
|
comparisonDelta,
|
|
|
comparisonType,
|
|
|
resolveThreshold,
|
|
|
+ sensitivity,
|
|
|
loading,
|
|
|
eventTypes,
|
|
|
dataset,
|
|
@@ -1120,6 +1177,7 @@ class RuleFormContainer extends DeprecatedAsyncComponent<Props, State> {
|
|
|
aggregate={aggregate}
|
|
|
isMigration={isMigration}
|
|
|
resolveThreshold={resolveThreshold}
|
|
|
+ sensitivity={sensitivity}
|
|
|
thresholdPeriod={thresholdPeriod}
|
|
|
thresholdType={thresholdType}
|
|
|
comparisonType={comparisonType}
|
|
@@ -1130,6 +1188,7 @@ class RuleFormContainer extends DeprecatedAsyncComponent<Props, State> {
|
|
|
onThresholdTypeChange={this.handleThresholdTypeChange}
|
|
|
onThresholdPeriodChange={this.handleThresholdPeriodChange}
|
|
|
onResolveThresholdChange={this.handleResolveThresholdChange}
|
|
|
+ onSensitivityChange={this.handleSensitivityChange}
|
|
|
/>
|
|
|
);
|
|
|
|