diff --git a/CHANGES.txt b/CHANGES.txt index 4a3e729d1..48455d7ca 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,8 @@ +11.9.0 (November 26, 2025) + - Updated @splitsoftware/splitio-commons package to version 2.9.0, which: + - updates the SDK’s initial synchronization in Node.js (server-side) to use the `startup.requestTimeoutBeforeReady` and `startup.retriesOnFailureBeforeReady` options to control the timeout and retry behavior of segment requests. + - updates the order of storage operations to prevent inconsistent states when using the `LOCALSTORAGE` storage type and the browser’s `localStorage` fails due to quota limits. + 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). diff --git a/package-lock.json b/package-lock.json index 136e015cf..552a8ed8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@splitsoftware/splitio", - "version": "11.8.0", + "version": "11.9.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio", - "version": "11.8.0", + "version": "11.9.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio-commons": "2.8.0", + "@splitsoftware/splitio-commons": "2.9.0", "bloom-filters": "^3.0.4", "ioredis": "^4.28.0", "js-yaml": "^3.13.1", @@ -157,10 +157,11 @@ "dev": true }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -351,9 +352,9 @@ "dev": true }, "node_modules/@splitsoftware/splitio-commons": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.8.0.tgz", - "integrity": "sha512-QgHUreMOEDwf4GZzVPu4AzkZJvuaeSoHsiJc4tT3CxSIYl2bKMz1SSDlI1tW/oVbIFeWjkrIp2lCYEyUBgcvyA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.9.0.tgz", + "integrity": "sha512-dfGqtiuYcWeR235NM43z3BOULTFi+hdkB1FbOHePrufWJTYBOfuBeIgPnsW3wyg+kXyGkNN49JyywZHrJtVpDA==", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", @@ -2629,10 +2630,11 @@ "dev": true }, "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -3998,9 +4000,10 @@ } }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -4382,10 +4385,11 @@ } }, "node_modules/min-document": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", + "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", "dev": true, + "license": "MIT", "dependencies": { "dom-walk": "^0.1.0" } @@ -7576,9 +7580,9 @@ "dev": true }, "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "requires": { "argparse": "^2.0.1" @@ -7740,9 +7744,9 @@ "dev": true }, "@splitsoftware/splitio-commons": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.8.0.tgz", - "integrity": "sha512-QgHUreMOEDwf4GZzVPu4AzkZJvuaeSoHsiJc4tT3CxSIYl2bKMz1SSDlI1tW/oVbIFeWjkrIp2lCYEyUBgcvyA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.9.0.tgz", + "integrity": "sha512-dfGqtiuYcWeR235NM43z3BOULTFi+hdkB1FbOHePrufWJTYBOfuBeIgPnsW3wyg+kXyGkNN49JyywZHrJtVpDA==", "requires": { "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" @@ -9466,9 +9470,9 @@ "dev": true }, "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "requires": { "argparse": "^2.0.1" @@ -10547,9 +10551,9 @@ } }, "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -10869,9 +10873,9 @@ } }, "min-document": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", + "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", "dev": true, "requires": { "dom-walk": "^0.1.0" diff --git a/package.json b/package.json index 9e8e13770..67480b7c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "11.8.0", + "version": "11.9.0", "description": "Split SDK", "files": [ "README.md", @@ -38,7 +38,7 @@ "node": ">=14.0.0" }, "dependencies": { - "@splitsoftware/splitio-commons": "2.8.0", + "@splitsoftware/splitio-commons": "2.9.0", "bloom-filters": "^3.0.4", "ioredis": "^4.28.0", "js-yaml": "^3.13.1", diff --git a/src/__tests__/browserSuites/ready-from-cache.spec.js b/src/__tests__/browserSuites/ready-from-cache.spec.js index 419728fca..bacbf2cbb 100644 --- a/src/__tests__/browserSuites/ready-from-cache.spec.js +++ b/src/__tests__/browserSuites/ready-from-cache.spec.js @@ -108,18 +108,34 @@ export const expectedHashWithFilter = '2ce5cc38'; // for SDK key ' { // Testing when we start from scratch + assert.test(t => { // Testing when we start from scratch, with an initial localStorage write operation fail (should retry splitChanges with -1) const testUrls = { sdk: 'https://sdk.baseurl/readyFromCacheEmpty', events: 'https://events.baseurl/readyFromCacheEmpty' }; localStorage.clear(); + + // simulate a localStorage failure when saving a FF and a membership + const originalSetItem = localStorage.setItem; + localStorage.setItem = (key, value) => { + if (key.includes('.nicolas@split.io.')) { + throw new Error('localStorage.setItem failed'); + } + if (key.includes('.split.')) { + localStorage.setItem = originalSetItem; + throw new Error('localStorage.setItem failed'); + } + return originalSetItem.call(localStorage, key, value); + }; + t.plan(4); - fetchMock.get(testUrls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1', { status: 200, body: splitChangesMock1 }); - fetchMock.get(testUrls.sdk + '/memberships/nicolas%40split.io', { status: 200, body: membershipsNicolas }); - fetchMock.get(testUrls.sdk + '/memberships/nicolas2%40split.io', { status: 200, body: { 'ms': {} } }); - fetchMock.get(testUrls.sdk + '/memberships/nicolas3%40split.io', { status: 200, body: { 'ms': {} } }); + fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1', { status: 200, body: splitChangesMock1 }); + fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1', { status: 200, body: splitChangesMock1 }); // retry + fetchMock.getOnce(testUrls.sdk + '/memberships/nicolas%40split.io', { status: 200, body: membershipsNicolas }); + fetchMock.getOnce(testUrls.sdk + '/memberships/nicolas%40split.io', { status: 200, body: membershipsNicolas }); // retry + fetchMock.getOnce(testUrls.sdk + '/memberships/nicolas2%40split.io', { status: 200, body: { 'ms': {} } }); + fetchMock.getOnce(testUrls.sdk + '/memberships/nicolas3%40split.io', { status: 200, body: { 'ms': {} } }); const splitio = SplitFactory({ ...baseConfig, diff --git a/src/__tests__/consumer/node_redis.spec.js b/src/__tests__/consumer/node_redis.spec.js index 00ff3467a..597fe748b 100644 --- a/src/__tests__/consumer/node_redis.spec.js +++ b/src/__tests__/consumer/node_redis.spec.js @@ -550,7 +550,14 @@ tape('Node.js Redis', function (t) { t.test('Connection error', assert => { initializeRedisServer() .then((server) => { - const sdk = SplitFactory(config); + const sdk = SplitFactory({ + ...config, + fallbackTreatments: { + byFlag: { + 'always-on': 'control_always_on' + } + } + }); const client = sdk.client(); client.once(client.Event.SDK_READY_TIMED_OUT, assert.fail); @@ -585,7 +592,7 @@ tape('Node.js Redis', function (t) { assert.equal(await client.getTreatment('UT_Segment_member', 'UT_NOT_SET_MATCHER', { permissions: ['not_matching'] }), 'control', 'In the event of a Redis error like a disconnection, getTreatments should not hang but resolve to "control".'); - assert.equal(await client.getTreatment('UT_Segment_member', 'always-on'), 'control', 'In the event of a Redis error like a disconnection, getTreatments should not hang but resolve to "control".'); + assert.equal(await client.getTreatment('UT_Segment_member', 'always-on'), 'control_always_on', 'In the event of a Redis error like a disconnection, getTreatments should not hang but resolve to fallback treatment or "control".'); assert.false(await client.track('nicolas@split.io', 'user', 'test.redis.event', 18), 'In the event of a Redis error like a disconnection, track should resolve to false.'); diff --git a/src/__tests__/nodeSuites/ready-promise.spec.js b/src/__tests__/nodeSuites/ready-promise.spec.js index 6e2ee9c91..c62c060fe 100644 --- a/src/__tests__/nodeSuites/ready-promise.spec.js +++ b/src/__tests__/nodeSuites/ready-promise.spec.js @@ -105,7 +105,11 @@ export default function readyPromiseAssertions(key, fetchMock, assert) { // /splitChanges takes longer than 'requestTimeoutBeforeReady' only for the first attempt fetchMock.getOnce(config.urls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1', splitChangesMock1, { delay: fromSecondsToMillis(config.startup.requestTimeoutBeforeReady) + 20 }); fetchMock.getOnce(config.urls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1', splitChangesMock1, { delay: fromSecondsToMillis(config.startup.requestTimeoutBeforeReady) - 20 }); - mockSegmentChanges(fetchMock, new RegExp(config.urls.sdk + '/segmentChanges/*'), ['some_key']); + mockSegmentChanges(fetchMock, new RegExp(config.urls.sdk + '/segmentChanges/(splitters|developers|segment_excluded_by_rbs)'), ['some_key']); + fetchMock.getOnce(config.urls.sdk + '/segmentChanges/employees?since=-1', { status: 500, body: 'server error' }); + fetchMock.getOnce(config.urls.sdk + '/segmentChanges/employees?since=-1', { since: -1, till: 10, name: 'employees', added: ['some_key'], removed: [] }); + fetchMock.getOnce(config.urls.sdk + '/segmentChanges/employees?since=10', { since: 10, till: 10, name: 'employees', added: [], removed: [] }); + fetchMock.postOnce(config.urls.events + '/testImpressions/bulk', 200); fetchMock.postOnce(config.urls.events + '/testImpressions/count', 200); diff --git a/src/settings/defaults/version.js b/src/settings/defaults/version.js index 378697e28..8d52cef10 100644 --- a/src/settings/defaults/version.js +++ b/src/settings/defaults/version.js @@ -1 +1 @@ -export const packageVersion = '11.8.0'; +export const packageVersion = '11.9.0'; diff --git a/ts-tests/index.ts b/ts-tests/index.ts index c1cf06960..e807fc913 100644 --- a/ts-tests/index.ts +++ b/ts-tests/index.ts @@ -263,7 +263,7 @@ promise = client.destroy(); promise = SDK.destroy(); // @TODO not public yet // promise = client.flush(); -const promiseWhenReadyFromCache: Promise = client.whenReadyFromCache(); +let promiseWhenReadyFromCache: Promise = client.whenReadyFromCache(); // Get readiness status let status: SplitIO.ReadinessStatus = client.getStatus(); @@ -378,10 +378,15 @@ nodeEventEmitter = asyncClient; // Ready, destroy and flush (same as for sync client, just for interface checking) promise = asyncClient.ready(); +promise = asyncClient.whenReady(); promise = asyncClient.destroy(); promise = AsyncSDK.destroy(); // @TODO not public yet // promise = asyncClient.flush(); +promiseWhenReadyFromCache = asyncClient.whenReadyFromCache(); + +// Get readiness status +status = asyncClient.getStatus(); // We can call getTreatment but always with a key. asyncTreatment = asyncClient.getTreatment(splitKey, 'mySplit');