Skip to content

Commit a6e4041

Browse files
authored
Merge pull request #486 from splitio/Feature/FlagSets
Feature/flag sets
2 parents e880f37 + b4dc2a1 commit a6e4041

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+3206
-1618
lines changed

CHANGES.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
9.6.0 (Nov 3, 2023)
2+
- Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation):
3+
- Added new variations of the get treatment methods to support evaluating flags in given flag set/s.
4+
- get_treatments_by_flag_set and get_treatments_by_flag_sets
5+
- get_treatments_with_config_by_flag_set and get_treatments_with_config_by_flag_sets
6+
- Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload.
7+
- Note: Only applicable when the SDK is in charge of the rollout data synchronization. When not applicable, the SDK will log a warning on init.
8+
- Updated the following SDK manager methods to expose flag sets on flag views.
9+
- Removed raising an exception when Telemetry post config data fails, SDK will only log the error.
10+
111
9.5.1 (Sep 5, 2023)
212
- Exclude tests from when building the package
313
- Fixed exception when fetching telemetry stats if no SSE Feature flags update events are stored

splitio/api/commons.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def record_telemetry(status_code, elapsed, metric_name, telemetry_runtime_produc
5757
class FetchOptions(object):
5858
"""Fetch Options object."""
5959

60-
def __init__(self, cache_control_headers=False, change_number=None):
60+
def __init__(self, cache_control_headers=False, change_number=None, sets=None):
6161
"""
6262
Class constructor.
6363
@@ -66,9 +66,13 @@ def __init__(self, cache_control_headers=False, change_number=None):
6666
6767
:param change_number: ChangeNumber to use for bypassing CDN in request.
6868
:type change_number: int
69+
70+
:param sets: list of flag sets
71+
:type sets: list
6972
"""
7073
self._cache_control_headers = cache_control_headers
7174
self._change_number = change_number
75+
self._sets = sets
7276

7377
@property
7478
def cache_control_headers(self):
@@ -80,12 +84,19 @@ def change_number(self):
8084
"""Return change number."""
8185
return self._change_number
8286

87+
@property
88+
def sets(self):
89+
"""Return sets."""
90+
return self._sets
91+
8392
def __eq__(self, other):
8493
"""Match between other options."""
8594
if self._cache_control_headers != other._cache_control_headers:
8695
return False
8796
if self._change_number != other._change_number:
8897
return False
98+
if self._sets != other._sets:
99+
return False
89100
return True
90101

91102

@@ -113,4 +124,6 @@ def build_fetch(change_number, fetch_options, metadata):
113124
extra_headers[_CACHE_CONTROL] = _CACHE_CONTROL_NO_CACHE
114125
if fetch_options.change_number is not None:
115126
query['till'] = fetch_options.change_number
127+
if fetch_options.sets is not None:
128+
query['sets'] = fetch_options.sets
116129
return query, extra_headers

splitio/api/splits.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ def fetch_splits(self, change_number, fetch_options):
5959
if 200 <= response.status_code < 300:
6060
return json.loads(response.body)
6161
else:
62+
if response.status_code == 414:
63+
_LOGGER.error('Error fetching feature flags; the amount of flag sets provided are too big, causing uri length error.')
6264
raise APIException(response.body, response.status_code)
6365
except HttpClientException as exc:
6466
_LOGGER.error('Error fetching feature flags because an exception was raised by the HTTPClient')

splitio/api/telemetry.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ def record_init(self, configs):
7676
'Error posting init config because an exception was raised by the HTTPClient'
7777
)
7878
_LOGGER.debug('Error: ', exc_info=True)
79-
raise APIException('Init config data not flushed properly.') from exc
8079

8180
def record_stats(self, stats):
8281
"""

splitio/client/client.py

Lines changed: 133 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from splitio.models.impressions import Impression, Label
77
from splitio.models.events import Event, EventWrapper
88
from splitio.models.telemetry import get_latency_bucket_index, MethodExceptionsAndLatencies
9-
from splitio.client import input_validator
9+
from splitio.client import input_validator, config
1010
from splitio.util.time import get_current_epoch_time_ms, utctime_ms
1111

1212
_LOGGER = logging.getLogger(__name__)
@@ -59,8 +59,9 @@ def destroyed(self):
5959
"""Return whether the factory holding this client has been destroyed."""
6060
return self._factory.destroyed
6161

62-
def _evaluate_if_ready(self, matching_key, bucketing_key, feature, attributes=None):
62+
def _evaluate_if_ready(self, matching_key, bucketing_key, feature, method, attributes=None):
6363
if not self.ready:
64+
_LOGGER.warning("%s: The SDK is not ready, results may be incorrect for feature flag %s. Make sure to wait for SDK readiness before using this method", method, feature)
6465
self._telemetry_init_producer.record_not_ready_usage()
6566
return {
6667
'treatment': CONTROL,
@@ -102,7 +103,7 @@ def _make_evaluation(self, key, feature_flag, attributes, method_name, metric_na
102103
or not input_validator.validate_attributes(attributes, method_name):
103104
return CONTROL, None
104105

105-
result = self._evaluate_if_ready(matching_key, bucketing_key, feature_flag, attributes)
106+
result = self._evaluate_if_ready(matching_key, bucketing_key, feature_flag, method_name, attributes)
106107

107108
impression = self._build_impression(
108109
matching_key,
@@ -167,7 +168,7 @@ def _make_evaluations(self, key, feature_flags, attributes, method_name, metric_
167168

168169
try:
169170
evaluations = self._evaluate_features_if_ready(matching_key, bucketing_key,
170-
list(feature_flags), attributes)
171+
list(feature_flags), method_name, attributes)
171172

172173
for feature_flag in feature_flags:
173174
try:
@@ -212,8 +213,9 @@ def _make_evaluations(self, key, feature_flags, attributes, method_name, metric_
212213
_LOGGER.debug('Error: ', exc_info=True)
213214
return input_validator.generate_control_treatments(list(feature_flags), method_name)
214215

215-
def _evaluate_features_if_ready(self, matching_key, bucketing_key, feature_flags, attributes=None):
216+
def _evaluate_features_if_ready(self, matching_key, bucketing_key, feature_flags, method, attributes=None):
216217
if not self.ready:
218+
_LOGGER.warning("%s: The SDK is not ready, results may be incorrect for feature flags %s. Make sure to wait for SDK readiness before using this method", method, ', '.join([feature for feature in feature_flags]))
217219
self._telemetry_init_producer.record_not_ready_usage()
218220
return {
219221
feature_flag: {
@@ -309,6 +311,132 @@ def get_treatments(self, key, feature_flags, attributes=None):
309311
MethodExceptionsAndLatencies.TREATMENTS)
310312
return {feature_flag: result[0] for (feature_flag, result) in with_config.items()}
311313

314+
def get_treatments_by_flag_set(self, key, flag_set, attributes=None):
315+
"""
316+
Get treatments for feature flags that contain given flag set.
317+
318+
This method never raises an exception. If there's a problem, the appropriate log message
319+
will be generated and the method will return the CONTROL treatment.
320+
321+
:param key: The key for which to get the treatment
322+
:type key: str
323+
:param flag_set: flag set
324+
:type flag_sets: str
325+
:param attributes: An optional dictionary of attributes
326+
:type attributes: dict
327+
328+
:return: Dictionary with the result of all the feature flags provided
329+
:rtype: dict
330+
"""
331+
return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, attributes)
332+
333+
def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None):
334+
"""
335+
Get treatments for feature flags that contain given flag sets.
336+
337+
This method never raises an exception. If there's a problem, the appropriate log message
338+
will be generated and the method will return the CONTROL treatment.
339+
340+
:param key: The key for which to get the treatment
341+
:type key: str
342+
:param flag_sets: list of flag sets
343+
:type flag_sets: list
344+
:param attributes: An optional dictionary of attributes
345+
:type attributes: dict
346+
347+
:return: Dictionary with the result of all the feature flags provided
348+
:rtype: dict
349+
"""
350+
return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, attributes)
351+
352+
def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None):
353+
"""
354+
Get treatments for feature flags that contain given flag set.
355+
356+
This method never raises an exception. If there's a problem, the appropriate log message
357+
will be generated and the method will return the CONTROL treatment.
358+
359+
:param key: The key for which to get the treatment
360+
:type key: str
361+
:param flag_set: flag set
362+
:type flag_sets: str
363+
:param attributes: An optional dictionary of attributes
364+
:type attributes: dict
365+
366+
:return: Dictionary with the result of all the feature flags provided
367+
:rtype: dict
368+
"""
369+
return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, attributes)
370+
371+
def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=None):
372+
"""
373+
Get treatments for feature flags that contain given flag set.
374+
375+
This method never raises an exception. If there's a problem, the appropriate log message
376+
will be generated and the method will return the CONTROL treatment.
377+
378+
:param key: The key for which to get the treatment
379+
:type key: str
380+
:param flag_set: flag set
381+
:type flag_sets: str
382+
:param attributes: An optional dictionary of attributes
383+
:type attributes: dict
384+
385+
:return: Dictionary with the result of all the feature flags provided
386+
:rtype: dict
387+
"""
388+
return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, attributes)
389+
390+
def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None):
391+
"""
392+
Get treatments for feature flags that contain given flag sets.
393+
394+
This method never raises an exception. If there's a problem, the appropriate log message
395+
will be generated and the method will return the CONTROL treatment.
396+
397+
:param key: The key for which to get the treatment
398+
:type key: str
399+
:param flag_sets: list of flag sets
400+
:type flag_sets: list
401+
:param method: Treatment by flag set method flavor
402+
:type method: splitio.models.telemetry.MethodExceptionsAndLatencies
403+
:param attributes: An optional dictionary of attributes
404+
:type attributes: dict
405+
406+
:return: Dictionary with the result of all the feature flags provided
407+
:rtype: dict
408+
"""
409+
feature_flags_names = self._get_feature_flag_names_by_flag_sets(flag_sets, method.value)
410+
if feature_flags_names == []:
411+
_LOGGER.warning("%s: No valid Flag set or no feature flags found for evaluating treatments" % (method.value))
412+
return {}
413+
414+
if 'config' in method.value:
415+
return self._make_evaluations(key, feature_flags_names, attributes, method.value,
416+
method)
417+
418+
with_config = self._make_evaluations(key, feature_flags_names, attributes, method.value,
419+
method)
420+
return {feature_flag: result[0] for (feature_flag, result) in with_config.items()}
421+
422+
423+
def _get_feature_flag_names_by_flag_sets(self, flag_sets, method_name):
424+
"""
425+
Sanitize given flag sets and return list of feature flag names associated with them
426+
427+
:param flag_sets: list of flag sets
428+
:type flag_sets: list
429+
430+
:return: list of feature flag names
431+
:rtype: list
432+
"""
433+
sanitized_flag_sets = input_validator.validate_flag_sets(flag_sets, method_name)
434+
feature_flags_by_set = self._split_storage.get_feature_flags_by_sets(sanitized_flag_sets)
435+
if feature_flags_by_set is None:
436+
_LOGGER.warning("Fetching feature flags for flag set %s encountered an error, skipping this flag set." % (flag_sets))
437+
return []
438+
return feature_flags_by_set
439+
312440
def _build_impression( # pylint: disable=too-many-arguments
313441
self,
314442
matching_key,

splitio/client/config.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
import logging
44

55
from splitio.engine.impressions import ImpressionsMode
6+
from splitio.client.input_validator import validate_flag_sets
67

78

89
_LOGGER = logging.getLogger(__name__)
910
DEFAULT_DATA_SAMPLING = 1
1011

11-
1212
DEFAULT_CONFIG = {
1313
'operationMode': 'standalone',
1414
'connectionTimeout': 1500,
@@ -58,10 +58,10 @@
5858
'dataSampling': DEFAULT_DATA_SAMPLING,
5959
'storageWrapper': None,
6060
'storagePrefix': None,
61-
'storageType': None
61+
'storageType': None,
62+
'flagSetsFilter': None
6263
}
6364

64-
6565
def _parse_operation_mode(sdk_key, config):
6666
"""
6767
Process incoming config to determine operation mode and storage type
@@ -118,7 +118,6 @@ def _sanitize_impressions_mode(storage_type, mode, refresh_rate=None):
118118

119119
return mode, refresh_rate
120120

121-
122121
def sanitize(sdk_key, config):
123122
"""
124123
Look for inconsistencies or ill-formed configs and tune it accordingly.
@@ -143,4 +142,10 @@ def sanitize(sdk_key, config):
143142
_LOGGER.warning('metricRefreshRate parameter minimum value is 60 seconds, defaulting to 3600 seconds.')
144143
processed['metricsRefreshRate'] = 3600
145144

145+
if config['operationMode'] == 'consumer' and config.get('flagSetsFilter') is not None:
146+
processed['flagSetsFilter'] = None
147+
_LOGGER.warning('config: FlagSets filter is not applicable for Consumer modes where the SDK does keep rollout data in sync. FlagSet filter was discarded.')
148+
else:
149+
processed['flagSetsFilter'] = sorted(validate_flag_sets(processed['flagSetsFilter'], 'SDK Config')) if processed['flagSetsFilter'] is not None else None
150+
146151
return processed

0 commit comments

Comments
 (0)