diff --git a/CHANGES.txt b/CHANGES.txt index ea434f8fc..b1a126457 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,10 @@ +11.8.0 (October 28, 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. + - Updated @splitsoftware/splitio-commons package to version 2.8.0. + 11.7.1 (October 8, 2025) - Bugfix - Updated @splitsoftware/splitio-commons package to version 2.7.1, which fixes the `debug` option to support log levels when the `logger` option is used. diff --git a/karma/e2e.online.karma.conf.js b/karma/e2e.online.karma.conf.js index a81d35a7f..3321b9e3d 100644 --- a/karma/e2e.online.karma.conf.js +++ b/karma/e2e.online.karma.conf.js @@ -1,6 +1,6 @@ const assign = require('lodash/assign'); -module.exports = function(config) { +module.exports = function (config) { 'use strict'; config.set(assign({}, require('./config'), { diff --git a/package-lock.json b/package-lock.json index 02fc53b90..0faf40dec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@splitsoftware/splitio", - "version": "11.7.1", + "version": "11.7.2-rc.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio", - "version": "11.7.1", + "version": "11.7.2-rc.4", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio-commons": "2.7.9-rc.1", + "@splitsoftware/splitio-commons": "2.7.9-rc.3", "bloom-filters": "^3.0.4", "ioredis": "^4.28.0", "js-yaml": "^3.13.1", @@ -351,9 +351,9 @@ "dev": true }, "node_modules/@splitsoftware/splitio-commons": { - "version": "2.7.9-rc.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.7.9-rc.1.tgz", - "integrity": "sha512-hmcekZebItAc67+AF3xGgKXIvnLybQhpaEtQKsqU3WbyRittUI9hN9HqA6qUiaLxFPwrsCxioihTYsnxxej/Rg==", + "version": "2.7.9-rc.3", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.7.9-rc.3.tgz", + "integrity": "sha512-momlpLuBt0yQXzo7blDWbNIs+H0fIPcxWukZVXMIKHiLiZtfu608diLT8EB/PNtA245OUMIRzachk5If4BBOWw==", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", @@ -7740,9 +7740,9 @@ "dev": true }, "@splitsoftware/splitio-commons": { - "version": "2.7.9-rc.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.7.9-rc.1.tgz", - "integrity": "sha512-hmcekZebItAc67+AF3xGgKXIvnLybQhpaEtQKsqU3WbyRittUI9hN9HqA6qUiaLxFPwrsCxioihTYsnxxej/Rg==", + "version": "2.7.9-rc.3", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.7.9-rc.3.tgz", + "integrity": "sha512-momlpLuBt0yQXzo7blDWbNIs+H0fIPcxWukZVXMIKHiLiZtfu608diLT8EB/PNtA245OUMIRzachk5If4BBOWw==", "requires": { "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" diff --git a/package.json b/package.json index f39ff2e23..67633080f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "11.7.1", + "version": "11.7.2-rc.4", "description": "Split SDK", "files": [ "README.md", @@ -38,7 +38,7 @@ "node": ">=14.0.0" }, "dependencies": { - "@splitsoftware/splitio-commons": "2.7.9-rc.1", + "@splitsoftware/splitio-commons": "2.7.9-rc.3", "bloom-filters": "^3.0.4", "ioredis": "^4.28.0", "js-yaml": "^3.13.1", diff --git a/src/__tests__/browserSuites/ready-from-cache-async-wrapper.spec.js b/src/__tests__/browserSuites/ready-from-cache-async-wrapper.spec.js index d4f37eed5..d66b2993b 100644 --- a/src/__tests__/browserSuites/ready-from-cache-async-wrapper.spec.js +++ b/src/__tests__/browserSuites/ready-from-cache-async-wrapper.spec.js @@ -70,17 +70,17 @@ export default function (fetchMock, assert) { t.end(); }); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY'); + t.true(client.getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY'); }); client.on(client.Event.SDK_READY, () => { - t.true(client.__getStatus().isReadyFromCache, 'Client should emit SDK_READY and it should be ready from cache'); + t.true(client.getStatus().isReadyFromCache, 'Client should emit SDK_READY and it should be ready from cache'); }); client2.on(client.Event.SDK_READY, () => { - t.true(client2.__getStatus().isReadyFromCache, 'Non-default client should emit SDK_READY and it should be ready from cache'); + t.true(client2.getStatus().isReadyFromCache, 'Non-default client should emit SDK_READY and it should be ready from cache'); }); client3.on(client.Event.SDK_READY, () => { - t.true(client2.__getStatus().isReadyFromCache, 'Non-default client should emit SDK_READY and it should be ready from cache'); + t.true(client2.getStatus().isReadyFromCache, 'Non-default client should emit SDK_READY and it should be ready from cache'); }); }); @@ -356,7 +356,7 @@ export default function (fetchMock, assert) { let manager = splitio.manager(); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY, because clearOnInit is true'); + t.true(client.getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY, because clearOnInit is true'); }); await client.whenReady(); @@ -400,7 +400,7 @@ export default function (fetchMock, assert) { manager = splitio.manager(); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY, because clearOnInit is true'); + t.true(client.getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY, because clearOnInit is true'); }); await new Promise(res => client.once(client.Event.SDK_READY, res)); diff --git a/src/__tests__/browserSuites/ready-from-cache.spec.js b/src/__tests__/browserSuites/ready-from-cache.spec.js index 2ff9cc183..419728fca 100644 --- a/src/__tests__/browserSuites/ready-from-cache.spec.js +++ b/src/__tests__/browserSuites/ready-from-cache.spec.js @@ -142,17 +142,17 @@ export default function (fetchMock, assert) { t.end(); }); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY'); + t.true(client.getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY'); }); client.on(client.Event.SDK_READY, () => { - t.true(client.__getStatus().isReadyFromCache, 'Client should emit SDK_READY and it should be ready from cache'); + t.true(client.getStatus().isReadyFromCache, 'Client should emit SDK_READY and it should be ready from cache'); }); client2.on(client.Event.SDK_READY, () => { - t.true(client2.__getStatus().isReadyFromCache, 'Non-default client should emit SDK_READY and it should be ready from cache'); + t.true(client2.getStatus().isReadyFromCache, 'Non-default client should emit SDK_READY and it should be ready from cache'); }); client3.on(client.Event.SDK_READY, () => { - t.true(client2.__getStatus().isReadyFromCache, 'Non-default client should emit SDK_READY and it should be ready from cache'); + t.true(client2.getStatus().isReadyFromCache, 'Non-default client should emit SDK_READY and it should be ready from cache'); }); }); @@ -331,7 +331,7 @@ export default function (fetchMock, assert) { client.on(client.Event.SDK_READY_FROM_CACHE, () => { t.true(Date.now() - startTime < 400, 'It should emit SDK_READY_FROM_CACHE on every client if there was data in the cache and we subscribe on time. Should be considerably faster than actual readiness from the cloud.'); - t.false(client.__getStatus().isReady, 'It should not be ready yet'); + t.false(client.getStatus().isReady, 'It should not be ready yet'); t.equal(client.getTreatment('always_on'), 'off', 'It should evaluate treatments with data from cache instead of control due to Input Validation'); @@ -515,13 +515,13 @@ export default function (fetchMock, assert) { }); }); - assert.test(t => { // Testing when we start with initial rollout plan data and sync storage type (is ready from cache immediately) + assert.test(async t => { // Testing when we start with initial rollout plan data and sync storage type (is ready from cache immediately) const testUrls = { sdk: 'https://sdk.baseurl/readyFromCacheWithInitialRolloutPlan', events: 'https://events.baseurl/readyFromCacheWithInitialRolloutPlan' }; - t.plan(5); + t.plan(6); fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.3&since=25&rbSince=-1', { status: 200, body: { ff: { ...splitChangesMock1.ff, s: 25 } } }); fetchMock.getOnce(testUrls.sdk + '/memberships/nicolas%40split.io', { status: 200, body: membershipsNicolas }); @@ -552,7 +552,7 @@ export default function (fetchMock, assert) { const client = splitio.client(); const client2 = splitio.client('emi@split.io'); - t.equal(client.__getStatus().isReadyFromCache, true, 'Client is ready from cache'); + t.equal(client.getStatus().isReadyFromCache, true, 'Client is ready from cache'); t.equal(client.getTreatment('always_on'), 'off', 'It should evaluate treatments with data from cache. Key without memberships'); t.equal(client2.getTreatment('always_on'), 'on', 'It should evaluate treatments with data from cache. Key with memberships'); @@ -577,6 +577,10 @@ export default function (fetchMock, assert) { t.end(); }); }); + + const startTime = Date.now(); + await client.whenReadyFromCache(); + t.true(nearlyEqual(Date.now() - startTime, 0), 'whenReadyFromCache should be resolved immediately'); }); /** Fetch specific splits **/ @@ -613,7 +617,7 @@ export default function (fetchMock, assert) { const manager = splitio.manager(); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY'); + t.true(client.getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY'); }); client.once(client.Event.SDK_READY, () => { @@ -657,7 +661,7 @@ export default function (fetchMock, assert) { const manager = splitio.manager(); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY'); + t.true(client.getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY'); }); client.once(client.Event.SDK_READY, () => { @@ -758,7 +762,7 @@ export default function (fetchMock, assert) { const manager = splitio.manager(); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY'); + t.true(client.getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY'); }); client.once(client.Event.SDK_READY, () => { @@ -819,7 +823,7 @@ export default function (fetchMock, assert) { const manager = splitio.manager(); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY'); + t.true(client.getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY'); }); client.once(client.Event.SDK_READY, () => { @@ -919,7 +923,7 @@ export default function (fetchMock, assert) { let manager = splitio.manager(); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY, because clearOnInit is true'); + t.true(client.getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY, because clearOnInit is true'); }); await client.whenReady(); @@ -959,7 +963,7 @@ export default function (fetchMock, assert) { manager = splitio.manager(); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY, because clearOnInit is true'); + t.true(client.getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY, because clearOnInit is true'); }); await new Promise(res => client.once(client.Event.SDK_READY, res)); diff --git a/src/__tests__/errorCatching/browser.spec.js b/src/__tests__/errorCatching/browser.spec.js index 4e422dc10..ef8d7ffdd 100644 --- a/src/__tests__/errorCatching/browser.spec.js +++ b/src/__tests__/errorCatching/browser.spec.js @@ -91,13 +91,13 @@ tape('Error catching on callbacks - Browsers', assert => { } client.on(client.Event.SDK_READY_TIMED_OUT, () => { - assert.true(client.__getStatus().hasTimedout); // SDK status should be already updated + assert.true(client.getStatus().hasTimedout); // SDK status should be already updated attachErrorHandlerIfApplicable(); null.willThrowForTimedOut(); }); client.once(client.Event.SDK_READY, () => { - assert.true(client.__getStatus().isReady); // SDK status should be already updated + assert.true(client.getStatus().isReady); // SDK status should be already updated attachErrorHandlerIfApplicable(); null.willThrowForReady(); }); @@ -108,7 +108,7 @@ tape('Error catching on callbacks - Browsers', assert => { }); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - assert.true(client.__getStatus().isReadyFromCache); // SDK status should be already updated + assert.true(client.getStatus().isReadyFromCache); // SDK status should be already updated attachErrorHandlerIfApplicable(); null.willThrowForReadyFromCache(); }); diff --git a/src/__tests__/nodeSuites/evaluations-fallback.spec.js b/src/__tests__/nodeSuites/evaluations-fallback.spec.js new file mode 100644 index 000000000..610d646eb --- /dev/null +++ b/src/__tests__/nodeSuites/evaluations-fallback.spec.js @@ -0,0 +1,275 @@ +import path from 'path'; +import sinon from 'sinon'; +import { SplitFactory } from '../../'; + +const listener = { + logImpression: sinon.stub() +}; + +const baseConfig = { + core: { + authorizationKey: '' + }, + sync: { + impressionsMode: 'DEBUG' + }, + streamingEnabled: false +}; + +export default async function (fetchMock, assert) { + + assert.test('FallbackTreatment / Split factory with no fallbackTreatment defined', async t => { + + const splitio = SplitFactory(baseConfig); + const client = splitio.client(); + + await client.whenReady(); + + t.equal(client.getTreatment('emi@harness.io', 'non_existent_flag'), 'control', 'The evaluation will return `control` if the flag does not exist and no fallbackTreatment is defined'); + t.equal(client.getTreatment('emma@harness.io', '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 = { + ...baseConfig, + fallbackTreatments: { + global: 'FALLBACK_TREATMENT' + } + }; + const splitio = SplitFactory(config); + const client = splitio.client(); + + await client.whenReady(); + + + t.equal(client.getTreatment('emi@harness.io', '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('emma@harness.io', '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 = { + ...baseConfig, + fallbackTreatments: { + byFlag: { + 'non_existent_flag': 'FALLBACK_TREATMENT', + } + } + }; + const splitio = SplitFactory(config); + const client = splitio.client(); + + await client.whenReady(); + + t.equal(client.getTreatment('emi@harness.io', '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('emi@harness.io', '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('emma@harness.io', '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('emma@harness.io', '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 = { + ...baseConfig, + fallbackTreatments: { + global: 'OFF_FALLBACK', + byFlag: { + 'my_flag': 'ON_FALLBACK', + } + } + }; + const splitio = SplitFactory(config); + const client = splitio.client(); + + await client.whenReady(); + + t.equal(client.getTreatment('emi@harness.io', '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('emi@harness.io', '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('emma@harness.io', '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('emma@harness.io', '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 = { + ...baseConfig, + fallbackTreatments: { + global: 'OFF_FALLBACK' + } + }; + const splitio = SplitFactory(config); + const client = splitio.client(); + + await client.whenReady(); + + t.equal(client.getTreatment('emma@harness.io', 'user_account_in_whitelist'), 'off', 'The evaluation will return the treatment defined in the flag if it exists'); + t.equal(client.getTreatment('emma@harness.io', '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 = { + ...baseConfig, + urls: { + events: 'https://events.fallbacktreatment/api' + }, + fallbackTreatments: { + byFlag: { + 'any_flag': 'OFF_FALLBACK' + } + } + }; + const splitio = SplitFactory(config); + const client = splitio.client(); + + t.equal(client.getTreatment('emi@harness.io', 'any_flag'), 'OFF_FALLBACK', 'The evaluation will return the fallbackTreatment if the client is not ready yet'); + t.equal(client.getTreatment('emma@harness.io', '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'); + + return 200; + }); + + await client.destroy(); + t.end(); + + }); + + assert.test('FallbackTreatment / Fallback dynamic config propagation', async t => { + + const config = { + ...baseConfig, + 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('emma@harness.io', '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('emma@harness.io', '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 = { + ...baseConfig, + urls: { + events: 'https://events.fallbacktreatment/api' + }, + fallbackTreatments: { + global: { treatment: 'OFF_FALLBACK', config: '{"global": true}' }, + byFlag: { + 'my_flag': { treatment: 'ON_FALLBACK', config: '{"flag": true}' } + } + }, + impressionListener: listener + }; + + const splitio = SplitFactory(config); + const client = splitio.client(); + + await client.whenReady(); + + t.deepEqual(client.getTreatmentWithConfig('emma@harness.io', '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('emma@harness.io', '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; + }); + t.equal(listener.logImpression.callCount, POSTED_IMPRESSIONS_COUNT, 'Impression listener should be called once per each impression generated.'); + await client.destroy(); + + t.end(); + }); + + assert.test('FallbackTreatment / LocalhostMode', async t => { + + const config = { + ...baseConfig, + core: { + ...baseConfig.core, + authorizationKey: 'localhost' + }, + fallbackTreatments: { + global: 'OFF_FALLBACK' + }, + features: path.join(__dirname, '../offline/split.yaml') + }; + const splitio = SplitFactory(config); + const client = splitio.client(); + + await client.whenReady(); + + t.deepEqual(client.getTreatment('emma@harness.io', 'testing_split_on'), 'on', 'The evaluation should return the treatment defined in localhost mode'); + t.deepEqual(client.getTreatment('emma@harness.io', 'non_existent_flag'), 'OFF_FALLBACK', 'The evaluation will return `OFF_FALLBACK` if the flag does not exist'); + + await client.destroy(); + + t.end(); + }); + +} diff --git a/src/__tests__/nodeSuites/lazy-init.spec.js b/src/__tests__/nodeSuites/lazy-init.spec.js index 44bfacaa6..7194ebb7a 100644 --- a/src/__tests__/nodeSuites/lazy-init.spec.js +++ b/src/__tests__/nodeSuites/lazy-init.spec.js @@ -41,14 +41,14 @@ export default function (settings, fetchMock, t) { splitio.init(); await splitio.client().ready(); - assert.deepEqual(splitio.client().__getStatus(), { isReady: true, isReadyFromCache: true, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: true, lastUpdate: splitio.client().__getStatus().lastUpdate }, 'Status'); + assert.deepEqual(splitio.client().getStatus(), { isReady: true, isReadyFromCache: true, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: true, lastUpdate: splitio.client().getStatus().lastUpdate }, 'Status'); await splitio.destroy(); - assert.deepEqual(splitio.client().__getStatus(), { isReady: true, isReadyFromCache: true, isTimedout: false, hasTimedout: false, isDestroyed: true, isOperational: false, lastUpdate: splitio.client().__getStatus().lastUpdate }, 'Status'); + assert.deepEqual(splitio.client().getStatus(), { isReady: true, isReadyFromCache: true, isTimedout: false, hasTimedout: false, isDestroyed: true, isOperational: false, lastUpdate: splitio.client().getStatus().lastUpdate }, 'Status'); splitio.init(); - assert.deepEqual(splitio.client().__getStatus(), { isReady: true, isReadyFromCache: true, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: true, lastUpdate: splitio.client().__getStatus().lastUpdate }, 'Status'); + assert.deepEqual(splitio.client().getStatus(), { isReady: true, isReadyFromCache: true, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: true, lastUpdate: splitio.client().getStatus().lastUpdate }, 'Status'); await splitio.destroy(); @@ -99,14 +99,14 @@ export default function (settings, fetchMock, t) { splitio.init(); await splitio.client().ready(); - assert.deepEqual(splitio.client().__getStatus(), { isReady: true, isReadyFromCache: true, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: true, lastUpdate: splitio.client().__getStatus().lastUpdate }, 'Status'); + assert.deepEqual(splitio.client().getStatus(), { isReady: true, isReadyFromCache: true, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: true, lastUpdate: splitio.client().getStatus().lastUpdate }, 'Status'); await splitio.destroy(); - assert.deepEqual(splitio.client().__getStatus(), { isReady: true, isReadyFromCache: true, isTimedout: false, hasTimedout: false, isDestroyed: true, isOperational: false, lastUpdate: splitio.client().__getStatus().lastUpdate }, 'Status'); + assert.deepEqual(splitio.client().getStatus(), { isReady: true, isReadyFromCache: true, isTimedout: false, hasTimedout: false, isDestroyed: true, isOperational: false, lastUpdate: splitio.client().getStatus().lastUpdate }, 'Status'); splitio.init(); - assert.deepEqual(splitio.client().__getStatus(), { isReady: true, isReadyFromCache: true, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: true, lastUpdate: splitio.client().__getStatus().lastUpdate }, 'Status'); + assert.deepEqual(splitio.client().getStatus(), { isReady: true, isReadyFromCache: true, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: true, lastUpdate: splitio.client().getStatus().lastUpdate }, 'Status'); await splitio.destroy(); diff --git a/src/__tests__/nodeSuites/readiness.spec.js b/src/__tests__/nodeSuites/readiness.spec.js index ebce11039..531cb0ad0 100644 --- a/src/__tests__/nodeSuites/readiness.spec.js +++ b/src/__tests__/nodeSuites/readiness.spec.js @@ -89,7 +89,7 @@ export default function (fetchMock, assert) { const client = splitio.client(); - t.equal(client.__getStatus().isReadyFromCache, true, 'Client is ready from cache'); + t.equal(client.getStatus().isReadyFromCache, true, 'Client is ready from cache'); t.equal(client.getTreatment('nicolas@split.io', 'always_on'), 'off', 'It should evaluate treatments with data from cache. Key not in segment'); t.equal(client.getTreatment('emi@split.io', 'always_on'), 'on', 'It should evaluate treatments with data from cache. Key in segment'); diff --git a/src/__tests__/nodeSuites/ready-promise.spec.js b/src/__tests__/nodeSuites/ready-promise.spec.js index b6b76c93e..6e2ee9c91 100644 --- a/src/__tests__/nodeSuites/ready-promise.spec.js +++ b/src/__tests__/nodeSuites/ready-promise.spec.js @@ -484,7 +484,7 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { client.off(client.Event.SDK_READY, onReadyCallback); const manager = splitio.manager(); - client.whenReadyFromCache().then(() => t.fail('SDK TIMED OUT - Should not resolve')).catch(() => t.pass('SDK TIMED OUT - Should reject')); + manager.whenReadyFromCache().then(() => t.fail('SDK TIMED OUT - Should not resolve')).catch(() => t.pass('SDK TIMED OUT - Should reject')); manager.on(manager.Event.SDK_READY, onReadyCallback); manager.off(manager.Event.SDK_READY, onReadyCallback); diff --git a/src/__tests__/offline/browser.spec.js b/src/__tests__/offline/browser.spec.js index f2cb8dac0..8edd7d50c 100644 --- a/src/__tests__/offline/browser.spec.js +++ b/src/__tests__/offline/browser.spec.js @@ -114,7 +114,7 @@ tape('Browser offline mode', function (assert) { const sdkReadyFromCache = (client) => () => { assert.equal(factory.settings.storage.type, 'MEMORY', 'In localhost mode, storage must fallback to memory storage'); - const clientStatus = client.__getStatus(); + const clientStatus = client.getStatus(); assert.equal(clientStatus.isReadyFromCache, true, 'If ready from cache, READY_FROM_CACHE status must be true'); assert.equal(clientStatus.isReady, configs[i].storage && configs[i].storage.type === 'LOCALSTORAGE' ? false : true, 'When not using LOCALSTORAGE, READY status is set together with READY_FROM_CACHE'); if (!clientStatus.isReady) readyFromCacheCount++; diff --git a/src/__tests__/online/node.spec.js b/src/__tests__/online/node.spec.js index c0f64b939..35d87a3c3 100644 --- a/src/__tests__/online/node.spec.js +++ b/src/__tests__/online/node.spec.js @@ -8,6 +8,7 @@ import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json'; import evaluationsSuite from '../nodeSuites/evaluations.spec'; import evaluationsSemverSuite from '../nodeSuites/evaluations-semver.spec'; +import evaluationsFallbackSuite from '../nodeSuites/evaluations-fallback.spec'; import eventsSuite from '../nodeSuites/events.spec'; import impressionsSuite from '../nodeSuites/impressions.spec'; import impressionsSuiteDebug from '../nodeSuites/impressions.debug.spec'; @@ -56,9 +57,11 @@ fetchMock.post(url(settings, '/v1/metrics/config'), 200); fetchMock.post(url(settings, '/v1/metrics/usage'), 200); tape('## Node.js - E2E CI Tests ##', async function (assert) { + /* Check client evaluations. */ assert.test('E2E / In Memory', evaluationsSuite.bind(null, config, key)); assert.test('E2E / In Memory - Semver', evaluationsSemverSuite.bind(null, fetchMock)); + assert.test('E2E / In Memory - Fallback treatment', evaluationsFallbackSuite.bind(null, fetchMock)); /* Check impressions */ assert.test('E2E / Impressions', impressionsSuite.bind(null, key, fetchMock)); diff --git a/src/settings/defaults/version.js b/src/settings/defaults/version.js index 2a4a3206c..500d24409 100644 --- a/src/settings/defaults/version.js +++ b/src/settings/defaults/version.js @@ -1 +1 @@ -export const packageVersion = '11.7.1'; +export const packageVersion = '11.7.2-rc.4'; diff --git a/ts-tests/index.ts b/ts-tests/index.ts index 271a437ea..c1cf06960 100644 --- a/ts-tests/index.ts +++ b/ts-tests/index.ts @@ -258,10 +258,24 @@ let nodeEventEmitter: NodeJS.EventEmitter = client; // Ready, destroy and flush let promise: Promise = client.ready(); +promise = client.whenReady(); promise = client.destroy(); promise = SDK.destroy(); // @TODO not public yet // promise = client.flush(); +const promiseWhenReadyFromCache: Promise = client.whenReadyFromCache(); + +// Get readiness status +let status: SplitIO.ReadinessStatus = client.getStatus(); +status = { + isReady: false, + isReadyFromCache: false, + isTimedout: false, + isDestroyed: false, + isOperational: false, + hasTimedout: false, + lastUpdate: 0 +} // We can call getTreatment with or without a key. treatment = client.getTreatment(splitKey, 'mySplit'); @@ -584,7 +598,14 @@ let fullBrowserSettings: SplitIO.IBrowserSettings = { getHeaderOverrides(context) { return { ...context.headers, 'header': 'value' } }, } }, - userConsent: 'GRANTED' + userConsent: 'GRANTED', + fallbackTreatments: { + global: { treatment: 'global-treatment', config: '{"global": true}' }, + byFlag: { + 'my_flag': { treatment: 'flag-treatment', config: '{"flag": true}' }, + 'my_other_flag': 'other-flag-treatment' + } + } }; fullBrowserSettings.storage.type = 'MEMORY'; fullBrowserSettings.userConsent = 'DECLINED'; @@ -644,6 +665,13 @@ let fullNodeSettings: SplitIO.INodeSettings = { getHeaderOverrides(context) { return { ...context.headers, 'header': 'value' } }, agent: new (require('https')).Agent(), } + }, + fallbackTreatments: { + global: { treatment: 'global-treatment', config: '{"global": true}' }, + byFlag: { + 'my_flag': { treatment: 'flag-treatment', config: '{"flag": true}' }, + 'my_other_flag': 'other-flag-treatment' + } } }; fullNodeSettings.storage.type = 'MEMORY'; @@ -692,6 +720,13 @@ let fullAsyncSettings: SplitIO.INodeAsyncSettings = { sync: { splitFilters: splitFilters, impressionsMode: 'DEBUG', + }, + fallbackTreatments: { + global: 'global-treatment', + byFlag: { + 'my_flag': { treatment: 'flag-treatment', config: '{"flag": true}' }, + 'my_other_flag': 'other-flag-treatment' + } } };