Skip to content

Commit ec45a23

Browse files
committed
Merge main into refactor/segment-change-requests-cleanup
2 parents e4d5b55 + 7e8f32e commit ec45a23

File tree

21 files changed

+1138
-148
lines changed

21 files changed

+1138
-148
lines changed

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "2.199.0"
2+
".": "2.200.0"
33
}

CHANGELOG.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,45 @@
11
# Changelog
22

3+
## [2.200.0](https://github.com/Flagsmith/flagsmith/compare/v2.199.0...v2.200.0) (2025-11-11)
4+
5+
6+
### Features
7+
8+
* add pylon chat ([#6214](https://github.com/Flagsmith/flagsmith/issues/6214)) ([7a5f64d](https://github.com/Flagsmith/flagsmith/commit/7a5f64dd4643012c0e9b691316de326c1478f89e))
9+
* added-query-param-to-get-segment-feature-states ([#6156](https://github.com/Flagsmith/flagsmith/issues/6156)) ([9ad82d5](https://github.com/Flagsmith/flagsmith/commit/9ad82d53818eecb6cda44de9cf61a9d5be3ae46e))
10+
* context values - implement multi-select dropdown on envs ([#6239](https://github.com/Flagsmith/flagsmith/issues/6239)) ([331cd9e](https://github.com/Flagsmith/flagsmith/commit/331cd9eadf74e62bb5809dd9f279b97df8f54589))
11+
* trigger FLAG_UPDATED webhooks for v2 versioning environments ([#6240](https://github.com/Flagsmith/flagsmith/issues/6240)) ([94afd46](https://github.com/Flagsmith/flagsmith/commit/94afd46a263172b11b9e212aa23cbe55bc644e20))
12+
13+
14+
### Bug Fixes
15+
16+
* added-tags-in-approval-has-permissions ([#6227](https://github.com/Flagsmith/flagsmith/issues/6227)) ([d4229a9](https://github.com/Flagsmith/flagsmith/commit/d4229a9f9a6aec01e201b5c43b13d0d738153b97))
17+
* metrics endpoint identity overrides ([#6159](https://github.com/Flagsmith/flagsmith/issues/6159)) ([b44cf42](https://github.com/Flagsmith/flagsmith/commit/b44cf4258f3e518e79448805dba08aab1a51e333))
18+
* Pin firefox version ([#6244](https://github.com/Flagsmith/flagsmith/issues/6244)) ([3f8b260](https://github.com/Flagsmith/flagsmith/commit/3f8b260832b9e964d2f4480394491bc83d35e34f))
19+
* properly deal with context values dropdown ([#6222](https://github.com/Flagsmith/flagsmith/issues/6222)) ([bca5744](https://github.com/Flagsmith/flagsmith/commit/bca5744614ccaca1b9eeec648748eca741eb8f0e))
20+
* resolve flaky test due to non-deterministic variation ordering ([#6269](https://github.com/Flagsmith/flagsmith/issues/6269)) ([f8a3381](https://github.com/Flagsmith/flagsmith/commit/f8a3381120649276440987dbe4bb7daf05fa6bc8))
21+
* Revert "infra: Remove PGP key from SaaS build" ([#6197](https://github.com/Flagsmith/flagsmith/issues/6197)) ([db16264](https://github.com/Flagsmith/flagsmith/commit/db162648b30105ae123bcb2b86be628a7f5535ea))
22+
* segment change request warning shows on all segments ([#6255](https://github.com/Flagsmith/flagsmith/issues/6255)) ([560a04d](https://github.com/Flagsmith/flagsmith/commit/560a04d0e8c40314e4bac4e0216d4b0360b324bf))
23+
* show whitespace warnings for IN operator comma-separated values ([#6203](https://github.com/Flagsmith/flagsmith/issues/6203)) ([1b75a10](https://github.com/Flagsmith/flagsmith/commit/1b75a10d0b951c8db30ce4a9322b9eb81ee11288))
24+
* **sse_stream_access_logs:** Add timeout ([#6198](https://github.com/Flagsmith/flagsmith/issues/6198)) ([4d15821](https://github.com/Flagsmith/flagsmith/commit/4d1582119cad72ea817c64bdfd22125d17c7a6a5))
25+
* switched-allow-context-values-to-disabled ([#6216](https://github.com/Flagsmith/flagsmith/issues/6216)) ([f95a4f6](https://github.com/Flagsmith/flagsmith/commit/f95a4f6f47b37b2b747a1fc58ff496e82aca3a64))
26+
27+
28+
### Infrastructure (Flagsmith SaaS Only)
29+
30+
* Remove PGP key from SaaS build ([#6194](https://github.com/Flagsmith/flagsmith/issues/6194)) ([9b43a37](https://github.com/Flagsmith/flagsmith/commit/9b43a379de18f0ddf18d7de470f94e9cb8531bf2))
31+
32+
33+
### Dependency Updates
34+
35+
* bump django from 4.2.25 to 4.2.26 in /api ([#6249](https://github.com/Flagsmith/flagsmith/issues/6249)) ([4e474f1](https://github.com/Flagsmith/flagsmith/commit/4e474f1e7615fa40691da102354e7e8498a579b9))
36+
* Bump flagsmith-common from 2.2.2 to 2.2.3 ([#6235](https://github.com/Flagsmith/flagsmith/issues/6235)) ([7474ba9](https://github.com/Flagsmith/flagsmith/commit/7474ba9961912be68cae6605179b3dc5a7b02605))
37+
* Bump flagsmith-common from 2.2.3 to 2.2.4 ([#6237](https://github.com/Flagsmith/flagsmith/issues/6237)) ([5926910](https://github.com/Flagsmith/flagsmith/commit/5926910729572f5ff6f330f9c309ae78f5f38f29))
38+
* bump flagsmith-common to 2.2.2 ([#6225](https://github.com/Flagsmith/flagsmith/issues/6225)) ([786cbd9](https://github.com/Flagsmith/flagsmith/commit/786cbd9d09605a35c3df5044b1fad099ed9eba91))
39+
* bump flagsmith-workflows to 3.1.0 ([#6140](https://github.com/Flagsmith/flagsmith/issues/6140)) ([33ad626](https://github.com/Flagsmith/flagsmith/commit/33ad6265af25e9887f2c158ccc381db449dabf06))
40+
* update flagsmith js client dependency ([#6257](https://github.com/Flagsmith/flagsmith/issues/6257)) ([c4b5a4a](https://github.com/Flagsmith/flagsmith/commit/c4b5a4a97bd1416bdd870a0fce5bceac731edb23))
41+
* update flagsmith-common 2.2.6 ([#6267](https://github.com/Flagsmith/flagsmith/issues/6267)) ([64aa842](https://github.com/Flagsmith/flagsmith/commit/64aa842e6ec7f5ac283ae3bb3171983bfcfe6f77))
42+
343
## [2.199.0](https://github.com/Flagsmith/flagsmith/compare/v2.198.1...v2.199.0) (2025-10-22)
444

545

api/environments/models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -635,9 +635,9 @@ def generate_webhook_feature_state_data(
635635
environment: Environment,
636636
enabled: bool,
637637
value: typing.Union[str, int, bool, type(None)], # type: ignore[valid-type]
638-
identity_id: typing.Union[int, str] = None, # type: ignore[assignment]
639-
identity_identifier: str = None, # type: ignore[assignment]
640-
feature_segment: FeatureSegment = None, # type: ignore[assignment]
638+
identity_id: int | str | None = None,
639+
identity_identifier: str | None = None,
640+
feature_segment: FeatureSegment | None = None,
641641
) -> dict: # type: ignore[type-arg]
642642
if (identity_id or identity_identifier) and not (
643643
identity_id and identity_identifier

api/features/tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def _get_feature_state_webhook_data(feature_state, previous=False): # type: ign
9090
enabled=feature_state.enabled,
9191
value=feature_state_value,
9292
identity_id=feature_state.identity_id,
93-
identity_identifier=getattr(feature_state.identity, "identifier", None), # type: ignore[arg-type]
93+
identity_identifier=getattr(feature_state.identity, "identifier", None),
9494
feature_segment=feature_state.feature_segment,
9595
)
9696

api/features/versioning/tasks.py

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@
2323
)
2424
from features.versioning.versioning_service import (
2525
get_environment_flags_queryset,
26+
get_updated_feature_states_for_version,
2627
)
2728
from users.models import FFAdminUser
28-
from webhooks.webhooks import WebhookEventType, call_environment_webhooks
29+
from webhooks import mappers as webhook_mappers
30+
from webhooks.tasks import call_environment_webhooks, call_organisation_webhooks
31+
from webhooks.webhooks import WebhookEventType
2932

3033
if typing.TYPE_CHECKING:
3134
from environments.models import Environment
@@ -131,6 +134,98 @@ def _create_initial_feature_versions(environment: "Environment"): # type: ignor
131134
)
132135

133136

137+
def _trigger_feature_state_webhooks_for_version(
138+
environment_feature_version: EnvironmentFeatureVersion,
139+
) -> None:
140+
"""
141+
Trigger FLAG_UPDATED webhooks for feature states that have changed in the newly published version.
142+
143+
This allows webhook consumers to receive granular per-featurestate updates in the same
144+
format as non-versioned environments, while NEW_VERSION_PUBLISHED serves as a
145+
summary event.
146+
"""
147+
from environments.models import Webhook
148+
149+
# Get metadata from the version
150+
assert environment_feature_version.published_at is not None
151+
timestamp = webhook_mappers.datetime_to_webhook_timestamp(
152+
environment_feature_version.published_at
153+
)
154+
changed_by = webhook_mappers.user_or_key_to_changed_by(
155+
user=environment_feature_version.published_by,
156+
api_key=environment_feature_version.published_by_api_key,
157+
)
158+
159+
changed_feature_states = get_updated_feature_states_for_version(
160+
environment_feature_version
161+
)
162+
163+
# Get previous version for retrieving previous states
164+
previous_version = environment_feature_version.get_previous_version()
165+
previous_feature_states_map = {}
166+
if previous_version:
167+
for fs in previous_version.feature_states.all():
168+
segment_id = fs.feature_segment.segment_id if fs.feature_segment else None
169+
key = (fs.identity_id, segment_id)
170+
previous_feature_states_map[key] = fs
171+
172+
# Trigger FLAG_UPDATED webhooks for each changed feature state
173+
for feature_state in changed_feature_states:
174+
# Get the current state data
175+
assert feature_state.environment is not None
176+
new_state = Webhook.generate_webhook_feature_state_data(
177+
feature_state.feature,
178+
environment=feature_state.environment,
179+
enabled=feature_state.enabled,
180+
value=feature_state.get_feature_state_value(),
181+
identity_id=feature_state.identity_id,
182+
identity_identifier=getattr(feature_state.identity, "identifier", None),
183+
feature_segment=feature_state.feature_segment,
184+
)
185+
186+
# Build webhook data
187+
data = {
188+
"new_state": new_state,
189+
"changed_by": changed_by,
190+
"timestamp": timestamp,
191+
}
192+
193+
# Add previous state if it exists
194+
segment_id = (
195+
feature_state.feature_segment.segment_id
196+
if feature_state.feature_segment
197+
else None
198+
)
199+
key = (feature_state.identity_id, segment_id)
200+
previous_fs = previous_feature_states_map.get(key)
201+
202+
if previous_fs:
203+
assert previous_fs.environment is not None
204+
previous_state = Webhook.generate_webhook_feature_state_data(
205+
previous_fs.feature,
206+
environment=previous_fs.environment,
207+
enabled=previous_fs.enabled,
208+
value=previous_fs.get_feature_state_value(),
209+
identity_id=previous_fs.identity_id,
210+
identity_identifier=getattr(previous_fs.identity, "identifier", None),
211+
feature_segment=previous_fs.feature_segment,
212+
)
213+
data["previous_state"] = previous_state
214+
215+
# Trigger webhooks
216+
call_environment_webhooks(
217+
environment_id=environment_feature_version.environment_id,
218+
data=data,
219+
event_type=WebhookEventType.FLAG_UPDATED.value,
220+
)
221+
222+
call_organisation_webhooks(
223+
organisation_id=environment_feature_version.environment.project.organisation_id,
224+
data=data,
225+
event_type=WebhookEventType.FLAG_UPDATED.value,
226+
)
227+
228+
134229
@register_task_handler()
135230
def trigger_update_version_webhooks(environment_feature_version_uuid: str) -> None:
136231
environment_feature_version = EnvironmentFeatureVersion.objects.get(
@@ -140,6 +235,10 @@ def trigger_update_version_webhooks(environment_feature_version_uuid: str) -> No
140235
logger.exception("Feature version has not been published.")
141236
return
142237

238+
# Trigger FLAG_UPDATED webhooks for any feature states that have changed
239+
_trigger_feature_state_webhooks_for_version(environment_feature_version)
240+
241+
# Then trigger the NEW_VERSION_PUBLISHED webhook as a summary event
143242
data = environment_feature_version_webhook_schema.dump(environment_feature_version)
144243
call_environment_webhooks(
145244
environment_id=environment_feature_version.environment_id,

api/features/versioning/versioning_service.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,52 @@ def get_current_live_environment_feature_version(
101101
)
102102

103103

104+
def get_updated_feature_states_for_version(
105+
version: EnvironmentFeatureVersion,
106+
) -> list[FeatureState]:
107+
"""
108+
Returns feature states that changed compared to the previous version.
109+
"""
110+
111+
def get_match_key(fs: FeatureState) -> tuple[int | None, int | None]:
112+
segment_id = fs.feature_segment.segment_id if fs.feature_segment else None
113+
return (fs.identity_id, segment_id)
114+
115+
def multivariate_values_changed(
116+
fs: FeatureState, previous_fs: FeatureState
117+
) -> bool:
118+
current_mv_values = {
119+
mv.multivariate_feature_option_id: mv.percentage_allocation
120+
for mv in fs.multivariate_feature_state_values.all()
121+
}
122+
previous_mv_values = {
123+
mv.multivariate_feature_option_id: mv.percentage_allocation
124+
for mv in previous_fs.multivariate_feature_state_values.all()
125+
}
126+
return current_mv_values != previous_mv_values
127+
128+
previous_version = version.get_previous_version()
129+
previous_feature_states_map = (
130+
{get_match_key(fs): fs for fs in previous_version.feature_states.all()}
131+
if previous_version
132+
else {}
133+
)
134+
135+
changed_feature_states = []
136+
for feature_state in version.feature_states.all():
137+
previous_fs = previous_feature_states_map.get(get_match_key(feature_state))
138+
139+
if previous_fs is None or (
140+
feature_state.enabled != previous_fs.enabled
141+
or feature_state.get_feature_state_value()
142+
!= previous_fs.get_feature_state_value()
143+
or multivariate_values_changed(feature_state, previous_fs)
144+
):
145+
changed_feature_states.append(feature_state)
146+
147+
return changed_feature_states
148+
149+
104150
def _get_feature_states_queryset(
105151
environment: "Environment",
106152
feature_name: str | None = None,

0 commit comments

Comments
 (0)