Skip to content

Commit 69d8dba

Browse files
Merge branch 'main' into warren/openai-agents-usage-cost
2 parents 955bd1a + 65d7d07 commit 69d8dba

10 files changed

Lines changed: 3538 additions & 85 deletions

File tree

openfeature-provider/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to `openfeature-provider-posthog` are documented here. This
44
file is maintained by [Sampo](https://github.com/bruits/sampo) from changesets in
55
`.sampo/changesets/` that target `pypi/openfeature-provider-posthog`.
66

7+
## 0.1.6 — 2026-07-10
8+
9+
### Patch changes
10+
11+
- Updated dependencies: posthog@7.22.1
12+
713
## 0.1.5 — 2026-07-09
814

915
### Patch changes

openfeature-provider/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "openfeature-provider-posthog"
7-
version = "0.1.5"
7+
version = "0.1.6"
88
description = "Official PostHog provider for the OpenFeature Python SDK."
99
readme = "README.md"
1010
authors = [{ name = "PostHog", email = "engineering@posthog.com" }]

posthog/CHANGELOG.md

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

3+
## 7.22.1 — 2026-07-10
4+
5+
### Patch changes
6+
7+
- [650d107](https://github.com/posthog/posthog-python/commit/650d107665207e5239b8613e19bcba76695076ee) Fix local evaluation of flag dependencies with a `flag_evaluates_to: false` condition: such conditions never matched, forcing the dependent flag to `false` for every locally-evaluated user. — Thanks @matheus-vb!
8+
39
## 7.22.0 — 2026-07-06
410

511
### Minor changes

posthog/feature_flags.py

Lines changed: 78 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,13 @@ def evaluate_flag_dependency(
115115
device_id=None,
116116
):
117117
"""
118-
Evaluate a flag dependency property according to the dependency chain algorithm.
118+
Evaluate a flag dependency condition under local evaluation.
119+
120+
The dependency_chain only establishes the order in which flags are evaluated
121+
and cached (the referenced flag itself is always the last member). The
122+
outcome is decided solely by comparing the referenced flag's evaluated value
123+
against the condition's expected value; ancestors influence it only through
124+
the referenced flag's own recursive evaluation.
119125
120126
Args:
121127
property: Flag property with type="flag" and dependency_chain
@@ -127,7 +133,12 @@ def evaluate_flag_dependency(
127133
device_id: The device ID for bucketing (optional)
128134
129135
Returns:
130-
bool: True if all dependencies in the chain evaluate to True, False otherwise
136+
bool: Whether the referenced flag's evaluated value matches the
137+
condition's expected value.
138+
139+
Raises:
140+
InconclusiveMatchError: If the condition is malformed or the chain
141+
cannot be conclusively evaluated locally.
131142
"""
132143
if flags_by_key is None or evaluation_cache is None:
133144
# Cannot evaluate flag dependencies without required context
@@ -151,93 +162,81 @@ def evaluate_flag_dependency(
151162
f"Circular dependency detected for flag '{property.get('key', 'unknown')}'"
152163
)
153164

154-
# Evaluate all dependencies in the chain order
155-
for dep_flag_key in dependency_chain:
156-
if dep_flag_key not in evaluation_cache:
157-
# Need to evaluate this dependency first
158-
dep_flag = flags_by_key.get(dep_flag_key)
159-
if not dep_flag:
160-
# Missing flag dependency - cannot evaluate locally
161-
evaluation_cache[dep_flag_key] = None
162-
raise InconclusiveMatchError(
163-
f"Cannot evaluate flag dependency '{dep_flag_key}' - flag not found in local flags"
164-
)
165-
else:
166-
# Check if the flag is active (same check as in client._compute_flag_locally)
167-
if not dep_flag.get("active"):
168-
evaluation_cache[dep_flag_key] = False
169-
else:
170-
# Recursively evaluate the dependency
171-
try:
172-
dep_flag_filters = dep_flag.get("filters") or {}
173-
dep_aggregation_group_type_index = dep_flag_filters.get(
174-
"aggregation_group_type_index"
175-
)
176-
if dep_aggregation_group_type_index is not None:
177-
# Group flags should continue bucketing by the group key
178-
# from the current evaluation context.
179-
dep_bucketing_value = distinct_id
180-
else:
181-
dep_bucketing_value = resolve_bucketing_value(
182-
dep_flag, distinct_id, device_id
183-
)
184-
dep_result = match_feature_flag_properties(
185-
dep_flag,
186-
distinct_id,
187-
properties,
188-
cohort_properties=cohort_properties,
189-
flags_by_key=flags_by_key,
190-
evaluation_cache=evaluation_cache,
191-
device_id=device_id,
192-
bucketing_value=dep_bucketing_value,
193-
)
194-
evaluation_cache[dep_flag_key] = dep_result
195-
except InconclusiveMatchError as e:
196-
# If we can't evaluate a dependency, store None and propagate the error
197-
evaluation_cache[dep_flag_key] = None
198-
raise InconclusiveMatchError(
199-
f"Cannot evaluate flag dependency '{dep_flag_key}': {e}"
200-
) from e
201-
202-
# Check the cached result
203-
cached_result = evaluation_cache[dep_flag_key]
204-
if cached_result is None:
205-
# Previously inconclusive - raise error again
206-
raise InconclusiveMatchError(
207-
f"Flag dependency '{dep_flag_key}' was previously inconclusive"
208-
)
209-
elif not cached_result:
210-
# Definitive False result - dependency failed
211-
return False
212-
213-
# All dependencies in the chain have been evaluated successfully
214-
# Now check if the final flag value matches the expected value in the property
165+
# Validate the condition shape before walking the chain. Test `is None`, not
166+
# truthiness: `False` is a valid expected value (the case this exists for).
215167
flag_key = property.get("key")
216168
expected_value = property.get("value")
217169
operator = property.get("operator", "exact")
218170

219-
if flag_key and expected_value is not None:
220-
# Get the actual value of the flag we're checking
221-
actual_value = evaluation_cache.get(flag_key)
171+
if operator != "flag_evaluates_to":
172+
raise InconclusiveMatchError(
173+
f"Flag dependency property for '{flag_key or 'unknown'}' has invalid operator '{operator}'"
174+
)
175+
if not flag_key or expected_value is None:
176+
raise InconclusiveMatchError(
177+
f"Flag dependency property for '{flag_key or 'unknown'}' is missing a key or value"
178+
)
179+
180+
# Evaluate and cache each flag in the chain; members already cached are
181+
# skipped. This does not decide the outcome — it only populates the cache.
182+
for dep_flag_key in dependency_chain:
183+
if dep_flag_key in evaluation_cache:
184+
continue
222185

223-
if actual_value is None:
224-
# Flag wasn't evaluated - this shouldn't happen if dependency chain is correct
186+
dep_flag = flags_by_key.get(dep_flag_key)
187+
if not dep_flag:
188+
# Missing flag dependency - cannot evaluate locally
189+
evaluation_cache[dep_flag_key] = None
225190
raise InconclusiveMatchError(
226-
f"Flag '{flag_key}' was not evaluated despite being in dependency chain"
191+
f"Cannot evaluate flag dependency '{dep_flag_key}' - flag not found in local flags"
227192
)
228193

229-
# For flag dependencies, we need to compare the actual flag result with expected value
230-
# using the flag_evaluates_to operator logic
231-
if operator == "flag_evaluates_to":
232-
return matches_dependency_value(expected_value, actual_value)
233-
else:
234-
# This should never happen, but just to be defensive.
235-
raise InconclusiveMatchError(
236-
f"Flag dependency property for '{property.get('key', 'unknown')}' has invalid operator '{operator}'"
194+
# Check if the flag is active (same check as in client._compute_flag_locally)
195+
if not dep_flag.get("active"):
196+
evaluation_cache[dep_flag_key] = False
197+
continue
198+
199+
# Recursively evaluate the dependency
200+
try:
201+
dep_flag_filters = dep_flag.get("filters") or {}
202+
dep_aggregation_group_type_index = dep_flag_filters.get(
203+
"aggregation_group_type_index"
204+
)
205+
if dep_aggregation_group_type_index is not None:
206+
# Group flags should continue bucketing by the group key
207+
# from the current evaluation context.
208+
dep_bucketing_value = distinct_id
209+
else:
210+
dep_bucketing_value = resolve_bucketing_value(
211+
dep_flag, distinct_id, device_id
212+
)
213+
dep_result = match_feature_flag_properties(
214+
dep_flag,
215+
distinct_id,
216+
properties,
217+
cohort_properties=cohort_properties,
218+
flags_by_key=flags_by_key,
219+
evaluation_cache=evaluation_cache,
220+
device_id=device_id,
221+
bucketing_value=dep_bucketing_value,
237222
)
223+
evaluation_cache[dep_flag_key] = dep_result
224+
except InconclusiveMatchError as e:
225+
# If we can't evaluate a dependency, store None and propagate the error
226+
evaluation_cache[dep_flag_key] = None
227+
raise InconclusiveMatchError(
228+
f"Cannot evaluate flag dependency '{dep_flag_key}': {e}"
229+
) from e
238230

239-
# If no value check needed, return True (all dependencies passed)
240-
return True
231+
# The condition matches iff the referenced flag's value matches the expected
232+
# value. None means inconclusive or not evaluated — distinct from a
233+
# definitive False, which must be allowed to match `expected_value=False`.
234+
actual_value = evaluation_cache.get(flag_key)
235+
if actual_value is None:
236+
raise InconclusiveMatchError(
237+
f"Flag dependency '{flag_key}' was inconclusive or not evaluated"
238+
)
239+
return matches_dependency_value(expected_value, actual_value)
241240

242241

243242
def matches_dependency_value(expected_value, actual_value):

0 commit comments

Comments
 (0)