From 7d5b0a3d404a0696f74410fa3aeefac507370c7f Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Wed, 11 Nov 2015 12:59:28 -0800 Subject: [PATCH 1/3] build snowplow for neon --- dist/.gitignore | 4 - dist/bundle.js | 8279 +++++++++++++++++++++++++++++++++++++++++++++ dist/snowplow.js | 8310 ++++++++++++++++++++++++++++++++++++++++++++++ dist/sp.js | 71 + 4 files changed, 16660 insertions(+), 4 deletions(-) delete mode 100644 dist/.gitignore create mode 100644 dist/bundle.js create mode 100644 dist/snowplow.js create mode 100644 dist/sp.js diff --git a/dist/.gitignore b/dist/.gitignore deleted file mode 100644 index 9170fd96c..000000000 --- a/dist/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# Assembled files -bundle.js -snowplow.js -sp.js diff --git a/dist/bundle.js b/dist/bundle.js new file mode 100644 index 000000000..62ad73b0c --- /dev/null +++ b/dist/bundle.js @@ -0,0 +1,8279 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o +* @license MIT License +*/ + + + +this.cookie = function(name, value, ttl, path, domain, secure) { + + if (arguments.length > 1) { + return document.cookie = name + "=" + escape(value) + + (ttl ? "; expires=" + new Date(+new Date()+(ttl*1000)).toUTCString() : "") + + (path ? "; path=" + path : "") + + (domain ? "; domain=" + domain : "") + + (secure ? "; secure" : "") + } + + return unescape((("; "+document.cookie).split("; "+name+"=")[1]||"").split(";")[0]) +} + + +},{}],3:[function(require,module,exports){ +/** + * This script gives you the zone info key representing your device's time zone setting. + * + * @name jsTimezoneDetect + * @version 1.0.5 + * @author Jon Nylander + * @license MIT License - http://www.opensource.org/licenses/mit-license.php + * + * For usage and examples, visit: + * http://pellepim.bitbucket.org/jstz/ + * + * Copyright (c) Jon Nylander + */ + +/*jslint undef: true */ +/*global console, exports*/ + +(function(root) { + /** + * Namespace to hold all the code for timezone detection. + */ + var jstz = (function () { + 'use strict'; + var HEMISPHERE_SOUTH = 's', + + /** + * Gets the offset in minutes from UTC for a certain date. + * @param {Date} date + * @returns {Number} + */ + get_date_offset = function (date) { + var offset = -date.getTimezoneOffset(); + return (offset !== null ? offset : 0); + }, + + get_date = function (year, month, date) { + var d = new Date(); + if (year !== undefined) { + d.setFullYear(year); + } + d.setMonth(month); + d.setDate(date); + return d; + }, + + get_january_offset = function (year) { + return get_date_offset(get_date(year, 0 ,2)); + }, + + get_june_offset = function (year) { + return get_date_offset(get_date(year, 5, 2)); + }, + + /** + * Private method. + * Checks whether a given date is in daylight saving time. + * If the date supplied is after august, we assume that we're checking + * for southern hemisphere DST. + * @param {Date} date + * @returns {Boolean} + */ + date_is_dst = function (date) { + var is_southern = date.getMonth() > 7, + base_offset = is_southern ? get_june_offset(date.getFullYear()) : + get_january_offset(date.getFullYear()), + date_offset = get_date_offset(date), + is_west = base_offset < 0, + dst_offset = base_offset - date_offset; + + if (!is_west && !is_southern) { + return dst_offset < 0; + } + + return dst_offset !== 0; + }, + + /** + * This function does some basic calculations to create information about + * the user's timezone. It uses REFERENCE_YEAR as a solid year for which + * the script has been tested rather than depend on the year set by the + * client device. + * + * Returns a key that can be used to do lookups in jstz.olson.timezones. + * eg: "720,1,2". + * + * @returns {String} + */ + + lookup_key = function () { + var january_offset = get_january_offset(), + june_offset = get_june_offset(), + diff = january_offset - june_offset; + + if (diff < 0) { + return january_offset + ",1"; + } else if (diff > 0) { + return june_offset + ",1," + HEMISPHERE_SOUTH; + } + + return january_offset + ",0"; + }, + + /** + * Uses get_timezone_info() to formulate a key to use in the olson.timezones dictionary. + * + * Returns a primitive object on the format: + * {'timezone': TimeZone, 'key' : 'the key used to find the TimeZone object'} + * + * @returns Object + */ + determine = function () { + var key = lookup_key(); + return new jstz.TimeZone(jstz.olson.timezones[key]); + }, + + /** + * This object contains information on when daylight savings starts for + * different timezones. + * + * The list is short for a reason. Often we do not have to be very specific + * to single out the correct timezone. But when we do, this list comes in + * handy. + * + * Each value is a date denoting when daylight savings starts for that timezone. + */ + dst_start_for = function (tz_name) { + + var ru_pre_dst_change = new Date(2010, 6, 15, 1, 0, 0, 0), // In 2010 Russia had DST, this allows us to detect Russia :) + dst_starts = { + 'America/Denver': new Date(2011, 2, 13, 3, 0, 0, 0), + 'America/Mazatlan': new Date(2011, 3, 3, 3, 0, 0, 0), + 'America/Chicago': new Date(2011, 2, 13, 3, 0, 0, 0), + 'America/Mexico_City': new Date(2011, 3, 3, 3, 0, 0, 0), + 'America/Asuncion': new Date(2012, 9, 7, 3, 0, 0, 0), + 'America/Santiago': new Date(2012, 9, 3, 3, 0, 0, 0), + 'America/Campo_Grande': new Date(2012, 9, 21, 5, 0, 0, 0), + 'America/Montevideo': new Date(2011, 9, 2, 3, 0, 0, 0), + 'America/Sao_Paulo': new Date(2011, 9, 16, 5, 0, 0, 0), + 'America/Los_Angeles': new Date(2011, 2, 13, 8, 0, 0, 0), + 'America/Santa_Isabel': new Date(2011, 3, 5, 8, 0, 0, 0), + 'America/Havana': new Date(2012, 2, 10, 2, 0, 0, 0), + 'America/New_York': new Date(2012, 2, 10, 7, 0, 0, 0), + 'Europe/Helsinki': new Date(2013, 2, 31, 5, 0, 0, 0), + 'Pacific/Auckland': new Date(2011, 8, 26, 7, 0, 0, 0), + 'America/Halifax': new Date(2011, 2, 13, 6, 0, 0, 0), + 'America/Goose_Bay': new Date(2011, 2, 13, 2, 1, 0, 0), + 'America/Miquelon': new Date(2011, 2, 13, 5, 0, 0, 0), + 'America/Godthab': new Date(2011, 2, 27, 1, 0, 0, 0), + 'Europe/Moscow': ru_pre_dst_change, + 'Asia/Amman': new Date(2013, 2, 29, 1, 0, 0, 0), + 'Asia/Beirut': new Date(2013, 2, 31, 2, 0, 0, 0), + 'Asia/Damascus': new Date(2013, 3, 6, 2, 0, 0, 0), + 'Asia/Jerusalem': new Date(2013, 2, 29, 5, 0, 0, 0), + 'Asia/Yekaterinburg': ru_pre_dst_change, + 'Asia/Omsk': ru_pre_dst_change, + 'Asia/Krasnoyarsk': ru_pre_dst_change, + 'Asia/Irkutsk': ru_pre_dst_change, + 'Asia/Yakutsk': ru_pre_dst_change, + 'Asia/Vladivostok': ru_pre_dst_change, + 'Asia/Baku': new Date(2013, 2, 31, 4, 0, 0), + 'Asia/Yerevan': new Date(2013, 2, 31, 3, 0, 0), + 'Asia/Kamchatka': ru_pre_dst_change, + 'Asia/Gaza': new Date(2010, 2, 27, 4, 0, 0), + 'Africa/Cairo': new Date(2010, 4, 1, 3, 0, 0), + 'Europe/Minsk': ru_pre_dst_change, + 'Pacific/Apia': new Date(2010, 10, 1, 1, 0, 0, 0), + 'Pacific/Fiji': new Date(2010, 11, 1, 0, 0, 0), + 'Australia/Perth': new Date(2008, 10, 1, 1, 0, 0, 0) + }; + + return dst_starts[tz_name]; + }; + + return { + determine: determine, + date_is_dst: date_is_dst, + dst_start_for: dst_start_for + }; + }()); + + /** + * Simple object to perform ambiguity check and to return name of time zone. + */ + jstz.TimeZone = function (tz_name) { + 'use strict'; + /** + * The keys in this object are timezones that we know may be ambiguous after + * a preliminary scan through the olson_tz object. + * + * The array of timezones to compare must be in the order that daylight savings + * starts for the regions. + */ + var AMBIGUITIES = { + 'America/Denver': ['America/Denver', 'America/Mazatlan'], + 'America/Chicago': ['America/Chicago', 'America/Mexico_City'], + 'America/Santiago': ['America/Santiago', 'America/Asuncion', 'America/Campo_Grande'], + 'America/Montevideo': ['America/Montevideo', 'America/Sao_Paulo'], + 'Asia/Beirut': ['Asia/Amman', 'Asia/Jerusalem', 'Asia/Beirut', 'Europe/Helsinki','Asia/Damascus'], + 'Pacific/Auckland': ['Pacific/Auckland', 'Pacific/Fiji'], + 'America/Los_Angeles': ['America/Los_Angeles', 'America/Santa_Isabel'], + 'America/New_York': ['America/Havana', 'America/New_York'], + 'America/Halifax': ['America/Goose_Bay', 'America/Halifax'], + 'America/Godthab': ['America/Miquelon', 'America/Godthab'], + 'Asia/Dubai': ['Europe/Moscow'], + 'Asia/Dhaka': ['Asia/Yekaterinburg'], + 'Asia/Jakarta': ['Asia/Omsk'], + 'Asia/Shanghai': ['Asia/Krasnoyarsk', 'Australia/Perth'], + 'Asia/Tokyo': ['Asia/Irkutsk'], + 'Australia/Brisbane': ['Asia/Yakutsk'], + 'Pacific/Noumea': ['Asia/Vladivostok'], + 'Pacific/Tarawa': ['Asia/Kamchatka', 'Pacific/Fiji'], + 'Pacific/Tongatapu': ['Pacific/Apia'], + 'Asia/Baghdad': ['Europe/Minsk'], + 'Asia/Baku': ['Asia/Yerevan','Asia/Baku'], + 'Africa/Johannesburg': ['Asia/Gaza', 'Africa/Cairo'] + }, + + timezone_name = tz_name, + + /** + * Checks if a timezone has possible ambiguities. I.e timezones that are similar. + * + * For example, if the preliminary scan determines that we're in America/Denver. + * We double check here that we're really there and not in America/Mazatlan. + * + * This is done by checking known dates for when daylight savings start for different + * timezones during 2010 and 2011. + */ + ambiguity_check = function () { + var ambiguity_list = AMBIGUITIES[timezone_name], + length = ambiguity_list.length, + i = 0, + tz = ambiguity_list[0]; + + for (; i < length; i += 1) { + tz = ambiguity_list[i]; + + if (jstz.date_is_dst(jstz.dst_start_for(tz))) { + timezone_name = tz; + return; + } + } + }, + + /** + * Checks if it is possible that the timezone is ambiguous. + */ + is_ambiguous = function () { + return typeof (AMBIGUITIES[timezone_name]) !== 'undefined'; + }; + + if (is_ambiguous()) { + ambiguity_check(); + } + + return { + name: function () { + return timezone_name; + } + }; + }; + + jstz.olson = {}; + + /* + * The keys in this dictionary are comma separated as such: + * + * First the offset compared to UTC time in minutes. + * + * Then a flag which is 0 if the timezone does not take daylight savings into account and 1 if it + * does. + * + * Thirdly an optional 's' signifies that the timezone is in the southern hemisphere, + * only interesting for timezones with DST. + * + * The mapped arrays is used for constructing the jstz.TimeZone object from within + * jstz.determine_timezone(); + */ + jstz.olson.timezones = { + '-720,0' : 'Pacific/Majuro', + '-660,0' : 'Pacific/Pago_Pago', + '-600,1' : 'America/Adak', + '-600,0' : 'Pacific/Honolulu', + '-570,0' : 'Pacific/Marquesas', + '-540,0' : 'Pacific/Gambier', + '-540,1' : 'America/Anchorage', + '-480,1' : 'America/Los_Angeles', + '-480,0' : 'Pacific/Pitcairn', + '-420,0' : 'America/Phoenix', + '-420,1' : 'America/Denver', + '-360,0' : 'America/Guatemala', + '-360,1' : 'America/Chicago', + '-360,1,s' : 'Pacific/Easter', + '-300,0' : 'America/Bogota', + '-300,1' : 'America/New_York', + '-270,0' : 'America/Caracas', + '-240,1' : 'America/Halifax', + '-240,0' : 'America/Santo_Domingo', + '-240,1,s' : 'America/Santiago', + '-210,1' : 'America/St_Johns', + '-180,1' : 'America/Godthab', + '-180,0' : 'America/Argentina/Buenos_Aires', + '-180,1,s' : 'America/Montevideo', + '-120,0' : 'America/Noronha', + '-120,1' : 'America/Noronha', + '-60,1' : 'Atlantic/Azores', + '-60,0' : 'Atlantic/Cape_Verde', + '0,0' : 'UTC', + '0,1' : 'Europe/London', + '60,1' : 'Europe/Berlin', + '60,0' : 'Africa/Lagos', + '60,1,s' : 'Africa/Windhoek', + '120,1' : 'Asia/Beirut', + '120,0' : 'Africa/Johannesburg', + '180,0' : 'Asia/Baghdad', + '180,1' : 'Europe/Moscow', + '210,1' : 'Asia/Tehran', + '240,0' : 'Asia/Dubai', + '240,1' : 'Asia/Baku', + '270,0' : 'Asia/Kabul', + '300,1' : 'Asia/Yekaterinburg', + '300,0' : 'Asia/Karachi', + '330,0' : 'Asia/Kolkata', + '345,0' : 'Asia/Kathmandu', + '360,0' : 'Asia/Dhaka', + '360,1' : 'Asia/Omsk', + '390,0' : 'Asia/Rangoon', + '420,1' : 'Asia/Krasnoyarsk', + '420,0' : 'Asia/Jakarta', + '480,0' : 'Asia/Shanghai', + '480,1' : 'Asia/Irkutsk', + '525,0' : 'Australia/Eucla', + '525,1,s' : 'Australia/Eucla', + '540,1' : 'Asia/Yakutsk', + '540,0' : 'Asia/Tokyo', + '570,0' : 'Australia/Darwin', + '570,1,s' : 'Australia/Adelaide', + '600,0' : 'Australia/Brisbane', + '600,1' : 'Asia/Vladivostok', + '600,1,s' : 'Australia/Sydney', + '630,1,s' : 'Australia/Lord_Howe', + '660,1' : 'Asia/Kamchatka', + '660,0' : 'Pacific/Noumea', + '690,0' : 'Pacific/Norfolk', + '720,1,s' : 'Pacific/Auckland', + '720,0' : 'Pacific/Tarawa', + '765,1,s' : 'Pacific/Chatham', + '780,0' : 'Pacific/Tongatapu', + '780,1,s' : 'Pacific/Apia', + '840,0' : 'Pacific/Kiritimati' + }; + + if (typeof exports !== 'undefined') { + exports.jstz = jstz; + } else { + root.jstz = jstz; + } +})(this); + + +},{}],4:[function(require,module,exports){ +(function(){ + var _global = this; + + /** + * JS Implementation of MurmurHash2 + * + * @author Gary Court + * @see http://github.com/garycourt/murmurhash-js + * @author Austin Appleby + * @see http://sites.google.com/site/murmurhash/ + * + * @param {string} str ASCII only + * @param {number} seed Positive integer only + * @return {number} 32-bit positive integer hash + */ + function MurmurHashV2(str, seed) { + var + l = str.length, + h = seed ^ l, + i = 0, + k; + + while (l >= 4) { + k = + ((str.charCodeAt(i) & 0xff)) | + ((str.charCodeAt(++i) & 0xff) << 8) | + ((str.charCodeAt(++i) & 0xff) << 16) | + ((str.charCodeAt(++i) & 0xff) << 24); + + k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16)); + k ^= k >>> 24; + k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16)); + + h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k; + + l -= 4; + ++i; + } + + switch (l) { + case 3: h ^= (str.charCodeAt(i + 2) & 0xff) << 16; + case 2: h ^= (str.charCodeAt(i + 1) & 0xff) << 8; + case 1: h ^= (str.charCodeAt(i) & 0xff); + h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)); + } + + h ^= h >>> 13; + h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)); + h ^= h >>> 15; + + return h >>> 0; + }; + + /** + * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011) + * + * @author Gary Court + * @see http://github.com/garycourt/murmurhash-js + * @author Austin Appleby + * @see http://sites.google.com/site/murmurhash/ + * + * @param {string} key ASCII only + * @param {number} seed Positive integer only + * @return {number} 32-bit positive integer hash + */ + function MurmurHashV3(key, seed) { + var remainder, bytes, h1, h1b, c1, c1b, c2, c2b, k1, i; + + remainder = key.length & 3; // key.length % 4 + bytes = key.length - remainder; + h1 = seed; + c1 = 0xcc9e2d51; + c2 = 0x1b873593; + i = 0; + + while (i < bytes) { + k1 = + ((key.charCodeAt(i) & 0xff)) | + ((key.charCodeAt(++i) & 0xff) << 8) | + ((key.charCodeAt(++i) & 0xff) << 16) | + ((key.charCodeAt(++i) & 0xff) << 24); + ++i; + + k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff; + k1 = (k1 << 15) | (k1 >>> 17); + k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff; + + h1 ^= k1; + h1 = (h1 << 13) | (h1 >>> 19); + h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff; + h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16)); + } + + k1 = 0; + + switch (remainder) { + case 3: k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16; + case 2: k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8; + case 1: k1 ^= (key.charCodeAt(i) & 0xff); + + k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff; + k1 = (k1 << 15) | (k1 >>> 17); + k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff; + h1 ^= k1; + } + + h1 ^= key.length; + + h1 ^= h1 >>> 16; + h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff; + h1 ^= h1 >>> 13; + h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff; + h1 ^= h1 >>> 16; + + return h1 >>> 0; + } + + var murmur = MurmurHashV3; + murmur.v2 = MurmurHashV2; + murmur.v3 = MurmurHashV3; + + if (typeof(module) != 'undefined') { + module.exports = murmur; + } else { + var _previousRoot = _global.murmur; + murmur.noConflict = function() { + _global.murmur = _previousRoot; + return murmur; + } + _global.murmur = murmur; + } +}()); + +},{}],5:[function(require,module,exports){ +var charenc = { + // UTF-8 encoding + utf8: { + // Convert a string to a byte array + stringToBytes: function(str) { + return charenc.bin.stringToBytes(unescape(encodeURIComponent(str))); + }, + + // Convert a byte array to a string + bytesToString: function(bytes) { + return decodeURIComponent(escape(charenc.bin.bytesToString(bytes))); + } + }, + + // Binary encoding + bin: { + // Convert a string to a byte array + stringToBytes: function(str) { + for (var bytes = [], i = 0; i < str.length; i++) + bytes.push(str.charCodeAt(i) & 0xFF); + return bytes; + }, + + // Convert a byte array to a string + bytesToString: function(bytes) { + for (var str = [], i = 0; i < bytes.length; i++) + str.push(String.fromCharCode(bytes[i])); + return str.join(''); + } + } +}; + +module.exports = charenc; + +},{}],6:[function(require,module,exports){ +(function() { + var base64map + = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', + + crypt = { + // Bit-wise rotation left + rotl: function(n, b) { + return (n << b) | (n >>> (32 - b)); + }, + + // Bit-wise rotation right + rotr: function(n, b) { + return (n << (32 - b)) | (n >>> b); + }, + + // Swap big-endian to little-endian and vice versa + endian: function(n) { + // If number given, swap endian + if (n.constructor == Number) { + return crypt.rotl(n, 8) & 0x00FF00FF | crypt.rotl(n, 24) & 0xFF00FF00; + } + + // Else, assume array and swap all items + for (var i = 0; i < n.length; i++) + n[i] = crypt.endian(n[i]); + return n; + }, + + // Generate an array of any length of random bytes + randomBytes: function(n) { + for (var bytes = []; n > 0; n--) + bytes.push(Math.floor(Math.random() * 256)); + return bytes; + }, + + // Convert a byte array to big-endian 32-bit words + bytesToWords: function(bytes) { + for (var words = [], i = 0, b = 0; i < bytes.length; i++, b += 8) + words[b >>> 5] |= bytes[i] << (24 - b % 32); + return words; + }, + + // Convert big-endian 32-bit words to a byte array + wordsToBytes: function(words) { + for (var bytes = [], b = 0; b < words.length * 32; b += 8) + bytes.push((words[b >>> 5] >>> (24 - b % 32)) & 0xFF); + return bytes; + }, + + // Convert a byte array to a hex string + bytesToHex: function(bytes) { + for (var hex = [], i = 0; i < bytes.length; i++) { + hex.push((bytes[i] >>> 4).toString(16)); + hex.push((bytes[i] & 0xF).toString(16)); + } + return hex.join(''); + }, + + // Convert a hex string to a byte array + hexToBytes: function(hex) { + for (var bytes = [], c = 0; c < hex.length; c += 2) + bytes.push(parseInt(hex.substr(c, 2), 16)); + return bytes; + }, + + // Convert a byte array to a base-64 string + bytesToBase64: function(bytes) { + for (var base64 = [], i = 0; i < bytes.length; i += 3) { + var triplet = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; + for (var j = 0; j < 4; j++) + if (i * 8 + j * 6 <= bytes.length * 8) + base64.push(base64map.charAt((triplet >>> 6 * (3 - j)) & 0x3F)); + else + base64.push('='); + } + return base64.join(''); + }, + + // Convert a base-64 string to a byte array + base64ToBytes: function(base64) { + // Remove non-base-64 characters + base64 = base64.replace(/[^A-Z0-9+\/]/ig, ''); + + for (var bytes = [], i = 0, imod4 = 0; i < base64.length; + imod4 = ++i % 4) { + if (imod4 == 0) continue; + bytes.push(((base64map.indexOf(base64.charAt(i - 1)) + & (Math.pow(2, -2 * imod4 + 8) - 1)) << (imod4 * 2)) + | (base64map.indexOf(base64.charAt(i)) >>> (6 - imod4 * 2))); + } + return bytes; + } + }; + + module.exports = crypt; +})(); + +},{}],7:[function(require,module,exports){ +(function() { + var crypt = require('crypt'), + utf8 = require('charenc').utf8, + bin = require('charenc').bin, + + // The core + sha1 = function (message) { + // Convert to byte array + if (message.constructor == String) + message = utf8.stringToBytes(message); + // otherwise assume byte array + + var m = crypt.bytesToWords(message), + l = message.length * 8, + w = [], + H0 = 1732584193, + H1 = -271733879, + H2 = -1732584194, + H3 = 271733878, + H4 = -1009589776; + + // Padding + m[l >> 5] |= 0x80 << (24 - l % 32); + m[((l + 64 >>> 9) << 4) + 15] = l; + + for (var i = 0; i < m.length; i += 16) { + var a = H0, + b = H1, + c = H2, + d = H3, + e = H4; + + for (var j = 0; j < 80; j++) { + + if (j < 16) + w[j] = m[i + j]; + else { + var n = w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16]; + w[j] = (n << 1) | (n >>> 31); + } + + var t = ((H0 << 5) | (H0 >>> 27)) + H4 + (w[j] >>> 0) + ( + j < 20 ? (H1 & H2 | ~H1 & H3) + 1518500249 : + j < 40 ? (H1 ^ H2 ^ H3) + 1859775393 : + j < 60 ? (H1 & H2 | H1 & H3 | H2 & H3) - 1894007588 : + (H1 ^ H2 ^ H3) - 899497514); + + H4 = H3; + H3 = H2; + H2 = (H1 << 30) | (H1 >>> 2); + H1 = H0; + H0 = t; + } + + H0 += a; + H1 += b; + H2 += c; + H3 += d; + H4 += e; + } + + return [H0, H1, H2, H3, H4]; + }, + + // Public API + api = function (message, options) { + var digestbytes = crypt.wordsToBytes(sha1(message)); + return options && options.asBytes ? digestbytes : + options && options.asString ? bin.bytesToString(digestbytes) : + crypt.bytesToHex(digestbytes); + }; + + api._blocksize = 16; + api._digestsize = 20; + + module.exports = api; +})(); + +},{"charenc":5,"crypt":6}],8:[function(require,module,exports){ +/* + * JavaScript tracker core for Snowplow: index.js + * + * Copyright (c) 2014 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +module.exports = require('./lib/core'); + +},{"./lib/core":10}],9:[function(require,module,exports){ +/* + * Copyright (c) 2013 Kevin van Zonneveld (http://kvz.io) + * and Contributors (http://phpjs.org/authors) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +(function() { + + var object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support + + function base64_encode(data) { + // discuss at: http://phpjs.org/functions/base64_encode/ + // original by: Tyler Akins (http://rumkin.com) + // improved by: Bayron Guevara + // improved by: Thunder.m + // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // improved by: Rafał Kukawski (http://kukawski.pl) + // bugfixed by: Pellentesque Malesuada + // example 1: base64_encode('Kevin van Zonneveld'); + // returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA==' + // example 2: base64_encode('a'); + // returns 2: 'YQ==' + // example 3: base64_encode('✓ à la mode'); + // returns 3: '4pyTIMOgIGxhIG1vZGU=' + + var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + enc = '', + tmp_arr = []; + + if (!data) { + return data; + } + + data = unescape(encodeURIComponent(data)); + + do { + // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = o1 << 16 | o2 << 8 | o3; + + h1 = bits >> 18 & 0x3f; + h2 = bits >> 12 & 0x3f; + h3 = bits >> 6 & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(''); + + var r = data.length % 3; + + return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3); + } + + object.base64encode = base64_encode; + +}()); + +},{}],10:[function(require,module,exports){ +/* + * JavaScript tracker core for Snowplow: core.js + * + * Copyright (c) 2014 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +var payload = require('./payload.js'); +var uuid = require('uuid'); + +/** + * Create a tracker core object + * + * @param boolean base64 Whether to base 64 encode contexts and unstructured event JSONs + * @param callback function Function applied to every payload dictionary object + * @return object Tracker core + */ +function trackerCore(base64, callback) { + + // base 64 encoding should default to true + if (typeof base64 === 'undefined') { + base64 = true; + } + + // Dictionary of key-value pairs which get added to every payload, e.g. tracker version + var payloadPairs = {}; + + /** + * Set a persistent key-value pair to be added to every payload + * + * @param string key Field name + * @param string value Field value + */ + function addPayloadPair(key, value) { + payloadPairs[key] = value; + } + + /** + * Returns a copy of a JSON with undefined and null properties removed + * + * @param object eventJson JSON to clean + * @param object exemptFields Set of fields which should not be removed even if empty + * @return object A cleaned copy of eventJson + */ + function removeEmptyProperties(eventJson, exemptFields) { + var ret = {}; + exemptFields = exemptFields || {}; + for (var k in eventJson) { + if (exemptFields[k] || (eventJson[k] !== null && typeof eventJson[k] !== 'undefined')) { + ret[k] = eventJson[k]; + } + } + return ret; + } + + /** + * Wraps an array of custom contexts in a self-describing JSON + * + * @param array contexts Array of custom context self-describing JSONs + * @return object Outer JSON + */ + function completeContexts(contexts) { + if (contexts && contexts.length) { + return { + schema: 'iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0', + data: contexts + }; + } + } + + /** + * Gets called by every trackXXX method + * Adds context and payloadPairs name-value pairs to the payload + * Applies the callback to the built payload + * + * @param sb object Payload + * @param array contexts Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload after the callback is applied + */ + function track(sb, context, tstamp) { + sb.addDict(payloadPairs); + sb.add('eid', uuid.v4()); + sb.add('dtm', tstamp || new Date().getTime()); + if (context) { + sb.addJson('cx', 'co', completeContexts(context)); + } + + if (typeof callback === 'function') { + callback(sb); + } + + return sb; + } + + /** + * Log an unstructured event + * + * @param object eventJson Contains the properties and schema location for the event + * @param array context Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + function trackUnstructEvent(properties, context, tstamp) { + var sb = payload.payloadBuilder(base64); + var ueJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0', + data: properties + }; + + sb.add('e', 'ue'); + sb.addJson('ue_px', 'ue_pr', ueJson); + + return track(sb, context, tstamp); + } + + return { + + /** + * Turn base 64 encoding on or off + * + * @param boolean encode key Field name + */ + setBase64Encoding: function (encode) { + base64 = encode; + }, + + addPayloadPair: addPayloadPair, + + /** + * Merges a dictionary into payloadPairs + * + * @param object dict Dictionary to add + */ + addPayloadDict: function (dict) { + for (var key in dict) { + if (dict.hasOwnProperty(key)) { + payloadPairs[key] = dict[key]; + } + } + }, + + /** + * Replace payloadPairs with a new dictionary + * + * @param object dict New dictionary + */ + resetPayloadPairs: function (dict) { + payloadPairs = payload.isJson(dict) ? dict : {}; + }, + + /** + * Set the tracker version + * + * @param string version + */ + setTrackerVersion: function (version) { + addPayloadPair('tv', version); + }, + + /** + * Set the tracker namespace + * + * @param string name + */ + setTrackerNamespace: function (name) { + addPayloadPair('tna', name); + }, + + /** + * Set the application ID + * + * @param string appId + */ + setAppId: function (appId) { + addPayloadPair('aid', appId); + }, + + /** + * Set the platform + * + * @param string value + */ + setPlatform: function (value) { + addPayloadPair('p', value); + }, + + /** + * Set the user ID + * + * @param string userId + */ + setUserId: function (userId) { + addPayloadPair('uid', userId); + }, + + /** + * Set the screen resolution + * + * @param number width + * @param number height + */ + setScreenResolution: function (width, height) { + addPayloadPair('res', width + 'x' + height); + }, + + /** + * Set the viewport dimensions + * + * @param number width + * @param number height + */ + setViewport: function (width, height) { + addPayloadPair('vp', width + 'x' + height); + }, + + /** + * Set the color depth + * + * @param number depth + */ + setColorDepth: function (depth) { + addPayloadPair('cd', depth); + }, + + /** + * Set the timezone + * + * @param string timezone + */ + setTimezone: function (timezone) { + addPayloadPair('tz', timezone); + }, + + /** + * Set the language + * + * @param string lang + */ + setLang: function (lang) { + addPayloadPair('lang', lang); + }, + + /** + * Set the IP address + * + * @param string appId + */ + setIpAddress: function (ip) { + addPayloadPair('ip', ip); + }, + + trackUnstructEvent: trackUnstructEvent, + + /** + * Log the page view / visit + * + * @param string customTitle The user-defined page title to attach to this page view + * @param array context Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackPageView: function (pageUrl, pageTitle, referrer, context, tstamp) { + var sb = payload.payloadBuilder(base64); + sb.add('e', 'pv'); // 'pv' for Page View + sb.add('url', pageUrl); + sb.add('page', pageTitle); + sb.add('refr', referrer); + + return track(sb, context, tstamp); + }, + /** + * Log that a user is still viewing a given page + * by sending a page ping. + * + * @param string pageTitle The page title to attach to this page ping + * @param minxoffset Minimum page x offset seen in the last ping period + * @param maxXOffset Maximum page x offset seen in the last ping period + * @param minYOffset Minimum page y offset seen in the last ping period + * @param maxYOffset Maximum page y offset seen in the last ping period + * @param array context Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackPagePing: function (pageUrl, pageTitle, referrer, minXOffset, maxXOffset, minYOffset, maxYOffset, context, tstamp) { + var sb = payload.payloadBuilder(base64); + sb.add('e', 'pp'); // 'pp' for Page Ping + sb.add('url', pageUrl); + sb.add('page', pageTitle); + sb.add('refr', referrer); + sb.add('pp_mix', minXOffset); + sb.add('pp_max', maxXOffset); + sb.add('pp_miy', minYOffset); + sb.add('pp_may', maxYOffset); + + return track(sb, context, tstamp); + }, + /** + * Track a structured event + * + * @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 array Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackStructEvent: function (category, action, label, property, value, context, tstamp) { + var sb = payload.payloadBuilder(base64); + sb.add('e', 'se'); // 'se' for Structured Event + sb.add('se_ca', category); + sb.add('se_ac', action); + sb.add('se_la', label); + sb.add('se_pr', property); + sb.add('se_va', value); + + return track(sb, context, tstamp); + }, + + /** + * 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 array context Optional. Context relating to the event. + * @param number tstamp Optional. Timestamp of the event + * @return object Payload + */ + trackEcommerceTransaction: function (orderId, affiliation, totalValue, taxValue, shipping, city, state, country, currency, context, tstamp) { + var sb = payload.payloadBuilder(base64); + sb.add('e', 'tr'); // 'tr' for Transaction + sb.add("tr_id", orderId); + sb.add("tr_af", affiliation); + sb.add("tr_tt", totalValue); + sb.add("tr_tx", taxValue); + sb.add("tr_sh", shipping); + sb.add("tr_ci", city); + sb.add("tr_st", state); + sb.add("tr_co", country); + sb.add("tr_cu", currency); + + return track(sb, context, tstamp); + }, + + /** + * 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 array context Optional. Context relating to the event. + * @param number tstamp Optional. Timestamp of the event + * @return object Payload + */ + trackEcommerceTransactionItem: function (orderId, sku, name, category, price, quantity, currency, context, tstamp) { + var sb = payload.payloadBuilder(base64); + sb.add("e", "ti"); // 'tr' for Transaction Item + sb.add("ti_id", orderId); + sb.add("ti_sk", sku); + sb.add("ti_nm", name); + sb.add("ti_ca", category); + sb.add("ti_pr", price); + sb.add("ti_qu", quantity); + sb.add("ti_cu", currency); + + return track(sb, context, tstamp); + }, + + /** + * Track a screen view unstructured event + * + * @param string name The name of the screen + * @param string id The ID of the screen + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackScreenView: function (name, id, context, tstamp) { + return trackUnstructEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0', + data: removeEmptyProperties({ + name: name, + id: id + }) + }, context, tstamp); + }, + + /** + * Log the link or click with the server + * + * @param string targetUrl + * @param string elementId + * @param array elementClasses + * @param string elementTarget + * @param string elementContent innerHTML of the link + * @param array context Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackLinkClick: function (targetUrl, elementId, elementClasses, elementTarget, elementContent, context, tstamp) { + var eventJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1', + data: removeEmptyProperties({ + targetUrl: targetUrl, + elementId: elementId, + elementClasses: elementClasses, + elementTarget: elementTarget, + elementContent: elementContent + }), + }; + + return trackUnstructEvent(eventJson, context, tstamp); + }, + + /** + * 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 array Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackAdImpression: function(impressionId, costModel, cost, targetUrl, bannerId, zoneId, advertiserId, campaignId, context, tstamp) { + var eventJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/ad_impression/jsonschema/1-0-0', + data: removeEmptyProperties({ + impressionId: impressionId, + costModel: costModel, + cost: cost, + targetUrl: targetUrl, + bannerId: bannerId, + zoneId: zoneId, + advertiserId: advertiserId, + campaignId: campaignId + }) + }; + + return trackUnstructEvent(eventJson, context, tstamp); + }, + + /** + * 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 array Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackAdClick: function (targetUrl, clickId, costModel, cost, bannerId, zoneId, impressionId, advertiserId, campaignId, context, tstamp) { + var eventJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/ad_click/jsonschema/1-0-0', + data: removeEmptyProperties({ + targetUrl: targetUrl, + clickId: clickId, + costModel: costModel, + cost: cost, + bannerId: bannerId, + zoneId: zoneId, + impressionId: impressionId, + advertiserId: advertiserId, + campaignId: campaignId + }) + }; + + return trackUnstructEvent(eventJson, context, tstamp); + }, + + /** + * 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 array Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackAdConversion: function (conversionId, costModel, cost, category, action, property, initialValue, advertiserId, campaignId, context, tstamp) { + var eventJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/ad_conversion/jsonschema/1-0-0', + data: removeEmptyProperties({ + conversionId: conversionId, + costModel: costModel, + cost: cost, + category: category, + action: action, + property: property, + initialValue: initialValue, + advertiserId: advertiserId, + campaignId: campaignId + }) + }; + + return trackUnstructEvent(eventJson, context, tstamp); + }, + + /** + * Track a social event + * + * @param string action Social action performed + * @param string network Social network + * @param string target Object of the social action e.g. the video liked, the tweet retweeted + * @param array Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackSocialInteraction: function (action, network, target, context, tstamp) { + var eventJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/social_interaction/jsonschema/1-0-0', + data: removeEmptyProperties({ + action: action, + network: network, + target: target + }) + }; + + return trackUnstructEvent(eventJson, context, tstamp); + }, + + /** + * 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. + * @param number tstamp Optional. Timestamp of the event + * @return object Payload + */ + trackAddToCart: function (sku, name, category, unitPrice, quantity, currency, context, tstamp) { + return trackUnstructEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/add_to_cart/jsonschema/1-0-0', + data: removeEmptyProperties({ + sku: sku, + name: name, + category: category, + unitPrice: unitPrice, + quantity: quantity, + currency: currency + }) + }, context, tstamp); + }, + + /** + * 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. + * @param number tstamp Optional. Timestamp of the event + * @return object Payload + */ + trackRemoveFromCart: function (sku, name, category, unitPrice, quantity, currency, context, tstamp) { + return trackUnstructEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/remove_from_cart/jsonschema/1-0-0', + data: removeEmptyProperties({ + sku: sku, + name: name, + category: category, + unitPrice: unitPrice, + quantity: quantity, + currency: currency + }) + }, context, tstamp); + }, + + /** + * Track the value of a form field changing + * + * @param string formId The parent form ID + * @param string elementId ID of the changed element + * @param string nodeName "INPUT", "TEXTAREA", or "SELECT" + * @param string type Type of the changed element if its type is "INPUT" + * @param array elementClasses List of classes of the changed element + * @param string value The new value of the changed element + * @param array context Optional. Context relating to the event. + * @param number tstamp Optional. Timestamp of the event + * @return object Payload + */ + trackFormChange: function(formId, elementId, nodeName, type, elementClasses, value, context, tstamp) { + return trackUnstructEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/change_form/jsonschema/1-0-0', + data: removeEmptyProperties({ + formId: formId, + elementId: elementId, + nodeName: nodeName, + type: type, + elementClasses: elementClasses, + value: value + }, {'value': true}) + }, context, tstamp); + }, + + /** + * Track a form submission event + * + * @param string formId ID of the form + * @param array formClasses Classes of the form + * @param array elements Mutable elements within the form + * @param array context Optional. Context relating to the event. + * @param number tstamp Optional. Timestamp of the event + * @return object Payload + */ + trackFormSubmission: function(formId, formClasses, elements, context, tstamp) { + return trackUnstructEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/submit_form/jsonschema/1-0-0', + data: removeEmptyProperties({ + formId: formId, + formClasses: formClasses, + elements: elements + }) + }, context, tstamp); + }, + + /** + * Track an internal search event + * + * @param array terms Search terms + * @param object filters Search filters + * @param totalResults Number of results + * @param pageResults Number of results displayed on page + * @param array context Optional. Context relating to the event. + * @param number tstamp Optional. Timestamp of the event + * @return object Payload + */ + trackSiteSearch: function(terms, filters, totalResults, pageResults, context, tstamp) { + return trackUnstructEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/site_search/jsonschema/1-0-0', + data: removeEmptyProperties({ + terms: terms, + filters: filters, + totalResults: totalResults, + pageResults: pageResults + }) + }, context, tstamp); + } + }; +}; + +module.exports = trackerCore; + +},{"./payload.js":11,"uuid":14}],11:[function(require,module,exports){ +/* + * JavaScript tracker core for Snowplow: payload.js + * + * Copyright (c) 2014 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +;(function() { + + var + json2 = require('JSON'), + base64 = require('./base64'), + + object = typeof exports !== 'undefined' ? exports : this; + + /* + * Bas64 encode data with URL and Filename Safe Alphabet (base64url) + * + * See: http://tools.ietf.org/html/rfc4648#page-7 + */ + function base64urlencode(data) { + if (!data) { + return data; + } + + var enc = base64.base64encode(data); + return enc.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); + } + + /* + * Is property a JSON? + */ + object.isJson = function (property) { + return (typeof property !== 'undefined' && property !== null && + (property.constructor === {}.constructor || property.constructor === [].constructor)); + }; + + /* + * Is property a non-empty JSON? + */ + object.isNonEmptyJson = function (property) { + if (!object.isJson(property)) { + return false; + } + for (var key in property) { + if (property.hasOwnProperty(key)) { + return true; + } + } + return false; + } + + /** + * A helper to build a Snowplow request string from an + * an optional initial value plus a set of individual + * name-value pairs, provided using the add method. + * + * @param boolean base64Encode Whether or not JSONs should be + * Base64-URL-safe-encoded + * + * @return object The request string builder, with add, addRaw and build methods + */ + object.payloadBuilder = function (base64Encode) { + var dict = {}; + + var add = function (key, value) { + if (value !== undefined && value !== null && value !== '') { + dict[key] = value; + } + }; + + var addDict = function (dict) { + for (var key in dict) { + if (dict.hasOwnProperty(key)) { + add(key, dict[key]); + } + } + }; + + var addJson = function (keyIfEncoded, keyIfNotEncoded, json) { + + if (object.isNonEmptyJson(json)) { + var str = json2.stringify(json); + if (base64Encode) { + add(keyIfEncoded, base64urlencode(str)); + } else { + add(keyIfNotEncoded, str); + } + } + }; + + return { + add: add, + addDict: addDict, + addJson: addJson, + build: function () { + return dict; + } + }; + }; + +}()); + +},{"./base64":9,"JSON":1}],12:[function(require,module,exports){ +module.exports = Array; + +},{}],13:[function(require,module,exports){ +(function (global){ + +var rng; + +if (global.crypto && crypto.getRandomValues) { + // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto + // Moderately fast, high quality + var _rnds8 = new Uint8Array(16); + rng = function whatwgRNG() { + crypto.getRandomValues(_rnds8); + return _rnds8; + }; +} + +if (!rng) { + // Math.random()-based (RNG) + // + // If all else fails, use Math.random(). It's fast, but is of unspecified + // quality. + var _rnds = new Array(16); + rng = function() { + for (var i = 0, r; i < 16; i++) { + if ((i & 0x03) === 0) r = Math.random() * 0x100000000; + _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff; + } + + return _rnds; + }; +} + +module.exports = rng; + + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],14:[function(require,module,exports){ +// uuid.js +// +// Copyright (c) 2010-2012 Robert Kieffer +// MIT License - http://opensource.org/licenses/mit-license.php + +// Unique ID creation requires a high quality random # generator. We feature +// detect to determine the best RNG source, normalizing to a function that +// returns 128-bits of randomness, since that's what's usually required +var _rng = require('./rng'); + +// Buffer class to use, +// we can't use `Buffer || Array` otherwise Buffer would be +// shimmed by browserify and added to the browser build +var BufferClass = require('./buffer'); + +// Maps for number <-> hex string conversion +var _byteToHex = []; +var _hexToByte = {}; +for (var i = 0; i < 256; i++) { + _byteToHex[i] = (i + 0x100).toString(16).substr(1); + _hexToByte[_byteToHex[i]] = i; +} + +// **`parse()` - Parse a UUID into it's component bytes** +function parse(s, buf, offset) { + var i = (buf && offset) || 0, ii = 0; + + buf = buf || []; + s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) { + if (ii < 16) { // Don't overflow! + buf[i + ii++] = _hexToByte[oct]; + } + }); + + // Zero out remaining bytes if string was short + while (ii < 16) { + buf[i + ii++] = 0; + } + + return buf; +} + +// **`unparse()` - Convert UUID byte array (ala parse()) into a string** +function unparse(buf, offset) { + var i = offset || 0, bth = _byteToHex; + return bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]]; +} + +// **`v1()` - Generate time-based UUID** +// +// Inspired by https://github.com/LiosK/UUID.js +// and http://docs.python.org/library/uuid.html + +// random #'s we need to init node and clockseq +var _seedBytes = _rng(); + +// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) +var _nodeId = [ + _seedBytes[0] | 0x01, + _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5] +]; + +// Per 4.2.2, randomize (14 bit) clockseq +var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff; + +// Previous uuid creation time +var _lastMSecs = 0, _lastNSecs = 0; + +// See https://github.com/broofa/node-uuid for API details +function v1(options, buf, offset) { + var i = buf && offset || 0; + var b = buf || []; + + options = options || {}; + + var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; + + // UUID timestamps are 100 nano-second units since the Gregorian epoch, + // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so + // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' + // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. + var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime(); + + // Per 4.2.1.2, use count of uuid's generated during the current clock + // cycle to simulate higher resolution clock + var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1; + + // Time since last uuid creation (in msecs) + var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000; + + // Per 4.2.1.2, Bump clockseq on clock regression + if (dt < 0 && options.clockseq === undefined) { + clockseq = clockseq + 1 & 0x3fff; + } + + // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new + // time interval + if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) { + nsecs = 0; + } + + // Per 4.2.1.2 Throw error if too many uuids are requested + if (nsecs >= 10000) { + throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec'); + } + + _lastMSecs = msecs; + _lastNSecs = nsecs; + _clockseq = clockseq; + + // Per 4.1.4 - Convert from unix epoch to Gregorian epoch + msecs += 12219292800000; + + // `time_low` + var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000; + b[i++] = tl >>> 24 & 0xff; + b[i++] = tl >>> 16 & 0xff; + b[i++] = tl >>> 8 & 0xff; + b[i++] = tl & 0xff; + + // `time_mid` + var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff; + b[i++] = tmh >>> 8 & 0xff; + b[i++] = tmh & 0xff; + + // `time_high_and_version` + b[i++] = tmh >>> 24 & 0xf | 0x10; // include version + b[i++] = tmh >>> 16 & 0xff; + + // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) + b[i++] = clockseq >>> 8 | 0x80; + + // `clock_seq_low` + b[i++] = clockseq & 0xff; + + // `node` + var node = options.node || _nodeId; + for (var n = 0; n < 6; n++) { + b[i + n] = node[n]; + } + + return buf ? buf : unparse(b); +} + +// **`v4()` - Generate random UUID** + +// See https://github.com/broofa/node-uuid for API details +function v4(options, buf, offset) { + // Deprecated - 'format' argument, as supported in v1.2 + var i = buf && offset || 0; + + if (typeof(options) == 'string') { + buf = options == 'binary' ? new BufferClass(16) : null; + options = null; + } + options = options || {}; + + var rnds = options.random || (options.rng || _rng)(); + + // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` + rnds[6] = (rnds[6] & 0x0f) | 0x40; + rnds[8] = (rnds[8] & 0x3f) | 0x80; + + // Copy bytes to buffer, if provided + if (buf) { + for (var ii = 0; ii < 16; ii++) { + buf[i + ii] = rnds[ii]; + } + } + + return buf || unparse(rnds); +} + +// Export public API +var uuid = v4; +uuid.v1 = v1; +uuid.v4 = v4; +uuid.parse = parse; +uuid.unparse = unparse; +uuid.BufferClass = BufferClass; + +module.exports = uuid; + +},{"./buffer":12,"./rng":13}],15:[function(require,module,exports){ +arguments[4][13][0].apply(exports,arguments) +},{"dup":13}],16:[function(require,module,exports){ +// uuid.js +// +// Copyright (c) 2010-2012 Robert Kieffer +// MIT License - http://opensource.org/licenses/mit-license.php + +// Unique ID creation requires a high quality random # generator. We feature +// detect to determine the best RNG source, normalizing to a function that +// returns 128-bits of randomness, since that's what's usually required +var _rng = require('./rng'); + +// Maps for number <-> hex string conversion +var _byteToHex = []; +var _hexToByte = {}; +for (var i = 0; i < 256; i++) { + _byteToHex[i] = (i + 0x100).toString(16).substr(1); + _hexToByte[_byteToHex[i]] = i; +} + +// **`parse()` - Parse a UUID into it's component bytes** +function parse(s, buf, offset) { + var i = (buf && offset) || 0, ii = 0; + + buf = buf || []; + s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) { + if (ii < 16) { // Don't overflow! + buf[i + ii++] = _hexToByte[oct]; + } + }); + + // Zero out remaining bytes if string was short + while (ii < 16) { + buf[i + ii++] = 0; + } + + return buf; +} + +// **`unparse()` - Convert UUID byte array (ala parse()) into a string** +function unparse(buf, offset) { + var i = offset || 0, bth = _byteToHex; + return bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]]; +} + +// **`v1()` - Generate time-based UUID** +// +// Inspired by https://github.com/LiosK/UUID.js +// and http://docs.python.org/library/uuid.html + +// random #'s we need to init node and clockseq +var _seedBytes = _rng(); + +// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) +var _nodeId = [ + _seedBytes[0] | 0x01, + _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5] +]; + +// Per 4.2.2, randomize (14 bit) clockseq +var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff; + +// Previous uuid creation time +var _lastMSecs = 0, _lastNSecs = 0; + +// See https://github.com/broofa/node-uuid for API details +function v1(options, buf, offset) { + var i = buf && offset || 0; + var b = buf || []; + + options = options || {}; + + var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; + + // UUID timestamps are 100 nano-second units since the Gregorian epoch, + // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so + // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' + // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. + var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime(); + + // Per 4.2.1.2, use count of uuid's generated during the current clock + // cycle to simulate higher resolution clock + var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1; + + // Time since last uuid creation (in msecs) + var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000; + + // Per 4.2.1.2, Bump clockseq on clock regression + if (dt < 0 && options.clockseq === undefined) { + clockseq = clockseq + 1 & 0x3fff; + } + + // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new + // time interval + if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) { + nsecs = 0; + } + + // Per 4.2.1.2 Throw error if too many uuids are requested + if (nsecs >= 10000) { + throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec'); + } + + _lastMSecs = msecs; + _lastNSecs = nsecs; + _clockseq = clockseq; + + // Per 4.1.4 - Convert from unix epoch to Gregorian epoch + msecs += 12219292800000; + + // `time_low` + var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000; + b[i++] = tl >>> 24 & 0xff; + b[i++] = tl >>> 16 & 0xff; + b[i++] = tl >>> 8 & 0xff; + b[i++] = tl & 0xff; + + // `time_mid` + var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff; + b[i++] = tmh >>> 8 & 0xff; + b[i++] = tmh & 0xff; + + // `time_high_and_version` + b[i++] = tmh >>> 24 & 0xf | 0x10; // include version + b[i++] = tmh >>> 16 & 0xff; + + // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) + b[i++] = clockseq >>> 8 | 0x80; + + // `clock_seq_low` + b[i++] = clockseq & 0xff; + + // `node` + var node = options.node || _nodeId; + for (var n = 0; n < 6; n++) { + b[i + n] = node[n]; + } + + return buf ? buf : unparse(b); +} + +// **`v4()` - Generate random UUID** + +// See https://github.com/broofa/node-uuid for API details +function v4(options, buf, offset) { + // Deprecated - 'format' argument, as supported in v1.2 + var i = buf && offset || 0; + + if (typeof(options) == 'string') { + buf = options == 'binary' ? new Array(16) : null; + options = null; + } + options = options || {}; + + var rnds = options.random || (options.rng || _rng)(); + + // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` + rnds[6] = (rnds[6] & 0x0f) | 0x40; + rnds[8] = (rnds[8] & 0x3f) | 0x80; + + // Copy bytes to buffer, if provided + if (buf) { + for (var ii = 0; ii < 16; ii++) { + buf[i + ii] = rnds[ii]; + } + } + + return buf || unparse(rnds); +} + +// Export public API +var uuid = v4; +uuid.v1 = v1; +uuid.v4 = v4; +uuid.parse = parse; +uuid.unparse = unparse; + +module.exports = uuid; + +},{"./rng":15}],17:[function(require,module,exports){ +/* + * 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. + * + * * 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. + */ + +var lodash = require('./lib_managed/lodash'), + helpers = require('./lib/helpers'), + object = typeof exports !== 'undefined' ? exports : this; + +/** + * Object for handling automatic form tracking + * + * @param object core The tracker core + * @param string trackerId Unique identifier for the tracker instance, used to mark tracked elements + * @param function contextAdder Function to add common contexts like PerformanceTiming to all events + * @return object formTrackingManager instance + */ +object.getFormTrackingManager = function (core, trackerId, contextAdder) { + + // 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 && elt.nodeName.toUpperCase() !== 'HTML' && elt.nodeName.toUpperCase() !== 'FORM') { + elt = elt.parentNode; + } + if (elt && elt.nodeName && 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 && 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, helpers.getCssClasses(elt), value, contextAdder(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), helpers.getCssClasses(elt), innerElements, contextAdder(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; + } + }); + } + }; +}; + +},{"./lib/helpers":21,"./lib_managed/lodash":23}],18:[function(require,module,exports){ +/* + * 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. + * + * * 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 + lodash = require('./lib_managed/lodash'), + helpers = require('./lib/helpers'), + uuid = require('uuid'), + + 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) { + + // Page view ID should be shared between all tracker instances + var pageViewId = uuid.v4(), + trackerDictionary = {} + + /** + * 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 || {}; + trackerDictionary[namespace] = new TrackerConstructor(functionName, namespace, version, pageViewId, 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 + }; + }; + +}()); + +},{"./lib/helpers":21,"./lib_managed/lodash":23,"uuid":16}],19:[function(require,module,exports){ +/* + * 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. + * + * * 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. + */ + +// Snowplow Asynchronous Queue + +/* + * Get the name of the global input function + */ + +var snowplow = require('./snowplow'), + queueName, + queue, + windowAlias = window; + +if (windowAlias.GlobalSnowplowNamespace && windowAlias.GlobalSnowplowNamespace.length > 0) { + queueName = windowAlias.GlobalSnowplowNamespace.shift(); + queue = windowAlias[queueName]; + queue.q = new snowplow.Snowplow(queue.q, queueName); +} else { + windowAlias._snaq = windowAlias._snaq || []; + windowAlias._snaq = new snowplow.Snowplow(windowAlias._snaq, '_snaq'); +} + +},{"./snowplow":26}],20:[function(require,module,exports){ +/* + * JavaScript tracker for Snowplow: detectors.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +;(function() { + + var + 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, + + // document.body may not have rendered, so check whether be.offsetHeight is null + bodyHeight = be ? Math.max(be.offsetHeight, be.scrollHeight) : 0; + var w = Math.max(de.clientWidth, de.offsetWidth, de.scrollWidth); + var h = Math.max(de.clientHeight, de.offsetHeight, de.scrollHeight, bodyHeight); + 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; + }; + +}()); + +},{"../lib_managed/lodash":23,"browser-cookie-lite":2,"jstimezonedetect":3,"murmurhash":4}],21:[function(require,module,exports){ +/* + * 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 + 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. 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, ' ')); + }; + + /* + * 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 (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 + * Build: `lodash exports="node" include="isArray, isFunction, isString, isObject, isDate, isUndefined, isNull, map, mapValues, forEach, filter, find, compact, isEmpty, clone" --debug --output src/js/lib_managed/lodash.js` + * Copyright 2012-2013 The Dojo Foundation + * Based on Underscore.js 1.5.2 + * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license + */ +;(function() { + + /** Used to pool arrays and objects used internally */ + var arrayPool = []; + + /** Used internally to indicate various things */ + var indicatorObject = {}; + + /** Used as the max size of the `arrayPool` and `objectPool` */ + var maxPoolSize = 40; + + /** Used to match regexp flags from their coerced string values */ + var reFlags = /\w*$/; + + /** Used to detected named functions */ + var reFuncName = /^\s*function[ \n\r\t]+\w/; + + /** Used to detect functions containing a `this` reference */ + var reThis = /\bthis\b/; + + /** Used to fix the JScript [[DontEnum]] bug */ + var shadowedProps = [ + 'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', + 'toLocaleString', 'toString', 'valueOf' + ]; + + /** `Object#toString` result shortcuts */ + var argsClass = '[object Arguments]', + arrayClass = '[object Array]', + boolClass = '[object Boolean]', + dateClass = '[object Date]', + errorClass = '[object Error]', + funcClass = '[object Function]', + numberClass = '[object Number]', + objectClass = '[object Object]', + regexpClass = '[object RegExp]', + stringClass = '[object String]'; + + /** Used to identify object classifications that `_.clone` supports */ + var cloneableClasses = {}; + cloneableClasses[funcClass] = false; + cloneableClasses[argsClass] = cloneableClasses[arrayClass] = + cloneableClasses[boolClass] = cloneableClasses[dateClass] = + cloneableClasses[numberClass] = cloneableClasses[objectClass] = + cloneableClasses[regexpClass] = cloneableClasses[stringClass] = true; + + /** Used as the property descriptor for `__bindData__` */ + var descriptor = { + 'configurable': false, + 'enumerable': false, + 'value': null, + 'writable': false + }; + + /** Used as the data object for `iteratorTemplate` */ + var iteratorData = { + 'args': '', + 'array': null, + 'bottom': '', + 'firstArg': '', + 'init': '', + 'keys': null, + 'loop': '', + 'shadowedProps': null, + 'support': null, + 'top': '', + 'useHas': false + }; + + /** Used to determine if values are of the language type Object */ + var objectTypes = { + 'boolean': false, + 'function': true, + 'object': true, + 'number': false, + 'string': false, + 'undefined': false + }; + + /** Used as a reference to the global object */ + var root = (objectTypes[typeof window] && window) || this; + + /** Detect free variable `exports` */ + var freeExports = objectTypes[typeof exports] && exports && !exports.nodeType && exports; + + /** Detect free variable `module` */ + var freeModule = objectTypes[typeof module] && module && !module.nodeType && module; + + /** Detect the popular CommonJS extension `module.exports` */ + var moduleExports = freeModule && freeModule.exports === freeExports && freeExports; + + /** Detect free variable `global` from Node.js or Browserified code and use it as `root` */ + var freeGlobal = objectTypes[typeof global] && global; + if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal)) { + root = freeGlobal; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Gets an array from the array pool or creates a new one if the pool is empty. + * + * @private + * @returns {Array} The array from the pool. + */ + function getArray() { + return arrayPool.pop() || []; + } + + /** + * Checks if `value` is a DOM node in IE < 9. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a DOM node, else `false`. + */ + function isNode(value) { + // IE < 9 presents DOM nodes as `Object` objects except they have `toString` + // methods that are `typeof` "string" and still can coerce nodes to strings + return typeof value.toString != 'function' && typeof (value + '') == 'string'; + } + + /** + * Releases the given array back to the array pool. + * + * @private + * @param {Array} [array] The array to release. + */ + function releaseArray(array) { + array.length = 0; + if (arrayPool.length < maxPoolSize) { + arrayPool.push(array); + } + } + + /** + * Slices the `collection` from the `start` index up to, but not including, + * the `end` index. + * + * Note: This function is used instead of `Array#slice` to support node lists + * in IE < 9 and to ensure dense arrays are returned. + * + * @private + * @param {Array|Object|string} collection The collection to slice. + * @param {number} start The start index. + * @param {number} end The end index. + * @returns {Array} Returns the new array. + */ + function slice(array, start, end) { + start || (start = 0); + if (typeof end == 'undefined') { + end = array ? array.length : 0; + } + var index = -1, + length = end - start || 0, + result = Array(length < 0 ? 0 : length); + + while (++index < length) { + result[index] = array[start + index]; + } + return result; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Used for `Array` method references. + * + * Normally `Array.prototype` would suffice, however, using an array literal + * avoids issues in Narwhal. + */ + var arrayRef = []; + + /** Used for native method references */ + var errorProto = Error.prototype, + objectProto = Object.prototype, + stringProto = String.prototype; + + /** Used to resolve the internal [[Class]] of values */ + var toString = objectProto.toString; + + /** Used to detect if a method is native */ + var reNative = RegExp('^' + + String(toString) + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + .replace(/toString| for [^\]]+/g, '.*?') + '$' + ); + + /** Native method shortcuts */ + var fnToString = Function.prototype.toString, + hasOwnProperty = objectProto.hasOwnProperty, + push = arrayRef.push, + propertyIsEnumerable = objectProto.propertyIsEnumerable, + unshift = arrayRef.unshift; + + /** Used to set meta data on functions */ + var defineProperty = (function() { + // IE 8 only accepts DOM elements + try { + var o = {}, + func = isNative(func = Object.defineProperty) && func, + result = func(o, o, o) && func; + } catch(e) { } + return result; + }()); + + /* Native method shortcuts for methods with the same name as other `lodash` methods */ + var nativeCreate = isNative(nativeCreate = Object.create) && nativeCreate, + nativeIsArray = isNative(nativeIsArray = Array.isArray) && nativeIsArray, + nativeKeys = isNative(nativeKeys = Object.keys) && nativeKeys; + + /** Used to lookup a built-in constructor by [[Class]] */ + var ctorByClass = {}; + ctorByClass[arrayClass] = Array; + ctorByClass[boolClass] = Boolean; + ctorByClass[dateClass] = Date; + ctorByClass[funcClass] = Function; + ctorByClass[objectClass] = Object; + ctorByClass[numberClass] = Number; + ctorByClass[regexpClass] = RegExp; + ctorByClass[stringClass] = String; + + /** Used to avoid iterating non-enumerable properties in IE < 9 */ + var nonEnumProps = {}; + nonEnumProps[arrayClass] = nonEnumProps[dateClass] = nonEnumProps[numberClass] = { 'constructor': true, 'toLocaleString': true, 'toString': true, 'valueOf': true }; + nonEnumProps[boolClass] = nonEnumProps[stringClass] = { 'constructor': true, 'toString': true, 'valueOf': true }; + nonEnumProps[errorClass] = nonEnumProps[funcClass] = nonEnumProps[regexpClass] = { 'constructor': true, 'toString': true }; + nonEnumProps[objectClass] = { 'constructor': true }; + + (function() { + var length = shadowedProps.length; + while (length--) { + var key = shadowedProps[length]; + for (var className in nonEnumProps) { + if (hasOwnProperty.call(nonEnumProps, className) && !hasOwnProperty.call(nonEnumProps[className], key)) { + nonEnumProps[className][key] = false; + } + } + } + }()); + + /*--------------------------------------------------------------------------*/ + + /** + * Creates a `lodash` object which wraps the given value to enable intuitive + * method chaining. + * + * In addition to Lo-Dash methods, wrappers also have the following `Array` methods: + * `concat`, `join`, `pop`, `push`, `reverse`, `shift`, `slice`, `sort`, `splice`, + * and `unshift` + * + * Chaining is supported in custom builds as long as the `value` method is + * implicitly or explicitly included in the build. + * + * The chainable wrapper functions are: + * `after`, `assign`, `bind`, `bindAll`, `bindKey`, `chain`, `compact`, + * `compose`, `concat`, `countBy`, `create`, `createCallback`, `curry`, + * `debounce`, `defaults`, `defer`, `delay`, `difference`, `filter`, `flatten`, + * `forEach`, `forEachRight`, `forIn`, `forInRight`, `forOwn`, `forOwnRight`, + * `functions`, `groupBy`, `indexBy`, `initial`, `intersection`, `invert`, + * `invoke`, `keys`, `map`, `max`, `memoize`, `merge`, `min`, `object`, `omit`, + * `once`, `pairs`, `partial`, `partialRight`, `pick`, `pluck`, `pull`, `push`, + * `range`, `reject`, `remove`, `rest`, `reverse`, `shuffle`, `slice`, `sort`, + * `sortBy`, `splice`, `tap`, `throttle`, `times`, `toArray`, `transform`, + * `union`, `uniq`, `unshift`, `unzip`, `values`, `where`, `without`, `wrap`, + * and `zip` + * + * The non-chainable wrapper functions are: + * `clone`, `cloneDeep`, `contains`, `escape`, `every`, `find`, `findIndex`, + * `findKey`, `findLast`, `findLastIndex`, `findLastKey`, `has`, `identity`, + * `indexOf`, `isArguments`, `isArray`, `isBoolean`, `isDate`, `isElement`, + * `isEmpty`, `isEqual`, `isFinite`, `isFunction`, `isNaN`, `isNull`, `isNumber`, + * `isObject`, `isPlainObject`, `isRegExp`, `isString`, `isUndefined`, `join`, + * `lastIndexOf`, `mixin`, `noConflict`, `parseInt`, `pop`, `random`, `reduce`, + * `reduceRight`, `result`, `shift`, `size`, `some`, `sortedIndex`, `runInContext`, + * `template`, `unescape`, `uniqueId`, and `value` + * + * The wrapper functions `first` and `last` return wrapped values when `n` is + * provided, otherwise they return unwrapped values. + * + * Explicit chaining can be enabled by using the `_.chain` method. + * + * @name _ + * @constructor + * @category Chaining + * @param {*} value The value to wrap in a `lodash` instance. + * @returns {Object} Returns a `lodash` instance. + * @example + * + * var wrapped = _([1, 2, 3]); + * + * // returns an unwrapped value + * wrapped.reduce(function(sum, num) { + * return sum + num; + * }); + * // => 6 + * + * // returns a wrapped value + * var squares = wrapped.map(function(num) { + * return num * num; + * }); + * + * _.isArray(squares); + * // => false + * + * _.isArray(squares.value()); + * // => true + */ + function lodash() { + // no operation performed + } + + /** + * An object used to flag environments features. + * + * @static + * @memberOf _ + * @type Object + */ + var support = lodash.support = {}; + + (function() { + var ctor = function() { this.x = 1; }, + object = { '0': 1, 'length': 1 }, + props = []; + + ctor.prototype = { 'valueOf': 1, 'y': 1 }; + for (var key in new ctor) { props.push(key); } + for (key in arguments) { } + + /** + * Detect if an `arguments` object's [[Class]] is resolvable (all but Firefox < 4, IE < 9). + * + * @memberOf _.support + * @type boolean + */ + support.argsClass = toString.call(arguments) == argsClass; + + /** + * Detect if `arguments` objects are `Object` objects (all but Narwhal and Opera < 10.5). + * + * @memberOf _.support + * @type boolean + */ + support.argsObject = arguments.constructor == Object && !(arguments instanceof Array); + + /** + * Detect if `name` or `message` properties of `Error.prototype` are + * enumerable by default. (IE < 9, Safari < 5.1) + * + * @memberOf _.support + * @type boolean + */ + support.enumErrorProps = propertyIsEnumerable.call(errorProto, 'message') || propertyIsEnumerable.call(errorProto, 'name'); + + /** + * Detect if `prototype` properties are enumerable by default. + * + * Firefox < 3.6, Opera > 9.50 - Opera < 11.60, and Safari < 5.1 + * (if the prototype or a property on the prototype has been set) + * incorrectly sets a function's `prototype` property [[Enumerable]] + * value to `true`. + * + * @memberOf _.support + * @type boolean + */ + support.enumPrototypes = propertyIsEnumerable.call(ctor, 'prototype'); + + /** + * Detect if functions can be decompiled by `Function#toString` + * (all but PS3 and older Opera mobile browsers & avoided in Windows 8 apps). + * + * @memberOf _.support + * @type boolean + */ + support.funcDecomp = !isNative(root.WinRTError) && reThis.test(function() { return this; }); + + /** + * Detect if `Function#name` is supported (all but IE). + * + * @memberOf _.support + * @type boolean + */ + support.funcNames = typeof Function.name == 'string'; + + /** + * Detect if `arguments` object indexes are non-enumerable + * (Firefox < 4, IE < 9, PhantomJS, Safari < 5.1). + * + * @memberOf _.support + * @type boolean + */ + support.nonEnumArgs = key != 0; + + /** + * Detect if properties shadowing those on `Object.prototype` are non-enumerable. + * + * In IE < 9 an objects own properties, shadowing non-enumerable ones, are + * made non-enumerable as well (a.k.a the JScript [[DontEnum]] bug). + * + * @memberOf _.support + * @type boolean + */ + support.nonEnumShadows = !/valueOf/.test(props); + + /** + * Detect if `Array#shift` and `Array#splice` augment array-like objects correctly. + * + * Firefox < 10, IE compatibility mode, and IE < 9 have buggy Array `shift()` + * and `splice()` functions that fail to remove the last element, `value[0]`, + * of array-like objects even though the `length` property is set to `0`. + * The `shift()` method is buggy in IE 8 compatibility mode, while `splice()` + * is buggy regardless of mode in IE < 9 and buggy in compatibility mode in IE 9. + * + * @memberOf _.support + * @type boolean + */ + support.spliceObjects = (arrayRef.splice.call(object, 0, 1), !object[0]); + + /** + * Detect lack of support for accessing string characters by index. + * + * IE < 8 can't access characters by index and IE 8 can only access + * characters by index on string literals. + * + * @memberOf _.support + * @type boolean + */ + support.unindexedChars = ('x'[0] + Object('x')[0]) != 'xx'; + + /** + * Detect if a DOM node's [[Class]] is resolvable (all but IE < 9) + * and that the JS engine errors when attempting to coerce an object to + * a string without a `toString` function. + * + * @memberOf _.support + * @type boolean + */ + try { + support.nodeClass = !(toString.call(document) == objectClass && !({ 'toString': 0 } + '')); + } catch(e) { + support.nodeClass = true; + } + }(1)); + + /*--------------------------------------------------------------------------*/ + + /** + * The template used to create iterator functions. + * + * @private + * @param {Object} data The data object used to populate the text. + * @returns {string} Returns the interpolated text. + */ + var iteratorTemplate = function(obj) { + + var __p = 'var index, iterable = ' + + (obj.firstArg) + + ', result = ' + + (obj.init) + + ';\nif (!iterable) return result;\n' + + (obj.top) + + ';'; + if (obj.array) { + __p += '\nvar length = iterable.length; index = -1;\nif (' + + (obj.array) + + ') { '; + if (support.unindexedChars) { + __p += '\n if (isString(iterable)) {\n iterable = iterable.split(\'\')\n } '; + } + __p += '\n while (++index < length) {\n ' + + (obj.loop) + + ';\n }\n}\nelse { '; + } else if (support.nonEnumArgs) { + __p += '\n var length = iterable.length; index = -1;\n if (length && isArguments(iterable)) {\n while (++index < length) {\n index += \'\';\n ' + + (obj.loop) + + ';\n }\n } else { '; + } + + if (support.enumPrototypes) { + __p += '\n var skipProto = typeof iterable == \'function\';\n '; + } + + if (support.enumErrorProps) { + __p += '\n var skipErrorProps = iterable === errorProto || iterable instanceof Error;\n '; + } + + var conditions = []; if (support.enumPrototypes) { conditions.push('!(skipProto && index == "prototype")'); } if (support.enumErrorProps) { conditions.push('!(skipErrorProps && (index == "message" || index == "name"))'); } + + if (obj.useHas && obj.keys) { + __p += '\n var ownIndex = -1,\n ownProps = objectTypes[typeof iterable] && keys(iterable),\n length = ownProps ? ownProps.length : 0;\n\n while (++ownIndex < length) {\n index = ownProps[ownIndex];\n'; + if (conditions.length) { + __p += ' if (' + + (conditions.join(' && ')) + + ') {\n '; + } + __p += + (obj.loop) + + '; '; + if (conditions.length) { + __p += '\n }'; + } + __p += '\n } '; + } else { + __p += '\n for (index in iterable) {\n'; + if (obj.useHas) { conditions.push("hasOwnProperty.call(iterable, index)"); } if (conditions.length) { + __p += ' if (' + + (conditions.join(' && ')) + + ') {\n '; + } + __p += + (obj.loop) + + '; '; + if (conditions.length) { + __p += '\n }'; + } + __p += '\n } '; + if (support.nonEnumShadows) { + __p += '\n\n if (iterable !== objectProto) {\n var ctor = iterable.constructor,\n isProto = iterable === (ctor && ctor.prototype),\n className = iterable === stringProto ? stringClass : iterable === errorProto ? errorClass : toString.call(iterable),\n nonEnum = nonEnumProps[className];\n '; + for (k = 0; k < 7; k++) { + __p += '\n index = \'' + + (obj.shadowedProps[k]) + + '\';\n if ((!(isProto && nonEnum[index]) && hasOwnProperty.call(iterable, index))'; + if (!obj.useHas) { + __p += ' || (!nonEnum[index] && iterable[index] !== objectProto[index])'; + } + __p += ') {\n ' + + (obj.loop) + + ';\n } '; + } + __p += '\n } '; + } + + } + + if (obj.array || support.nonEnumArgs) { + __p += '\n}'; + } + __p += + (obj.bottom) + + ';\nreturn result'; + + return __p + }; + + /*--------------------------------------------------------------------------*/ + + /** + * The base implementation of `_.bind` that creates the bound function and + * sets its meta data. + * + * @private + * @param {Array} bindData The bind data array. + * @returns {Function} Returns the new bound function. + */ + function baseBind(bindData) { + var func = bindData[0], + partialArgs = bindData[2], + thisArg = bindData[4]; + + function bound() { + // `Function#bind` spec + // http://es5.github.io/#x15.3.4.5 + if (partialArgs) { + // avoid `arguments` object deoptimizations by using `slice` instead + // of `Array.prototype.slice.call` and not assigning `arguments` to a + // variable as a ternary expression + var args = slice(partialArgs); + push.apply(args, arguments); + } + // mimic the constructor's `return` behavior + // http://es5.github.io/#x13.2.2 + if (this instanceof bound) { + // ensure `new bound` is an instance of `func` + var thisBinding = baseCreate(func.prototype), + result = func.apply(thisBinding, args || arguments); + return isObject(result) ? result : thisBinding; + } + return func.apply(thisArg, args || arguments); + } + setBindData(bound, bindData); + return bound; + } + + /** + * The base implementation of `_.clone` without argument juggling or support + * for `thisArg` binding. + * + * @private + * @param {*} value The value to clone. + * @param {boolean} [isDeep=false] Specify a deep clone. + * @param {Function} [callback] The function to customize cloning values. + * @param {Array} [stackA=[]] Tracks traversed source objects. + * @param {Array} [stackB=[]] Associates clones with source counterparts. + * @returns {*} Returns the cloned value. + */ + function baseClone(value, isDeep, callback, stackA, stackB) { + if (callback) { + var result = callback(value); + if (typeof result != 'undefined') { + return result; + } + } + // inspect [[Class]] + var isObj = isObject(value); + if (isObj) { + var className = toString.call(value); + if (!cloneableClasses[className] || (!support.nodeClass && isNode(value))) { + return value; + } + var ctor = ctorByClass[className]; + switch (className) { + case boolClass: + case dateClass: + return new ctor(+value); + + case numberClass: + case stringClass: + return new ctor(value); + + case regexpClass: + result = ctor(value.source, reFlags.exec(value)); + result.lastIndex = value.lastIndex; + return result; + } + } else { + return value; + } + var isArr = isArray(value); + if (isDeep) { + // check for circular references and return corresponding clone + var initedStack = !stackA; + stackA || (stackA = getArray()); + stackB || (stackB = getArray()); + + var length = stackA.length; + while (length--) { + if (stackA[length] == value) { + return stackB[length]; + } + } + result = isArr ? ctor(value.length) : {}; + } + else { + result = isArr ? slice(value) : assign({}, value); + } + // add array properties assigned by `RegExp#exec` + if (isArr) { + if (hasOwnProperty.call(value, 'index')) { + result.index = value.index; + } + if (hasOwnProperty.call(value, 'input')) { + result.input = value.input; + } + } + // exit for shallow clone + if (!isDeep) { + return result; + } + // add the source value to the stack of traversed objects + // and associate it with its clone + stackA.push(value); + stackB.push(result); + + // recursively populate clone (susceptible to call stack limits) + (isArr ? baseEach : forOwn)(value, function(objValue, key) { + result[key] = baseClone(objValue, isDeep, callback, stackA, stackB); + }); + + if (initedStack) { + releaseArray(stackA); + releaseArray(stackB); + } + return result; + } + + /** + * The base implementation of `_.create` without support for assigning + * properties to the created object. + * + * @private + * @param {Object} prototype The object to inherit from. + * @returns {Object} Returns the new object. + */ + function baseCreate(prototype, properties) { + return isObject(prototype) ? nativeCreate(prototype) : {}; + } + // fallback for browsers without `Object.create` + if (!nativeCreate) { + baseCreate = (function() { + function Object() {} + return function(prototype) { + if (isObject(prototype)) { + Object.prototype = prototype; + var result = new Object; + Object.prototype = null; + } + return result || root.Object(); + }; + }()); + } + + /** + * The base implementation of `_.createCallback` without support for creating + * "_.pluck" or "_.where" style callbacks. + * + * @private + * @param {*} [func=identity] The value to convert to a callback. + * @param {*} [thisArg] The `this` binding of the created callback. + * @param {number} [argCount] The number of arguments the callback accepts. + * @returns {Function} Returns a callback function. + */ + function baseCreateCallback(func, thisArg, argCount) { + if (typeof func != 'function') { + return identity; + } + // exit early for no `thisArg` or already bound by `Function#bind` + if (typeof thisArg == 'undefined' || !('prototype' in func)) { + return func; + } + var bindData = func.__bindData__; + if (typeof bindData == 'undefined') { + if (support.funcNames) { + bindData = !func.name; + } + bindData = bindData || !support.funcDecomp; + if (!bindData) { + var source = fnToString.call(func); + if (!support.funcNames) { + bindData = !reFuncName.test(source); + } + if (!bindData) { + // checks if `func` references the `this` keyword and stores the result + bindData = reThis.test(source); + setBindData(func, bindData); + } + } + } + // exit early if there are no `this` references or `func` is bound + if (bindData === false || (bindData !== true && bindData[1] & 1)) { + return func; + } + switch (argCount) { + case 1: return function(value) { + return func.call(thisArg, value); + }; + case 2: return function(a, b) { + return func.call(thisArg, a, b); + }; + case 3: return function(value, index, collection) { + return func.call(thisArg, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(thisArg, accumulator, value, index, collection); + }; + } + return bind(func, thisArg); + } + + /** + * The base implementation of `createWrapper` that creates the wrapper and + * sets its meta data. + * + * @private + * @param {Array} bindData The bind data array. + * @returns {Function} Returns the new function. + */ + function baseCreateWrapper(bindData) { + var func = bindData[0], + bitmask = bindData[1], + partialArgs = bindData[2], + partialRightArgs = bindData[3], + thisArg = bindData[4], + arity = bindData[5]; + + var isBind = bitmask & 1, + isBindKey = bitmask & 2, + isCurry = bitmask & 4, + isCurryBound = bitmask & 8, + key = func; + + function bound() { + var thisBinding = isBind ? thisArg : this; + if (partialArgs) { + var args = slice(partialArgs); + push.apply(args, arguments); + } + if (partialRightArgs || isCurry) { + args || (args = slice(arguments)); + if (partialRightArgs) { + push.apply(args, partialRightArgs); + } + if (isCurry && args.length < arity) { + bitmask |= 16 & ~32; + return baseCreateWrapper([func, (isCurryBound ? bitmask : bitmask & ~3), args, null, thisArg, arity]); + } + } + args || (args = arguments); + if (isBindKey) { + func = thisBinding[key]; + } + if (this instanceof bound) { + thisBinding = baseCreate(func.prototype); + var result = func.apply(thisBinding, args); + return isObject(result) ? result : thisBinding; + } + return func.apply(thisBinding, args); + } + setBindData(bound, bindData); + return bound; + } + + /** + * The base implementation of `_.isEqual`, without support for `thisArg` binding, + * that allows partial "_.where" style comparisons. + * + * @private + * @param {*} a The value to compare. + * @param {*} b The other value to compare. + * @param {Function} [callback] The function to customize comparing values. + * @param {Function} [isWhere=false] A flag to indicate performing partial comparisons. + * @param {Array} [stackA=[]] Tracks traversed `a` objects. + * @param {Array} [stackB=[]] Tracks traversed `b` objects. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + */ + function baseIsEqual(a, b, callback, isWhere, stackA, stackB) { + // used to indicate that when comparing objects, `a` has at least the properties of `b` + if (callback) { + var result = callback(a, b); + if (typeof result != 'undefined') { + return !!result; + } + } + // exit early for identical values + if (a === b) { + // treat `+0` vs. `-0` as not equal + return a !== 0 || (1 / a == 1 / b); + } + var type = typeof a, + otherType = typeof b; + + // exit early for unlike primitive values + if (a === a && + !(a && objectTypes[type]) && + !(b && objectTypes[otherType])) { + return false; + } + // exit early for `null` and `undefined` avoiding ES3's Function#call behavior + // http://es5.github.io/#x15.3.4.4 + if (a == null || b == null) { + return a === b; + } + // compare [[Class]] names + var className = toString.call(a), + otherClass = toString.call(b); + + if (className == argsClass) { + className = objectClass; + } + if (otherClass == argsClass) { + otherClass = objectClass; + } + if (className != otherClass) { + return false; + } + switch (className) { + case boolClass: + case dateClass: + // coerce dates and booleans to numbers, dates to milliseconds and booleans + // to `1` or `0` treating invalid dates coerced to `NaN` as not equal + return +a == +b; + + case numberClass: + // treat `NaN` vs. `NaN` as equal + return (a != +a) + ? b != +b + // but treat `+0` vs. `-0` as not equal + : (a == 0 ? (1 / a == 1 / b) : a == +b); + + case regexpClass: + case stringClass: + // coerce regexes to strings (http://es5.github.io/#x15.10.6.4) + // treat string primitives and their corresponding object instances as equal + return a == String(b); + } + var isArr = className == arrayClass; + if (!isArr) { + // unwrap any `lodash` wrapped values + var aWrapped = hasOwnProperty.call(a, '__wrapped__'), + bWrapped = hasOwnProperty.call(b, '__wrapped__'); + + if (aWrapped || bWrapped) { + return baseIsEqual(aWrapped ? a.__wrapped__ : a, bWrapped ? b.__wrapped__ : b, callback, isWhere, stackA, stackB); + } + // exit for functions and DOM nodes + if (className != objectClass || (!support.nodeClass && (isNode(a) || isNode(b)))) { + return false; + } + // in older versions of Opera, `arguments` objects have `Array` constructors + var ctorA = !support.argsObject && isArguments(a) ? Object : a.constructor, + ctorB = !support.argsObject && isArguments(b) ? Object : b.constructor; + + // non `Object` object instances with different constructors are not equal + if (ctorA != ctorB && + !(isFunction(ctorA) && ctorA instanceof ctorA && isFunction(ctorB) && ctorB instanceof ctorB) && + ('constructor' in a && 'constructor' in b) + ) { + return false; + } + } + // assume cyclic structures are equal + // the algorithm for detecting cyclic structures is adapted from ES 5.1 + // section 15.12.3, abstract operation `JO` (http://es5.github.io/#x15.12.3) + var initedStack = !stackA; + stackA || (stackA = getArray()); + stackB || (stackB = getArray()); + + var length = stackA.length; + while (length--) { + if (stackA[length] == a) { + return stackB[length] == b; + } + } + var size = 0; + result = true; + + // add `a` and `b` to the stack of traversed objects + stackA.push(a); + stackB.push(b); + + // recursively compare objects and arrays (susceptible to call stack limits) + if (isArr) { + // compare lengths to determine if a deep comparison is necessary + length = a.length; + size = b.length; + result = size == length; + + if (result || isWhere) { + // deep compare the contents, ignoring non-numeric properties + while (size--) { + var index = length, + value = b[size]; + + if (isWhere) { + while (index--) { + if ((result = baseIsEqual(a[index], value, callback, isWhere, stackA, stackB))) { + break; + } + } + } else if (!(result = baseIsEqual(a[size], value, callback, isWhere, stackA, stackB))) { + break; + } + } + } + } + else { + // deep compare objects using `forIn`, instead of `forOwn`, to avoid `Object.keys` + // which, in this case, is more costly + forIn(b, function(value, key, b) { + if (hasOwnProperty.call(b, key)) { + // count the number of properties. + size++; + // deep compare each property value. + return (result = hasOwnProperty.call(a, key) && baseIsEqual(a[key], value, callback, isWhere, stackA, stackB)); + } + }); + + if (result && !isWhere) { + // ensure both objects have the same number of properties + forIn(a, function(value, key, a) { + if (hasOwnProperty.call(a, key)) { + // `size` will be `-1` if `a` has more properties than `b` + return (result = --size > -1); + } + }); + } + } + stackA.pop(); + stackB.pop(); + + if (initedStack) { + releaseArray(stackA); + releaseArray(stackB); + } + return result; + } + + /** + * Creates a function that, when called, either curries or invokes `func` + * with an optional `this` binding and partially applied arguments. + * + * @private + * @param {Function|string} func The function or method name to reference. + * @param {number} bitmask The bitmask of method flags to compose. + * The bitmask may be composed of the following flags: + * 1 - `_.bind` + * 2 - `_.bindKey` + * 4 - `_.curry` + * 8 - `_.curry` (bound) + * 16 - `_.partial` + * 32 - `_.partialRight` + * @param {Array} [partialArgs] An array of arguments to prepend to those + * provided to the new function. + * @param {Array} [partialRightArgs] An array of arguments to append to those + * provided to the new function. + * @param {*} [thisArg] The `this` binding of `func`. + * @param {number} [arity] The arity of `func`. + * @returns {Function} Returns the new function. + */ + function createWrapper(func, bitmask, partialArgs, partialRightArgs, thisArg, arity) { + var isBind = bitmask & 1, + isBindKey = bitmask & 2, + isCurry = bitmask & 4, + isCurryBound = bitmask & 8, + isPartial = bitmask & 16, + isPartialRight = bitmask & 32; + + if (!isBindKey && !isFunction(func)) { + throw new TypeError; + } + if (isPartial && !partialArgs.length) { + bitmask &= ~16; + isPartial = partialArgs = false; + } + if (isPartialRight && !partialRightArgs.length) { + bitmask &= ~32; + isPartialRight = partialRightArgs = false; + } + var bindData = func && func.__bindData__; + if (bindData && bindData !== true) { + // clone `bindData` + bindData = slice(bindData); + if (bindData[2]) { + bindData[2] = slice(bindData[2]); + } + if (bindData[3]) { + bindData[3] = slice(bindData[3]); + } + // set `thisBinding` is not previously bound + if (isBind && !(bindData[1] & 1)) { + bindData[4] = thisArg; + } + // set if previously bound but not currently (subsequent curried functions) + if (!isBind && bindData[1] & 1) { + bitmask |= 8; + } + // set curried arity if not yet set + if (isCurry && !(bindData[1] & 4)) { + bindData[5] = arity; + } + // append partial left arguments + if (isPartial) { + push.apply(bindData[2] || (bindData[2] = []), partialArgs); + } + // append partial right arguments + if (isPartialRight) { + unshift.apply(bindData[3] || (bindData[3] = []), partialRightArgs); + } + // merge flags + bindData[1] |= bitmask; + return createWrapper.apply(null, bindData); + } + // fast path for `_.bind` + var creater = (bitmask == 1 || bitmask === 17) ? baseBind : baseCreateWrapper; + return creater([func, bitmask, partialArgs, partialRightArgs, thisArg, arity]); + } + + /** + * Creates compiled iteration functions. + * + * @private + * @param {...Object} [options] The compile options object(s). + * @param {string} [options.array] Code to determine if the iterable is an array or array-like. + * @param {boolean} [options.useHas] Specify using `hasOwnProperty` checks in the object loop. + * @param {Function} [options.keys] A reference to `_.keys` for use in own property iteration. + * @param {string} [options.args] A comma separated string of iteration function arguments. + * @param {string} [options.top] Code to execute before the iteration branches. + * @param {string} [options.loop] Code to execute in the object loop. + * @param {string} [options.bottom] Code to execute after the iteration branches. + * @returns {Function} Returns the compiled function. + */ + function createIterator() { + // data properties + iteratorData.shadowedProps = shadowedProps; + + // iterator options + iteratorData.array = iteratorData.bottom = iteratorData.loop = iteratorData.top = ''; + iteratorData.init = 'iterable'; + iteratorData.useHas = true; + + // merge options into a template data object + for (var object, index = 0; object = arguments[index]; index++) { + for (var key in object) { + iteratorData[key] = object[key]; + } + } + var args = iteratorData.args; + iteratorData.firstArg = /^[^,]+/.exec(args)[0]; + + // create the function factory + var factory = Function( + 'baseCreateCallback, errorClass, errorProto, hasOwnProperty, ' + + 'indicatorObject, isArguments, isArray, isString, keys, objectProto, ' + + 'objectTypes, nonEnumProps, stringClass, stringProto, toString', + 'return function(' + args + ') {\n' + iteratorTemplate(iteratorData) + '\n}' + ); + + // return the compiled function + return factory( + baseCreateCallback, errorClass, errorProto, hasOwnProperty, + indicatorObject, isArguments, isArray, isString, iteratorData.keys, objectProto, + objectTypes, nonEnumProps, stringClass, stringProto, toString + ); + } + + /** + * Checks if `value` is a native function. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a native function, else `false`. + */ + function isNative(value) { + return typeof value == 'function' && reNative.test(value); + } + + /** + * Sets `this` binding data on a given function. + * + * @private + * @param {Function} func The function to set data on. + * @param {Array} value The data array to set. + */ + var setBindData = !defineProperty ? noop : function(func, value) { + descriptor.value = value; + defineProperty(func, '__bindData__', descriptor); + }; + + /*--------------------------------------------------------------------------*/ + + /** + * Checks if `value` is an `arguments` object. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is an `arguments` object, else `false`. + * @example + * + * (function() { return _.isArguments(arguments); })(1, 2, 3); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ + function isArguments(value) { + return value && typeof value == 'object' && typeof value.length == 'number' && + toString.call(value) == argsClass || false; + } + // fallback for browsers that can't detect `arguments` objects by [[Class]] + if (!support.argsClass) { + isArguments = function(value) { + return value && typeof value == 'object' && typeof value.length == 'number' && + hasOwnProperty.call(value, 'callee') && !propertyIsEnumerable.call(value, 'callee') || false; + }; + } + + /** + * Checks if `value` is an array. + * + * @static + * @memberOf _ + * @type Function + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is an array, else `false`. + * @example + * + * (function() { return _.isArray(arguments); })(); + * // => false + * + * _.isArray([1, 2, 3]); + * // => true + */ + var isArray = nativeIsArray || function(value) { + return value && typeof value == 'object' && typeof value.length == 'number' && + toString.call(value) == arrayClass || false; + }; + + /** + * A fallback implementation of `Object.keys` which produces an array of the + * given object's own enumerable property names. + * + * @private + * @type Function + * @param {Object} object The object to inspect. + * @returns {Array} Returns an array of property names. + */ + var shimKeys = createIterator({ + 'args': 'object', + 'init': '[]', + 'top': 'if (!(objectTypes[typeof object])) return result', + 'loop': 'result.push(index)' + }); + + /** + * Creates an array composed of the own enumerable property names of an object. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to inspect. + * @returns {Array} Returns an array of property names. + * @example + * + * _.keys({ 'one': 1, 'two': 2, 'three': 3 }); + * // => ['one', 'two', 'three'] (property order is not guaranteed across environments) + */ + var keys = !nativeKeys ? shimKeys : function(object) { + if (!isObject(object)) { + return []; + } + if ((support.enumPrototypes && typeof object == 'function') || + (support.nonEnumArgs && object.length && isArguments(object))) { + return shimKeys(object); + } + return nativeKeys(object); + }; + + /** Reusable iterator options shared by `each`, `forIn`, and `forOwn` */ + var eachIteratorOptions = { + 'args': 'collection, callback, thisArg', + 'top': "callback = callback && typeof thisArg == 'undefined' ? callback : baseCreateCallback(callback, thisArg, 3)", + 'array': "typeof length == 'number'", + 'keys': keys, + 'loop': 'if (callback(iterable[index], index, collection) === false) return result' + }; + + /** Reusable iterator options for `assign` and `defaults` */ + var defaultsIteratorOptions = { + 'args': 'object, source, guard', + 'top': + 'var args = arguments,\n' + + ' argsIndex = 0,\n' + + " argsLength = typeof guard == 'number' ? 2 : args.length;\n" + + 'while (++argsIndex < argsLength) {\n' + + ' iterable = args[argsIndex];\n' + + ' if (iterable && objectTypes[typeof iterable]) {', + 'keys': keys, + 'loop': "if (typeof result[index] == 'undefined') result[index] = iterable[index]", + 'bottom': ' }\n}' + }; + + /** Reusable iterator options for `forIn` and `forOwn` */ + var forOwnIteratorOptions = { + 'top': 'if (!objectTypes[typeof iterable]) return result;\n' + eachIteratorOptions.top, + 'array': false + }; + + /** + * A function compiled to iterate `arguments` objects, arrays, objects, and + * strings consistenly across environments, executing the callback for each + * element in the collection. The callback is bound to `thisArg` and invoked + * with three arguments; (value, index|key, collection). Callbacks may exit + * iteration early by explicitly returning `false`. + * + * @private + * @type Function + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array|Object|string} Returns `collection`. + */ + var baseEach = createIterator(eachIteratorOptions); + + /*--------------------------------------------------------------------------*/ + + /** + * Assigns own enumerable properties of source object(s) to the destination + * object. Subsequent sources will overwrite property assignments of previous + * sources. If a callback is provided it will be executed to produce the + * assigned values. The callback is bound to `thisArg` and invoked with two + * arguments; (objectValue, sourceValue). + * + * @static + * @memberOf _ + * @type Function + * @alias extend + * @category Objects + * @param {Object} object The destination object. + * @param {...Object} [source] The source objects. + * @param {Function} [callback] The function to customize assigning values. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns the destination object. + * @example + * + * _.assign({ 'name': 'fred' }, { 'employer': 'slate' }); + * // => { 'name': 'fred', 'employer': 'slate' } + * + * var defaults = _.partialRight(_.assign, function(a, b) { + * return typeof a == 'undefined' ? b : a; + * }); + * + * var object = { 'name': 'barney' }; + * defaults(object, { 'name': 'fred', 'employer': 'slate' }); + * // => { 'name': 'barney', 'employer': 'slate' } + */ + var assign = createIterator(defaultsIteratorOptions, { + 'top': + defaultsIteratorOptions.top.replace(';', + ';\n' + + "if (argsLength > 3 && typeof args[argsLength - 2] == 'function') {\n" + + ' var callback = baseCreateCallback(args[--argsLength - 1], args[argsLength--], 2);\n' + + "} else if (argsLength > 2 && typeof args[argsLength - 1] == 'function') {\n" + + ' callback = args[--argsLength];\n' + + '}' + ), + 'loop': 'result[index] = callback ? callback(result[index], iterable[index]) : iterable[index]' + }); + + /** + * Creates a clone of `value`. If `isDeep` is `true` nested objects will also + * be cloned, otherwise they will be assigned by reference. If a callback + * is provided it will be executed to produce the cloned values. If the + * callback returns `undefined` cloning will be handled by the method instead. + * The callback is bound to `thisArg` and invoked with one argument; (value). + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to clone. + * @param {boolean} [isDeep=false] Specify a deep clone. + * @param {Function} [callback] The function to customize cloning values. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {*} Returns the cloned value. + * @example + * + * var characters = [ + * { 'name': 'barney', 'age': 36 }, + * { 'name': 'fred', 'age': 40 } + * ]; + * + * var shallow = _.clone(characters); + * shallow[0] === characters[0]; + * // => true + * + * var deep = _.clone(characters, true); + * deep[0] === characters[0]; + * // => false + * + * _.mixin({ + * 'clone': _.partialRight(_.clone, function(value) { + * return _.isElement(value) ? value.cloneNode(false) : undefined; + * }) + * }); + * + * var clone = _.clone(document.body); + * clone.childNodes.length; + * // => 0 + */ + function clone(value, isDeep, callback, thisArg) { + // allows working with "Collections" methods without using their `index` + // and `collection` arguments for `isDeep` and `callback` + if (typeof isDeep != 'boolean' && isDeep != null) { + thisArg = callback; + callback = isDeep; + isDeep = false; + } + return baseClone(value, isDeep, typeof callback == 'function' && baseCreateCallback(callback, thisArg, 1)); + } + + /** + * Iterates over own and inherited enumerable properties of an object, + * executing the callback for each property. The callback is bound to `thisArg` + * and invoked with three arguments; (value, key, object). Callbacks may exit + * iteration early by explicitly returning `false`. + * + * @static + * @memberOf _ + * @type Function + * @category Objects + * @param {Object} object The object to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns `object`. + * @example + * + * function Shape() { + * this.x = 0; + * this.y = 0; + * } + * + * Shape.prototype.move = function(x, y) { + * this.x += x; + * this.y += y; + * }; + * + * _.forIn(new Shape, function(value, key) { + * console.log(key); + * }); + * // => logs 'x', 'y', and 'move' (property order is not guaranteed across environments) + */ + var forIn = createIterator(eachIteratorOptions, forOwnIteratorOptions, { + 'useHas': false + }); + + /** + * Iterates over own enumerable properties of an object, executing the callback + * for each property. The callback is bound to `thisArg` and invoked with three + * arguments; (value, key, object). Callbacks may exit iteration early by + * explicitly returning `false`. + * + * @static + * @memberOf _ + * @type Function + * @category Objects + * @param {Object} object The object to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns `object`. + * @example + * + * _.forOwn({ '0': 'zero', '1': 'one', 'length': 2 }, function(num, key) { + * console.log(key); + * }); + * // => logs '0', '1', and 'length' (property order is not guaranteed across environments) + */ + var forOwn = createIterator(eachIteratorOptions, forOwnIteratorOptions); + + /** + * Checks if `value` is a date. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a date, else `false`. + * @example + * + * _.isDate(new Date); + * // => true + */ + function isDate(value) { + return value && typeof value == 'object' && toString.call(value) == dateClass || false; + } + + /** + * Checks if `value` is empty. Arrays, strings, or `arguments` objects with a + * length of `0` and objects with no own enumerable properties are considered + * "empty". + * + * @static + * @memberOf _ + * @category Objects + * @param {Array|Object|string} value The value to inspect. + * @returns {boolean} Returns `true` if the `value` is empty, else `false`. + * @example + * + * _.isEmpty([1, 2, 3]); + * // => false + * + * _.isEmpty({}); + * // => true + * + * _.isEmpty(''); + * // => true + */ + function isEmpty(value) { + var result = true; + if (!value) { + return result; + } + var className = toString.call(value), + length = value.length; + + if ((className == arrayClass || className == stringClass || + (support.argsClass ? className == argsClass : isArguments(value))) || + (className == objectClass && typeof length == 'number' && isFunction(value.splice))) { + return !length; + } + forOwn(value, function() { + return (result = false); + }); + return result; + } + + /** + * Checks if `value` is a function. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + */ + function isFunction(value) { + return typeof value == 'function'; + } + // fallback for older versions of Chrome and Safari + if (isFunction(/x/)) { + isFunction = function(value) { + return typeof value == 'function' && toString.call(value) == funcClass; + }; + } + + /** + * Checks if `value` is the language type of Object. + * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(1); + * // => false + */ + function isObject(value) { + // check if the value is the ECMAScript language type of Object + // http://es5.github.io/#x8 + // and avoid a V8 bug + // http://code.google.com/p/v8/issues/detail?id=2291 + return !!(value && objectTypes[typeof value]); + } + + /** + * Checks if `value` is `null`. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is `null`, else `false`. + * @example + * + * _.isNull(null); + * // => true + * + * _.isNull(undefined); + * // => false + */ + function isNull(value) { + return value === null; + } + + /** + * Checks if `value` is a string. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a string, else `false`. + * @example + * + * _.isString('fred'); + * // => true + */ + function isString(value) { + return typeof value == 'string' || + value && typeof value == 'object' && toString.call(value) == stringClass || false; + } + + /** + * Checks if `value` is `undefined`. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is `undefined`, else `false`. + * @example + * + * _.isUndefined(void 0); + * // => true + */ + function isUndefined(value) { + return typeof value == 'undefined'; + } + + /** + * Creates an object with the same keys as `object` and values generated by + * running each own enumerable property of `object` through the callback. + * The callback is bound to `thisArg` and invoked with three arguments; + * (value, key, object). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new object with values of the results of each `callback` execution. + * @example + * + * _.mapValues({ 'a': 1, 'b': 2, 'c': 3} , function(num) { return num * 3; }); + * // => { 'a': 3, 'b': 6, 'c': 9 } + * + * var characters = { + * 'fred': { 'name': 'fred', 'age': 40 }, + * 'pebbles': { 'name': 'pebbles', 'age': 1 } + * }; + * + * // using "_.pluck" callback shorthand + * _.mapValues(characters, 'age'); + * // => { 'fred': 40, 'pebbles': 1 } + */ + function mapValues(object, callback, thisArg) { + var result = {}; + callback = lodash.createCallback(callback, thisArg, 3); + + forOwn(object, function(value, key, object) { + result[key] = callback(value, key, object); + }); + return result; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Iterates over elements of a collection, returning an array of all elements + * the callback returns truey for. The callback is bound to `thisArg` and + * invoked with three arguments; (value, index|key, collection). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias select + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new array of elements that passed the callback check. + * @example + * + * var evens = _.filter([1, 2, 3, 4, 5, 6], function(num) { return num % 2 == 0; }); + * // => [2, 4, 6] + * + * var characters = [ + * { 'name': 'barney', 'age': 36, 'blocked': false }, + * { 'name': 'fred', 'age': 40, 'blocked': true } + * ]; + * + * // using "_.pluck" callback shorthand + * _.filter(characters, 'blocked'); + * // => [{ 'name': 'fred', 'age': 40, 'blocked': true }] + * + * // using "_.where" callback shorthand + * _.filter(characters, { 'age': 36 }); + * // => [{ 'name': 'barney', 'age': 36, 'blocked': false }] + */ + function filter(collection, callback, thisArg) { + var result = []; + callback = lodash.createCallback(callback, thisArg, 3); + + if (isArray(collection)) { + var index = -1, + length = collection.length; + + while (++index < length) { + var value = collection[index]; + if (callback(value, index, collection)) { + result.push(value); + } + } + } else { + baseEach(collection, function(value, index, collection) { + if (callback(value, index, collection)) { + result.push(value); + } + }); + } + return result; + } + + /** + * Iterates over elements of a collection, returning the first element that + * the callback returns truey for. The callback is bound to `thisArg` and + * invoked with three arguments; (value, index|key, collection). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias detect, findWhere + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {*} Returns the found element, else `undefined`. + * @example + * + * var characters = [ + * { 'name': 'barney', 'age': 36, 'blocked': false }, + * { 'name': 'fred', 'age': 40, 'blocked': true }, + * { 'name': 'pebbles', 'age': 1, 'blocked': false } + * ]; + * + * _.find(characters, function(chr) { + * return chr.age < 40; + * }); + * // => { 'name': 'barney', 'age': 36, 'blocked': false } + * + * // using "_.where" callback shorthand + * _.find(characters, { 'age': 1 }); + * // => { 'name': 'pebbles', 'age': 1, 'blocked': false } + * + * // using "_.pluck" callback shorthand + * _.find(characters, 'blocked'); + * // => { 'name': 'fred', 'age': 40, 'blocked': true } + */ + function find(collection, callback, thisArg) { + callback = lodash.createCallback(callback, thisArg, 3); + + if (isArray(collection)) { + var index = -1, + length = collection.length; + + while (++index < length) { + var value = collection[index]; + if (callback(value, index, collection)) { + return value; + } + } + } else { + var result; + baseEach(collection, function(value, index, collection) { + if (callback(value, index, collection)) { + result = value; + return false; + } + }); + return result; + } + } + + /** + * Iterates over elements of a collection, executing the callback for each + * element. The callback is bound to `thisArg` and invoked with three arguments; + * (value, index|key, collection). Callbacks may exit iteration early by + * explicitly returning `false`. + * + * Note: As with other "Collections" methods, objects with a `length` property + * are iterated like arrays. To avoid this behavior `_.forIn` or `_.forOwn` + * may be used for object iteration. + * + * @static + * @memberOf _ + * @alias each + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array|Object|string} Returns `collection`. + * @example + * + * _([1, 2, 3]).forEach(function(num) { console.log(num); }).join(','); + * // => logs each number and returns '1,2,3' + * + * _.forEach({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { console.log(num); }); + * // => logs each number and returns the object (property order is not guaranteed across environments) + */ + function forEach(collection, callback, thisArg) { + if (callback && typeof thisArg == 'undefined' && isArray(collection)) { + var index = -1, + length = collection.length; + + while (++index < length) { + if (callback(collection[index], index, collection) === false) { + break; + } + } + } else { + baseEach(collection, callback, thisArg); + } + return collection; + } + + /** + * Creates an array of values by running each element in the collection + * through the callback. The callback is bound to `thisArg` and invoked with + * three arguments; (value, index|key, collection). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias collect + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new array of the results of each `callback` execution. + * @example + * + * _.map([1, 2, 3], function(num) { return num * 3; }); + * // => [3, 6, 9] + * + * _.map({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { return num * 3; }); + * // => [3, 6, 9] (property order is not guaranteed across environments) + * + * var characters = [ + * { 'name': 'barney', 'age': 36 }, + * { 'name': 'fred', 'age': 40 } + * ]; + * + * // using "_.pluck" callback shorthand + * _.map(characters, 'name'); + * // => ['barney', 'fred'] + */ + function map(collection, callback, thisArg) { + var index = -1, + length = collection ? collection.length : 0, + result = Array(typeof length == 'number' ? length : 0); + + callback = lodash.createCallback(callback, thisArg, 3); + if (isArray(collection)) { + while (++index < length) { + result[index] = callback(collection[index], index, collection); + } + } else { + baseEach(collection, function(value, key, collection) { + result[++index] = callback(value, key, collection); + }); + } + return result; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Creates an array with all falsey values removed. The values `false`, `null`, + * `0`, `""`, `undefined`, and `NaN` are all falsey. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to compact. + * @returns {Array} Returns a new array of filtered values. + * @example + * + * _.compact([0, 1, false, 2, '', 3]); + * // => [1, 2, 3] + */ + function compact(array) { + var index = -1, + length = array ? array.length : 0, + result = []; + + while (++index < length) { + var value = array[index]; + if (value) { + result.push(value); + } + } + return result; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Creates a function that, when called, invokes `func` with the `this` + * binding of `thisArg` and prepends any additional `bind` arguments to those + * provided to the bound function. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to bind. + * @param {*} [thisArg] The `this` binding of `func`. + * @param {...*} [arg] Arguments to be partially applied. + * @returns {Function} Returns the new bound function. + * @example + * + * var func = function(greeting) { + * return greeting + ' ' + this.name; + * }; + * + * func = _.bind(func, { 'name': 'fred' }, 'hi'); + * func(); + * // => 'hi fred' + */ + function bind(func, thisArg) { + return arguments.length > 2 + ? createWrapper(func, 17, slice(arguments, 2), null, thisArg) + : createWrapper(func, 1, null, null, thisArg); + } + + /*--------------------------------------------------------------------------*/ + + /** + * Produces a callback bound to an optional `thisArg`. If `func` is a property + * name the created callback will return the property value for a given element. + * If `func` is an object the created callback will return `true` for elements + * that contain the equivalent object properties, otherwise it will return `false`. + * + * @static + * @memberOf _ + * @category Utilities + * @param {*} [func=identity] The value to convert to a callback. + * @param {*} [thisArg] The `this` binding of the created callback. + * @param {number} [argCount] The number of arguments the callback accepts. + * @returns {Function} Returns a callback function. + * @example + * + * var characters = [ + * { 'name': 'barney', 'age': 36 }, + * { 'name': 'fred', 'age': 40 } + * ]; + * + * // wrap to create custom callback shorthands + * _.createCallback = _.wrap(_.createCallback, function(func, callback, thisArg) { + * var match = /^(.+?)__([gl]t)(.+)$/.exec(callback); + * return !match ? func(callback, thisArg) : function(object) { + * return match[2] == 'gt' ? object[match[1]] > match[3] : object[match[1]] < match[3]; + * }; + * }); + * + * _.filter(characters, 'age__gt38'); + * // => [{ 'name': 'fred', 'age': 40 }] + */ + function createCallback(func, thisArg, argCount) { + var type = typeof func; + if (func == null || type == 'function') { + return baseCreateCallback(func, thisArg, argCount); + } + // handle "_.pluck" style callback shorthands + if (type != 'object') { + return property(func); + } + var props = keys(func), + key = props[0], + a = func[key]; + + // handle "_.where" style callback shorthands + if (props.length == 1 && a === a && !isObject(a)) { + // fast path the common case of providing an object with a single + // property containing a primitive value + return function(object) { + var b = object[key]; + return a === b && (a !== 0 || (1 / a == 1 / b)); + }; + } + return function(object) { + var length = props.length, + result = false; + + while (length--) { + if (!(result = baseIsEqual(object[props[length]], func[props[length]], null, true))) { + break; + } + } + return result; + }; + } + + /** + * This method returns the first argument provided to it. + * + * @static + * @memberOf _ + * @category Utilities + * @param {*} value Any value. + * @returns {*} Returns `value`. + * @example + * + * var object = { 'name': 'fred' }; + * _.identity(object) === object; + * // => true + */ + function identity(value) { + return value; + } + + /** + * A no-operation function. + * + * @static + * @memberOf _ + * @category Utilities + * @example + * + * var object = { 'name': 'fred' }; + * _.noop(object) === undefined; + * // => true + */ + function noop() { + // no operation performed + } + + /** + * Creates a "_.pluck" style function, which returns the `key` value of a + * given object. + * + * @static + * @memberOf _ + * @category Utilities + * @param {string} key The name of the property to retrieve. + * @returns {Function} Returns the new function. + * @example + * + * var characters = [ + * { 'name': 'fred', 'age': 40 }, + * { 'name': 'barney', 'age': 36 } + * ]; + * + * var getName = _.property('name'); + * + * _.map(characters, getName); + * // => ['barney', 'fred'] + * + * _.sortBy(characters, getName); + * // => [{ 'name': 'barney', 'age': 36 }, { 'name': 'fred', 'age': 40 }] + */ + function property(key) { + return function(object) { + return object[key]; + }; + } + + /*--------------------------------------------------------------------------*/ + + lodash.assign = assign; + lodash.bind = bind; + lodash.compact = compact; + lodash.createCallback = createCallback; + lodash.filter = filter; + lodash.forEach = forEach; + lodash.forIn = forIn; + lodash.forOwn = forOwn; + lodash.keys = keys; + lodash.map = map; + lodash.mapValues = mapValues; + lodash.property = property; + + // add aliases + lodash.collect = map; + lodash.each = forEach; + lodash.extend = assign; + lodash.select = filter; + + /*--------------------------------------------------------------------------*/ + + // add functions that return unwrapped values when chaining + lodash.clone = clone; + lodash.find = find; + lodash.identity = identity; + lodash.isArguments = isArguments; + lodash.isArray = isArray; + lodash.isDate = isDate; + lodash.isEmpty = isEmpty; + lodash.isFunction = isFunction; + lodash.isNull = isNull; + lodash.isObject = isObject; + lodash.isString = isString; + lodash.isUndefined = isUndefined; + lodash.noop = noop; + + lodash.detect = find; + lodash.findWhere = find; + + /*--------------------------------------------------------------------------*/ + + /** + * The semantic version number. + * + * @static + * @memberOf _ + * @type string + */ + lodash.VERSION = '2.4.1'; + + /*--------------------------------------------------------------------------*/ + + if (freeExports && freeModule) { + // in Node.js or RingoJS + if (moduleExports) { + (freeModule.exports = lodash)._ = lodash; + } + + } + +}.call(this)); + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],24:[function(require,module,exports){ +/* + * JavaScript tracker for Snowplow: links.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. + */ + +var lodash = require('./lib_managed/lodash'), + helpers = require('./lib/helpers'), + object = typeof exports !== 'undefined' ? exports : this; + +/** + * Object for handling automatic link tracking + * + * @param object core The tracker core + * @param string trackerId Unique identifier for the tracker instance, used to mark tracked links + * @param function contextAdder Function to add common contexts like PerformanceTiming to all events + * @return object linkTrackingManager instance + */ +object.getLinkTrackingManager = function (core, trackerId, contextAdder) { + + // Filter function used to determine whether clicks on a link should be tracked + var linkTrackingFilter, + + // Whether pseudo clicks are tracked + linkTrackingPseudoClicks, + + // Whether to track the innerHTML of clicked links + linkTrackingContent, + + // The context attached to link click events + linkTrackingContext, + + // Internal state of the pseudo click handler + lastButton, + lastTarget; + + /* + * Process clicks + */ + function processClick(sourceElement, context) { + + var parentElement, + tag, + elementId, + elementClasses, + elementTarget, + elementContent; + + while ((parentElement = sourceElement.parentNode) !== null && + !lodash.isUndefined(parentElement) && // buggy IE5.5 + ((tag = sourceElement.tagName.toUpperCase()) !== 'A' && tag !== 'AREA')) { + sourceElement = parentElement; + } + + if (!lodash.isUndefined(sourceElement.href)) { + // browsers, such as Safari, don't downcase hostname and href + var originalSourceHostName = sourceElement.hostname || helpers.getHostName(sourceElement.href), + sourceHostName = originalSourceHostName.toLowerCase(), + sourceHref = sourceElement.href.replace(originalSourceHostName, sourceHostName), + scriptProtocol = new RegExp('^(javascript|vbscript|jscript|mocha|livescript|ecmascript|mailto):', 'i'); + + // Ignore script pseudo-protocol links + if (!scriptProtocol.test(sourceHref)) { + + elementId = sourceElement.id; + elementClasses = helpers.getCssClasses(sourceElement); + elementTarget = sourceElement.target; + elementContent = linkTrackingContent ? sourceElement.innerHTML : null; + + // decodeUrl %xx + sourceHref = unescape(sourceHref); + core.trackLinkClick(sourceHref, elementId, elementClasses, elementTarget, elementContent, contextAdder(context)); + } + } + } + + /* + * Return function to handle click event + */ + function getClickHandler(context) { + return function (evt) { + var button, + target; + + evt = evt || window.event; + button = evt.which || evt.button; + target = evt.target || evt.srcElement; + + // Using evt.type (added in IE4), we avoid defining separate handlers for mouseup and mousedown. + if (evt.type === 'click') { + if (target) { + processClick(target, context); + } + } else if (evt.type === 'mousedown') { + if ((button === 1 || button === 2) && target) { + lastButton = button; + lastTarget = target; + } else { + lastButton = lastTarget = null; + } + } else if (evt.type === 'mouseup') { + if (button === lastButton && target === lastTarget) { + processClick(target, context); + } + lastButton = lastTarget = null; + } + }; + } + + /* + * Add click listener to a DOM element + */ + function addClickListener(element) { + if (linkTrackingPseudoClicks) { + // for simplicity and performance, we ignore drag events + helpers.addEventListener(element, 'mouseup', getClickHandler(linkTrackingContext), false); + helpers.addEventListener(element, 'mousedown', getClickHandler(linkTrackingContext), false); + } else { + helpers.addEventListener(element, 'click', getClickHandler(linkTrackingContext), false); + } + } + + + + /* + * Check whether a set of classes contains any of the classes of a link element + * Used to determine whether clicks on that link should be tracked + */ + function checkLink(linkElement, specifiedClasses) { + var linkClasses = helpers.getCssClasses(linkElement), + i; + + for (i = 0; i < linkClasses.length; i++) { + if (specifiedClasses[linkClasses[i]]) { + return true; + } + } + return false; + } + + return { + + /* + * Configures link click tracking: how to filter which links will be tracked, + * whether to use pseudo click tracking, and what context to attach to link_click events + */ + configureLinkClickTracking: function (criterion, pseudoClicks, trackContent, context) { + linkTrackingContent = trackContent; + linkTrackingContext = context; + linkTrackingPseudoClicks = pseudoClicks; + linkTrackingFilter = helpers.getFilter(criterion, true); + }, + + /* + * Add click handlers to anchor and AREA elements, except those to be ignored + */ + addClickListeners: function () { + + var linkElements = document.links, + i; + + for (i = 0; i < linkElements.length; i++) { + // Add a listener to link elements which pass the filter and aren't already tracked + if (linkTrackingFilter(linkElements[i]) && !linkElements[i][trackerId]) { + addClickListener(linkElements[i]); + linkElements[i][trackerId] = true; + } + } + } + }; +}; + +},{"./lib/helpers":21,"./lib_managed/lodash":23}],25:[function(require,module,exports){ +/* + * JavaScript tracker for Snowplow: out_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. + * + * * 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 + json2 = require('JSON'), + lodash = require('./lib_managed/lodash'), + localStorageAccessible = require('./lib/detectors').localStorageAccessible, + helpers = require('./lib/helpers'), + object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support + + /** + * 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. + * @param int maxPostBytes Maximum combined size in bytes of the event JSONs in a POST request + * @return object OutQueueManager instance + */ + object.OutQueueManager = function (functionName, namespace, mutSnowplowState, useLocalStorage, usePost, bufferSize, maxPostBytes) { + 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 ? 'post2' : 'get'].join('_'); + + if (useLocalStorage) { + // Catch any JSON parse errors or localStorage that might be thrown + try { + // TODO: backward compatibility with the old version of the queue for POST requests + 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 && bufferSize > 1) { + mutSnowplowState.bufferFlushers.push(function () { + if (!executingQueue) { + 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) { + var cleanedRequest = lodash.mapValues(request, function (v) { + return v.toString(); + }); + return { + evt: cleanedRequest, + bytes: getUTF8Length(json2.stringify(cleanedRequest)) + }; + } + + /** + * Count the number of bytes a string will occupy when UTF-8 encoded + * Taken from http://stackoverflow.com/questions/2848462/count-bytes-in-textarea-using-javascript/ + * + * @param string s + * @return number Length of s in bytes when UTF-8 encoded + */ + function getUTF8Length(s) { + var len = 0; + for (var i = 0; i < s.length; i++) { + var code = s.charCodeAt(i); + if (code <= 0x7f) { + len += 1; + } else if (code <= 0x7ff) { + len += 2; + } else if (code >= 0xd800 && code <= 0xdfff) { + // Surrogate pair: These take 4 bytes in UTF-8 and 2 chars in UCS-2 + // (Assume next char is the other [valid] half and just skip it) + len += 4; i++; + } else if (code < 0xffff) { + len += 3; + } else { + len += 4; + } + } + return len; + } + + /* + * Queue an image beacon for submission to the collector. + * If we're not processing the queue, we'll start. + */ + function enqueueRequest (request, url) { + + configCollectorUrl = url + path; + if (usePost) { + var body = getBody(request); + if (body.bytes >= maxPostBytes) { + helpers.warn("Event of size " + body.bytes + " is too long - the maximum size is " + maxPostBytes); + var xhr = initializeXMLHttpRequest(configCollectorUrl); + xhr.send(encloseInPayloadDataEnvelope([body.evt])); + return; + } else { + outQueue.push(body); + } + } else { + outQueue.push(getQuerystring(request)); + } + var savedToLocalStorage = false; + if (useLocalStorage) { + savedToLocalStorage = helpers.attemptWriteLocalStorage(queueName, json2.stringify(outQueue)); + } + + if (!executingQueue && (!savedToLocalStorage || 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 = initializeXMLHttpRequest(configCollectorUrl); + + // Time out POST requests after 5 seconds + var xhrTimeout = setTimeout(function () { + xhr.abort(); + executingQueue = false; + }, 5000); + + function chooseHowManyToExecute(q) { + var numberToSend = 0; + var byteCount = 0; + while (numberToSend < q.length) { + byteCount += q[numberToSend].bytes; + if (byteCount >= maxPostBytes) { + break; + } else { + numberToSend += 1; + } + } + return numberToSend; + } + + // Keep track of number of events to delete from queue + var numberToSend = chooseHowManyToExecute(outQueue); + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 400) { + for (var deleteCount = 0; deleteCount < numberToSend; deleteCount++) { + outQueue.shift(); + } + if (useLocalStorage) { + helpers.attemptWriteLocalStorage(queueName, json2.stringify(outQueue)); + } + clearTimeout(xhrTimeout); + executeQueue(); + } else if (xhr.readyState === 4 && xhr.status >= 400) { + clearTimeout(xhrTimeout); + executingQueue = false; + } + }; + + var batch = lodash.map(outQueue.slice(0, numberToSend), function (x) { + return x.evt; + }); + if (batch.length > 0) { + xhr.send(encloseInPayloadDataEnvelope(batch)); + } + + } else { + + var image = new Image(1, 1); + + image.onload = function () { + outQueue.shift(); + if (useLocalStorage) { + helpers.attemptWriteLocalStorage(queueName, json2.stringify(outQueue)); + } + executeQueue(); + }; + + image.onerror = function () { + executingQueue = false; + }; + + image.src = configCollectorUrl + nextRequest; + } + } + + /** + * Open an XMLHttpRequest for a given endpoint with the correct credentials and header + * + * @param string url The destination URL + * @return object The XMLHttpRequest + */ + function initializeXMLHttpRequest(url) { + var xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); + xhr.withCredentials = true; + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + return xhr; + } + + /** + * Enclose an array of events in a self-describing payload_data JSON string + * + * @param array events Batch of events + * @return string payload_data self-describing JSON + */ + function encloseInPayloadDataEnvelope(events) { + return json2.stringify({ + schema: 'iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-3', + data: events + }); + } + + return { + enqueueRequest: enqueueRequest, + executeQueue: executeQueue + }; + }; + +}()); + +},{"./lib/detectors":20,"./lib/helpers":21,"./lib_managed/lodash":23,"JSON":1}],26:[function(require,module,exports){ +/* + * 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. + */ + +/*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); + }; + +}()); + +},{"./in_queue":18,"./lib/helpers":21,"./lib_managed/lodash":23,"./tracker":27}],27:[function(require,module,exports){ +/* + * 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. + * + * * 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 + 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'), + uuid = require('uuid'), + + 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 pageViewId ID for the current page view, to be attached to all events in the web_page context + * + * @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. sessionCookieTimeout, 1800 + * 14. contexts, {} + * 15. post, false + * 16. bufferSize, 1 + * 17. crossDomainLinker, false + * 18. maxPostBytes, 40000 + */ + object.Tracker = function Tracker(functionName, namespace, version, pageViewId, 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], + + customReferrer, + + 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, + + // Site ID + configTrackerSiteId = argmap.hasOwnProperty('appId') ? argmap.appId : '', // Updated for Snowplow + + // Document URL + configCustomUrl, + + // Document title + lastDocumentTitle = documentAlias.title, + + // Custom title + lastConfigTitle, + + // 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, + + // Recurring heart beat after initial ping (in milliseconds) + configHeartBeatTimer, + + // 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 domain + // User agent defaults to origin hostname + configCookieDomain = argmap.hasOwnProperty('cookieDomain') ? argmap.cookieDomain : null, + + // First-party cookie path + // Default is user agent defined. + configCookiePath = '/', + + // Do Not Track browser feature + dnt = navigatorAlias.doNotTrack || navigatorAlias.msDoNotTrack, + + // Do Not Track + configDoNotTrack = argmap.hasOwnProperty('respectDoNotTrack') ? argmap.respectDoNotTrack && (dnt === 'yes' || dnt === '1') : false, + + // Count sites which are pre-rendered + configCountPreRendered, + + // Life of the visitor cookie (in seconds) + configVisitorCookieTimeout = 63072000, // 2 years + + // Life of the session cookie (in seconds) + configSessionCookieTimeout = argmap.hasOwnProperty('sessionCookieTimeout') ? argmap.sessionCookieTimeout : 1800, // 30 minutes + + // Default hash seed for MurmurHash3 in detectors.detectSignature + configUserFingerprintHashSeed = argmap.hasOwnProperty('userFingerprintSeed') ? argmap.userFingerprintSeed : 123412414, + + // 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, + + // 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, + + // 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')), + + // 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, + + // Guard against installing the activity tracker more than once per Tracker instance + activityTrackingInstalled = false, + + // Last activity timestamp + lastActivityTime, + + // The last time an event was fired on the page - used to invalidate session if cookies are disabled + lastEventTime = new Date().getTime(), + + // How are we scrolling? + minXOffset, + maxXOffset, + minYOffset, + maxYOffset, + + // Hash function + hash = sha1, + + // Domain hash value + domainHash, + + // Domain unique user ID + domainUserId, + + // ID for the current session + memorizedSessionId, + + // Index for the current session - kept in memory in case cookies are disabled + memorizedVisitCount = 1, + + // Business-defined unique user ID + businessUserId, + + // 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, addCommonContexts), + + // Manager for automatic form tracking + formTrackingManager = forms.getFormTrackingManager(core, trackerId, addCommonContexts), + + // Manager for local storage queue + outQueueManager = new requestQueue.OutQueueManager( + functionName, + namespace, + mutSnowplowState, + useLocalStorage, + argmap.post, + argmap.bufferSize, + argmap.maxPostBytes || 40000), + + // 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.webPage) { + commonContexts.push(getWebPageContext()); + } + + 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]); + } + } + } + + /** + * Recalculate the domain, URL, and referrer + */ + function refreshUrl() { + locationArray = proxies.fixupUrl(documentAlias.domain, windowAlias.location.href, helpers.getReferrer()); + + // If this is a single-page app and the page URL has changed, then: + // - if the new URL's querystring contains a "refer(r)er" parameter, use it as the referrer + // - otherwise use the old URL as the referer + if (locationArray[1] !== locationHrefAlias) { + configReferrerUrl = helpers.getReferrer(locationHrefAlias); + } + + domainAlias = helpers.fixupDomain(locationArray[0]); + locationHrefAlias = locationArray[1]; + } + + /** + * Decorate the querystring of a single link + * + * @param event e The event targeting the link + */ + function linkDecorationHandler(e) { + var tstamp = new Date().getTime(); + var initialQsParams = '_sp=' + domainUserId + '.' + tstamp; + if (this.href) { + this.href = helpers.decorateQuerystring(this.href, '_sp', domainUserId + '.' + tstamp); + } + } + + /** + * Enable querystring decoration for links pasing a filter + * Whenever such a link is clicked on or navigated to via the keyboard, + * add "_sp={{duid}}.{{timestamp}}" to its querystring + * + * @param function crossDomainLinker Function used to determine which links to decorate + */ + function decorateLinks(crossDomainLinker) { + for (var i=0; i= 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() { + refreshUrl(); + 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 or renews the session cookie + */ + function setSessionCookie() { + cookie.cookie(getSnowplowCookieName('ses'), '*', configSessionCookieTimeout, configCookiePath, configCookieDomain); + } + + /* + * 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, sessionId) { + cookie.cookie( + getSnowplowCookieName('id'), + _domainUserId + '.' + createTs + '.' + visitCount + '.' + nowTs + '.' + lastVisitTs + '.' + sessionId, + configVisitorCookieTimeout, + configCookiePath, + configCookieDomain); + } + + /** + * Generate a pseudo-unique ID to fingerprint this user + * Note: this isn't a RFC4122-compliant UUID + */ + function createNewDomainUserId() { + return hash( + (navigatorAlias.userAgent || '') + + (navigatorAlias.platform || '') + + json2.stringify(browserFeatures) + Math.round(new Date().getTime() / 1000) + ).slice(0, 16); // 16 hexits = 64 bits + } + + /* + * Load the domain user ID and the session ID + * Set the cookies (if cookies are enabled) + */ + function initializeIdsAndCookies() { + var sesCookieSet = configUseCookies && !!getSnowplowCookieValue('ses'); + var idCookieComponents = loadDomainUserIdCookie(); + + if (idCookieComponents[1]) { + domainUserId = idCookieComponents[1]; + } else { + domainUserId = createNewDomainUserId(); + idCookieComponents[1] = domainUserId; + } + + memorizedSessionId = idCookieComponents[6]; + + if (!sesCookieSet) { + + // Increment the session ID + idCookieComponents[3] ++; + + // Create a new sessionId + memorizedSessionId = uuid.v4(); + idCookieComponents[6] = memorizedSessionId; + // Set lastVisitTs to currentVisitTs + idCookieComponents[5] = idCookieComponents[4]; + } + + if (configUseCookies) { + setSessionCookie(); + // Update currentVisitTs + idCookieComponents[4] = Math.round(new Date().getTime() / 1000); + idCookieComponents.shift(); + setDomainUserIdCookie.apply(null, idCookieComponents); + } + } + + /* + * 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 { + + 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 + '' + ]; + } + + if (!tmpContainer[6]) { + tmpContainer[6] = uuid.v4(); + } + + 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(), + cookiesDisabled = id[0], + _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], + sessionIdFromCookie = id[6]; + + if (configDoNotTrack && configUseCookies) { + cookie.cookie(idname, '', -1, configCookiePath, configCookieDomain); + cookie.cookie(sesname, '', -1, configCookiePath, configCookieDomain); + return; + } + + // If cookies are enabled, base visit count and session ID on the cookies + if (cookiesDisabled === '0') { + memorizedSessionId = sessionIdFromCookie; + + // New session? + if (!ses && configUseCookies) { + // New session (aka new visit) + visitCount++; + // Update the last visit timestamp + lastVisitTs = currentVisitTs; + // Regenerate the session ID + memorizedSessionId = uuid.v4(); + } + + memorizedVisitCount = visitCount; + + // Otherwise, a new session starts if configSessionCookieTimeout seconds have passed since the last event + } else { + if ((new Date().getTime() - lastEventTime) > configSessionCookieTimeout * 1000) { + memorizedSessionId = uuid.v4(); + memorizedVisitCount++; + } + } + + // Build out the rest of the request + sb.add('vp', detectors.detectViewport()); + sb.add('ds', detectors.detectDocumentSize()); + sb.add('vid', memorizedVisitCount); + sb.add('sid', memorizedSessionId); + sb.add('duid', _domainUserId); // Set to our local variable + sb.add('fp', userFingerprint); + sb.add('uid', businessUserId); + + refreshUrl(); + + sb.add('refr', purify(customReferrer || 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) { + setDomainUserIdCookie(_domainUserId, createTs, memorizedVisitCount, nowTs, lastVisitTs, memorizedSessionId); + setSessionCookie(); + } + + lastEventTime = new Date().getTime(); + } + + /** + * 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) { + var combinedContexts = commonContexts.concat(userContexts || []); + if (autoContexts.performanceTiming) { + var performanceTimingContext = getPerformanceTimingContext(); + if (performanceTimingContext) { + combinedContexts.push(performanceTimingContext); + } + } + return combinedContexts; + } + + /** + * Put together a web page context with a unique UUID for the page view + * + * @return object web_page context + */ + function getWebPageContext() { + return { + schema: 'iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0', + data: { + id: pageViewId + } + }; + } + + /** + * 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 + }; + } + + /** + * Combine an array of unchanging contexts with the result of a context-creating function + * + * @param object staticContexts Array of custom contexts + * @param object contextCallback Function returning an array of contexts + */ + function finalizeContexts(staticContexts, contextCallback) { + return (staticContexts || []).concat(contextCallback ? contextCallback() : []); + } + + /** + * 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 + * @param object contextCallback Function returning an array of contexts + */ + function logPageView(customTitle, context, contextCallback) { + + refreshUrl(); + + // So we know what document.title was at the time of trackPageView + lastDocumentTitle = documentAlias.title; + lastConfigTitle = customTitle; + + // Fixup page title + var pageTitle = helpers.fixupTitle(lastConfigTitle || lastDocumentTitle); + + // Log page view + core.trackPageView( + purify(configCustomUrl || locationHrefAlias), + pageTitle, + purify(customReferrer || configReferrerUrl), + addCommonContexts(finalizeContexts(context, contextCallback))); + + // 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(finalizeContexts(context, contextCallback)); // 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 object context Custom context relating to the event + */ + function logPagePing(context) { + refreshUrl(); + newDocumentTitle = documentAlias.title; + if (newDocumentTitle !== lastDocumentTitle) { + lastDocumentTitle = newDocumentTitle; + lastConfigTitle = null; + } + core.trackPagePing( + purify(configCustomUrl || locationHrefAlias), + helpers.fixupTitle(lastConfigTitle || lastDocumentTitle), + purify(customReferrer || 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 (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(); + + initializeIdsAndCookies(); + + if (argmap.crossDomainLinker) { + decorateLinks(argmap.crossDomainLinker); + } + + /************************************************************ + * 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) { + customReferrer = url; + }, + + /** + * Override url + * + * @param string url + */ + setCustomUrl: function (url) { + refreshUrl(); + configCustomUrl = resolveRelativeReference(locationHrefAlias, url); + }, + + /** + * Override document.title + * + * @param string title + */ + setDocumentTitle: function (title) { + // So we know what document.title was at the time of trackPageView + lastDocumentTitle = documentAlias.title; + lastConfigTitle = 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'); + }, + + /** + * Enable querystring decoration for links pasing a filter + * + * @param function crossDomainLinker Function used to determine which links to decorate + */ + crossDomainLinker: function (crossDomainLinkerCriterion) { + decorateLinks(crossDomainLinkerCriterion); + }, + + /** + * 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) { + refreshUrl(); + 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) { + refreshUrl(); + 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 object Custom context relating to the event + * @param object contextCallback Function returning an array of contexts + */ + trackPageView: function (customTitle, context, contextCallback) { + trackCallback(function () { + logPageView(customTitle, context, contextCallback); + }); + }, + + /** + * 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)); + }, + + /** + * Track a timing event (such as the time taken for a resource to load) + * + * @param string category Required. + * @param string variable Required. + * @param number timing Required. + * @param string label Optional. + * @param array context Optional. Context relating to the event. + */ + trackTiming: function (category, variable, timing, label, context) { + core.trackUnstructEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/timing/jsonschema/1-0-0', + data: { + category: category, + variable: variable, + timing: timing, + label: label + } + }, addCommonContexts(context)) + } + }; + }; + +}()); + +},{"./forms":17,"./lib/detectors":20,"./lib/helpers":21,"./lib/proxies":22,"./lib_managed/lodash":23,"./links":24,"./out_queue":25,"JSON":1,"browser-cookie-lite":2,"sha1":7,"snowplow-tracker-core":8,"uuid":16}]},{},[19]); diff --git a/dist/snowplow.js b/dist/snowplow.js new file mode 100644 index 000000000..2fc2ba894 --- /dev/null +++ b/dist/snowplow.js @@ -0,0 +1,8310 @@ +/*! * Snowplow - The world's most powerful web analytics platform + * + * @description JavaScript tracker for Snowplow + * @version 2.5.3 + * @author Alex Dean, Simon Andersson, Anthon Pang, Fred Blundun + * @copyright Anthon Pang, Snowplow Analytics Ltd + * @license Simplified BSD + */ + +/* + * For technical documentation: + * https://github.com/snowplow/snowplow/wiki/javascript-tracker + * + * For the setup guide: + * https://github.com/snowplow/snowplow/wiki/javascript-tracker-setup + * / + +/* + * Browser [In]Compatibility + * - minimum required ECMAScript: ECMA-262, edition 3 + * + * Incompatible with these (and earlier) versions of: + * - IE4 - try..catch and for..in introduced in IE5 + *- IE5 - named anonymous functions, array.push, encodeURIComponent, decodeURIComponent, and getElementsByTagName introduced in IE5.5 + * - Firefox 1.0 and Netscape 8.x - FF1.5 adds array.indexOf, among other things + * - Mozilla 1.7 and Netscape 6.x-7.x + * - Netscape 4.8 + * - Opera 6 - Error object (and Presto) introduced in Opera 7 + * - Opera 7 + */ + +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o +* @license MIT License +*/ + + + +this.cookie = function(name, value, ttl, path, domain, secure) { + + if (arguments.length > 1) { + return document.cookie = name + "=" + escape(value) + + (ttl ? "; expires=" + new Date(+new Date()+(ttl*1000)).toUTCString() : "") + + (path ? "; path=" + path : "") + + (domain ? "; domain=" + domain : "") + + (secure ? "; secure" : "") + } + + return unescape((("; "+document.cookie).split("; "+name+"=")[1]||"").split(";")[0]) +} + + +},{}],3:[function(require,module,exports){ +/** + * This script gives you the zone info key representing your device's time zone setting. + * + * @name jsTimezoneDetect + * @version 1.0.5 + * @author Jon Nylander + * @license MIT License - http://www.opensource.org/licenses/mit-license.php + * + * For usage and examples, visit: + * http://pellepim.bitbucket.org/jstz/ + * + * Copyright (c) Jon Nylander + */ + +/*jslint undef: true */ +/*global console, exports*/ + +(function(root) { + /** + * Namespace to hold all the code for timezone detection. + */ + var jstz = (function () { + 'use strict'; + var HEMISPHERE_SOUTH = 's', + + /** + * Gets the offset in minutes from UTC for a certain date. + * @param {Date} date + * @returns {Number} + */ + get_date_offset = function (date) { + var offset = -date.getTimezoneOffset(); + return (offset !== null ? offset : 0); + }, + + get_date = function (year, month, date) { + var d = new Date(); + if (year !== undefined) { + d.setFullYear(year); + } + d.setMonth(month); + d.setDate(date); + return d; + }, + + get_january_offset = function (year) { + return get_date_offset(get_date(year, 0 ,2)); + }, + + get_june_offset = function (year) { + return get_date_offset(get_date(year, 5, 2)); + }, + + /** + * Private method. + * Checks whether a given date is in daylight saving time. + * If the date supplied is after august, we assume that we're checking + * for southern hemisphere DST. + * @param {Date} date + * @returns {Boolean} + */ + date_is_dst = function (date) { + var is_southern = date.getMonth() > 7, + base_offset = is_southern ? get_june_offset(date.getFullYear()) : + get_january_offset(date.getFullYear()), + date_offset = get_date_offset(date), + is_west = base_offset < 0, + dst_offset = base_offset - date_offset; + + if (!is_west && !is_southern) { + return dst_offset < 0; + } + + return dst_offset !== 0; + }, + + /** + * This function does some basic calculations to create information about + * the user's timezone. It uses REFERENCE_YEAR as a solid year for which + * the script has been tested rather than depend on the year set by the + * client device. + * + * Returns a key that can be used to do lookups in jstz.olson.timezones. + * eg: "720,1,2". + * + * @returns {String} + */ + + lookup_key = function () { + var january_offset = get_january_offset(), + june_offset = get_june_offset(), + diff = january_offset - june_offset; + + if (diff < 0) { + return january_offset + ",1"; + } else if (diff > 0) { + return june_offset + ",1," + HEMISPHERE_SOUTH; + } + + return january_offset + ",0"; + }, + + /** + * Uses get_timezone_info() to formulate a key to use in the olson.timezones dictionary. + * + * Returns a primitive object on the format: + * {'timezone': TimeZone, 'key' : 'the key used to find the TimeZone object'} + * + * @returns Object + */ + determine = function () { + var key = lookup_key(); + return new jstz.TimeZone(jstz.olson.timezones[key]); + }, + + /** + * This object contains information on when daylight savings starts for + * different timezones. + * + * The list is short for a reason. Often we do not have to be very specific + * to single out the correct timezone. But when we do, this list comes in + * handy. + * + * Each value is a date denoting when daylight savings starts for that timezone. + */ + dst_start_for = function (tz_name) { + + var ru_pre_dst_change = new Date(2010, 6, 15, 1, 0, 0, 0), // In 2010 Russia had DST, this allows us to detect Russia :) + dst_starts = { + 'America/Denver': new Date(2011, 2, 13, 3, 0, 0, 0), + 'America/Mazatlan': new Date(2011, 3, 3, 3, 0, 0, 0), + 'America/Chicago': new Date(2011, 2, 13, 3, 0, 0, 0), + 'America/Mexico_City': new Date(2011, 3, 3, 3, 0, 0, 0), + 'America/Asuncion': new Date(2012, 9, 7, 3, 0, 0, 0), + 'America/Santiago': new Date(2012, 9, 3, 3, 0, 0, 0), + 'America/Campo_Grande': new Date(2012, 9, 21, 5, 0, 0, 0), + 'America/Montevideo': new Date(2011, 9, 2, 3, 0, 0, 0), + 'America/Sao_Paulo': new Date(2011, 9, 16, 5, 0, 0, 0), + 'America/Los_Angeles': new Date(2011, 2, 13, 8, 0, 0, 0), + 'America/Santa_Isabel': new Date(2011, 3, 5, 8, 0, 0, 0), + 'America/Havana': new Date(2012, 2, 10, 2, 0, 0, 0), + 'America/New_York': new Date(2012, 2, 10, 7, 0, 0, 0), + 'Europe/Helsinki': new Date(2013, 2, 31, 5, 0, 0, 0), + 'Pacific/Auckland': new Date(2011, 8, 26, 7, 0, 0, 0), + 'America/Halifax': new Date(2011, 2, 13, 6, 0, 0, 0), + 'America/Goose_Bay': new Date(2011, 2, 13, 2, 1, 0, 0), + 'America/Miquelon': new Date(2011, 2, 13, 5, 0, 0, 0), + 'America/Godthab': new Date(2011, 2, 27, 1, 0, 0, 0), + 'Europe/Moscow': ru_pre_dst_change, + 'Asia/Amman': new Date(2013, 2, 29, 1, 0, 0, 0), + 'Asia/Beirut': new Date(2013, 2, 31, 2, 0, 0, 0), + 'Asia/Damascus': new Date(2013, 3, 6, 2, 0, 0, 0), + 'Asia/Jerusalem': new Date(2013, 2, 29, 5, 0, 0, 0), + 'Asia/Yekaterinburg': ru_pre_dst_change, + 'Asia/Omsk': ru_pre_dst_change, + 'Asia/Krasnoyarsk': ru_pre_dst_change, + 'Asia/Irkutsk': ru_pre_dst_change, + 'Asia/Yakutsk': ru_pre_dst_change, + 'Asia/Vladivostok': ru_pre_dst_change, + 'Asia/Baku': new Date(2013, 2, 31, 4, 0, 0), + 'Asia/Yerevan': new Date(2013, 2, 31, 3, 0, 0), + 'Asia/Kamchatka': ru_pre_dst_change, + 'Asia/Gaza': new Date(2010, 2, 27, 4, 0, 0), + 'Africa/Cairo': new Date(2010, 4, 1, 3, 0, 0), + 'Europe/Minsk': ru_pre_dst_change, + 'Pacific/Apia': new Date(2010, 10, 1, 1, 0, 0, 0), + 'Pacific/Fiji': new Date(2010, 11, 1, 0, 0, 0), + 'Australia/Perth': new Date(2008, 10, 1, 1, 0, 0, 0) + }; + + return dst_starts[tz_name]; + }; + + return { + determine: determine, + date_is_dst: date_is_dst, + dst_start_for: dst_start_for + }; + }()); + + /** + * Simple object to perform ambiguity check and to return name of time zone. + */ + jstz.TimeZone = function (tz_name) { + 'use strict'; + /** + * The keys in this object are timezones that we know may be ambiguous after + * a preliminary scan through the olson_tz object. + * + * The array of timezones to compare must be in the order that daylight savings + * starts for the regions. + */ + var AMBIGUITIES = { + 'America/Denver': ['America/Denver', 'America/Mazatlan'], + 'America/Chicago': ['America/Chicago', 'America/Mexico_City'], + 'America/Santiago': ['America/Santiago', 'America/Asuncion', 'America/Campo_Grande'], + 'America/Montevideo': ['America/Montevideo', 'America/Sao_Paulo'], + 'Asia/Beirut': ['Asia/Amman', 'Asia/Jerusalem', 'Asia/Beirut', 'Europe/Helsinki','Asia/Damascus'], + 'Pacific/Auckland': ['Pacific/Auckland', 'Pacific/Fiji'], + 'America/Los_Angeles': ['America/Los_Angeles', 'America/Santa_Isabel'], + 'America/New_York': ['America/Havana', 'America/New_York'], + 'America/Halifax': ['America/Goose_Bay', 'America/Halifax'], + 'America/Godthab': ['America/Miquelon', 'America/Godthab'], + 'Asia/Dubai': ['Europe/Moscow'], + 'Asia/Dhaka': ['Asia/Yekaterinburg'], + 'Asia/Jakarta': ['Asia/Omsk'], + 'Asia/Shanghai': ['Asia/Krasnoyarsk', 'Australia/Perth'], + 'Asia/Tokyo': ['Asia/Irkutsk'], + 'Australia/Brisbane': ['Asia/Yakutsk'], + 'Pacific/Noumea': ['Asia/Vladivostok'], + 'Pacific/Tarawa': ['Asia/Kamchatka', 'Pacific/Fiji'], + 'Pacific/Tongatapu': ['Pacific/Apia'], + 'Asia/Baghdad': ['Europe/Minsk'], + 'Asia/Baku': ['Asia/Yerevan','Asia/Baku'], + 'Africa/Johannesburg': ['Asia/Gaza', 'Africa/Cairo'] + }, + + timezone_name = tz_name, + + /** + * Checks if a timezone has possible ambiguities. I.e timezones that are similar. + * + * For example, if the preliminary scan determines that we're in America/Denver. + * We double check here that we're really there and not in America/Mazatlan. + * + * This is done by checking known dates for when daylight savings start for different + * timezones during 2010 and 2011. + */ + ambiguity_check = function () { + var ambiguity_list = AMBIGUITIES[timezone_name], + length = ambiguity_list.length, + i = 0, + tz = ambiguity_list[0]; + + for (; i < length; i += 1) { + tz = ambiguity_list[i]; + + if (jstz.date_is_dst(jstz.dst_start_for(tz))) { + timezone_name = tz; + return; + } + } + }, + + /** + * Checks if it is possible that the timezone is ambiguous. + */ + is_ambiguous = function () { + return typeof (AMBIGUITIES[timezone_name]) !== 'undefined'; + }; + + if (is_ambiguous()) { + ambiguity_check(); + } + + return { + name: function () { + return timezone_name; + } + }; + }; + + jstz.olson = {}; + + /* + * The keys in this dictionary are comma separated as such: + * + * First the offset compared to UTC time in minutes. + * + * Then a flag which is 0 if the timezone does not take daylight savings into account and 1 if it + * does. + * + * Thirdly an optional 's' signifies that the timezone is in the southern hemisphere, + * only interesting for timezones with DST. + * + * The mapped arrays is used for constructing the jstz.TimeZone object from within + * jstz.determine_timezone(); + */ + jstz.olson.timezones = { + '-720,0' : 'Pacific/Majuro', + '-660,0' : 'Pacific/Pago_Pago', + '-600,1' : 'America/Adak', + '-600,0' : 'Pacific/Honolulu', + '-570,0' : 'Pacific/Marquesas', + '-540,0' : 'Pacific/Gambier', + '-540,1' : 'America/Anchorage', + '-480,1' : 'America/Los_Angeles', + '-480,0' : 'Pacific/Pitcairn', + '-420,0' : 'America/Phoenix', + '-420,1' : 'America/Denver', + '-360,0' : 'America/Guatemala', + '-360,1' : 'America/Chicago', + '-360,1,s' : 'Pacific/Easter', + '-300,0' : 'America/Bogota', + '-300,1' : 'America/New_York', + '-270,0' : 'America/Caracas', + '-240,1' : 'America/Halifax', + '-240,0' : 'America/Santo_Domingo', + '-240,1,s' : 'America/Santiago', + '-210,1' : 'America/St_Johns', + '-180,1' : 'America/Godthab', + '-180,0' : 'America/Argentina/Buenos_Aires', + '-180,1,s' : 'America/Montevideo', + '-120,0' : 'America/Noronha', + '-120,1' : 'America/Noronha', + '-60,1' : 'Atlantic/Azores', + '-60,0' : 'Atlantic/Cape_Verde', + '0,0' : 'UTC', + '0,1' : 'Europe/London', + '60,1' : 'Europe/Berlin', + '60,0' : 'Africa/Lagos', + '60,1,s' : 'Africa/Windhoek', + '120,1' : 'Asia/Beirut', + '120,0' : 'Africa/Johannesburg', + '180,0' : 'Asia/Baghdad', + '180,1' : 'Europe/Moscow', + '210,1' : 'Asia/Tehran', + '240,0' : 'Asia/Dubai', + '240,1' : 'Asia/Baku', + '270,0' : 'Asia/Kabul', + '300,1' : 'Asia/Yekaterinburg', + '300,0' : 'Asia/Karachi', + '330,0' : 'Asia/Kolkata', + '345,0' : 'Asia/Kathmandu', + '360,0' : 'Asia/Dhaka', + '360,1' : 'Asia/Omsk', + '390,0' : 'Asia/Rangoon', + '420,1' : 'Asia/Krasnoyarsk', + '420,0' : 'Asia/Jakarta', + '480,0' : 'Asia/Shanghai', + '480,1' : 'Asia/Irkutsk', + '525,0' : 'Australia/Eucla', + '525,1,s' : 'Australia/Eucla', + '540,1' : 'Asia/Yakutsk', + '540,0' : 'Asia/Tokyo', + '570,0' : 'Australia/Darwin', + '570,1,s' : 'Australia/Adelaide', + '600,0' : 'Australia/Brisbane', + '600,1' : 'Asia/Vladivostok', + '600,1,s' : 'Australia/Sydney', + '630,1,s' : 'Australia/Lord_Howe', + '660,1' : 'Asia/Kamchatka', + '660,0' : 'Pacific/Noumea', + '690,0' : 'Pacific/Norfolk', + '720,1,s' : 'Pacific/Auckland', + '720,0' : 'Pacific/Tarawa', + '765,1,s' : 'Pacific/Chatham', + '780,0' : 'Pacific/Tongatapu', + '780,1,s' : 'Pacific/Apia', + '840,0' : 'Pacific/Kiritimati' + }; + + if (typeof exports !== 'undefined') { + exports.jstz = jstz; + } else { + root.jstz = jstz; + } +})(this); + + +},{}],4:[function(require,module,exports){ +(function(){ + var _global = this; + + /** + * JS Implementation of MurmurHash2 + * + * @author Gary Court + * @see http://github.com/garycourt/murmurhash-js + * @author Austin Appleby + * @see http://sites.google.com/site/murmurhash/ + * + * @param {string} str ASCII only + * @param {number} seed Positive integer only + * @return {number} 32-bit positive integer hash + */ + function MurmurHashV2(str, seed) { + var + l = str.length, + h = seed ^ l, + i = 0, + k; + + while (l >= 4) { + k = + ((str.charCodeAt(i) & 0xff)) | + ((str.charCodeAt(++i) & 0xff) << 8) | + ((str.charCodeAt(++i) & 0xff) << 16) | + ((str.charCodeAt(++i) & 0xff) << 24); + + k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16)); + k ^= k >>> 24; + k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16)); + + h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k; + + l -= 4; + ++i; + } + + switch (l) { + case 3: h ^= (str.charCodeAt(i + 2) & 0xff) << 16; + case 2: h ^= (str.charCodeAt(i + 1) & 0xff) << 8; + case 1: h ^= (str.charCodeAt(i) & 0xff); + h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)); + } + + h ^= h >>> 13; + h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)); + h ^= h >>> 15; + + return h >>> 0; + }; + + /** + * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011) + * + * @author Gary Court + * @see http://github.com/garycourt/murmurhash-js + * @author Austin Appleby + * @see http://sites.google.com/site/murmurhash/ + * + * @param {string} key ASCII only + * @param {number} seed Positive integer only + * @return {number} 32-bit positive integer hash + */ + function MurmurHashV3(key, seed) { + var remainder, bytes, h1, h1b, c1, c1b, c2, c2b, k1, i; + + remainder = key.length & 3; // key.length % 4 + bytes = key.length - remainder; + h1 = seed; + c1 = 0xcc9e2d51; + c2 = 0x1b873593; + i = 0; + + while (i < bytes) { + k1 = + ((key.charCodeAt(i) & 0xff)) | + ((key.charCodeAt(++i) & 0xff) << 8) | + ((key.charCodeAt(++i) & 0xff) << 16) | + ((key.charCodeAt(++i) & 0xff) << 24); + ++i; + + k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff; + k1 = (k1 << 15) | (k1 >>> 17); + k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff; + + h1 ^= k1; + h1 = (h1 << 13) | (h1 >>> 19); + h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff; + h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16)); + } + + k1 = 0; + + switch (remainder) { + case 3: k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16; + case 2: k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8; + case 1: k1 ^= (key.charCodeAt(i) & 0xff); + + k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff; + k1 = (k1 << 15) | (k1 >>> 17); + k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff; + h1 ^= k1; + } + + h1 ^= key.length; + + h1 ^= h1 >>> 16; + h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff; + h1 ^= h1 >>> 13; + h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff; + h1 ^= h1 >>> 16; + + return h1 >>> 0; + } + + var murmur = MurmurHashV3; + murmur.v2 = MurmurHashV2; + murmur.v3 = MurmurHashV3; + + if (typeof(module) != 'undefined') { + module.exports = murmur; + } else { + var _previousRoot = _global.murmur; + murmur.noConflict = function() { + _global.murmur = _previousRoot; + return murmur; + } + _global.murmur = murmur; + } +}()); + +},{}],5:[function(require,module,exports){ +var charenc = { + // UTF-8 encoding + utf8: { + // Convert a string to a byte array + stringToBytes: function(str) { + return charenc.bin.stringToBytes(unescape(encodeURIComponent(str))); + }, + + // Convert a byte array to a string + bytesToString: function(bytes) { + return decodeURIComponent(escape(charenc.bin.bytesToString(bytes))); + } + }, + + // Binary encoding + bin: { + // Convert a string to a byte array + stringToBytes: function(str) { + for (var bytes = [], i = 0; i < str.length; i++) + bytes.push(str.charCodeAt(i) & 0xFF); + return bytes; + }, + + // Convert a byte array to a string + bytesToString: function(bytes) { + for (var str = [], i = 0; i < bytes.length; i++) + str.push(String.fromCharCode(bytes[i])); + return str.join(''); + } + } +}; + +module.exports = charenc; + +},{}],6:[function(require,module,exports){ +(function() { + var base64map + = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', + + crypt = { + // Bit-wise rotation left + rotl: function(n, b) { + return (n << b) | (n >>> (32 - b)); + }, + + // Bit-wise rotation right + rotr: function(n, b) { + return (n << (32 - b)) | (n >>> b); + }, + + // Swap big-endian to little-endian and vice versa + endian: function(n) { + // If number given, swap endian + if (n.constructor == Number) { + return crypt.rotl(n, 8) & 0x00FF00FF | crypt.rotl(n, 24) & 0xFF00FF00; + } + + // Else, assume array and swap all items + for (var i = 0; i < n.length; i++) + n[i] = crypt.endian(n[i]); + return n; + }, + + // Generate an array of any length of random bytes + randomBytes: function(n) { + for (var bytes = []; n > 0; n--) + bytes.push(Math.floor(Math.random() * 256)); + return bytes; + }, + + // Convert a byte array to big-endian 32-bit words + bytesToWords: function(bytes) { + for (var words = [], i = 0, b = 0; i < bytes.length; i++, b += 8) + words[b >>> 5] |= bytes[i] << (24 - b % 32); + return words; + }, + + // Convert big-endian 32-bit words to a byte array + wordsToBytes: function(words) { + for (var bytes = [], b = 0; b < words.length * 32; b += 8) + bytes.push((words[b >>> 5] >>> (24 - b % 32)) & 0xFF); + return bytes; + }, + + // Convert a byte array to a hex string + bytesToHex: function(bytes) { + for (var hex = [], i = 0; i < bytes.length; i++) { + hex.push((bytes[i] >>> 4).toString(16)); + hex.push((bytes[i] & 0xF).toString(16)); + } + return hex.join(''); + }, + + // Convert a hex string to a byte array + hexToBytes: function(hex) { + for (var bytes = [], c = 0; c < hex.length; c += 2) + bytes.push(parseInt(hex.substr(c, 2), 16)); + return bytes; + }, + + // Convert a byte array to a base-64 string + bytesToBase64: function(bytes) { + for (var base64 = [], i = 0; i < bytes.length; i += 3) { + var triplet = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]; + for (var j = 0; j < 4; j++) + if (i * 8 + j * 6 <= bytes.length * 8) + base64.push(base64map.charAt((triplet >>> 6 * (3 - j)) & 0x3F)); + else + base64.push('='); + } + return base64.join(''); + }, + + // Convert a base-64 string to a byte array + base64ToBytes: function(base64) { + // Remove non-base-64 characters + base64 = base64.replace(/[^A-Z0-9+\/]/ig, ''); + + for (var bytes = [], i = 0, imod4 = 0; i < base64.length; + imod4 = ++i % 4) { + if (imod4 == 0) continue; + bytes.push(((base64map.indexOf(base64.charAt(i - 1)) + & (Math.pow(2, -2 * imod4 + 8) - 1)) << (imod4 * 2)) + | (base64map.indexOf(base64.charAt(i)) >>> (6 - imod4 * 2))); + } + return bytes; + } + }; + + module.exports = crypt; +})(); + +},{}],7:[function(require,module,exports){ +(function() { + var crypt = require('crypt'), + utf8 = require('charenc').utf8, + bin = require('charenc').bin, + + // The core + sha1 = function (message) { + // Convert to byte array + if (message.constructor == String) + message = utf8.stringToBytes(message); + // otherwise assume byte array + + var m = crypt.bytesToWords(message), + l = message.length * 8, + w = [], + H0 = 1732584193, + H1 = -271733879, + H2 = -1732584194, + H3 = 271733878, + H4 = -1009589776; + + // Padding + m[l >> 5] |= 0x80 << (24 - l % 32); + m[((l + 64 >>> 9) << 4) + 15] = l; + + for (var i = 0; i < m.length; i += 16) { + var a = H0, + b = H1, + c = H2, + d = H3, + e = H4; + + for (var j = 0; j < 80; j++) { + + if (j < 16) + w[j] = m[i + j]; + else { + var n = w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16]; + w[j] = (n << 1) | (n >>> 31); + } + + var t = ((H0 << 5) | (H0 >>> 27)) + H4 + (w[j] >>> 0) + ( + j < 20 ? (H1 & H2 | ~H1 & H3) + 1518500249 : + j < 40 ? (H1 ^ H2 ^ H3) + 1859775393 : + j < 60 ? (H1 & H2 | H1 & H3 | H2 & H3) - 1894007588 : + (H1 ^ H2 ^ H3) - 899497514); + + H4 = H3; + H3 = H2; + H2 = (H1 << 30) | (H1 >>> 2); + H1 = H0; + H0 = t; + } + + H0 += a; + H1 += b; + H2 += c; + H3 += d; + H4 += e; + } + + return [H0, H1, H2, H3, H4]; + }, + + // Public API + api = function (message, options) { + var digestbytes = crypt.wordsToBytes(sha1(message)); + return options && options.asBytes ? digestbytes : + options && options.asString ? bin.bytesToString(digestbytes) : + crypt.bytesToHex(digestbytes); + }; + + api._blocksize = 16; + api._digestsize = 20; + + module.exports = api; +})(); + +},{"charenc":5,"crypt":6}],8:[function(require,module,exports){ +/* + * JavaScript tracker core for Snowplow: index.js + * + * Copyright (c) 2014 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +module.exports = require('./lib/core'); + +},{"./lib/core":10}],9:[function(require,module,exports){ +/* + * Copyright (c) 2013 Kevin van Zonneveld (http://kvz.io) + * and Contributors (http://phpjs.org/authors) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +(function() { + + var object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support + + function base64_encode(data) { + // discuss at: http://phpjs.org/functions/base64_encode/ + // original by: Tyler Akins (http://rumkin.com) + // improved by: Bayron Guevara + // improved by: Thunder.m + // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // improved by: Rafał Kukawski (http://kukawski.pl) + // bugfixed by: Pellentesque Malesuada + // example 1: base64_encode('Kevin van Zonneveld'); + // returns 1: 'S2V2aW4gdmFuIFpvbm5ldmVsZA==' + // example 2: base64_encode('a'); + // returns 2: 'YQ==' + // example 3: base64_encode('✓ à la mode'); + // returns 3: '4pyTIMOgIGxhIG1vZGU=' + + var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + enc = '', + tmp_arr = []; + + if (!data) { + return data; + } + + data = unescape(encodeURIComponent(data)); + + do { + // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = o1 << 16 | o2 << 8 | o3; + + h1 = bits >> 18 & 0x3f; + h2 = bits >> 12 & 0x3f; + h3 = bits >> 6 & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(''); + + var r = data.length % 3; + + return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3); + } + + object.base64encode = base64_encode; + +}()); + +},{}],10:[function(require,module,exports){ +/* + * JavaScript tracker core for Snowplow: core.js + * + * Copyright (c) 2014 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +var payload = require('./payload.js'); +var uuid = require('uuid'); + +/** + * Create a tracker core object + * + * @param boolean base64 Whether to base 64 encode contexts and unstructured event JSONs + * @param callback function Function applied to every payload dictionary object + * @return object Tracker core + */ +function trackerCore(base64, callback) { + + // base 64 encoding should default to true + if (typeof base64 === 'undefined') { + base64 = true; + } + + // Dictionary of key-value pairs which get added to every payload, e.g. tracker version + var payloadPairs = {}; + + /** + * Set a persistent key-value pair to be added to every payload + * + * @param string key Field name + * @param string value Field value + */ + function addPayloadPair(key, value) { + payloadPairs[key] = value; + } + + /** + * Returns a copy of a JSON with undefined and null properties removed + * + * @param object eventJson JSON to clean + * @param object exemptFields Set of fields which should not be removed even if empty + * @return object A cleaned copy of eventJson + */ + function removeEmptyProperties(eventJson, exemptFields) { + var ret = {}; + exemptFields = exemptFields || {}; + for (var k in eventJson) { + if (exemptFields[k] || (eventJson[k] !== null && typeof eventJson[k] !== 'undefined')) { + ret[k] = eventJson[k]; + } + } + return ret; + } + + /** + * Wraps an array of custom contexts in a self-describing JSON + * + * @param array contexts Array of custom context self-describing JSONs + * @return object Outer JSON + */ + function completeContexts(contexts) { + if (contexts && contexts.length) { + return { + schema: 'iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0', + data: contexts + }; + } + } + + /** + * Gets called by every trackXXX method + * Adds context and payloadPairs name-value pairs to the payload + * Applies the callback to the built payload + * + * @param sb object Payload + * @param array contexts Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload after the callback is applied + */ + function track(sb, context, tstamp) { + sb.addDict(payloadPairs); + sb.add('eid', uuid.v4()); + sb.add('dtm', tstamp || new Date().getTime()); + if (context) { + sb.addJson('cx', 'co', completeContexts(context)); + } + + if (typeof callback === 'function') { + callback(sb); + } + + return sb; + } + + /** + * Log an unstructured event + * + * @param object eventJson Contains the properties and schema location for the event + * @param array context Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + function trackUnstructEvent(properties, context, tstamp) { + var sb = payload.payloadBuilder(base64); + var ueJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/unstruct_event/jsonschema/1-0-0', + data: properties + }; + + sb.add('e', 'ue'); + sb.addJson('ue_px', 'ue_pr', ueJson); + + return track(sb, context, tstamp); + } + + return { + + /** + * Turn base 64 encoding on or off + * + * @param boolean encode key Field name + */ + setBase64Encoding: function (encode) { + base64 = encode; + }, + + addPayloadPair: addPayloadPair, + + /** + * Merges a dictionary into payloadPairs + * + * @param object dict Dictionary to add + */ + addPayloadDict: function (dict) { + for (var key in dict) { + if (dict.hasOwnProperty(key)) { + payloadPairs[key] = dict[key]; + } + } + }, + + /** + * Replace payloadPairs with a new dictionary + * + * @param object dict New dictionary + */ + resetPayloadPairs: function (dict) { + payloadPairs = payload.isJson(dict) ? dict : {}; + }, + + /** + * Set the tracker version + * + * @param string version + */ + setTrackerVersion: function (version) { + addPayloadPair('tv', version); + }, + + /** + * Set the tracker namespace + * + * @param string name + */ + setTrackerNamespace: function (name) { + addPayloadPair('tna', name); + }, + + /** + * Set the application ID + * + * @param string appId + */ + setAppId: function (appId) { + addPayloadPair('aid', appId); + }, + + /** + * Set the platform + * + * @param string value + */ + setPlatform: function (value) { + addPayloadPair('p', value); + }, + + /** + * Set the user ID + * + * @param string userId + */ + setUserId: function (userId) { + addPayloadPair('uid', userId); + }, + + /** + * Set the screen resolution + * + * @param number width + * @param number height + */ + setScreenResolution: function (width, height) { + addPayloadPair('res', width + 'x' + height); + }, + + /** + * Set the viewport dimensions + * + * @param number width + * @param number height + */ + setViewport: function (width, height) { + addPayloadPair('vp', width + 'x' + height); + }, + + /** + * Set the color depth + * + * @param number depth + */ + setColorDepth: function (depth) { + addPayloadPair('cd', depth); + }, + + /** + * Set the timezone + * + * @param string timezone + */ + setTimezone: function (timezone) { + addPayloadPair('tz', timezone); + }, + + /** + * Set the language + * + * @param string lang + */ + setLang: function (lang) { + addPayloadPair('lang', lang); + }, + + /** + * Set the IP address + * + * @param string appId + */ + setIpAddress: function (ip) { + addPayloadPair('ip', ip); + }, + + trackUnstructEvent: trackUnstructEvent, + + /** + * Log the page view / visit + * + * @param string customTitle The user-defined page title to attach to this page view + * @param array context Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackPageView: function (pageUrl, pageTitle, referrer, context, tstamp) { + var sb = payload.payloadBuilder(base64); + sb.add('e', 'pv'); // 'pv' for Page View + sb.add('url', pageUrl); + sb.add('page', pageTitle); + sb.add('refr', referrer); + + return track(sb, context, tstamp); + }, + /** + * Log that a user is still viewing a given page + * by sending a page ping. + * + * @param string pageTitle The page title to attach to this page ping + * @param minxoffset Minimum page x offset seen in the last ping period + * @param maxXOffset Maximum page x offset seen in the last ping period + * @param minYOffset Minimum page y offset seen in the last ping period + * @param maxYOffset Maximum page y offset seen in the last ping period + * @param array context Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackPagePing: function (pageUrl, pageTitle, referrer, minXOffset, maxXOffset, minYOffset, maxYOffset, context, tstamp) { + var sb = payload.payloadBuilder(base64); + sb.add('e', 'pp'); // 'pp' for Page Ping + sb.add('url', pageUrl); + sb.add('page', pageTitle); + sb.add('refr', referrer); + sb.add('pp_mix', minXOffset); + sb.add('pp_max', maxXOffset); + sb.add('pp_miy', minYOffset); + sb.add('pp_may', maxYOffset); + + return track(sb, context, tstamp); + }, + /** + * Track a structured event + * + * @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 array Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackStructEvent: function (category, action, label, property, value, context, tstamp) { + var sb = payload.payloadBuilder(base64); + sb.add('e', 'se'); // 'se' for Structured Event + sb.add('se_ca', category); + sb.add('se_ac', action); + sb.add('se_la', label); + sb.add('se_pr', property); + sb.add('se_va', value); + + return track(sb, context, tstamp); + }, + + /** + * 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 array context Optional. Context relating to the event. + * @param number tstamp Optional. Timestamp of the event + * @return object Payload + */ + trackEcommerceTransaction: function (orderId, affiliation, totalValue, taxValue, shipping, city, state, country, currency, context, tstamp) { + var sb = payload.payloadBuilder(base64); + sb.add('e', 'tr'); // 'tr' for Transaction + sb.add("tr_id", orderId); + sb.add("tr_af", affiliation); + sb.add("tr_tt", totalValue); + sb.add("tr_tx", taxValue); + sb.add("tr_sh", shipping); + sb.add("tr_ci", city); + sb.add("tr_st", state); + sb.add("tr_co", country); + sb.add("tr_cu", currency); + + return track(sb, context, tstamp); + }, + + /** + * 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 array context Optional. Context relating to the event. + * @param number tstamp Optional. Timestamp of the event + * @return object Payload + */ + trackEcommerceTransactionItem: function (orderId, sku, name, category, price, quantity, currency, context, tstamp) { + var sb = payload.payloadBuilder(base64); + sb.add("e", "ti"); // 'tr' for Transaction Item + sb.add("ti_id", orderId); + sb.add("ti_sk", sku); + sb.add("ti_nm", name); + sb.add("ti_ca", category); + sb.add("ti_pr", price); + sb.add("ti_qu", quantity); + sb.add("ti_cu", currency); + + return track(sb, context, tstamp); + }, + + /** + * Track a screen view unstructured event + * + * @param string name The name of the screen + * @param string id The ID of the screen + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackScreenView: function (name, id, context, tstamp) { + return trackUnstructEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0', + data: removeEmptyProperties({ + name: name, + id: id + }) + }, context, tstamp); + }, + + /** + * Log the link or click with the server + * + * @param string targetUrl + * @param string elementId + * @param array elementClasses + * @param string elementTarget + * @param string elementContent innerHTML of the link + * @param array context Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackLinkClick: function (targetUrl, elementId, elementClasses, elementTarget, elementContent, context, tstamp) { + var eventJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1', + data: removeEmptyProperties({ + targetUrl: targetUrl, + elementId: elementId, + elementClasses: elementClasses, + elementTarget: elementTarget, + elementContent: elementContent + }), + }; + + return trackUnstructEvent(eventJson, context, tstamp); + }, + + /** + * 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 array Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackAdImpression: function(impressionId, costModel, cost, targetUrl, bannerId, zoneId, advertiserId, campaignId, context, tstamp) { + var eventJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/ad_impression/jsonschema/1-0-0', + data: removeEmptyProperties({ + impressionId: impressionId, + costModel: costModel, + cost: cost, + targetUrl: targetUrl, + bannerId: bannerId, + zoneId: zoneId, + advertiserId: advertiserId, + campaignId: campaignId + }) + }; + + return trackUnstructEvent(eventJson, context, tstamp); + }, + + /** + * 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 array Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackAdClick: function (targetUrl, clickId, costModel, cost, bannerId, zoneId, impressionId, advertiserId, campaignId, context, tstamp) { + var eventJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/ad_click/jsonschema/1-0-0', + data: removeEmptyProperties({ + targetUrl: targetUrl, + clickId: clickId, + costModel: costModel, + cost: cost, + bannerId: bannerId, + zoneId: zoneId, + impressionId: impressionId, + advertiserId: advertiserId, + campaignId: campaignId + }) + }; + + return trackUnstructEvent(eventJson, context, tstamp); + }, + + /** + * 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 array Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackAdConversion: function (conversionId, costModel, cost, category, action, property, initialValue, advertiserId, campaignId, context, tstamp) { + var eventJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/ad_conversion/jsonschema/1-0-0', + data: removeEmptyProperties({ + conversionId: conversionId, + costModel: costModel, + cost: cost, + category: category, + action: action, + property: property, + initialValue: initialValue, + advertiserId: advertiserId, + campaignId: campaignId + }) + }; + + return trackUnstructEvent(eventJson, context, tstamp); + }, + + /** + * Track a social event + * + * @param string action Social action performed + * @param string network Social network + * @param string target Object of the social action e.g. the video liked, the tweet retweeted + * @param array Custom contexts relating to the event + * @param number tstamp Timestamp of the event + * @return object Payload + */ + trackSocialInteraction: function (action, network, target, context, tstamp) { + var eventJson = { + schema: 'iglu:com.snowplowanalytics.snowplow/social_interaction/jsonschema/1-0-0', + data: removeEmptyProperties({ + action: action, + network: network, + target: target + }) + }; + + return trackUnstructEvent(eventJson, context, tstamp); + }, + + /** + * 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. + * @param number tstamp Optional. Timestamp of the event + * @return object Payload + */ + trackAddToCart: function (sku, name, category, unitPrice, quantity, currency, context, tstamp) { + return trackUnstructEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/add_to_cart/jsonschema/1-0-0', + data: removeEmptyProperties({ + sku: sku, + name: name, + category: category, + unitPrice: unitPrice, + quantity: quantity, + currency: currency + }) + }, context, tstamp); + }, + + /** + * 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. + * @param number tstamp Optional. Timestamp of the event + * @return object Payload + */ + trackRemoveFromCart: function (sku, name, category, unitPrice, quantity, currency, context, tstamp) { + return trackUnstructEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/remove_from_cart/jsonschema/1-0-0', + data: removeEmptyProperties({ + sku: sku, + name: name, + category: category, + unitPrice: unitPrice, + quantity: quantity, + currency: currency + }) + }, context, tstamp); + }, + + /** + * Track the value of a form field changing + * + * @param string formId The parent form ID + * @param string elementId ID of the changed element + * @param string nodeName "INPUT", "TEXTAREA", or "SELECT" + * @param string type Type of the changed element if its type is "INPUT" + * @param array elementClasses List of classes of the changed element + * @param string value The new value of the changed element + * @param array context Optional. Context relating to the event. + * @param number tstamp Optional. Timestamp of the event + * @return object Payload + */ + trackFormChange: function(formId, elementId, nodeName, type, elementClasses, value, context, tstamp) { + return trackUnstructEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/change_form/jsonschema/1-0-0', + data: removeEmptyProperties({ + formId: formId, + elementId: elementId, + nodeName: nodeName, + type: type, + elementClasses: elementClasses, + value: value + }, {'value': true}) + }, context, tstamp); + }, + + /** + * Track a form submission event + * + * @param string formId ID of the form + * @param array formClasses Classes of the form + * @param array elements Mutable elements within the form + * @param array context Optional. Context relating to the event. + * @param number tstamp Optional. Timestamp of the event + * @return object Payload + */ + trackFormSubmission: function(formId, formClasses, elements, context, tstamp) { + return trackUnstructEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/submit_form/jsonschema/1-0-0', + data: removeEmptyProperties({ + formId: formId, + formClasses: formClasses, + elements: elements + }) + }, context, tstamp); + }, + + /** + * Track an internal search event + * + * @param array terms Search terms + * @param object filters Search filters + * @param totalResults Number of results + * @param pageResults Number of results displayed on page + * @param array context Optional. Context relating to the event. + * @param number tstamp Optional. Timestamp of the event + * @return object Payload + */ + trackSiteSearch: function(terms, filters, totalResults, pageResults, context, tstamp) { + return trackUnstructEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/site_search/jsonschema/1-0-0', + data: removeEmptyProperties({ + terms: terms, + filters: filters, + totalResults: totalResults, + pageResults: pageResults + }) + }, context, tstamp); + } + }; +}; + +module.exports = trackerCore; + +},{"./payload.js":11,"uuid":14}],11:[function(require,module,exports){ +/* + * JavaScript tracker core for Snowplow: payload.js + * + * Copyright (c) 2014 Snowplow Analytics Ltd. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ + +;(function() { + + var + json2 = require('JSON'), + base64 = require('./base64'), + + object = typeof exports !== 'undefined' ? exports : this; + + /* + * Bas64 encode data with URL and Filename Safe Alphabet (base64url) + * + * See: http://tools.ietf.org/html/rfc4648#page-7 + */ + function base64urlencode(data) { + if (!data) { + return data; + } + + var enc = base64.base64encode(data); + return enc.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); + } + + /* + * Is property a JSON? + */ + object.isJson = function (property) { + return (typeof property !== 'undefined' && property !== null && + (property.constructor === {}.constructor || property.constructor === [].constructor)); + }; + + /* + * Is property a non-empty JSON? + */ + object.isNonEmptyJson = function (property) { + if (!object.isJson(property)) { + return false; + } + for (var key in property) { + if (property.hasOwnProperty(key)) { + return true; + } + } + return false; + } + + /** + * A helper to build a Snowplow request string from an + * an optional initial value plus a set of individual + * name-value pairs, provided using the add method. + * + * @param boolean base64Encode Whether or not JSONs should be + * Base64-URL-safe-encoded + * + * @return object The request string builder, with add, addRaw and build methods + */ + object.payloadBuilder = function (base64Encode) { + var dict = {}; + + var add = function (key, value) { + if (value !== undefined && value !== null && value !== '') { + dict[key] = value; + } + }; + + var addDict = function (dict) { + for (var key in dict) { + if (dict.hasOwnProperty(key)) { + add(key, dict[key]); + } + } + }; + + var addJson = function (keyIfEncoded, keyIfNotEncoded, json) { + + if (object.isNonEmptyJson(json)) { + var str = json2.stringify(json); + if (base64Encode) { + add(keyIfEncoded, base64urlencode(str)); + } else { + add(keyIfNotEncoded, str); + } + } + }; + + return { + add: add, + addDict: addDict, + addJson: addJson, + build: function () { + return dict; + } + }; + }; + +}()); + +},{"./base64":9,"JSON":1}],12:[function(require,module,exports){ +module.exports = Array; + +},{}],13:[function(require,module,exports){ +(function (global){ + +var rng; + +if (global.crypto && crypto.getRandomValues) { + // WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto + // Moderately fast, high quality + var _rnds8 = new Uint8Array(16); + rng = function whatwgRNG() { + crypto.getRandomValues(_rnds8); + return _rnds8; + }; +} + +if (!rng) { + // Math.random()-based (RNG) + // + // If all else fails, use Math.random(). It's fast, but is of unspecified + // quality. + var _rnds = new Array(16); + rng = function() { + for (var i = 0, r; i < 16; i++) { + if ((i & 0x03) === 0) r = Math.random() * 0x100000000; + _rnds[i] = r >>> ((i & 0x03) << 3) & 0xff; + } + + return _rnds; + }; +} + +module.exports = rng; + + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],14:[function(require,module,exports){ +// uuid.js +// +// Copyright (c) 2010-2012 Robert Kieffer +// MIT License - http://opensource.org/licenses/mit-license.php + +// Unique ID creation requires a high quality random # generator. We feature +// detect to determine the best RNG source, normalizing to a function that +// returns 128-bits of randomness, since that's what's usually required +var _rng = require('./rng'); + +// Buffer class to use, +// we can't use `Buffer || Array` otherwise Buffer would be +// shimmed by browserify and added to the browser build +var BufferClass = require('./buffer'); + +// Maps for number <-> hex string conversion +var _byteToHex = []; +var _hexToByte = {}; +for (var i = 0; i < 256; i++) { + _byteToHex[i] = (i + 0x100).toString(16).substr(1); + _hexToByte[_byteToHex[i]] = i; +} + +// **`parse()` - Parse a UUID into it's component bytes** +function parse(s, buf, offset) { + var i = (buf && offset) || 0, ii = 0; + + buf = buf || []; + s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) { + if (ii < 16) { // Don't overflow! + buf[i + ii++] = _hexToByte[oct]; + } + }); + + // Zero out remaining bytes if string was short + while (ii < 16) { + buf[i + ii++] = 0; + } + + return buf; +} + +// **`unparse()` - Convert UUID byte array (ala parse()) into a string** +function unparse(buf, offset) { + var i = offset || 0, bth = _byteToHex; + return bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]]; +} + +// **`v1()` - Generate time-based UUID** +// +// Inspired by https://github.com/LiosK/UUID.js +// and http://docs.python.org/library/uuid.html + +// random #'s we need to init node and clockseq +var _seedBytes = _rng(); + +// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) +var _nodeId = [ + _seedBytes[0] | 0x01, + _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5] +]; + +// Per 4.2.2, randomize (14 bit) clockseq +var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff; + +// Previous uuid creation time +var _lastMSecs = 0, _lastNSecs = 0; + +// See https://github.com/broofa/node-uuid for API details +function v1(options, buf, offset) { + var i = buf && offset || 0; + var b = buf || []; + + options = options || {}; + + var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; + + // UUID timestamps are 100 nano-second units since the Gregorian epoch, + // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so + // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' + // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. + var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime(); + + // Per 4.2.1.2, use count of uuid's generated during the current clock + // cycle to simulate higher resolution clock + var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1; + + // Time since last uuid creation (in msecs) + var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000; + + // Per 4.2.1.2, Bump clockseq on clock regression + if (dt < 0 && options.clockseq === undefined) { + clockseq = clockseq + 1 & 0x3fff; + } + + // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new + // time interval + if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) { + nsecs = 0; + } + + // Per 4.2.1.2 Throw error if too many uuids are requested + if (nsecs >= 10000) { + throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec'); + } + + _lastMSecs = msecs; + _lastNSecs = nsecs; + _clockseq = clockseq; + + // Per 4.1.4 - Convert from unix epoch to Gregorian epoch + msecs += 12219292800000; + + // `time_low` + var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000; + b[i++] = tl >>> 24 & 0xff; + b[i++] = tl >>> 16 & 0xff; + b[i++] = tl >>> 8 & 0xff; + b[i++] = tl & 0xff; + + // `time_mid` + var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff; + b[i++] = tmh >>> 8 & 0xff; + b[i++] = tmh & 0xff; + + // `time_high_and_version` + b[i++] = tmh >>> 24 & 0xf | 0x10; // include version + b[i++] = tmh >>> 16 & 0xff; + + // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) + b[i++] = clockseq >>> 8 | 0x80; + + // `clock_seq_low` + b[i++] = clockseq & 0xff; + + // `node` + var node = options.node || _nodeId; + for (var n = 0; n < 6; n++) { + b[i + n] = node[n]; + } + + return buf ? buf : unparse(b); +} + +// **`v4()` - Generate random UUID** + +// See https://github.com/broofa/node-uuid for API details +function v4(options, buf, offset) { + // Deprecated - 'format' argument, as supported in v1.2 + var i = buf && offset || 0; + + if (typeof(options) == 'string') { + buf = options == 'binary' ? new BufferClass(16) : null; + options = null; + } + options = options || {}; + + var rnds = options.random || (options.rng || _rng)(); + + // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` + rnds[6] = (rnds[6] & 0x0f) | 0x40; + rnds[8] = (rnds[8] & 0x3f) | 0x80; + + // Copy bytes to buffer, if provided + if (buf) { + for (var ii = 0; ii < 16; ii++) { + buf[i + ii] = rnds[ii]; + } + } + + return buf || unparse(rnds); +} + +// Export public API +var uuid = v4; +uuid.v1 = v1; +uuid.v4 = v4; +uuid.parse = parse; +uuid.unparse = unparse; +uuid.BufferClass = BufferClass; + +module.exports = uuid; + +},{"./buffer":12,"./rng":13}],15:[function(require,module,exports){ +arguments[4][13][0].apply(exports,arguments) +},{"dup":13}],16:[function(require,module,exports){ +// uuid.js +// +// Copyright (c) 2010-2012 Robert Kieffer +// MIT License - http://opensource.org/licenses/mit-license.php + +// Unique ID creation requires a high quality random # generator. We feature +// detect to determine the best RNG source, normalizing to a function that +// returns 128-bits of randomness, since that's what's usually required +var _rng = require('./rng'); + +// Maps for number <-> hex string conversion +var _byteToHex = []; +var _hexToByte = {}; +for (var i = 0; i < 256; i++) { + _byteToHex[i] = (i + 0x100).toString(16).substr(1); + _hexToByte[_byteToHex[i]] = i; +} + +// **`parse()` - Parse a UUID into it's component bytes** +function parse(s, buf, offset) { + var i = (buf && offset) || 0, ii = 0; + + buf = buf || []; + s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) { + if (ii < 16) { // Don't overflow! + buf[i + ii++] = _hexToByte[oct]; + } + }); + + // Zero out remaining bytes if string was short + while (ii < 16) { + buf[i + ii++] = 0; + } + + return buf; +} + +// **`unparse()` - Convert UUID byte array (ala parse()) into a string** +function unparse(buf, offset) { + var i = offset || 0, bth = _byteToHex; + return bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + '-' + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]] + + bth[buf[i++]] + bth[buf[i++]]; +} + +// **`v1()` - Generate time-based UUID** +// +// Inspired by https://github.com/LiosK/UUID.js +// and http://docs.python.org/library/uuid.html + +// random #'s we need to init node and clockseq +var _seedBytes = _rng(); + +// Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) +var _nodeId = [ + _seedBytes[0] | 0x01, + _seedBytes[1], _seedBytes[2], _seedBytes[3], _seedBytes[4], _seedBytes[5] +]; + +// Per 4.2.2, randomize (14 bit) clockseq +var _clockseq = (_seedBytes[6] << 8 | _seedBytes[7]) & 0x3fff; + +// Previous uuid creation time +var _lastMSecs = 0, _lastNSecs = 0; + +// See https://github.com/broofa/node-uuid for API details +function v1(options, buf, offset) { + var i = buf && offset || 0; + var b = buf || []; + + options = options || {}; + + var clockseq = options.clockseq !== undefined ? options.clockseq : _clockseq; + + // UUID timestamps are 100 nano-second units since the Gregorian epoch, + // (1582-10-15 00:00). JSNumbers aren't precise enough for this, so + // time is handled internally as 'msecs' (integer milliseconds) and 'nsecs' + // (100-nanoseconds offset from msecs) since unix epoch, 1970-01-01 00:00. + var msecs = options.msecs !== undefined ? options.msecs : new Date().getTime(); + + // Per 4.2.1.2, use count of uuid's generated during the current clock + // cycle to simulate higher resolution clock + var nsecs = options.nsecs !== undefined ? options.nsecs : _lastNSecs + 1; + + // Time since last uuid creation (in msecs) + var dt = (msecs - _lastMSecs) + (nsecs - _lastNSecs)/10000; + + // Per 4.2.1.2, Bump clockseq on clock regression + if (dt < 0 && options.clockseq === undefined) { + clockseq = clockseq + 1 & 0x3fff; + } + + // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new + // time interval + if ((dt < 0 || msecs > _lastMSecs) && options.nsecs === undefined) { + nsecs = 0; + } + + // Per 4.2.1.2 Throw error if too many uuids are requested + if (nsecs >= 10000) { + throw new Error('uuid.v1(): Can\'t create more than 10M uuids/sec'); + } + + _lastMSecs = msecs; + _lastNSecs = nsecs; + _clockseq = clockseq; + + // Per 4.1.4 - Convert from unix epoch to Gregorian epoch + msecs += 12219292800000; + + // `time_low` + var tl = ((msecs & 0xfffffff) * 10000 + nsecs) % 0x100000000; + b[i++] = tl >>> 24 & 0xff; + b[i++] = tl >>> 16 & 0xff; + b[i++] = tl >>> 8 & 0xff; + b[i++] = tl & 0xff; + + // `time_mid` + var tmh = (msecs / 0x100000000 * 10000) & 0xfffffff; + b[i++] = tmh >>> 8 & 0xff; + b[i++] = tmh & 0xff; + + // `time_high_and_version` + b[i++] = tmh >>> 24 & 0xf | 0x10; // include version + b[i++] = tmh >>> 16 & 0xff; + + // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) + b[i++] = clockseq >>> 8 | 0x80; + + // `clock_seq_low` + b[i++] = clockseq & 0xff; + + // `node` + var node = options.node || _nodeId; + for (var n = 0; n < 6; n++) { + b[i + n] = node[n]; + } + + return buf ? buf : unparse(b); +} + +// **`v4()` - Generate random UUID** + +// See https://github.com/broofa/node-uuid for API details +function v4(options, buf, offset) { + // Deprecated - 'format' argument, as supported in v1.2 + var i = buf && offset || 0; + + if (typeof(options) == 'string') { + buf = options == 'binary' ? new Array(16) : null; + options = null; + } + options = options || {}; + + var rnds = options.random || (options.rng || _rng)(); + + // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` + rnds[6] = (rnds[6] & 0x0f) | 0x40; + rnds[8] = (rnds[8] & 0x3f) | 0x80; + + // Copy bytes to buffer, if provided + if (buf) { + for (var ii = 0; ii < 16; ii++) { + buf[i + ii] = rnds[ii]; + } + } + + return buf || unparse(rnds); +} + +// Export public API +var uuid = v4; +uuid.v1 = v1; +uuid.v4 = v4; +uuid.parse = parse; +uuid.unparse = unparse; + +module.exports = uuid; + +},{"./rng":15}],17:[function(require,module,exports){ +/* + * 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. + * + * * 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. + */ + +var lodash = require('./lib_managed/lodash'), + helpers = require('./lib/helpers'), + object = typeof exports !== 'undefined' ? exports : this; + +/** + * Object for handling automatic form tracking + * + * @param object core The tracker core + * @param string trackerId Unique identifier for the tracker instance, used to mark tracked elements + * @param function contextAdder Function to add common contexts like PerformanceTiming to all events + * @return object formTrackingManager instance + */ +object.getFormTrackingManager = function (core, trackerId, contextAdder) { + + // 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 && elt.nodeName.toUpperCase() !== 'HTML' && elt.nodeName.toUpperCase() !== 'FORM') { + elt = elt.parentNode; + } + if (elt && elt.nodeName && 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 && 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, helpers.getCssClasses(elt), value, contextAdder(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), helpers.getCssClasses(elt), innerElements, contextAdder(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; + } + }); + } + }; +}; + +},{"./lib/helpers":21,"./lib_managed/lodash":23}],18:[function(require,module,exports){ +/* + * 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. + * + * * 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 + lodash = require('./lib_managed/lodash'), + helpers = require('./lib/helpers'), + uuid = require('uuid'), + + 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) { + + // Page view ID should be shared between all tracker instances + var pageViewId = uuid.v4(), + trackerDictionary = {} + + /** + * 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 || {}; + trackerDictionary[namespace] = new TrackerConstructor(functionName, namespace, version, pageViewId, 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 + }; + }; + +}()); + +},{"./lib/helpers":21,"./lib_managed/lodash":23,"uuid":16}],19:[function(require,module,exports){ +/* + * 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. + * + * * 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. + */ + +// Snowplow Asynchronous Queue + +/* + * Get the name of the global input function + */ + +var snowplow = require('./snowplow'), + queueName, + queue, + windowAlias = window; + +if (windowAlias.GlobalSnowplowNamespace && windowAlias.GlobalSnowplowNamespace.length > 0) { + queueName = windowAlias.GlobalSnowplowNamespace.shift(); + queue = windowAlias[queueName]; + queue.q = new snowplow.Snowplow(queue.q, queueName); +} else { + windowAlias._snaq = windowAlias._snaq || []; + windowAlias._snaq = new snowplow.Snowplow(windowAlias._snaq, '_snaq'); +} + +},{"./snowplow":26}],20:[function(require,module,exports){ +/* + * JavaScript tracker for Snowplow: detectors.js + * + * Significant portions copyright 2010 Anthon Pang. Remainder copyright + * 2012-2014 Snowplow Analytics Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of Anthon Pang nor Snowplow Analytics Ltd nor the + * names of their contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +;(function() { + + var + 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, + + // document.body may not have rendered, so check whether be.offsetHeight is null + bodyHeight = be ? Math.max(be.offsetHeight, be.scrollHeight) : 0; + var w = Math.max(de.clientWidth, de.offsetWidth, de.scrollWidth); + var h = Math.max(de.clientHeight, de.offsetHeight, de.scrollHeight, bodyHeight); + 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; + }; + +}()); + +},{"../lib_managed/lodash":23,"browser-cookie-lite":2,"jstimezonedetect":3,"murmurhash":4}],21:[function(require,module,exports){ +/* + * 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 + 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. 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, ' ')); + }; + + /* + * 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 (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 + * Build: `lodash exports="node" include="isArray, isFunction, isString, isObject, isDate, isUndefined, isNull, map, mapValues, forEach, filter, find, compact, isEmpty, clone" --debug --output src/js/lib_managed/lodash.js` + * Copyright 2012-2013 The Dojo Foundation + * Based on Underscore.js 1.5.2 + * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license + */ +;(function() { + + /** Used to pool arrays and objects used internally */ + var arrayPool = []; + + /** Used internally to indicate various things */ + var indicatorObject = {}; + + /** Used as the max size of the `arrayPool` and `objectPool` */ + var maxPoolSize = 40; + + /** Used to match regexp flags from their coerced string values */ + var reFlags = /\w*$/; + + /** Used to detected named functions */ + var reFuncName = /^\s*function[ \n\r\t]+\w/; + + /** Used to detect functions containing a `this` reference */ + var reThis = /\bthis\b/; + + /** Used to fix the JScript [[DontEnum]] bug */ + var shadowedProps = [ + 'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', + 'toLocaleString', 'toString', 'valueOf' + ]; + + /** `Object#toString` result shortcuts */ + var argsClass = '[object Arguments]', + arrayClass = '[object Array]', + boolClass = '[object Boolean]', + dateClass = '[object Date]', + errorClass = '[object Error]', + funcClass = '[object Function]', + numberClass = '[object Number]', + objectClass = '[object Object]', + regexpClass = '[object RegExp]', + stringClass = '[object String]'; + + /** Used to identify object classifications that `_.clone` supports */ + var cloneableClasses = {}; + cloneableClasses[funcClass] = false; + cloneableClasses[argsClass] = cloneableClasses[arrayClass] = + cloneableClasses[boolClass] = cloneableClasses[dateClass] = + cloneableClasses[numberClass] = cloneableClasses[objectClass] = + cloneableClasses[regexpClass] = cloneableClasses[stringClass] = true; + + /** Used as the property descriptor for `__bindData__` */ + var descriptor = { + 'configurable': false, + 'enumerable': false, + 'value': null, + 'writable': false + }; + + /** Used as the data object for `iteratorTemplate` */ + var iteratorData = { + 'args': '', + 'array': null, + 'bottom': '', + 'firstArg': '', + 'init': '', + 'keys': null, + 'loop': '', + 'shadowedProps': null, + 'support': null, + 'top': '', + 'useHas': false + }; + + /** Used to determine if values are of the language type Object */ + var objectTypes = { + 'boolean': false, + 'function': true, + 'object': true, + 'number': false, + 'string': false, + 'undefined': false + }; + + /** Used as a reference to the global object */ + var root = (objectTypes[typeof window] && window) || this; + + /** Detect free variable `exports` */ + var freeExports = objectTypes[typeof exports] && exports && !exports.nodeType && exports; + + /** Detect free variable `module` */ + var freeModule = objectTypes[typeof module] && module && !module.nodeType && module; + + /** Detect the popular CommonJS extension `module.exports` */ + var moduleExports = freeModule && freeModule.exports === freeExports && freeExports; + + /** Detect free variable `global` from Node.js or Browserified code and use it as `root` */ + var freeGlobal = objectTypes[typeof global] && global; + if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal)) { + root = freeGlobal; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Gets an array from the array pool or creates a new one if the pool is empty. + * + * @private + * @returns {Array} The array from the pool. + */ + function getArray() { + return arrayPool.pop() || []; + } + + /** + * Checks if `value` is a DOM node in IE < 9. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a DOM node, else `false`. + */ + function isNode(value) { + // IE < 9 presents DOM nodes as `Object` objects except they have `toString` + // methods that are `typeof` "string" and still can coerce nodes to strings + return typeof value.toString != 'function' && typeof (value + '') == 'string'; + } + + /** + * Releases the given array back to the array pool. + * + * @private + * @param {Array} [array] The array to release. + */ + function releaseArray(array) { + array.length = 0; + if (arrayPool.length < maxPoolSize) { + arrayPool.push(array); + } + } + + /** + * Slices the `collection` from the `start` index up to, but not including, + * the `end` index. + * + * Note: This function is used instead of `Array#slice` to support node lists + * in IE < 9 and to ensure dense arrays are returned. + * + * @private + * @param {Array|Object|string} collection The collection to slice. + * @param {number} start The start index. + * @param {number} end The end index. + * @returns {Array} Returns the new array. + */ + function slice(array, start, end) { + start || (start = 0); + if (typeof end == 'undefined') { + end = array ? array.length : 0; + } + var index = -1, + length = end - start || 0, + result = Array(length < 0 ? 0 : length); + + while (++index < length) { + result[index] = array[start + index]; + } + return result; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Used for `Array` method references. + * + * Normally `Array.prototype` would suffice, however, using an array literal + * avoids issues in Narwhal. + */ + var arrayRef = []; + + /** Used for native method references */ + var errorProto = Error.prototype, + objectProto = Object.prototype, + stringProto = String.prototype; + + /** Used to resolve the internal [[Class]] of values */ + var toString = objectProto.toString; + + /** Used to detect if a method is native */ + var reNative = RegExp('^' + + String(toString) + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + .replace(/toString| for [^\]]+/g, '.*?') + '$' + ); + + /** Native method shortcuts */ + var fnToString = Function.prototype.toString, + hasOwnProperty = objectProto.hasOwnProperty, + push = arrayRef.push, + propertyIsEnumerable = objectProto.propertyIsEnumerable, + unshift = arrayRef.unshift; + + /** Used to set meta data on functions */ + var defineProperty = (function() { + // IE 8 only accepts DOM elements + try { + var o = {}, + func = isNative(func = Object.defineProperty) && func, + result = func(o, o, o) && func; + } catch(e) { } + return result; + }()); + + /* Native method shortcuts for methods with the same name as other `lodash` methods */ + var nativeCreate = isNative(nativeCreate = Object.create) && nativeCreate, + nativeIsArray = isNative(nativeIsArray = Array.isArray) && nativeIsArray, + nativeKeys = isNative(nativeKeys = Object.keys) && nativeKeys; + + /** Used to lookup a built-in constructor by [[Class]] */ + var ctorByClass = {}; + ctorByClass[arrayClass] = Array; + ctorByClass[boolClass] = Boolean; + ctorByClass[dateClass] = Date; + ctorByClass[funcClass] = Function; + ctorByClass[objectClass] = Object; + ctorByClass[numberClass] = Number; + ctorByClass[regexpClass] = RegExp; + ctorByClass[stringClass] = String; + + /** Used to avoid iterating non-enumerable properties in IE < 9 */ + var nonEnumProps = {}; + nonEnumProps[arrayClass] = nonEnumProps[dateClass] = nonEnumProps[numberClass] = { 'constructor': true, 'toLocaleString': true, 'toString': true, 'valueOf': true }; + nonEnumProps[boolClass] = nonEnumProps[stringClass] = { 'constructor': true, 'toString': true, 'valueOf': true }; + nonEnumProps[errorClass] = nonEnumProps[funcClass] = nonEnumProps[regexpClass] = { 'constructor': true, 'toString': true }; + nonEnumProps[objectClass] = { 'constructor': true }; + + (function() { + var length = shadowedProps.length; + while (length--) { + var key = shadowedProps[length]; + for (var className in nonEnumProps) { + if (hasOwnProperty.call(nonEnumProps, className) && !hasOwnProperty.call(nonEnumProps[className], key)) { + nonEnumProps[className][key] = false; + } + } + } + }()); + + /*--------------------------------------------------------------------------*/ + + /** + * Creates a `lodash` object which wraps the given value to enable intuitive + * method chaining. + * + * In addition to Lo-Dash methods, wrappers also have the following `Array` methods: + * `concat`, `join`, `pop`, `push`, `reverse`, `shift`, `slice`, `sort`, `splice`, + * and `unshift` + * + * Chaining is supported in custom builds as long as the `value` method is + * implicitly or explicitly included in the build. + * + * The chainable wrapper functions are: + * `after`, `assign`, `bind`, `bindAll`, `bindKey`, `chain`, `compact`, + * `compose`, `concat`, `countBy`, `create`, `createCallback`, `curry`, + * `debounce`, `defaults`, `defer`, `delay`, `difference`, `filter`, `flatten`, + * `forEach`, `forEachRight`, `forIn`, `forInRight`, `forOwn`, `forOwnRight`, + * `functions`, `groupBy`, `indexBy`, `initial`, `intersection`, `invert`, + * `invoke`, `keys`, `map`, `max`, `memoize`, `merge`, `min`, `object`, `omit`, + * `once`, `pairs`, `partial`, `partialRight`, `pick`, `pluck`, `pull`, `push`, + * `range`, `reject`, `remove`, `rest`, `reverse`, `shuffle`, `slice`, `sort`, + * `sortBy`, `splice`, `tap`, `throttle`, `times`, `toArray`, `transform`, + * `union`, `uniq`, `unshift`, `unzip`, `values`, `where`, `without`, `wrap`, + * and `zip` + * + * The non-chainable wrapper functions are: + * `clone`, `cloneDeep`, `contains`, `escape`, `every`, `find`, `findIndex`, + * `findKey`, `findLast`, `findLastIndex`, `findLastKey`, `has`, `identity`, + * `indexOf`, `isArguments`, `isArray`, `isBoolean`, `isDate`, `isElement`, + * `isEmpty`, `isEqual`, `isFinite`, `isFunction`, `isNaN`, `isNull`, `isNumber`, + * `isObject`, `isPlainObject`, `isRegExp`, `isString`, `isUndefined`, `join`, + * `lastIndexOf`, `mixin`, `noConflict`, `parseInt`, `pop`, `random`, `reduce`, + * `reduceRight`, `result`, `shift`, `size`, `some`, `sortedIndex`, `runInContext`, + * `template`, `unescape`, `uniqueId`, and `value` + * + * The wrapper functions `first` and `last` return wrapped values when `n` is + * provided, otherwise they return unwrapped values. + * + * Explicit chaining can be enabled by using the `_.chain` method. + * + * @name _ + * @constructor + * @category Chaining + * @param {*} value The value to wrap in a `lodash` instance. + * @returns {Object} Returns a `lodash` instance. + * @example + * + * var wrapped = _([1, 2, 3]); + * + * // returns an unwrapped value + * wrapped.reduce(function(sum, num) { + * return sum + num; + * }); + * // => 6 + * + * // returns a wrapped value + * var squares = wrapped.map(function(num) { + * return num * num; + * }); + * + * _.isArray(squares); + * // => false + * + * _.isArray(squares.value()); + * // => true + */ + function lodash() { + // no operation performed + } + + /** + * An object used to flag environments features. + * + * @static + * @memberOf _ + * @type Object + */ + var support = lodash.support = {}; + + (function() { + var ctor = function() { this.x = 1; }, + object = { '0': 1, 'length': 1 }, + props = []; + + ctor.prototype = { 'valueOf': 1, 'y': 1 }; + for (var key in new ctor) { props.push(key); } + for (key in arguments) { } + + /** + * Detect if an `arguments` object's [[Class]] is resolvable (all but Firefox < 4, IE < 9). + * + * @memberOf _.support + * @type boolean + */ + support.argsClass = toString.call(arguments) == argsClass; + + /** + * Detect if `arguments` objects are `Object` objects (all but Narwhal and Opera < 10.5). + * + * @memberOf _.support + * @type boolean + */ + support.argsObject = arguments.constructor == Object && !(arguments instanceof Array); + + /** + * Detect if `name` or `message` properties of `Error.prototype` are + * enumerable by default. (IE < 9, Safari < 5.1) + * + * @memberOf _.support + * @type boolean + */ + support.enumErrorProps = propertyIsEnumerable.call(errorProto, 'message') || propertyIsEnumerable.call(errorProto, 'name'); + + /** + * Detect if `prototype` properties are enumerable by default. + * + * Firefox < 3.6, Opera > 9.50 - Opera < 11.60, and Safari < 5.1 + * (if the prototype or a property on the prototype has been set) + * incorrectly sets a function's `prototype` property [[Enumerable]] + * value to `true`. + * + * @memberOf _.support + * @type boolean + */ + support.enumPrototypes = propertyIsEnumerable.call(ctor, 'prototype'); + + /** + * Detect if functions can be decompiled by `Function#toString` + * (all but PS3 and older Opera mobile browsers & avoided in Windows 8 apps). + * + * @memberOf _.support + * @type boolean + */ + support.funcDecomp = !isNative(root.WinRTError) && reThis.test(function() { return this; }); + + /** + * Detect if `Function#name` is supported (all but IE). + * + * @memberOf _.support + * @type boolean + */ + support.funcNames = typeof Function.name == 'string'; + + /** + * Detect if `arguments` object indexes are non-enumerable + * (Firefox < 4, IE < 9, PhantomJS, Safari < 5.1). + * + * @memberOf _.support + * @type boolean + */ + support.nonEnumArgs = key != 0; + + /** + * Detect if properties shadowing those on `Object.prototype` are non-enumerable. + * + * In IE < 9 an objects own properties, shadowing non-enumerable ones, are + * made non-enumerable as well (a.k.a the JScript [[DontEnum]] bug). + * + * @memberOf _.support + * @type boolean + */ + support.nonEnumShadows = !/valueOf/.test(props); + + /** + * Detect if `Array#shift` and `Array#splice` augment array-like objects correctly. + * + * Firefox < 10, IE compatibility mode, and IE < 9 have buggy Array `shift()` + * and `splice()` functions that fail to remove the last element, `value[0]`, + * of array-like objects even though the `length` property is set to `0`. + * The `shift()` method is buggy in IE 8 compatibility mode, while `splice()` + * is buggy regardless of mode in IE < 9 and buggy in compatibility mode in IE 9. + * + * @memberOf _.support + * @type boolean + */ + support.spliceObjects = (arrayRef.splice.call(object, 0, 1), !object[0]); + + /** + * Detect lack of support for accessing string characters by index. + * + * IE < 8 can't access characters by index and IE 8 can only access + * characters by index on string literals. + * + * @memberOf _.support + * @type boolean + */ + support.unindexedChars = ('x'[0] + Object('x')[0]) != 'xx'; + + /** + * Detect if a DOM node's [[Class]] is resolvable (all but IE < 9) + * and that the JS engine errors when attempting to coerce an object to + * a string without a `toString` function. + * + * @memberOf _.support + * @type boolean + */ + try { + support.nodeClass = !(toString.call(document) == objectClass && !({ 'toString': 0 } + '')); + } catch(e) { + support.nodeClass = true; + } + }(1)); + + /*--------------------------------------------------------------------------*/ + + /** + * The template used to create iterator functions. + * + * @private + * @param {Object} data The data object used to populate the text. + * @returns {string} Returns the interpolated text. + */ + var iteratorTemplate = function(obj) { + + var __p = 'var index, iterable = ' + + (obj.firstArg) + + ', result = ' + + (obj.init) + + ';\nif (!iterable) return result;\n' + + (obj.top) + + ';'; + if (obj.array) { + __p += '\nvar length = iterable.length; index = -1;\nif (' + + (obj.array) + + ') { '; + if (support.unindexedChars) { + __p += '\n if (isString(iterable)) {\n iterable = iterable.split(\'\')\n } '; + } + __p += '\n while (++index < length) {\n ' + + (obj.loop) + + ';\n }\n}\nelse { '; + } else if (support.nonEnumArgs) { + __p += '\n var length = iterable.length; index = -1;\n if (length && isArguments(iterable)) {\n while (++index < length) {\n index += \'\';\n ' + + (obj.loop) + + ';\n }\n } else { '; + } + + if (support.enumPrototypes) { + __p += '\n var skipProto = typeof iterable == \'function\';\n '; + } + + if (support.enumErrorProps) { + __p += '\n var skipErrorProps = iterable === errorProto || iterable instanceof Error;\n '; + } + + var conditions = []; if (support.enumPrototypes) { conditions.push('!(skipProto && index == "prototype")'); } if (support.enumErrorProps) { conditions.push('!(skipErrorProps && (index == "message" || index == "name"))'); } + + if (obj.useHas && obj.keys) { + __p += '\n var ownIndex = -1,\n ownProps = objectTypes[typeof iterable] && keys(iterable),\n length = ownProps ? ownProps.length : 0;\n\n while (++ownIndex < length) {\n index = ownProps[ownIndex];\n'; + if (conditions.length) { + __p += ' if (' + + (conditions.join(' && ')) + + ') {\n '; + } + __p += + (obj.loop) + + '; '; + if (conditions.length) { + __p += '\n }'; + } + __p += '\n } '; + } else { + __p += '\n for (index in iterable) {\n'; + if (obj.useHas) { conditions.push("hasOwnProperty.call(iterable, index)"); } if (conditions.length) { + __p += ' if (' + + (conditions.join(' && ')) + + ') {\n '; + } + __p += + (obj.loop) + + '; '; + if (conditions.length) { + __p += '\n }'; + } + __p += '\n } '; + if (support.nonEnumShadows) { + __p += '\n\n if (iterable !== objectProto) {\n var ctor = iterable.constructor,\n isProto = iterable === (ctor && ctor.prototype),\n className = iterable === stringProto ? stringClass : iterable === errorProto ? errorClass : toString.call(iterable),\n nonEnum = nonEnumProps[className];\n '; + for (k = 0; k < 7; k++) { + __p += '\n index = \'' + + (obj.shadowedProps[k]) + + '\';\n if ((!(isProto && nonEnum[index]) && hasOwnProperty.call(iterable, index))'; + if (!obj.useHas) { + __p += ' || (!nonEnum[index] && iterable[index] !== objectProto[index])'; + } + __p += ') {\n ' + + (obj.loop) + + ';\n } '; + } + __p += '\n } '; + } + + } + + if (obj.array || support.nonEnumArgs) { + __p += '\n}'; + } + __p += + (obj.bottom) + + ';\nreturn result'; + + return __p + }; + + /*--------------------------------------------------------------------------*/ + + /** + * The base implementation of `_.bind` that creates the bound function and + * sets its meta data. + * + * @private + * @param {Array} bindData The bind data array. + * @returns {Function} Returns the new bound function. + */ + function baseBind(bindData) { + var func = bindData[0], + partialArgs = bindData[2], + thisArg = bindData[4]; + + function bound() { + // `Function#bind` spec + // http://es5.github.io/#x15.3.4.5 + if (partialArgs) { + // avoid `arguments` object deoptimizations by using `slice` instead + // of `Array.prototype.slice.call` and not assigning `arguments` to a + // variable as a ternary expression + var args = slice(partialArgs); + push.apply(args, arguments); + } + // mimic the constructor's `return` behavior + // http://es5.github.io/#x13.2.2 + if (this instanceof bound) { + // ensure `new bound` is an instance of `func` + var thisBinding = baseCreate(func.prototype), + result = func.apply(thisBinding, args || arguments); + return isObject(result) ? result : thisBinding; + } + return func.apply(thisArg, args || arguments); + } + setBindData(bound, bindData); + return bound; + } + + /** + * The base implementation of `_.clone` without argument juggling or support + * for `thisArg` binding. + * + * @private + * @param {*} value The value to clone. + * @param {boolean} [isDeep=false] Specify a deep clone. + * @param {Function} [callback] The function to customize cloning values. + * @param {Array} [stackA=[]] Tracks traversed source objects. + * @param {Array} [stackB=[]] Associates clones with source counterparts. + * @returns {*} Returns the cloned value. + */ + function baseClone(value, isDeep, callback, stackA, stackB) { + if (callback) { + var result = callback(value); + if (typeof result != 'undefined') { + return result; + } + } + // inspect [[Class]] + var isObj = isObject(value); + if (isObj) { + var className = toString.call(value); + if (!cloneableClasses[className] || (!support.nodeClass && isNode(value))) { + return value; + } + var ctor = ctorByClass[className]; + switch (className) { + case boolClass: + case dateClass: + return new ctor(+value); + + case numberClass: + case stringClass: + return new ctor(value); + + case regexpClass: + result = ctor(value.source, reFlags.exec(value)); + result.lastIndex = value.lastIndex; + return result; + } + } else { + return value; + } + var isArr = isArray(value); + if (isDeep) { + // check for circular references and return corresponding clone + var initedStack = !stackA; + stackA || (stackA = getArray()); + stackB || (stackB = getArray()); + + var length = stackA.length; + while (length--) { + if (stackA[length] == value) { + return stackB[length]; + } + } + result = isArr ? ctor(value.length) : {}; + } + else { + result = isArr ? slice(value) : assign({}, value); + } + // add array properties assigned by `RegExp#exec` + if (isArr) { + if (hasOwnProperty.call(value, 'index')) { + result.index = value.index; + } + if (hasOwnProperty.call(value, 'input')) { + result.input = value.input; + } + } + // exit for shallow clone + if (!isDeep) { + return result; + } + // add the source value to the stack of traversed objects + // and associate it with its clone + stackA.push(value); + stackB.push(result); + + // recursively populate clone (susceptible to call stack limits) + (isArr ? baseEach : forOwn)(value, function(objValue, key) { + result[key] = baseClone(objValue, isDeep, callback, stackA, stackB); + }); + + if (initedStack) { + releaseArray(stackA); + releaseArray(stackB); + } + return result; + } + + /** + * The base implementation of `_.create` without support for assigning + * properties to the created object. + * + * @private + * @param {Object} prototype The object to inherit from. + * @returns {Object} Returns the new object. + */ + function baseCreate(prototype, properties) { + return isObject(prototype) ? nativeCreate(prototype) : {}; + } + // fallback for browsers without `Object.create` + if (!nativeCreate) { + baseCreate = (function() { + function Object() {} + return function(prototype) { + if (isObject(prototype)) { + Object.prototype = prototype; + var result = new Object; + Object.prototype = null; + } + return result || root.Object(); + }; + }()); + } + + /** + * The base implementation of `_.createCallback` without support for creating + * "_.pluck" or "_.where" style callbacks. + * + * @private + * @param {*} [func=identity] The value to convert to a callback. + * @param {*} [thisArg] The `this` binding of the created callback. + * @param {number} [argCount] The number of arguments the callback accepts. + * @returns {Function} Returns a callback function. + */ + function baseCreateCallback(func, thisArg, argCount) { + if (typeof func != 'function') { + return identity; + } + // exit early for no `thisArg` or already bound by `Function#bind` + if (typeof thisArg == 'undefined' || !('prototype' in func)) { + return func; + } + var bindData = func.__bindData__; + if (typeof bindData == 'undefined') { + if (support.funcNames) { + bindData = !func.name; + } + bindData = bindData || !support.funcDecomp; + if (!bindData) { + var source = fnToString.call(func); + if (!support.funcNames) { + bindData = !reFuncName.test(source); + } + if (!bindData) { + // checks if `func` references the `this` keyword and stores the result + bindData = reThis.test(source); + setBindData(func, bindData); + } + } + } + // exit early if there are no `this` references or `func` is bound + if (bindData === false || (bindData !== true && bindData[1] & 1)) { + return func; + } + switch (argCount) { + case 1: return function(value) { + return func.call(thisArg, value); + }; + case 2: return function(a, b) { + return func.call(thisArg, a, b); + }; + case 3: return function(value, index, collection) { + return func.call(thisArg, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(thisArg, accumulator, value, index, collection); + }; + } + return bind(func, thisArg); + } + + /** + * The base implementation of `createWrapper` that creates the wrapper and + * sets its meta data. + * + * @private + * @param {Array} bindData The bind data array. + * @returns {Function} Returns the new function. + */ + function baseCreateWrapper(bindData) { + var func = bindData[0], + bitmask = bindData[1], + partialArgs = bindData[2], + partialRightArgs = bindData[3], + thisArg = bindData[4], + arity = bindData[5]; + + var isBind = bitmask & 1, + isBindKey = bitmask & 2, + isCurry = bitmask & 4, + isCurryBound = bitmask & 8, + key = func; + + function bound() { + var thisBinding = isBind ? thisArg : this; + if (partialArgs) { + var args = slice(partialArgs); + push.apply(args, arguments); + } + if (partialRightArgs || isCurry) { + args || (args = slice(arguments)); + if (partialRightArgs) { + push.apply(args, partialRightArgs); + } + if (isCurry && args.length < arity) { + bitmask |= 16 & ~32; + return baseCreateWrapper([func, (isCurryBound ? bitmask : bitmask & ~3), args, null, thisArg, arity]); + } + } + args || (args = arguments); + if (isBindKey) { + func = thisBinding[key]; + } + if (this instanceof bound) { + thisBinding = baseCreate(func.prototype); + var result = func.apply(thisBinding, args); + return isObject(result) ? result : thisBinding; + } + return func.apply(thisBinding, args); + } + setBindData(bound, bindData); + return bound; + } + + /** + * The base implementation of `_.isEqual`, without support for `thisArg` binding, + * that allows partial "_.where" style comparisons. + * + * @private + * @param {*} a The value to compare. + * @param {*} b The other value to compare. + * @param {Function} [callback] The function to customize comparing values. + * @param {Function} [isWhere=false] A flag to indicate performing partial comparisons. + * @param {Array} [stackA=[]] Tracks traversed `a` objects. + * @param {Array} [stackB=[]] Tracks traversed `b` objects. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + */ + function baseIsEqual(a, b, callback, isWhere, stackA, stackB) { + // used to indicate that when comparing objects, `a` has at least the properties of `b` + if (callback) { + var result = callback(a, b); + if (typeof result != 'undefined') { + return !!result; + } + } + // exit early for identical values + if (a === b) { + // treat `+0` vs. `-0` as not equal + return a !== 0 || (1 / a == 1 / b); + } + var type = typeof a, + otherType = typeof b; + + // exit early for unlike primitive values + if (a === a && + !(a && objectTypes[type]) && + !(b && objectTypes[otherType])) { + return false; + } + // exit early for `null` and `undefined` avoiding ES3's Function#call behavior + // http://es5.github.io/#x15.3.4.4 + if (a == null || b == null) { + return a === b; + } + // compare [[Class]] names + var className = toString.call(a), + otherClass = toString.call(b); + + if (className == argsClass) { + className = objectClass; + } + if (otherClass == argsClass) { + otherClass = objectClass; + } + if (className != otherClass) { + return false; + } + switch (className) { + case boolClass: + case dateClass: + // coerce dates and booleans to numbers, dates to milliseconds and booleans + // to `1` or `0` treating invalid dates coerced to `NaN` as not equal + return +a == +b; + + case numberClass: + // treat `NaN` vs. `NaN` as equal + return (a != +a) + ? b != +b + // but treat `+0` vs. `-0` as not equal + : (a == 0 ? (1 / a == 1 / b) : a == +b); + + case regexpClass: + case stringClass: + // coerce regexes to strings (http://es5.github.io/#x15.10.6.4) + // treat string primitives and their corresponding object instances as equal + return a == String(b); + } + var isArr = className == arrayClass; + if (!isArr) { + // unwrap any `lodash` wrapped values + var aWrapped = hasOwnProperty.call(a, '__wrapped__'), + bWrapped = hasOwnProperty.call(b, '__wrapped__'); + + if (aWrapped || bWrapped) { + return baseIsEqual(aWrapped ? a.__wrapped__ : a, bWrapped ? b.__wrapped__ : b, callback, isWhere, stackA, stackB); + } + // exit for functions and DOM nodes + if (className != objectClass || (!support.nodeClass && (isNode(a) || isNode(b)))) { + return false; + } + // in older versions of Opera, `arguments` objects have `Array` constructors + var ctorA = !support.argsObject && isArguments(a) ? Object : a.constructor, + ctorB = !support.argsObject && isArguments(b) ? Object : b.constructor; + + // non `Object` object instances with different constructors are not equal + if (ctorA != ctorB && + !(isFunction(ctorA) && ctorA instanceof ctorA && isFunction(ctorB) && ctorB instanceof ctorB) && + ('constructor' in a && 'constructor' in b) + ) { + return false; + } + } + // assume cyclic structures are equal + // the algorithm for detecting cyclic structures is adapted from ES 5.1 + // section 15.12.3, abstract operation `JO` (http://es5.github.io/#x15.12.3) + var initedStack = !stackA; + stackA || (stackA = getArray()); + stackB || (stackB = getArray()); + + var length = stackA.length; + while (length--) { + if (stackA[length] == a) { + return stackB[length] == b; + } + } + var size = 0; + result = true; + + // add `a` and `b` to the stack of traversed objects + stackA.push(a); + stackB.push(b); + + // recursively compare objects and arrays (susceptible to call stack limits) + if (isArr) { + // compare lengths to determine if a deep comparison is necessary + length = a.length; + size = b.length; + result = size == length; + + if (result || isWhere) { + // deep compare the contents, ignoring non-numeric properties + while (size--) { + var index = length, + value = b[size]; + + if (isWhere) { + while (index--) { + if ((result = baseIsEqual(a[index], value, callback, isWhere, stackA, stackB))) { + break; + } + } + } else if (!(result = baseIsEqual(a[size], value, callback, isWhere, stackA, stackB))) { + break; + } + } + } + } + else { + // deep compare objects using `forIn`, instead of `forOwn`, to avoid `Object.keys` + // which, in this case, is more costly + forIn(b, function(value, key, b) { + if (hasOwnProperty.call(b, key)) { + // count the number of properties. + size++; + // deep compare each property value. + return (result = hasOwnProperty.call(a, key) && baseIsEqual(a[key], value, callback, isWhere, stackA, stackB)); + } + }); + + if (result && !isWhere) { + // ensure both objects have the same number of properties + forIn(a, function(value, key, a) { + if (hasOwnProperty.call(a, key)) { + // `size` will be `-1` if `a` has more properties than `b` + return (result = --size > -1); + } + }); + } + } + stackA.pop(); + stackB.pop(); + + if (initedStack) { + releaseArray(stackA); + releaseArray(stackB); + } + return result; + } + + /** + * Creates a function that, when called, either curries or invokes `func` + * with an optional `this` binding and partially applied arguments. + * + * @private + * @param {Function|string} func The function or method name to reference. + * @param {number} bitmask The bitmask of method flags to compose. + * The bitmask may be composed of the following flags: + * 1 - `_.bind` + * 2 - `_.bindKey` + * 4 - `_.curry` + * 8 - `_.curry` (bound) + * 16 - `_.partial` + * 32 - `_.partialRight` + * @param {Array} [partialArgs] An array of arguments to prepend to those + * provided to the new function. + * @param {Array} [partialRightArgs] An array of arguments to append to those + * provided to the new function. + * @param {*} [thisArg] The `this` binding of `func`. + * @param {number} [arity] The arity of `func`. + * @returns {Function} Returns the new function. + */ + function createWrapper(func, bitmask, partialArgs, partialRightArgs, thisArg, arity) { + var isBind = bitmask & 1, + isBindKey = bitmask & 2, + isCurry = bitmask & 4, + isCurryBound = bitmask & 8, + isPartial = bitmask & 16, + isPartialRight = bitmask & 32; + + if (!isBindKey && !isFunction(func)) { + throw new TypeError; + } + if (isPartial && !partialArgs.length) { + bitmask &= ~16; + isPartial = partialArgs = false; + } + if (isPartialRight && !partialRightArgs.length) { + bitmask &= ~32; + isPartialRight = partialRightArgs = false; + } + var bindData = func && func.__bindData__; + if (bindData && bindData !== true) { + // clone `bindData` + bindData = slice(bindData); + if (bindData[2]) { + bindData[2] = slice(bindData[2]); + } + if (bindData[3]) { + bindData[3] = slice(bindData[3]); + } + // set `thisBinding` is not previously bound + if (isBind && !(bindData[1] & 1)) { + bindData[4] = thisArg; + } + // set if previously bound but not currently (subsequent curried functions) + if (!isBind && bindData[1] & 1) { + bitmask |= 8; + } + // set curried arity if not yet set + if (isCurry && !(bindData[1] & 4)) { + bindData[5] = arity; + } + // append partial left arguments + if (isPartial) { + push.apply(bindData[2] || (bindData[2] = []), partialArgs); + } + // append partial right arguments + if (isPartialRight) { + unshift.apply(bindData[3] || (bindData[3] = []), partialRightArgs); + } + // merge flags + bindData[1] |= bitmask; + return createWrapper.apply(null, bindData); + } + // fast path for `_.bind` + var creater = (bitmask == 1 || bitmask === 17) ? baseBind : baseCreateWrapper; + return creater([func, bitmask, partialArgs, partialRightArgs, thisArg, arity]); + } + + /** + * Creates compiled iteration functions. + * + * @private + * @param {...Object} [options] The compile options object(s). + * @param {string} [options.array] Code to determine if the iterable is an array or array-like. + * @param {boolean} [options.useHas] Specify using `hasOwnProperty` checks in the object loop. + * @param {Function} [options.keys] A reference to `_.keys` for use in own property iteration. + * @param {string} [options.args] A comma separated string of iteration function arguments. + * @param {string} [options.top] Code to execute before the iteration branches. + * @param {string} [options.loop] Code to execute in the object loop. + * @param {string} [options.bottom] Code to execute after the iteration branches. + * @returns {Function} Returns the compiled function. + */ + function createIterator() { + // data properties + iteratorData.shadowedProps = shadowedProps; + + // iterator options + iteratorData.array = iteratorData.bottom = iteratorData.loop = iteratorData.top = ''; + iteratorData.init = 'iterable'; + iteratorData.useHas = true; + + // merge options into a template data object + for (var object, index = 0; object = arguments[index]; index++) { + for (var key in object) { + iteratorData[key] = object[key]; + } + } + var args = iteratorData.args; + iteratorData.firstArg = /^[^,]+/.exec(args)[0]; + + // create the function factory + var factory = Function( + 'baseCreateCallback, errorClass, errorProto, hasOwnProperty, ' + + 'indicatorObject, isArguments, isArray, isString, keys, objectProto, ' + + 'objectTypes, nonEnumProps, stringClass, stringProto, toString', + 'return function(' + args + ') {\n' + iteratorTemplate(iteratorData) + '\n}' + ); + + // return the compiled function + return factory( + baseCreateCallback, errorClass, errorProto, hasOwnProperty, + indicatorObject, isArguments, isArray, isString, iteratorData.keys, objectProto, + objectTypes, nonEnumProps, stringClass, stringProto, toString + ); + } + + /** + * Checks if `value` is a native function. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a native function, else `false`. + */ + function isNative(value) { + return typeof value == 'function' && reNative.test(value); + } + + /** + * Sets `this` binding data on a given function. + * + * @private + * @param {Function} func The function to set data on. + * @param {Array} value The data array to set. + */ + var setBindData = !defineProperty ? noop : function(func, value) { + descriptor.value = value; + defineProperty(func, '__bindData__', descriptor); + }; + + /*--------------------------------------------------------------------------*/ + + /** + * Checks if `value` is an `arguments` object. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is an `arguments` object, else `false`. + * @example + * + * (function() { return _.isArguments(arguments); })(1, 2, 3); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ + function isArguments(value) { + return value && typeof value == 'object' && typeof value.length == 'number' && + toString.call(value) == argsClass || false; + } + // fallback for browsers that can't detect `arguments` objects by [[Class]] + if (!support.argsClass) { + isArguments = function(value) { + return value && typeof value == 'object' && typeof value.length == 'number' && + hasOwnProperty.call(value, 'callee') && !propertyIsEnumerable.call(value, 'callee') || false; + }; + } + + /** + * Checks if `value` is an array. + * + * @static + * @memberOf _ + * @type Function + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is an array, else `false`. + * @example + * + * (function() { return _.isArray(arguments); })(); + * // => false + * + * _.isArray([1, 2, 3]); + * // => true + */ + var isArray = nativeIsArray || function(value) { + return value && typeof value == 'object' && typeof value.length == 'number' && + toString.call(value) == arrayClass || false; + }; + + /** + * A fallback implementation of `Object.keys` which produces an array of the + * given object's own enumerable property names. + * + * @private + * @type Function + * @param {Object} object The object to inspect. + * @returns {Array} Returns an array of property names. + */ + var shimKeys = createIterator({ + 'args': 'object', + 'init': '[]', + 'top': 'if (!(objectTypes[typeof object])) return result', + 'loop': 'result.push(index)' + }); + + /** + * Creates an array composed of the own enumerable property names of an object. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to inspect. + * @returns {Array} Returns an array of property names. + * @example + * + * _.keys({ 'one': 1, 'two': 2, 'three': 3 }); + * // => ['one', 'two', 'three'] (property order is not guaranteed across environments) + */ + var keys = !nativeKeys ? shimKeys : function(object) { + if (!isObject(object)) { + return []; + } + if ((support.enumPrototypes && typeof object == 'function') || + (support.nonEnumArgs && object.length && isArguments(object))) { + return shimKeys(object); + } + return nativeKeys(object); + }; + + /** Reusable iterator options shared by `each`, `forIn`, and `forOwn` */ + var eachIteratorOptions = { + 'args': 'collection, callback, thisArg', + 'top': "callback = callback && typeof thisArg == 'undefined' ? callback : baseCreateCallback(callback, thisArg, 3)", + 'array': "typeof length == 'number'", + 'keys': keys, + 'loop': 'if (callback(iterable[index], index, collection) === false) return result' + }; + + /** Reusable iterator options for `assign` and `defaults` */ + var defaultsIteratorOptions = { + 'args': 'object, source, guard', + 'top': + 'var args = arguments,\n' + + ' argsIndex = 0,\n' + + " argsLength = typeof guard == 'number' ? 2 : args.length;\n" + + 'while (++argsIndex < argsLength) {\n' + + ' iterable = args[argsIndex];\n' + + ' if (iterable && objectTypes[typeof iterable]) {', + 'keys': keys, + 'loop': "if (typeof result[index] == 'undefined') result[index] = iterable[index]", + 'bottom': ' }\n}' + }; + + /** Reusable iterator options for `forIn` and `forOwn` */ + var forOwnIteratorOptions = { + 'top': 'if (!objectTypes[typeof iterable]) return result;\n' + eachIteratorOptions.top, + 'array': false + }; + + /** + * A function compiled to iterate `arguments` objects, arrays, objects, and + * strings consistenly across environments, executing the callback for each + * element in the collection. The callback is bound to `thisArg` and invoked + * with three arguments; (value, index|key, collection). Callbacks may exit + * iteration early by explicitly returning `false`. + * + * @private + * @type Function + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array|Object|string} Returns `collection`. + */ + var baseEach = createIterator(eachIteratorOptions); + + /*--------------------------------------------------------------------------*/ + + /** + * Assigns own enumerable properties of source object(s) to the destination + * object. Subsequent sources will overwrite property assignments of previous + * sources. If a callback is provided it will be executed to produce the + * assigned values. The callback is bound to `thisArg` and invoked with two + * arguments; (objectValue, sourceValue). + * + * @static + * @memberOf _ + * @type Function + * @alias extend + * @category Objects + * @param {Object} object The destination object. + * @param {...Object} [source] The source objects. + * @param {Function} [callback] The function to customize assigning values. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns the destination object. + * @example + * + * _.assign({ 'name': 'fred' }, { 'employer': 'slate' }); + * // => { 'name': 'fred', 'employer': 'slate' } + * + * var defaults = _.partialRight(_.assign, function(a, b) { + * return typeof a == 'undefined' ? b : a; + * }); + * + * var object = { 'name': 'barney' }; + * defaults(object, { 'name': 'fred', 'employer': 'slate' }); + * // => { 'name': 'barney', 'employer': 'slate' } + */ + var assign = createIterator(defaultsIteratorOptions, { + 'top': + defaultsIteratorOptions.top.replace(';', + ';\n' + + "if (argsLength > 3 && typeof args[argsLength - 2] == 'function') {\n" + + ' var callback = baseCreateCallback(args[--argsLength - 1], args[argsLength--], 2);\n' + + "} else if (argsLength > 2 && typeof args[argsLength - 1] == 'function') {\n" + + ' callback = args[--argsLength];\n' + + '}' + ), + 'loop': 'result[index] = callback ? callback(result[index], iterable[index]) : iterable[index]' + }); + + /** + * Creates a clone of `value`. If `isDeep` is `true` nested objects will also + * be cloned, otherwise they will be assigned by reference. If a callback + * is provided it will be executed to produce the cloned values. If the + * callback returns `undefined` cloning will be handled by the method instead. + * The callback is bound to `thisArg` and invoked with one argument; (value). + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to clone. + * @param {boolean} [isDeep=false] Specify a deep clone. + * @param {Function} [callback] The function to customize cloning values. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {*} Returns the cloned value. + * @example + * + * var characters = [ + * { 'name': 'barney', 'age': 36 }, + * { 'name': 'fred', 'age': 40 } + * ]; + * + * var shallow = _.clone(characters); + * shallow[0] === characters[0]; + * // => true + * + * var deep = _.clone(characters, true); + * deep[0] === characters[0]; + * // => false + * + * _.mixin({ + * 'clone': _.partialRight(_.clone, function(value) { + * return _.isElement(value) ? value.cloneNode(false) : undefined; + * }) + * }); + * + * var clone = _.clone(document.body); + * clone.childNodes.length; + * // => 0 + */ + function clone(value, isDeep, callback, thisArg) { + // allows working with "Collections" methods without using their `index` + // and `collection` arguments for `isDeep` and `callback` + if (typeof isDeep != 'boolean' && isDeep != null) { + thisArg = callback; + callback = isDeep; + isDeep = false; + } + return baseClone(value, isDeep, typeof callback == 'function' && baseCreateCallback(callback, thisArg, 1)); + } + + /** + * Iterates over own and inherited enumerable properties of an object, + * executing the callback for each property. The callback is bound to `thisArg` + * and invoked with three arguments; (value, key, object). Callbacks may exit + * iteration early by explicitly returning `false`. + * + * @static + * @memberOf _ + * @type Function + * @category Objects + * @param {Object} object The object to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns `object`. + * @example + * + * function Shape() { + * this.x = 0; + * this.y = 0; + * } + * + * Shape.prototype.move = function(x, y) { + * this.x += x; + * this.y += y; + * }; + * + * _.forIn(new Shape, function(value, key) { + * console.log(key); + * }); + * // => logs 'x', 'y', and 'move' (property order is not guaranteed across environments) + */ + var forIn = createIterator(eachIteratorOptions, forOwnIteratorOptions, { + 'useHas': false + }); + + /** + * Iterates over own enumerable properties of an object, executing the callback + * for each property. The callback is bound to `thisArg` and invoked with three + * arguments; (value, key, object). Callbacks may exit iteration early by + * explicitly returning `false`. + * + * @static + * @memberOf _ + * @type Function + * @category Objects + * @param {Object} object The object to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns `object`. + * @example + * + * _.forOwn({ '0': 'zero', '1': 'one', 'length': 2 }, function(num, key) { + * console.log(key); + * }); + * // => logs '0', '1', and 'length' (property order is not guaranteed across environments) + */ + var forOwn = createIterator(eachIteratorOptions, forOwnIteratorOptions); + + /** + * Checks if `value` is a date. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a date, else `false`. + * @example + * + * _.isDate(new Date); + * // => true + */ + function isDate(value) { + return value && typeof value == 'object' && toString.call(value) == dateClass || false; + } + + /** + * Checks if `value` is empty. Arrays, strings, or `arguments` objects with a + * length of `0` and objects with no own enumerable properties are considered + * "empty". + * + * @static + * @memberOf _ + * @category Objects + * @param {Array|Object|string} value The value to inspect. + * @returns {boolean} Returns `true` if the `value` is empty, else `false`. + * @example + * + * _.isEmpty([1, 2, 3]); + * // => false + * + * _.isEmpty({}); + * // => true + * + * _.isEmpty(''); + * // => true + */ + function isEmpty(value) { + var result = true; + if (!value) { + return result; + } + var className = toString.call(value), + length = value.length; + + if ((className == arrayClass || className == stringClass || + (support.argsClass ? className == argsClass : isArguments(value))) || + (className == objectClass && typeof length == 'number' && isFunction(value.splice))) { + return !length; + } + forOwn(value, function() { + return (result = false); + }); + return result; + } + + /** + * Checks if `value` is a function. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + */ + function isFunction(value) { + return typeof value == 'function'; + } + // fallback for older versions of Chrome and Safari + if (isFunction(/x/)) { + isFunction = function(value) { + return typeof value == 'function' && toString.call(value) == funcClass; + }; + } + + /** + * Checks if `value` is the language type of Object. + * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(1); + * // => false + */ + function isObject(value) { + // check if the value is the ECMAScript language type of Object + // http://es5.github.io/#x8 + // and avoid a V8 bug + // http://code.google.com/p/v8/issues/detail?id=2291 + return !!(value && objectTypes[typeof value]); + } + + /** + * Checks if `value` is `null`. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is `null`, else `false`. + * @example + * + * _.isNull(null); + * // => true + * + * _.isNull(undefined); + * // => false + */ + function isNull(value) { + return value === null; + } + + /** + * Checks if `value` is a string. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a string, else `false`. + * @example + * + * _.isString('fred'); + * // => true + */ + function isString(value) { + return typeof value == 'string' || + value && typeof value == 'object' && toString.call(value) == stringClass || false; + } + + /** + * Checks if `value` is `undefined`. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is `undefined`, else `false`. + * @example + * + * _.isUndefined(void 0); + * // => true + */ + function isUndefined(value) { + return typeof value == 'undefined'; + } + + /** + * Creates an object with the same keys as `object` and values generated by + * running each own enumerable property of `object` through the callback. + * The callback is bound to `thisArg` and invoked with three arguments; + * (value, key, object). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new object with values of the results of each `callback` execution. + * @example + * + * _.mapValues({ 'a': 1, 'b': 2, 'c': 3} , function(num) { return num * 3; }); + * // => { 'a': 3, 'b': 6, 'c': 9 } + * + * var characters = { + * 'fred': { 'name': 'fred', 'age': 40 }, + * 'pebbles': { 'name': 'pebbles', 'age': 1 } + * }; + * + * // using "_.pluck" callback shorthand + * _.mapValues(characters, 'age'); + * // => { 'fred': 40, 'pebbles': 1 } + */ + function mapValues(object, callback, thisArg) { + var result = {}; + callback = lodash.createCallback(callback, thisArg, 3); + + forOwn(object, function(value, key, object) { + result[key] = callback(value, key, object); + }); + return result; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Iterates over elements of a collection, returning an array of all elements + * the callback returns truey for. The callback is bound to `thisArg` and + * invoked with three arguments; (value, index|key, collection). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias select + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new array of elements that passed the callback check. + * @example + * + * var evens = _.filter([1, 2, 3, 4, 5, 6], function(num) { return num % 2 == 0; }); + * // => [2, 4, 6] + * + * var characters = [ + * { 'name': 'barney', 'age': 36, 'blocked': false }, + * { 'name': 'fred', 'age': 40, 'blocked': true } + * ]; + * + * // using "_.pluck" callback shorthand + * _.filter(characters, 'blocked'); + * // => [{ 'name': 'fred', 'age': 40, 'blocked': true }] + * + * // using "_.where" callback shorthand + * _.filter(characters, { 'age': 36 }); + * // => [{ 'name': 'barney', 'age': 36, 'blocked': false }] + */ + function filter(collection, callback, thisArg) { + var result = []; + callback = lodash.createCallback(callback, thisArg, 3); + + if (isArray(collection)) { + var index = -1, + length = collection.length; + + while (++index < length) { + var value = collection[index]; + if (callback(value, index, collection)) { + result.push(value); + } + } + } else { + baseEach(collection, function(value, index, collection) { + if (callback(value, index, collection)) { + result.push(value); + } + }); + } + return result; + } + + /** + * Iterates over elements of a collection, returning the first element that + * the callback returns truey for. The callback is bound to `thisArg` and + * invoked with three arguments; (value, index|key, collection). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias detect, findWhere + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {*} Returns the found element, else `undefined`. + * @example + * + * var characters = [ + * { 'name': 'barney', 'age': 36, 'blocked': false }, + * { 'name': 'fred', 'age': 40, 'blocked': true }, + * { 'name': 'pebbles', 'age': 1, 'blocked': false } + * ]; + * + * _.find(characters, function(chr) { + * return chr.age < 40; + * }); + * // => { 'name': 'barney', 'age': 36, 'blocked': false } + * + * // using "_.where" callback shorthand + * _.find(characters, { 'age': 1 }); + * // => { 'name': 'pebbles', 'age': 1, 'blocked': false } + * + * // using "_.pluck" callback shorthand + * _.find(characters, 'blocked'); + * // => { 'name': 'fred', 'age': 40, 'blocked': true } + */ + function find(collection, callback, thisArg) { + callback = lodash.createCallback(callback, thisArg, 3); + + if (isArray(collection)) { + var index = -1, + length = collection.length; + + while (++index < length) { + var value = collection[index]; + if (callback(value, index, collection)) { + return value; + } + } + } else { + var result; + baseEach(collection, function(value, index, collection) { + if (callback(value, index, collection)) { + result = value; + return false; + } + }); + return result; + } + } + + /** + * Iterates over elements of a collection, executing the callback for each + * element. The callback is bound to `thisArg` and invoked with three arguments; + * (value, index|key, collection). Callbacks may exit iteration early by + * explicitly returning `false`. + * + * Note: As with other "Collections" methods, objects with a `length` property + * are iterated like arrays. To avoid this behavior `_.forIn` or `_.forOwn` + * may be used for object iteration. + * + * @static + * @memberOf _ + * @alias each + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array|Object|string} Returns `collection`. + * @example + * + * _([1, 2, 3]).forEach(function(num) { console.log(num); }).join(','); + * // => logs each number and returns '1,2,3' + * + * _.forEach({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { console.log(num); }); + * // => logs each number and returns the object (property order is not guaranteed across environments) + */ + function forEach(collection, callback, thisArg) { + if (callback && typeof thisArg == 'undefined' && isArray(collection)) { + var index = -1, + length = collection.length; + + while (++index < length) { + if (callback(collection[index], index, collection) === false) { + break; + } + } + } else { + baseEach(collection, callback, thisArg); + } + return collection; + } + + /** + * Creates an array of values by running each element in the collection + * through the callback. The callback is bound to `thisArg` and invoked with + * three arguments; (value, index|key, collection). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias collect + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new array of the results of each `callback` execution. + * @example + * + * _.map([1, 2, 3], function(num) { return num * 3; }); + * // => [3, 6, 9] + * + * _.map({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { return num * 3; }); + * // => [3, 6, 9] (property order is not guaranteed across environments) + * + * var characters = [ + * { 'name': 'barney', 'age': 36 }, + * { 'name': 'fred', 'age': 40 } + * ]; + * + * // using "_.pluck" callback shorthand + * _.map(characters, 'name'); + * // => ['barney', 'fred'] + */ + function map(collection, callback, thisArg) { + var index = -1, + length = collection ? collection.length : 0, + result = Array(typeof length == 'number' ? length : 0); + + callback = lodash.createCallback(callback, thisArg, 3); + if (isArray(collection)) { + while (++index < length) { + result[index] = callback(collection[index], index, collection); + } + } else { + baseEach(collection, function(value, key, collection) { + result[++index] = callback(value, key, collection); + }); + } + return result; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Creates an array with all falsey values removed. The values `false`, `null`, + * `0`, `""`, `undefined`, and `NaN` are all falsey. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to compact. + * @returns {Array} Returns a new array of filtered values. + * @example + * + * _.compact([0, 1, false, 2, '', 3]); + * // => [1, 2, 3] + */ + function compact(array) { + var index = -1, + length = array ? array.length : 0, + result = []; + + while (++index < length) { + var value = array[index]; + if (value) { + result.push(value); + } + } + return result; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Creates a function that, when called, invokes `func` with the `this` + * binding of `thisArg` and prepends any additional `bind` arguments to those + * provided to the bound function. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to bind. + * @param {*} [thisArg] The `this` binding of `func`. + * @param {...*} [arg] Arguments to be partially applied. + * @returns {Function} Returns the new bound function. + * @example + * + * var func = function(greeting) { + * return greeting + ' ' + this.name; + * }; + * + * func = _.bind(func, { 'name': 'fred' }, 'hi'); + * func(); + * // => 'hi fred' + */ + function bind(func, thisArg) { + return arguments.length > 2 + ? createWrapper(func, 17, slice(arguments, 2), null, thisArg) + : createWrapper(func, 1, null, null, thisArg); + } + + /*--------------------------------------------------------------------------*/ + + /** + * Produces a callback bound to an optional `thisArg`. If `func` is a property + * name the created callback will return the property value for a given element. + * If `func` is an object the created callback will return `true` for elements + * that contain the equivalent object properties, otherwise it will return `false`. + * + * @static + * @memberOf _ + * @category Utilities + * @param {*} [func=identity] The value to convert to a callback. + * @param {*} [thisArg] The `this` binding of the created callback. + * @param {number} [argCount] The number of arguments the callback accepts. + * @returns {Function} Returns a callback function. + * @example + * + * var characters = [ + * { 'name': 'barney', 'age': 36 }, + * { 'name': 'fred', 'age': 40 } + * ]; + * + * // wrap to create custom callback shorthands + * _.createCallback = _.wrap(_.createCallback, function(func, callback, thisArg) { + * var match = /^(.+?)__([gl]t)(.+)$/.exec(callback); + * return !match ? func(callback, thisArg) : function(object) { + * return match[2] == 'gt' ? object[match[1]] > match[3] : object[match[1]] < match[3]; + * }; + * }); + * + * _.filter(characters, 'age__gt38'); + * // => [{ 'name': 'fred', 'age': 40 }] + */ + function createCallback(func, thisArg, argCount) { + var type = typeof func; + if (func == null || type == 'function') { + return baseCreateCallback(func, thisArg, argCount); + } + // handle "_.pluck" style callback shorthands + if (type != 'object') { + return property(func); + } + var props = keys(func), + key = props[0], + a = func[key]; + + // handle "_.where" style callback shorthands + if (props.length == 1 && a === a && !isObject(a)) { + // fast path the common case of providing an object with a single + // property containing a primitive value + return function(object) { + var b = object[key]; + return a === b && (a !== 0 || (1 / a == 1 / b)); + }; + } + return function(object) { + var length = props.length, + result = false; + + while (length--) { + if (!(result = baseIsEqual(object[props[length]], func[props[length]], null, true))) { + break; + } + } + return result; + }; + } + + /** + * This method returns the first argument provided to it. + * + * @static + * @memberOf _ + * @category Utilities + * @param {*} value Any value. + * @returns {*} Returns `value`. + * @example + * + * var object = { 'name': 'fred' }; + * _.identity(object) === object; + * // => true + */ + function identity(value) { + return value; + } + + /** + * A no-operation function. + * + * @static + * @memberOf _ + * @category Utilities + * @example + * + * var object = { 'name': 'fred' }; + * _.noop(object) === undefined; + * // => true + */ + function noop() { + // no operation performed + } + + /** + * Creates a "_.pluck" style function, which returns the `key` value of a + * given object. + * + * @static + * @memberOf _ + * @category Utilities + * @param {string} key The name of the property to retrieve. + * @returns {Function} Returns the new function. + * @example + * + * var characters = [ + * { 'name': 'fred', 'age': 40 }, + * { 'name': 'barney', 'age': 36 } + * ]; + * + * var getName = _.property('name'); + * + * _.map(characters, getName); + * // => ['barney', 'fred'] + * + * _.sortBy(characters, getName); + * // => [{ 'name': 'barney', 'age': 36 }, { 'name': 'fred', 'age': 40 }] + */ + function property(key) { + return function(object) { + return object[key]; + }; + } + + /*--------------------------------------------------------------------------*/ + + lodash.assign = assign; + lodash.bind = bind; + lodash.compact = compact; + lodash.createCallback = createCallback; + lodash.filter = filter; + lodash.forEach = forEach; + lodash.forIn = forIn; + lodash.forOwn = forOwn; + lodash.keys = keys; + lodash.map = map; + lodash.mapValues = mapValues; + lodash.property = property; + + // add aliases + lodash.collect = map; + lodash.each = forEach; + lodash.extend = assign; + lodash.select = filter; + + /*--------------------------------------------------------------------------*/ + + // add functions that return unwrapped values when chaining + lodash.clone = clone; + lodash.find = find; + lodash.identity = identity; + lodash.isArguments = isArguments; + lodash.isArray = isArray; + lodash.isDate = isDate; + lodash.isEmpty = isEmpty; + lodash.isFunction = isFunction; + lodash.isNull = isNull; + lodash.isObject = isObject; + lodash.isString = isString; + lodash.isUndefined = isUndefined; + lodash.noop = noop; + + lodash.detect = find; + lodash.findWhere = find; + + /*--------------------------------------------------------------------------*/ + + /** + * The semantic version number. + * + * @static + * @memberOf _ + * @type string + */ + lodash.VERSION = '2.4.1'; + + /*--------------------------------------------------------------------------*/ + + if (freeExports && freeModule) { + // in Node.js or RingoJS + if (moduleExports) { + (freeModule.exports = lodash)._ = lodash; + } + + } + +}.call(this)); + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],24:[function(require,module,exports){ +/* + * JavaScript tracker for Snowplow: links.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. + */ + +var lodash = require('./lib_managed/lodash'), + helpers = require('./lib/helpers'), + object = typeof exports !== 'undefined' ? exports : this; + +/** + * Object for handling automatic link tracking + * + * @param object core The tracker core + * @param string trackerId Unique identifier for the tracker instance, used to mark tracked links + * @param function contextAdder Function to add common contexts like PerformanceTiming to all events + * @return object linkTrackingManager instance + */ +object.getLinkTrackingManager = function (core, trackerId, contextAdder) { + + // Filter function used to determine whether clicks on a link should be tracked + var linkTrackingFilter, + + // Whether pseudo clicks are tracked + linkTrackingPseudoClicks, + + // Whether to track the innerHTML of clicked links + linkTrackingContent, + + // The context attached to link click events + linkTrackingContext, + + // Internal state of the pseudo click handler + lastButton, + lastTarget; + + /* + * Process clicks + */ + function processClick(sourceElement, context) { + + var parentElement, + tag, + elementId, + elementClasses, + elementTarget, + elementContent; + + while ((parentElement = sourceElement.parentNode) !== null && + !lodash.isUndefined(parentElement) && // buggy IE5.5 + ((tag = sourceElement.tagName.toUpperCase()) !== 'A' && tag !== 'AREA')) { + sourceElement = parentElement; + } + + if (!lodash.isUndefined(sourceElement.href)) { + // browsers, such as Safari, don't downcase hostname and href + var originalSourceHostName = sourceElement.hostname || helpers.getHostName(sourceElement.href), + sourceHostName = originalSourceHostName.toLowerCase(), + sourceHref = sourceElement.href.replace(originalSourceHostName, sourceHostName), + scriptProtocol = new RegExp('^(javascript|vbscript|jscript|mocha|livescript|ecmascript|mailto):', 'i'); + + // Ignore script pseudo-protocol links + if (!scriptProtocol.test(sourceHref)) { + + elementId = sourceElement.id; + elementClasses = helpers.getCssClasses(sourceElement); + elementTarget = sourceElement.target; + elementContent = linkTrackingContent ? sourceElement.innerHTML : null; + + // decodeUrl %xx + sourceHref = unescape(sourceHref); + core.trackLinkClick(sourceHref, elementId, elementClasses, elementTarget, elementContent, contextAdder(context)); + } + } + } + + /* + * Return function to handle click event + */ + function getClickHandler(context) { + return function (evt) { + var button, + target; + + evt = evt || window.event; + button = evt.which || evt.button; + target = evt.target || evt.srcElement; + + // Using evt.type (added in IE4), we avoid defining separate handlers for mouseup and mousedown. + if (evt.type === 'click') { + if (target) { + processClick(target, context); + } + } else if (evt.type === 'mousedown') { + if ((button === 1 || button === 2) && target) { + lastButton = button; + lastTarget = target; + } else { + lastButton = lastTarget = null; + } + } else if (evt.type === 'mouseup') { + if (button === lastButton && target === lastTarget) { + processClick(target, context); + } + lastButton = lastTarget = null; + } + }; + } + + /* + * Add click listener to a DOM element + */ + function addClickListener(element) { + if (linkTrackingPseudoClicks) { + // for simplicity and performance, we ignore drag events + helpers.addEventListener(element, 'mouseup', getClickHandler(linkTrackingContext), false); + helpers.addEventListener(element, 'mousedown', getClickHandler(linkTrackingContext), false); + } else { + helpers.addEventListener(element, 'click', getClickHandler(linkTrackingContext), false); + } + } + + + + /* + * Check whether a set of classes contains any of the classes of a link element + * Used to determine whether clicks on that link should be tracked + */ + function checkLink(linkElement, specifiedClasses) { + var linkClasses = helpers.getCssClasses(linkElement), + i; + + for (i = 0; i < linkClasses.length; i++) { + if (specifiedClasses[linkClasses[i]]) { + return true; + } + } + return false; + } + + return { + + /* + * Configures link click tracking: how to filter which links will be tracked, + * whether to use pseudo click tracking, and what context to attach to link_click events + */ + configureLinkClickTracking: function (criterion, pseudoClicks, trackContent, context) { + linkTrackingContent = trackContent; + linkTrackingContext = context; + linkTrackingPseudoClicks = pseudoClicks; + linkTrackingFilter = helpers.getFilter(criterion, true); + }, + + /* + * Add click handlers to anchor and AREA elements, except those to be ignored + */ + addClickListeners: function () { + + var linkElements = document.links, + i; + + for (i = 0; i < linkElements.length; i++) { + // Add a listener to link elements which pass the filter and aren't already tracked + if (linkTrackingFilter(linkElements[i]) && !linkElements[i][trackerId]) { + addClickListener(linkElements[i]); + linkElements[i][trackerId] = true; + } + } + } + }; +}; + +},{"./lib/helpers":21,"./lib_managed/lodash":23}],25:[function(require,module,exports){ +/* + * JavaScript tracker for Snowplow: out_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. + * + * * 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 + json2 = require('JSON'), + lodash = require('./lib_managed/lodash'), + localStorageAccessible = require('./lib/detectors').localStorageAccessible, + helpers = require('./lib/helpers'), + object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support + + /** + * 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. + * @param int maxPostBytes Maximum combined size in bytes of the event JSONs in a POST request + * @return object OutQueueManager instance + */ + object.OutQueueManager = function (functionName, namespace, mutSnowplowState, useLocalStorage, usePost, bufferSize, maxPostBytes) { + 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 ? 'post2' : 'get'].join('_'); + + if (useLocalStorage) { + // Catch any JSON parse errors or localStorage that might be thrown + try { + // TODO: backward compatibility with the old version of the queue for POST requests + 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 && bufferSize > 1) { + mutSnowplowState.bufferFlushers.push(function () { + if (!executingQueue) { + 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) { + var cleanedRequest = lodash.mapValues(request, function (v) { + return v.toString(); + }); + return { + evt: cleanedRequest, + bytes: getUTF8Length(json2.stringify(cleanedRequest)) + }; + } + + /** + * Count the number of bytes a string will occupy when UTF-8 encoded + * Taken from http://stackoverflow.com/questions/2848462/count-bytes-in-textarea-using-javascript/ + * + * @param string s + * @return number Length of s in bytes when UTF-8 encoded + */ + function getUTF8Length(s) { + var len = 0; + for (var i = 0; i < s.length; i++) { + var code = s.charCodeAt(i); + if (code <= 0x7f) { + len += 1; + } else if (code <= 0x7ff) { + len += 2; + } else if (code >= 0xd800 && code <= 0xdfff) { + // Surrogate pair: These take 4 bytes in UTF-8 and 2 chars in UCS-2 + // (Assume next char is the other [valid] half and just skip it) + len += 4; i++; + } else if (code < 0xffff) { + len += 3; + } else { + len += 4; + } + } + return len; + } + + /* + * Queue an image beacon for submission to the collector. + * If we're not processing the queue, we'll start. + */ + function enqueueRequest (request, url) { + + configCollectorUrl = url + path; + if (usePost) { + var body = getBody(request); + if (body.bytes >= maxPostBytes) { + helpers.warn("Event of size " + body.bytes + " is too long - the maximum size is " + maxPostBytes); + var xhr = initializeXMLHttpRequest(configCollectorUrl); + xhr.send(encloseInPayloadDataEnvelope([body.evt])); + return; + } else { + outQueue.push(body); + } + } else { + outQueue.push(getQuerystring(request)); + } + var savedToLocalStorage = false; + if (useLocalStorage) { + savedToLocalStorage = helpers.attemptWriteLocalStorage(queueName, json2.stringify(outQueue)); + } + + if (!executingQueue && (!savedToLocalStorage || 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 = initializeXMLHttpRequest(configCollectorUrl); + + // Time out POST requests after 5 seconds + var xhrTimeout = setTimeout(function () { + xhr.abort(); + executingQueue = false; + }, 5000); + + function chooseHowManyToExecute(q) { + var numberToSend = 0; + var byteCount = 0; + while (numberToSend < q.length) { + byteCount += q[numberToSend].bytes; + if (byteCount >= maxPostBytes) { + break; + } else { + numberToSend += 1; + } + } + return numberToSend; + } + + // Keep track of number of events to delete from queue + var numberToSend = chooseHowManyToExecute(outQueue); + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 400) { + for (var deleteCount = 0; deleteCount < numberToSend; deleteCount++) { + outQueue.shift(); + } + if (useLocalStorage) { + helpers.attemptWriteLocalStorage(queueName, json2.stringify(outQueue)); + } + clearTimeout(xhrTimeout); + executeQueue(); + } else if (xhr.readyState === 4 && xhr.status >= 400) { + clearTimeout(xhrTimeout); + executingQueue = false; + } + }; + + var batch = lodash.map(outQueue.slice(0, numberToSend), function (x) { + return x.evt; + }); + if (batch.length > 0) { + xhr.send(encloseInPayloadDataEnvelope(batch)); + } + + } else { + + var image = new Image(1, 1); + + image.onload = function () { + outQueue.shift(); + if (useLocalStorage) { + helpers.attemptWriteLocalStorage(queueName, json2.stringify(outQueue)); + } + executeQueue(); + }; + + image.onerror = function () { + executingQueue = false; + }; + + image.src = configCollectorUrl + nextRequest; + } + } + + /** + * Open an XMLHttpRequest for a given endpoint with the correct credentials and header + * + * @param string url The destination URL + * @return object The XMLHttpRequest + */ + function initializeXMLHttpRequest(url) { + var xhr = new XMLHttpRequest(); + xhr.open('POST', url, true); + xhr.withCredentials = true; + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + return xhr; + } + + /** + * Enclose an array of events in a self-describing payload_data JSON string + * + * @param array events Batch of events + * @return string payload_data self-describing JSON + */ + function encloseInPayloadDataEnvelope(events) { + return json2.stringify({ + schema: 'iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-3', + data: events + }); + } + + return { + enqueueRequest: enqueueRequest, + executeQueue: executeQueue + }; + }; + +}()); + +},{"./lib/detectors":20,"./lib/helpers":21,"./lib_managed/lodash":23,"JSON":1}],26:[function(require,module,exports){ +/* + * 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. + */ + +/*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-' + '2.5.3', // 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); + }; + +}()); + +},{"./in_queue":18,"./lib/helpers":21,"./lib_managed/lodash":23,"./tracker":27}],27:[function(require,module,exports){ +/* + * 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. + * + * * 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 + 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'), + uuid = require('uuid'), + + 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 pageViewId ID for the current page view, to be attached to all events in the web_page context + * + * @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. sessionCookieTimeout, 1800 + * 14. contexts, {} + * 15. post, false + * 16. bufferSize, 1 + * 17. crossDomainLinker, false + * 18. maxPostBytes, 40000 + */ + object.Tracker = function Tracker(functionName, namespace, version, pageViewId, 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], + + customReferrer, + + 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, + + // Site ID + configTrackerSiteId = argmap.hasOwnProperty('appId') ? argmap.appId : '', // Updated for Snowplow + + // Document URL + configCustomUrl, + + // Document title + lastDocumentTitle = documentAlias.title, + + // Custom title + lastConfigTitle, + + // 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, + + // Recurring heart beat after initial ping (in milliseconds) + configHeartBeatTimer, + + // 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 domain + // User agent defaults to origin hostname + configCookieDomain = argmap.hasOwnProperty('cookieDomain') ? argmap.cookieDomain : null, + + // First-party cookie path + // Default is user agent defined. + configCookiePath = '/', + + // Do Not Track browser feature + dnt = navigatorAlias.doNotTrack || navigatorAlias.msDoNotTrack, + + // Do Not Track + configDoNotTrack = argmap.hasOwnProperty('respectDoNotTrack') ? argmap.respectDoNotTrack && (dnt === 'yes' || dnt === '1') : false, + + // Count sites which are pre-rendered + configCountPreRendered, + + // Life of the visitor cookie (in seconds) + configVisitorCookieTimeout = 63072000, // 2 years + + // Life of the session cookie (in seconds) + configSessionCookieTimeout = argmap.hasOwnProperty('sessionCookieTimeout') ? argmap.sessionCookieTimeout : 1800, // 30 minutes + + // Default hash seed for MurmurHash3 in detectors.detectSignature + configUserFingerprintHashSeed = argmap.hasOwnProperty('userFingerprintSeed') ? argmap.userFingerprintSeed : 123412414, + + // 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, + + // 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, + + // 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')), + + // 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, + + // Guard against installing the activity tracker more than once per Tracker instance + activityTrackingInstalled = false, + + // Last activity timestamp + lastActivityTime, + + // The last time an event was fired on the page - used to invalidate session if cookies are disabled + lastEventTime = new Date().getTime(), + + // How are we scrolling? + minXOffset, + maxXOffset, + minYOffset, + maxYOffset, + + // Hash function + hash = sha1, + + // Domain hash value + domainHash, + + // Domain unique user ID + domainUserId, + + // ID for the current session + memorizedSessionId, + + // Index for the current session - kept in memory in case cookies are disabled + memorizedVisitCount = 1, + + // Business-defined unique user ID + businessUserId, + + // 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, addCommonContexts), + + // Manager for automatic form tracking + formTrackingManager = forms.getFormTrackingManager(core, trackerId, addCommonContexts), + + // Manager for local storage queue + outQueueManager = new requestQueue.OutQueueManager( + functionName, + namespace, + mutSnowplowState, + useLocalStorage, + argmap.post, + argmap.bufferSize, + argmap.maxPostBytes || 40000), + + // 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.webPage) { + commonContexts.push(getWebPageContext()); + } + + 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]); + } + } + } + + /** + * Recalculate the domain, URL, and referrer + */ + function refreshUrl() { + locationArray = proxies.fixupUrl(documentAlias.domain, windowAlias.location.href, helpers.getReferrer()); + + // If this is a single-page app and the page URL has changed, then: + // - if the new URL's querystring contains a "refer(r)er" parameter, use it as the referrer + // - otherwise use the old URL as the referer + if (locationArray[1] !== locationHrefAlias) { + configReferrerUrl = helpers.getReferrer(locationHrefAlias); + } + + domainAlias = helpers.fixupDomain(locationArray[0]); + locationHrefAlias = locationArray[1]; + } + + /** + * Decorate the querystring of a single link + * + * @param event e The event targeting the link + */ + function linkDecorationHandler(e) { + var tstamp = new Date().getTime(); + var initialQsParams = '_sp=' + domainUserId + '.' + tstamp; + if (this.href) { + this.href = helpers.decorateQuerystring(this.href, '_sp', domainUserId + '.' + tstamp); + } + } + + /** + * Enable querystring decoration for links pasing a filter + * Whenever such a link is clicked on or navigated to via the keyboard, + * add "_sp={{duid}}.{{timestamp}}" to its querystring + * + * @param function crossDomainLinker Function used to determine which links to decorate + */ + function decorateLinks(crossDomainLinker) { + for (var i=0; i= 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() { + refreshUrl(); + 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 or renews the session cookie + */ + function setSessionCookie() { + cookie.cookie(getSnowplowCookieName('ses'), '*', configSessionCookieTimeout, configCookiePath, configCookieDomain); + } + + /* + * 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, sessionId) { + cookie.cookie( + getSnowplowCookieName('id'), + _domainUserId + '.' + createTs + '.' + visitCount + '.' + nowTs + '.' + lastVisitTs + '.' + sessionId, + configVisitorCookieTimeout, + configCookiePath, + configCookieDomain); + } + + /** + * Generate a pseudo-unique ID to fingerprint this user + * Note: this isn't a RFC4122-compliant UUID + */ + function createNewDomainUserId() { + return hash( + (navigatorAlias.userAgent || '') + + (navigatorAlias.platform || '') + + json2.stringify(browserFeatures) + Math.round(new Date().getTime() / 1000) + ).slice(0, 16); // 16 hexits = 64 bits + } + + /* + * Load the domain user ID and the session ID + * Set the cookies (if cookies are enabled) + */ + function initializeIdsAndCookies() { + var sesCookieSet = configUseCookies && !!getSnowplowCookieValue('ses'); + var idCookieComponents = loadDomainUserIdCookie(); + + if (idCookieComponents[1]) { + domainUserId = idCookieComponents[1]; + } else { + domainUserId = createNewDomainUserId(); + idCookieComponents[1] = domainUserId; + } + + memorizedSessionId = idCookieComponents[6]; + + if (!sesCookieSet) { + + // Increment the session ID + idCookieComponents[3] ++; + + // Create a new sessionId + memorizedSessionId = uuid.v4(); + idCookieComponents[6] = memorizedSessionId; + // Set lastVisitTs to currentVisitTs + idCookieComponents[5] = idCookieComponents[4]; + } + + if (configUseCookies) { + setSessionCookie(); + // Update currentVisitTs + idCookieComponents[4] = Math.round(new Date().getTime() / 1000); + idCookieComponents.shift(); + setDomainUserIdCookie.apply(null, idCookieComponents); + } + } + + /* + * 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 { + + 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 + '' + ]; + } + + if (!tmpContainer[6]) { + tmpContainer[6] = uuid.v4(); + } + + 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(), + cookiesDisabled = id[0], + _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], + sessionIdFromCookie = id[6]; + + if (configDoNotTrack && configUseCookies) { + cookie.cookie(idname, '', -1, configCookiePath, configCookieDomain); + cookie.cookie(sesname, '', -1, configCookiePath, configCookieDomain); + return; + } + + // If cookies are enabled, base visit count and session ID on the cookies + if (cookiesDisabled === '0') { + memorizedSessionId = sessionIdFromCookie; + + // New session? + if (!ses && configUseCookies) { + // New session (aka new visit) + visitCount++; + // Update the last visit timestamp + lastVisitTs = currentVisitTs; + // Regenerate the session ID + memorizedSessionId = uuid.v4(); + } + + memorizedVisitCount = visitCount; + + // Otherwise, a new session starts if configSessionCookieTimeout seconds have passed since the last event + } else { + if ((new Date().getTime() - lastEventTime) > configSessionCookieTimeout * 1000) { + memorizedSessionId = uuid.v4(); + memorizedVisitCount++; + } + } + + // Build out the rest of the request + sb.add('vp', detectors.detectViewport()); + sb.add('ds', detectors.detectDocumentSize()); + sb.add('vid', memorizedVisitCount); + sb.add('sid', memorizedSessionId); + sb.add('duid', _domainUserId); // Set to our local variable + sb.add('fp', userFingerprint); + sb.add('uid', businessUserId); + + refreshUrl(); + + sb.add('refr', purify(customReferrer || 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) { + setDomainUserIdCookie(_domainUserId, createTs, memorizedVisitCount, nowTs, lastVisitTs, memorizedSessionId); + setSessionCookie(); + } + + lastEventTime = new Date().getTime(); + } + + /** + * 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) { + var combinedContexts = commonContexts.concat(userContexts || []); + if (autoContexts.performanceTiming) { + var performanceTimingContext = getPerformanceTimingContext(); + if (performanceTimingContext) { + combinedContexts.push(performanceTimingContext); + } + } + return combinedContexts; + } + + /** + * Put together a web page context with a unique UUID for the page view + * + * @return object web_page context + */ + function getWebPageContext() { + return { + schema: 'iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0', + data: { + id: pageViewId + } + }; + } + + /** + * 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 + }; + } + + /** + * Combine an array of unchanging contexts with the result of a context-creating function + * + * @param object staticContexts Array of custom contexts + * @param object contextCallback Function returning an array of contexts + */ + function finalizeContexts(staticContexts, contextCallback) { + return (staticContexts || []).concat(contextCallback ? contextCallback() : []); + } + + /** + * 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 + * @param object contextCallback Function returning an array of contexts + */ + function logPageView(customTitle, context, contextCallback) { + + refreshUrl(); + + // So we know what document.title was at the time of trackPageView + lastDocumentTitle = documentAlias.title; + lastConfigTitle = customTitle; + + // Fixup page title + var pageTitle = helpers.fixupTitle(lastConfigTitle || lastDocumentTitle); + + // Log page view + core.trackPageView( + purify(configCustomUrl || locationHrefAlias), + pageTitle, + purify(customReferrer || configReferrerUrl), + addCommonContexts(finalizeContexts(context, contextCallback))); + + // 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(finalizeContexts(context, contextCallback)); // 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 object context Custom context relating to the event + */ + function logPagePing(context) { + refreshUrl(); + newDocumentTitle = documentAlias.title; + if (newDocumentTitle !== lastDocumentTitle) { + lastDocumentTitle = newDocumentTitle; + lastConfigTitle = null; + } + core.trackPagePing( + purify(configCustomUrl || locationHrefAlias), + helpers.fixupTitle(lastConfigTitle || lastDocumentTitle), + purify(customReferrer || 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 (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(); + + initializeIdsAndCookies(); + + if (argmap.crossDomainLinker) { + decorateLinks(argmap.crossDomainLinker); + } + + /************************************************************ + * 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) { + customReferrer = url; + }, + + /** + * Override url + * + * @param string url + */ + setCustomUrl: function (url) { + refreshUrl(); + configCustomUrl = resolveRelativeReference(locationHrefAlias, url); + }, + + /** + * Override document.title + * + * @param string title + */ + setDocumentTitle: function (title) { + // So we know what document.title was at the time of trackPageView + lastDocumentTitle = documentAlias.title; + lastConfigTitle = 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'); + }, + + /** + * Enable querystring decoration for links pasing a filter + * + * @param function crossDomainLinker Function used to determine which links to decorate + */ + crossDomainLinker: function (crossDomainLinkerCriterion) { + decorateLinks(crossDomainLinkerCriterion); + }, + + /** + * 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) { + refreshUrl(); + 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) { + refreshUrl(); + 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 object Custom context relating to the event + * @param object contextCallback Function returning an array of contexts + */ + trackPageView: function (customTitle, context, contextCallback) { + trackCallback(function () { + logPageView(customTitle, context, contextCallback); + }); + }, + + /** + * 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)); + }, + + /** + * Track a timing event (such as the time taken for a resource to load) + * + * @param string category Required. + * @param string variable Required. + * @param number timing Required. + * @param string label Optional. + * @param array context Optional. Context relating to the event. + */ + trackTiming: function (category, variable, timing, label, context) { + core.trackUnstructEvent({ + schema: 'iglu:com.snowplowanalytics.snowplow/timing/jsonschema/1-0-0', + data: { + category: category, + variable: variable, + timing: timing, + label: label + } + }, addCommonContexts(context)) + } + }; + }; + +}()); + +},{"./forms":17,"./lib/detectors":20,"./lib/helpers":21,"./lib/proxies":22,"./lib_managed/lodash":23,"./links":24,"./out_queue":25,"JSON":1,"browser-cookie-lite":2,"sha1":7,"snowplow-tracker-core":8,"uuid":16}]},{},[19]); diff --git a/dist/sp.js b/dist/sp.js new file mode 100644 index 000000000..971c496bd --- /dev/null +++ b/dist/sp.js @@ -0,0 +1,71 @@ +/*! * Snowplow - The world's most powerful web analytics platform + * + * @description JavaScript tracker for Snowplow + * @version 2.5.3 + * @author Alex Dean, Simon Andersson, Anthon Pang, Fred Blundun + * @copyright Anthon Pang, Snowplow Analytics Ltd + * @license Simplified BSD + */ +(function e(b,g,d){function c(n,j){if(!g[n]){if(!b[n]){var i=typeof require=="function"&&require;if(!j&&i){return i(n,!0)}if(a){return a(n,!0)}var m=new Error("Cannot find module '"+n+"'");throw m.code="MODULE_NOT_FOUND",m}var h=g[n]={exports:{}};b[n][0].call(h.exports,function(l){var o=b[n][1][l];return c(o?o:l)},h,h.exports,e,b,g,d)}return g[n].exports}var a=typeof require=="function"&&require;for(var f=0;f1){return document.cookie=f+"="+escape(h)+(d?"; expires="+new Date(+new Date()+(d*1000)).toUTCString():"")+(j?"; path="+j:"")+(g?"; domain="+g:"")+(i?"; secure":"")}return unescape((("; "+document.cookie).split("; "+f+"=")[1]||"").split(";")[0])}},{}],3:[function(b,c,a){(function(d){var f=(function(){var h="s",i=function(q){var r=-q.getTimezoneOffset();return(r!==null?r:0)},m=function(r,s,q){var t=new Date();if(r!==undefined){t.setFullYear(r)}t.setMonth(s);t.setDate(q);return t},j=function(q){return i(m(q,0,2))},n=function(q){return i(m(q,5,2))},g=function(r){var s=r.getMonth()>7,v=s?n(r.getFullYear()):j(r.getFullYear()),q=i(r),u=v<0,t=v-q;if(!u&&!s){return t<0}return t!==0},l=function(){var q=j(),r=n(),s=q-r;if(s<0){return q+",1"}else{if(s>0){return r+",1,"+h}}return q+",0"},o=function(){var q=l();return new f.TimeZone(f.olson.timezones[q])},p=function(q){var r=new Date(2010,6,15,1,0,0,0),s={"America/Denver":new Date(2011,2,13,3,0,0,0),"America/Mazatlan":new Date(2011,3,3,3,0,0,0),"America/Chicago":new Date(2011,2,13,3,0,0,0),"America/Mexico_City":new Date(2011,3,3,3,0,0,0),"America/Asuncion":new Date(2012,9,7,3,0,0,0),"America/Santiago":new Date(2012,9,3,3,0,0,0),"America/Campo_Grande":new Date(2012,9,21,5,0,0,0),"America/Montevideo":new Date(2011,9,2,3,0,0,0),"America/Sao_Paulo":new Date(2011,9,16,5,0,0,0),"America/Los_Angeles":new Date(2011,2,13,8,0,0,0),"America/Santa_Isabel":new Date(2011,3,5,8,0,0,0),"America/Havana":new Date(2012,2,10,2,0,0,0),"America/New_York":new Date(2012,2,10,7,0,0,0),"Europe/Helsinki":new Date(2013,2,31,5,0,0,0),"Pacific/Auckland":new Date(2011,8,26,7,0,0,0),"America/Halifax":new Date(2011,2,13,6,0,0,0),"America/Goose_Bay":new Date(2011,2,13,2,1,0,0),"America/Miquelon":new Date(2011,2,13,5,0,0,0),"America/Godthab":new Date(2011,2,27,1,0,0,0),"Europe/Moscow":r,"Asia/Amman":new Date(2013,2,29,1,0,0,0),"Asia/Beirut":new Date(2013,2,31,2,0,0,0),"Asia/Damascus":new Date(2013,3,6,2,0,0,0),"Asia/Jerusalem":new Date(2013,2,29,5,0,0,0),"Asia/Yekaterinburg":r,"Asia/Omsk":r,"Asia/Krasnoyarsk":r,"Asia/Irkutsk":r,"Asia/Yakutsk":r,"Asia/Vladivostok":r,"Asia/Baku":new Date(2013,2,31,4,0,0),"Asia/Yerevan":new Date(2013,2,31,3,0,0),"Asia/Kamchatka":r,"Asia/Gaza":new Date(2010,2,27,4,0,0),"Africa/Cairo":new Date(2010,4,1,3,0,0),"Europe/Minsk":r,"Pacific/Apia":new Date(2010,10,1,1,0,0,0),"Pacific/Fiji":new Date(2010,11,1,0,0,0),"Australia/Perth":new Date(2008,10,1,1,0,0,0)}; +return s[q]};return{determine:o,date_is_dst:g,dst_start_for:p}}());f.TimeZone=function(g){var h={"America/Denver":["America/Denver","America/Mazatlan"],"America/Chicago":["America/Chicago","America/Mexico_City"],"America/Santiago":["America/Santiago","America/Asuncion","America/Campo_Grande"],"America/Montevideo":["America/Montevideo","America/Sao_Paulo"],"Asia/Beirut":["Asia/Amman","Asia/Jerusalem","Asia/Beirut","Europe/Helsinki","Asia/Damascus"],"Pacific/Auckland":["Pacific/Auckland","Pacific/Fiji"],"America/Los_Angeles":["America/Los_Angeles","America/Santa_Isabel"],"America/New_York":["America/Havana","America/New_York"],"America/Halifax":["America/Goose_Bay","America/Halifax"],"America/Godthab":["America/Miquelon","America/Godthab"],"Asia/Dubai":["Europe/Moscow"],"Asia/Dhaka":["Asia/Yekaterinburg"],"Asia/Jakarta":["Asia/Omsk"],"Asia/Shanghai":["Asia/Krasnoyarsk","Australia/Perth"],"Asia/Tokyo":["Asia/Irkutsk"],"Australia/Brisbane":["Asia/Yakutsk"],"Pacific/Noumea":["Asia/Vladivostok"],"Pacific/Tarawa":["Asia/Kamchatka","Pacific/Fiji"],"Pacific/Tongatapu":["Pacific/Apia"],"Asia/Baghdad":["Europe/Minsk"],"Asia/Baku":["Asia/Yerevan","Asia/Baku"],"Africa/Johannesburg":["Asia/Gaza","Africa/Cairo"]},i=g,l=function(){var m=h[i],o=m.length,n=0,p=m[0]; +for(;n=4){n=((q.charCodeAt(o)&255))|((q.charCodeAt(++o)&255)<<8)|((q.charCodeAt(++o)&255)<<16)|((q.charCodeAt(++o)&255)<<24);n=(((n&65535)*1540483477)+((((n>>>16)*1540483477)&65535)<<16));n^=n>>>24;n=(((n&65535)*1540483477)+((((n>>>16)*1540483477)&65535)<<16));p=(((p&65535)*1540483477)+((((p>>>16)*1540483477)&65535)<<16))^n;j-=4;++o}switch(j){case 3:p^=(q.charCodeAt(o+2)&255)<<16;case 2:p^=(q.charCodeAt(o+1)&255)<<8;case 1:p^=(q.charCodeAt(o)&255);p=(((p&65535)*1540483477)+((((p>>>16)*1540483477)&65535)<<16))}p^=p>>>13;p=(((p&65535)*1540483477)+((((p>>>16)*1540483477)&65535)<<16));p^=p>>>15;return p>>>0}function f(t,p){var u,v,r,l,o,j,m,s,q,n;u=t.length&3;v=t.length-u;r=p;o=3432918353;m=461845907;n=0;while(n>>16)*o)&65535)<<16)))&4294967295; +q=(q<<15)|(q>>>17);q=((((q&65535)*m)+((((q>>>16)*m)&65535)<<16)))&4294967295;r^=q;r=(r<<13)|(r>>>19);l=((((r&65535)*5)+((((r>>>16)*5)&65535)<<16)))&4294967295;r=(((l&65535)+27492)+((((l>>>16)+58964)&65535)<<16))}q=0;switch(u){case 3:q^=(t.charCodeAt(n+2)&255)<<16;case 2:q^=(t.charCodeAt(n+1)&255)<<8;case 1:q^=(t.charCodeAt(n)&255);q=(((q&65535)*o)+((((q>>>16)*o)&65535)<<16))&4294967295;q=(q<<15)|(q>>>17);q=(((q&65535)*m)+((((q>>>16)*m)&65535)<<16))&4294967295;r^=q}r^=t.length;r^=r>>>16;r=(((r&65535)*2246822507)+((((r>>>16)*2246822507)&65535)<<16))&4294967295;r^=r>>>13;r=((((r&65535)*3266489909)+((((r>>>16)*3266489909)&65535)<<16)))&4294967295;r^=r>>>16;return r>>>0}var d=f;d.v2=g;d.v3=f;if(typeof(c)!="undefined"){c.exports=d}else{var h=i.murmur;d.noConflict=function(){i.murmur=h;return d};i.murmur=d}}())},{}],5:[function(c,d,b){var a={utf8:{stringToBytes:function(f){return a.bin.stringToBytes(unescape(encodeURIComponent(f)))},bytesToString:function(f){return decodeURIComponent(escape(a.bin.bytesToString(f))) +}},bin:{stringToBytes:function(h){for(var f=[],g=0;g>>(32-g))},rotr:function(h,g){return(h<<(32-g))|(h>>>g)},endian:function(h){if(h.constructor==Number){return f.rotl(h,8)&16711935|f.rotl(h,24)&4278255360}for(var g=0;g0;h--){g.push(Math.floor(Math.random()*256))}return g},bytesToWords:function(h){for(var l=[],j=0,g=0;j>>5]|=h[j]<<(24-g%32)}return l},wordsToBytes:function(i){for(var h=[],g=0;g>>5]>>>(24-g%32))&255)}return h},bytesToHex:function(g){for(var j=[],h=0;h>>4).toString(16));j.push((g[h]&15).toString(16)) +}return j.join("")},hexToBytes:function(h){for(var g=[],i=0;i>>6*(3-l))&63))}else{g.push("=")}}}return g.join("")},base64ToBytes:function(h){h=h.replace(/[^A-Z0-9+\/]/ig,"");for(var g=[],j=0,l=0;j>>(6-l*2)))}return g}};c.exports=f})()},{}],7:[function(b,c,a){(function(){var h=b("crypt"),d=b("charenc").utf8,f=b("charenc").bin,i=function(r){if(r.constructor==String){r=d.stringToBytes(r)}var z=h.bytesToWords(r),A=r.length*8,s=[],v=1732584193,u=-271733879,q=-1732584194,p=271733878,o=-1009589776;z[A>>5]|=128<<(24-A%32);z[((A+64>>>9)<<4)+15]=A;for(var C=0;C>>31)}var x=((v<<5)|(v>>>27))+o+(s[B]>>>0)+(B<20?(u&q|~u&p)+1518500249:B<40?(u^q^p)+1859775393:B<60?(u&q|u&p|q&p)-1894007588:(u^q^p)-899497514);o=p;p=q;q=(u<<30)|(u>>>2);u=v;v=x}v+=H;u+=G;q+=F;p+=E;o+=D}return[v,u,q,p,o]},g=function(m,j){var l=h.wordsToBytes(i(m));return j&&j.asBytes?l:j&&j.asString?f.bytesToString(l):h.bytesToHex(l)};g._blocksize=16;g._digestsize=20;c.exports=g})()},{charenc:5,crypt:6}],8:[function(b,c,a){c.exports=b("./lib/core")},{"./lib/core":10}],9:[function(b,c,a){(function(){var d=typeof a!=="undefined"?a:this;function f(q){var m="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";var l,j,h,v,u,t,s,w,p=0,x=0,o="",n=[];if(!q){return q}q=unescape(encodeURIComponent(q));do{l=q.charCodeAt(p++);j=q.charCodeAt(p++);h=q.charCodeAt(p++);w=l<<16|j<<8|h;v=w>>18&63;u=w>>12&63;t=w>>6&63;s=w&63;n[x++]=m.charAt(v)+m.charAt(u)+m.charAt(t)+m.charAt(s)}while(p>>((j&3)<<3)&255}return h}}c.exports=f}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],14:[function(j,c,u){var l=j("./rng");var o=j("./buffer");var g=[];var h={};for(var r=0;r<256;r++){g[r]=(r+256).toString(16).substr(1); +h[g[r]]=r}function n(y,v,z){var w=(v&&z)||0,x=0;v=v||[];y.toLowerCase().replace(/[0-9a-f]{2}/g,function(i){if(x<16){v[w+x++]=h[i]}});while(x<16){v[w+x++]=0}return v}function m(v,x){var w=x||0,y=g;return y[v[w++]]+y[v[w++]]+y[v[w++]]+y[v[w++]]+"-"+y[v[w++]]+y[v[w++]]+"-"+y[v[w++]]+y[v[w++]]+"-"+y[v[w++]]+y[v[w++]]+"-"+y[v[w++]]+y[v[w++]]+y[v[w++]]+y[v[w++]]+y[v[w++]]+y[v[w++]]}var f=l();var s=[f[0]|1,f[1],f[2],f[3],f[4],f[5]];var t=(f[6]<<8|f[7])&16383;var b=0,p=0;function d(H,x,B){var C=x&&B||0;var D=x||[];H=H||{};var A=H.clockseq!==undefined?H.clockseq:t;var v=H.msecs!==undefined?H.msecs:new Date().getTime();var G=H.nsecs!==undefined?H.nsecs:p+1;var w=(v-b)+(G-p)/10000;if(w<0&&H.clockseq===undefined){A=A+1&16383}if((w<0||v>b)&&H.nsecs===undefined){G=0}if(G>=10000){throw new Error("uuid.v1(): Can't create more than 10M uuids/sec")}b=v;p=G;t=A;v+=12219292800000;var F=((v&268435455)*10000+G)%4294967296;D[C++]=F>>>24&255;D[C++]=F>>>16&255;D[C++]=F>>>8&255;D[C++]=F&255;var E=(v/4294967296*10000)&268435455; +D[C++]=E>>>8&255;D[C++]=E&255;D[C++]=E>>>24&15|16;D[C++]=E>>>16&255;D[C++]=A>>>8|128;D[C++]=A&255;var z=H.node||s;for(var y=0;y<6;y++){D[C+y]=z[y]}return x?x:m(D)}function a(w,v,A){var x=v&&A||0;if(typeof(w)=="string"){v=w=="binary"?new o(16):null;w=null}w=w||{};var z=w.random||(w.rng||l)();z[6]=(z[6]&15)|64;z[8]=(z[8]&63)|128;if(v){for(var y=0;y<16;y++){v[x+y]=z[y]}}return v||m(z)}var q=a;q.v1=d;q.v4=a;q.parse=n;q.unparse=m;q.BufferClass=o;c.exports=q},{"./buffer":12,"./rng":13}],15:[function(b,c,a){arguments[4][13][0].apply(a,arguments)},{dup:13}],16:[function(c,b,h){var d=c("./rng");var r=[];var j={};for(var l=0;l<256;l++){r[l]=(l+256).toString(16).substr(1);j[r[l]]=l}function f(x,u,y){var v=(u&&y)||0,w=0;u=u||[];x.toLowerCase().replace(/[0-9a-f]{2}/g,function(i){if(w<16){u[v+w++]=j[i]}});while(w<16){u[v+w++]=0}return u}function n(u,w){var v=w||0,x=r;return x[u[v++]]+x[u[v++]]+x[u[v++]]+x[u[v++]]+"-"+x[u[v++]]+x[u[v++]]+"-"+x[u[v++]]+x[u[v++]]+"-"+x[u[v++]]+x[u[v++]]+"-"+x[u[v++]]+x[u[v++]]+x[u[v++]]+x[u[v++]]+x[u[v++]]+x[u[v++]] +}var t=d();var q=[t[0]|1,t[1],t[2],t[3],t[4],t[5]];var g=(t[6]<<8|t[7])&16383;var o=0,m=0;function s(G,w,A){var B=w&&A||0;var C=w||[];G=G||{};var z=G.clockseq!==undefined?G.clockseq:g;var u=G.msecs!==undefined?G.msecs:new Date().getTime();var F=G.nsecs!==undefined?G.nsecs:m+1;var v=(u-o)+(F-m)/10000;if(v<0&&G.clockseq===undefined){z=z+1&16383}if((v<0||u>o)&&G.nsecs===undefined){F=0}if(F>=10000){throw new Error("uuid.v1(): Can't create more than 10M uuids/sec")}o=u;m=F;g=z;u+=12219292800000;var E=((u&268435455)*10000+F)%4294967296;C[B++]=E>>>24&255;C[B++]=E>>>16&255;C[B++]=E>>>8&255;C[B++]=E&255;var D=(u/4294967296*10000)&268435455;C[B++]=D>>>8&255;C[B++]=D&255;C[B++]=D>>>24&15|16;C[B++]=D>>>16&255;C[B++]=z>>>8|128;C[B++]=z&255;var y=G.node||q;for(var x=0;x<6;x++){C[B+x]=y[x]}return w?w:n(C)}function p(v,u,z){var w=u&&z||0;if(typeof(v)=="string"){u=v=="binary"?new Array(16):null;v=null}v=v||{};var y=v.random||(v.rng||d)();y[6]=(y[6]&15)|64;y[8]=(y[8]&63)|128;if(u){for(var x=0;x<16;x++){u[w+x]=y[x] +}}return u||n(y)}var a=p;a.v1=s;a.v4=p;a.parse=f;a.unparse=n;b.exports=a},{"./rng":15}],17:[function(c,d,a){var g=c("./lib_managed/lodash"),f=c("./lib/helpers"),b=typeof a!=="undefined"?a:this;b.getFormTrackingManager=function(m,l,o){var r=["textarea","input","select"];var h=l+"form";var s=function(u){return true};var j=function(u){return true};function q(u){return u[g.find(["name","id","type","nodeName"],function(v){return u[v]&&typeof u[v]==="string"})]}function t(u){while(u&&u.nodeName&&u.nodeName.toUpperCase()!=="HTML"&&u.nodeName.toUpperCase()!=="FORM"){u=u.parentNode}if(u&&u.nodeName&&u.nodeName.toUpperCase()==="FORM"){return q(u)}}function i(v){var u=[];g.forEach(r,function(w){var x=g.filter(v.getElementsByTagName(w),function(y){return y.hasOwnProperty(h)});g.forEach(x,function(z){if(z.type==="submit"){return}var y={name:q(z),value:z.value,nodeName:z.nodeName};if(z.type&&z.nodeName.toUpperCase()==="INPUT"){y.type=z.type}if((z.type==="checkbox"||z.type==="radio")&&!z.checked){y.value=null +}u.push(y)})});return u}function n(u){return function(y){var v=y.target;var w=(v.nodeName&&v.nodeName.toUpperCase()==="INPUT")?v.type:null;var x=(v.type==="checkbox"&&!v.checked)?null:v.value;m.trackFormChange(t(v),q(v),v.nodeName,w,f.getCssClasses(v),x,o(u))}}function p(u){return function(x){var w=x.target;var v=i(w);m.trackFormSubmission(q(w),f.getCssClasses(w),v,o(u))}}return{configureFormTracking:function(u,v){if(u){s=f.getFilter(u.forms,true);j=f.getFilter(u.fields,false)}},addFormListeners:function(u){g.forEach(document.getElementsByTagName("form"),function(v){if(s(v)&&!v[h]){g.forEach(r,function(w){g.forEach(v.getElementsByTagName(w),function(x){if(j(x)&&!x[h]){f.addEventListener(x,"change",n(u),false);x[h]=true}})});f.addEventListener(v,"submit",p(u));v[h]=true}})}}}},{"./lib/helpers":21,"./lib_managed/lodash":23}],18:[function(b,c,a){(function(){var h=b("./lib_managed/lodash"),g=b("./lib/helpers"),f=b("uuid"),d=typeof a!=="undefined"?a:this;d.InQueueManager=function(j,r,q,n,s){var o=f.v4(),l={}; +function u(y){var z=[];if(!y||y.length===0){z=h.map(l)}else{for(var x=0;x1)?y[1].split(";"):[];return[i,x]}function v(){var z,y,B,A,x,E,C,D;for(z=0;z0){g=c.GlobalSnowplowNamespace.shift();a=c[g];a.q=new h.Snowplow(a.q,g)}else{c._snaq=c._snaq||[];c._snaq=new h.Snowplow(c._snaq,"_snaq")}},{"./snowplow":26}],20:[function(b,c,a){(function(){var n=b("../lib_managed/lodash"),m=b("murmurhash").v3,h=b("jstimezonedetect").jstz.determine(),f=b("browser-cookie-lite"),i=typeof a!=="undefined"?a:this,l=window,d=navigator,j=screen,g=document;i.hasSessionStorage=function(){try{return !!l.sessionStorage}catch(o){return true}};i.hasLocalStorage=function(){try{return !!l.localStorage}catch(o){return true}};i.localStorageAccessible=function(){var o="modernizr";if(!i.hasLocalStorage()){return false}try{l.localStorage.setItem(o,o);l.localStorage.removeItem(o);return true}catch(p){return false}};i.hasCookies=function(o){var p=o||"testcookie";if(n.isUndefined(d.cookieEnabled)){f.cookie(p,"1"); +return f.cookie(p)==="1"?"1":"0"}return d.cookieEnabled?"1":"0"};i.detectSignature=function(t){var r=[d.userAgent,[j.height,j.width,j.colorDepth].join("x"),(new Date()).getTimezoneOffset(),i.hasSessionStorage(),i.hasLocalStorage()];var o=[];if(d.plugins){for(var s=0;s-1)}})}}bd.pop();bc.pop();if(a4){az(bd);az(bc)}return a5}function F(aZ,aW,a0,aV,a5,aT){var aY=aW&1,a6=aW&2,a3=aW&4,a2=aW&8,aU=aW&16,a1=aW&32;if(!a6&&!aA(aZ)){throw new TypeError}if(aU&&!a0.length){aW&=~16;aU=a0=false}if(a1&&!aV.length){aW&=~32;a1=aV=false +}var aX=aZ&&aZ.__bindData__;if(aX&&aX!==true){aX=z(aX);if(aX[2]){aX[2]=z(aX[2])}if(aX[3]){aX[3]=z(aX[3])}if(aY&&!(aX[1]&1)){aX[4]=a5}if(!aY&&aX[1]&1){aW|=8}if(a3&&!(aX[1]&4)){aX[5]=aT}if(aU){i.apply(aX[2]||(aX[2]=[]),a0)}if(a1){aH.apply(aX[3]||(aX[3]=[]),aV)}aX[1]|=aW;return F.apply(null,aX)}var a4=(aW==1||aW===17)?G:Q;return a4([aZ,aW,a0,aV,a5,aT])}function ar(){U.shadowedProps=K;U.array=U.bottom=U.loop=U.top="";U.init="iterable";U.useHas=true;for(var aW,aV=0;aW=arguments[aV];aV++){for(var aX in aW){U[aX]=aW[aX]}}var aU=U.args;U.firstArg=/^[^,]+/.exec(aU)[0];var aT=Function("baseCreateCallback, errorClass, errorProto, hasOwnProperty, indicatorObject, isArguments, isArray, isString, keys, objectProto, objectTypes, nonEnumProps, stringClass, stringProto, toString","return function("+aU+") {\n"+ab(U)+"\n}");return aT(aS,aL,l,ad,aa,g,f,aJ,U.keys,aF,am,au,R,Z,al)}function O(aT){return typeof aT=="function"&&q.test(aT)}var aM=!w?aw:function(aT,aU){aI.value=aU;w(aT,"__bindData__",aI)};function g(aT){return aT&&typeof aT=="object"&&typeof aT.length=="number"&&al.call(aT)==j||false +}if(!aG.argsClass){g=function(aT){return aT&&typeof aT=="object"&&typeof aT.length=="number"&&ad.call(aT,"callee")&&!ac.call(aT,"callee")||false}}var f=t||function(aT){return aT&&typeof aT=="object"&&typeof aT.length=="number"&&al.call(aT)==H||false};var V=ar({args:"object",init:"[]",top:"if (!(objectTypes[typeof object])) return result",loop:"result.push(index)"});var J=!aO?V:function(aT){if(!E(aT)){return[]}if((aG.enumPrototypes&&typeof aT=="function")||(aG.nonEnumArgs&&aT.length&&g(aT))){return V(aT)}return aO(aT)};var aK={args:"collection, callback, thisArg",top:"callback = callback && typeof thisArg == 'undefined' ? callback : baseCreateCallback(callback, thisArg, 3)",array:"typeof length == 'number'",keys:J,loop:"if (callback(iterable[index], index, collection) === false) return result"};var I={args:"object, source, guard",top:"var args = arguments,\n argsIndex = 0,\n argsLength = typeof guard == 'number' ? 2 : args.length;\nwhile (++argsIndex < argsLength) {\n iterable = args[argsIndex];\n if (iterable && objectTypes[typeof iterable]) {",keys:J,loop:"if (typeof result[index] == 'undefined') result[index] = iterable[index]",bottom:" }\n}"}; +var ak={top:"if (!objectTypes[typeof iterable]) return result;\n"+aK.top,array:false};var L=ar(aK);var aC=ar(I,{top:I.top.replace(";",";\nif (argsLength > 3 && typeof args[argsLength - 2] == 'function') {\n var callback = baseCreateCallback(args[--argsLength - 1], args[argsLength--], 2);\n} else if (argsLength > 2 && typeof args[argsLength - 1] == 'function') {\n callback = args[--argsLength];\n}"),loop:"result[index] = callback ? callback(result[index], iterable[index]) : iterable[index]"});function B(aV,aU,aW,aT){if(typeof aU!="boolean"&&aU!=null){aT=aW;aW=aU;aU=false}return ao(aV,aU,typeof aW=="function"&&aS(aW,aT,1))}var ai=ar(aK,ak,{useHas:false});var Y=ar(aK,ak);function m(aT){return aT&&typeof aT=="object"&&al.call(aT)==r||false}function p(aW){var aT=true;if(!aW){return aT}var aU=al.call(aW),aV=aW.length;if((aU==H||aU==R||(aG.argsClass?aU==j:g(aW)))||(aU==av&&typeof aV=="number"&&aA(aW.splice))){return !aV}Y(aW,function(){return(aT=false)});return aT}function aA(aT){return typeof aT=="function" +}if(aA(/x/)){aA=function(aT){return typeof aT=="function"&&al.call(aT)==u}}function E(aT){return !!(aT&&am[typeof aT])}function aQ(aT){return aT===null}function aJ(aT){return typeof aT=="string"||aT&&typeof aT=="object"&&al.call(aT)==R||false}function M(aT){return typeof aT=="undefined"}function ah(aV,aW,aU){var aT={};aW=A.createCallback(aW,aU,3);Y(aV,function(aZ,aY,aX){aT[aY]=aW(aZ,aY,aX)});return aT}function D(aY,aZ,aU){var aT=[];aZ=A.createCallback(aZ,aU,3);if(f(aY)){var aV=-1,aW=aY.length;while(++aV2?F(aU,17,z(arguments,2),null,aT):F(aU,1,null,null,aT)}function o(aY,aU,aZ){var aX=typeof aY;if(aY==null||aX=="function"){return aS(aY,aU,aZ)}if(aX!="object"){return aq(aY)}var aW=J(aY),aV=aW[0],aT=aY[aV];if(aW.length==1&&aT===aT&&!E(aT)){return function(a1){var a0=a1[aV];return aT===a0&&(aT!==0||(1/aT==1/a0))}}return function(a1){var a2=aW.length,a0=false;while(a2--){if(!(a0=aB(a1[aW[a2]],aY[aW[a2]],null,true))){break}}return a0}}function T(aT){return aT}function aw(){}function aq(aT){return function(aU){return aU[aT]}}A.assign=aC;A.bind=aE;A.compact=C;A.createCallback=o;A.filter=D;A.forEach=ax;A.forIn=ai; +A.forOwn=Y;A.keys=J;A.map=x;A.mapValues=ah;A.property=aq;A.collect=x;A.each=ax;A.extend=aC;A.select=D;A.clone=B;A.find=aR;A.identity=T;A.isArguments=g;A.isArray=f;A.isDate=m;A.isEmpty=p;A.isFunction=aA;A.isNull=aQ;A.isObject=E;A.isString=aJ;A.isUndefined=M;A.noop=aw;A.detect=aR;A.findWhere=aR;A.VERSION="2.4.1";if(v&&N){if(aN){(N.exports=A)._=A}}}.call(this))}).call(this,typeof global!=="undefined"?global:typeof self!=="undefined"?self:typeof window!=="undefined"?window:{})},{}],24:[function(c,d,a){var g=c("./lib_managed/lodash"),f=c("./lib/helpers"),b=typeof a!=="undefined"?a:this;b.getLinkTrackingManager=function(p,j,s){var i,h,o,t,n,q;function u(x,w){var E,G,C,D,B,F;while((E=x.parentNode)!==null&&!g.isUndefined(E)&&((G=x.tagName.toUpperCase())!=="A"&&G!=="AREA")){x=E}if(!g.isUndefined(x.href)){var A=x.hostname||f.getHostName(x.href),y=A.toLowerCase(),v=x.href.replace(A,y),z=new RegExp("^(javascript|vbscript|jscript|mocha|livescript|ecmascript|mailto):","i");if(!z.test(v)){C=x.id;D=f.getCssClasses(x); +B=x.target;F=o?x.innerHTML:null;v=unescape(v);p.trackLinkClick(v,C,D,B,F,s(w))}}}function r(v){return function(w){var x,y;w=w||window.event;x=w.which||w.button;y=w.target||w.srcElement;if(w.type==="click"){if(y){u(y,v)}}else{if(w.type==="mousedown"){if((x===1||x===2)&&y){n=x;q=y}else{n=q=null}}else{if(w.type==="mouseup"){if(x===n&&y===q){u(y,v)}n=q=null}}}}}function m(v){if(h){f.addEventListener(v,"mouseup",r(t),false);f.addEventListener(v,"mousedown",r(t),false)}else{f.addEventListener(v,"click",r(t),false)}}function l(w,y){var v=f.getCssClasses(w),x;for(x=0;x1){u.bufferFlushers.push(function(){if(!j){A()}})}function q(J){var F="?",G={co:true,cx:true},E=true;for(var I in J){if(J.hasOwnProperty(I)&&!(G.hasOwnProperty(I))){if(!E){F+="&"}else{E=false}F+=encodeURIComponent(I)+"="+encodeURIComponent(J[I])}}for(var H in G){if(J.hasOwnProperty(H)&&G.hasOwnProperty(H)){F+="&"+H+"="+encodeURIComponent(J[H])}}return F}function C(E){var F=i.mapValues(E,function(G){return G.toString()});return{evt:F,bytes:D(g.stringify(F))}}function D(G){var E=0;for(var F=0;F=55296&&H<=57343){E+=4;F++}else{if(H<65535){E+=3}else{E+=4 +}}}}}return E}function v(H,G){x=G+t;if(y){var E=C(H);if(E.bytes>=m){h.warn("Event of size "+E.bytes+" is too long - the maximum size is "+m);var I=p(x);I.send(r([E.evt]));return}else{l.push(E)}}else{l.push(q(H))}var F=false;if(B){F=h.attemptWriteLocalStorage(n,g.stringify(l))}if(!j&&(!F||l.length>=o)){A()}}function A(){while(l.length&&typeof l[0]!=="string"&&typeof l[0]!=="object"){l.shift()}if(l.length<1){j=false;return}if(!i.isString(x)){throw"No Snowplow collector configured, cannot track"}j=true;var I=l[0];if(y){var J=p(x);var E=setTimeout(function(){J.abort();j=false},5000);function F(L){var N=0;var M=0;while(N=m){break}else{N+=1}}return N}var K=F(l);J.onreadystatechange=function(){if(J.readyState===4&&J.status>=200&&J.status<400){for(var L=0;L=400){clearTimeout(E);j=false}}};var G=i.map(l.slice(0,K),function(L){return L.evt});if(G.length>0){J.send(r(G)) +}}else{var H=new Image(1,1);H.onload=function(){l.shift();if(B){h.attemptWriteLocalStorage(n,g.stringify(l))}A()};H.onerror=function(){j=false};H.src=x+I}}function p(E){var F=new XMLHttpRequest();F.open("POST",E,true);F.withCredentials=true;F.setRequestHeader("Content-Type","application/json; charset=UTF-8");return F}function r(E){return g.stringify({schema:"iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-3",data:E})}return{enqueueRequest:v,executeQueue:A}}}())},{"./lib/detectors":20,"./lib/helpers":21,"./lib_managed/lodash":23,JSON:1}],26:[function(b,c,a){(function(){var i=b("./lib_managed/lodash"),h=b("./lib/helpers"),d=b("./in_queue"),g=b("./tracker"),f=typeof a!=="undefined"?a:this;f.Snowplow=function(m,r){var l=document,n=window,p="js-2.5.3",o={outQueues:[],bufferFlushers:[],expireDateTime:null,hasLoaded:false,registeredOnLoadHandlers:[]};function q(){var t;i.forEach(o.bufferFlushers,function(u){u()});if(o.expireDateTime){do{t=new Date();if(i.filter(o.outQueues,function(u){return u.length>0 +}).length===0){break}}while(t.getTime()=0){bj=bj.slice(0,bi)}if((bi=bj.lastIndexOf("/"))!==bj.length-1){bj=bj.slice(0,bi+1)}return bj+bh}function ae(bj,bi){var bh=new Date();if(!a0){a3.enqueueRequest(bj.build(),x);B.expireDateTime=bh.getTime()+bi}}function N(bh){return a7+bh+"."+aJ}function ag(bh){return h.cookie(N(bh))}function az(){bf();aJ=ab((H||aT)+(a8||"/")).slice(0,4)}function aU(){var bh=new Date();aF=bh.getTime()}function aN(){ap();aU()}function au(){var bh=(am.compatMode&&am.compatMode!="BackCompat")?am.documentElement:am.body;return[bh.scrollLeft||ac.pageXOffset,bh.scrollTop||ac.pageYOffset]}function ay(){var bi=au();var bh=bi[0];aB=bh;ar=bh;var bj=bi[1]; +an=bj;Z=bj}function ap(){var bi=au();var bh=bi[0];if(bhar){ar=bh}}var bj=bi[1];if(bjZ){Z=bj}}}function U(bi){var bh=Math.round(bi);if(!isNaN(bh)){return bh}}function al(){h.cookie(N("ses"),"*",O,a8,H)}function a5(bj,bi,bh,bl,bk,bm){h.cookie(N("id"),bj+"."+bi+"."+bh+"."+bl+"."+bk+"."+bm,K,a8,H)}function aA(){return ab((S.userAgent||"")+(S.platform||"")+l.stringify(aX)+Math.round(new Date().getTime()/1000)).slice(0,16)}function A(){var bi=ax&&!!ag("ses");var bh=aO();if(bh[1]){aj=bh[1]}else{aj=aA();bh[1]=aj}W=bh[6];if(!bi){bh[3]++;W=d.v4();bh[6]=W;bh[5]=bh[4]}if(ax){al();bh[4]=Math.round(new Date().getTime()/1000);bh.shift();a5.apply(null,bh)}}function aO(){if(!ax){return[]}var bi=new Date(),bh=Math.round(bi.getTime()/1000),bk=ag("id"),bj;if(bk){bj=bk.split(".");bj.unshift("0")}else{bj=["1",aj,bh,0,bh,""]}if(!bj[6]){bj[6]=d.v4()}return bj}function L(bo){var bj=Math.round(new Date().getTime()/1000),bm=N("id"),bl=N("ses"),bq=ag("ses"),bi=aO(),br=bi[0],bp=bi[1],bt=bi[2],bk=bi[3],bs=bi[4],bh=bi[5],bn=bi[6]; +if(a0&&ax){h.cookie(bm,"",-1,a8,H);h.cookie(bl,"",-1,a8,H);return}if(br==="0"){W=bn;if(!bq&&ax){bk++;bh=bs;W=d.v4()}V=bk}else{if((new Date().getTime()-a6)>O*1000){W=d.v4();V++}}bo.add("vp",q.detectViewport());bo.add("ds",q.detectDocumentSize());bo.add("vid",V);bo.add("sid",W);bo.add("duid",bp);bo.add("fp",y);bo.add("uid",bg);bf();bo.add("refr",a1(af||aG));bo.add("url",a1(aq||ba));if(ax){a5(bp,bt,V,bj,bh,W);al()}a6=new Date().getTime()}function ad(bh){return aR(bh+".cloudfront.net")}function aR(bh){if(aL){return("https://"+bh)}else{return("https:"===am.location.protocol?"https":"http")+"://"+bh}}function a4(bh){var bj=bd.concat(bh||[]);if(bc.performanceTiming){var bi=P();if(bi){bj.push(bi)}}return bj}function aV(){return{schema:"iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0",data:{id:a2}}}function P(){var bj=ac.performance||ac.mozPerformance||ac.msPerformance||ac.webkitPerformance;if(bj){var bi={};for(var bh in bj.timing){if(!s.isFunction(bj.timing[bh])){bi[bh]=bj.timing[bh] +}}delete bi.requestEnd;if(ac.chrome&&ac.chrome.loadTimes&&typeof ac.chrome.loadTimes().firstPaintTime==="number"){bi.chromeFirstPaint=Math.round(ac.chrome.loadTimes().firstPaintTime*1000)}return{schema:"iglu:org.w3/PerformanceTiming/jsonschema/1-0-0",data:bi}}}function a9(){if(!aP&&S.geolocation&&S.geolocation.getCurrentPosition){aP=true;navigator.geolocation.getCurrentPosition(function(bh){var bj=bh.coords;var bi={schema:"iglu:com.snowplowanalytics.snowplow/geolocation_context/jsonschema/1-1-0",data:{latitude:bj.latitude,longitude:bj.longitude,latitudeLongitudeAccuracy:bj.accuracy,altitude:bj.altitude,altitudeAccuracy:bj.altitudeAccuracy,bearing:bj.heading,speed:bj.speed,timestamp:bh.timestamp}};bd.push(bi)})}}function G(){var bh={};s.forEach(["__utma","__utmb","__utmc","__utmv","__utmz","_ga"],function(bi){var bj=h.cookie(bi);if(bj){bh[bi]=bj}});return{schema:"iglu:com.google.analytics/cookies/jsonschema/1-0-0",data:bh}}function aC(bi,bh){return(bi||[]).concat(bh?bh():[])}function aw(bm,bj,bh){bf(); +aI=am.title;C=bm;var bl=g.fixupTitle(C||aI);z.trackPageView(a1(aq||ba),bl,a1(af||aG),a4(aC(bj,bh)));var bi=new Date();if(E&&R&&!aH){aH=true;ay();g.addEventListener(am,"click",aU);g.addEventListener(am,"mouseup",aU);g.addEventListener(am,"mousedown",aU);g.addEventListener(am,"mousemove",aU);g.addEventListener(am,"mousewheel",aU);g.addEventListener(ac,"DOMMouseScroll",aU);g.addEventListener(ac,"scroll",aN);g.addEventListener(am,"keypress",aU);g.addEventListener(am,"keydown",aU);g.addEventListener(am,"keyup",aU);g.addEventListener(ac,"resize",aU);g.addEventListener(ac,"focus",aU);g.addEventListener(ac,"blur",aU);aF=bi.getTime();setInterval(function bk(){var bn=new Date();if((aF+R)>bn.getTime()){if(E Date: Wed, 11 Nov 2015 16:30:30 -0800 Subject: [PATCH 2/3] ignore IntelliJ based IDE files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 9a14c1bf1..9afa544b1 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ aws.json # Vagrant .vagrant VERSION + +# IDEs +.idea From 58c1617e16aff4efc90b1ef5d7d2caaaf3b1d54f Mon Sep 17 00:00:00 2001 From: Owen Buckley Date: Wed, 11 Nov 2015 17:20:49 -0800 Subject: [PATCH 3/3] added bower.json file with main property --- bower.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 bower.json diff --git a/bower.json b/bower.json new file mode 100644 index 000000000..2472a8ef8 --- /dev/null +++ b/bower.json @@ -0,0 +1,12 @@ +{ + "name": "snowplow-tracker", + "version": "2.5.3", + "repository": { + "type": "git", + "url": "git@github.com:ThomsonReuters-IPS/snowplow-javascript-tracker.git" + }, + "license": "Simplified BSD", + "main":[ + "dist/sp.js" + ] +}