diff --git a/.eslintrc b/.eslintrc index dabb965..9954be9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -30,7 +30,8 @@ "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "error", "keyword-spacing": "error", - "comma-style": "error" + "comma-style": "error", + "no-trailing-spaces": "error" }, "overrides": [ diff --git a/.github/workflows/sonar-scan.yml b/.github/workflows/sonar-scan.yml new file mode 100644 index 0000000..7e428af --- /dev/null +++ b/.github/workflows/sonar-scan.yml @@ -0,0 +1,65 @@ +name: sonar-scan +on: + pull_request: + branches: + - main + - development + push: + branches: + - main + - development + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up nodejs + uses: actions/setup-node@v2 + with: + node-version: '14' + cache: 'npm' + + - name: npm CI + run: npm ci + + - name: npm Check + run: npm run check + + - name: npm Build + run: BUILD_BRANCH=$(echo "${GITHUB_REF#refs/heads/}") BUILD_COMMIT=${{ github.sha }} npm run build + + - name: SonarQube Scan (Push) + if: github.event_name == 'push' + uses: SonarSource/sonarcloud-github-action@v1.6 + env: + SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} + with: + projectBaseDir: . + args: > + -Dsonar.host.url=${{ secrets.SONARQUBE_HOST }} + -Dsonar.projectName=${{ github.event.repository.name }} + -Dsonar.projectKey=${{ github.event.repository.name }} + -Dsonar.links.ci="https://github.com/splitio/${{ github.event.repository.name }}/actions" + -Dsonar.links.scm="https://github.com/splitio/${{ github.event.repository.name }}" + - name: SonarQube Scan (Pull Request) + if: github.event_name == 'pull_request' + uses: SonarSource/sonarcloud-github-action@v1.6 + env: + SONAR_TOKEN: ${{ secrets.SONARQUBE_TOKEN }} + with: + projectBaseDir: . + args: > + -Dsonar.host.url=${{ secrets.SONARQUBE_HOST }} + -Dsonar.projectName=${{ github.event.repository.name }} + -Dsonar.projectKey=${{ github.event.repository.name }} + -Dsonar.links.ci="https://github.com/splitio/${{ github.event.repository.name }}/actions" + -Dsonar.links.scm="https://github.com/splitio/${{ github.event.repository.name }}" + -Dsonar.pullrequest.key=${{ github.event.pull_request.number }} + -Dsonar.pullrequest.branch=${{ github.event.pull_request.head.ref }} + -Dsonar.pullrequest.base=${{ github.event.pull_request.base.ref }} diff --git a/CHANGES.txt b/CHANGES.txt index e52991f..55c830a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,8 @@ +0.9.0 (October 5, 2022) +- Added a new impressions mode for the SDK called NONE, to be used in factory when there is no desire to capture impressions on an SDK factory to feed Split's analytics engine. Running NONE mode, the SDK will only capture unique keys evaluated for a particular feature flag instead of full blown impressions. +- Updated default value of `scheduler.featuresRefreshRate` config parameter from 30 seconds to 60 seconds. +- Updated @splitsoftware/splitio-commons package to version 1.7.1, that improves the performance of split evaluations (i.e., `getTreatment(s)` method calls) when using the default storage in memory, among other improvements. + 0.8.0 (July 22, 2022) - Added `autoRequire` configuration option to the Google Analytics to Split integration, which takes care of requiring the splitTracker plugin on trackers dynamically created by Google tag managers (See https://help.split.io/hc/en-us/articles/360040838752#set-up-with-gtm-and-gtag.js). - Updated browser listener to push remaining impressions and events on 'visibilitychange' and 'pagehide' DOM events, instead of 'unload', which is not reliable in modern mobile and desktop Web browsers. diff --git a/karma/config.debug.js b/karma/config.debug.js new file mode 100644 index 0000000..4e2bfe9 --- /dev/null +++ b/karma/config.debug.js @@ -0,0 +1,15 @@ +'use strict'; + +const merge = require('lodash/merge'); + +module.exports = merge({}, require('./config'), { + browsers: [ + 'Chrome' + ], + rollupPreprocessor: { + output: { + sourcemap: 'inline', + }, + }, + singleRun: false +}); diff --git a/karma/e2e.consumer.karma.conf.js b/karma/e2e.consumer.karma.conf.js index bd39c5a..68791c4 100644 --- a/karma/e2e.consumer.karma.conf.js +++ b/karma/e2e.consumer.karma.conf.js @@ -6,12 +6,12 @@ module.exports = function(config) { config.set(assign({}, require('./config'), { // list of files / patterns to load in the browser files: [ - '__tests__/consumerMode/browser_consumer.spec.js', - '__tests__/consumerMode/browser_consumer_partial.spec.js' + '__tests__/consumer/browser_consumer.spec.js', + '__tests__/consumer/browser_consumer_partial.spec.js' ], // prepare code for the browser using rollup preprocessors: { - '__tests__/consumerMode/*.spec.js': ['rollup'] + '__tests__/consumer/*.spec.js': ['rollup'] }, // level of logging diff --git a/karma/e2e.errors.karma.conf.js b/karma/e2e.errorCatching.karma.conf.js similarity index 80% rename from karma/e2e.errors.karma.conf.js rename to karma/e2e.errorCatching.karma.conf.js index 0010be8..69863c8 100644 --- a/karma/e2e.errors.karma.conf.js +++ b/karma/e2e.errorCatching.karma.conf.js @@ -6,11 +6,11 @@ module.exports = function(config) { config.set(assign({}, require('./config'), { // list of files / patterns to load in the browser files: [ - '__tests__/errors/browser.spec.js' + '__tests__/errorCatching/browser.spec.js' ], // prepare code for the browser using rollup preprocessors: { - '__tests__/errors/browser.spec.js': ['rollup'] + '__tests__/errorCatching/browser.spec.js': ['rollup'] }, // level of logging diff --git a/karma/e2e.gaintegration.karma.conf.js b/karma/e2e.gaIntegration.karma.conf.js similarity index 100% rename from karma/e2e.gaintegration.karma.conf.js rename to karma/e2e.gaIntegration.karma.conf.js diff --git a/package-lock.json b/package-lock.json index 91f54f5..1c98851 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-browserjs", - "version": "0.8.0", + "version": "0.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2194,9 +2194,9 @@ "dev": true }, "@splitsoftware/splitio-commons": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.6.1.tgz", - "integrity": "sha512-lijSMpX5a7HiphPnzr7fKlicGSd5TeBWX5URLPWfL87SPlO+8iF7S7PNFPg8GMu+44CdRrp4REHPJo97ht6ePQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.7.1.tgz", + "integrity": "sha512-kiHHH8oRx7HKx2cJko11kfEXIvLbPUdlJp6yqNC7wWspUCGhPmarm7qrTD3lsene3j0dJ2fpfEc19ElulxtMkQ==", "requires": { "tslib": "^2.3.1" } diff --git a/package.json b/package.json index dcb575e..1568e7e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio-browserjs", - "version": "0.8.0", + "version": "0.9.0", "description": "Split SDK for Javascript on Browser", "main": "cjs/index.js", "module": "esm/index.js", @@ -30,18 +30,19 @@ "build:ga-to-split-autorequire": "terser ./node_modules/@splitsoftware/splitio-commons/src/integrations/ga/autoRequire.js --mangle --output ./scripts/ga-to-split-autorequire.js", "test": "npm run test:unit && npm run test:e2e", "test:unit": "jest", - "test:e2e": "npm run test:e2e-logger && npm run test:e2e-offline && npm run test:e2e-online && npm run test:e2e-destroy && npm run test:e2e-errors && npm run test:e2e-push && npm run test:e2e-gaintegration && npm run test:e2e-consumer", + "test:e2e": "npm run test:e2e-logger && npm run test:e2e-offline && npm run test:e2e-online && npm run test:e2e-destroy && npm run test:e2e-errorCatching && npm run test:e2e-push && npm run test:e2e-gaIntegration && npm run test:e2e-consumer", "test:e2e-logger": "karma start karma/e2e.logger.karma.conf.js", "test:e2e-offline": "karma start karma/e2e.offline.karma.conf.js", "test:e2e-online": "karma start karma/e2e.online.karma.conf.js", "test:e2e-destroy": "karma start karma/e2e.destroy.karma.conf.js", - "test:e2e-errors": "karma start karma/e2e.errors.karma.conf.js", + "test:e2e-errorCatching": "karma start karma/e2e.errorCatching.karma.conf.js", "test:e2e-push": "karma start karma/e2e.push.karma.conf.js", - "test:e2e-gaintegration": "karma start karma/e2e.gaintegration.karma.conf.js", + "test:e2e-gaIntegration": "karma start karma/e2e.gaIntegration.karma.conf.js", "test:e2e-consumer": "karma start karma/e2e.consumer.karma.conf.js", "pretest-ts-decls": "npm run build:esm && npm run build:cjs && npm link", "test-ts-decls": "./scripts/ts-tests.sh", "posttest-ts-decls": "npm unlink && npm install", + "all": "npm run check && npm run build && npm run test-ts-decls && npm run test", "publish:rc": "npm run check && npm run build && npm publish --tag rc", "publish:stable": "npm run check && npm run build && npm publish" }, @@ -63,7 +64,7 @@ "bugs": "https://github.com/splitio/javascript-browser-client/issues", "homepage": "https://github.com/splitio/javascript-browser-client#readme", "dependencies": { - "@splitsoftware/splitio-commons": "1.6.1", + "@splitsoftware/splitio-commons": "1.7.1", "@types/google.analytics": "0.0.40" }, "devDependencies": { diff --git a/src/__tests__/online/evaluations.spec.js b/src/__tests__/browserSuites/evaluations.spec.js similarity index 99% rename from src/__tests__/online/evaluations.spec.js rename to src/__tests__/browserSuites/evaluations.spec.js index c084dc9..cc8bee2 100644 --- a/src/__tests__/online/evaluations.spec.js +++ b/src/__tests__/browserSuites/evaluations.spec.js @@ -1,4 +1,4 @@ -import { SplitFactory } from '../../index'; +import { SplitFactory } from '../../'; const SDK_INSTANCES_TO_TEST = 4; @@ -362,7 +362,6 @@ export default function (config, fetchMock, assert) { }; - for (i; i < SDK_INSTANCES_TO_TEST; i++) { let splitio = SplitFactory(config); diff --git a/src/__tests__/online/events.spec.js b/src/__tests__/browserSuites/events.spec.js similarity index 98% rename from src/__tests__/online/events.spec.js rename to src/__tests__/browserSuites/events.spec.js index e98c7cb..a33dd79 100644 --- a/src/__tests__/online/events.spec.js +++ b/src/__tests__/browserSuites/events.spec.js @@ -1,8 +1,8 @@ -import { SplitFactory } from '../../index'; -import { settingsValidator } from '../../settings'; +import { SplitFactory } from '../../'; +import { settingsFactory } from '../../settings'; import { url } from '../testUtils'; -const settings = settingsValidator({ +const settings = settingsFactory({ core: { key: 'asd' }, @@ -93,6 +93,7 @@ export function withoutBindingTT(fetchMock, assert) { assert.notOk(client.track('othertraffictype', 'my.checkout.event', null, 'asd'), 'client.track returns false if an event properties was incorrect and it could not be added to the queue.'); } +// Not for JS Browser SDK, because it doesn't let bind traffic types to clients export function bindingTT(fetchMock, assert) { const localSettings = Object.assign({}, baseSettings); localSettings.core.trafficType = 'binded_tt'; diff --git a/src/__tests__/online/fetch-specific-splits.spec.js b/src/__tests__/browserSuites/fetch-specific-splits.spec.js similarity index 97% rename from src/__tests__/online/fetch-specific-splits.spec.js rename to src/__tests__/browserSuites/fetch-specific-splits.spec.js index ffed3cc..14dc031 100644 --- a/src/__tests__/online/fetch-specific-splits.spec.js +++ b/src/__tests__/browserSuites/fetch-specific-splits.spec.js @@ -1,4 +1,4 @@ -import { SplitFactory } from '../../index'; +import { SplitFactory } from '../../'; import { splitFilters, queryStrings, groupedFilters } from '../mocks/fetchSpecificSplits'; const baseConfig = { diff --git a/src/__tests__/online/ignore-ip-addresses-setting.spec.js b/src/__tests__/browserSuites/ignore-ip-addresses-setting.spec.js similarity index 96% rename from src/__tests__/online/ignore-ip-addresses-setting.spec.js rename to src/__tests__/browserSuites/ignore-ip-addresses-setting.spec.js index 4503d82..6266976 100644 --- a/src/__tests__/online/ignore-ip-addresses-setting.spec.js +++ b/src/__tests__/browserSuites/ignore-ip-addresses-setting.spec.js @@ -1,5 +1,5 @@ -import { SplitFactory } from '../../index'; -import { settingsValidator } from '../../settings'; +import { SplitFactory } from '../../'; +import { settingsFactory } from '../../settings'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import { DEBUG } from '@splitsoftware/splitio-commons/src/utils/constants'; import { url } from '../testUtils'; @@ -100,7 +100,7 @@ export default function (fetchMock, assert) { }; // Mock GET endpoints before creating the client - const settings = settingsValidator(config); + const settings = settingsFactory(config); fetchMock.getOnce(url(settings, '/splitChanges?since=-1'), { status: 200, body: splitChangesMock1 }); fetchMock.getOnce(url(settings, '/splitChanges?since=1457552620999'), { status: 200, body: { splits: [], since: 1457552620999, till: 1457552620999 } }); fetchMock.getOnce(url(settings, `/mySegments/${encodeURIComponent(config.core.key)}`), { status: 200, body: { mySegments: [] } }); diff --git a/src/__tests__/online/impressions-listener.spec.js b/src/__tests__/browserSuites/impressions-listener.spec.js similarity index 95% rename from src/__tests__/online/impressions-listener.spec.js rename to src/__tests__/browserSuites/impressions-listener.spec.js index a48b75b..de6c41e 100644 --- a/src/__tests__/online/impressions-listener.spec.js +++ b/src/__tests__/browserSuites/impressions-listener.spec.js @@ -1,8 +1,9 @@ import sinon from 'sinon'; -import { SplitFactory } from '../../index'; -import { settingsValidator } from '../../settings'; -const settings = settingsValidator({ +import { SplitFactory } from '../../'; +import { settingsFactory } from '../../settings'; + +const settings = settingsFactory({ core: { key: '' }, diff --git a/src/__tests__/online/impressions.debug.spec.js b/src/__tests__/browserSuites/impressions.debug.spec.js similarity index 96% rename from src/__tests__/online/impressions.debug.spec.js rename to src/__tests__/browserSuites/impressions.debug.spec.js index ba8fa8a..bbccfc9 100644 --- a/src/__tests__/online/impressions.debug.spec.js +++ b/src/__tests__/browserSuites/impressions.debug.spec.js @@ -1,5 +1,5 @@ -import { SplitFactory } from '../../index'; -import { settingsValidator } from '../../settings'; +import { SplitFactory } from '../../'; +import { settingsFactory } from '../../settings'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json'; import mySegmentsFacundo from '../mocks/mysegments.facundo@split.io.json'; @@ -11,7 +11,7 @@ const baseUrls = { events: 'https://events.baseurl/impressionsDebugSuite' }; -const settings = settingsValidator({ +const settings = settingsFactory({ core: { key: 'asd' }, diff --git a/src/__tests__/online/impressions.spec.js b/src/__tests__/browserSuites/impressions.spec.js similarity index 95% rename from src/__tests__/online/impressions.spec.js rename to src/__tests__/browserSuites/impressions.spec.js index 1f2f71d..6be91e6 100644 --- a/src/__tests__/online/impressions.spec.js +++ b/src/__tests__/browserSuites/impressions.spec.js @@ -1,5 +1,5 @@ -import { SplitFactory } from '../../index'; -import { settingsValidator } from '../../settings'; +import { SplitFactory } from '../../'; +import { settingsFactory } from '../../settings'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json'; import mySegmentsFacundo from '../mocks/mysegments.facundo@split.io.json'; @@ -12,7 +12,7 @@ const baseUrls = { events: 'https://events.baseurl/impressionsSuite' }; -const settings = settingsValidator({ +const settings = settingsFactory({ core: { key: 'asd' }, @@ -94,7 +94,7 @@ 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 generated 2 impressions count.'); + assert.equal(data.pf.length, 1, 'We should generate impressions count for one feature.'); // finding these validate the feature names collection too const dependencyChildImpr = data.pf.filter(e => e.f === 'hierarchical_splits_test')[0]; diff --git a/src/__tests__/online/manager.spec.js b/src/__tests__/browserSuites/manager.spec.js similarity index 98% rename from src/__tests__/online/manager.spec.js rename to src/__tests__/browserSuites/manager.spec.js index a530dd8..ce13cd3 100644 --- a/src/__tests__/online/manager.spec.js +++ b/src/__tests__/browserSuites/manager.spec.js @@ -1,4 +1,4 @@ -import { SplitFactory } from '../../index'; +import { SplitFactory } from '../../'; import splitChangesMockReal from '../mocks/splitchanges.real.json'; import map from 'lodash/map'; import { url } from '../testUtils'; diff --git a/src/__tests__/push/push-corner-cases.spec.js b/src/__tests__/browserSuites/push-corner-cases.spec.js similarity index 96% rename from src/__tests__/push/push-corner-cases.spec.js rename to src/__tests__/browserSuites/push-corner-cases.spec.js index 25f38e7..0b659c6 100644 --- a/src/__tests__/push/push-corner-cases.spec.js +++ b/src/__tests__/browserSuites/push-corner-cases.spec.js @@ -8,7 +8,7 @@ import EventSourceMock, { setMockListener } from '../testUtils/eventSourceMock'; window.EventSource = EventSourceMock; import { SplitFactory, InLocalStorage } from '../../'; -import { settingsValidator } from '../../settings'; +import { settingsFactory } from '../../settings'; const userKey = 'nicolas@split.io'; @@ -27,7 +27,7 @@ const config = { prefix: 'pushCornerCase' }), }; -const settings = settingsValidator(config); +const settings = settingsFactory(config); const MILLIS_SSE_OPEN = 100; const MILLIS_SPLIT_KILL_EVENT = 200; @@ -95,6 +95,6 @@ export function testSplitKillOnReadyFromCache(fetchMock, assert) { const lapse = Date.now() - start; assert.true(nearlyEqual(lapse, MILLIS_SPLIT_CHANGES_RESPONSE), 'SDK_READY once split changes arrives'); - client.destroy().then(()=> { assert.end();}); + client.destroy().then(() => { assert.end(); }); }); } diff --git a/src/__tests__/push/push-fallbacking.spec.js b/src/__tests__/browserSuites/push-fallbacking.spec.js similarity index 77% rename from src/__tests__/push/push-fallbacking.spec.js rename to src/__tests__/browserSuites/push-fallbacking.spec.js index 3410126..fe12676 100644 --- a/src/__tests__/push/push-fallbacking.spec.js +++ b/src/__tests__/browserSuites/push-fallbacking.spec.js @@ -16,6 +16,7 @@ import occupancy0ControlSecMessage from '../mocks/message.OCCUPANCY.0.control_se import streamingPausedControlPriMessage from '../mocks/message.CONTROL.STREAMING_PAUSED.control_pri.1586987434750.json'; import streamingResumedControlPriMessage from '../mocks/message.CONTROL.STREAMING_RESUMED.control_pri.1586987434850.json'; +import streamingPausedControlPriMessage2 from '../mocks/message.CONTROL.STREAMING_PAUSED.control_pri.1586987434900.json'; import streamingDisabledControlPriMessage from '../mocks/message.CONTROL.STREAMING_DISABLED.control_pri.1586987434950.json'; import splitUpdateMessage from '../mocks/message.SPLIT_UPDATE.1457552649999.json'; @@ -24,17 +25,18 @@ import mySegmentsUpdateMessage from '../mocks/message.MY_SEGMENTS_UPDATE.nicolas import authPushEnabledNicolas from '../mocks/auth.pushEnabled.nicolas@split.io.json'; import authPushEnabledNicolasAndMarcio from '../mocks/auth.pushEnabled.nicolas@split.io.marcio@split.io.json'; +import streamingResetMessage from '../mocks/message.STREAMING_RESET.json'; + import { nearlyEqual, url } from '../testUtils'; import EventSourceMock, { setMockListener } from '../testUtils/eventSourceMock'; window.EventSource = EventSourceMock; -import { SplitFactory } from '../../index'; -import { settingsValidator } from '../../settings'; +import { SplitFactory } from '../../'; +import { settingsFactory } from '../../settings'; const userKey = 'nicolas@split.io'; const secondUserKey = 'marcio@split.io'; -const thirdUserKey = 'facundo@split.io'; const baseUrls = { sdk: 'https://sdk.push-fallbacking/api', @@ -55,7 +57,7 @@ const config = { streamingEnabled: true, // debug: true, }; -const settings = settingsValidator(config); +const settings = settingsFactory(config); const MILLIS_SSE_OPEN = 100; const MILLIS_STREAMING_DOWN_OCCUPANCY = MILLIS_SSE_OPEN + 100; @@ -68,7 +70,11 @@ const MILLIS_STREAMING_PAUSED_CONTROL = MILLIS_SPLIT_UPDATE_EVENT_DURING_PUSH + const MILLIS_MY_SEGMENTS_UPDATE_EVENT_DURING_POLLING = MILLIS_STREAMING_PAUSED_CONTROL + 100; const MILLIS_STREAMING_RESUMED_CONTROL = MILLIS_STREAMING_PAUSED_CONTROL + settings.scheduler.featuresRefreshRate + 100; const MILLIS_MY_SEGMENTS_UPDATE_EVENT_DURING_PUSH = MILLIS_STREAMING_RESUMED_CONTROL + 100; -const MILLIS_STREAMING_DISABLED_CONTROL = MILLIS_MY_SEGMENTS_UPDATE_EVENT_DURING_PUSH + 100; + +const MILLIS_STREAMING_PAUSED_CONTROL_2 = MILLIS_MY_SEGMENTS_UPDATE_EVENT_DURING_PUSH + 100; +const MILLIS_STREAMING_RESET_WHILE_PUSH_DOWN = MILLIS_STREAMING_PAUSED_CONTROL_2 + 100; +const MILLIS_STREAMING_RESET_WHILE_PUSH_UP = MILLIS_STREAMING_RESET_WHILE_PUSH_DOWN + settings.scheduler.featuresRefreshRate; +const MILLIS_STREAMING_DISABLED_CONTROL = MILLIS_STREAMING_RESET_WHILE_PUSH_UP + 100; const MILLIS_DESTROY = MILLIS_STREAMING_DISABLED_CONTROL + settings.scheduler.featuresRefreshRate * 2 + 100; /** @@ -83,26 +89,27 @@ const MILLIS_DESTROY = MILLIS_STREAMING_DISABLED_CONTROL + settings.scheduler.fe * 0.55 secs: create a new client while streaming -> initial fetch (/mySegments/marcio), auth, SSE connection and syncAll (/splitChanges, /mySegments/nicolas, /mySegments/marcio) * 0.6 secs: SPLIT_UPDATE event -> /splitChanges * 0.7 secs: Streaming down (CONTROL event) -> fetch due to fallback to polling (/splitChanges, /mySegments/nicolas, /mySegments/marcio) - * 0.7 secs: create a new client while polling -> initial fetch (/mySegments/facundo), auth fail (continue polling) * 0.8 secs: MY_SEGMENTS_UPDATE event ignored * 0.9 secs: periodic fetch due to polling (/splitChanges) * 0.95 secs: periodic fetch due to polling (/mySegments/nicolas, /mySegments/marcio, /mySegments/facundo) * 1.0 secs: Streaming up (CONTROL event) -> syncAll (/splitChanges, /mySegments/nicolas, /mySegments/marcio, /mySegments/facundo) * 1.1 secs: MY_SEGMENTS_UPDATE event -> /mySegments/nicolas * 1.2 secs: Streaming down (CONTROL event) -> fetch due to fallback to polling (/splitChanges, /mySegments/nicolas, /mySegments/marcio, /mySegments/facundo) - * 1.4 secs: periodic fetch due to polling (/splitChanges): due to update without segments, mySegments are not fetched - * 1.6 secs: periodic fetch due to polling (/splitChanges) - * 1.7 secs: destroy client + * 1.3 secs: STREAMING_RESET control event -> auth, SSE connection, syncAll and stop polling + * 1.5 secs: STREAMING_RESET control event -> auth, SSE connection, syncAll + * 1.6 secs: Streaming closed (CONTROL STREAMING_DISABLED event) -> fetch due to fallback to polling (/splitChanges, /mySegments/nicolas, /mySegments/marcio, /mySegments/facundo) + * 1.8 secs: periodic fetch due to polling (/splitChanges): due to update without segments, mySegments are not fetched + * 2.0 secs: periodic fetch due to polling (/splitChanges) + * 2.1 secs: destroy client */ export function testFallbacking(fetchMock, assert) { - assert.plan(16); + assert.plan(20); fetchMock.reset(); - let start, splitio, client; - let secondClient, thirdClient; + let start, splitio, client, secondClient; // mock SSE open and message events - setMockListener(function (eventSourceInstance) { + setMockListener((eventSourceInstance) => { const expectedSSEurl = `${url(settings, '/sse')}?channels=NzM2MDI5Mzc0_NDEzMjQ1MzA0Nw%3D%3D_NTcwOTc3MDQx_mySegments,NzM2MDI5Mzc0_NDEzMjQ1MzA0Nw%3D%3D_splits,%5B%3Foccupancy%3Dmetrics.publishers%5Dcontrol_pri,%5B%3Foccupancy%3Dmetrics.publishers%5Dcontrol_sec&accessToken=${authPushEnabledNicolas.token}&v=1.1&heartbeats=true&SplitSDKVersion=${settings.version}&SplitSDKClientKey=h-1>`; assert.equals(eventSourceInstance.url, expectedSSEurl, 'EventSource URL is the expected'); @@ -118,6 +125,7 @@ export function testFallbacking(fetchMock, assert) { }, MILLIS_STREAMING_DOWN_OCCUPANCY); // send an OCCUPANCY event for switching to polling setTimeout(() => { + assert.equal(eventSourceInstance.readyState, EventSourceMock.OPEN, 'EventSource connection keeps opened after PUSH_SUBSYSTEM_DOWN (occupancy 0)'); eventSourceInstance.emitMessage(splitUpdateMessage); }, MILLIS_SPLIT_UPDATE_EVENT_DURING_POLLING); // send a SPLIT_UPDATE event while polling, to check that we are ignoring it @@ -126,10 +134,9 @@ export function testFallbacking(fetchMock, assert) { }, MILLIS_STREAMING_UP_OCCUPANCY); // send a OCCUPANCY event for switching to push setTimeout(() => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars secondClient = splitio.client(secondUserKey); - setMockListener(function (eventSourceInstance) { + setMockListener((eventSourceInstance) => { const expectedSSEurl = `${url(settings, '/sse')}?channels=NzM2MDI5Mzc0_NDEzMjQ1MzA0Nw%3D%3D_MjE0MTkxOTU2Mg%3D%3D_mySegments,NzM2MDI5Mzc0_NDEzMjQ1MzA0Nw%3D%3D_NTcwOTc3MDQx_mySegments,NzM2MDI5Mzc0_NDEzMjQ1MzA0Nw%3D%3D_splits,%5B%3Foccupancy%3Dmetrics.publishers%5Dcontrol_pri,%5B%3Foccupancy%3Dmetrics.publishers%5Dcontrol_sec&accessToken=${authPushEnabledNicolasAndMarcio.token}&v=1.1&heartbeats=true&SplitSDKVersion=${settings.version}&SplitSDKClientKey=h-1>`; assert.equals(eventSourceInstance.url, expectedSSEurl, 'new EventSource URL is the expected'); eventSourceInstance.emitOpen(); @@ -144,11 +151,10 @@ export function testFallbacking(fetchMock, assert) { setTimeout(() => { eventSourceInstance.emitMessage(streamingPausedControlPriMessage); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - thirdClient = splitio.client(thirdUserKey); }, MILLIS_STREAMING_PAUSED_CONTROL - MILLIS_CREATE_CLIENT_DURING_PUSH); // send a CONTROL event for switching to polling setTimeout(() => { + assert.equal(eventSourceInstance.readyState, EventSourceMock.OPEN, 'EventSource connection keeps opened after PUSH_SUBSYSTEM_DOWN (STREAMING_PAUSED event)'); eventSourceInstance.emitMessage(mySegmentsUpdateMessage); }, MILLIS_MY_SEGMENTS_UPDATE_EVENT_DURING_POLLING - MILLIS_CREATE_CLIENT_DURING_PUSH); // send a MY_SEGMENTS_UPDATE event while polling, to check that we are ignoring it @@ -165,15 +171,35 @@ export function testFallbacking(fetchMock, assert) { }, MILLIS_MY_SEGMENTS_UPDATE_EVENT_DURING_PUSH - MILLIS_CREATE_CLIENT_DURING_PUSH); // send a MY_SEGMENTS_UPDATE event setTimeout(() => { - eventSourceInstance.emitMessage(streamingDisabledControlPriMessage); - assert.equal(eventSourceInstance.readyState, EventSourceMock.CLOSED, 'EventSource connection closed on STREAMING_DISABLED CONTROL event'); - }, MILLIS_STREAMING_DISABLED_CONTROL - MILLIS_CREATE_CLIENT_DURING_PUSH); // send a CONTROL event for disabling push and switching to polling + eventSourceInstance.emitMessage(streamingPausedControlPriMessage2); + }, MILLIS_STREAMING_PAUSED_CONTROL_2 - MILLIS_CREATE_CLIENT_DURING_PUSH); // send a CONTROL event for switching back to polling setTimeout(() => { - client.destroy().then(() => { - assert.pass('client destroyed'); + eventSourceInstance.emitMessage(streamingResetMessage); + + setMockListener((eventSourceInstance) => { + eventSourceInstance.emitOpen(); + + setTimeout(() => { + eventSourceInstance.emitMessage(streamingResetMessage); + + setMockListener((eventSourceInstance) => { + eventSourceInstance.emitOpen(); + + setTimeout(() => { + eventSourceInstance.emitMessage(streamingDisabledControlPriMessage); + assert.equal(eventSourceInstance.readyState, EventSourceMock.CLOSED, 'EventSource connection closed on STREAMING_DISABLED CONTROL event'); + }, MILLIS_STREAMING_DISABLED_CONTROL - MILLIS_STREAMING_RESET_WHILE_PUSH_UP); // send a CONTROL event for disabling push and switching to polling + + setTimeout(() => { + Promise.all([secondClient.destroy(), client.destroy()]).then(() => { + assert.pass('client destroyed'); + }); + }, MILLIS_DESTROY - MILLIS_STREAMING_RESET_WHILE_PUSH_UP); // destroy client + }); + }, MILLIS_STREAMING_RESET_WHILE_PUSH_UP - MILLIS_STREAMING_RESET_WHILE_PUSH_DOWN); // send a new STREAMING_RESET event while PUSH is up }); - }, MILLIS_DESTROY - MILLIS_CREATE_CLIENT_DURING_PUSH); // destroy client after 1.7 seconds + }, MILLIS_STREAMING_RESET_WHILE_PUSH_DOWN - MILLIS_CREATE_CLIENT_DURING_PUSH); // send a STREAMING_RESET event while PUSH is down, for re-connecting push }); @@ -211,7 +237,7 @@ export function testFallbacking(fetchMock, assert) { // creating of second client during streaming: initial mysegment sync, reauth and syncAll due to new client fetchMock.getOnce(url(settings, '/mySegments/marcio%40split.io'), { status: 200, body: mySegmentsMarcio }); - fetchMock.getOnce(url(settings, `/v2/auth?users=${encodeURIComponent(userKey)}&users=${encodeURIComponent(secondUserKey)}`), function (url, opts) { + fetchMock.get({ url: url(settings, `/v2/auth?users=${encodeURIComponent(userKey)}&users=${encodeURIComponent(secondUserKey)}`), repeat: 3 /* initial + 2 STREAMING_RESET */ }, (url, opts) => { if (!opts.headers['Authorization']) assert.fail('`/v2/auth` request must include `Authorization` header'); assert.pass('second auth success'); return { status: 200, body: authPushEnabledNicolasAndMarcio }; @@ -232,11 +258,6 @@ export function testFallbacking(fetchMock, assert) { fetchMock.getOnce(url(settings, '/mySegments/nicolas%40split.io'), { status: 200, body: mySegmentsNicolasMock1 }); fetchMock.getOnce(url(settings, '/mySegments/marcio%40split.io'), { status: 200, body: mySegmentsMarcio }); - // creation of third client during polling: initial mysegment sync and authentication - fetchMock.getOnce(url(settings, '/mySegments/facundo%40split.io'), { status: 200, body: mySegmentsMarcio }); - // authentication fail, so we keep polling. next auth attempt is scheduled in one second (after the test finishes) - fetchMock.getOnce(url(settings, `/v2/auth?users=${encodeURIComponent(userKey)}&users=${encodeURIComponent(secondUserKey)}&users=${encodeURIComponent(thirdUserKey)}`), { throws: new TypeError('Network error') }); - // continue fetches due to second fallback to polling fetchMock.getOnce(url(settings, '/splitChanges?since=1457552649999'), function () { const lapse = Date.now() - start; @@ -245,13 +266,11 @@ export function testFallbacking(fetchMock, assert) { }); fetchMock.getOnce(url(settings, '/mySegments/nicolas%40split.io'), { status: 200, body: mySegmentsNicolasMock1 }); fetchMock.getOnce(url(settings, '/mySegments/marcio%40split.io'), { status: 200, body: mySegmentsMarcio }); - fetchMock.getOnce(url(settings, '/mySegments/facundo%40split.io'), { status: 200, body: mySegmentsMarcio }); // split and segment sync due to streaming up (CONTROL event) fetchMock.getOnce(url(settings, '/splitChanges?since=1457552649999'), { status: 200, body: { splits: [], since: 1457552649999, till: 1457552649999 } }); fetchMock.getOnce(url(settings, '/mySegments/nicolas%40split.io'), { status: 200, body: mySegmentsNicolasMock1 }); fetchMock.getOnce(url(settings, '/mySegments/marcio%40split.io'), { status: 200, body: mySegmentsMarcio }); - fetchMock.getOnce(url(settings, '/mySegments/facundo%40split.io'), { status: 200, body: mySegmentsMarcio }); // fetch due to MY_SEGMENTS_UPDATE event fetchMock.getOnce(url(settings, '/mySegments/nicolas%40split.io'), function () { @@ -260,19 +279,20 @@ export function testFallbacking(fetchMock, assert) { return { status: 200, body: mySegmentsNicolasMock2 }; }); - // fetches due to third fallback to polling (mySegments is not fetched after the first iteration) - fetchMock.getOnce(url(settings, '/splitChanges?since=1457552649999'), { status: 200, body: { splits: [], since: 1457552649999, till: 1457552649999 } }); - fetchMock.getOnce(url(settings, '/mySegments/nicolas%40split.io'), { status: 200, body: mySegmentsNicolasMock1 }); - fetchMock.getOnce(url(settings, '/mySegments/marcio%40split.io'), { status: 200, body: mySegmentsMarcio }); - fetchMock.getOnce(url(settings, '/mySegments/facundo%40split.io'), { status: 200, body: mySegmentsMarcio }); + // fetches due to third fallback to polling (STREAMING_PAUSED), two sync all (two STREAMING_RESET events) and fourth fallback (STREAMING_DISABLED) + fetchMock.get({ url: url(settings, '/splitChanges?since=1457552649999'), repeat: 4 }, { status: 200, body: { splits: [], since: 1457552649999, till: 1457552649999 } }); + fetchMock.get({ url: url(settings, '/mySegments/nicolas%40split.io'), repeat: 4 }, { status: 200, body: mySegmentsNicolasMock1 }); + fetchMock.get({ url: url(settings, '/mySegments/marcio%40split.io'), repeat: 4 }, { status: 200, body: mySegmentsMarcio }); + + // Periodic fetch due to polling (mySegments is not fetched due to smart pausing) fetchMock.getOnce(url(settings, '/splitChanges?since=1457552649999'), function () { const lapse = Date.now() - start; - assert.true(nearlyEqual(lapse, MILLIS_STREAMING_DISABLED_CONTROL + settings.scheduler.featuresRefreshRate), 'fetch due to third fallback to polling'); + assert.true(nearlyEqual(lapse, MILLIS_STREAMING_DISABLED_CONTROL + settings.scheduler.featuresRefreshRate, 75), 'fetch due to fourth fallback to polling'); return { status: 200, body: splitChangesMock3 }; }); fetchMock.getOnce(url(settings, '/splitChanges?since=1457552669999'), function () { const lapse = Date.now() - start; - assert.true(nearlyEqual(lapse, MILLIS_STREAMING_DISABLED_CONTROL + settings.scheduler.featuresRefreshRate * 2), 'fetch due to third fallback to polling'); + assert.true(nearlyEqual(lapse, MILLIS_STREAMING_DISABLED_CONTROL + settings.scheduler.featuresRefreshRate * 2, 75), 'fetch due to fourth fallback to polling'); return { status: 200, body: { splits: [], since: 1457552669999, till: 1457552669999 } }; }); diff --git a/src/__tests__/push/push-initialization-nopush.spec.js b/src/__tests__/browserSuites/push-initialization-nopush.spec.js similarity index 97% rename from src/__tests__/push/push-initialization-nopush.spec.js rename to src/__tests__/browserSuites/push-initialization-nopush.spec.js index b8e7bc4..6e0e0a4 100644 --- a/src/__tests__/push/push-initialization-nopush.spec.js +++ b/src/__tests__/browserSuites/push-initialization-nopush.spec.js @@ -1,5 +1,5 @@ -import { SplitFactory } from '../../index'; -import { settingsValidator } from '../../settings'; +import { SplitFactory } from '../../'; +import { settingsFactory } from '../../settings'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json'; import mySegmentsNicolas from '../mocks/mysegments.nicolas@split.io.json'; @@ -7,6 +7,7 @@ import authPushDisabled from '../mocks/auth.pushDisabled.json'; import authPushEnabledNicolas from '../mocks/auth.pushEnabled.nicolas@split.io.json'; import authInvalidCredentials from '../mocks/auth.invalidCredentials.txt'; import { nearlyEqual, url } from '../testUtils'; + import EventSourceMock, { setMockListener } from '../testUtils/eventSourceMock'; const baseUrls = { @@ -33,7 +34,7 @@ const config = { streamingEnabled: true, debug: true, }; -const settings = settingsValidator(config); +const settings = settingsFactory(config); /** * Sequence of calls: diff --git a/src/__tests__/push/push-initialization-retries.spec.js b/src/__tests__/browserSuites/push-initialization-retries.spec.js similarity index 98% rename from src/__tests__/push/push-initialization-retries.spec.js rename to src/__tests__/browserSuites/push-initialization-retries.spec.js index 0879954..4d72ee1 100644 --- a/src/__tests__/push/push-initialization-retries.spec.js +++ b/src/__tests__/browserSuites/push-initialization-retries.spec.js @@ -6,9 +6,11 @@ import authPushBadToken from '../mocks/auth.pushBadToken.json'; import mySegmentsNicolasMock from '../mocks/mysegments.nicolas@split.io.json'; import { nearlyEqual, url } from '../testUtils'; + import EventSourceMock, { setMockListener } from '../testUtils/eventSourceMock'; -import { SplitFactory } from '../../index'; -import { settingsValidator } from '../../settings'; + +import { SplitFactory } from '../../'; +import { settingsFactory } from '../../settings'; const baseUrls = { sdk: 'https://sdk.push-initialization-retries/api', @@ -34,7 +36,7 @@ const config = { streamingEnabled: true, // debug: true, }; -const settings = settingsValidator(config); +const settings = settingsFactory(config); /** * Sequence of calls: diff --git a/src/__tests__/push/push-refresh-token.spec.js b/src/__tests__/browserSuites/push-refresh-token.spec.js similarity index 97% rename from src/__tests__/push/push-refresh-token.spec.js rename to src/__tests__/browserSuites/push-refresh-token.spec.js index 39a0b24..8a22650 100644 --- a/src/__tests__/push/push-refresh-token.spec.js +++ b/src/__tests__/browserSuites/push-refresh-token.spec.js @@ -1,17 +1,18 @@ import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json'; import mySegmentsNicolasMock1 from '../mocks/mysegments.nicolas@split.io.json'; + import authPushEnabledNicolas from '../mocks/auth.pushEnabled.nicolas@split.io.601secs.json'; import authPushDisabled from '../mocks/auth.pushDisabled.json'; import { nearlyEqual, url } from '../testUtils'; -// Polyfill EventSource with mock +// Replace global EventSource with mock import EventSourceMock, { setMockListener } from '../testUtils/eventSourceMock'; window.EventSource = EventSourceMock; -import { SplitFactory } from '../../index'; -import { settingsValidator } from '../../settings'; +import { SplitFactory } from '../../'; +import { settingsFactory } from '../../settings'; const userKey = 'nicolas@split.io'; @@ -32,7 +33,7 @@ const config = { }, // debug: true, }; -const settings = settingsValidator(config); +const settings = settingsFactory(config); const MILLIS_CONNDELAY = 500; const MILLIS_REFRESH_TOKEN = 1000; diff --git a/src/__tests__/push/push-synchronization-retries.spec.js b/src/__tests__/browserSuites/push-synchronization-retries.spec.js similarity index 98% rename from src/__tests__/push/push-synchronization-retries.spec.js rename to src/__tests__/browserSuites/push-synchronization-retries.spec.js index c106710..03c7955 100644 --- a/src/__tests__/push/push-synchronization-retries.spec.js +++ b/src/__tests__/browserSuites/push-synchronization-retries.spec.js @@ -4,10 +4,12 @@ import splitChangesMock3 from '../mocks/splitchanges.since.1457552620999.till.14 import mySegmentsNicolasMock1 from '../mocks/mysegments.nicolas@split.io.json'; import mySegmentsNicolasMock2 from '../mocks/mysegments.nicolas@split.io.mock2.json'; import mySegmentsMarcio from '../mocks/mysegments.marcio@split.io.json'; + import splitUpdateMessage from '../mocks/message.SPLIT_UPDATE.1457552649999.json'; import oldSplitUpdateMessage from '../mocks/message.SPLIT_UPDATE.1457552620999.json'; import mySegmentsUpdateMessage from '../mocks/message.MY_SEGMENTS_UPDATE.nicolas@split.io.1457552640000.json'; import splitKillMessage from '../mocks/message.SPLIT_KILL.1457552650000.json'; + import authPushEnabledNicolas from '../mocks/auth.pushEnabled.nicolas@split.io.json'; import { nearlyEqual, url } from '../testUtils'; @@ -17,8 +19,8 @@ import { Backoff } from '@splitsoftware/splitio-commons/src/utils/Backoff'; import EventSourceMock, { setMockListener } from '../testUtils/eventSourceMock'; window.EventSource = EventSourceMock; -import { SplitFactory } from '../../index'; -import { settingsValidator } from '../../settings'; +import { SplitFactory } from '../../'; +import { settingsFactory } from '../../settings'; const userKey = 'nicolas@split.io'; const otherUserKeySync = 'marcio@split.io'; @@ -37,7 +39,7 @@ const config = { streamingEnabled: true, // debug: true, }; -const settings = settingsValidator(config); +const settings = settingsFactory(config); const MILLIS_SSE_OPEN = 100; diff --git a/src/__tests__/push/push-synchronization.spec.js b/src/__tests__/browserSuites/push-synchronization.spec.js similarity index 98% rename from src/__tests__/push/push-synchronization.spec.js rename to src/__tests__/browserSuites/push-synchronization.spec.js index 4f5d01a..f6ffa61 100644 --- a/src/__tests__/push/push-synchronization.spec.js +++ b/src/__tests__/browserSuites/push-synchronization.spec.js @@ -21,15 +21,16 @@ import segmentRemovalMessage from '../mocks/message.V2.SEGMENT_REMOVAL.145755265 import authPushEnabledNicolas from '../mocks/auth.pushEnabled.nicolas@split.io.json'; import authPushEnabledNicolasAndMarcio from '../mocks/auth.pushEnabled.nicolas@split.io.marcio@split.io.json'; -import { nearlyEqual, url, hasNoCacheHeader, triggerUnloadEvent } from '../testUtils'; +import { nearlyEqual, url, hasNoCacheHeader } from '../testUtils'; +import { triggerUnloadEvent } from '../testUtils/browser'; import includes from 'lodash/includes'; // Replace original EventSource with mock import EventSourceMock, { setMockListener } from '../testUtils/eventSourceMock'; window.EventSource = EventSourceMock; -import { SplitFactory, DebugLogger } from '../../index'; -import { settingsValidator } from '../../settings'; +import { SplitFactory } from '../../'; +import { settingsFactory } from '../../settings'; const userKey = 'nicolas@split.io'; const otherUserKey = 'marcio@split.io'; @@ -49,9 +50,8 @@ const config = { }, urls: baseUrls, streamingEnabled: true, - debug: DebugLogger(), }; -const settings = settingsValidator(config); +const settings = settingsFactory(config); const MILLIS_SSE_OPEN = 100; const MILLIS_FIRST_SPLIT_UPDATE_EVENT = 200; diff --git a/src/__tests__/online/readiness.spec.js b/src/__tests__/browserSuites/readiness.spec.js similarity index 99% rename from src/__tests__/online/readiness.spec.js rename to src/__tests__/browserSuites/readiness.spec.js index 8d50634..ee92746 100644 --- a/src/__tests__/online/readiness.spec.js +++ b/src/__tests__/browserSuites/readiness.spec.js @@ -1,4 +1,4 @@ -import { SplitFactory, InLocalStorage } from '../../index'; +import { SplitFactory, InLocalStorage } from '../../'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json'; diff --git a/src/__tests__/online/ready-from-cache.spec.js b/src/__tests__/browserSuites/ready-from-cache.spec.js similarity index 97% rename from src/__tests__/online/ready-from-cache.spec.js rename to src/__tests__/browserSuites/ready-from-cache.spec.js index 7c5c0d9..38aceae 100644 --- a/src/__tests__/online/ready-from-cache.spec.js +++ b/src/__tests__/browserSuites/ready-from-cache.spec.js @@ -1,4 +1,4 @@ -import { SplitFactory, InLocalStorage } from '../../index'; +import { SplitFactory, InLocalStorage } from '../../'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json'; @@ -113,11 +113,11 @@ export default function (fetchMock, assert) { const client2 = splitio.client('nicolas2@split.io'); const client3 = splitio.client('nicolas3@split.io'); - client.on(client.Event.SDK_READY_TIMED_OUT, () => { + client.once(client.Event.SDK_READY_TIMED_OUT, () => { t.fail('It should not timeout in this scenario.'); t.end(); }); - client.on(client.Event.SDK_READY_FROM_CACHE, () => { + client.once(client.Event.SDK_READY_FROM_CACHE, () => { t.fail('It should not emit SDK_READY_FROM_CACHE if there is no cache.'); t.end(); }); @@ -503,7 +503,7 @@ export default function (fetchMock, assert) { }); client.once(client.Event.SDK_READY, () => { - t.deepEqual(manager.names(), ['p1__split', 'p2__split'], 'p1__split should be added for evaluation'); + t.deepEqual(manager.names().sort(), ['p1__split', 'p2__split'], 'p1__split should be added for evaluation'); client.destroy().then(() => { t.equal(localStorage.getItem('some_user_item'), 'user_item', 'user items at localStorage must not be changed'); @@ -547,7 +547,7 @@ export default function (fetchMock, assert) { }); client.once(client.Event.SDK_READY, () => { - t.deepEqual(manager.names(), ['p1__split', 'p2__split'], 'p1__split should be added for evaluation'); + t.deepEqual(manager.names().sort(), ['p1__split', 'p2__split'], 'p1__split should be added for evaluation'); client.destroy().then(() => { t.equal(localStorage.getItem('readyFromCache_5B.SPLITIO.splits.till'), '1457552620999', 'splits.till must correspond to the till of the last successfully fetched Splits'); @@ -591,11 +591,11 @@ export default function (fetchMock, assert) { const manager = splitio.manager(); client.once(client.Event.SDK_READY_FROM_CACHE, () => { - t.deepEqual(manager.names(), ['p2__split', 'p1__split'], 'splits shouldn\'t be removed for evaluation'); + t.deepEqual(manager.names().sort(), ['p1__split', 'p2__split'], 'splits shouldn\'t be removed for evaluation'); }); client.once(client.Event.SDK_READY, () => { - t.deepEqual(manager.names(), ['p2__split', 'p1__split'], 'active splits should be present for evaluation'); + t.deepEqual(manager.names().sort(), ['p1__split', 'p2__split'], 'active splits should be present for evaluation'); client.destroy().then(() => { t.equal(localStorage.getItem('some_user_item'), 'user_item', 'user items at localStorage must not be changed'); @@ -646,7 +646,7 @@ export default function (fetchMock, assert) { }); client.once(client.Event.SDK_READY, () => { - t.deepEqual(manager.names(), ['p2__split', 'p1__split'], 'active splits should be present for evaluation'); + t.deepEqual(manager.names().sort(), ['p1__split', 'p2__split'], 'active splits should be present for evaluation'); client.destroy().then(() => { t.equal(localStorage.getItem('some_user_item'), 'user_item', 'user items at localStorage must not be changed'); @@ -687,7 +687,7 @@ export default function (fetchMock, assert) { localStorage.setItem('readyFromCache_8.SPLITIO.splits.till', 25); localStorage.setItem('readyFromCache_8.SPLITIO.split.p1__split', JSON.stringify(splitDeclarations.p1__split)); localStorage.setItem('readyFromCache_8.SPLITIO.split.p2__split', JSON.stringify(splitDeclarations.p2__split)); - localStorage.setItem('readyFromCache_8.SPLITIO.split.deleted__split', 'deleted_split'); + localStorage.setItem('readyFromCache_8.SPLITIO.split.deleted__split', '{ "name": "deleted_split" }'); localStorage.setItem('readyFromCache_8.SPLITIO.splits.filterQuery', '&names=p2__split&prefixes=p1'); const splitio = SplitFactory({ @@ -708,7 +708,7 @@ export default function (fetchMock, assert) { }); client.once(client.Event.SDK_READY, () => { - t.deepEqual(manager.names(), ['p1__split', 'p2__split', 'p3__split'], 'active splits should be present for evaluation'); + t.deepEqual(manager.names().sort(), ['p1__split', 'p2__split', 'p3__split'], 'active splits should be present for evaluation'); client.destroy().then(() => { t.equal(localStorage.getItem('some_user_item'), 'user_item', 'user items at localStorage must not be changed'); @@ -761,7 +761,7 @@ export default function (fetchMock, assert) { }); client.once(client.Event.SDK_READY, () => { - t.deepEqual(manager.names(), ['p3__split', 'p2__split'], 'active splits should be present for evaluation'); + t.deepEqual(manager.names().sort(), ['p2__split', 'p3__split'], 'active splits should be present for evaluation'); client.destroy().then(() => { t.equal(localStorage.getItem('some_user_item'), 'user_item', 'user items at localStorage must not be changed'); diff --git a/src/__tests__/online/ready-promise.spec.js b/src/__tests__/browserSuites/ready-promise.spec.js similarity index 99% rename from src/__tests__/online/ready-promise.spec.js rename to src/__tests__/browserSuites/ready-promise.spec.js index 0a35a4d..0a111e3 100644 --- a/src/__tests__/online/ready-promise.spec.js +++ b/src/__tests__/browserSuites/ready-promise.spec.js @@ -10,7 +10,7 @@ const consoleSpy = { error: sinon.spy(console, 'error'), }; -import { SplitFactory, WarnLogger } from '../../index'; +import { SplitFactory, WarnLogger } from '../../'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import mySegmentsFacundo from '../mocks/mysegments.facundo@split.io.json'; @@ -546,12 +546,11 @@ export default function readyPromiseAssertions(fetchMock, assert) { t.false(consoleSpy.log.calledWithExactly('[WARN] splitio => No listeners for SDK Readiness detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet ready.'), 'No warning logged'); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const sharedClientWithoutCb = splitio.client('emiliano@split.io'); setTimeout(() => { t.true(consoleSpy.log.calledWithExactly('[WARN] splitio => No listeners for SDK Readiness detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet ready.'), 'Warning logged'); - client.destroy().then(() => { + Promise.all([sharedClientWithoutCb.destroy(), client.destroy()]).then(() => { client.ready() .then(() => { t.pass('### SDK IS READY - the promise remains resolved after client destruction.'); @@ -563,7 +562,7 @@ export default function readyPromiseAssertions(fetchMock, assert) { t.end(); }); }); - }, 0); + }, 10); }); }, fromSecondsToMillis(0.2)); diff --git a/src/__tests__/online/shared-instantiation.spec.js b/src/__tests__/browserSuites/shared-instantiation.spec.js similarity index 96% rename from src/__tests__/online/shared-instantiation.spec.js rename to src/__tests__/browserSuites/shared-instantiation.spec.js index 4208932..108ad1f 100644 --- a/src/__tests__/online/shared-instantiation.spec.js +++ b/src/__tests__/browserSuites/shared-instantiation.spec.js @@ -1,8 +1,8 @@ -import { SplitFactory } from '../../index'; -import { settingsValidator } from '../../settings'; +import { SplitFactory } from '../../'; +import { settingsFactory } from '../../settings'; import { url } from '../testUtils'; -const settings = settingsValidator({ +const settings = settingsFactory({ core: { key: 'asd' }, @@ -13,7 +13,7 @@ const settings = settingsValidator({ * @param {boolean} startWithTT whether the SDK settings includes a `core.trafficType` config param or not * @param {boolean} sdkIgnoredTT whether the SDK ignores TT (i.e, clients without bound TT) or not (client with optional bound TT) */ -export default function (startWithTT, sdkIgnoresTT, fetchMock, assert) { +export default function sharedInstantiationSuite(startWithTT, sdkIgnoresTT, fetchMock, assert) { // mocking mySegments endpoints with delays for new clients fetchMock.get(url(settings, '/mySegments/emiliano%2Fsplit.io'), { status: 200, body: { mySegments: [] } }, { delay: 100 }); fetchMock.get(url(settings, '/mySegments/matias%25split.io'), { status: 200, body: { mySegments: [] } }, { delay: 200 }); @@ -42,10 +42,10 @@ export default function (startWithTT, sdkIgnoresTT, fetchMock, assert) { assert.throws(factory.client.bind(factory, null), 'Calling factory.client() with a key parameter that is not a valid key should throw.'); assert.throws(factory.client.bind(factory, {}), 'Calling factory.client() with a key parameter that is not a valid key should throw.'); - if (sdkIgnoresTT) { // Browser JS SDK + if (sdkIgnoresTT) { // JS Browser SDK assert.doesNotThrow(factory.client.bind(factory, 'facundo@split.io', null), 'Calling factory.client() with a traffic type parameter that is not valid shouldn\'t throw because it is ignored.'); assert.doesNotThrow(factory.client.bind(factory, 'facundo@split.io', []), 'Calling factory.client() with a traffic type parameter that is not valid shouldn\'t throw because it is ignored.'); - } else { // Isomorphic JS SDK + } else { // JS SDK (Isomorphic) assert.throws(factory.client.bind(factory, 'facundo@split.io', null), 'Calling factory.client() with a traffic type parameter that is not a valid should throw.'); assert.throws(factory.client.bind(factory, 'facundo@split.io', []), 'Calling factory.client() with a traffic type parameter that is not a valid should throw.'); } @@ -143,9 +143,6 @@ export default function (startWithTT, sdkIgnoresTT, fetchMock, assert) { /* Assert client.track(), no need to wait for ready. */ trackAssertions(); - emmanuelClient.setAttribute('attr', 10); - nicolasClient.setAttributes({ attr: 9 }); - /* Assert getTreatment/s */ const expectControls = ['control', 'control', 'control', 'control']; // If main is not ready and returning controls, they all return controls. @@ -155,6 +152,9 @@ export default function (startWithTT, sdkIgnoresTT, fetchMock, assert) { getTreatmentsAssertions(emilianoClient, expectControls); getTreatmentsAssertions(emmanuelClient, expectControls); + emmanuelClient.setAttribute('attr', 10); + nicolasClient.setAttributes({ attr: 9 }); + // Each client is ready when splits and its segments are fetched mainClient.ready().then(() => { assert.comment('Main instance - facundo@split.io'); diff --git a/src/__tests__/browserSuites/single-sync.spec.js b/src/__tests__/browserSuites/single-sync.spec.js new file mode 100644 index 0000000..f87783d --- /dev/null +++ b/src/__tests__/browserSuites/single-sync.spec.js @@ -0,0 +1,65 @@ +import { SplitFactory } from '../../'; +import { settingsFactory } from '../../settings'; +import { url } from '../testUtils'; + +import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; +import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json'; +import mySegmentsNicolasMock2 from '../mocks/mysegments.nicolas@split.io.json'; + +const baseUrls = { + sdk: 'https://sdk.single-sync/api', + events: 'https://events.single-sync/api', +}; +const userKey = 'nicolas@split.io'; +const config = { + core: { + authorizationKey: '', + key: userKey + }, + scheduler: { + featuresRefreshRate: 0.2, + segmentsRefreshRate: 0.2, + impressionsRefreshRate: 3000, + pushRetryBackoffBase: 0.1 + }, + urls: baseUrls, + userConsent: 'UNKNOWN', + startup: { + eventsFirstPushWindow: 3000 + }, + sync: { + enabled: false + }, + streamingEnabled: true, +}; +const settings = settingsFactory(config); + +export default function singleSync(fetchMock, assert) { + + fetchMock.getOnce(url(settings, '/splitChanges?since=-1'), function () { + assert.pass('first splitChanges fetch'); + return { status: 200, body: splitChangesMock1 }; + }); + fetchMock.getOnce('begin:'+url(settings, '/splitChanges?'), function () { + assert.fail('splitChanges should not be called again'); + return { status: 200, body: splitChangesMock2 }; + }); + + fetchMock.getOnce(url(settings, '/mySegments/nicolas%40split.io'), function () { + assert.pass('first mySegments fetch'); + return { status: 200, body: mySegmentsNicolasMock2 }; + }); + fetchMock.getOnce(url(settings, '/mySegments/nicolas%40split.io'), function () { + assert.fail('mySegments should not be called again'); + return { status: 200, body: mySegmentsNicolasMock2 }; + }); + + let splitio, client = false; + + splitio = SplitFactory(config); + client = splitio.client(); + client.on(client.Event.SDK_READY, () => { + setTimeout(() => client.destroy().then(() => assert.end()), 1000); + }); + +} diff --git a/src/__tests__/online/telemetry.spec.js b/src/__tests__/browserSuites/telemetry.spec.js similarity index 96% rename from src/__tests__/online/telemetry.spec.js rename to src/__tests__/browserSuites/telemetry.spec.js index e6ebbad..80f1ac1 100644 --- a/src/__tests__/online/telemetry.spec.js +++ b/src/__tests__/browserSuites/telemetry.spec.js @@ -69,8 +69,9 @@ export default async function telemetryBrowserSuite(fetchMock, assert) { assert.equal(getLatencyCount(data.mL.tr), 1, 'One latency metric for track'); delete data.hL; delete data.mL; + // @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: 3, iDr: 0, spC: 31, seC: 1, skC: 1, eQ: 1, eD: 0, sE: [], t: [] + mE: {}, hE: { sp: { 500: 1 }, ms: { 500: 1 } }, tR: 0, aR: 0, iQ: 4, iDe: 1, iDr: 0, spC: 31, seC: 1, skC: 1, eQ: 1, eD: 0, sE: [], t: [] }, 'metrics/usage JSON payload should be the expected'); finish.next(); @@ -87,9 +88,10 @@ export default async function telemetryBrowserSuite(fetchMock, assert) { assert.true(data.sL > 0, 'sessionLengthMs must be defined'); delete data.sL; + // @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: 3, iDr: 0, spC: 31, seC: 1, skC: 1, eQ: 1, eD: 0, sE: [], t: [] + tR: 0, aR: 0, iQ: 4, iDe: 1, iDr: 0, spC: 31, seC: 1, skC: 1, eQ: 1, eD: 0, sE: [], t: [] }, '2nd metrics/usage JSON payload should be the expected'); return 200; }); diff --git a/src/__tests__/online/use-beacon-api.debug.spec.js b/src/__tests__/browserSuites/use-beacon-api.debug.spec.js similarity index 96% rename from src/__tests__/online/use-beacon-api.debug.spec.js rename to src/__tests__/browserSuites/use-beacon-api.debug.spec.js index 2b11880..96320f2 100644 --- a/src/__tests__/online/use-beacon-api.debug.spec.js +++ b/src/__tests__/browserSuites/use-beacon-api.debug.spec.js @@ -1,10 +1,11 @@ import sinon from 'sinon'; -import { SplitFactory } from '../../index'; -import { settingsValidator } from '../../settings'; +import { SplitFactory } from '../../'; +import { settingsFactory } from '../../settings'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import mySegmentsFacundo from '../mocks/mysegments.facundo@split.io.json'; import { DEBUG } from '@splitsoftware/splitio-commons/src/utils/constants'; -import { url, triggerPagehideEvent, triggerVisibilitychange } from '../testUtils'; +import { url } from '../testUtils'; +import { triggerPagehideEvent, triggerVisibilitychange } from '../testUtils/browser'; const config = { core: { @@ -21,7 +22,7 @@ const config = { } }; -const settings = settingsValidator(config); +const settings = settingsFactory(config); // Spy calls to Beacon API method let sendBeaconSpyDebug; diff --git a/src/__tests__/online/use-beacon-api.spec.js b/src/__tests__/browserSuites/use-beacon-api.spec.js similarity index 92% rename from src/__tests__/online/use-beacon-api.spec.js rename to src/__tests__/browserSuites/use-beacon-api.spec.js index d09102c..41ab094 100644 --- a/src/__tests__/online/use-beacon-api.spec.js +++ b/src/__tests__/browserSuites/use-beacon-api.spec.js @@ -1,10 +1,11 @@ import sinon from 'sinon'; -import { SplitFactory } from '../../index'; -import { settingsValidator } from '../../settings'; +import { SplitFactory } from '../../'; +import { settingsFactory } from '../../settings'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import mySegmentsFacundo from '../mocks/mysegments.facundo@split.io.json'; -import { url, triggerPagehideEvent, triggerVisibilitychange } from '../testUtils'; +import { url } from '../testUtils'; import { OPTIMIZED } from '@splitsoftware/splitio-commons/src/utils/constants'; +import { triggerPagehideEvent, triggerVisibilitychange } from '../testUtils/browser'; const config = { core: { @@ -18,7 +19,7 @@ const config = { streamingEnabled: false }; -const settings = settingsValidator(config); +const settings = settingsFactory(config); // Spy calls to Beacon API method let sendBeaconSpy; @@ -109,7 +110,8 @@ function beaconApiSendTest(fetchMock, assert) { const splitio = SplitFactory(config); const client = splitio.client(); client.on(client.Event.SDK_READY, () => { - client.getTreatment('hierarchical_splits_test'); + client.getTreatment('hierarchical_splits_test'); // first impression counted in backend + client.getTreatment('hierarchical_splits_test'); // impression counted in sdk client.track('sometraffictype', 'someEvent', 10); // trigger both events inmmediatly, before scheduled push of events and impressions, to assert that beacon requests are not duplicated @@ -173,7 +175,8 @@ function fallbackTest(fetchMock, assert) { }); client.on(client.Event.SDK_READY, () => { - client.getTreatment('hierarchical_splits_test'); + client.getTreatment('hierarchical_splits_test');// first impression counted in backend + client.getTreatment('hierarchical_splits_test');// impression counted in sdk client.track('sometraffictype', 'someEvent', 10); // trigger both events inmmediatly, before scheduled push of events and impressions, to assert that POST requests are not duplicated triggerPagehideEvent(); diff --git a/src/__tests__/online/user-consent.spec.js b/src/__tests__/browserSuites/user-consent.spec.js similarity index 95% rename from src/__tests__/online/user-consent.spec.js rename to src/__tests__/browserSuites/user-consent.spec.js index ef4fbd7..024fcb4 100644 --- a/src/__tests__/online/user-consent.spec.js +++ b/src/__tests__/browserSuites/user-consent.spec.js @@ -1,6 +1,7 @@ import sinon from 'sinon'; -import { SplitFactory } from '../../index'; -import { nearlyEqual, url, triggerPagehideEvent } from '../testUtils'; +import { SplitFactory } from '../../'; +import { triggerPagehideEvent } from '../testUtils/browser'; +import { nearlyEqual, url } from '../testUtils'; const trackedImpressions = []; @@ -157,7 +158,8 @@ export default function userConsent(fetchMock, t) { 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'); + assert.equal(client.getTreatment('always_on'), 'on', 'Impressions queue is full, but submitter is not executed');// First impression counted in backend + assert.equal(client.getTreatment('always_on'), 'on', 'Impressions queue is full, but submitter is not executed');// impression counted in sdk let submitterCalls = 0; const start = Date.now(); diff --git a/src/__tests__/consumer/browser_consumer.spec.js b/src/__tests__/consumer/browser_consumer.spec.js new file mode 100644 index 0000000..1144853 --- /dev/null +++ b/src/__tests__/consumer/browser_consumer.spec.js @@ -0,0 +1,562 @@ +import tape from 'tape-catch'; +import sinon from 'sinon'; +import { inMemoryWrapperFactory } from '@splitsoftware/splitio-commons/src/storages/pluggable/inMemoryWrapper'; +import { SDK_NOT_READY, EXCEPTION } from '@splitsoftware/splitio-commons/src/utils/labels'; +import { truncateTimeFrame } from '@splitsoftware/splitio-commons/src/utils/time'; +import { applyOperations } from './wrapper-commands'; +import { nearlyEqual } from '../testUtils'; +import { version } from '../../../package.json'; + +import { SplitFactory, PluggableStorage } from '../../'; + +const expectedSplitName = 'hierarchical_splits_testing_on'; +const expectedSplitView = { name: 'hierarchical_splits_testing_on', trafficType: 'user', killed: false, changeNumber: 1487277320548, treatments: ['on', 'off'], configs: {} }; + +const wrapperPrefix = 'PLUGGABLE_STORAGE_UT'; +const wrapperInstance = inMemoryWrapperFactory(); +const TOTAL_RAW_IMPRESSIONS = 18; +const TOTAL_EVENTS = 5; +const DEDUPED_IMPRESSIONS = 2; +const timeFrame = Date.now(); + +/** @type SplitIO.IBrowserAsyncSettings */ +const config = { + core: { + authorizationKey: 'SOME API KEY', // in consumer mode, api key is only used to identify the sdk instance + key: 'UT_Segment_member' + }, + mode: 'consumer', + storage: PluggableStorage({ + prefix: wrapperPrefix, + wrapper: wrapperInstance + }), + sync: { + impressionsMode: 'DEBUG' + }, +}; + +tape('Browser Consumer mode with pluggable storage', function (t) { + + t.test('Regular usage - DEBUG strategy', async (assert) => { + + // Load wrapper with data to do the proper tests + await applyOperations(wrapperInstance); + + /** @type SplitIO.ImpressionData[] */ + const impressions = []; + const disconnectSpy = sinon.spy(wrapperInstance, 'disconnect'); + + // Overwrite Math.random to track telemetry + const originalMathRandom = Math.random; Math.random = () => 0.001; + const sdk = SplitFactory({ + ...config, + impressionListener: { + logImpression(data) { impressions.push(data); } + } + }); + Math.random = originalMathRandom; // restore + + const client = sdk.client(); + const otherClient = sdk.client('emi@split.io'); + const manager = sdk.manager(); + + /** Evaluation, track and manager methods before SDK_READY */ + + const getTreatmentResult = client.getTreatment('UT_IN_SEGMENT'); + + const namesResult = manager.names(); + const splitResult = manager.split(expectedSplitName); + const splitsResult = manager.splits(); + + assert.equal(typeof getTreatmentResult.then, 'function', 'GetTreatment calls should always return a promise on Consumer mode.'); + assert.equal(await getTreatmentResult, 'control', 'Evaluations using pluggable storage should be control if initiated before SDK_READY.'); + assert.equal(client.__getStatus().isReadyFromCache, false, 'SDK in consumer mode is not operational inmediatelly'); + assert.equal(client.__getStatus().isReady, false, 'SDK in consumer mode is not operational inmediatelly'); + + const trackResult = otherClient.track('user', 'test.event', 18); + assert.equal(typeof trackResult.then, 'function', 'Track calls should always return a promise on Consumer mode.'); + assert.true(await trackResult, 'If the wrapper operation success to queue the event, the promise will resolve to true'); + + // Manager methods + assert.deepEqual(await namesResult, [], 'manager `names` method returns an empty list of split names if called before SDK_READY or wrapper operation fail'); + assert.deepEqual(await splitResult, null, 'manager `split` method returns a null split view if called before SDK_READY or wrapper operation fail'); + assert.deepEqual(await splitsResult, [], 'manager `splits` method returns an empty list of split views if called before SDK_READY or wrapper operation fail'); + + /** Evaluation, track and manager methods on SDK_READY */ + + await client.ready(); + await otherClient.ready(); // waiting to avoid 'control' with label 'sdk not ready' + + assert.equal(await client.getTreatment('UT_IN_SEGMENT'), 'on', '`getTreatment` evaluation using pluggable storage should be correct.'); + assert.equal((await otherClient.getTreatmentWithConfig('UT_IN_SEGMENT')).treatment, 'off', '`getTreatmentWithConfig` evaluation using pluggable storage should be correct.'); + + assert.equal((await client.getTreatments(['UT_NOT_IN_SEGMENT']))['UT_NOT_IN_SEGMENT'], 'off', '`getTreatments` evaluation using pluggable storage should be correct.'); + assert.equal((await otherClient.getTreatmentsWithConfig(['UT_NOT_IN_SEGMENT']))['UT_NOT_IN_SEGMENT'].treatment, 'on', '`getTreatmentsWithConfig` evaluation using pluggable storage should be correct.'); + + client.setAttribute('permissions', ['not_matching']); + assert.equal(await client.getTreatment('UT_SET_MATCHER', { + permissions: ['admin'] + }), 'on', 'Evaluations using pluggable storage should be correct.'); + client.setAttributes({ permissions: ['admin'] }); + assert.equal(await client.getTreatment('UT_SET_MATCHER', { + permissions: ['not_matching'] + }), 'off', 'Evaluations using pluggable storage should be correct.'); + assert.equal(await client.getTreatment('UT_SET_MATCHER'), 'on', 'Evaluations using pluggable storage and attributes binding should be correct.'); + + client.setAttribute('permissions', ['not_matching']); + assert.equal(await client.getTreatment('UT_NOT_SET_MATCHER', { + permissions: ['create'] + }), 'off', 'Evaluations using pluggable storage should be correct.'); + assert.equal(await client.getTreatment('UT_NOT_SET_MATCHER'), 'on', 'Evaluations using pluggable storage should be correct.'); + assert.deepEqual(await client.getTreatmentWithConfig('UT_NOT_SET_MATCHER'), { + treatment: 'on', + config: null + }, 'Evaluations using pluggable storage should be correct, including configs.'); + client.clearAttributes(); + assert.deepEqual(await client.getTreatmentWithConfig('always-o.n-with-config'), { + treatment: 'o.n', + config: '{"color":"brown"}' + }, 'Evaluations using pluggable storage should be correct, including configs.'); + + assert.equal(await client.getTreatment('always-on'), 'on', 'Evaluations using pluggable storage should be correct.'); + + // Below splits were added manually. + // 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('hierarchical_splits_testing_on'), 'on', 'Evaluations using pluggable storage should be correct.'); + 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.'); + + 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.'); + + assert.true(await client.track('user', 'test.event', 18), 'If the event was succesfully queued the promise will resolve to true'); + assert.false(await client.track(), 'If the event was NOT succesfully queued the promise will resolve to false'); + + // 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.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.deepEqual(splitViews.find(splitView => splitView.name === expectedSplitName), expectedSplitView, 'manager `split` method returns the split view of the given split name asynchronously'); + + // New shared client created + const newClient = sdk.client('other'); + newClient.track('user', 'test.event', 18).then(result => assert.true(result, 'Track should attempt to track, whether SDK_READY has been emitted or not.')); + newClient.getTreatment('UT_IN_SEGMENT').then(result => assert.equal(result, 'control', '`getTreatment` evaluation is control if shared client is not ready yet.')); + + await newClient.ready(); + assert.true(await newClient.track('user', 'test.event', 18), 'Track should attempt to track, whether SDK_READY has been emitted or not.'); + assert.equal((await newClient.getTreatment('UT_IN_SEGMENT')), 'off', '`getTreatment` evaluation using pluggable storage should be correct.'); + + await client.ready(); // promise already resolved + await newClient.destroy(); + await otherClient.destroy(); + await client.destroy(); + + assert.equal(disconnectSpy.callCount, 1, 'Wrapper disconnect method should be called only once, when the main client is destroyed'); + + // Validate stored impressions and events + const trackedImpressions = await wrapperInstance.popItems('PLUGGABLE_STORAGE_UT.SPLITIO.impressions', await wrapperInstance.getItemsCount('PLUGGABLE_STORAGE_UT.SPLITIO.impressions')); + const trackedEvents = await wrapperInstance.popItems('PLUGGABLE_STORAGE_UT.SPLITIO.events', await wrapperInstance.getItemsCount('PLUGGABLE_STORAGE_UT.SPLITIO.events')); + assert.equal(trackedImpressions.length, TOTAL_RAW_IMPRESSIONS, 'Tracked impressions should be present in the external storage'); + assert.equal(trackedEvents.length, TOTAL_EVENTS, 'Tracked events should be present in the external storage'); + + // Validate stored telemetry + const latencies = await wrapperInstance.getKeysByPrefix(`PLUGGABLE_STORAGE_UT.SPLITIO.telemetry.latencies::browserjs-${version}/unknown/unknown`); + assert.true(latencies.length > 0, 'There are stored latencies'); + const exceptions = await wrapperInstance.getKeysByPrefix(`PLUGGABLE_STORAGE_UT.SPLITIO.telemetry.exceptions::browserjs-${version}/unknown/unknown`); + assert.true(exceptions.length === 0, 'There aren\'t stored exceptions'); + const configValue = await wrapperInstance.get(`PLUGGABLE_STORAGE_UT.SPLITIO.telemetry.init::browserjs-${version}/unknown/unknown`); + assert.deepEqual(JSON.parse(configValue), { oM: 1, st: 'pluggable', aF: 1, rF: 0 }, 'There is stored telemetry config'); + + // Assert impressionsListener + setTimeout(() => { + assert.equal(impressions.length, TOTAL_RAW_IMPRESSIONS, 'Each evaluation has its corresponting impression'); + assert.equal(impressions[0].impression.label, SDK_NOT_READY, 'The first impression is control with label "sdk not ready"'); + + assert.end(); + }); + }); + + t.test('Regular usage - OPTIMIZED strategy', async (assert) => { + const wrapperInstance = inMemoryWrapperFactory(); + // Load wrapper with data to do the proper tests + await applyOperations(wrapperInstance); + + /** @type SplitIO.ImpressionData[] */ + const impressions = []; + const disconnectSpy = sinon.spy(wrapperInstance, 'disconnect'); + + // Overwrite Math.random to track telemetry + const originalMathRandom = Math.random; Math.random = () => 0.5; + const sdk = SplitFactory({ + ...config, + storage: PluggableStorage({ + prefix: wrapperPrefix, + wrapper: wrapperInstance + }), + sync: { + impressionsMode: 'OPTIMIZED' + }, + impressionListener: { + logImpression(data) { impressions.push(data); } + } + }); + Math.random = originalMathRandom; // restore + + const client = sdk.client(); + const otherClient = sdk.client('emi@split.io'); + + /** Evaluation and track methods before SDK_READY */ + + const getTreatmentResult = client.getTreatment('UT_IN_SEGMENT'); + + assert.equal(typeof getTreatmentResult.then, 'function', 'GetTreatment calls should always return a promise on Consumer mode.'); + assert.equal(await getTreatmentResult, 'control', 'Evaluations using pluggable storage should be control if initiated before SDK_READY.'); + assert.equal(client.__getStatus().isReadyFromCache, false, 'SDK in consumer mode is not operational inmediatelly'); + assert.equal(client.__getStatus().isReady, false, 'SDK in consumer mode is not operational inmediatelly'); + + const trackResult = otherClient.track('user', 'test.event', 18); + assert.equal(typeof trackResult.then, 'function', 'Track calls should always return a promise on Consumer mode.'); + assert.true(await trackResult, 'If the wrapper operation success to queue the event, the promise will resolve to true'); + + /** Evaluation and track methods on SDK_READY with OPTIMIZED impressions mode */ + + await client.ready(); + await otherClient.ready(); // waiting to avoid 'control' with label 'sdk not ready' + + assert.equal(await client.getTreatment('UT_IN_SEGMENT'), 'on', '`getTreatment` evaluation using pluggable storage should be correct.'); + assert.equal((await otherClient.getTreatmentWithConfig('UT_IN_SEGMENT')).treatment, 'off', '`getTreatmentWithConfig` evaluation using pluggable storage should be correct.'); + + assert.equal((await client.getTreatments(['UT_NOT_IN_SEGMENT']))['UT_NOT_IN_SEGMENT'], 'off', '`getTreatments` evaluation using pluggable storage should be correct.'); + assert.equal((await otherClient.getTreatmentsWithConfig(['UT_NOT_IN_SEGMENT']))['UT_NOT_IN_SEGMENT'].treatment, 'on', '`getTreatmentsWithConfig` evaluation using pluggable storage should be correct.'); + + client.setAttribute('permissions', ['not_matching']); + assert.equal(await client.getTreatment('UT_SET_MATCHER', { + permissions: ['admin'] + }), 'on', 'Evaluations using pluggable storage should be correct.'); + client.setAttributes({ permissions: ['admin'] }); + assert.equal(await client.getTreatment('UT_SET_MATCHER', { + permissions: ['not_matching'] + }), 'off', 'Evaluations using pluggable storage should be correct.'); + assert.equal(await client.getTreatment('UT_SET_MATCHER'), 'on', 'Evaluations using pluggable storage and attributes binding should be correct.'); + + client.setAttribute('permissions', ['not_matching']); + assert.equal(await client.getTreatment('UT_NOT_SET_MATCHER', { + permissions: ['create'] + }), 'off', 'Evaluations using pluggable storage should be correct.'); + assert.equal(await client.getTreatment('UT_NOT_SET_MATCHER'), 'on', 'Evaluations using pluggable storage should be correct.'); + assert.deepEqual(await client.getTreatmentWithConfig('UT_NOT_SET_MATCHER'), { + treatment: 'on', + config: null + }, 'Evaluations using pluggable storage should be correct, including configs.'); + client.clearAttributes(); + assert.deepEqual(await client.getTreatmentWithConfig('always-o.n-with-config'), { + treatment: 'o.n', + config: '{"color":"brown"}' + }, 'Evaluations using pluggable storage should be correct, including configs.'); + + assert.equal(await client.getTreatment('always-on'), 'on', 'Evaluations using pluggable storage should be correct.'); + + // Below splits were added manually. + // 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('hierarchical_splits_testing_on'), 'on', 'Evaluations using pluggable storage should be correct.'); + 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.'); + + 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.'); + + assert.true(await client.track('user', 'test.event', 18), 'If the event was succesfully queued the promise will resolve to true'); + assert.false(await client.track(), 'If the event was NOT succesfully queued the promise will resolve to false'); + + // New shared client created + const newClient = sdk.client('other'); + newClient.track('user', 'test.event', 18).then(result => assert.true(result, 'Track should attempt to track, whether SDK_READY has been emitted or not.')); + newClient.getTreatment('UT_IN_SEGMENT').then(result => assert.equal(result, 'control', '`getTreatment` evaluation is control if shared client is not ready yet.')); + + await newClient.ready(); + assert.true(await newClient.track('user', 'test.event', 18), 'Track should attempt to track, whether SDK_READY has been emitted or not.'); + assert.equal((await newClient.getTreatment('UT_IN_SEGMENT')), 'off', '`getTreatment` evaluation using pluggable storage should be correct.'); + + await client.ready(); // promise already resolved + await newClient.destroy(); + await otherClient.destroy(); + await client.destroy(); + + assert.equal(disconnectSpy.callCount, 1, 'Wrapper disconnect method should be called only once, when the main client is destroyed'); + + // Validate stored impressions and events + const trackedImpressions = await wrapperInstance.popItems('PLUGGABLE_STORAGE_UT.SPLITIO.impressions', await wrapperInstance.getItemsCount('PLUGGABLE_STORAGE_UT.SPLITIO.impressions')); + const trackedEvents = await wrapperInstance.popItems('PLUGGABLE_STORAGE_UT.SPLITIO.events', await wrapperInstance.getItemsCount('PLUGGABLE_STORAGE_UT.SPLITIO.events')); + assert.equal(trackedImpressions.length, TOTAL_RAW_IMPRESSIONS - DEDUPED_IMPRESSIONS, 'Tracked impressions should be present in the external storage'); + assert.equal(trackedEvents.length, TOTAL_EVENTS, 'Tracked events should be present in the external storage'); + + // Validate impression counts + const trackedImpressionCounts = await wrapperInstance.getKeysByPrefix('PLUGGABLE_STORAGE_UT.SPLITIO.impressions.count') + .then(impressionCountsKeys => Promise.all(impressionCountsKeys.map(key => wrapperInstance.get(key).then(count => ([key, count]))))); + const expectedImpressionCount = [ + [`${wrapperPrefix}.SPLITIO.impressions.count::UT_SET_MATCHER::${truncateTimeFrame(timeFrame)}`, '1'], + [`${wrapperPrefix}.SPLITIO.impressions.count::UT_NOT_SET_MATCHER::${truncateTimeFrame(timeFrame)}`, '1'], + ]; + assert.deepEqual(trackedImpressionCounts, expectedImpressionCount, 'Impression counts should be present in the external storage'); + + // Assert impressionsListener + setTimeout(() => { + assert.equal(impressions.length, TOTAL_RAW_IMPRESSIONS, 'Each evaluation has its corresponting impression'); + assert.equal(impressions[0].impression.label, SDK_NOT_READY, 'The first impression is control with label "sdk not ready"'); + + assert.end(); + }); + }); + + t.test('Regular usage - NONE strategy', async (assert) => { + const wrapperInstance = inMemoryWrapperFactory(); + // Load wrapper with data to do the proper tests + await applyOperations(wrapperInstance); + + /** @type SplitIO.ImpressionData[] */ + const impressions = []; + const disconnectSpy = sinon.spy(wrapperInstance, 'disconnect'); + + // Overwrite Math.random to not track telemetry + const originalMathRandom = Math.random; Math.random = () => 0.5; + const sdk = SplitFactory({ + ...config, + storage: PluggableStorage({ + prefix: wrapperPrefix, + wrapper: wrapperInstance + }), + sync: { + impressionsMode: 'NONE' + }, + impressionListener: { + logImpression(data) { impressions.push(data); } + } + }); + Math.random = originalMathRandom; // restore + + const client = sdk.client(); + const otherClient = sdk.client('emi@split.io'); + + /** Evaluation and track methods before SDK_READY */ + + const getTreatmentResult = client.getTreatment('UT_IN_SEGMENT'); + + assert.equal(typeof getTreatmentResult.then, 'function', 'GetTreatment calls should always return a promise on Consumer mode.'); + assert.equal(await getTreatmentResult, 'control', 'Evaluations using pluggable storage should be control if initiated before SDK_READY.'); + assert.equal(client.__getStatus().isReadyFromCache, false, 'SDK in consumer mode is not operational inmediatelly'); + assert.equal(client.__getStatus().isReady, false, 'SDK in consumer mode is not operational inmediatelly'); + + const trackResult = otherClient.track('user', 'test.event', 18); + assert.equal(typeof trackResult.then, 'function', 'Track calls should always return a promise on Consumer mode.'); + assert.true(await trackResult, 'If the wrapper operation success to queue the event, the promise will resolve to true'); + + /** Evaluation and track methods on SDK_READY with OPTIMIZED impressions mode */ + + await client.ready(); + await otherClient.ready(); // waiting to avoid 'control' with label 'sdk not ready' + + assert.equal(await client.getTreatment('UT_IN_SEGMENT'), 'on', '`getTreatment` evaluation using pluggable storage should be correct.'); + assert.equal((await otherClient.getTreatmentWithConfig('UT_IN_SEGMENT')).treatment, 'off', '`getTreatmentWithConfig` evaluation using pluggable storage should be correct.'); + + assert.equal((await client.getTreatments(['UT_NOT_IN_SEGMENT']))['UT_NOT_IN_SEGMENT'], 'off', '`getTreatments` evaluation using pluggable storage should be correct.'); + assert.equal((await otherClient.getTreatmentsWithConfig(['UT_NOT_IN_SEGMENT']))['UT_NOT_IN_SEGMENT'].treatment, 'on', '`getTreatmentsWithConfig` evaluation using pluggable storage should be correct.'); + + client.setAttribute('permissions', ['not_matching']); + assert.equal(await client.getTreatment('UT_SET_MATCHER', { + permissions: ['admin'] + }), 'on', 'Evaluations using pluggable storage should be correct.'); + client.setAttributes({ permissions: ['admin'] }); + assert.equal(await client.getTreatment('UT_SET_MATCHER', { + permissions: ['not_matching'] + }), 'off', 'Evaluations using pluggable storage should be correct.'); + assert.equal(await client.getTreatment('UT_SET_MATCHER'), 'on', 'Evaluations using pluggable storage and attributes binding should be correct.'); + + client.setAttribute('permissions', ['not_matching']); + assert.equal(await client.getTreatment('UT_NOT_SET_MATCHER', { + permissions: ['create'] + }), 'off', 'Evaluations using pluggable storage should be correct.'); + assert.equal(await client.getTreatment('UT_NOT_SET_MATCHER'), 'on', 'Evaluations using pluggable storage should be correct.'); + assert.deepEqual(await client.getTreatmentWithConfig('UT_NOT_SET_MATCHER'), { + treatment: 'on', + config: null + }, 'Evaluations using pluggable storage should be correct, including configs.'); + client.clearAttributes(); + assert.deepEqual(await client.getTreatmentWithConfig('always-o.n-with-config'), { + treatment: 'o.n', + config: '{"color":"brown"}' + }, 'Evaluations using pluggable storage should be correct, including configs.'); + + assert.equal(await client.getTreatment('always-on'), 'on', 'Evaluations using pluggable storage should be correct.'); + + // Below splits were added manually. + // 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('hierarchical_splits_testing_on'), 'on', 'Evaluations using pluggable storage should be correct.'); + 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.'); + + 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.'); + + assert.true(await client.track('user', 'test.event', 18), 'If the event was succesfully queued the promise will resolve to true'); + assert.false(await client.track(), 'If the event was NOT succesfully queued the promise will resolve to false'); + + // New shared client created + const newClient = sdk.client('other'); + newClient.track('user', 'test.event', 18).then(result => assert.true(result, 'Track should attempt to track, whether SDK_READY has been emitted or not.')); + newClient.getTreatment('UT_IN_SEGMENT').then(result => assert.equal(result, 'control', '`getTreatment` evaluation is control if shared client is not ready yet.')); + + await newClient.ready(); + assert.true(await newClient.track('user', 'test.event', 18), 'Track should attempt to track, whether SDK_READY has been emitted or not.'); + assert.equal((await newClient.getTreatment('UT_IN_SEGMENT')), 'off', '`getTreatment` evaluation using pluggable storage should be correct.'); + + await client.ready(); // promise already resolved + await newClient.destroy(); + await otherClient.destroy(); + await client.destroy(); + + assert.equal(disconnectSpy.callCount, 1, 'Wrapper disconnect method should be called only once, when the main client is destroyed'); + + // Validate stored events and no impressions + const trackedImpressions = await wrapperInstance.popItems('PLUGGABLE_STORAGE_UT.SPLITIO.impressions', await wrapperInstance.getItemsCount('PLUGGABLE_STORAGE_UT.SPLITIO.impressions')); + const trackedEvents = await wrapperInstance.popItems('PLUGGABLE_STORAGE_UT.SPLITIO.events', await wrapperInstance.getItemsCount('PLUGGABLE_STORAGE_UT.SPLITIO.events')); + assert.equal(trackedImpressions.length, 0, 'No impressions are tracked in NONE impressions mode'); + assert.equal(trackedEvents.length, TOTAL_EVENTS, 'Tracked events should be present in the external storage'); + + // Validate impression counts + const trackedImpressionCounts = await wrapperInstance.getKeysByPrefix('PLUGGABLE_STORAGE_UT.SPLITIO.impressions.count') + .then(impressionCountsKeys => Promise.all(impressionCountsKeys.map(key => wrapperInstance.get(key).then(count => ([key, count]))))); + const expectedImpressionCount = [ + [`${wrapperPrefix}.SPLITIO.impressions.count::UT_IN_SEGMENT::${truncateTimeFrame(timeFrame)}`, '5'], + [`${wrapperPrefix}.SPLITIO.impressions.count::UT_NOT_IN_SEGMENT::${truncateTimeFrame(timeFrame)}`, '2'], + [`${wrapperPrefix}.SPLITIO.impressions.count::UT_SET_MATCHER::${truncateTimeFrame(timeFrame)}`, '3'], + [`${wrapperPrefix}.SPLITIO.impressions.count::UT_NOT_SET_MATCHER::${truncateTimeFrame(timeFrame)}`, '3'], + [`${wrapperPrefix}.SPLITIO.impressions.count::always-o.n-with-config::${truncateTimeFrame(timeFrame)}`, '1'], + [`${wrapperPrefix}.SPLITIO.impressions.count::always-on::${truncateTimeFrame(timeFrame)}`, '1'], + [`${wrapperPrefix}.SPLITIO.impressions.count::hierarchical_splits_testing_on::${truncateTimeFrame(timeFrame)}`, '1'], + [`${wrapperPrefix}.SPLITIO.impressions.count::hierarchical_splits_testing_off::${truncateTimeFrame(timeFrame)}`, '1'], + [`${wrapperPrefix}.SPLITIO.impressions.count::hierarchical_splits_testing_on_negated::${truncateTimeFrame(timeFrame)}`, '1'] + ]; + assert.deepEqual(trackedImpressionCounts, expectedImpressionCount, 'Impression counts should be present in the external storage'); + + // Validate unique keys + const trackedUniqueKeys = await wrapperInstance.popItems('PLUGGABLE_STORAGE_UT.SPLITIO.uniquekeys', await wrapperInstance.getItemsCount('PLUGGABLE_STORAGE_UT.SPLITIO.uniquekeys')); + const expectedUniqueKeys = [ + JSON.stringify({ 'f': 'UT_IN_SEGMENT', 'ks': ['UT_Segment_member', 'emi@split.io', 'other'] }), + JSON.stringify({ 'f': 'UT_NOT_IN_SEGMENT', 'ks': ['UT_Segment_member', 'emi@split.io'] }), + JSON.stringify({ 'f': 'UT_SET_MATCHER', 'ks': ['UT_Segment_member'] }), + JSON.stringify({ 'f': 'UT_NOT_SET_MATCHER', 'ks': ['UT_Segment_member'] }), + JSON.stringify({ 'f': 'always-o.n-with-config', 'ks': ['UT_Segment_member'] }), + JSON.stringify({ 'f': 'always-on', 'ks': ['UT_Segment_member'] }), + JSON.stringify({ 'f': 'hierarchical_splits_testing_on', 'ks': ['UT_Segment_member'] }), + JSON.stringify({ 'f': 'hierarchical_splits_testing_off', 'ks': ['UT_Segment_member'] }), + JSON.stringify({ 'f': 'hierarchical_splits_testing_on_negated', 'ks': ['UT_Segment_member'] }), + ]; + assert.deepEqual(trackedUniqueKeys, expectedUniqueKeys); + + // Assert impressionsListener + setTimeout(() => { + assert.equal(impressions.length, TOTAL_RAW_IMPRESSIONS, 'Each evaluation has its corresponting impression'); + assert.equal(impressions[0].impression.label, SDK_NOT_READY, 'The first impression is control with label "sdk not ready"'); + + assert.end(); + }); + }); + + t.test('Connection timeout and then ready', assert => { + const connDelay = 110; + const readyTimeout = 0.1; // 100 millis + const configWithShortTimeout = { ...config, startup: { readyTimeout } }; + wrapperInstance._setConnDelay(connDelay); + const sdk = SplitFactory(configWithShortTimeout); + wrapperInstance._setConnDelay(0); // restore + const client = sdk.client(); + const otherClient = sdk.client('other'); + + const start = Date.now(); + assert.plan(12); + + client.getTreatment('always-on').then(treatment => { + assert.equal(treatment, 'control', 'Evaluations using pluggable storage should be control if SDK is not ready'); + }); + client.track('user', 'test.event', 18).then(result => { + assert.true(result, 'If the wrapper operation success to queue the event, the promise will resolve to true, even if the SDK is not ready'); + }); + + // SDK_READY_TIMED_OUT event must be emitted after 100 millis + [client, otherClient].forEach(client => client.on(client.Event.SDK_READY_TIMED_OUT, () => { + const delay = Date.now() - start; + assert.true(nearlyEqual(delay, readyTimeout * 1000), 'SDK_READY_TIMED_OUT event must be emitted after 100 millis'); + })); + + // Also, ready promise must be rejected after 100 millis + [client, otherClient].forEach(client => client.ready().catch(() => { + const delay = Date.now() - start; + assert.true(nearlyEqual(delay, readyTimeout * 1000), 'Ready promise must be rejected after 100 millis'); + })); + + // subscribe to SDK_READY event to assert regular usage + client.on(client.Event.SDK_READY, async () => { + const delay = Date.now() - start; + assert.true(nearlyEqual(delay, connDelay), 'SDK_READY event must be emitted once wrapper is connected'); + + await client.ready(); + assert.pass('Ready promise is resolved once SDK_READY is emitted'); + + // some asserts to test regular usage + assert.equal(await client.getTreatment('UT_IN_SEGMENT'), 'on', 'Evaluations using pluggable storage should be correct.'); + assert.equal(await otherClient.getTreatment('UT_IN_SEGMENT'), 'off', 'Evaluations using pluggable storage should be correct.'); + assert.true(await client.track('user', 'test.event', 18), 'If the event was succesfully queued the promise will resolve to true'); + assert.false(await client.track(), 'If the event was NOT succesfully queued the promise will resolve to false'); + + await client.destroy(); + assert.end(); + }); + + }); + + t.test('Wrapper errors', async (assert) => { + const impressions = []; + + const sdk = SplitFactory({ + ...config, + impressionListener: { + logImpression(data) { impressions.push(data); } + } + }); + const client = sdk.client(); + const manager = sdk.manager(); + await client.ready(); + assert.equal(await client.getTreatment('UT_IN_SEGMENT'), 'on', '`getTreatment` evaluation correct.'); + assert.true(await client.track('user', 'test.event', 18), 'Event queued'); + + // Stubbing wrapper methods to make client and manager methods fail + const methods = ['pushItems', 'get', 'getKeysByPrefix']; + methods.forEach(method => sinon.stub(wrapperInstance, method).callsFake(() => { Promise.reject(); })); + + assert.equal(await client.getTreatment('UT_IN_SEGMENT'), 'control', '`getTreatment` evaluation correct.'); + assert.false(await client.track('user', 'test.event', 18), 'If the event failed to be queued due to a wrapper operation failure, the promise will resolve to false'); + assert.deepEqual(await manager.names(), [], 'manager `names` method returns an empty list of split names if called before SDK_READY or wrapper operation fail'); + assert.deepEqual(await manager.split(expectedSplitName), null, 'manager `split` method returns a null split view if called before SDK_READY or wrapper operation fail'); + assert.deepEqual(await manager.splits(), [], 'manager `splits` method returns an empty list of split views if called before SDK_READY or wrapper operation fail'); + + // Restore wrapper + methods.forEach(method => wrapperInstance[method].restore()); + + await client.destroy(); + + // Assert impressionsListener + setTimeout(() => { + assert.equal(impressions.length, 2, 'Each evaluation has its corresponting impression'); + assert.equal(impressions[1].impression.label, EXCEPTION, 'The last impression is control with label "exception"'); + + assert.end(); + }); + + }); + + +}); diff --git a/src/__tests__/consumerMode/browser_consumer_partial.spec.js b/src/__tests__/consumer/browser_consumer_partial.spec.js similarity index 52% rename from src/__tests__/consumerMode/browser_consumer_partial.spec.js rename to src/__tests__/consumer/browser_consumer_partial.spec.js index c3fd126..5c705ab 100644 --- a/src/__tests__/consumerMode/browser_consumer_partial.spec.js +++ b/src/__tests__/consumer/browser_consumer_partial.spec.js @@ -7,13 +7,13 @@ import { SDK_NOT_READY } from '@splitsoftware/splitio-commons/src/utils/labels'; import { url } from '../testUtils'; import { applyOperations } from './wrapper-commands'; -import { SplitFactory, PluggableStorage } from '../../index'; +import { SplitFactory, PluggableStorage } from '../../'; const expectedSplitName = 'hierarchical_splits_testing_on'; const expectedSplitView = { name: 'hierarchical_splits_testing_on', trafficType: 'user', killed: false, changeNumber: 1487277320548, treatments: ['on', 'off'], configs: {} }; const wrapperPrefix = 'PLUGGABLE_STORAGE_UT'; -const wrapperInstance = inMemoryWrapperFactory(0); +const wrapperInstance = inMemoryWrapperFactory(); const TOTAL_RAW_IMPRESSIONS = 17; const TOTAL_EVENTS = 5; @@ -33,7 +33,8 @@ const config = { // }, urls: { sdk: 'https://sdk.baseurl/impressionsSuite', - events: 'https://events.baseurl/impressionsSuite' + events: 'https://events.baseurl/impressionsSuite', + telemetry: 'https://telemetry.baseurl/impressionsSuite' } }; @@ -43,7 +44,7 @@ tape('Browser Consumer Partial mode with pluggable storage', function (t) { * We only validates regular usage. * Corner cases, such as wrapper connection timeout and operation errors, are validated in `browser_consumer.spec.js` */ - t.test('Regular usage', async (assert) => { + t.test('Regular usage - OPTIMIZED strategy', async (assert) => { fetchMock.postOnce(url(config, '/testImpressions/bulk'), (url, req) => { assert.equal(req.headers.SplitSDKImpressionsMode, OPTIMIZED, 'Impressions mode is OPTIMIZED by default'); @@ -55,6 +56,8 @@ tape('Browser Consumer Partial mode with pluggable storage', function (t) { }); fetchMock.postOnce(url(config, '/testImpressions/count'), 200); + fetchMock.postOnce(url(config, '/v1/metrics/config'), 200); + fetchMock.postOnce(url(config, '/v1/metrics/usage'), 200); fetchMock.postOnce(url(config, '/events/bulk'), (url, req) => { const resp = JSON.parse(req.body); @@ -69,13 +72,16 @@ tape('Browser Consumer Partial mode with pluggable storage', function (t) { const impressions = []; const disconnectSpy = sinon.spy(wrapperInstance, 'disconnect'); - const expectedConfig = '{"color":"brown"}'; + // Overwrite Math.random to track telemetry + const originalMathRandom = Math.random; Math.random = () => 0.001; const sdk = SplitFactory({ ...config, impressionListener: { logImpression(data) { impressions.push(data); } } }); + Math.random = originalMathRandom; // restore + const client = sdk.client(); const otherClient = sdk.client('emi@split.io'); const manager = sdk.manager(); @@ -89,7 +95,6 @@ tape('Browser Consumer Partial mode with pluggable storage', function (t) { const splitsResult = manager.splits(); assert.equal(typeof getTreatmentResult.then, 'function', 'GetTreatment calls should always return a promise on Consumer mode.'); - // @TODO caveat: if wrapper.connect is resolved before wrapper operations, next evaluation might be different than control assert.equal(await getTreatmentResult, 'control', 'Evaluations using pluggable storage should be control if initiated before SDK_READY.'); assert.equal(client.__getStatus().isReadyFromCache, false, 'SDK in consumer mode is not operational inmediatelly'); assert.equal(client.__getStatus().isReady, false, 'SDK in consumer mode is not operational inmediatelly'); @@ -135,7 +140,7 @@ tape('Browser Consumer Partial mode with pluggable storage', function (t) { }, 'Evaluations using pluggable storage should be correct, including configs.'); assert.deepEqual(await client.getTreatmentWithConfig('always-o.n-with-config'), { treatment: 'o.n', - config: expectedConfig + config: '{"color":"brown"}' }, 'Evaluations using pluggable storage should be correct, including configs.'); assert.equal(await client.getTreatment('always-on'), 'on', 'Evaluations using pluggable storage should be correct.'); @@ -163,8 +168,157 @@ tape('Browser Consumer Partial mode with pluggable storage', function (t) { // New shared client created const newClient = sdk.client('other'); + newClient.track('user', 'test.event', 18).then(result => assert.true(result, 'Track should attempt to track, whether SDK_READY has been emitted or not.')); + newClient.getTreatment('UT_IN_SEGMENT').then(result => assert.equal(result, 'control', '`getTreatment` evaluation is control if shared client is not ready yet.')); + + await newClient.ready(); assert.true(await newClient.track('user', 'test.event', 18), 'Track should attempt to track, whether SDK_READY has been emitted or not.'); - assert.equal((await newClient.getTreatment('UT_IN_SEGMENT')), 'control', '`getTreatment` evaluation is control if shared client is not ready yet.'); + assert.equal((await newClient.getTreatment('UT_IN_SEGMENT')), 'off', '`getTreatment` evaluation using pluggable storage should be correct.'); + + await client.ready(); // promise already resolved + await newClient.destroy(); + await otherClient.destroy(); + + assert.equal(fetchMock.calls().length, 1, 'fetch has been called once for posting telemetry config stats'); + await client.destroy(); + assert.equal(fetchMock.calls().length, 5, 'fetch has been called 5 times after main client is destroyed, for posting events, impressions, impression counts, telemetry config and usage stats'); + + assert.equal(disconnectSpy.callCount, 1, 'Wrapper disconnect method should be called only once, when the main client is destroyed'); + + // Assert impressionsListener + setTimeout(() => { + assert.equal(impressions.length, TOTAL_RAW_IMPRESSIONS, 'Each evaluation has its corresponting impression'); + assert.equal(impressions[0].impression.label, SDK_NOT_READY, 'The first impression is control with label "sdk not ready"'); + + assert.end(); + }); + }); + + t.test('Regular usage - NONE strategy', async (assert) => { + + fetchMock.postOnce(url(config, '/testImpressions/count'), 200); + fetchMock.postOnce(url(config, '/v1/keys/cs'), (url, req) => { + const data = JSON.parse(req.body); + + assert.deepEqual(data, { + keys: [ + { + k: 'UT_Segment_member', + fs: ['UT_IN_SEGMENT', 'UT_NOT_IN_SEGMENT', 'UT_SET_MATCHER', 'UT_NOT_SET_MATCHER', 'always-o.n-with-config', 'always-on', 'hierarchical_splits_testing_on', 'hierarchical_splits_testing_off', 'hierarchical_splits_testing_on_negated'] + }, + { + k: 'emi@split.io', + fs: ['UT_IN_SEGMENT', 'UT_NOT_IN_SEGMENT'] + }, + { + k: 'other', + fs: ['UT_IN_SEGMENT'] + }] + }, 'We performed evaluations for 3 keys, so we should have 3 items total.'); + + return 200; + }); + + fetchMock.postOnce(url(config, '/events/bulk'), (url, req) => { + const resp = JSON.parse(req.body); + assert.equal(resp.length, TOTAL_EVENTS, 'All successfully tracked events were sent'); + return 200; + }); + + // Load wrapper with data to do the proper tests + const wrapperInstance = inMemoryWrapperFactory(); + await applyOperations(wrapperInstance); + + /** @type SplitIO.ImpressionData[] */ + const impressions = []; + const disconnectSpy = sinon.spy(wrapperInstance, 'disconnect'); + + // Overwrite Math.random to NOT track telemetry + const originalMathRandom = Math.random; Math.random = () => 0.5; + const sdk = SplitFactory({ + ...config, + storage: PluggableStorage({ + prefix: wrapperPrefix, + wrapper: wrapperInstance + }), + sync: { + impressionsMode: 'NONE' + }, + impressionListener: { + logImpression(data) { impressions.push(data); } + } + }); + Math.random = originalMathRandom; // restore + + const client = sdk.client(); + const otherClient = sdk.client('emi@split.io'); + + /** Evaluation, track and manager methods before SDK_READY */ + + const getTreatmentResult = client.getTreatment('UT_IN_SEGMENT'); + + assert.equal(typeof getTreatmentResult.then, 'function', 'GetTreatment calls should always return a promise on Consumer mode.'); + assert.equal(await getTreatmentResult, 'control', 'Evaluations using pluggable storage should be control if initiated before SDK_READY.'); + assert.equal(client.__getStatus().isReadyFromCache, false, 'SDK in consumer mode is not operational inmediatelly'); + assert.equal(client.__getStatus().isReady, false, 'SDK in consumer mode is not operational inmediatelly'); + + const trackResult = otherClient.track('user', 'test.event', 18); + assert.equal(typeof trackResult.then, 'function', 'Track calls should always return a promise on Consumer mode.'); + assert.true(await trackResult, 'If the wrapper operation success to queue the event, the promise will resolve to true'); + + /** Evaluation, track and manager methods on SDK_READY */ + + await client.ready(); + await otherClient.ready(); // waiting to avoid 'control' with label 'sdk not ready' + + assert.equal(await client.getTreatment('UT_IN_SEGMENT'), 'on', '`getTreatment` evaluation using pluggable storage should be correct.'); + assert.equal((await otherClient.getTreatmentWithConfig('UT_IN_SEGMENT')).treatment, 'off', '`getTreatmentWithConfig` evaluation using pluggable storage should be correct.'); + + assert.equal((await client.getTreatments(['UT_NOT_IN_SEGMENT']))['UT_NOT_IN_SEGMENT'], 'off', '`getTreatments` evaluation using pluggable storage should be correct.'); + assert.equal((await otherClient.getTreatmentsWithConfig(['UT_NOT_IN_SEGMENT']))['UT_NOT_IN_SEGMENT'].treatment, 'on', '`getTreatmentsWithConfig` evaluation using pluggable storage should be correct.'); + + assert.equal(await client.getTreatment('UT_SET_MATCHER', { + permissions: ['admin'] + }), 'on', 'Evaluations using pluggable storage should be correct.'); + assert.equal(await client.getTreatment('UT_SET_MATCHER', { + permissions: ['not_matching'] + }), 'off', 'Evaluations using pluggable storage should be correct.'); + + assert.equal(await client.getTreatment('UT_NOT_SET_MATCHER', { + permissions: ['create'] + }), 'off', 'Evaluations using pluggable storage should be correct.'); + assert.equal(await client.getTreatment('UT_NOT_SET_MATCHER', { + permissions: ['not_matching'] + }), 'on', 'Evaluations using pluggable storage should be correct.'); + assert.deepEqual(await client.getTreatmentWithConfig('UT_NOT_SET_MATCHER', { + permissions: ['not_matching'] + }), { + treatment: 'on', + config: null + }, 'Evaluations using pluggable storage should be correct, including configs.'); + assert.deepEqual(await client.getTreatmentWithConfig('always-o.n-with-config'), { + treatment: 'o.n', + config: '{"color":"brown"}' + }, 'Evaluations using pluggable storage should be correct, including configs.'); + + assert.equal(await client.getTreatment('always-on'), 'on', 'Evaluations using pluggable storage should be correct.'); + + // Below splits were added manually. + // 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('hierarchical_splits_testing_on'), 'on', 'Evaluations using pluggable storage should be correct.'); + 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.'); + + 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.'); + + assert.true(await client.track('user', 'test.event', 18), 'If the event was succesfully queued the promise will resolve to true'); + assert.false(await client.track(), 'If the event was NOT succesfully queued the promise will resolve to false'); + + // New shared client created + const newClient = sdk.client('other'); + newClient.track('user', 'test.event', 18).then(result => assert.true(result, 'Track should attempt to track, whether SDK_READY has been emitted or not.')); + newClient.getTreatment('UT_IN_SEGMENT').then(result => assert.equal(result, 'control', '`getTreatment` evaluation is control if shared client is not ready yet.')); await newClient.ready(); assert.true(await newClient.track('user', 'test.event', 18), 'Track should attempt to track, whether SDK_READY has been emitted or not.'); @@ -174,9 +328,7 @@ tape('Browser Consumer Partial mode with pluggable storage', function (t) { await newClient.destroy(); await otherClient.destroy(); - assert.false(fetchMock.called(), 'fetch hasn\'t been called yet'); await client.destroy(); - assert.equal(fetchMock.calls().length, 3, 'fetch has been called 3 times, for posting events, impressions and impression counts, after main client is destroyed'); assert.equal(disconnectSpy.callCount, 1, 'Wrapper disconnect method should be called only once, when the main client is destroyed'); diff --git a/src/__tests__/consumerMode/wrapper-commands.js b/src/__tests__/consumer/wrapper-commands.js similarity index 100% rename from src/__tests__/consumerMode/wrapper-commands.js rename to src/__tests__/consumer/wrapper-commands.js diff --git a/src/__tests__/consumerMode/browser_consumer.spec.js b/src/__tests__/consumerMode/browser_consumer.spec.js deleted file mode 100644 index c8e29a0..0000000 --- a/src/__tests__/consumerMode/browser_consumer.spec.js +++ /dev/null @@ -1,258 +0,0 @@ -import tape from 'tape-catch'; -import sinon from 'sinon'; -import { inMemoryWrapperFactory } from '@splitsoftware/splitio-commons/src/storages/pluggable/inMemoryWrapper'; -import { SDK_NOT_READY, EXCEPTION } from '@splitsoftware/splitio-commons/src/utils/labels'; -import { applyOperations } from './wrapper-commands'; -import { nearlyEqual } from '../testUtils'; - -import { SplitFactory, PluggableStorage } from '../../index'; - -const expectedSplitName = 'hierarchical_splits_testing_on'; -const expectedSplitView = { name: 'hierarchical_splits_testing_on', trafficType: 'user', killed: false, changeNumber: 1487277320548, treatments: ['on', 'off'], configs: {} }; - -const wrapperPrefix = 'PLUGGABLE_STORAGE_UT'; -const wrapperInstance = inMemoryWrapperFactory(0); -const TOTAL_RAW_IMPRESSIONS = 18; -const TOTAL_EVENTS = 5; - -/** @type SplitIO.IBrowserAsyncSettings */ -const config = { - core: { - authorizationKey: 'SOME API KEY', // in consumer mode, api key is only used to identify the sdk instance - key: 'UT_Segment_member' - }, - mode: 'consumer', - storage: PluggableStorage({ - prefix: wrapperPrefix, - wrapper: wrapperInstance - }) -}; - -tape('Browser Consumer mode with pluggable storage', function (t) { - - t.test('Regular usage', async (assert) => { - - // Load wrapper with data to do the proper tests - await applyOperations(wrapperInstance); - - /** @type SplitIO.ImpressionData[] */ - const impressions = []; - const disconnectSpy = sinon.spy(wrapperInstance, 'disconnect'); - - const expectedConfig = '{"color":"brown"}'; - const sdk = SplitFactory({ - ...config, - impressionListener: { - logImpression(data) { impressions.push(data); } - } - }); - const client = sdk.client(); - const otherClient = sdk.client('emi@split.io'); - const manager = sdk.manager(); - - /** Evaluation, track and manager methods before SDK_READY */ - - const getTreatmentResult = client.getTreatment('UT_IN_SEGMENT'); - - const namesResult = manager.names(); - const splitResult = manager.split(expectedSplitName); - const splitsResult = manager.splits(); - - assert.equal(typeof getTreatmentResult.then, 'function', 'GetTreatment calls should always return a promise on Consumer mode.'); - // @TODO caveat: if wrapper.connect is resolved before wrapper operations, next evaluation might be different than control - assert.equal(await getTreatmentResult, 'control', 'Evaluations using pluggable storage should be control if initiated before SDK_READY.'); - assert.equal(client.__getStatus().isReadyFromCache, false, 'SDK in consumer mode is not operational inmediatelly'); - assert.equal(client.__getStatus().isReady, false, 'SDK in consumer mode is not operational inmediatelly'); - - const trackResult = otherClient.track('user', 'test.event', 18); - assert.equal(typeof trackResult.then, 'function', 'Track calls should always return a promise on Consumer mode.'); - assert.true(await trackResult, 'If the wrapper operation success to queue the event, the promise will resolve to true'); - - // Manager methods - assert.deepEqual(await namesResult, [], 'manager `names` method returns an empty list of split names if called before SDK_READY or wrapper operation fail'); - assert.deepEqual(await splitResult, null, 'manager `split` method returns a null split view if called before SDK_READY or wrapper operation fail'); - assert.deepEqual(await splitsResult, [], 'manager `splits` method returns an empty list of split views if called before SDK_READY or wrapper operation fail'); - - /** Evaluation, track and manager methods on SDK_READY */ - - await client.ready(); - await otherClient.ready(); // waiting to avoid 'control' with label 'sdk not ready' - - assert.equal(await client.getTreatment('UT_IN_SEGMENT'), 'on', '`getTreatment` evaluation using pluggable storage should be correct.'); - assert.equal((await otherClient.getTreatmentWithConfig('UT_IN_SEGMENT')).treatment, 'off', '`getTreatmentWithConfig` evaluation using pluggable storage should be correct.'); - - assert.equal((await client.getTreatments(['UT_NOT_IN_SEGMENT']))['UT_NOT_IN_SEGMENT'], 'off', '`getTreatments` evaluation using pluggable storage should be correct.'); - assert.equal((await otherClient.getTreatmentsWithConfig(['UT_NOT_IN_SEGMENT']))['UT_NOT_IN_SEGMENT'].treatment, 'on', '`getTreatmentsWithConfig` evaluation using pluggable storage should be correct.'); - - client.setAttribute('permissions', ['not_matching']); - assert.equal(await client.getTreatment('UT_SET_MATCHER', { - permissions: ['admin'] - }), 'on', 'Evaluations using pluggable storage should be correct.'); - client.setAttributes({ permissions: ['admin'] }); - assert.equal(await client.getTreatment('UT_SET_MATCHER', { - permissions: ['not_matching'] - }), 'off', 'Evaluations using pluggable storage should be correct.'); - assert.equal(await client.getTreatment('UT_SET_MATCHER'), 'on', 'Evaluations using pluggable storage and attributes binding should be correct.'); - - client.setAttribute('permissions', ['not_matching']); - assert.equal(await client.getTreatment('UT_NOT_SET_MATCHER', { - permissions: ['create'] - }), 'off', 'Evaluations using pluggable storage should be correct.'); - assert.equal(await client.getTreatment('UT_NOT_SET_MATCHER'), 'on', 'Evaluations using pluggable storage should be correct.'); - assert.deepEqual(await client.getTreatmentWithConfig('UT_NOT_SET_MATCHER'), { - treatment: 'on', - config: null - }, 'Evaluations using pluggable storage should be correct, including configs.'); - client.clearAttributes(); - assert.deepEqual(await client.getTreatmentWithConfig('always-o.n-with-config'), { - treatment: 'o.n', - config: expectedConfig - }, 'Evaluations using pluggable storage should be correct, including configs.'); - - assert.equal(await client.getTreatment('always-on'), 'on', 'Evaluations using pluggable storage should be correct.'); - - // Below splits were added manually. - // 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('hierarchical_splits_testing_on'), 'on', 'Evaluations using pluggable storage should be correct.'); - 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.'); - - 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.'); - - assert.true(await client.track('user', 'test.event', 18), 'If the event was succesfully queued the promise will resolve to true'); - assert.false(await client.track(), 'If the event was NOT succesfully queued the promise will resolve to false'); - - // 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.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.deepEqual(splitViews.find(splitView => splitView.name === expectedSplitName), expectedSplitView, 'manager `split` method returns the split view of the given split name asynchronously'); - - // New shared client created - const newClient = sdk.client('other'); - assert.true(await newClient.track('user', 'test.event', 18), 'Track should attempt to track, whether SDK_READY has been emitted or not.'); - assert.equal((await newClient.getTreatment('UT_IN_SEGMENT')), 'control', '`getTreatment` evaluation is control if shared client is not ready yet.'); - - await newClient.ready(); - assert.true(await newClient.track('user', 'test.event', 18), 'Track should attempt to track, whether SDK_READY has been emitted or not.'); - assert.equal((await newClient.getTreatment('UT_IN_SEGMENT')), 'off', '`getTreatment` evaluation using pluggable storage should be correct.'); - - await client.ready(); // promise already resolved - await newClient.destroy(); - await otherClient.destroy(); - await client.destroy(); - - assert.equal(disconnectSpy.callCount, 1, 'Wrapper disconnect method should be called only once, when the main client is destroyed'); - - const trackedImpressions = await wrapperInstance.popItems('PLUGGABLE_STORAGE_UT.SPLITIO.impressions', await wrapperInstance.getItemsCount('PLUGGABLE_STORAGE_UT.SPLITIO.impressions')); - const trackedEvents = await wrapperInstance.popItems('PLUGGABLE_STORAGE_UT.SPLITIO.events', await wrapperInstance.getItemsCount('PLUGGABLE_STORAGE_UT.SPLITIO.events')); - assert.equal(trackedImpressions.length, TOTAL_RAW_IMPRESSIONS, 'Tracked impressions should be present in the external storage'); - assert.equal(trackedEvents.length, TOTAL_EVENTS, 'Tracked events should be present in the external storage'); - - // Assert impressionsListener - setTimeout(() => { - assert.equal(impressions.length, TOTAL_RAW_IMPRESSIONS, 'Each evaluation has its corresponting impression'); - assert.equal(impressions[0].impression.label, SDK_NOT_READY, 'The first impression is control with label "sdk not ready"'); - - assert.end(); - }); - }); - - t.test('Connection timeout and then ready', assert => { - const connDelay = 110; - const readyTimeout = 0.1; // 100 millis - const configWithShortTimeout = { ...config, startup: { readyTimeout } }; - wrapperInstance._setConnDelay(connDelay); - const sdk = SplitFactory(configWithShortTimeout); - wrapperInstance._setConnDelay(0); // restore - const client = sdk.client(); - const otherClient = sdk.client('other'); - - const start = Date.now(); - assert.plan(12); - - client.getTreatment('always-on').then(treatment => { - assert.equal(treatment, 'control', 'Evaluations using pluggable storage should be control if SDK is not ready'); - }); - client.track('user', 'test.event', 18).then(result => { - assert.true(result, 'If the wrapper operation success to queue the event, the promise will resolve to true, even if the SDK is not ready'); - }); - - // SDK_READY_TIMED_OUT event must be emitted after 100 millis - [client, otherClient].forEach(client => client.on(client.Event.SDK_READY_TIMED_OUT, () => { - const delay = Date.now() - start; - assert.true(nearlyEqual(delay, readyTimeout * 1000), 'SDK_READY_TIMED_OUT event must be emitted after 100 millis'); - })); - - // Also, ready promise must be rejected after 100 millis - [client, otherClient].forEach(client => client.ready().catch(() => { - const delay = Date.now() - start; - assert.true(nearlyEqual(delay, readyTimeout * 1000), 'Ready promise must be rejected after 100 millis'); - })); - - // subscribe to SDK_READY event to assert regular usage - client.on(client.Event.SDK_READY, async () => { - const delay = Date.now() - start; - assert.true(nearlyEqual(delay, connDelay), 'SDK_READY event must be emitted once wrapper is connected'); - - await client.ready(); - assert.pass('Ready promise is resolved once SDK_READY is emitted'); - - // some asserts to test regular usage - assert.equal(await client.getTreatment('UT_IN_SEGMENT'), 'on', 'Evaluations using pluggable storage should be correct.'); - assert.equal(await otherClient.getTreatment('UT_IN_SEGMENT'), 'off', 'Evaluations using pluggable storage should be correct.'); - assert.true(await client.track('user', 'test.event', 18), 'If the event was succesfully queued the promise will resolve to true'); - assert.false(await client.track(), 'If the event was NOT succesfully queued the promise will resolve to false'); - - await client.destroy(); - assert.end(); - }); - - }); - - t.test('Wrapper errors', async (assert) => { - const impressions = []; - - const sdk = SplitFactory({ - ...config, - impressionListener: { - logImpression(data) { impressions.push(data); } - } - }); - const client = sdk.client(); - const manager = sdk.manager(); - await client.ready(); - assert.equal(await client.getTreatment('UT_IN_SEGMENT'), 'on', '`getTreatment` evaluation correct.'); - assert.true(await client.track('user', 'test.event', 18), 'Event queued'); - - // Stubbing wrapper methods to make client and manager methods fail - const methods = ['pushItems', 'get', 'getKeysByPrefix']; - methods.forEach(method => sinon.stub(wrapperInstance, method).callsFake(() => { Promise.reject(); })); - - assert.equal(await client.getTreatment('UT_IN_SEGMENT'), 'control', '`getTreatment` evaluation correct.'); - assert.false(await client.track('user', 'test.event', 18), 'If the event failed to be queued due to a wrapper operation failure, the promise will resolve to false'); - assert.deepEqual(await manager.names(), [], 'manager `names` method returns an empty list of split names if called before SDK_READY or wrapper operation fail'); - assert.deepEqual(await manager.split(expectedSplitName), null, 'manager `split` method returns a null split view if called before SDK_READY or wrapper operation fail'); - assert.deepEqual(await manager.splits(), [], 'manager `splits` method returns an empty list of split views if called before SDK_READY or wrapper operation fail'); - - // Restore wrapper - methods.forEach(method => wrapperInstance[method].restore()); - - await client.destroy(); - - // Assert impressionsListener - setTimeout(() => { - assert.equal(impressions.length, 2, 'Each evaluation has its corresponting impression'); - assert.equal(impressions[1].impression.label, EXCEPTION, 'The last impression is control with label "exception"'); - - assert.end(); - }); - - }); - - -}); diff --git a/src/__tests__/destroy/browser.spec.js b/src/__tests__/destroy/browser.spec.js index 79be403..2c1b3d2 100644 --- a/src/__tests__/destroy/browser.spec.js +++ b/src/__tests__/destroy/browser.spec.js @@ -3,14 +3,15 @@ import fetchMock from '../testUtils/fetchMock'; import { url } from '../testUtils'; import map from 'lodash/map'; import pick from 'lodash/pick'; -import { SplitFactory, DebugLogger } from '../../index'; -import { settingsValidator } from '../../settings'; +import { SplitFactory } from '../../'; +import { settingsFactory } from '../../settings'; + import splitChangesMock1 from '../mocks/splitChanges.since.-1.till.1500492097547.json'; import splitChangesMock2 from '../mocks/splitChanges.since.1500492097547.json'; import mySegmentsMock from '../mocks/mySegmentsEmpty.json'; import impressionsMock from '../mocks/impressions.json'; -const settings = settingsValidator({ +const settings = settingsFactory({ core: { key: 'facundo@split.io' }, @@ -30,7 +31,6 @@ tape('SDK destroy for BrowserJS', async function (assert) { authorizationKey: 'fake-key', key: 'ut1' }, - debug: DebugLogger(), streamingEnabled: false }; diff --git a/src/__tests__/errors/browser.spec.js b/src/__tests__/errorCatching/browser.spec.js similarity index 96% rename from src/__tests__/errors/browser.spec.js rename to src/__tests__/errorCatching/browser.spec.js index e21f05d..3853da4 100644 --- a/src/__tests__/errors/browser.spec.js +++ b/src/__tests__/errorCatching/browser.spec.js @@ -7,10 +7,10 @@ import splitChangesMock1 from '../mocks/splitChanges.since.-1.till.1500492097547 import mySegmentsMock from '../mocks/mySegmentsEmpty.json'; import splitChangesMock2 from '../mocks/splitChanges.since.1500492097547.till.1500492297547.json'; import splitChangesMock3 from '../mocks/splitChanges.since.1500492297547.json'; -import { SplitFactory, InLocalStorage } from '../../index'; -import { settingsValidator } from '../../settings'; +import { SplitFactory, InLocalStorage } from '../../'; +import { settingsFactory } from '../../settings'; -const settings = settingsValidator({ +const settings = settingsFactory({ core: { authorizationKey: '' }, diff --git a/src/__tests__/gaIntegration/both-integrations.spec.js b/src/__tests__/gaIntegration/both-integrations.spec.js index 0ba6dc2..884a411 100644 --- a/src/__tests__/gaIntegration/both-integrations.spec.js +++ b/src/__tests__/gaIntegration/both-integrations.spec.js @@ -1,5 +1,5 @@ -import { SplitFactory, GoogleAnalyticsToSplit, SplitToGoogleAnalytics } from '../../index'; -import { settingsValidator } from '../../settings'; +import { SplitFactory, GoogleAnalyticsToSplit, SplitToGoogleAnalytics } from '../../'; +import { settingsFactory } from '../../settings'; import { gaSpy, gaTag } from './gaTestUtils'; import includes from 'lodash/includes'; import { DEBUG } from '@splitsoftware/splitio-commons/src/utils/constants'; @@ -13,7 +13,7 @@ function countImpressions(parsedImpressionsBulkPayload) { const config = { core: { key: 'facundo@split.io', - trafficType: 'user', // ignored for default client, but used as default identity in GaToSplit integration + trafficType: 'user', // Traffic type is not bound to default client in JS Browser SDK, but it is used as identity in GaToSplit integration }, integrations: [GoogleAnalyticsToSplit(), SplitToGoogleAnalytics()], streamingEnabled: false, @@ -21,7 +21,7 @@ const config = { impressionsMode: DEBUG, } }; -const settings = settingsValidator(config); +const settings = settingsFactory(config); export default function (fetchMock, assert) { diff --git a/src/__tests__/gaIntegration/browser.spec.js b/src/__tests__/gaIntegration/browser.spec.js index c1fbe67..6d61aea 100644 --- a/src/__tests__/gaIntegration/browser.spec.js +++ b/src/__tests__/gaIntegration/browser.spec.js @@ -4,11 +4,13 @@ import { url } from '../testUtils'; import gaToSplitSuite from './ga-to-split.spec'; import splitToGaSuite from './split-to-ga.spec'; import bothIntegrationsSuite from './both-integrations.spec'; -import { settingsValidator } from '../../settings'; + +import { settingsFactory } from '../../settings'; + import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import mySegmentsFacundo from '../mocks/mysegments.facundo@split.io.json'; -const settings = settingsValidator({ +const settings = settingsFactory({ core: { key: 'facundo@split.io' } diff --git a/src/__tests__/gaIntegration/ga-to-split.spec.js b/src/__tests__/gaIntegration/ga-to-split.spec.js index 815f4b1..e26c441 100644 --- a/src/__tests__/gaIntegration/ga-to-split.spec.js +++ b/src/__tests__/gaIntegration/ga-to-split.spec.js @@ -1,13 +1,13 @@ import sinon from 'sinon'; -import { SplitFactory, GoogleAnalyticsToSplit, DebugLogger } from '../../index'; -import { settingsValidator } from '../../settings'; +import { SplitFactory, GoogleAnalyticsToSplit, DebugLogger } from '../../'; +import { settingsFactory } from '../../settings'; import { gaSpy, gaTag, addGaTag, removeGaTag } from './gaTestUtils'; import { url } from '../testUtils'; const config = { core: { key: 'facundo@split.io', - trafficType: 'user', // ignored for default client, but used as default identity in GaToSplit integration + trafficType: 'user', // Traffic type is not bound to default client in JS Browser SDK, but it is used as identity in GaToSplit integration }, integrations: [GoogleAnalyticsToSplit()], startup: { @@ -17,7 +17,7 @@ const config = { debug: DebugLogger() }; -const settings = settingsValidator(config); +const settings = settingsFactory(config); export default function (fetchMock, assert) { diff --git a/src/__tests__/gaIntegration/split-to-ga.spec.js b/src/__tests__/gaIntegration/split-to-ga.spec.js index dd36617..e6b43d5 100644 --- a/src/__tests__/gaIntegration/split-to-ga.spec.js +++ b/src/__tests__/gaIntegration/split-to-ga.spec.js @@ -1,6 +1,6 @@ import sinon from 'sinon'; -import { SplitFactory, SplitToGoogleAnalytics, DebugLogger } from '../../index'; -import { settingsValidator } from '../../settings'; +import { SplitFactory, SplitToGoogleAnalytics, DebugLogger } from '../../'; +import { settingsFactory } from '../../settings'; import { gaSpy, gaTag, removeGaTag, addGaTag } from './gaTestUtils'; import { SPLIT_IMPRESSION, SPLIT_EVENT, DEBUG } from '@splitsoftware/splitio-commons/src/utils/constants'; import { url } from '../testUtils'; @@ -26,7 +26,7 @@ const config = { } }; -const settings = settingsValidator(config); +const settings = settingsFactory(config); export default function (fetchMock, assert) { diff --git a/src/__tests__/logger/browser.spec.js b/src/__tests__/logger/browser.spec.js index 6b1a652..14549a8 100644 --- a/src/__tests__/logger/browser.spec.js +++ b/src/__tests__/logger/browser.spec.js @@ -3,7 +3,7 @@ import { initialLogLevel } from './setLocalStorage'; import tape from 'tape'; import sinon from 'sinon'; import fetchMock from '../testUtils/fetchMock'; -import { SplitFactory, ErrorLogger, DebugLogger } from '../../index'; +import { SplitFactory, ErrorLogger, DebugLogger } from '../../'; // Don't care about SDK readiness fetchMock.get('*', { throws: new TypeError('Network error') }); diff --git a/src/__tests__/mocks/message.STREAMING_RESET.json b/src/__tests__/mocks/message.STREAMING_RESET.json index ee1c13e..791d9de 100644 --- a/src/__tests__/mocks/message.STREAMING_RESET.json +++ b/src/__tests__/mocks/message.STREAMING_RESET.json @@ -1,4 +1,4 @@ { "type": "message", - "data": "{\"timestamp\":1457552660000,\"data\":\"{\\\"type\\\":\\\"STREAMING_RESET\\\"}\"}" + "data": "{\"timestamp\":1457552660000,\"data\":\"{\\\"type\\\":\\\"CONTROL\\\",\\\"controlType\\\":\\\"STREAMING_RESET\\\"}\"}" } \ No newline at end of file diff --git a/src/__tests__/offline/browser.spec.js b/src/__tests__/offline/browser.spec.js index 892d277..0b2be89 100644 --- a/src/__tests__/offline/browser.spec.js +++ b/src/__tests__/offline/browser.spec.js @@ -4,9 +4,9 @@ import fetchMock from '../testUtils/fetchMock'; import { url } from '../testUtils'; import { SplitFactory, InLocalStorage } from '../../full'; import { SplitFactory as SplitFactorySlim, LocalhostFromObject } from '../../'; -import { settingsValidator } from '../../settings'; +import { settingsFactory } from '../../settings'; -const settings = settingsValidator({ core: { key: 'facundo@split.io' } }); +const settings = settingsFactory({ core: { key: 'facundo@split.io' } }); const spySplitChanges = sinon.spy(); const spySegmentChanges = sinon.spy(); @@ -32,8 +32,6 @@ const configMocks = () => { fetchMock.mock(url(settings, '/events/bulk'), () => replySpy(spyEventsBulk)); fetchMock.mock(url(settings, '/testImpressions/bulk'), () => replySpy(spyTestImpressionsBulk)); fetchMock.mock(url(settings, '/testImpressions/count'), () => replySpy(spyTestImpressionsCount)); - fetchMock.mock(url(settings, '/metrics/times'), () => replySpy(spyMetricsTimes)); - fetchMock.mock(url(settings, '/metrics/counters'), () => replySpy(spyMetricsCounters)); fetchMock.mock('*', () => replySpy(spyAny)); }; diff --git a/src/__tests__/online/browser.spec.js b/src/__tests__/online/browser.spec.js index 00a165c..cf75666 100644 --- a/src/__tests__/online/browser.spec.js +++ b/src/__tests__/online/browser.spec.js @@ -1,24 +1,25 @@ import tape from 'tape-catch'; import fetchMock from '../testUtils/fetchMock'; import { url } from '../testUtils'; -import evaluationsSuite from './evaluations.spec'; -import impressionsSuite from './impressions.spec'; -import impressionsSuiteDebug from './impressions.debug.spec'; -import telemetrySuite from './telemetry.spec'; -import impressionsListenerSuite from './impressions-listener.spec'; -import readinessSuite from './readiness.spec'; -import readyFromCache from './ready-from-cache.spec'; -import { withoutBindingTT /*, bindingTT */ } from './events.spec'; -import sharedInstantiationSuite from './shared-instantiation.spec'; -import managerSuite from './manager.spec'; -import ignoreIpAddressesSettingSuite from './ignore-ip-addresses-setting.spec'; -import useBeaconApiSuite from './use-beacon-api.spec'; -import useBeaconDebugApiSuite from './use-beacon-api.debug.spec'; -import readyPromiseSuite from './ready-promise.spec'; -import fetchSpecificSplits from './fetch-specific-splits.spec'; -import userConsent from './user-consent.spec'; +import evaluationsSuite from '../browserSuites/evaluations.spec'; +import impressionsSuite from '../browserSuites/impressions.spec'; +import impressionsSuiteDebug from '../browserSuites/impressions.debug.spec'; +import telemetrySuite from '../browserSuites/telemetry.spec'; +import impressionsListenerSuite from '../browserSuites/impressions-listener.spec'; +import readinessSuite from '../browserSuites/readiness.spec'; +import readyFromCache from '../browserSuites/ready-from-cache.spec'; +import { withoutBindingTT /*, bindingTT */ } from '../browserSuites/events.spec'; +import sharedInstantiationSuite from '../browserSuites/shared-instantiation.spec'; +import managerSuite from '../browserSuites/manager.spec'; +import ignoreIpAddressesSettingSuite from '../browserSuites/ignore-ip-addresses-setting.spec'; +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 singleSync from '../browserSuites/single-sync.spec'; -import { settingsValidator } from '../../settings'; +import { settingsFactory } from '../../settings'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import splitChangesMock2 from '../mocks/splitchanges.since.1457552620999.json'; import mySegmentsFacundo from '../mocks/mysegments.facundo@split.io.json'; @@ -27,7 +28,7 @@ import mySegmentsMarcio from '../mocks/mysegments.marcio@split.io.json'; import mySegmentsEmmanuel from '../mocks/mysegments.emmanuel@split.io.json'; import { InLocalStorage } from '../../index'; -const settings = settingsValidator({ +const settings = settingsFactory({ core: { key: 'facundo@split.io' }, @@ -129,6 +130,8 @@ tape('## E2E CI Tests ##', function (assert) { assert.test('E2E / Ready promise', readyPromiseSuite.bind(null, fetchMock)); /* Validate fetching specific splits */ assert.test('E2E / Fetch specific splits', fetchSpecificSplits.bind(null, fetchMock)); + /* Validate single sync */ + assert.test('E2E / Single sync', singleSync.bind(null, fetchMock)); //If we change the mocks, we need to clear localstorage. Cleaning up after testing ensures "fresh data". localStorage.clear(); diff --git a/src/__tests__/push/browser.spec.js b/src/__tests__/push/browser.spec.js index bcc033a..f4d5103 100644 --- a/src/__tests__/push/browser.spec.js +++ b/src/__tests__/push/browser.spec.js @@ -1,12 +1,12 @@ import tape from 'tape-catch'; import fetchMock from '../testUtils/fetchMock'; -import { testAuthWithPushDisabled, testAuthWith401, testNoEventSource, testSSEWithNonRetryableError } from './push-initialization-nopush.spec'; -import { testPushRetriesDueToAuthErrors, testPushRetriesDueToSseErrors, testSdkDestroyWhileAuthRetries, testSdkDestroyWhileAuthSuccess, testSdkDestroyWhileConnDelay } from './push-initialization-retries.spec'; -import { testSynchronization } from './push-synchronization.spec'; -import { testSynchronizationRetries } from './push-synchronization-retries.spec'; -import { testFallbacking } from './push-fallbacking.spec'; -import { testRefreshToken } from './push-refresh-token.spec'; -import { testSplitKillOnReadyFromCache } from './push-corner-cases.spec'; +import { testAuthWithPushDisabled, testAuthWith401, testNoEventSource, testSSEWithNonRetryableError } from '../browserSuites/push-initialization-nopush.spec'; +import { testPushRetriesDueToAuthErrors, testPushRetriesDueToSseErrors, testSdkDestroyWhileAuthRetries, testSdkDestroyWhileAuthSuccess, testSdkDestroyWhileConnDelay } from '../browserSuites/push-initialization-retries.spec'; +import { testSynchronization } from '../browserSuites/push-synchronization.spec'; +import { testSynchronizationRetries } from '../browserSuites/push-synchronization-retries.spec'; +import { testFallbacking } from '../browserSuites/push-fallbacking.spec'; +import { testRefreshToken } from '../browserSuites/push-refresh-token.spec'; +import { testSplitKillOnReadyFromCache } from '../browserSuites/push-corner-cases.spec'; fetchMock.config.overwriteRoutes = false; Math.random = () => 0.5; // SDKs without telemetry diff --git a/src/__tests__/testUtils/browser.js b/src/__tests__/testUtils/browser.js new file mode 100644 index 0000000..6028492 --- /dev/null +++ b/src/__tests__/testUtils/browser.js @@ -0,0 +1,19 @@ +function triggerEvent(eventName) { + const event = document.createEvent('HTMLEvents'); + event.initEvent(eventName, true, true); + event.eventName = eventName; + window.dispatchEvent(event); +} + +export function triggerUnloadEvent() { + triggerEvent('unload'); +} + +export function triggerPagehideEvent() { + triggerEvent('pagehide'); +} + +export function triggerVisibilitychange(state = 'hidden' /* 'hidden' | 'visible' */) { + Object.defineProperty(document, 'visibilityState', { value: state, writable: true }); + document.dispatchEvent(new Event('visibilitychange')); +} diff --git a/src/__tests__/testUtils/index.js b/src/__tests__/testUtils/index.js index a140557..2717982 100644 --- a/src/__tests__/testUtils/index.js +++ b/src/__tests__/testUtils/index.js @@ -43,7 +43,7 @@ export function hasNoCacheHeader(fetchMockOpts) { return fetchMockOpts.headers['Cache-Control'] === 'no-cache'; } -const telemetryEndpointMatcher = /^\/v1\/metrics\/(config|usage)/; +const telemetryEndpointMatcher = /^\/v1\/(metrics|keys)\/(config|usage|ss|cs)/; const eventsEndpointMatcher = /^\/(testImpressions|metrics|events)/; const authEndpointMatcher = /^\/v2\/auth/; const streamingEndpointMatcher = /^\/(sse|event-stream)/; @@ -71,23 +71,3 @@ export function url(settings, target) { } return `${settings.urls.sdk}${target}`; } - -function triggerEvent(eventName) { - const event = document.createEvent('HTMLEvents'); - event.initEvent(eventName, true, true); - event.eventName = eventName; - window.dispatchEvent(event); -} - -export function triggerUnloadEvent() { - triggerEvent('unload'); -} - -export function triggerPagehideEvent() { - triggerEvent('pagehide'); -} - -export function triggerVisibilitychange(state = 'hidden' /* 'hidden' | 'visible' */) { - Object.defineProperty(document, 'visibilityState', { value: state, writable: true }); - document.dispatchEvent(new Event('visibilitychange')); -} diff --git a/src/full/splitFactory.ts b/src/full/splitFactory.ts index 9c2864b..83ba000 100644 --- a/src/full/splitFactory.ts +++ b/src/full/splitFactory.ts @@ -1,4 +1,4 @@ -import { settingsValidator } from '../settings/full'; +import { settingsFactory } from '../settings/full'; import { getModules } from '../platform/getModules'; import { sdkFactory } from '@splitsoftware/splitio-commons/src/sdkFactory/index'; import { ISdkFactoryParams } from '@splitsoftware/splitio-commons/src/sdkFactory/types'; @@ -6,6 +6,7 @@ import { getFetch } from '../platform/getFetchFull'; import { getEventSource } from '../platform/getEventSource'; import { EventEmitter } from '@splitsoftware/splitio-commons/src/utils/MinEvents'; import { now } from '@splitsoftware/splitio-commons/src/utils/timeTracker/now/browser'; +import { IBrowserSettings } from '../../types/splitio'; const platform = { getFetch, getEventSource, EventEmitter, now }; @@ -18,8 +19,8 @@ const platform = { getFetch, getEventSource, EventEmitter, now }; * caution since, unlike `config`, this param is not validated neither considered part of the public API. * @throws Will throw an error if the provided config is invalid. */ -export function SplitFactory(config: any, __updateModules?: (modules: ISdkFactoryParams) => void) { - const settings = settingsValidator(config); +export function SplitFactory(config: IBrowserSettings, __updateModules?: (modules: ISdkFactoryParams) => void) { + const settings = settingsFactory(config); const modules = getModules(settings, platform); if (__updateModules) __updateModules(modules); return sdkFactory(modules); diff --git a/src/platform/getModules.ts b/src/platform/getModules.ts index 8d5416f..90c45e9 100644 --- a/src/platform/getModules.ts +++ b/src/platform/getModules.ts @@ -8,7 +8,6 @@ import { BrowserSignalListener } from '@splitsoftware/splitio-commons/src/listen import { impressionObserverCSFactory } from '@splitsoftware/splitio-commons/src/trackers/impressionObserver/impressionObserverCS'; import { pluggableIntegrationsManagerFactory } from '@splitsoftware/splitio-commons/src/integrations/pluggable'; -import { shouldAddPt } from '@splitsoftware/splitio-commons/src/trackers/impressionObserver/utils'; import { IPlatform, ISdkFactoryParams } from '@splitsoftware/splitio-commons/src/sdkFactory/types'; import { ISettings } from '@splitsoftware/splitio-commons/src/types'; import { CONSUMER_MODE, CONSUMER_PARTIAL_MODE, LOCALHOST_MODE } from '@splitsoftware/splitio-commons/src/utils/constants'; @@ -40,7 +39,7 @@ export function getModules(settings: ISettings, platform: IPlatform): ISdkFactor integrationsManagerFactory: settings.integrations && settings.integrations.length > 0 ? pluggableIntegrationsManagerFactory.bind(null, settings.integrations) : undefined, - impressionsObserverFactory: shouldAddPt(settings) ? impressionObserverCSFactory : undefined, + impressionsObserverFactory: impressionObserverCSFactory, extraProps: (params) => { return { diff --git a/src/settings/__tests__/index.spec.ts b/src/settings/__tests__/index.spec.ts index a9fc014..4b1ad49 100644 --- a/src/settings/__tests__/index.spec.ts +++ b/src/settings/__tests__/index.spec.ts @@ -1,18 +1,18 @@ -import { settingsValidator as slimSettingsValidator } from '../index'; -import { settingsValidator as fullSettingsValidator } from '../index'; +import { settingsFactory as slimSettingsFactory } from '../index'; +import { settingsFactory as fullSettingsFactory } from '../full'; test('SETTINGS / Consent is overwritable and "GRANTED" by default', () => { - [slimSettingsValidator, fullSettingsValidator].forEach((settingsValidator) => { - let settings = settingsValidator({}); + [slimSettingsFactory, fullSettingsFactory].forEach((settingsFactory) => { + let settings = settingsFactory({}); expect(settings.userConsent).toEqual('GRANTED'); // userConsent defaults to granted if not provided - settings = settingsValidator({ userConsent: 'INVALID-VALUE' }); + settings = settingsFactory({ userConsent: 'INVALID-VALUE' }); expect(settings.userConsent).toEqual('GRANTED'); // userConsent defaults to granted if a wrong value is provided - settings = settingsValidator({ userConsent: 'UNKNOWN' }); + settings = settingsFactory({ userConsent: 'UNKNOWN' }); expect(settings.userConsent).toEqual('UNKNOWN'); // userConsent can be overwritten - settings = settingsValidator({ userConsent: 'declined' }); + settings = settingsFactory({ userConsent: 'declined' }); expect(settings.userConsent).toEqual('DECLINED'); // userConsent can be overwritten }); }); diff --git a/src/settings/defaults.ts b/src/settings/defaults.ts index 5a9b687..60aaad6 100644 --- a/src/settings/defaults.ts +++ b/src/settings/defaults.ts @@ -2,7 +2,7 @@ import { LogLevels, isLogLevelString } from '@splitsoftware/splitio-commons/src/ import { ConsentStatus, LogLevel } from '@splitsoftware/splitio-commons/src/types'; import { CONSENT_GRANTED } from '@splitsoftware/splitio-commons/src/utils/constants'; -const packageVersion = '0.8.0'; +const packageVersion = '0.9.0'; /** * In browser, the default debug level, can be set via the `localStorage.splitio_debug` item. diff --git a/src/settings/full.ts b/src/settings/full.ts index bd39f10..8afa840 100644 --- a/src/settings/full.ts +++ b/src/settings/full.ts @@ -18,6 +18,6 @@ const params = { consent: validateConsent, }; -export function settingsValidator(config: any) { +export function settingsFactory(config: any) { return settingsValidation(config, params); } diff --git a/src/settings/index.ts b/src/settings/index.ts index 9b0f0d2..bfe6541 100644 --- a/src/settings/index.ts +++ b/src/settings/index.ts @@ -18,6 +18,6 @@ const params = { consent: validateConsent, }; -export function settingsValidator(config: any) { +export function settingsFactory(config: any) { return settingsValidation(config, params); } diff --git a/src/splitFactory.ts b/src/splitFactory.ts index c782d43..db14cd3 100644 --- a/src/splitFactory.ts +++ b/src/splitFactory.ts @@ -1,4 +1,4 @@ -import { settingsValidator } from './settings'; +import { settingsFactory } from './settings'; import { getModules } from './platform/getModules'; import { sdkFactory } from '@splitsoftware/splitio-commons/src/sdkFactory/index'; import { ISdkFactoryParams } from '@splitsoftware/splitio-commons/src/sdkFactory/types'; @@ -6,6 +6,7 @@ import { getFetch } from './platform/getFetchSlim'; import { getEventSource } from './platform/getEventSource'; import { EventEmitter } from '@splitsoftware/splitio-commons/src/utils/MinEvents'; import { now } from '@splitsoftware/splitio-commons/src/utils/timeTracker/now/browser'; +import { IBrowserSettings } from '../types/splitio'; const platform = { getFetch, getEventSource, EventEmitter, now }; @@ -18,8 +19,8 @@ const platform = { getFetch, getEventSource, EventEmitter, now }; * caution since, unlike `config`, this param is not validated neither considered part of the public API. * @throws Will throw an error if the provided config is invalid. */ -export function SplitFactory(config: any, __updateModules?: (modules: ISdkFactoryParams) => void) { - const settings = settingsValidator(config); +export function SplitFactory(config: IBrowserSettings, __updateModules?: (modules: ISdkFactoryParams) => void) { + const settings = settingsFactory(config); const modules = getModules(settings, platform); if (__updateModules) __updateModules(modules); return sdkFactory(modules); diff --git a/types/splitio.d.ts b/types/splitio.d.ts index be289de..9a4d665 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -91,8 +91,8 @@ interface ISettings { readonly sync: { splitFilters: SplitIO.SplitFilter[], impressionsMode: SplitIO.ImpressionsMode, - localhostMode?: SplitIO.LocalhostFactory, - enabled?: boolean + enabled: boolean, + localhostMode?: SplitIO.LocalhostFactory }, readonly userConsent: SplitIO.ConsentStatus } @@ -217,9 +217,11 @@ interface ISharedSettings { splitFilters?: SplitIO.SplitFilter[] /** * Impressions Collection Mode. Option to determine how impressions are going to be sent to Split Servers. - * Possible values are 'DEBUG' and 'OPTIMIZED'. + * Possible values are 'DEBUG', 'OPTIMIZED', and 'NONE'. * - DEBUG: will send all the impressions generated (recommended only for debugging purposes). - * - OPTIMIZED: will send unique impressions to Split Servers avoiding a considerable amount of traffic that duplicated impressions could generate. + * - OPTIMIZED: will send unique impressions to Split Servers, avoiding a considerable amount of traffic that duplicated impressions could generate. + * - NONE: will send unique keys evaluated per feature to Split Servers instead of full blown impressions, avoiding a considerable amount of traffic that impressions could generate. + * * @property {String} impressionsMode * @default 'OPTIMIZED' */ @@ -247,7 +249,7 @@ interface ISharedSettings { * Controls the SDK continuous synchronization flags. * * When `true` a running SDK will process rollout plan updates performed on the UI (default). - * When false it'll just fetch all data upon init + * When false it'll just fetch all data upon init. * * @property {boolean} enabled * @default true @@ -809,7 +811,7 @@ declare namespace SplitIO { * ImpressionsMode type * @typedef {string} ImpressionsMode */ - type ImpressionsMode = 'OPTIMIZED' | 'DEBUG'; + type ImpressionsMode = 'OPTIMIZED' | 'DEBUG' | 'NONE'; /** * User consent status. * @typedef {string} ConsentStatus @@ -955,7 +957,7 @@ declare namespace SplitIO { /** * The SDK polls Split servers for changes to feature roll-out plans. This parameter controls this polling period in seconds. * @property {number} featuresRefreshRate - * @default 30 + * @default 60 */ featuresRefreshRate?: number, /**