88
99from sentry import features , quotas
1010from sentry .constants import ObjectStatus
11- from sentry .incidents .logic import enable_disable_subscriptions
11+ from sentry .incidents .logic import (
12+ DEFAULT_CMP_ALERT_RULE_RESOLUTION_MULTIPLIER ,
13+ enable_disable_subscriptions ,
14+ get_alert_resolution ,
15+ )
1216from sentry .incidents .models .alert_rule import AlertRuleDetectionType
1317from sentry .relay .config .metric_extraction import on_demand_metrics_feature_flags
1418from sentry .search .eap .trace_metrics .validator import validate_trace_metrics_aggregate
@@ -239,6 +243,24 @@ def _validate_extrapolation_mode(self, extrapolation_mode: ExtrapolationMode) ->
239243 f"{ extrapolation_mode .name .lower ()} extrapolation mode is not supported for new detectors. Allowed modes are: client_and_server_weighted, unknown."
240244 )
241245
246+ def _get_resolution_for_window (
247+ self , time_window_seconds : int , detection_type : str | None , comparison_delta : int | None
248+ ) -> timedelta :
249+ """
250+ Compute the appropriate SnubaQuery resolution for a given time window,
251+ mirroring the logic in create_alert_rule / update_alert_rule.
252+ """
253+ organization = self .context ["organization" ]
254+
255+ if detection_type == AlertRuleDetectionType .DYNAMIC :
256+ resolution = timedelta (seconds = time_window_seconds )
257+ else :
258+ resolution = get_alert_resolution (time_window_seconds // 60 , organization )
259+ if comparison_delta is not None :
260+ resolution *= DEFAULT_CMP_ALERT_RULE_RESOLUTION_MULTIPLIER
261+
262+ return resolution
263+
242264 def get_quota (self ) -> DetectorQuota :
243265 organization = self .context .get ("organization" )
244266 request = self .context .get ("request" )
@@ -336,14 +358,21 @@ def update_data_source(
336358 data_source .get ("extrapolation_mode" , snuba_query .extrapolation_mode )
337359 )
338360
361+ new_time_window = data_source .get ("time_window" , snuba_query .time_window )
362+ resolution = self ._get_resolution_for_window (
363+ new_time_window ,
364+ instance .config .get ("detection_type" ),
365+ instance .config .get ("comparison_delta" ),
366+ )
367+
339368 update_snuba_query (
340369 snuba_query = snuba_query ,
341370 query_type = data_source .get ("query_type" , snuba_query .type ),
342371 dataset = data_source .get ("dataset" , snuba_query .dataset ),
343372 query = data_source .get ("query" , snuba_query .query ),
344373 aggregate = data_source .get ("aggregate" , snuba_query .aggregate ),
345- time_window = timedelta (seconds = data_source . get ( "time_window" , snuba_query . time_window ) ),
346- resolution = timedelta ( seconds = data_source . get ( " resolution" , snuba_query . resolution )) ,
374+ time_window = timedelta (seconds = new_time_window ),
375+ resolution = resolution ,
347376 environment = data_source .get ("environment" , snuba_query .environment ),
348377 event_types = data_source .get ("event_types" , [event_type for event_type in event_types ]),
349378 extrapolation_mode = extrapolation_mode ,
@@ -407,6 +436,10 @@ def update(self, instance: Detector, validated_data: dict[str, Any]):
407436
408437 if data_source is not None :
409438 self .update_data_source (instance , data_source , seer_updated )
439+ elif "config" in validated_data :
440+ # Config changed (e.g. detection_type or comparison_delta) without a
441+ # data_source update — recalculate resolution to match the new config.
442+ self .update_data_source (instance , {}, seer_updated )
410443
411444 instance .save ()
412445
@@ -415,10 +448,20 @@ def update(self, instance: Detector, validated_data: dict[str, Any]):
415448
416449 def create (self , validated_data : dict [str , Any ]):
417450 if "data_sources" in validated_data :
451+ config = validated_data .get ("config" , {})
452+ detection_type = config .get ("detection_type" )
453+ comparison_delta = config .get ("comparison_delta" )
454+
418455 for validated_data_source in validated_data ["data_sources" ]:
419456 self ._validate_transaction_dataset_deprecation (validated_data_source .get ("dataset" ))
420457 self ._validate_extrapolation_mode (validated_data_source .get ("extrapolation_mode" ))
421458
459+ time_window = validated_data_source .get ("time_window" )
460+ if time_window is not None :
461+ validated_data_source ["resolution" ] = self ._get_resolution_for_window (
462+ time_window , detection_type , comparison_delta
463+ )
464+
422465 detector = super ().create (validated_data )
423466
424467 if detector .config .get ("detection_type" ) == AlertRuleDetectionType .DYNAMIC .value :
0 commit comments