Skip to content

Commit 27932e7

Browse files
Merge pull request #870 from splitio/rb_segments_baseline
Rule-based segments support
2 parents 42d839a + 44ea95c commit 27932e7

File tree

92 files changed

+4746
-4024
lines changed

Some content is hidden

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

92 files changed

+4746
-4024
lines changed

CHANGES.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
11.4.0 (May 28, 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+
- Updated @splitsoftware/splitio-commons package to version 2.4.0.
5+
16
11.3.0 (May 16, 2025)
27
- Updated @splitsoftware/splitio-commons package to version 2.3.0, which optimizes the Redis storage to:
38
- Avoid lazy require of the `ioredis` dependency when the SDK is initialized, and

karma/config.debug.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33
const merge = require('lodash/merge');
44

55
module.exports = merge({}, require('./config'), {
6+
customLaunchers: {
7+
ChromeNoSandbox: {
8+
base: 'Chrome',
9+
flags: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']
10+
}
11+
},
612
browsers: [
7-
'Chrome'
13+
'ChromeNoSandbox'
814
],
915
webpack: {
1016
mode: 'development'

package-lock.json

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@splitsoftware/splitio",
3-
"version": "11.3.0",
3+
"version": "11.4.0",
44
"description": "Split SDK",
55
"files": [
66
"README.md",
@@ -38,7 +38,7 @@
3838
"node": ">=14.0.0"
3939
},
4040
"dependencies": {
41-
"@splitsoftware/splitio-commons": "2.3.0",
41+
"@splitsoftware/splitio-commons": "2.4.0",
4242
"bloom-filters": "^3.0.4",
4343
"ioredis": "^4.28.0",
4444
"js-yaml": "^3.13.1",

src/__tests__/browserSuites/evaluations-semver.spec.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ const config = {
2525

2626
export default async function (fetchMock, assert) {
2727

28-
fetchMock.getOnce(config.urls.sdk + '/splitChanges?s=1.2&since=-1', { status: 200, body: splitChangesMock1 });
29-
fetchMock.getOnce(config.urls.sdk + '/splitChanges?s=1.2&since=1675259356568', { status: 200, body: { splits: [], since: 1675259356568, till: 1675259356568 } });
28+
fetchMock.getOnce(config.urls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1', { status: 200, body: splitChangesMock1 });
3029
fetchMock.getOnce(config.urls.sdk + '/memberships/emi%40split.io', { status: 200, body: { ms: {} } });
3130
fetchMock.getOnce(config.urls.sdk + '/memberships/2nd', { status: 200, body: { ms: {} } });
3231

src/__tests__/browserSuites/evaluations.spec.js

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,41 @@ export default function (config, fetchMock, assert) {
362362

363363
};
364364

365+
const evaluationsWithRuleBasedSegmentsAndPrerequisites = async (splitio) => {
366+
fetchMock.getOnce('https://sdk.split.io/api/memberships/emi%40split.io', { status: 200, body: { ms: { k: [{ n: 'segment_excluded_by_rbs' }] } } });
367+
fetchMock.getOnce('https://sdk.split.io/api/memberships/mauro%40split.io', { status: 200, body: { ms: {} } });
368+
fetchMock.getOnce('https://sdk.split.io/api/memberships/bilal%40split.io', { status: 200, body: { ms: {} } });
369+
fetchMock.getOnce('https://sdk.split.io/api/memberships/other_key', { status: 200, body: { ms: {} } });
370+
371+
const client1 = splitio.client('emi@split.io');
372+
await client1.ready();
373+
assert.equal(client1.getTreatment('rbs_test_flag'), 'v2', 'key in excluded segment');
374+
assert.equal(client1.getTreatment('rbs_test_flag_negated'), 'v1', 'key in excluded segment');
375+
assert.equal(client1.getTreatment('always_on_if_prerequisite'), 'off', 'prerequisite not satisfied (key in excluded segment)');
376+
await client1.destroy();
377+
378+
const client2 = splitio.client('mauro@split.io');
379+
await client2.ready();
380+
assert.equal(client2.getTreatment('rbs_test_flag'), 'v2', 'excluded key');
381+
assert.equal(client2.getTreatment('rbs_test_flag_negated'), 'v1', 'excluded key');
382+
assert.equal(client2.getTreatment('always_on_if_prerequisite'), 'off', 'prerequisite not satisfied (excluded key)');
383+
await client2.destroy();
384+
385+
const client3 = splitio.client('bilal@split.io');
386+
await client3.ready();
387+
assert.equal(client3.getTreatment('rbs_test_flag'), 'v1', 'key satisfies the rbs condition');
388+
assert.equal(client3.getTreatment('rbs_test_flag_negated'), 'v2', 'key satisfies the rbs condition');
389+
assert.equal(client3.getTreatment('always_on_if_prerequisite'), 'on', 'prerequisite satisfied (key satisfies the rbs condition)');
390+
await client3.destroy();
391+
392+
const client4 = splitio.client('other_key');
393+
await client4.ready();
394+
assert.equal(client4.getTreatment('rbs_test_flag'), 'v2', 'key not in segment');
395+
assert.equal(client4.getTreatment('rbs_test_flag_negated'), 'v1', 'key not in segment');
396+
assert.equal(client4.getTreatment('always_on_if_prerequisite'), 'off', 'prerequisite not satisfied (key not in segment)');
397+
await client4.destroy();
398+
};
399+
365400
for (i; i < SDK_INSTANCES_TO_TEST; i++) {
366401
let splitio = SplitFactory(config);
367402

@@ -376,13 +411,16 @@ export default function (config, fetchMock, assert) {
376411
getTreatmentsTests(client);
377412
getTreatmentsWithConfigTests(client);
378413
getTreatmentsWithInMemoryAttributes(client);
379-
clientTABucket1.destroy();
380-
client.destroy();
381-
tested++;
382414

383-
if (tested === SDK_INSTANCES_TO_TEST) {
384-
assert.end();
385-
}
415+
evaluationsWithRuleBasedSegmentsAndPrerequisites(splitio).then(() => {
416+
clientTABucket1.destroy();
417+
client.destroy();
418+
tested++;
419+
420+
if (tested === SDK_INSTANCES_TO_TEST) {
421+
assert.end();
422+
}
423+
});
386424
});
387425
}
388426
}

src/__tests__/browserSuites/fetch-specific-splits.spec.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ export function fetchSpecificSplits(fetchMock, assert) {
2525
const queryString = queryStrings[i] || '';
2626
let factory;
2727

28-
fetchMock.getOnce(urls.sdk + '/splitChanges?s=1.2&since=-1' + queryString, { status: 200, body: { splits: [], since: -1, till: 1457552620999 } });
29-
fetchMock.getOnce(urls.sdk + '/splitChanges?s=1.2&since=1457552620999' + queryString, { status: 200, body: { splits: [], since: 1457552620999, till: 1457552620999 } });
30-
fetchMock.getOnce(urls.sdk + '/splitChanges?s=1.2&since=1457552620999' + queryString, function () {
28+
fetchMock.getOnce(urls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1' + queryString, { status: 200, body: { ff: { d: [], s: -1, t: 1457552620999 } } });
29+
fetchMock.getOnce(urls.sdk + '/splitChanges?s=1.3&since=1457552620999&rbSince=-1' + queryString, { status: 200, body: { ff: { d: [], s: 1457552620999, t: 1457552620999 } } });
30+
fetchMock.getOnce(urls.sdk + '/splitChanges?s=1.3&since=1457552620999&rbSince=-1' + queryString, function () {
3131
factory.client().destroy().then(() => {
3232
assert.pass(`splitFilters #${i}`);
3333
});
34-
return { status: 200, body: { splits: [], since: 1457552620999, till: 1457552620999 } };
34+
return { status: 200, body: { ff: { d: [], s: 1457552620999, t: 1457552620999 } } };
3535
});
3636
fetchMock.get(urls.sdk + '/memberships/nicolas%40split.io', { status: 200, body: { 'ms': {} } });
3737

@@ -70,8 +70,8 @@ export function fetchSpecificSplitsForFlagSets(fetchMock, assert) {
7070
const queryString = '&sets=4_valid,set_2,set_3,set_ww,set_x';
7171
fetchMock.get(baseUrls.sdk + '/memberships/nicolas%40split.io', { status: 200, body: { 'ms': {} } });
7272

73-
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.2&since=-1' + queryString, { status: 200, body: { splits: [], since: 1457552620999, till: 1457552620999 }});
74-
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.2&since=1457552620999' + queryString, async function () {
73+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1' + queryString, { status: 200, body: { ff: { d: [], s: 1457552620999, t: 1457552620999 } }});
74+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1457552620999&rbSince=-1' + queryString, async function () {
7575
t.pass('flag set query correctly formed');
7676
t.true(logSpy.calledWithExactly('[WARN] splitio => settings: bySet filter value "set_x " has extra whitespace, trimming.'));
7777
t.true(logSpy.calledWithExactly('[WARN] splitio => settings: you passed invalid+, flag set must adhere to the regular expressions /^[a-z0-9][_a-z0-9]{0,49}$/. This means a flag set must start with a letter or number, be in lowercase, alphanumeric and have a max length of 50 characters. invalid+ was discarded.'));

src/__tests__/browserSuites/flag-sets.spec.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ export default function flagSets(fetchMock, t) {
2424
let manager;
2525

2626
// Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3
27-
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.2&since=-1&sets=set_1,set_2', function () {
27+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1&sets=set_1,set_2', function () {
2828
return { status: 200, body: splitChange2};
2929
});
3030

3131
// Receive split change with 1 split belonging to set_1 only
32-
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.2&since=1602796638344&sets=set_1,set_2', function () {
32+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602796638344&rbSince=-1&sets=set_1,set_2', function () {
3333
// stored feature flags before update
3434
const storedFlags = manager.splits();
3535
assert.true(storedFlags.length === 1, 'only one feature flag should be added');
@@ -41,7 +41,7 @@ export default function flagSets(fetchMock, t) {
4141
});
4242

4343
// Receive split change with 1 split belonging to set_3 only
44-
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.2&since=1602797638344&sets=set_1,set_2', function () {
44+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602797638344&rbSince=-1&sets=set_1,set_2', function () {
4545
// stored feature flags before update
4646
const storedFlags = manager.splits();
4747
assert.true(storedFlags.length === 1);
@@ -52,7 +52,7 @@ export default function flagSets(fetchMock, t) {
5252
return { status: 200, body: splitChange0};
5353
});
5454

55-
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.2&since=1602798638344&sets=set_1,set_2', async function () {
55+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602798638344&rbSince=-1&sets=set_1,set_2', async function () {
5656
// stored feature flags before update
5757
const storedFlags = manager.splits();
5858
assert.true(storedFlags.length === 0, 'the feature flag should be removed');
@@ -75,12 +75,12 @@ export default function flagSets(fetchMock, t) {
7575
let manager;
7676

7777
// Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3
78-
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.2&since=-1', function () {
78+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1', function () {
7979
return { status: 200, body: splitChange2};
8080
});
8181

8282
// Receive split change with 1 split belonging to set_1 only
83-
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.2&since=1602796638344', function () {
83+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602796638344&rbSince=-1', function () {
8484
// stored feature flags before update
8585
const storedFlags = manager.splits();
8686
assert.true(storedFlags.length === 2, 'every feature flag should be added');
@@ -94,7 +94,7 @@ export default function flagSets(fetchMock, t) {
9494
});
9595

9696
// Receive split change with 1 split belonging to set_3 only
97-
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.2&since=1602797638344', function () {
97+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602797638344&rbSince=-1', function () {
9898
// stored feature flags before update
9999
const storedFlags = manager.splits();
100100
assert.true(storedFlags.length === 2);
@@ -107,7 +107,7 @@ export default function flagSets(fetchMock, t) {
107107
return { status: 200, body: splitChange0};
108108
});
109109

110-
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.2&since=1602798638344', async function () {
110+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602798638344&rbSince=-1', async function () {
111111
// stored feature flags before update
112112
const storedFlags = manager.splits();
113113
assert.true(storedFlags.length === 2);
@@ -137,11 +137,11 @@ export default function flagSets(fetchMock, t) {
137137

138138
fetchMock.get(baseUrls.sdk + '/memberships/nicolas%40split.io', { status: 200, body: { 'ms': {} } });
139139
// Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3
140-
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.2&since=-1&sets=set_1', function () {
140+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1&sets=set_1', function () {
141141
return { status: 200, body: splitChange2};
142142
});
143143

144-
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.2&since=1602796638344&sets=set_1', async function () {
144+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602796638344&rbSince=-1&sets=set_1', async function () {
145145
// stored feature flags before update
146146
assert.deepEqual(client.getTreatmentsByFlagSet('set_1'), {workm: 'on'}, 'only the flag in set_1 can be evaluated');
147147
assert.deepEqual(client.getTreatmentsByFlagSet('set_2'), {}, 'only the flag in set_1 can be evaluated');
@@ -174,11 +174,11 @@ export default function flagSets(fetchMock, t) {
174174

175175
fetchMock.get(baseUrls.sdk + '/memberships/nicolas%40split.io', { status: 200, body: { 'ms': {} } });
176176
// Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3
177-
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.2&since=-1', function () {
177+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1', function () {
178178
return { status: 200, body: splitChange2};
179179
});
180180

181-
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.2&since=1602796638344', async function () {
181+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602796638344&rbSince=-1', async function () {
182182
// stored feature flags before update
183183
assert.deepEqual(client.getTreatmentsByFlagSet('set_1'), {workm: 'on'}, 'all flags can be evaluated');
184184
assert.deepEqual(client.getTreatmentsByFlagSet('set_2'), {workm: 'on'}, 'all flags can be evaluated');

src/__tests__/browserSuites/ignore-ip-addresses-setting.spec.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,7 @@ export default function (fetchMock, assert) {
101101

102102
// Mock GET endpoints before creating the client
103103
const settings = settingsFactory(config);
104-
fetchMock.getOnce(url(settings, '/splitChanges?s=1.2&since=-1'), { status: 200, body: splitChangesMock1 });
105-
fetchMock.getOnce(url(settings, '/splitChanges?s=1.2&since=1457552620999'), { status: 200, body: { splits: [], since: 1457552620999, till: 1457552620999 } });
104+
fetchMock.getOnce(url(settings, '/splitChanges?s=1.3&since=-1&rbSince=-1'), { status: 200, body: splitChangesMock1 });
106105
fetchMock.getOnce(url(settings, `/memberships/${encodeURIComponent(config.core.key)}`), { status: 200, body: { ms: {} } });
107106

108107
// Init Split client

src/__tests__/browserSuites/impressions.debug.spec.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { SplitFactory } from '../../';
22
import { settingsFactory } from '../../settings';
33
import splitChangesMock1 from '../mocks/splitchanges.since.-1.json';
4-
import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json';
54
import membershipsFacundo from '../mocks/memberships.facundo@split.io.json';
65
import { DEBUG } from '@splitsoftware/splitio-commons/src/utils/constants';
76
import { truncateTimeFrame } from '@splitsoftware/splitio-commons/src/utils/time';
@@ -24,8 +23,7 @@ let truncatedTimeFrame;
2423

2524
export default function (fetchMock, assert) {
2625
// Mocking this specific route to make sure we only get the items we want to test from the handlers.
27-
fetchMock.getOnce(url(settings, '/splitChanges?s=1.2&since=-1'), { status: 200, body: splitChangesMock1 });
28-
fetchMock.get(url(settings, '/splitChanges?s=1.2&since=1457552620999'), { status: 200, body: splitChangesMock2 });
26+
fetchMock.getOnce(url(settings, '/splitChanges?s=1.3&since=-1&rbSince=-1'), { status: 200, body: splitChangesMock1 });
2927
fetchMock.get(url(settings, '/memberships/facundo%40split.io'), { status: 200, body: membershipsFacundo });
3028

3129
const splitio = SplitFactory({

src/__tests__/browserSuites/impressions.none.spec.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { SplitFactory } from '../..';
22
import { settingsFactory } from '../../settings/node';
33
import splitChangesMock1 from '../mocks/splitchanges.since.-1.json';
4-
import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json';
54
import membershipsFacundo from '../mocks/memberships.facundo@split.io.json';
65
import { NONE } from '@splitsoftware/splitio-commons/src/utils/constants';
76
import { truncateTimeFrame } from '@splitsoftware/splitio-commons/src/utils/time';
@@ -41,8 +40,7 @@ const config = {
4140

4241
export default async function (fetchMock, assert) {
4342
// Mocking this specific route to make sure we only get the items we want to test from the handlers.
44-
fetchMock.getOnce(url(settings, '/splitChanges?s=1.2&since=-1'), { status: 200, body: splitChangesMock1 });
45-
fetchMock.get(url(settings, '/splitChanges?s=1.2&since=1457552620999'), { status: 200, body: splitChangesMock2 });
43+
fetchMock.getOnce(url(settings, '/splitChanges?s=1.3&since=-1&rbSince=-1'), { status: 200, body: splitChangesMock1 });
4644
fetchMock.get(url(settings, '/memberships/facundo%40split.io'), { status: 200, body: membershipsFacundo });
4745
fetchMock.get(url(settings, '/memberships/emma%40split.io'), { status: 200, body: membershipsFacundo });
4846

src/__tests__/browserSuites/impressions.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ let truncatedTimeFrame;
2424

2525
export default function (fetchMock, assert) {
2626
// Mocking this specific route to make sure we only get the items we want to test from the handlers.
27-
fetchMock.getOnce(url(settings, '/splitChanges?s=1.2&since=-1'), { status: 200, body: splitChangesMock1 });
28-
fetchMock.get(url(settings, '/splitChanges?s=1.2&since=1457552620999'), { status: 200, body: splitChangesMock2 });
27+
fetchMock.getOnce(url(settings, '/splitChanges?s=1.3&since=-1&rbSince=-1'), { status: 200, body: splitChangesMock1 });
28+
fetchMock.get(url(settings, '/splitChanges?s=1.3&since=1457552620999&rbSince=100'), { status: 200, body: splitChangesMock2 });
2929
fetchMock.get(url(settings, '/memberships/facundo%40split.io'), { status: 200, body: membershipsFacundo });
3030

3131
const splitio = SplitFactory({

0 commit comments

Comments
 (0)