Skip to content

fix(feature_flags): missing context key never satisfies a condition - #8429

Open
dreamorosi wants to merge 2 commits into
developfrom
fix/missing-context-key-never-matches
Open

fix(feature_flags): missing context key never satisfies a condition#8429
dreamorosi wants to merge 2 commits into
developfrom
fix/missing-context-key-never-matches

Conversation

@dreamorosi

@dreamorosi dreamorosi commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Issue number: closes #8424

Summary

Changes

A condition whose key is absent from the evaluation context no longer matches, regardless of action.

_evaluate_conditions read the context with context.get(key), so an absent key was compared as None. Negative actions then matched trivially: None != "premium" is True, None not in [...] is True. Rules such as "tier is not premium" fired for anonymous or partially populated requests that carried no tier at all.

_match_by_action now takes a context_key_present flag. The comparator runs exactly as before; if the key was absent and the comparator returned normally, the result is forced to False. Running the comparator first matters: actions such as ANY_IN_VALUE raise ValueError for a None context value, and that exception must keep reaching any validation_exception_handler the user registered, with the handler's return value honoured. Only the "comparator quietly returned True for an absent key" path changes.

A key that is present with an explicit None value is still compared as before, so this is about absence rather than falsiness. Time-based actions are unaffected since they never read the user context.

Why this is a regression fix, not a behaviour change

From the utility's introduction (#563, 2021) until v2.11.0, _match_by_action opened with:

if not context_value:
    return False

so a missing key (None) could never satisfy a condition, for any action. #2052 (fixing #2051) removed that guard so that falsy values such as 0, "", and False could be compared. That was the intended scope of the change; allowing an absent key to satisfy NOT_EQUALS and friends was a side effect, never documented, and no test asserted it. This PR restores the original "missing key never matches" semantics while keeping the #2051 behaviour for keys that are present with a falsy value.

Tests cover: the four negative actions with a missing key; an explicit None value still being compared; a missing key with ANY_IN_VALUE and a registered ValueError handler, asserting the handler is called and its result used; and the same without a handler, asserting no match. The behaviour is also now documented in the rule actions section of the feature flags docs.

User experience

Given:

{
  "discount_banner": {
    "default": false,
    "rules": {
      "non premium users": {
        "when_match": true,
        "conditions": [{ "action": "NOT_EQUALS", "key": "tier", "value": "premium" }]
      }
    }
  }
}
context Before v2.11.0 v2.11.0 to now After
{} False True False
{"tier": None} False True True
{"tier": ""} False True True
{"tier": "free"} True True True
{"tier": "premium"} False False False

Only the first row changes. validation_exception_handler behaviour is unchanged: comparators raise for the same inputs as before and handlers see the same exceptions.


By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Disclaimer: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful.

@powertools-for-aws-oss-automation powertools-for-aws-oss-automation Bot added the size/M Denotes a PR that changes 30-99 lines, ignoring generated files. label Sep 3, 2026
@boring-cyborg boring-cyborg Bot added documentation Improvements or additions to documentation tests labels Sep 3, 2026
Conditions read the context with context.get(key), so an absent key was
compared as None. Negative actions (NOT_EQUALS, NOT_IN, KEY_NOT_IN_VALUE,
VALUE_NOT_IN_KEY) therefore matched requests that carried no such key at
all, firing rules meant for one segment on anonymous or partially
populated traffic.

This is a regression from ab9078c (#2052). Until v2.11.0 _match_by_action
returned False for any falsy context value, so a missing key never
matched. That guard was removed to allow falsy values such as 0 and ""
to be compared, and allowing absent keys to match was an unintended side
effect. Restore the original semantics for missing keys by checking key
presence before invoking the comparator, while keeping the #2051
behaviour for keys that are present with a falsy or None value. Time
based actions are unaffected since they never read the user context.

Document the behaviour in the rule actions section.

Fixes #8424
@dreamorosi
dreamorosi force-pushed the fix/missing-context-key-never-matches branch from c53a906 to 80e50af Compare September 3, 2026 16:19
@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.65%. Comparing base (a39e101) to head (95094f9).
⚠️ Report is 2 commits behind head on develop.

Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #8429   +/-   ##
========================================
  Coverage    96.65%   96.65%           
========================================
  Files          296      296           
  Lines        14767    14774    +7     
  Branches      1246     1247    +1     
========================================
+ Hits         14273    14280    +7     
  Misses         359      359           
  Partials       135      135           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@dreamorosi
dreamorosi marked this pull request as ready for review September 3, 2026 16:32
@dreamorosi
dreamorosi requested a review from a team as a code owner September 3, 2026 16:32

@leandrodamascena leandrodamascena 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.

Thanks for working on this. There is an existing Feature Flags behavior that this change does not take into account, and the result is a regression.

Users can register a validation_exception_handler to catch errors raised while evaluating conditions and decide how to handle them:

@feature_flags.validation_exception_handler(ValueError)
def handle_invalid_context(exc):
    return True

For example, evaluating ANY_IN_VALUE without the expected context key currently reaches the comparator, raises ValueError, and calls the registered handler.

With this PR, the missing-key check returns False before _match_by_action runs. The registered handler is never called. I reproduced the difference:

  • develop: the handler is called and its result is used.
  • This branch: the handler is skipped and the condition returns False.

Could you fix this and add a test covering a missing key with a registered handler? Please hold off merging for now. Ping me when it is ready and I will review it again.

…text keys

The missing-key guard returned False before the comparator ran, so a
comparator that raises for a None context value (ANY_IN_VALUE,
ALL_IN_VALUE, NONE_IN_VALUE) never reached a registered
validation_exception_handler. That was a regression from develop, where
the handler was called and its result used.

Run the comparator first and only then apply the missing-key rule. Any
exception still flows through the existing handler lookup unchanged; the
only difference from develop is that a comparator returning True for an
absent key now yields False.
@powertools-for-aws-oss-automation powertools-for-aws-oss-automation Bot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. and removed size/M Denotes a PR that changes 30-99 lines, ignoring generated files. labels Sep 3, 2026
@dreamorosi

Copy link
Copy Markdown
Contributor Author

@leandrodamascena thanks, good catch. Fixed in 95094f9.

The guard now lives inside _match_by_action and runs after the comparator. The comparator is invoked exactly as on develop, so ANY_IN_VALUE (and ALL_IN_VALUE, NONE_IN_VALUE) still raise ValueError for a None context value and the registered handler is called with its return value honoured. Only the case where the comparator returns normally with True for an absent key is forced to False.

Added two tests at the end of test_feature_flags.py:

  • test_flags_missing_context_key_still_invokes_validation_exception_handler: your scenario, asserts the handler is called once and evaluate returns the handler's True. This fails on the previous revision of the branch.
  • test_flags_missing_context_key_no_match_without_handler_for_raising_action: same rule, no handler, asserts False.

Ready for another look whenever you have time.

@sonarqubecloud

sonarqubecloud Bot commented Sep 3, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/L Denotes a PR that changes 100-499 lines, ignoring generated files. tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: a missing context key satisfies negative conditions (NOT_EQUALS, NOT_IN, KEY_NOT_IN_VALUE, VALUE_NOT_IN_KEY)

2 participants