diff --git a/static/app/types/workflowEngine/dataConditions.tsx b/static/app/types/workflowEngine/dataConditions.tsx
index 69fcdf001d1fa1..783e19aa3a57dc 100644
--- a/static/app/types/workflowEngine/dataConditions.tsx
+++ b/static/app/types/workflowEngine/dataConditions.tsx
@@ -50,6 +50,7 @@ export enum DataConditionType {
EVENT_FREQUENCY = 'event_frequency',
EVENT_UNIQUE_USER_FREQUENCY = 'event_unique_user_frequency',
PERCENT_SESSIONS = 'percent_sessions',
+ ANOMALY_DETECTION = 'anomaly_detection',
}
export enum DataConditionGroupLogicType {
diff --git a/static/app/types/workflowEngine/detectors.tsx b/static/app/types/workflowEngine/detectors.tsx
index 755e84bcdbf7c3..7b21b748b94efc 100644
--- a/static/app/types/workflowEngine/detectors.tsx
+++ b/static/app/types/workflowEngine/detectors.tsx
@@ -80,21 +80,17 @@ export type DetectorType =
| 'uptime_domain_failure'
| 'issue_stream';
-interface BaseMetricDetectorConfig {
- thresholdPeriod: number;
-}
-
/**
* Configuration for static/threshold-based detection
*/
-interface MetricDetectorConfigStatic extends BaseMetricDetectorConfig {
+interface MetricDetectorConfigStatic {
detectionType: 'static';
}
/**
* Configuration for percentage-based change detection
*/
-interface MetricDetectorConfigPercent extends BaseMetricDetectorConfig {
+interface MetricDetectorConfigPercent {
comparisonDelta: number;
detectionType: 'percent';
}
@@ -102,11 +98,8 @@ interface MetricDetectorConfigPercent extends BaseMetricDetectorConfig {
/**
* Configuration for dynamic/anomaly detection
*/
-interface MetricDetectorConfigDynamic extends BaseMetricDetectorConfig {
+interface MetricDetectorConfigDynamic {
detectionType: 'dynamic';
- seasonality?: 'auto' | 'daily' | 'weekly' | 'monthly';
- sensitivity?: AlertRuleSensitivity;
- thresholdType?: AlertRuleThresholdType;
}
export type MetricDetectorConfig =
@@ -233,7 +226,7 @@ export interface MetricCondition {
/**
* See AnomalyDetectionHandler
*/
-interface AnomalyDetectionComparison {
+export interface AnomalyDetectionComparison {
seasonality:
| 'auto'
| 'hourly'
@@ -243,8 +236,8 @@ interface AnomalyDetectionComparison {
| 'hourly_weekly'
| 'hourly_daily_weekly'
| 'daily_weekly';
- sensitivity: 'low' | 'medium' | 'high';
- threshold_type: 0 | 1 | 2;
+ sensitivity: AlertRuleSensitivity;
+ thresholdType: AlertRuleThresholdType;
}
type MetricDataCondition = AnomalyDetectionComparison | number;
diff --git a/static/app/views/detectors/components/forms/metric/metric.tsx b/static/app/views/detectors/components/forms/metric/metric.tsx
index 16ae23de66714b..05daadd16bb89d 100644
--- a/static/app/views/detectors/components/forms/metric/metric.tsx
+++ b/static/app/views/detectors/components/forms/metric/metric.tsx
@@ -584,10 +584,14 @@ function DetectSection() {
)}
-
- {t('Resolve')}
-
-
+ {detectionType !== 'dynamic' && (
+
+
+ {t('Resolve')}
+
+
+
+ )}
);
diff --git a/static/app/views/detectors/components/forms/metric/metricFormData.tsx b/static/app/views/detectors/components/forms/metric/metricFormData.tsx
index 0f1e404ddd7b70..bc622b7dfff406 100644
--- a/static/app/views/detectors/components/forms/metric/metricFormData.tsx
+++ b/static/app/views/detectors/components/forms/metric/metricFormData.tsx
@@ -5,6 +5,7 @@ import {
DetectorPriorityLevel,
} from 'sentry/types/workflowEngine/dataConditions';
import type {
+ AnomalyDetectionComparison,
Detector,
MetricCondition,
MetricConditionGroup,
@@ -183,6 +184,22 @@ interface NewDataSource {
timeWindow: number;
}
+function createAnomalyDetectionCondition(
+ data: Pick
+): NewConditionGroup['conditions'] {
+ return [
+ {
+ type: DataConditionType.ANOMALY_DETECTION,
+ comparison: {
+ sensitivity: data.sensitivity,
+ seasonality: 'auto' as const,
+ thresholdType: data.thresholdType,
+ },
+ conditionResult: DetectorPriorityLevel.HIGH,
+ },
+ ];
+}
+
/**
* Creates escalation conditions based on priority level and available thresholds
*/
@@ -303,7 +320,11 @@ function createDataSource(data: MetricDetectorFormData): NewDataSource {
export function metricDetectorFormDataToEndpointPayload(
data: MetricDetectorFormData
): MetricDetectorUpdatePayload {
- const conditions = createConditions(data);
+ const conditions =
+ data.detectionType === 'dynamic'
+ ? createAnomalyDetectionCondition(data)
+ : createConditions(data);
+
const dataSource = createDataSource(data);
// Create config based on detection type
@@ -311,22 +332,18 @@ export function metricDetectorFormDataToEndpointPayload(
switch (data.detectionType) {
case 'percent':
config = {
- thresholdPeriod: 1,
detectionType: 'percent',
comparisonDelta: data.conditionComparisonAgo || 3600,
};
break;
case 'dynamic':
config = {
- thresholdPeriod: 1,
detectionType: 'dynamic',
- sensitivity: data.sensitivity,
};
break;
case 'static':
default:
config = {
- thresholdPeriod: 1,
detectionType: 'static',
};
break;
@@ -423,6 +440,24 @@ function processDetectorConditions(
};
}
+function getAnomalyCondition(detector: MetricDetector): AnomalyDetectionComparison {
+ const anomalyCondition = detector.conditionGroup?.conditions?.find(
+ condition => condition.type === DataConditionType.ANOMALY_DETECTION
+ );
+
+ const comparison = anomalyCondition?.comparison;
+ if (typeof comparison === 'object') {
+ return comparison;
+ }
+
+ // Fallback to default values
+ return {
+ sensitivity: AlertRuleSensitivity.MEDIUM,
+ seasonality: 'auto',
+ thresholdType: AlertRuleThresholdType.ABOVE_AND_BELOW,
+ };
+}
+
/**
* Converts a Detector to MetricDetectorFormData for editing
*/
@@ -444,6 +479,7 @@ export function metricSavedDetectorToFormData(
: DetectorDataset.SPANS;
const datasetConfig = getDatasetConfig(dataset);
+ const anomalyCondition = getAnomalyCondition(detector);
return {
// Core detector fields
@@ -471,15 +507,8 @@ export function metricSavedDetectorToFormData(
? detector.config.comparisonDelta
: DEFAULT_THRESHOLD_METRIC_FORM_DATA.conditionComparisonAgo,
- // Dynamic fields - extract from config for dynamic detectors
- sensitivity:
- detector.config.detectionType === 'dynamic' && defined(detector.config.sensitivity)
- ? detector.config.sensitivity
- : DEFAULT_THRESHOLD_METRIC_FORM_DATA.sensitivity,
- thresholdType:
- detector.config.detectionType === 'dynamic' &&
- defined(detector.config.thresholdType)
- ? detector.config.thresholdType
- : DEFAULT_THRESHOLD_METRIC_FORM_DATA.thresholdType,
+ // Dynamic fields - extract from anomaly detection condition for dynamic detectors
+ sensitivity: anomalyCondition.sensitivity,
+ thresholdType: anomalyCondition.thresholdType,
};
}
diff --git a/static/app/views/detectors/edit.spec.tsx b/static/app/views/detectors/edit.spec.tsx
index a7064c6f6e18c0..3aee57abd96c0a 100644
--- a/static/app/views/detectors/edit.spec.tsx
+++ b/static/app/views/detectors/edit.spec.tsx
@@ -24,7 +24,12 @@ import {
DataConditionType,
DetectorPriorityLevel,
} from 'sentry/types/workflowEngine/dataConditions';
-import {Dataset, EventTypes} from 'sentry/views/alerts/rules/metric/types';
+import {
+ AlertRuleSensitivity,
+ AlertRuleThresholdType,
+ Dataset,
+ EventTypes,
+} from 'sentry/views/alerts/rules/metric/types';
import {SnubaQueryType} from 'sentry/views/detectors/components/forms/metric/metricFormData';
import DetectorEdit from 'sentry/views/detectors/edit';
@@ -312,6 +317,9 @@ describe('DetectorEdit', () => {
projectId: project.id,
type: 'metric_issue',
workflowIds: mockDetector.workflowIds,
+ config: {
+ detectionType: 'static',
+ },
dataSources: [
{
environment: 'production',
@@ -330,7 +338,6 @@ describe('DetectorEdit', () => {
],
logicType: 'any',
},
- config: {detectionType: 'static', thresholdPeriod: 1},
},
})
);
@@ -627,6 +634,56 @@ describe('DetectorEdit', () => {
expect(await screen.findByText('15 minutes')).toBeInTheDocument();
});
+ it('prefills thresholdType from anomaly detection condition when editing dynamic detector', async () => {
+ const dynamicDetector = MetricDetectorFixture({
+ name: 'Dynamic Detector',
+ projectId: project.id,
+ config: {
+ detectionType: 'dynamic',
+ },
+ conditionGroup: {
+ id: 'cg-dynamic',
+ logicType: DataConditionGroupLogicType.ANY,
+ conditions: [
+ {
+ id: 'c-anomaly',
+ type: DataConditionType.ANOMALY_DETECTION,
+ comparison: {
+ sensitivity: AlertRuleSensitivity.HIGH,
+ seasonality: 'auto',
+ thresholdType: AlertRuleThresholdType.BELOW,
+ },
+ conditionResult: DetectorPriorityLevel.HIGH,
+ },
+ ],
+ },
+ });
+
+ MockApiClient.addMockResponse({
+ url: `/organizations/${organization.slug}/detectors/${dynamicDetector.id}/`,
+ body: dynamicDetector,
+ });
+
+ render(, {
+ organization,
+ initialRouterConfig: {
+ route: '/organizations/:orgId/monitors/:detectorId/edit/',
+ location: {
+ pathname: `/organizations/${organization.slug}/monitors/${dynamicDetector.id}/edit/`,
+ },
+ },
+ });
+
+ expect(
+ await screen.findByRole('link', {name: 'Dynamic Detector'})
+ ).toBeInTheDocument();
+
+ expect(screen.getByRole('radio', {name: 'Dynamic'})).toBeChecked();
+
+ // Verify thresholdType field is prefilled with "Below"
+ expect(screen.getByText('Below')).toBeInTheDocument();
+ });
+
it('calls anomaly API when using dynamic detection', async () => {
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/detectors/${mockDetector.id}/`,
diff --git a/static/app/views/detectors/list/allMonitors.spec.tsx b/static/app/views/detectors/list/allMonitors.spec.tsx
index 97fda38969a583..4192369a965a22 100644
--- a/static/app/views/detectors/list/allMonitors.spec.tsx
+++ b/static/app/views/detectors/list/allMonitors.spec.tsx
@@ -53,7 +53,6 @@ describe('DetectorsList', () => {
config: {
detectionType: 'percent',
comparisonDelta: 10,
- thresholdPeriod: 10,
},
conditionGroup: {
id: '1',
diff --git a/static/app/views/detectors/new-setting.spec.tsx b/static/app/views/detectors/new-setting.spec.tsx
index ebd72ccd4bc029..2fbc48cf2ad6e5 100644
--- a/static/app/views/detectors/new-setting.spec.tsx
+++ b/static/app/views/detectors/new-setting.spec.tsx
@@ -197,7 +197,6 @@ describe('DetectorEdit', () => {
},
config: {
detectionType: 'static',
- thresholdPeriod: 1,
},
dataSources: [
{
@@ -282,7 +281,7 @@ describe('DetectorEdit', () => {
],
logicType: 'any',
},
- config: {detectionType: 'static', thresholdPeriod: 1},
+ config: {detectionType: 'static'},
dataSources: [
{
aggregate: 'count_unique(tags[sentry:user])',
@@ -352,7 +351,7 @@ describe('DetectorEdit', () => {
],
logicType: 'any',
},
- config: {detectionType: 'static', thresholdPeriod: 1},
+ config: {detectionType: 'static'},
dataSources: [
{
aggregate: 'count()',
@@ -570,15 +569,23 @@ describe('DetectorEdit', () => {
projectId: project.id,
owner: null,
workflowIds: [],
- // Dynamic detection should have empty conditions (no resolution thresholds)
+ // Dynamic detection should have anomaly detection condition
conditionGroup: {
- conditions: [],
+ conditions: [
+ {
+ type: 'anomaly_detection',
+ comparison: {
+ sensitivity: 'high',
+ seasonality: 'auto',
+ thresholdType: 0,
+ },
+ conditionResult: 75,
+ },
+ ],
logicType: 'any',
},
config: {
detectionType: 'dynamic',
- sensitivity: 'high',
- thresholdPeriod: 1,
},
dataSources: [
{
diff --git a/tests/js/fixtures/detectors.ts b/tests/js/fixtures/detectors.ts
index d3585daaddebec..5f1d61f71cdf06 100644
--- a/tests/js/fixtures/detectors.ts
+++ b/tests/js/fixtures/detectors.ts
@@ -78,7 +78,6 @@ export function MetricDetectorFixture(
name: 'detector',
config: {
detectionType: 'static',
- thresholdPeriod: 1,
},
type: 'metric_issue',
enabled: true,