Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 1 deletion CHANGES.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
1.6.0 (October XX, 2025)
1.6.0 (October 30, 2025)
- Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs.
- Added `client.getStatus()` method to retrieve the client readiness status properties (`isReady`, `isReadyFromCache`, etc).
- Added `client.whenReady()` and `client.whenReadyFromCache()` methods to replace the deprecated `client.ready()` method, which has an issue causing the returned promise to hang when using async/await syntax if it was rejected.
- Updated the SDK_READY_FROM_CACHE event to be emitted alongside the SDK_READY event if it hasn’t already been emitted.
Expand Down
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@splitsoftware/splitio-browserjs",
"version": "1.5.1",
"version": "1.6.0",
"description": "Split SDK for JavaScript on Browser",
"main": "cjs/index.js",
"module": "esm/index.js",
Expand Down Expand Up @@ -59,7 +59,7 @@
"bugs": "https://github.com/splitio/javascript-browser-client/issues",
"homepage": "https://github.com/splitio/javascript-browser-client#readme",
"dependencies": {
"@splitsoftware/splitio-commons": "2.7.9-rc.2",
"@splitsoftware/splitio-commons": "2.8.0",
"tslib": "^2.3.1",
"unfetch": "^4.2.0"
},
Expand Down
317 changes: 317 additions & 0 deletions src/__tests__/browserSuites/evaluations-fallback.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
import sinon from 'sinon';
import { SplitFactory } from '../../';

const listener = {
logImpression: sinon.stub()
};

export default function (configInMemory, configInLocalStorage, fetchMock, assert) {

assert.test('FallbackTreatment / Split factory with no fallbackTreatment defined', async t => {

const splitio = SplitFactory(configInMemory);
const client = splitio.client();

await client.whenReady();

t.equal(client.getTreatment('non_existent_flag'), 'control', 'The evaluation will return `control` if the flag does not exist and no fallbackTreatment is defined');
t.equal(client.getTreatment('non_existent_flag_2'), 'control', 'The evaluation will return `control` if the flag does not exist and no fallbackTreatment is defined');

await client.destroy();
t.end();

});

assert.test('FallbackTreatment / Split factory with global fallbackTreatment defined', async t => {

const config = {
...configInMemory,
fallbackTreatments: {
global: 'FALLBACK_TREATMENT'
}
};
const splitio = SplitFactory(config);
const client = splitio.client();

await client.whenReady();


t.equal(client.getTreatment('non_existent_flag'), 'FALLBACK_TREATMENT', 'The evaluation will return `FALLBACK_TREATMENT` if the flag does not exist and no fallbackTreatment is defined');
t.equal(client.getTreatment('non_existent_flag_2'), 'FALLBACK_TREATMENT', 'The evaluation will return `FALLBACK_TREATMENT` if the flag does not exist and no fallbackTreatment is defined');

await client.destroy();
t.end();

});

assert.test('FallbackTreatment / Split factory with specific fallbackTreatment defined', async t => {

const config = {
...configInMemory,
fallbackTreatments: {
byFlag: {
'non_existent_flag': 'FALLBACK_TREATMENT',
}
}
};
const splitio = SplitFactory(config);
const client = splitio.client();

await client.whenReady();

t.equal(client.getTreatment('non_existent_flag'), 'FALLBACK_TREATMENT', 'The evaluation will return `FALLBACK_TREATMENT` if the flag does not exist and no fallbackTreatment is defined');
t.equal(client.getTreatment('non_existent_flag_2'), 'control', 'The evaluation will return `control` if the flag does not exist and no fallbackTreatment is defined');

t.equal(client.getTreatment('non_existent_flag'), 'FALLBACK_TREATMENT', 'The evaluation will return `FALLBACK_TREATMENT` if the flag does not exist and no fallbackTreatment is defined');
t.equal(client.getTreatment('non_existent_flag_2'), 'control', 'The evaluation will return `control` if the flag does not exist and no fallbackTreatment is defined');

await client.destroy();
t.end();

});


assert.test('FallbackTreatment / flag override beats global fallbackTreatment', async t => {

const config = {
...configInMemory,
fallbackTreatments: {
global: 'OFF_FALLBACK',
byFlag: {
'my_flag': 'ON_FALLBACK',
}
}
};
const splitio = SplitFactory(config);
const client = splitio.client();

await client.whenReady();

t.equal(client.getTreatment('my_flag'), 'ON_FALLBACK', 'The evaluation will return `ON_FALLBACK` if the flag does not exist and no fallbackTreatment is defined');
t.equal(client.getTreatment('non_existent_flag_2'), 'OFF_FALLBACK', 'The evaluation will return `OFF_FALLBACK` if the flag does not exist and no fallbackTreatment is defined');

t.equal(client.getTreatment('my_flag'), 'ON_FALLBACK', 'The evaluation will return `ON_FALLBACK` if the flag does not exist and no fallbackTreatment is defined');
t.equal(client.getTreatment('non_existent_flag_2'), 'OFF_FALLBACK', 'The evaluation will return `OFF_FALLBACK` if the flag does not exist and no fallbackTreatment is defined');

await client.destroy();
t.end();

});

assert.test('FallbackTreatment / override applies only when original is control', async t => {

const config = {
...configInMemory,
fallbackTreatments: {
global: 'OFF_FALLBACK'
}
};
const splitio = SplitFactory(config);
const client = splitio.client();

await client.whenReady();

t.equal(client.getTreatment('user_account_in_whitelist'), 'off', 'The evaluation will return the treatment defined in the flag if it exists');
t.equal(client.getTreatment('non_existent_flag'), 'OFF_FALLBACK', 'The evaluation will return `OFF_FALLBACK` if the flag does not exist and no fallbackTreatment is defined');

await client.destroy();
t.end();

});


assert.test('FallbackTreatment / override applies only when original is control - inLocalStorage', async t => {

const config = {
...configInLocalStorage,
fallbackTreatments: {
global: 'OFF_FALLBACK'
}
};
const splitio = SplitFactory(config);
const client = splitio.client();

await client.whenReady();

t.equal(client.getTreatment('user_account_in_whitelist'), 'off', 'The evaluation will return the treatment defined in the flag if it exists');
t.equal(client.getTreatment('non_existent_flag'), 'OFF_FALLBACK', 'The evaluation will return `OFF_FALLBACK` if the flag does not exist and no fallbackTreatment is defined');

await client.destroy();
t.end();

});

assert.test('FallbackTreatment / Impressions correctness with fallback when client is not ready', async t => {

const config = {
...configInMemory,
urls: {
events: 'https://events.fallbacktreatment/api'
},
fallbackTreatments: {
byFlag: {
'any_flag': 'OFF_FALLBACK'
}
}
};
const splitio = SplitFactory(config);
const client = splitio.client();

t.equal(client.getTreatment('any_flag'), 'OFF_FALLBACK', 'The evaluation will return the fallbackTreatment if the client is not ready yet');
t.equal(client.getTreatment('user_account_in_whitelist'), 'control', 'The evaluation will return the fallbackTreatment if the client is not ready yet');

await client.whenReady();

fetchMock.postOnce(config.urls.events + '/testImpressions/bulk', (_, opts) => {

const payload = JSON.parse(opts.body);

function validateImpressionData(featureFlagName, expectedLabel) {
const impressions = payload.find(e => e.f === featureFlagName).i;

t.equal(impressions[0].r, expectedLabel, `${featureFlagName} impression with label ${expectedLabel}`);
}

validateImpressionData('any_flag', 'fallback - not ready');
validateImpressionData('user_account_in_whitelist', 'not ready');
t.end();

return 200;
});

await client.destroy();

});

assert.test('FallbackTreatment / Fallback dynamic config propagation', async t => {

const config = {
...configInMemory,
fallbackTreatments: {
global: { treatment: 'OFF_FALLBACK', config: '{"global": true}' },
byFlag: {
'my_flag': { treatment: 'ON_FALLBACK', config: '{"flag": true}' }
}
}
};
const splitio = SplitFactory(config);
const client = splitio.client();

await client.whenReady();

t.deepEqual(client.getTreatmentWithConfig('my_flag'), { treatment: 'ON_FALLBACK', config: '{"flag": true}' }, 'The evaluation will propagate the config along with the treatment from the fallbackTreatment');
t.deepEqual(client.getTreatmentWithConfig('non_existent_flag'), { treatment: 'OFF_FALLBACK', config: '{"global": true}' }, 'The evaluation will propagate the config along with the treatment from the fallbackTreatment');

await client.destroy();
t.end();

});

assert.test('FallbackTreatment / Fallback dynamic config propagation - inLocalStorage', async t => {

const config = {
...configInLocalStorage,
fallbackTreatments: {
global: { treatment: 'OFF_FALLBACK', config: '{"global": true}' },
byFlag: {
'my_flag': { treatment: 'ON_FALLBACK', config: '{"flag": true}' }
}
}
};
const splitio = SplitFactory(config);
const client = splitio.client();

await client.whenReady();

t.deepEqual(client.getTreatmentWithConfig('my_flag'), { treatment: 'ON_FALLBACK', config: '{"flag": true}' }, 'The evaluation will propagate the config along with the treatment from the fallbackTreatment');
t.deepEqual(client.getTreatmentWithConfig('non_existent_flag'), { treatment: 'OFF_FALLBACK', config: '{"global": true}' }, 'The evaluation will propagate the config along with the treatment from the fallbackTreatment');

await client.destroy();
t.end();

});

assert.test('FallbackTreatment / Evaluations non existing flags with fallback do not generate impressions', async t => {

const config = {
...configInMemory,
urls: {
events: 'https://events.fallbacktreatment/api'
},
fallbackTreatments: {
global: { treatment: 'OFF_FALLBACK', config: '{"global": true}' },
byFlag: {
'my_flag': { treatment: 'ON_FALLBACK', config: '{"flag": true}' }
}
}
};
config.impressionListener = listener;

const splitio = SplitFactory(config);
const client = splitio.client();

await client.whenReady();

t.deepEqual(client.getTreatmentWithConfig('my_flag'), { treatment: 'ON_FALLBACK', config: '{"flag": true}' }, 'The evaluation will propagate the config along with the treatment from the fallbackTreatment');
t.deepEqual(client.getTreatmentWithConfig('non_existent_flag'), { treatment: 'OFF_FALLBACK', config: '{"global": true}' }, 'The evaluation will propagate the config along with the treatment from the fallbackTreatment');

let POSTED_IMPRESSIONS_COUNT = 0;

fetchMock.postOnce(config.urls.events + '/testImpressions/bulk', (_, opts) => {

const payload = JSON.parse(opts.body);
t.equal(payload.length, 1, 'We should have just one impression for the two evaluated flags');

function validateImpressionData(featureFlagName, expectedLength) {

const impressions = payload.find(e => e.f === featureFlagName).i;
t.equal(impressions.length, expectedLength, `${featureFlagName} has ${expectedLength} impressions`);
}

validateImpressionData('my_flag', 1);
validateImpressionData('non_existent_flag', 0);
POSTED_IMPRESSIONS_COUNT = payload.reduce((acc, curr) => acc + curr.i.length, 0);
t.equal(POSTED_IMPRESSIONS_COUNT, 1, 'We should have just one impression in total.');

return 200;
});

setTimeout(() => {
t.equal(listener.logImpression.callCount, POSTED_IMPRESSIONS_COUNT, 'Impression listener should be called once per each impression generated.');

t.end();
}, 0);
await client.destroy();


});

assert.test('FallbackTreatment / LocalhostMode', async t => {

const config = {
...configInMemory,
core: {
...configInMemory.core,
authorizationKey: 'localhost',
},
fallbackTreatments: {
global: 'OFF_FALLBACK'
},
features: {
testing_split: 'on',
}
};
const splitio = SplitFactory(config);
const client = splitio.client();

await client.whenReady();

t.deepEqual(client.getTreatment('testing_split'), 'on', 'The evaluation should return the treatment defined in localhost mode');
t.deepEqual(client.getTreatment('non_existent_flag'), 'OFF_FALLBACK', 'The evaluation will return `OFF_FALLBACK` if the flag does not exist');

await client.destroy();

t.end();
});

}
Loading