From 49bd4a123a65d430ee6246e3647c6c5608803d24 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 20 Jun 2025 09:29:07 -0700 Subject: [PATCH 1/8] Updated validation and model --- splitio/client/input_validator.py | 15 +++++++-------- splitio/models/impressions.py | 3 ++- tests/client/test_input_validator.py | 18 +++++++++--------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index b9201346..0b502244 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -564,7 +564,7 @@ def validate_factory_instantiation(sdk_key): return True -def valid_properties(properties): +def valid_properties(properties, source): """ Check if properties is a valid dict and returns the properties that will be sent to the track method, avoiding unexpected types. @@ -580,7 +580,7 @@ def valid_properties(properties): return True, None, size if not isinstance(properties, dict): - _LOGGER.error('track: properties must be of type dictionary.') + _LOGGER.error('%s: properties must be of type dictionary.', source) return False, None, 0 valid_properties = dict() @@ -597,7 +597,7 @@ def valid_properties(properties): if not isinstance(element, str) and not isinstance(element, Number) \ and not isinstance(element, bool): - _LOGGER.warning('Property %s is of invalid type. Setting value to None', element) + _LOGGER.warning('%s: Property %s is of invalid type. Setting value to None', source, element) element = None valid_properties[property] = element @@ -607,14 +607,13 @@ def valid_properties(properties): if size > MAX_PROPERTIES_LENGTH_BYTES: _LOGGER.error( - 'The maximum size allowed for the properties is 32768 bytes. ' + - 'Current one is ' + str(size) + ' bytes. Event not queued' - ) + '%s: The maximum size allowed for the properties is 32768 bytes. ' + + 'Current one is ' + str(size) + ' bytes. Event not queued', source) return False, None, size if len(valid_properties.keys()) > 300: - _LOGGER.warning('Event has more than 300 properties. Some of them will be trimmed' + - ' when processed') + _LOGGER.warning('%s: Event has more than 300 properties. Some of them will be trimmed' + + ' when processed', source) return True, valid_properties if len(valid_properties) else None, size def validate_pluggable_adapter(config): diff --git a/splitio/models/impressions.py b/splitio/models/impressions.py index 9224d15b..ff5ad33a 100644 --- a/splitio/models/impressions.py +++ b/splitio/models/impressions.py @@ -12,7 +12,8 @@ 'change_number', 'bucketing_key', 'time', - 'previous_time' + 'previous_time', + 'impression_properties' ] ) diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index 0659ee43..1ba6b610 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -499,17 +499,17 @@ def _configs(treatment): def test_valid_properties(self, mocker): """Test valid_properties() method.""" - assert input_validator.valid_properties(None) == (True, None, 1024) - assert input_validator.valid_properties([]) == (False, None, 0) - assert input_validator.valid_properties(True) == (False, None, 0) - assert input_validator.valid_properties(dict()) == (True, None, 1024) - assert input_validator.valid_properties({2: 123}) == (True, None, 1024) + assert input_validator.valid_properties(None, '') == (True, None, 1024) + assert input_validator.valid_properties([], '') == (False, None, 0) + assert input_validator.valid_properties(True, '') == (False, None, 0) + assert input_validator.valid_properties(dict(), '') == (True, None, 1024) + assert input_validator.valid_properties({2: 123}, '') == (True, None, 1024) class Test: pass assert input_validator.valid_properties({ "test": Test() - }) == (True, {"test": None}, 1028) + }, '') == (True, {"test": None}, 1028) props1 = { "test1": "test", @@ -519,7 +519,7 @@ class Test: "test5": [], 2: "t", } - r1, r2, r3 = input_validator.valid_properties(props1) + r1, r2, r3 = input_validator.valid_properties(props1, '') assert r1 is True assert len(r2.keys()) == 5 assert r2["test1"] == "test" @@ -532,12 +532,12 @@ class Test: props2 = dict() for i in range(301): props2[str(i)] = i - assert input_validator.valid_properties(props2) == (True, props2, 1817) + assert input_validator.valid_properties(props2, '') == (True, props2, 1817) props3 = dict() for i in range(100, 210): props3["prop" + str(i)] = "a" * 300 - r1, r2, r3 = input_validator.valid_properties(props3) + r1, r2, r3 = input_validator.valid_properties(props3, '') assert r1 is False assert r3 == 32952 From e20e10298d4fe90f333e1501e4fe31f5eb936d8e Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 20 Jun 2025 10:13:26 -0700 Subject: [PATCH 2/8] Updated strategies --- splitio/engine/impressions/strategies.py | 16 +- splitio/models/impressions.py | 2 +- tests/api/test_impressions_api.py | 6 +- tests/engine/test_impressions.py | 268 +++++++++++------------ 4 files changed, 152 insertions(+), 140 deletions(-) diff --git a/splitio/engine/impressions/strategies.py b/splitio/engine/impressions/strategies.py index 42b66011..71763fc9 100644 --- a/splitio/engine/impressions/strategies.py +++ b/splitio/engine/impressions/strategies.py @@ -38,7 +38,13 @@ def process_impressions(self, impressions): :returns: Tuple of to be stored, observed and counted impressions, and unique keys tuple :rtype: list[tuple[splitio.models.impression.Impression, dict]], list[], list[], list[] """ - imps = [(self._observer.test_and_set(imp), attrs) for imp, attrs in impressions] + imps = [] + for imp, attrs in impressions: + if imp.properties is not None: + continue + + imps.append((self._observer.test_and_set(imp), attrs)) + return [i for i, _ in imps], imps, [], [] class StrategyNoneMode(BaseStrategy): @@ -85,7 +91,13 @@ def process_impressions(self, impressions): :returns: Tuple of to be stored, observed and counted impressions, and unique keys tuple :rtype: list[tuple[splitio.models.impression.Impression, dict]], list[splitio.models.impression.Impression], list[splitio.models.impression.Impression], list[] """ - imps = [(self._observer.test_and_set(imp), attrs) for imp, attrs in impressions] + imps = [] + for imp, attrs in impressions: + if imp.properties is not None: + continue + + imps.append((self._observer.test_and_set(imp), attrs)) + counter_imps = [imp for imp, _ in imps if imp.previous_time != None] this_hour = truncate_time(utctime_ms()) return [i for i, _ in imps if i.previous_time is None or i.previous_time < this_hour], imps, counter_imps, [] diff --git a/splitio/models/impressions.py b/splitio/models/impressions.py index ff5ad33a..0c6d50f7 100644 --- a/splitio/models/impressions.py +++ b/splitio/models/impressions.py @@ -13,7 +13,7 @@ 'bucketing_key', 'time', 'previous_time', - 'impression_properties' + 'properties' ] ) diff --git a/tests/api/test_impressions_api.py b/tests/api/test_impressions_api.py index 7c8c1510..63193021 100644 --- a/tests/api/test_impressions_api.py +++ b/tests/api/test_impressions_api.py @@ -14,9 +14,9 @@ from splitio.storage.inmemmory import InMemoryTelemetryStorage, InMemoryTelemetryStorageAsync impressions_mock = [ - Impression('k1', 'f1', 'on', 'l1', 123456, 'b1', 321654), - Impression('k2', 'f2', 'off', 'l1', 123456, 'b1', 321654), - Impression('k3', 'f1', 'on', 'l1', 123456, 'b1', 321654) + Impression('k1', 'f1', 'on', 'l1', 123456, 'b1', 321654, {}), + Impression('k2', 'f2', 'off', 'l1', 123456, 'b1', 321654, {}), + Impression('k3', 'f1', 'on', 'l1', 123456, 'b1', 321654, {}) ] expectedImpressions = [{ 'f': 'f1', diff --git a/tests/engine/test_impressions.py b/tests/engine/test_impressions.py index b9f6a607..38c988d5 100644 --- a/tests/engine/test_impressions.py +++ b/tests/engine/test_impressions.py @@ -23,16 +23,16 @@ def test_changes_are_reflected(self): """Test that change in any field changes the resulting hash.""" total = set() hasher = Hasher() - total.add(hasher.process(Impression('key1', 'feature1', 'on', 'killed', 123, None, 456))) - total.add(hasher.process(Impression('key2', 'feature1', 'on', 'killed', 123, None, 456))) - total.add(hasher.process(Impression('key1', 'feature2', 'on', 'killed', 123, None, 456))) - total.add(hasher.process(Impression('key1', 'feature1', 'off', 'killed', 123, None, 456))) - total.add(hasher.process(Impression('key1', 'feature1', 'on', 'not killed', 123, None, 456))) - total.add(hasher.process(Impression('key1', 'feature1', 'on', 'killed', 321, None, 456))) + total.add(hasher.process(Impression('key1', 'feature1', 'on', 'killed', 123, None, 456, {}))) + total.add(hasher.process(Impression('key2', 'feature1', 'on', 'killed', 123, None, 456, {}))) + total.add(hasher.process(Impression('key1', 'feature2', 'on', 'killed', 123, None, 456, {}))) + total.add(hasher.process(Impression('key1', 'feature1', 'off', 'killed', 123, None, 456, {}))) + total.add(hasher.process(Impression('key1', 'feature1', 'on', 'not killed', 123, None, 456, {}))) + total.add(hasher.process(Impression('key1', 'feature1', 'on', 'killed', 321, None, 456, {}))) assert len(total) == 6 # Re-adding the first-one should not increase the number of different hashes - total.add(hasher.process(Impression('key1', 'feature1', 'on', 'killed', 123, None, 456))) + total.add(hasher.process(Impression('key1', 'feature1', 'on', 'killed', 123, None, 456, {}))) assert len(total) == 6 @@ -42,26 +42,26 @@ class ImpressionObserverTests(object): def test_previous_time_properly_calculated(self): """Test that the previous time is properly set.""" observer = Observer(5) - assert (observer.test_and_set(Impression('key1', 'f1', 'on', 'killed', 123, None, 456)) - == Impression('key1', 'f1', 'on', 'killed', 123, None, 456)) - assert (observer.test_and_set(Impression('key1', 'f1', 'on', 'killed', 123, None, 457)) - == Impression('key1', 'f1', 'on', 'killed', 123, None, 457, 456)) + assert (observer.test_and_set(Impression('key1', 'f1', 'on', 'killed', 123, None, 456, {})) + == Impression('key1', 'f1', 'on', 'killed', 123, None, 456, {})) + assert (observer.test_and_set(Impression('key1', 'f1', 'on', 'killed', 123, None, 457, {})) + == Impression('key1', 'f1', 'on', 'killed', 123, None, 457, 456, {})) # Add 5 new impressions to evict the first one and check that previous time is None again - assert (observer.test_and_set(Impression('key2', 'f1', 'on', 'killed', 123, None, 456)) - == Impression('key2', 'f1', 'on', 'killed', 123, None, 456)) - assert (observer.test_and_set(Impression('key3', 'f1', 'on', 'killed', 123, None, 456)) - == Impression('key3', 'f1', 'on', 'killed', 123, None, 456)) - assert (observer.test_and_set(Impression('key4', 'f1', 'on', 'killed', 123, None, 456)) - == Impression('key4', 'f1', 'on', 'killed', 123, None, 456)) - assert (observer.test_and_set(Impression('key5', 'f1', 'on', 'killed', 123, None, 456)) - == Impression('key5', 'f1', 'on', 'killed', 123, None, 456)) - assert (observer.test_and_set(Impression('key6', 'f1', 'on', 'killed', 123, None, 456)) - == Impression('key6', 'f1', 'on', 'killed', 123, None, 456)) + assert (observer.test_and_set(Impression('key2', 'f1', 'on', 'killed', 123, None, 456, {})) + == Impression('key2', 'f1', 'on', 'killed', 123, None, 456, {})) + assert (observer.test_and_set(Impression('key3', 'f1', 'on', 'killed', 123, None, 456, {})) + == Impression('key3', 'f1', 'on', 'killed', 123, None, 456, {})) + assert (observer.test_and_set(Impression('key4', 'f1', 'on', 'killed', 123, None, 456, {})) + == Impression('key4', 'f1', 'on', 'killed', 123, None, 456, {})) + assert (observer.test_and_set(Impression('key5', 'f1', 'on', 'killed', 123, None, 456, {})) + == Impression('key5', 'f1', 'on', 'killed', 123, None, 456, {})) + assert (observer.test_and_set(Impression('key6', 'f1', 'on', 'killed', 123, None, 456, {})) + == Impression('key6', 'f1', 'on', 'killed', 123, None, 456, {})) # Re-process the first-one - assert (observer.test_and_set(Impression('key1', 'f1', 'on', 'killed', 123, None, 456)) - == Impression('key1', 'f1', 'on', 'killed', 123, None, 456)) + assert (observer.test_and_set(Impression('key1', 'f1', 'on', 'killed', 123, None, 456, {})) + == Impression('key1', 'f1', 'on', 'killed', 123, None, 456, {})) class ImpressionCounterTests(object): @@ -72,15 +72,15 @@ def test_tracking_and_popping(self): counter = Counter() utc_now = utctime_ms_reimplement() utc_1_hour_after = utc_now + (3600 * 1000) - counter.track([Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now), - Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now), - Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now)]) + counter.track([Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now, {}), + Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now, {}), + Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now, {})]) - counter.track([Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now), - Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now)]) + counter.track([Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now, {}), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now, {})]) - counter.track([Impression('k1', 'f1', 'on', 'l1', 123, None, utc_1_hour_after), - Impression('k1', 'f2', 'on', 'l1', 123, None, utc_1_hour_after)]) + counter.track([Impression('k1', 'f1', 'on', 'l1', 123, None, utc_1_hour_after, {}), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_1_hour_after, {})]) assert set(counter.pop_all()) == set([ Counter.CountPerFeature('f1', truncate_time(utc_now), 3), @@ -112,18 +112,18 @@ def test_standalone_optimized(self, mocker): # An impression that hasn't happened in the last hour (pt = None) should be tracked imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), False), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), False), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) ]) assert for_unique_keys_tracker == [] - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), - Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3)] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {})] assert deduped == 0 # Tracking the same impression a ms later should be empty imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) ]) assert imps == [] assert deduped == 1 @@ -131,9 +131,9 @@ def test_standalone_optimized(self, mocker): # Tracking an impression with a different key makes it to the queue imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1), False), None) + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None) ]) - assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1)] + assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {})] assert deduped == 0 # Advance the perceived clock one hour @@ -144,30 +144,30 @@ def test_standalone_optimized(self, mocker): # Track the same impressions but "one hour later" imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1), False), None), - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None), + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3), - Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1)] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, {}), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, {})] assert deduped == 0 assert for_unique_keys_tracker == [] assert len(manager._strategy._observer._cache._data) == 3 # distinct impressions seen - assert for_counter == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3), - Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1)] + assert for_counter == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, {}), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, {})] # Test counting only from the second impression imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1), False), None) + (ImpressionDecorated(Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, {}), False), None) ]) assert for_counter == [] assert deduped == 0 assert for_unique_keys_tracker == [] imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1), False), None) + (ImpressionDecorated(Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, {}), False), None) ]) - assert for_counter == [Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, utc_now-1)] + assert for_counter == [Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, utc_now-1, {})] assert deduped == 1 assert for_unique_keys_tracker == [] @@ -186,27 +186,27 @@ def test_standalone_debug(self, mocker): # An impression that hasn't happened in the last hour (pt = None) should be tracked imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), False), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), False), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), - Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3)] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {})] assert for_counter == [] assert for_unique_keys_tracker == [] # Tracking the same impression a ms later should return the impression imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, utc_now-3)] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, utc_now-3, {})] assert for_counter == [] assert for_unique_keys_tracker == [] # Tracking a in impression with a different key makes it to the queue imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1), False), None) + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None) ]) - assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1)] + assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {})] assert for_counter == [] assert for_unique_keys_tracker == [] @@ -218,11 +218,11 @@ def test_standalone_debug(self, mocker): # Track the same impressions but "one hour later" imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1), False), None), - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None), + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3), - Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1)] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, {}), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, {})] assert for_counter == [] assert for_unique_keys_tracker == [] @@ -242,30 +242,30 @@ def test_standalone_none(self, mocker): # no impressions are tracked, only counter and mtk imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), False), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), False), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) ]) assert imps == [] assert for_counter == [ - Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), - Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3) + Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}) ] assert for_unique_keys_tracker == [('k1', 'f1'), ('k1', 'f2')] # Tracking the same impression a ms later should not return the impression and no change on mtk cache imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) ]) assert imps == [] # Tracking an impression with a different key, will only increase mtk imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k3', 'f1', 'on', 'l1', 123, None, utc_now-1), False), None) + (ImpressionDecorated(Impression('k3', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None) ]) assert imps == [] assert for_unique_keys_tracker == [('k3', 'f1')] assert for_counter == [ - Impression('k3', 'f1', 'on', 'l1', 123, None, utc_now-1) + Impression('k3', 'f1', 'on', 'l1', 123, None, utc_now-1, {}) ] # Advance the perceived clock one hour @@ -276,13 +276,13 @@ def test_standalone_none(self, mocker): # Track the same impressions but "one hour later", no changes on mtk imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1), False), None), - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None), + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) ]) assert imps == [] assert for_counter == [ - Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1), - Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2) + Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, {}) ] def test_standalone_optimized_listener(self, mocker): @@ -301,32 +301,32 @@ def test_standalone_optimized_listener(self, mocker): # An impression that hasn't happened in the last hour (pt = None) should be tracked imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), False), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), False), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), - Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3)] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {})] assert deduped == 0 - assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), None), - (Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3), None)] + assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), None), + (Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), None)] assert for_unique_keys_tracker == [] # Tracking the same impression a ms later should return empty imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) ]) assert imps == [] assert deduped == 1 - assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, utc_now-3), None)] + assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, utc_now-3, {}), None)] assert for_unique_keys_tracker == [] # Tracking a in impression with a different key makes it to the queue imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1), False), None) + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None) ]) - assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1)] + assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {})] assert deduped == 0 - assert listen == [(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1), None)] + assert listen == [(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), None)] assert for_unique_keys_tracker == [] # Advance the perceived clock one hour @@ -337,36 +337,36 @@ def test_standalone_optimized_listener(self, mocker): # Track the same impressions but "one hour later" imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1), False), None), - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None), + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3), - Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1)] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, {}), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, {})] assert deduped == 0 assert listen == [ - (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3), None), - (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1), None), + (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, {}), None), + (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, {}), None), ] assert for_unique_keys_tracker == [] assert len(manager._strategy._observer._cache._data) == 3 # distinct impressions seen assert for_counter == [ - Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3), - Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1) + Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, {}), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, {}) ] # Test counting only from the second impression imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1), False), None) + (ImpressionDecorated(Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, {}), False), None) ]) assert for_counter == [] assert deduped == 0 assert for_unique_keys_tracker == [] imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1), False), None) + (ImpressionDecorated(Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, {}), False), None) ]) assert for_counter == [ - Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, utc_now-1) + Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, utc_now-1, {}) ] assert deduped == 1 assert for_unique_keys_tracker == [] @@ -387,30 +387,30 @@ def test_standalone_debug_listener(self, mocker): # An impression that hasn't happened in the last hour (pt = None) should be tracked imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), False), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), False), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), - Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3)] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {})] - assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), None), - (Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3), None)] + assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), None), + (Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), None)] # Tracking the same impression a ms later should return the imp imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, utc_now-3)] - assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, utc_now-3), None)] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, utc_now-3, {})] + assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, utc_now-3, {}), None)] assert for_counter == [] assert for_unique_keys_tracker == [] # Tracking a in impression with a different key makes it to the queue imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1), False), None) + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None) ]) - assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1)] - assert listen == [(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1), None)] + assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {})] + assert listen == [(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), None)] assert for_counter == [] assert for_unique_keys_tracker == [] @@ -422,14 +422,14 @@ def test_standalone_debug_listener(self, mocker): # Track the same impressions but "one hour later" imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1), False), None), - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None), + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3), - Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1)] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, {}), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, {})] assert listen == [ - (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3), None), - (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1), None) + (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, {}), None), + (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, {}), None) ] assert len(manager._strategy._observer._cache._data) == 3 # distinct impressions seen assert for_counter == [] @@ -449,33 +449,33 @@ def test_standalone_none_listener(self, mocker): # An impression that hasn't happened in the last hour (pt = None) should not be tracked imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), False), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), False), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) ]) assert imps == [] - assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), None), - (Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3), None)] + assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), None), + (Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), None)] - assert for_counter == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), - Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3)] + assert for_counter == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {})] assert for_unique_keys_tracker == [('k1', 'f1'), ('k1', 'f2')] # Tracking the same impression a ms later should return empty, no updates on mtk imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) ]) assert imps == [] - assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, None), None)] - assert for_counter == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2)] + assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, None, {}), None)] + assert for_counter == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, {})] assert for_unique_keys_tracker == [('k1', 'f1')] # Tracking a in impression with a different key update mtk imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1), False), None) + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None) ]) assert imps == [] assert listen == [(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1), None)] - assert for_counter == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1)] + assert for_counter == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {})] assert for_unique_keys_tracker == [('k2', 'f1')] # Advance the perceived clock one hour @@ -486,15 +486,15 @@ def test_standalone_none_listener(self, mocker): # Track the same impressions but "one hour later" imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1), False), None), - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None), + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) ]) assert imps == [] - assert for_counter == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1), - Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2)] + assert for_counter == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, {})] assert listen == [ - (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, None), None), - (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, None), None) + (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, None, {}), None), + (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, None, {}), None) ] assert for_unique_keys_tracker == [('k1', 'f1'), ('k2', 'f1')] @@ -517,12 +517,12 @@ def test_impression_toggle_optimized(self, mocker): # An impression that hasn't happened in the last hour (pt = None) should be tracked imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), True), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), True), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) ]) assert for_unique_keys_tracker == [('k1', 'f1')] - assert imps == [Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3)] + assert imps == [Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {})] assert deduped == 1 def test_impression_toggle_debug(self, mocker): @@ -542,12 +542,12 @@ def test_impression_toggle_debug(self, mocker): # An impression that hasn't happened in the last hour (pt = None) should be tracked imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), True), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), True), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) ]) assert for_unique_keys_tracker == [('k1', 'f1')] - assert imps == [Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3)] + assert imps == [Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {})] assert deduped == 1 def test_impression_toggle_none(self, mocker): @@ -567,8 +567,8 @@ def test_impression_toggle_none(self, mocker): # An impression that hasn't happened in the last hour (pt = None) should be tracked imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3), True), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), True), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) ]) assert for_unique_keys_tracker == [('k1', 'f1'), ('k1', 'f2')] From 35475f5df372734bcfedebd54f9262060d5ce633 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 20 Jun 2025 11:29:22 -0700 Subject: [PATCH 3/8] Updated tests --- splitio/engine/impressions/strategies.py | 2 + tests/engine/test_impressions.py | 363 ++++++++++++++--------- 2 files changed, 230 insertions(+), 135 deletions(-) diff --git a/splitio/engine/impressions/strategies.py b/splitio/engine/impressions/strategies.py index 71763fc9..c2b0c565 100644 --- a/splitio/engine/impressions/strategies.py +++ b/splitio/engine/impressions/strategies.py @@ -41,6 +41,7 @@ def process_impressions(self, impressions): imps = [] for imp, attrs in impressions: if imp.properties is not None: + imps.append((imp, attrs)) continue imps.append((self._observer.test_and_set(imp), attrs)) @@ -94,6 +95,7 @@ def process_impressions(self, impressions): imps = [] for imp, attrs in impressions: if imp.properties is not None: + imps.append((imp, attrs)) continue imps.append((self._observer.test_and_set(imp), attrs)) diff --git a/tests/engine/test_impressions.py b/tests/engine/test_impressions.py index 38c988d5..715bfe1b 100644 --- a/tests/engine/test_impressions.py +++ b/tests/engine/test_impressions.py @@ -23,16 +23,16 @@ def test_changes_are_reflected(self): """Test that change in any field changes the resulting hash.""" total = set() hasher = Hasher() - total.add(hasher.process(Impression('key1', 'feature1', 'on', 'killed', 123, None, 456, {}))) - total.add(hasher.process(Impression('key2', 'feature1', 'on', 'killed', 123, None, 456, {}))) - total.add(hasher.process(Impression('key1', 'feature2', 'on', 'killed', 123, None, 456, {}))) - total.add(hasher.process(Impression('key1', 'feature1', 'off', 'killed', 123, None, 456, {}))) - total.add(hasher.process(Impression('key1', 'feature1', 'on', 'not killed', 123, None, 456, {}))) - total.add(hasher.process(Impression('key1', 'feature1', 'on', 'killed', 321, None, 456, {}))) + total.add(hasher.process(Impression('key1', 'feature1', 'on', 'killed', 123, None, 456, None, {}))) + total.add(hasher.process(Impression('key2', 'feature1', 'on', 'killed', 123, None, 456, None, {}))) + total.add(hasher.process(Impression('key1', 'feature2', 'on', 'killed', 123, None, 456, None, {}))) + total.add(hasher.process(Impression('key1', 'feature1', 'off', 'killed', 123, None, 456, None, {}))) + total.add(hasher.process(Impression('key1', 'feature1', 'on', 'not killed', 123, None, 456, None, {}))) + total.add(hasher.process(Impression('key1', 'feature1', 'on', 'killed', 321, None, 456, None, {}))) assert len(total) == 6 # Re-adding the first-one should not increase the number of different hashes - total.add(hasher.process(Impression('key1', 'feature1', 'on', 'killed', 123, None, 456, {}))) + total.add(hasher.process(Impression('key1', 'feature1', 'on', 'killed', 123, None, 456, None, {}))) assert len(total) == 6 @@ -42,26 +42,26 @@ class ImpressionObserverTests(object): def test_previous_time_properly_calculated(self): """Test that the previous time is properly set.""" observer = Observer(5) - assert (observer.test_and_set(Impression('key1', 'f1', 'on', 'killed', 123, None, 456, {})) - == Impression('key1', 'f1', 'on', 'killed', 123, None, 456, {})) - assert (observer.test_and_set(Impression('key1', 'f1', 'on', 'killed', 123, None, 457, {})) - == Impression('key1', 'f1', 'on', 'killed', 123, None, 457, 456, {})) + assert (observer.test_and_set(Impression('key1', 'f1', 'on', 'killed', 123, None, 456, None, None)) + == Impression('key1', 'f1', 'on', 'killed', 123, None, 456, None, None)) + assert (observer.test_and_set(Impression('key1', 'f1', 'on', 'killed', 123, None, 457, None, None)) + == Impression('key1', 'f1', 'on', 'killed', 123, None, 457, 456, None)) # Add 5 new impressions to evict the first one and check that previous time is None again - assert (observer.test_and_set(Impression('key2', 'f1', 'on', 'killed', 123, None, 456, {})) - == Impression('key2', 'f1', 'on', 'killed', 123, None, 456, {})) - assert (observer.test_and_set(Impression('key3', 'f1', 'on', 'killed', 123, None, 456, {})) - == Impression('key3', 'f1', 'on', 'killed', 123, None, 456, {})) - assert (observer.test_and_set(Impression('key4', 'f1', 'on', 'killed', 123, None, 456, {})) - == Impression('key4', 'f1', 'on', 'killed', 123, None, 456, {})) - assert (observer.test_and_set(Impression('key5', 'f1', 'on', 'killed', 123, None, 456, {})) - == Impression('key5', 'f1', 'on', 'killed', 123, None, 456, {})) - assert (observer.test_and_set(Impression('key6', 'f1', 'on', 'killed', 123, None, 456, {})) - == Impression('key6', 'f1', 'on', 'killed', 123, None, 456, {})) + assert (observer.test_and_set(Impression('key2', 'f1', 'on', 'killed', 123, None, 456, None, None)) + == Impression('key2', 'f1', 'on', 'killed', 123, None, 456, None, None)) + assert (observer.test_and_set(Impression('key3', 'f1', 'on', 'killed', 123, None, 456, None, None)) + == Impression('key3', 'f1', 'on', 'killed', 123, None, 456, None, None)) + assert (observer.test_and_set(Impression('key4', 'f1', 'on', 'killed', 123, None, 456, None, None)) + == Impression('key4', 'f1', 'on', 'killed', 123, None, 456, None, None)) + assert (observer.test_and_set(Impression('key5', 'f1', 'on', 'killed', 123, None, 456, None, None)) + == Impression('key5', 'f1', 'on', 'killed', 123, None, 456, None, None)) + assert (observer.test_and_set(Impression('key6', 'f1', 'on', 'killed', 123, None, 456, None, None)) + == Impression('key6', 'f1', 'on', 'killed', 123, None, 456, None, None)) # Re-process the first-one - assert (observer.test_and_set(Impression('key1', 'f1', 'on', 'killed', 123, None, 456, {})) - == Impression('key1', 'f1', 'on', 'killed', 123, None, 456, {})) + assert (observer.test_and_set(Impression('key1', 'f1', 'on', 'killed', 123, None, 456, None, None)) + == Impression('key1', 'f1', 'on', 'killed', 123, None, 456, None, None)) class ImpressionCounterTests(object): @@ -72,15 +72,15 @@ def test_tracking_and_popping(self): counter = Counter() utc_now = utctime_ms_reimplement() utc_1_hour_after = utc_now + (3600 * 1000) - counter.track([Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now, {}), - Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now, {}), - Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now, {})]) + counter.track([Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now, None, None), + Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now, None, None), + Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now, None, None)]) - counter.track([Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now, {}), - Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now, {})]) + counter.track([Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now, None, None)]) - counter.track([Impression('k1', 'f1', 'on', 'l1', 123, None, utc_1_hour_after, {}), - Impression('k1', 'f2', 'on', 'l1', 123, None, utc_1_hour_after, {})]) + counter.track([Impression('k1', 'f1', 'on', 'l1', 123, None, utc_1_hour_after, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_1_hour_after, None, None)]) assert set(counter.pop_all()) == set([ Counter.CountPerFeature('f1', truncate_time(utc_now), 3), @@ -112,18 +112,18 @@ def test_standalone_optimized(self, mocker): # An impression that hasn't happened in the last hour (pt = None) should be tracked imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), False), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), False), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None), False), None) ]) assert for_unique_keys_tracker == [] - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), - Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {})] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None)] assert deduped == 0 # Tracking the same impression a ms later should be empty imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None), False), None) ]) assert imps == [] assert deduped == 1 @@ -131,9 +131,9 @@ def test_standalone_optimized(self, mocker): # Tracking an impression with a different key makes it to the queue imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None) + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), False), None) ]) - assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {})] + assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None)] assert deduped == 0 # Advance the perceived clock one hour @@ -144,30 +144,30 @@ def test_standalone_optimized(self, mocker): # Track the same impressions but "one hour later" imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None), - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), False), None), + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, {}), - Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, {})] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, None), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, None)] assert deduped == 0 assert for_unique_keys_tracker == [] assert len(manager._strategy._observer._cache._data) == 3 # distinct impressions seen - assert for_counter == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, {}), - Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, {})] + assert for_counter == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, None), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, None)] # Test counting only from the second impression imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, {}), False), None) + (ImpressionDecorated(Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, None, None), False), None) ]) assert for_counter == [] assert deduped == 0 assert for_unique_keys_tracker == [] imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, {}), False), None) + (ImpressionDecorated(Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, None, None), False), None) ]) - assert for_counter == [Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, utc_now-1, {})] + assert for_counter == [Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, utc_now-1, None)] assert deduped == 1 assert for_unique_keys_tracker == [] @@ -186,27 +186,27 @@ def test_standalone_debug(self, mocker): # An impression that hasn't happened in the last hour (pt = None) should be tracked imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), False), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), False), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), - Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {})] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None)] assert for_counter == [] assert for_unique_keys_tracker == [] # Tracking the same impression a ms later should return the impression imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, utc_now-3, {})] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, utc_now-3, None)] assert for_counter == [] assert for_unique_keys_tracker == [] # Tracking a in impression with a different key makes it to the queue imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None) + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), False), None) ]) - assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {})] + assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None)] assert for_counter == [] assert for_unique_keys_tracker == [] @@ -218,11 +218,11 @@ def test_standalone_debug(self, mocker): # Track the same impressions but "one hour later" imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None), - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), False), None), + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, {}), - Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, {})] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, None), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, None)] assert for_counter == [] assert for_unique_keys_tracker == [] @@ -242,30 +242,30 @@ def test_standalone_none(self, mocker): # no impressions are tracked, only counter and mtk imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), False), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), False), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None), False), None) ]) assert imps == [] assert for_counter == [ - Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), - Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}) + Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None) ] assert for_unique_keys_tracker == [('k1', 'f1'), ('k1', 'f2')] # Tracking the same impression a ms later should not return the impression and no change on mtk cache imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None), False), None) ]) assert imps == [] # Tracking an impression with a different key, will only increase mtk imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k3', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None) + (ImpressionDecorated(Impression('k3', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), False), None) ]) assert imps == [] assert for_unique_keys_tracker == [('k3', 'f1')] assert for_counter == [ - Impression('k3', 'f1', 'on', 'l1', 123, None, utc_now-1, {}) + Impression('k3', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None) ] # Advance the perceived clock one hour @@ -276,13 +276,13 @@ def test_standalone_none(self, mocker): # Track the same impressions but "one hour later", no changes on mtk imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None), - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), False), None), + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None), False), None) ]) assert imps == [] assert for_counter == [ - Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), - Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, {}) + Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None) ] def test_standalone_optimized_listener(self, mocker): @@ -301,32 +301,32 @@ def test_standalone_optimized_listener(self, mocker): # An impression that hasn't happened in the last hour (pt = None) should be tracked imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), False), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), False), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), - Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {})] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None)] assert deduped == 0 - assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), None), - (Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), None)] + assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), None), + (Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None), None)] assert for_unique_keys_tracker == [] # Tracking the same impression a ms later should return empty imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None), False), None) ]) assert imps == [] assert deduped == 1 - assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, utc_now-3, {}), None)] + assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, utc_now-3, None), None)] assert for_unique_keys_tracker == [] # Tracking a in impression with a different key makes it to the queue imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None) + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), False), None) ]) - assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {})] + assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None)] assert deduped == 0 - assert listen == [(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), None)] + assert listen == [(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), None)] assert for_unique_keys_tracker == [] # Advance the perceived clock one hour @@ -337,36 +337,36 @@ def test_standalone_optimized_listener(self, mocker): # Track the same impressions but "one hour later" imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None), - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), False), None), + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, {}), - Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, {})] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, None), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, None)] assert deduped == 0 assert listen == [ - (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, {}), None), - (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, {}), None), + (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, None), None), + (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, None), None), ] assert for_unique_keys_tracker == [] assert len(manager._strategy._observer._cache._data) == 3 # distinct impressions seen assert for_counter == [ - Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, {}), - Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, {}) + Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, None), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, None) ] # Test counting only from the second impression imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, {}), False), None) + (ImpressionDecorated(Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, None, None), False), None) ]) assert for_counter == [] assert deduped == 0 assert for_unique_keys_tracker == [] imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, {}), False), None) + (ImpressionDecorated(Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, None, None), False), None) ]) assert for_counter == [ - Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, utc_now-1, {}) + Impression('k3', 'f3', 'on', 'l1', 123, None, utc_now-1, utc_now-1, None) ] assert deduped == 1 assert for_unique_keys_tracker == [] @@ -387,30 +387,30 @@ def test_standalone_debug_listener(self, mocker): # An impression that hasn't happened in the last hour (pt = None) should be tracked imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), False), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), False), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), - Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {})] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None)] - assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), None), - (Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), None)] + assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), None), + (Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None), None)] # Tracking the same impression a ms later should return the imp imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, utc_now-3, {})] - assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, utc_now-3, {}), None)] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, utc_now-3, None)] + assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, utc_now-3, None), None)] assert for_counter == [] assert for_unique_keys_tracker == [] # Tracking a in impression with a different key makes it to the queue imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None) + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), False), None) ]) - assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {})] - assert listen == [(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), None)] + assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None)] + assert listen == [(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), None)] assert for_counter == [] assert for_unique_keys_tracker == [] @@ -422,14 +422,14 @@ def test_standalone_debug_listener(self, mocker): # Track the same impressions but "one hour later" imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None), - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), False), None), + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None), False), None) ]) - assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, {}), - Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, {})] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, None), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, None)] assert listen == [ - (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, {}), None), - (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, {}), None) + (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, old_utc-3, None), None), + (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, None), None) ] assert len(manager._strategy._observer._cache._data) == 3 # distinct impressions seen assert for_counter == [] @@ -449,33 +449,33 @@ def test_standalone_none_listener(self, mocker): # An impression that hasn't happened in the last hour (pt = None) should not be tracked imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), False), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), False), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None), False), None) ]) assert imps == [] - assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), None), - (Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), None)] + assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), None), + (Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None), None)] - assert for_counter == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), - Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {})] + assert for_counter == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None)] assert for_unique_keys_tracker == [('k1', 'f1'), ('k1', 'f2')] # Tracking the same impression a ms later should return empty, no updates on mtk imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None), False), None) ]) assert imps == [] - assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, None, {}), None)] - assert for_counter == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, {})] + assert listen == [(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None), None)] + assert for_counter == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-2, None)] assert for_unique_keys_tracker == [('k1', 'f1')] # Tracking a in impression with a different key update mtk imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None) + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), False), None) ]) assert imps == [] - assert listen == [(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1), None)] - assert for_counter == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, {})] + assert listen == [(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), None)] + assert for_counter == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None)] assert for_unique_keys_tracker == [('k2', 'f1')] # Advance the perceived clock one hour @@ -486,15 +486,15 @@ def test_standalone_none_listener(self, mocker): # Track the same impressions but "one hour later" imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), False), None), - (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), False), None), + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None), False), None) ]) assert imps == [] - assert for_counter == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, {}), - Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, {})] + assert for_counter == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None)] assert listen == [ - (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, None, {}), None), - (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, None, {}), None) + (Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), None), + (Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None), None) ] assert for_unique_keys_tracker == [('k1', 'f1'), ('k2', 'f1')] @@ -517,12 +517,12 @@ def test_impression_toggle_optimized(self, mocker): # An impression that hasn't happened in the last hour (pt = None) should be tracked imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), True), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), True), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None), False), None) ]) assert for_unique_keys_tracker == [('k1', 'f1')] - assert imps == [Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {})] + assert imps == [Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None)] assert deduped == 1 def test_impression_toggle_debug(self, mocker): @@ -542,12 +542,12 @@ def test_impression_toggle_debug(self, mocker): # An impression that hasn't happened in the last hour (pt = None) should be tracked imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), True), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), True), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None), False), None) ]) assert for_unique_keys_tracker == [('k1', 'f1')] - assert imps == [Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {})] + assert imps == [Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None)] assert deduped == 1 def test_impression_toggle_none(self, mocker): @@ -567,10 +567,103 @@ def test_impression_toggle_none(self, mocker): # An impression that hasn't happened in the last hour (pt = None) should be tracked imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ - (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, {}), True), None), - (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, {}), False), None) + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), True), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None), False), None) ]) assert for_unique_keys_tracker == [('k1', 'f1'), ('k1', 'f2')] assert imps == [] assert deduped == 2 + + def test_impressions_properties_optimized(self, mocker): + """Test impressions manager in optimized mode with impressions properties.""" + + # Mock utc_time function to be able to play with the clock + utc_now = truncate_time(utctime_ms_reimplement()) + 1800 * 1000 + utc_time_mock = mocker.Mock() + utc_time_mock.return_value = utc_now + mocker.patch('splitio.engine.impressions.strategies.utctime_ms', return_value=utc_time_mock()) + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() + + manager = Manager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener + assert manager._strategy._observer is not None + assert isinstance(manager._strategy, StrategyOptimizedMode) + assert isinstance(manager._none_strategy, StrategyNoneMode) + + # An impression that hasn't happened in the last hour (pt = None) should be tracked + imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), False), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None), False), None) + ]) + + assert for_unique_keys_tracker == [] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None)] + assert deduped == 0 + imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), False), None) + ]) + assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None)] + + # Advance the perceived clock one hour + old_utc = utc_now # save it to compare captured impressions + utc_now += 3600 * 1000 + utc_time_mock.return_value = utc_now + mocker.patch('splitio.engine.impressions.strategies.utctime_ms', return_value=utc_time_mock()) + + # Track the same impressions but "one hour later" + imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, None, {'prop': 'value'}), False), None), + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None), False), None) + ]) + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, None, {'prop': 'value'}), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, None)] + assert deduped == 0 + assert for_unique_keys_tracker == [] + + def test_impressions_properties_debug(self, mocker): + """Test impressions manager in optimized mode with impressions properties.""" + + # Mock utc_time function to be able to play with the clock + utc_now = truncate_time(utctime_ms_reimplement()) + 1800 * 1000 + utc_time_mock = mocker.Mock() + utc_time_mock.return_value = utc_now + mocker.patch('splitio.engine.impressions.strategies.utctime_ms', return_value=utc_time_mock()) + telemetry_storage = InMemoryTelemetryStorage() + telemetry_producer = TelemetryStorageProducer(telemetry_storage) + telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() + + manager = Manager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener + + # An impression that hasn't happened in the last hour (pt = None) should be tracked + imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), False), None), + (ImpressionDecorated(Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None), False), None) + ]) + + assert for_unique_keys_tracker == [] + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-3, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, utc_now-3, None, None)] + assert deduped == 0 + imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None), False), None) + ]) + assert imps == [Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-1, None, None)] + + # Advance the perceived clock one hour + old_utc = utc_now # save it to compare captured impressions + utc_now += 3600 * 1000 + utc_time_mock.return_value = utc_now + mocker.patch('splitio.engine.impressions.strategies.utctime_ms', return_value=utc_time_mock()) + + # Track the same impressions but "one hour later" + imps, deduped, listen, for_counter, for_unique_keys_tracker = manager.process_impressions([ + (ImpressionDecorated(Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, None, {'prop': 'value'}), False), None), + (ImpressionDecorated(Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, None, None), False), None) + ]) + assert imps == [Impression('k1', 'f1', 'on', 'l1', 123, None, utc_now-1, None, {'prop': 'value'}), + Impression('k2', 'f1', 'on', 'l1', 123, None, utc_now-2, old_utc-1, None)] + assert deduped == 0 + assert for_unique_keys_tracker == [] \ No newline at end of file From 529c177c25602ce0b66777ea6dc7e2cdd606af03 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 20 Jun 2025 11:33:58 -0700 Subject: [PATCH 4/8] 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 5/8] 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"}')] From 12ec9ae04b36b77b296487f5aa7f473822d7a130 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Fri, 27 Jun 2025 12:39:12 -0700 Subject: [PATCH 6/8] Updated integration tests --- splitio/api/impressions.py | 3 +- splitio/client/client.py | 2 +- splitio/storage/pluggable.py | 1 + splitio/storage/redis.py | 1 + tests/api/test_impressions_api.py | 12 +-- tests/client/test_client.py | 86 +++++++++---------- tests/client/test_input_validator.py | 20 ++--- tests/integration/test_client_e2e.py | 50 +++++++---- .../integration/test_pluggable_integration.py | 6 +- tests/integration/test_redis_integration.py | 12 +-- tests/recorder/test_recorder.py | 64 +++++++------- tests/storage/test_inmemory_storage.py | 68 +++++++-------- tests/storage/test_pluggable.py | 36 ++++---- tests/storage/test_redis.py | 54 ++++++------ tests/sync/test_impressions_synchronizer.py | 16 ++-- tests/tasks/test_impressions_sync.py | 20 ++--- 16 files changed, 239 insertions(+), 212 deletions(-) diff --git a/splitio/api/impressions.py b/splitio/api/impressions.py index 4d1993ae..19c79a88 100644 --- a/splitio/api/impressions.py +++ b/splitio/api/impressions.py @@ -37,7 +37,8 @@ def _build_bulk(impressions): 'c': impression.change_number, 'r': impression.label, 'b': impression.bucketing_key, - 'pt': impression.previous_time + 'pt': impression.previous_time, + 'properties': impression.properties } for impression in imps ] diff --git a/splitio/client/client.py b/splitio/client/client.py index ca5df5fa..94413289 100644 --- a/splitio/client/client.py +++ b/splitio/client/client.py @@ -137,7 +137,7 @@ def _build_impression(self, key, bucketing, feature, result, properties=None): bucketing_key=bucketing, time=utctime_ms(), previous_time=None, - properties=json.dumps(properties)), + properties=json.dumps(properties) if properties is not None else None), disabled=result['impressions_disabled']) def _build_impressions(self, key, bucketing, results, properties=None): diff --git a/splitio/storage/pluggable.py b/splitio/storage/pluggable.py index 36b27d7d..71e487c6 100644 --- a/splitio/storage/pluggable.py +++ b/splitio/storage/pluggable.py @@ -1231,6 +1231,7 @@ def _wrap_impressions(self, impressions): 'r': impression.label, 'c': impression.change_number, 'm': impression.time, + 'properties': impression.properties } } bulk_impressions.append(json.dumps(to_store)) diff --git a/splitio/storage/redis.py b/splitio/storage/redis.py index 09ddee29..ad1badf0 100644 --- a/splitio/storage/redis.py +++ b/splitio/storage/redis.py @@ -1100,6 +1100,7 @@ def _wrap_impressions(self, impressions): 'r': impression.label, 'c': impression.change_number, 'm': impression.time, + 'properties': impression.properties } } bulk_impressions.append(json.dumps(to_store)) diff --git a/tests/api/test_impressions_api.py b/tests/api/test_impressions_api.py index 63193021..2215aa04 100644 --- a/tests/api/test_impressions_api.py +++ b/tests/api/test_impressions_api.py @@ -14,20 +14,20 @@ from splitio.storage.inmemmory import InMemoryTelemetryStorage, InMemoryTelemetryStorageAsync impressions_mock = [ - Impression('k1', 'f1', 'on', 'l1', 123456, 'b1', 321654, {}), - Impression('k2', 'f2', 'off', 'l1', 123456, 'b1', 321654, {}), - Impression('k3', 'f1', 'on', 'l1', 123456, 'b1', 321654, {}) + Impression('k1', 'f1', 'on', 'l1', 123456, 'b1', 321654, None, {'prop': 'val'}), + Impression('k2', 'f2', 'off', 'l1', 123456, 'b1', 321654, None, None), + Impression('k3', 'f1', 'on', 'l1', 123456, 'b1', 321654, None, None) ] expectedImpressions = [{ 'f': 'f1', 'i': [ - {'k': 'k1', 'b': 'b1', 't': 'on', 'r': 'l1', 'm': 321654, 'c': 123456, 'pt': None}, - {'k': 'k3', 'b': 'b1', 't': 'on', 'r': 'l1', 'm': 321654, 'c': 123456, 'pt': None}, + {'k': 'k1', 'b': 'b1', 't': 'on', 'r': 'l1', 'm': 321654, 'c': 123456, 'pt': None, 'properties': {"prop": "val"}}, + {'k': 'k3', 'b': 'b1', 't': 'on', 'r': 'l1', 'm': 321654, 'c': 123456, 'pt': None, 'properties': None}, ], }, { 'f': 'f2', 'i': [ - {'k': 'k2', 'b': 'b1', 't': 'off', 'r': 'l1', 'm': 321654, 'c': 123456, 'pt': None}, + {'k': 'k2', 'b': 'b1', 't': 'off', 'r': 'l1', 'm': 321654, 'c': 123456, 'pt': None, 'properties': None}, ] }] diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 601e0ecb..66c7c195 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, None, 'null')] + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, None)] 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, None, 'null')] + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', Label.NOT_READY, None, None, 1000, None, None)] # 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, None, 'null')] + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', 'exception', None, None, 1000, None, None)] 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, None, 'null')] + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, None)] assert _logger.mock_calls == [] # Test with client not ready @@ -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, None, 'null')] + assert impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', 'exception', None, None, 1000, None, None)] 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, None, 'null') 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, None) in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, None) in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -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, None, 'null') 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, None) in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, None) in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -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, None, 'null') 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, None) in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, None) in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -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, None, 'null') 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, None) in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, None) in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -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, None, 'null') 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, None) in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, None) in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -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, None, 'null') 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, None) in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, None) in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -1331,12 +1331,12 @@ def synchronize_config(*_): 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 impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, None)] 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 impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] 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) @@ -1347,12 +1347,12 @@ def synchronize_config(*_): _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 impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] 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 impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] 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)} @@ -1425,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, None, 'null')] + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, None)] assert _logger.mock_calls == [] # Test with client not ready @@ -1433,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, None, 'null')] + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', Label.NOT_READY, None, None, 1000, None, None)] # Test with exception: ready_property.return_value = True @@ -1441,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, None, 'null')] + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', 'exception', None, None, 1000, None, None)] await factory.destroy() @pytest.mark.asyncio @@ -1501,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, None, 'null')] + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, None)] assert _logger.mock_calls == [] # Test with client not ready @@ -1518,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, None, 'null')] + assert await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'control', 'exception', None, None, 1000, None, None)] await factory.destroy() @pytest.mark.asyncio @@ -1581,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, None, 'null') 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, None) in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, None) in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -1661,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, None, 'null') 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, None) in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, None) in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -1741,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, None, 'null') 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, None) in impressions_called + assert Impression('key', 'SPLIT_1', 'on', 'some_label', 123, None, 1000, None, None) in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -1822,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, None, 'null') 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, None) in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, None) in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -1906,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, None, 'null') 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, None) in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, None) in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -1990,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, None, 'null') 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, None) in impressions_called + assert Impression('key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, None) in impressions_called assert _logger.mock_calls == [] # Test with client not ready @@ -2488,7 +2488,7 @@ async def test_impressions_properties_async(self, mocker): 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()) + recorder = StandardRecorderAsync(impmanager, event_storage, impression_storage, telemetry_producer.get_telemetry_evaluation_producer(), telemetry_producer.get_telemetry_runtime_producer(), imp_counter=ImpressionsCounter()) await split_storage.update([from_raw(splits_json['splitChange1_1']['ff']['d'][0])], [], -1) destroyed_property = mocker.PropertyMock() @@ -2537,12 +2537,12 @@ async def synchronize_config(*_): 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 await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, None, None)] 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 await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] 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) @@ -2553,12 +2553,12 @@ async def synchronize_config(*_): _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 await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] 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 await impression_storage.pop_many(100) == [Impression('some_key', 'SPLIT_2', 'on', 'some_label', 123, None, 1000, 1000, None)] 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)} diff --git a/tests/client/test_input_validator.py b/tests/client/test_input_validator.py index 1ba6b610..a5a1c91a 100644 --- a/tests/client/test_input_validator.py +++ b/tests/client/test_input_validator.py @@ -766,14 +766,14 @@ def test_track(self, mocker): _logger.reset_mock() assert client.track("some_key", "traffic_type", "event_type", 1, []) is False assert _logger.error.mock_calls == [ - mocker.call("track: properties must be of type dictionary.") + mocker.call("%s: properties must be of type dictionary.", "track") ] # Test track with invalid properties _logger.reset_mock() assert client.track("some_key", "traffic_type", "event_type", 1, True) is False assert _logger.error.mock_calls == [ - mocker.call("track: properties must be of type dictionary.") + mocker.call("%s: properties must be of type dictionary.", "track") ] # Test track with properties @@ -788,7 +788,7 @@ def test_track(self, mocker): _logger.reset_mock() assert client.track("some_key", "traffic_type", "event_type", 1, props1) is True assert _logger.warning.mock_calls == [ - mocker.call("Property %s is of invalid type. Setting value to None", []) + mocker.call("%s: Property %s is of invalid type. Setting value to None", "track", []) ] # Test track with more than 300 properties @@ -798,7 +798,7 @@ def test_track(self, mocker): _logger.reset_mock() assert client.track("some_key", "traffic_type", "event_type", 1, props2) is True assert _logger.warning.mock_calls == [ - mocker.call("Event has more than 300 properties. Some of them will be trimmed when processed") + mocker.call("%s: Event has more than 300 properties. Some of them will be trimmed when processed", "track") ] # Test track with properties higher than 32kb @@ -808,7 +808,7 @@ def test_track(self, mocker): props3["prop" + str(i)] = "a" * 300 assert client.track("some_key", "traffic_type", "event_type", 1, props3) is False assert _logger.error.mock_calls == [ - mocker.call("The maximum size allowed for the properties is 32768 bytes. Current one is 32952 bytes. Event not queued") + mocker.call("%s: The maximum size allowed for the properties is 32768 bytes. Current one is 32952 bytes. Event not queued", "track") ] factory.destroy @@ -2378,14 +2378,14 @@ async def is_valid_traffic_type(*_): _logger.reset_mock() assert await client.track("some_key", "traffic_type", "event_type", 1, []) is False assert _logger.error.mock_calls == [ - mocker.call("track: properties must be of type dictionary.") + mocker.call("%s: properties must be of type dictionary.", "track") ] # Test track with invalid properties _logger.reset_mock() assert await client.track("some_key", "traffic_type", "event_type", 1, True) is False assert _logger.error.mock_calls == [ - mocker.call("track: properties must be of type dictionary.") + mocker.call("%s: properties must be of type dictionary.", "track") ] # Test track with properties @@ -2400,7 +2400,7 @@ async def is_valid_traffic_type(*_): _logger.reset_mock() assert await client.track("some_key", "traffic_type", "event_type", 1, props1) is True assert _logger.warning.mock_calls == [ - mocker.call("Property %s is of invalid type. Setting value to None", []) + mocker.call("%s: Property %s is of invalid type. Setting value to None", "track", []) ] # Test track with more than 300 properties @@ -2410,7 +2410,7 @@ async def is_valid_traffic_type(*_): _logger.reset_mock() assert await client.track("some_key", "traffic_type", "event_type", 1, props2) is True assert _logger.warning.mock_calls == [ - mocker.call("Event has more than 300 properties. Some of them will be trimmed when processed") + mocker.call("%s: Event has more than 300 properties. Some of them will be trimmed when processed", "track") ] # Test track with properties higher than 32kb @@ -2420,7 +2420,7 @@ async def is_valid_traffic_type(*_): props3["prop" + str(i)] = "a" * 300 assert await client.track("some_key", "traffic_type", "event_type", 1, props3) is False assert _logger.error.mock_calls == [ - mocker.call("The maximum size allowed for the properties is 32768 bytes. Current one is 32952 bytes. Event not queued") + mocker.call("%s: The maximum size allowed for the properties is 32768 bytes. Current one is 32952 bytes. Event not queued", "track") ] await factory.destroy() diff --git a/tests/integration/test_client_e2e.py b/tests/integration/test_client_e2e.py index f50869cf..96384f55 100644 --- a/tests/integration/test_client_e2e.py +++ b/tests/integration/test_client_e2e.py @@ -49,6 +49,7 @@ def _validate_last_impressions(client, *to_validate): """Validate the last N impressions are present disregarding the order.""" imp_storage = client._factory._get_storage('impressions') + as_tup_set = set() if isinstance(client._factory._get_storage('splits'), RedisSplitStorage) or isinstance(client._factory._get_storage('splits'), PluggableSplitStorage): if isinstance(client._factory._get_storage('splits'), RedisSplitStorage): redis_client = imp_storage._redis @@ -64,15 +65,28 @@ def _validate_last_impressions(client, *to_validate): json.loads(i) for i in results ] - as_tup_set = set( - (i['i']['f'], i['i']['k'], i['i']['t']) - for i in impressions_raw - ) + if to_validate != (): + if len(to_validate[0]) == 3: + as_tup_set = set( + (i['i']['f'], i['i']['k'], i['i']['t']) + for i in impressions_raw + ) + else: + as_tup_set = set( + (i['i']['f'], i['i']['k'], i['i']['t'], i['i']['properties']) + for i in impressions_raw + ) + assert as_tup_set == set(to_validate) time.sleep(0.2) # delay for redis to sync else: impressions = imp_storage.pop_many(len(to_validate)) - as_tup_set = set((i.feature_name, i.matching_key, i.treatment) for i in impressions) + if to_validate != (): + if len(to_validate[0]) == 3: + as_tup_set = set((i.feature_name, i.matching_key, i.treatment) for i in impressions) + else: + as_tup_set = set((i.feature_name, i.matching_key, i.treatment, i.properties) for i in impressions) + assert as_tup_set == set(to_validate) def _validate_last_events(client, *to_validate): @@ -108,9 +122,9 @@ def _get_treatment(factory, skip_rbs=False): except: pass - assert client.get_treatment('user1', 'sample_feature') == 'on' + assert client.get_treatment('user1', 'sample_feature', impressions_properties={'prop':'value'}) == 'on' if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): - _validate_last_impressions(client, ('sample_feature', 'user1', 'on')) + _validate_last_impressions(client, ('sample_feature', 'user1', 'on', '{"prop": "value"}')) assert client.get_treatment('invalidKey', 'sample_feature') == 'off' if not isinstance(factory._recorder._impressions_manager._strategy, StrategyNoneMode): @@ -514,7 +528,7 @@ def setup_method(self): 'events': InMemoryEventStorage(5000, telemetry_runtime_producer), } impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener - recorder = StandardRecorder(impmanager, storages['events'], storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer) + recorder = StandardRecorder(impmanager, storages['events'], storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer, imp_counter=ImpressionsCounter()) # Since we are passing None as SDK_Ready event, the factory will use the Redis telemetry call, using try catch to ignore the exception. try: self.factory = SplitFactory('some_api_key', @@ -674,7 +688,7 @@ def setup_method(self): 'events': InMemoryEventStorage(5000, telemetry_runtime_producer), } impmanager = ImpressionsManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener - recorder = StandardRecorder(impmanager, storages['events'], storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer) + recorder = StandardRecorder(impmanager, storages['events'], storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer, imp_counter=ImpressionsCounter()) self.factory = SplitFactory('some_api_key', storages, True, @@ -967,7 +981,7 @@ def setup_method(self): } impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener recorder = PipelinedRecorder(redis_client.pipeline, impmanager, storages['events'], - storages['impressions'], telemetry_redis_storage) + storages['impressions'], telemetry_redis_storage, imp_counter=ImpressionsCounter()) self.factory = SplitFactory('some_api_key', storages, True, @@ -1155,7 +1169,7 @@ def setup_method(self): } impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener recorder = PipelinedRecorder(redis_client.pipeline, impmanager, - storages['events'], storages['impressions'], telemetry_redis_storage) + storages['events'], storages['impressions'], telemetry_redis_storage, imp_counter=ImpressionsCounter()) self.factory = SplitFactory('some_api_key', storages, True, @@ -1375,7 +1389,7 @@ def setup_method(self): impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener recorder = StandardRecorder(impmanager, storages['events'], - storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer) + storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer, imp_counter=ImpressionsCounter()) self.factory = SplitFactory('some_api_key', storages, @@ -1570,7 +1584,7 @@ def setup_method(self): impmanager = ImpressionsManager(StrategyOptimizedMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener recorder = StandardRecorder(impmanager, storages['events'], - storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer) + storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer, imp_counter=ImpressionsCounter()) self.factory = SplitFactory('some_api_key', storages, @@ -1617,7 +1631,7 @@ def test_get_treatment(self): client.get_treatment('user1', 'sample_feature') client.get_treatment('user1', 'sample_feature') client.get_treatment('user1', 'sample_feature') - assert self.pluggable_storage_adapter._keys['SPLITIO.impressions'] == [] + assert len(self.pluggable_storage_adapter._keys['SPLITIO.impressions']) == 1 def test_get_treatment_with_config(self): """Test client.get_treatment_with_config().""" @@ -2317,7 +2331,7 @@ async def _setup_method(self): 'events': InMemoryEventStorageAsync(5000, telemetry_runtime_producer), } impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener - recorder = StandardRecorderAsync(impmanager, storages['events'], storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer) + recorder = StandardRecorderAsync(impmanager, storages['events'], storages['impressions'], telemetry_evaluation_producer, telemetry_runtime_producer, imp_counter=ImpressionsCounter()) # Since we are passing None as SDK_Ready event, the factory will use the Redis telemetry call, using try catch to ignore the exception. try: self.factory = SplitFactoryAsync('some_api_key', @@ -2839,7 +2853,7 @@ async def _setup_method(self): } impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener recorder = PipelinedRecorderAsync(redis_client.pipeline, impmanager, storages['events'], - storages['impressions'], telemetry_redis_storage) + storages['impressions'], telemetry_redis_storage, imp_counter=ImpressionsCounter()) self.factory = SplitFactoryAsync('some_api_key', storages, True, @@ -3061,7 +3075,7 @@ async def _setup_method(self): } impmanager = ImpressionsManager(StrategyDebugMode(), StrategyNoneMode(), telemetry_runtime_producer) # no listener recorder = PipelinedRecorderAsync(redis_client.pipeline, impmanager, storages['events'], - storages['impressions'], telemetry_redis_storage) + storages['impressions'], telemetry_redis_storage, imp_counter=ImpressionsCounter()) self.factory = SplitFactoryAsync('some_api_key', storages, True, @@ -3293,7 +3307,7 @@ async def _setup_method(self): recorder = StandardRecorderAsync(impmanager, storages['events'], storages['impressions'], telemetry_producer.get_telemetry_evaluation_producer(), - telemetry_runtime_producer) + telemetry_runtime_producer, imp_counter=ImpressionsCounter()) self.factory = SplitFactoryAsync('some_api_key', storages, diff --git a/tests/integration/test_pluggable_integration.py b/tests/integration/test_pluggable_integration.py index 20545da5..59534193 100644 --- a/tests/integration/test_pluggable_integration.py +++ b/tests/integration/test_pluggable_integration.py @@ -158,9 +158,9 @@ class PluggableImpressionsStorageIntegrationTests(object): def _put_impressions(self, adapter, metadata): storage = PluggableImpressionsStorage(adapter, metadata) storage.put([ - impressions.Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654), - impressions.Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654), - impressions.Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654) + impressions.Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None), + impressions.Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None), + impressions.Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) ]) diff --git a/tests/integration/test_redis_integration.py b/tests/integration/test_redis_integration.py index 4b70898b..4c85beda 100644 --- a/tests/integration/test_redis_integration.py +++ b/tests/integration/test_redis_integration.py @@ -161,9 +161,9 @@ class RedisImpressionsStorageTests(object): def _put_impressions(self, adapter, metadata): storage = RedisImpressionsStorage(adapter, metadata) storage.put([ - impressions.Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654), - impressions.Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654), - impressions.Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654) + impressions.Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None), + impressions.Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None), + impressions.Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) ]) @@ -394,9 +394,9 @@ class RedisImpressionsStorageAsyncTests(object): async def _put_impressions(self, adapter, metadata): storage = RedisImpressionsStorageAsync(adapter, metadata) await storage.put([ - impressions.Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654), - impressions.Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654), - impressions.Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654) + impressions.Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None), + impressions.Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None), + impressions.Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) ]) diff --git a/tests/recorder/test_recorder.py b/tests/recorder/test_recorder.py index e7a32711..cf226613 100644 --- a/tests/recorder/test_recorder.py +++ b/tests/recorder/test_recorder.py @@ -20,13 +20,13 @@ class StandardRecorderTests(object): def test_standard_recorder(self, mocker): impressions = [ - Impression('k1', 'f1', 'on', 'l1', 123, None, None), - Impression('k1', 'f2', 'on', 'l1', 123, None, None) + Impression('k1', 'f1', 'on', 'l1', 123, None, None, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, None, None, None) ] impmanager = mocker.Mock(spec=ImpressionsManager) impmanager.process_impressions.return_value = impressions, 0, [ - (Impression('k1', 'f1', 'on', 'l1', 123, None, None), None), - (Impression('k1', 'f2', 'on', 'l1', 123, None, None), None)], \ + (Impression('k1', 'f1', 'on', 'l1', 123, None, None, None, None), None), + (Impression('k1', 'f2', 'on', 'l1', 123, None, None, None, None), None)], \ [{"f": "f1", "ks": ["l1"]}, {"f": "f2", "ks": ["l1"]}], [('k1', 'f1'), ('k1', 'f2')] event = mocker.Mock(spec=EventStorage) impression = mocker.Mock(spec=ImpressionStorage) @@ -49,16 +49,16 @@ def record_latency(*args, **kwargs): assert(self.passed_args[0] == MethodExceptionsAndLatencies.TREATMENT) assert(self.passed_args[1] == 1) assert listener.log_impression.mock_calls == [ - mocker.call(Impression('k1', 'f1', 'on', 'l1', 123, None, None), None), - mocker.call(Impression('k1', 'f2', 'on', 'l1', 123, None, None), None) + mocker.call(Impression('k1', 'f1', 'on', 'l1', 123, None, None, None, None), None), + mocker.call(Impression('k1', 'f2', 'on', 'l1', 123, None, None, None, None), None) ] assert recorder._imp_counter.track.mock_calls == [mocker.call([{"f": "f1", "ks": ["l1"]}, {"f": "f2", "ks": ["l1"]}])] assert recorder._unique_keys_tracker.track.mock_calls == [mocker.call('k1', 'f1'), mocker.call('k1', 'f2')] def test_pipelined_recorder(self, mocker): impressions = [ - Impression('k1', 'f1', 'on', 'l1', 123, None, None), - Impression('k1', 'f2', 'on', 'l1', 123, None, None) + Impression('k1', 'f1', 'on', 'l1', 123, None, None, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, None, None, None) ] redis = mocker.Mock(spec=RedisAdapter) def execute(): @@ -67,8 +67,8 @@ def execute(): impmanager = mocker.Mock(spec=ImpressionsManager) impmanager.process_impressions.return_value = impressions, 0, [ - (Impression('k1', 'f1', 'on', 'l1', 123, None, None), None), - (Impression('k1', 'f2', 'on', 'l1', 123, None, None), None)], \ + (Impression('k1', 'f1', 'on', 'l1', 123, None, None, None, None), None), + (Impression('k1', 'f2', 'on', 'l1', 123, None, None, None, None), None)], \ [{"f": "f1", "ks": ["l1"]}, {"f": "f2", "ks": ["l1"]}], [('k1', 'f1'), ('k1', 'f2')] event = mocker.Mock(spec=RedisEventsStorage) impression = mocker.Mock(spec=RedisImpressionsStorage) @@ -83,22 +83,22 @@ def execute(): assert recorder._telemetry_redis_storage.add_latency_to_pipe.mock_calls[0][1][0] == MethodExceptionsAndLatencies.TREATMENT assert recorder._telemetry_redis_storage.add_latency_to_pipe.mock_calls[0][1][1] == 1 assert listener.log_impression.mock_calls == [ - mocker.call(Impression('k1', 'f1', 'on', 'l1', 123, None, None), None), - mocker.call(Impression('k1', 'f2', 'on', 'l1', 123, None, None), None) + mocker.call(Impression('k1', 'f1', 'on', 'l1', 123, None, None, None, None), None), + mocker.call(Impression('k1', 'f2', 'on', 'l1', 123, None, None, None, None), None) ] assert recorder._imp_counter.track.mock_calls == [mocker.call([{"f": "f1", "ks": ["l1"]}, {"f": "f2", "ks": ["l1"]}])] assert recorder._unique_keys_tracker.track.mock_calls == [mocker.call('k1', 'f1'), mocker.call('k1', 'f2')] def test_sampled_recorder(self, mocker): impressions = [ - Impression('k1', 'f1', 'on', 'l1', 123, None, None), - Impression('k1', 'f2', 'on', 'l1', 123, None, None) + Impression('k1', 'f1', 'on', 'l1', 123, None, None, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, None, None, None) ] redis = mocker.Mock(spec=RedisAdapter) impmanager = mocker.Mock(spec=ImpressionsManager) impmanager.process_impressions.return_value = impressions, 0, [ - (Impression('k1', 'f1', 'on', 'l1', 123, None, None), None), - (Impression('k1', 'f2', 'on', 'l1', 123, None, None), None) + (Impression('k1', 'f1', 'on', 'l1', 123, None, None, None, None), None), + (Impression('k1', 'f2', 'on', 'l1', 123, None, None, None, None), None) ], [], [] event = mocker.Mock(spec=EventStorage) @@ -124,13 +124,13 @@ class StandardRecorderAsyncTests(object): @pytest.mark.asyncio async def test_standard_recorder(self, mocker): impressions = [ - Impression('k1', 'f1', 'on', 'l1', 123, None, None), - Impression('k1', 'f2', 'on', 'l1', 123, None, None) + Impression('k1', 'f1', 'on', 'l1', 123, None, None, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, None, None, None) ] impmanager = mocker.Mock(spec=ImpressionsManager) impmanager.process_impressions.return_value = impressions, 0, [ - (Impression('k1', 'f1', 'on', 'l1', 123, None, None), {'att1': 'val'}), - (Impression('k1', 'f2', 'on', 'l1', 123, None, None), None)], \ + (Impression('k1', 'f1', 'on', 'l1', 123, None, None, None, None), {'att1': 'val'}), + (Impression('k1', 'f2', 'on', 'l1', 123, None, None, None, None), None)], \ [{"f": "f1", "ks": ["l1"]}, {"f": "f2", "ks": ["l1"]}], [('k1', 'f1'), ('k1', 'f2')] event = mocker.Mock(spec=InMemoryEventStorageAsync) impression = mocker.Mock(spec=InMemoryImpressionStorageAsync) @@ -175,8 +175,8 @@ async def track2(x, y): assert(self.passed_args[0] == MethodExceptionsAndLatencies.TREATMENT) assert(self.passed_args[1] == 1) assert self.listener_impressions == [ - Impression('k1', 'f1', 'on', 'l1', 123, None, None), - Impression('k1', 'f2', 'on', 'l1', 123, None, None), + Impression('k1', 'f1', 'on', 'l1', 123, None, None, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, None, None, None), ] assert self.listener_attributes == [{'att1': 'val'}, None] assert self.count == [{"f": "f1", "ks": ["l1"]}, {"f": "f2", "ks": ["l1"]}] @@ -185,8 +185,8 @@ async def track2(x, y): @pytest.mark.asyncio async def test_pipelined_recorder(self, mocker): impressions = [ - Impression('k1', 'f1', 'on', 'l1', 123, None, None), - Impression('k1', 'f2', 'on', 'l1', 123, None, None) + Impression('k1', 'f1', 'on', 'l1', 123, None, None, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, None, None, None) ] redis = mocker.Mock(spec=RedisAdapterAsync) async def execute(): @@ -194,8 +194,8 @@ async def execute(): redis().execute = execute impmanager = mocker.Mock(spec=ImpressionsManager) impmanager.process_impressions.return_value = impressions, 0, [ - (Impression('k1', 'f1', 'on', 'l1', 123, None, None), {'att1': 'val'}), - (Impression('k1', 'f2', 'on', 'l1', 123, None, None), None)], \ + (Impression('k1', 'f1', 'on', 'l1', 123, None, None, None, None), {'att1': 'val'}), + (Impression('k1', 'f2', 'on', 'l1', 123, None, None, None, None), None)], \ [{"f": "f1", "ks": ["l1"]}, {"f": "f2", "ks": ["l1"]}], [('k1', 'f1'), ('k1', 'f2')] event = mocker.Mock(spec=RedisEventsStorageAsync) impression = mocker.Mock(spec=RedisImpressionsStorageAsync) @@ -227,8 +227,8 @@ async def track2(x, y): assert recorder._telemetry_redis_storage.add_latency_to_pipe.mock_calls[0][1][0] == MethodExceptionsAndLatencies.TREATMENT assert recorder._telemetry_redis_storage.add_latency_to_pipe.mock_calls[0][1][1] == 1 assert self.listener_impressions == [ - Impression('k1', 'f1', 'on', 'l1', 123, None, None), - Impression('k1', 'f2', 'on', 'l1', 123, None, None), + Impression('k1', 'f1', 'on', 'l1', 123, None, None, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, None, None, None), ] assert self.listener_attributes == [{'att1': 'val'}, None] assert self.count == [{"f": "f1", "ks": ["l1"]}, {"f": "f2", "ks": ["l1"]}] @@ -237,14 +237,14 @@ async def track2(x, y): @pytest.mark.asyncio async def test_sampled_recorder(self, mocker): impressions = [ - Impression('k1', 'f1', 'on', 'l1', 123, None, None), - Impression('k1', 'f2', 'on', 'l1', 123, None, None) + Impression('k1', 'f1', 'on', 'l1', 123, None, None, None, None), + Impression('k1', 'f2', 'on', 'l1', 123, None, None, None, None) ] redis = mocker.Mock(spec=RedisAdapterAsync) impmanager = mocker.Mock(spec=ImpressionsManager) impmanager.process_impressions.return_value = impressions, 0, [ - (Impression('k1', 'f1', 'on', 'l1', 123, None, None), None), - (Impression('k1', 'f2', 'on', 'l1', 123, None, None), None) + (Impression('k1', 'f1', 'on', 'l1', 123, None, None, None, None), None), + (Impression('k1', 'f2', 'on', 'l1', 123, None, None, None, None), None) ], [], [] event = mocker.Mock(spec=RedisEventsStorageAsync) impression = mocker.Mock(spec=RedisImpressionsStorageAsync) diff --git a/tests/storage/test_inmemory_storage.py b/tests/storage/test_inmemory_storage.py index 9c5b6ed2..2bb113d7 100644 --- a/tests/storage/test_inmemory_storage.py +++ b/tests/storage/test_inmemory_storage.py @@ -794,39 +794,39 @@ def test_push_pop_impressions(self, mocker): telemetry_producer = TelemetryStorageProducer(telemetry_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storage = InMemoryImpressionStorage(100, telemetry_runtime_producer) - storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) - storage.put([Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) - storage.put([Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) + storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None)]) + storage.put([Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None)]) + storage.put([Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None)]) assert(telemetry_storage._counters._impressions_queued == 3) # Assert impressions are retrieved in the same order they are inserted. assert storage.pop_many(1) == [ - Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654) + Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) ] assert storage.pop_many(1) == [ - Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654) + Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) ] assert storage.pop_many(1) == [ - Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654) + Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) ] # Assert inserting multiple impressions at once works and maintains order. impressions = [ - Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654), - Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654), - Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654) + Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None), + Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None), + Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) ] assert storage.put(impressions) # Assert impressions are retrieved in the same order they are inserted. assert storage.pop_many(1) == [ - Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654) + Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) ] assert storage.pop_many(1) == [ - Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654) + Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) ] assert storage.pop_many(1) == [ - Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654) + Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) ] def test_queue_full_hook(self, mocker): @@ -835,7 +835,7 @@ def test_queue_full_hook(self, mocker): queue_full_hook = mocker.Mock() storage.set_queue_full_hook(queue_full_hook) impressions = [ - Impression('key%d' % i, 'feature1', 'on', 'l1', 123456, 'b1', 321654) + Impression('key%d' % i, 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) for i in range(0, 101) ] storage.put(impressions) @@ -844,7 +844,7 @@ def test_queue_full_hook(self, mocker): def test_clear(self, mocker): """Test clear method.""" storage = InMemoryImpressionStorage(100, mocker.Mock()) - storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) + storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None)]) assert storage._impressions.qsize() == 1 storage.clear() @@ -856,9 +856,9 @@ def test_impressions_dropped(self, mocker): telemetry_producer = TelemetryStorageProducer(telemetry_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storage = InMemoryImpressionStorage(2, telemetry_runtime_producer) - storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) - storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) - storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) + storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None)]) + storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None)]) + storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None)]) assert(telemetry_storage._counters._impressions_dropped == 1) assert(telemetry_storage._counters._impressions_queued == 2) @@ -873,39 +873,39 @@ async def test_push_pop_impressions(self, mocker): telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storage = InMemoryImpressionStorageAsync(100, telemetry_runtime_producer) - await storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) - await storage.put([Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) - await storage.put([Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) + await storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None)]) + await storage.put([Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None)]) + await storage.put([Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None)]) assert(telemetry_storage._counters._impressions_queued == 3) # Assert impressions are retrieved in the same order they are inserted. assert await storage.pop_many(1) == [ - Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654) + Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) ] assert await storage.pop_many(1) == [ - Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654) + Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) ] assert await storage.pop_many(1) == [ - Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654) + Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) ] # Assert inserting multiple impressions at once works and maintains order. impressions = [ - Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654), - Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654), - Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654) + Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None), + Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None), + Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) ] assert await storage.put(impressions) # Assert impressions are retrieved in the same order they are inserted. assert await storage.pop_many(1) == [ - Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654) + Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) ] assert await storage.pop_many(1) == [ - Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654) + Impression('key2', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) ] assert await storage.pop_many(1) == [ - Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654) + Impression('key3', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) ] @pytest.mark.asyncio @@ -921,7 +921,7 @@ async def queue_full_hook(): storage.set_queue_full_hook(queue_full_hook) impressions = [ - Impression('key%d' % i, 'feature1', 'on', 'l1', 123456, 'b1', 321654) + Impression('key%d' % i, 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None) for i in range(0, 101) ] await storage.put(impressions) @@ -935,7 +935,7 @@ async def test_clear(self, mocker): telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storage = InMemoryImpressionStorageAsync(100, telemetry_runtime_producer) - await storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) + await storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None)]) assert storage._impressions.qsize() == 1 await storage.clear() assert storage._impressions.qsize() == 0 @@ -947,9 +947,9 @@ async def test_impressions_dropped(self, mocker): telemetry_producer = TelemetryStorageProducerAsync(telemetry_storage) telemetry_runtime_producer = telemetry_producer.get_telemetry_runtime_producer() storage = InMemoryImpressionStorageAsync(2, telemetry_runtime_producer) - await storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) - await storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) - await storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654)]) + await storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None)]) + await storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None)]) + await storage.put([Impression('key1', 'feature1', 'on', 'l1', 123456, 'b1', 321654, None, None)]) assert(telemetry_storage._counters._impressions_dropped == 1) assert(telemetry_storage._counters._impressions_queued == 2) diff --git a/tests/storage/test_pluggable.py b/tests/storage/test_pluggable.py index 283eb8e3..8b5f9a95 100644 --- a/tests/storage/test_pluggable.py +++ b/tests/storage/test_pluggable.py @@ -646,10 +646,10 @@ def test_put(self): prefix = '' pluggable_imp_storage = PluggableImpressionsStorage(self.mock_adapter, self.metadata, prefix=sprefix) impressions = [ - Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654) + Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None) ] assert(pluggable_imp_storage.put(impressions)) assert(pluggable_imp_storage._impressions_queue_key in self.mock_adapter._keys) @@ -657,8 +657,8 @@ def test_put(self): assert(self.mock_adapter._expire[prefix + "SPLITIO.impressions"] == PluggableImpressionsStorage.IMPRESSIONS_KEY_DEFAULT_TTL) impressions2 = [ - Impression('key5', 'feature1', 'off', 'some_label', 123456, 'buck1', 321654), - Impression('key6', 'feature2', 'off', 'some_label', 123456, 'buck1', 321654), + Impression('key5', 'feature1', 'off', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key6', 'feature2', 'off', 'some_label', 123456, 'buck1', 321654, None, None), ] assert(pluggable_imp_storage.put(impressions2)) assert(self.mock_adapter._keys[prefix + "SPLITIO.impressions"] == pluggable_imp_storage._wrap_impressions(impressions + impressions2)) @@ -667,8 +667,8 @@ def test_wrap_impressions(self): for sprefix in [None, 'myprefix']: pluggable_imp_storage = PluggableImpressionsStorage(self.mock_adapter, self.metadata, prefix=sprefix) impressions = [ - Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key2', 'feature2', 'off', 'some_label', 123456, 'buck1', 321654), + Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key2', 'feature2', 'off', 'some_label', 123456, 'buck1', 321654, None, None), ] assert(pluggable_imp_storage._wrap_impressions(impressions) == [ json.dumps({ @@ -685,6 +685,7 @@ def test_wrap_impressions(self): 'r': 'some_label', 'c': 123456, 'm': 321654, + 'properties': None } }), json.dumps({ @@ -701,6 +702,7 @@ def test_wrap_impressions(self): 'r': 'some_label', 'c': 123456, 'm': 321654, + 'properties': None } }) ]) @@ -763,10 +765,10 @@ async def test_put(self): prefix = '' pluggable_imp_storage = PluggableImpressionsStorageAsync(self.mock_adapter, self.metadata, prefix=sprefix) impressions = [ - Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654) + Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None) ] assert(await pluggable_imp_storage.put(impressions)) assert(pluggable_imp_storage._impressions_queue_key in self.mock_adapter._keys) @@ -774,8 +776,8 @@ async def test_put(self): assert(self.mock_adapter._expire[prefix + "SPLITIO.impressions"] == PluggableImpressionsStorageAsync.IMPRESSIONS_KEY_DEFAULT_TTL) impressions2 = [ - Impression('key5', 'feature1', 'off', 'some_label', 123456, 'buck1', 321654), - Impression('key6', 'feature2', 'off', 'some_label', 123456, 'buck1', 321654), + Impression('key5', 'feature1', 'off', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key6', 'feature2', 'off', 'some_label', 123456, 'buck1', 321654, None, None), ] assert(await pluggable_imp_storage.put(impressions2)) assert(self.mock_adapter._keys[prefix + "SPLITIO.impressions"] == pluggable_imp_storage._wrap_impressions(impressions + impressions2)) @@ -784,8 +786,8 @@ def test_wrap_impressions(self): for sprefix in [None, 'myprefix']: pluggable_imp_storage = PluggableImpressionsStorageAsync(self.mock_adapter, self.metadata, prefix=sprefix) impressions = [ - Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key2', 'feature2', 'off', 'some_label', 123456, 'buck1', 321654), + Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key2', 'feature2', 'off', 'some_label', 123456, 'buck1', 321654, None, None), ] assert(pluggable_imp_storage._wrap_impressions(impressions) == [ json.dumps({ @@ -802,6 +804,7 @@ def test_wrap_impressions(self): 'r': 'some_label', 'c': 123456, 'm': 321654, + 'properties': None } }), json.dumps({ @@ -818,6 +821,7 @@ def test_wrap_impressions(self): 'r': 'some_label', 'c': 123456, 'm': 321654, + 'properties': None } }) ]) diff --git a/tests/storage/test_redis.py b/tests/storage/test_redis.py index 4537998c..de5ebfd5 100644 --- a/tests/storage/test_redis.py +++ b/tests/storage/test_redis.py @@ -587,10 +587,10 @@ def test_wrap_impressions(self, mocker): storage = RedisImpressionsStorage(adapter, metadata) impressions = [ - Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654) + Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None) ] to_validate = [json.dumps({ @@ -607,6 +607,7 @@ def test_wrap_impressions(self, mocker): 'r': impression.label, 'c': impression.change_number, 'm': impression.time, + 'properties': None } }) for impression in impressions] @@ -619,10 +620,10 @@ def test_add_impressions(self, mocker): storage = RedisImpressionsStorage(adapter, metadata) impressions = [ - Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654) + Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None) ] assert storage.put(impressions) is True @@ -641,6 +642,7 @@ def test_add_impressions(self, mocker): 'r': impression.label, 'c': impression.change_number, 'm': impression.time, + 'properties': None } }) for impression in impressions] @@ -661,10 +663,10 @@ def test_add_impressions_to_pipe(self, mocker): storage = RedisImpressionsStorage(adapter, metadata) impressions = [ - Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654) + Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None) ] to_validate = [json.dumps({ @@ -681,6 +683,7 @@ def test_add_impressions_to_pipe(self, mocker): 'r': impression.label, 'c': impression.change_number, 'm': impression.time, + 'properties': None } }) for impression in impressions] @@ -718,10 +721,10 @@ def test_wrap_impressions(self, mocker): storage = RedisImpressionsStorageAsync(adapter, metadata) impressions = [ - Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654) + Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None) ] to_validate = [json.dumps({ @@ -738,6 +741,7 @@ def test_wrap_impressions(self, mocker): 'r': impression.label, 'c': impression.change_number, 'm': impression.time, + 'properties': None } }) for impression in impressions] @@ -751,10 +755,10 @@ async def test_add_impressions(self, mocker): storage = RedisImpressionsStorageAsync(adapter, metadata) impressions = [ - Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654) + Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None) ] self.key = None self.imps = None @@ -779,6 +783,7 @@ async def rpush(key, *imps): 'r': impression.label, 'c': impression.change_number, 'm': impression.time, + 'properties': None } }) for impression in impressions] @@ -800,10 +805,10 @@ def test_add_impressions_to_pipe(self, mocker): storage = RedisImpressionsStorageAsync(adapter, metadata) impressions = [ - Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654), - Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654) + Impression('key1', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key2', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key3', 'feature2', 'on', 'some_label', 123456, 'buck1', 321654, None, None), + Impression('key4', 'feature1', 'on', 'some_label', 123456, 'buck1', 321654, None, None) ] to_validate = [json.dumps({ @@ -820,6 +825,7 @@ def test_add_impressions_to_pipe(self, mocker): 'r': impression.label, 'c': impression.change_number, 'm': impression.time, + 'properties': None } }) for impression in impressions] diff --git a/tests/sync/test_impressions_synchronizer.py b/tests/sync/test_impressions_synchronizer.py index 1deaa833..00b65833 100644 --- a/tests/sync/test_impressions_synchronizer.py +++ b/tests/sync/test_impressions_synchronizer.py @@ -17,8 +17,8 @@ class ImpressionsSynchronizerTests(object): def test_synchronize_impressions_error(self, mocker): storage = mocker.Mock(spec=ImpressionStorage) storage.pop_many.return_value = [ - Impression('key1', 'split1', 'on', 'l1', 123456, 'b1', 321654), - Impression('key2', 'split1', 'on', 'l1', 123456, 'b1', 321654), + Impression('key1', 'split1', 'on', 'l1', 123456, 'b1', 321654, None, None), + Impression('key2', 'split1', 'on', 'l1', 123456, 'b1', 321654, None, None), ] api = mocker.Mock() @@ -49,8 +49,8 @@ def run(x): def test_synchronize_impressions(self, mocker): storage = mocker.Mock(spec=ImpressionStorage) storage.pop_many.return_value = [ - Impression('key1', 'split1', 'on', 'l1', 123456, 'b1', 321654), - Impression('key2', 'split1', 'on', 'l1', 123456, 'b1', 321654), + Impression('key1', 'split1', 'on', 'l1', 123456, 'b1', 321654, None, None), + Impression('key2', 'split1', 'on', 'l1', 123456, 'b1', 321654, None, None), ] api = mocker.Mock() @@ -76,8 +76,8 @@ async def test_synchronize_impressions_error(self, mocker): storage = mocker.Mock(spec=ImpressionStorage) async def pop_many(*args): return [ - Impression('key1', 'split1', 'on', 'l1', 123456, 'b1', 321654), - Impression('key2', 'split1', 'on', 'l1', 123456, 'b1', 321654), + Impression('key1', 'split1', 'on', 'l1', 123456, 'b1', 321654, None, None), + Impression('key2', 'split1', 'on', 'l1', 123456, 'b1', 321654, None, None), ] storage.pop_many = pop_many api = mocker.Mock() @@ -113,8 +113,8 @@ async def test_synchronize_impressions(self, mocker): storage = mocker.Mock(spec=ImpressionStorage) async def pop_many(*args): return [ - Impression('key1', 'split1', 'on', 'l1', 123456, 'b1', 321654), - Impression('key2', 'split1', 'on', 'l1', 123456, 'b1', 321654), + Impression('key1', 'split1', 'on', 'l1', 123456, 'b1', 321654, None, None), + Impression('key2', 'split1', 'on', 'l1', 123456, 'b1', 321654, None, None), ] storage.pop_many = pop_many diff --git a/tests/tasks/test_impressions_sync.py b/tests/tasks/test_impressions_sync.py index f19be535..78bbf979 100644 --- a/tests/tasks/test_impressions_sync.py +++ b/tests/tasks/test_impressions_sync.py @@ -20,11 +20,11 @@ def test_normal_operation(self, mocker): """Test that the task works properly under normal circumstances.""" storage = mocker.Mock(spec=ImpressionStorage) impressions = [ - Impression('key1', 'split1', 'on', 'l1', 123456, 'b1', 321654), - Impression('key2', 'split1', 'on', 'l1', 123456, 'b1', 321654), - Impression('key3', 'split2', 'off', 'l1', 123456, 'b1', 321654), - Impression('key4', 'split2', 'on', 'l1', 123456, 'b1', 321654), - Impression('key5', 'split3', 'off', 'l1', 123456, 'b1', 321654) + Impression('key1', 'split1', 'on', 'l1', 123456, 'b1', 321654, None, None), + Impression('key2', 'split1', 'on', 'l1', 123456, 'b1', 321654, None, None), + Impression('key3', 'split2', 'off', 'l1', 123456, 'b1', 321654, None, None), + Impression('key4', 'split2', 'on', 'l1', 123456, 'b1', 321654, None, None), + Impression('key5', 'split3', 'off', 'l1', 123456, 'b1', 321654, None, None) ] storage.pop_many.return_value = impressions api = mocker.Mock(spec=ImpressionsAPI) @@ -55,11 +55,11 @@ async def test_normal_operation(self, mocker): """Test that the task works properly under normal circumstances.""" storage = mocker.Mock(spec=ImpressionStorage) impressions = [ - Impression('key1', 'split1', 'on', 'l1', 123456, 'b1', 321654), - Impression('key2', 'split1', 'on', 'l1', 123456, 'b1', 321654), - Impression('key3', 'split2', 'off', 'l1', 123456, 'b1', 321654), - Impression('key4', 'split2', 'on', 'l1', 123456, 'b1', 321654), - Impression('key5', 'split3', 'off', 'l1', 123456, 'b1', 321654) + Impression('key1', 'split1', 'on', 'l1', 123456, 'b1', 321654, None, None), + Impression('key2', 'split1', 'on', 'l1', 123456, 'b1', 321654, None, None), + Impression('key3', 'split2', 'off', 'l1', 123456, 'b1', 321654, None, None), + Impression('key4', 'split2', 'on', 'l1', 123456, 'b1', 321654, None, None), + Impression('key5', 'split3', 'off', 'l1', 123456, 'b1', 321654, None, None) ] self.pop_called = 0 async def pop_many(*args): From bac9e800be4767c604030509c2353f5fce188eb9 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 2 Jul 2025 12:13:45 -0700 Subject: [PATCH 7/8] Removed properties if none in sender --- splitio/api/impressions.py | 35 ++++++++++++++++++++++--------- tests/api/test_impressions_api.py | 4 ++-- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/splitio/api/impressions.py b/splitio/api/impressions.py index 19c79a88..da85691b 100644 --- a/splitio/api/impressions.py +++ b/splitio/api/impressions.py @@ -30,16 +30,7 @@ def _build_bulk(impressions): { 'f': test_name, 'i': [ - { - 'k': impression.matching_key, - 't': impression.treatment, - 'm': impression.time, - 'c': impression.change_number, - 'r': impression.label, - 'b': impression.bucketing_key, - 'pt': impression.previous_time, - 'properties': impression.properties - } + ImpressionsAPIBase._filter_out_null_prop(impression) for impression in imps ] } @@ -49,6 +40,30 @@ def _build_bulk(impressions): ) ] + @staticmethod + def _filter_out_null_prop(impression): + if impression.properties == None: + return { + 'k': impression.matching_key, + 't': impression.treatment, + 'm': impression.time, + 'c': impression.change_number, + 'r': impression.label, + 'b': impression.bucketing_key, + 'pt': impression.previous_time + } + + return { + 'k': impression.matching_key, + 't': impression.treatment, + 'm': impression.time, + 'c': impression.change_number, + 'r': impression.label, + 'b': impression.bucketing_key, + 'pt': impression.previous_time, + 'properties': impression.properties + } + @staticmethod def _build_counters(counters): """ diff --git a/tests/api/test_impressions_api.py b/tests/api/test_impressions_api.py index 2215aa04..b022a464 100644 --- a/tests/api/test_impressions_api.py +++ b/tests/api/test_impressions_api.py @@ -22,12 +22,12 @@ 'f': 'f1', 'i': [ {'k': 'k1', 'b': 'b1', 't': 'on', 'r': 'l1', 'm': 321654, 'c': 123456, 'pt': None, 'properties': {"prop": "val"}}, - {'k': 'k3', 'b': 'b1', 't': 'on', 'r': 'l1', 'm': 321654, 'c': 123456, 'pt': None, 'properties': None}, + {'k': 'k3', 'b': 'b1', 't': 'on', 'r': 'l1', 'm': 321654, 'c': 123456, 'pt': None}, ], }, { 'f': 'f2', 'i': [ - {'k': 'k2', 'b': 'b1', 't': 'off', 'r': 'l1', 'm': 321654, 'c': 123456, 'pt': None, 'properties': None}, + {'k': 'k2', 'b': 'b1', 't': 'off', 'r': 'l1', 'm': 321654, 'c': 123456, 'pt': None}, ] }] From 69a39b8527a0b57970d0b2964ef20162181ca1d2 Mon Sep 17 00:00:00 2001 From: Bilal Al-Shahwany Date: Wed, 2 Jul 2025 12:43:37 -0700 Subject: [PATCH 8/8] polish --- splitio/client/input_validator.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/splitio/client/input_validator.py b/splitio/client/input_validator.py index 0b502244..f2ad03a5 100644 --- a/splitio/client/input_validator.py +++ b/splitio/client/input_validator.py @@ -595,8 +595,7 @@ def valid_properties(properties, source): if element is None: continue - if not isinstance(element, str) and not isinstance(element, Number) \ - and not isinstance(element, bool): + if not _check_element_type(element): _LOGGER.warning('%s: Property %s is of invalid type. Setting value to None', source, element) element = None @@ -616,6 +615,13 @@ def valid_properties(properties, source): ' when processed', source) return True, valid_properties if len(valid_properties) else None, size +def _check_element_type(element): + if not isinstance(element, str) and not isinstance(element, Number) \ + and not isinstance(element, bool): + return False + + return True + def validate_pluggable_adapter(config): """ Check if pluggable adapter contains the expected method signature