From 1c26978581b11ec4372731c3297bb0d15c72278f Mon Sep 17 00:00:00 2001 From: Scotty Jacobson Date: Tue, 5 Feb 2019 16:21:10 -0500 Subject: [PATCH 01/11] add eventMethod option 'beacon:text/plain', which uses navigator.sendBeacon but lies about the Content-Type and makes it `text/plain` to get around Chrome limitations --- src/js/out_queue.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/js/out_queue.js b/src/js/out_queue.js index e1f021427..a8fcdc5d2 100644 --- a/src/js/out_queue.js +++ b/src/js/out_queue.js @@ -68,12 +68,16 @@ //Force to lower case if its a string eventMethod = eventMethod.toLowerCase ? eventMethod.toLowerCase() : eventMethod; - - // Use the Beacon API if eventMethod is set null, true, or 'beacon'. - var isBeaconRequested = (eventMethod === null) || (eventMethod === true) || (eventMethod === "beacon") || (eventMethod === "true"); + + // Use the Beacon API if eventMethod is set null, true, or starts with 'beacon'. + var isBeaconRequested = (eventMethod === null) || + (eventMethod === true) || + (eventMethod.indexOf("beacon") === 0) || + (eventMethod === "true"); // Fall back to POST or GET for browsers which don't support Beacon API var isBeaconAvailable = Boolean(isBeaconRequested && navigator && navigator.sendBeacon); var useBeacon = (isBeaconAvailable && isBeaconRequested); + var beaconTextPlain = eventMethod === 'beacon:text/plain'; // (dishonestly) set beacon headers to Content-Type: text/plain // Use GET if specified var isGetRequested = (eventMethod === "get"); @@ -294,7 +298,12 @@ var beaconStatus; if (useBeacon) { - const headers = { type: 'application/json' }; + let headers = {}; + if (beaconTextPlain) { + headers.type = 'text/plain'; + } else { + headers.type = 'application/json'; + } const blob = new Blob([encloseInPayloadDataEnvelope(attachStmToEvent(batch))], headers); try { beaconStatus = navigator.sendBeacon(configCollectorUrl, blob); From b566f16ec877d4a8dfedfe7f302cdb91eb74552e Mon Sep 17 00:00:00 2001 From: Scotty Jacobson Date: Wed, 6 Feb 2019 19:32:51 -0500 Subject: [PATCH 02/11] During beforeunload, set bufferSize to 1, so that any events sent afterwards don't get buffered and are immediately sent --- src/js/out_queue.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/js/out_queue.js b/src/js/out_queue.js index 99b243741..24b7e5952 100644 --- a/src/js/out_queue.js +++ b/src/js/out_queue.js @@ -119,6 +119,8 @@ if (!executingQueue) { executeQueue(); } + // Any subsequent events (during beforeunload / unload) should be sent immediately so they don't get buffered and ignored + bufferSize = 1; }); } From 1e45c28acd67f740a66bbdf109559af5212bd69c Mon Sep 17 00:00:00 2001 From: Scotty Jacobson Date: Tue, 5 Mar 2019 18:10:50 -0500 Subject: [PATCH 03/11] iOS workaround: listen to `pagehide` as well as `beforeunload` so that we can flush our out queue on iOS (beforeunload doesn't seem to exist on iOS) --- src/js/snowplow.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/js/snowplow.js b/src/js/snowplow.js index 008976a8f..87f8486fe 100644 --- a/src/js/snowplow.js +++ b/src/js/snowplow.js @@ -259,6 +259,7 @@ // initialize the Snowplow singleton helpers.addEventListener(windowAlias, 'beforeunload', beforeUnloadHandler, false); + helpers.addEventListener(windowAlias, 'pagehide', beforeUnloadHandler, false); addReadyListener(); // Now replace initialization array with queue manager object From 8bee92c60338aecd227dd45f1cf16c2e506dd488 Mon Sep 17 00:00:00 2001 From: Scotty Jacobson Date: Thu, 7 Mar 2019 15:58:56 -0500 Subject: [PATCH 04/11] Workaround attempt for iOS webviews' rejecting HTTPS beacon calls unless an XHR request has been sent to that address --- src/js/lib/beacon_ios_workaround.js | 75 +++++++++++++++++++++++++++++ src/js/tracker.js | 2 + 2 files changed, 77 insertions(+) create mode 100644 src/js/lib/beacon_ios_workaround.js diff --git a/src/js/lib/beacon_ios_workaround.js b/src/js/lib/beacon_ios_workaround.js new file mode 100644 index 000000000..602d8920f --- /dev/null +++ b/src/js/lib/beacon_ios_workaround.js @@ -0,0 +1,75 @@ +/* + * JavaScript tracker for Snowplow: detectors.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +; +(function () { + + var object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support + + var navigatorAlias = navigator; + + var collectorUrl; + + // It also looks like, every time we switch away from the tab and come back + // We need to "unlock" the connection again when we return and the tab is visible + document.addEventListener('visibilitychange', function () { + if (!document.hidden) { + doUnlockBeacon(); + } + }); + + object.unlockBeacon = function unlockBeacon(_collectorUrl) { + // We get passed collectorUrl as soon as we have the URL + if (_collectorUrl) { + collectorUrl = _collectorUrl; + } + + // TODO: only if webview? (is that possible without deep user agent parsing?) + var iOS = !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform); + + if (!iOS || !collectorUrl) { + return; + } + + // Send a dummy HTTPS request to get around the iOS WebView beacon first-https-connection bug + // https://robertsahlin.com/analytics-beacon-transport-mechanism-gotchas/ + // https://bugs.webkit.org/show_bug.cgi?id=193508 + var xhr = new XMLHttpRequest(); + xhr.open('POST', collectorUrl + '/com.snowplowanalytics/tp2', true); + xhr.setRequestHeader('Content-Type', 'text/plain'); + xhr.withCredentials = true; + xhr.send('unlockBeacon'); + } + +}()); diff --git a/src/js/tracker.js b/src/js/tracker.js index 3ab6bb092..b6851e518 100755 --- a/src/js/tracker.js +++ b/src/js/tracker.js @@ -45,6 +45,7 @@ links = require('./links'), forms = require('./forms'), errors = require('./errors'), + beaconiOSWorkaround = require('./lib/beacon_ios_workaround'), requestQueue = require('./out_queue'), coreConstructor = require('snowplow-tracker-core').trackerCore, productionize = require('./guard').productionize, @@ -2138,6 +2139,7 @@ */ apiMethods.setCollectorUrl = function (rawUrl) { configCollectorUrl = asCollectorUrl(rawUrl); + beaconiOSWorkaround.unlockBeacon(configCollectorUrl); }; /** From 563330e48058ab78c734328a38e9e05f41c913ad Mon Sep 17 00:00:00 2001 From: Scotty Jacobson Date: Thu, 7 Mar 2019 16:12:10 -0500 Subject: [PATCH 05/11] package.json to allow building + importing with npm into (client-side) lib --- package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/package.json b/package.json index 90f9ab91c..f5898e79c 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,11 @@ "type": "git", "url": "https://github.com/snowplow/snowplow-javascript-tracker.git" }, + "scripts": { + "build": "grunt", + "prepare": "npm run build" + }, + "main": "dist/bundle-postbabel", "bugs": "https://github.com/snowplow/snowplow-javascript-tracker/issues", "keywords": [ "tracking", From c43a70ff718eb377161cb31399d974494a6a61af Mon Sep 17 00:00:00 2001 From: Scotty Jacobson Date: Tue, 12 Mar 2019 14:01:38 -0400 Subject: [PATCH 06/11] fix bug (misnamed function) in unlockbeacon after 'visibilitychange' callback --- src/js/lib/beacon_ios_workaround.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/js/lib/beacon_ios_workaround.js b/src/js/lib/beacon_ios_workaround.js index 602d8920f..edc510f8d 100644 --- a/src/js/lib/beacon_ios_workaround.js +++ b/src/js/lib/beacon_ios_workaround.js @@ -45,11 +45,11 @@ // We need to "unlock" the connection again when we return and the tab is visible document.addEventListener('visibilitychange', function () { if (!document.hidden) { - doUnlockBeacon(); + unlockBeacon(); } }); - object.unlockBeacon = function unlockBeacon(_collectorUrl) { + function unlockBeacon(_collectorUrl) { // We get passed collectorUrl as soon as we have the URL if (_collectorUrl) { collectorUrl = _collectorUrl; @@ -72,4 +72,6 @@ xhr.send('unlockBeacon'); } + object.unlockBeacon = unlockBeacon; + }()); From 233eb05ff6e6287db5aca893e84e72516a64d62b Mon Sep 17 00:00:00 2001 From: Scotty Jacobson Date: Tue, 12 Mar 2019 19:58:04 -0400 Subject: [PATCH 07/11] iOS workaround: send a POST XHR request before _every_ sendBeacon call this is more heavy-handed than we'd like and will pollute our bad-events stream; i'll tweak and find the exact times i need to make this request --- src/js/lib/beacon_ios_workaround.js | 2 +- src/js/out_queue.js | 2 ++ src/js/tracker.js | 2 -- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/js/lib/beacon_ios_workaround.js b/src/js/lib/beacon_ios_workaround.js index edc510f8d..e398226ad 100644 --- a/src/js/lib/beacon_ios_workaround.js +++ b/src/js/lib/beacon_ios_workaround.js @@ -66,7 +66,7 @@ // https://robertsahlin.com/analytics-beacon-transport-mechanism-gotchas/ // https://bugs.webkit.org/show_bug.cgi?id=193508 var xhr = new XMLHttpRequest(); - xhr.open('POST', collectorUrl + '/com.snowplowanalytics/tp2', true); + xhr.open('POST', collectorUrl, true); xhr.setRequestHeader('Content-Type', 'text/plain'); xhr.withCredentials = true; xhr.send('unlockBeacon'); diff --git a/src/js/out_queue.js b/src/js/out_queue.js index 24b7e5952..ccf3007ad 100644 --- a/src/js/out_queue.js +++ b/src/js/out_queue.js @@ -39,6 +39,7 @@ isString = require('lodash/isString'), map = require('lodash/map'), localStorageAccessible = require('./lib/detectors').localStorageAccessible, + beaconiOSWorkaround = require('./lib/beacon_ios_workaround'), helpers = require('./lib/helpers'), object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support @@ -314,6 +315,7 @@ } const blob = new Blob([encloseInPayloadDataEnvelope(attachStmToEvent(batch))], headers); try { + beaconiOSWorkaround.unlockBeacon(configCollectorUrl); beaconStatus = navigator.sendBeacon(configCollectorUrl, blob); } catch(error) { diff --git a/src/js/tracker.js b/src/js/tracker.js index b6851e518..3ab6bb092 100755 --- a/src/js/tracker.js +++ b/src/js/tracker.js @@ -45,7 +45,6 @@ links = require('./links'), forms = require('./forms'), errors = require('./errors'), - beaconiOSWorkaround = require('./lib/beacon_ios_workaround'), requestQueue = require('./out_queue'), coreConstructor = require('snowplow-tracker-core').trackerCore, productionize = require('./guard').productionize, @@ -2139,7 +2138,6 @@ */ apiMethods.setCollectorUrl = function (rawUrl) { configCollectorUrl = asCollectorUrl(rawUrl); - beaconiOSWorkaround.unlockBeacon(configCollectorUrl); }; /** From 5e3046b0ec37cf58a2a603cd78b4bf2a9840b7e4 Mon Sep 17 00:00:00 2001 From: Scotty Jacobson Date: Tue, 9 Apr 2019 15:38:25 -0400 Subject: [PATCH 08/11] use dist/snowplow.js as "main", instead of dist/bundle-postbabel.js. this should fix EA-1, where the version was always js-<%= pkg.version %> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f5898e79c..b7a84dfb5 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "build": "grunt", "prepare": "npm run build" }, - "main": "dist/bundle-postbabel", + "main": "dist/snowplow", "bugs": "https://github.com/snowplow/snowplow-javascript-tracker/issues", "keywords": [ "tracking", From a37e9652c72b95fce4815e12c9ef8eca283b56ef Mon Sep 17 00:00:00 2001 From: Scotty Jacobson Date: Wed, 30 Oct 2019 15:40:12 -0400 Subject: [PATCH 09/11] force "postinstall" to also run "prepare" script it looks like there is a bug in npm that sometimes doesn't run "prepare" when installing from a git repo, even though "prepare" should be run https://npm.community/t/npm-lifecycle-scripts-such-as-prepare-not-run-when-install-from-git-repo/10253/2 if we force it to "prepare" on "postinstall" then we should be almost assured that "prepare" is always run --- npm-shrinkwrap.json | 41 ++++++++++++++++++++++++++++++----------- package.json | 11 ++++++----- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 4127bbe7b..d8a44895b 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2583,7 +2583,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -2604,12 +2605,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2624,17 +2627,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -2751,7 +2757,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -2763,6 +2770,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2777,6 +2785,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2784,12 +2793,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -2808,6 +2819,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -2888,7 +2900,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -2900,6 +2913,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -2985,7 +2999,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -3021,6 +3036,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3040,6 +3056,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3083,12 +3100,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/package.json b/package.json index b7a84dfb5..c8ee98f97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,12 @@ { "name": "snowplow-tracker", "version": "2.10.1", + "scripts": { + "build": "grunt", + "prepare": "npm run build", + "postinstall": "npm run prepare" + }, + "main": "dist/snowplow", "dependencies": { "snowplow-tracker-core": "0.7.0", "browser-cookie-lite": "0.3.1", @@ -37,11 +43,6 @@ "type": "git", "url": "https://github.com/snowplow/snowplow-javascript-tracker.git" }, - "scripts": { - "build": "grunt", - "prepare": "npm run build" - }, - "main": "dist/snowplow", "bugs": "https://github.com/snowplow/snowplow-javascript-tracker/issues", "keywords": [ "tracking", From 9d3c2f0d695dcda3f0f46279e369e65ed435225a Mon Sep 17 00:00:00 2001 From: Elad Gil Date: Mon, 25 Nov 2019 16:24:53 +0200 Subject: [PATCH 10/11] Update package.json --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index c8ee98f97..57529fd1f 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,7 @@ "version": "2.10.1", "scripts": { "build": "grunt", - "prepare": "npm run build", - "postinstall": "npm run prepare" + "prepare": "npm run build" }, "main": "dist/snowplow", "dependencies": { From e86a66e9df59e00980ee43b4d5cd0eb17dd8fa13 Mon Sep 17 00:00:00 2001 From: Elad Gil Date: Mon, 25 Nov 2019 16:26:50 +0200 Subject: [PATCH 11/11] Update package.json --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 57529fd1f..a33bd1b6c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "version": "2.10.1", "scripts": { "build": "grunt", - "prepare": "npm run build" + "prepare": "npm run build", + "prepublish": "npm run build" }, "main": "dist/snowplow", "dependencies": {