diff --git a/CHANGES.txt b/CHANGES.txt index dcc52ef76..070dc76c4 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +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. + - Updated @splitsoftware/splitio-commons package to version 2.1.0. + 11.0.4 (January 9, 2025) - Bugfixing - Updated @splitsoftware/splitio-commons package to version 2.0.3, which properly handles rejected promises when using targeting rules with segment matchers in consumer modes (e.g., Redis and Pluggable storages). diff --git a/package-lock.json b/package-lock.json index a7b829acb..9c46f2f66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@splitsoftware/splitio", - "version": "11.0.4", + "version": "11.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio", - "version": "11.0.4", + "version": "11.1.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio-commons": "2.0.3", + "@splitsoftware/splitio-commons": "2.1.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.0.3", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.0.3.tgz", - "integrity": "sha512-weOdbLCBImBD9napQyDdACi35GrKJvh7nYLOYklStJZcNmdSWb7vMk+AFWdKdVjv8Ee23myo6/+1segDiCRJ8g==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.1.0.tgz", + "integrity": "sha512-7SJRBia0Pi72s76drH8kG2cVnCqkjMHMJQWJSFnG+rE/UOx9AROmuviOkY6tv6qYPJFqFQQGHGX6lXjxZhYzkw==", "dependencies": { "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" @@ -7537,9 +7537,9 @@ "dev": true }, "@splitsoftware/splitio-commons": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.0.3.tgz", - "integrity": "sha512-weOdbLCBImBD9napQyDdACi35GrKJvh7nYLOYklStJZcNmdSWb7vMk+AFWdKdVjv8Ee23myo6/+1segDiCRJ8g==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.1.0.tgz", + "integrity": "sha512-7SJRBia0Pi72s76drH8kG2cVnCqkjMHMJQWJSFnG+rE/UOx9AROmuviOkY6tv6qYPJFqFQQGHGX6lXjxZhYzkw==", "requires": { "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" diff --git a/package.json b/package.json index 1b7e916f0..efc19f4d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "11.0.4", + "version": "11.1.0", "description": "Split SDK", "files": [ "README.md", @@ -38,7 +38,7 @@ "node": ">=14.0.0" }, "dependencies": { - "@splitsoftware/splitio-commons": "2.0.3", + "@splitsoftware/splitio-commons": "2.1.0", "bloom-filters": "^3.0.4", "ioredis": "^4.28.0", "js-yaml": "^3.13.1", diff --git a/src/__tests__/browserSuites/impressions-listener.spec.js b/src/__tests__/browserSuites/impressions-listener.spec.js index 299dacaad..ca47995c2 100644 --- a/src/__tests__/browserSuites/impressions-listener.spec.js +++ b/src/__tests__/browserSuites/impressions-listener.spec.js @@ -57,17 +57,21 @@ export default function (assert) { keyName: 'marcio@split.io', treatment: 'no', bucketingKey: 'impr_bucketing_2', - label: 'default rule' + label: 'default rule', + pt: 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.true(listener.logImpression.getCall(0).calledWithMatch({ + assert.true(listener.logImpression.getCall(0).calledWithExactly({ impression: { feature: 'hierarchical_splits_test', keyName: 'nicolas@split.io', treatment: 'on', + time: listener.logImpression.getCall(0).args[0].impression.time, bucketingKey: undefined, label: 'expected label', + changeNumber: 2828282828, + pt: undefined }, attributes: undefined, ...metaData diff --git a/src/__tests__/browserSuites/impressions.debug.spec.js b/src/__tests__/browserSuites/impressions.debug.spec.js index 18d875b4e..5f90b7004 100644 --- a/src/__tests__/browserSuites/impressions.debug.spec.js +++ b/src/__tests__/browserSuites/impressions.debug.spec.js @@ -4,6 +4,7 @@ import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json'; import membershipsFacundo from '../mocks/memberships.facundo@split.io.json'; import { DEBUG } from '@splitsoftware/splitio-commons/src/utils/constants'; +import { truncateTimeFrame } from '@splitsoftware/splitio-commons/src/utils/time'; import { url } from '../testUtils'; const baseUrls = { @@ -19,6 +20,8 @@ const settings = settingsFactory({ streamingEnabled: false }); +let truncatedTimeFrame; + export default function (fetchMock, assert) { // Mocking this specific route to make sure we only get the items we want to test from the handlers. fetchMock.getOnce(url(settings, '/splitChanges?s=1.2&since=-1'), { status: 200, body: splitChangesMock1 }); @@ -47,41 +50,21 @@ export default function (fetchMock, assert) { }); const client = splitio.client(); - const assertPayload = req => { - const resp = JSON.parse(req.body); - - assert.equal(resp.length, 1, 'We performed three evaluations so we should have 1 impressions type'); - - const alwaysOnWithConfigImpr = resp.filter(e => e.f === 'split_with_config')[0]; - - assert.equal(alwaysOnWithConfigImpr.i.length, 3); - - function validateImpressionData(output, expected) { - assert.equal(output.k, expected.keyName, 'Present impressions should have the correct key.'); - assert.equal(output.b, expected.bucketingKey, 'Present impressions should have the correct bucketingKey.'); - assert.equal(output.t, expected.treatment, 'Present impressions should have the correct treatment.'); - assert.equal(output.r, expected.label, 'Present impressions should have the correct label.'); - assert.equal(output.c, expected.changeNumber, 'Present impressions should have the correct changeNumber.'); - assert.equal(output.pt, expected.pt, 'Present impressions should have the correct previousTime.'); - } - - validateImpressionData(alwaysOnWithConfigImpr.i[0], { - keyName: 'facundo@split.io', label: 'another expected label', treatment: 'o.n', - bucketingKey: undefined, changeNumber: 828282828282, pt: undefined - }); - validateImpressionData(alwaysOnWithConfigImpr.i[1], { - keyName: 'facundo@split.io', label: 'another expected label', treatment: 'o.n', - bucketingKey: undefined, changeNumber: 828282828282, pt: alwaysOnWithConfigImpr.i[0].m - }); - validateImpressionData(alwaysOnWithConfigImpr.i[2], { - keyName: 'facundo@split.io', label: 'another expected label', treatment: 'o.n', - bucketingKey: undefined, changeNumber: 828282828282, pt: alwaysOnWithConfigImpr.i[1].m - }); - }; fetchMock.postOnce(url(settings, '/testImpressions/bulk'), (url, req) => { assert.equal(req.headers.SplitSDKImpressionsMode, DEBUG); - assertPayload(req); + const data = JSON.parse(req.body); + + assert.deepEqual(data, [{ + f: 'split_with_config', + i: [{ + k: 'facundo@split.io', t: 'o.n', m: data[0].i[0].m, c: 828282828282, r: 'another expected label' + }, { + k: 'facundo@split.io', t: 'o.n', m: data[0].i[1].m, c: 828282828282, r: 'another expected label', pt: data[0].i[0].m, + }, { + 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 + }] + }]); client.destroy().then(() => { assert.end(); @@ -90,9 +73,28 @@ export default function (fetchMock, assert) { return 200; }); + fetchMock.postOnce(url(settings, '/testImpressions/count'), (url, opts) => { + assert.deepEqual(JSON.parse(opts.body), { + pf: [{ f: 'always_on_track_impressions_false', m: truncatedTimeFrame, rc: 1 }] + }, 'We should generate impression count for the feature with track impressions disabled.'); + + return 200; + }); + + fetchMock.postOnce(url(settings, '/v1/keys/cs'), (url, opts) => { + assert.deepEqual(JSON.parse(opts.body), { + keys: [{ fs: ['always_on_track_impressions_false'], k: 'facundo@split.io' }] + }, 'We should track unique keys for the feature with track impressions disabled.'); + + return 200; + }); + client.ready().then(() => { + truncatedTimeFrame = truncateTimeFrame(Date.now()); + client.getTreatment('split_with_config'); client.getTreatment('split_with_config'); client.getTreatment('split_with_config'); + assert.equal(client.getTreatment('always_on_track_impressions_false'), 'on'); }); } diff --git a/src/__tests__/browserSuites/impressions.none.spec.js b/src/__tests__/browserSuites/impressions.none.spec.js index d90c407a7..2294c9695 100644 --- a/src/__tests__/browserSuites/impressions.none.spec.js +++ b/src/__tests__/browserSuites/impressions.none.spec.js @@ -58,7 +58,8 @@ export default async function (fetchMock, assert) { pf: [ { f: 'split_with_config', m: truncatedTimeFrame, rc: 2 }, { f: 'always_off', m: truncatedTimeFrame, rc: 4 }, - { f: 'always_on', m: truncatedTimeFrame, rc: 2 } + { f: 'always_on', m: truncatedTimeFrame, rc: 2 }, + { f: 'always_on_track_impressions_false', m: truncatedTimeFrame, rc: 1 } ] }); return 200; @@ -75,7 +76,7 @@ export default async function (fetchMock, assert) { }, { k: 'emma@split.io', - fs: ['always_off', 'always_on'] + fs: ['always_off', 'always_on', 'always_on_track_impressions_false'] } ] }, 'We performed evaluations for two keys, so we should have 2 item total.'); @@ -93,6 +94,7 @@ export default async function (fetchMock, assert) { client.getTreatment('always_on'); client.getTreatment('always_off'); client.getTreatment('split_with_config'); + sharedClient.getTreatment('always_on_track_impressions_false'); client.destroy().then(() => { assert.end(); diff --git a/src/__tests__/browserSuites/impressions.spec.js b/src/__tests__/browserSuites/impressions.spec.js index aa4cff4c0..33e60106a 100644 --- a/src/__tests__/browserSuites/impressions.spec.js +++ b/src/__tests__/browserSuites/impressions.spec.js @@ -49,17 +49,19 @@ export default function (fetchMock, assert) { const assertPayload = req => { const resp = JSON.parse(req.body); - assert.equal(resp.length, 2, 'We performed three evaluations so we should have 2 impressions type'); + assert.equal(resp.length, 2, 'We performed evaluations for 3 features, but one with `impressionsDisabled` true, so we should have 2 items total'); const dependencyChildImpr = resp.filter(e => e.f === 'hierarchical_splits_test')[0]; - const alwaysOnWithConfigImpr = resp.filter(e => e.f === 'split_with_config')[0]; + const splitWithConfigImpr = resp.filter(e => e.f === 'split_with_config')[0]; + const alwaysOnWithTrackImpressionsFalse = resp.filter(e => e.f === 'always_on_track_impressions_false'); 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.'); assert.false(resp.some(e => e.f === 'hierarchical_dep_hierarchical'), 'No matter how deep is the chain.'); - assert.true(alwaysOnWithConfigImpr, 'Split evaluated with config should have generated an impression too.'); - assert.false(Object.prototype.hasOwnProperty.call(alwaysOnWithConfigImpr.i[0], 'configuration'), 'Impressions do not change with configuration evaluations.'); - assert.false(Object.prototype.hasOwnProperty.call(alwaysOnWithConfigImpr.i[0], 'config'), 'Impressions do not change with configuration evaluations.'); + assert.true(splitWithConfigImpr, 'Split evaluated with config should have generated an impression too.'); + 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(alwaysOnWithTrackImpressionsFalse.length, 0); const { k, @@ -94,18 +96,26 @@ export default function (fetchMock, assert) { fetchMock.postOnce(url(settings, '/testImpressions/count'), (url, opts) => { const data = JSON.parse(opts.body); - assert.equal(data.pf.length, 1, 'We should generate impressions count for one feature.'); + assert.equal(data.pf.length, 2, 'We should generate impressions count for 2 features.'); // finding these validate the feature names collection too - const dependencyChildImpr = data.pf.filter(e => e.f === 'hierarchical_splits_test')[0]; - const alwaysOnWithConfigImpr = data.pf.filter(e => e.f === 'split_with_config')[0]; + const splitWithConfigImpr = data.pf.filter(e => e.f === 'split_with_config')[0]; + const alwaysOnWithTrackImpressionsFalse = data.pf.filter(e => e.f === 'always_on_track_impressions_false')[0]; - assert.equal(dependencyChildImpr.rc, 1); - assert.equal(typeof dependencyChildImpr.m, 'number'); - assert.equal(dependencyChildImpr.m, truncatedTimeFrame); - assert.equal(alwaysOnWithConfigImpr.rc, 3); - assert.equal(typeof alwaysOnWithConfigImpr.m, 'number'); - assert.equal(alwaysOnWithConfigImpr.m, truncatedTimeFrame); + assert.equal(splitWithConfigImpr.rc, 2); + assert.equal(typeof splitWithConfigImpr.m, 'number'); + assert.equal(splitWithConfigImpr.m, truncatedTimeFrame); + assert.equal(alwaysOnWithTrackImpressionsFalse.rc, 1); + assert.equal(typeof alwaysOnWithTrackImpressionsFalse.m, 'number'); + assert.equal(alwaysOnWithTrackImpressionsFalse.m, truncatedTimeFrame); + + return 200; + }); + + fetchMock.postOnce(url(settings, '/v1/keys/cs'), (url, opts) => { + assert.deepEqual(JSON.parse(opts.body), { + keys: [{ fs: [ 'always_on_track_impressions_false' ], k: 'facundo@split.io' }] + }, 'We should only track unique keys for features flags with track impressions disabled.'); return 200; }); @@ -120,5 +130,8 @@ export default function (fetchMock, assert) { }, 'We should get an evaluation as always.'); client.getTreatmentWithConfig('split_with_config'); client.getTreatmentWithConfig('split_with_config'); + + // Impression should not be tracked + assert.equal(client.getTreatment('always_on_track_impressions_false'), 'on'); }); } diff --git a/src/__tests__/browserSuites/manager.spec.js b/src/__tests__/browserSuites/manager.spec.js index a5a09ff6d..9c64cc52c 100644 --- a/src/__tests__/browserSuites/manager.spec.js +++ b/src/__tests__/browserSuites/manager.spec.js @@ -41,7 +41,8 @@ export default async function (settings, fetchMock, assert) { 'treatments': map(mockSplits.splits[index].conditions[0].partitions, partition => partition.treatment), 'configs': mockSplits.splits[index].configurations || {}, 'sets': mockSplits.splits[index].sets || [], - 'defaultTreatment': mockSplits.splits[index].defaultTreatment + 'defaultTreatment': mockSplits.splits[index].defaultTreatment, + 'impressionsDisabled': false }); assert.equal(manager.split('non_existent'), null, 'Trying to get a manager.split() of a Split that does not exist returns null.'); diff --git a/src/__tests__/browserSuites/telemetry.spec.js b/src/__tests__/browserSuites/telemetry.spec.js index 5ed7c331d..7f6fbc8bc 100644 --- a/src/__tests__/browserSuites/telemetry.spec.js +++ b/src/__tests__/browserSuites/telemetry.spec.js @@ -76,7 +76,7 @@ export default async function telemetryBrowserSuite(fetchMock, t) { // @TODO check if iDe value is correct assert.deepEqual(data, { - mE: {}, hE: { sp: { 500: 1 }, ms: { 500: 1 } }, tR: 0, aR: 0, iQ: 4, iDe: 1, iDr: 0, spC: 32, seC: 1, skC: 1, eQ: 1, eD: 0, sE: [], t: [], ufs: {} + mE: {}, hE: { sp: { 500: 1 }, ms: { 500: 1 } }, tR: 0, aR: 0, iQ: 4, iDe: 1, iDr: 0, spC: 33, seC: 1, skC: 1, eQ: 1, eD: 0, sE: [], t: [], ufs: {} }, 'metrics/usage JSON payload should be the expected'); finish.next(); @@ -96,7 +96,7 @@ export default async function telemetryBrowserSuite(fetchMock, t) { // @TODO check if iDe value is correct assert.deepEqual(data, { mL: {}, mE: {}, hE: {}, hL: {}, // errors and latencies were popped - tR: 0, aR: 0, iQ: 4, iDe: 1, iDr: 0, spC: 32, seC: 1, skC: 1, eQ: 1, eD: 0, sE: [], t: [], ufs: {} + tR: 0, aR: 0, iQ: 4, iDe: 1, iDr: 0, spC: 33, seC: 1, skC: 1, eQ: 1, eD: 0, sE: [], t: [], ufs: {} }, '2nd metrics/usage JSON payload should be the expected'); return 200; }); diff --git a/src/__tests__/consumer/node_redis.spec.js b/src/__tests__/consumer/node_redis.spec.js index 457631bac..a2317ce48 100644 --- a/src/__tests__/consumer/node_redis.spec.js +++ b/src/__tests__/consumer/node_redis.spec.js @@ -61,7 +61,7 @@ const expectedImpressionCount = [ ]; const expectedSplitName = 'hierarchical_splits_testing_on'; -const expectedSplitView = { name: 'hierarchical_splits_testing_on', trafficType: 'user', killed: false, changeNumber: 1487277320548, treatments: ['on', 'off'], configs: {}, sets: [], defaultTreatment: 'off' }; +const expectedSplitView = { name: 'hierarchical_splits_testing_on', trafficType: 'user', killed: false, changeNumber: 1487277320548, treatments: ['on', 'off'], configs: {}, sets: [], defaultTreatment: 'off', impressionsDisabled: false }; const MOCKS = { '': 'redis-commands', @@ -150,11 +150,12 @@ tape('Node.js Redis', function (t) { assert.equal(await client.getTreatment('UT_Segment_member', 'always-on'), 'on', 'Evaluations using Redis storage should be correct.'); - // Below splits were added manually to the redis_mock.json file. + // Below feature flags were added manually to the redis_mock.json file. // They are all_keys (always evaluate to on) which depend from always-on split. the _on/off is what treatment they are expecting there. assert.equal(await client.getTreatment('UT_Segment_member', 'hierarchical_splits_testing_on'), 'on', 'Evaluations using Redis storage should be correct.'); 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.'); + assert.equal(await client.getTreatment('other_key', 'always-on-impressions-disabled-true'), 'on', 'Evaluations using Redis storage should be correct.'); assert.equal(typeof client.track().then, 'function', 'Track calls should always return a promise on Redis mode, even when parameters are incorrect.'); @@ -163,35 +164,46 @@ tape('Node.js Redis', function (t) { // Manager methods const splitNames = await manager.names(); - assert.equal(splitNames.length, 25, 'manager `names` method returns the list of split names asynchronously'); + assert.equal(splitNames.length, 26, 'manager `names` method returns the list of split names asynchronously'); assert.equal(splitNames.indexOf(expectedSplitName) > -1, true, 'list of split names should contain expected splits'); assert.deepEqual(await manager.split(expectedSplitName), expectedSplitView, 'manager `split` method returns the split view of the given split name asynchronously'); const splitViews = await manager.splits(); - assert.equal(splitViews.length, 25, 'manager `splits` method returns the list of split views asynchronously'); + assert.equal(splitViews.length, 26, 'manager `splits` method returns the list of split views asynchronously'); assert.deepEqual(splitViews.find(splitView => splitView.name === expectedSplitName), expectedSplitView, 'manager `split` method returns the split view of the given split name asynchronously'); await client.ready(); // promise already resolved await client.destroy(); - // 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) => { - if (error) assert.fail('Redis server should be reachable'); + // 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'); - const trackedImpressionsAndEvents = stdout.split('\n').filter(line => line !== '').map(line => parseInt(line)); - assert.deepEqual(trackedImpressionsAndEvents, [TOTAL_RAW_IMPRESSIONS, TOTAL_EVENTS], 'Tracked impressions and events should be stored in Redis'); + 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'); - // Validate stored telemetry - exec(`echo "HLEN ${config.storage.prefix}.SPLITIO.telemetry.latencies \n HLEN ${config.storage.prefix}.SPLITIO.telemetry.exceptions \n HGET ${config.storage.prefix}.SPLITIO.telemetry.init 'nodejs-${version}/${HOSTNAME_VALUE}/${IP_VALUE}'" | redis-cli -p ${redisPort}`, (error, stdout) => { - if (error) assert.fail('Redis server should be reachable'); + // 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) => { + if (error) assert.fail('Redis server should be reachable'); - const [latencies, exceptions, configValue] = stdout.split('\n').filter(line => line !== '').map(JSON.parse); + const trackedImpressionsAndEvents = stdout.split('\n').filter(line => line !== '').map(line => parseInt(line)); + assert.deepEqual(trackedImpressionsAndEvents, [TOTAL_RAW_IMPRESSIONS, TOTAL_EVENTS], 'Tracked impressions and events should be stored in Redis'); - assert.true(latencies > 0, 'There are stored latencies'); - assert.true(exceptions === 0, 'There aren\'t stored exceptions'); - assert.deepEqual(configValue, { oM: 1, st: 'redis', aF: 1, rF: 0 }, 'There is stored telemetry config'); + // Validate stored telemetry + exec(`echo "HLEN ${config.storage.prefix}.SPLITIO.telemetry.latencies \n HLEN ${config.storage.prefix}.SPLITIO.telemetry.exceptions \n HGET ${config.storage.prefix}.SPLITIO.telemetry.init 'nodejs-${version}/${HOSTNAME_VALUE}/${IP_VALUE}'" | redis-cli -p ${redisPort}`, (error, stdout) => { + if (error) assert.fail('Redis server should be reachable'); + + const [latencies, exceptions, configValue] = stdout.split('\n').filter(line => line !== '').map(JSON.parse); - // close server connection - server.close().then(assert.end); + assert.true(latencies > 0, 'There are stored latencies'); + assert.true(exceptions === 0, 'There aren\'t stored exceptions'); + assert.deepEqual(configValue, { oM: 1, st: 'redis', aF: 1, rF: 0 }, 'There is stored telemetry config'); + + // close server connection + server.close().then(assert.end); + }); + }); }); }); }); @@ -270,7 +282,7 @@ tape('Node.js Redis', function (t) { // this should be deduped assert.equal(await client.getTreatment('UT_Segment_member', 'always-on'), 'on', 'Evaluations using Redis storage should be correct.'); - // Below splits were added manually to the redis_mock.json file. + // Below feature flags were added manually to the redis_mock.json file. // They are all_keys (always evaluate to on) which depend from always-on split. the _on/off is what treatment they are expecting there. assert.equal(await client.getTreatment('UT_Segment_member', 'hierarchical_splits_testing_on'), 'on', 'Evaluations using Redis storage should be correct.'); // this should be deduped @@ -372,7 +384,7 @@ tape('Node.js Redis', function (t) { assert.equal(await client.getTreatment('UT_Segment_member', 'always-on'), 'on', 'Evaluations using Redis storage should be correct.'); - // Below splits were added manually to the redis_mock.json file. + // Below feature flags were added manually to the redis_mock.json file. // They are all_keys (always evaluate to on) which depend from always-on split. the _on/off is what treatment they are expecting there. assert.equal(await client.getTreatment('UT_Segment_member', 'hierarchical_splits_testing_on'), 'on', 'Evaluations using Redis storage should be correct.'); assert.equal(await client.getTreatment('UT_Segment_member', 'hierarchical_splits_testing_off'), 'off', 'Evaluations using Redis storage should be correct.'); diff --git a/src/__tests__/mocks/redis-commands.txt b/src/__tests__/mocks/redis-commands.txt index 2e5daae42..e3ad7c35a 100644 --- a/src/__tests__/mocks/redis-commands.txt +++ b/src/__tests__/mocks/redis-commands.txt @@ -21,8 +21,8 @@ SET 'REDIS_NODE_UT.SPLITIO.split.always-off' '{"changeNumber":1491519038393, SET 'REDIS_NODE_UT.SPLITIO.split.always-on' '{"changeNumber":1487277320548,"trafficTypeName":"user","name":"always-on","seed":1684183541,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"in segment all"}]}' SET 'REDIS_NODE_UT.SPLITIO.split.always-o.n-with-config' '{"changeNumber":1487277320548,"trafficTypeName":"user","name":"always-o.n-with-config","seed":1684183541,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"o.n","size":100},{"treatment":"off","size":0}],"label":"in segment all"}],"configurations":{"o.n":"{\"color\":\"brown\"}"}}' SET 'REDIS_NODE_UT.SPLITIO.split.hierarchical_splits_testing_off' '{"changeNumber":1487277320548,"trafficTypeName":"user","name":"hierarchical_splits_testing_off","seed":1684183541,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}},{"keySelector":{"trafficType":"user","attribute":""},"dependencyMatcherData":{"split":"always-on","treatments":["off"]},"matcherType":"IN_SPLIT_TREATMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":null,"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"in segment all"}]}' -SET 'REDIS_NODE_UT.SPLITIO.split.hierarchical_splits_testing_on' '{"changeNumber":1487277320548,"trafficTypeName":"user","name":"hierarchical_splits_testing_on","seed":1684183541,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}},{"keySelector":{"trafficType":"user","attribute":""},"dependencyMatcherData":{"split":"always-on","treatments":["on"]},"matcherType":"IN_SPLIT_TREATMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":null,"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"in segment all"}]}' -SET 'REDIS_NODE_UT.SPLITIO.split.hierarchical_splits_testing_on_negated' '{"changeNumber":1487277320548,"trafficTypeName":"user","name":"hierarchical_splits_testing_on","seed":1684183541,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":true,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}},{"keySelector":{"trafficType":"user","attribute":""},"dependencyMatcherData":{"split":"always-on","treatments":["on"]},"matcherType":"IN_SPLIT_TREATMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":null,"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"in segment all"}]}' +SET 'REDIS_NODE_UT.SPLITIO.split.hierarchical_splits_testing_on' '{"changeNumber":1487277320548,"trafficTypeName":"user","name":"hierarchical_splits_testing_on","impressionsDisabled":false,"seed":1684183541,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}},{"keySelector":{"trafficType":"user","attribute":""},"dependencyMatcherData":{"split":"always-on","treatments":["on"]},"matcherType":"IN_SPLIT_TREATMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":null,"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"in segment all"}]}' +SET 'REDIS_NODE_UT.SPLITIO.split.hierarchical_splits_testing_on_negated' '{"changeNumber":1487277320548,"trafficTypeName":"user","name":"hierarchical_splits_testing_on_negated","seed":1684183541,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":true,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}},{"keySelector":{"trafficType":"user","attribute":""},"dependencyMatcherData":{"split":"always-on","treatments":["on"]},"matcherType":"IN_SPLIT_TREATMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":null,"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"in segment all"}]}' SET 'REDIS_NODE_UT.SPLITIO.split.labels' '{"changeNumber":1492023661334,"trafficTypeName":"user","name":"labels","seed":-1240661267,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}},{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"demo"},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}},{"keySelector":{"trafficType":"user","attribute":"n"},"matcherType":"EQUAL_TO","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"NUMBER","value":123},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":100}],"label":"in segment all and in segment demo and n = 123"}]}' SET 'REDIS_NODE_UT.SPLITIO.split.nico_not' '{"changeNumber":1489412422181,"trafficTypeName":"user","name":"nico_not","seed":-788702424,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"qa"},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":100}],"label":"in segment qa"}]}' SET 'REDIS_NODE_UT.SPLITIO.split.not_part_of' '{"changeNumber":1492627582227,"trafficTypeName":"user","name":"not_part_of","seed":-1643575289,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":"setx"},"matcherType":"PART_OF_SET","negate":true,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":["a","b","c"]},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":100}],"label":"setx not part of [a, b, ...]"}]}' @@ -39,3 +39,4 @@ SET 'REDIS_NODE_UT.SPLITIO.split.testing_traffic_type' '{"changeNumber":1489 SET 'REDIS_NODE_UT.SPLITIO.split.testing_traffic_types' '{"changeNumber":1490974465415,"trafficTypeName":"machine","name":"testing_traffic_types","seed":475616886,"status":"ACTIVE","killed":false,"defaultTreatment":"on","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"","attribute":""},"matcherType":"WHITELIST","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":["sarasa"]},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100}],"label":"whitelisted"},{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"","attribute":""},"matcherType":"WHITELIST","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":["excluded"]},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"off","size":100}],"label":"whitelisted"},{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}},{"keySelector":{"trafficType":"machine","attribute":""},"matcherType":"IN_SEGMENT","negate":false,"userDefinedSegmentMatcherData":{"segmentName":"testing_traffic_type"},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":0},{"treatment":"off","size":100}],"label":"in segment all and in segment testing_traffic_type"}]}' SET 'REDIS_NODE_UT.SPLITIO.split.traffic_allocation_testing' '{"changeNumber":1490974123779,"trafficTypeName":"user","name":"traffic_allocation_testing","seed":1716284102,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"in segment all"}]}' SET 'REDIS_NODE_UT.SPLITIO.splits.till' 1492723024413 +SET 'REDIS_NODE_UT.SPLITIO.split.always-on-impressions-disabled-true' '{"changeNumber":1487277320548,"trafficTypeName":"user","name":"always-on-impressions-disabled-true","impressionsDisabled":true,"seed":1684183541,"status":"ACTIVE","killed":false,"defaultTreatment":"off","conditions":[{"matcherGroup":{"combiner":"AND","matchers":[{"keySelector":{"trafficType":"user","attribute":""},"matcherType":"ALL_KEYS","negate":false,"userDefinedSegmentMatcherData":{"segmentName":""},"unaryNumericMatcherData":{"dataType":"","value":0},"whitelistMatcherData":{"whitelist":null},"betweenMatcherData":{"dataType":"","start":0,"end":0}}]},"partitions":[{"treatment":"on","size":100},{"treatment":"off","size":0}],"label":"in segment all"}]}' diff --git a/src/__tests__/mocks/splitchanges.since.-1.json b/src/__tests__/mocks/splitchanges.since.-1.json index ee21cf9cc..372b030b8 100644 --- a/src/__tests__/mocks/splitchanges.since.-1.json +++ b/src/__tests__/mocks/splitchanges.since.-1.json @@ -1265,6 +1265,7 @@ "trafficTypeId": null, "trafficTypeName": null, "name": "hierarchical_splits_test", + "impressionsDisabled": false, "seed": 1276793945, "changeNumber": 2828282828, "status": "ACTIVE", @@ -1381,6 +1382,45 @@ } ] }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "always_on_track_impressions_false", + "impressionsDisabled": true, + "seed": -790401604, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": null + }, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + } + ] + } + ] + }, { "orgId": null, "environment": null, @@ -1426,6 +1466,7 @@ { "trafficTypeName": null, "name": "split_with_config", + "impressionsDisabled": false, "algo": 2, "seed": -1222652064, "trafficAllocation": 100, diff --git a/src/__tests__/nodeSuites/impressions-listener.spec.js b/src/__tests__/nodeSuites/impressions-listener.spec.js index 7fda529f4..984c36d1e 100644 --- a/src/__tests__/nodeSuites/impressions-listener.spec.js +++ b/src/__tests__/nodeSuites/impressions-listener.spec.js @@ -46,13 +46,16 @@ export default function (assert) { setTimeout(() => { assert.true(listener.logImpression.callCount, 4, 'Impression listener logImpression method should be called after we call client.getTreatment, once per each impression generated.'); - assert.true(listener.logImpression.getCall(0).calledWithMatch({ + assert.true(listener.logImpression.getCall(0).calledWithExactly({ impression: { feature: 'hierarchical_splits_test', keyName: 'nicolas@split.io', treatment: 'on', + time: listener.logImpression.getCall(0).args[0].impression.time, bucketingKey: undefined, label: 'expected label', + changeNumber: 2828282828, + pt: undefined }, attributes: undefined, ...metaData diff --git a/src/__tests__/nodeSuites/impressions.debug.spec.js b/src/__tests__/nodeSuites/impressions.debug.spec.js index 9d09415c0..08faa13e3 100644 --- a/src/__tests__/nodeSuites/impressions.debug.spec.js +++ b/src/__tests__/nodeSuites/impressions.debug.spec.js @@ -3,6 +3,7 @@ import { settingsFactory } from '../../settings'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json'; import { DEBUG } from '@splitsoftware/splitio-commons/src/utils/constants'; +import { truncateTimeFrame } from '@splitsoftware/splitio-commons/src/utils/time'; import { url } from '../testUtils'; const baseUrls = { @@ -38,6 +39,8 @@ const config = { streamingEnabled: false }; +let truncatedTimeFrame; + export default async function (key, fetchMock, assert) { // Mocking this specific route to make sure we only get the items we want to test from the handlers. fetchMock.getOnce(url(settings, '/splitChanges?s=1.1&since=-1'), { status: 200, body: splitChangesMock1 }); @@ -46,58 +49,53 @@ export default async function (key, fetchMock, assert) { const splitio = SplitFactory(config); const client = splitio.client(); - let evaluationsStart = 0, readyEvaluationsStart = 0, evaluationsEnd = 0; + let readyEvaluationsStart = 0; fetchMock.postOnce(url(settings, '/testImpressions/bulk'), (url, opts) => { assert.equal(opts.headers.SplitSDKImpressionsMode, DEBUG); const data = JSON.parse(opts.body); - assert.equal(data.length, 1, 'We performed evaluations for one split, so we should have 1 item total.'); - - // finding these validate the feature names collection too - const alwaysOnWithConfigImpr = data.filter(e => e.f === 'split_with_config')[0]; - - assert.equal(alwaysOnWithConfigImpr.i.length, 3); - - function validateImpressionData(output, expected, performedWhenReady = true) { - assert.equal(output.k, expected.keyName, 'Present impressions should have the correct key.'); - assert.equal(output.b, expected.bucketingKey, 'Present impressions should have the correct bucketingKey.'); - assert.equal(output.t, expected.treatment, 'Present impressions should have the correct treatment.'); - assert.equal(output.r, expected.label, 'Present impressions should have the correct label.'); - assert.equal(output.c, expected.changeNumber, 'Present impressions should have the correct changeNumber.'); - assert.equal(output.pt, expected.pt, 'Present impressions should have the correct previousTime.'); - assert.true(output.m >= (performedWhenReady ? readyEvaluationsStart : evaluationsStart) && output.m <= evaluationsEnd, 'Present impressions should have the correct timestamp (test with error margin).'); - } + assert.deepEqual(data, [{ + f: 'split_with_config', + i: [{ + k: 'facundo@split.io', t: 'o.n', m: data[0].i[0].m, c: 828282828282, r: 'another expected label', b: 'test_buck_key' + }, { + k: 'facundo@split.io', t: 'o.n', m: data[0].i[1].m, c: 828282828282, r: 'another expected label', b: 'test_buck_key', pt: data[0].i[0].m + }, { + k: 'facundo@split.io', t: 'o.n', m: data[0].i[2].m, c: 828282828282, r: 'another expected label', b: 'test_buck_key', pt: data[0].i[1].m + }] + }], 'We performed evaluations for one split, so we should have 1 item total.'); client.destroy().then(() => { - validateImpressionData(alwaysOnWithConfigImpr.i[0], { - keyName: 'facundo@split.io', label: 'another expected label', treatment: 'o.n', - bucketingKey: 'test_buck_key', changeNumber: 828282828282, pt: undefined - }); - validateImpressionData(alwaysOnWithConfigImpr.i[1], { - keyName: 'facundo@split.io', label: 'another expected label', treatment: 'o.n', - bucketingKey: 'test_buck_key', changeNumber: 828282828282, pt: alwaysOnWithConfigImpr.i[0].m - }); - validateImpressionData(alwaysOnWithConfigImpr.i[2], { - keyName: 'facundo@split.io', label: 'another expected label', treatment: 'o.n', - bucketingKey: 'test_buck_key', changeNumber: 828282828282, pt: alwaysOnWithConfigImpr.i[1].m - }); - assert.end(); }); return 200; }); - evaluationsStart = Date.now(); + fetchMock.postOnce(url(settings, '/testImpressions/count'), (url, opts) => { + assert.deepEqual(JSON.parse(opts.body), { + pf: [{ f: 'always_on_track_impressions_false', m: truncatedTimeFrame, rc: 1 }] + }, 'We should generate impression count for the feature with track impressions disabled.'); + + return 200; + }); + + fetchMock.postOnce(url(settings, '/v1/keys/ss'), (url, opts) => { + assert.deepEqual(JSON.parse(opts.body), { + keys: [{ f: 'always_on_track_impressions_false', ks: ['other_key'] }] + }, 'We should track unique keys for the feature with track impressions disabled.'); + + return 200; + }); await client.ready(); readyEvaluationsStart = Date.now(); + truncatedTimeFrame = truncateTimeFrame(readyEvaluationsStart); - client.getTreatment({ matchingKey: key, bucketingKey: 'test_buck_key' }, 'split_with_config'); - client.getTreatment({ matchingKey: key, bucketingKey: 'test_buck_key' }, 'split_with_config'); - client.getTreatment({ matchingKey: key, bucketingKey: 'test_buck_key' }, 'split_with_config'); - - evaluationsEnd = Date.now(); + assert.equal(client.getTreatment({ matchingKey: key, bucketingKey: 'test_buck_key' }, 'split_with_config'), 'o.n'); + assert.equal(client.getTreatment({ matchingKey: key, bucketingKey: 'test_buck_key' }, 'split_with_config'), 'o.n'); + assert.equal(client.getTreatment({ matchingKey: key, bucketingKey: 'test_buck_key' }, 'split_with_config'), 'o.n'); + assert.equal(client.getTreatment({ matchingKey: 'other_key', bucketingKey: 'test_buck_key' }, 'always_on_track_impressions_false'), 'on'); } diff --git a/src/__tests__/nodeSuites/impressions.none.spec.js b/src/__tests__/nodeSuites/impressions.none.spec.js index 4c726bca4..2e47f32e1 100644 --- a/src/__tests__/nodeSuites/impressions.none.spec.js +++ b/src/__tests__/nodeSuites/impressions.none.spec.js @@ -54,7 +54,8 @@ export default async function (key, fetchMock, assert) { pf: [ { 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', m: truncatedTimeFrame, rc: 5 }, + { f: 'always_on_track_impressions_false', m: truncatedTimeFrame, rc: 1 } ] }); return 200; @@ -76,9 +77,13 @@ export default async function (key, fetchMock, assert) { { f: 'always_on', ks: ['emma@split.io', 'emi@split.io', 'nico@split.io'] + }, + { + f: 'always_on_track_impressions_false', + ks: ['emi@split.io'] } ] - }, 'We performed evaluations for three split, so we should have 3 item total.'); + }, 'We performed evaluations for 4 flags, so we should have 4 items total.'); return 200; }); @@ -95,6 +100,7 @@ export default async function (key, fetchMock, assert) { client.getTreatment('nico@split.io', 'always_on'); client.getTreatment('emi@split.io', 'split_with_config'); client.getTreatment('emma@split.io', 'split_with_config'); + client.getTreatment('emi@split.io', 'always_on_track_impressions_false'); client.destroy().then(() => { assert.end(); diff --git a/src/__tests__/nodeSuites/impressions.spec.js b/src/__tests__/nodeSuites/impressions.spec.js index b5fb102a7..e7615140c 100644 --- a/src/__tests__/nodeSuites/impressions.spec.js +++ b/src/__tests__/nodeSuites/impressions.spec.js @@ -49,23 +49,25 @@ 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 three splits, so we should have 3 items total.'); + assert.equal(data.length, 3, 'We performed evaluations for 4 features, but one 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 alwaysOnWithConfigImpr = data.filter(e => e.f === 'split_with_config')[0]; + const splitWithConfigImpr = data.filter(e => e.f === 'split_with_config')[0]; const notExistentSplitImpr = data.filter(e => e.f === 'not_existent_split')[0]; + const alwaysOnWithTrackImpressionsFalse = data.filter(e => e.f === 'always_on_track_impressions_false'); assert.equal(notExistentSplitImpr.i.length, 1); // Only one, the split not found is filtered by the non existent Split check. - assert.equal(alwaysOnWithConfigImpr.i.length, 2); + assert.equal(splitWithConfigImpr.i.length, 2); assert.equal(dependencyChildImpr.i.length, 1); + assert.equal(alwaysOnWithTrackImpressionsFalse.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.'); assert.false(data.some(e => e.f === 'hierarchical_dep_hierarchical'), 'No matter how deep is the chain.'); - assert.true(alwaysOnWithConfigImpr, 'Split evaluated with config should have generated an impression too.'); - assert.false(Object.prototype.hasOwnProperty.call(alwaysOnWithConfigImpr.i[0], 'configuration'), 'Impressions do not change with configuration evaluations.'); - assert.false(Object.prototype.hasOwnProperty.call(alwaysOnWithConfigImpr.i[0], 'config'), 'Impressions do not change with configuration evaluations.'); + assert.true(splitWithConfigImpr, 'Split evaluated with config should have generated an impression too.'); + 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.'); function validateImpressionData(output, expected, performedWhenReady = true) { assert.equal(output.k, expected.keyName, 'Present impressions should have the correct key.'); @@ -84,7 +86,7 @@ export default async function (key, fetchMock, assert) { keyName: 'facundo@split.io', label: 'expected label', treatment: 'on', bucketingKey: undefined, changeNumber: 2828282828 }); - validateImpressionData(alwaysOnWithConfigImpr.i[0], { + validateImpressionData(splitWithConfigImpr.i[0], { keyName: 'facundo@split.io', label: 'another expected label', treatment: 'o.n', bucketingKey: 'test_buck_key', changeNumber: 828282828282 }); @@ -105,22 +107,26 @@ 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, 1, 'We should generate impression count for one feature.'); + assert.equal(data.pf.length, 2, 'We should generate impression count for 2 features.'); // finding these validate the feature names collection too - const dependencyChildImpr = data.pf.filter(e => e.f === 'hierarchical_splits_test')[0]; - const alwaysOnWithConfigImpr = data.pf.filter(e => e.f === 'split_with_config')[0]; - const notExistentSplitImpr = data.pf.filter(e => e.f === 'not_existent_split')[0]; - - assert.equal(dependencyChildImpr.rc, 1); - assert.equal(typeof dependencyChildImpr.m, 'number'); - assert.equal(dependencyChildImpr.m, truncatedTimeFrame); - assert.equal(alwaysOnWithConfigImpr.rc, 3); - assert.equal(typeof alwaysOnWithConfigImpr.m, 'number'); - assert.equal(alwaysOnWithConfigImpr.m, truncatedTimeFrame); - assert.equal(notExistentSplitImpr.rc, 1); - assert.equal(typeof notExistentSplitImpr.m, 'number'); - assert.equal(notExistentSplitImpr.m, truncatedTimeFrame); + const splitWithConfigImpr = data.pf.filter(e => e.f === 'split_with_config')[0]; + const alwaysOnWithTrackImpressionsFalse = data.pf.filter(e => e.f === 'always_on_track_impressions_false')[0]; + + assert.equal(splitWithConfigImpr.rc, 1); + assert.equal(typeof splitWithConfigImpr.m, 'number'); + assert.equal(splitWithConfigImpr.m, truncatedTimeFrame); + assert.equal(alwaysOnWithTrackImpressionsFalse.rc, 1); + assert.equal(typeof alwaysOnWithTrackImpressionsFalse.m, 'number'); + assert.equal(alwaysOnWithTrackImpressionsFalse.m, truncatedTimeFrame); + + return 200; + }); + + fetchMock.postOnce(url(settings, '/v1/keys/ss'), (url, opts) => { + assert.deepEqual(JSON.parse(opts.body), { + keys: [{ f: 'always_on_track_impressions_false', ks: ['other_key'] }] + }, 'We should only track unique keys for features flags with track impressions disabled.'); return 200; }); @@ -147,5 +153,8 @@ export default async function (key, fetchMock, assert) { client.getTreatmentWithConfig({ matchingKey: key, bucketingKey: 'test_buck_key' }, 'split_with_config'); client.getTreatmentWithConfig({ matchingKey: 'different', bucketingKey: 'test_buck_key' }, 'split_with_config'); + // Impression should not be tracked + assert.equal(client.getTreatment('other_key', 'always_on_track_impressions_false'), 'on'); + evaluationsEnd = Date.now(); } diff --git a/src/__tests__/nodeSuites/lazy-init.spec.js b/src/__tests__/nodeSuites/lazy-init.spec.js index bee1efbc9..9cb5f9735 100644 --- a/src/__tests__/nodeSuites/lazy-init.spec.js +++ b/src/__tests__/nodeSuites/lazy-init.spec.js @@ -33,13 +33,23 @@ export default function (settings, fetchMock, t) { fetchMock.getOnce('https://not-called/api/splitChanges?s=1.1&since=1457552620999', { status: 200, body: { splits: [], since: 1457552620999, till: 1457552620999 } }); fetchMock.postOnce('https://not-called/api/testImpressions/bulk', 200); fetchMock.postOnce('https://not-called/api/events/bulk', 200); + fetchMock.get('https://not-called/api/v2/auth?s=1.1', 200); // Validate that init and destroy are idempotent for (let i = 0; i < 3; i++) { splitio.init(); splitio.init(); splitio.destroy(); splitio.destroy(); } splitio.init(); await splitio.client().ready(); - assert.true(splitio.client().__getStatus().isReady, 'Split SDK is ready'); + + assert.deepEqual(splitio.client().__getStatus(), { isReady: true, isReadyFromCache: false, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: true, lastUpdate: splitio.client().__getStatus().lastUpdate }, 'Status'); + + await splitio.destroy(); + assert.deepEqual(splitio.client().__getStatus(), { isReady: true, isReadyFromCache: false, isTimedout: false, hasTimedout: false, isDestroyed: true, isOperational: false, lastUpdate: splitio.client().__getStatus().lastUpdate }, 'Status'); + + splitio.init(); + + assert.deepEqual(splitio.client().__getStatus(), { isReady: true, isReadyFromCache: false, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: true, lastUpdate: splitio.client().__getStatus().lastUpdate }, 'Status'); + await splitio.destroy(); assert.end(); @@ -81,13 +91,23 @@ export default function (settings, fetchMock, t) { fetchMock.getOnce('https://not-called/api/memberships/other-user', { status: 200, body: {} }); fetchMock.postOnce('https://not-called/api/testImpressions/bulk', 200); fetchMock.postOnce('https://not-called/api/events/bulk', 200); + fetchMock.get('https://not-called/api/v2/auth?s=1.2&users=user-99', 200); // Validate that init and destroy are idempotent for (let i = 0; i < 3; i++) { splitio.init(); splitio.init(); splitio.destroy(); splitio.destroy(); } splitio.init(); await splitio.client().ready(); - assert.true(splitio.client().__getStatus().isReady, 'Split SDK is ready'); + + assert.deepEqual(splitio.client().__getStatus(), { isReady: true, isReadyFromCache: false, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: true, lastUpdate: splitio.client().__getStatus().lastUpdate }, 'Status'); + + await splitio.destroy(); + assert.deepEqual(splitio.client().__getStatus(), { isReady: true, isReadyFromCache: false, isTimedout: false, hasTimedout: false, isDestroyed: true, isOperational: false, lastUpdate: splitio.client().__getStatus().lastUpdate }, 'Status'); + + splitio.init(); + + assert.deepEqual(splitio.client().__getStatus(), { isReady: true, isReadyFromCache: false, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: true, lastUpdate: splitio.client().__getStatus().lastUpdate }, 'Status'); + await splitio.destroy(); assert.end(); diff --git a/src/__tests__/nodeSuites/manager.spec.js b/src/__tests__/nodeSuites/manager.spec.js index 00a707dc9..869d65e3b 100644 --- a/src/__tests__/nodeSuites/manager.spec.js +++ b/src/__tests__/nodeSuites/manager.spec.js @@ -40,7 +40,8 @@ export default async function (settings, fetchMock, assert) { 'treatments': map(mockSplits.splits[index].conditions[0].partitions, partition => partition.treatment), 'configs': mockSplits.splits[index].configurations || {}, 'sets': mockSplits.splits[index].sets || [], - 'defaultTreatment': mockSplits.splits[index].defaultTreatment + 'defaultTreatment': mockSplits.splits[index].defaultTreatment, + 'impressionsDisabled': false }); assert.equal(manager.split('non_existent'), null, 'Trying to get a manager.split() of a Split that does not exist returns null.'); diff --git a/src/__tests__/nodeSuites/telemetry.spec.js b/src/__tests__/nodeSuites/telemetry.spec.js index a6a6bb66f..751e762e2 100644 --- a/src/__tests__/nodeSuites/telemetry.spec.js +++ b/src/__tests__/nodeSuites/telemetry.spec.js @@ -66,7 +66,7 @@ export default async function telemetryNodejsSuite(key, fetchMock, assert) { // @TODO check if iDe value is correct assert.deepEqual(data, { - mE: {}, hE: { sp: { 500: 1 } }, tR: 0, aR: 0, iQ: 4, iDe: 1, iDr: 0, spC: 32, seC: 3, skC: 3, eQ: 1, eD: 0, sE: [], t: [], ufs: {} + mE: {}, hE: { sp: { 500: 1 } }, tR: 0, aR: 0, iQ: 4, iDe: 1, iDr: 0, spC: 33, seC: 3, skC: 3, eQ: 1, eD: 0, sE: [], t: [], ufs: {} }, 'metrics/usage JSON payload should be the expected'); finish.next(); @@ -85,7 +85,7 @@ export default async function telemetryNodejsSuite(key, fetchMock, assert) { // @TODO check if iDe value is correct assert.deepEqual(data, { mL: {}, mE: {}, hE: {}, hL: {}, // errors and latencies were popped - tR: 0, aR: 0, iQ: 4, iDe: 1, iDr: 0, spC: 32, seC: 3, skC: 3, eQ: 1, eD: 0, sE: [], t: [], ufs: {} + tR: 0, aR: 0, iQ: 4, iDe: 1, iDr: 0, spC: 33, seC: 3, skC: 3, eQ: 1, eD: 0, sE: [], t: [], ufs: {} }, '2nd metrics/usage JSON payload should be the expected'); return 200; }); diff --git a/src/__tests__/offline/browser.spec.js b/src/__tests__/offline/browser.spec.js index bce2020eb..4411914cf 100644 --- a/src/__tests__/offline/browser.spec.js +++ b/src/__tests__/offline/browser.spec.js @@ -168,10 +168,10 @@ tape('Browser offline mode', function (assert) { // Manager tests const expectedSplitView1 = { - name: 'testing_split', trafficType: 'localhost', killed: false, changeNumber: 0, treatments: ['on'], configs: {}, defaultTreatment: 'control', sets: [] + name: 'testing_split', trafficType: 'localhost', killed: false, changeNumber: 0, treatments: ['on'], configs: {}, defaultTreatment: 'control', sets: [], impressionsDisabled: false }; const expectedSplitView2 = { - name: 'testing_split_with_config', trafficType: 'localhost', killed: false, changeNumber: 0, treatments: ['off'], configs: { off: '{ "color": "blue" }' }, defaultTreatment: 'control', sets: [] + name: 'testing_split_with_config', trafficType: 'localhost', killed: false, changeNumber: 0, treatments: ['off'], configs: { off: '{ "color": "blue" }' }, defaultTreatment: 'control', sets: [], impressionsDisabled: false }; assert.deepEqual(manager.names(), ['testing_split', 'testing_split_with_config']); assert.deepEqual(manager.split('testing_split'), expectedSplitView1); @@ -282,7 +282,7 @@ tape('Browser offline mode', function (assert) { // Manager tests const expectedSplitView3 = { - name: 'testing_split_with_config', trafficType: 'localhost', killed: false, changeNumber: 0, treatments: ['nope'], configs: {}, defaultTreatment: 'control', sets: [] + name: 'testing_split_with_config', trafficType: 'localhost', killed: false, changeNumber: 0, treatments: ['nope'], configs: {}, defaultTreatment: 'control', sets: [], impressionsDisabled: false }; assert.deepEqual(manager.names(), ['testing_split', 'testing_split_2', 'testing_split_3', 'testing_split_with_config']); assert.deepEqual(manager.split('testing_split'), expectedSplitView1); diff --git a/src/__tests__/offline/node.spec.js b/src/__tests__/offline/node.spec.js index ef12324b2..cbc6e40d3 100644 --- a/src/__tests__/offline/node.spec.js +++ b/src/__tests__/offline/node.spec.js @@ -250,17 +250,17 @@ function ManagerDotSplitTests(assert) { const expectedView1 = { name: 'testing_split', changeNumber: 0, killed: false, trafficType: 'localhost', treatments: ['on'], configs: {}, defaultTreatment: 'control', - sets: [] + sets: [], impressionsDisabled: false }; const expectedView2 = { name: 'testing_split2', changeNumber: 0, killed: false, trafficType: 'localhost', treatments: ['off'], configs: {}, defaultTreatment: 'control', - sets: [] + sets: [], impressionsDisabled: false }; const expectedView3 = { name: 'testing_split3', changeNumber: 0, killed: false, trafficType: 'localhost', treatments: ['custom_treatment'], configs: {}, defaultTreatment: 'control', - sets: [] + sets: [], impressionsDisabled: false }; assert.deepEqual(manager.split('testing_split'), expectedView1); @@ -294,7 +294,8 @@ function ManagerDotYamlTests(mockFileName, assert) { treatments: ['on'], configs: {}, sets: [], - defaultTreatment: 'control' + defaultTreatment: 'control', + impressionsDisabled: false }; const expectedView2 = { name: 'testing_split_only_wl', @@ -304,7 +305,8 @@ function ManagerDotYamlTests(mockFileName, assert) { treatments: ['whitelisted'], configs: {}, sets: [], - defaultTreatment: 'control' + defaultTreatment: 'control', + impressionsDisabled: false }; const expectedView3 = { name: 'testing_split_with_wl', @@ -317,7 +319,8 @@ function ManagerDotYamlTests(mockFileName, assert) { multi_key_wl: '{"color": "brown"}' }, sets: [], - defaultTreatment: 'control' + defaultTreatment: 'control', + impressionsDisabled: false }; const expectedView4 = { name: 'testing_split_off_with_config', @@ -329,7 +332,8 @@ function ManagerDotYamlTests(mockFileName, assert) { off: '{"color": "green"}' }, sets: [], - defaultTreatment: 'control' + defaultTreatment: 'control', + impressionsDisabled: false }; assert.deepEqual(manager.split('testing_split_on'), expectedView1); @@ -410,15 +414,15 @@ function MultipleInstancesTests(assert) { const expectedView1 = { name: 'testing_split', changeNumber: 0, killed: false, trafficType: 'localhost', - treatments: ['on'], configs: {}, sets: [] + treatments: ['on'], configs: {}, sets: [], impressionsDisabled: false }; const expectedView2 = { name: 'testing_split2', changeNumber: 0, killed: false, trafficType: 'localhost', - treatments: ['off'], configs: {}, sets: [] + treatments: ['off'], configs: {}, sets: [], impressionsDisabled: false }; const expectedView3 = { name: 'testing_split3', changeNumber: 0, killed: false, trafficType: 'localhost', - treatments: ['custom_treatment'], configs: {}, sets: [] + treatments: ['custom_treatment'], configs: {}, sets: [], impressionsDisabled: false }; assert.deepEqual(manager.split('testing_split'), expectedView1); diff --git a/src/settings/defaults/version.js b/src/settings/defaults/version.js index 563a20863..eb209ae90 100644 --- a/src/settings/defaults/version.js +++ b/src/settings/defaults/version.js @@ -1 +1 @@ -export const packageVersion = '11.0.4'; +export const packageVersion = '11.1.0'; diff --git a/ts-tests/index.ts b/ts-tests/index.ts index 9e9a2c32a..823e00771 100644 --- a/ts-tests/index.ts +++ b/ts-tests/index.ts @@ -134,7 +134,8 @@ splitView = { off: '{"dimensions":"{\"height\":20,\"width\":40}"}' }, sets: ['set_a', 'set_b'], - defaultTreatment: 'off' + defaultTreatment: 'off', + impressionsDisabled: false }; splitViews = [splitView];