diff --git a/package-lock.json b/package-lock.json index aa1e9c5..ee7cf0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.6.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio-commons": "2.8.0", + "@splitsoftware/splitio-commons": "2.8.1-rc.1", "tslib": "^2.3.1", "unfetch": "^4.2.0" }, @@ -1396,9 +1396,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", @@ -10493,9 +10493,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 c710d4b..f7f85e4 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "bugs": "https://github.com/splitio/javascript-browser-client/issues", "homepage": "https://github.com/splitio/javascript-browser-client#readme", "dependencies": { - "@splitsoftware/splitio-commons": "2.8.0", + "@splitsoftware/splitio-commons": "2.8.1-rc.1", "tslib": "^2.3.1", "unfetch": "^4.2.0" }, diff --git a/src/__tests__/browserSuites/evaluations-impressionsDisabled.spec.js b/src/__tests__/browserSuites/evaluations-impressionsDisabled.spec.js new file mode 100644 index 0000000..c6fab82 --- /dev/null +++ b/src/__tests__/browserSuites/evaluations-impressionsDisabled.spec.js @@ -0,0 +1,70 @@ +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 +}); + +export default async function (configInMemory, configInLocalStorage, 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(configInMemory); + const client = splitio.client(); + + await client.ready(); + + // getTreatment + t.equal(client.getTreatment('split_with_config', { impressionsDisabled: true }), 'o.n', 'getTreatment with impressionsDisabled: true returns correct treatment'); + t.equal(client.getTreatment('split_with_config', { impressionsDisabled: false }), 'o.n', 'getTreatment with impressionsDisabled: false returns correct treatment'); + + // getTreatments + t.deepEqual(client.getTreatments(['split_with_config', 'whitelist'], { impressionsDisabled: true }), { + split_with_config: 'o.n', + whitelist: '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('split_with_config', { impressionsDisabled: true }), { + treatment: 'o.n', + config: expectedConfig + }, 'getTreatmentWithConfig with impressionsDisabled: true returns correct treatment and config'); + + // getTreatmentsWithConfig + t.deepEqual(client.getTreatmentsWithConfig(['split_with_config', 'whitelist'], { impressionsDisabled: true }), { + split_with_config: { treatment: 'o.n', config: expectedConfig }, + whitelist: { treatment: 'allowed', config: null } + }, 'getTreatmentsWithConfig with impressionsDisabled: true returns correct treatments and configs'); + + await client.destroy(); + t.end(); + }); + +} diff --git a/src/__tests__/browserSuites/impressions-listener.spec.js b/src/__tests__/browserSuites/impressions-listener.spec.js index 9907eab..703b6e0 100644 --- a/src/__tests__/browserSuites/impressions-listener.spec.js +++ b/src/__tests__/browserSuites/impressions-listener.spec.js @@ -50,6 +50,7 @@ export default function (assert) { client2.getTreatment('qc_team'); client2.getTreatmentWithConfig('qc_team'); // Validate that the impression is the same. client3.getTreatment('qc_team', testAttrs); + client.getTreatment('whitelist', testAttrs, { impressionsDisabled: true }); setTimeout(() => { const secondImpression = { @@ -62,7 +63,7 @@ export default function (assert) { properties: undefined }; - 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', @@ -98,6 +99,17 @@ export default function (assert) { attributes: testAttrs, ...metaData })); + assert.true(listener.logImpression.getCall(4).calledWithMatch({ + impression: { + feature: 'whitelist', + keyName: 'nicolas@split.io', + treatment: 'not_allowed', + bucketingKey: undefined, + label: 'default rule', + }, + attributes: testAttrs, + ...metaData + })); client3.destroy(); client2.destroy(); diff --git a/src/__tests__/browserSuites/impressions.debug.spec.js b/src/__tests__/browserSuites/impressions.debug.spec.js index 3ca81a7..12773b3 100644 --- a/src/__tests__/browserSuites/impressions.debug.spec.js +++ b/src/__tests__/browserSuites/impressions.debug.spec.js @@ -62,6 +62,11 @@ 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 }] + }, { + f: 'whitelist', + i: [{ + k: 'facundo@split.io', t: 'allowed', m: data[1].i[0].m, r: 'default rule', properties: '{"prop1":"value2"}' + }] }]); client.destroy().then(() => { @@ -73,7 +78,10 @@ export default function (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; @@ -81,7 +89,9 @@ export default function (fetchMock, assert) { fetchMock.postOnce(url(settings, '/v1/keys/cs'), (url, opts) => { assert.deepEqual(JSON.parse(opts.body), { - keys: [{ fs: ['always_on_impressions_disabled_true'], k: 'facundo@split.io' }] + keys: [ + { k: 'facundo@split.io', fs: ['always_on_impressions_disabled_true', 'whitelist'] } + ] }, 'We should track unique keys for the feature with track impressions disabled.'); return 200; @@ -94,5 +104,19 @@ export default function (fetchMock, assert) { client.getTreatment('split_with_config'); client.getTreatment('split_with_config'); assert.equal(client.getTreatment('always_on_impressions_disabled_true'), 'on'); + + // 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('whitelist', undefined, { impressionsDisabled: true, properties: { prop1: 'value1' } }), 'allowed'); + assert.equal(client.getTreatments(['whitelist'], undefined, { properties: { prop1: 'value2' } }).whitelist, 'allowed'); + assert.equal(client.getTreatmentWithConfig('whitelist', undefined, { impressionsDisabled: true, properties: { prop1: 'value3' } }).treatment, 'allowed'); + assert.equal(client.getTreatmentsWithConfig(['whitelist'], undefined, { impressionsDisabled: true, properties: { prop1: 'value4' } }).whitelist.treatment, 'allowed'); + + // Flags with impression disabled should only generate impressions count and unique keys + assert.equal(client.getTreatment('always_on_impressions_disabled_true', undefined, { impressionsDisabled: true }), 'on'); + assert.equal(client.getTreatment('always_on_impressions_disabled_true', undefined, { impressionsDisabled: false }), 'on'); }); } diff --git a/src/__tests__/browserSuites/impressions.spec.js b/src/__tests__/browserSuites/impressions.spec.js index a6caa47..eb0734d 100644 --- a/src/__tests__/browserSuites/impressions.spec.js +++ b/src/__tests__/browserSuites/impressions.spec.js @@ -49,11 +49,12 @@ export default function (fetchMock, assert) { const assertPayload = req => { const resp = JSON.parse(req.body); - assert.equal(resp.length, 2, 'We performed evaluations for 3 features, but one with `impressionsDisabled` true, so we should have 2 items total'); + assert.equal(resp.length, 2, 'We performed evaluations for 4 features, but two with `impressionsDisabled` true, so we should have 2 items total'); const dependencyChildImpr = resp.filter(e => e.f === 'hierarchical_splits_test')[0]; const splitWithConfigImpr = resp.filter(e => e.f === 'split_with_config')[0]; const alwaysOnWithImpressionsDisabledTrue = resp.filter(e => e.f === 'always_on_impressions_disabled_true'); + const whitelist = resp.filter(e => e.f === 'whitelist'); assert.true(dependencyChildImpr, 'Split we wanted to evaluate should be present on the impressions.'); assert.false(resp.some(e => e.f === 'hierarchical_dep_always_on'), 'Parent split evaluations should not result in impressions.'); @@ -62,6 +63,7 @@ export default function (fetchMock, assert) { assert.false(Object.prototype.hasOwnProperty.call(splitWithConfigImpr.i[0], 'configuration'), 'Impressions do not change with configuration evaluations.'); assert.false(Object.prototype.hasOwnProperty.call(splitWithConfigImpr.i[0], 'config'), 'Impressions do not change with configuration evaluations.'); assert.equal(alwaysOnWithImpressionsDisabledTrue.length, 0); + assert.equal(whitelist.length, 0); const { k, @@ -96,11 +98,12 @@ export default function (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 impressions count for 2 features.'); + assert.equal(data.pf.length, 3, 'We should generate impressions 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, 2); assert.equal(typeof splitWithConfigImpr.m, 'number'); @@ -108,13 +111,16 @@ export default function (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/cs'), (url, opts) => { assert.deepEqual(JSON.parse(opts.body), { - keys: [{ fs: [ 'always_on_impressions_disabled_true' ], k: 'facundo@split.io' }] + keys: [ { k: 'facundo@split.io', fs: [ 'whitelist', 'always_on_impressions_disabled_true' ]}] }, 'We should only track unique keys for features flags with track impressions disabled.'); return 200; @@ -130,6 +136,7 @@ export default function (fetchMock, assert) { }, 'We should get an evaluation as always.'); client.getTreatmentWithConfig('split_with_config'); client.getTreatmentWithConfig('split_with_config'); + client.getTreatmentWithConfig('whitelist', undefined, { impressionsDisabled: true }); // Impression should not be tracked assert.equal(client.getTreatment('always_on_impressions_disabled_true'), 'on'); diff --git a/src/__tests__/consumer/browser_consumer_partial.spec.js b/src/__tests__/consumer/browser_consumer_partial.spec.js index a83386d..077bea6 100644 --- a/src/__tests__/consumer/browser_consumer_partial.spec.js +++ b/src/__tests__/consumer/browser_consumer_partial.spec.js @@ -14,7 +14,7 @@ const expectedSplitView = { name: 'hierarchical_splits_testing_on', trafficType: const wrapperPrefix = 'PLUGGABLE_STORAGE_UT'; const wrapperInstance = inMemoryWrapperFactory(); -const TOTAL_RAW_IMPRESSIONS = 17; +const TOTAL_RAW_IMPRESSIONS = 21; const TOTAL_EVENTS = 5; /** @type SplitIO.IBrowserAsyncSettings */ @@ -48,7 +48,7 @@ tape('Browser Consumer Partial mode with pluggable storage', function (t) { const resp = JSON.parse(req.body); assert.equal(resp.reduce((prev, cur) => { return prev + cur.i.length; - }, 0), TOTAL_RAW_IMPRESSIONS - 1, 'Impressions were deduped'); + }, 0), TOTAL_RAW_IMPRESSIONS - 5, 'Impressions were deduped'); return 200; }); @@ -58,7 +58,7 @@ tape('Browser Consumer Partial mode with pluggable storage', function (t) { assert.deepEqual(data, { keys: [{ k: 'UT_Segment_member', - fs: ['always-on-impressions-disabled-true'] + fs: ['always-on-impressions-disabled-true', 'always-on'] }] }, 'Unique keys for the evaluation with impressions disabled true.'); @@ -153,6 +153,12 @@ tape('Browser Consumer Partial mode with pluggable storage', function (t) { assert.equal(await client.getTreatment('hierarchical_splits_testing_on_negated'), 'off', 'Evaluations using pluggable storage should be correct.'); assert.equal(await client.getTreatment('always-on-impressions-disabled-true'), 'on', 'Evaluations using pluggable storage should be correct.'); + // Verify impressionsDisabled option + assert.deepEqual(await client.getTreatment('always-on', undefined, { impressionsDisabled: true }), 'on', 'Evaluations with impressionsDisabled: true should be correct.'); + assert.deepEqual(await client.getTreatmentWithConfig('always-on', undefined, { impressionsDisabled: true }), { treatment: 'on', config: null }, 'Evaluations with impressionsDisabled: true should be correct.'); + assert.deepEqual(await client.getTreatments(['always-on'], undefined, { impressionsDisabled: true }), { 'always-on': 'on' }, 'Evaluations with impressionsDisabled: true should be correct.'); + assert.deepEqual(await client.getTreatmentsWithConfig(['always-on'], undefined, { impressionsDisabled: true }), { 'always-on': { treatment: 'on', config: null } }, 'Evaluations with impressionsDisabled: true should be correct.'); + assert.equal(typeof client.track('user', 'test.event', 18).then, 'function', 'Track calls should always return a promise on Consumer mode.'); assert.equal(typeof client.track().then, 'function', 'Track calls should always return a promise on Consumer mode, even when parameters are incorrect.'); @@ -311,6 +317,13 @@ tape('Browser Consumer Partial mode with pluggable storage', function (t) { assert.equal(await client.getTreatment('hierarchical_splits_testing_off'), 'off', 'Evaluations using pluggable storage should be correct.'); assert.equal(await client.getTreatment('hierarchical_splits_testing_on_negated'), 'off', 'Evaluations using pluggable storage should be correct.'); + + // Verify impressionsDisabled option + assert.deepEqual(await client.getTreatment('always-on', undefined, { impressionsDisabled: true }), 'on', 'Evaluations with impressionsDisabled: true should be correct.'); + assert.deepEqual(await client.getTreatmentWithConfig('always-on', undefined, { impressionsDisabled: true }), { treatment: 'on', config: null }, 'Evaluations with impressionsDisabled: true should be correct.'); + assert.deepEqual(await client.getTreatments(['always-on'], undefined, { impressionsDisabled: true }), { 'always-on': 'on' }, 'Evaluations with impressionsDisabled: true should be correct.'); + assert.deepEqual(await client.getTreatmentsWithConfig(['always-on'], undefined, { impressionsDisabled: true }), { 'always-on': { treatment: 'on', config: null } }, 'Evaluations with impressionsDisabled: true should be correct.'); + assert.equal(typeof client.track('user', 'test.event', 18).then, 'function', 'Track calls should always return a promise on Consumer mode.'); assert.equal(typeof client.track().then, 'function', 'Track calls should always return a promise on Consumer mode, even when parameters are incorrect.'); diff --git a/src/__tests__/online/browser.spec.js b/src/__tests__/online/browser.spec.js index 42870e2..7ed3b7a 100644 --- a/src/__tests__/online/browser.spec.js +++ b/src/__tests__/online/browser.spec.js @@ -29,6 +29,7 @@ import membershipsMarcio from '../mocks/memberships.marcio@split.io.json'; import membershipsEmmanuel from '../mocks/memberships.emmanuel@split.io.json'; import { InLocalStorage } from '../../index'; import evaluationsFallbackSuite from '../browserSuites/evaluations-fallback.spec'; +import evaluationsImpressionsDisabledSuite from '../browserSuites/evaluations-impressionsDisabled.spec'; const settings = settingsFactory({ core: { @@ -106,6 +107,8 @@ tape('## E2E CI Tests ##', function (assert) { assert.test('E2E / Impressions Debug Mode', impressionsSuiteDebug.bind(null, fetchMock)); /* Check impression listener */ assert.test('E2E / Impression listener', impressionsListenerSuite); + /* Check impressions disabled */ + assert.test('E2E / Impressions Disabled', evaluationsImpressionsDisabledSuite.bind(null, configInMemory, configInLocalStorage, fetchMock)); /* Check telemetry */ assert.test('E2E / Telemetry', telemetrySuite.bind(null, fetchMock)); /* Check events */