99from audit .related_object_type import RelatedObjectType
1010from audit .serializers import AuditLogListSerializer
1111from audit .services import get_audited_instance_from_audit_log_record
12- from features .models import FeatureState
12+ from features .models import FeatureState , FeatureStateValue
1313from features .signals import feature_state_change_went_live
1414from integrations .common .models import IntegrationsModel
1515from integrations .datadog .datadog import DataDogWrapper
@@ -212,21 +212,30 @@ def send_audit_log_event_to_slack(sender, instance, **kwargs): # type: ignore[n
212212@receiver (post_save , sender = AuditLog )
213213@track_only ([RelatedObjectType .FEATURE_STATE ])
214214def send_feature_flag_went_live_signal (sender , instance , ** kwargs ): # type: ignore[no-untyped-def]
215- feature_state = get_audited_instance_from_audit_log_record (instance )
216- if not isinstance (feature_state , FeatureState ):
215+ audited_instance = get_audited_instance_from_audit_log_record (instance )
216+
217+ # Handle both FeatureState and FeatureStateValue audit logs
218+ # FeatureStateValue changes also have related_object_type=FEATURE_STATE
219+ if isinstance (audited_instance , FeatureStateValue ):
220+ feature_state = audited_instance .feature_state
221+ elif isinstance (audited_instance , FeatureState ):
222+ feature_state = audited_instance
223+ else :
217224 return
218225
219226 if feature_state .is_scheduled :
220227 return # This is handled by audit.tasks.create_feature_state_went_live_audit_log
221228
222- feature_state_change_went_live .send (instance )
229+ feature_state_change_went_live .send (feature_state , audit_log = instance )
223230
224231
225232@receiver (feature_state_change_went_live )
226- def send_audit_log_event_to_sentry (sender : AuditLog , ** kwargs : Any ) -> None :
233+ def send_audit_log_event_to_sentry (
234+ sender : FeatureState , audit_log : AuditLog , ** kwargs : Any
235+ ) -> None :
227236 try :
228237 sentry_configuration = SentryChangeTrackingConfiguration .objects .get (
229- environment = sender .environment ,
238+ environment = audit_log .environment ,
230239 deleted_at__isnull = True ,
231240 )
232241 except SentryChangeTrackingConfiguration .DoesNotExist :
@@ -237,4 +246,35 @@ def send_audit_log_event_to_sentry(sender: AuditLog, **kwargs: Any) -> None:
237246 secret = sentry_configuration .secret ,
238247 )
239248
240- _track_event_async (sender , sentry_change_tracking ) # type: ignore[no-untyped-call]
249+ _track_event_async (audit_log , sentry_change_tracking ) # type: ignore[no-untyped-call]
250+
251+
252+ @receiver (feature_state_change_went_live )
253+ def trigger_feature_state_change_webhooks (
254+ sender : FeatureState ,
255+ ** kwargs : Any ,
256+ ) -> None :
257+ """
258+ Trigger FLAG_UPDATED webhooks when a feature state change goes live.
259+
260+ Triggered from AuditLog post_save. Fetches a fresh feature state from the
261+ database to ensure we get the latest data (including FeatureStateValue),
262+ since drf-writable-nested saves the parent before nested objects.
263+ """
264+ from features import tasks
265+
266+ # Fetch fresh data from the database to ensure we have the latest state
267+ # This is necessary because:
268+ # 1. drf-writable-nested saves FeatureState before FeatureStateValue
269+ # 2. The history record's instance may have stale cached values
270+ try :
271+ fresh_feature_state = FeatureState .objects .get (id = sender .id )
272+ except FeatureState .DoesNotExist :
273+ # Skip deleted feature states - handled in views
274+ return
275+
276+ # Skip versioned environments - handled by trigger_update_version_webhooks
277+ if fresh_feature_state .environment_feature_version_id :
278+ return
279+
280+ tasks .trigger_feature_state_change_webhooks (fresh_feature_state )
0 commit comments