Skip to content

Commit f840f06

Browse files
author
Matias Melograno
committed
merged with dev
2 parents c2351a6 + 19f27b0 commit f840f06

File tree

20 files changed

+339
-115
lines changed

20 files changed

+339
-115
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/api/events.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from splitio.api.client import HttpClientException
88

99

10-
class EventsAPI(object): #pylint: disable=too-few-public-methods
10+
class EventsAPI(object): # pylint: disable=too-few-public-methods
1111
"""Class that uses an httpClient to communicate with the events API."""
1212

1313
def __init__(self, http_client, apikey, sdk_metadata):
@@ -43,7 +43,8 @@ def _build_bulk(events):
4343
'trafficTypeName': event.traffic_type_name,
4444
'eventTypeId': event.event_type_id,
4545
'value': event.value,
46-
'timestamp': event.timestamp
46+
'timestamp': event.timestamp,
47+
'properties': event.properties,
4748
}
4849
for event in events
4950
]

splitio/api/impressions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from splitio.api.client import HttpClientException
1010

1111

12-
class ImpressionsAPI(object): # pylint: disable=too-few-public-methods
12+
class ImpressionsAPI(object): # pylint: disable=too-few-public-methods
1313
"""Class that uses an httpClient to communicate with the impressions API."""
1414

1515
def __init__(self, client, apikey, sdk_metadata):

splitio/client/client.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
from splitio.engine.evaluator import Evaluator, CONTROL
99
from splitio.engine.splitters import Splitter
1010
from splitio.models.impressions import Impression, Label
11-
from splitio.models.events import Event
11+
from splitio.models.events import Event, EventWrapper
1212
from splitio.models.telemetry import get_latency_bucket_index
1313
from splitio.client import input_validator
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):
@@ -154,7 +154,7 @@ def get_treatment_with_config(self, key, feature, attributes=None):
154154
self._record_stats(impression, start, self._METRIC_GET_TREATMENT)
155155
self._send_impression_to_listener(impression, attributes)
156156
return result['treatment'], result['configurations']
157-
except Exception: #pylint: disable=broad-except
157+
except Exception: # pylint: disable=broad-except
158158
self._logger.error('Error getting treatment for feature')
159159
self._logger.debug('Error: ', exc_info=True)
160160
try:
@@ -247,7 +247,7 @@ def get_treatments_with_config(self, key, features, attributes=None):
247247
bulk_impressions.append(impression)
248248
treatments[feature] = (result['treatment'], result['configurations'])
249249

250-
except Exception: #pylint: disable=broad-except
250+
except Exception: # pylint: disable=broad-except
251251
self._logger.error('get_treatments: An exception occured when evaluating '
252252
'feature ' + feature + ' returning CONTROL.')
253253
treatments[feature] = CONTROL, None
@@ -260,14 +260,13 @@ def get_treatments_with_config(self, key, features, attributes=None):
260260
self._record_stats(bulk_impressions, start, self._METRIC_GET_TREATMENTS)
261261
for impression in bulk_impressions:
262262
self._send_impression_to_listener(impression, attributes)
263-
except Exception: #pylint: disable=broad-except
263+
except Exception: # pylint: disable=broad-except
264264
self._logger.error('get_treatments: An exception when trying to store '
265265
'impressions.')
266266
self._logger.debug('Error: ', exc_info=True)
267267

268268
return treatments
269269

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

290-
def _build_impression( #pylint: disable=too-many-arguments
289+
def _build_impression( # pylint: disable=too-many-arguments
291290
self,
292291
matching_key,
293292
feature_name,
@@ -327,11 +326,11 @@ def _record_stats(self, impressions, start, operation):
327326
else:
328327
self._impressions_storage.put(impressions)
329328
self._telemetry_storage.inc_latency(operation, get_latency_bucket_index(end - start))
330-
except Exception: #pylint: disable=broad-except
329+
except Exception: # pylint: disable=broad-except
331330
self._logger.error('Error recording impressions and metrics')
332331
self._logger.debug('Error: ', exc_info=True)
333332

334-
def track(self, key, traffic_type, event_type, value=None):
333+
def track(self, key, traffic_type, event_type, value=None, properties=None):
335334
"""
336335
Track an event.
337336
@@ -343,6 +342,8 @@ def track(self, key, traffic_type, event_type, value=None):
343342
:type event_type: str
344343
:param value: (Optional) value associated to the event
345344
:type value: Number
345+
:param properties: (Optional) properties associated to the event
346+
:type properties: dict
346347
347348
:return: Whether the event was created or not.
348349
:rtype: bool
@@ -361,15 +362,21 @@ def track(self, key, traffic_type, event_type, value=None):
361362
)
362363

363364
value = input_validator.validate_value(value)
365+
valid, properties, size = input_validator.valid_properties(properties)
364366

365-
if key is None or event_type is None or traffic_type is None or value is False:
367+
if key is None or event_type is None or traffic_type is None or value is False \
368+
or valid is False:
366369
return False
367370

368371
event = Event(
369372
key=key,
370373
traffic_type_name=traffic_type,
371374
event_type_id=event_type,
372375
value=value,
373-
timestamp=int(time.time()*1000)
376+
timestamp=int(time.time()*1000),
377+
properties=properties,
374378
)
375-
return self._events_storage.put([event])
379+
return self._events_storage.put([EventWrapper(
380+
event=event,
381+
size=size,
382+
)])

splitio/client/factory.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from splitio.client import util
1717
from splitio.client.listener import ImpressionListenerWrapper
1818

19-
#Storage
19+
# Storage
2020
from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, \
2121
InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage
2222
from splitio.storage.adapters import redis
@@ -65,10 +65,10 @@ class TimeoutException(Exception):
6565
pass
6666

6767

68-
class SplitFactory(object): #pylint: disable=too-many-instance-attributes
68+
class SplitFactory(object): # pylint: disable=too-many-instance-attributes
6969
"""Split Factory/Container class."""
7070

71-
def __init__( #pylint: disable=too-many-arguments
71+
def __init__( # pylint: disable=too-many-arguments
7272
self,
7373
apikey,
7474
storages,
@@ -233,7 +233,7 @@ def _wrap_impression_listener(listener, metadata):
233233
return None
234234

235235

236-
def _build_in_memory_factory(api_key, config, sdk_url=None, events_url=None): #pylint: disable=too-many-locals
236+
def _build_in_memory_factory(api_key, config, sdk_url=None, events_url=None): # pylint: disable=too-many-locals
237237
"""Build and return a split factory tailored to the supplied config."""
238238
if not input_validator.validate_factory_instantiation(api_key):
239239
return None
@@ -314,6 +314,9 @@ def _build_in_memory_factory(api_key, config, sdk_url=None, events_url=None): #
314314
tasks['events'].start()
315315
tasks['telemetry'].start()
316316

317+
storages['events'].set_queue_full_hook(tasks['events'].flush)
318+
storages['impressions'].set_queue_full_hook(tasks['impressions'].flush)
319+
317320
def split_ready_task():
318321
"""Wait for splits to be ready and start fetching segments."""
319322
splits_ready_flag.wait()

splitio/client/input_validator.py

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
_LOGGER = logging.getLogger(__name__)
1919
MAX_LENGTH = 250
2020
EVENT_TYPE_PATTERN = r'^[a-zA-Z0-9][-_.:a-zA-Z0-9]{0,79}$'
21+
MAX_PROPERTIES_LENGTH_BYTES = 32768
2122

2223

2324
def _get_first_split_sdk_call():
@@ -33,9 +34,10 @@ def _get_first_split_sdk_call():
3334
if calls:
3435
return calls[-1]
3536
return unknown_method
36-
except Exception: #pylint: disable=broad-except
37+
except Exception: # pylint: disable=broad-except
3738
return unknown_method
3839

40+
3941
def _check_not_null(value, name, operation):
4042
"""
4143
Check if value is null.
@@ -390,7 +392,7 @@ def validate_manager_feature_name(feature_name, should_validate_existance, split
390392
return feature_name
391393

392394

393-
def validate_features_get_treatments(features, should_validate_existance=False, split_storage=None): #pylint: disable=invalid-name
395+
def validate_features_get_treatments(features, should_validate_existance=False, split_storage=None): # pylint: disable=invalid-name
394396
"""
395397
Check if features is valid for get_treatments.
396398
@@ -476,7 +478,7 @@ def validate_apikey_type(segment_api):
476478
"""
477479
api_messages_filter = _ApiLogFilter()
478480
try:
479-
segment_api._logger.addFilter(api_messages_filter) #pylint: disable=protected-access
481+
segment_api._logger.addFilter(api_messages_filter) # pylint: disable=protected-access
480482
segment_api.fetch_segment('__SOME_INVALID_SEGMENT__', -1)
481483
except APIException as exc:
482484
if exc.status_code == 403:
@@ -485,7 +487,7 @@ def validate_apikey_type(segment_api):
485487
+ 'console that is of type sdk')
486488
return False
487489
finally:
488-
segment_api._logger.removeFilter(api_messages_filter) #pylint: disable=protected-access
490+
segment_api._logger.removeFilter(api_messages_filter) # pylint: disable=protected-access
489491

490492
# True doesn't mean that the APIKEY is right, only that it's not of type "browser"
491493
return True
@@ -497,10 +499,6 @@ def validate_factory_instantiation(apikey):
497499
498500
:param apikey: str
499501
:type apikey: str
500-
:param config: dict
501-
:type config: dict
502-
:param segment_api: Segment API client
503-
:type segment_api: splitio.api.segments.SegmentsAPI
504502
:return: bool
505503
:rtype: True|False
506504
"""
@@ -511,3 +509,56 @@ def validate_factory_instantiation(apikey):
511509
(not _check_string_not_empty(apikey, 'apikey', 'factory_instantiation')):
512510
return False
513511
return True
512+
513+
514+
def valid_properties(properties):
515+
"""
516+
Check if properties is a valid dict and returns the properties
517+
that will be sent to the track method, avoiding unexpected types.
518+
519+
:param properties: dict
520+
:type properties: dict
521+
:return: tuple
522+
:rtype: (bool,dict,int)
523+
"""
524+
size = 1024 # We assume 1kb events without properties (750 bytes avg measured)
525+
526+
if properties is None:
527+
return True, None, size
528+
if not isinstance(properties, dict):
529+
_LOGGER.error('track: properties must be of type dictionary.')
530+
return False, None, 0
531+
532+
valid_properties = dict()
533+
534+
for property, element in six.iteritems(properties):
535+
if not isinstance(property, six.string_types): # Exclude property if is not string
536+
continue
537+
538+
valid_properties[property] = None
539+
size += len(property)
540+
541+
if element is None:
542+
continue
543+
544+
if not isinstance(element, six.string_types) and not isinstance(element, Number) \
545+
and not isinstance(element, bool):
546+
_LOGGER.warning('Property %s is of invalid type. Setting value to None', element)
547+
element = None
548+
549+
valid_properties[property] = element
550+
551+
if isinstance(element, six.string_types):
552+
size += len(element)
553+
554+
if size > MAX_PROPERTIES_LENGTH_BYTES:
555+
_LOGGER.error(
556+
'The maximum size allowed for the properties is 32768 bytes. ' +
557+
'Current one is ' + str(size) + ' bytes. Event not queued'
558+
)
559+
return False, None, size
560+
561+
if len(valid_properties.keys()) > 300:
562+
_LOGGER.warning('Event has more than 300 properties. Some of them will be trimmed' +
563+
' when processed')
564+
return True, valid_properties if len(valid_properties) else None, size

splitio/engine/evaluator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
CONTROL = 'control'
88

99

10-
class Evaluator(object): #pylint: disable=too-few-public-methods
10+
class Evaluator(object): # pylint: disable=too-few-public-methods
1111
"""Split Evaluator class."""
1212

1313
def __init__(self, split_storage, segment_storage, splitter):

splitio/models/events.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,10 @@
1414
'event_type_id',
1515
'value',
1616
'timestamp',
17+
'properties',
18+
])
19+
20+
EventWrapper = namedtuple('EventWrapper', [
21+
'event',
22+
'size',
1723
])

0 commit comments

Comments
 (0)