Skip to content

Commit 00eb469

Browse files
author
Matias Melograno
committed
added properties to track method
1 parent 71dfab2 commit 00eb469

File tree

6 files changed

+171
-24
lines changed

6 files changed

+171
-24
lines changed

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
8.1.0 (May, 2019)
2+
- Added properties to track method.
13
8.0.0 (Apr 24, 2019)
24
- Full SDK Refactor/rewrite.
35
- New block until ready behaviour.

splitio/client/client.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from splitio.client.listener import ImpressionListenerException
1515

1616

17-
class Client(object): #pylint: disable=too-many-instance-attributes
17+
class Client(object): # pylint: disable=too-many-instance-attributes
1818
"""Entry point for the split sdk."""
1919

2020
_METRIC_GET_TREATMENT = 'sdk.getTreatment'
@@ -41,11 +41,11 @@ def __init__(self, factory, labels_enabled=True, impression_listener=None):
4141
self._impression_listener = impression_listener
4242

4343
self._splitter = Splitter()
44-
self._split_storage = factory._get_storage('splits') #pylint: disable=protected-access
45-
self._segment_storage = factory._get_storage('segments') #pylint: disable=protected-access
46-
self._impressions_storage = factory._get_storage('impressions') #pylint: disable=protected-access
47-
self._events_storage = factory._get_storage('events') #pylint: disable=protected-access
48-
self._telemetry_storage = factory._get_storage('telemetry') #pylint: disable=protected-access
44+
self._split_storage = factory._get_storage('splits') # pylint: disable=protected-access
45+
self._segment_storage = factory._get_storage('segments') # pylint: disable=protected-access
46+
self._impressions_storage = factory._get_storage('impressions') # pylint: disable=protected-access
47+
self._events_storage = factory._get_storage('events') # pylint: disable=protected-access
48+
self._telemetry_storage = factory._get_storage('telemetry') # pylint: disable=protected-access
4949
self._evaluator = Evaluator(self._split_storage, self._segment_storage, self._splitter)
5050

5151
def destroy(self):
@@ -136,7 +136,7 @@ def get_treatment_with_config(self, key, feature, attributes=None):
136136
self._record_stats(impression, start, self._METRIC_GET_TREATMENT)
137137
self._send_impression_to_listener(impression, attributes)
138138
return result['treatment'], result['configurations']
139-
except Exception: #pylint: disable=broad-except
139+
except Exception: # pylint: disable=broad-except
140140
self._logger.error('Error getting treatment for feature')
141141
self._logger.debug('Error: ', exc_info=True)
142142
try:
@@ -231,7 +231,7 @@ def get_treatments_with_config(self, key, features, attributes=None):
231231
bulk_impressions.append(impression)
232232
treatments[feature] = (treatment['treatment'], treatment['configurations'])
233233

234-
except Exception: #pylint: disable=broad-except
234+
except Exception: # pylint: disable=broad-except
235235
self._logger.error('get_treatments: An exception occured when evaluating '
236236
'feature ' + feature + ' returning CONTROL.')
237237
treatments[feature] = CONTROL, None
@@ -244,14 +244,13 @@ def get_treatments_with_config(self, key, features, attributes=None):
244244
self._record_stats(bulk_impressions, start, self._METRIC_GET_TREATMENTS)
245245
for impression in bulk_impressions:
246246
self._send_impression_to_listener(impression, attributes)
247-
except Exception: #pylint: disable=broad-except
247+
except Exception: # pylint: disable=broad-except
248248
self._logger.error('get_treatments: An exception when trying to store '
249249
'impressions.')
250250
self._logger.debug('Error: ', exc_info=True)
251251

252252
return treatments
253253

254-
255254
def get_treatments(self, key, features, attributes=None):
256255
"""
257256
Evaluate multiple features and return a dictionary with all the feature/treatments.
@@ -271,7 +270,7 @@ def get_treatments(self, key, features, attributes=None):
271270
with_config = self.get_treatments_with_config(key, features, attributes)
272271
return {feature: result[0] for (feature, result) in six.iteritems(with_config)}
273272

274-
def _build_impression( #pylint: disable=too-many-arguments
273+
def _build_impression( # pylint: disable=too-many-arguments
275274
self,
276275
matching_key,
277276
feature_name,
@@ -311,11 +310,11 @@ def _record_stats(self, impressions, start, operation):
311310
else:
312311
self._impressions_storage.put(impressions)
313312
self._telemetry_storage.inc_latency(operation, get_latency_bucket_index(end - start))
314-
except Exception: #pylint: disable=broad-except
313+
except Exception: # pylint: disable=broad-except
315314
self._logger.error('Error recording impressions and metrics')
316315
self._logger.debug('Error: ', exc_info=True)
317316

318-
def track(self, key, traffic_type, event_type, value=None):
317+
def track(self, key, traffic_type, event_type, value=None, properties=None):
319318
"""
320319
Track an event.
321320
@@ -327,6 +326,8 @@ def track(self, key, traffic_type, event_type, value=None):
327326
:type event_type: str
328327
:param value: (Optional) value associated to the event
329328
:type value: Number
329+
:param properties: (Optional) properties associated to the event
330+
:type properties: dict
330331
331332
:return: Whether the event was created or not.
332333
:rtype: bool
@@ -339,15 +340,18 @@ def track(self, key, traffic_type, event_type, value=None):
339340
event_type = input_validator.validate_event_type(event_type)
340341
traffic_type = input_validator.validate_traffic_type(traffic_type)
341342
value = input_validator.validate_value(value)
343+
valid, properties = input_validator.valid_properties(properties)
342344

343-
if key is None or event_type is None or traffic_type is None or value is False:
345+
if key is None or event_type is None or traffic_type is None or value is False \
346+
or valid is False:
344347
return False
345348

346349
event = Event(
347350
key=key,
348351
traffic_type_name=traffic_type,
349352
event_type_id=event_type,
350353
value=value,
351-
timestamp=int(time.time()*1000)
354+
timestamp=int(time.time()*1000),
355+
properties=properties,
352356
)
353357
return self._events_storage.put([event])

splitio/client/input_validator.py

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import math
99

1010
import six
11+
import sys
1112

1213
from splitio.api import APIException
1314
from splitio.client.key import Key
@@ -18,6 +19,7 @@
1819
_LOGGER = logging.getLogger(__name__)
1920
MAX_LENGTH = 250
2021
EVENT_TYPE_PATTERN = r'^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$'
22+
MAX_PROPERTIES_LENGTH_BYTES = 32768
2123

2224

2325
def _get_first_split_sdk_call():
@@ -33,9 +35,10 @@ def _get_first_split_sdk_call():
3335
if calls:
3436
return calls[-1]
3537
return unknown_method
36-
except Exception: #pylint: disable=broad-except
38+
except Exception: # pylint: disable=broad-except
3739
return unknown_method
3840

41+
3942
def _check_not_null(value, name, operation):
4043
"""
4144
Check if value is null.
@@ -358,7 +361,7 @@ def validate_manager_feature_name(feature_name):
358361
return feature_name
359362

360363

361-
def validate_features_get_treatments(features): #pylint: disable=invalid-name
364+
def validate_features_get_treatments(features): # pylint: disable=invalid-name
362365
"""
363366
Check if features is valid for get_treatments.
364367
@@ -432,7 +435,7 @@ def validate_apikey_type(segment_api):
432435
"""
433436
api_messages_filter = _ApiLogFilter()
434437
try:
435-
segment_api._logger.addFilter(api_messages_filter) #pylint: disable=protected-access
438+
segment_api._logger.addFilter(api_messages_filter) # pylint: disable=protected-access
436439
segment_api.fetch_segment('__SOME_INVALID_SEGMENT__', -1)
437440
except APIException as exc:
438441
if exc.status_code == 403:
@@ -441,7 +444,7 @@ def validate_apikey_type(segment_api):
441444
+ 'console that is of type sdk')
442445
return False
443446
finally:
444-
segment_api._logger.removeFilter(api_messages_filter) #pylint: disable=protected-access
447+
segment_api._logger.removeFilter(api_messages_filter) # pylint: disable=protected-access
445448

446449
# True doesn't mean that the APIKEY is right, only that it's not of type "browser"
447450
return True
@@ -453,10 +456,6 @@ def validate_factory_instantiation(apikey):
453456
454457
:param apikey: str
455458
:type apikey: str
456-
:param config: dict
457-
:type config: dict
458-
:param segment_api: Segment API client
459-
:type segment_api: splitio.api.segments.SegmentsAPI
460459
:return: bool
461460
:rtype: True|False
462461
"""
@@ -467,3 +466,57 @@ def validate_factory_instantiation(apikey):
467466
(not _check_string_not_empty(apikey, 'apikey', 'factory_instantiation')):
468467
return False
469468
return True
469+
470+
471+
def valid_properties(properties):
472+
"""
473+
Check if properties is a valid dict and returns the properties
474+
that will be sent to the track method, avoiding unexpected types.
475+
476+
:param properties: dict
477+
:type properties: dict
478+
:return: tuple
479+
:rtype: (bool,dict)
480+
"""
481+
if properties is None:
482+
return True, None
483+
if not isinstance(properties, dict):
484+
_LOGGER.error('track: properties must be of type dictionary.')
485+
return False, None
486+
487+
size = 1024 # We assume 1kb events without properties (750 bytes avg measured)
488+
valid_properties = None
489+
490+
for property, element in properties.items():
491+
if not isinstance(property, six.string_types): # Exclude property if is not string
492+
continue
493+
494+
if valid_properties is None:
495+
valid_properties = dict()
496+
497+
valid_properties[property] = None
498+
size += sys.getsizeof(property)
499+
500+
if element is None:
501+
continue
502+
503+
if not isinstance(element, six.string_types) and not isinstance(element, Number) \
504+
and not isinstance(element, bool):
505+
_LOGGER.warning('Property %s is of invalid type. Setting value to None', element)
506+
element = None
507+
508+
valid_properties[property] = element
509+
size += sys.getsizeof(str(element))
510+
511+
if size > MAX_PROPERTIES_LENGTH_BYTES:
512+
_LOGGER.error(
513+
'The maximum size allowed for the properties is 32768 bytes. ' +
514+
'Current one is ' + str(size) + ' bytes. Event not queued'
515+
)
516+
return False, None
517+
518+
if isinstance(valid_properties, dict) and len(valid_properties.keys()) > 300:
519+
_LOGGER.warning('Event has more than 300 properties. Some of them will be trimmed' +
520+
' when processed')
521+
522+
return True, valid_properties

splitio/models/events.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@
1414
'event_type_id',
1515
'value',
1616
'timestamp',
17+
'properties',
1718
])

splitio/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '8.0.0'
1+
__version__ = '8.1.0-rc1'

tests/client/test_input_validator.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from splitio.storage import SplitStorage, EventStorage, ImpressionStorage, TelemetryStorage, \
1414
SegmentStorage
1515
from splitio.models.splits import Split
16+
from splitio.client import input_validator
1617

1718

1819
class ClientInputValidationTests(object):
@@ -445,6 +446,41 @@ def _get_storage_mock(storage):
445446
mocker.call('%s: feature_name \'%s\' has extra whitespace, trimming.', 'get_treatment_with_config', ' some_feature ')
446447
]
447448

449+
def test_valid_properties(self, mocker):
450+
"""Test valid_properties() method"""
451+
assert input_validator.valid_properties(None) == (True, None)
452+
assert input_validator.valid_properties([]) == (False, None)
453+
assert input_validator.valid_properties(True) == (False, None)
454+
455+
props1 = {
456+
"test1": "test",
457+
"test2": 1,
458+
"test3": True,
459+
"test4": None,
460+
"test5": [],
461+
2: "t",
462+
}
463+
r1, r2 = input_validator.valid_properties(props1)
464+
assert r1 == True
465+
assert len(r2.keys()) == 5
466+
assert r2["test1"] == "test"
467+
assert r2["test2"] == 1
468+
assert r2["test3"] == True
469+
assert r2["test4"] == None
470+
assert r2["test5"] == None
471+
472+
props2 = dict();
473+
for i in range(301):
474+
props2[str(i)] = i
475+
assert input_validator.valid_properties(props2) == (True, props2)
476+
477+
props3 = dict();
478+
for i in range(110):
479+
props3[str(i)] = str(i) * 300
480+
r1, r2 = input_validator.valid_properties(props3)
481+
assert r1 == False
482+
483+
448484
def test_track(self, mocker):
449485
"""Test track method()."""
450486
events_storage_mock = mocker.Mock(spec=EventStorage)
@@ -602,6 +638,57 @@ def test_track(self, mocker):
602638
mocker.call("track: value must be a number.")
603639
]
604640

641+
client._logger.reset_mock()
642+
assert client.track("some_key", "traffic_type", "event_type", 1, []) is False
643+
assert client._logger.error.mock_calls == [
644+
mocker.call("track: properties must be of type dictionary.")
645+
]
646+
647+
client._logger.reset_mock()
648+
assert client.track("some_key", "traffic_type", "event_type", 1, []) is False
649+
assert client._logger.error.mock_calls == [
650+
mocker.call("track: properties must be of type dictionary.")
651+
]
652+
653+
client._logger.reset_mock()
654+
assert client.track("some_key", "traffic_type", "event_type", 1, True) is False
655+
assert client._logger.error.mock_calls == [
656+
mocker.call("track: properties must be of type dictionary.")
657+
]
658+
659+
props1 = {
660+
"test1": "test",
661+
"test2": 1,
662+
"test3": True,
663+
"test4": None,
664+
"test5": [],
665+
2: "t",
666+
}
667+
client._logger.reset_mock()
668+
assert client.track("some_key", "traffic_type", "event_type", 1, props1) is True
669+
assert client._logger.warning.mock_calls == [
670+
mocker.call("Property %s is of invalid type. Setting value to None", [])
671+
]
672+
673+
props2 = dict();
674+
for i in range(301):
675+
props2[str(i)] = i
676+
client._logger.reset_mock()
677+
assert client.track("some_key", "traffic_type", "event_type", 1, props2) is True
678+
assert client._logger.warning.mock_calls == [
679+
mocker.call("Event has more than 300 properties. Some of them will be trimmed when processed")
680+
]
681+
682+
client._logger.reset_mock()
683+
props3 = dict();
684+
for i in range(110):
685+
props3[str(i)] = str(i) * 300
686+
assert client.track("some_key", "traffic_type", "event_type", 1, props3) is False
687+
assert client._logger.error.mock_calls == [
688+
mocker.call("The maximum size allowed for the properties is 32768 bytes. Current one is 32949 bytes. Event not queued")
689+
]
690+
691+
605692
def test_get_treatments(self, mocker):
606693
"""Test getTreatments() method."""
607694
split_mock = mocker.Mock(spec=Split)

0 commit comments

Comments
 (0)