Skip to content

Commit b927fac

Browse files
Merge pull request #771 from splitio/sdks-7437
Flag sets
2 parents 5da6000 + 3e5741a commit b927fac

37 files changed

+2056
-187
lines changed

CHANGES.txt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
1-
10.24.0 (October XX, 2023)
1+
10.24.0 (December 4, 2023)
2+
- Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation):
3+
- Added new variations of the get treatment methods to support evaluating flags in given flag set/s.
4+
- getTreatmentsByFlagSet and getTreatmentsByFlagSets
5+
- getTreatmentsWithConfigByFlagSets and getTreatmentsWithConfigByFlagSets
6+
- Added a new optional Split Filter configuration option. This allows the SDK and Split services to only synchronize the flags in the specified flag sets, avoiding unused or unwanted flags from being synced on the SDK instance, bringing all the benefits from a reduced payload.
7+
- Note: Only applicable when the SDK is in charge of the rollout data synchronization. When not applicable, the SDK will log a warning on init.
8+
- Added `sets` property to the `SplitView` object returned by the `split` and `splits` methods of the SDK manager to expose flag sets on flag views.
29
- Added `defaultTreatment` property to the `SplitView` object returned by the `split` and `splits` methods of the SDK manager (Related to issue https://github.com/splitio/javascript-commons/issues/225).
3-
- Updated @splitsoftware/splitio-commons package to version 1.10.0 that includes vulnerability fixes, and adds the `defaultTreatment` property to the `SplitView` object.
10+
- Updated @splitsoftware/splitio-commons package to version 1.12.0 that includes vulnerability fixes, flag sets support, and other improvements.
11+
- Updated Redis adapter to handle timeouts and queueing of some missing commands: 'hincrby', 'popNRaw', and 'pipeline.exec'.
12+
- Bugfixing - Fixed manager methods in consumer modes to return results in a promise when the SDK is not operational (not ready or destroyed).
13+
- Bugfixing - Fixed SDK key validation in NodeJS to ensure the SDK_READY_TIMED_OUT event is emitted when a client-side type SDK key is provided instead of a server-side one (Related to issue https://github.com/splitio/javascript-client/issues/768).
414

515
10.23.1 (September 22, 2023)
616
- Updated @splitsoftware/splitio-commons package to version 1.9.1. This update removes the handler for 'unload' DOM events, that can prevent browsers from being able to put pages in the back/forward cache for faster back and forward loads (Related to issue https://github.com/splitio/javascript-client/issues/759).

package-lock.json

Lines changed: 15 additions & 15 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": "10.23.1",
3+
"version": "10.24.0-rc.1",
44
"description": "Split SDK",
55
"files": [
66
"README.md",
@@ -40,7 +40,7 @@
4040
"node": ">=6"
4141
},
4242
"dependencies": {
43-
"@splitsoftware/splitio-commons": "1.10.0",
43+
"@splitsoftware/splitio-commons": "1.12.1-rc.4",
4444
"@types/google.analytics": "0.0.40",
4545
"@types/ioredis": "^4.28.0",
4646
"bloom-filters": "^3.0.0",

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sinon from 'sinon';
12
import { SplitFactory } from '../../';
23
import { splitFilters, queryStrings, groupedFilters } from '../mocks/fetchSpecificSplits';
34

@@ -12,7 +13,7 @@ const baseConfig = {
1213
streamingEnabled: false,
1314
};
1415

15-
export default function fetchSpecificSplits(fetchMock, assert) {
16+
export function fetchSpecificSplits(fetchMock, assert) {
1617

1718
assert.plan(splitFilters.length);
1819

@@ -46,3 +47,40 @@ export default function fetchSpecificSplits(fetchMock, assert) {
4647

4748
}
4849
}
50+
51+
export function fetchSpecificSplitsForFlagSets(fetchMock, assert) {
52+
// Flag sets
53+
assert.test(async (t) => {
54+
55+
const splitFilters = [{ type: 'bySet', values: ['set_x ', 'set_x', 'set_3', 'set_2', 'set_3', 'set_ww', 'invalid+', '_invalid', '4_valid'] }];
56+
const baseUrls = { sdk: 'https://sdk.baseurl' };
57+
58+
const config = {
59+
...baseConfig,
60+
urls: baseUrls,
61+
debug: 'WARN',
62+
sync: {
63+
splitFilters
64+
}
65+
};
66+
67+
const logSpy = sinon.spy(console, 'log');
68+
69+
let factory;
70+
const queryString = '&sets=4_valid,set_2,set_3,set_ww,set_x';
71+
fetchMock.get(baseUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { 'mySegments': [] } });
72+
73+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1' + queryString, { status: 200, body: { splits: [], since: 1457552620999, till: 1457552620999 }});
74+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1457552620999' + queryString, async function () {
75+
t.pass('flag set query correctly formed');
76+
t.true(logSpy.calledWithExactly('[WARN] splitio => settings: bySet filter value "set_x " has extra whitespace, trimming.'));
77+
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.'));
78+
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.'));
79+
logSpy.restore();
80+
factory.client().destroy().then(() => {
81+
t.end();
82+
});
83+
});
84+
factory = SplitFactory(config);
85+
}, 'FlagSets config');
86+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { SplitFactory } from '../..';
2+
3+
import splitChange2 from '../mocks/splitchanges.since.-1.till.1602796638344.json';
4+
import splitChange1 from '../mocks/splitchanges.since.1602796638344.till.1602797638344.json';
5+
import splitChange0 from '../mocks/splitchanges.since.1602797638344.till.1602798638344.json';
6+
7+
const baseUrls = { sdk: 'https://sdk.baseurl' };
8+
9+
const baseConfig = {
10+
core: {
11+
authorizationKey: '<fake-token>',
12+
key: 'nicolas@split.io'
13+
},
14+
urls: baseUrls,
15+
scheduler: { featuresRefreshRate: 0.01 },
16+
streamingEnabled: false
17+
};
18+
19+
export default function flagSets(fetchMock, t) {
20+
fetchMock.get(baseUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { 'mySegments': [] } });
21+
22+
t.test(async (assert) => {
23+
let factory;
24+
let manager;
25+
26+
// Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3
27+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1&sets=set_1,set_2', function () {
28+
return { status: 200, body: splitChange2};
29+
});
30+
31+
// Receive split change with 1 split belonging to set_1 only
32+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602796638344&sets=set_1,set_2', function () {
33+
// stored feature flags before update
34+
const storedFlags = manager.splits();
35+
assert.true(storedFlags.length === 1, 'only one feature flag should be added');
36+
assert.true(storedFlags[0].name === 'workm');
37+
assert.deepEqual(storedFlags[0].sets, ['set_1','set_2']);
38+
39+
// send split change
40+
return { status: 200, body: splitChange1};
41+
});
42+
43+
// Receive split change with 1 split belonging to set_3 only
44+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602797638344&sets=set_1,set_2', function () {
45+
// stored feature flags before update
46+
const storedFlags = manager.splits();
47+
assert.true(storedFlags.length === 1);
48+
assert.true(storedFlags[0].name === 'workm');
49+
assert.deepEqual(storedFlags[0].sets, ['set_1'], 'the feature flag should be updated');
50+
51+
// send split change
52+
return { status: 200, body: splitChange0};
53+
});
54+
55+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602798638344&sets=set_1,set_2', async function () {
56+
// stored feature flags before update
57+
const storedFlags = manager.splits();
58+
assert.true(storedFlags.length === 0, 'the feature flag should be removed');
59+
await factory.client().destroy();
60+
assert.end();
61+
62+
return { status: 200, body: {} };
63+
});
64+
65+
// Initialize a factory with polling and sets set_1 & set_2 configured.
66+
const splitFilters = [{ type: 'bySet', values: ['set_1','set_2'] }];
67+
factory = SplitFactory({ ...baseConfig, sync: { splitFilters }});
68+
await factory.client().ready();
69+
manager = factory.manager();
70+
71+
}, 'Polling - SDK with sets configured updates flags according to sets');
72+
73+
t.test(async (assert) => {
74+
let factory;
75+
let manager;
76+
77+
// Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3
78+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1', function () {
79+
return { status: 200, body: splitChange2};
80+
});
81+
82+
// Receive split change with 1 split belonging to set_1 only
83+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602796638344', function () {
84+
// stored feature flags before update
85+
const storedFlags = manager.splits();
86+
assert.true(storedFlags.length === 2, 'every feature flag should be added');
87+
assert.true(storedFlags[0].name === 'workm');
88+
assert.true(storedFlags[1].name === 'workm_set_3');
89+
assert.deepEqual(storedFlags[0].sets, ['set_1','set_2']);
90+
assert.deepEqual(storedFlags[1].sets, ['set_3']);
91+
92+
// send split change
93+
return { status: 200, body: splitChange1};
94+
});
95+
96+
// Receive split change with 1 split belonging to set_3 only
97+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602797638344', function () {
98+
// stored feature flags before update
99+
const storedFlags = manager.splits();
100+
assert.true(storedFlags.length === 2);
101+
assert.true(storedFlags[0].name === 'workm');
102+
assert.true(storedFlags[1].name === 'workm_set_3');
103+
assert.deepEqual(storedFlags[0].sets, ['set_1'], 'the feature flag should be updated');
104+
assert.deepEqual(storedFlags[1].sets, ['set_3'], 'the feature flag should remain as it was');
105+
106+
// send split change
107+
return { status: 200, body: splitChange0};
108+
});
109+
110+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602798638344', async function () {
111+
// stored feature flags before update
112+
const storedFlags = manager.splits();
113+
assert.true(storedFlags.length === 2);
114+
assert.true(storedFlags[0].name === 'workm');
115+
assert.true(storedFlags[1].name === 'workm_set_3');
116+
assert.deepEqual(storedFlags[0].sets, ['set_3'], 'the feature flag should be updated');
117+
assert.deepEqual(storedFlags[1].sets, ['set_3'], 'the feature flag should remain as it was');
118+
await factory.client().destroy();
119+
assert.end();
120+
return { status: 200, body: {} };
121+
});
122+
123+
// Initialize a factory with polling and no sets configured.
124+
factory = SplitFactory(baseConfig);
125+
await factory.client().ready();
126+
manager = factory.manager();
127+
128+
}, 'Poling - SDK with no sets configured does not take sets into account when updating flags');
129+
130+
// EVALUATION
131+
132+
t.test(async (assert) => {
133+
fetchMock.reset();
134+
fetchMock.post('*', 200);
135+
136+
let factory, client = [];
137+
138+
fetchMock.get(baseUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { 'mySegments': [] } });
139+
// Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3
140+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1&sets=set_1', function () {
141+
return { status: 200, body: splitChange2};
142+
});
143+
144+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602796638344&sets=set_1', async function () {
145+
// stored feature flags before update
146+
assert.deepEqual(client.getTreatmentsByFlagSet('set_1'), {workm: 'on'}, 'only the flag in set_1 can be evaluated');
147+
assert.deepEqual(client.getTreatmentsByFlagSet('set_2'), {}, 'only the flag in set_1 can be evaluated');
148+
assert.deepEqual(client.getTreatmentsByFlagSet('set_3'), {}, 'only the flag in set_1 can be evaluated');
149+
assert.deepEqual(client.getTreatmentsWithConfigByFlagSet('set_1'), { workm: { treatment: 'on', config: null } }, 'only the flag in set_1 can be evaluated');
150+
assert.deepEqual(client.getTreatmentsWithConfigByFlagSet('set_2'), {}, 'only the flag in set_1 can be evaluated');
151+
assert.deepEqual(client.getTreatmentsWithConfigByFlagSet('set_3'), {}, 'only the flag in set_1 can be evaluated');
152+
assert.deepEqual(client.getTreatmentsByFlagSets(['set_1','set_2','set_3']), {workm: 'on'}, 'only the flag in set_1 can be evaluated');
153+
assert.deepEqual(client.getTreatmentsWithConfigByFlagSets(['set_1','set_2','set_3']), { workm: { treatment: 'on', config: null } }, 'only the flag in set_1 can be evaluated');
154+
await client.destroy();
155+
assert.end();
156+
157+
// send split change
158+
return { status: 200, body: splitChange1};
159+
});
160+
161+
// Initialize a factory with set_1 configured.
162+
const splitFilters = [{ type: 'bySet', values: ['set_1'] }];
163+
factory = SplitFactory({ ...baseConfig, sync: { splitFilters }});
164+
client = factory.client();
165+
await client.ready();
166+
167+
}, 'SDK with sets configured can only evaluate configured sets');
168+
169+
t.test(async (assert) => {
170+
fetchMock.reset();
171+
fetchMock.post('*', 200);
172+
173+
let factory, client = [];
174+
175+
fetchMock.get(baseUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { 'mySegments': [] } });
176+
// Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3
177+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=-1', function () {
178+
return { status: 200, body: splitChange2};
179+
});
180+
181+
fetchMock.getOnce(baseUrls.sdk + '/splitChanges?since=1602796638344', async function () {
182+
// stored feature flags before update
183+
assert.deepEqual(client.getTreatmentsByFlagSet('set_1'), {workm: 'on'}, 'all flags can be evaluated');
184+
assert.deepEqual(client.getTreatmentsByFlagSet('set_2'), {workm: 'on'}, 'all flags can be evaluated');
185+
assert.deepEqual(client.getTreatmentsByFlagSet('set_3'), { workm_set_3: 'on' }, 'all flags can be evaluated');
186+
assert.deepEqual(client.getTreatmentsWithConfigByFlagSet('set_1'), { workm: { treatment: 'on', config: null } }, 'all flags can be evaluated');
187+
assert.deepEqual(client.getTreatmentsWithConfigByFlagSet('set_2'), { workm: { treatment: 'on', config: null } }, 'all flags can be evaluated');
188+
assert.deepEqual(client.getTreatmentsWithConfigByFlagSet('set_3'), { workm_set_3: { treatment: 'on', config: null } }, 'all flags can be evaluated');
189+
assert.deepEqual(client.getTreatmentsByFlagSets(['set_1','set_2','set_3']), { workm: 'on', workm_set_3: 'on' }, 'all flags can be evaluated');
190+
assert.deepEqual(client.getTreatmentsWithConfigByFlagSets(['set_1','set_2','set_3']), { workm: { treatment: 'on', config: null }, workm_set_3: { treatment: 'on', config: null } }, 'all flags can be evaluated');
191+
await client.destroy();
192+
assert.end();
193+
194+
// send split change
195+
return { status: 200, body: splitChange1};
196+
});
197+
198+
factory = SplitFactory(baseConfig);
199+
client = factory.client();
200+
await client.ready();
201+
202+
}, 'SDK with no sets configured can evaluate any set');
203+
204+
}

src/__tests__/browserSuites/manager.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export default async function (settings, fetchMock, assert) {
4040
'changeNumber': mockSplits.splits[index].changeNumber,
4141
'treatments': map(mockSplits.splits[index].conditions[0].partitions, partition => partition.treatment),
4242
'configs': mockSplits.splits[index].configurations || {},
43+
'sets': mockSplits.splits[index].sets || [],
4344
'defaultTreatment': mockSplits.splits[index].defaultTreatment
4445
});
4546

0 commit comments

Comments
 (0)