Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions karma/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
const puppeteer = require('puppeteer');
process.env.CHROME_BIN = puppeteer.executablePath();

const webpack = require('webpack');
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');

module.exports = {
Expand Down Expand Up @@ -79,11 +78,7 @@ module.exports = {
]
},
plugins: [
new NodePolyfillPlugin(),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('test'),
__DEV__: true
})
new NodePolyfillPlugin()
],
resolve: {
extensions: ['.ts', '.js'],
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@splitsoftware/splitio",
"version": "10.17.3",
"version": "10.17.4-rc.0",
"description": "Split SDK",
"files": [
"README.md",
Expand Down Expand Up @@ -32,7 +32,7 @@
"node": ">=6"
},
"dependencies": {
"@splitsoftware/splitio-commons": "1.2.1-rc.5",
"@splitsoftware/splitio-commons": "1.2.1-rc.8",
"@types/google.analytics": "0.0.40",
"ioredis": "^4.28.0",
"ip": "1.1.5",
Expand Down
3 changes: 3 additions & 0 deletions src/__tests__/browser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import useBeaconApiSuite from './browserSuites/use-beacon-api.spec';
import useBeaconDebugApiSuite from './browserSuites/use-beacon-api.debug.spec';
import readyPromiseSuite from './browserSuites/ready-promise.spec';
import fetchSpecificSplits from './browserSuites/fetch-specific-splits.spec';
import userConsent from './browserSuites/user-consent.spec';

import { settingsFactory } from '../settings';

Expand Down Expand Up @@ -116,6 +117,8 @@ tape('## E2E CI Tests ##', function(assert) {
/* Check shared clients */
assert.test('E2E / Shared instances', sharedInstantiationSuite.bind(null, false, fetchMock));
assert.test('E2E / Shared instances with Traffic Type on factory settings', sharedInstantiationSuite.bind(null, true, fetchMock));
/* Validate user consent */
assert.test('E2E / User consent', userConsent.bind(null, fetchMock));
/* Check basic manager functionality */
assert.test('E2E / Manager API', managerSuite.bind(null, settings, fetchMock));
/* Validate readiness */
Expand Down
186 changes: 186 additions & 0 deletions src/__tests__/browserSuites/user-consent.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import sinon from 'sinon';
import { SplitFactory } from '../../';
import { triggerUnloadEvent } from '../testUtils/browser';
import { nearlyEqual, url } from '../testUtils';

const trackedImpressions = [];

const baseConfig = {
core: {
authorizationKey: '<fake-token>',
key: 'facundo@split.io'
},
startup: {
eventsFirstPushWindow: 0
},
urls: {
events: 'https://events.user-consent.io/api'
},
streamingEnabled: false,
impressionListener: {
logImpression: (impression) => {
trackedImpressions.push(impression);
}
}
};

const usageFlows = [{
// Consent granted after usage: user consent is unknown initially, and then set to granted
initialUserConsent: 'UNKNOWN',
setUserConsent: true,
}, {
// Consent declined after usage: user consent is unknown initially, and then set to declined
initialUserConsent: 'UNKNOWN',
setUserConsent: false,
}, {
// Consent granted before usage (default behavior): userConsent config param is granted
initialUserConsent: 'GRANTED',
setUserConsent: true, // no transition
}, {
// Consent granted before usage (default behavior): userConsent config param is not defined
initialUserConsent: undefined,
setUserConsent: true, // no transition
}, {
// Consent declined before usage: userConsent config param is declined
initialUserConsent: 'DECLINED',
setUserConsent: false, // no transition
}, {
// Consent granted after declined: user consent is declined initially, and then set to granted
initialUserConsent: 'DECLINED',
setUserConsent: true,
}, {
// Consent declined after granted: user consent is granted initially, and then set to declined
initialUserConsent: 'GRANTED',
setUserConsent: false,
}];

function mockSubmittersRequests(fetchMock, assert, impressionFeature, eventTypeId) {
fetchMock.postOnce(url(baseConfig, '/testImpressions/count'), 200); // OPTIMIZED impressions mode
fetchMock.postOnce(url(baseConfig, '/testImpressions/bulk'), (url, opts) => {
const resp = JSON.parse(opts.body);
assert.equal(resp[0].f, impressionFeature, 'The expected impression is submitted');
assert.equal(resp[0].i.length, 2, '2 impressions are expected');
return 200;
});
fetchMock.postOnce(url(baseConfig, '/events/bulk'), (url, opts) => {
const resp = JSON.parse(opts.body);
assert.equal(resp[0].eventTypeId, eventTypeId, 'The expected event is submitted');
assert.equal(resp.length, 2, '2 events are expected');
return 200;
});
}

export default function userConsent(fetchMock, t) {

// Validate trackers, submitters and browser listener behaviour on different consent status transitions
t.test(async (assert) => {
const sendBeaconSpy = sinon.spy(window.navigator, 'sendBeacon');
let expectedTrackedImpressions = 0;

for (let i = 0; i < usageFlows.length; i++) {
const { initialUserConsent, setUserConsent } = usageFlows[i];
const config = { ...baseConfig, userConsent: initialUserConsent };
const factory = SplitFactory(config);
const client = factory.client();
const sharedClient = factory.client('marcio@split.io');

await client.ready();
await sharedClient.ready();

let isTracking = factory.getUserConsent() !== 'DECLINED';
assert.deepEqual([client.track('user', 'event1'), sharedClient.track('user', 'event1')], [isTracking, isTracking], 'tracking events on SDK ready');
assert.deepEqual([
client.getTreatment('always_on'), sharedClient.getTreatment('always_on'),
client.getTreatments(['always_on'])['always_on'], sharedClient.getTreatments(['always_on'])['always_on'],
client.getTreatmentWithConfig('always_on').treatment, sharedClient.getTreatmentWithConfig('always_on').treatment,
client.getTreatmentsWithConfig(['always_on'])['always_on'].treatment, sharedClient.getTreatmentsWithConfig(['always_on'])['always_on'].treatment,
], ['on', 'on', 'on', 'on', 'on', 'on', 'on', 'on'], 'evaluating on SDK ready');
if (isTracking) expectedTrackedImpressions += 8;

// Trigger unload event to validate browser listener behaviour
// Beacon API is used only if user consent is GRANTED
triggerUnloadEvent();
if (factory.getUserConsent() === 'GRANTED') {
assert.ok(sendBeaconSpy.calledThrice, 'sendBeacon should have been called thrice');
} else {
assert.ok(sendBeaconSpy.notCalled, 'sendBeacon should not be called if user consent is not granted');
}
sendBeaconSpy.resetHistory();

// If transitioning from UNKNOWN to GRANTED, data was tracked and will be submitted
if (factory.getUserConsent() === 'UNKNOWN' && setUserConsent) {
mockSubmittersRequests(fetchMock, assert, 'always_on', 'event1');
}
if (setUserConsent !== undefined) factory.setUserConsent(setUserConsent);

// Await to track events and impressions with empty queues
await new Promise(res => setTimeout(res));
isTracking = factory.getUserConsent() !== 'DECLINED';
assert.deepEqual([client.track('user', 'event2'), sharedClient.track('user', 'event2')], [isTracking, isTracking], 'tracking events after updating user consent');
assert.deepEqual([
client.getTreatment('always_off'), sharedClient.getTreatment('always_off'),
client.getTreatments(['always_off'])['always_off'], sharedClient.getTreatments(['always_off'])['always_off'],
client.getTreatmentWithConfig('always_off').treatment, sharedClient.getTreatmentWithConfig('always_off').treatment,
client.getTreatmentsWithConfig(['always_off'])['always_off'].treatment, sharedClient.getTreatmentsWithConfig(['always_off'])['always_off'].treatment,
], ['off', 'off', 'off', 'off', 'off', 'off', 'off', 'off'], 'evaluating after updating user consent');
if (isTracking) expectedTrackedImpressions += 8;

// If destroyed while user consent is GRANTED, last tracked data is submitted
if (factory.getUserConsent() === 'GRANTED') {
mockSubmittersRequests(fetchMock, assert, 'always_off', 'event2');
}
await sharedClient.destroy();
await client.destroy();

}

assert.equal(trackedImpressions.length, expectedTrackedImpressions, 'Tracked impressions are the expected');
sendBeaconSpy.restore();
assert.end();
}, 'Validate trackers, submitters and browser listener behaviour on different consent status transitions');

// Validate submitter's behaviour with full queues and with events first push window
t.test(async (assert) => {
const config = {
...baseConfig,
userConsent: 'UNKNOWN',
scheduler: {
eventsQueueSize: 1,
impressionsQueueSize: 1
},
startup: {
eventsFirstPushWindow: 0.1 // 100 millis
},
};
const factory = SplitFactory(config);
const client = factory.client();

await client.ready();

assert.equal(client.track('user', 'event1'), true, 'Events queue is full, but submitter is not executed');
assert.equal(client.getTreatment('always_on'), 'on', 'Impressions queue is full, but submitter is not executed');

let submitterCalls = 0;
const start = Date.now();
fetchMock.postOnce(url(baseConfig, '/testImpressions/count'), () => { submitterCalls++; return 200; }); // OPTIMIZED impressions mode
fetchMock.postOnce(url(baseConfig, '/testImpressions/bulk'), () => { submitterCalls++; return 200; });
fetchMock.postOnce(url(baseConfig, '/events/bulk'), () => {
const lapseSinceConsentGranted = Date.now() - start;
assert.true(nearlyEqual(lapseSinceConsentGranted, config.startup.eventsFirstPushWindow * 1000), 'Events should be posted considering first push window');
submitterCalls++; return 200;
});

factory.setUserConsent(true);

assert.equal(submitterCalls, 2, 'Submitter is resumed and POST requests executed when consent status change to GRANTED, except for events due to first push window');

// Awaits until events PUSH request is resolved
await new Promise(res => setTimeout(res, config.startup.eventsFirstPushWindow * 1000 + 50));
assert.equal(submitterCalls, 3, 'Events POST requests have been executed');

await client.destroy();

assert.end();
}, 'Validate submitter\'s behaviour with full queues and with events first push window');

}
8 changes: 4 additions & 4 deletions src/__tests__/destroy/browser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,23 +107,23 @@ tape('SDK destroy for BrowserJS', async function (assert) {
assert.equal(client3.getTreatment('Single_Test'), 'control', 'After destroy, getTreatment returns control for every destroyed client.');
assert.deepEqual(client3.getTreatments(['Single_Test']), { 'Single_Test': 'control' }, 'After destroy, getTreatments returns map of controls for every destroyed client.');
assert.ok(manager.names().length > 0, 'control assertion');
assert.notOk(client3.track('tt2', 'otherEventType', 3), 'After destroy, track calls return false.');
assert.notOk(client3.track('tt2', 'otherEventType', 3), 'After destroy, track calls return false.');

await client2.destroy();
assert.equal(client2.getTreatment('Single_Test'), 'control', 'After destroy, getTreatment returns control for every destroyed client.');
assert.deepEqual(client2.getTreatments(['Single_Test']), { 'Single_Test': 'control' }, 'After destroy, getTreatments returns map of controls for every destroyed client.');
assert.ok(manager.names().length > 0, 'control assertion');
assert.notOk(client2.track('tt', 'eventType', 2), 'After destroy, track calls return false.');
assert.notOk(client2.track('tt', 'eventType', 2), 'After destroy, track calls return false.');

await client.destroy();
fetchMock.restore();

assert.equal(client.getTreatment('Single_Test'), 'control', 'After destroy, getTreatment returns control for every destroyed client.');
assert.deepEqual(client.getTreatments(['Single_Test']), { 'Single_Test': 'control' }, 'After destroy, getTreatments returns map of controls for every destroyed client.');
assert.notOk(client2.track('tt2', 'eventType', 1), 'After destroy, track calls return false.');
assert.notOk(client2.track('tt2', 'eventType', 1), 'After destroy, track calls return false.');

assert.equal(manager.splits().length, 0, 'After the main client is destroyed, manager.splits will return empty array');
assert.equal(manager.names().length, 0, 'After the main client is destroyed, manager.names will return empty array');
assert.equal(manager.names().length, 0, 'After the main client is destroyed, manager.names will return empty array');
assert.equal(manager.split('Single_Test'), null, 'After the main client is destroyed, manager.split will return null');

assert.end();
Expand Down
16 changes: 8 additions & 8 deletions src/__tests__/destroy/node.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ tape('SDK destroy for NodeJS', async function (assert) {
});

// Events tracking do not need to wait for ready.
client.track('nicolas.zelaya@split.io','tt', 'invalidEventType', 'invalid value' /* Invalid values are not tracked */);
client.track('nicolas.zelaya@gmail.com','tt', 'validEventType', 1);
client.track('nicolas.zelaya@split.io', 'tt', 'invalidEventType', 'invalid value' /* Invalid values are not tracked */);
client.track('nicolas.zelaya@gmail.com', 'tt', 'validEventType', 1);

// Assert we are sending the events while doing the destroy
fetchMock.postOnce(url(settings, '/events/bulk'), (url, opts) => {
Expand Down Expand Up @@ -86,14 +86,14 @@ tape('SDK destroy for NodeJS', async function (assert) {

await destroyPromise;

assert.equal( client.getTreatment('ut1', 'Single_Test'), 'control', 'After destroy, getTreatment returns control.');
assert.deepEqual( client.getTreatments('ut1', ['Single_Test', 'another_split']), {
assert.equal(client.getTreatment('ut1', 'Single_Test'), 'control', 'After destroy, getTreatment returns control.');
assert.deepEqual(client.getTreatments('ut1', ['Single_Test', 'another_split']), {
Single_Test: 'control', another_split: 'control'
}, 'After destroy, getTreatments returns a map of control.');
assert.notOk( client.track('key', 'tt', 'event'), 'After destroy, track calls return false.');
assert.equal( manager.splits().length , 0 , 'After destroy, manager.splits returns empty array.');
assert.equal( manager.names().length , 0 , 'After destroy, manager.names returns empty array.');
assert.equal( manager.split('Single_Test') , null , 'After destroy, manager.split returns null.');
assert.notOk(client.track('key', 'tt', 'event'), 'After destroy, track calls return false.');
assert.equal(manager.splits().length, 0, 'After destroy, manager.splits returns empty array.');
assert.equal(manager.names().length, 0, 'After destroy, manager.names returns empty array.');
assert.equal(manager.split('Single_Test'), null, 'After destroy, manager.split returns null.');

assert.end();
});
3 changes: 3 additions & 0 deletions src/__tests__/nodeSuites/evaluations.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,9 @@ export default async function(config, key, assert) {
assert.deepEqual(client.getAttributes, undefined, 'should not be available');
assert.deepEqual(client.clearAttributes, undefined, 'should not be available');

assert.deepEqual(splitio.setUserConsent, undefined, 'setUserConsent should not be available');
assert.deepEqual(splitio.getUserConsent, undefined, 'getUserConsent should not be available');

getTreatmentTests(client, i);
getTreatmentsTests(client, i);
getTreatmentsWithConfigTests(client, i);
Expand Down
2 changes: 0 additions & 2 deletions src/factory/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ function getModules(settings) {

SignalListener,

impressionListener: settings.impressionListener,

integrationsManagerFactory: settings.integrations && settings.integrations.length > 0 ? integrationsManagerFactory.bind(null, settings.integrations) : undefined,

impressionsObserverFactory: shouldAddPt(settings) ? impressionObserverCSFactory : undefined,
Expand Down
2 changes: 0 additions & 2 deletions src/factory/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ function getModules(settings) {

SignalListener,

impressionListener: settings.impressionListener,

impressionsObserverFactory: shouldAddPt(settings) ? impressionObserverSSFactory : undefined,
};

Expand Down
3 changes: 3 additions & 0 deletions src/settings/__tests__/browser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,8 @@ tape('SETTINGS / Consent is overwritable and "GRANTED" by default in client-side
settings = settingsFactory({ userConsent: 'UNKNOWN' });
assert.equal(settings.userConsent, 'UNKNOWN', 'userConsent can be overwritten.');

settings = settingsFactory({ userConsent: 'declined' });
assert.equal(settings.userConsent, 'DECLINED', 'userConsent can be overwritten.');

assert.end();
});
2 changes: 1 addition & 1 deletion src/settings/defaults/version.js
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const packageVersion = '10.17.3';
export const packageVersion = '10.17.4-rc.0';
2 changes: 1 addition & 1 deletion types/splitio.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1316,7 +1316,7 @@ declare namespace SplitIO {
/**
* Add an attribute to client's in memory attributes storage.
*
* @param {string} attributeName Attrinute name
* @param {string} attributeName Attribute name
* @param {AttributeType} attributeValue Attribute value
* @returns {boolean} true if the attribute was stored and false otherwise
*/
Expand Down