Skip to content

feat: Evaluate flags that depend on other flags - #343

Merged
emyller merged 9 commits into
mainfrom
feat/dependent-flags
Sep 1, 2026
Merged

feat: Evaluate flags that depend on other flags#343
emyller merged 9 commits into
mainfrom
feat/dependent-flags

Conversation

@khvn26

@khvn26 khvn26 commented Aug 27, 2026

Copy link
Copy Markdown
Member

@khvn26
khvn26 force-pushed the feat/dependent-flags branch from 4e7cf34 to b0b6007 Compare August 27, 2026 18:00
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown

File Coverage Missing
All files 100%

Minimum allowed coverage is 100%

Generated by 🐒 cobertura-action against aac80cb

@codspeed-hq

codspeed-hq Bot commented Aug 27, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 1 untouched benchmark


Comparing feat/dependent-flags (aac80cb) with main (b57f3ba)

Open in CodSpeed

@khvn26

khvn26 commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

@themis-blindfold review

@khvn26

khvn26 commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: 137a7ea1-cbf0-4a08-a1fa-e9bae0cdf4b9

📥 Commits

Reviewing files that changed from the base of the PR and between bf7ef51 and aac80cb.

📒 Files selected for processing (3)
  • .gitmodules
  • flag_engine/segments/evaluator.py
  • tests/engine_tests/engine-test-data

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The evaluation context now accepts optional flag results. Segment evaluation now injects a lazy $.flags mapping and resolves dependent flags during condition checks. _DependencyResolver memoises segment matches and flag results, applies override priority, and returns a circular-dependency reason instead of raising. The JSONPath prefix check uses a shared constant. The test data submodule pointer and branch were updated.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: 🔵 Low · up to aac80

Dependent flags can now influence segment matching and returned values, while circular dependencies are contained. A sufficiently deep dependency chain could still consume excessive evaluator resources, so owners should consider adding an explicit depth or work limit; the PR remains mergeable with that bounded risk understood.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread flag_engine/segments/evaluator.py
Comment thread flag_engine/segments/evaluator.py
Comment thread .gitmodules Outdated
@themis-blindfold

Copy link
Copy Markdown

⚖️ Themis review: 🟠 Fix before merge

TL;DR: Dependent flags work for the straightforward cases covered by the shared corpus, and all completed CI checks passed. Two paths need correction before merging: ordinary evaluations now pay resolver setup costs, and cycles can return a default flag while reporting its dependent segment as matched.

Area Score
🎯 Correctness 2/5
🧪 Test coverage 3/5
📐 Code quality 2/5
🚀 Product impact 4/5

🟠 Majors

🧹 Nits

  • .gitmodules — pin the shared test corpus to its release tag rather than a mutable feature branch.
📝 Walkthrough
  • Evaluation context - adds a per-call lazy flags mapping for segment conditions to resolve prerequisite flags.
  • Dependency resolver - resolves overriding segments recursively, memoises segment outcomes, and marks cycles.
  • Result assembly - combines the usual segment pass with lazily resolved prerequisite flags.
  • Shared test corpus - advances to the dependency cases, but configures its source as a feature branch.
🧪 How to verify
  1. Evaluate a context without any $.flags condition and assert no resolver or replacement context is created.
  2. Add a direct prerequisite condition and verify enabled, value, and variant dependencies select the expected override.
  3. Exercise a ↔ b with b enabled by default and a disabled by default; assert neither cyclic segment is reported as matched and neither override is applied.
  4. Run pytest tests/unit/segments/test_segments_evaluator.py tests/engine_tests/test_engine.py with the updated corpus checked out.
  5. Run the benchmark suite against a no-dependency context and compare it with the base branch.
    Automate: add regression tests for the no-dependency allocation path and for cycles with truthy default prerequisites.

Product take: This is a major capability for composing rollouts, but evaluation is a hot path and cycle handling must stay deterministic. Once those paths are fixed, the feature is a solid improvement.

🧭 Assumptions & unverified claims

The cycle reproduction should be rerun with the pinned jsonpath-rfc9535==0.1.6 package; that package was unavailable in the local review environment.

Dependent flags are useful; dependent defaults are less so. · reviewed at 63716d9

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2fe46096-59c6-498b-8776-03a0eaba34c8

📥 Commits

Reviewing files that changed from the base of the PR and between cc74d0a and 63716d9.

📒 Files selected for processing (4)
  • .gitmodules
  • flag_engine/context/types.py
  • flag_engine/segments/evaluator.py
  • tests/engine_tests/engine-test-data

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread .gitmodules Outdated
khvn26 added 2 commits August 31, 2026 12:44
Adds support for segments conditioned on another flag's result, via a
`$.flags.<feature name>` condition property, as the evaluation half of
dependent flags.

Segment conditions are already JSONPath, so a dependency needs no new
operator or condition type. What it does need is for the flag to be
resolved by the time the condition reads it, and evaluation resolved
every segment before any flag.

Rather than establish up front which segments depend on which flags,
`$.flags` is a mapping that resolves a flag when a condition first
reads it. Resolving a flag evaluates the segments overriding it, which
in turn resolves whatever flags their conditions read, memoised
throughout. The existing single pass is otherwise untouched: a context
whose segments read no flag never enters the resolver, and pays only
for one empty mapping.

Resolving on read rather than in advance means there is no need to
recognise a dependency in a condition property, and so no need to
reimplement enough of the JSONPath grammar to tell that
`$.flags.a['enabled']` and `$.flags.a.enabled` are the same query.
Every spelling works because resolution is triggered by the read
itself. It also costs only what is read: rule short-circuiting means a
condition that is never evaluated resolves nothing.

Measured against main across the benchmark contexts, interleaved to
cancel drift: +3.7%, from the mapping and a shallow copy of the context
to hold it. Establishing up front that an environment has no
dependencies would remove that residual entirely, but needs a field on
the context to carry it.

A flag whose dependencies form a cycle is not resolvable. It serves its
environment default and reports `ERROR; code=CIRCULAR_DEPENDENCY`, so
that a flag which could not be resolved is distinguishable from one
that was never gated, and only flags in the cycle are reported that
way. Crucially the value it falls back to is never published to
`$.flags`, so no other condition can match on a value that exists only
because the cycle was cut — otherwise a segment gated on a flag that
defaults to enabled would be reported as matched while the flag it
overrides stayed at its default. Cycles are still expected to be
rejected where dependencies are written.

Behaviour is covered by test cases in Flagsmith/engine-test-data#59
rather than by unit tests here, so that every engine is held to it.
REVERT BEFORE MERGE.

Dependent flags behaviour is covered by test cases in
Flagsmith/engine-test-data#59 rather than by unit tests here, so that
every engine is held to it. Those cases aren't in a release yet, so
without this the new code is exercised by nothing and CI's 100%
coverage gate fails.

Once #59 is merged and tagged, this goes back to a semver tag, which
Renovate now tracks as of #337.
@khvn26
khvn26 force-pushed the feat/dependent-flags branch from 63716d9 to 99dea3e Compare August 31, 2026 11:44
@khvn26
khvn26 marked this pull request as ready for review August 31, 2026 11:54
@khvn26
khvn26 requested a review from a team as a code owner August 31, 2026 11:54
@khvn26
khvn26 requested review from matthewelwell and removed request for a team August 31, 2026 11:54

@emyller emyller left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few comments towards improving the architecture. I believe we could achieve a simpler and clearer design overall if this was re-approached, but let me know if you'd rather move forward for now — tests pass.

Comment thread flag_engine/context/types.py
Comment thread flag_engine/segments/evaluator.py Outdated
Comment thread flag_engine/segments/evaluator.py Outdated
Follows Flagsmith/engine-test-data#59, where two cases were dropped as
redundant and one gained a clarifying note. Coverage is unaffected.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: 8d03f90a-0de7-469e-851d-96c427239b41

📥 Commits

Reviewing files that changed from the base of the PR and between 63716d9 and bf7ef51.

📒 Files selected for processing (2)
  • flag_engine/segments/evaluator.py
  • tests/engine_tests/engine-test-data

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread flag_engine/segments/evaluator.py Outdated
`IS_NOT_SET` matches when a property has no value, and a flag left
unresolved by a cycle has none. A segment overriding the very flag it
tests for absence therefore had its condition made true by the cycle,
and its override applied — reported with the circular dependency reason
but carrying the override's value rather than the environment default
the reason claims.

A segment whose own evaluation cut a cycle read a flag that could not
be resolved, so neither its match nor its failure to match rests on
anything, and it must not be applied either way. The cycle counter
moves onto the flags mapping so that both the resolver and the single
pass over segments can see it, and the mapping is now passed to
`evaluate_segments` explicitly rather than read back out of the
context.

Covered by `cyclic_is_not_set__segment_should_not_match` in
Flagsmith/engine-test-data#59, which fails without this.
Detecting whether a segment consulted a flag was done by reaching into
`__dict__` to see whether a cached property had been built, which is
both obscure and an access to a private member from outside. An empty
mapping cannot answer the question on its own, because a flag in a
cycle resolves to nothing and is deliberately not published.

The mapping now records it directly. `used` says a condition read a
flag, and `cyclic` — which the resolver already maintained — moves
alongside it, so nothing outside the resolver reaches into it. Both are
created on the first read rather than up front, so a context whose
segments read no flag allocates neither.
Which of two competing segment overrides wins was implemented twice,
once while collecting overrides across segments and once while
resolving a single feature's dependencies. Two copies of a precedence
rule is the kind of thing that drifts, and the second copy tracked the
winning priority in a separate variable to do it.

Both now call `_wins_over`, which states the rule once: lower priority
wins, and the first seen wins a tie.
emyller
emyller previously approved these changes Sep 1, 2026

@emyller emyller left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! 🌴

Discharges the REVERT BEFORE MERGE on 99dea3e. Flagsmith/engine-test-data#59
is merged and released, so the submodule goes back to a semver tag —
the form Renovate tracks as of #337 — rather than a branch.
@emyller
emyller merged commit d1f379c into main Sep 1, 2026
9 checks passed
@emyller
emyller deleted the feat/dependent-flags branch September 1, 2026 17:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support $.flags context values in segment conditions

3 participants