Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
e3fd558
telemetry support
mrm9084 Apr 4, 2024
a9847cc
updating from feedback
mrm9084 Apr 15, 2024
de6a442
Changes from design meeting
mrm9084 Apr 17, 2024
089268f
formatting, updating samples
mrm9084 Apr 17, 2024
2dbecec
Merge branch 'main' into Telemetry
mrm9084 Apr 17, 2024
43e2c04
fixing merge issue
mrm9084 Apr 17, 2024
4219544
typing issue
mrm9084 Apr 17, 2024
86a50e1
Merge branch 'main' into Telemetry
mrm9084 May 3, 2024
545175a
fixing test validations from changes
mrm9084 May 3, 2024
eb1c8c2
Update test_json_validations.py
mrm9084 May 10, 2024
61d0022
Apply suggestions from code review
mrm9084 May 13, 2024
b1a2b25
Updating doc strings
mrm9084 May 13, 2024
8025453
Spelling
mrm9084 May 13, 2024
06f9536
Updating async name.
mrm9084 May 13, 2024
40a1275
Adding missing eval reason. Fixed formatting.
mrm9084 May 13, 2024
3ec867b
Merge branch 'main' into Telemetry
mrm9084 Jul 1, 2024
7a41fa3
Fixing Merge issue
mrm9084 Jul 3, 2024
f839860
fixing merge
mrm9084 Jul 3, 2024
48a4c22
fixing merge
mrm9084 Jul 8, 2024
4cd65c3
review comments
mrm9084 Jul 9, 2024
0fb2000
Updating evaluation event usage
mrm9084 Jul 10, 2024
851e6bd
Updated feature flag usage
mrm9084 Jul 10, 2024
3ff66b3
updating assign allocation logic
mrm9084 Jul 10, 2024
7297743
formatting
mrm9084 Jul 10, 2024
7cb8935
Adding Just Open Telemetry Support
mrm9084 Jul 12, 2024
c5dd07d
Update _send_telemetry.py
mrm9084 Jul 12, 2024
765f38e
review items
mrm9084 Jul 15, 2024
b086f3b
Update test_send_telemetry_appinsights.py
mrm9084 Jul 15, 2024
ca41bd5
review comments
mrm9084 Jul 16, 2024
0d76953
fixing default
mrm9084 Jul 16, 2024
cead033
Removing Just Open Telemetry
mrm9084 Jul 17, 2024
ade2027
Removing open telemetry test
mrm9084 Jul 18, 2024
96b96b2
rename to azuremonitor
mrm9084 Jul 18, 2024
996e730
Update test_send_telemetry_appinsights.py
mrm9084 Jul 18, 2024
cfcc158
Update feature_variant_sample_with_telemetry.py
mrm9084 Jul 18, 2024
f6dddbd
Fixing Enabled = False usage with status override
mrm9084 Jul 19, 2024
b42b9c6
Removed extra false
mrm9084 Jul 23, 2024
822e4cd
Removed extra enabled
mrm9084 Jul 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Updated feature flag usage
  • Loading branch information
mrm9084 committed Jul 10, 2024
commit 851e6bd0366b6a79a59d05248aae7fb1b394dfa3
65 changes: 35 additions & 30 deletions featuremanagement/_featuremanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,38 +93,42 @@ def __init__(self, configuration, **kwargs):
self._filters[feature_filter.name] = feature_filter

@staticmethod
def _check_default_disabled_variant(feature_flag, evaluation_event):
def _check_default_disabled_variant(evaluation_event):
"""
A method called when the feature flag is disabled, to determine what the default variant should be. If there is
no allocation, then None is set as the value of the variant in the EvaluationEvent.

:param FeatureFlag feature_flag: Feature flag object.
:param EvaluationEvent evaluation_event: Evaluation event object.
"""
if not feature_flag.allocation:
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_DISABLED
if not evaluation_event.feature.allocation:
evaluation_event.enabled = False
return
FeatureManager._check_variant_override(
feature_flag.variants, feature_flag.allocation.default_when_disabled, False, evaluation_event
evaluation_event.feature.variants,
evaluation_event.feature.allocation.default_when_disabled,
False,
evaluation_event,
)
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_DISABLED

@staticmethod
def _check_default_enabled_variant(feature_flag, evaluation_event):
def _check_default_enabled_variant(evaluation_event):
"""
A method called when the feature flag is enabled, to determine what the default variant should be. If there is
no allocation, then None is set as the value of the variant in the EvaluationEvent.

:param FeatureFlag feature_flag: Feature flag object.
:param EvaluationEvent evaluation_event: Evaluation event object.
"""
if not feature_flag.allocation:
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_ENABLED
if not evaluation_event.feature.allocation:
evaluation_event.enabled = True
return
FeatureManager._check_variant_override(
feature_flag.variants, feature_flag.allocation.default_when_enabled, True, evaluation_event
evaluation_event.feature.variants,
evaluation_event.feature.allocation.default_when_enabled,
True,
evaluation_event,
)
evaluation_event.reason = VariantAssignmentReason.DEFAULT_WHEN_ENABLED

@staticmethod
def _check_variant_override(variants, default_variant_name, status, evaluation_event):
Expand Down Expand Up @@ -156,33 +160,32 @@ def _is_targeted(context_id):

return (context_marker / (2**32 - 1)) * 100

def _assign_variant(self, feature_flag, targeting_context, evaluation_event):
def _assign_variant(self, targeting_context, evaluation_event):
"""
Assign a variant to the user based on the allocation.

:param FeatureFlag feature_flag: Feature flag object.
:param TargetingContext targeting_context: Targeting context.
:param EvaluationEvent evaluation_event: Evaluation event object.
:return: Variant name.
"""
evaluation_event.feature = feature_flag
if not feature_flag.variants or not feature_flag.allocation:
feature = evaluation_event.feature
if not feature.variants or not feature.allocation:
return None
if feature_flag.allocation.user and targeting_context.user_id:
for user_allocation in feature_flag.allocation.user:
if feature.allocation.user and targeting_context.user_id:
for user_allocation in feature.allocation.user:
if targeting_context.user_id in user_allocation.users:
evaluation_event.reason = VariantAssignmentReason.USER
return user_allocation.variant
if feature_flag.allocation.group and len(targeting_context.groups) > 0:
for group_allocation in feature_flag.allocation.group:
if feature.allocation.group and len(targeting_context.groups) > 0:
for group_allocation in feature.allocation.group:
for group in targeting_context.groups:
if group in group_allocation.groups:
evaluation_event.reason = VariantAssignmentReason.GROUP
return group_allocation.variant
if feature_flag.allocation.percentile:
context_id = targeting_context.user_id + "\n" + feature_flag.allocation.seed
if feature.allocation.percentile:
context_id = targeting_context.user_id + "\n" + feature.allocation.seed
box = self._is_targeted(context_id)
for percentile_allocation in feature_flag.allocation.percentile:
for percentile_allocation in feature.allocation.percentile:
if box == 100 and percentile_allocation.percentile_to == 100:
return percentile_allocation.variant
if percentile_allocation.percentile_from <= box < percentile_allocation.percentile_to:
Expand Down Expand Up @@ -277,7 +280,8 @@ def get_variant(self, feature_flag_id, *args, **kwargs):
self._on_feature_evaluated(result)
return result.variant

def _check_feature_filters(self, feature_flag, evaluation_event, targeting_context, **kwargs):
def _check_feature_filters(self, evaluation_event, targeting_context, **kwargs):
feature_flag = evaluation_event.feature
feature_conditions = feature_flag.conditions
feature_filters = feature_conditions.client_filters

Expand All @@ -303,10 +307,11 @@ def _check_feature_filters(self, feature_flag, evaluation_event, targeting_conte
evaluation_event.enabled = True
break

def _assign_allocation(self, feature_flag, evaluation_event, targeting_context):
def _assign_allocation(self, evaluation_event, targeting_context):
feature_flag = evaluation_event.feature
if feature_flag.allocation and feature_flag.variants:
default_enabled = evaluation_event.enabled
variant_name = self._assign_variant(feature_flag, targeting_context, evaluation_event)
variant_name = self._assign_variant(targeting_context, evaluation_event)
evaluation_event.enabled = default_enabled
if variant_name:
FeatureManager._check_variant_override(
Expand All @@ -318,11 +323,11 @@ def _assign_allocation(self, feature_flag, evaluation_event, targeting_context):

variant_name = None
if evaluation_event.enabled:
FeatureManager._check_default_enabled_variant(feature_flag, evaluation_event)
FeatureManager._check_default_enabled_variant(evaluation_event)
if feature_flag.allocation:
variant_name = feature_flag.allocation.default_when_enabled
else:
FeatureManager._check_default_disabled_variant(feature_flag, evaluation_event)
FeatureManager._check_default_disabled_variant(evaluation_event)
if feature_flag.allocation:
variant_name = feature_flag.allocation.default_when_disabled
evaluation_event.variant = self._variant_name_to_variant(feature_flag, variant_name)
Expand All @@ -336,7 +341,6 @@ def _check_feature(self, feature_flag_id, targeting_context, **kwargs):
:return: True if the feature flag is enabled for the given context.
:rtype: bool
"""
evaluation_event = EvaluationEvent(enabled=False)
if self._copy is not self._configuration.get(FEATURE_MANAGEMENT_KEY):
self._cache = {}
self._copy = self._configuration.get(FEATURE_MANAGEMENT_KEY)
Expand All @@ -347,23 +351,24 @@ def _check_feature(self, feature_flag_id, targeting_context, **kwargs):
else:
feature_flag = self._cache.get(feature_flag_id)

evaluation_event = EvaluationEvent(feature_flag)
if not feature_flag:
logging.warning("Feature flag %s not found", feature_flag_id)
# Unknown feature flags are disabled by default
return evaluation_event

if not feature_flag.enabled:
# Feature flags that are disabled are always disabled
FeatureManager._check_default_disabled_variant(feature_flag, evaluation_event)
FeatureManager._check_default_disabled_variant(evaluation_event)
Copy link
Member

Choose a reason for hiding this comment

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

check_variant_override is called in _check_default_disabled_variant. However, status_override should not work when feature_flag.enabled == false. Current behavior is wrong.

Currently, you have check_variant_override called in _check_default_disabled_variant, _check_default_enabled_variant and _assign_variant. The code path is complex.

I suggest we have the code path as below

def _check_feature():
    feature_flag = _get_feature_flag()
    ...

    evaluation_event = EvaluationEvent(feature_flag)

    _check_feature_filters(evaluation_event)

    _assign_allocation(evaluation_event)

    if feature_flag.enabled:
        _check_variant_override()

_assign_allocation follows
#24 (comment)

I am ok with the current change on the _assign_variant method where default_when_enabled is also handled there.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rossgrambo, @zhiyuanliang-ms, doesn't the fact that this override doesn't work mean that it is more complex as in the is_enabled case we need to return false always if the feature flag isn't enabled, but the correct override variant in the case of get_variant?

Copy link
Member

@rossgrambo rossgrambo Jul 19, 2024

Choose a reason for hiding this comment

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

However, status_override should not work when feature_flag.enabled == false. Current behavior is wrong.

I don't think this is correct. We run both IsEnabled and Variant allocation on both .IsEnabled and .GetVariant calls, regardless of if the flag is enabled or disabled- then apply the override https://github.com/microsoft/FeatureManagement-Dotnet/blob/preview/src/Microsoft.FeatureManagement/FeatureManager.cs#L282

Copy link
Member

@rossgrambo rossgrambo Jul 19, 2024

Choose a reason for hiding this comment

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

But according to the flowcharts it should be disabled if the flag is disabled... So actually you're right.

I misread the code. We have a check at the end to see if the flag is not disabled before overriding: https://github.com/microsoft/FeatureManagement-Dotnet/blob/preview/src/Microsoft.FeatureManagement/FeatureManager.cs#L354

Copy link
Member

Choose a reason for hiding this comment

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

doesn't the fact that this override doesn't work mean that it is more complex as in the is_enabled case we need to return false always if the feature flag isn't enabled, but the correct override variant in the case of get_variant?

Yes but it's not complex if you just do both everytime. Dotnet just always determines both the IsEnabled result and the Variant result (which will be defaultWhenDisabled in the disabled case). Then if IsEnabled was the calling function- just returns the bool, and if GetVariant was the calling function, returns the variant.

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 code works the same as DotNet, which is why is does the slightly more complex operation, but it does simplify the whole logic as variants and is_enabled mostly work the same.

if feature_flag.allocation:
variant_name = feature_flag.allocation.default_when_disabled
evaluation_event.variant = self._variant_name_to_variant(feature_flag, variant_name)
evaluation_event.feature = feature_flag
return evaluation_event

self._check_feature_filters(feature_flag, evaluation_event, targeting_context, **kwargs)
self._check_feature_filters(evaluation_event, targeting_context, **kwargs)

self._assign_allocation(feature_flag, evaluation_event, targeting_context)
self._assign_allocation(evaluation_event, targeting_context)
return evaluation_event

def list_feature_flag_names(self):
Expand Down
4 changes: 2 additions & 2 deletions featuremanagement/_models/_evaluation_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ class EvaluationEvent:
Represents a feature flag evaluation event.
"""

def __init__(self, *, enabled=False, feature_flag=None):
def __init__(self, feature_flag):
"""
Initialize the EvaluationEvent.
"""
self.feature = feature_flag
self.user = ""
self.enabled = enabled
self.enabled = False
self.variant = None
self.reason = None
48 changes: 26 additions & 22 deletions featuremanagement/aio/_featuremanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,35 +47,39 @@ def __init__(self, configuration, **kwargs):
self._filters[feature_filter.name] = feature_filter

@staticmethod
def _check_default_disabled_variant(feature_flag, evaluation_event):
def _check_default_disabled_variant(evaluation_event):
"""
A method called when the feature flag is disabled, to determine what the default variant should be. If there is
no allocation, then None is set as the value of the variant in the EvaluationEvent.

:param FeatureFlag feature_flag: Feature flag object.
:param EvaluationEvent evaluation_event: Evaluation event object.
"""
if not feature_flag.allocation:
if not evaluation_event.feature.allocation:
evaluation_event.enabled = False
Copy link
Member

@zhiyuanliang-ms zhiyuanliang-ms Jul 23, 2024

Choose a reason for hiding this comment

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

In line 286,

if not evaluation_event.enabled:
    FeatureManager._check_default_disabled_variant(evaluation_event)

Setting evaluation_event.enabled = False in _check_default_disabled_variant is redundant.

In line 327,

if not feature_flag.enabled:
    # Feature flags that are disabled are always disabled
    FeatureManager._check_default_disabled_variant(evaluation_event)
    if feature_flag.allocation:
        variant_name = feature_flag.allocation.default_when_disabled
        evaluation_event.variant = self._variant_name_to_variant(feature_flag, variant_name)
    evaluation_event.feature = feature_flag

    # If a feature flag is disabled and override can't enable it
    evaluation_event.enabled = False
    return evaluation_event

After calling _check_default_disabled_variant, you set evaluation_event.enabled = False again. I understand this is handing the corner case for status_override.

Why not extract the logic of handling status_override and put it at the end of `_check_feature"?

You have short cut when if not feature_flag.enabled. Except for this case, the code path will always fall into the status override check.

I don't think it's necessary to add this layer of abstraction for both _check_default_disabled_variant and _check_default_enabled_variant methods. The logic within these two functions isn't complex, and they aren't used frequently. Writing the logic inline won't make the code harder to read.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For the second part, I still want to set the variant info for is_enabled as it will be provided to telemetry either way. And I believe that is how DotNet also provides things.

return
FeatureManager._check_variant_override(
feature_flag.variants, feature_flag.allocation.default_when_disabled, False, evaluation_event
evaluation_event.feature.variants,
evaluation_event.feature.allocation.default_when_disabled,
False,
evaluation_event,
)

@staticmethod
def _check_default_enabled_variant(feature_flag, evaluation_event):
def _check_default_enabled_variant(evaluation_event):
"""
A method called when the feature flag is enabled, to determine what the default variant should be. If there is
no allocation, then None is set as the value of the variant in the EvaluationEvent.

:param FeatureFlag feature_flag: Feature flag object.
:param EvaluationEvent evaluation_event: Evaluation event object.
"""
if not feature_flag.allocation:
if not evaluation_event.feature.allocation:
evaluation_event.enabled = True
return
FeatureManager._check_variant_override(
feature_flag.variants, feature_flag.allocation.default_when_enabled, True, evaluation_event
evaluation_event.feature.variants,
evaluation_event.feature.allocation.default_when_enabled,
True,
evaluation_event,
)

@staticmethod
Expand Down Expand Up @@ -109,16 +113,15 @@ def _is_targeted(context_id):

return (context_marker / (2**32 - 1)) * 100

def _assign_variant(self, feature_flag, targeting_context, evaluation_event):
def _assign_variant(self, targeting_context, evaluation_event):
"""
Assign a variant to the user based on the allocation.

:param FeatureFlag feature_flag: Feature flag object.
:param TargetingContext targeting_context: Targeting context.
:param EvaluationEvent evaluation_event: Evaluation event object.
:return: Variant name.
"""
evaluation_event.feature = feature_flag
feature_flag = evaluation_event.feature
if not feature_flag.variants or not feature_flag.allocation:
return None
if feature_flag.allocation.user and targeting_context.user_id:
Expand Down Expand Up @@ -228,8 +231,8 @@ async def get_variant(self, feature_flag_id, *args, **kwargs):
result = await self._check_feature(feature_flag_id, targeting_context, **kwargs)
return result.variant

async def _check_feature_filters(self, feature_flag, evaluation_event, targeting_context, **kwargs):
feature_conditions = feature_flag.conditions
async def _check_feature_filters(self, evaluation_event, targeting_context, **kwargs):
feature_conditions = evaluation_event.feature.conditions
feature_filters = feature_conditions.client_filters

if len(feature_filters) == 0:
Expand All @@ -245,7 +248,7 @@ async def _check_feature_filters(self, feature_flag, evaluation_event, targeting
kwargs["user"] = targeting_context.user_id
kwargs["groups"] = targeting_context.groups
if filter_name not in self._filters:
raise ValueError(f"Feature flag {feature_flag.name} has unknown filter {filter_name}")
raise ValueError(f"Feature flag {evaluation_event.feature.name} has unknown filter {filter_name}")
if feature_conditions.requirement_type == REQUIREMENT_TYPE_ALL:
if not await self._filters[filter_name].evaluate(feature_filter, **kwargs):
evaluation_event.enabled = False
Expand All @@ -254,10 +257,11 @@ async def _check_feature_filters(self, feature_flag, evaluation_event, targeting
evaluation_event.enabled = True
break

def _assign_allocation(self, feature_flag, evaluation_event, targeting_context, **kwargs):
def _assign_allocation(self, evaluation_event, targeting_context, **kwargs):
feature_flag = evaluation_event.feature
if feature_flag.allocation and feature_flag.variants:
default_enabled = evaluation_event.enabled
variant_name = self._assign_variant(feature_flag, targeting_context, evaluation_event, **kwargs)
variant_name = self._assign_variant(targeting_context, evaluation_event, **kwargs)
evaluation_event.enabled = default_enabled
if variant_name:
FeatureManager._check_variant_override(
Expand All @@ -269,11 +273,11 @@ def _assign_allocation(self, feature_flag, evaluation_event, targeting_context,

variant_name = None
if evaluation_event.enabled:
FeatureManager._check_default_enabled_variant(feature_flag, evaluation_event)
FeatureManager._check_default_enabled_variant(evaluation_event)
if feature_flag.allocation:
variant_name = feature_flag.allocation.default_when_enabled
else:
FeatureManager._check_default_disabled_variant(feature_flag, evaluation_event)
FeatureManager._check_default_disabled_variant(evaluation_event)
if feature_flag.allocation:
variant_name = feature_flag.allocation.default_when_disabled
evaluation_event.variant = self._variant_name_to_variant(feature_flag, variant_name)
Expand All @@ -287,7 +291,6 @@ async def _check_feature(self, feature_flag_id, targeting_context, **kwargs):
:return: True if the feature flag is enabled for the given context.
:rtype: bool
"""
evaluation_event = EvaluationEvent(enabled=False)
if self._copy is not self._configuration.get(FEATURE_MANAGEMENT_KEY):
self._cache = {}
self._copy = self._configuration.get(FEATURE_MANAGEMENT_KEY)
Expand All @@ -298,23 +301,24 @@ async def _check_feature(self, feature_flag_id, targeting_context, **kwargs):
else:
feature_flag = self._cache.get(feature_flag_id)

evaluation_event = EvaluationEvent(feature_flag)
if not feature_flag:
logging.warning("Feature flag %s not found", feature_flag_id)
# Unknown feature flags are disabled by default
return evaluation_event

if not feature_flag.enabled:
# Feature flags that are disabled are always disabled
FeatureManager._check_default_disabled_variant(feature_flag, evaluation_event)
FeatureManager._check_default_disabled_variant(evaluation_event)
if feature_flag.allocation:
variant_name = feature_flag.allocation.default_when_disabled
evaluation_event.variant = self._variant_name_to_variant(feature_flag, variant_name)
evaluation_event.feature = feature_flag
return evaluation_event

await self._check_feature_filters(feature_flag, evaluation_event, targeting_context, **kwargs)
await self._check_feature_filters(evaluation_event, targeting_context, **kwargs)

self._assign_allocation(feature_flag, evaluation_event, targeting_context, **kwargs)
self._assign_allocation(evaluation_event, targeting_context, **kwargs)
return evaluation_event

def list_feature_flag_names(self):
Expand Down
8 changes: 4 additions & 4 deletions tests/test_send_telemetry_appinsights.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
class TestSendTelemetryAppinsights:

def test_send_telemetry_appinsights(self):
evaluation_event = EvaluationEvent()
feature_flag = FeatureFlag.convert_from_json({"id": "TestFeature"})
evaluation_event = EvaluationEvent(feature_flag)
variant = Variant("TestVariant", None)
evaluation_event.feature = feature_flag
evaluation_event.enabled = True
Expand All @@ -40,8 +40,8 @@ def test_send_telemetry_appinsights(self):
assert mock_track_event.call_args[0][1]["VariantAssignmentReason"] == "DefaultWhenDisabled"

def test_send_telemetry_appinsights_no_user(self):
evaluation_event = EvaluationEvent()
feature_flag = FeatureFlag.convert_from_json({"id": "TestFeature"})
evaluation_event = EvaluationEvent(feature_flag)
variant = Variant("TestVariant", None)
evaluation_event.feature = feature_flag
evaluation_event.enabled = False
Expand All @@ -62,8 +62,8 @@ def test_send_telemetry_appinsights_no_user(self):
assert mock_track_event.call_args[0][1]["VariantAssignmentReason"] == "DefaultWhenDisabled"

def test_send_telemetry_appinsights_no_variant(self):
evaluation_event = EvaluationEvent()
feature_flag = FeatureFlag.convert_from_json({"id": "TestFeature"})
evaluation_event = EvaluationEvent(feature_flag)
evaluation_event.feature = feature_flag
evaluation_event.enabled = True
evaluation_event.user = "test_user"
Expand All @@ -82,8 +82,8 @@ def test_send_telemetry_appinsights_no_variant(self):
assert "Reason" not in mock_track_event.call_args[0][1]

def test_send_telemetry_appinsights_no_import(self, caplog):
evaluation_event = EvaluationEvent()
feature_flag = FeatureFlag.convert_from_json({"id": "TestFeature"})
evaluation_event = EvaluationEvent(feature_flag)
evaluation_event.feature = feature_flag
evaluation_event.enabled = True

Expand Down