From 2b91eef4ee55c413a3699080a01743c240ade46d Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 20 Oct 2025 18:19:45 -0700 Subject: [PATCH 01/16] moving request tracing to it's own file --- .../_azureappconfigurationproviderbase.py | 128 ++---------- .../appconfiguration/provider/_constants.py | 6 + .../provider/_request_tracing_context.py | 192 ++++++++++++++++++ 3 files changed, 219 insertions(+), 107 deletions(-) create mode 100644 sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py index 42f3527a10f9..476b1b9bdeee 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -9,7 +9,6 @@ import os import time import datetime -from importlib.metadata import version, PackageNotFoundError from threading import Lock import logging from typing import ( @@ -33,20 +32,7 @@ ) from ._models import SettingSelector from ._constants import ( - REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, - ServiceFabricEnvironmentVariable, - AzureFunctionEnvironmentVariable, - AzureWebAppEnvironmentVariable, - ContainerAppEnvironmentVariable, - KubernetesEnvironmentVariable, NULL_CHAR, - CUSTOM_FILTER_KEY, - PERCENTAGE_FILTER_KEY, - TIME_WINDOW_FILTER_KEY, - TARGETING_FILTER_KEY, - PERCENTAGE_FILTER_NAMES, - TIME_WINDOW_FILTER_NAMES, - TARGETING_FILTER_NAMES, TELEMETRY_KEY, METADATA_KEY, ETAG_KEY, @@ -58,6 +44,8 @@ FEATURE_FLAG_KEY, ) from ._refresh_timer import _RefreshTimer +from ._request_tracing_context import _RequestTracingContext + JSON = Mapping[str, Any] _T = TypeVar("_T") @@ -237,10 +225,7 @@ def __init__(self, **kwargs: Any) -> None: self._watched_feature_flags: Dict[Tuple[str, str], Optional[str]] = {} self._feature_flag_refresh_timer: _RefreshTimer = _RefreshTimer(**kwargs) self._feature_flag_refresh_enabled = kwargs.pop("feature_flag_refresh_enabled", False) - self._feature_filter_usage: Dict[str, bool] = {} - self._uses_load_balancing = kwargs.pop("load_balancing_enabled", False) - self._uses_ai_configuration = False - self._uses_aicc_configuration = False # AI Chat Completion + self._tracing_context = _RequestTracingContext(kwargs.pop("load_balancing_enabled", False)) self._update_lock = Lock() self._refresh_lock = Lock() @@ -276,30 +261,18 @@ def _update_ff_telemetry_metadata( feature_flag_reference = f"{endpoint}kv/{feature_flag.key}" if feature_flag.label and not feature_flag.label.isspace(): feature_flag_reference += f"?label={feature_flag.label}" + if feature_flag_value[TELEMETRY_KEY].get("allocation") and feature_flag_value[TELEMETRY_KEY].get("allocation").get("seed"): + self._tracing_context.uses_seed = True + if feature_flag_value[TELEMETRY_KEY].get("variant"): + size = len(feature_flag_value[TELEMETRY_KEY].get("variant")) + self._tracing_context.update_max_variants(size) if feature_flag_value[TELEMETRY_KEY].get("enabled"): + self._tracing_context.uses_telemetry = True 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 _update_feature_filter_telemetry(self, feature_flag: FeatureFlagConfigurationSetting): - """ - Track feature filter usage for App Configuration telemetry. - - :param feature_flag: The feature flag to analyze for filter usage. - :type feature_flag: FeatureFlagConfigurationSetting - """ - if feature_flag.filters: - for filter in feature_flag.filters: - if filter.get("name") in PERCENTAGE_FILTER_NAMES: - self._feature_filter_usage[PERCENTAGE_FILTER_KEY] = True - elif filter.get("name") in TIME_WINDOW_FILTER_NAMES: - self._feature_filter_usage[TIME_WINDOW_FILTER_KEY] = True - elif filter.get("name") in TARGETING_FILTER_NAMES: - self._feature_filter_usage[TARGETING_FILTER_KEY] = True - else: - self._feature_filter_usage[CUSTOM_FILTER_KEY] = True - @staticmethod def _generate_allocation_id(feature_flag_value: Dict[str, JSON]) -> Optional[str]: """ @@ -477,9 +450,9 @@ def _process_key_value_base(self, config: ConfigurationSetting) -> Union[str, Di # Feature flags are of type json, but don't treat them as such try: if APP_CONFIG_AI_MIME_PROFILE in config.content_type: - self._uses_ai_configuration = True + self._tracing_context.uses_ai_configuration = True if APP_CONFIG_AICC_MIME_PROFILE in config.content_type: - self._uses_aicc_configuration = True + self._tracing_context.uses_aicc_configuration = True return json.loads(config.value) except json.JSONDecodeError: try: @@ -500,7 +473,7 @@ def _process_feature_flags( ) -> Dict[str, Any]: if feature_flags: # Reset feature flag usage - self._feature_filter_usage = {} + self._tracing_context.reset_feature_filter_usage() processed_feature_flags = [self._process_feature_flag(ff) for ff in feature_flags] self._watched_feature_flags = self._update_watched_feature_flags(feature_flags) @@ -512,7 +485,7 @@ def _process_feature_flags( def _process_feature_flag(self, feature_flag: FeatureFlagConfigurationSetting) -> Dict[str, Any]: feature_flag_value = json.loads(feature_flag.value) self._update_ff_telemetry_metadata(self._origin_endpoint, feature_flag, feature_flag_value) - self._update_feature_filter_telemetry(feature_flag) + self._tracing_context.update_feature_filter_telemetry(feature_flag) return feature_flag_value def _update_watched_settings( @@ -568,73 +541,14 @@ def _update_correlation_context_header( :return: The updated headers dictionary. :rtype: Dict[str, str] """ - if os.environ.get(REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, default="").lower() == "true": - return headers - correlation_context = f"RequestType={request_type}" - - if len(self._feature_filter_usage) > 0: - filters_used = "" - if CUSTOM_FILTER_KEY in self._feature_filter_usage: - filters_used = CUSTOM_FILTER_KEY - if PERCENTAGE_FILTER_KEY in self._feature_filter_usage: - filters_used += ("+" if filters_used else "") + PERCENTAGE_FILTER_KEY - if TIME_WINDOW_FILTER_KEY in self._feature_filter_usage: - filters_used += ("+" if filters_used else "") + TIME_WINDOW_FILTER_KEY - if TARGETING_FILTER_KEY in self._feature_filter_usage: - filters_used += ("+" if filters_used else "") + TARGETING_FILTER_KEY - correlation_context += f",Filters={filters_used}" - - correlation_context += self._uses_feature_flags() - - if uses_key_vault: - correlation_context += ",UsesKeyVault" - host_type = "" - if AzureFunctionEnvironmentVariable in os.environ: - host_type = "AzureFunction" - elif AzureWebAppEnvironmentVariable in os.environ: - host_type = "AzureWebApp" - elif ContainerAppEnvironmentVariable in os.environ: - host_type = "ContainerApp" - elif KubernetesEnvironmentVariable in os.environ: - host_type = "Kubernetes" - elif ServiceFabricEnvironmentVariable in os.environ: - host_type = "ServiceFabric" - if host_type: - correlation_context += f",Host={host_type}" - - if replica_count > 0: - correlation_context += f",ReplicaCount={replica_count}" - - if is_failover_request: - correlation_context += ",Failover" - - features = "" - - if self._uses_load_balancing: - features += "LB+" - - if self._uses_ai_configuration: - features += "AI+" - - if self._uses_aicc_configuration: - features += "AICC+" - - if features: - correlation_context += f",Features={features[:-1]}" - - headers["Correlation-Context"] = correlation_context - return headers - - def _uses_feature_flags(self) -> str: - if not self._feature_flag_enabled: - return "" - package_name = "featuremanagement" - try: - feature_management_version = version(package_name) - return f",FMPyVer={feature_management_version}" - except PackageNotFoundError: - pass - return "" + return self._tracing_context.update_correlation_context_header( + headers=headers, + request_type=request_type, + replica_count=replica_count, + uses_key_vault=uses_key_vault, + feature_flag_enabled=self._feature_flag_enabled, + is_failover_request=is_failover_request, + ) def _deduplicate_settings( self, configuration_settings: List[ConfigurationSetting] 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 1d092e80aa55..daf2c9895e3f 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py @@ -33,6 +33,12 @@ TIME_WINDOW_FILTER_KEY = "TIME" TARGETING_FILTER_KEY = "TRGT" # cspell:disable-line +FEATURE_FLAG_USES_TELEMETRY_TAG = "Telemetry" +FEATURE_FLAG_USES_SEED_TAG = "Seed" +FEATURE_FLAG_MAX_VARIANTS_KEY = "MaxVariants" +FEATURE_FLAG_USES_VARIANT_CONFIGURATION_REFERENCE_TAG = "ConfigRef" +FEATURE_FLAG_FEATURES_KEY = "FFFeatures" + # Mime profiles APP_CONFIG_AI_MIME_PROFILE = "https://azconfig.io/mime-profiles/ai/" APP_CONFIG_AICC_MIME_PROFILE = "https://azconfig.io/mime-profiles/ai/chat-completion" diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py new file mode 100644 index 000000000000..1c7495c8a945 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py @@ -0,0 +1,192 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- +import os +from typing import Dict, Optional +from importlib.metadata import version, PackageNotFoundError +from ._constants import ( + REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, + ServiceFabricEnvironmentVariable, + AzureFunctionEnvironmentVariable, + AzureWebAppEnvironmentVariable, + ContainerAppEnvironmentVariable, + KubernetesEnvironmentVariable, + CUSTOM_FILTER_KEY, + PERCENTAGE_FILTER_KEY, + TIME_WINDOW_FILTER_KEY, + TARGETING_FILTER_KEY, + PERCENTAGE_FILTER_NAMES, + TIME_WINDOW_FILTER_NAMES, + TARGETING_FILTER_NAMES, + FEATURE_FLAG_USES_SEED_TAG, + FEATURE_FLAG_USES_VARIANT_CONFIGURATION_REFERENCE_TAG, + FEATURE_FLAG_USES_TELEMETRY_TAG, +) + +Delimiter = "+" + +class _RequestTracingContext: + """ + Encapsulates request tracing and telemetry configuration values. + """ + + def __init__(self, load_balancing_enabled: bool = False) -> None: + self.uses_load_balancing = load_balancing_enabled + self.uses_ai_configuration = False + self.uses_aicc_configuration = False # AI Chat Completion + self.uses_telemetry = False + self.uses_seed = False + self.uses_variant_configuration_reference = False + self.max_variants: Optional[int] = None + self.feature_filter_usage: Dict[str, bool] = {} + + def update_max_variants(self, size: int) -> None: + """Update max_variants if the new size is larger.""" + if self.max_variants is None or size > self.max_variants: + self.max_variants = size + + def get_features_string(self) -> str: + """Generate the features string for correlation context.""" + features_list = [] + + if self.uses_load_balancing: + features_list.append("LB") + if self.uses_ai_configuration: + features_list.append("AI") + if self.uses_aicc_configuration: + features_list.append("AICC") + + return Delimiter.join(features_list) + + def create_features_string(self) -> str: + """ + Generate the features string for feature flag usage tracking. + + :return: A string containing feature flag usage tags separated by delimiters. + :rtype: str + """ + features_list = [] + + if self.uses_seed: + features_list.append(FEATURE_FLAG_USES_SEED_TAG) + + if self.uses_variant_configuration_reference: + features_list.append(FEATURE_FLAG_USES_VARIANT_CONFIGURATION_REFERENCE_TAG) + + if self.uses_telemetry: + features_list.append(FEATURE_FLAG_USES_TELEMETRY_TAG) + + return Delimiter.join(features_list) + + + + def update_correlation_context_header( + self, + headers: Dict[str, str], + request_type: str, + replica_count: int, + uses_key_vault: bool, + feature_flag_enabled: bool, + is_failover_request: bool = False, + ) -> Dict[str, str]: + """ + Update the correlation context header with telemetry information. + + :param headers: The headers dictionary to update. + :type headers: Dict[str, str] + :param request_type: The type of request (e.g., "Startup", "Watch"). + :type request_type: str + :param replica_count: The number of replica endpoints. + :type replica_count: int + :param uses_key_vault: Whether this request uses Key Vault. + :type uses_key_vault: bool + :param feature_flag_enabled: Whether feature flags are enabled. + :type feature_flag_enabled: bool + :param is_failover_request: Whether this is a failover request. + :type is_failover_request: bool + :return: The updated headers dictionary. + :rtype: Dict[str, str] + """ + if os.environ.get(REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, default="").lower() == "true": + return headers + correlation_context = f"RequestType={request_type}" + + if len(self.feature_filter_usage) > 0: + filters_used = "" + if CUSTOM_FILTER_KEY in self.feature_filter_usage: + filters_used = CUSTOM_FILTER_KEY + if PERCENTAGE_FILTER_KEY in self.feature_filter_usage: + filters_used += ("+" if filters_used else "") + PERCENTAGE_FILTER_KEY + if TIME_WINDOW_FILTER_KEY in self.feature_filter_usage: + filters_used += ("+" if filters_used else "") + TIME_WINDOW_FILTER_KEY + if TARGETING_FILTER_KEY in self.feature_filter_usage: + filters_used += ("+" if filters_used else "") + TARGETING_FILTER_KEY + correlation_context += f",Filters={filters_used}" + + correlation_context += self._get_feature_flags_version(feature_flag_enabled) + + if uses_key_vault: + correlation_context += ",UsesKeyVault" + host_type = "" + if AzureFunctionEnvironmentVariable in os.environ: + host_type = "AzureFunction" + elif AzureWebAppEnvironmentVariable in os.environ: + host_type = "AzureWebApp" + elif ContainerAppEnvironmentVariable in os.environ: + host_type = "ContainerApp" + elif KubernetesEnvironmentVariable in os.environ: + host_type = "Kubernetes" + elif ServiceFabricEnvironmentVariable in os.environ: + host_type = "ServiceFabric" + if host_type: + correlation_context += f",Host={host_type}" + + if replica_count > 0: + correlation_context += f",ReplicaCount={replica_count}" + + if is_failover_request: + correlation_context += ",Failover" + + features = self.get_features_string() + if features: + correlation_context += f",Features={features}" + + headers["Correlation-Context"] = correlation_context + return headers + + def update_feature_filter_telemetry(self, feature_flag) -> None: + """ + Track feature filter usage for App Configuration telemetry. + + :param feature_flag: The feature flag to analyze for filter usage. + """ + # Constants are already imported at module level + + if feature_flag.filters: + for filter in feature_flag.filters: + if filter.get("name") in PERCENTAGE_FILTER_NAMES: + self.feature_filter_usage[PERCENTAGE_FILTER_KEY] = True + elif filter.get("name") in TIME_WINDOW_FILTER_NAMES: + self.feature_filter_usage[TIME_WINDOW_FILTER_KEY] = True + elif filter.get("name") in TARGETING_FILTER_NAMES: + self.feature_filter_usage[TARGETING_FILTER_KEY] = True + else: + self.feature_filter_usage[CUSTOM_FILTER_KEY] = True + + def reset_feature_filter_usage(self) -> None: + """Reset the feature filter usage tracking.""" + self.feature_filter_usage = {} + + def _get_feature_flags_version(self, feature_flag_enabled: bool) -> str: + """Get feature flags version string for correlation context.""" + if not feature_flag_enabled: + return "" + package_name = "featuremanagement" + try: + feature_management_version = version(package_name) + return f",FMPyVer={feature_management_version}" + except PackageNotFoundError: + pass + return "" \ No newline at end of file From 4eba284eca8a7513e15e3692ac34440db1d38b47 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 22 Oct 2025 16:51:01 -0700 Subject: [PATCH 02/16] clearing up + tests --- .../_azureappconfigurationprovider.py | 4 +- .../_azureappconfigurationproviderbase.py | 8 +- .../provider/_request_tracing_context.py | 267 ++++++++--- .../_azureappconfigurationproviderasync.py | 4 +- .../tests/aio/test_async_provider.py | 12 +- .../test_azureappconfigurationproviderbase.py | 193 +------- .../tests/test_provider.py | 12 +- .../tests/test_request_tracing_context.py | 448 ++++++++++++++++++ 8 files changed, 671 insertions(+), 277 deletions(-) create mode 100644 sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py index d70b116744c8..b5190d9b26f1 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py @@ -267,7 +267,7 @@ def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_f configuration_refresh_attempted = False feature_flag_refresh_attempted = False updated_watched_settings: Mapping[Tuple[str, str], Optional[str]] = {} - existing_feature_flag_usage = self._feature_filter_usage.copy() + existing_feature_flag_usage = self._tracing_context.feature_filter_usage.copy() try: if self._watched_settings and self._refresh_timer.needs_refresh(): configuration_refresh_attempted = True @@ -317,7 +317,7 @@ def _attempt_refresh(self, client: ConfigurationClient, replica_count: int, is_f logger.warning("Failed to refresh configurations from endpoint %s", client.endpoint) self._replica_client_manager.backoff(client) # Restore feature flag usage on failure - self._feature_filter_usage = existing_feature_flag_usage + self._tracing_context.feature_filter_usage = existing_feature_flag_usage raise e def refresh(self, **kwargs) -> None: diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py index 476b1b9bdeee..1cacca6c64f9 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -6,7 +6,6 @@ import base64 import hashlib import json -import os import time import datetime from threading import Lock @@ -261,11 +260,12 @@ def _update_ff_telemetry_metadata( feature_flag_reference = f"{endpoint}kv/{feature_flag.key}" if feature_flag.label and not feature_flag.label.isspace(): feature_flag_reference += f"?label={feature_flag.label}" - if feature_flag_value[TELEMETRY_KEY].get("allocation") and feature_flag_value[TELEMETRY_KEY].get("allocation").get("seed"): + if feature_flag_value[TELEMETRY_KEY].get("allocation") and feature_flag_value[TELEMETRY_KEY].get( + "allocation" + ).get("seed"): self._tracing_context.uses_seed = True if feature_flag_value[TELEMETRY_KEY].get("variant"): - size = len(feature_flag_value[TELEMETRY_KEY].get("variant")) - self._tracing_context.update_max_variants(size) + self._tracing_context.update_max_variants(len(feature_flag_value[TELEMETRY_KEY].get("variant"))) if feature_flag_value[TELEMETRY_KEY].get("enabled"): self._tracing_context.uses_telemetry = True feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_REFERENCE_KEY] = feature_flag_reference diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py index 1c7495c8a945..907f279f258a 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py @@ -4,7 +4,7 @@ # license information. # ------------------------------------------------------------------------- import os -from typing import Dict, Optional +from typing import Dict, Optional, List, Tuple from importlib.metadata import version, PackageNotFoundError from ._constants import ( REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, @@ -25,14 +25,31 @@ FEATURE_FLAG_USES_TELEMETRY_TAG, ) + +class HostType: + UNIDENTIFIED = "" + AZURE_WEB_APP = "AzureWebApp" + AZURE_FUNCTION = "AzureFunction" + CONTAINER_APP = "ContainerApp" + KUBERNETES = "Kubernetes" + SERVICE_FABRIC = "ServiceFabric" + + +class RequestType: + STARTUP = "Startup" + WATCH = "Watch" + + Delimiter = "+" -class _RequestTracingContext: + +class _RequestTracingContext: # pylint: disable=too-many-instance-attributes """ Encapsulates request tracing and telemetry configuration values. """ def __init__(self, load_balancing_enabled: bool = False) -> None: + # Main feature tracking properties self.uses_load_balancing = load_balancing_enabled self.uses_ai_configuration = False self.uses_aicc_configuration = False # AI Chat Completion @@ -41,29 +58,48 @@ def __init__(self, load_balancing_enabled: bool = False) -> None: self.uses_variant_configuration_reference = False self.max_variants: Optional[int] = None self.feature_filter_usage: Dict[str, bool] = {} + self.is_key_vault_configured: bool = False + self.replica_count: int = 0 + self.is_failover_request: bool = False + + # Host and environment detection properties + self.host_type: str = _RequestTracingContext.get_host_type() + + # Version tracking + self.feature_management_version: Optional[str] = None def update_max_variants(self, size: int) -> None: - """Update max_variants if the new size is larger.""" + """ + Update max_variants if the new size is larger. + + :param size: size of variant + :type size: int + """ if self.max_variants is None or size > self.max_variants: self.max_variants = size - def get_features_string(self) -> str: - """Generate the features string for correlation context.""" + def _get_features_string(self) -> str: + """ + Generate the features string for correlation context. + + :return: A string containing features used separated by delimiters. + :rtype: str + """ features_list = [] - + if self.uses_load_balancing: features_list.append("LB") if self.uses_ai_configuration: features_list.append("AI") if self.uses_aicc_configuration: features_list.append("AICC") - + return Delimiter.join(features_list) - def create_features_string(self) -> str: + def _create_features_string(self) -> str: """ Generate the features string for feature flag usage tracking. - + :return: A string containing feature flag usage tags separated by delimiters. :rtype: str """ @@ -79,8 +115,70 @@ def create_features_string(self) -> str: features_list.append(FEATURE_FLAG_USES_TELEMETRY_TAG) return Delimiter.join(features_list) - + @staticmethod + def get_host_type() -> str: + """ + Detect the host environment type based on environment variables. + + :return: The detected host type. + :rtype: str + """ + if os.environ.get(AzureFunctionEnvironmentVariable): + return HostType.AZURE_FUNCTION + if os.environ.get(AzureWebAppEnvironmentVariable): + return HostType.AZURE_WEB_APP + if os.environ.get(ContainerAppEnvironmentVariable): + return HostType.CONTAINER_APP + if os.environ.get(KubernetesEnvironmentVariable): + return HostType.KUBERNETES + if os.environ.get(ServiceFabricEnvironmentVariable): + return HostType.SERVICE_FABRIC + + return HostType.UNIDENTIFIED + + @staticmethod + def get_assembly_version(package_name: str) -> Optional[str]: + """ + Get the version of a Python package. + + :param package_name: The name of the package to get version for. + :type package_name: str + :return: Package version string or None if not found. + :rtype: Optional[str] + """ + if not package_name: + return None + + try: + return version(package_name) + except PackageNotFoundError: + pass + + return None + + def reset_ai_configuration_tracing(self) -> None: + """ + Reset AI configuration tracing flags. + """ + self.uses_ai_configuration = False + self.uses_aicc_configuration = False + + def update_ai_configuration_tracing(self, content_type: Optional[str]) -> None: + """ + Update AI configuration tracing based on content type. + + :param content_type: The content type to analyze. + :type content_type: Optional[str] + """ + if not content_type or self.uses_aicc_configuration: + return + + # Check for AI mime profiles in content type + if "https://azconfig.io/mime-profiles/ai" in content_type: + self.uses_ai_configuration = True + if "chat-completion" in content_type: + self.uses_aicc_configuration = True def update_correlation_context_header( self, @@ -109,61 +207,112 @@ def update_correlation_context_header( :return: The updated headers dictionary. :rtype: Dict[str, str] """ - if os.environ.get(REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, default="").lower() == "true": + if os.environ.get(REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, "").lower() == "true": return headers - correlation_context = f"RequestType={request_type}" + # Update instance properties for the correlation context + self.replica_count = replica_count + self.is_key_vault_configured = uses_key_vault + self.is_failover_request = is_failover_request + + # Update feature management version if needed and feature flags are enabled + if feature_flag_enabled and not self.feature_management_version: + self.feature_management_version = self.get_assembly_version("featuremanagement") + + # Key-value pairs for the correlation context + key_values: List[Tuple[str, str]] = [] + tags: List[str] = [] + + # Add request type + key_values.append(("RequestType", request_type)) + + # Add replica count if configured + if self.replica_count > 0: + key_values.append(("ReplicaCount", str(self.replica_count))) + + # Add host type if identified + if self.host_type != HostType.UNIDENTIFIED: + key_values.append(("Host", self.host_type)) + + # Add feature filter information if len(self.feature_filter_usage) > 0: - filters_used = "" - if CUSTOM_FILTER_KEY in self.feature_filter_usage: - filters_used = CUSTOM_FILTER_KEY - if PERCENTAGE_FILTER_KEY in self.feature_filter_usage: - filters_used += ("+" if filters_used else "") + PERCENTAGE_FILTER_KEY - if TIME_WINDOW_FILTER_KEY in self.feature_filter_usage: - filters_used += ("+" if filters_used else "") + TIME_WINDOW_FILTER_KEY - if TARGETING_FILTER_KEY in self.feature_filter_usage: - filters_used += ("+" if filters_used else "") + TARGETING_FILTER_KEY - correlation_context += f",Filters={filters_used}" - - correlation_context += self._get_feature_flags_version(feature_flag_enabled) - - if uses_key_vault: - correlation_context += ",UsesKeyVault" - host_type = "" - if AzureFunctionEnvironmentVariable in os.environ: - host_type = "AzureFunction" - elif AzureWebAppEnvironmentVariable in os.environ: - host_type = "AzureWebApp" - elif ContainerAppEnvironmentVariable in os.environ: - host_type = "ContainerApp" - elif KubernetesEnvironmentVariable in os.environ: - host_type = "Kubernetes" - elif ServiceFabricEnvironmentVariable in os.environ: - host_type = "ServiceFabric" - if host_type: - correlation_context += f",Host={host_type}" - - if replica_count > 0: - correlation_context += f",ReplicaCount={replica_count}" - - if is_failover_request: - correlation_context += ",Failover" - - features = self.get_features_string() - if features: - correlation_context += f",Features={features}" - - headers["Correlation-Context"] = correlation_context + filters_string = self._create_filters_string() + if filters_string: + key_values.append(("Filter", filters_string)) + + # Add max variants if present + if self.max_variants and self.max_variants > 0: + key_values.append(("MaxVariants", str(self.max_variants))) + + # Add feature flag features if present + if self.uses_seed or self.uses_telemetry or self.uses_variant_configuration_reference: + ff_features_string = self._create_features_string() + if ff_features_string: + key_values.append(("FFFeatures", ff_features_string)) + + # Add version information + if self.feature_management_version: + key_values.append(("FMPyVer", self.feature_management_version)) + + # Add general features if present + if self.uses_load_balancing or self.uses_ai_configuration or self.uses_aicc_configuration: + features_string = self._get_features_string() + if features_string: + key_values.append(("Features", features_string)) + + # Add tags + if self.is_key_vault_configured: + tags.append("UsesKeyVault") + + if self.is_failover_request: + tags.append("Failover") + + # Build the correlation context string + context_parts: List[str] = [] + + # Add key-value pairs + for key, value in key_values: + context_parts.append(f"{key}={value}") + + # Add tags + context_parts.extend(tags) + + correlation_context = ",".join(context_parts) + + if correlation_context: + headers["Correlation-Context"] = correlation_context + return headers + def _create_filters_string(self) -> str: + """ + Create a string representing the feature filters in use. + + :return: String of filter names separated by delimiters. + :rtype: str + """ + filters: List[str] = [] + + if CUSTOM_FILTER_KEY in self.feature_filter_usage: + filters.append(CUSTOM_FILTER_KEY) + if PERCENTAGE_FILTER_KEY in self.feature_filter_usage: + filters.append(PERCENTAGE_FILTER_KEY) + if TIME_WINDOW_FILTER_KEY in self.feature_filter_usage: + filters.append(TIME_WINDOW_FILTER_KEY) + if TARGETING_FILTER_KEY in self.feature_filter_usage: + filters.append(TARGETING_FILTER_KEY) + + return Delimiter.join(filters) + def update_feature_filter_telemetry(self, feature_flag) -> None: """ Track feature filter usage for App Configuration telemetry. :param feature_flag: The feature flag to analyze for filter usage. + :type feature_flag: FeatureFlagConfigurationSetting """ # Constants are already imported at module level - + if feature_flag.filters: for filter in feature_flag.filters: if filter.get("name") in PERCENTAGE_FILTER_NAMES: @@ -178,15 +327,3 @@ def update_feature_filter_telemetry(self, feature_flag) -> None: def reset_feature_filter_usage(self) -> None: """Reset the feature filter usage tracking.""" self.feature_filter_usage = {} - - def _get_feature_flags_version(self, feature_flag_enabled: bool) -> str: - """Get feature flags version string for correlation context.""" - if not feature_flag_enabled: - return "" - package_name = "featuremanagement" - try: - feature_management_version = version(package_name) - return f",FMPyVer={feature_management_version}" - except PackageNotFoundError: - pass - return "" \ No newline at end of file diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py index 65bc3b8070d6..565fb6c5bd03 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/aio/_azureappconfigurationproviderasync.py @@ -278,7 +278,7 @@ async def _attempt_refresh( configuration_refresh_attempted = False feature_flag_refresh_attempted = False updated_watched_settings: Mapping[Tuple[str, str], Optional[str]] = {} - existing_feature_flag_usage = self._feature_filter_usage.copy() + existing_feature_flag_usage = self._tracing_context.feature_filter_usage.copy() try: if self._watched_settings and self._refresh_timer.needs_refresh(): configuration_refresh_attempted = True @@ -329,7 +329,7 @@ async def _attempt_refresh( logger.warning("Failed to refresh configurations from endpoint %s", client.endpoint) self._replica_client_manager.backoff(client) # Restore feature flag usage on failure - self._feature_filter_usage = existing_feature_flag_usage + self._tracing_context.feature_filter_usage = existing_feature_flag_usage raise e async def refresh(self, **kwargs) -> None: diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_provider.py index bb2c3ac46fe7..0a01cfb92e19 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/aio/test_async_provider.py @@ -139,8 +139,8 @@ async def test_process_key_value_content_type(self, appconfiguration_connection_ # Assert the processed value is as expected assert processed_value == {"key": "value"} - assert provider._uses_ai_configuration == False - assert provider._uses_aicc_configuration == False + assert provider._tracing_context.uses_ai_configuration == False + assert provider._tracing_context.uses_aicc_configuration == False mock_client_manager.load_configuration_settings.return_value = [ { @@ -157,8 +157,8 @@ async def test_process_key_value_content_type(self, appconfiguration_connection_ ) assert processed_value == {"key": "value"} - assert provider._uses_ai_configuration == True - assert provider._uses_aicc_configuration == False + assert provider._tracing_context.uses_ai_configuration == True + assert provider._tracing_context.uses_aicc_configuration == False mock_client_manager.load_configuration_settings.return_value = [ { @@ -175,8 +175,8 @@ async def test_process_key_value_content_type(self, appconfiguration_connection_ ) assert processed_value == {"key": "value"} - assert provider._uses_ai_configuration == True - assert provider._uses_aicc_configuration == True + assert provider._tracing_context.uses_ai_configuration == True + assert provider._tracing_context.uses_aicc_configuration == True @app_config_decorator_async @recorded_by_proxy_async diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py index 44e3e80f4f27..351545255859 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py @@ -4,7 +4,6 @@ # license information. # ------------------------------------------------------------------------- import unittest -import os import time import datetime import json @@ -18,17 +17,10 @@ is_json_content_type, _build_watched_setting, sdk_allowed_kwargs, - _RefreshTimer, AzureAppConfigurationProviderBase, ) from azure.appconfiguration.provider._models import SettingSelector from azure.appconfiguration.provider._constants import ( - REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, - ServiceFabricEnvironmentVariable, - AzureFunctionEnvironmentVariable, - AzureWebAppEnvironmentVariable, - ContainerAppEnvironmentVariable, - KubernetesEnvironmentVariable, NULL_CHAR, CUSTOM_FILTER_KEY, PERCENTAGE_FILTER_KEY, @@ -44,6 +36,7 @@ APP_CONFIG_AI_MIME_PROFILE, APP_CONFIG_AICC_MIME_PROFILE, ) +from azure.appconfiguration.provider._refresh_timer import _RefreshTimer class TestDelayFailure(unittest.TestCase): @@ -68,153 +61,6 @@ def test_delay_failure_when_insufficient_time_passed(self): self.assertLess(called_delay, 4) -class TestUpdateCorrelationContextHeader(unittest.TestCase): - """Test the _update_correlation_context_header instance method.""" - - def setUp(self): - """Set up test environment.""" - self.headers = {} - self.request_type = "Test" - self.replica_count = 2 - self.is_failover_request = False - - # Create provider instance with test configuration - self.provider = AzureAppConfigurationProviderBase( - endpoint="https://test.azconfig.io", - feature_flag_enabled=True, - ) - # Set up provider state for testing - self.provider._feature_filter_usage = {} - self.provider._uses_key_vault = False - self.provider._uses_load_balancing = False - self.provider._uses_ai_configuration = False - self.provider._uses_aicc_configuration = False - - def test_disabled_tracing_returns_unchanged_headers(self): - """Test that tracing disabled returns headers unchanged.""" - with patch.dict(os.environ, {REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE: "true"}): - result = self.provider._update_correlation_context_header( - self.headers, - self.request_type, - self.replica_count, - self.is_failover_request, - ) - self.assertEqual(result, {}) - - def test_basic_correlation_context(self): - """Test basic correlation context generation.""" - result = self.provider._update_correlation_context_header( - self.headers, - self.request_type, - self.replica_count, - self.is_failover_request, - ) - self.assertIn("Correlation-Context", result) - self.assertIn("RequestType=Test", result["Correlation-Context"]) - self.assertIn("ReplicaCount=2", result["Correlation-Context"]) - - def test_feature_filters_in_correlation_context(self): - """Test feature filters are included in correlation context.""" - # Set up feature filters in provider - self.provider._feature_filter_usage = { - CUSTOM_FILTER_KEY: True, - PERCENTAGE_FILTER_KEY: True, - TIME_WINDOW_FILTER_KEY: True, - TARGETING_FILTER_KEY: True, - } - result = self.provider._update_correlation_context_header( - self.headers, - self.request_type, - self.replica_count, - self.is_failover_request, - ) - context = result["Correlation-Context"] - self.assertIn("Filters=", context) - self.assertIn(CUSTOM_FILTER_KEY, context) - self.assertIn(PERCENTAGE_FILTER_KEY, context) - self.assertIn(TIME_WINDOW_FILTER_KEY, context) - self.assertIn(TARGETING_FILTER_KEY, context) - - def test_host_type_detection(self): - """Test host type detection in various environments.""" - test_cases = [ - (AzureFunctionEnvironmentVariable, "AzureFunction"), - (AzureWebAppEnvironmentVariable, "AzureWebApp"), - (ContainerAppEnvironmentVariable, "ContainerApp"), - (KubernetesEnvironmentVariable, "Kubernetes"), - (ServiceFabricEnvironmentVariable, "ServiceFabric"), - ] - - for env_var, expected_host in test_cases: - with patch.dict(os.environ, {env_var: "test_value"}, clear=True): - result = self.provider._update_correlation_context_header( - {}, - self.request_type, - self.replica_count, - self.is_failover_request, - ) - self.assertIn(f"Host={expected_host}", result["Correlation-Context"]) - - def test_features_in_correlation_context(self): - """Test that features are included in correlation context.""" - # Configure provider with all features enabled - self.provider._uses_load_balancing = True - self.provider._uses_ai_configuration = True - self.provider._uses_aicc_configuration = True - - result = self.provider._update_correlation_context_header( - self.headers, - self.request_type, - self.replica_count, - self.is_failover_request, - ) - context = result["Correlation-Context"] - self.assertIn("Features=LB+AI+AICC", context) - - def test_failover_request_in_correlation_context(self): - """Test that failover request is included in correlation context.""" - result = self.provider._update_correlation_context_header( - self.headers, - self.request_type, - self.replica_count, - False, # uses key vault - True, # is_failover_request=True - ) - self.assertIn("Failover", result["Correlation-Context"]) - - -class TestUsesFeatureFlags(unittest.TestCase): - """Test the _uses_feature_flags instance method.""" - - def setUp(self): - """Set up test environment.""" - self.provider = AzureAppConfigurationProviderBase(endpoint="https://test.azconfig.io") - - def test_no_feature_flags_returns_empty(self): - """Test that no feature flags returns empty string.""" - self.provider._feature_flag_enabled = False - result = self.provider._uses_feature_flags() - self.assertEqual(result, "") - - @patch("azure.appconfiguration.provider._azureappconfigurationproviderbase.version") - def test_feature_flags_with_version(self, mock_version): - """Test that feature flags with version returns version string.""" - mock_version.return_value = "1.0.0" - self.provider._feature_flag_enabled = True - result = self.provider._uses_feature_flags() - self.assertEqual(result, ",FMPyVer=1.0.0") - - @patch("azure.appconfiguration.provider._azureappconfigurationproviderbase.version") - def test_feature_flags_without_package(self, mock_version): - """Test that feature flags without package returns empty string.""" - from importlib.metadata import PackageNotFoundError - - mock_version.side_effect = PackageNotFoundError() - self.provider._feature_flag_enabled = True - result = self.provider._uses_feature_flags() - self.assertEqual(result, "") - - class TestIsJsonContentType(unittest.TestCase): """Test the is_json_content_type function.""" @@ -500,26 +346,6 @@ def test_process_key_value_base_invalid_json(self): result = self.provider._process_key_value_base(config) self.assertEqual(result, '{"invalid": json}') # Should return as string - def test_process_key_value_base_ai_configuration(self): - """Test processing AI configuration content type.""" - config = Mock() - config.content_type = f"application/json; {APP_CONFIG_AI_MIME_PROFILE}" - config.value = '{"ai_config": "value"}' - - result = self.provider._process_key_value_base(config) - self.assertTrue(self.provider._uses_ai_configuration) - self.assertEqual(result, {"ai_config": "value"}) - - def test_process_key_value_base_aicc_configuration(self): - """Test processing AI Chat Completion configuration content type.""" - config = Mock() - config.content_type = f"application/json; {APP_CONFIG_AICC_MIME_PROFILE}" - config.value = '{"aicc_config": "value"}' - - result = self.provider._process_key_value_base(config) - self.assertTrue(self.provider._uses_aicc_configuration) - self.assertEqual(result, {"aicc_config": "value"}) - def test_update_ff_telemetry_metadata(self): """Test feature flag telemetry processing.""" feature_flag = Mock(spec=FeatureFlagConfigurationSetting) @@ -542,23 +368,6 @@ def test_update_ff_telemetry_metadata(self): self.assertIn(FEATURE_FLAG_REFERENCE_KEY, metadata) self.assertIn("test_feature", metadata[FEATURE_FLAG_REFERENCE_KEY]) - def test_update_feature_filter_telemetry(self): - """Test feature flag app config telemetry tracking.""" - feature_flag = Mock(spec=FeatureFlagConfigurationSetting) - feature_flag.filters = [ - {"name": PERCENTAGE_FILTER_NAMES[0]}, - {"name": TIME_WINDOW_FILTER_NAMES[0]}, - {"name": "CustomFilter"}, - {"name": TARGETING_FILTER_NAMES[0]}, - ] - - self.provider._update_feature_filter_telemetry(feature_flag) - - self.assertTrue(self.provider._feature_filter_usage[PERCENTAGE_FILTER_KEY]) - self.assertTrue(self.provider._feature_filter_usage[TIME_WINDOW_FILTER_KEY]) - self.assertTrue(self.provider._feature_filter_usage[CUSTOM_FILTER_KEY]) - self.assertTrue(self.provider._feature_filter_usage[TARGETING_FILTER_KEY]) - def test_generate_allocation_id_no_allocation(self): """Test allocation ID generation with no allocation.""" feature_flag_value: Dict[str, Any] = {"no_allocation": "here"} diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py index 4a1ebf9dccdb..ff010100c3e4 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider.py @@ -157,8 +157,8 @@ def test_process_key_value_content_type(self): # Assert the processed value is as expected assert processed_value == {"key": "value"} - assert provider._uses_ai_configuration == False - assert provider._uses_aicc_configuration == False + assert provider._tracing_context.uses_ai_configuration == False + assert provider._tracing_context.uses_aicc_configuration == False mock_client_manager.load_configuration_settings.return_value = [ { @@ -175,8 +175,8 @@ def test_process_key_value_content_type(self): ) assert processed_value == {"key": "value"} - assert provider._uses_ai_configuration == True - assert provider._uses_aicc_configuration == False + assert provider._tracing_context.uses_ai_configuration == True + assert provider._tracing_context.uses_aicc_configuration == False mock_client_manager.load_configuration_settings.return_value = [ { @@ -193,8 +193,8 @@ def test_process_key_value_content_type(self): ) assert processed_value == {"key": "value"} - assert provider._uses_ai_configuration == True - assert provider._uses_aicc_configuration == True + assert provider._tracing_context.uses_ai_configuration == True + assert provider._tracing_context.uses_aicc_configuration == True @recorded_by_proxy @app_config_decorator diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py new file mode 100644 index 000000000000..66e9e6b37198 --- /dev/null +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py @@ -0,0 +1,448 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- +import unittest +import os +from unittest.mock import patch, Mock + +from azure.appconfiguration.provider._request_tracing_context import ( + _RequestTracingContext, + HostType, + RequestType, +) +from azure.appconfiguration.provider._constants import ( + REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, + ServiceFabricEnvironmentVariable, + AzureFunctionEnvironmentVariable, + AzureWebAppEnvironmentVariable, + ContainerAppEnvironmentVariable, + KubernetesEnvironmentVariable, + CUSTOM_FILTER_KEY, + PERCENTAGE_FILTER_KEY, + TIME_WINDOW_FILTER_KEY, + TARGETING_FILTER_KEY, + PERCENTAGE_FILTER_NAMES, + TIME_WINDOW_FILTER_NAMES, + TARGETING_FILTER_NAMES, + FEATURE_FLAG_USES_SEED_TAG, + FEATURE_FLAG_USES_VARIANT_CONFIGURATION_REFERENCE_TAG, + FEATURE_FLAG_USES_TELEMETRY_TAG, +) + + +class TestRequestTracingContext(unittest.TestCase): + """Test the _RequestTracingContext class.""" + + def setUp(self): + """Set up test environment.""" + self.context = _RequestTracingContext() + + def test_initialization_with_defaults(self): + """Test initialization with default values.""" + context = _RequestTracingContext() + self.assertFalse(context.uses_load_balancing) + self.assertFalse(context.uses_ai_configuration) + self.assertFalse(context.uses_aicc_configuration) + self.assertFalse(context.uses_telemetry) + self.assertFalse(context.uses_seed) + self.assertFalse(context.uses_variant_configuration_reference) + self.assertIsNone(context.max_variants) + self.assertEqual(context.feature_filter_usage, {}) + self.assertEqual(context.host_type, HostType.UNIDENTIFIED) + self.assertFalse(context.is_key_vault_configured) + self.assertEqual(context.replica_count, 0) + self.assertFalse(context.is_failover_request) + + def test_initialization_with_load_balancing(self): + """Test initialization with load balancing enabled.""" + context = _RequestTracingContext(load_balancing_enabled=True) + self.assertTrue(context.uses_load_balancing) + + def testupdate_max_variants(self): + """Test updating max variants.""" + self.context.update_max_variants(5) + self.assertEqual(self.context.max_variants, 5) + + # Should update to larger value + self.context.update_max_variants(10) + self.assertEqual(self.context.max_variants, 10) + + # Should not update to smaller value + self.context.update_max_variants(3) + self.assertEqual(self.context.max_variants, 10) + + def test__get_features_string_empty(self): + """Test _get_features_string with no features enabled.""" + result = self.context._get_features_string() + self.assertEqual(result, "") + + def test__get_features_string_with_features(self): + """Test _get_features_string with features enabled.""" + self.context.uses_load_balancing = True + self.context.uses_ai_configuration = True + self.context.uses_aicc_configuration = True + + result = self.context._get_features_string() + self.assertEqual(result, "LB+AI+AICC") + + def test__create_features_string_empty(self): + """Test _create_features_string with no FF features enabled.""" + result = self.context._create_features_string() + self.assertEqual(result, "") + + def test__create_features_string_with_features(self): + """Test _create_features_string with FF features enabled.""" + self.context.uses_seed = True + self.context.uses_variant_configuration_reference = True + self.context.uses_telemetry = True + + result = self.context._create_features_string() + expected = f"{FEATURE_FLAG_USES_SEED_TAG}+{FEATURE_FLAG_USES_VARIANT_CONFIGURATION_REFERENCE_TAG}+{FEATURE_FLAG_USES_TELEMETRY_TAG}" + self.assertEqual(result, expected) + + def test_get_host_type_unidentified(self): + """Test host type detection with no environment variables.""" + with patch.dict(os.environ, {}, clear=True): + result = _RequestTracingContext.get_host_type() + self.assertEqual(result, HostType.UNIDENTIFIED) + + def test_get_host_type_azure_function(self): + """Test host type detection for Azure Functions.""" + with patch.dict(os.environ, {AzureFunctionEnvironmentVariable: "test_value"}): + result = _RequestTracingContext.get_host_type() + self.assertEqual(result, HostType.AZURE_FUNCTION) + + def test_get_host_type_azure_web_app(self): + """Test host type detection for Azure Web App.""" + with patch.dict(os.environ, {AzureWebAppEnvironmentVariable: "test_value"}): + result = _RequestTracingContext.get_host_type() + self.assertEqual(result, HostType.AZURE_WEB_APP) + + def test_get_host_type_container_app(self): + """Test host type detection for Container App.""" + with patch.dict(os.environ, {ContainerAppEnvironmentVariable: "test_value"}): + result = _RequestTracingContext.get_host_type() + self.assertEqual(result, HostType.CONTAINER_APP) + + def test_get_host_type_kubernetes(self): + """Test host type detection for Kubernetes.""" + with patch.dict(os.environ, {KubernetesEnvironmentVariable: "test_value"}): + result = _RequestTracingContext.get_host_type() + self.assertEqual(result, HostType.KUBERNETES) + + def test_get_host_type_service_fabric(self): + """Test host type detection for Service Fabric.""" + with patch.dict(os.environ, {ServiceFabricEnvironmentVariable: "test_value"}): + result = _RequestTracingContext.get_host_type() + self.assertEqual(result, HostType.SERVICE_FABRIC) + + @patch("azure.appconfiguration.provider._request_tracing_context.version") + def test_get_assembly_version_success(self, mock_version): + """Test successful package version retrieval.""" + mock_version.return_value = "1.2.3" + result = _RequestTracingContext.get_assembly_version("test_package") + self.assertEqual(result, "1.2.3") + + @patch("azure.appconfiguration.provider._request_tracing_context.version") + def test_get_assembly_version_not_found(self, mock_version): + """Test package version retrieval when package not found.""" + from importlib.metadata import PackageNotFoundError + + mock_version.side_effect = PackageNotFoundError() + result = _RequestTracingContext.get_assembly_version("nonexistent_package") + self.assertIsNone(result) + + def test_get_assembly_version_empty_package_name(self): + """Test package version retrieval with empty package name.""" + result = _RequestTracingContext.get_assembly_version("") + self.assertIsNone(result) + + def test_reset_ai_configuration_tracing(self): + """Test reset_ai_configuration_tracing method.""" + self.context.uses_ai_configuration = True + self.context.uses_aicc_configuration = True + + self.context.reset_ai_configuration_tracing() + + self.assertFalse(self.context.uses_ai_configuration) + self.assertFalse(self.context.uses_aicc_configuration) + + def test_update_ai_configuration_tracing_ai_profile(self): + """Test update_ai_configuration_tracing with AI profile.""" + content_type = "application/json; https://azconfig.io/mime-profiles/ai" + self.context.update_ai_configuration_tracing(content_type) + + self.assertTrue(self.context.uses_ai_configuration) + self.assertFalse(self.context.uses_aicc_configuration) + + def test_update_ai_configuration_tracing_aicc_profile(self): + """Test update_ai_configuration_tracing with AI Chat Completion profile.""" + content_type = "application/json; https://azconfig.io/mime-profiles/ai+chat-completion" + self.context.update_ai_configuration_tracing(content_type) + + self.assertTrue(self.context.uses_ai_configuration) + self.assertTrue(self.context.uses_aicc_configuration) + + def test_update_ai_configuration_tracing_no_content_type(self): + """Test update_ai_configuration_tracing with no content type.""" + self.context.update_ai_configuration_tracing(None) + + self.assertFalse(self.context.uses_ai_configuration) + self.assertFalse(self.context.uses_aicc_configuration) + + def test_create_filters_string_empty(self): + """Test _create_filters_string with no filters.""" + result = self.context._create_filters_string() + self.assertEqual(result, "") + + def test_create_filters_string_with_filters(self): + """Test _create_filters_string with all filter types.""" + self.context.feature_filter_usage = { + CUSTOM_FILTER_KEY: True, + PERCENTAGE_FILTER_KEY: True, + TIME_WINDOW_FILTER_KEY: True, + TARGETING_FILTER_KEY: True, + } + + result = self.context._create_filters_string() + expected = f"{CUSTOM_FILTER_KEY}+{PERCENTAGE_FILTER_KEY}+{TIME_WINDOW_FILTER_KEY}+{TARGETING_FILTER_KEY}" + self.assertEqual(result, expected) + + def test_update_feature_filter_telemetry(self): + """Test update_feature_filter_telemetry method.""" + feature_flag = Mock() + feature_flag.filters = [ + {"name": PERCENTAGE_FILTER_NAMES[0]}, + {"name": TIME_WINDOW_FILTER_NAMES[0]}, + {"name": TARGETING_FILTER_NAMES[0]}, + {"name": "CustomFilter"}, + ] + + self.context.update_feature_filter_telemetry(feature_flag) + + self.assertTrue(self.context.feature_filter_usage[PERCENTAGE_FILTER_KEY]) + self.assertTrue(self.context.feature_filter_usage[TIME_WINDOW_FILTER_KEY]) + self.assertTrue(self.context.feature_filter_usage[TARGETING_FILTER_KEY]) + self.assertTrue(self.context.feature_filter_usage[CUSTOM_FILTER_KEY]) + + def test_update_feature_filter_telemetry_no_filters(self): + """Test update_feature_filter_telemetry with no filters.""" + feature_flag = Mock() + feature_flag.filters = None + + self.context.update_feature_filter_telemetry(feature_flag) + + self.assertEqual(self.context.feature_filter_usage, {}) + + def test_reset_feature_filter_usage(self): + """Test reset_feature_filter_usage method.""" + self.context.feature_filter_usage = {CUSTOM_FILTER_KEY: True} + + self.context.reset_feature_filter_usage() + + self.assertEqual(self.context.feature_filter_usage, {}) + + +class TestCreateCorrelationContextHeader(unittest.TestCase): + """Test the update_correlation_context_header method.""" + + def setUp(self): + """Set up test environment.""" + self.context = _RequestTracingContext() + + def test_disabled_tracing_returns_empty_string(self): + """Test that disabled tracing returns empty string.""" + with patch.dict(os.environ, {REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE: "true"}): + headers = {} + result = self.context.update_correlation_context_header(headers, RequestType.STARTUP, 0, False, False) + self.assertEqual(result, headers) # Should return headers unchanged + self.assertNotIn("Correlation-Context", result) + + def test_basic_correlation_context(self): + """Test basic correlation context generation.""" + headers = {} + result = self.context.update_correlation_context_header(headers, RequestType.STARTUP, 0, False, False) + self.assertIn("Correlation-Context", result) + self.assertIn("RequestType=Startup", result["Correlation-Context"]) + + def test_correlation_context_with_replica_count(self): + """Test correlation context with replica count.""" + headers = {} + result = self.context.update_correlation_context_header(headers, RequestType.WATCH, 3, False, False) + self.assertIn("Correlation-Context", result) + self.assertIn("RequestType=Watch", result["Correlation-Context"]) + self.assertIn("ReplicaCount=3", result["Correlation-Context"]) + + def test_correlation_context_with_host_type(self): + """Test correlation context with host type detection.""" + with patch.object(_RequestTracingContext, "get_host_type", return_value=HostType.AZURE_FUNCTION): + # Update host_type since it's not automatically set + self.context.host_type = HostType.AZURE_FUNCTION + headers = {} + result = self.context.update_correlation_context_header(headers, RequestType.STARTUP, 0, False, False) + self.assertIn("Correlation-Context", result) + self.assertIn("Host=AzureFunction", result["Correlation-Context"]) + + def test_correlation_context_with_feature_filters(self): + """Test correlation context with feature filters.""" + self.context.feature_filter_usage = { + CUSTOM_FILTER_KEY: True, + PERCENTAGE_FILTER_KEY: True, + } + headers = {} + result = self.context.update_correlation_context_header(headers, RequestType.STARTUP, 0, False, False) + self.assertIn("Correlation-Context", result) + self.assertIn("Filter=", result["Correlation-Context"]) + self.assertIn(CUSTOM_FILTER_KEY, result["Correlation-Context"]) + self.assertIn(PERCENTAGE_FILTER_KEY, result["Correlation-Context"]) + + def test_correlation_context_with_max_variants(self): + """Test correlation context with max variants.""" + self.context.max_variants = 5 + headers = {} + result = self.context.update_correlation_context_header(headers, RequestType.STARTUP, 0, False, False) + self.assertIn("Correlation-Context", result) + self.assertIn("MaxVariants=5", result["Correlation-Context"]) + + def test_correlation_context_with_ff_features(self): + """Test correlation context with feature flag features.""" + self.context.uses_seed = True + self.context.uses_telemetry = True + headers = {} + result = self.context.update_correlation_context_header(headers, RequestType.STARTUP, 0, False, False) + self.assertIn("Correlation-Context", result) + self.assertIn("FFFeatures=", result["Correlation-Context"]) + self.assertIn(FEATURE_FLAG_USES_SEED_TAG, result["Correlation-Context"]) + self.assertIn(FEATURE_FLAG_USES_TELEMETRY_TAG, result["Correlation-Context"]) + + @patch("azure.appconfiguration.provider._request_tracing_context.version") + def test_correlation_context_with_version(self, mock_version): + """Test correlation context with feature management version.""" + mock_version.return_value = "1.0.0" + self.context.feature_management_version = "1.0.0" + headers = {} + result = self.context.update_correlation_context_header(headers, RequestType.STARTUP, 0, False, False) + self.assertIn("Correlation-Context", result) + self.assertIn("FMPyVer=1.0.0", result["Correlation-Context"]) + + def test_correlation_context_with_general_features(self): + """Test correlation context with general features.""" + self.context.uses_load_balancing = True + self.context.uses_ai_configuration = True + headers = {} + result = self.context.update_correlation_context_header(headers, RequestType.STARTUP, 0, False, False) + self.assertIn("Correlation-Context", result) + self.assertIn("Features=LB+AI", result["Correlation-Context"]) + + def test_correlation_context_with_tags(self): + """Test correlation context with various tags.""" + self.context.is_key_vault_configured = True + self.context.is_failover_request = True + self.context.is_push_refresh_used = True + + headers = {} + result = self.context.update_correlation_context_header(headers, RequestType.STARTUP, 0, True, False, True) + self.assertIn("Correlation-Context", result) + self.assertIn("UsesKeyVault", result["Correlation-Context"]) + self.assertIn("Failover", result["Correlation-Context"]) + + def test_correlation_context_comprehensive(self): + """Test correlation context with all features enabled.""" + # Set up all possible features + self.context.host_type = HostType.AZURE_WEB_APP + self.context.feature_filter_usage = {CUSTOM_FILTER_KEY: True} + self.context.max_variants = 3 + self.context.uses_seed = True + self.context.feature_management_version = "1.0.0" + self.context.uses_load_balancing = True + self.context.is_key_vault_configured = True + self.context.is_failover_request = True + + headers = {} + result = self.context.update_correlation_context_header(headers, RequestType.WATCH, 2, True, True, True) + + # Verify all components are present + self.assertIn("Correlation-Context", result) + correlation_context = result["Correlation-Context"] + self.assertIn("RequestType=Watch", correlation_context) + self.assertIn("ReplicaCount=2", correlation_context) + self.assertIn("Host=AzureWebApp", correlation_context) + self.assertIn("Filter=", correlation_context) + self.assertIn("MaxVariants=3", correlation_context) + self.assertIn("FFFeatures=", correlation_context) + self.assertIn("FMPyVer=1.0.0", correlation_context) + self.assertIn("Features=LB", correlation_context) + self.assertIn("UsesKeyVault", correlation_context) + self.assertIn("Failover", correlation_context) + + +class TestUpdateCorrelationContextHeader(unittest.TestCase): + """Test the update_correlation_context_header method.""" + + def setUp(self): + """Set up test environment.""" + self.context = _RequestTracingContext() + self.headers = {} + + def test_update_correlation_context_header_basic(self): + """Test basic correlation context header update.""" + result = self.context.update_correlation_context_header( + self.headers, + RequestType.STARTUP, + 2, # replica_count + True, # uses_key_vault + True, # feature_flag_enabled + False, # is_failover_request + ) + + self.assertIn("Correlation-Context", result) + self.assertIn("RequestType=Startup", result["Correlation-Context"]) + self.assertIn("ReplicaCount=2", result["Correlation-Context"]) + self.assertIn("UsesKeyVault", result["Correlation-Context"]) + + @patch("azure.appconfiguration.provider._request_tracing_context.version") + def test_update_correlation_context_header_with_version(self, mock_version): + """Test correlation context header update with feature management version.""" + mock_version.return_value = "1.0.0" + + result = self.context.update_correlation_context_header( + self.headers, + RequestType.STARTUP, + 0, # replica_count + False, # uses_key_vault + True, # feature_flag_enabled + False, # is_failover_request + ) + + self.assertIn("Correlation-Context", result) + self.assertIn("FMPyVer=1.0.0", result["Correlation-Context"]) + + def test_update_correlation_context_header_failover(self): + """Test correlation context header update with failover request.""" + result = self.context.update_correlation_context_header( + self.headers, + RequestType.WATCH, + 1, # replica_count + False, # uses_key_vault + False, # feature_flag_enabled + True, # is_failover_request + ) + + self.assertIn("Correlation-Context", result) + self.assertIn("Failover", result["Correlation-Context"]) + + def test_update_correlation_context_header_disabled_tracing(self): + """Test correlation context header update with disabled tracing.""" + with patch.dict(os.environ, {REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE: "true"}): + result = self.context.update_correlation_context_header( + self.headers, + RequestType.STARTUP, + 1, # replica_count + False, # uses_key_vault + False, # feature_flag_enabled + False, # is_failover_request + ) + + self.assertNotIn("Correlation-Context", result) From 01be102e23823d0286f99dea247f47f2c08336e6 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 22 Oct 2025 16:53:47 -0700 Subject: [PATCH 03/16] uses aap --- .../azure/appconfiguration/provider/_request_tracing_context.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py index 907f279f258a..8d2359d46b71 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py @@ -93,6 +93,8 @@ def _get_features_string(self) -> str: features_list.append("AI") if self.uses_aicc_configuration: features_list.append("AICC") + if self.get_assembly_version("azure-ai-projects"): + features_list.append("USE_AI") return Delimiter.join(features_list) From 2555ed3169ecb8633aa663a86463997b8d23d9b5 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Wed, 22 Oct 2025 17:05:40 -0700 Subject: [PATCH 04/16] removed unused feature --- .../azure-appconfiguration-provider/CHANGELOG.md | 2 ++ .../azure/appconfiguration/provider/_constants.py | 1 - .../appconfiguration/provider/_request_tracing_context.py | 7 +------ .../tests/test_request_tracing_context.py | 5 +---- 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md index f109419187a6..a01e112cc200 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md +++ b/sdk/appconfiguration/azure-appconfiguration-provider/CHANGELOG.md @@ -15,6 +15,8 @@ ### Other Changes +* Updated Request Tracing + ## 2.2.0 (2025-08-08) ### Features Added 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 daf2c9895e3f..7bea6bdbc136 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py @@ -36,7 +36,6 @@ FEATURE_FLAG_USES_TELEMETRY_TAG = "Telemetry" FEATURE_FLAG_USES_SEED_TAG = "Seed" FEATURE_FLAG_MAX_VARIANTS_KEY = "MaxVariants" -FEATURE_FLAG_USES_VARIANT_CONFIGURATION_REFERENCE_TAG = "ConfigRef" FEATURE_FLAG_FEATURES_KEY = "FFFeatures" # Mime profiles diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py index 8d2359d46b71..948b221027f1 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py @@ -21,7 +21,6 @@ TIME_WINDOW_FILTER_NAMES, TARGETING_FILTER_NAMES, FEATURE_FLAG_USES_SEED_TAG, - FEATURE_FLAG_USES_VARIANT_CONFIGURATION_REFERENCE_TAG, FEATURE_FLAG_USES_TELEMETRY_TAG, ) @@ -55,7 +54,6 @@ def __init__(self, load_balancing_enabled: bool = False) -> None: self.uses_aicc_configuration = False # AI Chat Completion self.uses_telemetry = False self.uses_seed = False - self.uses_variant_configuration_reference = False self.max_variants: Optional[int] = None self.feature_filter_usage: Dict[str, bool] = {} self.is_key_vault_configured: bool = False @@ -110,9 +108,6 @@ def _create_features_string(self) -> str: if self.uses_seed: features_list.append(FEATURE_FLAG_USES_SEED_TAG) - if self.uses_variant_configuration_reference: - features_list.append(FEATURE_FLAG_USES_VARIANT_CONFIGURATION_REFERENCE_TAG) - if self.uses_telemetry: features_list.append(FEATURE_FLAG_USES_TELEMETRY_TAG) @@ -247,7 +242,7 @@ def update_correlation_context_header( key_values.append(("MaxVariants", str(self.max_variants))) # Add feature flag features if present - if self.uses_seed or self.uses_telemetry or self.uses_variant_configuration_reference: + if self.uses_seed or self.uses_telemetry: ff_features_string = self._create_features_string() if ff_features_string: key_values.append(("FFFeatures", ff_features_string)) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py index 66e9e6b37198..0060451352e2 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py @@ -27,7 +27,6 @@ TIME_WINDOW_FILTER_NAMES, TARGETING_FILTER_NAMES, FEATURE_FLAG_USES_SEED_TAG, - FEATURE_FLAG_USES_VARIANT_CONFIGURATION_REFERENCE_TAG, FEATURE_FLAG_USES_TELEMETRY_TAG, ) @@ -47,7 +46,6 @@ def test_initialization_with_defaults(self): self.assertFalse(context.uses_aicc_configuration) self.assertFalse(context.uses_telemetry) self.assertFalse(context.uses_seed) - self.assertFalse(context.uses_variant_configuration_reference) self.assertIsNone(context.max_variants) self.assertEqual(context.feature_filter_usage, {}) self.assertEqual(context.host_type, HostType.UNIDENTIFIED) @@ -95,11 +93,10 @@ def test__create_features_string_empty(self): def test__create_features_string_with_features(self): """Test _create_features_string with FF features enabled.""" self.context.uses_seed = True - self.context.uses_variant_configuration_reference = True self.context.uses_telemetry = True result = self.context._create_features_string() - expected = f"{FEATURE_FLAG_USES_SEED_TAG}+{FEATURE_FLAG_USES_VARIANT_CONFIGURATION_REFERENCE_TAG}+{FEATURE_FLAG_USES_TELEMETRY_TAG}" + expected = f"{FEATURE_FLAG_USES_SEED_TAG}+{FEATURE_FLAG_USES_TELEMETRY_TAG}" self.assertEqual(result, expected) def test_get_host_type_unidentified(self): From f141fcdf3ecc2d0142cc26e89040d8dedfc270d5 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 23 Oct 2025 09:07:56 -0700 Subject: [PATCH 05/16] Update _azureappconfigurationproviderbase.py --- .../provider/_azureappconfigurationproviderbase.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py index 1cacca6c64f9..14595fe1c936 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -260,9 +260,8 @@ def _update_ff_telemetry_metadata( feature_flag_reference = f"{endpoint}kv/{feature_flag.key}" if feature_flag.label and not feature_flag.label.isspace(): feature_flag_reference += f"?label={feature_flag.label}" - if feature_flag_value[TELEMETRY_KEY].get("allocation") and feature_flag_value[TELEMETRY_KEY].get( - "allocation" - ).get("seed"): + allocation = feature_flag_value[TELEMETRY_KEY].get("allocation") + if allocation and allocation.get("seed"): self._tracing_context.uses_seed = True if feature_flag_value[TELEMETRY_KEY].get("variant"): self._tracing_context.update_max_variants(len(feature_flag_value[TELEMETRY_KEY].get("variant"))) From 13c9a2bd514a6bfeab16269dee64e25bea494039 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 3 Nov 2025 14:52:56 -0800 Subject: [PATCH 06/16] Updating Constants --- .../provider/_request_tracing_context.py | 65 +++++++++++++------ 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py index 948b221027f1..d151a4dd2969 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py @@ -24,6 +24,33 @@ FEATURE_FLAG_USES_TELEMETRY_TAG, ) +# Feature flag constants for telemetry +LOAD_BALANCING_FEATURE = "LB" +AI_CONFIGURATION_FEATURE = "AI" +AI_CHAT_COMPLETION_FEATURE = "AICC" +AI_FOUNDRY_SDK_FEATURE = "USE_AI_FOUNDRY_SDK" + +# Package name constants +AZURE_AI_PROJECTS_PACKAGE = "azure-ai-projects" + +# MIME profile constants +AI_MIME_PROFILE = "https://azconfig.io/mime-profiles/ai" +CHAT_COMPLETION_PROFILE = "chat-completion" + +# Correlation context constants +FEATUREMANAGEMENT_PACKAGE = "featuremanagement" +CORRELATION_CONTEXT_HEADER = "Correlation-Context" +REQUEST_TYPE_KEY = "RequestType" +REPLICA_COUNT_KEY = "ReplicaCount" +HOST_KEY = "Host" +FILTER_KEY = "Filter" +MAX_VARIANTS_KEY = "MaxVariants" +FF_FEATURES_KEY = "FFFeatures" +FM_PY_VER_KEY = "FMPyVer" +FEATURES_KEY = "Features" +USES_KEY_VAULT_TAG = "UsesKeyVault" +FAILOVER_TAG = "Failover" + class HostType: UNIDENTIFIED = "" @@ -86,13 +113,13 @@ def _get_features_string(self) -> str: features_list = [] if self.uses_load_balancing: - features_list.append("LB") + features_list.append(LOAD_BALANCING_FEATURE) if self.uses_ai_configuration: - features_list.append("AI") + features_list.append(AI_CONFIGURATION_FEATURE) if self.uses_aicc_configuration: - features_list.append("AICC") - if self.get_assembly_version("azure-ai-projects"): - features_list.append("USE_AI") + features_list.append(AI_CHAT_COMPLETION_FEATURE) + if self.get_assembly_version(AZURE_AI_PROJECTS_PACKAGE): + features_list.append(AI_FOUNDRY_SDK_FEATURE) return Delimiter.join(features_list) @@ -172,9 +199,9 @@ def update_ai_configuration_tracing(self, content_type: Optional[str]) -> None: return # Check for AI mime profiles in content type - if "https://azconfig.io/mime-profiles/ai" in content_type: + if AI_MIME_PROFILE in content_type: self.uses_ai_configuration = True - if "chat-completion" in content_type: + if CHAT_COMPLETION_PROFILE in content_type: self.uses_aicc_configuration = True def update_correlation_context_header( @@ -214,55 +241,55 @@ def update_correlation_context_header( # Update feature management version if needed and feature flags are enabled if feature_flag_enabled and not self.feature_management_version: - self.feature_management_version = self.get_assembly_version("featuremanagement") + self.feature_management_version = self.get_assembly_version(FEATUREMANAGEMENT_PACKAGE) # Key-value pairs for the correlation context key_values: List[Tuple[str, str]] = [] tags: List[str] = [] # Add request type - key_values.append(("RequestType", request_type)) + key_values.append((REQUEST_TYPE_KEY, request_type)) # Add replica count if configured if self.replica_count > 0: - key_values.append(("ReplicaCount", str(self.replica_count))) + key_values.append((REPLICA_COUNT_KEY, str(self.replica_count))) # Add host type if identified if self.host_type != HostType.UNIDENTIFIED: - key_values.append(("Host", self.host_type)) + key_values.append((HOST_KEY, self.host_type)) # Add feature filter information if len(self.feature_filter_usage) > 0: filters_string = self._create_filters_string() if filters_string: - key_values.append(("Filter", filters_string)) + key_values.append((FILTER_KEY, filters_string)) # Add max variants if present if self.max_variants and self.max_variants > 0: - key_values.append(("MaxVariants", str(self.max_variants))) + key_values.append((MAX_VARIANTS_KEY, str(self.max_variants))) # Add feature flag features if present if self.uses_seed or self.uses_telemetry: ff_features_string = self._create_features_string() if ff_features_string: - key_values.append(("FFFeatures", ff_features_string)) + key_values.append((FF_FEATURES_KEY, ff_features_string)) # Add version information if self.feature_management_version: - key_values.append(("FMPyVer", self.feature_management_version)) + key_values.append((FM_PY_VER_KEY, self.feature_management_version)) # Add general features if present if self.uses_load_balancing or self.uses_ai_configuration or self.uses_aicc_configuration: features_string = self._get_features_string() if features_string: - key_values.append(("Features", features_string)) + key_values.append((FEATURES_KEY, features_string)) # Add tags if self.is_key_vault_configured: - tags.append("UsesKeyVault") + tags.append(USES_KEY_VAULT_TAG) if self.is_failover_request: - tags.append("Failover") + tags.append(FAILOVER_TAG) # Build the correlation context string context_parts: List[str] = [] @@ -277,7 +304,7 @@ def update_correlation_context_header( correlation_context = ",".join(context_parts) if correlation_context: - headers["Correlation-Context"] = correlation_context + headers[CORRELATION_CONTEXT_HEADER] = correlation_context return headers From 25916c92c24020b3a3009f417448a01810eabb2c Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 6 Nov 2025 14:37:35 -0800 Subject: [PATCH 07/16] Updating imports and constants --- .../appconfiguration/provider/_constants.py | 14 ----- .../provider/_request_tracing_context.py | 52 ++++++++++--------- .../test_azureappconfigurationproviderbase.py | 9 ---- .../tests/test_request_tracing_context.py | 22 ++++---- 4 files changed, 40 insertions(+), 57 deletions(-) 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 7bea6bdbc136..c683b43ecfba 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py @@ -24,20 +24,6 @@ ETAG_KEY = "ETag" FEATURE_FLAG_REFERENCE_KEY = "FeatureFlagReference" -PERCENTAGE_FILTER_NAMES = ["Percentage", "PercentageFilter", "Microsoft.Percentage", "Microsoft.PercentageFilter"] -TIME_WINDOW_FILTER_NAMES = ["TimeWindow", "TimeWindowFilter", "Microsoft.TimeWindow", "Microsoft.TimeWindowFilter"] -TARGETING_FILTER_NAMES = ["Targeting", "TargetingFilter", "Microsoft.Targeting", "Microsoft.TargetingFilter"] - -CUSTOM_FILTER_KEY = "CSTM" # cspell:disable-line -PERCENTAGE_FILTER_KEY = "PRCNT" # cspell:disable-line -TIME_WINDOW_FILTER_KEY = "TIME" -TARGETING_FILTER_KEY = "TRGT" # cspell:disable-line - -FEATURE_FLAG_USES_TELEMETRY_TAG = "Telemetry" -FEATURE_FLAG_USES_SEED_TAG = "Seed" -FEATURE_FLAG_MAX_VARIANTS_KEY = "MaxVariants" -FEATURE_FLAG_FEATURES_KEY = "FFFeatures" - # Mime profiles APP_CONFIG_AI_MIME_PROFILE = "https://azconfig.io/mime-profiles/ai/" APP_CONFIG_AICC_MIME_PROFILE = "https://azconfig.io/mime-profiles/ai/chat-completion" diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py index d151a4dd2969..ec26c326a757 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py @@ -13,17 +13,27 @@ AzureWebAppEnvironmentVariable, ContainerAppEnvironmentVariable, KubernetesEnvironmentVariable, - CUSTOM_FILTER_KEY, - PERCENTAGE_FILTER_KEY, - TIME_WINDOW_FILTER_KEY, - TARGETING_FILTER_KEY, - PERCENTAGE_FILTER_NAMES, - TIME_WINDOW_FILTER_NAMES, - TARGETING_FILTER_NAMES, - FEATURE_FLAG_USES_SEED_TAG, - FEATURE_FLAG_USES_TELEMETRY_TAG, + APP_CONFIG_AI_MIME_PROFILE, ) +# Feature flag filter names +PERCENTAGE_FILTER_NAMES = ["Percentage", "PercentageFilter", "Microsoft.Percentage", "Microsoft.PercentageFilter"] +TIME_WINDOW_FILTER_NAMES = ["TimeWindow", "TimeWindowFilter", "Microsoft.TimeWindow", "Microsoft.TimeWindowFilter"] +TARGETING_FILTER_NAMES = ["Targeting", "TargetingFilter", "Microsoft.Targeting", "Microsoft.TargetingFilter"] + +CUSTOM_FILTER_KEY = "CSTM" # cspell:disable-line +PERCENTAGE_FILTER_KEY = "PRCNT" # cspell:disable-line +TIME_WINDOW_FILTER_KEY = "TIME" +TARGETING_FILTER_KEY = "TRGT" # cspell:disable-line + +FEATURE_FLAG_USES_TELEMETRY_TAG = "Telemetry" +FEATURE_FLAG_USES_SEED_TAG = "Seed" +FEATURE_FLAG_MAX_VARIANTS_KEY = "MaxVariants" +FEATURE_FLAG_FEATURES_KEY = "FFFeatures" + +CHAT_COMPLETION_PROFILE = "chat-completion" + + # Feature flag constants for telemetry LOAD_BALANCING_FEATURE = "LB" AI_CONFIGURATION_FEATURE = "AI" @@ -33,10 +43,6 @@ # Package name constants AZURE_AI_PROJECTS_PACKAGE = "azure-ai-projects" -# MIME profile constants -AI_MIME_PROFILE = "https://azconfig.io/mime-profiles/ai" -CHAT_COMPLETION_PROFILE = "chat-completion" - # Correlation context constants FEATUREMANAGEMENT_PACKAGE = "featuremanagement" CORRELATION_CONTEXT_HEADER = "Correlation-Context" @@ -199,7 +205,7 @@ def update_ai_configuration_tracing(self, content_type: Optional[str]) -> None: return # Check for AI mime profiles in content type - if AI_MIME_PROFILE in content_type: + if APP_CONFIG_AI_MIME_PROFILE in content_type: self.uses_ai_configuration = True if CHAT_COMPLETION_PROFILE in content_type: self.uses_aicc_configuration = True @@ -269,21 +275,19 @@ def update_correlation_context_header( key_values.append((MAX_VARIANTS_KEY, str(self.max_variants))) # Add feature flag features if present - if self.uses_seed or self.uses_telemetry: - ff_features_string = self._create_features_string() - if ff_features_string: - key_values.append((FF_FEATURES_KEY, ff_features_string)) + ff_features_string = self._create_features_string() + if ff_features_string: + key_values.append((FF_FEATURES_KEY, ff_features_string)) + + # Add general features if present + features_string = self._get_features_string() + if features_string: + key_values.append((FEATURES_KEY, features_string)) # Add version information if self.feature_management_version: key_values.append((FM_PY_VER_KEY, self.feature_management_version)) - # Add general features if present - if self.uses_load_balancing or self.uses_ai_configuration or self.uses_aicc_configuration: - features_string = self._get_features_string() - if features_string: - key_values.append((FEATURES_KEY, features_string)) - # Add tags if self.is_key_vault_configured: tags.append(USES_KEY_VAULT_TAG) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py index 351545255859..8e370f9378ec 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py @@ -22,19 +22,10 @@ from azure.appconfiguration.provider._models import SettingSelector from azure.appconfiguration.provider._constants import ( NULL_CHAR, - CUSTOM_FILTER_KEY, - PERCENTAGE_FILTER_KEY, - TIME_WINDOW_FILTER_KEY, - TARGETING_FILTER_KEY, - PERCENTAGE_FILTER_NAMES, - TIME_WINDOW_FILTER_NAMES, - TARGETING_FILTER_NAMES, TELEMETRY_KEY, METADATA_KEY, ETAG_KEY, FEATURE_FLAG_REFERENCE_KEY, - APP_CONFIG_AI_MIME_PROFILE, - APP_CONFIG_AICC_MIME_PROFILE, ) from azure.appconfiguration.provider._refresh_timer import _RefreshTimer diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py index 0060451352e2..db2e30d409fc 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py @@ -11,14 +11,6 @@ _RequestTracingContext, HostType, RequestType, -) -from azure.appconfiguration.provider._constants import ( - REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, - ServiceFabricEnvironmentVariable, - AzureFunctionEnvironmentVariable, - AzureWebAppEnvironmentVariable, - ContainerAppEnvironmentVariable, - KubernetesEnvironmentVariable, CUSTOM_FILTER_KEY, PERCENTAGE_FILTER_KEY, TIME_WINDOW_FILTER_KEY, @@ -29,6 +21,16 @@ FEATURE_FLAG_USES_SEED_TAG, FEATURE_FLAG_USES_TELEMETRY_TAG, ) +from azure.appconfiguration.provider._constants import ( + REQUEST_TRACING_DISABLED_ENVIRONMENT_VARIABLE, + ServiceFabricEnvironmentVariable, + AzureFunctionEnvironmentVariable, + AzureWebAppEnvironmentVariable, + ContainerAppEnvironmentVariable, + KubernetesEnvironmentVariable, + APP_CONFIG_AI_MIME_PROFILE, + APP_CONFIG_AICC_MIME_PROFILE, +) class TestRequestTracingContext(unittest.TestCase): @@ -168,7 +170,7 @@ def test_reset_ai_configuration_tracing(self): def test_update_ai_configuration_tracing_ai_profile(self): """Test update_ai_configuration_tracing with AI profile.""" - content_type = "application/json; https://azconfig.io/mime-profiles/ai" + content_type = "application/json; " + APP_CONFIG_AI_MIME_PROFILE self.context.update_ai_configuration_tracing(content_type) self.assertTrue(self.context.uses_ai_configuration) @@ -176,7 +178,7 @@ def test_update_ai_configuration_tracing_ai_profile(self): def test_update_ai_configuration_tracing_aicc_profile(self): """Test update_ai_configuration_tracing with AI Chat Completion profile.""" - content_type = "application/json; https://azconfig.io/mime-profiles/ai+chat-completion" + content_type = "application/json; " + APP_CONFIG_AICC_MIME_PROFILE self.context.update_ai_configuration_tracing(content_type) self.assertTrue(self.context.uses_ai_configuration) From 3c5c747643bb8d8aa4520f08d9ce71eda01b2b0f Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 6 Nov 2025 14:39:37 -0800 Subject: [PATCH 08/16] Update _request_tracing_context.py --- .../appconfiguration/provider/_request_tracing_context.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py index ec26c326a757..97635c0275a5 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py @@ -14,6 +14,7 @@ ContainerAppEnvironmentVariable, KubernetesEnvironmentVariable, APP_CONFIG_AI_MIME_PROFILE, + APP_CONFIG_AICC_MIME_PROFILE, ) # Feature flag filter names @@ -207,7 +208,7 @@ def update_ai_configuration_tracing(self, content_type: Optional[str]) -> None: # Check for AI mime profiles in content type if APP_CONFIG_AI_MIME_PROFILE in content_type: self.uses_ai_configuration = True - if CHAT_COMPLETION_PROFILE in content_type: + if APP_CONFIG_AICC_MIME_PROFILE in content_type: self.uses_aicc_configuration = True def update_correlation_context_header( From 1cc0c1f3f009ac4b2f939c9f73402222cff61e9c Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 6 Nov 2025 17:01:39 -0800 Subject: [PATCH 09/16] review comments --- .../_azureappconfigurationproviderbase.py | 6 +- .../provider/_request_tracing_context.py | 196 +++++++++--------- .../tests/test_request_tracing_context.py | 24 +-- 3 files changed, 113 insertions(+), 113 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py index 14595fe1c936..0a260fc5325a 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -260,11 +260,11 @@ def _update_ff_telemetry_metadata( feature_flag_reference = f"{endpoint}kv/{feature_flag.key}" if feature_flag.label and not feature_flag.label.isspace(): feature_flag_reference += f"?label={feature_flag.label}" - allocation = feature_flag_value[TELEMETRY_KEY].get("allocation") + allocation = feature_flag_value.get("allocation") if allocation and allocation.get("seed"): self._tracing_context.uses_seed = True - if feature_flag_value[TELEMETRY_KEY].get("variant"): - self._tracing_context.update_max_variants(len(feature_flag_value[TELEMETRY_KEY].get("variant"))) + if feature_flag_value.get("variant"): + self._tracing_context.update_max_variants(len(feature_flag_value.get("variant", ""))) if feature_flag_value[TELEMETRY_KEY].get("enabled"): self._tracing_context.uses_telemetry = True feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_REFERENCE_KEY] = feature_flag_reference diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py index 97635c0275a5..1a67c9200bcb 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py @@ -110,84 +110,6 @@ def update_max_variants(self, size: int) -> None: if self.max_variants is None or size > self.max_variants: self.max_variants = size - def _get_features_string(self) -> str: - """ - Generate the features string for correlation context. - - :return: A string containing features used separated by delimiters. - :rtype: str - """ - features_list = [] - - if self.uses_load_balancing: - features_list.append(LOAD_BALANCING_FEATURE) - if self.uses_ai_configuration: - features_list.append(AI_CONFIGURATION_FEATURE) - if self.uses_aicc_configuration: - features_list.append(AI_CHAT_COMPLETION_FEATURE) - if self.get_assembly_version(AZURE_AI_PROJECTS_PACKAGE): - features_list.append(AI_FOUNDRY_SDK_FEATURE) - - return Delimiter.join(features_list) - - def _create_features_string(self) -> str: - """ - Generate the features string for feature flag usage tracking. - - :return: A string containing feature flag usage tags separated by delimiters. - :rtype: str - """ - features_list = [] - - if self.uses_seed: - features_list.append(FEATURE_FLAG_USES_SEED_TAG) - - if self.uses_telemetry: - features_list.append(FEATURE_FLAG_USES_TELEMETRY_TAG) - - return Delimiter.join(features_list) - - @staticmethod - def get_host_type() -> str: - """ - Detect the host environment type based on environment variables. - - :return: The detected host type. - :rtype: str - """ - if os.environ.get(AzureFunctionEnvironmentVariable): - return HostType.AZURE_FUNCTION - if os.environ.get(AzureWebAppEnvironmentVariable): - return HostType.AZURE_WEB_APP - if os.environ.get(ContainerAppEnvironmentVariable): - return HostType.CONTAINER_APP - if os.environ.get(KubernetesEnvironmentVariable): - return HostType.KUBERNETES - if os.environ.get(ServiceFabricEnvironmentVariable): - return HostType.SERVICE_FABRIC - - return HostType.UNIDENTIFIED - - @staticmethod - def get_assembly_version(package_name: str) -> Optional[str]: - """ - Get the version of a Python package. - - :param package_name: The name of the package to get version for. - :type package_name: str - :return: Package version string or None if not found. - :rtype: Optional[str] - """ - if not package_name: - return None - - try: - return version(package_name) - except PackageNotFoundError: - pass - - return None - def reset_ai_configuration_tracing(self) -> None: """ Reset AI configuration tracing flags. @@ -276,12 +198,12 @@ def update_correlation_context_header( key_values.append((MAX_VARIANTS_KEY, str(self.max_variants))) # Add feature flag features if present - ff_features_string = self._create_features_string() + ff_features_string = self._create_ff_feature_string() if ff_features_string: key_values.append((FF_FEATURES_KEY, ff_features_string)) # Add general features if present - features_string = self._get_features_string() + features_string = self._create_features_string() if features_string: key_values.append((FEATURES_KEY, features_string)) @@ -313,6 +235,67 @@ def update_correlation_context_header( return headers + def update_feature_filter_telemetry(self, feature_flag) -> None: + """ + Track feature filter usage for App Configuration telemetry. + + :param feature_flag: The feature flag to analyze for filter usage. + :type feature_flag: FeatureFlagConfigurationSetting + """ + # Constants are already imported at module level + + if feature_flag.filters: + for filter in feature_flag.filters: + if filter.get("name") in PERCENTAGE_FILTER_NAMES: + self.feature_filter_usage[PERCENTAGE_FILTER_KEY] = True + elif filter.get("name") in TIME_WINDOW_FILTER_NAMES: + self.feature_filter_usage[TIME_WINDOW_FILTER_KEY] = True + elif filter.get("name") in TARGETING_FILTER_NAMES: + self.feature_filter_usage[TARGETING_FILTER_KEY] = True + else: + self.feature_filter_usage[CUSTOM_FILTER_KEY] = True + + def reset_feature_filter_usage(self) -> None: + """Reset the feature filter usage tracking.""" + self.feature_filter_usage = {} + + def _create_features_string(self) -> str: + """ + Generate the features string for correlation context. + + :return: A string containing features used separated by delimiters. + :rtype: str + """ + features_list = [] + + if self.uses_load_balancing: + features_list.append(LOAD_BALANCING_FEATURE) + if self.uses_ai_configuration: + features_list.append(AI_CONFIGURATION_FEATURE) + if self.uses_aicc_configuration: + features_list.append(AI_CHAT_COMPLETION_FEATURE) + if self.get_assembly_version(AZURE_AI_PROJECTS_PACKAGE): + features_list.append(AI_FOUNDRY_SDK_FEATURE) + + return Delimiter.join(features_list) + + def _create_ff_feature_string(self) -> str: + """ + Generate the features string for feature flag usage tracking. + + :return: A string containing feature flag usage tags separated by delimiters. + :rtype: str + """ + features_list = [] + + if self.uses_seed: + features_list.append(FEATURE_FLAG_USES_SEED_TAG) + + if self.uses_telemetry: + features_list.append(FEATURE_FLAG_USES_TELEMETRY_TAG) + + return Delimiter.join(features_list) + def _create_filters_string(self) -> str: """ Create a string representing the feature filters in use. @@ -333,26 +316,43 @@ def _create_filters_string(self) -> str: return Delimiter.join(filters) - def update_feature_filter_telemetry(self, feature_flag) -> None: + @staticmethod + def get_host_type() -> str: """ - Track feature filter usage for App Configuration telemetry. + Detect the host environment type based on environment variables. - :param feature_flag: The feature flag to analyze for filter usage. - :type feature_flag: FeatureFlagConfigurationSetting + :return: The detected host type. + :rtype: str """ - # Constants are already imported at module level + if os.environ.get(AzureFunctionEnvironmentVariable): + return HostType.AZURE_FUNCTION + if os.environ.get(AzureWebAppEnvironmentVariable): + return HostType.AZURE_WEB_APP + if os.environ.get(ContainerAppEnvironmentVariable): + return HostType.CONTAINER_APP + if os.environ.get(KubernetesEnvironmentVariable): + return HostType.KUBERNETES + if os.environ.get(ServiceFabricEnvironmentVariable): + return HostType.SERVICE_FABRIC - if feature_flag.filters: - for filter in feature_flag.filters: - if filter.get("name") in PERCENTAGE_FILTER_NAMES: - self.feature_filter_usage[PERCENTAGE_FILTER_KEY] = True - elif filter.get("name") in TIME_WINDOW_FILTER_NAMES: - self.feature_filter_usage[TIME_WINDOW_FILTER_KEY] = True - elif filter.get("name") in TARGETING_FILTER_NAMES: - self.feature_filter_usage[TARGETING_FILTER_KEY] = True - else: - self.feature_filter_usage[CUSTOM_FILTER_KEY] = True + return HostType.UNIDENTIFIED - def reset_feature_filter_usage(self) -> None: - """Reset the feature filter usage tracking.""" - self.feature_filter_usage = {} + @staticmethod + def get_assembly_version(package_name: str) -> Optional[str]: + """ + Get the version of a Python package. + + :param package_name: The name of the package to get version for. + :type package_name: str + :return: Package version string or None if not found. + :rtype: Optional[str] + """ + if not package_name: + return None + + try: + return version(package_name) + except PackageNotFoundError: + pass + + return None diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py index db2e30d409fc..aeb3bddd2d49 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py @@ -73,31 +73,31 @@ def testupdate_max_variants(self): self.context.update_max_variants(3) self.assertEqual(self.context.max_variants, 10) - def test__get_features_string_empty(self): - """Test _get_features_string with no features enabled.""" - result = self.context._get_features_string() + def test__create_features_string_empty(self): + """Test _create_features_string with no features enabled.""" + result = self.context._create_features_string() self.assertEqual(result, "") - def test__get_features_string_with_features(self): - """Test _get_features_string with features enabled.""" + def test__create_features_string_with_features(self): + """Test _create_features_string with features enabled.""" self.context.uses_load_balancing = True self.context.uses_ai_configuration = True self.context.uses_aicc_configuration = True - result = self.context._get_features_string() + result = self.context._create_features_string() self.assertEqual(result, "LB+AI+AICC") - def test__create_features_string_empty(self): - """Test _create_features_string with no FF features enabled.""" - result = self.context._create_features_string() + def test__create_ff_features_string_empty(self): + """Test _create_feature_string with no FF features enabled.""" + result = self.context._create_ff_feature_string() self.assertEqual(result, "") - def test__create_features_string_with_features(self): - """Test _create_features_string with FF features enabled.""" + def test__create_ff_features_string_with_features(self): + """Test _create_ff_feature_string with FF features enabled.""" self.context.uses_seed = True self.context.uses_telemetry = True - result = self.context._create_features_string() + result = self.context._create_ff_feature_string() expected = f"{FEATURE_FLAG_USES_SEED_TAG}+{FEATURE_FLAG_USES_TELEMETRY_TAG}" self.assertEqual(result, expected) From 102a772e378c4edd9e3f6149f3c5d654dbcb792b Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 7 Nov 2025 09:09:12 -0800 Subject: [PATCH 10/16] consistant name --- .../appconfiguration/provider/_request_tracing_context.py | 4 ++-- .../tests/test_request_tracing_context.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py index 1a67c9200bcb..8b5fa3d94bc9 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_request_tracing_context.py @@ -198,7 +198,7 @@ def update_correlation_context_header( key_values.append((MAX_VARIANTS_KEY, str(self.max_variants))) # Add feature flag features if present - ff_features_string = self._create_ff_feature_string() + ff_features_string = self._create_ff_features_string() if ff_features_string: key_values.append((FF_FEATURES_KEY, ff_features_string)) @@ -279,7 +279,7 @@ def _create_features_string(self) -> str: return Delimiter.join(features_list) - def _create_ff_feature_string(self) -> str: + def _create_ff_features_string(self) -> str: """ Generate the features string for feature flag usage tracking. diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py index aeb3bddd2d49..1c2537c00086 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_request_tracing_context.py @@ -89,15 +89,15 @@ def test__create_features_string_with_features(self): def test__create_ff_features_string_empty(self): """Test _create_feature_string with no FF features enabled.""" - result = self.context._create_ff_feature_string() + result = self.context._create_ff_features_string() self.assertEqual(result, "") def test__create_ff_features_string_with_features(self): - """Test _create_ff_feature_string with FF features enabled.""" + """Test _create_ff_features_string with FF features enabled.""" self.context.uses_seed = True self.context.uses_telemetry = True - result = self.context._create_ff_feature_string() + result = self.context._create_ff_features_string() expected = f"{FEATURE_FLAG_USES_SEED_TAG}+{FEATURE_FLAG_USES_TELEMETRY_TAG}" self.assertEqual(result, expected) From 88058bf47d846aaae0e7efeb2290230c2955cbcf Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 7 Nov 2025 10:24:13 -0800 Subject: [PATCH 11/16] Update _azureappconfigurationproviderbase.py --- .../provider/_azureappconfigurationproviderbase.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py index 0a260fc5325a..17061bec2b8b 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -251,6 +251,7 @@ def _update_ff_telemetry_metadata( :type feature_flag_value: Dict[str, Any] """ if TELEMETRY_KEY in feature_flag_value: + # Update telemetry metadata for application insights/logging in feature management if METADATA_KEY not in feature_flag_value[TELEMETRY_KEY]: feature_flag_value[TELEMETRY_KEY][METADATA_KEY] = {} feature_flag_value[TELEMETRY_KEY][METADATA_KEY][ETAG_KEY] = feature_flag.etag @@ -263,8 +264,6 @@ def _update_ff_telemetry_metadata( allocation = feature_flag_value.get("allocation") if allocation and allocation.get("seed"): self._tracing_context.uses_seed = True - if feature_flag_value.get("variant"): - self._tracing_context.update_max_variants(len(feature_flag_value.get("variant", ""))) if feature_flag_value[TELEMETRY_KEY].get("enabled"): self._tracing_context.uses_telemetry = True feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_REFERENCE_KEY] = feature_flag_reference @@ -272,6 +271,11 @@ def _update_ff_telemetry_metadata( if allocation_id: feature_flag_value[TELEMETRY_KEY][METADATA_KEY][ALLOCATION_ID_KEY] = allocation_id + variants = feature_flag_value.get("variants") + if variants: + # Update Usage Data for Telemetry + self._tracing_context.update_max_variants(len(variants)) + @staticmethod def _generate_allocation_id(feature_flag_value: Dict[str, JSON]) -> Optional[str]: """ From 8b5b4c8a84646d3a40c34ddb6edfcd0d90d4eac3 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 7 Nov 2025 16:14:59 -0800 Subject: [PATCH 12/16] Update test_azureappconfigurationproviderbase.py --- .../test_azureappconfigurationproviderbase.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py index 8e370f9378ec..1c6532869928 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py @@ -359,6 +359,37 @@ def test_update_ff_telemetry_metadata(self): self.assertIn(FEATURE_FLAG_REFERENCE_KEY, metadata) self.assertIn("test_feature", metadata[FEATURE_FLAG_REFERENCE_KEY]) + def test_update_ff_telemetry_metadata_max_variants(self): + """Test that max_variants only increases, never decreases.""" + feature_flag = Mock(spec=FeatureFlagConfigurationSetting) + + self.assertIsNone(self.provider._tracing_context.max_variants) + + feature_flag_value: Dict[str, Any] = {} + + self.provider._update_ff_telemetry_metadata("", feature_flag, feature_flag_value) + + # Verify max_variants remains None + self.assertIsNone(self.provider._tracing_context.max_variants) + + # First call with 3 variants + feature_flag_value_3: Dict[str, Any] = {"variants": [{}, {}, {}]} # 3 variants + + self.provider._update_ff_telemetry_metadata("", feature_flag, feature_flag_value_3) + self.assertEqual(self.provider._tracing_context.max_variants, 3) + + # Second call with 1 variant (should not decrease) + feature_flag_value_1: Dict[str, Any] = {"variants": [{}]} # 1 variant + + self.provider._update_ff_telemetry_metadata("", feature_flag, feature_flag_value_1) + self.assertEqual(self.provider._tracing_context.max_variants, 3) # Should remain 3 + + # Third call with 5 variants (should increase) + feature_flag_value_5: Dict[str, Any] = {"variants": [{}, {}, {}, {}, {}]} # 5 variants + + self.provider._update_ff_telemetry_metadata("", feature_flag, feature_flag_value_5) + self.assertEqual(self.provider._tracing_context.max_variants, 5) # Should increase to 5 + def test_generate_allocation_id_no_allocation(self): """Test allocation ID generation with no allocation.""" feature_flag_value: Dict[str, Any] = {"no_allocation": "here"} From e86437d436bbb3bcb36dc11e77c3508e38d96ee3 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Fri, 7 Nov 2025 17:01:40 -0800 Subject: [PATCH 13/16] code review comment --- .../_azureappconfigurationproviderbase.py | 43 ++++++++++--------- .../test_azureappconfigurationproviderbase.py | 1 + .../tests/test_provider_feature_management.py | 6 ++- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py index 17061bec2b8b..cf468c1c0795 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -250,26 +250,29 @@ def _update_ff_telemetry_metadata( :param feature_flag_value: The feature flag value dictionary to update. :type feature_flag_value: Dict[str, Any] """ - if TELEMETRY_KEY in feature_flag_value: - # Update telemetry metadata for application insights/logging in feature management - if METADATA_KEY not in feature_flag_value[TELEMETRY_KEY]: - feature_flag_value[TELEMETRY_KEY][METADATA_KEY] = {} - feature_flag_value[TELEMETRY_KEY][METADATA_KEY][ETAG_KEY] = feature_flag.etag - - if not endpoint.endswith("/"): - endpoint += "/" - feature_flag_reference = f"{endpoint}kv/{feature_flag.key}" - if feature_flag.label and not feature_flag.label.isspace(): - feature_flag_reference += f"?label={feature_flag.label}" - allocation = feature_flag_value.get("allocation") - if allocation and allocation.get("seed"): - self._tracing_context.uses_seed = True - if feature_flag_value[TELEMETRY_KEY].get("enabled"): - self._tracing_context.uses_telemetry = True - 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 + if TELEMETRY_KEY not in feature_flag_value: + # Initialize telemetry dictionary if not present + feature_flag_value[TELEMETRY_KEY] = {} + + # Update telemetry metadata for application insights/logging in feature management + if METADATA_KEY not in feature_flag_value[TELEMETRY_KEY]: + feature_flag_value[TELEMETRY_KEY][METADATA_KEY] = {} + feature_flag_value[TELEMETRY_KEY][METADATA_KEY][ETAG_KEY] = feature_flag.etag + + if not endpoint.endswith("/"): + endpoint += "/" + feature_flag_reference = f"{endpoint}kv/{feature_flag.key}" + if feature_flag.label and not feature_flag.label.isspace(): + feature_flag_reference += f"?label={feature_flag.label}" + allocation = feature_flag_value.get("allocation") + if allocation and allocation.get("seed"): + self._tracing_context.uses_seed = True + if feature_flag_value[TELEMETRY_KEY].get("enabled"): + self._tracing_context.uses_telemetry = True + 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 variants = feature_flag_value.get("variants") if variants: diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py index 1c6532869928..72fae8c5a39f 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py @@ -362,6 +362,7 @@ def test_update_ff_telemetry_metadata(self): def test_update_ff_telemetry_metadata_max_variants(self): """Test that max_variants only increases, never decreases.""" feature_flag = Mock(spec=FeatureFlagConfigurationSetting) + feature_flag.etag = "test_etag" self.assertIsNone(self.provider._tracing_context.max_variants) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_management.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_management.py index 180fd7612a02..ffb2c09535b4 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_management.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_provider_feature_management.py @@ -23,8 +23,10 @@ def test_load_only_feature_flags(self, appconfiguration_connection_string): ) assert len(client.keys()) == 1 assert FEATURE_MANAGEMENT_KEY in client - assert has_feature_flag(client, "Alpha") - assert "telemetry" not in get_feature_flag(client, "Alpha") + alpha = get_feature_flag(client, "Alpha") + assert alpha + assert "telemetry" in alpha + assert "enabled" not in alpha.get("telemetry") # method: load @recorded_by_proxy From b08da5e7c60a2f32ae0ae69f00269950e8a619e5 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 10 Nov 2025 09:52:04 -0800 Subject: [PATCH 14/16] Update test_azureappconfigurationproviderbase.py --- .../tests/test_azureappconfigurationproviderbase.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py index 72fae8c5a39f..5e90496b3a6a 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/tests/test_azureappconfigurationproviderbase.py @@ -361,8 +361,7 @@ def test_update_ff_telemetry_metadata(self): def test_update_ff_telemetry_metadata_max_variants(self): """Test that max_variants only increases, never decreases.""" - feature_flag = Mock(spec=FeatureFlagConfigurationSetting) - feature_flag.etag = "test_etag" + feature_flag = FeatureFlagConfigurationSetting("test_feature") self.assertIsNone(self.provider._tracing_context.max_variants) From bc5c02b9a2987ef16b0a91fc7b401c41db5b4fe0 Mon Sep 17 00:00:00 2001 From: Matthew Metcalf Date: Mon, 10 Nov 2025 11:24:34 -0800 Subject: [PATCH 15/16] Update sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py Co-authored-by: Ross Grambo --- .../_azureappconfigurationproviderbase.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py index cf468c1c0795..5afa6b9509e6 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -259,20 +259,22 @@ def _update_ff_telemetry_metadata( feature_flag_value[TELEMETRY_KEY][METADATA_KEY] = {} feature_flag_value[TELEMETRY_KEY][METADATA_KEY][ETAG_KEY] = feature_flag.etag - if not endpoint.endswith("/"): - endpoint += "/" - feature_flag_reference = f"{endpoint}kv/{feature_flag.key}" - if feature_flag.label and not feature_flag.label.isspace(): - feature_flag_reference += f"?label={feature_flag.label}" - allocation = feature_flag_value.get("allocation") - if allocation and allocation.get("seed"): - self._tracing_context.uses_seed = True if feature_flag_value[TELEMETRY_KEY].get("enabled"): self._tracing_context.uses_telemetry = True + if not endpoint.endswith("/"): + endpoint += "/" + feature_flag_reference = f"{endpoint}kv/{feature_flag.key}" + if feature_flag.label and not feature_flag.label.isspace(): + feature_flag_reference += f"?label={feature_flag.label}" + 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 + + allocation = feature_flag_value.get("allocation") + if allocation and allocation.get("seed"): + self._tracing_context.uses_seed = True variants = feature_flag_value.get("variants") if variants: From c5c5c9c220eb17c10370ab415293a8b1fc7a4c96 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 10 Nov 2025 13:05:34 -0800 Subject: [PATCH 16/16] Update _azureappconfigurationproviderbase.py --- .../provider/_azureappconfigurationproviderbase.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py index 5afa6b9509e6..3d71c38d6473 100644 --- a/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py +++ b/sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationproviderbase.py @@ -266,12 +266,12 @@ def _update_ff_telemetry_metadata( feature_flag_reference = f"{endpoint}kv/{feature_flag.key}" if feature_flag.label and not feature_flag.label.isspace(): feature_flag_reference += f"?label={feature_flag.label}" - + 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 - + allocation = feature_flag_value.get("allocation") if allocation and allocation.get("seed"): self._tracing_context.uses_seed = True