diff --git a/CHANGES.txt b/CHANGES.txt index f988a347d..fdbf0d362 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,9 @@ 11.2.0 (March 28, 2025) - - Added new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impression object sent to Split's backend. Read more in our docs. + - Added a new optional argument to the client `getTreatment` methods to allow passing additional evaluation options, such as a map of properties to append to the generated impressions sent to Split backend. Read more in our docs. + - Added two new configuration options for the SDK's `LOCALSTORAGE` storage type to control the behavior of the persisted rollout plan cache in the browser: + - `storage.expirationDays` to specify the validity period of the rollout plan cache in days. + - `storage.clearOnInit` to clear the rollout plan cache on SDK initialization. + - Updated SDK_READY_FROM_CACHE event when using the `LOCALSTORAGE` storage type to be emitted alongside the SDK_READY event if it has not already been emitted. 11.1.0 (January 17, 2025) - Added support for the new impressions tracking toggle available on feature flags, both respecting the setting and including the new field being returned on `SplitView` type objects. Read more in our docs. diff --git a/package-lock.json b/package-lock.json index 1a846b344..a5142e979 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@splitsoftware/splitio", - "version": "11.1.1-rc.0", + "version": "11.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio", - "version": "11.1.1-rc.0", + "version": "11.2.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio-commons": "2.1.1-rc.1", + "@splitsoftware/splitio-commons": "2.2.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.1.1-rc.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.1.1-rc.1.tgz", - "integrity": "sha512-+I5K5qmF8YyJlac4g4DFSijWXbtSoq4neVt/hCBFL5c5cPMZ1OnHvbEuU8uXzoJct7o7RSaXS5ystkLarqEeCA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.2.0.tgz", + "integrity": "sha512-ywWDh2fM4/EqJ1AByjXM13gAal+z/WSGiBQ5OZmjpL/iqFLENy3yo/GwsxR/ataOi27XbRQTeQbE/eD7HVnWiA==", "dependencies": { "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" @@ -7537,9 +7537,9 @@ "dev": true }, "@splitsoftware/splitio-commons": { - "version": "2.1.1-rc.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.1.1-rc.1.tgz", - "integrity": "sha512-+I5K5qmF8YyJlac4g4DFSijWXbtSoq4neVt/hCBFL5c5cPMZ1OnHvbEuU8uXzoJct7o7RSaXS5ystkLarqEeCA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.2.0.tgz", + "integrity": "sha512-ywWDh2fM4/EqJ1AByjXM13gAal+z/WSGiBQ5OZmjpL/iqFLENy3yo/GwsxR/ataOi27XbRQTeQbE/eD7HVnWiA==", "requires": { "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" diff --git a/package.json b/package.json index 12807311e..c3ac42b76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "11.1.1-rc.0", + "version": "11.2.0", "description": "Split SDK", "files": [ "README.md", @@ -38,7 +38,7 @@ "node": ">=14.0.0" }, "dependencies": { - "@splitsoftware/splitio-commons": "2.1.1-rc.1", + "@splitsoftware/splitio-commons": "2.2.0", "bloom-filters": "^3.0.4", "ioredis": "^4.28.0", "js-yaml": "^3.13.1", diff --git a/src/__tests__/browserSuites/impressions.debug.spec.js b/src/__tests__/browserSuites/impressions.debug.spec.js index 73515531b..887d2f4fd 100644 --- a/src/__tests__/browserSuites/impressions.debug.spec.js +++ b/src/__tests__/browserSuites/impressions.debug.spec.js @@ -64,13 +64,13 @@ export default function (fetchMock, assert) { }, { k: 'facundo@split.io', t: 'o.n', m: data[0].i[2].m, c: 828282828282, r: 'another expected label', pt: data[0].i[1].m }, { - k: 'facundo@split.io', t: 'o.n', m: data[0].i[3].m, c: 828282828282, r: 'another expected label', pt: data[0].i[2].m, properties: '{"prop1":"value1"}' + k: 'facundo@split.io', t: 'o.n', m: data[0].i[3].m, c: 828282828282, r: 'another expected label', properties: '{"prop1":"value1"}' }, { - k: 'facundo@split.io', t: 'o.n', m: data[0].i[4].m, c: 828282828282, r: 'another expected label', pt: data[0].i[3].m, properties: '{"prop1":"value2"}' + k: 'facundo@split.io', t: 'o.n', m: data[0].i[4].m, c: 828282828282, r: 'another expected label', properties: '{"prop1":"value2"}' }, { - k: 'facundo@split.io', t: 'o.n', m: data[0].i[5].m, c: 828282828282, r: 'another expected label', pt: data[0].i[4].m, properties: '{"prop1":"value3"}' + k: 'facundo@split.io', t: 'o.n', m: data[0].i[5].m, c: 828282828282, r: 'another expected label', properties: '{"prop1":"value3"}' }, { - k: 'facundo@split.io', t: 'o.n', m: data[0].i[6].m, c: 828282828282, r: 'another expected label', pt: data[0].i[5].m, properties: '{"prop1":"value4"}' + k: 'facundo@split.io', t: 'o.n', m: data[0].i[6].m, c: 828282828282, r: 'another expected label', properties: '{"prop1":"value4"}' }] }]); diff --git a/src/__tests__/browserSuites/ready-from-cache.spec.js b/src/__tests__/browserSuites/ready-from-cache.spec.js index 0e9cdd90f..289b34809 100644 --- a/src/__tests__/browserSuites/ready-from-cache.spec.js +++ b/src/__tests__/browserSuites/ready-from-cache.spec.js @@ -1,3 +1,5 @@ +import sinon from 'sinon'; +import { nearlyEqual } from '../testUtils'; import { getStorageHash } from '@splitsoftware/splitio-commons/src/storages/KeyBuilder'; import { SplitFactory } from '../../'; @@ -5,7 +7,6 @@ import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json'; import membershipsNicolas from '../mocks/memberships.nicolas@split.io.json'; -import { nearlyEqual } from '../testUtils'; const DEFAULT_CACHE_EXPIRATION_IN_MILLIS = 864000000; // 10 days @@ -94,7 +95,7 @@ export default function (fetchMock, assert) { events: 'https://events.baseurl/readyFromCacheEmpty' }; localStorage.clear(); - t.plan(3); + t.plan(4); fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=-1', { status: 200, body: splitChangesMock1 }); fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=1457552620999', { status: 200, body: splitChangesMock2 }); @@ -123,18 +124,17 @@ export default function (fetchMock, assert) { t.end(); }); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - t.fail('It should not emit SDK_READY_FROM_CACHE if there is no cache.'); - t.end(); + t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY'); }); client.on(client.Event.SDK_READY, () => { - t.pass('It should emit SDK_READY alone, since there was no 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.pass('It should emit SDK_READY alone, since there was no 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.pass('It should emit SDK_READY alone, since there was no cache.'); + t.true(client2.__getStatus().isReadyFromCache, 'Non-default client should emit SDK_READY and it should be ready from cache'); }); }); @@ -147,17 +147,17 @@ export default function (fetchMock, assert) { localStorage.clear(); t.plan(12 * 2 + 3); - fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=25', function () { + fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=25', () => { return new Promise(res => { setTimeout(() => res({ status: 200, body: { ...splitChangesMock1, since: 25 }, headers: {} }), 200); }); // 400ms is how long it'll take to reply with Splits, no SDK_READY should be emitted before that. }); fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=1457552620999', { status: 200, body: splitChangesMock2 }); - fetchMock.get(testUrls.sdk + '/memberships/nicolas%40split.io', function () { + fetchMock.get(testUrls.sdk + '/memberships/nicolas%40split.io', () => { return new Promise(res => { setTimeout(() => res({ status: 200, body: membershipsNicolas, headers: {} }), 400); }); // First client gets segments before splits. No segment cache loading (yet) }); - fetchMock.get(testUrls.sdk + '/memberships/nicolas2%40split.io', function () { + fetchMock.get(testUrls.sdk + '/memberships/nicolas2%40split.io', () => { return new Promise(res => { setTimeout(() => res({ status: 200, body: { 'ms': {} }, headers: {} }), 700); }); // Second client gets segments after 700ms }); - fetchMock.get(testUrls.sdk + '/memberships/nicolas3%40split.io', function () { + fetchMock.get(testUrls.sdk + '/memberships/nicolas3%40split.io', () => { return new Promise(res => { setTimeout(() => res({ status: 200, body: { 'ms': {} }, headers: {} }), 1000); }); // Third client memberships will come after 1s }); fetchMock.postOnce(testUrls.events + '/testImpressions/bulk', 200); @@ -179,7 +179,6 @@ export default function (fetchMock, assert) { readyTimeout: 0.85 }, urls: testUrls, - debug: true }); const client = splitio.client(); const client2 = splitio.client('nicolas2@split.io'); @@ -247,7 +246,7 @@ export default function (fetchMock, assert) { }); }); - assert.test(t => { // Testing when we start with cached data and not expired (lastUpdate item higher than expirationTimestamp) + assert.test(t => { // Testing when we start with cached data and not expired (lastUpdate timestamp higher than default (10) expirationDays ago) const testUrls = { sdk: 'https://sdk.baseurl/readyFromCacheWithData3', events: 'https://events.baseurl/readyFromCacheWithData3' @@ -255,18 +254,18 @@ export default function (fetchMock, assert) { localStorage.clear(); t.plan(12 * 2 + 5); - fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=25', function () { + fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=25', () => { t.equal(localStorage.getItem('readyFromCache_3.SPLITIO.split.always_on'), alwaysOnSplitInverted, 'feature flags must not be cleaned from cache'); return new Promise(res => { setTimeout(() => res({ status: 200, body: { ...splitChangesMock1, since: 25 }, headers: {} }), 200); }); // 400ms is how long it'll take to reply with Splits, no SDK_READY should be emitted before that. }); fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=1457552620999', { status: 200, body: splitChangesMock2 }); - fetchMock.get(testUrls.sdk + '/memberships/nicolas%40split.io', function () { + fetchMock.get(testUrls.sdk + '/memberships/nicolas%40split.io', () => { return new Promise(res => { setTimeout(() => res({ status: 200, body: membershipsNicolas, headers: {} }), 400); }); // First client gets segments before splits. No segment cache loading (yet) }); - fetchMock.get(testUrls.sdk + '/memberships/nicolas2%40split.io', function () { + fetchMock.get(testUrls.sdk + '/memberships/nicolas2%40split.io', () => { return new Promise(res => { setTimeout(() => res({ status: 200, body: { 'ms': {} }, headers: {} }), 700); }); // Second client gets segments after 700ms }); - fetchMock.get(testUrls.sdk + '/memberships/nicolas3%40split.io', function () { + fetchMock.get(testUrls.sdk + '/memberships/nicolas3%40split.io', () => { 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': {} }); @@ -290,7 +289,6 @@ export default function (fetchMock, assert) { readyTimeout: 0.85 }, urls: testUrls, - debug: true }); const client = splitio.client(); const client2 = splitio.client('nicolas2@split.io'); @@ -365,35 +363,38 @@ export default function (fetchMock, assert) { }); }); - assert.test(t => { // Testing when we start with cached data but expired (lastUpdate item lower than expirationTimestamp) + assert.test(t => { // Testing when we start with cached data but expired (lastUpdate timestamp lower than custom (1) expirationDays ago) + const CLIENT_READY_MS = 400, CLIENT2_READY_MS = 700, CLIENT3_READY_MS = 1000; + const testUrls = { sdk: 'https://sdk.baseurl/readyFromCacheWithData4', events: 'https://events.baseurl/readyFromCacheWithData4' }; localStorage.clear(); - fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=-1', function () { + fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=-1', () => { t.equal(localStorage.getItem('some_user_item'), 'user_item', 'user items at localStorage must not be changed'); t.equal(localStorage.getItem('readyFromCache_4.SPLITIO.hash'), expectedHashNullFilter, 'storage hash must not be changed'); - t.equal(localStorage.length, 2, 'feature flags cache data must be cleaned from localStorage'); + t.true(nearlyEqual(parseInt(localStorage.getItem('readyFromCache_4.SPLITIO.lastClear'), 10), Date.now()), 'storage lastClear timestamp must be updated'); + t.equal(localStorage.length, 3, 'feature flags cache data must be cleaned from localStorage'); return { status: 200, body: splitChangesMock1 }; }); fetchMock.get(testUrls.sdk + '/splitChanges?s=1.2&since=1457552620999', { status: 200, body: splitChangesMock2 }); - fetchMock.get(testUrls.sdk + '/memberships/nicolas%40split.io', function () { - return new Promise(res => { setTimeout(() => res({ status: 200, body: membershipsNicolas, headers: {} }), 400); }); // First client gets segments before splits. No segment cache loading (yet) + fetchMock.get(testUrls.sdk + '/memberships/nicolas%40split.io', () => { + return new Promise(res => { setTimeout(() => res({ status: 200, body: membershipsNicolas, headers: {} }), CLIENT_READY_MS); }); // First client gets segments before splits. No segment cache loading (yet) }); - fetchMock.get(testUrls.sdk + '/memberships/nicolas2%40split.io', function () { - return new Promise(res => { setTimeout(() => res({ status: 200, body: { 'ms': {} }, headers: {} }), 700); }); // Second client gets segments after 700ms + fetchMock.get(testUrls.sdk + '/memberships/nicolas2%40split.io', () => { + return new Promise(res => { setTimeout(() => res({ status: 200, body: { 'ms': {} }, headers: {} }), CLIENT2_READY_MS); }); // Second client gets segments after 700ms }); - fetchMock.get(testUrls.sdk + '/memberships/nicolas3%40split.io', function () { - return new Promise(res => { setTimeout(() => res({ status: 200, body: { 'ms': {} }, headers: {} }), 1000); }); // Third client memberships will come after 1s + fetchMock.get(testUrls.sdk + '/memberships/nicolas3%40split.io', () => { + return new Promise(res => { setTimeout(() => res({ status: 200, body: { 'ms': {} }, headers: {} }), CLIENT3_READY_MS); }); // Third client memberships will come after 1s }); fetchMock.postOnce(testUrls.events + '/testImpressions/bulk', 200); fetchMock.postOnce(testUrls.events + '/testImpressions/count', 200); localStorage.setItem('some_user_item', 'user_item'); localStorage.setItem('readyFromCache_4.SPLITIO.splits.till', 25); - localStorage.setItem('readyFromCache_4.SPLITIO.splits.lastUpdated', Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS - 1); // -1 to ensure having an expired lastUpdated item + localStorage.setItem('readyFromCache_4.SPLITIO.splits.lastUpdated', Date.now() - DEFAULT_CACHE_EXPIRATION_IN_MILLIS / 10 - 1); // -1 to ensure having an expired lastUpdated item localStorage.setItem('readyFromCache_4.SPLITIO.split.always_on', alwaysOnSplitInverted); localStorage.setItem('readyFromCache_4.SPLITIO.hash', expectedHashNullFilter); @@ -402,13 +403,13 @@ export default function (fetchMock, assert) { ...baseConfig, storage: { type: 'LOCALSTORAGE', - prefix: 'readyFromCache_4' + prefix: 'readyFromCache_4', + expirationDays: 1, }, startup: { readyTimeout: 0.85 }, urls: testUrls, - debug: true }); const client = splitio.client(); const client2 = splitio.client('nicolas2@split.io'); @@ -423,37 +424,34 @@ export default function (fetchMock, assert) { }); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - t.fail('It should not emit SDK_READY_FROM_CACHE if there is expired cache.'); - t.end(); + t.true(nearlyEqual(Date.now() - startTime, CLIENT_READY_MS), 'It should emit SDK_READY_FROM_CACHE alongside SDK_READY'); }); client2.once(client2.Event.SDK_READY_FROM_CACHE, () => { - t.fail('It should not emit SDK_READY_FROM_CACHE if there is expired cache.'); - t.end(); + t.true(nearlyEqual(Date.now() - startTime, CLIENT2_READY_MS), 'It should emit SDK_READY_FROM_CACHE alongside SDK_READY'); }); client3.once(client3.Event.SDK_READY_FROM_CACHE, () => { - t.fail('It should not emit SDK_READY_FROM_CACHE if there is expired cache.'); - t.end(); + t.true(nearlyEqual(Date.now() - startTime, CLIENT3_READY_MS), 'It should emit SDK_READY_FROM_CACHE alongside SDK_READY'); }); client.on(client.Event.SDK_READY, () => { - t.true(Date.now() - startTime >= 400, 'It should emit SDK_READY after syncing with the cloud.'); + 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(() => { - t.true(Date.now() - startTime >= 400, 'It should resolve ready promise after syncing with the cloud.'); + 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.'); }); client2.on(client2.Event.SDK_READY, () => { - t.true(Date.now() - startTime >= 700, 'It should emit SDK_READY after syncing with the cloud.'); + 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(() => { - t.true(Date.now() - startTime >= 700, 'It should resolve ready promise after syncing with the cloud.'); + 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(() => { - t.true(Date.now() - startTime >= 1000, 'It should resolve ready promise after syncing with the cloud.'); + 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.'); // Last cb: destroy clients and check that localstorage has the expected items @@ -486,7 +484,7 @@ export default function (fetchMock, assert) { events: 'https://events.baseurl/readyFromCache_5' }; localStorage.clear(); - t.plan(7); + t.plan(8); fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.2&since=-1&names=p1__split,p2__split', { status: 200, body: { splits: [splitDeclarations.p1__split, splitDeclarations.p2__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE fetchMock.getOnce(testUrls.sdk + '/memberships/nicolas%40split.io', { status: 200, body: { ms: {} } }); @@ -507,14 +505,12 @@ export default function (fetchMock, assert) { sync: { splitFilters: [{ type: 'byName', values: ['p2__split', 'p1__split'] }, { type: 'byName', values: ['p2__split', null] }] }, - debug: true }); const client = splitio.client(); const manager = splitio.manager(); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - t.fail('It should not emit SDK_READY_FROM_CACHE because localStorage is cleaned and there isn\'t cached feature flags'); - t.end(); + t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY'); }); client.once(client.Event.SDK_READY, () => { @@ -538,7 +534,7 @@ export default function (fetchMock, assert) { events: 'https://events.baseurl/readyFromCache_5B' }; localStorage.clear(); - t.plan(5); + t.plan(6); fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.2&since=-1&names=p1__split,p2__split', { status: 200, body: { splits: [splitDeclarations.p1__split, splitDeclarations.p2__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE fetchMock.getOnce(testUrls.sdk + '/memberships/nicolas%40split.io', { status: 200, body: { ms: {} } }); @@ -553,14 +549,12 @@ export default function (fetchMock, assert) { sync: { splitFilters: [{ type: 'byName', values: ['p2__split', 'p1__split'] }, { type: 'byName', values: ['p2__split', null] }] }, - debug: true }); const client = splitio.client(); const manager = splitio.manager(); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - t.fail('It should not emit SDK_READY_FROM_CACHE if cache is empty.'); - t.end(); + t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY'); }); client.once(client.Event.SDK_READY, () => { @@ -604,7 +598,6 @@ export default function (fetchMock, assert) { sync: { splitFilters: [{ type: 'byName', values: [undefined, true, 'p2__split'] }, { type: 'byPrefix', values: ['p1'] }, { type: 'byName', values: ['p2__split'] }] }, - debug: true }); const client = splitio.client(); const manager = splitio.manager(); @@ -633,7 +626,7 @@ export default function (fetchMock, assert) { events: 'https://events.baseurl/readyFromCache_7' }; localStorage.clear(); - t.plan(6); + t.plan(7); fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.2&since=-1&prefixes=p1,p2', { status: 200, body: { splits: [splitDeclarations.p1__split, splitDeclarations.p2__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE fetchMock.getOnce(testUrls.sdk + '/memberships/nicolas%40split.io', { status: 200, body: { ms: {} } }); @@ -650,20 +643,19 @@ export default function (fetchMock, assert) { ...baseConfig, storage: { type: 'LOCALSTORAGE', - prefix: 'readyFromCache_7' + prefix: 'readyFromCache_7', + expirationDays: 0, // invalid value, will use default (10) }, urls: testUrls, sync: { splitFilters: [{ type: 'byPrefix', values: ['p2'] }, { type: 'byPrefix', values: ['p1', ''] }, { type: '', values: [] }, {}, { type: 'byPrefix' }] }, - debug: true }); const client = splitio.client(); const manager = splitio.manager(); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - t.fail('It should not emit SDK_READY_FROM_CACHE if cache has expired.'); - t.end(); + t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY'); }); client.once(client.Event.SDK_READY, () => { @@ -699,7 +691,7 @@ export default function (fetchMock, assert) { events: 'https://events.baseurl/readyFromCache_8' }; localStorage.clear(); - t.plan(7); + t.plan(8); fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.2&since=-1', { status: 200, body: { splits: [splitDeclarations.p1__split, splitDeclarations.p2__split, splitDeclarations.p3__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE fetchMock.getOnce(testUrls.sdk + '/memberships/nicolas%40split.io', { status: 200, body: { ms: {} } }); @@ -719,14 +711,12 @@ export default function (fetchMock, assert) { }, urls: testUrls, sync: syncParam, - debug: true }); const client = splitio.client(); const manager = splitio.manager(); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - t.fail('It should not emit SDK_READY_FROM_CACHE because all feature flags were removed from cache since the filter query changed.'); - t.end(); + t.true(client.__getStatus().isReady, 'Client should emit SDK_READY_FROM_CACHE alongside SDK_READY'); }); client.once(client.Event.SDK_READY, () => { @@ -774,7 +764,6 @@ export default function (fetchMock, assert) { sync: { splitFilters: [{ type: 'byName', values: ['p3__split'] }, { type: 'byPrefix', values: [' p2', ' p2', ' p2', ' p2', 'no exist trim '] }, { type: 'byName', values: ['no_exist', ' no exist trim '] }] }, - debug: true }); const client = splitio.client(); const manager = splitio.manager(); @@ -793,4 +782,91 @@ export default function (fetchMock, assert) { }); }); + assert.test(async t => { // Testing clearOnInit config true + sinon.spy(console, 'log'); + + const testUrls = { + sdk: 'https://sdk.baseurl/readyFromCache_10', + events: 'https://events.baseurl/readyFromCache_10' + }; + const clearOnInitConfig = { + ...baseConfig, + storage: { + type: 'LOCALSTORAGE', + prefix: 'readyFromCache_10', + clearOnInit: true + }, + urls: testUrls, + debug: true + }; + + // Start with cached data but without lastClear item (JS SDK below 11.1.0) -> cache cleanup + localStorage.clear(); + localStorage.setItem('readyFromCache_10.SPLITIO.splits.till', 25); + localStorage.setItem('readyFromCache_10.SPLITIO.split.p1__split', JSON.stringify(splitDeclarations.p1__split)); + localStorage.setItem('readyFromCache_10.SPLITIO.split.p2__split', JSON.stringify(splitDeclarations.p2__split)); + localStorage.setItem('readyFromCache_10.SPLITIO.hash', expectedHashNullFilter); + + fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.2&since=-1', { status: 200, body: splitChangesMock1 }); + fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.2&since=1457552620999', { status: 200, body: splitChangesMock2 }); + fetchMock.get(testUrls.sdk + '/memberships/nicolas%40split.io', { status: 200, body: { ms: {} } }); + + let splitio = SplitFactory(clearOnInitConfig); + let client = splitio.client(); + let manager = splitio.manager(); + + 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'); + + 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'); + }); + + await client.ready(); + t.equal(manager.names().sort().length, 33, 'active splits should be present for evaluation'); + + await splitio.destroy(); + t.equal(localStorage.getItem('readyFromCache_10.SPLITIO.splits.till'), '1457552620999', 'splits.till must correspond to the till of the last successfully fetched Splits'); + t.equal(localStorage.getItem('readyFromCache_10.SPLITIO.hash'), expectedHashNullFilter, 'Storage hash must not be changed'); + t.true(nearlyEqual(parseInt(localStorage.getItem('readyFromCache_10.SPLITIO.lastClear')), Date.now()), 'lastClear timestamp must be set'); + + // Start again with cached data and lastClear item within the last 24 hours -> no cache cleanup + console.log.resetHistory(); + fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.2&since=1457552620999', { status: 200, body: splitChangesMock2 }); + + splitio = SplitFactory(clearOnInitConfig); + client = splitio.client(); + manager = splitio.manager(); + + await new Promise(res => client.once(client.Event.SDK_READY_FROM_CACHE, res)); + + t.equal(manager.names().sort().length, 33, 'active splits should be present for evaluation'); + t.false(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'); + + await splitio.destroy(); + + // Start again with cached data and lastClear item older than 24 hours -> cache cleanup + console.log.resetHistory(); + localStorage.setItem('readyFromCache_10.SPLITIO.lastClear', Date.now() - 25 * 60 * 60 * 1000); // 25 hours ago + fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.2&since=-1', { status: 200, body: splitChangesMock1 }); + fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.2&since=1457552620999', { status: 200, body: splitChangesMock2 }); + + splitio = SplitFactory(clearOnInitConfig); + client = splitio.client(); + 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'); + }); + + await new Promise(res => client.once(client.Event.SDK_READY, res)); + + t.equal(manager.names().sort().length, 33, 'active splits should be present for evaluation'); + 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'); + + await splitio.destroy(); + + console.log.restore(); + t.end(); + }); + } diff --git a/src/__tests__/nodeSuites/impressions.debug.spec.js b/src/__tests__/nodeSuites/impressions.debug.spec.js index 5cffc7ba5..f0f194f9b 100644 --- a/src/__tests__/nodeSuites/impressions.debug.spec.js +++ b/src/__tests__/nodeSuites/impressions.debug.spec.js @@ -66,11 +66,11 @@ export default async function (key, fetchMock, assert) { }, { k: 'emi@split.io', t: 'o.n', m: data[0].i[3].m, c: 828282828282, r: 'another expected label', properties: '{"prop1":"value1"}' }, { - k: 'emi@split.io', t: 'o.n', m: data[0].i[4].m, c: 828282828282, r: 'another expected label', pt: data[0].i[3].m, properties: '{"prop1":"value2"}' + k: 'emi@split.io', t: 'o.n', m: data[0].i[4].m, c: 828282828282, r: 'another expected label', properties: '{"prop1":"value2"}' }, { - k: 'emi@split.io', t: 'o.n', m: data[0].i[5].m, c: 828282828282, r: 'another expected label', pt: data[0].i[4].m, properties: '{"prop1":"value3"}' + k: 'emi@split.io', t: 'o.n', m: data[0].i[5].m, c: 828282828282, r: 'another expected label', properties: '{"prop1":"value3"}' }, { - k: 'emi@split.io', t: 'o.n', m: data[0].i[6].m, c: 828282828282, r: 'another expected label', pt: data[0].i[5].m, properties: '{"prop1":"value4"}' + k: 'emi@split.io', t: 'o.n', m: data[0].i[6].m, c: 828282828282, r: 'another expected label', properties: '{"prop1":"value4"}' }] }], 'We performed evaluations for one split, so we should have 1 item total.'); diff --git a/src/settings/defaults/version.js b/src/settings/defaults/version.js index 547cb690c..e116c4aa8 100644 --- a/src/settings/defaults/version.js +++ b/src/settings/defaults/version.js @@ -1 +1 @@ -export const packageVersion = '11.1.1-rc.0'; +export const packageVersion = '11.2.0'; diff --git a/src/settings/storage/browser.js b/src/settings/storage/browser.js index 10a9e3eea..0165d45e3 100644 --- a/src/settings/storage/browser.js +++ b/src/settings/storage/browser.js @@ -10,7 +10,9 @@ export function validateStorage(settings) { storage: { type, options = {}, - prefix + prefix, + expirationDays, + clearOnInit } = { type: STORAGE_MEMORY }, } = settings; let __originalType; @@ -38,6 +40,8 @@ export function validateStorage(settings) { type, options, prefix, + expirationDays, + clearOnInit, __originalType }; } diff --git a/ts-tests/index.ts b/ts-tests/index.ts index 823e00771..cc6421447 100644 --- a/ts-tests/index.ts +++ b/ts-tests/index.ts @@ -531,7 +531,9 @@ let fullBrowserSettings: SplitIO.IBrowserSettings = { features: mockedFeaturesMap, storage: { type: 'LOCALSTORAGE', - prefix: 'PREFIX' + prefix: 'PREFIX', + expirationDays: 1, + clearOnInit: true }, impressionListener: impressionListener, debug: true,