Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Analysing the code with pylint
run: |
pylint featuremanagement
- uses: psf/black@stable
- uses: psf/black@24.8.0
- name: Run mypy
run: |
mypy featuremanagement
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Release History

## 2.0.0b2 (Unreleased)

* Adds VariantAllocationPercentage, DefaultWhenEnabled, and AllocationId to telemetry.
* Allocation seed value is now None by default, and only defaults to `allocation\n<feature.id>` when assigning variants.

## 2.0.0b1 (09/10/2024)

* Adds support for Feature Variants.
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
project = "FeatureManagement"
copyright = "2024, Microsoft"
author = "Microsoft"
release = "2.0.0b1"
release = "2.0.0b2"

# -- General configuration ---------------------------------------------------

Expand Down
5 changes: 4 additions & 1 deletion featuremanagement/_featuremanagerbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,10 @@ def _assign_variant(
evaluation_event.reason = VariantAssignmentReason.GROUP
variant_name = group_allocation.variant
if not variant_name and feature.allocation.percentile:
context_id = targeting_context.user_id + "\n" + feature.allocation.seed
seed = feature.allocation.seed
if not seed:
seed = "allocation\n" + feature.name
context_id = targeting_context.user_id + "\n" + seed
box: float = self._is_targeted(context_id)
for percentile_allocation in feature.allocation.percentile:
if box == 100 and percentile_allocation.percentile_to == 100:
Expand Down
10 changes: 5 additions & 5 deletions featuremanagement/_models/_allocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,16 @@ class Allocation:
Represents an allocation configuration for a feature flag.
"""

def __init__(self, feature_name: str) -> None:
def __init__(self) -> None:
self._default_when_enabled = None
self._default_when_disabled = None
self._user: List[UserAllocation] = []
self._group: List[GroupAllocation] = []
self._percentile: List[PercentileAllocation] = []
self._seed = "allocation\n" + feature_name
self._seed = None

@classmethod
def convert_from_json(cls, json: Dict[str, Any], feature_name: str) -> Optional["Allocation"]:
def convert_from_json(cls, json: Dict[str, Any]) -> Optional["Allocation"]:
"""
Convert a JSON object to Allocation.

Expand All @@ -123,7 +123,7 @@ def convert_from_json(cls, json: Dict[str, Any], feature_name: str) -> Optional[
"""
if not json:
return None
allocation = cls(feature_name)
allocation = cls()
allocation._default_when_enabled = json.get(DEFAULT_WHEN_ENABLED)
allocation._default_when_disabled = json.get(DEFAULT_WHEN_DISABLED)
allocation._user = []
Expand Down Expand Up @@ -197,7 +197,7 @@ def percentile(self) -> List[PercentileAllocation]:
return self._percentile

@property
def seed(self) -> str:
def seed(self) -> Optional[str]:
"""
Get the seed for the allocation.

Expand Down
6 changes: 2 additions & 4 deletions featuremanagement/_models/_feature_flag.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ def convert_from_json(cls, json_value: Mapping[str, Any]) -> "FeatureFlag":
)
else:
feature_flag._conditions = FeatureConditions()
feature_flag._allocation = Allocation.convert_from_json(
json_value.get(FEATURE_FLAG_ALLOCATION, None), feature_flag._id
)
feature_flag._allocation = Allocation.convert_from_json(json_value.get(FEATURE_FLAG_ALLOCATION, None))
if FEATURE_FLAG_VARIANTS in json_value:
variants: List[Mapping[str, Any]] = json_value.get(FEATURE_FLAG_VARIANTS, [])
feature_flag._variants = []
Expand All @@ -66,7 +64,7 @@ def convert_from_json(cls, json_value: Mapping[str, Any]) -> "FeatureFlag":
return feature_flag

@property
def name(self) -> Optional[str]:
def name(self) -> str:
"""
Get the name of the feature flag.

Expand Down
2 changes: 1 addition & 1 deletion featuremanagement/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
# license information.
# -------------------------------------------------------------------------

VERSION = "2.0.0b1"
VERSION = "2.0.0b2"
42 changes: 38 additions & 4 deletions featuremanagement/azuremonitor/_send_telemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

EVENT_NAME = "FeatureEvaluation"

EVALUATION_EVENT_VERSION = "1.0.0"


def track_event(event_name: str, user: str, event_properties: Optional[Dict[str, Optional[str]]] = None) -> None:
"""
Expand All @@ -51,17 +53,49 @@ def publish_telemetry(evaluation_event: EvaluationEvent) -> None:
"""
if not HAS_AZURE_MONITOR_EVENTS_EXTENSION:
return
event = {}
if evaluation_event.feature:
event[FEATURE_NAME] = evaluation_event.feature.name
event: Dict[str, Optional[str]] = {}
if not evaluation_event.feature:
return
event[FEATURE_NAME] = evaluation_event.feature.name
event[ENABLED] = str(evaluation_event.enabled)
event["Version"] = EVALUATION_EVENT_VERSION

# VariantAllocationPercentage
if evaluation_event.reason and evaluation_event.reason != VariantAssignmentReason.NONE:
if evaluation_event.variant:
event[VARIANT] = evaluation_event.variant.name
event[REASON] = evaluation_event.reason.value

if evaluation_event.feature and evaluation_event.feature.telemetry:
if evaluation_event.reason == VariantAssignmentReason.DEFAULT_WHEN_ENABLED:
allocation_percentage = 0

if evaluation_event.feature.allocation and evaluation_event.feature.allocation.percentile:
for allocation in evaluation_event.feature.allocation.percentile:
if (
evaluation_event.variant
and allocation.variant == evaluation_event.variant.name
Copy link
Member

Choose a reason for hiding this comment

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

@rossgrambo @mrm9084 Can you confirm whether this line is by design please?

Copy link
Member

Choose a reason for hiding this comment

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

and allocation.percentile_to
):
allocation_percentage += allocation.percentile_to - allocation.percentile_from

event["VariantAssignmentPercentage"] = str(100 - allocation_percentage)
elif evaluation_event.reason == VariantAssignmentReason.PERCENTILE:
if evaluation_event.feature.allocation and evaluation_event.feature.allocation.percentile:
allocation_percentage = 0
Copy link
Member

Choose a reason for hiding this comment

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

Can you move this line before the above if statement to make it consistent with line 70

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See my latest PR. I already made these changes.

for allocation in evaluation_event.feature.allocation.percentile:
if (
evaluation_event.variant
Copy link
Member

@zhiyuanliang-ms zhiyuanliang-ms Oct 18, 2024

Choose a reason for hiding this comment

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

It would be better to check whether evaluation_event.variant is None in advance so that we can shortcircuit before the for loop.

Copy link
Member

Choose a reason for hiding this comment

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

Is it a possible case that variantAssignmentReason is not None but variant is None? I cannot imagine it will 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.

@zhiyuanliang-ms, I've made some changes already to this in my already existing PR that has some updates. #45

and allocation.variant == evaluation_event.variant.name
and allocation.percentile_to
):
allocation_percentage += allocation.percentile_to - allocation.percentile_from
event["VariantAssignmentPercentage"] = str(allocation_percentage)

# DefaultWhenEnabled
if evaluation_event.feature.allocation and evaluation_event.feature.allocation.default_when_enabled:
event["DefaultWhenEnabled"] = evaluation_event.feature.allocation.default_when_enabled
Copy link
Member

Choose a reason for hiding this comment

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

Just want to call out that it is possible that variant assignment reason is defaultWhenEnabled but the assigned variant is empty/none. The reason of this design is that if we have empty allocation configuration, (in .NET) we can consider it as:

{
    "allocation": {
        "default_when_enabled" : ""
    }
}

Copy link
Member

Choose a reason for hiding this comment

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

I don't whether it is a better choice to have event["DefaultWhenEnabled"] = "" when there is no allocation.default_when_enabled. But it could be another option for us.

Copy link
Contributor Author

Choose a reason for hiding this comment

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


if evaluation_event.feature.telemetry:
for metadata_key, metadata_value in evaluation_event.feature.telemetry.metadata.items():
if metadata_key not in event:
event[metadata_key] = metadata_value
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "FeatureManagement"
version = "2.0.0b1"
version = "2.0.0b2"
authors = [
{ name="Microsoft Corporation", email="appconfig@microsoft.com" },
]
Expand Down
Loading