437 lines
12 KiB
JavaScript
Executable File
437 lines
12 KiB
JavaScript
Executable File
/*
|
|
* 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.
|
|
*
|
|
* * Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the
|
|
* names of their contributors may be used to endorse or promote products
|
|
* derived from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
;(function () {
|
|
|
|
var
|
|
isString = require('lodash/isString'),
|
|
isUndefined = require('lodash/isUndefined'),
|
|
isObject = require('lodash/isObject'),
|
|
map = require('lodash/map'),
|
|
cookie = require('browser-cookie-lite'),
|
|
|
|
object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support
|
|
|
|
/**
|
|
* Cleans up the page title
|
|
*/
|
|
object.fixupTitle = function (title) {
|
|
if (!isString(title)) {
|
|
title = title.text || '';
|
|
|
|
var tmp = document.getElementsByTagName('title');
|
|
if (tmp && !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. In the case of a single-page app,
|
|
* if the URL changes without the page reloading, pass
|
|
* in the old URL. It will be returned unless overriden
|
|
* by a "refer(r)er" parameter in the querystring.
|
|
*
|
|
* @param string oldLocation Optional.
|
|
* @return string The referrer
|
|
*/
|
|
object.getReferrer = function (oldLocation) {
|
|
|
|
var referrer = '';
|
|
|
|
var fromQs = object.fromQuerystring('referrer', window.location.href) ||
|
|
object.fromQuerystring('referer', window.location.href);
|
|
|
|
// Short-circuit
|
|
if (fromQs) {
|
|
return fromQs;
|
|
}
|
|
|
|
// In the case of a single-page app, return the old URL
|
|
if (oldLocation) {
|
|
return oldLocation;
|
|
}
|
|
|
|
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, ' '));
|
|
};
|
|
|
|
/*
|
|
* Find dynamic context generating functions and merge their results into the static contexts
|
|
* Combine an array of unchanging contexts with the result of a context-creating function
|
|
*
|
|
* @param {(object|function(...*): ?object)[]} dynamicOrStaticContexts Array of custom context Objects or custom context generating functions
|
|
* @param {...*} Parameters to pass to dynamic callbacks
|
|
*/
|
|
object.resolveDynamicContexts = function (dynamicOrStaticContexts) {
|
|
let params = Array.prototype.slice.call(arguments, 1);
|
|
return map(dynamicOrStaticContexts, function(context) {
|
|
if (typeof context === 'function') {
|
|
try {
|
|
return context.apply(null, params);
|
|
} catch (e) {
|
|
//TODO: provide warning
|
|
}
|
|
} else {
|
|
return context;
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Only log deprecation warnings if they won't cause an error
|
|
*/
|
|
object.warn = function(message) {
|
|
if (typeof console !== 'undefined') {
|
|
console.warn('Snowplow: ' + message);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* List the classes of a DOM element without using elt.classList (for compatibility with IE 9)
|
|
*/
|
|
object.getCssClasses = function (elt) {
|
|
return elt.className.match(/\S+/g) || [];
|
|
};
|
|
|
|
/**
|
|
* Check whether an element has at least one class from a given list
|
|
*/
|
|
function checkClass(elt, classList) {
|
|
var classes = object.getCssClasses(elt),
|
|
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 (Array.isArray(criterion) || !isObject(criterion)) {
|
|
return function () {
|
|
return true;
|
|
};
|
|
}
|
|
|
|
if (criterion.hasOwnProperty('filter')) {
|
|
return criterion.filter;
|
|
} else {
|
|
var inclusive = criterion.hasOwnProperty('whitelist');
|
|
var specifiedClasses = criterion.whitelist || criterion.blacklist;
|
|
if (!Array.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<specifiedClasses.length; i++) {
|
|
specifiedClassesSet[specifiedClasses[i]] = true;
|
|
}
|
|
|
|
if (byClass) {
|
|
return function (elt) {
|
|
return checkClass(elt, specifiedClassesSet) === inclusive;
|
|
};
|
|
} else {
|
|
return function (elt) {
|
|
return elt.name in specifiedClassesSet === inclusive;
|
|
};
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Convert a criterion object to a transform function
|
|
*
|
|
* @param object criterion {transform: function (elt) {return the result of transform function applied to element}
|
|
*/
|
|
object.getTransform = function (criterion) {
|
|
if (!isObject(criterion)) {
|
|
return function(x) { return x };
|
|
}
|
|
|
|
if (criterion.hasOwnProperty('transform')) {
|
|
return criterion.transform;
|
|
} else {
|
|
return function(x) { return x };
|
|
}
|
|
|
|
return function(x) { return x };
|
|
};
|
|
|
|
/**
|
|
* Add a name-value pair to the querystring of a URL
|
|
*
|
|
* @param string url URL to decorate
|
|
* @param string name Name of the querystring pair
|
|
* @param string value Value of the querystring pair
|
|
*/
|
|
object.decorateQuerystring = function (url, name, value) {
|
|
var initialQsParams = name + '=' + value;
|
|
var hashSplit = url.split('#');
|
|
var qsSplit = hashSplit[0].split('?');
|
|
var beforeQuerystring = qsSplit.shift();
|
|
// Necessary because a querystring may contain multiple question marks
|
|
var querystring = qsSplit.join('?');
|
|
if (!querystring) {
|
|
querystring = initialQsParams;
|
|
} else {
|
|
// Whether this is the first time the link has been decorated
|
|
var initialDecoration = true;
|
|
var qsFields = querystring.split('&');
|
|
for (var i=0; i<qsFields.length; i++) {
|
|
if (qsFields[i].substr(0, name.length + 1) === name + '=') {
|
|
initialDecoration = false;
|
|
qsFields[i] = initialQsParams;
|
|
querystring = qsFields.join('&');
|
|
break;
|
|
}
|
|
}
|
|
if (initialDecoration) {
|
|
querystring = initialQsParams + '&' + querystring;
|
|
}
|
|
}
|
|
hashSplit[0] = beforeQuerystring + '?' + querystring;
|
|
return hashSplit.join('#');
|
|
};
|
|
|
|
/**
|
|
* Attempt to get a value from localStorage
|
|
*
|
|
* @param string key
|
|
* @return string The value obtained from localStorage, or
|
|
* undefined if localStorage is inaccessible
|
|
*/
|
|
object.attemptGetLocalStorage = function (key) {
|
|
try {
|
|
return localStorage.getItem(key);
|
|
} catch(e) {}
|
|
};
|
|
|
|
/**
|
|
* Attempt to write a value to localStorage
|
|
*
|
|
* @param string key
|
|
* @param string value
|
|
* @return boolean Whether the operation succeeded
|
|
*/
|
|
object.attemptWriteLocalStorage = function (key, value) {
|
|
try {
|
|
localStorage.setItem(key, value);
|
|
return true;
|
|
} catch(e) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Finds the root domain
|
|
*/
|
|
object.findRootDomain = function () {
|
|
var cookiePrefix = '_sp_root_domain_test_';
|
|
var cookieName = cookiePrefix + new Date().getTime();
|
|
var cookieValue = '_test_value_' + new Date().getTime();
|
|
|
|
var split = window.location.hostname.split('.');
|
|
var position = split.length - 1;
|
|
while (position >= 0) {
|
|
var currentDomain = split.slice(position, split.length).join('.');
|
|
cookie.cookie(cookieName, cookieValue, 0, '/', currentDomain);
|
|
if (cookie.cookie(cookieName) === cookieValue) {
|
|
|
|
// Clean up created cookie(s)
|
|
object.deleteCookie(cookieName, currentDomain);
|
|
var cookieNames = object.getCookiesWithPrefix(cookiePrefix);
|
|
for (var i = 0; i < cookieNames.length; i++) {
|
|
object.deleteCookie(cookieNames[i], currentDomain);
|
|
}
|
|
|
|
return currentDomain;
|
|
}
|
|
position -= 1;
|
|
}
|
|
|
|
// Cookies cannot be read
|
|
return window.location.hostname;
|
|
};
|
|
|
|
/**
|
|
* Checks whether a value is present within an array
|
|
*
|
|
* @param val The value to check for
|
|
* @param array The array to check within
|
|
* @return boolean Whether it exists
|
|
*/
|
|
object.isValueInArray = function (val, array) {
|
|
for (var i = 0; i < array.length; i++) {
|
|
if (array[i] === val) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Deletes an arbitrary cookie by setting the expiration date to the past
|
|
*
|
|
* @param cookieName The name of the cookie to delete
|
|
* @param domainName The domain the cookie is in
|
|
*/
|
|
object.deleteCookie = function (cookieName, domainName) {
|
|
cookie.cookie(cookieName, '', -1, '/', domainName);
|
|
};
|
|
|
|
/**
|
|
* Fetches the name of all cookies beginning with a certain prefix
|
|
*
|
|
* @param cookiePrefix The prefix to check for
|
|
* @return array The cookies that begin with the prefix
|
|
*/
|
|
object.getCookiesWithPrefix = function (cookiePrefix) {
|
|
var cookies = document.cookie.split("; ");
|
|
var cookieNames = [];
|
|
for (var i = 0; i < cookies.length; i++) {
|
|
if (cookies[i].substring(0, cookiePrefix.length) === cookiePrefix) {
|
|
cookieNames.push(cookies[i]);
|
|
}
|
|
}
|
|
return cookieNames;
|
|
};
|
|
|
|
/**
|
|
* Parses an object and returns either the
|
|
* integer or undefined.
|
|
*
|
|
* @param obj The object to parse
|
|
* @return the result of the parse operation
|
|
*/
|
|
object.parseInt = function (obj) {
|
|
var result = parseInt(obj);
|
|
return isNaN(result) ? undefined : result;
|
|
};
|
|
|
|
/**
|
|
* Parses an object and returns either the
|
|
* number or undefined.
|
|
*
|
|
* @param obj The object to parse
|
|
* @return the result of the parse operation
|
|
*/
|
|
object.parseFloat = function (obj) {
|
|
var result = parseFloat(obj);
|
|
return isNaN(result) ? undefined : result;
|
|
}
|
|
|
|
}());
|