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 90f9ab91c..a33bd1b6c 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", + "prepublish": "npm run build" + }, + "main": "dist/snowplow", "dependencies": { "snowplow-tracker-core": "0.7.0", "browser-cookie-lite": "0.3.1", diff --git a/src/js/lib/beacon_ios_workaround.js b/src/js/lib/beacon_ios_workaround.js new file mode 100644 index 000000000..e398226ad --- /dev/null +++ b/src/js/lib/beacon_ios_workaround.js @@ -0,0 +1,77 @@ +/* + * 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) { + 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, true); + xhr.setRequestHeader('Content-Type', 'text/plain'); + xhr.withCredentials = true; + xhr.send('unlockBeacon'); + } + + object.unlockBeacon = unlockBeacon; + +}()); diff --git a/src/js/out_queue.js b/src/js/out_queue.js index ee383d9cb..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 @@ -68,12 +69,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"); @@ -115,6 +120,8 @@ if (!executingQueue) { executeQueue(); } + // Any subsequent events (during beforeunload / unload) should be sent immediately so they don't get buffered and ignored + bufferSize = 1; }); } @@ -300,9 +307,15 @@ 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 { + beaconiOSWorkaround.unlockBeacon(configCollectorUrl); beaconStatus = navigator.sendBeacon(configCollectorUrl, blob); } catch(error) { 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