From 529c177c25602ce0b66777ea6dc7e2cdd606af03 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 20 Jun 2025 11:33:58 -0700 Subject: [PATCH 1/2] Updated client --- splitio/client/client.py | 142 +++++++++++++++++++++------------------ 1 file changed, 76 insertions(+), 66 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index 8e71030e..acc3d197 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -84,7 +84,7 @@ def _client_is_usable(self): return True @staticmethod - def _validate_treatment_input(key, feature, attributes, method): + def _validate_treatment_input(key, feature, attributes, method, impressions_properties=None): """Perform all static validations on user supplied input.""" matching_key, bucketing_key = input_validator.validate_key(key, 'get_' + method.value) if not matching_key: @@ -97,10 +97,11 @@ def _validate_treatment_input(key, feature, attributes, method): if not input_validator.validate_attributes(attributes, 'get_' + method.value): raise _InvalidInputError() - return matching_key, bucketing_key, feature, attributes + impressions_properties = ClientBase._validate_treatment_properties(method, impressions_properties) + return matching_key, bucketing_key, feature, attributes, impressions_properties @staticmethod - def _validate_treatments_input(key, features, attributes, method): + def _validate_treatments_input(key, features, attributes, method, impressions_properties=None): """Perform all static validations on user supplied input.""" matching_key, bucketing_key = input_validator.validate_key(key, 'get_' + method.value) if not matching_key: @@ -113,10 +114,18 @@ def _validate_treatments_input(key, features, attributes, method): if not input_validator.validate_attributes(attributes, method): raise _InvalidInputError() - return matching_key, bucketing_key, features, attributes + impressions_properties = ClientBase._validate_treatment_properties(method, impressions_properties) + return matching_key, bucketing_key, features, attributes, impressions_properties - - def _build_impression(self, key, bucketing, feature, result): + @staticmethod + def _validate_treatment_properties(method, properties=None): + if properties is not None: + valid, properties, size = input_validator.valid_properties(properties, 'get_' + method.value) + if not valid: + properties = None + return properties + + def _build_impression(self, key, bucketing, feature, result, properties=None): """Build an impression based on evaluation data & it's result.""" return ImpressionDecorated( Impression(matching_key=key, @@ -125,7 +134,8 @@ def _build_impression(self, key, bucketing, feature, result): label=result['impression']['label'] if self._labels_enabled else None, change_number=result['impression']['change_number'], bucketing_key=bucketing, - time=utctime_ms()), + time=utctime_ms(), + impression_properties=properties), disabled=result['impressions_disabled']) def _build_impressions(self, key, bucketing, results): @@ -164,7 +174,7 @@ def _validate_track(self, key, traffic_type, event_type, value=None, properties= key = input_validator.validate_track_key(key) event_type = input_validator.validate_event_type(event_type) value = input_validator.validate_value(value) - valid, properties, size = input_validator.valid_properties(properties) + valid, properties, size = input_validator.valid_properties(properties, 'track') if key is None or event_type is None or traffic_type is None or value is False \ or valid is False: @@ -211,7 +221,7 @@ def destroy(self): """ self._factory.destroy() - def get_treatment(self, key, feature_flag_name, attributes=None): + def get_treatment(self, key, feature_flag_name, attributes=None, impressions_properties=None): """ Get the treatment for a feature flag and key, with an optional dictionary of attributes. @@ -228,14 +238,14 @@ def get_treatment(self, key, feature_flag_name, attributes=None): :rtype: str """ try: - treatment, _ = self._get_treatment(MethodExceptionsAndLatencies.TREATMENT, key, feature_flag_name, attributes) + treatment, _ = self._get_treatment(MethodExceptionsAndLatencies.TREATMENT, key, feature_flag_name, attributes, impressions_properties) return treatment except: _LOGGER.error('get_treatment failed') return CONTROL - def get_treatment_with_config(self, key, feature_flag_name, attributes=None): + def get_treatment_with_config(self, key, feature_flag_name, attributes=None, impressions_properties=None): """ Get the treatment and config for a feature flag and key, with optional dictionary of attributes. @@ -252,13 +262,13 @@ def get_treatment_with_config(self, key, feature_flag_name, attributes=None): :rtype: tuple(str, str) """ try: - return self._get_treatment(MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG, key, feature_flag_name, attributes) + return self._get_treatment(MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG, key, feature_flag_name, attributes, impressions_properties) except Exception: _LOGGER.error('get_treatment_with_config failed') return CONTROL, None - def _get_treatment(self, method, key, feature, attributes=None): + def _get_treatment(self, method, key, feature, attributes=None, impressions_properties=None): """ Validate key, feature flag name and object, and get the treatment and config with an optional dictionary of attributes. @@ -282,7 +292,7 @@ def _get_treatment(self, method, key, feature, attributes=None): self._telemetry_init_producer.record_not_ready_usage() try: - key, bucketing, feature, attributes = self._validate_treatment_input(key, feature, attributes, method) + key, bucketing, feature, attributes, impressions_properties = self._validate_treatment_input(key, feature, attributes, method, impressions_properties) except _InvalidInputError: return CONTROL, None @@ -299,12 +309,12 @@ def _get_treatment(self, method, key, feature, attributes=None): result = self._FAILED_EVAL_RESULT if result['impression']['label'] != Label.SPLIT_NOT_FOUND: - impression_decorated = self._build_impression(key, bucketing, feature, result) + impression_decorated = self._build_impression(key, bucketing, feature, result, impressions_properties) self._record_stats([(impression_decorated, attributes)], start, method) return result['treatment'], result['configurations'] - def get_treatments(self, key, feature_flag_names, attributes=None): + def get_treatments(self, key, feature_flag_names, attributes=None, impressions_properties=None): """ Evaluate multiple feature flags and return a dictionary with all the feature flag/treatments. @@ -321,13 +331,13 @@ def get_treatments(self, key, feature_flag_names, attributes=None): :rtype: dict """ try: - with_config = self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS, attributes) + with_config = self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS, attributes, impressions_properties) return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} except Exception: return {feature: CONTROL for feature in feature_flag_names} - def get_treatments_with_config(self, key, feature_flag_names, attributes=None): + def get_treatments_with_config(self, key, feature_flag_names, attributes=None, impressions_properties=None): """ Evaluate multiple feature flags and return a dict with feature flag -> (treatment, config). @@ -344,12 +354,12 @@ def get_treatments_with_config(self, key, feature_flag_names, attributes=None): :rtype: dict """ try: - return self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, attributes) + return self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, attributes, impressions_properties) except Exception: return {feature: (CONTROL, None) for feature in feature_flag_names} - def get_treatments_by_flag_set(self, key, flag_set, attributes=None): + def get_treatments_by_flag_set(self, key, flag_set, attributes=None, impressions_properties=None): """ Get treatments for feature flags that contain given flag set. This method never raises an exception. If there's a problem, the appropriate log message @@ -363,9 +373,9 @@ def get_treatments_by_flag_set(self, key, flag_set, attributes=None): :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, attributes) + return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, attributes, impressions_properties) - def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None): + def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None, impressions_properties=None): """ Get treatments for feature flags that contain given flag sets. This method never raises an exception. If there's a problem, the appropriate log message @@ -379,9 +389,9 @@ def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None): :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, attributes) + return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, attributes, impressions_properties) - def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None): + def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None, impressions_properties=None): """ Get treatments for feature flags that contain given flag set. This method never raises an exception. If there's a problem, the appropriate log message @@ -395,9 +405,9 @@ def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None) :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, attributes) + return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, attributes, impressions_properties) - def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=None): + def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=None, impressions_properties=None): """ Get treatments for feature flags that contain given flag set. This method never raises an exception. If there's a problem, the appropriate log message @@ -411,9 +421,9 @@ def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=Non :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, attributes) + return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, attributes, impressions_properties) - def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None): + def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None, impressions_properties=None): """ Get treatments for feature flags that contain given flag sets. This method never raises an exception. If there's a problem, the appropriate log message @@ -435,12 +445,12 @@ def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None): return {} if 'config' in method.value: - return self._get_treatments(key, feature_flags_names, method, attributes) + return self._get_treatments(key, feature_flags_names, method, attributes, impressions_properties) - with_config = self._get_treatments(key, feature_flags_names, method, attributes) + with_config = self._get_treatments(key, feature_flags_names, method, attributes, impressions_properties) return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} - def get_treatments_by_flag_set(self, key, flag_set, attributes=None): + def get_treatments_by_flag_set(self, key, flag_set, attributes=None, impressions_properties=None): """ Get treatments for feature flags that contain given flag set. @@ -457,9 +467,9 @@ def get_treatments_by_flag_set(self, key, flag_set, attributes=None): :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, attributes) + return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, attributes, impressions_properties) - def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None): + def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None, impressions_properties=None): """ Get treatments for feature flags that contain given flag sets. @@ -476,9 +486,9 @@ def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None): :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, attributes) + return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, attributes, impressions_properties) - def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None): + def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None, impressions_properties=None): """ Get treatments for feature flags that contain given flag set. @@ -495,9 +505,9 @@ def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None) :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, attributes) + return self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, attributes, impressions_properties) - def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=None): + def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=None, impressions_properties=None): """ Get treatments for feature flags that contain given flag set. @@ -514,7 +524,7 @@ def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=Non :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, attributes) + return self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, attributes, impressions_properties) def _get_feature_flag_names_by_flag_sets(self, flag_sets, method_name): """ @@ -533,7 +543,7 @@ def _get_feature_flag_names_by_flag_sets(self, flag_sets, method_name): return feature_flags_by_set - def _get_treatments(self, key, features, method, attributes=None): + def _get_treatments(self, key, features, method, attributes=None, impressions_properties=None): """ Validate key, feature flag names and objects, and get the treatments and configs with an optional dictionary of attributes. @@ -558,7 +568,7 @@ def _get_treatments(self, key, features, method, attributes=None): self._telemetry_init_producer.record_not_ready_usage() try: - key, bucketing, features, attributes = self._validate_treatments_input(key, features, attributes, method) + key, bucketing, features, attributes = self._validate_treatments_input(key, features, attributes, method, impressions_properties) except _InvalidInputError: return input_validator.generate_control_treatments(features) @@ -575,7 +585,7 @@ def _get_treatments(self, key, features, method, attributes=None): results = {n: self._FAILED_EVAL_RESULT for n in features} imp_decorated_attrs = [ - (i, attributes) for i in self._build_impressions(key, bucketing, results) + (i, attributes) for i in self._build_impressions(key, bucketing, results, impressions_properties) if i.Impression.label != Label.SPLIT_NOT_FOUND ] self._record_stats(imp_decorated_attrs, start, method) @@ -678,7 +688,7 @@ async def destroy(self): """ await self._factory.destroy() - async def get_treatment(self, key, feature_flag_name, attributes=None): + async def get_treatment(self, key, feature_flag_name, attributes=None, impressions_properties=None): """ Get the treatment for a feature and key, with an optional dictionary of attributes, for async calls @@ -695,14 +705,14 @@ async def get_treatment(self, key, feature_flag_name, attributes=None): :rtype: str """ try: - treatment, _ = await self._get_treatment(MethodExceptionsAndLatencies.TREATMENT, key, feature_flag_name, attributes) + treatment, _ = await self._get_treatment(MethodExceptionsAndLatencies.TREATMENT, key, feature_flag_name, attributes, impressions_properties) return treatment except: _LOGGER.error('get_treatment failed') return CONTROL - async def get_treatment_with_config(self, key, feature_flag_name, attributes=None): + async def get_treatment_with_config(self, key, feature_flag_name, attributes=None, impressions_properties=None): """ Get the treatment for a feature and key, with an optional dictionary of attributes, for async calls @@ -719,13 +729,13 @@ async def get_treatment_with_config(self, key, feature_flag_name, attributes=Non :rtype: str """ try: - return await self._get_treatment(MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG, key, feature_flag_name, attributes) + return await self._get_treatment(MethodExceptionsAndLatencies.TREATMENT_WITH_CONFIG, key, feature_flag_name, attributes, impressions_properties) except Exception: _LOGGER.error('get_treatment_with_config failed') return CONTROL, None - async def _get_treatment(self, method, key, feature, attributes=None): + async def _get_treatment(self, method, key, feature, attributes=None, impressions_properties=None): """ Validate key, feature flag name and object, and get the treatment and config with an optional dictionary of attributes, for async calls @@ -749,7 +759,7 @@ async def _get_treatment(self, method, key, feature, attributes=None): await self._telemetry_init_producer.record_not_ready_usage() try: - key, bucketing, feature, attributes = self._validate_treatment_input(key, feature, attributes, method) + key, bucketing, feature, attributes, impressions_properties = self._validate_treatment_input(key, feature, attributes, method, impressions_properties) except _InvalidInputError: return CONTROL, None @@ -766,11 +776,11 @@ async def _get_treatment(self, method, key, feature, attributes=None): result = self._FAILED_EVAL_RESULT if result['impression']['label'] != Label.SPLIT_NOT_FOUND: - impression_decorated = self._build_impression(key, bucketing, feature, result) + impression_decorated = self._build_impression(key, bucketing, feature, result, impressions_properties) await self._record_stats([(impression_decorated, attributes)], start, method) return result['treatment'], result['configurations'] - async def get_treatments(self, key, feature_flag_names, attributes=None): + async def get_treatments(self, key, feature_flag_names, attributes=None, impressions_properties=None): """ Evaluate multiple feature flags and return a dictionary with all the feature flag/treatments, for async calls @@ -787,13 +797,13 @@ async def get_treatments(self, key, feature_flag_names, attributes=None): :rtype: dict """ try: - with_config = await self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS, attributes) + with_config = await self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS, attributes, impressions_properties) return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} except Exception: return {feature: CONTROL for feature in feature_flag_names} - async def get_treatments_with_config(self, key, feature_flag_names, attributes=None): + async def get_treatments_with_config(self, key, feature_flag_names, attributes=None, impressions_properties=None): """ Evaluate multiple feature flags and return a dict with feature flag -> (treatment, config), for async calls @@ -810,13 +820,13 @@ async def get_treatments_with_config(self, key, feature_flag_names, attributes=N :rtype: dict """ try: - return await self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, attributes) + return await self._get_treatments(key, feature_flag_names, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG, attributes, impressions_properties) except Exception: _LOGGER.error("AA", exc_info=True) return {feature: (CONTROL, None) for feature in feature_flag_names} - async def get_treatments_by_flag_set(self, key, flag_set, attributes=None): + async def get_treatments_by_flag_set(self, key, flag_set, attributes=None, impressions_properties=None): """ Get treatments for feature flags that contain given flag set. This method never raises an exception. If there's a problem, the appropriate log message @@ -830,9 +840,9 @@ async def get_treatments_by_flag_set(self, key, flag_set, attributes=None): :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return await self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, attributes) + return await self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SET, attributes, impressions_properties) - async def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None): + async def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None, impressions_properties=None): """ Get treatments for feature flags that contain given flag sets. This method never raises an exception. If there's a problem, the appropriate log message @@ -846,9 +856,9 @@ async def get_treatments_by_flag_sets(self, key, flag_sets, attributes=None): :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return await self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, attributes) + return await self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_BY_FLAG_SETS, attributes, impressions_properties) - async def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None): + async def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes=None, impressions_properties=None): """ Get treatments for feature flags that contain given flag set. This method never raises an exception. If there's a problem, the appropriate log message @@ -862,9 +872,9 @@ async def get_treatments_with_config_by_flag_set(self, key, flag_set, attributes :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return await self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, attributes) + return await self._get_treatments_by_flag_sets( key, [flag_set], MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SET, attributes, impressions_properties) - async def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=None): + async def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attributes=None, impressions_properties=None): """ Get treatments for feature flags that contain given flag set. This method never raises an exception. If there's a problem, the appropriate log message @@ -878,9 +888,9 @@ async def get_treatments_with_config_by_flag_sets(self, key, flag_sets, attribut :return: Dictionary with the result of all the feature flags provided :rtype: dict """ - return await self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, attributes) + return await self._get_treatments_by_flag_sets( key, flag_sets, MethodExceptionsAndLatencies.TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, attributes, impressions_properties) - async def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None): + async def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes=None, impressions_properties=None): """ Get treatments for feature flags that contain given flag sets. This method never raises an exception. If there's a problem, the appropriate log message @@ -902,9 +912,9 @@ async def _get_treatments_by_flag_sets(self, key, flag_sets, method, attributes= return {} if 'config' in method.value: - return await self._get_treatments(key, feature_flags_names, method, attributes) + return await self._get_treatments(key, feature_flags_names, method, attributes, impressions_properties) - with_config = await self._get_treatments(key, feature_flags_names, method, attributes) + with_config = await self._get_treatments(key, feature_flags_names, method, attributes, impressions_properties) return {feature_flag: result[0] for (feature_flag, result) in with_config.items()} async def _get_feature_flag_names_by_flag_sets(self, flag_sets, method_name): @@ -923,7 +933,7 @@ async def _get_feature_flag_names_by_flag_sets(self, flag_sets, method_name): return feature_flags_by_set - async def _get_treatments(self, key, features, method, attributes=None): + async def _get_treatments(self, key, features, method, attributes=None, impressions_properties=None): """ Validate key, feature flag names and objects, and get the treatments and configs with an optional dictionary of attributes, for async calls @@ -947,7 +957,7 @@ async def _get_treatments(self, key, features, method, attributes=None): await self._telemetry_init_producer.record_not_ready_usage() try: - key, bucketing, features, attributes = self._validate_treatments_input(key, features, attributes, method) + key, bucketing, features, attributes = self._validate_treatments_input(key, features, attributes, method, impressions_properties) except _InvalidInputError: return input_validator.generate_control_treatments(features) @@ -964,7 +974,7 @@ async def _get_treatments(self, key, features, method, attributes=None): results = {n: self._FAILED_EVAL_RESULT for n in features} imp_decorated_attrs = [ - (i, attributes) for i in self._build_impressions(key, bucketing, results) + (i, attributes) for i in self._build_impressions(key, bucketing, results, impressions_properties) if i.Impression.label != Label.SPLIT_NOT_FOUND ] await self._record_stats(imp_decorated_attrs, start, method) From 3055fb9e9d6b9287c4460257831a4f23fa62e04e Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Mon, 23 Jun 2025 10:10:48 -0700 Subject: [PATCH 2/2] updated tests --- splitio/client/client.py | 12 +- tests/client/test_client.py | 301 ++++++++++++++++++++++++++++++------ 2 files changed, 260 insertions(+), 53 deletions(-) diff --git a/splitio/client/client.py b/splitio/client/client.py index acc3d197..ca5df5fa 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -1,5 +1,6 @@ """A module for Split.io SDK API clients.""" import logging +import json from splitio.engine.evaluator import Evaluator, CONTROL, EvaluationDataFactory, AsyncEvaluationDataFactory from splitio.engine.splitters import Splitter @@ -135,13 +136,14 @@ def _build_impression(self, key, bucketing, feature, result, properties=None): change_number=result['impression']['change_number'], bucketing_key=bucketing, time=utctime_ms(), - impression_properties=properties), + previous_time=None, + properties=json.dumps(properties)), disabled=result['impressions_disabled']) - def _build_impressions(self, key, bucketing, results): + def _build_impressions(self, key, bucketing, results, properties=None): """Build an impression based on evaluation data & it's result.""" return [ - self._build_impression(key, bucketing, feature, result) + self._build_impression(key, bucketing, feature, result, properties) for feature, result in results.items() ] @@ -568,7 +570,7 @@ def _get_treatments(self, key, features, method, attributes=None, impressions_pr self._telemetry_init_producer.record_not_ready_usage() try: - key, bucketing, features, attributes = self._validate_treatments_input(key, features, attributes, method, impressions_properties) + key, bucketing, features, attributes, impressions_properties = self._validate_treatments_input(key, features, attributes, method, impressions_properties) except _InvalidInputError: return input_validator.generate_control_treatments(features) @@ -957,7 +959,7 @@ async def _get_treatments(self, key, features, method, attributes=None, impressi await self._telemetry_init_producer.record_not_ready_usage() try: - key, bucketing, features, attributes = self._validate_treatments_input(key, features, attributes, method, impressions_properties) + key, bucketing, features, attributes, impressions_properties = self._validate_treatments_input(key, features, attributes, method, impressions_properties) except _InvalidInputError: return input_validator.generate_control_treatments(features) diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 49b6ba7a..601e0ecb 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -87,7 +87,7 @@ def synchronize_config(*_): } _logger = mocker.Mock() assert client.get_treatment('some_key', 'SPLIT_2') == 'on' - assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000)] + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null')] assert _logger.mock_calls == [] # Test with client not ready @@ -96,7 +96,7 @@ def synchronize_config(*_): type(factory).ready = ready_property # pytest.set_trace() assert client.get_treatment('some_key', 'SPLIT_2', {'some_attribute': 1}) == 'control' - assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', Label.NOT_READY, None, None, 1000)] + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', Label.NOT_READY, None, None, 1000, None, 'null')] # Test with exception: ready_property.return_value = True @@ -104,7 +104,7 @@ def _raise(*_): raise RuntimeError('something') client._evaluator.eval_with_context.side_effect = _raise assert client.get_treatment('some_key', 'SPLIT_2') == 'control' - assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', 'exception', None, None, 1000)] + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', 'exception', None, None, 1000, None, 'null')] factory.destroy() def test_get_treatment_with_config(self, mocker): @@ -164,7 +164,7 @@ def synchronize_config(*_): 'some_key', 'SPLIT_2' ) == ('on', '{"some_config": True}') - assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000)] + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null')] assert _logger.mock_calls == [] # Test with client not ready @@ -172,7 +172,7 @@ def synchronize_config(*_): ready_property.return_value = False type(factory).ready = ready_property assert client.get_treatment_with_config('some_key', 'SPLIT_2', {'some_attribute': 1}) == ('control', None) - assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY)] + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY)] # Test with exception: ready_property.return_value = True @@ -181,7 +181,7 @@ def _raise(*_): raise RuntimeError('something') client._evaluator.eval_with_context.side_effect = _raise assert client.get_treatment_with_config('some_key', 'SPLIT_2') == ('control', None) - assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', 'exception', None, None, 1000)] + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', 'exception', None, None, 1000, None, 'null')] factory.destroy() def test_get_treatments(self, mocker): @@ -244,8 +244,8 @@ def synchronize_config(*_): assert treatments == {'SPLIT_2': 'on', 'SPLIT_1': 'on'} impressions_called = impression_storage.pop_many(100) - assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000) in impressions_called - assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000) in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -253,7 +253,7 @@ def synchronize_config(*_): ready_property.return_value = False type(factory).ready = ready_property assert client.get_treatments('some_key', ['SPLIT_2'], {'some_attribute': 1}) == {'SPLIT_2': 'control'} - assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY)] + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY)] # Test with exception: ready_property.return_value = True @@ -323,8 +323,8 @@ def synchronize_config(*_): assert client.get_treatments_by_flag_set('key', 'set_1') == {'SPLIT_2': 'on', 'SPLIT_1': 'on'} impressions_called = impression_storage.pop_many(100) - assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000) in impressions_called - assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000) in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -332,7 +332,7 @@ def synchronize_config(*_): ready_property.return_value = False type(factory).ready = ready_property assert client.get_treatments_by_flag_set('some_key', 'set_2', {'some_attribute': 1}) == {'SPLIT_1': 'control'} - assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY)] + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY)] # Test with exception: ready_property.return_value = True @@ -402,8 +402,8 @@ def synchronize_config(*_): assert client.get_treatments_by_flag_sets('key', ['set_1']) == {'SPLIT_2': 'on', 'SPLIT_1': 'on'} impressions_called = impression_storage.pop_many(100) - assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000) in impressions_called - assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000) in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -411,7 +411,7 @@ def synchronize_config(*_): ready_property.return_value = False type(factory).ready = ready_property assert client.get_treatments_by_flag_sets('some_key', ['set_2'], {'some_attribute': 1}) == {'SPLIT_1': 'control'} - assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY)] + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY)] # Test with exception: ready_property.return_value = True @@ -482,8 +482,8 @@ def synchronize_config(*_): } impressions_called = impression_storage.pop_many(100) - assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000) in impressions_called - assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000) in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -491,7 +491,7 @@ def synchronize_config(*_): ready_property.return_value = False type(factory).ready = ready_property assert client.get_treatments_with_config('some_key', ['SPLIT_1'], {'some_attribute': 1}) == {'SPLIT_1': ('control', None)} - assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY)] + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY)] # Test with exception: ready_property.return_value = True @@ -565,8 +565,8 @@ def synchronize_config(*_): } impressions_called = impression_storage.pop_many(100) - assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000) in impressions_called - assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000) in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -574,7 +574,7 @@ def synchronize_config(*_): ready_property.return_value = False type(factory).ready = ready_property assert client.get_treatments_with_config_by_flag_set('some_key', 'set_2', {'some_attribute': 1}) == {'SPLIT_1': ('control', None)} - assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY)] + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY)] # Test with exception: ready_property.return_value = True @@ -645,8 +645,8 @@ def synchronize_config(*_): } impressions_called = impression_storage.pop_many(100) - assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000) in impressions_called - assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000) in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -654,7 +654,7 @@ def synchronize_config(*_): ready_property.return_value = False type(factory).ready = ready_property assert client.get_treatments_with_config_by_flag_sets('some_key', ['set_2'], {'some_attribute': 1}) == {'SPLIT_1': ('control', None)} - assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY)] + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY)] # Test with exception: ready_property.return_value = True @@ -1264,6 +1264,111 @@ def synchronize_config(*_): assert(telemetry_storage._method_exceptions._track == 1) factory.destroy() + def test_impressions_properties(self, mocker): + """Test get_treatment execution paths.""" + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + split_storage = InMemorySplitStorage() + segment_storage = InMemorySegmentStorage() + rb_segment_storage = InMemoryRuleBasedSegmentStorage() + telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() + impression_storage = InMemoryImpressionStorage(10, telemetry_runtime_producer) + event_storage = mocker.Mock(spec=EventStorage) + + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + impmanager = ImpressionManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) + recorder = StandardRecorder(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer(), + unique_keys_tracker=UniqueKeysTracker(), + imp_counter=ImpressionsCounter()) + class TelemetrySubmitterMock(): + def synchronize_config(*_): + pass + + factory = SplitFactory(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + mocker.Mock(), + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + TelemetrySubmitterMock(), + ) + ready_property = mocker.PropertyMock() + ready_property.return_value = True + type(factory).ready = ready_property + factory.block_until_ready(5) + + split_storage.update([from_raw(splits_json['splitChange1_1']['ff']['d'][0])], [], -1) + client = Client(factory, recorder, True) + client._evaluator = mocker.Mock(spec=Evaluator) + evaluation = { + 'treatment': 'on', + 'configurations': None, + 'impression': { + 'label': 'some_label', + 'change_number': 123 + }, + 'impressions_disabled': False + } + client._evaluator.eval_with_context.return_value = evaluation + client._evaluator.eval_many_with_context.return_value = { + 'SPLIT_2': evaluation + } + + _logger = mocker.Mock() + mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) + assert client.get_treatment('some_key', 'SPLIT_2', impressions_properties={"prop": "value"}) == 'on' + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + + assert client.get_treatment('some_key', 'SPLIT_2', impressions_properties=12) == 'on' + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null')] + assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatment')] + + _logger.reset_mock() + assert client.get_treatment('some_key', 'SPLIT_2', impressions_properties='12') == 'on' + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null')] + assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatment')] + + assert client.get_treatment_with_config('some_key', 'SPLIT_2', impressions_properties={"prop": "value"}) == ('on', None) + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + + assert client.get_treatments('some_key', ['SPLIT_2'], impressions_properties={"prop": "value"}) == {'SPLIT_2': 'on'} + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + + _logger.reset_mock() + assert client.get_treatments('some_key', ['SPLIT_2'], impressions_properties="prop") == {'SPLIT_2': 'on'} + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null')] + assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatments')] + + _logger.reset_mock() + assert client.get_treatments('some_key', ['SPLIT_2'], impressions_properties=123) == {'SPLIT_2': 'on'} + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null')] + assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatments')] + + assert client.get_treatments_with_config('some_key', ['SPLIT_2'], impressions_properties={"prop": "value"}) == {'SPLIT_2': ('on', None)} + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + + assert client.get_treatments_by_flag_set('some_key', 'set_1', impressions_properties={"prop": "value"}) == {'SPLIT_2': 'on'} + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + + assert client.get_treatments_by_flag_sets('some_key', ['set_1'], impressions_properties={"prop": "value"}) == {'SPLIT_2': 'on'} + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + + assert client.get_treatments_with_config_by_flag_set('some_key', 'set_1', impressions_properties={"prop": "value"}) == {'SPLIT_2': ('on', None)} + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + + assert client.get_treatments_with_config_by_flag_sets('some_key', ['set_1'], impressions_properties={"prop": "value"}) == {'SPLIT_2': ('on', None)} + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] class ClientAsyncTests(object): # pylint: disable=too-few-public-methods """Split client async test cases.""" @@ -1320,7 +1425,7 @@ async def synchronize_config(*_): } _logger = mocker.Mock() assert await client.get_treatment('some_key', 'SPLIT_2') == 'on' - assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000)] + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null')] assert _logger.mock_calls == [] # Test with client not ready @@ -1328,7 +1433,7 @@ async def synchronize_config(*_): ready_property.return_value = False type(factory).ready = ready_property assert await client.get_treatment('some_key', 'SPLIT_2', {'some_attribute': 1}) == 'control' - assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', Label.NOT_READY, None, None, 1000)] + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', Label.NOT_READY, None, None, 1000, None, 'null')] # Test with exception: ready_property.return_value = True @@ -1336,7 +1441,7 @@ def _raise(*_): raise RuntimeError('something') client._evaluator.eval_with_context.side_effect = _raise assert await client.get_treatment('some_key', 'SPLIT_2') == 'control' - assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', 'exception', None, None, 1000)] + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', 'exception', None, None, 1000, None, 'null')] await factory.destroy() @pytest.mark.asyncio @@ -1396,7 +1501,7 @@ async def synchronize_config(*_): 'some_key', 'SPLIT_2' ) == ('on', '{"some_config": True}') - assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000)] + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null')] assert _logger.mock_calls == [] # Test with client not ready @@ -1404,7 +1509,7 @@ async def synchronize_config(*_): ready_property.return_value = False type(factory).ready = ready_property assert await client.get_treatment_with_config('some_key', 'SPLIT_2', {'some_attribute': 1}) == ('control', None) - assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY)] + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY)] # Test with exception: ready_property.return_value = True @@ -1413,7 +1518,7 @@ def _raise(*_): raise RuntimeError('something') client._evaluator.eval_with_context.side_effect = _raise assert await client.get_treatment_with_config('some_key', 'SPLIT_2') == ('control', None) - assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', 'exception', None, None, 1000)] + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', 'exception', None, None, 1000, None, 'null')] await factory.destroy() @pytest.mark.asyncio @@ -1476,8 +1581,8 @@ async def synchronize_config(*_): assert await client.get_treatments('key', ['SPLIT_2', 'SPLIT_1']) == {'SPLIT_2': 'on', 'SPLIT_1': 'on'} impressions_called = await impression_storage.pop_many(100) - assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000) in impressions_called - assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000) in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -1485,7 +1590,7 @@ async def synchronize_config(*_): ready_property.return_value = False type(factory).ready = ready_property assert await client.get_treatments('some_key', ['SPLIT_2'], {'some_attribute': 1}) == {'SPLIT_2': 'control'} - assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY)] + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY)] # Test with exception: ready_property.return_value = True @@ -1556,8 +1661,8 @@ async def synchronize_config(*_): assert await client.get_treatments_by_flag_set('key', 'set_1') == {'SPLIT_2': 'on', 'SPLIT_1': 'on'} impressions_called = await impression_storage.pop_many(100) - assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000) in impressions_called - assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000) in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -1565,7 +1670,7 @@ async def synchronize_config(*_): ready_property.return_value = False type(factory).ready = ready_property assert await client.get_treatments_by_flag_set('some_key', 'set_2', {'some_attribute': 1}) == {'SPLIT_1': 'control'} - assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY)] + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY)] # Test with exception: ready_property.return_value = True @@ -1636,8 +1741,8 @@ async def synchronize_config(*_): assert await client.get_treatments_by_flag_sets('key', ['set_1']) == {'SPLIT_2': 'on', 'SPLIT_1': 'on'} impressions_called = await impression_storage.pop_many(100) - assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000) in impressions_called - assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000) in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -1645,7 +1750,7 @@ async def synchronize_config(*_): ready_property.return_value = False type(factory).ready = ready_property assert await client.get_treatments_by_flag_sets('some_key', ['set_2'], {'some_attribute': 1}) == {'SPLIT_1': 'control'} - assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY)] + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY)] # Test with exception: ready_property.return_value = True @@ -1717,8 +1822,8 @@ async def synchronize_config(*_): } impressions_called = await impression_storage.pop_many(100) - assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000) in impressions_called - assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000) in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -1726,7 +1831,7 @@ async def synchronize_config(*_): ready_property.return_value = False type(factory).ready = ready_property assert await client.get_treatments_with_config('some_key', ['SPLIT_1'], {'some_attribute': 1}) == {'SPLIT_1': ('control', None)} - assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY)] + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY)] # Test with exception: ready_property.return_value = True @@ -1801,8 +1906,8 @@ async def synchronize_config(*_): } impressions_called = await impression_storage.pop_many(100) - assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000) in impressions_called - assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000) in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -1810,7 +1915,7 @@ async def synchronize_config(*_): ready_property.return_value = False type(factory).ready = ready_property assert await client.get_treatments_with_config_by_flag_set('some_key', 'set_2', {'some_attribute': 1}) == {'SPLIT_1': ('control', None)} - assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY)] + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY)] # Test with exception: ready_property.return_value = True @@ -1885,8 +1990,8 @@ async def synchronize_config(*_): } impressions_called = await impression_storage.pop_many(100) - assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000) in impressions_called - assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000) in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null') in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -1894,7 +1999,7 @@ async def synchronize_config(*_): ready_property.return_value = False type(factory).ready = ready_property assert await client.get_treatments_with_config_by_flag_sets('some_key', ['set_2'], {'some_attribute': 1}) == {'SPLIT_1': ('control', None)} - assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY)] + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_1', 'control', Label.NOT_READY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY, mocker.ANY)] # Test with exception: ready_property.return_value = True @@ -2370,3 +2475,103 @@ async def exc(*_): pass assert(telemetry_storage._method_exceptions._track == 1) await factory.destroy() + + @pytest.mark.asyncio + async def test_impressions_properties_async(self, mocker): + """Test get_treatment_async execution paths.""" + telemetry_storage = await InMemoryTelemetryStorageAsync.create() + telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) + split_storage = InMemorySplitStorageAsync() + segment_storage = InMemorySegmentStorageAsync() + rb_segment_storage = InMemoryRuleBasedSegmentStorageAsync() + telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() + impression_storage = InMemoryImpressionStorageAsync(10, telemetry_runtime_producer) + event_storage = mocker.Mock(spec=EventStorage) + impmanager = ImpressionManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) + recorder = StandardRecorderAsync(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer()) + await split_storage.update([from_raw(splits_json['splitChange1_1']['ff']['d'][0])], [], -1) + + destroyed_property = mocker.PropertyMock() + destroyed_property.return_value = False + + mocker.patch('splitio.client.client.utctime_ms', new=lambda: 1000) + mocker.patch('splitio.client.client.get_latency_bucket_index', new=lambda x: 5) + + class TelemetrySubmitterMock(): + async def synchronize_config(*_): + pass + factory = SplitFactoryAsync(mocker.Mock(), + {'splits': split_storage, + 'segments': segment_storage, + 'rule_based_segments': rb_segment_storage, + 'impressions': impression_storage, + 'events': event_storage}, + mocker.Mock(), + recorder, + mocker.Mock(), + telemetry_producer, + telemetry_producer.get_telemetry_init_producer(), + TelemetrySubmitterMock(), + ) + + await factory.block_until_ready(1) + client = ClientAsync(factory, recorder, True) + client._evaluator = mocker.Mock(spec=Evaluator) + evaluation = { + 'treatment': 'on', + 'configurations': None, + 'impression': { + 'label': 'some_label', + 'change_number': 123 + }, + 'impressions_disabled': False + } + client._evaluator.eval_with_context.return_value = evaluation + client._evaluator.eval_many_with_context.return_value = { + 'SPLIT_2': evaluation + } + + _logger = mocker.Mock() + mocker.patch('splitio.client.input_validator._LOGGER', new=_logger) + assert await client.get_treatment('some_key', 'SPLIT_2', impressions_properties={"prop": "value"}) == 'on' + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + + assert await client.get_treatment('some_key', 'SPLIT_2', impressions_properties=12) == 'on' + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null')] + assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatment')] + + _logger.reset_mock() + assert await client.get_treatment('some_key', 'SPLIT_2', impressions_properties='12') == 'on' + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null')] + assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatment')] + + assert await client.get_treatment_with_config('some_key', 'SPLIT_2', impressions_properties={"prop": "value"}) == ('on', None) + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + + assert await client.get_treatments('some_key', ['SPLIT_2'], impressions_properties={"prop": "value"}) == {'SPLIT_2': 'on'} + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + + _logger.reset_mock() + assert await client.get_treatments('some_key', ['SPLIT_2'], impressions_properties="prop") == {'SPLIT_2': 'on'} + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null')] + assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatments')] + + _logger.reset_mock() + assert await client.get_treatments('some_key', ['SPLIT_2'], impressions_properties=123) == {'SPLIT_2': 'on'} + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, 'null')] + assert _logger.error.mock_calls == [mocker.call('%s: properties must be of type dictionary.', 'get_treatments')] + + assert await client.get_treatments_with_config('some_key', ['SPLIT_2'], impressions_properties={"prop": "value"}) == {'SPLIT_2': ('on', None)} + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + + assert await client.get_treatments_by_flag_set('some_key', 'set_1', impressions_properties={"prop": "value"}) == {'SPLIT_2': 'on'} + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + + assert await client.get_treatments_by_flag_sets('some_key', ['set_1'], impressions_properties={"prop": "value"}) == {'SPLIT_2': 'on'} + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + + assert await client.get_treatments_with_config_by_flag_set('some_key', 'set_1', impressions_properties={"prop": "value"}) == {'SPLIT_2': ('on', None)} + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')] + + assert await client.get_treatments_with_config_by_flag_sets('some_key', ['set_1'], impressions_properties={"prop": "value"}) == {'SPLIT_2': ('on', None)} + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, '{"prop": "value"}')]