diff --git a/package-lock.json b/package-lock.json index ea3c1e993..45142afd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "10.15.10-rc.0", + "version": "10.15.10-rc.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index fb775ca82..67a59e76a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "10.15.10-rc.0", + "version": "10.15.10-rc.1", "description": "Split SDK", "files": [ "README.md", diff --git a/src/__tests__/browserSuites/ready-from-cache.spec.js b/src/__tests__/browserSuites/ready-from-cache.spec.js index 8f556e170..647ce05b2 100644 --- a/src/__tests__/browserSuites/ready-from-cache.spec.js +++ b/src/__tests__/browserSuites/ready-from-cache.spec.js @@ -183,20 +183,20 @@ export default function (fetchMock, assert) { t.equal(client.getTreatment('always_on'), 'control', 'It should evaluate control treatments if not ready neither by cache nor the cloud'); t.equal(client3.getTreatment('always_on'), 'control', 'It should evaluate control treatments if not ready neither by cache nor the cloud'); - client.once(client.Event.SDK_READY_TIMED_OUT, () => { + client.on(client.Event.SDK_READY_TIMED_OUT, () => { t.fail('It should not timeout in this scenario.'); t.end(); }); - client.once(client.Event.SDK_READY_FROM_CACHE, () => { + 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.equal(client.getTreatment('always_on'), 'off', 'It should evaluate treatments with data from cache instead of control due to Input Validation'); }); - client2.once(client2.Event.SDK_READY_FROM_CACHE, () => { + client2.on(client2.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.equal(client2.getTreatment('always_on'), 'off', 'It should evaluate treatments with data from cache instead of control due to Input Validation'); }); - client3.once(client3.Event.SDK_READY_FROM_CACHE, () => { + client3.on(client3.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.equal(client3.getTreatment('always_on'), 'off', 'It should evaluate treatments with data from cache instead of control due to Input Validation'); }); @@ -248,7 +248,7 @@ export default function (fetchMock, assert) { events: 'https://events.baseurl/readyFromCacheWithData3' }; localStorage.clear(); - t.plan(12 * 2 + 4); + t.plan(12 * 2 + 5); fetchMock.get(testUrls.sdk + '/splitChanges?since=25', function () { t.equal(localStorage.getItem('readyFromCache_3.SPLITIO.split.always_on'), alwaysOnSplitInverted, 'splits must not be cleaned from cache'); @@ -264,6 +264,7 @@ export default function (fetchMock, assert) { fetchMock.get(testUrls.sdk + '/mySegments/nicolas3%40split.io', function () { return new Promise(res => { setTimeout(() => res({ status: 200, body: { 'mySegments': [] }, headers: {} }), 1000); }); // Third client mySegments will come after 1s }); + fetchMock.get(testUrls.sdk + '/mySegments/nicolas4%40split.io', { 'mySegments': [] }); fetchMock.postOnce(testUrls.events + '/testImpressions/bulk', 200); fetchMock.postOnce(testUrls.events + '/testImpressions/count', 200); @@ -292,20 +293,27 @@ export default function (fetchMock, assert) { t.equal(client.getTreatment('always_on'), 'control', 'It should evaluate control treatments if not ready neither by cache nor the cloud'); t.equal(client3.getTreatment('always_on'), 'control', 'It should evaluate control treatments if not ready neither by cache nor the cloud'); - client.once(client.Event.SDK_READY_TIMED_OUT, () => { + client.on(client.Event.SDK_READY_TIMED_OUT, () => { t.fail('It should not timeout in this scenario.'); t.end(); }); - client.once(client.Event.SDK_READY_FROM_CACHE, () => { + 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.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'); + t.equal(client4.getTreatment('always_on'), 'off', 'It should evaluate treatments with data from cache instead of control'); + + client4.on(client4.Event.SDK_READY_FROM_CACHE, () => { + t.fail('It should not emit SDK_READY_FROM_CACHE if already done.'); + }); }); - client2.once(client2.Event.SDK_READY_FROM_CACHE, () => { + client2.on(client2.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.equal(client2.getTreatment('always_on'), 'off', 'It should evaluate treatments with data from cache instead of control due to Input Validation'); }); - client3.once(client3.Event.SDK_READY_FROM_CACHE, () => { + client3.on(client3.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.equal(client3.getTreatment('always_on'), 'off', 'It should evaluate treatments with data from cache instead of control due to Input Validation'); }); diff --git a/src/__tests__/offline/browser.spec.js b/src/__tests__/offline/browser.spec.js index 186510c7e..721d98dec 100644 --- a/src/__tests__/offline/browser.spec.js +++ b/src/__tests__/offline/browser.spec.js @@ -71,8 +71,8 @@ tape('Browser offline mode', function (assert) { assert.false(client.track({}, [], 'invalid_stuff')); assert.true(sharedClient.track('a_tt', 'another_ev_id', 10)); - assert.equal(client.getTreatment('testing_split'), 'control'); - assert.equal(sharedClient.getTreatment('testing_split'), 'control'); + assert.equal(client.getTreatment('testing_split'), 'control', 'control due to not ready'); + assert.equal(sharedClient.getTreatment('testing_split'), 'control', 'control due to not ready'); assert.equal(manager.splits().length, 0); // SDK events on shared client @@ -90,12 +90,13 @@ tape('Browser offline mode', function (assert) { const factories = [ SplitFactory(config), SplitFactory({ ...config }), - SplitFactory({ ...config, features: { ...config.features } }) + SplitFactory({ ...config, features: { ...config.features } }), + SplitFactory({ ...config, storage: { type: 'LOCALSTORAGE' } }) ]; - let readyCount = 0, updateCount = 0; + let readyCount = 0, updateCount = 0, readyFromCacheCount = 0; for (let i = 0; i < factories.length; i++) { - let client = factories[i].client(), manager = factories[i].manager(); + const factory = factories[i], client = factory.client(), manager = factory.manager(), client2 = factory.client('other'); client.on(client.Event.SDK_READY, () => { assert.deepEqual(manager.names(), ['testing_split', 'testing_split_with_config']); @@ -103,10 +104,32 @@ tape('Browser offline mode', function (assert) { readyCount++; }); client.on(client.Event.SDK_UPDATE, () => { - assert.equal(client.getTreatment('testing_split_with_config'), 'nope'); assert.deepEqual(manager.names(), ['testing_split', 'testing_split_2', 'testing_split_3', 'testing_split_with_config']); + assert.equal(client.getTreatment('testing_split_with_config'), 'nope'); updateCount++; }); + + const sdkReadyFromCache = (client) => () => { + assert.equal(client.__context.get(client.__context.constants.READY_FROM_CACHE, true), true, 'If ready from cache, READY_FROM_CACHE status must be true'); + assert.equal(client.__context.get(client.__context.constants.READY, true), undefined, 'READY status must not be set before READY_FROM_CACHE'); + + 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.'); + }); + }; + + client.on(client.Event.SDK_READY_FROM_CACHE, sdkReadyFromCache(client)); + client2.on(client2.Event.SDK_READY_FROM_CACHE, sdkReadyFromCache(client2)); } client.once(client.Event.SDK_READY, function () { @@ -140,10 +163,10 @@ tape('Browser offline mode', function (assert) { // Manager tests const expectedSplitView1 = { - name: 'testing_split', trafficType: null, killed: false, changeNumber: 0, treatments: ['on'], configs: {} + name: 'testing_split', trafficType: 'localhost', killed: false, changeNumber: 0, treatments: ['on'], configs: {} }; const expectedSplitView2 = { - name: 'testing_split_with_config', trafficType: null, killed: false, changeNumber: 0, treatments: ['off'], configs: { off: '{ "color": "blue" }' } + name: 'testing_split_with_config', trafficType: 'localhost', killed: false, changeNumber: 0, treatments: ['off'], configs: { off: '{ "color": "blue" }' } }; assert.deepEqual(manager.names(), ['testing_split', 'testing_split_with_config']); assert.deepEqual(manager.split('testing_split'), expectedSplitView1); @@ -241,7 +264,7 @@ tape('Browser offline mode', function (assert) { // Manager tests const expectedSplitView3 = { - name: 'testing_split_with_config', trafficType: null, killed: false, changeNumber: 0, treatments: ['nope'], configs: {} + name: 'testing_split_with_config', trafficType: 'localhost', killed: false, changeNumber: 0, treatments: ['nope'], configs: {} }; assert.deepEqual(manager.names(), ['testing_split', 'testing_split_2', 'testing_split_3', 'testing_split_with_config']); assert.deepEqual(manager.split('testing_split'), expectedSplitView1); @@ -310,6 +333,7 @@ tape('Browser offline mode', function (assert) { // SDK events on other factory clients assert.equal(readyCount, factories.length, 'Each factory client should have emitted SDK_READY event once'); assert.equal(updateCount, factories.length - 1, 'Each factory client except one should have emitted SDK_UPDATE event once'); + assert.equal(readyFromCacheCount, 2, 'The main and shared client of the factory with LOCALSTORAGE should have emitted SDK_READY_FROM_CACHE event'); assert.end(); }); diff --git a/src/__tests__/offline/node.spec.js b/src/__tests__/offline/node.spec.js index ac2b8ad79..ed3453292 100644 --- a/src/__tests__/offline/node.spec.js +++ b/src/__tests__/offline/node.spec.js @@ -251,15 +251,15 @@ function ManagerDotSplitTests(assert) { assert.deepEqual(manager.names(), ['testing_split', 'testing_split2', 'testing_split3']); const expectedView1 = { - name: 'testing_split', changeNumber: 0, killed: false, trafficType: null, + name: 'testing_split', changeNumber: 0, killed: false, trafficType: 'localhost', treatments: ['on'], configs: {} }; const expectedView2 = { - name: 'testing_split2', changeNumber: 0, killed: false, trafficType: null, + name: 'testing_split2', changeNumber: 0, killed: false, trafficType: 'localhost', treatments: ['off'], configs: {} }; const expectedView3 = { - name: 'testing_split3', changeNumber: 0, killed: false, trafficType: null, + name: 'testing_split3', changeNumber: 0, killed: false, trafficType: 'localhost', treatments: ['custom_treatment'], configs: {} }; @@ -397,15 +397,15 @@ function MultipleInstancesTests(assert) { assert.deepEqual(manager.names(), ['testing_split', 'testing_split2', 'testing_split3']); const expectedView1 = { - name: 'testing_split', changeNumber: 0, killed: false, trafficType: null, + name: 'testing_split', changeNumber: 0, killed: false, trafficType: 'localhost', treatments: ['on'], configs: {} }; const expectedView2 = { - name: 'testing_split2', changeNumber: 0, killed: false, trafficType: null, + name: 'testing_split2', changeNumber: 0, killed: false, trafficType: 'localhost', treatments: ['off'], configs: {} }; const expectedView3 = { - name: 'testing_split3', changeNumber: 0, killed: false, trafficType: null, + name: 'testing_split3', changeNumber: 0, killed: false, trafficType: 'localhost', treatments: ['custom_treatment'], configs: {} }; diff --git a/src/impressions/hasher/__tests__/buildKey.spec.js b/src/impressions/hasher/__tests__/buildKey.spec.js new file mode 100644 index 000000000..8c93e8439 --- /dev/null +++ b/src/impressions/hasher/__tests__/buildKey.spec.js @@ -0,0 +1,17 @@ +import tape from 'tape-catch'; +import { buildKey } from '../buildKey'; + +tape('buildKey', assert => { + assert.equal(buildKey({}), 'undefined:undefined:undefined:undefined:undefined'); + + const imp = { + feature: 'feature_0', + keyName: 'key_0', + changeNumber: 0, + label: 'in segment all', + treatment: 'someTreatment', + }; + assert.equal(buildKey(imp), 'key_0:feature_0:someTreatment:in segment all:0'); + + assert.end(); +}); diff --git a/src/impressions/hasher/hashImpression32.js b/src/impressions/hasher/hashImpression32.js index a424bef1d..7b0833135 100644 --- a/src/impressions/hasher/hashImpression32.js +++ b/src/impressions/hasher/hashImpression32.js @@ -2,5 +2,5 @@ import murmur from '../../engine/engine/murmur3/murmur3'; import { buildKey } from './buildKey'; export function hashImpression32(impression) { - return murmur.hash(buildKey(impression)).toString(); + return murmur.hash(buildKey(impression)); } diff --git a/src/metrics/index.js b/src/metrics/index.js index 2147c37ba..908b881ba 100644 --- a/src/metrics/index.js +++ b/src/metrics/index.js @@ -49,7 +49,7 @@ const MetricsFactory = context => { const pushMetrics = () => { if (storage.metrics.isEmpty() && storage.count.isEmpty()) return Promise.resolve(); - log.info('Pushing metrics'); + log.debug('Pushing metrics'); const latencyTrackerStop = tracker.start(tracker.TaskNames.METRICS_PUSH); // POST latencies diff --git a/src/producer/updater/SplitChanges.js b/src/producer/updater/SplitChanges.js index 909d3e6ab..6acc512b4 100644 --- a/src/producer/updater/SplitChanges.js +++ b/src/producer/updater/SplitChanges.js @@ -120,8 +120,11 @@ export default function SplitChangesUpdaterFactory(context, isNode = false) { return false; }); - // After triggering the requests, if we have cached splits information let's notify that. - if (startingUp && storage.splits.checkCache()) splitsEventEmitter.emit(splitsEventEmitter.SDK_SPLITS_CACHE_LOADED); + // After triggering the requests, if we have cached splits information let's notify + // that asynchronously, to let attach a listener for SDK_READY_FROM_CACHE + if (startingUp && storage.splits.checkCache()) { + setTimeout(splitsEventEmitter.emit(splitsEventEmitter.SDK_SPLITS_CACHE_LOADED), 0); + } return fetcherPromise; } diff --git a/src/producer/updater/SplitChangesFromObject.js b/src/producer/updater/SplitChangesFromObject.js index f61e2cc4e..e11c6b9b4 100644 --- a/src/producer/updater/SplitChangesFromObject.js +++ b/src/producer/updater/SplitChangesFromObject.js @@ -24,7 +24,7 @@ function FromObjectUpdaterFactory(Fetcher, context) { [context.constants.STORAGE]: storage } = context.getAll(); - let firstTime = true; + let startingUp = true; return function ObjectUpdater() { const splits = []; @@ -58,13 +58,17 @@ function FromObjectUpdaterFactory(Fetcher, context) { }); return Promise.all([ - storage.splits.flush(), + storage.splits.flush(), // required to sync removed splits from mock + storage.splits.setChangeNumber(Date.now()), storage.splits.addSplits(splits) ]).then(() => { readiness.splits.emit(readiness.splits.SDK_SPLITS_ARRIVED); - // Only emits SDK_SEGMENTS_ARRIVED the first time for SDK_READY - if (firstTime) { - firstTime = false; + + if (startingUp) { + startingUp = false; + // Emits SDK_READY_FROM_CACHE + if (storage.splits.checkCache()) readiness.splits.emit(readiness.splits.SDK_SPLITS_CACHE_LOADED); + // Only emits SDK_SEGMENTS_ARRIVED the first time for SDK_READY readiness.segments.emit(readiness.segments.SDK_SEGMENTS_ARRIVED); } }); diff --git a/src/readiness/index.js b/src/readiness/index.js index 773a1f41e..c31c8ed57 100644 --- a/src/readiness/index.js +++ b/src/readiness/index.js @@ -54,8 +54,7 @@ function GateContext() { }); splits.once(Events.SDK_SPLITS_CACHE_LOADED, () => { - // Make it async - setTimeout(() => gate.emit(Events.SDK_READY_FROM_CACHE), 0); + gate.emit(Events.SDK_READY_FROM_CACHE); }); segments.on(Events.SDK_SEGMENTS_ARRIVED, () => { diff --git a/src/services/splitChanges/offline/browser.js b/src/services/splitChanges/offline/browser.js index 3273764a5..bc6673e52 100644 --- a/src/services/splitChanges/offline/browser.js +++ b/src/services/splitChanges/offline/browser.js @@ -54,6 +54,7 @@ export default function createGetConfigurationFromSettings() { if (config !== null) configurations[treatment] = config; splitObjects[splitName] = { + trafficTypeName: 'localhost', conditions: [parseCondition({ treatment })], configurations }; diff --git a/src/services/splitChanges/offline/node.js b/src/services/splitChanges/offline/node.js index 1386285fd..1037adfe8 100644 --- a/src/services/splitChanges/offline/node.js +++ b/src/services/splitChanges/offline/node.js @@ -111,7 +111,7 @@ export default function createGetSplitConfigForFile() { } else { const splitName = tuple[SPLIT_POSITION]; const condition = parseCondition({ treatment: tuple[TREATMENT_POSITION] }); - accum[splitName] = { conditions: [condition], configurations: {} }; + accum[splitName] = { conditions: [condition], configurations: {}, trafficTypeName: 'localhost' }; } } diff --git a/src/sync/PushManager/index.js b/src/sync/PushManager/index.js index 9e58fb608..e7e9ed66e 100644 --- a/src/sync/PushManager/index.js +++ b/src/sync/PushManager/index.js @@ -206,7 +206,6 @@ export default function PushManagerFactory(context, clientContexts /* undefined pushEmitter.on(SPLIT_UPDATE, splitUpdateWorker.put); if (clientContexts) { // browser - // @TODO remove pushEmitter.on(MY_SEGMENTS_UPDATE, function handleMySegmentsUpdate(parsedData, channel) { const userKeyHash = channel.split('_')[2]; const userKey = userKeyHashes[userKeyHash]; @@ -231,8 +230,7 @@ export default function PushManagerFactory(context, clientContexts /* undefined forOwn(clients, ({ hash64, worker }) => { if (isInBitmap(bitmap, hash64.hex)) { - // fetch mySegments - worker.put(parsedData.changeNumber); + worker.put(parsedData.changeNumber); // fetch mySegments } }); return; diff --git a/src/sync/SSEHandler/index.js b/src/sync/SSEHandler/index.js index 993e5916c..fededd0cd 100644 --- a/src/sync/SSEHandler/index.js +++ b/src/sync/SSEHandler/index.js @@ -77,7 +77,6 @@ export default function SSEHandlerFactory(pushEmitter) { parsedData.changeNumber, parsedData.segmentName); break; - // @TODO remove case MY_SEGMENTS_UPDATE: pushEmitter.emit(MY_SEGMENTS_UPDATE, parsedData, diff --git a/src/utils/__tests__/settings/index.spec.js b/src/utils/__tests__/settings/index.spec.js index 18129c8c4..83447a9be 100644 --- a/src/utils/__tests__/settings/index.spec.js +++ b/src/utils/__tests__/settings/index.spec.js @@ -35,6 +35,7 @@ tape('SETTINGS / check defaults', assert => { }); assert.equal(settings.sync.impressionsMode, OPTIMIZED); assert.equal(settings.version, `${language}-${packageJson.version}`); + assert.true(packageJson.version.length <= 16, 'SDK version must not exceed 16 chars length'); assert.end(); }); diff --git a/src/utils/settings/index.js b/src/utils/settings/index.js index bac1c8aed..b8828c052 100644 --- a/src/utils/settings/index.js +++ b/src/utils/settings/index.js @@ -27,7 +27,7 @@ import { API } from '../../utils/logger'; import { STANDALONE_MODE, STORAGE_MEMORY, CONSUMER_MODE, OPTIMIZED } from '../../utils/constants'; import validImpressionsMode from './impressionsMode'; -const version = '10.15.10-rc.0'; +const version = '10.15.10-rc.1'; const eventsEndpointMatcher = /^\/(testImpressions|metrics|events)/; const authEndpointMatcher = /^\/v2\/auth/; const streamingEndpointMatcher = /^\/(sse|event-stream)/; diff --git a/src/utils/settings/storage/browser.js b/src/utils/settings/storage/browser.js index 2618e784b..0b90d5b05 100644 --- a/src/utils/settings/storage/browser.js +++ b/src/utils/settings/storage/browser.js @@ -18,14 +18,12 @@ import logFactory from '../../../utils/logger'; const log = logFactory('splitio-settings'); import isLocalStorageAvailable from '../../../utils/localstorage/isAvailable'; import { - LOCALHOST_MODE, STORAGE_MEMORY, STORAGE_LOCALSTORAGE } from '../../../utils/constants'; const ParseStorageSettings = settings => { let { - mode, storage: { type = STORAGE_MEMORY, options = {}, @@ -39,11 +37,6 @@ const ParseStorageSettings = settings => { prefix = 'SPLITIO'; } - if (mode === LOCALHOST_MODE) return { - type: STORAGE_MEMORY, - prefix - }; - // If an invalid storage type is provided OR we want to use LOCALSTORAGE and // it's not available, fallback into MEMORY if (type !== STORAGE_MEMORY && type !== STORAGE_LOCALSTORAGE ||