2525 get_environment_flags_queryset ,
2626)
2727from users .models import FFAdminUser
28- from webhooks .webhooks import WebhookEventType , call_environment_webhooks
28+ from webhooks .tasks import call_environment_webhooks , call_organisation_webhooks
29+ from webhooks .webhooks import WebhookEventType
2930
3031if typing .TYPE_CHECKING :
3132 from environments .models import Environment
@@ -131,6 +132,102 @@ def _create_initial_feature_versions(environment: "Environment"): # type: ignor
131132 )
132133
133134
135+ def _trigger_feature_state_webhooks_for_version (
136+ environment_feature_version : EnvironmentFeatureVersion ,
137+ ) -> None :
138+ """
139+ Trigger FLAG_UPDATED webhooks for feature states that have changed in the newly published version.
140+
141+ This allows webhook consumers to receive granular per-featurestate updates in the same
142+ format as non-versioned environments, while NEW_VERSION_PUBLISHED serves as a
143+ summary event.
144+ """
145+ from environments .models import Webhook
146+ from webhooks .constants import WEBHOOK_DATETIME_FORMAT
147+
148+ # Get metadata from the version
149+ timestamp = environment_feature_version .published_at .strftime ( # type: ignore[union-attr]
150+ WEBHOOK_DATETIME_FORMAT
151+ )
152+ changed_by = (
153+ environment_feature_version .published_by .email
154+ if environment_feature_version .published_by
155+ else (
156+ environment_feature_version .published_by_api_key .name
157+ if environment_feature_version .published_by_api_key
158+ else ""
159+ )
160+ )
161+
162+ # Get only the feature states that have changed
163+ changed_feature_states = environment_feature_version .get_updated_feature_states ()
164+
165+ # Get previous version for retrieving previous states
166+ previous_version = environment_feature_version .get_previous_version ()
167+ previous_feature_states_map = {}
168+ if previous_version :
169+ for fs in previous_version .feature_states .all ():
170+ segment_id = fs .feature_segment .segment_id if fs .feature_segment else None
171+ key = (fs .identity_id , segment_id )
172+ previous_feature_states_map [key ] = fs
173+
174+ # Trigger FLAG_UPDATED webhooks for each changed feature state
175+ for feature_state in changed_feature_states :
176+ # Get the current state data
177+ assert feature_state .environment is not None
178+ new_state = Webhook .generate_webhook_feature_state_data (
179+ feature_state .feature ,
180+ environment = feature_state .environment ,
181+ enabled = feature_state .enabled ,
182+ value = feature_state .get_feature_state_value (),
183+ identity_id = feature_state .identity_id ,
184+ identity_identifier = getattr (feature_state .identity , "identifier" , None ),
185+ feature_segment = feature_state .feature_segment ,
186+ )
187+
188+ # Build webhook data
189+ data = {
190+ "new_state" : new_state ,
191+ "changed_by" : changed_by ,
192+ "timestamp" : timestamp ,
193+ }
194+
195+ # Add previous state if it exists
196+ segment_id = (
197+ feature_state .feature_segment .segment_id
198+ if feature_state .feature_segment
199+ else None
200+ )
201+ key = (feature_state .identity_id , segment_id )
202+ previous_fs = previous_feature_states_map .get (key )
203+
204+ if previous_fs :
205+ assert previous_fs .environment is not None
206+ previous_state = Webhook .generate_webhook_feature_state_data (
207+ previous_fs .feature ,
208+ environment = previous_fs .environment ,
209+ enabled = previous_fs .enabled ,
210+ value = previous_fs .get_feature_state_value (),
211+ identity_id = previous_fs .identity_id ,
212+ identity_identifier = getattr (previous_fs .identity , "identifier" , None ),
213+ feature_segment = previous_fs .feature_segment ,
214+ )
215+ data ["previous_state" ] = previous_state
216+
217+ # Trigger webhooks
218+ call_environment_webhooks (
219+ environment_id = environment_feature_version .environment_id ,
220+ data = data ,
221+ event_type = WebhookEventType .FLAG_UPDATED .value ,
222+ )
223+
224+ call_organisation_webhooks (
225+ organisation_id = environment_feature_version .environment .project .organisation_id ,
226+ data = data ,
227+ event_type = WebhookEventType .FLAG_UPDATED .value ,
228+ )
229+
230+
134231@register_task_handler ()
135232def trigger_update_version_webhooks (environment_feature_version_uuid : str ) -> None :
136233 environment_feature_version = EnvironmentFeatureVersion .objects .get (
@@ -140,6 +237,10 @@ def trigger_update_version_webhooks(environment_feature_version_uuid: str) -> No
140237 logger .exception ("Feature version has not been published." )
141238 return
142239
240+ # Trigger FLAG_UPDATED webhooks for any feature states that have changed
241+ _trigger_feature_state_webhooks_for_version (environment_feature_version )
242+
243+ # Then trigger the NEW_VERSION_PUBLISHED webhook as a summary event
143244 data = environment_feature_version_webhook_schema .dump (environment_feature_version )
144245 call_environment_webhooks (
145246 environment_id = environment_feature_version .environment_id ,
0 commit comments