diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index f6fd86ccb..7b5cb50ea 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -50,6 +50,9 @@ jobs: - name: npm Build run: BUILD_BRANCH=$(echo "${GITHUB_REF#refs/heads/}") BUILD_COMMIT=${{ github.sha }} npm run build + - name: npm Build GaToSplit auto-require script + run: npm run build:ga-to-split-autorequire + - name: Configure AWS credentials (development) if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/development' }} uses: aws-actions/configure-aws-credentials@v1 diff --git a/CHANGES.txt b/CHANGES.txt index 9b549227d..e8c60d6c6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,10 @@ +10.21.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. +- Updated the synchronization flow to be more reliable in the event of an edge case generating delay in cache purge propagation, keeping the SDK cache properly synced. +- Updated some dependencies for vulnerability fixes. +- Bugfixing - Moved js-yaml dependency from @splitsoftware/splitio-commons to avoid resolution to an incompatible version on certain npm versions when installing third-party dependencies that also define js-yaml as transitive dependency (Related to issue https://github.com/splitio/javascript-client/issues/662). + 10.20.0 (June 29, 2022) - Added a new config option to control the tasks that listen or poll for updates on feature flags and segments, via the new config sync.enabled . Running online Split will always pull the most recent updates upon initialization, this only affects updates fetching on a running instance. Useful when a consistent session experience is a must or to save resources when updates are not being used. - Updated telemetry logic to track the anonymous config for user consent flag set to declined or unknown. diff --git a/package-lock.json b/package-lock.json index bb6095050..2e16e0cb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "10.20.0", + "version": "10.21.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -389,6 +389,55 @@ "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", "dev": true }, + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", + "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "@mdn/browser-compat-data": { "version": "3.3.14", "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-3.3.14.tgz", @@ -437,9 +486,9 @@ "dev": true }, "@splitsoftware/splitio-commons": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.5.0.tgz", - "integrity": "sha512-7vBv4Yn+EKaAxJ+9RdKOqJNee+izj6BieG0ZmivWk8fTpKFpcvqKes/Y0kXk0tfOiMmUFV9dRr2xDHRYIf0lnw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.6.1.tgz", + "integrity": "sha512-lijSMpX5a7HiphPnzr7fKlicGSd5TeBWX5URLPWfL87SPlO+8iF7S7PNFPg8GMu+44CdRrp4REHPJo97ht6ePQ==", "requires": { "tslib": "^2.3.1" } @@ -1911,7 +1960,7 @@ "custom-event": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", - "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", "dev": true }, "date-format": { @@ -1992,7 +2041,7 @@ "di": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", - "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", "dev": true }, "diff": { @@ -2032,7 +2081,7 @@ "dom-serialize": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", - "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", "dev": true, "requires": { "custom-event": "~1.0.0", @@ -2071,7 +2120,7 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true }, "electron-to-chromium": { @@ -2106,7 +2155,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true }, "end-of-stream": { @@ -2183,7 +2232,7 @@ "ent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", "dev": true }, "envinfo": { @@ -2260,7 +2309,7 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true }, "escape-string-regexp": { @@ -4037,7 +4086,7 @@ "lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, "lodash.flatten": { "version": "4.4.0", @@ -4053,7 +4102,7 @@ "lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=" + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" }, "lodash.isequal": { "version": "4.5.0", @@ -4146,7 +4195,7 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true }, "merge-descriptors": { @@ -4524,7 +4573,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true }, "object-inspect": { @@ -5230,7 +5279,7 @@ "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, "resolve": { @@ -5522,6 +5571,24 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -5892,40 +5959,15 @@ } }, "terser": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", - "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "dev": true, "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", "source-map-support": "~0.5.20" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - }, - "source-map-support": { - "version": "0.5.20", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", - "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - } } }, "terser-webpack-plugin": { @@ -6234,7 +6276,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true }, "untildify": { @@ -6299,7 +6341,7 @@ "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true }, "v8-compile-cache": { @@ -6317,7 +6359,7 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true }, "vm-browserify": { @@ -6329,7 +6371,7 @@ "void-elements": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", "dev": true }, "watchpack": { diff --git a/package.json b/package.json index 815066319..fbe315e14 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "10.20.0", + "version": "10.21.0", "description": "Split SDK", "files": [ "README.md", @@ -10,7 +10,8 @@ "lib", "types", "es", - "src" + "src", + "scripts/ga-to-split-autorequire.js" ], "repository": "splitio/javascript-client", "homepage": "https://github.com/splitio/javascript-client#readme", @@ -32,11 +33,11 @@ "node": ">=6" }, "dependencies": { - "@splitsoftware/splitio-commons": "1.5.0", + "@splitsoftware/splitio-commons": "1.6.1", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "ioredis": "^4.28.0", - "js-yaml": "3.13.1", + "js-yaml": "^3.13.1", "node-fetch": "^2.6.7", "unfetch": "^4.2.0" }, @@ -83,6 +84,7 @@ "postbuild-cjs": "cross-env NODE_ENV=cjs node scripts/copy.packages.json.js && ./scripts/build_cjs_replace_imports.sh", "build-umd": "webpack --config webpack.dev.js --env branch=$BUILD_BRANCH --env commit_hash=$BUILD_COMMIT && webpack --config webpack.prod.js --env branch=$BUILD_BRANCH --env commit_hash=$BUILD_COMMIT && ./scripts/clean_umd_build.sh", "build:npm": "rimraf lib es && npm run build-cjs && npm run build-esm", + "build:ga-to-split-autorequire": "terser ./node_modules/@splitsoftware/splitio-commons/src/integrations/ga/autoRequire.js --mangle --output ./scripts/ga-to-split-autorequire.js && cp ./scripts/ga-to-split-autorequire.js umd/ga-to-split-autorequire.js", "build": "rimraf lib umd es && npm run build-cjs && npm run build-esm && npm run build-umd", "check": "npm run check:lint && npm run check:version", "check:lint": "eslint src", diff --git a/scripts/ga-to-split-autorequire.js b/scripts/ga-to-split-autorequire.js new file mode 100644 index 000000000..7f8b88597 --- /dev/null +++ b/scripts/ga-to-split-autorequire.js @@ -0,0 +1 @@ +(function(n,t,e){n[e]=n[e]||t;n[t]=n[t]||function(){n[t].q.push(arguments)};n[t].q=n[t].q||[];var r={};function i(n){return typeof n==="object"&&typeof n.name==="string"&&n.name}function o(e){if(e&&e[0]==="create"){var o=i(e[1])||i(e[2])||i(e[3])||(typeof e[3]==="string"?e[3]:undefined);if(!r[o]){r[o]=true;n[t]((o?o+".":"")+"require","splitTracker")}}}n[t].q.forEach(o);var u=n[t].q.push;n[t].q.push=function(n){var t=u.apply(this,arguments);o(n);return t}})(window,"ga","GoogleAnalyticsObject"); \ No newline at end of file diff --git a/src/__tests__/browser.spec.js b/src/__tests__/browser.spec.js index 254ff0fc7..2c396b437 100644 --- a/src/__tests__/browser.spec.js +++ b/src/__tests__/browser.spec.js @@ -8,10 +8,7 @@ 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 { 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'; @@ -84,7 +81,7 @@ const configInLocalStorage = { streamingEnabled: false }; -tape('## E2E CI Tests ##', function(assert) { +tape('## E2E CI Tests ##', function (assert) { //If we change the mocks, we need to clear localstorage. Cleaning up after testing ensures "fresh data". localStorage.clear(); @@ -96,6 +93,7 @@ tape('## E2E CI Tests ##', function(assert) { fetchMock.get(url(settings, '/mySegments/emmanuel%40split.io'), { status: 200, body: mySegmentsEmmanuel }); fetchMock.post(url(settings, '/testImpressions/bulk'), 200); fetchMock.post(url(settings, '/testImpressions/count'), 200); + Math.random = () => 0.5; // SDKs without telemetry /* Check client evaluations. */ assert.test('E2E / In Memory', evaluationsSuite.bind(null, configInMemory, fetchMock)); @@ -122,9 +120,9 @@ tape('## E2E CI Tests ##', function(assert) { assert.test('E2E / Readiness', readinessSuite.bind(null, fetchMock)); /* Validate headers for ip and hostname are not sended with requests (ignore setting IPAddressesEnabled) */ assert.test('E2E / Ignore setting IPAddressesEnabled', ignoreIpAddressesSettingSuite.bind(null, fetchMock)); - /* Check that impressions and events are sended to backend via Beacon API or Fetch when page unload is triggered. */ - assert.test('E2E / Use Beacon API (or Fetch if not available) to send remaining impressions and events when browser page is unload', useBeaconApiSuite.bind(null, fetchMock)); - assert.test('E2E / Use Beacon API DEBUG (or Fetch if not available) to send remaining impressions and events when browser page is unload', useBeaconDebugApiSuite.bind(null, fetchMock)); + /* Check that impressions and events are sended to backend via Beacon API or Fetch when pagehide/visibilitychange events are triggered. */ + assert.test('E2E / Use Beacon API (or Fetch if not available) to send remaining impressions and events when browser page is unload or hidden', useBeaconApiSuite.bind(null, fetchMock)); + assert.test('E2E / Use Beacon API DEBUG (or Fetch if not available) to send remaining impressions and events when browser page is unload or hidden', useBeaconDebugApiSuite.bind(null, fetchMock)); /* Validate ready from cache behaviour (might be merged into another suite if we end up having simple behavior around it as expected) */ assert.test('E2E / Readiness from cache', readyFromCache.bind(null, fetchMock)); /* Validate readiness with ready promises */ diff --git a/src/__tests__/browserSuites/impressions.debug.spec.js b/src/__tests__/browserSuites/impressions.debug.spec.js index a4b182dce..bbccfc9db 100644 --- a/src/__tests__/browserSuites/impressions.debug.spec.js +++ b/src/__tests__/browserSuites/impressions.debug.spec.js @@ -7,8 +7,8 @@ import { DEBUG } from '@splitsoftware/splitio-commons/src/utils/constants'; import { url } from '../testUtils'; const baseUrls = { - sdk: 'https://sdk.baseurl/impressionsSuite', - events: 'https://events.baseurl/impressionsSuite' + sdk: 'https://sdk.baseurl/impressionsDebugSuite', + events: 'https://events.baseurl/impressionsDebugSuite' }; const settings = settingsFactory({ @@ -83,12 +83,12 @@ export default function (fetchMock, assert) { assert.equal(req.headers.SplitSDKImpressionsMode, DEBUG); assertPayload(req); - client.destroy(); - assert.end(); + client.destroy().then(() => { + assert.end(); + }); return 200; }); - fetchMock.postOnce(url(settings, '/testImpressions/bulk'), 200); client.ready().then(() => { client.getTreatment('split_with_config'); diff --git a/src/__tests__/browserSuites/impressions.spec.js b/src/__tests__/browserSuites/impressions.spec.js index e5ebb5092..8e73c5df2 100644 --- a/src/__tests__/browserSuites/impressions.spec.js +++ b/src/__tests__/browserSuites/impressions.spec.js @@ -84,12 +84,12 @@ export default function (fetchMock, assert) { assert.comment('We do one retry, so after a failed impressions post we will try once more.'); assertPayload(req); - client.destroy(); - assert.end(); + client.destroy().then(() => { + assert.end(); + }); return 200; }); - fetchMock.postOnce(url(settings, '/testImpressions/bulk'), 200); fetchMock.postOnce(url(settings, '/testImpressions/count'), (url, opts) => { const data = JSON.parse(opts.body); @@ -109,7 +109,6 @@ export default function (fetchMock, assert) { return 200; }); - fetchMock.postOnce(url(settings, '/testImpressions/count'), 200); client.ready().then(() => { truncatedTimeFrame = truncateTimeFrame(Date.now()); diff --git a/src/__tests__/browserSuites/push-synchronization-retries.spec.js b/src/__tests__/browserSuites/push-synchronization-retries.spec.js index 5839264e3..03c79554f 100644 --- a/src/__tests__/browserSuites/push-synchronization-retries.spec.js +++ b/src/__tests__/browserSuites/push-synchronization-retries.spec.js @@ -76,9 +76,8 @@ const MILLIS_THIRD_RETRY_FOR_SPLIT_KILL_EVENT = 2000; * (we destroy the client here, to assert that all scheduled tasks are clean) */ export function testSynchronizationRetries(fetchMock, assert) { - // we update the backoff default base, to reduce the time of the test - const ORIGINAL_DEFAULT_BASE_MILLIS = Backoff.DEFAULT_BASE_MILLIS; - Backoff.DEFAULT_BASE_MILLIS = 100; + // Force the backoff base of UpdateWorkers, from 10 secs to 100 ms, to reduce test time + Backoff.__TEST__BASE_MILLIS = 100; assert.plan(17); fetchMock.reset(); @@ -196,7 +195,7 @@ export function testSynchronizationRetries(fetchMock, assert) { setTimeout(() => { Promise.all([otherClientSync.destroy(), client.destroy()]).then(() => { assert.equal(client.getTreatment('whitelist'), 'control', 'evaluation returns control if client is destroyed'); - Backoff.DEFAULT_BASE_MILLIS = ORIGINAL_DEFAULT_BASE_MILLIS; + Backoff.__TEST__BASE_MILLIS = undefined; assert.end(); }); }); diff --git a/src/__tests__/browserSuites/use-beacon-api.debug.spec.js b/src/__tests__/browserSuites/use-beacon-api.debug.spec.js index 4615f1cd0..9d604b494 100644 --- a/src/__tests__/browserSuites/use-beacon-api.debug.spec.js +++ b/src/__tests__/browserSuites/use-beacon-api.debug.spec.js @@ -5,7 +5,7 @@ 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 } from '../testUtils'; -import { triggerUnloadEvent } from '../testUtils/browser'; +import { triggerPagehideEvent, triggerVisibilitychange } from '../testUtils/browser'; const config = { core: { @@ -61,7 +61,7 @@ const assertCallsToBeaconAPI = (assert) => { assertEventSent(assert, parsedPayload.entries[0]); }; -// This E2E test checks that Beacon API is not called when page unload is triggered and there are not events or impressions to send. +// This E2E test checks that Beacon API is not called when page is hidden and there are not events or impressions to send. function beaconApiNotSendTestDebug(fetchMock, assert) { sendBeaconSpyDebug = sinon.spy(window.navigator, 'sendBeacon'); @@ -75,8 +75,9 @@ function beaconApiNotSendTestDebug(fetchMock, assert) { const client = splitio.client(); client.on(client.Event.SDK_READY, () => { - // trigger unload event, without tracked events and impressions - triggerUnloadEvent(); + // trigger events, without tracked events and impressions + triggerPagehideEvent(); + triggerVisibilitychange(); // destroy the client and execute the next E2E test named beaconApiSendTest setTimeout(() => { @@ -90,7 +91,7 @@ function beaconApiNotSendTestDebug(fetchMock, assert) { }); } -// This E2E test checks that impressions and events are sent to backend via Beacon API when page unload is triggered. +// This E2E test checks that impressions and events are sent to backend via Beacon API when page is hidden. function beaconApiSendTestDebug(fetchMock, assert) { // Init and run Split client @@ -100,8 +101,8 @@ function beaconApiSendTestDebug(fetchMock, assert) { client.getTreatment('hierarchical_splits_test'); client.track('sometraffictype', 'someEvent', 10); - // trigger unload event inmmediatly, before scheduled push of events and impressions - triggerUnloadEvent(); + // trigger visibilitychange event inmmediatly, before scheduled push of events and impressions + triggerVisibilitychange(); // queue the assertion of the Beacon requests, destroy the client and execute the next E2E test named fallbackTest setTimeout(() => { @@ -115,7 +116,7 @@ function beaconApiSendTestDebug(fetchMock, assert) { }); } -// This E2E test checks that impressions and events are sent to backend via Axios when page unload is triggered and Beacon API is not available. +// This E2E test checks that impressions and events are sent to backend via Fetch API when page is hidden and Beacon API is not available. function fallbackTest(fetchMock, assert) { // destroy reference to Beacon API @@ -136,7 +137,7 @@ function fallbackTest(fetchMock, assert) { }, 0); })(); - // Mock endpoints used by Axios + // Mock endpoints fetchMock.postOnce(url(settings, '/testImpressions/bulk'), (url, opts) => { const resp = JSON.parse(opts.body); assert.ok(opts, 'Fallback to /testImpressions/bulk'); @@ -155,8 +156,8 @@ function fallbackTest(fetchMock, assert) { client.on(client.Event.SDK_READY, () => { client.getTreatment('hierarchical_splits_test'); client.track('sometraffictype', 'someEvent', 10); - // trigger unload event inmmediatly, before scheduled push of events and impressions - triggerUnloadEvent(); + // trigger pagehide event inmmediatly, before scheduled push of events and impressions + triggerPagehideEvent(); }); } diff --git a/src/__tests__/browserSuites/use-beacon-api.spec.js b/src/__tests__/browserSuites/use-beacon-api.spec.js index f6c6505fa..d36ceaf45 100644 --- a/src/__tests__/browserSuites/use-beacon-api.spec.js +++ b/src/__tests__/browserSuites/use-beacon-api.spec.js @@ -5,7 +5,7 @@ import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; import mySegmentsFacundo from '../mocks/mysegments.facundo@split.io.json'; import { url } from '../testUtils'; import { OPTIMIZED } from '@splitsoftware/splitio-commons/src/utils/constants'; -import { triggerUnloadEvent } from '../testUtils/browser'; +import { triggerPagehideEvent, triggerVisibilitychange } from '../testUtils/browser'; const config = { core: { @@ -73,7 +73,7 @@ const assertCallsToBeaconAPI = (assert) => { assertImpressionsCountSent(assert, parsedPayload.entries); }; -// This E2E test checks that Beacon API is not called when page unload is triggered and there are not events or impressions to send. +// This E2E test checks that Beacon API is not called when page is hidden and there are not events or impressions to send. function beaconApiNotSendTest(fetchMock, assert) { sendBeaconSpy = sinon.spy(window.navigator, 'sendBeacon'); @@ -87,8 +87,9 @@ function beaconApiNotSendTest(fetchMock, assert) { const client = splitio.client(); client.on(client.Event.SDK_READY, () => { - // trigger unload event, without tracked events and impressions - triggerUnloadEvent(); + // trigger events, without tracked events and impressions + triggerPagehideEvent(); + triggerVisibilitychange(); // destroy the client and execute the next E2E test named beaconApiSendTest setTimeout(() => { @@ -102,7 +103,7 @@ function beaconApiNotSendTest(fetchMock, assert) { }); } -// This E2E test checks that impressions and events are sent to backend via Beacon API when page unload is triggered. +// This E2E test checks that impressions and events are sent to backend via Beacon API when page is hidden. function beaconApiSendTest(fetchMock, assert) { // Init and run Split client @@ -112,8 +113,9 @@ function beaconApiSendTest(fetchMock, assert) { client.getTreatment('hierarchical_splits_test'); client.track('sometraffictype', 'someEvent', 10); - // trigger unload event inmmediatly, before scheduled push of events and impressions - triggerUnloadEvent(); + // trigger both events inmmediatly, before scheduled push of events and impressions, to assert that beacon requests are not duplicated + triggerPagehideEvent(); + triggerVisibilitychange(); // queue the assertion of the Beacon requests, destroy the client and execute the next E2E test named fallbackTest setTimeout(() => { @@ -127,7 +129,7 @@ function beaconApiSendTest(fetchMock, assert) { }); } -// This E2E test checks that impressions and events are sent to backend via Axios when page unload is triggered and Beacon API is not available. +// This E2E test checks that impressions and events are sent to backend via Fetch API when page is hidden and Beacon API is not available. function fallbackTest(fetchMock, assert) { // destroy reference to Beacon API @@ -149,7 +151,7 @@ function fallbackTest(fetchMock, assert) { }, 0); })(); - // Mock endpoints used by Axios + // Mock endpoints fetchMock.postOnce(url(settings, '/testImpressions/bulk'), (url, opts) => { const resp = JSON.parse(opts.body); assert.ok(opts, 'Fallback to /testImpressions/bulk'); @@ -175,8 +177,9 @@ function fallbackTest(fetchMock, assert) { client.on(client.Event.SDK_READY, () => { client.getTreatment('hierarchical_splits_test'); client.track('sometraffictype', 'someEvent', 10); - // trigger unload event inmmediatly, before scheduled push of events and impressions - triggerUnloadEvent(); + // trigger both events inmmediatly, before scheduled push of events and impressions, to assert that POST requests are not duplicated + triggerPagehideEvent(); + triggerVisibilitychange(); }); } diff --git a/src/__tests__/browserSuites/user-consent.spec.js b/src/__tests__/browserSuites/user-consent.spec.js index 32ecd90c4..7dd3cad9e 100644 --- a/src/__tests__/browserSuites/user-consent.spec.js +++ b/src/__tests__/browserSuites/user-consent.spec.js @@ -1,6 +1,6 @@ import sinon from 'sinon'; import { SplitFactory } from '../../'; -import { triggerUnloadEvent } from '../testUtils/browser'; +import { triggerPagehideEvent } from '../testUtils/browser'; import { nearlyEqual, url } from '../testUtils'; const trackedImpressions = []; @@ -97,9 +97,9 @@ export default function userConsent(fetchMock, t) { ], ['on', 'on', 'on', 'on', 'on', 'on', 'on', 'on'], 'evaluating on SDK ready'); if (isTracking) expectedTrackedImpressions += 8; - // Trigger unload event to validate browser listener behaviour + // Trigger pagehide event to validate browser listener behaviour // Beacon API is used only if user consent is GRANTED - triggerUnloadEvent(); + triggerPagehideEvent(); if (factory.UserConsent.getStatus() === factory.UserConsent.Status.GRANTED) { assert.ok(sendBeaconSpy.calledThrice, 'sendBeacon should have been called thrice'); } else { diff --git a/src/__tests__/destroy/browser.spec.js b/src/__tests__/destroy/browser.spec.js index e61e9aaf0..167ca78fc 100644 --- a/src/__tests__/destroy/browser.spec.js +++ b/src/__tests__/destroy/browser.spec.js @@ -20,10 +20,10 @@ const settings = settingsFactory({ fetchMock.getOnce(url(settings, '/splitChanges?since=-1'), { status: 200, body: splitChangesMock1 }); fetchMock.getOnce(url(settings, '/splitChanges?since=-1500492097547'), { status: 200, body: splitChangesMock2 }); - fetchMock.getOnce(url(settings, '/mySegments/ut1'), { status: 200, body: mySegmentsMock }); fetchMock.getOnce(url(settings, '/mySegments/ut2'), { status: 200, body: mySegmentsMock }); fetchMock.getOnce(url(settings, '/mySegments/ut3'), { status: 200, body: mySegmentsMock }); +fetchMock.postOnce(url(settings, '/v1/metrics/config'), 200); // 0.1% sample rate tape('SDK destroy for BrowserJS', async function (assert) { const config = { diff --git a/src/__tests__/gaIntegration/browser.spec.js b/src/__tests__/gaIntegration/browser.spec.js index 02d96ae94..6d61aea16 100644 --- a/src/__tests__/gaIntegration/browser.spec.js +++ b/src/__tests__/gaIntegration/browser.spec.js @@ -16,10 +16,11 @@ const settings = settingsFactory({ } }); -tape('## E2E CI Tests ##', function(assert) { +tape('## E2E CI Tests ##', function (assert) { fetchMock.get(url(settings, '/splitChanges?since=-1'), { status: 200, body: splitChangesMock1 }); fetchMock.get(url(settings, '/mySegments/facundo%40split.io'), { status: 200, body: mySegmentsFacundo }); + fetchMock.post(/\/v1\/metrics/, 200); // 0.1% sample rate /* Validate GA integration */ assert.test('E2E / GA-to-Split', gaToSplitSuite.bind(null, fetchMock)); diff --git a/src/__tests__/gaIntegration/ga-to-split.spec.js b/src/__tests__/gaIntegration/ga-to-split.spec.js index 350d6627b..747f80642 100644 --- a/src/__tests__/gaIntegration/ga-to-split.spec.js +++ b/src/__tests__/gaIntegration/ga-to-split.spec.js @@ -412,4 +412,51 @@ export default function (fetchMock, assert) { }); -} \ No newline at end of file + // test 'autoRequire' script placed right after GA script tag. + // We get same result if it is placed right before, and also applies for Universal Analytics configured with GTM and gtag.js tags. + // If it is executed asynchronously, trackers creation might be missed. + assert.test(t => { + fetchMock.postOnce(url(settings, '/events/bulk'), (url, opts) => { + const resp = JSON.parse(opts.body); + const sentHitsTracker1 = window.gaSpy.getHits('tracker1'); + const sentHitsTracker2 = window.gaSpy.getHits('tracker2'); + + t.equal(resp.length, sentHitsTracker1.length + sentHitsTracker2.length, 'All hits of all trackers are captured as Split events'); + + setTimeout(() => { + client.destroy(); + t.end(); + }); + return 200; + }); + + gaTag(); + + // Run autoRequire iife: + // require('@splitsoftware/splitio-commons/src/integrations/ga/autoRequire'); + require('../../../scripts/ga-to-split-autorequire'); + + window.ga('create', 'UA-00000000-1', { name: 'tracker1', cookieDomain: 'auto', siteSpeedSampleRate: 0 }); + + gaSpy(['tracker1']); + + window.ga('tracker1.send', 'event', 'mycategory', 'myaction1'); // Captured + + const factory = SplitFactory({ + ...config, + integrations: [{ + type: 'GOOGLE_ANALYTICS_TO_SPLIT', + autoRequire: true + }], + }); + + window.ga('tracker1.send', 'event', 'mycategory', 'myaction2'); // Captured + window.ga('create', 'UA-00000001-1', 'auto', 'tracker2', { siteSpeedSampleRate: 0 }); // New tracker + gaSpy(['tracker2'], false); + window.ga('tracker2.send', 'event', 'mycategory', 'myaction3'); // Captured + + client = factory.client(); + + }); + +} diff --git a/src/__tests__/gaIntegration/gaTestUtils.js b/src/__tests__/gaIntegration/gaTestUtils.js index 2f607034c..dd8ba5ac0 100644 --- a/src/__tests__/gaIntegration/gaTestUtils.js +++ b/src/__tests__/gaIntegration/gaTestUtils.js @@ -2,19 +2,21 @@ export const DEFAULT_TRACKER = 't0'; const HIT_FIELDS = ['hitType', 'nonInteraction']; const EVENT_FIELDS = ['eventCategory', 'eventAction', 'eventLabel', 'eventValue']; +const FIELDS = [...HIT_FIELDS, ...EVENT_FIELDS]; // List of hit fields to spy, which are the ones set by the default SplitToGa mapper. + +let hits = {}; /** * Spy ga hits per tracker. * * @param {string[]} trackerNames names of the trackers to spy. If not provided, it spies the default tracker. i.e., `gaSpy()` is equivalent to `gaSpy(['t0'])`. - * @param {string[]} fieldNames names of the hit fields to spy. If not provided, it spies hit and event related fields. i.e., 'hitType', 'nonInteraction', - * 'eventCategory', 'eventAction', 'eventLabel', and 'eventValue', which are the ones set by default SplitToGa mapper. + * @param {boolean} resetSpy true to reset the list of captured hits. * * @see {@link https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference} */ -export function gaSpy(trackerNames = [DEFAULT_TRACKER], fieldNames = [...HIT_FIELDS, ...EVENT_FIELDS]) { +export function gaSpy(trackerNames = [DEFAULT_TRACKER], resetSpy = true) { - const hits = {}; + if (resetSpy) hits = {}; // access ga via its gaAlias, accounting for the possibility that the global command queue // has been renamed or not yet defined (analytics.js mutates window[gaAlias] reference) @@ -31,7 +33,7 @@ export function gaSpy(trackerNames = [DEFAULT_TRACKER], fieldNames = [...HIT_FIE trackerToSniff.set('sendHitTask', function (model) { originalSendHitTask(model); const hit = {}; - fieldNames.forEach(fieldName => { + FIELDS.forEach(fieldName => { hit[fieldName] = model.get(fieldName); }); hits[trackerName].push(hit); diff --git a/src/__tests__/nodeSuites/impressions.debug.spec.js b/src/__tests__/nodeSuites/impressions.debug.spec.js index 2005d96c4..5c10f8065 100644 --- a/src/__tests__/nodeSuites/impressions.debug.spec.js +++ b/src/__tests__/nodeSuites/impressions.debug.spec.js @@ -6,8 +6,8 @@ import { DEBUG } from '@splitsoftware/splitio-commons/src/utils/constants'; import { url } from '../testUtils'; const baseUrls = { - sdk: 'https://sdk.baseurl/impressionsSuite', - events: 'https://events.baseurl/impressionsSuite' + sdk: 'https://sdk.baseurl/impressionsDebugSuite', + events: 'https://events.baseurl/impressionsDebugSuite' }; const settings = settingsFactory({ @@ -88,7 +88,6 @@ export default async function (key, fetchMock, assert) { return 200; }); - fetchMock.postOnce(url(settings, '/testImpressions/bulk'), 200); splitio.Logger.enable(); evaluationsStart = Date.now(); diff --git a/src/__tests__/nodeSuites/impressions.spec.js b/src/__tests__/nodeSuites/impressions.spec.js index 21324b560..098aefdf2 100644 --- a/src/__tests__/nodeSuites/impressions.spec.js +++ b/src/__tests__/nodeSuites/impressions.spec.js @@ -101,7 +101,6 @@ export default async function(key, fetchMock, assert) { return 200; }); - fetchMock.postOnce(url(settings, '/testImpressions/bulk'), 200); fetchMock.postOnce(url(settings, '/testImpressions/count'), (url, opts) => { const data = JSON.parse(opts.body); diff --git a/src/__tests__/nodeSuites/push-synchronization-retries.spec.js b/src/__tests__/nodeSuites/push-synchronization-retries.spec.js index fce201003..3e71548d6 100644 --- a/src/__tests__/nodeSuites/push-synchronization-retries.spec.js +++ b/src/__tests__/nodeSuites/push-synchronization-retries.spec.js @@ -70,9 +70,8 @@ const MILLIS_THIRD_RETRY_FOR_SPLIT_KILL_EVENT = 2000; * (we destroy the client here, to assert that all scheduled tasks are clean) */ export function testSynchronizationRetries(fetchMock, assert) { - // we update the backoff default base, to reduce the time of the test - const ORIGINAL_DEFAULT_BASE_MILLIS = Backoff.DEFAULT_BASE_MILLIS; - Backoff.DEFAULT_BASE_MILLIS = 100; + // Force the backoff base of UpdateWorkers, from 10 secs to 100 ms, to reduce test time + Backoff.__TEST__BASE_MILLIS = 100; assert.plan(19); fetchMock.reset(); @@ -201,7 +200,7 @@ export function testSynchronizationRetries(fetchMock, assert) { setTimeout(() => { client.destroy().then(() => { assert.equal(client.getTreatment(key, 'whitelist'), 'control', 'evaluation returns control if client is destroyed'); - Backoff.DEFAULT_BASE_MILLIS = ORIGINAL_DEFAULT_BASE_MILLIS; + Backoff.__TEST__BASE_MILLIS = undefined; assert.end(); }); }); diff --git a/src/__tests__/nodeSuites/push-synchronization.spec.js b/src/__tests__/nodeSuites/push-synchronization.spec.js index d4857c8f8..e3e078237 100644 --- a/src/__tests__/nodeSuites/push-synchronization.spec.js +++ b/src/__tests__/nodeSuites/push-synchronization.spec.js @@ -128,7 +128,7 @@ export function testSynchronization(fetchMock, assert) { assert.equal(sdkUpdateCount, 5, 'SDK_UPDATE should be emitted 5 times'); assert.end(); }); - }, MILLIS_DESTROY); // destroy client after 0.6 seconds + }, MILLIS_DESTROY); // destroy client after 0.7 seconds }); // initial auth diff --git a/src/__tests__/push/browser.spec.js b/src/__tests__/push/browser.spec.js index d06c014e7..ecef8596c 100644 --- a/src/__tests__/push/browser.spec.js +++ b/src/__tests__/push/browser.spec.js @@ -9,6 +9,7 @@ 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 tape('## Browser JS - E2E CI Tests for PUSH ##', function (assert) { diff --git a/src/__tests__/testUtils/browser.js b/src/__tests__/testUtils/browser.js index b185e93da..60284923d 100644 --- a/src/__tests__/testUtils/browser.js +++ b/src/__tests__/testUtils/browser.js @@ -1,7 +1,19 @@ -// Util method to trigger 'unload' DOM event -export function triggerUnloadEvent() { +function triggerEvent(eventName) { const event = document.createEvent('HTMLEvents'); - event.initEvent('unload', true, true); - event.eventName = 'unload'; + 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/settings/defaults/version.js b/src/settings/defaults/version.js index ac6e8280b..bfb2bd5ce 100644 --- a/src/settings/defaults/version.js +++ b/src/settings/defaults/version.js @@ -1 +1 @@ -export const packageVersion = '10.20.0'; +export const packageVersion = '10.21.0'; diff --git a/src/settings/node.js b/src/settings/node.js index 00a9a6d45..2dc97cb73 100644 --- a/src/settings/node.js +++ b/src/settings/node.js @@ -1,6 +1,6 @@ import { settingsValidation } from '@splitsoftware/splitio-commons/src/utils/settingsValidation'; import { validateLogger } from '@splitsoftware/splitio-commons/src/utils/settingsValidation/logger/builtinLogger'; -import { LocalhostFromFile } from '@splitsoftware/splitio-commons/src/sync/offline/LocalhostFromFile'; +import { LocalhostFromFile } from '../sync/offline/LocalhostFromFile'; import { defaults } from './defaults/node'; import { validateStorage } from './storage/node'; diff --git a/src/sync/offline/LocalhostFromFile.js b/src/sync/offline/LocalhostFromFile.js new file mode 100644 index 000000000..3f63173ac --- /dev/null +++ b/src/sync/offline/LocalhostFromFile.js @@ -0,0 +1,11 @@ +import { splitsParserFromFileFactory } from './splitsParserFromFile'; +import { syncManagerOfflineFactory } from '@splitsoftware/splitio-commons/src/sync/offline/syncManagerOffline'; + +// Singleton instance of the factory function for offline SyncManager from YAML file (a.k.a. localhostFromFile) +// It uses NodeJS APIs. +const localhostFromFile = syncManagerOfflineFactory(splitsParserFromFileFactory); +localhostFromFile.type = 'LocalhostFromFile'; + +export function LocalhostFromFile() { + return localhostFromFile; +} diff --git a/src/sync/offline/splitsParserFromFile.js b/src/sync/offline/splitsParserFromFile.js new file mode 100644 index 000000000..ef25e8e14 --- /dev/null +++ b/src/sync/offline/splitsParserFromFile.js @@ -0,0 +1,172 @@ +import fs from 'fs'; +import path from 'path'; +import yaml from 'js-yaml'; +import { isString, endsWith, find, forOwn, uniq, } from '@splitsoftware/splitio-commons/src/utils/lang'; +import { parseCondition } from '@splitsoftware/splitio-commons/src/sync/offline/splitsParser/parseCondition'; + +const logPrefix = 'sync:offline:splits-fetcher: '; + +const DEFAULT_FILENAME = '.split'; + +function configFilesPath(configFilePath) { + if (configFilePath === DEFAULT_FILENAME || !isString(configFilePath)) { + let root = process.env.HOME; + + if (process.env.SPLIT_CONFIG_ROOT) root = process.env.SPLIT_CONFIG_ROOT; + + if (!root) throw new Error('Missing split mock configuration root.'); + + configFilePath = path.join(root, DEFAULT_FILENAME); + } + + // Validate the extensions + if (!(endsWith(configFilePath, '.yaml', true) || endsWith(configFilePath, '.yml', true) || endsWith(configFilePath, '.split', true))) + throw new Error(`Invalid extension specified for Splits mock file. Accepted extensions are ".yml" and ".yaml". Your specified file is ${configFilePath}`); + + if (!fs.existsSync(configFilePath)) + throw new Error(`Split configuration not found in ${configFilePath} - Please review your Split file location.`); + + return configFilePath; +} + +// This function is not pure nor meant to be. Here we apply modifications to cover +// for behaviour that's ensured by the BE. +function arrangeConditions(mocksData) { + // Iterate through each Split data + forOwn(mocksData, data => { + const conditions = data.conditions; + + // On the manager, as the split jsons come with all treatments on the partitions prop, + // we'll add all the treatments to the first condition. + const firstRolloutCondition = find(conditions, cond => cond.conditionType === 'ROLLOUT'); + // Malformed mocks may have + const treatments = uniq(data.treatments); + // If they're only specifying a whitelist we add the treatments there. + const allTreatmentsCondition = firstRolloutCondition ? firstRolloutCondition : conditions[0]; + + const fullyAllocatedTreatment = allTreatmentsCondition.partitions[0].treatment; + + treatments.forEach(treatment => { + if (treatment !== fullyAllocatedTreatment) { + allTreatmentsCondition.partitions.push({ + treatment, size: 0 + }); + } + }); + + // Don't need these anymore + delete data.treatments; + }); +} + +export function splitsParserFromFileFactory() { + + let previousMock = 'NO_MOCK_LOADED'; + + // Parse `.split` configuration file and return a map of "Split Objects" + function readSplitConfigFile(log, filePath) { + const SPLIT_POSITION = 0; + const TREATMENT_POSITION = 1; + let data; + + try { + data = fs.readFileSync(filePath, 'utf-8'); + } catch (e) { + log.error(e && e.message); + + return {}; + } + + if (data === previousMock) return false; + previousMock = data; + + const splitObjects = data.split(/\r?\n/).reduce((accum, line, index) => { + let tuple = line.trim(); + + if (tuple === '' || tuple.charAt(0) === '#') { + log.debug(logPrefix + `Ignoring empty line or comment at #${index}`); + } else { + tuple = tuple.split(/\s+/); + + if (tuple.length !== 2) { + log.debug(logPrefix + `Ignoring line since it does not have exactly two columns #${index}`); + } else { + const splitName = tuple[SPLIT_POSITION]; + const condition = parseCondition({ treatment: tuple[TREATMENT_POSITION] }); + accum[splitName] = { conditions: [condition], configurations: {}, trafficTypeName: 'localhost' }; + } + } + + return accum; + }, {}); + + return splitObjects; + } + + // Parse `.yml` or `.yaml` configuration files and return a map of "Split Objects" + function readYAMLConfigFile(log, filePath) { + let data = ''; + let yamldoc = null; + + try { + data = fs.readFileSync(filePath, 'utf8'); + + if (data === previousMock) return false; + previousMock = data; + + yamldoc = yaml.safeLoad(data); + } catch (e) { + log.error(e); + + return {}; + } + + // Each entry will be mapped to a condition, but we'll also keep the configurations map. + const mocksData = (yamldoc).reduce((accum, splitEntry) => { + const splitName = Object.keys(splitEntry)[0]; + + if (!splitName || !isString(splitEntry[splitName].treatment)) + log.error(logPrefix + 'Ignoring entry on YAML since the format is incorrect.'); + + const mockData = splitEntry[splitName]; + + // "Template" for each split accumulated data + if (!accum[splitName]) { + accum[splitName] = { + configurations: {}, conditions: [], treatments: [], trafficTypeName: 'localhost' + }; + } + + // Assign the config if there is one on the mock + if (mockData.config) accum[splitName].configurations[mockData.treatment] = mockData.config; + // Parse the condition from the entry. + const condition = parseCondition(mockData); + accum[splitName].conditions[condition.conditionType === 'ROLLOUT' ? 'push' : 'unshift'](condition); + // Also keep track of the treatments, will be useful for manager functionality. + accum[splitName].treatments.push(mockData.treatment); + + return accum; + }, {}); + + arrangeConditions(mocksData); + + return mocksData; + } + + // Load the content of a configuration file into an Object + return function splitsParserFromFile({ features, log }) { + const filePath = configFilesPath(features); + let mockData; + + // If we have a filePath, it means the extension is correct, choose the parser. + if (endsWith(filePath, '.split')) { + log.warn(logPrefix + '.split mocks will be deprecated soon in favor of YAML files, which provide more targeting power. Take a look in our documentation.'); + mockData = readSplitConfigFile(log, filePath); + } else { + mockData = readYAMLConfigFile(log, filePath); + } + + return mockData; + }; + +} diff --git a/ts-tests/index.ts b/ts-tests/index.ts index 27926f4fc..31d3416c7 100644 --- a/ts-tests/index.ts +++ b/ts-tests/index.ts @@ -456,6 +456,7 @@ let customGoogleAnalyticsToSplitConfig: SplitIO.IGoogleAnalyticsToSplitConfig = mapper: function (model: UniversalAnalytics.Model, defaultMapping: SplitIO.EventData): SplitIO.EventData { return eventDataSample; }, prefix: 'PREFIX', identities: [{ key: 'key1', trafficType: 'tt1' }, { key: 'key2', trafficType: 'tt2' }], + autoRequire: true }; let customSplitToGoogleAnalyticsConfig: SplitIO.ISplitToGoogleAnalyticsConfig = { type: 'SPLIT_TO_GOOGLE_ANALYTICS', diff --git a/types/splitio.d.ts b/types/splitio.d.ts index 7f138a353..eff7d5850 100644 --- a/types/splitio.d.ts +++ b/types/splitio.d.ts @@ -716,7 +716,7 @@ declare namespace SplitIO { /** * Enable 'Google Analytics to Split' integration, to track Google Analytics hits as Split events. * - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#integrations} + * @see {@link https://help.split.io/hc/en-us/articles/360040838752#google-analytics-to-split} */ interface IGoogleAnalyticsToSplitConfig { type: 'GOOGLE_ANALYTICS_TO_SPLIT', @@ -758,6 +758,17 @@ declare namespace SplitIO { * If not provided, events are sent using the key and traffic type provided at SDK config */ identities?: Identity[], + /** + * Optional flag to log an error if the `auto-require` script is not detected. + * The auto-require script automatically requires the `splitTracker` plugin for created trackers, + * and should be placed right after your Google Analytics, Google Tag Manager or gtag.js script tag. + * + * @see {@link https://help.split.io/hc/en-us/articles/360040838752#set-up-with-gtm-and-gtag.js} + * + * @property {boolean} autoRequire + * @default false + */ + autoRequire?: boolean, } /** * Object representing the data sent by Split (events and impressions). @@ -769,7 +780,7 @@ declare namespace SplitIO { /** * Enable 'Split to Google Analytics' integration, to track Split impressions and events as Google Analytics hits. * - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#integrations} + * @see {@link https://help.split.io/hc/en-us/articles/360040838752#split-to-google-analytics} */ interface ISplitToGoogleAnalyticsConfig { type: 'SPLIT_TO_GOOGLE_ANALYTICS',