From 7796ab0e0916016410527cd1b0ec969e7d09ddef Mon Sep 17 00:00:00 2001 From: Stephane Bellity Date: Tue, 10 Mar 2015 21:35:49 +0100 Subject: [PATCH] Modularize --- Gruntfile.js | 24 +- package.json | 17 +- src/js/forms.js | 313 ++--- src/js/in_queue.js | 359 +++-- src/js/init.js | 52 +- src/js/lib/detectors.js | 461 ++++--- src/js/lib/helpers.js | 396 +++--- src/js/lib/proxies.js | 159 ++- src/js/lib/utils.js | 17 + src/js/links.js | 306 ++--- src/js/out_queue.js | 450 +++--- src/js/snowplow.js | 457 +++---- src/js/tracker.js | 2886 +++++++++++++++++++-------------------- 13 files changed, 2928 insertions(+), 2969 deletions(-) create mode 100644 src/js/lib/utils.js diff --git a/Gruntfile.js b/Gruntfile.js index 6b02d65c1..75f7cf0ca 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -77,17 +77,6 @@ module.exports = function(grunt) { pkg: pkg, - lodash: { - build: { - dest: 'src/js/lib_managed/lodash.js', - options: { - exports: 'node', - include: 'isArray, isFunction, isString, isObject, isDate, isUndefined, isNull, map, mapValues, forEach, filter, find, compact, isEmpty, clone', - flags: ['debug'] - } - } - }, - browserify: { main: { files: { @@ -175,7 +164,6 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-cloudfront'); grunt.loadNpmTasks('grunt-browserify'); grunt.loadNpmTasks('intern'); - grunt.loadNpmTasks('grunt-lodash'); grunt.registerTask('upload_setup', 'Read aws.json and configure upload tasks', function() { var aws = grunt.file.readJSON('aws.json'); @@ -213,9 +201,9 @@ module.exports = function(grunt) { { src: 'dist/sp.js', dest: '<%= pkg.pinnedVersion %>/sp.js' - } + } ] - }, + }, }); grunt.config('cloudfront', { @@ -246,10 +234,10 @@ module.exports = function(grunt) { }); }); - grunt.registerTask('default', 'Build lodash, Browserify, add banner, and minify', ['lodash', 'browserify:main', 'concat:deploy', 'min:deploy']); - grunt.registerTask('publish', 'Upload to S3 and invalidate Cloudfront (full semantic version only)', ['upload_setup', 'lodash', 'browserify:main', 'concat:deploy', 'min:deploy', 's3:not_pinned', 'cloudfront:not_pinned']); - grunt.registerTask('publish-pinned', 'Upload to S3 and invalidate Cloudfront (full semantic version and semantic major version)', ['upload_setup', 'lodash', 'browserify:main', 'concat:deploy', 'min:deploy', 's3', 'cloudfront']); + grunt.registerTask('default', 'Browserify, add banner, and minify', ['browserify:main', 'concat:deploy', 'min:deploy']); + grunt.registerTask('publish', 'Upload to S3 and invalidate Cloudfront (full semantic version only)', ['upload_setup', 'browserify:main', 'concat:deploy', 'min:deploy', 's3:not_pinned', 'cloudfront:not_pinned']); + grunt.registerTask('publish-pinned', 'Upload to S3 and invalidate Cloudfront (full semantic version and semantic major version)', ['upload_setup', 'browserify:main', 'concat:deploy', 'min:deploy', 's3', 'cloudfront']); grunt.registerTask('test', 'Intern tests', ['browserify:test', 'intern']); - grunt.registerTask('travis', 'Intern tests for Travis CI', ['lodash','browserify:test','intern']); + grunt.registerTask('travis', 'Intern tests for Travis CI', ['browserify:test','intern']); grunt.registerTask('tags', 'Minifiy the Snowplow invocation tag', ['min:tag', 'concat:tag']); } diff --git a/package.json b/package.json index 28b63ee98..26f29bb5a 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,17 @@ { "name": "snowplow-tracker", "version": "2.3.0", - "devDependencies": { + "main" : "src/js/snowplow.js", + "dependencies": { "JSON": "~1.0.0", "browser-cookie-lite": "~0.3.1", + "jstimezonedetect": "~1.0.5", + "lodash": "^3.1.0", + "murmurhash": "0.0.2", + "sha1": "git://github.com/pvorb/node-sha1.git#v1.0.0", + "snowplow-tracker-core": "0.3.0" + }, + "devDependencies": { "browserify": "~3.28.1", "grunt": "~0.4.2", "grunt-browserify": "~1.3.0", @@ -13,13 +21,8 @@ "grunt-s3": "~0.2.0-alpha.3", "grunt-yui-compressor": "git://github.com/snowplow/grunt-yui-compressor.git#feature/issue-8", "intern": "~1.5.0", - "jstimezonedetect": "~1.0.5", - "lodash": "^3.1.0", "lodash-cli": "^2.4.2", - "murmurhash": "0.0.2", - "semver": "~2.2.1", - "sha1": "git://github.com/pvorb/node-sha1.git#v1.0.0", - "snowplow-tracker-core": "0.3.0" + "semver": "~2.2.1" }, "contributors": [ "Alex Dean", diff --git a/src/js/forms.js b/src/js/forms.js index 818901565..31338b0e3 100644 --- a/src/js/forms.js +++ b/src/js/forms.js @@ -1,40 +1,41 @@ /* * JavaScript tracker for Snowplow: forms.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. + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. * - * * 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. + * 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. + * 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 + * 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. */ -var lodash = require('./lib_managed/lodash'), - helpers = require('./lib/helpers'), - object = typeof exports !== 'undefined' ? exports : this; +var lodash = require('./lib/utils'), + helpers = require('./lib/helpers'); + +module.exports = { getFormTrackingManager: getFormTrackingManager }; /** * Object for handling automatic form tracking @@ -43,134 +44,134 @@ var lodash = require('./lib_managed/lodash'), * @param string trackerId Unique identifier for the tracker instance, used to mark tracked elements * @return object formTrackingManager instance */ -object.getFormTrackingManager = function (core, trackerId) { - - // Tag names of mutable elements inside a form - var innerElementTags = ['textarea', 'input', 'select']; - - // Used to mark elements with event listeners - var trackingMarker = trackerId + 'form'; - - // Filter to determine which forms should be tracked - var formFilter = function (e) {return true;} - - // Filter to determine which form fields should be tracked - var fieldFilter = function (e) {return true;} - - /* - * Get an identifier for a form, input, textarea, or select element - */ - function getFormElementName(elt) { - return elt[lodash.find(['name', 'id', 'type', 'nodeName'], function (propName) { - - // If elt has a child whose name is "id", that element will be returned - // instead of the actual id of elt unless we ensure that a string is returned - return elt[propName] && typeof elt[propName] === 'string'; - })]; - } - - /* - * Identifies the parent form in which an element is contained - */ - function getParentFormName(elt) { - while (elt && elt.nodeName.toUpperCase() !== 'HTML' && elt.nodeName.toUpperCase() !== 'FORM') { - elt = elt.parentNode; - } - if (elt.nodeName.toUpperCase() === 'FORM') { - return getFormElementName(elt); - } - } - - /* - * Returns a list of the input, textarea, and select elements inside a form along with their values - */ - function getInnerFormElements(elt) { - var innerElements = []; - lodash.forEach(innerElementTags, function (tagname) { - - var trackedChildren = lodash.filter(elt.getElementsByTagName(tagname), function (child) { - return child.hasOwnProperty(trackingMarker); - }); - - lodash.forEach(trackedChildren, function (child) { - if (child.type === 'submit') { - return; - } - var elementJson = { - name: getFormElementName(child), - value: child.value, - nodeName: child.nodeName - }; - if (child.type && child.nodeName.toUpperCase() === 'INPUT') { - elementJson.type = child.type; - } - - if ((child.type === 'checkbox' || child.type === 'radio') && !child.checked) { - elementJson.value = null; - } - innerElements.push(elementJson); - }); - }); - - return innerElements; - } - - /* - * Return function to handle form field change event - */ - function getFormChangeListener(context) { - return function (e) { - var elt = e.target; - var type = elt.nodeName.toUpperCase() === 'INPUT' ? elt.type : null; - var value = (elt.type === 'checkbox' && !elt.checked) ? null : elt.value; - core.trackFormChange(getParentFormName(elt), getFormElementName(elt), elt.nodeName, type, lodash.map(elt.classList), value, context); - }; - } - - /* - * Return function to handle form submission event - */ - function getFormSubmissionListener(context) { - return function (e) { - var elt = e.target; - var innerElements = getInnerFormElements(elt); - core.trackFormSubmission(getFormElementName(elt), lodash.map(elt.classList), innerElements, context); - }; - } - - return { - - /* - * Configures form tracking: which forms and fields will be tracked, and the context to attach - */ - configureFormTracking: function (config, context) { - if (config) { - formFilter = helpers.getFilter(config.forms, true); - fieldFilter = helpers.getFilter(config.fields, false); - } - }, - - /* - * Add submission event listeners to all form elements - * Add value change event listeners to all mutable inner form elements - */ - addFormListeners: function (context) { - lodash.forEach(document.getElementsByTagName('form'), function (form) { - if (formFilter(form) && !form[trackingMarker]) { - - lodash.forEach(innerElementTags, function (tagname) { - lodash.forEach(form.getElementsByTagName(tagname), function (innerElement) { - if (fieldFilter(innerElement) && !innerElement[trackingMarker]) { - helpers.addEventListener(innerElement, 'change', getFormChangeListener(context), false); - innerElement[trackingMarker] = true; - } - }); - }); - - helpers.addEventListener(form, 'submit', getFormSubmissionListener(context)); - form[trackingMarker] = true; - } - }); - } - }; +function getFormTrackingManager(core, trackerId) { + + // Tag names of mutable elements inside a form + var innerElementTags = ['textarea', 'input', 'select']; + + // Used to mark elements with event listeners + var trackingMarker = trackerId + 'form'; + + // Filter to determine which forms should be tracked + var formFilter = function (e) {return true;} + + // Filter to determine which form fields should be tracked + var fieldFilter = function (e) {return true;} + + /* + * Get an identifier for a form, input, textarea, or select element + */ + function getFormElementName(elt) { + return elt[lodash.find(['name', 'id', 'type', 'nodeName'], function (propName) { + + // If elt has a child whose name is "id", that element will be returned + // instead of the actual id of elt unless we ensure that a string is returned + return elt[propName] && typeof elt[propName] === 'string'; + })]; + } + + /* + * Identifies the parent form in which an element is contained + */ + function getParentFormName(elt) { + while (elt && elt.nodeName.toUpperCase() !== 'HTML' && elt.nodeName.toUpperCase() !== 'FORM') { + elt = elt.parentNode; + } + if (elt.nodeName.toUpperCase() === 'FORM') { + return getFormElementName(elt); + } + } + + /* + * Returns a list of the input, textarea, and select elements inside a form along with their values + */ + function getInnerFormElements(elt) { + var innerElements = []; + lodash.forEach(innerElementTags, function (tagname) { + + var trackedChildren = lodash.filter(elt.getElementsByTagName(tagname), function (child) { + return child.hasOwnProperty(trackingMarker); + }); + + lodash.forEach(trackedChildren, function (child) { + if (child.type === 'submit') { + return; + } + var elementJson = { + name: getFormElementName(child), + value: child.value, + nodeName: child.nodeName + }; + if (child.type && child.nodeName.toUpperCase() === 'INPUT') { + elementJson.type = child.type; + } + + if ((child.type === 'checkbox' || child.type === 'radio') && !child.checked) { + elementJson.value = null; + } + innerElements.push(elementJson); + }); + }); + + return innerElements; + } + + /* + * Return function to handle form field change event + */ + function getFormChangeListener(context) { + return function (e) { + var elt = e.target; + var type = elt.nodeName.toUpperCase() === 'INPUT' ? elt.type : null; + var value = (elt.type === 'checkbox' && !elt.checked) ? null : elt.value; + core.trackFormChange(getParentFormName(elt), getFormElementName(elt), elt.nodeName, type, lodash.map(elt.classList), value, context); + }; + } + + /* + * Return function to handle form submission event + */ + function getFormSubmissionListener(context) { + return function (e) { + var elt = e.target; + var innerElements = getInnerFormElements(elt); + core.trackFormSubmission(getFormElementName(elt), lodash.map(elt.classList), innerElements, context); + }; + } + + return { + + /* + * Configures form tracking: which forms and fields will be tracked, and the context to attach + */ + configureFormTracking: function (config, context) { + if (config) { + formFilter = helpers.getFilter(config.forms, true); + fieldFilter = helpers.getFilter(config.fields, false); + } + }, + + /* + * Add submission event listeners to all form elements + * Add value change event listeners to all mutable inner form elements + */ + addFormListeners: function (context) { + lodash.forEach(document.getElementsByTagName('form'), function (form) { + if (formFilter(form) && !form[trackingMarker]) { + + lodash.forEach(innerElementTags, function (tagname) { + lodash.forEach(form.getElementsByTagName(tagname), function (innerElement) { + if (fieldFilter(innerElement) && !innerElement[trackingMarker]) { + helpers.addEventListener(innerElement, 'change', getFormChangeListener(context), false); + innerElement[trackingMarker] = true; + } + }); + }); + + helpers.addEventListener(form, 'submit', getFormSubmissionListener(context)); + form[trackingMarker] = true; + } + }); + } + }; }; diff --git a/src/js/in_queue.js b/src/js/in_queue.js index 883dfb4c7..2eccfa59e 100644 --- a/src/js/in_queue.js +++ b/src/js/in_queue.js @@ -1,191 +1,190 @@ /* * JavaScript tracker for Snowplow: queue.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. + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. * - * * 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. + * 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. + * 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 + * 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 - lodash = require('./lib_managed/lodash'), - helpers = require('./lib/helpers'), - - object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support - - /************************************************************ - * Proxy object - * - this allows the caller to continue push()'ing to _snaq - * after the Tracker has been initialized and loaded - ************************************************************/ - - object.InQueueManager = function(TrackerConstructor, version, mutSnowplowState, asyncQueue, functionName) { - - var trackerDictionary = {}, - usedCookieNames = {}; - - /** - * Get an array of trackers to which a function should be applied. - * - * @param array names List of namespaces to use. If empty, use all namespaces. - */ - function getNamedTrackers(names) { - var namedTrackers = []; - - if (!names || names.length === 0) { - namedTrackers = lodash.map(trackerDictionary); - } else { - for (var i = 0; i < names.length; i++) { - if (trackerDictionary.hasOwnProperty(names[i])) { - namedTrackers.push(trackerDictionary[names[i]]); - } else { - helpers.warn('Warning: Tracker namespace "' + names[i] + '" not configured'); - } - } - } - - if (namedTrackers.length === 0) { - helpers.warn('Warning: No tracker configured'); - } - - return namedTrackers; - } - - /** - * Legacy support for input of the form _snaq.push(['setCollectorCf', 'd34uzc5hjrimh8']) - * - * @param string f Either 'setCollectorCf' or 'setCollectorUrl' - * @param string endpoint - * @param string namespace Optional tracker name - * - * TODO: remove this in 2.1.0 - */ - function legacyCreateNewNamespace(f, endpoint, namespace) { - helpers.warn(f + ' is deprecated. Set the collector when a new tracker instance using newTracker.'); - - var name; - - if (lodash.isUndefined(namespace)) { - name = 'sp'; - } else { - name = namespace; - } - - createNewNamespace(name); - trackerDictionary[name][f](endpoint); - } - - /** - * Initiate a new tracker namespace - * - * @param string namespace - * @param string endpoint Of the form d3rkrsqld9gmqf.cloudfront.net - */ - function createNewNamespace(namespace, endpoint, argmap) { - argmap = argmap || {}; - if ((!argmap.writeCookies) && (argmap.cookieName in usedCookieNames)) { - argmap.writeCookies = false; - } else { - usedCookieNames[argmap.cookieName] = true; - } - trackerDictionary[namespace] = new TrackerConstructor(functionName, namespace, version, mutSnowplowState, argmap); - trackerDictionary[namespace].setCollectorUrl(endpoint); - } - - /** - * Output an array of the form ['functionName', [trackerName1, trackerName2, ...]] - * - * @param string inputString - */ - function parseInputString(inputString) { - var separatedString = inputString.split(':'), - extractedFunction = separatedString[0], - extractedNames = (separatedString.length > 1) ? separatedString[1].split(';') : []; - - return [extractedFunction, extractedNames]; - } - - /** - * apply wrapper - * - * @param array parameterArray An array comprising either: - * [ 'methodName', optional_parameters ] - * or: - * [ functionObject, optional_parameters ] - */ - function applyAsyncFunction() { - var i, j, f, parameterArray, input, parsedString, names, namedTrackers; - - // Outer loop in case someone push'es in zarg of arrays - for (i = 0; i < arguments.length; i += 1) { - parameterArray = arguments[i]; - - // Arguments is not an array, so we turn it into one - input = Array.prototype.shift.call(parameterArray); - - // Custom callback rather than tracker method, called with trackerDictionary as the context - if (lodash.isFunction(input)) { - input.apply(trackerDictionary, parameterArray); - continue; - } - - parsedString = parseInputString(input); - f = parsedString[0]; - names = parsedString[1]; - - if (f === 'newTracker') { - createNewNamespace(parameterArray[0], parameterArray[1], parameterArray[2]); - continue; - } - - if ((f === 'setCollectorCf' || f === 'setCollectorUrl') && (!names || names.length === 0)) { - legacyCreateNewNamespace(f, parameterArray[0], parameterArray[1]); - continue; - } - - namedTrackers = getNamedTrackers(names); - - for (j = 0; j < namedTrackers.length; j++) { - namedTrackers[j][f].apply(namedTrackers[j], parameterArray); - } - } - } - - // We need to manually apply any events collected before this initialization - for (var i = 0; i < asyncQueue.length; i++) { - applyAsyncFunction(asyncQueue[i]); - } - - return { - push: applyAsyncFunction - }; - }; - -}()); +var + lodash = require('./lib/utils'), + helpers = require('./lib/helpers'); + +module.exports = {}; + +/************************************************************ + * Proxy object + * - this allows the caller to continue push()'ing to _snaq + * after the Tracker has been initialized and loaded + ************************************************************/ + +module.exports = { InQueueManager: InQueueManager }; + + +function InQueueManager(TrackerConstructor, version, mutSnowplowState, asyncQueue, functionName) { + + var trackerDictionary = {}, + usedCookieNames = {}; + + /** + * Get an array of trackers to which a function should be applied. + * + * @param array names List of namespaces to use. If empty, use all namespaces. + */ + function getNamedTrackers(names) { + var namedTrackers = []; + + if (!names || names.length === 0) { + namedTrackers = lodash.map(trackerDictionary); + } else { + for (var i = 0; i < names.length; i++) { + if (trackerDictionary.hasOwnProperty(names[i])) { + namedTrackers.push(trackerDictionary[names[i]]); + } else { + helpers.warn('Warning: Tracker namespace "' + names[i] + '" not configured'); + } + } + } + + if (namedTrackers.length === 0) { + helpers.warn('Warning: No tracker configured'); + } + + return namedTrackers; + } + + /** + * Legacy support for input of the form _snaq.push(['setCollectorCf', 'd34uzc5hjrimh8']) + * + * @param string f Either 'setCollectorCf' or 'setCollectorUrl' + * @param string endpoint + * @param string namespace Optional tracker name + * + * TODO: remove this in 2.1.0 + */ + function legacyCreateNewNamespace(f, endpoint, namespace) { + helpers.warn(f + ' is deprecated. Set the collector when a new tracker instance using newTracker.'); + + var name; + + if (lodash.isUndefined(namespace)) { + name = 'sp'; + } else { + name = namespace; + } + + createNewNamespace(name); + trackerDictionary[name][f](endpoint); + } + + /** + * Initiate a new tracker namespace + * + * @param string namespace + * @param string endpoint Of the form d3rkrsqld9gmqf.cloudfront.net + */ + function createNewNamespace(namespace, endpoint, argmap) { + argmap = argmap || {}; + if ((!argmap.writeCookies) && (argmap.cookieName in usedCookieNames)) { + argmap.writeCookies = false; + } else { + usedCookieNames[argmap.cookieName] = true; + } + trackerDictionary[namespace] = new TrackerConstructor(functionName, namespace, version, mutSnowplowState, argmap); + trackerDictionary[namespace].setCollectorUrl(endpoint); + } + + /** + * Output an array of the form ['functionName', [trackerName1, trackerName2, ...]] + * + * @param string inputString + */ + function parseInputString(inputString) { + var separatedString = inputString.split(':'), + extractedFunction = separatedString[0], + extractedNames = (separatedString.length > 1) ? separatedString[1].split(';') : []; + + return [extractedFunction, extractedNames]; + } + + /** + * apply wrapper + * + * @param array parameterArray An array comprising either: + * [ 'methodName', optional_parameters ] + * or: + * [ functionObject, optional_parameters ] + */ + function applyAsyncFunction() { + var i, j, f, parameterArray, input, parsedString, names, namedTrackers; + + // Outer loop in case someone push'es in zarg of arrays + for (i = 0; i < arguments.length; i += 1) { + parameterArray = arguments[i]; + + // Arguments is not an array, so we turn it into one + input = Array.prototype.shift.call(parameterArray); + + // Custom callback rather than tracker method, called with trackerDictionary as the context + if (lodash.isFunction(input)) { + input.apply(trackerDictionary, parameterArray); + continue; + } + + parsedString = parseInputString(input); + f = parsedString[0]; + names = parsedString[1]; + + if (f === 'newTracker') { + createNewNamespace(parameterArray[0], parameterArray[1], parameterArray[2]); + continue; + } + + if ((f === 'setCollectorCf' || f === 'setCollectorUrl') && (!names || names.length === 0)) { + legacyCreateNewNamespace(f, parameterArray[0], parameterArray[1]); + continue; + } + + namedTrackers = getNamedTrackers(names); + + for (j = 0; j < namedTrackers.length; j++) { + namedTrackers[j][f].apply(namedTrackers[j], parameterArray); + } + } + } + + // We need to manually apply any events collected before this initialization + for (var i = 0; i < asyncQueue.length; i++) { + applyAsyncFunction(asyncQueue[i]); + } + + return { + push: applyAsyncFunction + }; +}; diff --git a/src/js/init.js b/src/js/init.js index 9e243b192..1b6cd77af 100644 --- a/src/js/init.js +++ b/src/js/init.js @@ -1,34 +1,34 @@ /* * JavaScript tracker for Snowplow: init.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. + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. * - * * 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. + * 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. + * 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 + * 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. */ @@ -38,7 +38,7 @@ * Get the name of the global input function */ -var snowplow = require('./snowplow'), +var Snowplow = require('./snowplow').Snowplow, queueName, queue, windowAlias = window; @@ -46,8 +46,8 @@ var snowplow = require('./snowplow'), if (windowAlias.GlobalSnowplowNamespace && windowAlias.GlobalSnowplowNamespace.length > 0) { queueName = windowAlias.GlobalSnowplowNamespace.shift(); queue = windowAlias[queueName]; - queue.q = new snowplow.Snowplow(queue.q, queueName); + queue.q = new Snowplow(queue.q, queueName); } else { windowAlias._snaq = windowAlias._snaq || []; - windowAlias._snaq = new snowplow.Snowplow(windowAlias._snaq, '_snaq'); + windowAlias._snaq = new Snowplow(windowAlias._snaq, '_snaq'); } diff --git a/src/js/lib/detectors.js b/src/js/lib/detectors.js index 2a3372af2..78a1eaf8b 100644 --- a/src/js/lib/detectors.js +++ b/src/js/lib/detectors.js @@ -1,243 +1,240 @@ /* * 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. + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. * - * * 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. + * 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. + * 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 + * 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 - lodash = require('../lib_managed/lodash'), - murmurhash3_32_gc = require('murmurhash').v3, - tz = require('jstimezonedetect').jstz.determine(), - cookie = require('browser-cookie-lite'), - - object = typeof exports !== 'undefined' ? exports : this, // For eventual node.js environment support - - windowAlias = window, - navigatorAlias = navigator, - screenAlias = screen, - documentAlias = document; - - /* - * Checks whether sessionStorage is available, in a way that - * does not throw a SecurityError in Firefox if "always ask" - * is enabled for cookies (https://github.com/snowplow/snowplow/issues/163). - */ - object.hasSessionStorage = function () { - try { - return !!windowAlias.sessionStorage; - } catch (e) { - return true; // SecurityError when referencing it means it exists - } - }; - - /* - * Checks whether localStorage is available, in a way that - * does not throw a SecurityError in Firefox if "always ask" - * is enabled for cookies (https://github.com/snowplow/snowplow/issues/163). - */ - object.hasLocalStorage = function () { - try { - return !!windowAlias.localStorage; - } catch (e) { - return true; // SecurityError when referencing it means it exists - } - }; - - /* - * Checks whether localStorage is accessible - * sets and removes an item to handle private IOS5 browsing - * (http://git.io/jFB2Xw) - */ - object.localStorageAccessible = function() { - var mod = 'modernizr'; - if (!object.hasLocalStorage()) { - return false; - } - try { - windowAlias.localStorage.setItem(mod, mod); - windowAlias.localStorage.removeItem(mod); - return true; - } catch(e) { - return false; - } - }; - - /* - * Does browser have cookies enabled (for this site)? - */ - object.hasCookies = function(testCookieName) { - var cookieName = testCookieName || 'testcookie'; - - if (lodash.isUndefined(navigatorAlias.cookieEnabled)) { - cookie.cookie(cookieName, '1'); - return cookie.cookie(cookieName) === '1' ? '1' : '0'; - } - - return navigatorAlias.cookieEnabled ? '1' : '0'; - }; - - /** - * JS Implementation for browser fingerprint. - * Does not require any external resources. - * Based on https://github.com/carlo/jquery-browser-fingerprint - * @return {number} 32-bit positive integer hash - */ - object.detectSignature = function(hashSeed) { - - var fingerprint = [ - navigatorAlias.userAgent, - [ screenAlias.height, screenAlias.width, screenAlias.colorDepth ].join("x"), - ( new Date() ).getTimezoneOffset(), - object.hasSessionStorage(), - object.hasLocalStorage() - ]; - - var plugins = []; - if (navigatorAlias.plugins) - { - for(var i = 0; i < navigatorAlias.plugins.length; i++) - { - var mt = []; - for(var j = 0; j < navigatorAlias.plugins[i].length; j++) - { - mt.push([navigatorAlias.plugins[i][j].type, navigatorAlias.plugins[i][j].suffixes]); - } - plugins.push([navigatorAlias.plugins[i].name + "::" + navigatorAlias.plugins[i].description, mt.join("~")]); - } - } - return murmurhash3_32_gc(fingerprint.join("###") + "###" + plugins.sort().join(";"), hashSeed); - }; - - /* - * Returns visitor timezone - */ - object.detectTimezone = function() { - return (typeof (tz) === 'undefined') ? '' : tz.name(); - }; - - /** - * Gets the current viewport. - * - * Code based on: - * - http://andylangton.co.uk/articles/javascript/get-viewport-size-javascript/ - * - http://responsejs.com/labs/dimensions/ - */ - object.detectViewport = function() { - var e = windowAlias, a = 'inner'; - if (!('innerWidth' in windowAlias)) { - a = 'client'; - e = documentAlias.documentElement || documentAlias.body; - } - return e[a+'Width'] + 'x' + e[a+'Height']; - }; - - /** - * Gets the dimensions of the current - * document. - * - * Code based on: - * - http://andylangton.co.uk/articles/javascript/get-viewport-size-javascript/ - */ - object.detectDocumentSize = function() { - var de = documentAlias.documentElement, // Alias - be = documentAlias.body; - var w = Math.max(de.clientWidth, de.offsetWidth, de.scrollWidth); - var h = Math.max(de.clientHeight, de.offsetHeight, de.scrollHeight, - be.offsetHeight, be.scrollHeight); - return isNaN(w) || isNaN(h) ? '' : w + 'x' + h; - }; - - /** - * Returns browser features (plugins, resolution, cookies) - * - * @param boolean useCookies Whether to test for cookies - * @param string testCookieName Name to use for the test cookie - * @return Object containing browser features - */ - object.detectBrowserFeatures = function(useCookies, testCookieName) { - var i, - mimeType, - pluginMap = { - // document types - pdf: 'application/pdf', - - // media players - qt: 'video/quicktime', - realp: 'audio/x-pn-realaudio-plugin', - wma: 'application/x-mplayer2', - - // interactive multimedia - dir: 'application/x-director', - fla: 'application/x-shockwave-flash', - - // RIA - java: 'application/x-java-vm', - gears: 'application/x-googlegears', - ag: 'application/x-silverlight' - }, - features = {}; - - // General plugin detection - if (navigatorAlias.mimeTypes && navigatorAlias.mimeTypes.length) { - for (i in pluginMap) { - if (Object.prototype.hasOwnProperty.call(pluginMap, i)) { - mimeType = navigatorAlias.mimeTypes[pluginMap[i]]; - features[i] = (mimeType && mimeType.enabledPlugin) ? '1' : '0'; - } - } - } - - // Safari and Opera - // IE6/IE7 navigator.javaEnabled can't be aliased, so test directly - if (typeof navigatorAlias.javaEnabled !== 'unknown' && - !lodash.isUndefined(navigatorAlias.javaEnabled) && - navigatorAlias.javaEnabled()) { - features.java = '1'; - } - - // Firefox - if (lodash.isFunction(windowAlias.GearsFactory)) { - features.gears = '1'; - } - - // Other browser features - features.res = screenAlias.width + 'x' + screenAlias.height; - features.cd = screenAlias.colorDepth; - if (useCookies) { - features.cookie = object.hasCookies(testCookieName); - } - - return features; - }; - -}()); +var + lodash = require('./utils'), + murmurhash3_32_gc = require('murmurhash').v3, + tz = require('jstimezonedetect').jstz.determine(), + cookie = require('browser-cookie-lite'), + + windowAlias = window, + navigatorAlias = navigator, + screenAlias = screen, + documentAlias = document; + + +module.exports = {}; + +/* + * Checks whether sessionStorage is available, in a way that + * does not throw a SecurityError in Firefox if "always ask" + * is enabled for cookies (https://github.com/snowplow/snowplow/issues/163). + */ +module.exports.hasSessionStorage = function () { + try { + return !!windowAlias.sessionStorage; + } catch (e) { + return true; // SecurityError when referencing it means it exists + } +}; + +/* + * Checks whether localStorage is available, in a way that + * does not throw a SecurityError in Firefox if "always ask" + * is enabled for cookies (https://github.com/snowplow/snowplow/issues/163). + */ +module.exports.hasLocalStorage = function () { + try { + return !!windowAlias.localStorage; + } catch (e) { + return true; // SecurityError when referencing it means it exists + } +}; + +/* + * Checks whether localStorage is accessible + * sets and removes an item to handle private IOS5 browsing + * (http://git.io/jFB2Xw) + */ +module.exports.localStorageAccessible = function() { + var mod = 'modernizr'; + if (!module.exports.hasLocalStorage()) { + return false; + } + try { + windowAlias.localStorage.setItem(mod, mod); + windowAlias.localStorage.removeItem(mod); + return true; + } catch(e) { + return false; + } + }; + +/* + * Does browser have cookies enabled (for this site)? + */ +module.exports.hasCookies = function(testCookieName) { + var cookieName = testCookieName || 'testcookie'; + + if (lodash.isUndefined(navigatorAlias.cookieEnabled)) { + cookie.cookie(cookieName, '1'); + return cookie.cookie(cookieName) === '1' ? '1' : '0'; + } + + return navigatorAlias.cookieEnabled ? '1' : '0'; +}; + +/** + * JS Implementation for browser fingerprint. + * Does not require any external resources. + * Based on https://github.com/carlo/jquery-browser-fingerprint + * @return {number} 32-bit positive integer hash + */ +module.exports.detectSignature = function(hashSeed) { + + var fingerprint = [ + navigatorAlias.userAgent, + [ screenAlias.height, screenAlias.width, screenAlias.colorDepth ].join("x"), + ( new Date() ).getTimezoneOffset(), + module.exports.hasSessionStorage(), + module.exports.hasLocalStorage() + ]; + + var plugins = []; + if (navigatorAlias.plugins) + { + for(var i = 0; i < navigatorAlias.plugins.length; i++) + { + var mt = []; + for(var j = 0; j < navigatorAlias.plugins[i].length; j++) + { + mt.push([navigatorAlias.plugins[i][j].type, navigatorAlias.plugins[i][j].suffixes]); + } + plugins.push([navigatorAlias.plugins[i].name + "::" + navigatorAlias.plugins[i].description, mt.join("~")]); + } + } + return murmurhash3_32_gc(fingerprint.join("###") + "###" + plugins.sort().join(";"), hashSeed); +}; + +/* + * Returns visitor timezone + */ +module.exports.detectTimezone = function() { + return (typeof (tz) === 'undefined') ? '' : tz.name(); +}; + +/** + * Gets the current viewport. + * + * Code based on: + * - http://andylangton.co.uk/articles/javascript/get-viewport-size-javascript/ + * - http://responsejs.com/labs/dimensions/ + */ +module.exports.detectViewport = function() { + var e = windowAlias, a = 'inner'; + if (!('innerWidth' in windowAlias)) { + a = 'client'; + e = documentAlias.documentElement || documentAlias.body; + } + return e[a+'Width'] + 'x' + e[a+'Height']; +}; + +/** + * Gets the dimensions of the current + * document. + * + * Code based on: + * - http://andylangton.co.uk/articles/javascript/get-viewport-size-javascript/ + */ +module.exports.detectDocumentSize = function() { + var de = documentAlias.documentElement, // Alias + be = documentAlias.body; + var w = Math.max(de.clientWidth, de.offsetWidth, de.scrollWidth); + var h = Math.max(de.clientHeight, de.offsetHeight, de.scrollHeight, + be.offsetHeight, be.scrollHeight); + return isNaN(w) || isNaN(h) ? '' : w + 'x' + h; +}; + +/** + * Returns browser features (plugins, resolution, cookies) + * + * @param boolean useCookies Whether to test for cookies + * @param string testCookieName Name to use for the test cookie + * @return Object containing browser features + */ +module.exports.detectBrowserFeatures = function(useCookies, testCookieName) { + var i, + mimeType, + pluginMap = { + // document types + pdf: 'application/pdf', + + // media players + qt: 'video/quicktime', + realp: 'audio/x-pn-realaudio-plugin', + wma: 'application/x-mplayer2', + + // interactive multimedia + dir: 'application/x-director', + fla: 'application/x-shockwave-flash', + + // RIA + java: 'application/x-java-vm', + gears: 'application/x-googlegears', + ag: 'application/x-silverlight' + }, + features = {}; + + // General plugin detection + if (navigatorAlias.mimeTypes && navigatorAlias.mimeTypes.length) { + for (i in pluginMap) { + if (Object.prototype.hasOwnProperty.call(pluginMap, i)) { + mimeType = navigatorAlias.mimeTypes[pluginMap[i]]; + features[i] = (mimeType && mimeType.enabledPlugin) ? '1' : '0'; + } + } + } + + // Safari and Opera + // IE6/IE7 navigator.javaEnabled can't be aliased, so test directly + if (typeof navigatorAlias.javaEnabled !== 'unknown' && + !lodash.isUndefined(navigatorAlias.javaEnabled) && + navigatorAlias.javaEnabled()) { + features.java = '1'; + } + + // Firefox + if (lodash.isFunction(windowAlias.GearsFactory)) { + features.gears = '1'; + } + + // Other browser features + features.res = screenAlias.width + 'x' + screenAlias.height; + features.cd = screenAlias.colorDepth; + if (useCookies) { + features.cookie = module.exports.hasCookies(testCookieName); + } + + return features; +}; diff --git a/src/js/lib/helpers.js b/src/js/lib/helpers.js index bad8bd2c2..a7b6e770b 100644 --- a/src/js/lib/helpers.js +++ b/src/js/lib/helpers.js @@ -1,210 +1,206 @@ /* * JavaScript tracker for Snowplow: Snowplow.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. + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. * - * * 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. + * 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. + * 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 + * 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 - lodash = require('../lib_managed/lodash'), - - object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support - - /** - * Cleans up the page title - */ - object.fixupTitle = function (title) { - if (!lodash.isString(title)) { - title = title.text || ''; - - var tmp = document.getElementsByTagName('title'); - if (tmp && !lodash.isUndefined(tmp[0])) { - title = tmp[0].text; - } - } - return title; - }; - - /* - * Extract hostname from URL - */ - object.getHostName = function (url) { - // scheme : // [username [: password] @] hostname [: port] [/ [path] [? query] [# fragment]] - var e = new RegExp('^(?:(?:https?|ftp):)/*(?:[^@]+@)?([^:/#]+)'), - matches = e.exec(url); - - return matches ? matches[1] : url; - }; - - /* - * Fix-up domain - */ - object.fixupDomain = function (domain) { - var dl = domain.length; - - // remove trailing '.' - if (domain.charAt(--dl) === '.') { - domain = domain.slice(0, dl); - } - // remove leading '*' - if (domain.slice(0, 2) === '*.') { - domain = domain.slice(1); - } - return domain; - }; - - /* - * Get page referrer - */ - object.getReferrer = function () { - - var referrer = ''; - - var fromQs = object.fromQuerystring('referrer', window.location.href) || - object.fromQuerystring('referer', window.location.href); - - // Short-circuit - if (fromQs) { - return fromQs; - } - - try { - referrer = window.top.document.referrer; - } catch (e) { - if (window.parent) { - try { - referrer = window.parent.document.referrer; - } catch (e2) { - referrer = ''; - } - } - } - if (referrer === '') { - referrer = document.referrer; - } - return referrer; - }; - - /* - * Cross-browser helper function to add event handler - */ - object.addEventListener = function (element, eventType, eventHandler, useCapture) { - if (element.addEventListener) { - element.addEventListener(eventType, eventHandler, useCapture); - return true; - } - if (element.attachEvent) { - return element.attachEvent('on' + eventType, eventHandler); - } - element['on' + eventType] = eventHandler; - }; - - /* - * Return value from name-value pair in querystring - */ - object.fromQuerystring = function (field, url) { - var match = new RegExp('^[^#]*[?&]' + field + '=([^&#]*)').exec(url); - if (!match) { - return null; - } - return decodeURIComponent(match[1].replace(/\+/g, ' ')); - }; - - /* - * Only log deprecation warnings if they won't cause an error - */ - object.warn = function(message) { - if (typeof console !== 'undefined') { - console.warn('Snowplow: ' + message); - } - }; - - /* - * Check whether an element has at least one class from a given list - */ - function checkClass(elt, classList) { - var classes = lodash.map(elt.classList), - i; - - for (i = 0; i < classes.length; i++) { - if (classList[classes[i]]) { - return true; - } - } - return false; - } - - /** - * Convert a criterion object to a filter function - * - * @param object criterion Either {whitelist: [array of allowable strings]} - * or {blacklist: [array of allowable strings]} - * or {filter: function (elt) {return whether to track the element}} - * @param boolean byClass Whether to whitelist/blacklist based on an element's classes (for forms) - * or name attribute (for fields) - */ - object.getFilter = function (criterion, byClass) { - - // If the criterion argument is not an object, add listeners to all elements - if (lodash.isArray(criterion) || !lodash.isObject(criterion)) { - return function (elt) { - return true; - }; - } - - if (criterion.hasOwnProperty('filter')) { - return criterion.filter; - } else { - var inclusive = criterion.hasOwnProperty('whitelist'); - var specifiedClasses = criterion.whitelist || criterion.blacklist; - if (!lodash.isArray(specifiedClasses)) { - specifiedClasses = [specifiedClasses]; - } - - // Convert the array of classes to an object of the form {class1: true, class2: true, ...} - var specifiedClassesSet = {}; - for (var i=0; i= bufferSize) { - executeQueue(); - } - } - - /* - * Run through the queue of image beacons, sending them one at a time. - * Stops processing when we run out of queued requests, or we get an error. - */ - function executeQueue () { - - // Failsafe in case there is some way for a bad value like "null" to end up in the outQueue - while (outQueue.length && typeof outQueue[0] !== 'string' && typeof outQueue[0] !== 'object') { - outQueue.shift(); - } - - if (outQueue.length < 1) { - executingQueue = false; - return; - } - - // Let's check that we have a Url to ping - if (!lodash.isString(configCollectorUrl)) { - throw "No Snowplow collector configured, cannot track"; - } - - executingQueue = true; - - var nextRequest = outQueue[0]; - - if (usePost) { - - var xhr = new XMLHttpRequest(); - xhr.open('POST', configCollectorUrl, true); - xhr.withCredentials = true; - - // Time out POST requests after 5 seconds - var xhrTimeout = setTimeout(function () { - xhr.abort(); - executingQueue = false; - }, 5000); - - // Keep track of number of events to delete from queue - var eventCount = outQueue.length; - - xhr.onreadystatechange = function () { - if (xhr.readyState === 4 && xhr.status === 200) { - for (var deleteCount = 0; deleteCount < eventCount; deleteCount++) { - outQueue.shift(); - } - if (localStorageAccessible() && useLocalStorage) { - localStorage.setItem(queueName, json2.stringify(outQueue)); - } - clearTimeout(xhrTimeout); - executeQueue(); - } else if (xhr.readyState === 4 && xhr.status === 404) { - executingQueue = false; - } - }; - - xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); - var batchRequest = { - schema: 'iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-2', - data: outQueue - }; - if (outQueue.length > 0) { - xhr.send(json2.stringify(batchRequest)); - } - - } else { - - var image = new Image(1, 1); - - image.onload = function () { - outQueue.shift(); - if (localStorageAccessible() && useLocalStorage) { - localStorage.setItem(queueName, json2.stringify(outQueue)); - } - executeQueue(); - }; - - image.onerror = function () { - executingQueue = false; - }; - - image.src = configCollectorUrl + nextRequest; - } - - } - - return { - enqueueRequest: enqueueRequest, - executeQueue: executeQueue - }; - }; + var + json2 = require('JSON'), + lodash = require('./lib/utils'), + localStorageAccessible = require('./lib/detectors').localStorageAccessible; + + /** + * Object handling sending events to a collector. + * Instantiated once per tracker instance. + * + * @param string functionName The Snowplow function name (used to generate the localStorage key) + * @param string namespace The tracker instance's namespace (used to generate the localStorage key) + * @param object mutSnowplowState Gives the pageUnloadGuard a reference to the outbound queue + * so it can unload the page when all queues are empty + * @param boolean useLocalStorage Whether to use localStorage at all + * @param boolean usePost Whether to send events by POST or GET + * @param int bufferSize How many events to batch in localStorage before sending them all. + * Only applies when sending POST requests and when localStorage is available. + * @return object OutQueueManager instance + */ + + module.exports = { OutQueueManager: OutQueueManager }; + + function OutQueueManager(functionName, namespace, mutSnowplowState, useLocalStorage, usePost, bufferSize) { + var queueName, + executingQueue = false, + configCollectorUrl, + outQueue; + + // Fall back to GET for browsers which don't support CORS XMLHttpRequests (e.g. IE <= 9) + usePost = usePost && window.XMLHttpRequest && ('withCredentials' in new XMLHttpRequest()); + + var path = usePost ? '/com.snowplowanalytics.snowplow/tp2' : '/i'; + + bufferSize = (localStorageAccessible() && useLocalStorage && usePost && bufferSize) || 1; + + // Different queue names for GET and POST since they are stored differently + queueName = ['snowplowOutQueue', functionName, namespace, usePost ? 'post' : 'get'].join('_'); + + if (localStorageAccessible() && useLocalStorage) { + // Catch any JSON parse errors that might be thrown + try { + outQueue = json2.parse(localStorage.getItem(queueName)); + } + catch(e) {} + } + + // Initialize to and empty array if we didn't get anything out of localStorage + if (!lodash.isArray(outQueue)) { + outQueue = []; + } + + // Used by pageUnloadGuard + mutSnowplowState.outQueues.push(outQueue); + + if (usePost) { + mutSnowplowState.bufferFlushers.push(executeQueue); + } + + /* + * Convert a dictionary to a querystring + * The context field is the last in the querystring + */ + function getQuerystring(request) { + var querystring = '?', + lowPriorityKeys = {'co': true, 'cx': true}, + firstPair = true; + + for (var key in request) { + if (request.hasOwnProperty(key) && !(lowPriorityKeys.hasOwnProperty(key))) { + if (!firstPair) { + querystring += '&'; + } else { + firstPair = false; + } + querystring += encodeURIComponent(key) + '=' + encodeURIComponent(request[key]); + } + } + + for (var contextKey in lowPriorityKeys) { + if (request.hasOwnProperty(contextKey) && lowPriorityKeys.hasOwnProperty(contextKey)) { + querystring += '&' + contextKey + '=' + encodeURIComponent(request[contextKey]); + } + } + + return querystring; + } + + /* + * Convert numeric fields to strings to match payload_data schema + */ + function getBody(request) { + return lodash.mapValues(request, function (v) { + return v.toString(); + }); + } + + /* + * Queue an image beacon for submission to the collector. + * If we're not processing the queue, we'll start. + */ + function enqueueRequest (request, url) { + + outQueue.push(usePost ? getBody(request) : getQuerystring(request)); + configCollectorUrl = url + path; + if (localStorageAccessible() && useLocalStorage) { + localStorage.setItem(queueName, json2.stringify(outQueue)); + } + + if (!executingQueue && outQueue.length >= bufferSize) { + executeQueue(); + } + } + + /* + * Run through the queue of image beacons, sending them one at a time. + * Stops processing when we run out of queued requests, or we get an error. + */ + function executeQueue () { + + // Failsafe in case there is some way for a bad value like "null" to end up in the outQueue + while (outQueue.length && typeof outQueue[0] !== 'string' && typeof outQueue[0] !== 'object') { + outQueue.shift(); + } + + if (outQueue.length < 1) { + executingQueue = false; + return; + } + + // Let's check that we have a Url to ping + if (!lodash.isString(configCollectorUrl)) { + throw "No Snowplow collector configured, cannot track"; + } + + executingQueue = true; + + var nextRequest = outQueue[0]; + + if (usePost) { + + var xhr = new XMLHttpRequest(); + xhr.open('POST', configCollectorUrl, true); + xhr.withCredentials = true; + + // Time out POST requests after 5 seconds + var xhrTimeout = setTimeout(function () { + xhr.abort(); + executingQueue = false; + }, 5000); + + // Keep track of number of events to delete from queue + var eventCount = outQueue.length; + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status === 200) { + for (var deleteCount = 0; deleteCount < eventCount; deleteCount++) { + outQueue.shift(); + } + if (localStorageAccessible() && useLocalStorage) { + localStorage.setItem(queueName, json2.stringify(outQueue)); + } + clearTimeout(xhrTimeout); + executeQueue(); + } else if (xhr.readyState === 4 && xhr.status === 404) { + executingQueue = false; + } + }; + + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + var batchRequest = { + schema: 'iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-2', + data: outQueue + }; + if (outQueue.length > 0) { + xhr.send(json2.stringify(batchRequest)); + } + + } else { + + var image = new Image(1, 1); + + image.onload = function () { + outQueue.shift(); + if (localStorageAccessible() && useLocalStorage) { + localStorage.setItem(queueName, json2.stringify(outQueue)); + } + executeQueue(); + }; + + image.onerror = function () { + executingQueue = false; + }; + + image.src = configCollectorUrl + nextRequest; + } + + } + + return { + enqueueRequest: enqueueRequest, + executeQueue: executeQueue + }; + }; }()); diff --git a/src/js/snowplow.js b/src/js/snowplow.js index 1e817c88e..2fc363017 100644 --- a/src/js/snowplow.js +++ b/src/js/snowplow.js @@ -1,262 +1,221 @@ /* * JavaScript tracker for Snowplow: snowplow.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. + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. * - * * 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. + * 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. + * 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 + * 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. */ -/*jslint browser:true, plusplus:true, vars:true, nomen:true, evil:true */ -/*global window */ -/*global unescape */ -/*global ActiveXObject */ -/*global _snaq:true */ -/*members encodeURIComponent, decodeURIComponent, getElementsByTagName, - shift, unshift, - addEventListener, attachEvent, removeEventListener, detachEvent, - cookie, domain, readyState, documentElement, doScroll, title, text, - location, top, document, referrer, parent, links, href, protocol, GearsFactory, - event, which, button, srcElement, type, target, - parentNode, tagName, hostname, className, - userAgent, cookieEnabled, platform, mimeTypes, enabledPlugin, javaEnabled, - XDomainRequest, XMLHttpRequest, ActiveXObject, open, setRequestHeader, onreadystatechange, setRequestHeader, send, readyState, status, - getTime, getTimeAlias, setTime, toGMTString, getHours, getMinutes, getSeconds, - toLowerCase, charAt, indexOf, lastIndexOf, split, slice, toUpperCase, - onload, src, - round, random, - exec, - res, width, height, - pdf, qt, realp, wma, dir, fla, java, gears, ag, - hook, getHook, - setCollectorCf, setCollectorUrl, setAppId, - setDownloadExtensions, addDownloadExtensions, - setDomains, setIgnoreClasses, setRequestMethod, - setReferrerUrl, setCustomUrl, setDocumentTitle, - setDownloadClasses, setLinkClasses, - discardHashTag, - setCookieNamePrefix, setCookieDomain, setCookiePath, setVisitorIdCookie, - setVisitorCookieTimeout, setSessionCookieTimeout, setReferralCookieTimeout, - doNotTrack, respectDoNotTrack, msDoNotTrack, getTimestamp, getCookieValue, - detectTimezone, detectViewport, - addListener, enableLinkTracking, enableActivityTracking, setLinkTrackingTimer, - enableDarkSocialTracking, - killFrame, redirectFile, setCountPreRendered, - trackLink, trackPageView, trackImpression, - addPlugin, getAsyncTracker -*/ - -;(function() { - - // Load all our modules (at least until we fully modularize & remove grunt-concat) - var - lodash = require('./lib_managed/lodash'), - helpers = require('./lib/helpers'), - queue = require('./in_queue'), - tracker = require('./tracker'), - - object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support - - object.Snowplow = function(asynchronousQueue, functionName) { - - var - documentAlias = document, - windowAlias = window, - - /* Tracker identifier with version */ - version = 'js-' + '<%= pkg.version %>', // Update banner.js too - - /* Contains four variables that are shared with tracker.js and must be passed by reference */ - mutSnowplowState = { - - /* List of request queues - one per Tracker instance */ - outQueues: [], - bufferFlushers: [], - - /* Time at which to stop blocking excecution */ - expireDateTime: null, - - /* DOM Ready */ - hasLoaded: false, - registeredOnLoadHandlers: [] - }; - - /************************************************************ - * Private methods - ************************************************************/ - - - /* - * Handle beforeunload event - * - * Subject to Safari's "Runaway JavaScript Timer" and - * Chrome V8 extension that terminates JS that exhibits - * "slow unload", i.e., calling getTime() > 1000 times - */ - function beforeUnloadHandler() { - var now; - - // Flush all POST queues - lodash.forEach(mutSnowplowState.bufferFlushers, function (flusher) { - flusher(); - }) - - /* - * Delay/pause (blocks UI) - */ - if (mutSnowplowState.expireDateTime) { - // the things we do for backwards compatibility... - // in ECMA-262 5th ed., we could simply use: - // while (Date.now() < mutSnowplowState.expireDateTime) { } - do { - now = new Date(); - if (lodash.filter(mutSnowplowState.outQueues, function (queue) { - return queue.length > 0; - }).length === 0) { - break; - } - } while (now.getTime() < mutSnowplowState.expireDateTime); - } - } - - /* - * Handler for onload event - */ - function loadHandler() { - var i; - - if (!mutSnowplowState.hasLoaded) { - mutSnowplowState.hasLoaded = true; - for (i = 0; i < mutSnowplowState.registeredOnLoadHandlers.length; i++) { - mutSnowplowState.registeredOnLoadHandlers[i](); - } - } - return true; - } - - /* - * Add onload or DOM ready handler - */ - function addReadyListener() { - var _timer; - - if (documentAlias.addEventListener) { - helpers.addEventListener(documentAlias, 'DOMContentLoaded', function ready() { - documentAlias.removeEventListener('DOMContentLoaded', ready, false); - loadHandler(); - }); - } else if (documentAlias.attachEvent) { - documentAlias.attachEvent('onreadystatechange', function ready() { - if (documentAlias.readyState === 'complete') { - documentAlias.detachEvent('onreadystatechange', ready); - loadHandler(); - } - }); - - if (documentAlias.documentElement.doScroll && windowAlias === windowAlias.top) { - (function ready() { - if (!mutSnowplowState.hasLoaded) { - try { - documentAlias.documentElement.doScroll('left'); - } catch (error) { - setTimeout(ready, 0); - return; - } - loadHandler(); - } - }()); - } - } - - // sniff for older WebKit versions - if ((new RegExp('WebKit')).test(navigator.userAgent)) { - _timer = setInterval(function () { - if (mutSnowplowState.hasLoaded || /loaded|complete/.test(documentAlias.readyState)) { - clearInterval(_timer); - loadHandler(); - } - }, 10); - } - - // fallback - helpers.addEventListener(windowAlias, 'load', loadHandler, false); - } - - /************************************************************ - * Public data and methods - ************************************************************/ - - windowAlias.Snowplow = { - - /** - * Returns a Tracker object, configured with a - * CloudFront collector. - * - * @param string distSubdomain The subdomain on your CloudFront collector's distribution - */ - getTrackerCf: function (distSubdomain) { - var t = new tracker.Tracker(functionName, '', version, mutSnowplowState, {}); - t.setCollectorCf(distSubdomain); - return t; - }, - - /** - * Returns a Tracker object, configured with the - * URL to the collector to use. - * - * @param string rawUrl The collector URL minus protocol and /i - */ - getTrackerUrl: function (rawUrl) { - var t = new tracker.Tracker(functionName, '', version, mutSnowplowState, {}); - t.setCollectorUrl(rawUrl); - return t; - }, - - /** - * Get internal asynchronous tracker object - * - * @return Tracker - */ - getAsyncTracker: function () { - return new tracker.Tracker(functionName, '', version, mutSnowplowState, {}); - } - }; - - /************************************************************ - * Constructor - ************************************************************/ - - // initialize the Snowplow singleton - helpers.addEventListener(windowAlias, 'beforeunload', beforeUnloadHandler, false); - addReadyListener(); - // Now replace initialization array with queue manager object - return new queue.InQueueManager(tracker.Tracker, version, mutSnowplowState, asynchronousQueue, functionName); - }; +// Load all our modules (at least until we fully modularize & remove grunt-concat) +var + lodash = require('./lib/utils'), + helpers = require('./lib/helpers'), + InQueueManager = require('./in_queue').InQueueManager, + Tracker = require('./tracker').Tracker; + +module.exports = { Snowplow: Snowplow }; + +function Snowplow(asynchronousQueue, functionName) { + + var + documentAlias = document, + windowAlias = window, + + /* Tracker identifier with version */ + version = 'js-' + '<%= pkg.version %>', // Update banner.js too + + /* Contains four variables that are shared with tracker.js and must be passed by reference */ + mutSnowplowState = { + + /* List of request queues - one per Tracker instance */ + outQueues: [], + bufferFlushers: [], + + /* Time at which to stop blocking excecution */ + expireDateTime: null, + + /* DOM Ready */ + hasLoaded: false, + registeredOnLoadHandlers: [] + }; + + /************************************************************ + * Private methods + ************************************************************/ + + + /* + * Handle beforeunload event + * + * Subject to Safari's "Runaway JavaScript Timer" and + * Chrome V8 extension that terminates JS that exhibits + * "slow unload", i.e., calling getTime() > 1000 times + */ + function beforeUnloadHandler() { + var now; + + // Flush all POST queues + lodash.forEach(mutSnowplowState.bufferFlushers, function (flusher) { + flusher(); + }) + + /* + * Delay/pause (blocks UI) + */ + if (mutSnowplowState.expireDateTime) { + // the things we do for backwards compatibility... + // in ECMA-262 5th ed., we could simply use: + // while (Date.now() < mutSnowplowState.expireDateTime) { } + do { + now = new Date(); + if (lodash.filter(mutSnowplowState.outQueues, function (queue) { + return queue.length > 0; + }).length === 0) { + break; + } + } while (now.getTime() < mutSnowplowState.expireDateTime); + } + } + + /* + * Handler for onload event + */ + function loadHandler() { + var i; + + if (!mutSnowplowState.hasLoaded) { + mutSnowplowState.hasLoaded = true; + for (i = 0; i < mutSnowplowState.registeredOnLoadHandlers.length; i++) { + mutSnowplowState.registeredOnLoadHandlers[i](); + } + } + return true; + } + + /* + * Add onload or DOM ready handler + */ + function addReadyListener() { + var _timer; + + if (documentAlias.addEventListener) { + helpers.addEventListener(documentAlias, 'DOMContentLoaded', function ready() { + documentAlias.removeEventListener('DOMContentLoaded', ready, false); + loadHandler(); + }); + } else if (documentAlias.attachEvent) { + documentAlias.attachEvent('onreadystatechange', function ready() { + if (documentAlias.readyState === 'complete') { + documentAlias.detachEvent('onreadystatechange', ready); + loadHandler(); + } + }); + + if (documentAlias.documentElement.doScroll && windowAlias === windowAlias.top) { + (function ready() { + if (!mutSnowplowState.hasLoaded) { + try { + documentAlias.documentElement.doScroll('left'); + } catch (error) { + setTimeout(ready, 0); + return; + } + loadHandler(); + } + }()); + } + } + + // sniff for older WebKit versions + if ((new RegExp('WebKit')).test(navigator.userAgent)) { + _timer = setInterval(function () { + if (mutSnowplowState.hasLoaded || /loaded|complete/.test(documentAlias.readyState)) { + clearInterval(_timer); + loadHandler(); + } + }, 10); + } + + // fallback + helpers.addEventListener(windowAlias, 'load', loadHandler, false); + } + + /************************************************************ + * Public data and methods + ************************************************************/ + + windowAlias.Snowplow = { + + /** + * Returns a Tracker object, configured with a + * CloudFront collector. + * + * @param string distSubdomain The subdomain on your CloudFront collector's distribution + */ + getTrackerCf: function (distSubdomain) { + var t = new Tracker(functionName, '', version, mutSnowplowState, {}); + t.setCollectorCf(distSubdomain); + return t; + }, + + /** + * Returns a Tracker object, configured with the + * URL to the collector to use. + * + * @param string rawUrl The collector URL minus protocol and /i + */ + getTrackerUrl: function (rawUrl) { + var t = new Tracker(functionName, '', version, mutSnowplowState, {}); + t.setCollectorUrl(rawUrl); + return t; + }, + + /** + * Get internal asynchronous tracker object + * + * @return Tracker + */ + getAsyncTracker: function () { + return new Tracker(functionName, '', version, mutSnowplowState, {}); + } + }; + + /************************************************************ + * Constructor + ************************************************************/ + + // initialize the Snowplow singleton + helpers.addEventListener(windowAlias, 'beforeunload', beforeUnloadHandler, false); + addReadyListener(); + + // Now replace initialization array with queue manager object + return new InQueueManager(Tracker, version, mutSnowplowState, asynchronousQueue, functionName); +}; -}()); diff --git a/src/js/tracker.js b/src/js/tracker.js index a624a7c40..c7e134d91 100644 --- a/src/js/tracker.js +++ b/src/js/tracker.js @@ -1,1491 +1,1489 @@ /* * JavaScript tracker for Snowplow: tracker.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. + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. * - * * 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. + * 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. + * 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 + * 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 - lodash = require('./lib_managed/lodash'), - helpers = require('./lib/helpers'), - proxies = require('./lib/proxies'), - cookie = require('browser-cookie-lite'), - detectors = require('./lib/detectors'), - json2 = require('JSON'), - sha1 = require('sha1'), - links = require('./links'), - forms = require('./forms'), - requestQueue = require('./out_queue'), - coreConstructor = require('snowplow-tracker-core'), - - object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support - - /** - * Snowplow Tracker class - * - * @param namespace The namespace of the tracker object - - * @param version The current version of the JavaScript Tracker - * - * @param mutSnowplowState An object containing hasLoaded, registeredOnLoadHandlers, and expireDateTime - * Passed in by reference in case they are altered by snowplow.js - * - * @param argmap Optional dictionary of configuration options. Supported fields and their default values: - * - * 1. encodeBase64, true - * 2. cookieDomain, null - * 3. cookieName, '_sp_' - * 4. appId, '' - * 5. platform, 'web' - * 6. respectDoNotTrack, false - * 7. userFingerprint, true - * 8. userFingerprintSeed, 123412414 - * 9. pageUnloadTimer, 500 - * 10. forceSecureTracker, false - * 11. useLocalStorage, true - * 12. useCookies, true - * 13. writeCookies, true - * 14. contexts, {} - * 15. post, false - * 16. bufferSize, 1 - */ - object.Tracker = function Tracker(functionName, namespace, version, mutSnowplowState, argmap) { - - /************************************************************ - * Private members - ************************************************************/ - var - // Tracker core - core = coreConstructor(true, function(payload) { - addBrowserData(payload); - sendRequest(payload, configTrackerPause); - }), - - // Aliases - documentAlias = document, - windowAlias = window, - navigatorAlias = navigator, - - // Current URL and Referrer URL - locationArray = proxies.fixupUrl(documentAlias.domain, windowAlias.location.href, helpers.getReferrer()), - domainAlias = helpers.fixupDomain(locationArray[0]), - locationHrefAlias = locationArray[1], - configReferrerUrl = locationArray[2], - - argmap = argmap || {}, - - // Request method is always GET for Snowplow - configRequestMethod = 'GET', - // Platform defaults to web for this tracker - configPlatform = argmap.hasOwnProperty('platform') ? argmap.platform : 'web', +var + lodash = require('./lib/utils'), + helpers = require('./lib/helpers'), + proxies = require('./lib/proxies'), + cookie = require('browser-cookie-lite'), + detectors = require('./lib/detectors'), + json2 = require('JSON'), + sha1 = require('sha1'), + links = require('./links'), + forms = require('./forms'), + OutQueueManager = require('./out_queue').OutQueueManager, + coreConstructor = require('snowplow-tracker-core'); + +/** + * Snowplow Tracker class + * + * @param namespace The namespace of the tracker object + + * @param version The current version of the JavaScript Tracker + * + * @param mutSnowplowState An object containing hasLoaded, registeredOnLoadHandlers, and expireDateTime + * Passed in by reference in case they are altered by snowplow.js + * + * @param argmap Optional dictionary of configuration options. Supported fields and their default values: + * + * 1. encodeBase64, true + * 2. cookieDomain, null + * 3. cookieName, '_sp_' + * 4. appId, '' + * 5. platform, 'web' + * 6. respectDoNotTrack, false + * 7. userFingerprint, true + * 8. userFingerprintSeed, 123412414 + * 9. pageUnloadTimer, 500 + * 10. forceSecureTracker, false + * 11. useLocalStorage, true + * 12. useCookies, true + * 13. writeCookies, true + * 14. contexts, {} + * 15. post, false + * 16. bufferSize, 1 + */ + +module.exports = { Tracker: Tracker }; + +function Tracker(functionName, namespace, version, mutSnowplowState, argmap) { + + /************************************************************ + * Private members + ************************************************************/ + var + // Tracker core + core = coreConstructor(true, function(payload) { + addBrowserData(payload); + sendRequest(payload, configTrackerPause); + }), + + // Aliases + documentAlias = document, + windowAlias = window, + navigatorAlias = navigator, + + // Current URL and Referrer URL + locationArray = proxies.fixupUrl(documentAlias.domain, windowAlias.location.href, helpers.getReferrer()), + domainAlias = helpers.fixupDomain(locationArray[0]), + locationHrefAlias = locationArray[1], + configReferrerUrl = locationArray[2], + + argmap = argmap || {}, + + // Request method is always GET for Snowplow + configRequestMethod = 'GET', + + // Platform defaults to web for this tracker + configPlatform = argmap.hasOwnProperty('platform') ? argmap.platform : 'web', + + // Snowplow collector URL + configCollectorUrl, - // Snowplow collector URL - configCollectorUrl, + // Site ID + configTrackerSiteId = argmap.hasOwnProperty('appId') ? argmap.appId : '', // Updated for Snowplow - // Site ID - configTrackerSiteId = argmap.hasOwnProperty('appId') ? argmap.appId : '', // Updated for Snowplow - - // Document URL - configCustomUrl, + // Document URL + configCustomUrl, - // Document title - configTitle = documentAlias.title, + // Document title + configTitle = documentAlias.title, - // Maximum delay to wait for web bug image to be fetched (in milliseconds) - configTrackerPause = argmap.hasOwnProperty('pageUnloadTimer') ? argmap.pageUnloadTimer : 500, + // Maximum delay to wait for web bug image to be fetched (in milliseconds) + configTrackerPause = argmap.hasOwnProperty('pageUnloadTimer') ? argmap.pageUnloadTimer : 500, - // Minimum visit time after initial page view (in milliseconds) - configMinimumVisitTime, + // Minimum visit time after initial page view (in milliseconds) + configMinimumVisitTime, - // Recurring heart beat after initial ping (in milliseconds) - configHeartBeatTimer, + // Recurring heart beat after initial ping (in milliseconds) + configHeartBeatTimer, - // Disallow hash tags in URL. TODO: Should this be set to true by default? - configDiscardHashTag, + // Disallow hash tags in URL. TODO: Should this be set to true by default? + configDiscardHashTag, - // First-party cookie name prefix - configCookieNamePrefix = argmap.hasOwnProperty('cookieName') ? argmap.cookieName : '_sp_', + // First-party cookie name prefix + configCookieNamePrefix = argmap.hasOwnProperty('cookieName') ? argmap.cookieName : '_sp_', - // First-party cookie domain - // User agent defaults to origin hostname - configCookieDomain = argmap.hasOwnProperty('cookieDomain') ? argmap.cookieDomain : null, + // First-party cookie domain + // User agent defaults to origin hostname + configCookieDomain = argmap.hasOwnProperty('cookieDomain') ? argmap.cookieDomain : null, - // First-party cookie path - // Default is user agent defined. - configCookiePath = '/', + // First-party cookie path + // Default is user agent defined. + configCookiePath = '/', - configWriteCookies = argmap.hasOwnProperty('writeCookies') ? argmap.writeCookies : true, + configWriteCookies = argmap.hasOwnProperty('writeCookies') ? argmap.writeCookies : true, - // Do Not Track browser feature - dnt = navigatorAlias.doNotTrack || navigatorAlias.msDoNotTrack, + // Do Not Track browser feature + dnt = navigatorAlias.doNotTrack || navigatorAlias.msDoNotTrack, - // Do Not Track - configDoNotTrack = argmap.hasOwnProperty('respectDoNotTrack') ? argmap.respectDoNotTrack && (dnt === 'yes' || dnt === '1') : false, + // Do Not Track + configDoNotTrack = argmap.hasOwnProperty('respectDoNotTrack') ? argmap.respectDoNotTrack && (dnt === 'yes' || dnt === '1') : false, - // Count sites which are pre-rendered - configCountPreRendered, + // Count sites which are pre-rendered + configCountPreRendered, - // Life of the visitor cookie (in seconds) - configVisitorCookieTimeout = 63072000, // 2 years + // Life of the visitor cookie (in seconds) + configVisitorCookieTimeout = 63072000, // 2 years - // Life of the session cookie (in seconds) - configSessionCookieTimeout = 1800, // 30 minutes + // Life of the session cookie (in seconds) + configSessionCookieTimeout = 1800, // 30 minutes - // Default hash seed for MurmurHash3 in detectors.detectSignature - configUserFingerprintHashSeed = argmap.hasOwnProperty('userFingerprintSeed') ? argmap.userFingerprintSeed : 123412414, + // Default hash seed for MurmurHash3 in detectors.detectSignature + configUserFingerprintHashSeed = argmap.hasOwnProperty('userFingerprintSeed') ? argmap.userFingerprintSeed : 123412414, - // Document character set - documentCharset = documentAlias.characterSet || documentAlias.charset, + // Document character set + documentCharset = documentAlias.characterSet || documentAlias.charset, - // This forces the tracker to be HTTPS even if the page is not secure - forceSecureTracker = argmap.hasOwnProperty('forceSecureTracker') ? (argmap.forceSecureTracker === true) : false, + // This forces the tracker to be HTTPS even if the page is not secure + forceSecureTracker = argmap.hasOwnProperty('forceSecureTracker') ? (argmap.forceSecureTracker === true) : false, - // Whether to use localStorage to store events between sessions while offline - useLocalStorage = argmap.hasOwnProperty('useLocalStorage') ? argmap.useLocalStorage : true, + // Whether to use localStorage to store events between sessions while offline + useLocalStorage = argmap.hasOwnProperty('useLocalStorage') ? argmap.useLocalStorage : true, - // Whether to use cookies - configUseCookies = argmap.hasOwnProperty('useCookies') ? argmap.useCookies : true, + // Whether to use cookies + configUseCookies = argmap.hasOwnProperty('useCookies') ? argmap.useCookies : true, - // Browser language (or Windows language for IE). Imperfect but CloudFront doesn't log the Accept-Language header - browserLanguage = navigatorAlias.userLanguage || navigatorAlias.language, + // Browser language (or Windows language for IE). Imperfect but CloudFront doesn't log the Accept-Language header + browserLanguage = navigatorAlias.userLanguage || navigatorAlias.language, - // Browser features via client-side data collection - browserFeatures = detectors.detectBrowserFeatures(configUseCookies, getSnowplowCookieName('testcookie')), + // Browser features via client-side data collection + browserFeatures = detectors.detectBrowserFeatures(configUseCookies, getSnowplowCookieName('testcookie')), - // Visitor fingerprint - userFingerprint = (argmap.userFingerprint === false) ? '' : detectors.detectSignature(configUserFingerprintHashSeed), + // Visitor fingerprint + userFingerprint = (argmap.userFingerprint === false) ? '' : detectors.detectSignature(configUserFingerprintHashSeed), - // Unique ID for the tracker instance used to mark links which are being tracked - trackerId = functionName + '_' + namespace, + // Unique ID for the tracker instance used to mark links which are being tracked + trackerId = functionName + '_' + namespace, - // Guard against installing the activity tracker more than once per Tracker instance - activityTrackingInstalled = false, + // Guard against installing the activity tracker more than once per Tracker instance + activityTrackingInstalled = false, - // Last activity timestamp - lastActivityTime, + // Last activity timestamp + lastActivityTime, - // How are we scrolling? - minXOffset, - maxXOffset, - minYOffset, - maxYOffset, + // How are we scrolling? + minXOffset, + maxXOffset, + minYOffset, + maxYOffset, - // Hash function - hash = sha1, + // Hash function + hash = sha1, - // Domain hash value - domainHash, + // Domain hash value + domainHash, - // Domain unique user ID - domainUserId, + // Domain unique user ID + domainUserId, - // Business-defined unique user ID - businessUserId, + // Business-defined unique user ID + businessUserId, - // Ecommerce transaction data - // Will be committed, sent and emptied by a call to trackTrans. - ecommerceTransaction = ecommerceTransactionTemplate(), + // Ecommerce transaction data + // Will be committed, sent and emptied by a call to trackTrans. + ecommerceTransaction = ecommerceTransactionTemplate(), - // Manager for automatic link click tracking - linkTrackingManager = links.getLinkTrackingManager(core, trackerId), + // Manager for automatic link click tracking + linkTrackingManager = links.getLinkTrackingManager(core, trackerId), - // Manager for automatic form tracking - formTrackingManager = forms.getFormTrackingManager(core, trackerId), - - // Manager for local storage queue - outQueueManager = new requestQueue.OutQueueManager(functionName, namespace, mutSnowplowState, useLocalStorage, argmap.post, argmap.bufferSize), - - // Flag to prevent the geolocation context being added multiple times - geolocationContextAdded = false, - - // Set of contexts to be added to every event - autoContexts = argmap.contexts || {}, - - // Context to be added to every event - commonContexts = []; - - if (autoContexts.performanceTiming) { - commonContexts.push(getPerformanceTimingContext()); - } - - if (autoContexts.gaCookies) { - commonContexts.push(getGaCookiesContext()); - } - - if (autoContexts.geolocation) { - enableGeolocationContext(); - } - - // Enable base 64 encoding for unstructured events and custom contexts - core.setBase64Encoding(argmap.hasOwnProperty('encodeBase64') ? argmap.encodeBase64 : true); - - // Set up unchanging name-value pairs - core.setTrackerVersion(version); - core.setTrackerNamespace(namespace); - core.setAppId(configTrackerSiteId); - core.setPlatform(configPlatform); - core.setTimezone(detectors.detectTimezone()); - core.addPayloadPair('lang', browserLanguage); - core.addPayloadPair('cs', documentCharset); - - // Browser features. Cookies, color depth and resolution don't get prepended with f_ (because they're not optional features) - for (var i in browserFeatures) { - if (Object.prototype.hasOwnProperty.call(browserFeatures, i)) { - if (i === 'res' || i === 'cd' || i === 'cookie') { - core.addPayloadPair(i, browserFeatures[i]); - } else { - core.addPayloadPair('f_' + i, browserFeatures[i]); - } - } - } - - /* - * Initializes an empty ecommerce - * transaction and line items - */ - function ecommerceTransactionTemplate() { - return { - transaction: {}, - items: [] - }; - } - - /* - * Removes hash tag from the URL - * - * URLs are purified before being recorded in the cookie, - * or before being sent as GET parameters - */ - function purify(url) { - var targetPattern; - - if (configDiscardHashTag) { - targetPattern = new RegExp('#.*'); - return url.replace(targetPattern, ''); - } - return url; - } - - /* - * Extract scheme/protocol from URL - */ - function getProtocolScheme(url) { - var e = new RegExp('^([a-z]+):'), - matches = e.exec(url); - - return matches ? matches[1] : null; - } - - /* - * Resolve relative reference - * - * Note: not as described in rfc3986 section 5.2 - */ - function resolveRelativeReference(baseUrl, url) { - var protocol = getProtocolScheme(url), - i; - - if (protocol) { - return url; - } - - if (url.slice(0, 1) === '/') { - return getProtocolScheme(baseUrl) + '://' + helpers.getHostName(baseUrl) + url; - } - - baseUrl = purify(baseUrl); - if ((i = baseUrl.indexOf('?')) >= 0) { - baseUrl = baseUrl.slice(0, i); - } - if ((i = baseUrl.lastIndexOf('/')) !== baseUrl.length - 1) { - baseUrl = baseUrl.slice(0, i + 1); - } - - return baseUrl + url; - } - - /* - * Send request - */ - function sendRequest(request, delay) { - var now = new Date(); - - if (!configDoNotTrack) { - outQueueManager.enqueueRequest(request.build(), configCollectorUrl); - mutSnowplowState.expireDateTime = now.getTime() + delay; - } - } - - /* - * Get cookie name with prefix and domain hash - */ - function getSnowplowCookieName(baseName) { - return configCookieNamePrefix + baseName + '.' + domainHash; - } - - /* - * Cookie getter. - */ - function getSnowplowCookieValue(cookieName) { - return cookie.cookie(getSnowplowCookieName(cookieName)); - } - - /* - * Update domain hash - */ - function updateDomainHash() { - domainHash = hash((configCookieDomain || domainAlias) + (configCookiePath || '/')).slice(0, 4); // 4 hexits = 16 bits - } - - /* - * Process all "activity" events. - * For performance, this function must have low overhead. - */ - function activityHandler() { - var now = new Date(); - lastActivityTime = now.getTime(); - } - - /* - * Process all "scroll" events. - */ - function scrollHandler() { - updateMaxScrolls(); - activityHandler(); - } - - /* - * Returns [pageXOffset, pageYOffset]. - * Adapts code taken from: http://www.javascriptkit.com/javatutors/static2.shtml - */ - function getPageOffsets() { - var iebody = (documentAlias.compatMode && documentAlias.compatMode != "BackCompat") ? - documentAlias.documentElement : - documentAlias.body; - return [iebody.scrollLeft || windowAlias.pageXOffset, iebody.scrollTop || windowAlias.pageYOffset]; - } - - /* - * Quick initialization/reset of max scroll levels - */ - function resetMaxScrolls() { - var offsets = getPageOffsets(); - - var x = offsets[0]; - minXOffset = x; - maxXOffset = x; - - var y = offsets[1]; - minYOffset = y; - maxYOffset = y; - } - - /* - * Check the max scroll levels, updating as necessary - */ - function updateMaxScrolls() { - var offsets = getPageOffsets(); - - var x = offsets[0]; - if (x < minXOffset) { - minXOffset = x; - } else if (x > maxXOffset) { - maxXOffset = x; - } - - var y = offsets[1]; - if (y < minYOffset) { - minYOffset = y; - } else if (y > maxYOffset) { - maxYOffset = y; - } - } - - /* - * Prevents offsets from being decimal or NaN - * See https://github.com/snowplow/snowplow-javascript-tracker/issues/324 - * TODO: the NaN check should be moved into the core - */ - function cleanOffset(offset) { - var rounded = Math.round(offset); - if (!isNaN(rounded)) { - return rounded; - } - } - - /* - * Sets the Visitor ID cookie: either the first time loadDomainUserIdCookie is called - * or when there is a new visit or a new page view - */ - function setDomainUserIdCookie(_domainUserId, createTs, visitCount, nowTs, lastVisitTs) { - cookie.cookie(getSnowplowCookieName('id'), _domainUserId + '.' + createTs + '.' + visitCount + '.' + nowTs + '.' + lastVisitTs, configVisitorCookieTimeout, configCookiePath, configCookieDomain); - } - - /* - * Load visitor ID cookie - */ - function loadDomainUserIdCookie() { - if (!configUseCookies) { - return []; - } - var now = new Date(), - nowTs = Math.round(now.getTime() / 1000), - id = getSnowplowCookieValue('id'), - tmpContainer; - - if (id) { - tmpContainer = id.split('.'); - // New visitor set to 0 now - tmpContainer.unshift('0'); - } else { - // Domain - generate a pseudo-unique ID to fingerprint this user; - // Note: this isn't a RFC4122-compliant UUID - if (!domainUserId) { - domainUserId = hash( - (navigatorAlias.userAgent || '') + - (navigatorAlias.platform || '') + - json2.stringify(browserFeatures) + nowTs - ).slice(0, 16); // 16 hexits = 64 bits - } - - tmpContainer = [ - // New visitor - '1', - // Domain user ID - domainUserId, - // Creation timestamp - seconds since Unix epoch - nowTs, - // visitCount - 0 = no previous visit - 0, - // Current visit timestamp - nowTs, - // Last visit timestamp - blank meaning no previous visit - '' - ]; - } - return tmpContainer; - } - - /* - * Attaches common web fields to every request - * (resolution, url, referrer, etc.) - * Also sets the required cookies. - */ - function addBrowserData(sb) { - var nowTs = Math.round(new Date().getTime() / 1000), - idname = getSnowplowCookieName('id'), - sesname = getSnowplowCookieName('ses'), - ses = getSnowplowCookieValue('ses'), // aka cookie.cookie(sesname) - id = loadDomainUserIdCookie(), - _domainUserId = id[1], // We could use the global (domainUserId) but this is better etiquette - createTs = id[2], - visitCount = id[3], - currentVisitTs = id[4], - lastVisitTs = id[5]; - - if (configDoNotTrack && configUseCookies && configWriteCookies) { - cookie.cookie(idname, '', -1, configCookiePath, configCookieDomain); - cookie.cookie(sesname, '', -1, configCookiePath, configCookieDomain); - return; - } - - // New session? - if (!ses && configUseCookies) { - // New session (aka new visit) - visitCount++; - // Update the last visit timestamp - lastVisitTs = currentVisitTs; - } - - // Build out the rest of the request - sb.add('vp', detectors.detectViewport()); - sb.add('ds', detectors.detectDocumentSize()); - sb.add('vid', visitCount); - sb.add('duid', _domainUserId); // Set to our local variable - sb.add('fp', userFingerprint); - sb.add('uid', businessUserId); - - // Adds with custom conditions - if (configReferrerUrl.length) { - sb.add('refr', purify(configReferrerUrl)); - } - - // Add the page URL last as it may take us over the IE limit (and we don't always need it) - sb.add('url', purify(configCustomUrl || locationHrefAlias)); - - - // Update cookies - if (configUseCookies && configWriteCookies) { - setDomainUserIdCookie(_domainUserId, createTs, visitCount, nowTs, lastVisitTs); - cookie.cookie(sesname, '*', configSessionCookieTimeout, configCookiePath, configCookieDomain); - } - } - - /** - * Builds a collector URL from a CloudFront distribution. - * We don't bother to support custom CNAMEs because Amazon CloudFront doesn't support that for SSL. - * - * @param string account The account ID to build the tracker URL from - * - * @return string The URL on which the collector is hosted - */ - function collectorUrlFromCfDist(distSubdomain) { - return asCollectorUrl(distSubdomain + '.cloudfront.net'); - } - - /** - * Adds the protocol in front of our collector URL, and i to the end - * - * @param string rawUrl The collector URL without protocol - * - * @return string collectorUrl The tracker URL with protocol - */ - function asCollectorUrl(rawUrl) { - if (forceSecureTracker) - return ('https' + '://' + rawUrl); - else - return ('https:' === documentAlias.location.protocol ? 'https' : 'http') + '://' + rawUrl; - } - - /** - * Add common contexts to every event - * TODO: move this functionality into the core - * - * @param array userContexts List of user-defined contexts - * @return userContexts combined with commonContexts - */ - function addCommonContexts(userContexts) { - return commonContexts.concat(userContexts || []); - } - - /** - * Creates a context from the window.performance.timing object - * - * @return object PerformanceTiming context - */ - function getPerformanceTimingContext() { - var performance = windowAlias.performance || windowAlias.mozPerformance || windowAlias.msPerformance || windowAlias.webkitPerformance; - if (performance) { - - // On Safari, the fields we are interested in are on the prototype chain of - // performance.timing so we cannot copy them using lodash.clone - var performanceTiming = {}; - for (var field in performance.timing) { - // Don't copy the toJSON method - if (!lodash.isFunction(performance.timing[field])) { - performanceTiming[field] = performance.timing[field]; - } - } - - // Old Chrome versions add an unwanted requestEnd field - delete performanceTiming.requestEnd; - - // Add the Chrome firstPaintTime to the performance if it exists - if (windowAlias.chrome && windowAlias.chrome.loadTimes && typeof windowAlias.chrome.loadTimes().firstPaintTime === 'number') { - performanceTiming.chromeFirstPaint = Math.round(windowAlias.chrome.loadTimes().firstPaintTime * 1000); - } - - return { - schema: 'iglu:org.w3/PerformanceTiming/jsonschema/1-0-0', - data: performanceTiming - }; - } - } - - /** - * Attempts to create a context using the geolocation API and add it to commonContexts - */ - function enableGeolocationContext() { - if (!geolocationContextAdded && navigatorAlias.geolocation && navigatorAlias.geolocation.getCurrentPosition) { - geolocationContextAdded = true; - navigator.geolocation.getCurrentPosition(function (position) { - var coords = position.coords; - var geolocationContext = { - schema: 'iglu:com.snowplowanalytics.snowplow/geolocation_context/jsonschema/1-1-0', - data: { - latitude: coords.latitude, - longitude: coords.longitude, - latitudeLongitudeAccuracy: coords.accuracy, - altitude: coords.altitude, - altitudeAccuracy: coords.altitudeAccuracy, - bearing: coords.heading, - speed: coords.speed, - timestamp: position.timestamp - } - }; - commonContexts.push(geolocationContext); - }); - } - } - - /** - * Creates a context containing the values of the cookies set by GA - * - * @return object GA cookies context - */ - function getGaCookiesContext() { - var gaCookieData = {}; - lodash.forEach(['__utma', '__utmb', '__utmc', '__utmv', '__utmz', '_ga'], function (cookieType) { - var value = cookie.cookie(cookieType); - if (value) { - gaCookieData[cookieType] = value; - } - }); - return { - schema: 'iglu:com.google.analytics/cookies/jsonschema/1-0-0', - data: gaCookieData - }; - } - - /** - * Log the page view / visit - * - * @param string customTitle The user-defined page title to attach to this page view - * @param object context Custom context relating to the event - */ - function logPageView(customTitle, context) { - - // Fixup page title. We'll pass this to logPagePing too. - var pageTitle = helpers.fixupTitle(customTitle || configTitle); - - // Log page view - core.trackPageView(purify(configCustomUrl || locationHrefAlias), pageTitle, purify(configReferrerUrl), addCommonContexts(context)); - - // Send ping (to log that user has stayed on page) - var now = new Date(); - if (configMinimumVisitTime && configHeartBeatTimer && !activityTrackingInstalled) { - activityTrackingInstalled = true; - - // Capture our initial scroll points - resetMaxScrolls(); - - // Add event handlers; cross-browser compatibility here varies significantly - // @see http://quirksmode.org/dom/events - helpers.addEventListener(documentAlias, 'click', activityHandler); - helpers.addEventListener(documentAlias, 'mouseup', activityHandler); - helpers.addEventListener(documentAlias, 'mousedown', activityHandler); - helpers.addEventListener(documentAlias, 'mousemove', activityHandler); - helpers.addEventListener(documentAlias, 'mousewheel', activityHandler); - helpers.addEventListener(windowAlias, 'DOMMouseScroll', activityHandler); - helpers.addEventListener(windowAlias, 'scroll', scrollHandler); // Will updateMaxScrolls() for us - helpers.addEventListener(documentAlias, 'keypress', activityHandler); - helpers.addEventListener(documentAlias, 'keydown', activityHandler); - helpers.addEventListener(documentAlias, 'keyup', activityHandler); - helpers.addEventListener(windowAlias, 'resize', activityHandler); - helpers.addEventListener(windowAlias, 'focus', activityHandler); - helpers.addEventListener(windowAlias, 'blur', activityHandler); - - // Periodic check for activity. - lastActivityTime = now.getTime(); - setInterval(function heartBeat() { - var now = new Date(); - - // There was activity during the heart beat period; - // on average, this is going to overstate the visitDuration by configHeartBeatTimer/2 - if ((lastActivityTime + configHeartBeatTimer) > now.getTime()) { - // Send ping if minimum visit time has elapsed - if (configMinimumVisitTime < now.getTime()) { - logPagePing(pageTitle, context); // Grab the min/max globals - } - } - }, configHeartBeatTimer); - } - } - - /** - * Log that a user is still viewing a given page - * by sending a page ping. - * Not part of the public API - only called from - * logPageView() above. - * - * @param string pageTitle The page title to attach to this page ping - * @param object context Custom context relating to the event - */ - function logPagePing(pageTitle, context) { - core.trackPagePing( - purify(configCustomUrl || locationHrefAlias), - pageTitle, - purify(configReferrerUrl), - cleanOffset(minXOffset), - cleanOffset(maxXOffset), - cleanOffset(minYOffset), - cleanOffset(maxYOffset), - addCommonContexts(context)); - resetMaxScrolls(); - } - - /** - * Log ecommerce transaction metadata - * - * @param string orderId - * @param string affiliation - * @param string total - * @param string tax - * @param string shipping - * @param string city - * @param string state - * @param string country - * @param string currency The currency the total/tax/shipping are expressed in - * @param object context Custom context relating to the event - */ - function logTransaction(orderId, affiliation, total, tax, shipping, city, state, country, currency, context) { - core.trackEcommerceTransaction(orderId, affiliation, total, tax, shipping, city, state, country, currency, addCommonContexts(context)); - } - - /** - * Log ecommerce transaction item - * - * @param string orderId - * @param string sku - * @param string name - * @param string category - * @param string price - * @param string quantity - * @param string currency The currency the price is expressed in - * @param object context Custom context relating to the event - */ - function logTransactionItem(orderId, sku, name, category, price, quantity, currency, context) { - core.trackEcommerceTransactionItem(orderId, sku, name, category, price, quantity, currency, addCommonContexts(context)); - } - - /* - * Browser prefix - */ - function prefixPropertyName(prefix, propertyName) { - - if (prefix !== '') { - return prefix + propertyName.charAt(0).toUpperCase() + propertyName.slice(1); - } - - return propertyName; - } - - /** - * Check for pre-rendered web pages, and log the page view/link - * according to the configuration and/or visibility - * - * @see http://dvcs.w3.org/hg/webperf/raw-file/tip/specs/PageVisibility/Overview.html - */ - function trackCallback(callback) { - var isPreRendered, - i, - // Chrome 13, IE10, FF10 - prefixes = ['', 'webkit', 'ms', 'moz'], - prefix; - - if (!configCountPreRendered) { - for (i = 0; i < prefixes.length; i++) { - prefix = prefixes[i]; - - // does this browser support the page visibility API? - if (Object.prototype.hasOwnProperty.call(documentAlias, prefixPropertyName(prefix, 'hidden'))) { - // if pre-rendered, then defer callback until page visibility changes - if (documentAlias[prefixPropertyName(prefix, 'visibilityState')] === 'prerender') { - isPreRendered = true; - } - break; - } - } - } - - if (isPreRendered) { - // note: the event name doesn't follow the same naming convention as vendor properties - helpers.addEventListener(documentAlias, prefix + 'visibilitychange', function ready() { - documentAlias.removeEventListener(prefix + 'visibilitychange', ready, false); - callback(); - }); - return; - } - - // configCountPreRendered === true || isPreRendered === false - callback(); - } - - - /************************************************************ - * Constructor - ************************************************************/ - - /* - * Initialize tracker - */ - updateDomainHash(); - - - /************************************************************ - * Public data and methods - ************************************************************/ - - return { - - /** - * Get the current user ID (as set previously - * with setUserId()). - * - * @return string Business-defined user ID - */ - getUserId: function () { - return businessUserId; - }, - - /** - * Get visitor ID (from first party cookie) - * - * @return string Visitor ID in hexits (or null, if not yet known) - */ - getDomainUserId: function () { - return (loadDomainUserIdCookie())[1]; - }, - - /** - * Get the visitor information (from first party cookie) - * - * @return array - */ - getDomainUserInfo: function () { - return loadDomainUserIdCookie(); - }, - - /** - * Get the user fingerprint - * - * @return string The user fingerprint - */ - getUserFingerprint: function () { - return userFingerprint; - }, - - /** - * Specify the app ID - * - * @param int|string appId - */ - setAppId: function (appId) { - helpers.warn('setAppId is deprecated. Instead add an "appId" field to the argmap argument of newTracker.'); - core.setAppId(appId); - }, - - /** - * Override referrer - * - * @param string url - */ - setReferrerUrl: function (url) { - configReferrerUrl = url; - }, - - /** - * Override url - * - * @param string url - */ - setCustomUrl: function (url) { - configCustomUrl = resolveRelativeReference(locationHrefAlias, url); - }, - - /** - * Override document.title - * - * @param string title - */ - setDocumentTitle: function (title) { - configTitle = title; - }, - - /** - * Strip hash tag (or anchor) from URL - * - * @param bool enableFilter - */ - discardHashTag: function (enableFilter) { - configDiscardHashTag = enableFilter; - }, - - /** - * Set first-party cookie name prefix - * - * @param string cookieNamePrefix - */ - setCookieNamePrefix: function (cookieNamePrefix) { - helpers.warn('setCookieNamePrefix is deprecated. Instead add a "cookieName" field to the argmap argument of newTracker.'); - configCookieNamePrefix = cookieNamePrefix; - }, - - /** - * Set first-party cookie domain - * - * @param string domain - */ - setCookieDomain: function (domain) { - helpers.warn('setCookieDomain is deprecated. Instead add a "cookieDomain" field to the argmap argument of newTracker.'); - configCookieDomain = helpers.fixupDomain(domain); - updateDomainHash(); - }, - - /** - * Set first-party cookie path - * - * @param string domain - */ - setCookiePath: function (path) { - configCookiePath = path; - updateDomainHash(); - }, - - /** - * Set visitor cookie timeout (in seconds) - * - * @param int timeout - */ - setVisitorCookieTimeout: function (timeout) { - configVisitorCookieTimeout = timeout; - }, - - /** - * Set session cookie timeout (in seconds) - * - * @param int timeout - */ - setSessionCookieTimeout: function (timeout) { - configSessionCookieTimeout = timeout; - }, - - /** - * @param number seed The seed used for MurmurHash3 - */ - setUserFingerprintSeed: function(seed) { - helpers.warn('setUserFingerprintSeed is deprecated. Instead add a "userFingerprintSeed" field to the argmap argument of newTracker.'); - configUserFingerprintHashSeed = seed; - userFingerprint = detectors.detectSignature(configUserFingerprintHashSeed); - }, - - /** - * Enable/disable user fingerprinting. User fingerprinting is enabled by default. - * @param bool enable If false, turn off user fingerprinting - */ - enableUserFingerprint: function(enable) { - helpers.warn('enableUserFingerprintSeed is deprecated. Instead add a "userFingerprint" field to the argmap argument of newTracker.'); - if (!enable) { - userFingerprint = ''; - } - }, - - /** - * Prevent tracking if user's browser has Do Not Track feature enabled, - * where tracking is: - * 1) Sending events to a collector - * 2) Setting first-party cookies - * @param bool enable If true and Do Not Track feature enabled, don't track. - */ - respectDoNotTrack: function (enable) { - helpers.warn('This usage of respectDoNotTrack is deprecated. Instead add a "respectDoNotTrack" field to the argmap argument of newTracker.'); - var dnt = navigatorAlias.doNotTrack || navigatorAlias.msDoNotTrack; - - configDoNotTrack = enable && (dnt === 'yes' || dnt === '1'); - }, - - /** - * Add click listener to a specific link element. - * When clicked, Piwik will log the click automatically. - * - * @param DOMElement element - * @param bool enable If true, use pseudo click-handler (mousedown+mouseup) - */ - addListener: function (element, pseudoClicks, context) { - addClickListener(element, pseudoClicks, context); - }, - - /** - * Install link tracker - * - * The default behaviour is to use actual click events. However, some browsers - * (e.g., Firefox, Opera, and Konqueror) don't generate click events for the middle mouse button. - * - * To capture more "clicks", the pseudo click-handler uses mousedown + mouseup events. - * This is not industry standard and is vulnerable to false positives (e.g., drag events). - * - * There is a Safari/Chrome/Webkit bug that prevents tracking requests from being sent - * by either click handler. The workaround is to set a target attribute (which can't - * be "_self", "_top", or "_parent"). - * - * @see https://bugs.webkit.org/show_bug.cgi?id=54783 - * - * @param object criterion Criterion by which it will be decided whether a link will be tracked - * @param bool pseudoClicks If true, use pseudo click-handler (mousedown+mouseup) - * @param bool trackContent Whether to track the innerHTML of the link element - * @param array context Context for all link click events - */ - enableLinkClickTracking: function (criterion, pseudoClicks, trackContent, context) { - if (mutSnowplowState.hasLoaded) { - // the load event has already fired, add the click listeners now - linkTrackingManager.configureLinkClickTracking(criterion, pseudoClicks, trackContent, context); - linkTrackingManager.addClickListeners(); - } else { - // defer until page has loaded - mutSnowplowState.registeredOnLoadHandlers.push(function () { - linkTrackingManager.configureLinkClickTracking(criterion, pseudoClicks, trackContent, context); - linkTrackingManager.addClickListeners(); - }); - } - }, - - /** - * Add click event listeners to links which have been added to the page since the - * last time enableLinkClickTracking or refreshLinkClickTracking was used - */ - refreshLinkClickTracking: function () { - if (mutSnowplowState.hasLoaded) { - linkTrackingManager.addClickListeners(); - } else { - mutSnowplowState.registeredOnLoadHandlers.push(function () { - linkTrackingManager.addClickListeners(); - }); - } - }, - - /** - * Enables page activity tracking (sends page - * pings to the Collector regularly). - * - * @param int minimumVisitLength Seconds to wait before sending first page ping - * @param int heartBeatDelay Seconds to wait between pings - */ - enableActivityTracking: function (minimumVisitLength, heartBeatDelay) { - configMinimumVisitTime = new Date().getTime() + minimumVisitLength * 1000; - configHeartBeatTimer = heartBeatDelay * 1000; - }, - - /** - * Enables automatic form tracking. - * An event will be fired when a form field is changed or a form submitted. - * This can be called multiple times: only forms not already tracked will be tracked. - * - * @param object config Configuration object determining which forms and fields to track. - * Has two properties: "forms" and "fields" - * @param array context Context for all form tracking events - */ - enableFormTracking: function (config, context) { - if (mutSnowplowState.hasLoaded) { - formTrackingManager.configureFormTracking(config); - formTrackingManager.addFormListeners(context); - } else { - mutSnowplowState.registeredOnLoadHandlers.push(function () { - formTrackingManager.configureFormTracking(config); - formTrackingManager.addFormListeners(context); - }); - } - }, - - /** - * Frame buster - */ - killFrame: function () { - if (windowAlias.location !== windowAlias.top.location) { - windowAlias.top.location = windowAlias.location; - } - }, - - /** - * Redirect if browsing offline (aka file: buster) - * - * @param string url Redirect to this URL - */ - redirectFile: function (url) { - if (windowAlias.location.protocol === 'file:') { - windowAlias.location = url; - } - }, - - /** - * Count sites in pre-rendered state - * - * @param bool enable If true, track when in pre-rendered state - */ - setCountPreRendered: function (enable) { - configCountPreRendered = enable; - }, - - /** - * Set the business-defined user ID for this user. - * - * @param string userId The business-defined user ID - */ - setUserId: function(userId) { - businessUserId = userId; - }, - - /** - * Set the business-defined user ID for this user using the location querystring. - * - * @param string queryName Name of a querystring name-value pair - */ - setUserIdFromLocation: function(querystringField) { - businessUserId = helpers.fromQuerystring(querystringField, locationHrefAlias); - }, - - /** - * Set the business-defined user ID for this user using the referrer querystring. - * - * @param string queryName Name of a querystring name-value pair - */ - setUserIdFromReferrer: function(querystringField) { - businessUserId = helpers.fromQuerystring(querystringField, configReferrerUrl); - }, - - /** - * Set the business-defined user ID for this user to the value of a cookie. - * - * @param string cookieName Name of the cookie whose value will be assigned to businessUserId - */ - setUserIdFromCookie: function(cookieName) { - businessUserId = cookie.cookie(cookieName); - }, - - /** - * Configure this tracker to log to a CloudFront collector. - * - * @param string distSubdomain The subdomain on your CloudFront collector's distribution - */ - setCollectorCf: function (distSubdomain) { - configCollectorUrl = collectorUrlFromCfDist(distSubdomain); - }, - - /** - * - * Specify the Snowplow collector URL. No need to include HTTP - * or HTTPS - we will add this. - * - * @param string rawUrl The collector URL minus protocol and /i - */ - setCollectorUrl: function (rawUrl) { - configCollectorUrl = asCollectorUrl(rawUrl); - }, - - /** - * Specify the platform - * - * @param string platform Overrides the default tracking platform - */ - setPlatform: function(platform) { - helpers.warn('setPlatform is deprecated. Instead add a "platform" field to the argmap argument of newTracker.'); - core.setPlatform(platform); - }, - - /** - * - * Enable Base64 encoding for unstructured event payload - * - * @param bool enabled A boolean value indicating if the Base64 encoding for unstructured events should be enabled or not - */ - encodeBase64: function (enabled) { - helpers.warn('This usage of encodeBase64 is deprecated. Instead add an "encodeBase64" field to the argmap argument of newTracker.'); - core.setBase64Encoding(enabled); - }, - - /** - * Send all events in the outQueue - * Use only when sending POSTs with a bufferSize of at least 2 - */ - flushBuffer: function () { - outQueueManager.executeQueue(); - }, - - /** - * Add the geolocation context to all events - */ - enableGeolocationContext: enableGeolocationContext, - - /** - * Log visit to this page - * - * @param string customTitle - * @param boolean DEPRECATED performanceTiming - * @param object Custom context relating to the event - */ - trackPageView: function (customTitle, performanceTiming, context) { - if (performanceTiming !== true && performanceTiming !== false) { - context = context || performanceTiming; - } else { - helpers.warn('The performanceTiming argument to trackPageView is deprecated. Instead use the "contexts" field in the argmap argument of newTracker.'); - } - trackCallback(function () { - logPageView(customTitle, context); - }); - }, - - /** - * Track a structured event happening on this page. - * - * Replaces trackEvent, making clear that the type - * of event being tracked is a structured one. - * - * @param string category The name you supply for the group of objects you want to track - * @param string action A string that is uniquely paired with each category, and commonly used to define the type of user interaction for the web object - * @param string label (optional) An optional string to provide additional dimensions to the event data - * @param string property (optional) Describes the object or the action performed on it, e.g. quantity of item added to basket - * @param int|float|string value (optional) An integer that you can use to provide numerical data about the user event - * @param object Custom context relating to the event - */ - trackStructEvent: function (category, action, label, property, value, context) { - core.trackStructEvent(category, action, label, property, value, addCommonContexts(context)); - }, - - /** - * Track an unstructured event happening on this page. - * - * @param object eventJson Contains the properties and schema location for the event - * @param object context Custom context relating to the event - */ - trackUnstructEvent: function (eventJson, context) { - core.trackUnstructEvent(eventJson, addCommonContexts(context)); - }, - - /** - * Track an ecommerce transaction - * - * @param string orderId Required. Internal unique order id number for this transaction. - * @param string affiliation Optional. Partner or store affiliation. - * @param string total Required. Total amount of the transaction. - * @param string tax Optional. Tax amount of the transaction. - * @param string shipping Optional. Shipping charge for the transaction. - * @param string city Optional. City to associate with transaction. - * @param string state Optional. State to associate with transaction. - * @param string country Optional. Country to associate with transaction. - * @param string currency Optional. Currency to associate with this transaction. - * @param object context Optional. Context relating to the event. - */ - addTrans: function(orderId, affiliation, total, tax, shipping, city, state, country, currency, context) { - ecommerceTransaction.transaction = { - orderId: orderId, - affiliation: affiliation, - total: total, - tax: tax, - shipping: shipping, - city: city, - state: state, - country: country, - currency: currency, - context: context - }; - }, - - /** - * Track an ecommerce transaction item - * - * @param string orderId Required Order ID of the transaction to associate with item. - * @param string sku Required. Item's SKU code. - * @param string name Optional. Product name. - * @param string category Optional. Product category. - * @param string price Required. Product price. - * @param string quantity Required. Purchase quantity. - * @param string currency Optional. Product price currency. - * @param object context Optional. Context relating to the event. - */ - addItem: function(orderId, sku, name, category, price, quantity, currency, context) { - ecommerceTransaction.items.push({ - orderId: orderId, - sku: sku, - name: name, - category: category, - price: price, - quantity: quantity, - currency: currency, - context: context - }); - }, - - /** - * Commit the ecommerce transaction - * - * This call will send the data specified with addTrans, - * addItem methods to the tracking server. - */ - trackTrans: function() { - logTransaction( - ecommerceTransaction.transaction.orderId, - ecommerceTransaction.transaction.affiliation, - ecommerceTransaction.transaction.total, - ecommerceTransaction.transaction.tax, - ecommerceTransaction.transaction.shipping, - ecommerceTransaction.transaction.city, - ecommerceTransaction.transaction.state, - ecommerceTransaction.transaction.country, - ecommerceTransaction.transaction.currency, - ecommerceTransaction.transaction.context - ); - for (var i = 0; i < ecommerceTransaction.items.length; i++) { - var item = ecommerceTransaction.items[i]; - logTransactionItem( - item.orderId, - item.sku, - item.name, - item.category, - item.price, - item.quantity, - item.currency, - item.context - ); - } - - ecommerceTransaction = ecommerceTransactionTemplate(); - }, - - /** - * Manually log a click from your own code - * - * @param string elementId - * @param array elementClasses - * @param string elementTarget - * @param string targetUrl - * @param string elementContent innerHTML of the element - * @param object Custom context relating to the event - */ - // TODO: break this into trackLink(destUrl) and trackDownload(destUrl) - trackLinkClick: function(targetUrl, elementId, elementClasses, elementTarget, elementContent, context) { - trackCallback(function () { - core.trackLinkClick(targetUrl, elementId, elementClasses, elementTarget, elementContent, addCommonContexts(context)); - }); - }, - - /** - * Track an ad being served - * - * @param string impressionId Identifier for a particular ad impression - * @param string costModel The cost model. 'cpa', 'cpc', or 'cpm' - * @param number cost Cost - * @param string bannerId Identifier for the ad banner displayed - * @param string zoneId Identifier for the ad zone - * @param string advertiserId Identifier for the advertiser - * @param string campaignId Identifier for the campaign which the banner belongs to - * @param object Custom context relating to the event - */ - trackAdImpression: function(impressionId, costModel, cost, targetUrl, bannerId, zoneId, advertiserId, campaignId, context) { - trackCallback(function () { - core.trackAdImpression(impressionId, costModel, cost, targetUrl, bannerId, zoneId, advertiserId, campaignId, addCommonContexts(context)); - }); - }, - - /** - * Track an ad being clicked - * - * @param string clickId Identifier for the ad click - * @param string costModel The cost model. 'cpa', 'cpc', or 'cpm' - * @param number cost Cost - * @param string targetUrl (required) The link's target URL - * @param string bannerId Identifier for the ad banner displayed - * @param string zoneId Identifier for the ad zone - * @param string impressionId Identifier for a particular ad impression - * @param string advertiserId Identifier for the advertiser - * @param string campaignId Identifier for the campaign which the banner belongs to - * @param object Custom context relating to the event - */ - trackAdClick: function(targetUrl, clickId, costModel, cost, bannerId, zoneId, impressionId, advertiserId, campaignId, context) { - core.trackAdClick(targetUrl, clickId, costModel, cost, bannerId, zoneId, impressionId, advertiserId, campaignId, addCommonContexts(context)); - }, - - /** - * Track an ad conversion event - * - * @param string conversionId Identifier for the ad conversion event - * @param number cost Cost - * @param string category The name you supply for the group of objects you want to track - * @param string action A string that is uniquely paired with each category - * @param string property Describes the object of the conversion or the action performed on it - * @param number initialValue Revenue attributable to the conversion at time of conversion - * @param string advertiserId Identifier for the advertiser - * @param string costModel The cost model. 'cpa', 'cpc', or 'cpm' - * @param string campaignId Identifier for the campaign which the banner belongs to - * @param object Custom context relating to the event - */ - trackAdConversion: function(conversionId, costModel, cost, category, action, property, initialValue, advertiserId, campaignId, context) { - core.trackAdConversion(conversionId, costModel, cost, category, action, property, initialValue, advertiserId, campaignId, addCommonContexts(context)); - }, - - /** - * Track a social interaction event - * - * @param string action (required) Social action performed - * @param string network (required) Social network - * @param string target Object of the social action e.g. the video liked, the tweet retweeted - * @param object Custom context relating to the event - */ - trackSocialInteraction: function(action, network, target, context) { - core.trackSocialInteraction(action, network, target, addCommonContexts(context)); - }, - - /** - * Track an add-to-cart event - * - * @param string sku Required. Item's SKU code. - * @param string name Optional. Product name. - * @param string category Optional. Product category. - * @param string unitPrice Optional. Product price. - * @param string quantity Required. Quantity added. - * @param string currency Optional. Product price currency. - * @param array context Optional. Context relating to the event. - */ - trackAddToCart: function(sku, name, category, unitPrice, quantity, currency, context) { - core.trackAddToCart(sku, name, category, unitPrice, quantity, currency, addCommonContexts(context)); - }, - - /** - * Track a remove-from-cart event - * - * @param string sku Required. Item's SKU code. - * @param string name Optional. Product name. - * @param string category Optional. Product category. - * @param string unitPrice Optional. Product price. - * @param string quantity Required. Quantity removed. - * @param string currency Optional. Product price currency. - * @param array context Optional. Context relating to the event. - */ - trackRemoveFromCart: function(sku, name, category, unitPrice, quantity, currency, context) { - core.trackRemoveFromCart(sku, name, category, unitPrice, quantity, currency, addCommonContexts(context)); - }, - - /** - * Track an internal search event - * - * @param array terms Search terms - * @param object filters Search filters - * @param number totalResults Number of results - * @param number pageResults Number of results displayed on page - * @param array context Optional. Context relating to the event. - */ - trackSiteSearch: function(terms, filters, totalResults, pageResults, context) { - core.trackSiteSearch(terms, filters, totalResults, pageResults, addCommonContexts(context)); - } - }; - }; - -}()); + // Manager for automatic form tracking + formTrackingManager = forms.getFormTrackingManager(core, trackerId), + + // Manager for local storage queue + outQueueManager = new OutQueueManager(functionName, namespace, mutSnowplowState, useLocalStorage, argmap.post, argmap.bufferSize), + + // Flag to prevent the geolocation context being added multiple times + geolocationContextAdded = false, + + // Set of contexts to be added to every event + autoContexts = argmap.contexts || {}, + + // Context to be added to every event + commonContexts = []; + + if (autoContexts.performanceTiming) { + commonContexts.push(getPerformanceTimingContext()); + } + + if (autoContexts.gaCookies) { + commonContexts.push(getGaCookiesContext()); + } + + if (autoContexts.geolocation) { + enableGeolocationContext(); + } + + // Enable base 64 encoding for unstructured events and custom contexts + core.setBase64Encoding(argmap.hasOwnProperty('encodeBase64') ? argmap.encodeBase64 : true); + + // Set up unchanging name-value pairs + core.setTrackerVersion(version); + core.setTrackerNamespace(namespace); + core.setAppId(configTrackerSiteId); + core.setPlatform(configPlatform); + core.setTimezone(detectors.detectTimezone()); + core.addPayloadPair('lang', browserLanguage); + core.addPayloadPair('cs', documentCharset); + + // Browser features. Cookies, color depth and resolution don't get prepended with f_ (because they're not optional features) + for (var i in browserFeatures) { + if (Object.prototype.hasOwnProperty.call(browserFeatures, i)) { + if (i === 'res' || i === 'cd' || i === 'cookie') { + core.addPayloadPair(i, browserFeatures[i]); + } else { + core.addPayloadPair('f_' + i, browserFeatures[i]); + } + } + } + + /* + * Initializes an empty ecommerce + * transaction and line items + */ + function ecommerceTransactionTemplate() { + return { + transaction: {}, + items: [] + }; + } + + /* + * Removes hash tag from the URL + * + * URLs are purified before being recorded in the cookie, + * or before being sent as GET parameters + */ + function purify(url) { + var targetPattern; + + if (configDiscardHashTag) { + targetPattern = new RegExp('#.*'); + return url.replace(targetPattern, ''); + } + return url; + } + + /* + * Extract scheme/protocol from URL + */ + function getProtocolScheme(url) { + var e = new RegExp('^([a-z]+):'), + matches = e.exec(url); + + return matches ? matches[1] : null; + } + + /* + * Resolve relative reference + * + * Note: not as described in rfc3986 section 5.2 + */ + function resolveRelativeReference(baseUrl, url) { + var protocol = getProtocolScheme(url), + i; + + if (protocol) { + return url; + } + + if (url.slice(0, 1) === '/') { + return getProtocolScheme(baseUrl) + '://' + helpers.getHostName(baseUrl) + url; + } + + baseUrl = purify(baseUrl); + if ((i = baseUrl.indexOf('?')) >= 0) { + baseUrl = baseUrl.slice(0, i); + } + if ((i = baseUrl.lastIndexOf('/')) !== baseUrl.length - 1) { + baseUrl = baseUrl.slice(0, i + 1); + } + + return baseUrl + url; + } + + /* + * Send request + */ + function sendRequest(request, delay) { + var now = new Date(); + + if (!configDoNotTrack) { + outQueueManager.enqueueRequest(request.build(), configCollectorUrl); + mutSnowplowState.expireDateTime = now.getTime() + delay; + } + } + + /* + * Get cookie name with prefix and domain hash + */ + function getSnowplowCookieName(baseName) { + return configCookieNamePrefix + baseName + '.' + domainHash; + } + + /* + * Cookie getter. + */ + function getSnowplowCookieValue(cookieName) { + return cookie.cookie(getSnowplowCookieName(cookieName)); + } + + /* + * Update domain hash + */ + function updateDomainHash() { + domainHash = hash((configCookieDomain || domainAlias) + (configCookiePath || '/')).slice(0, 4); // 4 hexits = 16 bits + } + + /* + * Process all "activity" events. + * For performance, this function must have low overhead. + */ + function activityHandler() { + var now = new Date(); + lastActivityTime = now.getTime(); + } + + /* + * Process all "scroll" events. + */ + function scrollHandler() { + updateMaxScrolls(); + activityHandler(); + } + + /* + * Returns [pageXOffset, pageYOffset]. + * Adapts code taken from: http://www.javascriptkit.com/javatutors/static2.shtml + */ + function getPageOffsets() { + var iebody = (documentAlias.compatMode && documentAlias.compatMode != "BackCompat") ? + documentAlias.documentElement : + documentAlias.body; + return [iebody.scrollLeft || windowAlias.pageXOffset, iebody.scrollTop || windowAlias.pageYOffset]; + } + + /* + * Quick initialization/reset of max scroll levels + */ + function resetMaxScrolls() { + var offsets = getPageOffsets(); + + var x = offsets[0]; + minXOffset = x; + maxXOffset = x; + + var y = offsets[1]; + minYOffset = y; + maxYOffset = y; + } + + /* + * Check the max scroll levels, updating as necessary + */ + function updateMaxScrolls() { + var offsets = getPageOffsets(); + + var x = offsets[0]; + if (x < minXOffset) { + minXOffset = x; + } else if (x > maxXOffset) { + maxXOffset = x; + } + + var y = offsets[1]; + if (y < minYOffset) { + minYOffset = y; + } else if (y > maxYOffset) { + maxYOffset = y; + } + } + + /* + * Prevents offsets from being decimal or NaN + * See https://github.com/snowplow/snowplow-javascript-tracker/issues/324 + * TODO: the NaN check should be moved into the core + */ + function cleanOffset(offset) { + var rounded = Math.round(offset); + if (!isNaN(rounded)) { + return rounded; + } + } + + /* + * Sets the Visitor ID cookie: either the first time loadDomainUserIdCookie is called + * or when there is a new visit or a new page view + */ + function setDomainUserIdCookie(_domainUserId, createTs, visitCount, nowTs, lastVisitTs) { + cookie.cookie(getSnowplowCookieName('id'), _domainUserId + '.' + createTs + '.' + visitCount + '.' + nowTs + '.' + lastVisitTs, configVisitorCookieTimeout, configCookiePath, configCookieDomain); + } + + /* + * Load visitor ID cookie + */ + function loadDomainUserIdCookie() { + if (!configUseCookies) { + return []; + } + var now = new Date(), + nowTs = Math.round(now.getTime() / 1000), + id = getSnowplowCookieValue('id'), + tmpContainer; + + if (id) { + tmpContainer = id.split('.'); + // New visitor set to 0 now + tmpContainer.unshift('0'); + } else { + // Domain - generate a pseudo-unique ID to fingerprint this user; + // Note: this isn't a RFC4122-compliant UUID + if (!domainUserId) { + domainUserId = hash( + (navigatorAlias.userAgent || '') + + (navigatorAlias.platform || '') + + json2.stringify(browserFeatures) + nowTs + ).slice(0, 16); // 16 hexits = 64 bits + } + + tmpContainer = [ + // New visitor + '1', + // Domain user ID + domainUserId, + // Creation timestamp - seconds since Unix epoch + nowTs, + // visitCount - 0 = no previous visit + 0, + // Current visit timestamp + nowTs, + // Last visit timestamp - blank meaning no previous visit + '' + ]; + } + return tmpContainer; + } + + /* + * Attaches common web fields to every request + * (resolution, url, referrer, etc.) + * Also sets the required cookies. + */ + function addBrowserData(sb) { + var nowTs = Math.round(new Date().getTime() / 1000), + idname = getSnowplowCookieName('id'), + sesname = getSnowplowCookieName('ses'), + ses = getSnowplowCookieValue('ses'), // aka cookie.cookie(sesname) + id = loadDomainUserIdCookie(), + _domainUserId = id[1], // We could use the global (domainUserId) but this is better etiquette + createTs = id[2], + visitCount = id[3], + currentVisitTs = id[4], + lastVisitTs = id[5]; + + if (configDoNotTrack && configUseCookies && configWriteCookies) { + cookie.cookie(idname, '', -1, configCookiePath, configCookieDomain); + cookie.cookie(sesname, '', -1, configCookiePath, configCookieDomain); + return; + } + + // New session? + if (!ses && configUseCookies) { + // New session (aka new visit) + visitCount++; + // Update the last visit timestamp + lastVisitTs = currentVisitTs; + } + + // Build out the rest of the request + sb.add('vp', detectors.detectViewport()); + sb.add('ds', detectors.detectDocumentSize()); + sb.add('vid', visitCount); + sb.add('duid', _domainUserId); // Set to our local variable + sb.add('fp', userFingerprint); + sb.add('uid', businessUserId); + + // Adds with custom conditions + if (configReferrerUrl.length) { + sb.add('refr', purify(configReferrerUrl)); + } + + // Add the page URL last as it may take us over the IE limit (and we don't always need it) + sb.add('url', purify(configCustomUrl || locationHrefAlias)); + + + // Update cookies + if (configUseCookies && configWriteCookies) { + setDomainUserIdCookie(_domainUserId, createTs, visitCount, nowTs, lastVisitTs); + cookie.cookie(sesname, '*', configSessionCookieTimeout, configCookiePath, configCookieDomain); + } + } + + /** + * Builds a collector URL from a CloudFront distribution. + * We don't bother to support custom CNAMEs because Amazon CloudFront doesn't support that for SSL. + * + * @param string account The account ID to build the tracker URL from + * + * @return string The URL on which the collector is hosted + */ + function collectorUrlFromCfDist(distSubdomain) { + return asCollectorUrl(distSubdomain + '.cloudfront.net'); + } + + /** + * Adds the protocol in front of our collector URL, and i to the end + * + * @param string rawUrl The collector URL without protocol + * + * @return string collectorUrl The tracker URL with protocol + */ + function asCollectorUrl(rawUrl) { + if (forceSecureTracker) + return ('https' + '://' + rawUrl); + else + return ('https:' === documentAlias.location.protocol ? 'https' : 'http') + '://' + rawUrl; + } + + /** + * Add common contexts to every event + * TODO: move this functionality into the core + * + * @param array userContexts List of user-defined contexts + * @return userContexts combined with commonContexts + */ + function addCommonContexts(userContexts) { + return commonContexts.concat(userContexts || []); + } + + /** + * Creates a context from the window.performance.timing object + * + * @return object PerformanceTiming context + */ + function getPerformanceTimingContext() { + var performance = windowAlias.performance || windowAlias.mozPerformance || windowAlias.msPerformance || windowAlias.webkitPerformance; + if (performance) { + + // On Safari, the fields we are interested in are on the prototype chain of + // performance.timing so we cannot copy them using lodash.clone + var performanceTiming = {}; + for (var field in performance.timing) { + // Don't copy the toJSON method + if (!lodash.isFunction(performance.timing[field])) { + performanceTiming[field] = performance.timing[field]; + } + } + + // Old Chrome versions add an unwanted requestEnd field + delete performanceTiming.requestEnd; + + // Add the Chrome firstPaintTime to the performance if it exists + if (windowAlias.chrome && windowAlias.chrome.loadTimes && typeof windowAlias.chrome.loadTimes().firstPaintTime === 'number') { + performanceTiming.chromeFirstPaint = Math.round(windowAlias.chrome.loadTimes().firstPaintTime * 1000); + } + + return { + schema: 'iglu:org.w3/PerformanceTiming/jsonschema/1-0-0', + data: performanceTiming + }; + } + } + + /** + * Attempts to create a context using the geolocation API and add it to commonContexts + */ + function enableGeolocationContext() { + if (!geolocationContextAdded && navigatorAlias.geolocation && navigatorAlias.geolocation.getCurrentPosition) { + geolocationContextAdded = true; + navigator.geolocation.getCurrentPosition(function (position) { + var coords = position.coords; + var geolocationContext = { + schema: 'iglu:com.snowplowanalytics.snowplow/geolocation_context/jsonschema/1-1-0', + data: { + latitude: coords.latitude, + longitude: coords.longitude, + latitudeLongitudeAccuracy: coords.accuracy, + altitude: coords.altitude, + altitudeAccuracy: coords.altitudeAccuracy, + bearing: coords.heading, + speed: coords.speed, + timestamp: position.timestamp + } + }; + commonContexts.push(geolocationContext); + }); + } + } + + /** + * Creates a context containing the values of the cookies set by GA + * + * @return object GA cookies context + */ + function getGaCookiesContext() { + var gaCookieData = {}; + lodash.forEach(['__utma', '__utmb', '__utmc', '__utmv', '__utmz', '_ga'], function (cookieType) { + var value = cookie.cookie(cookieType); + if (value) { + gaCookieData[cookieType] = value; + } + }); + return { + schema: 'iglu:com.google.analytics/cookies/jsonschema/1-0-0', + data: gaCookieData + }; + } + + /** + * Log the page view / visit + * + * @param string customTitle The user-defined page title to attach to this page view + * @param object context Custom context relating to the event + */ + function logPageView(customTitle, context) { + + // Fixup page title. We'll pass this to logPagePing too. + var pageTitle = helpers.fixupTitle(customTitle || configTitle); + + // Log page view + core.trackPageView(purify(configCustomUrl || locationHrefAlias), pageTitle, purify(configReferrerUrl), addCommonContexts(context)); + + // Send ping (to log that user has stayed on page) + var now = new Date(); + if (configMinimumVisitTime && configHeartBeatTimer && !activityTrackingInstalled) { + activityTrackingInstalled = true; + + // Capture our initial scroll points + resetMaxScrolls(); + + // Add event handlers; cross-browser compatibility here varies significantly + // @see http://quirksmode.org/dom/events + helpers.addEventListener(documentAlias, 'click', activityHandler); + helpers.addEventListener(documentAlias, 'mouseup', activityHandler); + helpers.addEventListener(documentAlias, 'mousedown', activityHandler); + helpers.addEventListener(documentAlias, 'mousemove', activityHandler); + helpers.addEventListener(documentAlias, 'mousewheel', activityHandler); + helpers.addEventListener(windowAlias, 'DOMMouseScroll', activityHandler); + helpers.addEventListener(windowAlias, 'scroll', scrollHandler); // Will updateMaxScrolls() for us + helpers.addEventListener(documentAlias, 'keypress', activityHandler); + helpers.addEventListener(documentAlias, 'keydown', activityHandler); + helpers.addEventListener(documentAlias, 'keyup', activityHandler); + helpers.addEventListener(windowAlias, 'resize', activityHandler); + helpers.addEventListener(windowAlias, 'focus', activityHandler); + helpers.addEventListener(windowAlias, 'blur', activityHandler); + + // Periodic check for activity. + lastActivityTime = now.getTime(); + setInterval(function heartBeat() { + var now = new Date(); + + // There was activity during the heart beat period; + // on average, this is going to overstate the visitDuration by configHeartBeatTimer/2 + if ((lastActivityTime + configHeartBeatTimer) > now.getTime()) { + // Send ping if minimum visit time has elapsed + if (configMinimumVisitTime < now.getTime()) { + logPagePing(pageTitle, context); // Grab the min/max globals + } + } + }, configHeartBeatTimer); + } + } + + /** + * Log that a user is still viewing a given page + * by sending a page ping. + * Not part of the public API - only called from + * logPageView() above. + * + * @param string pageTitle The page title to attach to this page ping + * @param object context Custom context relating to the event + */ + function logPagePing(pageTitle, context) { + core.trackPagePing( + purify(configCustomUrl || locationHrefAlias), + pageTitle, + purify(configReferrerUrl), + cleanOffset(minXOffset), + cleanOffset(maxXOffset), + cleanOffset(minYOffset), + cleanOffset(maxYOffset), + addCommonContexts(context)); + resetMaxScrolls(); + } + + /** + * Log ecommerce transaction metadata + * + * @param string orderId + * @param string affiliation + * @param string total + * @param string tax + * @param string shipping + * @param string city + * @param string state + * @param string country + * @param string currency The currency the total/tax/shipping are expressed in + * @param object context Custom context relating to the event + */ + function logTransaction(orderId, affiliation, total, tax, shipping, city, state, country, currency, context) { + core.trackEcommerceTransaction(orderId, affiliation, total, tax, shipping, city, state, country, currency, addCommonContexts(context)); + } + + /** + * Log ecommerce transaction item + * + * @param string orderId + * @param string sku + * @param string name + * @param string category + * @param string price + * @param string quantity + * @param string currency The currency the price is expressed in + * @param object context Custom context relating to the event + */ + function logTransactionItem(orderId, sku, name, category, price, quantity, currency, context) { + core.trackEcommerceTransactionItem(orderId, sku, name, category, price, quantity, currency, addCommonContexts(context)); + } + + /* + * Browser prefix + */ + function prefixPropertyName(prefix, propertyName) { + + if (prefix !== '') { + return prefix + propertyName.charAt(0).toUpperCase() + propertyName.slice(1); + } + + return propertyName; + } + + /** + * Check for pre-rendered web pages, and log the page view/link + * according to the configuration and/or visibility + * + * @see http://dvcs.w3.org/hg/webperf/raw-file/tip/specs/PageVisibility/Overview.html + */ + function trackCallback(callback) { + var isPreRendered, + i, + // Chrome 13, IE10, FF10 + prefixes = ['', 'webkit', 'ms', 'moz'], + prefix; + + if (!configCountPreRendered) { + for (i = 0; i < prefixes.length; i++) { + prefix = prefixes[i]; + + // does this browser support the page visibility API? + if (Object.prototype.hasOwnProperty.call(documentAlias, prefixPropertyName(prefix, 'hidden'))) { + // if pre-rendered, then defer callback until page visibility changes + if (documentAlias[prefixPropertyName(prefix, 'visibilityState')] === 'prerender') { + isPreRendered = true; + } + break; + } + } + } + + if (isPreRendered) { + // note: the event name doesn't follow the same naming convention as vendor properties + helpers.addEventListener(documentAlias, prefix + 'visibilitychange', function ready() { + documentAlias.removeEventListener(prefix + 'visibilitychange', ready, false); + callback(); + }); + return; + } + + // configCountPreRendered === true || isPreRendered === false + callback(); + } + + + /************************************************************ + * Constructor + ************************************************************/ + + /* + * Initialize tracker + */ + updateDomainHash(); + + + /************************************************************ + * Public data and methods + ************************************************************/ + + return { + + /** + * Get the current user ID (as set previously + * with setUserId()). + * + * @return string Business-defined user ID + */ + getUserId: function () { + return businessUserId; + }, + + /** + * Get visitor ID (from first party cookie) + * + * @return string Visitor ID in hexits (or null, if not yet known) + */ + getDomainUserId: function () { + return (loadDomainUserIdCookie())[1]; + }, + + /** + * Get the visitor information (from first party cookie) + * + * @return array + */ + getDomainUserInfo: function () { + return loadDomainUserIdCookie(); + }, + + /** + * Get the user fingerprint + * + * @return string The user fingerprint + */ + getUserFingerprint: function () { + return userFingerprint; + }, + + /** + * Specify the app ID + * + * @param int|string appId + */ + setAppId: function (appId) { + helpers.warn('setAppId is deprecated. Instead add an "appId" field to the argmap argument of newTracker.'); + core.setAppId(appId); + }, + + /** + * Override referrer + * + * @param string url + */ + setReferrerUrl: function (url) { + configReferrerUrl = url; + }, + + /** + * Override url + * + * @param string url + */ + setCustomUrl: function (url) { + configCustomUrl = resolveRelativeReference(locationHrefAlias, url); + }, + + /** + * Override document.title + * + * @param string title + */ + setDocumentTitle: function (title) { + configTitle = title; + }, + + /** + * Strip hash tag (or anchor) from URL + * + * @param bool enableFilter + */ + discardHashTag: function (enableFilter) { + configDiscardHashTag = enableFilter; + }, + + /** + * Set first-party cookie name prefix + * + * @param string cookieNamePrefix + */ + setCookieNamePrefix: function (cookieNamePrefix) { + helpers.warn('setCookieNamePrefix is deprecated. Instead add a "cookieName" field to the argmap argument of newTracker.'); + configCookieNamePrefix = cookieNamePrefix; + }, + + /** + * Set first-party cookie domain + * + * @param string domain + */ + setCookieDomain: function (domain) { + helpers.warn('setCookieDomain is deprecated. Instead add a "cookieDomain" field to the argmap argument of newTracker.'); + configCookieDomain = helpers.fixupDomain(domain); + updateDomainHash(); + }, + + /** + * Set first-party cookie path + * + * @param string domain + */ + setCookiePath: function (path) { + configCookiePath = path; + updateDomainHash(); + }, + + /** + * Set visitor cookie timeout (in seconds) + * + * @param int timeout + */ + setVisitorCookieTimeout: function (timeout) { + configVisitorCookieTimeout = timeout; + }, + + /** + * Set session cookie timeout (in seconds) + * + * @param int timeout + */ + setSessionCookieTimeout: function (timeout) { + configSessionCookieTimeout = timeout; + }, + + /** + * @param number seed The seed used for MurmurHash3 + */ + setUserFingerprintSeed: function(seed) { + helpers.warn('setUserFingerprintSeed is deprecated. Instead add a "userFingerprintSeed" field to the argmap argument of newTracker.'); + configUserFingerprintHashSeed = seed; + userFingerprint = detectors.detectSignature(configUserFingerprintHashSeed); + }, + + /** + * Enable/disable user fingerprinting. User fingerprinting is enabled by default. + * @param bool enable If false, turn off user fingerprinting + */ + enableUserFingerprint: function(enable) { + helpers.warn('enableUserFingerprintSeed is deprecated. Instead add a "userFingerprint" field to the argmap argument of newTracker.'); + if (!enable) { + userFingerprint = ''; + } + }, + + /** + * Prevent tracking if user's browser has Do Not Track feature enabled, + * where tracking is: + * 1) Sending events to a collector + * 2) Setting first-party cookies + * @param bool enable If true and Do Not Track feature enabled, don't track. + */ + respectDoNotTrack: function (enable) { + helpers.warn('This usage of respectDoNotTrack is deprecated. Instead add a "respectDoNotTrack" field to the argmap argument of newTracker.'); + var dnt = navigatorAlias.doNotTrack || navigatorAlias.msDoNotTrack; + + configDoNotTrack = enable && (dnt === 'yes' || dnt === '1'); + }, + + /** + * Add click listener to a specific link element. + * When clicked, Piwik will log the click automatically. + * + * @param DOMElement element + * @param bool enable If true, use pseudo click-handler (mousedown+mouseup) + */ + addListener: function (element, pseudoClicks, context) { + addClickListener(element, pseudoClicks, context); + }, + + /** + * Install link tracker + * + * The default behaviour is to use actual click events. However, some browsers + * (e.g., Firefox, Opera, and Konqueror) don't generate click events for the middle mouse button. + * + * To capture more "clicks", the pseudo click-handler uses mousedown + mouseup events. + * This is not industry standard and is vulnerable to false positives (e.g., drag events). + * + * There is a Safari/Chrome/Webkit bug that prevents tracking requests from being sent + * by either click handler. The workaround is to set a target attribute (which can't + * be "_self", "_top", or "_parent"). + * + * @see https://bugs.webkit.org/show_bug.cgi?id=54783 + * + * @param object criterion Criterion by which it will be decided whether a link will be tracked + * @param bool pseudoClicks If true, use pseudo click-handler (mousedown+mouseup) + * @param bool trackContent Whether to track the innerHTML of the link element + * @param array context Context for all link click events + */ + enableLinkClickTracking: function (criterion, pseudoClicks, trackContent, context) { + if (mutSnowplowState.hasLoaded) { + // the load event has already fired, add the click listeners now + linkTrackingManager.configureLinkClickTracking(criterion, pseudoClicks, trackContent, context); + linkTrackingManager.addClickListeners(); + } else { + // defer until page has loaded + mutSnowplowState.registeredOnLoadHandlers.push(function () { + linkTrackingManager.configureLinkClickTracking(criterion, pseudoClicks, trackContent, context); + linkTrackingManager.addClickListeners(); + }); + } + }, + + /** + * Add click event listeners to links which have been added to the page since the + * last time enableLinkClickTracking or refreshLinkClickTracking was used + */ + refreshLinkClickTracking: function () { + if (mutSnowplowState.hasLoaded) { + linkTrackingManager.addClickListeners(); + } else { + mutSnowplowState.registeredOnLoadHandlers.push(function () { + linkTrackingManager.addClickListeners(); + }); + } + }, + + /** + * Enables page activity tracking (sends page + * pings to the Collector regularly). + * + * @param int minimumVisitLength Seconds to wait before sending first page ping + * @param int heartBeatDelay Seconds to wait between pings + */ + enableActivityTracking: function (minimumVisitLength, heartBeatDelay) { + configMinimumVisitTime = new Date().getTime() + minimumVisitLength * 1000; + configHeartBeatTimer = heartBeatDelay * 1000; + }, + + /** + * Enables automatic form tracking. + * An event will be fired when a form field is changed or a form submitted. + * This can be called multiple times: only forms not already tracked will be tracked. + * + * @param object config Configuration object determining which forms and fields to track. + * Has two properties: "forms" and "fields" + * @param array context Context for all form tracking events + */ + enableFormTracking: function (config, context) { + if (mutSnowplowState.hasLoaded) { + formTrackingManager.configureFormTracking(config); + formTrackingManager.addFormListeners(context); + } else { + mutSnowplowState.registeredOnLoadHandlers.push(function () { + formTrackingManager.configureFormTracking(config); + formTrackingManager.addFormListeners(context); + }); + } + }, + + /** + * Frame buster + */ + killFrame: function () { + if (windowAlias.location !== windowAlias.top.location) { + windowAlias.top.location = windowAlias.location; + } + }, + + /** + * Redirect if browsing offline (aka file: buster) + * + * @param string url Redirect to this URL + */ + redirectFile: function (url) { + if (windowAlias.location.protocol === 'file:') { + windowAlias.location = url; + } + }, + + /** + * Count sites in pre-rendered state + * + * @param bool enable If true, track when in pre-rendered state + */ + setCountPreRendered: function (enable) { + configCountPreRendered = enable; + }, + + /** + * Set the business-defined user ID for this user. + * + * @param string userId The business-defined user ID + */ + setUserId: function(userId) { + businessUserId = userId; + }, + + /** + * Set the business-defined user ID for this user using the location querystring. + * + * @param string queryName Name of a querystring name-value pair + */ + setUserIdFromLocation: function(querystringField) { + businessUserId = helpers.fromQuerystring(querystringField, locationHrefAlias); + }, + + /** + * Set the business-defined user ID for this user using the referrer querystring. + * + * @param string queryName Name of a querystring name-value pair + */ + setUserIdFromReferrer: function(querystringField) { + businessUserId = helpers.fromQuerystring(querystringField, configReferrerUrl); + }, + + /** + * Set the business-defined user ID for this user to the value of a cookie. + * + * @param string cookieName Name of the cookie whose value will be assigned to businessUserId + */ + setUserIdFromCookie: function(cookieName) { + businessUserId = cookie.cookie(cookieName); + }, + + /** + * Configure this tracker to log to a CloudFront collector. + * + * @param string distSubdomain The subdomain on your CloudFront collector's distribution + */ + setCollectorCf: function (distSubdomain) { + configCollectorUrl = collectorUrlFromCfDist(distSubdomain); + }, + + /** + * + * Specify the Snowplow collector URL. No need to include HTTP + * or HTTPS - we will add this. + * + * @param string rawUrl The collector URL minus protocol and /i + */ + setCollectorUrl: function (rawUrl) { + configCollectorUrl = asCollectorUrl(rawUrl); + }, + + /** + * Specify the platform + * + * @param string platform Overrides the default tracking platform + */ + setPlatform: function(platform) { + helpers.warn('setPlatform is deprecated. Instead add a "platform" field to the argmap argument of newTracker.'); + core.setPlatform(platform); + }, + + /** + * + * Enable Base64 encoding for unstructured event payload + * + * @param bool enabled A boolean value indicating if the Base64 encoding for unstructured events should be enabled or not + */ + encodeBase64: function (enabled) { + helpers.warn('This usage of encodeBase64 is deprecated. Instead add an "encodeBase64" field to the argmap argument of newTracker.'); + core.setBase64Encoding(enabled); + }, + + /** + * Send all events in the outQueue + * Use only when sending POSTs with a bufferSize of at least 2 + */ + flushBuffer: function () { + outQueueManager.executeQueue(); + }, + + /** + * Add the geolocation context to all events + */ + enableGeolocationContext: enableGeolocationContext, + + /** + * Log visit to this page + * + * @param string customTitle + * @param boolean DEPRECATED performanceTiming + * @param object Custom context relating to the event + */ + trackPageView: function (customTitle, performanceTiming, context) { + if (performanceTiming !== true && performanceTiming !== false) { + context = context || performanceTiming; + } else { + helpers.warn('The performanceTiming argument to trackPageView is deprecated. Instead use the "contexts" field in the argmap argument of newTracker.'); + } + trackCallback(function () { + logPageView(customTitle, context); + }); + }, + + /** + * Track a structured event happening on this page. + * + * Replaces trackEvent, making clear that the type + * of event being tracked is a structured one. + * + * @param string category The name you supply for the group of objects you want to track + * @param string action A string that is uniquely paired with each category, and commonly used to define the type of user interaction for the web object + * @param string label (optional) An optional string to provide additional dimensions to the event data + * @param string property (optional) Describes the object or the action performed on it, e.g. quantity of item added to basket + * @param int|float|string value (optional) An integer that you can use to provide numerical data about the user event + * @param object Custom context relating to the event + */ + trackStructEvent: function (category, action, label, property, value, context) { + core.trackStructEvent(category, action, label, property, value, addCommonContexts(context)); + }, + + /** + * Track an unstructured event happening on this page. + * + * @param object eventJson Contains the properties and schema location for the event + * @param object context Custom context relating to the event + */ + trackUnstructEvent: function (eventJson, context) { + core.trackUnstructEvent(eventJson, addCommonContexts(context)); + }, + + /** + * Track an ecommerce transaction + * + * @param string orderId Required. Internal unique order id number for this transaction. + * @param string affiliation Optional. Partner or store affiliation. + * @param string total Required. Total amount of the transaction. + * @param string tax Optional. Tax amount of the transaction. + * @param string shipping Optional. Shipping charge for the transaction. + * @param string city Optional. City to associate with transaction. + * @param string state Optional. State to associate with transaction. + * @param string country Optional. Country to associate with transaction. + * @param string currency Optional. Currency to associate with this transaction. + * @param object context Optional. Context relating to the event. + */ + addTrans: function(orderId, affiliation, total, tax, shipping, city, state, country, currency, context) { + ecommerceTransaction.transaction = { + orderId: orderId, + affiliation: affiliation, + total: total, + tax: tax, + shipping: shipping, + city: city, + state: state, + country: country, + currency: currency, + context: context + }; + }, + + /** + * Track an ecommerce transaction item + * + * @param string orderId Required Order ID of the transaction to associate with item. + * @param string sku Required. Item's SKU code. + * @param string name Optional. Product name. + * @param string category Optional. Product category. + * @param string price Required. Product price. + * @param string quantity Required. Purchase quantity. + * @param string currency Optional. Product price currency. + * @param object context Optional. Context relating to the event. + */ + addItem: function(orderId, sku, name, category, price, quantity, currency, context) { + ecommerceTransaction.items.push({ + orderId: orderId, + sku: sku, + name: name, + category: category, + price: price, + quantity: quantity, + currency: currency, + context: context + }); + }, + + /** + * Commit the ecommerce transaction + * + * This call will send the data specified with addTrans, + * addItem methods to the tracking server. + */ + trackTrans: function() { + logTransaction( + ecommerceTransaction.transaction.orderId, + ecommerceTransaction.transaction.affiliation, + ecommerceTransaction.transaction.total, + ecommerceTransaction.transaction.tax, + ecommerceTransaction.transaction.shipping, + ecommerceTransaction.transaction.city, + ecommerceTransaction.transaction.state, + ecommerceTransaction.transaction.country, + ecommerceTransaction.transaction.currency, + ecommerceTransaction.transaction.context + ); + for (var i = 0; i < ecommerceTransaction.items.length; i++) { + var item = ecommerceTransaction.items[i]; + logTransactionItem( + item.orderId, + item.sku, + item.name, + item.category, + item.price, + item.quantity, + item.currency, + item.context + ); + } + + ecommerceTransaction = ecommerceTransactionTemplate(); + }, + + /** + * Manually log a click from your own code + * + * @param string elementId + * @param array elementClasses + * @param string elementTarget + * @param string targetUrl + * @param string elementContent innerHTML of the element + * @param object Custom context relating to the event + */ + // TODO: break this into trackLink(destUrl) and trackDownload(destUrl) + trackLinkClick: function(targetUrl, elementId, elementClasses, elementTarget, elementContent, context) { + trackCallback(function () { + core.trackLinkClick(targetUrl, elementId, elementClasses, elementTarget, elementContent, addCommonContexts(context)); + }); + }, + + /** + * Track an ad being served + * + * @param string impressionId Identifier for a particular ad impression + * @param string costModel The cost model. 'cpa', 'cpc', or 'cpm' + * @param number cost Cost + * @param string bannerId Identifier for the ad banner displayed + * @param string zoneId Identifier for the ad zone + * @param string advertiserId Identifier for the advertiser + * @param string campaignId Identifier for the campaign which the banner belongs to + * @param object Custom context relating to the event + */ + trackAdImpression: function(impressionId, costModel, cost, targetUrl, bannerId, zoneId, advertiserId, campaignId, context) { + trackCallback(function () { + core.trackAdImpression(impressionId, costModel, cost, targetUrl, bannerId, zoneId, advertiserId, campaignId, addCommonContexts(context)); + }); + }, + + /** + * Track an ad being clicked + * + * @param string clickId Identifier for the ad click + * @param string costModel The cost model. 'cpa', 'cpc', or 'cpm' + * @param number cost Cost + * @param string targetUrl (required) The link's target URL + * @param string bannerId Identifier for the ad banner displayed + * @param string zoneId Identifier for the ad zone + * @param string impressionId Identifier for a particular ad impression + * @param string advertiserId Identifier for the advertiser + * @param string campaignId Identifier for the campaign which the banner belongs to + * @param object Custom context relating to the event + */ + trackAdClick: function(targetUrl, clickId, costModel, cost, bannerId, zoneId, impressionId, advertiserId, campaignId, context) { + core.trackAdClick(targetUrl, clickId, costModel, cost, bannerId, zoneId, impressionId, advertiserId, campaignId, addCommonContexts(context)); + }, + + /** + * Track an ad conversion event + * + * @param string conversionId Identifier for the ad conversion event + * @param number cost Cost + * @param string category The name you supply for the group of objects you want to track + * @param string action A string that is uniquely paired with each category + * @param string property Describes the object of the conversion or the action performed on it + * @param number initialValue Revenue attributable to the conversion at time of conversion + * @param string advertiserId Identifier for the advertiser + * @param string costModel The cost model. 'cpa', 'cpc', or 'cpm' + * @param string campaignId Identifier for the campaign which the banner belongs to + * @param object Custom context relating to the event + */ + trackAdConversion: function(conversionId, costModel, cost, category, action, property, initialValue, advertiserId, campaignId, context) { + core.trackAdConversion(conversionId, costModel, cost, category, action, property, initialValue, advertiserId, campaignId, addCommonContexts(context)); + }, + + /** + * Track a social interaction event + * + * @param string action (required) Social action performed + * @param string network (required) Social network + * @param string target Object of the social action e.g. the video liked, the tweet retweeted + * @param object Custom context relating to the event + */ + trackSocialInteraction: function(action, network, target, context) { + core.trackSocialInteraction(action, network, target, addCommonContexts(context)); + }, + + /** + * Track an add-to-cart event + * + * @param string sku Required. Item's SKU code. + * @param string name Optional. Product name. + * @param string category Optional. Product category. + * @param string unitPrice Optional. Product price. + * @param string quantity Required. Quantity added. + * @param string currency Optional. Product price currency. + * @param array context Optional. Context relating to the event. + */ + trackAddToCart: function(sku, name, category, unitPrice, quantity, currency, context) { + core.trackAddToCart(sku, name, category, unitPrice, quantity, currency, addCommonContexts(context)); + }, + + /** + * Track a remove-from-cart event + * + * @param string sku Required. Item's SKU code. + * @param string name Optional. Product name. + * @param string category Optional. Product category. + * @param string unitPrice Optional. Product price. + * @param string quantity Required. Quantity removed. + * @param string currency Optional. Product price currency. + * @param array context Optional. Context relating to the event. + */ + trackRemoveFromCart: function(sku, name, category, unitPrice, quantity, currency, context) { + core.trackRemoveFromCart(sku, name, category, unitPrice, quantity, currency, addCommonContexts(context)); + }, + + /** + * Track an internal search event + * + * @param array terms Search terms + * @param object filters Search filters + * @param number totalResults Number of results + * @param number pageResults Number of results displayed on page + * @param array context Optional. Context relating to the event. + */ + trackSiteSearch: function(terms, filters, totalResults, pageResults, context) { + core.trackSiteSearch(terms, filters, totalResults, pageResults, addCommonContexts(context)); + } + }; +};