Skip to content

Commit be1b8fd

Browse files
mrm9084avanigupta
andauthored
Allocation Id (#37840)
* Adding Telemetry * Telemetry Support * fixing formatting * Update _azureappconfigurationprovider.py * Update _azureappconfigurationproviderasync.py * formatting * changing doc style due to pylint-next * fixing kwargs docs * Formatting * Review comments * Changed label checking. * black format changes * pylint * Update sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_azureappconfigurationprovider.py Co-authored-by: Avani Gupta <[email protected]> * added space checks * Update conftest.py * moved telemetry to client wrapper * fixing format * updating after merge * fixing black issue * removing unused imports * AllocationId * Update CODEOWNERS * Update CODEOWNERS * fixing issues * Update _client_manager_base.py * Fixing configuration value empty in calc * fixing pylint * Update _constants.py * review comments * fixing allocation check * format fix --------- Co-authored-by: Avani Gupta <[email protected]>
1 parent a107c2b commit be1b8fd

File tree

3 files changed

+96
-9
lines changed

3 files changed

+96
-9
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
# ServiceLabel: %App Configuration Provider
8888

8989
# PRLabel: %App Configuration Provider
90-
/sdk/appconfiguration/azure-appconfiguration-provider/ @mametcal @albertofori @avanigupta @mrm9084
90+
/sdk/appconfiguration/azure-appconfiguration-provider/ @albertofori @avanigupta @mrm9084 @rossgrambo
9191

9292
# ServiceLabel: %Attestation
9393
# PRLabel: %Attestation

sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_client_manager_base.py

Lines changed: 92 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
# Licensed under the MIT License. See License.txt in the project root for
44
# license information.
55
# -------------------------------------------------------------------------
6+
import json
67
import time
78
import random
89
import hashlib
910
import base64
1011
from dataclasses import dataclass
11-
from typing import Dict, List
12+
from typing import Dict, List, Optional, Mapping, Any
1213
from azure.appconfiguration import ( # type:ignore # pylint:disable=no-name-in-module
1314
FeatureFlagConfigurationSetting,
1415
)
@@ -25,24 +26,104 @@
2526
ETAG_KEY,
2627
FEATURE_FLAG_REFERENCE_KEY,
2728
FEATURE_FLAG_ID_KEY,
29+
ALLOCATION_ID_KEY,
2830
)
2931

3032
FALLBACK_CLIENT_REFRESH_EXPIRED_INTERVAL = 3600 # 1 hour in seconds
3133
MINIMAL_CLIENT_REFRESH_INTERVAL = 30 # 30 seconds
3234

35+
JSON = Mapping[str, Any]
36+
3337

3438
@dataclass
3539
class _ConfigurationClientWrapperBase:
3640
endpoint: str
3741

42+
@staticmethod
43+
def _generate_allocation_id(feature_flag_value: Dict[str, JSON]) -> Optional[str]:
44+
"""
45+
Generates an allocation ID for the specified feature.
46+
seed=123abc\ndefault_when_enabled=Control\npercentiles=0,Control,20;20,Test,100\nvariants=Control,standard;Test,special # pylint:disable=line-too-long
47+
48+
:param Dict[str, JSON] feature_flag_value: The feature to generate an allocation ID for.
49+
:rtype: str
50+
:return: The allocation ID.
51+
"""
52+
53+
allocation_id = ""
54+
allocated_variants = []
55+
56+
allocation: Optional[JSON] = feature_flag_value.get("allocation")
57+
58+
if allocation:
59+
# Seed
60+
allocation_id = f"seed={allocation.get('seed', '')}"
61+
62+
# DefaultWhenEnabled
63+
if "default_when_enabled" in allocation:
64+
allocated_variants.append(allocation.get("default_when_enabled"))
65+
66+
allocation_id += f"\ndefault_when_enabled={allocation.get('default_when_enabled', '')}"
67+
68+
# Percentile
69+
allocation_id += "\npercentiles="
70+
71+
percentile = allocation.get("percentile")
72+
73+
if percentile:
74+
percentile_allocations = sorted(
75+
(x for x in percentile if x.get("from") != x.get("to")),
76+
key=lambda x: x.get("from"),
77+
)
78+
79+
for percentile_allocation in percentile_allocations:
80+
if "variant" in percentile_allocation:
81+
allocated_variants.append(percentile_allocation.get("variant"))
82+
83+
allocation_id += ";".join(
84+
f"{pa.get('from')}," f"{base64.b64encode(pa.get('variant').encode()).decode()}," f"{pa.get('to')}"
85+
for pa in percentile_allocations
86+
)
87+
else:
88+
allocation_id = "seed=\ndefault_when_enabled=\npercentiles="
89+
90+
if not allocated_variants and (not allocation or not allocation.get("seed")):
91+
return None
92+
93+
# Variants
94+
allocation_id += "\nvariants="
95+
96+
variants_value = feature_flag_value.get("variants")
97+
if variants_value and (isinstance(variants_value, list) or all(isinstance(v, dict) for v in variants_value)):
98+
if allocated_variants:
99+
if isinstance(variants_value, list) and all(isinstance(v, dict) for v in variants_value):
100+
sorted_variants: List[Dict[str, Any]] = sorted(
101+
(v for v in variants_value if v.get("name") in allocated_variants),
102+
key=lambda v: v.get("name"),
103+
)
104+
105+
for v in sorted_variants:
106+
allocation_id += f"{base64.b64encode(v.get('name', '').encode()).decode()},"
107+
if "configuration_value" in v:
108+
allocation_id += f"{json.dumps(v.get('configuration_value', ''), separators=(',', ':'))}"
109+
allocation_id += ";"
110+
allocation_id = allocation_id[:-1]
111+
112+
# Create a sha256 hash of the allocation_id
113+
hash_object = hashlib.sha256(allocation_id.encode())
114+
hash_digest = hash_object.digest()
115+
116+
# Encode the first 15 bytes in base64 url
117+
allocation_id_hash = base64.urlsafe_b64encode(hash_digest[:15]).decode()
118+
return allocation_id_hash
119+
38120
@staticmethod
39121
def _calculate_feature_id(key, label):
40122
basic_value = f"{key}\n"
41123
if label and not label.isspace():
42124
basic_value += f"{label}"
43125
feature_flag_id_hash_bytes = hashlib.sha256(basic_value.encode()).digest()
44-
encoded_flag = base64.b64encode(feature_flag_id_hash_bytes)
45-
encoded_flag = encoded_flag.replace(b"+", b"-").replace(b"/", b"_")
126+
encoded_flag = base64.urlsafe_b64encode(feature_flag_id_hash_bytes)
46127
return encoded_flag[: encoded_flag.find(b"=")]
47128

48129
def _feature_flag_telemetry(
@@ -58,10 +139,14 @@ def _feature_flag_telemetry(
58139
feature_flag_reference = f"{endpoint}kv/{feature_flag.key}"
59140
if feature_flag.label and not feature_flag.label.isspace():
60141
feature_flag_reference += f"?label={feature_flag.label}"
61-
feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_REFERENCE_KEY] = feature_flag_reference
62-
feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_ID_KEY] = self._calculate_feature_id(
63-
feature_flag.key, feature_flag.label
64-
)
142+
if feature_flag_value[TELEMETRY_KEY].get("enabled"):
143+
feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_REFERENCE_KEY] = feature_flag_reference
144+
feature_flag_value[TELEMETRY_KEY][METADATA_KEY][FEATURE_FLAG_ID_KEY] = self._calculate_feature_id(
145+
feature_flag.key, feature_flag.label
146+
)
147+
allocation_id = self._generate_allocation_id(feature_flag_value)
148+
if allocation_id:
149+
feature_flag_value[TELEMETRY_KEY][METADATA_KEY][ALLOCATION_ID_KEY] = allocation_id
65150

66151
def _feature_flag_appconfig_telemetry(
67152
self, feature_flag: FeatureFlagConfigurationSetting, filters_used: Dict[str, bool]

sdk/appconfiguration/azure-appconfiguration-provider/azure/appconfiguration/provider/_constants.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919

2020
TELEMETRY_KEY = "telemetry"
2121
METADATA_KEY = "metadata"
22-
ETAG_KEY = "ETag"
2322

23+
ALLOCATION_ID_KEY = "AllocationId"
24+
ETAG_KEY = "ETag"
2425
FEATURE_FLAG_REFERENCE_KEY = "FeatureFlagReference"
2526
FEATURE_FLAG_ID_KEY = "FeatureFlagId"
27+
2628
PERCENTAGE_FILTER_NAMES = ["Percentage", "PercentageFilter", "Microsoft.Percentage", "Microsoft.PercentageFilter"]
2729
TIME_WINDOW_FILTER_NAMES = ["TimeWindow", "TimeWindowFilter", "Microsoft.TimeWindow", "Microsoft.TimeWindowFilter"]
2830
TARGETING_FILTER_NAMES = ["Targeting", "TargetingFilter", "Microsoft.Targeting", "Microsoft.TargetingFilter"]

0 commit comments

Comments
 (0)