Skip to content

Release 10.4.0 #591

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Aug 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
49bd4a1
Updated validation and model
chillaq Jun 20, 2025
e20e102
Updated strategies
chillaq Jun 20, 2025
35475f5
Updated tests
chillaq Jun 20, 2025
529c177
Updated client
chillaq Jun 20, 2025
3055fb9
updated tests
chillaq Jun 23, 2025
12ec9ae
Updated integration tests
chillaq Jun 27, 2025
6b217e7
Merge pull request #584 from splitio/FME-4287-imp-prop-validator
chillaq Jun 30, 2025
a32a289
Merge pull request #585 from splitio/FME-4288-imp-prop-client
chillaq Jun 30, 2025
24b63d9
Merge pull request #586 from splitio/FME-4286-imp-prop-integration
chillaq Jun 30, 2025
bac9e80
Removed properties if none in sender
chillaq Jul 2, 2025
69a39b8
polish
chillaq Jul 2, 2025
bd7357a
Merge pull request #587 from splitio/feature/impressions-properties
chillaq Jul 2, 2025
81780a4
Added evaluation options
chillaq Jul 8, 2025
90ef85f
Added EvaluationOption tuple class
chillaq Jul 9, 2025
1ebc3f1
polish
chillaq Jul 9, 2025
bedcbce
Update splitio/client/input_validator.py
chillaq Jul 10, 2025
4f85231
Update tests/client/test_client.py
chillaq Jul 10, 2025
72d2e6a
Update tests/client/test_client.py
chillaq Jul 10, 2025
b3c4cf6
Merge pull request #588 from splitio/imp-prop-evaluation-options
chillaq Jul 10, 2025
e589d66
Updated version and changes
chillaq Jul 31, 2025
bc65da6
fixed changes
chillaq Aug 1, 2025
2512740
Merge pull request #590 from splitio/prepare-release-10.4.0
chillaq Aug 1, 2025
9699a43
Added prevention for telemetry post if counters are empty
chillaq Aug 4, 2025
3b8283d
Merge branch 'development' into prepare-release-10.4.0
chillaq Aug 4, 2025
9180c03
Merge pull request #592 from splitio/prepare-release-10.4.0
chillaq Aug 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
10.4.0 (Aug 4, 2025)
- Added a new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impressions sent to Split backend. Read more in our docs.

10.3.0 (Jun 17, 2025)
- Added support for rule-based segments. These segments determine membership at runtime by evaluating their configured rules against the user attributes provided to the SDK.
- Added support for feature flag prerequisites. This allows customers to define dependency conditions between flags, which are evaluated before any allowlists or targeting rules.
Expand Down
34 changes: 25 additions & 9 deletions splitio/api/impressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +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
}
ImpressionsAPIBase._filter_out_null_prop(impression)
for impression in imps
]
}
Expand All @@ -48,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):
"""
Expand Down
273 changes: 175 additions & 98 deletions splitio/client/client.py

Large diffs are not rendered by default.

35 changes: 25 additions & 10 deletions splitio/client/input_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import inspect

from splitio.client.key import Key
from splitio.client import client
from splitio.engine.evaluator import CONTROL


Expand Down Expand Up @@ -538,6 +539,15 @@ def validate_attributes(attributes, method_name):

return True

def validate_evaluation_options(evaluation_options, method_name):
if evaluation_options == None:
return None

if not isinstance(evaluation_options, client.EvaluationOptions):
_LOGGER.error("%s: evaluation options should be an instance of EvaluationOptions. Setting its value to None.", method_name)
return None

return evaluation_options

class _ApiLogFilter(logging.Filter): # pylint: disable=too-few-public-methods
def filter(self, record):
Expand All @@ -564,7 +574,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.
Expand All @@ -580,7 +590,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()
Expand All @@ -595,9 +605,8 @@ def valid_properties(properties):
if element is None:
continue

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)
if not _check_element_type(element):
_LOGGER.warning('%s: Property %s is of invalid type. Setting value to None', source, element)
element = None

valid_properties[property] = element
Expand All @@ -607,16 +616,22 @@ 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 _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
Expand Down
16 changes: 16 additions & 0 deletions splitio/engine/impressions/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ async def record_unique_keys(self, uniques):
:param uniques: unique keys disctionary
:type uniques: Dictionary {'feature_flag1': set(), 'feature_flag2': set(), .. }
"""
if len(uniques) == 0:
return

await self._telemtry_http_client.record_unique_keys({'keys': self._uniques_formatter(uniques)})


Expand Down Expand Up @@ -184,6 +187,9 @@ async def record_unique_keys(self, uniques):
:param uniques: unique keys disctionary
:type uniques: Dictionary {'feature_flag1': set(), 'feature_flag2': set(), .. }
"""
if len(uniques) == 0:
return True

bulk_mtks = _uniques_formatter(uniques)
try:
inserted = await self._redis_client.rpush(_MTK_QUEUE_KEY, *bulk_mtks)
Expand All @@ -202,6 +208,9 @@ async def flush_counters(self, to_send):
:param to_send: unique keys disctionary
:type to_send: Dictionary {'feature_flag1': set(), 'feature_flag2': set(), .. }
"""
if len(to_send) == 0:
return True

try:
resulted = 0
counted = 0
Expand Down Expand Up @@ -277,6 +286,7 @@ def flush_counters(self, to_send):
"""
if len(to_send) == 0:
return

try:
resulted = 0
for pf_count in to_send:
Expand Down Expand Up @@ -325,6 +335,9 @@ async def record_unique_keys(self, uniques):
:param uniques: unique keys disctionary
:type uniques: Dictionary {'feature_flag1': set(), 'feature_flag2': set(), .. }
"""
if len(uniques) == 0:
return True

bulk_mtks = _uniques_formatter(uniques)
try:
inserted = await self._adapter_client.push_items(self._prefix + _MTK_QUEUE_KEY, *bulk_mtks)
Expand All @@ -343,6 +356,9 @@ async def flush_counters(self, to_send):
:param to_send: unique keys disctionary
:type to_send: Dictionary {'feature_flag1': set(), 'feature_flag2': set(), .. }
"""
if len(to_send) == 0:
return True

try:
resulted = 0
for pf_count in to_send:
Expand Down
18 changes: 16 additions & 2 deletions splitio/engine/impressions/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,14 @@ 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:
imps.append((imp, attrs))
continue

imps.append((self._observer.test_and_set(imp), attrs))

return [i for i, _ in imps], imps, [], []

class StrategyNoneMode(BaseStrategy):
Expand Down Expand Up @@ -85,7 +92,14 @@ 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:
imps.append((imp, attrs))
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, []
3 changes: 2 additions & 1 deletion splitio/models/impressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
'change_number',
'bucketing_key',
'time',
'previous_time'
'previous_time',
'properties'
]
)

Expand Down
1 change: 1 addition & 0 deletions splitio/storage/pluggable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions splitio/storage/redis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion splitio/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '10.3.0'
__version__ = '10.4.0'
8 changes: 4 additions & 4 deletions tests/api/test_impressions_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
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': '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},
],
}, {
Expand Down
Loading
Loading