11import re
2- from typing import Any , Literal
2+ from typing import Literal
33
44import sentry_sdk
55from cronsim import CronSimError
66from django .core .exceptions import ValidationError
7- from django .db .models import F
8- from django .db .models .functions import TruncMinute
97from django .utils import timezone
108from drf_spectacular .types import OpenApiTypes
119from drf_spectacular .utils import extend_schema_field , extend_schema_serializer
1210from rest_framework import serializers
1311
14- from sentry import audit_log , quotas
12+ from sentry import quotas
1513from sentry .api .fields .actor import ActorField
1614from sentry .api .fields .empty_integer import EmptyIntegerField
1715from sentry .api .fields .sentry_slug import SentrySerializerSlugField
2018from sentry .constants import ObjectStatus
2119from sentry .db .models import BoundedPositiveIntegerField
2220from sentry .db .models .fields .slug import DEFAULT_SLUG_MAX_LENGTH
23- from sentry .models .project import Project
2421from sentry .monitors .constants import MAX_MARGIN , MAX_THRESHOLD , MAX_TIMEOUT
25- from sentry .monitors .models import (
26- CheckInStatus ,
27- Monitor ,
28- MonitorCheckIn ,
29- MonitorEnvironment ,
30- ScheduleType ,
31- )
22+ from sentry .monitors .models import CheckInStatus , Monitor , ScheduleType
3223from sentry .monitors .schedule import get_next_schedule , get_prev_schedule
3324from sentry .monitors .types import CrontabSchedule , slugify_monitor_slug
34- from sentry .monitors .utils import (
35- create_issue_alert_rule ,
36- get_checkin_margin ,
37- get_max_runtime ,
38- signal_monitor_created ,
39- update_issue_alert_rule ,
40- )
41- from sentry .utils .audit import create_audit_entry
25+ from sentry .monitors .utils import create_issue_alert_rule , signal_monitor_created
4226from sentry .utils .dates import AVAILABLE_TIMEZONES
4327from sentry .utils .outcomes import Outcome
4428
@@ -178,7 +162,7 @@ def bind(self, *args, **kwargs):
178162 super ().bind (* args , ** kwargs )
179163 # Inherit instance data when used as a nested serializer
180164 if self .parent .instance :
181- self .instance = self .parent .instance .config
165+ self .instance = self .parent .instance .get ( " config" )
182166 self .partial = self .parent .partial
183167
184168 def validate_schedule_type (self , value ):
@@ -316,7 +300,7 @@ def validate_slug(self, value):
316300
317301 value = slugify_monitor_slug (value )
318302 # Ignore if slug is equal to current value
319- if self .instance and value == self .instance .slug :
303+ if self .instance and value == self .instance .get ( " slug" ) :
320304 return value
321305
322306 if Monitor .objects .filter (
@@ -325,6 +309,14 @@ def validate_slug(self, value):
325309 raise ValidationError (f'The slug "{ value } " is already in use.' )
326310 return value
327311
312+ def update (self , instance , validated_data ):
313+ config = instance .get ("config" , {})
314+ config .update (validated_data .get ("config" , {}))
315+ instance .update (validated_data )
316+ if "config" in instance or "config" in validated_data :
317+ instance ["config" ] = config
318+ return instance
319+
328320 def create (self , validated_data ):
329321 project = validated_data .get ("project" , self .context .get ("project" ))
330322 organization = self .context ["organization" ]
@@ -369,118 +361,6 @@ def create(self, validated_data):
369361 monitor .update (config = config )
370362 return monitor
371363
372- def update (self , instance , validated_data ):
373- """
374- Update an existing Monitor instance.
375- """
376- if "project" in validated_data and validated_data ["project" ].id != instance .project_id :
377- raise serializers .ValidationError (
378- {"detail" : {"message" : "existing monitors may not be moved between projects" }}
379- )
380-
381- existing_config = instance .config .copy ()
382- existing_margin = existing_config .get ("checkin_margin" )
383- existing_max_runtime = existing_config .get ("max_runtime" )
384- existing_slug = instance .slug
385-
386- params : dict [str , Any ] = {}
387- if "owner" in validated_data :
388- owner = validated_data ["owner" ]
389- params ["owner_user_id" ] = None
390- params ["owner_team_id" ] = None
391- if owner and owner .is_user :
392- params ["owner_user_id" ] = owner .id
393- elif owner and owner .is_team :
394- params ["owner_team_id" ] = owner .id
395-
396- if "name" in validated_data :
397- params ["name" ] = validated_data ["name" ]
398- if "slug" in validated_data :
399- params ["slug" ] = validated_data ["slug" ]
400- if "status" in validated_data :
401- params ["status" ] = validated_data ["status" ]
402- if "is_muted" in validated_data :
403- params ["is_muted" ] = validated_data ["is_muted" ]
404- if "config" in validated_data :
405- params ["config" ] = validated_data ["config" ]
406-
407- if "status" in params :
408- # Attempt to assign a monitor seat
409- if params ["status" ] == ObjectStatus .ACTIVE and instance .status != ObjectStatus .ACTIVE :
410- outcome = quotas .backend .assign_monitor_seat (instance )
411- # The MonitorValidator checks if a seat assignment is available.
412- # This protects against a race condition
413- if outcome != Outcome .ACCEPTED :
414- raise serializers .ValidationError (
415- {"status" : "Failed to enable monitor due to quota limits" }
416- )
417-
418- # Attempt to unassign the monitor seat
419- if (
420- params ["status" ] == ObjectStatus .DISABLED
421- and instance .status != ObjectStatus .DISABLED
422- ):
423- quotas .backend .disable_monitor_seat (instance )
424-
425- if params :
426- instance .update (** params )
427- create_audit_entry (
428- request = self .context ["request" ],
429- organization_id = instance .organization_id ,
430- target_object = instance .id ,
431- event = audit_log .get_event_id ("MONITOR_EDIT" ),
432- data = instance .get_audit_log_data (),
433- )
434-
435- # Update monitor slug in billing
436- if "slug" in params :
437- quotas .backend .update_monitor_slug (existing_slug , params ["slug" ], instance .project_id )
438-
439- if "config" in validated_data :
440- new_config = validated_data ["config" ]
441- checkin_margin = new_config .get ("checkin_margin" )
442- if checkin_margin != existing_margin :
443- MonitorEnvironment .objects .filter (monitor_id = instance .id ).update (
444- next_checkin_latest = F ("next_checkin" ) + get_checkin_margin (checkin_margin )
445- )
446-
447- max_runtime = new_config .get ("max_runtime" )
448- if max_runtime != existing_max_runtime :
449- MonitorCheckIn .objects .filter (
450- monitor_id = instance .id , status = CheckInStatus .IN_PROGRESS
451- ).update (timeout_at = TruncMinute (F ("date_added" )) + get_max_runtime (max_runtime ))
452-
453- # Update alert rule after in case slug or name changed
454- if "alert_rule" in validated_data :
455- alert_rule_data = validated_data ["alert_rule" ]
456- request = self .context .get ("request" )
457- if not request :
458- return instance
459-
460- project = Project .objects .get (id = instance .project_id )
461-
462- # Check to see if rule exists
463- issue_alert_rule = instance .get_issue_alert_rule ()
464- # If rule exists, update as necessary
465- if issue_alert_rule :
466- issue_alert_rule_id = update_issue_alert_rule (
467- request , project , instance , issue_alert_rule , alert_rule_data
468- )
469- # If rule does not exist, create
470- else :
471- # Type assertion for mypy - create_issue_alert_rule expects AuthenticatedHttpRequest
472- # but in tests we might have a regular Request object
473- issue_alert_rule_id = create_issue_alert_rule (
474- request , project , instance , alert_rule_data
475- )
476-
477- if issue_alert_rule_id :
478- # If config is not sent, use existing config to update alert_rule_id
479- instance .config ["alert_rule_id" ] = issue_alert_rule_id
480- instance .update (config = instance .config )
481-
482- return instance
483-
484364
485365class TraceContextValidator (serializers .Serializer ):
486366 trace_id = serializers .UUIDField (format = "hex" )
0 commit comments