diff --git a/CHANGES.txt b/CHANGES.txt index ea434f8fc..4a3e729d1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,10 @@ +11.8.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. + - 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 2cc7b9b9a..136e015cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@splitsoftware/splitio", - "version": "11.7.1", + "version": "11.8.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio", - "version": "11.7.1", + "version": "11.8.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio-commons": "2.7.1", + "@splitsoftware/splitio-commons": "2.8.0", "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.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.7.1.tgz", - "integrity": "sha512-7a4VVMczh0YKVRi35EhD0FOAEwzqfJRcCiKqLLhZCxAvrZBpE2khpGn8pOP+y6TefdPVtblW8GIku4O4r0KRdQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.8.0.tgz", + "integrity": "sha512-QgHUreMOEDwf4GZzVPu4AzkZJvuaeSoHsiJc4tT3CxSIYl2bKMz1SSDlI1tW/oVbIFeWjkrIp2lCYEyUBgcvyA==", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", @@ -7740,9 +7740,9 @@ "dev": true }, "@splitsoftware/splitio-commons": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.7.1.tgz", - "integrity": "sha512-7a4VVMczh0YKVRi35EhD0FOAEwzqfJRcCiKqLLhZCxAvrZBpE2khpGn8pOP+y6TefdPVtblW8GIku4O4r0KRdQ==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.8.0.tgz", + "integrity": "sha512-QgHUreMOEDwf4GZzVPu4AzkZJvuaeSoHsiJc4tT3CxSIYl2bKMz1SSDlI1tW/oVbIFeWjkrIp2lCYEyUBgcvyA==", "requires": { "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" diff --git a/package.json b/package.json index ea3ad14b7..9e8e13770 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "11.7.1", + "version": "11.8.0", "description": "Split SDK", "files": [ "README.md", @@ -38,7 +38,7 @@ "node": ">=14.0.0" }, "dependencies": { - "@splitsoftware/splitio-commons": "2.7.1", + "@splitsoftware/splitio-commons": "2.8.0", "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 73350b3c6..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'); }); }); @@ -168,7 +168,7 @@ export default function (fetchMock, assert) { t.true(Date.now() - startTime >= 400, 'It should emit SDK_READY too but after syncing with the cloud.'); t.equal(client.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); - client.ready().then(() => { + client.whenReady().then(() => { t.true(Date.now() - startTime >= 400, 'It should resolve ready promise after syncing with the cloud.'); t.equal(client.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); @@ -176,12 +176,12 @@ export default function (fetchMock, assert) { t.true(Date.now() - startTime >= 700, 'It should emit SDK_READY too but after syncing with the cloud.'); t.equal(client2.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); - client2.ready().then(() => { + client2.whenReady().then(() => { t.true(Date.now() - startTime >= 700, 'It should resolve ready promise after syncing with the cloud.'); t.equal(client2.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); client3.on(client3.Event.SDK_READY, () => { - client3.ready().then(() => { + client3.whenReady().then(() => { t.true(Date.now() - startTime >= 1000, 'It should resolve ready promise after syncing with the cloud.'); t.equal(client3.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); @@ -196,7 +196,7 @@ export default function (fetchMock, assert) { t.equal(client3.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); client3.on(client3.Event.SDK_READY_TIMED_OUT, () => { - client3.ready().catch(() => { + client3.whenReady().catch(() => { t.true(Date.now() - startTime >= 850, 'It should reject ready promise before syncing memberships data with the cloud.'); t.equal(client3.getTreatment('always_on'), 'on', 'It should evaluate treatments with memberships data from cache.'); }); @@ -280,7 +280,7 @@ export default function (fetchMock, assert) { t.true(nearlyEqual(Date.now() - startTime, CLIENT_READY_MS), 'It should emit SDK_READY after syncing with the cloud.'); t.equal(client.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); - client.ready().then(() => { + client.whenReady().then(() => { t.true(nearlyEqual(Date.now() - startTime, CLIENT_READY_MS), 'It should resolve ready promise after syncing with the cloud.'); t.equal(client.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); @@ -288,12 +288,12 @@ export default function (fetchMock, assert) { t.true(nearlyEqual(Date.now() - startTime, CLIENT2_READY_MS), 'It should emit SDK_READY after syncing with the cloud.'); t.equal(client2.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); - client2.ready().then(() => { + client2.whenReady().then(() => { t.true(nearlyEqual(Date.now() - startTime, CLIENT2_READY_MS), 'It should resolve ready promise after syncing with the cloud.'); t.equal(client2.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); client3.on(client3.Event.SDK_READY, () => { - client3.ready().then(() => { + client3.whenReady().then(() => { t.true(nearlyEqual(Date.now() - startTime, CLIENT3_READY_MS), 'It should resolve ready promise after syncing with the cloud.'); t.equal(client3.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); @@ -310,7 +310,7 @@ export default function (fetchMock, assert) { t.equal(client3.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); client3.on(client3.Event.SDK_READY_TIMED_OUT, () => { - client3.ready().catch(() => { + client3.whenReady().catch(() => { t.true(Date.now() - startTime >= 850, 'It should reject ready promise before syncing memberships data with the cloud.'); t.equal(client3.getTreatment('always_on'), 'control', 'It should not evaluate treatments with memberships data from cache.'); }); @@ -356,10 +356,10 @@ 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.ready(); + await client.whenReady(); t.true(console.log.calledWithMatch('clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'), 'It should log a message about cleaning up cache'); @@ -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 23053b239..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'); }); }); @@ -226,7 +226,7 @@ export default function (fetchMock, assert) { t.true(Date.now() - startTime >= 400, 'It should emit SDK_READY too but after syncing with the cloud.'); t.equal(client.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); - client.ready().then(() => { + client.whenReady().then(() => { t.true(Date.now() - startTime >= 400, 'It should resolve ready promise after syncing with the cloud.'); t.equal(client.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); @@ -234,12 +234,12 @@ export default function (fetchMock, assert) { t.true(Date.now() - startTime >= 700, 'It should emit SDK_READY too but after syncing with the cloud.'); t.equal(client2.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); - client2.ready().then(() => { + client2.whenReady().then(() => { t.true(Date.now() - startTime >= 700, 'It should resolve ready promise after syncing with the cloud.'); t.equal(client2.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); client3.on(client3.Event.SDK_READY, () => { - client3.ready().then(() => { + client3.whenReady().then(() => { t.true(Date.now() - startTime >= 1000, 'It should resolve ready promise after syncing with the cloud.'); t.equal(client3.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); @@ -254,7 +254,7 @@ export default function (fetchMock, assert) { t.equal(client3.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); client3.on(client3.Event.SDK_READY_TIMED_OUT, () => { - client3.ready().catch(() => { + client3.whenReady().catch(() => { t.true(Date.now() - startTime >= 850, 'It should reject ready promise before syncing memberships data with the cloud.'); t.equal(client3.getTreatment('always_on'), 'on', 'It should evaluate treatments with memberships data from cache.'); }); @@ -269,7 +269,7 @@ export default function (fetchMock, assert) { events: 'https://events.baseurl/readyFromCacheWithData3' }; localStorage.clear(); - t.plan(12 * 2 + 5); + t.plan(12 * 2 + 14); fetchMock.get(testUrls.sdk + '/splitChanges?s=1.3&since=25&rbSince=-1', () => { t.equal(localStorage.getItem('readyFromCache_3.SPLITIO.split.always_on'), alwaysOnSplitInverted, 'feature flags must not be cleaned from cache'); @@ -285,8 +285,16 @@ export default function (fetchMock, assert) { return new Promise(res => { setTimeout(() => res({ status: 200, body: { 'ms': {} }, headers: {} }), 1000); }); // Third client memberships will come after 1s }); fetchMock.get(testUrls.sdk + '/memberships/nicolas4%40split.io', { 'ms': {} }); - fetchMock.postOnce(testUrls.events + '/testImpressions/bulk', 200); - fetchMock.postOnce(testUrls.events + '/testImpressions/count', 200); + fetchMock.postOnce(testUrls.events + '/testImpressions/bulk', (_, opts) => { + const payload = JSON.parse(opts.body); + t.equal(payload.length, 1, 'Only one flag was evaluated'); + t.equal(payload[0].i.length, 14, '14 impressions were queued, one per getTreatment call'); + t.equal(payload[0].i.filter((imp) => imp.t === 'control').length, 2, '2 impressions were queued for control (not ready from cache)'); + t.equal(payload[0].i.filter((imp) => imp.t === 'off').length, 4, '4 impressions were queued for off (ready from cache)'); + t.equal(payload[0].i.filter((imp) => imp.t === 'on').length, 8, '8 impressions were queued for on (ready)'); + + return 200; + }); localStorage.setItem('some_user_item', 'user_item'); localStorage.setItem('readyFromCache_3.SPLITIO.splits.till', 25); @@ -301,6 +309,9 @@ export default function (fetchMock, assert) { type: 'LOCALSTORAGE', prefix: 'readyFromCache_3' }, + sync: { + impressionsMode: 'DEBUG' + }, startup: { readyTimeout: 0.85 }, @@ -320,6 +331,8 @@ 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.equal(client.getTreatment('always_on'), 'off', 'It should evaluate treatments with data from cache instead of control due to Input Validation'); const client4 = splitio.client('nicolas4@split.io'); @@ -342,7 +355,11 @@ export default function (fetchMock, assert) { t.true(Date.now() - startTime >= 400, 'It should emit SDK_READY too but after syncing with the cloud.'); t.equal(client.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); - client.ready().then(() => { + client.whenReadyFromCache().then((isReady) => { + t.false(isReady, 'It should be ready from cache before ready (syncing with the cloud).'); + t.true(Date.now() - startTime < 50, 'It should resolve ready from cache promise almost immediately.'); + }); + client.whenReady().then(() => { t.true(Date.now() - startTime >= 400, 'It should resolve ready promise after syncing with the cloud.'); t.equal(client.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); @@ -350,12 +367,15 @@ export default function (fetchMock, assert) { t.true(Date.now() - startTime >= 700, 'It should emit SDK_READY too but after syncing with the cloud.'); t.equal(client2.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); - client2.ready().then(() => { + client2.whenReadyFromCache().then((isReady) => { + t.false(isReady, 'It should be ready from cache before ready (syncing with the cloud).'); + }); + client2.whenReady().then(() => { t.true(Date.now() - startTime >= 700, 'It should resolve ready promise after syncing with the cloud.'); t.equal(client2.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); client3.on(client3.Event.SDK_READY, () => { - client3.ready().then(() => { + client3.whenReady().then(() => { t.true(Date.now() - startTime >= 1000, 'It should resolve ready promise after syncing with the cloud.'); t.equal(client3.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); @@ -370,7 +390,7 @@ export default function (fetchMock, assert) { t.equal(client3.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); client3.on(client3.Event.SDK_READY_TIMED_OUT, () => { - client3.ready().catch(() => { + client3.whenReady().catch(() => { t.true(Date.now() - startTime >= 850, 'It should reject ready promise before syncing memberships data with the cloud.'); t.equal(client3.getTreatment('always_on'), 'on', 'It should evaluate treatments with memberships data from cache.'); }); @@ -452,7 +472,11 @@ export default function (fetchMock, assert) { t.true(nearlyEqual(Date.now() - startTime, CLIENT_READY_MS), 'It should emit SDK_READY after syncing with the cloud.'); t.equal(client.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); - client.ready().then(() => { + client.whenReadyFromCache().then((isReady) => { + t.true(isReady, 'It should be ready from cache and ready.'); + t.true(nearlyEqual(Date.now() - startTime, CLIENT_READY_MS), 'It should resolve ready from cache promise after syncing with the cloud.'); + }); + client.whenReady().then(() => { t.true(nearlyEqual(Date.now() - startTime, CLIENT_READY_MS), 'It should resolve ready promise after syncing with the cloud.'); t.equal(client.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); @@ -460,12 +484,12 @@ export default function (fetchMock, assert) { t.true(nearlyEqual(Date.now() - startTime, CLIENT2_READY_MS), 'It should emit SDK_READY after syncing with the cloud.'); t.equal(client2.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); - client2.ready().then(() => { + client2.whenReady().then(() => { t.true(nearlyEqual(Date.now() - startTime, CLIENT2_READY_MS), 'It should resolve ready promise after syncing with the cloud.'); t.equal(client2.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); client3.on(client3.Event.SDK_READY, () => { - client3.ready().then(() => { + client3.whenReady().then(() => { t.true(nearlyEqual(Date.now() - startTime, CLIENT3_READY_MS), 'It should resolve ready promise after syncing with the cloud.'); t.equal(client3.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); @@ -482,7 +506,7 @@ export default function (fetchMock, assert) { t.equal(client3.getTreatment('always_on'), 'on', 'It should evaluate treatments with updated data after syncing with the cloud.'); }); client3.on(client3.Event.SDK_READY_TIMED_OUT, () => { - client3.ready().catch(() => { + client3.whenReady().catch(() => { t.true(Date.now() - startTime >= 850, 'It should reject ready promise before syncing memberships data with the cloud.'); t.equal(client3.getTreatment('always_on'), 'control', 'It should not evaluate treatments with memberships data from cache.'); }); @@ -491,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 }); @@ -528,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'); @@ -553,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 **/ @@ -589,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, () => { @@ -633,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, () => { @@ -734,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, () => { @@ -795,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, () => { @@ -895,10 +923,10 @@ 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.ready(); + await client.whenReady(); t.true(console.log.calledWithMatch('clearOnInit was set and cache was not cleared in the last 24 hours. Cleaning up cache'), 'It should log a message about cleaning up cache'); @@ -935,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__/browserSuites/ready-promise.spec.js b/src/__tests__/browserSuites/ready-promise.spec.js index 3371d1618..f632a177c 100644 --- a/src/__tests__/browserSuites/ready-promise.spec.js +++ b/src/__tests__/browserSuites/ready-promise.spec.js @@ -31,7 +31,7 @@ function assertGetTreatmentWhenReady(assert, client) { function assertGetTreatmentControlNotReady(assert, client) { consoleSpy.log.resetHistory(); assert.equal(client.getTreatment('hierarchical_splits_test'), 'control', 'We should get control if client is not ready.'); - assert.true(consoleSpy.log.calledWithExactly('[WARN] splitio => getTreatment: the SDK is not ready, results may be incorrect for feature flag hierarchical_splits_test. Make sure to wait for SDK readiness before using this method.'), 'Telling us that calling getTreatment would return CONTROL since SDK is not ready at this point.'); + assert.true(consoleSpy.log.calledWithExactly('[WARN] splitio => getTreatment: the SDK is not ready to evaluate. Results may be incorrect for feature flag hierarchical_splits_test. Make sure to wait for SDK readiness before using this method.'), 'Telling us that calling getTreatment would return CONTROL since SDK is not ready at this point.'); } function assertGetTreatmentControlNotReadyOnDestroy(assert, client) { @@ -68,7 +68,7 @@ export default function readyPromiseAssertions(fetchMock, assert) { const splitio = SplitFactory(config); const client = splitio.client(); - client.ready() + client.whenReady() .then(() => { t.fail('### SDK IS READY - not TIMED OUT when it should.'); }) @@ -77,7 +77,7 @@ export default function readyPromiseAssertions(fetchMock, assert) { assertGetTreatmentControlNotReady(t, client); client.destroy().then(() => { - client.ready() + client.whenReady() .then(() => { t.fail('### SDK IS READY - It should not in this scenario.'); t.end(); @@ -118,13 +118,13 @@ export default function readyPromiseAssertions(fetchMock, assert) { // In this case, we use the manager instead of the client to get the ready promise const manager = splitio.manager(); - manager.ready() + manager.whenReady() .then(() => { t.pass('### SDK IS READY - the retry request is under the limits.'); assertGetTreatmentWhenReady(t, client); client.destroy().then(() => { - client.ready() + client.whenReady() .then(() => { t.pass('### SDK IS READY - the promise remains resolved after client destruction.'); assertGetTreatmentControlNotReadyOnDestroy(t, client); @@ -166,7 +166,7 @@ export default function readyPromiseAssertions(fetchMock, assert) { const splitio = SplitFactory(config); const client = splitio.client(); - client.ready() + client.whenReady() .then(() => { t.fail('### SDK IS READY - not TIMED OUT when it should.'); }) @@ -175,13 +175,13 @@ export default function readyPromiseAssertions(fetchMock, assert) { assertGetTreatmentControlNotReady(t, client); setTimeout(() => { - client.ready() + client.whenReady() .then(() => { t.pass('### SDK IS READY - retry attempt finishes before the requestTimeoutBeforeReady limit'); assertGetTreatmentWhenReady(t, client); client.destroy().then(() => { - client.ready() + client.whenReady() .then(() => { t.pass('### SDK IS READY - the promise remains resolved after client destruction.'); assertGetTreatmentControlNotReadyOnDestroy(t, client); @@ -244,7 +244,7 @@ export default function readyPromiseAssertions(fetchMock, assert) { const client = splitio.client(); const nicolasClient = splitio.client('nicolas@split.io'); - client.ready() + client.whenReady() .then(() => { t.fail('### SDK IS READY - not TIMED OUT when it should.'); }) @@ -253,25 +253,25 @@ export default function readyPromiseAssertions(fetchMock, assert) { assertGetTreatmentControlNotReady(t, client); client.on(client.Event.SDK_READY, () => { - client.ready().then(() => { + client.whenReady().then(() => { t.pass('### SDK IS READY - the scheduled refresh changes the client state into "ready"'); assertGetTreatmentWhenReady(t, client); const tStart = Date.now(); - nicolasClient.ready().then(() => { + nicolasClient.whenReady().then(() => { const delta = Date.now() - tStart; t.true(nearlyEqual(delta, 0), 'shared client is ready as soon as main client is ready (i.e., splits have arrived)'); const timeoutClient = splitio.client('emiliano@split.io'); - timeoutClient.ready().then(undefined, () => { // setting onRejected handler via `then` method + timeoutClient.whenReady().then(undefined, () => { // setting onRejected handler via `then` method t.pass('### Shared client TIMED OUT - promise rejected since memberships fetch took more time than readyTimeout'); - timeoutClient.ready().catch(() => { // setting onRejected handler via `catch` method + timeoutClient.whenReady().catch(() => { // setting onRejected handler via `catch` method t.pass('### Shared client TIMED OUT - promise keeps being rejected'); timeoutClient.on(timeoutClient.Event.SDK_READY, () => { - timeoutClient.ready().then(() => { + timeoutClient.whenReady().then(() => { t.pass('### Shared client READY - `ready` returns a new resolved promise'); Promise.all([timeoutClient.destroy(), nicolasClient.destroy(), client.destroy()]).then(() => { - client.ready() + client.whenReady() .then(() => { t.pass('### SDK IS READY - the promise remains resolved after client destruction.'); assertGetTreatmentControlNotReadyOnDestroy(t, client); @@ -318,7 +318,7 @@ export default function readyPromiseAssertions(fetchMock, assert) { const splitio = SplitFactory(config); const client = splitio.client(); - client.ready() + client.whenReady() .then(() => { t.fail('### SDK IS READY - not TIMED OUT when it should.'); client.destroy().then(() => { t.end(); }); @@ -330,7 +330,7 @@ export default function readyPromiseAssertions(fetchMock, assert) { .catch((error) => { t.equal(error, 'error', '### Handled thrown exception on onRejected callback.'); client.destroy().then(() => { - client.ready() + client.whenReady() .then(() => { t.fail('### SDK IS READY - It should not in this scenario.'); t.end(); @@ -368,7 +368,7 @@ export default function readyPromiseAssertions(fetchMock, assert) { const splitio = SplitFactory(config); const client = splitio.client(); - client.ready() + client.whenReady() .then(() => { t.pass('### SDK IS READY as it should, request is under the limits.'); assertGetTreatmentWhenReady(t, client); @@ -377,7 +377,7 @@ export default function readyPromiseAssertions(fetchMock, assert) { .catch((error) => { t.equal(error, 'error', '### Handled thrown exception on onRejected callback.'); client.destroy().then(() => { - client.ready() + client.whenReady() .then(() => { t.pass('### SDK IS READY - the promise remains resolved after client destruction.'); assertGetTreatmentControlNotReadyOnDestroy(t, client); @@ -421,7 +421,7 @@ export default function readyPromiseAssertions(fetchMock, assert) { // `ready` is called immediately. Thus, the 'reject' callback is expected to be called in 0.15 seconds aprox. setTimeout(() => { const tStart = Date.now(); - client.ready() + client.whenReady() .then(() => { t.fail('### SDK IS READY - not TIMED OUT when it should.'); }) @@ -436,7 +436,7 @@ export default function readyPromiseAssertions(fetchMock, assert) { // `ready` is called in 0.15 seconds, when the promise is just rejected. Thus, the 'reject' callback is expected to be called immediately (0 seconds aprox). setTimeout(() => { const tStart = Date.now(); - manager.ready() + manager.whenReady() .then(() => { t.fail('### SDK IS READY - not TIMED OUT when it should.'); }) @@ -451,7 +451,7 @@ export default function readyPromiseAssertions(fetchMock, assert) { // `ready` is called in 0.25 seconds, right after the promise is resolved (0.2 secs). Thus, the 'resolve' callback is expected to be called immediately (0 seconds aprox). setTimeout(() => { const tStart = Date.now(); - manager.ready() + manager.whenReady() .then(() => { t.pass('### SDK IS READY - retry attempt finishes before the requestTimeoutBeforeReady limit'); assertGetTreatmentWhenReady(t, client); @@ -465,7 +465,7 @@ export default function readyPromiseAssertions(fetchMock, assert) { }) .then(() => { client.destroy().then(() => { - client.ready() + client.whenReady() .then(() => { t.pass('### SDK IS READY - the promise remains resolved after client destruction.'); assertGetTreatmentControlNotReadyOnDestroy(t, client); @@ -506,26 +506,24 @@ export default function readyPromiseAssertions(fetchMock, assert) { const splitio = SplitFactory(config); - const onReadycallback = function () { }; + const onReadyCallback = function () { }; // We invoke the ready method and also add and remove SDK_READY event listeners using the client and manager instances const client = splitio.client(); - client.ready(); - client.on(client.Event.SDK_READY, onReadycallback); - client.off(client.Event.SDK_READY, onReadycallback); + client.whenReady().then(() => t.fail('SDK TIMED OUT - Should not resolve')).catch(() => t.pass('SDK TIMED OUT - Should reject')); + client.on(client.Event.SDK_READY, onReadyCallback); + client.off(client.Event.SDK_READY, onReadyCallback); const manager = splitio.manager(); - manager.ready(); - manager.on(manager.Event.SDK_READY, onReadycallback); - manager.off(manager.Event.SDK_READY, onReadycallback); + client.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); consoleSpy.log.resetHistory(); setTimeout(() => { - client.ready(); + client.whenReadyFromCache().then((isReady) => t.true(isReady, 'SDK IS READY (& READY FROM CACHE) - Should resolve')).catch(() => t.fail('SDK TIMED OUT - Should not reject')); assertGetTreatmentWhenReady(t, client); - t.true(consoleSpy.log.calledWithExactly('[WARN] splitio => No listeners for SDK Readiness detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet ready.'), - 'Warning that there are not listeners for SDK_READY event'); // assert error messages when adding event listeners after SDK has already triggered them consoleSpy.log.resetHistory(); @@ -540,15 +538,15 @@ export default function readyPromiseAssertions(fetchMock, assert) { consoleSpy.log.resetHistory(); const sharedClientWithCb = splitio.client('nicolas@split.io'); sharedClientWithCb.on(client.Event.SDK_READY, () => { - t.false(consoleSpy.log.calledWithExactly('[WARN] splitio => No listeners for SDK Readiness detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet ready.'), + t.false(consoleSpy.log.calledWithExactly('[WARN] splitio => No listeners for SDK_READY event detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet synchronized with the backend.'), 'No warning logged'); const sharedClientWithoutCb = splitio.client('emiliano@split.io'); setTimeout(() => { - t.true(consoleSpy.log.calledWithExactly('[WARN] splitio => No listeners for SDK Readiness detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet ready.'), + t.true(consoleSpy.log.calledWithExactly('[WARN] splitio => No listeners for SDK_READY event detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet synchronized with the backend.'), 'Warning logged'); Promise.all([sharedClientWithoutCb.destroy(), client.destroy()]).then(() => { - client.ready() + client.whenReady() .then(() => { t.pass('### SDK IS READY - the promise remains resolved after client destruction.'); assertGetTreatmentControlNotReadyOnDestroy(t, client); @@ -594,18 +592,12 @@ export default function readyPromiseAssertions(fetchMock, assert) { // Assert getTreatment return CONTROL and trigger warning when SDK is not ready yet assertGetTreatmentControlNotReady(t, client); - client.ready() - .then(() => { - t.fail('### SDK IS READY - not TIMED OUT when it should.'); - }); - otherClient.ready() - .then(() => { - t.fail('### SDK IS READY - not TIMED OUT when it should.'); - }); + client.whenReady().then(() => t.fail('SDK TIMED OUT - Should not resolve')).catch(() => t.pass('SDK TIMED OUT - Should reject')); + otherClient.whenReady().then(() => t.fail('SDK TIMED OUT - Should not resolve')).catch(() => t.pass('SDK TIMED OUT - Should reject')); setTimeout(() => { Promise.all([client.destroy(), otherClient.destroy()]).then(() => { - client.ready() + client.whenReady() .then(() => { t.fail('### SDK IS READY - It should not in this scenario.'); t.end(); diff --git a/src/__tests__/browserSuites/telemetry.spec.js b/src/__tests__/browserSuites/telemetry.spec.js index 29facccf4..a1a64b4fd 100644 --- a/src/__tests__/browserSuites/telemetry.spec.js +++ b/src/__tests__/browserSuites/telemetry.spec.js @@ -105,14 +105,14 @@ export default async function telemetryBrowserSuite(fetchMock, t) { const data = JSON.parse(opts.body); assert.true(data.tR > 0, 'timeUntilReady is larger than 0'); - delete data.tR; // delete to validate other properties + assert.true(data.tC > 0, 'timeUntilReadyFromCache is larger than 0'); assert.deepEqual(data, { oM: 0, st: 'memory', aF: 1, rF: 0, sE: false, rR: { sp: 99999, ms: 60, im: 300, ev: 60, te: 1 } /* override featuresRefreshRate */, uO: { s: true, e: true, a: false, st: false, t: true } /* override sdk, events and telemetry URLs */, iQ: 30000, eQ: 500, iM: 0, iL: false, hP: false, nR: 1 /* 1 non ready usage */, t: [], uC: 2 /* Default GRANTED */, - fsT: 0, fsI: 0 + fsT: 0, fsI: 0, tR: data.tR, tC: data.tC }, 'metrics/config JSON payload should be the expected'); finish.next(); 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 c4d94c1d2..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: false, 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: false, 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: false, 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: false, 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: false, 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: false, 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 7d0f35619..6e2ee9c91 100644 --- a/src/__tests__/nodeSuites/ready-promise.spec.js +++ b/src/__tests__/nodeSuites/ready-promise.spec.js @@ -28,7 +28,7 @@ function assertGetTreatmentWhenReady(assert, client, key) { function assertGetTreatmentControlNotReady(assert, client, key) { consoleSpy.log.resetHistory(); assert.equal(client.getTreatment(key, 'hierarchical_splits_test'), 'control', 'We should get control if client is not ready.'); - assert.true(consoleSpy.log.calledWithExactly('[WARN] splitio => getTreatment: the SDK is not ready, results may be incorrect for feature flag hierarchical_splits_test. Make sure to wait for SDK readiness before using this method.'), 'Telling us that calling getTreatment would return CONTROL since SDK is not ready at this point.'); + assert.true(consoleSpy.log.calledWithExactly('[WARN] splitio => getTreatment: the SDK is not ready to evaluate. Results may be incorrect for feature flag hierarchical_splits_test. Make sure to wait for SDK readiness before using this method.'), 'Telling us that calling getTreatment would return CONTROL since SDK is not ready at this point.'); } function assertGetTreatmentControlNotReadyOnDestroy(assert, client, key) { @@ -64,7 +64,7 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { const splitio = SplitFactory(config); const client = splitio.client(); - client.ready() + client.whenReady() .then(() => { t.fail('### SDK IS READY - not TIMED OUT when it should.'); }) @@ -73,7 +73,7 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { assertGetTreatmentControlNotReady(t, client, key); client.destroy().then(() => { - client.ready() + client.whenReady() .then(() => { t.fail('### SDK IS READY - It should not in this scenario.'); t.end(); @@ -114,13 +114,13 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { // In this case, we use the manager instead of the client to get the ready promise const manager = splitio.manager(); - manager.ready() + manager.whenReady() .then(() => { t.pass('### SDK IS READY - the retry request is under the limits.'); assertGetTreatmentWhenReady(t, client, key); client.destroy().then(() => { - client.ready() + client.whenReady() .then(() => { t.pass('### SDK IS READY - the promise remains resolved after client destruction.'); assertGetTreatmentControlNotReadyOnDestroy(t, client, key); @@ -162,7 +162,7 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { const splitio = SplitFactory(config); const client = splitio.client(); - client.ready() + client.whenReady() .then(() => { t.fail('### SDK IS READY - not TIMED OUT when it should.'); }) @@ -171,13 +171,13 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { assertGetTreatmentControlNotReady(t, client, key); setTimeout(() => { - client.ready() + client.whenReady() .then(() => { t.pass('### SDK IS READY - retry attempt finishes before the requestTimeoutBeforeReady limit'); assertGetTreatmentWhenReady(t, client, key); client.destroy().then(() => { - client.ready() + client.whenReady() .then(() => { t.pass('### SDK IS READY - the promise remains resolved after client destruction.'); assertGetTreatmentControlNotReadyOnDestroy(t, client, key); @@ -232,7 +232,7 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { const splitio = SplitFactory(config); const client = splitio.client(); - client.ready() + client.whenReady() .then(() => { t.fail('### SDK IS READY - not TIMED OUT when it should.'); }) @@ -241,13 +241,13 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { assertGetTreatmentControlNotReady(t, client, key); setTimeout(() => { - client.ready() + client.whenReady() .then(() => { t.pass('### SDK IS READY - the scheduled refresh changes the client state into "is ready"'); assertGetTreatmentWhenReady(t, client, key); client.destroy().then(() => { - client.ready() + client.whenReady() .then(() => { t.pass('### SDK IS READY - the promise remains resolved after client destruction.'); assertGetTreatmentControlNotReadyOnDestroy(t, client, key); @@ -289,7 +289,7 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { const splitio = SplitFactory(config); const client = splitio.client(); - client.ready() + client.whenReady() .then(() => { t.fail('### SDK IS READY - not TIMED OUT when it should.'); client.destroy().then(() => { t.end(); }); @@ -301,7 +301,7 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { .catch((error) => { t.equal(error, 'error', '### Handled thrown exception on onRejected callback.'); client.destroy().then(() => { - client.ready() + client.whenReady() .then(() => { t.fail('### SDK IS READY - It should not in this scenario.'); t.end(); @@ -339,7 +339,7 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { const splitio = SplitFactory(config); const client = splitio.client(); - client.ready() + client.whenReady() .then(() => { t.pass('### SDK IS READY as it should, request is under the limits.'); assertGetTreatmentWhenReady(t, client, key); @@ -348,7 +348,7 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { .catch((error) => { t.equal(error, 'error', '### Handled thrown exception on onRejected callback.'); client.destroy().then(() => { - client.ready() + client.whenReady() .then(() => { t.pass('### SDK IS READY - the promise remains resolved after client destruction.'); assertGetTreatmentControlNotReadyOnDestroy(t, client, key); @@ -392,7 +392,7 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { // `ready` is called immediately. Thus, the 'reject' callback is expected to be called in 0.15 seconds aprox. setTimeout(() => { const tStart = Date.now(); - client.ready() + client.whenReady() .then(() => { t.fail('### SDK IS READY - not TIMED OUT when it should.'); }) @@ -407,7 +407,7 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { // `ready` is called in 0.15 seconds, when the promise is just rejected. Thus, the 'reject' callback is expected to be called immediately (0 seconds aprox). setTimeout(() => { const tStart = Date.now(); - manager.ready() + manager.whenReady() .then(() => { t.fail('### SDK IS READY - not TIMED OUT when it should.'); }) @@ -422,7 +422,7 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { // `ready` is called in 0.25 seconds, right after the promise is resolved (0.2 secs). Thus, the 'resolve' callback is expected to be called immediately (0 seconds aprox). setTimeout(() => { const tStart = Date.now(); - manager.ready() + manager.whenReady() .then(() => { t.pass('### SDK IS READY - retry attempt finishes before the requestTimeoutBeforeReady limit'); assertGetTreatmentWhenReady(t, client, key); @@ -436,7 +436,7 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { }) .then(() => { client.destroy().then(() => { - client.ready() + client.whenReady() .then(() => { t.pass('### SDK IS READY - the promise remains resolved after client destruction.'); assertGetTreatmentControlNotReadyOnDestroy(t, client, key); @@ -475,26 +475,24 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { const splitio = SplitFactory(config); - const onReadycallback = function () { }; + const onReadyCallback = function () { }; // We invoke the ready method and also add and remove SDK_READY event listeners using the client and manager instances const client = splitio.client(); - client.ready(); - client.on(client.Event.SDK_READY, onReadycallback); - client.off(client.Event.SDK_READY, onReadycallback); + client.whenReady().then(() => t.fail('SDK TIMED OUT - Should not resolve')).catch(() => t.pass('SDK TIMED OUT - Should reject')); + client.on(client.Event.SDK_READY, onReadyCallback); + client.off(client.Event.SDK_READY, onReadyCallback); const manager = splitio.manager(); - manager.ready(); - manager.on(manager.Event.SDK_READY, onReadycallback); - manager.off(manager.Event.SDK_READY, onReadycallback); + 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); consoleSpy.log.resetHistory(); setTimeout(() => { - client.ready(); + client.whenReadyFromCache().then((isReady) => t.true(isReady, 'SDK IS READY (& READY FROM CACHE) - Should resolve')).catch(() => t.fail('SDK TIMED OUT - Should not reject')); assertGetTreatmentWhenReady(t, client, key); - t.true(consoleSpy.log.calledWithExactly('[WARN] splitio => No listeners for SDK Readiness detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet ready.'), - 'Warning that there are not listeners for SDK_READY event'); // assert error messages when adding event listeners after SDK has already triggered them consoleSpy.log.resetHistory(); @@ -506,7 +504,7 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { 'Logging error that a listeners for SDK_READY_TIMED_OUT event was added after triggered'); client.destroy().then(() => { - client.ready() + client.whenReady() .then(() => { t.pass('### SDK IS READY - the promise remains resolved after client destruction.'); assertGetTreatmentControlNotReadyOnDestroy(t, client, key); @@ -549,14 +547,11 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { // Assert getTreatment return CONTROL and trigger warning when SDK is not ready yet assertGetTreatmentControlNotReady(t, client, key); - client.ready() - .then(() => { - t.fail('### SDK IS READY - not TIMED OUT when it should.'); - }); + client.whenReady().then(() => t.fail('SDK TIMED OUT - Should not resolve')).catch(() => t.pass('SDK TIMED OUT - Should reject')); setTimeout(() => { client.destroy().then(() => { - client.ready() + client.whenReady() .then(() => { t.fail('### SDK IS READY - It should not in this scenario.'); t.end(); diff --git a/src/__tests__/nodeSuites/telemetry.spec.js b/src/__tests__/nodeSuites/telemetry.spec.js index 85f7cdf67..e73f8c4f1 100644 --- a/src/__tests__/nodeSuites/telemetry.spec.js +++ b/src/__tests__/nodeSuites/telemetry.spec.js @@ -94,14 +94,14 @@ export default async function telemetryNodejsSuite(key, fetchMock, assert) { const data = JSON.parse(opts.body); assert.true(data.tR > 0, 'timeUntilReady is larger than 0'); - delete data.tR; // delete to validate other properties + assert.true(data.tC > 0, 'timeUntilReadyFromCache is larger than 0'); assert.deepEqual(data, { oM: 0, st: 'memory', aF: 1, rF: 0, sE: false, rR: { sp: 99999, se: 60, im: 300, ev: 60, te: 1 } /* override featuresRefreshRate */, uO: { s: true, e: true, a: false, st: false, t: true } /* override sdk, events and telemetry URLs */, iQ: 30000, eQ: 500, iM: 0, iL: false, hP: false, nR: 1 /* 1 non ready usage */, t: [], uC: 0 /* NA */, - fsT: 0, fsI: 0 + fsT: 0, fsI: 0, tR: data.tR, tC: data.tC }, 'metrics/config JSON payload should be the expected'); finish.next(); diff --git a/src/__tests__/offline/browser.spec.js b/src/__tests__/offline/browser.spec.js index 8507e080b..8edd7d50c 100644 --- a/src/__tests__/offline/browser.spec.js +++ b/src/__tests__/offline/browser.spec.js @@ -114,23 +114,25 @@ 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, false, 'READY status must not be set before READY_FROM_CACHE'); + 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++; assert.deepEqual(manager.names(), ['testing_split', 'testing_split_with_config']); assert.equal(client.getTreatment('testing_split_with_config'), 'off'); - readyFromCacheCount++; client.on(client.Event.SDK_READY_FROM_CACHE, () => { assert.fail('It should not emit SDK_READY_FROM_CACHE again'); }); - const newClient = factory.client('another'); - assert.equal(newClient.getTreatment('testing_split_with_config'), 'off', 'It should evaluate treatments with data from cache instead of control'); - newClient.on(newClient.Event.SDK_READY_FROM_CACHE, () => { - assert.fail('It should not emit SDK_READY_FROM_CACHE if already done.'); - }); + if (configs[i].storage && configs[i].storage.type === 'LOCALSTORAGE') { + const newClient = factory.client('another'); + assert.equal(newClient.getTreatment('testing_split_with_config'), 'off', 'It should evaluate treatments with data from cache instead of control'); + newClient.on(newClient.Event.SDK_READY_FROM_CACHE, () => { + assert.fail('It should not emit SDK_READY_FROM_CACHE if already done.'); + }); + } }; client.on(client.Event.SDK_READY_FROM_CACHE, sdkReadyFromCache(client)); 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..378697e28 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.8.0'; 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' + } } };