Skip to content

Commit 0e87c99

Browse files
authored
Merge pull request #580 from splitio/development
Release 10.3.0
2 parents fb2723a + 5328afb commit 0e87c99

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+6831
-1341
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ concurrency:
1616
jobs:
1717
test:
1818
name: Test
19-
runs-on: ubuntu-20.04
19+
runs-on: ubuntu-22.04
2020
services:
2121
redis:
2222
image: redis
@@ -35,6 +35,7 @@ jobs:
3535

3636
- name: Install dependencies
3737
run: |
38+
sudo apt update
3839
sudo apt-get install -y libkrb5-dev
3940
pip install -U setuptools pip wheel
4041
pip install -e .[cpphash,redis,uwsgi]

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
10.3.0 (Jun 17, 2025)
2+
- 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.
3+
- Added support for feature flag prerequisites. This allows customers to define dependency conditions between flags, which are evaluated before any allowlists or targeting rules.
4+
15
10.2.0 (Jan 17, 2025)
26
- Added support for the new impressions tracking toggle available on feature flags, both respecting the setting and including the new field being returned on SplitView type objects. Read more in our docs.
37

setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
'flake8',
99
'pytest==7.0.1',
1010
'pytest-mock==3.11.1',
11-
'coverage',
11+
'coverage==7.0.0',
1212
'pytest-cov==4.1.0',
1313
'importlib-metadata==6.7',
1414
'tomli==1.2.3',
@@ -17,7 +17,8 @@
1717
'pytest-asyncio==0.21.0',
1818
'aiohttp>=3.8.4',
1919
'aiofiles>=23.1.0',
20-
'requests-kerberos>=0.15.0'
20+
'requests-kerberos>=0.15.0',
21+
'urllib3==2.0.7'
2122
]
2223

2324
INSTALL_REQUIRES = [

splitio/api/client.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,25 @@ def proxy_headers(self, proxy):
9292
class HttpClientBase(object, metaclass=abc.ABCMeta):
9393
"""HttpClient wrapper template."""
9494

95+
def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, telemetry_url=None):
96+
"""
97+
Class constructor.
98+
99+
:param timeout: How many milliseconds to wait until the server responds.
100+
:type timeout: int
101+
:param sdk_url: Optional alternative sdk URL.
102+
:type sdk_url: str
103+
:param events_url: Optional alternative events URL.
104+
:type events_url: str
105+
:param auth_url: Optional alternative auth URL.
106+
:type auth_url: str
107+
:param telemetry_url: Optional alternative telemetry URL.
108+
:type telemetry_url: str
109+
"""
110+
_LOGGER.debug("Initializing httpclient")
111+
self._timeout = timeout/1000 if timeout else None # Convert ms to seconds.
112+
self._urls = _construct_urls(sdk_url, events_url, auth_url, telemetry_url)
113+
95114
@abc.abstractmethod
96115
def get(self, server, path, apikey):
97116
"""http get request"""
@@ -113,6 +132,9 @@ def set_telemetry_data(self, metric_name, telemetry_runtime_producer):
113132
self._telemetry_runtime_producer = telemetry_runtime_producer
114133
self._metric_name = metric_name
115134

135+
def is_sdk_endpoint_overridden(self):
136+
return self._urls['sdk'] != SDK_URL
137+
116138
def _get_headers(self, extra_headers, sdk_key):
117139
headers = _build_basic_headers(sdk_key)
118140
if extra_headers is not None:
@@ -154,10 +176,8 @@ def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, t
154176
:param telemetry_url: Optional alternative telemetry URL.
155177
:type telemetry_url: str
156178
"""
157-
_LOGGER.debug("Initializing httpclient")
158-
self._timeout = timeout/1000 if timeout else None # Convert ms to seconds.
159-
self._urls = _construct_urls(sdk_url, events_url, auth_url, telemetry_url)
160-
179+
HttpClientBase.__init__(self, timeout, sdk_url, events_url, auth_url, telemetry_url)
180+
161181
def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: disable=too-many-arguments
162182
"""
163183
Issue a get request.
@@ -187,7 +207,11 @@ def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint:
187207
self._record_telemetry(response.status_code, get_current_epoch_time_ms() - start)
188208
return HttpResponse(response.status_code, response.text, response.headers)
189209

190-
except Exception as exc: # pylint: disable=broad-except
210+
except requests.exceptions.ChunkedEncodingError as exc:
211+
_LOGGER.error("IncompleteRead exception detected: %s", exc)
212+
return HttpResponse(400, "", {})
213+
214+
except Exception as exc: # pylint: disable=broad-except
191215
raise HttpClientException(_EXC_MSG.format(source='request')) from exc
192216

193217
def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # pylint: disable=too-many-arguments
@@ -241,8 +265,7 @@ def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, t
241265
:param telemetry_url: Optional alternative telemetry URL.
242266
:type telemetry_url: str
243267
"""
244-
self._timeout = timeout/1000 if timeout else None # Convert ms to seconds.
245-
self._urls = _construct_urls(sdk_url, events_url, auth_url, telemetry_url)
268+
HttpClientBase.__init__(self, timeout, sdk_url, events_url, auth_url, telemetry_url)
246269
self._session = aiohttp.ClientSession()
247270

248271
async def get(self, server, path, apikey, query=None, extra_headers=None): # pylint: disable=too-many-arguments
@@ -281,6 +304,10 @@ async def get(self, server, path, apikey, query=None, extra_headers=None): # py
281304
await self._record_telemetry(response.status, get_current_epoch_time_ms() - start)
282305
return HttpResponse(response.status, body, response.headers)
283306

307+
except aiohttp.ClientPayloadError as exc:
308+
_LOGGER.error("ContentLengthError exception detected: %s", exc)
309+
return HttpResponse(400, "", {})
310+
284311
except aiohttp.ClientError as exc: # pylint: disable=broad-except
285312
raise HttpClientException(_EXC_MSG.format(source='aiohttp')) from exc
286313

splitio/api/commons.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def record_telemetry(status_code, elapsed, metric_name, telemetry_runtime_produc
5757
class FetchOptions(object):
5858
"""Fetch Options object."""
5959

60-
def __init__(self, cache_control_headers=False, change_number=None, sets=None, spec=SPEC_VERSION):
60+
def __init__(self, cache_control_headers=False, change_number=None, rbs_change_number=None, sets=None, spec=SPEC_VERSION):
6161
"""
6262
Class constructor.
6363
@@ -72,6 +72,7 @@ def __init__(self, cache_control_headers=False, change_number=None, sets=None, s
7272
"""
7373
self._cache_control_headers = cache_control_headers
7474
self._change_number = change_number
75+
self._rbs_change_number = rbs_change_number
7576
self._sets = sets
7677
self._spec = spec
7778

@@ -85,6 +86,11 @@ def change_number(self):
8586
"""Return change number."""
8687
return self._change_number
8788

89+
@property
90+
def rbs_change_number(self):
91+
"""Return change number."""
92+
return self._rbs_change_number
93+
8894
@property
8995
def sets(self):
9096
"""Return sets."""
@@ -103,14 +109,19 @@ def __eq__(self, other):
103109
if self._change_number != other._change_number:
104110
return False
105111

112+
if self._rbs_change_number != other._rbs_change_number:
113+
return False
114+
106115
if self._sets != other._sets:
107116
return False
117+
108118
if self._spec != other._spec:
109119
return False
120+
110121
return True
111122

112123

113-
def build_fetch(change_number, fetch_options, metadata):
124+
def build_fetch(change_number, fetch_options, metadata, rbs_change_number=None):
114125
"""
115126
Build fetch with new flags if that is the case.
116127
@@ -123,11 +134,16 @@ def build_fetch(change_number, fetch_options, metadata):
123134
:param metadata: Metadata Headers.
124135
:type metadata: dict
125136
137+
:param rbs_change_number: Last known timestamp of a rule based segment modification.
138+
:type rbs_change_number: int
139+
126140
:return: Objects for fetch
127141
:rtype: dict, dict
128142
"""
129143
query = {'s': fetch_options.spec} if fetch_options.spec is not None else {}
130144
query['since'] = change_number
145+
if rbs_change_number is not None:
146+
query['rbSince'] = rbs_change_number
131147
extra_headers = metadata
132148
if fetch_options is None:
133149
return query, extra_headers

splitio/api/splits.py

Lines changed: 95 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@
44
import json
55

66
from splitio.api import APIException, headers_from_metadata
7-
from splitio.api.commons import build_fetch
7+
from splitio.api.commons import build_fetch, FetchOptions
88
from splitio.api.client import HttpClientException
99
from splitio.models.telemetry import HTTPExceptionsAndLatencies
10+
from splitio.util.time import utctime_ms
11+
from splitio.spec import SPEC_VERSION
12+
from splitio.sync import util
1013

1114
_LOGGER = logging.getLogger(__name__)
15+
_SPEC_1_1 = "1.1"
16+
_PROXY_CHECK_INTERVAL_MILLISECONDS_SS = 24 * 60 * 60 * 1000
1217

13-
14-
class SplitsAPI(object): # pylint: disable=too-few-public-methods
18+
class SplitsAPIBase(object): # pylint: disable=too-few-public-methods
1519
"""Class that uses an httpClient to communicate with the splits API."""
1620

1721
def __init__(self, client, sdk_key, sdk_metadata, telemetry_runtime_producer):
@@ -30,22 +34,66 @@ def __init__(self, client, sdk_key, sdk_metadata, telemetry_runtime_producer):
3034
self._metadata = headers_from_metadata(sdk_metadata)
3135
self._telemetry_runtime_producer = telemetry_runtime_producer
3236
self._client.set_telemetry_data(HTTPExceptionsAndLatencies.SPLIT, self._telemetry_runtime_producer)
37+
self._spec_version = SPEC_VERSION
38+
self._last_proxy_check_timestamp = 0
39+
self.clear_storage = False
40+
self._old_spec_since = None
41+
42+
def _check_last_proxy_check_timestamp(self, since):
43+
if self._spec_version == _SPEC_1_1 and ((utctime_ms() - self._last_proxy_check_timestamp) >= _PROXY_CHECK_INTERVAL_MILLISECONDS_SS):
44+
_LOGGER.info("Switching to new Feature flag spec (%s) and fetching.", SPEC_VERSION);
45+
self._spec_version = SPEC_VERSION
46+
self._old_spec_since = since
47+
48+
def _check_old_spec_since(self, change_number):
49+
if self._spec_version == _SPEC_1_1 and self._old_spec_since is not None:
50+
since = self._old_spec_since
51+
self._old_spec_since = None
52+
return since
53+
return change_number
54+
55+
56+
class SplitsAPI(SplitsAPIBase): # pylint: disable=too-few-public-methods
57+
"""Class that uses an httpClient to communicate with the splits API."""
58+
59+
def __init__(self, client, sdk_key, sdk_metadata, telemetry_runtime_producer):
60+
"""
61+
Class constructor.
62+
63+
:param client: HTTP Client responsble for issuing calls to the backend.
64+
:type client: HttpClient
65+
:param sdk_key: User sdk_key token.
66+
:type sdk_key: string
67+
:param sdk_metadata: SDK version & machine name & IP.
68+
:type sdk_metadata: splitio.client.util.SdkMetadata
69+
"""
70+
SplitsAPIBase.__init__(self, client, sdk_key, sdk_metadata, telemetry_runtime_producer)
3371

34-
def fetch_splits(self, change_number, fetch_options):
72+
def fetch_splits(self, change_number, rbs_change_number, fetch_options):
3573
"""
3674
Fetch feature flags from backend.
3775
3876
:param change_number: Last known timestamp of a split modification.
3977
:type change_number: int
4078
79+
:param rbs_change_number: Last known timestamp of a rule based segment modification.
80+
:type rbs_change_number: int
81+
4182
:param fetch_options: Fetch options for getting feature flag definitions.
4283
:type fetch_options: splitio.api.commons.FetchOptions
4384
4485
:return: Json representation of a splitChanges response.
4586
:rtype: dict
4687
"""
4788
try:
48-
query, extra_headers = build_fetch(change_number, fetch_options, self._metadata)
89+
self._check_last_proxy_check_timestamp(change_number)
90+
change_number = self._check_old_spec_since(change_number)
91+
92+
if self._spec_version == _SPEC_1_1:
93+
fetch_options = FetchOptions(fetch_options.cache_control_headers, fetch_options.change_number,
94+
None, fetch_options.sets, self._spec_version)
95+
rbs_change_number = None
96+
query, extra_headers = build_fetch(change_number, fetch_options, self._metadata, rbs_change_number)
4997
response = self._client.get(
5098
'sdk',
5199
'splitChanges',
@@ -54,19 +102,32 @@ def fetch_splits(self, change_number, fetch_options):
54102
query=query,
55103
)
56104
if 200 <= response.status_code < 300:
105+
if self._spec_version == _SPEC_1_1:
106+
return util.convert_to_new_spec(json.loads(response.body))
107+
108+
self.clear_storage = self._last_proxy_check_timestamp != 0
109+
self._last_proxy_check_timestamp = 0
57110
return json.loads(response.body)
58111

59112
else:
60113
if response.status_code == 414:
61114
_LOGGER.error('Error fetching feature flags; the amount of flag sets provided are too big, causing uri length error.')
115+
116+
if self._client.is_sdk_endpoint_overridden() and response.status_code == 400 and self._spec_version == SPEC_VERSION:
117+
_LOGGER.warning('Detected proxy response error, changing spec version from %s to %s and re-fetching.', self._spec_version, _SPEC_1_1)
118+
self._spec_version = _SPEC_1_1
119+
self._last_proxy_check_timestamp = utctime_ms()
120+
return self.fetch_splits(change_number, None, FetchOptions(fetch_options.cache_control_headers, fetch_options.change_number,
121+
None, fetch_options.sets, self._spec_version))
122+
62123
raise APIException(response.body, response.status_code)
124+
63125
except HttpClientException as exc:
64126
_LOGGER.error('Error fetching feature flags because an exception was raised by the HTTPClient')
65127
_LOGGER.debug('Error: ', exc_info=True)
66128
raise APIException('Feature flags not fetched correctly.') from exc
67129

68-
69-
class SplitsAPIAsync(object): # pylint: disable=too-few-public-methods
130+
class SplitsAPIAsync(SplitsAPIBase): # pylint: disable=too-few-public-methods
70131
"""Class that uses an httpClient to communicate with the splits API."""
71132

72133
def __init__(self, client, sdk_key, sdk_metadata, telemetry_runtime_producer):
@@ -80,18 +141,17 @@ def __init__(self, client, sdk_key, sdk_metadata, telemetry_runtime_producer):
80141
:param sdk_metadata: SDK version & machine name & IP.
81142
:type sdk_metadata: splitio.client.util.SdkMetadata
82143
"""
83-
self._client = client
84-
self._sdk_key = sdk_key
85-
self._metadata = headers_from_metadata(sdk_metadata)
86-
self._telemetry_runtime_producer = telemetry_runtime_producer
87-
self._client.set_telemetry_data(HTTPExceptionsAndLatencies.SPLIT, self._telemetry_runtime_producer)
144+
SplitsAPIBase.__init__(self, client, sdk_key, sdk_metadata, telemetry_runtime_producer)
88145

89-
async def fetch_splits(self, change_number, fetch_options):
146+
async def fetch_splits(self, change_number, rbs_change_number, fetch_options):
90147
"""
91148
Fetch feature flags from backend.
92149
93150
:param change_number: Last known timestamp of a split modification.
94151
:type change_number: int
152+
153+
:param rbs_change_number: Last known timestamp of a rule based segment modification.
154+
:type rbs_change_number: int
95155
96156
:param fetch_options: Fetch options for getting feature flag definitions.
97157
:type fetch_options: splitio.api.commons.FetchOptions
@@ -100,7 +160,14 @@ async def fetch_splits(self, change_number, fetch_options):
100160
:rtype: dict
101161
"""
102162
try:
103-
query, extra_headers = build_fetch(change_number, fetch_options, self._metadata)
163+
self._check_last_proxy_check_timestamp(change_number)
164+
change_number = self._check_old_spec_since(change_number)
165+
if self._spec_version == _SPEC_1_1:
166+
fetch_options = FetchOptions(fetch_options.cache_control_headers, fetch_options.change_number,
167+
None, fetch_options.sets, self._spec_version)
168+
rbs_change_number = None
169+
170+
query, extra_headers = build_fetch(change_number, fetch_options, self._metadata, rbs_change_number)
104171
response = await self._client.get(
105172
'sdk',
106173
'splitChanges',
@@ -109,12 +176,26 @@ async def fetch_splits(self, change_number, fetch_options):
109176
query=query,
110177
)
111178
if 200 <= response.status_code < 300:
179+
if self._spec_version == _SPEC_1_1:
180+
return util.convert_to_new_spec(json.loads(response.body))
181+
182+
self.clear_storage = self._last_proxy_check_timestamp != 0
183+
self._last_proxy_check_timestamp = 0
112184
return json.loads(response.body)
113185

114186
else:
115187
if response.status_code == 414:
116188
_LOGGER.error('Error fetching feature flags; the amount of flag sets provided are too big, causing uri length error.')
189+
190+
if self._client.is_sdk_endpoint_overridden() and response.status_code == 400 and self._spec_version == SPEC_VERSION:
191+
_LOGGER.warning('Detected proxy response error, changing spec version from %s to %s and re-fetching.', self._spec_version, _SPEC_1_1)
192+
self._spec_version = _SPEC_1_1
193+
self._last_proxy_check_timestamp = utctime_ms()
194+
return await self.fetch_splits(change_number, None, FetchOptions(fetch_options.cache_control_headers, fetch_options.change_number,
195+
None, fetch_options.sets, self._spec_version))
196+
117197
raise APIException(response.body, response.status_code)
198+
118199
except HttpClientException as exc:
119200
_LOGGER.error('Error fetching feature flags because an exception was raised by the HTTPClient')
120201
_LOGGER.debug('Error: ', exc_info=True)

0 commit comments

Comments
 (0)