diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md index 475a258bdbf7..80d60449d9b8 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md @@ -1,20 +1,24 @@ # Release History -## 2.0.3 (Unreleased) +## 2.1.0 (2025-04-28) ### Features Added -### Breaking Changes +* Added AllocationId to the feature flag telemetry metadata when the feature flag has telemetry enabled. + +## 2.1.0b1 (2025-04-10) ### Bugs Fixed -### Other Changes +* Updates the feature flag telemetry to use the provided endpoint instead of the endpoint of the store the feature flag was loaded from. +* Removes FeatureFlagId from feature flag telemetry. ## 2.0.2 (2025-04-17) ### Other Changes * Updates telemetry for JSON usage. + ## 2.0.1 (2025-03-07) ### Bugs Fixed diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager_base.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager_base.py index 5dbab5b4c561..d80355e589a8 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager_base.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager_base.py @@ -3,9 +3,12 @@ # Licensed under the MIT License. See License.txt in the project root for # license information. # ------------------------------------------------------------------------- +import json import random +import base64 +import hashlib from dataclasses import dataclass -from typing import Dict, Mapping, Any, Optional +from typing import Dict, List, Optional, Mapping, Any from azure.appconfiguration import ( # type:ignore # pylint:disable=no-name-in-module FeatureFlagConfigurationSetting, ) @@ -21,6 +24,7 @@ METADATA_KEY, ETAG_KEY, FEATURE_FLAG_REFERENCE_KEY, + ALLOCATION_ID_KEY, ) FALLBACK_CLIENT_REFRESH_EXPIRED_INTERVAL = 3600 # 1 hour in seconds @@ -33,6 +37,89 @@ class _ConfigurationClientWrapperBase: endpoint: str + @staticmethod + def _generate_allocation_id(feature_flag_value: Dict[str, JSON]) -> Optional[str]: + """ + Generates an allocation ID for the specified feature. + seed=123abc\ndefault_when_enabled=Control\npercentiles=0,Control,20;20,Test,100\nvariants=Control,standard;Test,special # pylint:disable=line-too-long + + :param Dict[str, JSON] feature_flag_value: The feature to generate an allocation ID for. + :rtype: str + :return: The allocation ID. + """ + + allocation_id = "" + allocated_variants = [] + + allocation: Optional[JSON] = feature_flag_value.get("allocation") + + if not allocation: + return None + + # Seed + allocation_id = f"seed={allocation.get('seed', '')}" + + # DefaultWhenEnabled + if "default_when_enabled" in allocation: + allocated_variants.append(allocation.get("default_when_enabled")) + + allocation_id += f"\ndefault_when_enabled={allocation.get('default_when_enabled', '')}" + + # Percentile + allocation_id += "\npercentiles=" + + percentile = allocation.get("percentile") + + if percentile: + percentile_allocations = sorted( + (x for x in percentile if x.get("from") != x.get("to")), + key=lambda x: x.get("from"), + ) + + for percentile_allocation in percentile_allocations: + if "variant" in percentile_allocation: + allocated_variants.append(percentile_allocation.get("variant")) + + allocation_id += ";".join( + f"{pa.get('from')}," f"{base64.b64encode(pa.get('variant').encode()).decode()}," f"{pa.get('to')}" + for pa in percentile_allocations + ) + + if not allocated_variants and not allocation.get("seed"): + return None + + # Variants + allocation_id += "\nvariants=" + + variants_value = feature_flag_value.get("variants") + if variants_value and (isinstance(variants_value, list) or all(isinstance(v, dict) for v in variants_value)): + if ( + allocated_variants + and isinstance(variants_value, list) + and all(isinstance(v, dict) for v in variants_value) + ): + sorted_variants: List[Dict[str, Any]] = sorted( + (v for v in variants_value if v.get("name") in allocated_variants), + key=lambda v: v.get("name"), + ) + + for v in sorted_variants: + allocation_id += f"{base64.b64encode(v.get('name', '').encode()).decode()}," + if "configuration_value" in v: + allocation_id += ( + f"{json.dumps(v.get('configuration_value', ''), separators=(',', ':'), sort_keys=True)}" + ) + allocation_id += ";" + if sorted_variants: + allocation_id = allocation_id[:-1] + + # Create a sha256 hash of the allocation_id + hash_object = hashlib.sha256(allocation_id.encode()) + hash_digest = hash_object.digest() + + # Encode the first 15 bytes in base64 url + return base64.urlsafe_b64encode(hash_digest[:15]).decode() + def _feature_flag_telemetry( self, endpoint: str, feature_flag: FeatureFlagConfigurationSetting, feature_flag_value: Dict ): @@ -48,6 +135,9 @@ def _feature_flag_telemetry( feature_flag_reference += f"?label={feature_flag.label}" if feature_flag_value[TELEMETRY_KEY].get("enabled"): feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_REFERENCE_KEY] = feature_flag_reference + allocation_id = self._generate_allocation_id(feature_flag_value) + if allocation_id: + feature_flag_value[TELEMETRY_KEY][METADATA_KEY][ALLOCATION_ID_KEY] = allocation_id def _feature_flag_appconfig_telemetry( self, feature_flag: FeatureFlagConfigurationSetting, filters_used: Dict[str, bool] diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py index 9cdb7235e388..cd593b483e92 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py @@ -20,6 +20,7 @@ TELEMETRY_KEY = "telemetry" METADATA_KEY = "metadata" +ALLOCATION_ID_KEY = "AllocationId" ETAG_KEY = "ETag" FEATURE_FLAG_REFERENCE_KEY = "FeatureFlagReference" diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_version.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_version.py index 920a4130e7ae..bade305c8529 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_version.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_version.py @@ -4,4 +4,4 @@ # license information. # ------------------------------------------------------------------------- -VERSION = "2.0.3" +VERSION = "2.1.0"