Skip to content

Commit 19f27b0

Browse files
authored
Merge pull request #142 from splitio/feature/TrackProperties
[SDKS-705]: Add properties to track method
2 parents cd2f4b2 + c118d1e commit 19f27b0

File tree

20 files changed

+341
-115
lines changed

20 files changed

+341
-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):
@@ -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,21 @@ 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, size = 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
)
353-
return self._events_storage.put([event])
357+
return self._events_storage.put([EventWrapper(
358+
event=event,
359+
size=size,
360+
)])

splitio/client/factory.py

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

18-
#Storage
18+
# Storage
1919
from splitio.storage.inmemmory import InMemorySplitStorage, InMemorySegmentStorage, \
2020
InMemoryImpressionStorage, InMemoryEventStorage, InMemoryTelemetryStorage
2121
from splitio.storage.adapters import redis
@@ -59,10 +59,10 @@ class TimeoutException(Exception):
5959
pass
6060

6161

62-
class SplitFactory(object): #pylint: disable=too-many-instance-attributes
62+
class SplitFactory(object): # pylint: disable=too-many-instance-attributes
6363
"""Split Factory/Container class."""
6464

65-
def __init__( #pylint: disable=too-many-arguments
65+
def __init__( # pylint: disable=too-many-arguments
6666
self,
6767
storages,
6868
labels_enabled,
@@ -223,7 +223,7 @@ def _wrap_impression_listener(listener, metadata):
223223
return None
224224

225225

226-
def _build_in_memory_factory(api_key, config, sdk_url=None, events_url=None): #pylint: disable=too-many-locals
226+
def _build_in_memory_factory(api_key, config, sdk_url=None, events_url=None): # pylint: disable=too-many-locals
227227
"""Build and return a split factory tailored to the supplied config."""
228228
if not input_validator.validate_factory_instantiation(api_key):
229229
return None
@@ -304,6 +304,9 @@ def _build_in_memory_factory(api_key, config, sdk_url=None, events_url=None): #
304304
tasks['events'].start()
305305
tasks['telemetry'].start()
306306

307+
storages['events'].set_queue_full_hook(tasks['events'].flush)
308+
storages['impressions'].set_queue_full_hook(tasks['impressions'].flush)
309+
307310
def split_ready_task():
308311
"""Wait for splits to be ready and start fetching segments."""
309312
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.
@@ -358,7 +360,7 @@ def validate_manager_feature_name(feature_name):
358360
return feature_name
359361

360362

361-
def validate_features_get_treatments(features): #pylint: disable=invalid-name
363+
def validate_features_get_treatments(features): # pylint: disable=invalid-name
362364
"""
363365
Check if features is valid for get_treatments.
364366
@@ -432,7 +434,7 @@ def validate_apikey_type(segment_api):
432434
"""
433435
api_messages_filter = _ApiLogFilter()
434436
try:
435-
segment_api._logger.addFilter(api_messages_filter) #pylint: disable=protected-access
437+
segment_api._logger.addFilter(api_messages_filter) # pylint: disable=protected-access
436438
segment_api.fetch_segment('__SOME_INVALID_SEGMENT__', -1)
437439
except APIException as exc:
438440
if exc.status_code == 403:
@@ -441,7 +443,7 @@ def validate_apikey_type(segment_api):
441443
+ 'console that is of type sdk')
442444
return False
443445
finally:
444-
segment_api._logger.removeFilter(api_messages_filter) #pylint: disable=protected-access
446+
segment_api._logger.removeFilter(api_messages_filter) # pylint: disable=protected-access
445447

446448
# True doesn't mean that the APIKEY is right, only that it's not of type "browser"
447449
return True
@@ -453,10 +455,6 @@ def validate_factory_instantiation(apikey):
453455
454456
:param apikey: str
455457
: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
460458
:return: bool
461459
:rtype: True|False
462460
"""
@@ -467,3 +465,56 @@ def validate_factory_instantiation(apikey):
467465
(not _check_string_not_empty(apikey, 'apikey', 'factory_instantiation')):
468466
return False
469467
return True
468+
469+
470+
def valid_properties(properties):
471+
"""
472+
Check if properties is a valid dict and returns the properties
473+
that will be sent to the track method, avoiding unexpected types.
474+
475+
:param properties: dict
476+
:type properties: dict
477+
:return: tuple
478+
:rtype: (bool,dict,int)
479+
"""
480+
size = 1024 # We assume 1kb events without properties (750 bytes avg measured)
481+
482+
if properties is None:
483+
return True, None, size
484+
if not isinstance(properties, dict):
485+
_LOGGER.error('track: properties must be of type dictionary.')
486+
return False, None, 0
487+
488+
valid_properties = dict()
489+
490+
for property, element in six.iteritems(properties):
491+
if not isinstance(property, six.string_types): # Exclude property if is not string
492+
continue
493+
494+
valid_properties[property] = None
495+
size += len(property)
496+
497+
if element is None:
498+
continue
499+
500+
if not isinstance(element, six.string_types) and not isinstance(element, Number) \
501+
and not isinstance(element, bool):
502+
_LOGGER.warning('Property %s is of invalid type. Setting value to None', element)
503+
element = None
504+
505+
valid_properties[property] = element
506+
507+
if isinstance(element, six.string_types):
508+
size += len(element)
509+
510+
if size > MAX_PROPERTIES_LENGTH_BYTES:
511+
_LOGGER.error(
512+
'The maximum size allowed for the properties is 32768 bytes. ' +
513+
'Current one is ' + str(size) + ' bytes. Event not queued'
514+
)
515+
return False, None, size
516+
517+
if len(valid_properties.keys()) > 300:
518+
_LOGGER.warning('Event has more than 300 properties. Some of them will be trimmed' +
519+
' when processed')
520+
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)