From 5bbcaa9c3e2b77352549254d8e4b4abaf9ae8b92 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Tue, 25 Nov 2025 19:11:51 -0300 Subject: [PATCH 1/2] [FME-11258] Add impressions disabled by evaluation --- package-lock.json | 14 ++-- package.json | 4 +- src/__tests__/consumer/node_redis.spec.js | 26 ++++++- .../evaluations-impressionsDisabled.spec.js | 77 +++++++++++++++++++ src/__tests__/nodeSuites/flag-sets.spec.js | 72 +++++++++-------- .../nodeSuites/impressions-listener.spec.js | 14 +++- .../nodeSuites/impressions.debug.spec.js | 31 +++++++- .../nodeSuites/impressions.none.spec.js | 12 ++- src/__tests__/nodeSuites/impressions.spec.js | 13 +++- src/__tests__/offline/node.spec.js | 6 ++ src/__tests__/online/node.spec.js | 2 + ts-tests/index.ts | 1 + 12 files changed, 218 insertions(+), 54 deletions(-) create mode 100644 src/__tests__/nodeSuites/evaluations-impressionsDisabled.spec.js diff --git a/package-lock.json b/package-lock.json index 136e015cf..47f93289b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "11.8.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio-commons": "2.8.0", + "@splitsoftware/splitio-commons": "2.8.1-rc.1", "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.8.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.8.0.tgz", - "integrity": "sha512-QgHUreMOEDwf4GZzVPu4AzkZJvuaeSoHsiJc4tT3CxSIYl2bKMz1SSDlI1tW/oVbIFeWjkrIp2lCYEyUBgcvyA==", + "version": "2.8.1-rc.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.8.1-rc.1.tgz", + "integrity": "sha512-I9GlUjXi/OFRBdyT92NBTudXj9dmiZsdN2fbg5CrJ0txycdGkkN1BVCEKrjnfTAsP0evnXSGOtLOVSFQeNo2pA==", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", @@ -7740,9 +7740,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.8.1-rc.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.8.1-rc.1.tgz", + "integrity": "sha512-I9GlUjXi/OFRBdyT92NBTudXj9dmiZsdN2fbg5CrJ0txycdGkkN1BVCEKrjnfTAsP0evnXSGOtLOVSFQeNo2pA==", "requires": { "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" diff --git a/package.json b/package.json index 9e8e13770..da3611c5e 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "node": ">=14.0.0" }, "dependencies": { - "@splitsoftware/splitio-commons": "2.8.0", + "@splitsoftware/splitio-commons": "2.8.1-rc.1", "bloom-filters": "^3.0.4", "ioredis": "^4.28.0", "js-yaml": "^3.13.1", @@ -107,7 +107,7 @@ "test-node-e2e-destroy": "cross-env NODE_ENV=test tape -r ./ts-node.register src/__tests__/destroy/node.spec.js | tap-min", "test-node-e2e-errorCatching": "cross-env NODE_ENV=test tape -r ./ts-node.register src/__tests__/errorCatching/node.spec.js | tap-min", "test-node-e2e-push": "cross-env NODE_ENV=test tape -r ./ts-node.register src/__tests__/push/node.spec.js | tap-min", - "test-node-e2e-redis": "cross-env NODE_ENV=test tape -r ./ts-node.register src/__tests__/consumer/node_redis.spec.js | tap-min", + "test-node-e2e-redis": "cross-env NODE_ENV=test tape -r ./ts-node.register src/__tests__/consumer/node_redis.spec.js", "test-ts-decls": "tsc --build ts-tests", "test": "npm run test-node && npm run test-browser", "all": "npm run check && npm run build && npm run test-ts-decls && npm run test", diff --git a/src/__tests__/consumer/node_redis.spec.js b/src/__tests__/consumer/node_redis.spec.js index 00ff3467a..f0e96eb7a 100644 --- a/src/__tests__/consumer/node_redis.spec.js +++ b/src/__tests__/consumer/node_redis.spec.js @@ -54,7 +54,7 @@ const expectedImpressionCount = [ `UT_SET_MATCHER::${truncateTimeFrame(timeFrame)}`, '2', `UT_NOT_SET_MATCHER::${truncateTimeFrame(timeFrame)}`, '3', `always-o.n-with-config::${truncateTimeFrame(timeFrame)}`, '1', - `always-on::${truncateTimeFrame(timeFrame)}`, '1', + `always-on::${truncateTimeFrame(timeFrame)}`, '5', `hierarchical_splits_testing_on::${truncateTimeFrame(timeFrame)}`, '1', `hierarchical_splits_testing_off::${truncateTimeFrame(timeFrame)}`, '1', `hierarchical_splits_testing_on_negated::${truncateTimeFrame(timeFrame)}`, '1', @@ -158,6 +158,12 @@ tape('Node.js Redis', function (t) { assert.equal(await client.getTreatment('UT_Segment_member', 'hierarchical_splits_testing_on_negated'), 'off', 'Evaluations using Redis storage should be correct.'); assert.equal(await client.getTreatment('other_key', 'always-on-impressions-disabled-true'), 'on', 'Evaluations using Redis storage should be correct.'); + // Verify impressionsDisabled option + assert.deepEqual(await client.getTreatment('other_key', 'always-on', undefined, { impressionsDisabled: true }), 'on', 'Evaluations with impressionsDisabled: true should be correct.'); + assert.deepEqual(await client.getTreatmentWithConfig('other_key', 'always-on', undefined, { impressionsDisabled: true }), { treatment: 'on', config: null }, 'Evaluations with impressionsDisabled: true should be correct.'); + assert.deepEqual(await client.getTreatments('other_key', ['always-on'], undefined, { impressionsDisabled: true }), { 'always-on': 'on' }, 'Evaluations with impressionsDisabled: true should be correct.'); + assert.deepEqual(await client.getTreatmentsWithConfig('other_key', ['always-on'], undefined, { impressionsDisabled: true }), { 'always-on': { treatment: 'on', config: null } }, 'Evaluations with impressionsDisabled: true should be correct.'); + // Evaluations with rule-based segments assert.equal(await client.getTreatment('emi@split.io', 'rbs_test_flag'), 'v2', 'key in excluded segment'); assert.equal(await client.getTreatment('mauro@split.io', 'rbs_test_flag'), 'v2', 'excluded key'); @@ -189,11 +195,11 @@ tape('Node.js Redis', function (t) { // Validate Impression Counts and Unique Keys for 'always-on-impressions-disabled-true' exec(`echo "HGETALL ${config.storage.prefix}.SPLITIO.impressions.count" | redis-cli -p ${redisPort}`, async (error, stdout) => { const trackedImpressionCounts = stdout.split('\n').filter(line => line !== ''); - assert.deepEqual(trackedImpressionCounts, [`always-on-impressions-disabled-true::${truncateTimeFrame(timeFrame)}`, '1',], 'Tracked impression counts should be stored in Redis TODO'); + assert.deepEqual(trackedImpressionCounts, [`always-on-impressions-disabled-true::${truncateTimeFrame(timeFrame)}`, '1',`always-on::${truncateTimeFrame(timeFrame)}`, '4',], 'Tracked impression counts should be stored in Redis TODO'); exec(`echo "LRANGE ${config.storage.prefix}.SPLITIO.uniquekeys 0 20" | redis-cli -p ${redisPort}`, async (error, stdout) => { const storedUniqueKeys = stdout.split('\n').filter(line => line !== '').map(JSON.parse); - assert.deepEqual(storedUniqueKeys, [{ 'f': 'always-on-impressions-disabled-true', 'ks': ['other_key'] }], 'Unique keys should be stored in Redis TODO'); + assert.deepEqual(storedUniqueKeys, [{ 'f': 'always-on-impressions-disabled-true', 'ks': ['other_key'] }, { f: 'always-on', ks: [ 'other_key' ] }], 'Unique keys should be stored in Redis TODO'); // Validate stored impressions and events exec(`echo "LLEN ${config.storage.prefix}.SPLITIO.impressions \n LLEN ${config.storage.prefix}.SPLITIO.events" | redis-cli -p ${redisPort}`, (error, stdout) => { @@ -308,6 +314,12 @@ tape('Node.js Redis', function (t) { // this should be deduped assert.equal(await client.getTreatment('UT_Segment_member', 'hierarchical_splits_testing_on_negated'), 'off', 'Evaluations using Redis storage should be correct.'); + // Verify impressionsDisabled option + assert.deepEqual(await client.getTreatment('other_key', 'always-on', undefined, { impressionsDisabled: true }), 'on', 'Evaluations with impressionsDisabled: true should be correct.'); + assert.deepEqual(await client.getTreatmentWithConfig('other_key', 'always-on', undefined, { impressionsDisabled: true }), { treatment: 'on', config: null }, 'Evaluations with impressionsDisabled: true should be correct.'); + assert.deepEqual(await client.getTreatments('other_key', ['always-on'], undefined, { impressionsDisabled: true }), { 'always-on': 'on' }, 'Evaluations with impressionsDisabled: true should be correct.'); + assert.deepEqual(await client.getTreatmentsWithConfig('other_key', ['always-on'], undefined, { impressionsDisabled: true }), { 'always-on': { treatment: 'on', config: null } }, 'Evaluations with impressionsDisabled: true should be correct.'); + assert.equal(typeof client.track('nicolas@split.io', 'user', 'test.redis.event', 18).then, 'function', 'Track calls should always return a promise on Redis mode.'); assert.equal(typeof client.track().then, 'function', 'Track calls should always return a promise on Redis mode, even when parameters are incorrect.'); @@ -357,7 +369,7 @@ tape('Node.js Redis', function (t) { { 'f': 'UT_SET_MATCHER', 'ks': ['UT_Segment_member'] }, { 'f': 'UT_NOT_SET_MATCHER', 'ks': ['UT_Segment_member'] }, { 'f': 'always-o.n-with-config', 'ks': ['UT_Segment_member'] }, - { 'f': 'always-on', 'ks': ['UT_Segment_member'] }, + { 'f': 'always-on', 'ks': ['UT_Segment_member', 'other_key'] }, { 'f': 'hierarchical_splits_testing_on', 'ks': ['UT_Segment_member'] }, { 'f': 'hierarchical_splits_testing_off', 'ks': ['UT_Segment_member'] }, { 'f': 'hierarchical_splits_testing_on_negated', 'ks': ['UT_Segment_member'] }, @@ -404,6 +416,12 @@ tape('Node.js Redis', function (t) { assert.equal(await client.getTreatment('UT_Segment_member', 'hierarchical_splits_testing_off'), 'off', 'Evaluations using Redis storage should be correct.'); assert.equal(await client.getTreatment('UT_Segment_member', 'hierarchical_splits_testing_on_negated'), 'off', 'Evaluations using Redis storage should be correct.'); + // Verify impressionsDisabled option + assert.deepEqual(await client.getTreatment('other_key', 'always-on', undefined, { impressionsDisabled: true }), 'on', 'Evaluations with impressionsDisabled: true should be correct.'); + assert.deepEqual(await client.getTreatmentWithConfig('other_key', 'always-on', undefined, { impressionsDisabled: true }), { treatment: 'on', config: null }, 'Evaluations with impressionsDisabled: true should be correct.'); + assert.deepEqual(await client.getTreatments('other_key', ['always-on'], undefined, { impressionsDisabled: true }), { 'always-on': 'on' }, 'Evaluations with impressionsDisabled: true should be correct.'); + assert.deepEqual(await client.getTreatmentsWithConfig('other_key', ['always-on'], undefined, { impressionsDisabled: true }), { 'always-on': { treatment: 'on', config: null } }, 'Evaluations with impressionsDisabled: true should be correct.'); + assert.equal(typeof client.track('nicolas@split.io', 'user', 'test.redis.event', 18).then, 'function', 'Track calls should always return a promise on Redis mode.'); assert.equal(typeof client.track().then, 'function', 'Track calls should always return a promise on Redis mode, even when parameters are incorrect.'); diff --git a/src/__tests__/nodeSuites/evaluations-impressionsDisabled.spec.js b/src/__tests__/nodeSuites/evaluations-impressionsDisabled.spec.js new file mode 100644 index 000000000..6863230a7 --- /dev/null +++ b/src/__tests__/nodeSuites/evaluations-impressionsDisabled.spec.js @@ -0,0 +1,77 @@ +import { SplitFactory } from '../..'; +import { settingsFactory } from '../../settings'; +import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; +import { url } from '../testUtils'; + +const baseUrls = { + sdk: 'https://sdk.baseurl/evaluationsImpressionsDisabledSuite', + events: 'https://events.baseurl/evaluationsImpressionsDisabledSuite', + telemetry: 'https://telemetry.baseurl/evaluationsImpressionsDisabledSuite' +}; + +const settings = settingsFactory({ + core: { + key: '' + }, + urls: baseUrls, + streamingEnabled: false +}); + +const config = { + core: { + authorizationKey: '' + }, + urls: baseUrls, + streamingEnabled: false +}; + +export default async function (fetchMock, assert) { + + assert.test('Evaluations / impressionsDisabled option', async t => { + // Mocking split changes + fetchMock.getOnce(url(settings, '/splitChanges?s=1.3&since=-1&rbSince=-1'), { status: 200, body: splitChangesMock1 }); + fetchMock.get(new RegExp(`${url(settings, '/segmentChanges/')}.*`), { status: 200, body: { since: 10, till: 10, name: 'segmentName', added: [], removed: [] } }); + fetchMock.post(url(settings, '/v1/keys/ss'), 200); + fetchMock.post(url(settings, '/v1/metrics/usage'), 200); + fetchMock.post(url(settings, '/v1/metrics/config'), 200); + // Mock default telemetry URLs as fallback + fetchMock.post('https://telemetry.split.io/api/v1/keys/ss', 200); + fetchMock.post('https://telemetry.split.io/api/v1/metrics/usage', 200); + fetchMock.post('https://telemetry.split.io/api/v1/metrics/usage', 200); + fetchMock.post('https://telemetry.split.io/api/v1/metrics/config', 200); + + fetchMock.post(url(settings, '/testImpressions/bulk'), 200); + fetchMock.post(url(settings, '/testImpressions/count'), 200); + + const splitio = SplitFactory(config); + const client = splitio.client(); + + await client.ready(); + + // getTreatment + t.equal(client.getTreatment('emi@split.io', 'split_with_config', { impressionsDisabled: true }), 'o.n', 'getTreatment with impressionsDisabled: true returns correct treatment'); + t.equal(client.getTreatment('emi@split.io', 'split_with_config', { impressionsDisabled: false }), 'o.n', 'getTreatment with impressionsDisabled: false returns correct treatment'); + + // getTreatments + t.deepEqual(client.getTreatments('emi@split.io', ['split_with_config', 'whitelist'], { impressionsDisabled: true }), { + split_with_config: 'o.n', + whitelist: 'not_allowed' + }, 'getTreatments with impressionsDisabled: true returns correct treatments'); + + // getTreatmentWithConfig + const expectedConfig = '{"color":"brown","dimensions":{"height":12,"width":14},"text":{"inner":"click me"}}'; + t.deepEqual(client.getTreatmentWithConfig('emi@split.io', 'split_with_config', { impressionsDisabled: true }), { + treatment: 'o.n', + config: expectedConfig + }, 'getTreatmentWithConfig with impressionsDisabled: true returns correct treatment and config'); + + // getTreatmentsWithConfig + t.deepEqual(client.getTreatmentsWithConfig('emi@split.io', ['split_with_config', 'whitelist'], { impressionsDisabled: true }), { + split_with_config: { treatment: 'o.n', config: expectedConfig }, + whitelist: { treatment: 'not_allowed', config: null } + }, 'getTreatmentsWithConfig with impressionsDisabled: true returns correct treatments and configs'); + + await client.destroy(); + t.end(); + }); +} diff --git a/src/__tests__/nodeSuites/flag-sets.spec.js b/src/__tests__/nodeSuites/flag-sets.spec.js index bacbd507a..d62974fee 100644 --- a/src/__tests__/nodeSuites/flag-sets.spec.js +++ b/src/__tests__/nodeSuites/flag-sets.spec.js @@ -26,24 +26,24 @@ export default function flagSets(fetchMock, t) { let factory, manager, client = []; // Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3 - fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1&sets=set_1,set_2', function () { - return { status: 200, body: splitChange2}; + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1&sets=set_1,set_2', function () { + return { status: 200, body: splitChange2 }; }); // Receive split change with 1 split belonging to set_1 only - fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602796638344&rbSince=-1&sets=set_1,set_2', function () { + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602796638344&rbSince=-1&sets=set_1,set_2', function () { // stored feature flags before update const storedFlags = manager.splits(); assert.true(storedFlags.length === 1, 'only one feature flag should be added'); assert.true(storedFlags[0].name === 'workm'); - assert.deepEqual(storedFlags[0].sets, ['set_1','set_2']); + assert.deepEqual(storedFlags[0].sets, ['set_1', 'set_2']); // send split change - return { status: 200, body: splitChange1}; + return { status: 200, body: splitChange1 }; }); // Receive split change with 1 split belonging to set_3 only - fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602797638344&rbSince=-1&sets=set_1,set_2', function () { + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602797638344&rbSince=-1&sets=set_1,set_2', function () { // stored feature flags before update const storedFlags = manager.splits(); assert.true(storedFlags.length === 1); @@ -51,10 +51,10 @@ export default function flagSets(fetchMock, t) { assert.deepEqual(storedFlags[0].sets, ['set_1'], 'the feature flag should be updated'); // send split change - return { status: 200, body: splitChange0}; + return { status: 200, body: splitChange0 }; }); - fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602798638344&rbSince=-1&sets=set_1,set_2', async function () { + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602798638344&rbSince=-1&sets=set_1,set_2', async function () { // stored feature flags before update const storedFlags = manager.splits(); assert.true(storedFlags.length === 0, 'the feature flag should be removed'); @@ -66,8 +66,8 @@ export default function flagSets(fetchMock, t) { }); // Initialize a factory with polling and sets set_1 & set_2 configured. - const splitFilters = [{ type: 'bySet', values: ['set_1','set_2'] }]; - factory = SplitFactory({ ...baseConfig, sync: { splitFilters }}); + const splitFilters = [{ type: 'bySet', values: ['set_1', 'set_2'] }]; + factory = SplitFactory({ ...baseConfig, sync: { splitFilters } }); client = factory.client(); await client.ready(); manager = factory.manager(); @@ -79,26 +79,26 @@ export default function flagSets(fetchMock, t) { let factory, manager, client = []; // Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3 - fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1', function () { - return { status: 200, body: splitChange2}; + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1', function () { + return { status: 200, body: splitChange2 }; }); // Receive split change with 1 split belonging to set_1 only - fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602796638344&rbSince=-1', function () { + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602796638344&rbSince=-1', function () { // stored feature flags before update const storedFlags = manager.splits(); assert.true(storedFlags.length === 2, 'every feature flag should be added'); assert.true(storedFlags[0].name === 'workm'); assert.true(storedFlags[1].name === 'workm_set_3'); - assert.deepEqual(storedFlags[0].sets, ['set_1','set_2']); + assert.deepEqual(storedFlags[0].sets, ['set_1', 'set_2']); assert.deepEqual(storedFlags[1].sets, ['set_3']); // send split change - return { status: 200, body: splitChange1}; + return { status: 200, body: splitChange1 }; }); // Receive split change with 1 split belonging to set_3 only - fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602797638344&rbSince=-1', function () { + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602797638344&rbSince=-1', function () { // stored feature flags before update const storedFlags = manager.splits(); assert.true(storedFlags.length === 2); @@ -108,10 +108,10 @@ export default function flagSets(fetchMock, t) { assert.deepEqual(storedFlags[1].sets, ['set_3'], 'the feature flag should remain as it was'); // send split change - return { status: 200, body: splitChange0}; + return { status: 200, body: splitChange0 }; }); - fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602798638344&rbSince=-1', async function () { + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602798638344&rbSince=-1', async function () { // stored feature flags before update const storedFlags = manager.splits(); assert.true(storedFlags.length === 2); @@ -142,30 +142,36 @@ export default function flagSets(fetchMock, t) { mockSegmentChanges(fetchMock, new RegExp(baseUrls.sdk + '/segmentChanges/*'), []); fetchMock.post('*', 200); // Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3 - fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1&sets=set_1', function () { - return { status: 200, body: splitChange2}; + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1&sets=set_1', function () { + return { status: 200, body: splitChange2 }; }); fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602796638344&rbSince=-1&sets=set_1', async function () { // stored feature flags before update - assert.deepEqual(client.getTreatmentsByFlagSet(key, 'set_1'), {workm: 'on'}, 'only the flag in set_1 can be evaluated'); + assert.deepEqual(client.getTreatmentsByFlagSet(key, 'set_1'), { workm: 'on' }, 'only the flag in set_1 can be evaluated'); assert.deepEqual(client.getTreatmentsByFlagSet(key, 'set_2'), {}, 'only the flag in set_1 can be evaluated'); assert.deepEqual(client.getTreatmentsByFlagSet(key, 'set_3'), {}, 'only the flag in set_1 can be evaluated'); assert.deepEqual(client.getTreatmentsWithConfigByFlagSet(key, 'set_1'), { workm: { treatment: 'on', config: null } }, 'only the flag in set_1 can be evaluated'); assert.deepEqual(client.getTreatmentsWithConfigByFlagSet(key, 'set_2'), {}, 'only the flag in set_1 can be evaluated'); assert.deepEqual(client.getTreatmentsWithConfigByFlagSet(key, 'set_3'), {}, 'only the flag in set_1 can be evaluated'); - assert.deepEqual(client.getTreatmentsByFlagSets(key, ['set_1','set_2','set_3']), {workm: 'on'}, 'only the flag in set_1 can be evaluated'); - assert.deepEqual(client.getTreatmentsWithConfigByFlagSets(key, ['set_1','set_2','set_3']), { workm: { treatment: 'on', config: null } }, 'only the flag in set_1 can be evaluated'); + assert.deepEqual(client.getTreatmentsByFlagSets(key, ['set_1', 'set_2', 'set_3']), { workm: 'on' }, 'only the flag in set_1 can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSets(key, ['set_1', 'set_2', 'set_3']), { workm: { treatment: 'on', config: null } }, 'only the flag in set_1 can be evaluated'); + + // New assertions for impressionsDisabled + assert.deepEqual(client.getTreatmentsByFlagSet(key, 'set_1', { impressionsDisabled: true }), { workm: 'on' }, 'impressionsDisabled: true supported in getTreatmentsByFlagSet'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSet(key, 'set_1', { impressionsDisabled: true }), { workm: { treatment: 'on', config: null } }, 'impressionsDisabled: true supported in getTreatmentsWithConfigByFlagSet'); + assert.deepEqual(client.getTreatmentsByFlagSets(key, ['set_1', 'set_2', 'set_3'], { impressionsDisabled: true }), { workm: 'on' }, 'impressionsDisabled: true supported in getTreatmentsByFlagSets'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSets(key, ['set_1', 'set_2', 'set_3'], { impressionsDisabled: true }), { workm: { treatment: 'on', config: null } }, 'impressionsDisabled: true supported in getTreatmentsWithConfigByFlagSets'); await client.destroy(); assert.end(); // send split change - return { status: 200, body: splitChange1}; + return { status: 200, body: splitChange1 }; }); // Initialize a factory with set_1 configured. const splitFilters = [{ type: 'bySet', values: ['set_1'] }]; - factory = SplitFactory({ ...baseConfig, sync: { splitFilters }}); + factory = SplitFactory({ ...baseConfig, sync: { splitFilters } }); client = factory.client(); await client.ready(); @@ -180,25 +186,25 @@ export default function flagSets(fetchMock, t) { fetchMock.post('*', 200); // Receive split change with 1 split belonging to set_1 & set_2 and one belonging to set_3 - fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1', function () { - return { status: 200, body: splitChange2}; + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=-1&rbSince=-1', function () { + return { status: 200, body: splitChange2 }; }); - fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602796638344&rbSince=-1', async function () { + fetchMock.getOnce(baseUrls.sdk + '/splitChanges?s=1.3&since=1602796638344&rbSince=-1', async function () { // stored feature flags before update - assert.deepEqual(client.getTreatmentsByFlagSet(key, 'set_1'), {workm: 'on'}, 'all flags can be evaluated'); - assert.deepEqual(client.getTreatmentsByFlagSet(key, 'set_2'), {workm: 'on'}, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsByFlagSet(key, 'set_1'), { workm: 'on' }, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsByFlagSet(key, 'set_2'), { workm: 'on' }, 'all flags can be evaluated'); assert.deepEqual(client.getTreatmentsByFlagSet(key, 'set_3'), { workm_set_3: 'on' }, 'all flags can be evaluated'); assert.deepEqual(client.getTreatmentsWithConfigByFlagSet(key, 'set_1'), { workm: { treatment: 'on', config: null } }, 'all flags can be evaluated'); assert.deepEqual(client.getTreatmentsWithConfigByFlagSet(key, 'set_2'), { workm: { treatment: 'on', config: null } }, 'all flags can be evaluated'); assert.deepEqual(client.getTreatmentsWithConfigByFlagSet(key, 'set_3'), { workm_set_3: { treatment: 'on', config: null } }, 'all flags can be evaluated'); - assert.deepEqual(client.getTreatmentsByFlagSets(key, ['set_1','set_2','set_3']), { workm: 'on', workm_set_3: 'on' }, 'all flags can be evaluated'); - assert.deepEqual(client.getTreatmentsWithConfigByFlagSets(key, ['set_1','set_2','set_3']), { workm: { treatment: 'on', config: null }, workm_set_3: { treatment: 'on', config: null } }, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsByFlagSets(key, ['set_1', 'set_2', 'set_3']), { workm: 'on', workm_set_3: 'on' }, 'all flags can be evaluated'); + assert.deepEqual(client.getTreatmentsWithConfigByFlagSets(key, ['set_1', 'set_2', 'set_3']), { workm: { treatment: 'on', config: null }, workm_set_3: { treatment: 'on', config: null } }, 'all flags can be evaluated'); await client.destroy(); assert.end(); // send split change - return { status: 200, body: splitChange1}; + return { status: 200, body: splitChange1 }; }); diff --git a/src/__tests__/nodeSuites/impressions-listener.spec.js b/src/__tests__/nodeSuites/impressions-listener.spec.js index 5a09d532c..841bdf1e4 100644 --- a/src/__tests__/nodeSuites/impressions-listener.spec.js +++ b/src/__tests__/nodeSuites/impressions-listener.spec.js @@ -43,9 +43,10 @@ export default function (assert) { client.getTreatment({ matchingKey: 'marcio@split.io', bucketingKey: 'impr_bucketing_2' }, 'qc_team'); client.getTreatment('facundo@split.io', 'qc_team', testAttrs); client.getTreatment('facundo@split.io', 'qc_team', testAttrs); + client.getTreatment('facundo@split.io', 'whitelist', testAttrs, { impressionsDisabled: false }); setTimeout(() => { - assert.equal(listener.logImpression.callCount, 4, 'Impression listener logImpression method should be called after we call client.getTreatment, once per each impression generated.'); + assert.equal(listener.logImpression.callCount, 5, 'Impression listener logImpression method should be called after we call client.getTreatment, once per each impression generated.'); assert.true(listener.logImpression.getCall(0).calledWithExactly({ impression: { feature: 'hierarchical_splits_test', @@ -94,6 +95,17 @@ export default function (assert) { attributes: testAttrs, ...metaData })); + assert.true(listener.logImpression.getCall(4).calledWithMatch({ + impression: { + feature: 'whitelist', + keyName: 'facundo@split.io', + treatment: 'allowed', + bucketingKey: undefined, + label: 'default rule', + }, + attributes: testAttrs, + ...metaData + })); client.destroy(); assert.end(); diff --git a/src/__tests__/nodeSuites/impressions.debug.spec.js b/src/__tests__/nodeSuites/impressions.debug.spec.js index 8b77e218d..a12003ba7 100644 --- a/src/__tests__/nodeSuites/impressions.debug.spec.js +++ b/src/__tests__/nodeSuites/impressions.debug.spec.js @@ -70,7 +70,12 @@ export default async function (key, fetchMock, assert) { }, { 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.'); + }, { + f: 'whitelist', + i: [{ + k: 'emi@split.io', t: 'not_allowed', m: data[1].i[0].m, r: 'default rule', properties: '{"prop1":"value2"}' + }] + }], 'We performed evaluations for two splits, so we should have 2 items total.'); client.destroy().then(() => { assert.end(); @@ -81,7 +86,10 @@ export default async function (key, fetchMock, assert) { fetchMock.postOnce(url(settings, '/testImpressions/count'), (url, opts) => { assert.deepEqual(JSON.parse(opts.body), { - pf: [{ f: 'always_on_impressions_disabled_true', m: truncatedTimeFrame, rc: 1 }] + pf: [ + { f: 'always_on_impressions_disabled_true', m: truncatedTimeFrame, rc: 3 }, + { f: 'whitelist', m: truncatedTimeFrame, rc: 3 } + ] }, 'We should generate impression count for the feature with track impressions disabled.'); return 200; @@ -89,7 +97,10 @@ export default async function (key, fetchMock, assert) { fetchMock.postOnce(url(settings, '/v1/keys/ss'), (url, opts) => { assert.deepEqual(JSON.parse(opts.body), { - keys: [{ f: 'always_on_impressions_disabled_true', ks: ['other_key'] }] + keys: [ + { f: 'always_on_impressions_disabled_true', ks: ['other_key', 'emma@split.io'] }, + { f: 'whitelist', ks: ['emi@split.io', 'emma@split.io'] } + ] }, 'We should track unique keys for the feature with track impressions disabled.'); return 200; @@ -109,4 +120,18 @@ export default async function (key, fetchMock, assert) { assert.equal(client.getTreatments('emi@split.io', ['split_with_config'], undefined, { properties: { prop1: 'value2' } }).split_with_config, 'o.n'); assert.equal(client.getTreatmentWithConfig('emi@split.io', 'split_with_config', undefined, { properties: { prop1: 'value3' } }).treatment, 'o.n'); assert.equal(client.getTreatmentsWithConfig('emi@split.io', ['split_with_config'], undefined, { properties: { prop1: 'value4' } }).split_with_config.treatment, 'o.n'); + + // impressions disabled + // Flags with impression enabled should generate: + // - 1 impression for whitelist + // - 3 impressions count for whitelist + // - 2 impressions unique keys for whitelist + assert.equal(client.getTreatment('emi@split.io', 'whitelist', undefined, { impressionsDisabled: true, properties: { prop1: 'value1' } }), 'not_allowed'); + assert.equal(client.getTreatments('emi@split.io', ['whitelist'], undefined, { properties: { prop1: 'value2' } }).whitelist, 'not_allowed'); + assert.equal(client.getTreatmentWithConfig('emi@split.io', 'whitelist', undefined, { impressionsDisabled: true, properties: { prop1: 'value3' } }).treatment, 'not_allowed'); + assert.equal(client.getTreatmentsWithConfig('emma@split.io', ['whitelist'], undefined, { impressionsDisabled: true, properties: { prop1: 'value4' } }).whitelist.treatment, 'not_allowed'); + + // Flags with impression disabled should only generate impressions count and unique keys + assert.equal(client.getTreatment('emma@split.io', 'always_on_impressions_disabled_true', undefined, { impressionsDisabled: true }), 'on'); + assert.equal(client.getTreatment('emma@split.io', 'always_on_impressions_disabled_true', undefined, { impressionsDisabled: false }), 'on'); } diff --git a/src/__tests__/nodeSuites/impressions.none.spec.js b/src/__tests__/nodeSuites/impressions.none.spec.js index df3256ab8..99c735c71 100644 --- a/src/__tests__/nodeSuites/impressions.none.spec.js +++ b/src/__tests__/nodeSuites/impressions.none.spec.js @@ -53,7 +53,8 @@ export default async function (key, fetchMock, assert) { { f: 'split_with_config', m: truncatedTimeFrame, rc: 3 }, { f: 'always_off', m: truncatedTimeFrame, rc: 3 }, { f: 'always_on', m: truncatedTimeFrame, rc: 5 }, - { f: 'always_on_impressions_disabled_true', m: truncatedTimeFrame, rc: 1 } + { f: 'always_on_impressions_disabled_true', m: truncatedTimeFrame, rc: 3 }, + { f: 'whitelist', m: truncatedTimeFrame, rc: 2 } ] }); return 200; @@ -79,6 +80,10 @@ export default async function (key, fetchMock, assert) { { f: 'always_on_impressions_disabled_true', ks: ['emi@split.io'] + }, + { + f: 'whitelist', + ks: ['emma@split.io'] } ] }, 'We performed evaluations for 4 flags, so we should have 4 items total.'); @@ -99,6 +104,11 @@ export default async function (key, fetchMock, assert) { client.getTreatment('emi@split.io', 'split_with_config'); client.getTreatment('emma@split.io', 'split_with_config'); client.getTreatment('emi@split.io', 'always_on_impressions_disabled_true'); + client.getTreatment('emma@split.io', 'whitelist', undefined, { impressionsDisabled: false }); + client.getTreatment('emma@split.io', 'whitelist', undefined, { impressionsDisabled: true }); + client.getTreatment('emi@split.io', 'always_on_impressions_disabled_true', undefined, { impressionsDisabled: true }); + client.getTreatment('emi@split.io', 'always_on_impressions_disabled_true', undefined, { impressionsDisabled: false }); + client.destroy().then(() => { assert.end(); diff --git a/src/__tests__/nodeSuites/impressions.spec.js b/src/__tests__/nodeSuites/impressions.spec.js index 3c883d34f..caa1b339e 100644 --- a/src/__tests__/nodeSuites/impressions.spec.js +++ b/src/__tests__/nodeSuites/impressions.spec.js @@ -49,18 +49,20 @@ export default async function (key, fetchMock, assert) { assert.equal(opts.headers.SplitSDKImpressionsMode, OPTIMIZED); const data = JSON.parse(opts.body); - assert.equal(data.length, 3, 'We performed evaluations for 4 features, but one with `impressionsDisabled` true, so we should have 3 items total.'); + assert.equal(data.length, 3, 'We performed evaluations for 5 features, but two with `impressionsDisabled` true, so we should have 3 items total.'); // finding these validate the feature names collection too const dependencyChildImpr = data.filter(e => e.f === 'hierarchical_splits_test')[0]; const splitWithConfigImpr = data.filter(e => e.f === 'split_with_config')[0]; const notExistentSplitImpr = data.filter(e => e.f === 'not_existent_split')[0]; const alwaysOnWithImpressionsDisabledTrue = data.filter(e => e.f === 'always_on_impressions_disabled_true'); + const whitelist = data.filter(e => e.f === 'whitelist'); assert.equal(notExistentSplitImpr.i.length, 1); // Only one, the split not found is filtered by the non existent Split check. assert.equal(splitWithConfigImpr.i.length, 3); assert.equal(dependencyChildImpr.i.length, 1); assert.equal(alwaysOnWithImpressionsDisabledTrue.length, 0); + assert.equal(whitelist.length, 0); assert.true(dependencyChildImpr, 'Split we wanted to evaluate should be present on the impressions.'); assert.false(data.some(e => e.f === 'hierarchical_dep_always_on'), 'Parent split evaluations should not result in impressions.'); @@ -112,11 +114,12 @@ export default async function (key, fetchMock, assert) { fetchMock.postOnce(url(settings, '/testImpressions/count'), (url, opts) => { const data = JSON.parse(opts.body); - assert.equal(data.pf.length, 2, 'We should generate impression count for 2 features.'); + assert.equal(data.pf.length, 3, 'We should generate impression count for 3 features.'); // finding these validate the feature names collection too const splitWithConfigImpr = data.pf.filter(e => e.f === 'split_with_config')[0]; const alwaysOnWithImpressionsDisabledTrue = data.pf.filter(e => e.f === 'always_on_impressions_disabled_true')[0]; + const whitelist = data.pf.filter(e => e.f === 'whitelist')[0]; assert.equal(splitWithConfigImpr.rc, 1); assert.equal(typeof splitWithConfigImpr.m, 'number'); @@ -124,13 +127,16 @@ export default async function (key, fetchMock, assert) { assert.equal(alwaysOnWithImpressionsDisabledTrue.rc, 1); assert.equal(typeof alwaysOnWithImpressionsDisabledTrue.m, 'number'); assert.equal(alwaysOnWithImpressionsDisabledTrue.m, truncatedTimeFrame); + assert.equal(whitelist.rc, 1); + assert.equal(typeof whitelist.m, 'number'); + assert.equal(whitelist.m, truncatedTimeFrame); return 200; }); fetchMock.postOnce(url(settings, '/v1/keys/ss'), (url, opts) => { assert.deepEqual(JSON.parse(opts.body), { - keys: [{ f: 'always_on_impressions_disabled_true', ks: ['other_key'] }] + keys: [{ f: 'whitelist', ks: ['facundo@split.io'] }, { f: 'always_on_impressions_disabled_true', ks: ['other_key'] }] }, 'We should only track unique keys for features flags with track impressions disabled.'); return 200; @@ -156,6 +162,7 @@ export default async function (key, fetchMock, assert) { config: '{"color":"brown","dimensions":{"height":12,"width":14},"text":{"inner":"click me"}}' }, 'We should get an evaluation as always.'); client.getTreatmentWithConfig({ matchingKey: key, bucketingKey: 'test_buck_key' }, 'split_with_config'); + client.getTreatmentWithConfig({ matchingKey: key, bucketingKey: 'test_buck_key' }, 'whitelist', undefined, { impressionsDisabled: true }); client.getTreatmentWithConfig({ matchingKey: 'different', bucketingKey: 'test_buck_key' }, 'split_with_config'); // Impression should not be tracked (passed properties will not be submitted) diff --git a/src/__tests__/offline/node.spec.js b/src/__tests__/offline/node.spec.js index afbb495e0..2b8e9a22e 100644 --- a/src/__tests__/offline/node.spec.js +++ b/src/__tests__/offline/node.spec.js @@ -199,6 +199,12 @@ function DotYAMLTests(mockFileName, mockFileExt, assert) { assert.equal(client.getTreatment('qa-user', 'testing_split_off_with_config'), 'off'); assert.equal(client.getTreatment('qa-user', 'not_existent'), 'control'); + // Verify impressionsDisabled option + assert.equal(client.getTreatment('qa-user', 'testing_split_on', { impressionsDisabled: false }), 'on'); + assert.deepEqual(client.getTreatmentWithConfig('qa-user', 'testing_split_on', { impressionsDisabled: false }), { treatment: 'on', config: null }); + assert.deepEqual(client.getTreatments('qa-user', ['testing_split_on'], { impressionsDisabled: true }), { testing_split_on: 'on' }); + assert.deepEqual(client.getTreatmentsWithConfig('qa-user', ['testing_split_on'], { impressionsDisabled: true }), { testing_split_on: { treatment: 'on', config: null } }); + assert.deepEqual(client.getTreatmentWithConfig('qa-user', 'testing_split_on'), { treatment: 'on', config: null }); assert.deepEqual(client.getTreatmentWithConfig('qa-user', 'testing_split_only_wl'), { treatment: 'control', config: null }); assert.deepEqual(client.getTreatmentWithConfig('key_for_wl', 'testing_split_only_wl'), { treatment: 'whitelisted', config: null }); diff --git a/src/__tests__/online/node.spec.js b/src/__tests__/online/node.spec.js index 35d87a3c3..ea1539df0 100644 --- a/src/__tests__/online/node.spec.js +++ b/src/__tests__/online/node.spec.js @@ -9,6 +9,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 evaluationsImpressionsDisabledSuite from '../nodeSuites/evaluations-impressionsDisabled.spec'; import eventsSuite from '../nodeSuites/events.spec'; import impressionsSuite from '../nodeSuites/impressions.spec'; import impressionsSuiteDebug from '../nodeSuites/impressions.debug.spec'; @@ -62,6 +63,7 @@ tape('## Node.js - E2E CI Tests ##', async function (assert) { 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)); + assert.test('E2E / In Memory - Impressions Disabled', evaluationsImpressionsDisabledSuite.bind(null, fetchMock)); /* Check impressions */ assert.test('E2E / Impressions', impressionsSuite.bind(null, key, fetchMock)); diff --git a/ts-tests/index.ts b/ts-tests/index.ts index c1cf06960..945f8c475 100644 --- a/ts-tests/index.ts +++ b/ts-tests/index.ts @@ -86,6 +86,7 @@ const attributes: SplitIO.Attributes = { attr7: true }; const evaluationOptions: SplitIO.EvaluationOptions = { + impressionsDisabled: true, properties: { prop1: 1, prop2: '2', From 871a93561ac6a3e9b8d16869f60dd191cb51da31 Mon Sep 17 00:00:00 2001 From: Emmanuel Zamora Date: Wed, 26 Nov 2025 14:47:40 -0300 Subject: [PATCH 2/2] Update version --- package-lock.json | 4 ++-- package.json | 4 ++-- src/settings/defaults/version.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 47f93289b..2c6c47399 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@splitsoftware/splitio", - "version": "11.8.0", + "version": "11.8.1-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio", - "version": "11.8.0", + "version": "11.8.1-rc.1", "license": "Apache-2.0", "dependencies": { "@splitsoftware/splitio-commons": "2.8.1-rc.1", diff --git a/package.json b/package.json index da3611c5e..9437f49d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "11.8.0", + "version": "11.8.1-rc.1", "description": "Split SDK", "files": [ "README.md", @@ -107,7 +107,7 @@ "test-node-e2e-destroy": "cross-env NODE_ENV=test tape -r ./ts-node.register src/__tests__/destroy/node.spec.js | tap-min", "test-node-e2e-errorCatching": "cross-env NODE_ENV=test tape -r ./ts-node.register src/__tests__/errorCatching/node.spec.js | tap-min", "test-node-e2e-push": "cross-env NODE_ENV=test tape -r ./ts-node.register src/__tests__/push/node.spec.js | tap-min", - "test-node-e2e-redis": "cross-env NODE_ENV=test tape -r ./ts-node.register src/__tests__/consumer/node_redis.spec.js", + "test-node-e2e-redis": "cross-env NODE_ENV=test tape -r ./ts-node.register src/__tests__/consumer/node_redis.spec.js | tap-min", "test-ts-decls": "tsc --build ts-tests", "test": "npm run test-node && npm run test-browser", "all": "npm run check && npm run build && npm run test-ts-decls && npm run test", diff --git a/src/settings/defaults/version.js b/src/settings/defaults/version.js index 378697e28..80a104c79 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.8.1-rc.1';