Skip to content
This repository has been archived by the owner on Nov 8, 2024. It is now read-only.

FF-1677 Python SDK UFC update #28

Merged
merged 40 commits into from
Apr 24, 2024
Merged

FF-1677 Python SDK UFC update #28

merged 40 commits into from
Apr 24, 2024

Conversation

schmit
Copy link
Contributor

@schmit schmit commented Mar 5, 2024

Fixes: #issue

Motivation and Context

Migrating the Python SDK to the UFC to enable more flexible feature flag evaluation and advanced use cases.

Description

  • Leverage UFC to evaluate flags
  • Create explicit integer and float variation types

How has this been tested?

  • Updated unit tests
  • Updated cross-SDK UFC test cases

test/rules_test.py Outdated Show resolved Hide resolved
eppo_client/client.py Outdated Show resolved Hide resolved
sharder: Sharder

def evaluate_flag(
self, flag, subject_key, subject_attributes
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also decided to pass along subject_key so we can include it in rule evaluation if we want (so you don't also have to add id to subject attributes), which has been a pet-peeve. We could just inject "id": subject_key into subject_attributes somewhere

Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice quality of life improvement!

Copy link
Collaborator

@giorgiomartini0 giorgiomartini0 left a comment

Choose a reason for hiding this comment

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

Love how fast this is coming together! Partial review for now

eppo_client/client.py Outdated Show resolved Hide resolved
eppo_client/eval.py Outdated Show resolved Hide resolved
sharder: Sharder

def evaluate_flag(
self, flag, subject_key, subject_attributes
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nice quality of life improvement!

eppo_client/eval.py Outdated Show resolved Hide resolved
eppo_client/eval.py Outdated Show resolved Hide resolved
Copy link
Collaborator

@giorgiomartini0 giorgiomartini0 left a comment

Choose a reason for hiding this comment

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

A few more comments - haven't started on the tests yet.

allocation_key=None,
variation=None,
extra_logging={},
do_log=False,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thinking out loud: the more we see the none/default outcome as an error/abnormal, the more I'd like to log it:

  • when a flag doesn't exist in the RAC, logging the attempted evaluation will help to discover mismatches between the flag key here and what was inputted in the UI; or help with autocomplete at flag creation time
  • when no allocations matched (not even the default one, somehow), it's basically an error and we would want to keep track of it

Copy link
Contributor

Choose a reason for hiding this comment

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

This would be benefit of having complex return type that includes evaluation reason.

You can see us passing around a "variation reason" for when we hacked a version with that for Palta: Eppo-exp/js-sdk-common@v2.1.0...v2.1.1-alpha.0

I think we can include the reason IN the FAC itself for most cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think of the FlagEvaluation object as capturing the evaluation reason (in particular, the allocation key: we returned variation X because we matched allocation Y)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems like the main thing we should consider adding is an "error" attribute to flag evaluation that we can set

eppo_client/models.py Outdated Show resolved Hide resolved
eppo_client/config.py Outdated Show resolved Hide resolved
eppo_client/rules.py Outdated Show resolved Hide resolved
eppo_client/client.py Outdated Show resolved Hide resolved
@schmit schmit requested review from leoromanovsky and aarsilv March 7, 2024 17:21
Copy link
Contributor

@aarsilv aarsilv left a comment

Choose a reason for hiding this comment

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

This is looking good!

allocation_key=None,
variation=None,
extra_logging={},
do_log=False,
Copy link
Contributor

Choose a reason for hiding this comment

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

This would be benefit of having complex return type that includes evaluation reason.

You can see us passing around a "variation reason" for when we hacked a version with that for Palta: Eppo-exp/js-sdk-common@v2.1.0...v2.1.1-alpha.0

I think we can include the reason IN the FAC itself for most cases.

eppo_client/models.py Outdated Show resolved Hide resolved
eppo_client/eval.py Show resolved Hide resolved
@schmit schmit force-pushed the allocation-revamp branch from 5c98d88 to 0df2809 Compare March 14, 2024 03:51
subject_attributes: Dict[str, Union[str, float, int, bool]]
allocation_key: str
variation: Variation
extra_logging: Dict[str, str]
Copy link
Contributor

Choose a reason for hiding this comment

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

is extra_logging where the evaluation reasoning would work its way in?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, this is a generic object that allows us to add extra logging fields. The immediate use case is to log holdout assignments

@schmit schmit changed the title United RAC revamp [wip] Python SDK UFC update Mar 15, 2024
raise e

def get_assignment_variation(
def get_assignment_detail(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@aarsilv this function returns the FlagEvaluation object, which I currently consider to be the "detailed view of assignment".

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this part of the "public" API then? Developers who want the full return object call this one instead of get__assignment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes that is my thought -- but open to feedback.

For example: should we have typed versions of the detailed version?

eppo_client/client.py Outdated Show resolved Hide resolved
"[Eppo SDK] No assigned variation. Subject attributes do not match targeting rules: {0}".format(
subject_attributes
)
if not check_type_match(expected_variation_type, flag.variation_type):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Check this before we check whether the flag is enabled so that we can notify developers even for disabled flags.

configs = cast(dict, self.__http_client.get(RAC_ENDPOINT).get("flags", {}))
for exp_key, exp_config in configs.items():
configs[exp_key] = ExperimentConfigurationDto(**exp_config)
configs = cast(dict, self.__http_client.get(UFC_ENDPOINT).get("flags", {}))
Copy link
Member

Choose a reason for hiding this comment

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

It's official 🥳

Copy link
Collaborator

Choose a reason for hiding this comment

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

I would think that not finding flags in the response should trigger an exception, not silently return an empty config

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that's fair, but this is how we have been doing things and I think it may be better to change this in a separate PR

eppo_client/__init__.py Show resolved Hide resolved
eppo_client/client.py Outdated Show resolved Hide resolved
eppo_client/client.py Outdated Show resolved Hide resolved
Comment on lines +190 to +191
if subject_attributes is None:
subject_attributes = {}
Copy link
Member

Choose a reason for hiding this comment

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

why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So that from that point onwards we can assume subject_attributes is a dictionary and can avoid the "null" case

class FlagEvaluation:
flag_key: str
subject_key: str
subject_attributes: Dict[str, Union[str, float, int, bool]]
Copy link
Member

Choose a reason for hiding this comment

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

optional?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We always return a dict here -- it might just be empty. I imagine that's easier to deal with downstream than having either nulls or dictionaries with values. Open to feedback

eppo_client/rules.py Show resolved Hide resolved
eppo_client/sharding.py Outdated Show resolved Hide resolved
eppo_client/configuration_requestor.py Show resolved Hide resolved
configs = cast(dict, self.__http_client.get(RAC_ENDPOINT).get("flags", {}))
for exp_key, exp_config in configs.items():
configs[exp_key] = ExperimentConfigurationDto(**exp_config)
configs = cast(dict, self.__http_client.get(UFC_ENDPOINT).get("flags", {}))
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would think that not finding flags in the response should trigger an exception, not silently return an empty config

eppo_client/configuration_requestor.py Outdated Show resolved Hide resolved
eppo_client/client.py Outdated Show resolved Hide resolved
eppo_client/client.py Outdated Show resolved Hide resolved
eppo_client/client.py Show resolved Hide resolved
@@ -4,7 +4,8 @@
from enum import Enum
from typing import Any, List

from eppo_client.base_model import SdkBaseModel
from eppo_client.models import SdkBaseModel
from eppo_client.types import AttributeType, ConditionValueType, SubjectAttributes


class OperatorType(Enum):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would you be open to adding "IS_NULL", "IS_NOT_NULL" here or would you prefer that to be a minor version bump once we support it in the frontend?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we should add it, and we can do that before publishing this version of the SDK, but let's move it to a different PR

store.set_configurations({"randomization_algo": test_exp})
assert store.get_configuration("randomization_algo") == test_exp
store.set_configurations({"flag": mock_flag})
assert store.get_configuration("flag") == mock_flag


def test_evicts_old_entries_when_max_size_exceeded():
Copy link
Collaborator

Choose a reason for hiding this comment

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

What's this? Do we have a max size concept elsewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure -- I did not change the logic here

test/eval_test.py Show resolved Hide resolved
giorgiomartini0

This comment was marked as duplicate.

eppo_client/rules.py Show resolved Hide resolved
test/rules_test.py Show resolved Hide resolved
eppo_client/client.py Show resolved Hide resolved
eppo_client/client.py Outdated Show resolved Hide resolved
test/rules_test.py Outdated Show resolved Hide resolved
Comment on lines +191 to +194
logger.error(
"[Eppo SDK] Variation value does not have the correct type for the flag: "
f"{flag_key} and variation key {result.variation.key}"
)
Copy link
Member

Choose a reason for hiding this comment

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

when does this situation happen?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should never happen, but can theoretically happen if the backend sends a value that does not have the correct type

@schmit schmit merged commit d7cba6b into main Apr 24, 2024
4 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants