3
3
# Licensed under the MIT License. See License.txt in the project root for
4
4
# license information.
5
5
# -------------------------------------------------------------------------
6
+ import json
6
7
import random
8
+ import base64
9
+ import hashlib
7
10
from dataclasses import dataclass
8
- from typing import Dict , Mapping , Any , Optional
11
+ from typing import Dict , List , Optional , Mapping , Any
9
12
from azure .appconfiguration import ( # type:ignore # pylint:disable=no-name-in-module
10
13
FeatureFlagConfigurationSetting ,
11
14
)
21
24
METADATA_KEY ,
22
25
ETAG_KEY ,
23
26
FEATURE_FLAG_REFERENCE_KEY ,
27
+ ALLOCATION_ID_KEY ,
24
28
)
25
29
26
30
FALLBACK_CLIENT_REFRESH_EXPIRED_INTERVAL = 3600 # 1 hour in seconds
33
37
class _ConfigurationClientWrapperBase :
34
38
endpoint : str
35
39
40
+ @staticmethod
41
+ def _generate_allocation_id (feature_flag_value : Dict [str , JSON ]) -> Optional [str ]:
42
+ """
43
+ Generates an allocation ID for the specified feature.
44
+ seed=123abc\n default_when_enabled=Control\n percentiles=0,Control,20;20,Test,100\n variants=Control,standard;Test,special # pylint:disable=line-too-long
45
+
46
+ :param Dict[str, JSON] feature_flag_value: The feature to generate an allocation ID for.
47
+ :rtype: str
48
+ :return: The allocation ID.
49
+ """
50
+
51
+ allocation_id = ""
52
+ allocated_variants = []
53
+
54
+ allocation : Optional [JSON ] = feature_flag_value .get ("allocation" )
55
+
56
+ if not allocation :
57
+ return None
58
+
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"\n default_when_enabled={ allocation .get ('default_when_enabled' , '' )} "
67
+
68
+ # Percentile
69
+ allocation_id += "\n percentiles="
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
+
88
+ if not allocated_variants and not allocation .get ("seed" ):
89
+ return None
90
+
91
+ # Variants
92
+ allocation_id += "\n variants="
93
+
94
+ variants_value = feature_flag_value .get ("variants" )
95
+ if variants_value and (isinstance (variants_value , list ) or all (isinstance (v , dict ) for v in variants_value )):
96
+ if (
97
+ allocated_variants
98
+ and isinstance (variants_value , list )
99
+ and all (isinstance (v , dict ) for v in variants_value )
100
+ ):
101
+ sorted_variants : List [Dict [str , Any ]] = sorted (
102
+ (v for v in variants_value if v .get ("name" ) in allocated_variants ),
103
+ key = lambda v : v .get ("name" ),
104
+ )
105
+
106
+ for v in sorted_variants :
107
+ allocation_id += f"{ base64 .b64encode (v .get ('name' , '' ).encode ()).decode ()} ,"
108
+ if "configuration_value" in v :
109
+ allocation_id += (
110
+ f"{ json .dumps (v .get ('configuration_value' , '' ), separators = (',' , ':' ), sort_keys = True )} "
111
+ )
112
+ allocation_id += ";"
113
+ if sorted_variants :
114
+ allocation_id = allocation_id [:- 1 ]
115
+
116
+ # Create a sha256 hash of the allocation_id
117
+ hash_object = hashlib .sha256 (allocation_id .encode ())
118
+ hash_digest = hash_object .digest ()
119
+
120
+ # Encode the first 15 bytes in base64 url
121
+ return base64 .urlsafe_b64encode (hash_digest [:15 ]).decode ()
122
+
36
123
def _feature_flag_telemetry (
37
124
self , endpoint : str , feature_flag : FeatureFlagConfigurationSetting , feature_flag_value : Dict
38
125
):
@@ -48,6 +135,9 @@ def _feature_flag_telemetry(
48
135
feature_flag_reference += f"?label={ feature_flag .label } "
49
136
if feature_flag_value [TELEMETRY_KEY ].get ("enabled" ):
50
137
feature_flag_value [TELEMETRY_KEY ][METADATA_KEY ][FEATURE_FLAG_REFERENCE_KEY ] = feature_flag_reference
138
+ allocation_id = self ._generate_allocation_id (feature_flag_value )
139
+ if allocation_id :
140
+ feature_flag_value [TELEMETRY_KEY ][METADATA_KEY ][ALLOCATION_ID_KEY ] = allocation_id
51
141
52
142
def _feature_flag_appconfig_telemetry (
53
143
self , feature_flag : FeatureFlagConfigurationSetting , filters_used : Dict [str , bool ]
0 commit comments