2739 lines
87 KiB
JavaScript
Executable file
2739 lines
87 KiB
JavaScript
Executable file
/*
|
|
* JavaScript tracker for Snowplow: tracker.js
|
|
*
|
|
* Significant portions copyright 2010 Anthon Pang. Remainder copyright
|
|
* 2012-2016 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
|
|
forEach = require('lodash/forEach'),
|
|
map = require('lodash/map'),
|
|
helpers = require('./lib/helpers'),
|
|
proxies = require('./lib/proxies'),
|
|
cookie = require('browser-cookie-lite'),
|
|
detectors = require('./lib/detectors'),
|
|
sha1 = require('sha1'),
|
|
links = require('./links'),
|
|
forms = require('./forms'),
|
|
errors = require('./errors'),
|
|
requestQueue = require('./out_queue'),
|
|
coreConstructor = require('snowplow-tracker-core').trackerCore,
|
|
productionize = require('./guard').productionize,
|
|
uuid = require('uuid'),
|
|
|
|
object = typeof exports !== 'undefined' ? exports : this; // For eventual node.js environment support
|
|
|
|
/**
|
|
* Snowplow Tracker class
|
|
*
|
|
* @param functionName global function name
|
|
* @param namespace The namespace of the tracker object
|
|
* @param version The current version of the JavaScript Tracker
|
|
* @param mutSnowplowState An object containing hasLoaded, registeredOnLoadHandlers, and expireDateTime
|
|
* Passed in by reference in case they are altered by snowplow.js
|
|
* @param argmap Optional dictionary of configuration options. Supported fields and their default values:
|
|
*
|
|
* 1. encodeBase64, true
|
|
* 2. cookieDomain, null
|
|
* 3. cookieName, '_sp_'
|
|
* 4. appId, ''
|
|
* 5. platform, 'web'
|
|
* 6. respectDoNotTrack, false
|
|
* 7. userFingerprintSeed, 123412414
|
|
* 8. pageUnloadTimer, 500
|
|
* 9. forceSecureTracker, false
|
|
* 10. forceUnsecureTracker, false
|
|
* 11. useLocalStorage, true
|
|
* 12. useCookies, true
|
|
* 13. sessionCookieTimeout, 1800
|
|
* 14. contexts, {}
|
|
* 15. eventMethod, 'beacon'
|
|
* 16. post, false *DEPRECATED use eventMethod instead*
|
|
* 17. postPath, null
|
|
* 18. bufferSize, 1
|
|
* 19. crossDomainLinker, false
|
|
* 20. maxPostBytes, 40000
|
|
* 21. discoverRootDomain, false
|
|
* 22. cookieLifetime, 63072000
|
|
* 23. stateStorageStrategy, 'cookieAndLocalStorage'
|
|
*/
|
|
object.Tracker = function Tracker(functionName, namespace, version, mutSnowplowState, argmap) {
|
|
|
|
/************************************************************
|
|
* Private members
|
|
************************************************************/
|
|
|
|
var argmap = argmap || {};
|
|
|
|
//use POST if that property is present on the argmap
|
|
if(argmap.hasOwnProperty('post')) {
|
|
argmap.eventMethod = argmap.post === true ? 'post' : 'get';
|
|
} else {
|
|
argmap.eventMethod = argmap.eventMethod || 'beacon'
|
|
}
|
|
|
|
var
|
|
// Tracker core
|
|
core = coreConstructor(true, function(payload) {
|
|
addBrowserData(payload);
|
|
sendRequest(payload, configTrackerPause);
|
|
}),
|
|
|
|
// Debug - whether to raise errors to console and log to console
|
|
// or silence all errors from public methods
|
|
debug = false,
|
|
|
|
// API functions of the tracker
|
|
apiMethods = {},
|
|
|
|
// Safe methods (i.e. ones that won't raise errors)
|
|
// These values should be guarded publicMethods
|
|
safeMethods = {},
|
|
|
|
// The client-facing methods returned from tracker IIFE
|
|
returnMethods = {},
|
|
|
|
// 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],
|
|
|
|
// Holder of the logPagePing interval
|
|
pagePingInterval,
|
|
|
|
customReferrer,
|
|
|
|
// 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,
|
|
|
|
// Custom path for post requests (to get around adblockers)
|
|
configPostPath = argmap.hasOwnProperty('postPath') ? argmap.postPath : '/com.snowplowanalytics.snowplow/tp2',
|
|
|
|
// 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,
|
|
|
|
// Whether appropriate values have been supplied to enableActivityTracking
|
|
activityTrackingEnabled = false,
|
|
|
|
// 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 || windowAlias.doNotTrack,
|
|
|
|
// Do Not Track
|
|
configDoNotTrack = argmap.hasOwnProperty('respectDoNotTrack') ? argmap.respectDoNotTrack && (dnt === 'yes' || dnt === '1') : false,
|
|
|
|
// Opt out of cookie tracking
|
|
configOptOutCookie,
|
|
|
|
// Count sites which are pre-rendered
|
|
configCountPreRendered,
|
|
|
|
// Life of the visitor cookie (in seconds)
|
|
configVisitorCookieTimeout = argmap.hasOwnProperty('cookieLifetime') ? argmap.cookieLifetime : 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,
|
|
|
|
// This forces the tracker to be HTTP even if the page is secure
|
|
forceUnsecureTracker = !forceSecureTracker && argmap.hasOwnProperty('forceUnsecureTracker') ? (argmap.forceUnsecureTracker === true) : false,
|
|
|
|
// Whether to use localStorage to store events between sessions while offline
|
|
useLocalStorage = argmap.hasOwnProperty('useLocalStorage') ? (
|
|
helpers.warn('argmap.useLocalStorage is deprecated. ' +
|
|
'Use argmap.stateStorageStrategy instead.'),
|
|
argmap.useLocalStorage
|
|
) : true,
|
|
|
|
// Whether to use cookies
|
|
configUseCookies = argmap.hasOwnProperty('useCookies') ? (
|
|
helpers.warn(
|
|
'argmap.useCookies is deprecated. Use argmap.stateStorageStrategy instead.'),
|
|
argmap.useCookies
|
|
) : true,
|
|
|
|
// Strategy defining how to store the state: cookie, localStorage or none
|
|
configStateStorageStrategy = argmap.hasOwnProperty('stateStorageStrategy') ?
|
|
argmap.stateStorageStrategy : (!configUseCookies && !useLocalStorage ?
|
|
'none' : (configUseCookies && useLocalStorage ?
|
|
'cookieAndLocalStorage' : (configUseCookies ? 'cookie' : 'localStorage'))),
|
|
|
|
// 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(
|
|
configStateStorageStrategy == 'cookie' ||
|
|
configStateStorageStrategy == 'cookieAndLocalStorage',
|
|
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 tracking unhandled exceptions
|
|
errorManager = errors.errorManager(core),
|
|
|
|
// Manager for local storage queue
|
|
outQueueManager = new requestQueue.OutQueueManager(
|
|
functionName,
|
|
namespace,
|
|
mutSnowplowState,
|
|
configStateStorageStrategy == 'localStorage' ||
|
|
configStateStorageStrategy == 'cookieAndLocalStorage',
|
|
argmap.eventMethod,
|
|
configPostPath,
|
|
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 = [],
|
|
|
|
// Enhanced Ecommerce Contexts to be added on every `trackEnhancedEcommerceAction` call
|
|
enhancedEcommerceContexts = [],
|
|
|
|
// Whether pageViewId should be regenerated after each trackPageView. Affect web_page context
|
|
preservePageViewId = false,
|
|
|
|
// Whether first trackPageView was fired and pageViewId should not be changed anymore until reload
|
|
pageViewSent = false;
|
|
|
|
if (argmap.hasOwnProperty('discoverRootDomain') && argmap.discoverRootDomain) {
|
|
configCookieDomain = helpers.findRootDomain();
|
|
}
|
|
|
|
if (autoContexts.gaCookies) {
|
|
commonContexts.push(getGaCookiesContext());
|
|
}
|
|
|
|
if (autoContexts.geolocation) {
|
|
enableGeolocationContext();
|
|
}
|
|
|
|
// Enable base 64 encoding for self-describing 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() {
|
|
var tstamp = new Date().getTime();
|
|
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 crossDomainLinker Function used to determine which links to decorate
|
|
*/
|
|
function decorateLinks(crossDomainLinker) {
|
|
for (var i=0; i<documentAlias.links.length; i++) {
|
|
var elt = documentAlias.links[i];
|
|
if (!elt.spDecorationEnabled && crossDomainLinker(elt)) {
|
|
helpers.addEventListener(elt, 'click', linkDecorationHandler, true);
|
|
helpers.addEventListener(elt, 'mousedown', linkDecorationHandler, true);
|
|
|
|
// Don't add event listeners more than once
|
|
elt.spDecorationEnabled = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Initializes an empty ecommerce
|
|
* transaction and line items
|
|
*/
|
|
function ecommerceTransactionTemplate() {
|
|
return {
|
|
transaction: {},
|
|
items: []
|
|
};
|
|
}
|
|
|
|
/*
|
|
* Removes hash tag from the URL
|
|
*
|
|
* URLs are purified before being recorded in the cookie,
|
|
* or before being sent as GET parameters
|
|
*/
|
|
function purify(url) {
|
|
var targetPattern;
|
|
|
|
if (configDiscardHashTag) {
|
|
targetPattern = new RegExp('#.*');
|
|
return url.replace(targetPattern, '');
|
|
}
|
|
return url;
|
|
}
|
|
|
|
/*
|
|
* Extract scheme/protocol from URL
|
|
*/
|
|
function getProtocolScheme(url) {
|
|
var e = new RegExp('^([a-z]+):'),
|
|
matches = e.exec(url);
|
|
|
|
return matches ? matches[1] : null;
|
|
}
|
|
|
|
/*
|
|
* Resolve relative reference
|
|
*
|
|
* Note: not as described in rfc3986 section 5.2
|
|
*/
|
|
function resolveRelativeReference(baseUrl, url) {
|
|
var protocol = getProtocolScheme(url),
|
|
i;
|
|
|
|
if (protocol) {
|
|
return url;
|
|
}
|
|
|
|
if (url.slice(0, 1) === '/') {
|
|
return getProtocolScheme(baseUrl) + '://' + helpers.getHostName(baseUrl) + url;
|
|
}
|
|
|
|
baseUrl = purify(baseUrl);
|
|
if ((i = baseUrl.indexOf('?')) >= 0) {
|
|
baseUrl = baseUrl.slice(0, i);
|
|
}
|
|
if ((i = baseUrl.lastIndexOf('/')) !== baseUrl.length - 1) {
|
|
baseUrl = baseUrl.slice(0, i + 1);
|
|
}
|
|
|
|
return baseUrl + url;
|
|
}
|
|
|
|
/*
|
|
* Send request
|
|
*/
|
|
function sendRequest(request, delay) {
|
|
var now = new Date();
|
|
|
|
// Set to true if Opt-out cookie is defined
|
|
var toOptoutByCookie;
|
|
if (configOptOutCookie) {
|
|
toOptoutByCookie = !!cookie.cookie(configOptOutCookie);
|
|
} else {
|
|
toOptoutByCookie = false;
|
|
}
|
|
|
|
if (!(configDoNotTrack || toOptoutByCookie)) {
|
|
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) {
|
|
var fullName = getSnowplowCookieName(cookieName);
|
|
if (configStateStorageStrategy == 'localStorage') {
|
|
return helpers.attemptGetLocalStorage(fullName);
|
|
} else if (configStateStorageStrategy == 'cookie' ||
|
|
configStateStorageStrategy == 'cookieAndLocalStorage') {
|
|
return cookie.cookie(fullName);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* 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() {
|
|
var cookieName = getSnowplowCookieName('ses');
|
|
var cookieValue = '*';
|
|
setCookie(cookieName, cookieValue, configSessionCookieTimeout);
|
|
}
|
|
|
|
/*
|
|
* 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) {
|
|
var cookieName = getSnowplowCookieName('id');
|
|
var cookieValue = _domainUserId + '.' + createTs + '.' + visitCount + '.' + nowTs +
|
|
'.' + lastVisitTs + '.' + sessionId;
|
|
setCookie(cookieName, cookieValue, configVisitorCookieTimeout);
|
|
}
|
|
|
|
/*
|
|
* Sets a cookie based on the storage strategy:
|
|
* - if 'localStorage': attemps to write to local storage
|
|
* - if 'cookie': writes to cookies
|
|
* - otherwise: no-op
|
|
*/
|
|
function setCookie(name, value, timeout) {
|
|
if (configStateStorageStrategy == 'localStorage') {
|
|
helpers.attemptWriteLocalStorage(name, value);
|
|
} else if (configStateStorageStrategy == 'cookie' ||
|
|
configStateStorageStrategy == 'cookieAndLocalStorage') {
|
|
cookie.cookie(name, value, timeout, configCookiePath, configCookieDomain);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a pseudo-unique ID to fingerprint this user
|
|
*/
|
|
function createNewDomainUserId() {
|
|
return uuid.v4();
|
|
}
|
|
|
|
/*
|
|
* Load the domain user ID and the session ID
|
|
* Set the cookies (if cookies are enabled)
|
|
*/
|
|
function initializeIdsAndCookies() {
|
|
var sesCookieSet =
|
|
configStateStorageStrategy != 'none' && !!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 (configStateStorageStrategy != 'none') {
|
|
setSessionCookie();
|
|
// Update currentVisitTs
|
|
idCookieComponents[4] = Math.round(new Date().getTime() / 1000);
|
|
idCookieComponents.shift();
|
|
setDomainUserIdCookie.apply(null, idCookieComponents);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Load visitor ID cookie
|
|
*/
|
|
function loadDomainUserIdCookie() {
|
|
if (configStateStorageStrategy == 'none') {
|
|
return [];
|
|
}
|
|
var now = new Date(),
|
|
nowTs = Math.round(now.getTime() / 1000),
|
|
id = getSnowplowCookieValue('id'),
|
|
tmpContainer;
|
|
|
|
if (id) {
|
|
tmpContainer = id.split('.');
|
|
// cookies enabled
|
|
tmpContainer.unshift('0');
|
|
} else {
|
|
|
|
tmpContainer = [
|
|
// cookies disabled
|
|
'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]) {
|
|
// session id
|
|
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'),
|
|
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];
|
|
|
|
var toOptoutByCookie;
|
|
if (configOptOutCookie) {
|
|
toOptoutByCookie = !!cookie.cookie(configOptOutCookie);
|
|
} else {
|
|
toOptoutByCookie = false;
|
|
}
|
|
|
|
|
|
if ((configDoNotTrack || toOptoutByCookie) &&
|
|
configStateStorageStrategy != 'none') {
|
|
if (configStateStorageStrategy == 'localStorage') {
|
|
helpers.attemptWriteLocalStorage(idname, '');
|
|
helpers.attemptWriteLocalStorage(sesname, '');
|
|
} else if (configStateStorageStrategy == 'cookie' ||
|
|
configStateStorageStrategy == 'cookieAndLocalStorage') {
|
|
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 && configStateStorageStrategy != 'none') {
|
|
// 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 (configStateStorageStrategy != 'none') {
|
|
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);
|
|
}
|
|
if (forceUnsecureTracker) {
|
|
return ('http' + '://' + rawUrl);
|
|
}
|
|
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.webPage) {
|
|
combinedContexts.push(getWebPageContext());
|
|
}
|
|
|
|
// Add PerformanceTiming Context
|
|
if (autoContexts.performanceTiming) {
|
|
var performanceTimingContext = getPerformanceTimingContext();
|
|
if (performanceTimingContext) {
|
|
combinedContexts.push(performanceTimingContext);
|
|
}
|
|
}
|
|
|
|
// Add Optimizely Contexts
|
|
if (windowAlias.optimizely) {
|
|
|
|
if (autoContexts.optimizelySummary) {
|
|
var activeExperiments = getOptimizelySummaryContexts();
|
|
forEach(activeExperiments, function (e) {
|
|
combinedContexts.push(e)
|
|
})
|
|
}
|
|
|
|
if (autoContexts.optimizelyXSummary) {
|
|
var activeExperiments = getOptimizelyXSummaryContexts();
|
|
forEach(activeExperiments, function (e) {
|
|
combinedContexts.push(e);
|
|
})
|
|
}
|
|
|
|
if (autoContexts.optimizelyExperiments) {
|
|
var experimentContexts = getOptimizelyExperimentContexts();
|
|
for (var i = 0; i < experimentContexts.length; i++) {
|
|
combinedContexts.push(experimentContexts[i]);
|
|
}
|
|
}
|
|
|
|
if (autoContexts.optimizelyStates) {
|
|
var stateContexts = getOptimizelyStateContexts();
|
|
for (var i = 0; i < stateContexts.length; i++) {
|
|
combinedContexts.push(stateContexts[i]);
|
|
}
|
|
}
|
|
|
|
if (autoContexts.optimizelyVariations) {
|
|
var variationContexts = getOptimizelyVariationContexts();
|
|
for (var i = 0; i < variationContexts.length; i++) {
|
|
combinedContexts.push(variationContexts[i]);
|
|
}
|
|
}
|
|
|
|
if (autoContexts.optimizelyVisitor) {
|
|
var optimizelyVisitorContext = getOptimizelyVisitorContext();
|
|
if (optimizelyVisitorContext) {
|
|
combinedContexts.push(optimizelyVisitorContext);
|
|
}
|
|
}
|
|
|
|
if (autoContexts.optimizelyAudiences) {
|
|
var audienceContexts = getOptimizelyAudienceContexts();
|
|
for (var i = 0; i < audienceContexts.length; i++) {
|
|
combinedContexts.push(audienceContexts[i]);
|
|
}
|
|
}
|
|
|
|
if (autoContexts.optimizelyDimensions) {
|
|
var dimensionContexts = getOptimizelyDimensionContexts();
|
|
for (var i = 0; i < dimensionContexts.length; i++) {
|
|
combinedContexts.push(dimensionContexts[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add Augur Context
|
|
if (autoContexts.augurIdentityLite) {
|
|
var augurIdentityLiteContext = getAugurIdentityLiteContext();
|
|
if (augurIdentityLiteContext) {
|
|
combinedContexts.push(augurIdentityLiteContext);
|
|
}
|
|
}
|
|
|
|
//Add Parrable Context
|
|
if (autoContexts.parrable) {
|
|
var parrableContext = getParrableContext();
|
|
if (parrableContext) {
|
|
combinedContexts.push(parrableContext);
|
|
}
|
|
}
|
|
return combinedContexts;
|
|
}
|
|
|
|
/**
|
|
* Initialize new `pageViewId` if it shouldn't be preserved.
|
|
* Should be called when `trackPageView` is invoked
|
|
*/
|
|
function resetPageView() {
|
|
if (!preservePageViewId || mutSnowplowState.pageViewId == null) {
|
|
mutSnowplowState.pageViewId = uuid.v4();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Safe function to get `pageViewId`.
|
|
* Generates it if it wasn't initialized by other tracker
|
|
*/
|
|
function getPageViewId() {
|
|
if (mutSnowplowState.pageViewId == null) {
|
|
mutSnowplowState.pageViewId = uuid.v4();
|
|
}
|
|
return mutSnowplowState.pageViewId
|
|
}
|
|
|
|
/**
|
|
* 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: getPageViewId()
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a context from the window.performance.timing object
|
|
*
|
|
* @return object PerformanceTiming context
|
|
*/
|
|
function getPerformanceTimingContext() {
|
|
var allowedKeys = [
|
|
'navigationStart', 'redirectStart', 'redirectEnd', 'fetchStart', 'domainLookupStart', 'domainLookupEnd', 'connectStart',
|
|
'secureConnectionStart', 'connectEnd', 'requestStart', 'responseStart', 'responseEnd', 'unloadEventStart', 'unloadEventEnd',
|
|
'domLoading', 'domInteractive', 'domContentLoadedEventStart', 'domContentLoadedEventEnd', 'domComplete', 'loadEventStart',
|
|
'loadEventEnd', 'msFirstPaint', 'chromeFirstPaint', 'requestEnd', 'proxyStart', 'proxyEnd'
|
|
];
|
|
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) {
|
|
if (helpers.isValueInArray(field, allowedKeys) && (performance.timing[field] !== null)) {
|
|
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
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check that *both* optimizely and optimizely.data exist and return
|
|
* optimizely.data.property
|
|
*
|
|
* @param property optimizely data property
|
|
* @param snd optional nested property
|
|
*/
|
|
function getOptimizelyData(property, snd) {
|
|
var data;
|
|
if (windowAlias.optimizely && windowAlias.optimizely.data) {
|
|
data = windowAlias.optimizely.data[property];
|
|
if (typeof snd !== 'undefined' && data !== undefined) {
|
|
data = data[snd]
|
|
}
|
|
}
|
|
return data
|
|
}
|
|
|
|
/**
|
|
* Check that *both* optimizely and optimizely.data exist
|
|
*
|
|
* @param property optimizely data property
|
|
* @param snd optional nested property
|
|
*/
|
|
function getOptimizelyXData(property, snd) {
|
|
var data;
|
|
if (windowAlias.optimizely) {
|
|
data = windowAlias.optimizely.get(property);
|
|
if (typeof snd !== 'undefined' && data !== undefined) {
|
|
data = data[snd]
|
|
}
|
|
}
|
|
return data
|
|
}
|
|
|
|
/**
|
|
* Get data for Optimizely "lite" contexts - active experiments on current page
|
|
*
|
|
* @returns Array content of lite optimizely lite context
|
|
*/
|
|
function getOptimizelySummary() {
|
|
var state = getOptimizelyData('state');
|
|
var experiments = getOptimizelyData('experiments');
|
|
|
|
return map(state && experiments && state.activeExperiments, function (activeExperiment) {
|
|
var current = experiments[activeExperiment];
|
|
return {
|
|
activeExperimentId: activeExperiment.toString(),
|
|
// User can be only in one variation (don't know why is this array)
|
|
variation: state.variationIdsMap[activeExperiment][0].toString(),
|
|
conditional: current && current.conditional,
|
|
manual: current && current.manual,
|
|
name: current && current.name
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get data for OptimizelyX contexts - active experiments on current page
|
|
*
|
|
* @returns Array content of lite optimizely lite context
|
|
*/
|
|
function getOptimizelyXSummary() {
|
|
var state = getOptimizelyXData('state');
|
|
var experiment_ids = state.getActiveExperimentIds();
|
|
var experiments = getOptimizelyXData('data', 'experiments');
|
|
var visitor = getOptimizelyXData('visitor');
|
|
|
|
return map(experiment_ids, function(activeExperiment) {
|
|
variation = state.getVariationMap()[activeExperiment];
|
|
variationName = variation.name;
|
|
variationId = variation.id;
|
|
visitorId = visitor.visitorId;
|
|
return {
|
|
experimentId: parseInt(activeExperiment),
|
|
variationName: variationName,
|
|
variation: parseInt(variationId),
|
|
visitorId: visitorId
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Creates a context from the window['optimizely'].data.experiments object
|
|
*
|
|
* @return Array Experiment contexts
|
|
*/
|
|
function getOptimizelyExperimentContexts() {
|
|
var experiments = getOptimizelyData('experiments');
|
|
if (experiments) {
|
|
var contexts = [];
|
|
|
|
for (var key in experiments) {
|
|
if (experiments.hasOwnProperty(key)) {
|
|
var context = {};
|
|
context.id = key;
|
|
var experiment = experiments[key];
|
|
context.code = experiment.code;
|
|
context.manual = experiment.manual;
|
|
context.conditional = experiment.conditional;
|
|
context.name = experiment.name;
|
|
context.variationIds = experiment.variation_ids;
|
|
|
|
contexts.push({
|
|
schema: 'iglu:com.optimizely/experiment/jsonschema/1-0-0',
|
|
data: context
|
|
});
|
|
}
|
|
}
|
|
return contexts;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Creates a context from the window['optimizely'].data.state object
|
|
*
|
|
* @return Array State contexts
|
|
*/
|
|
function getOptimizelyStateContexts() {
|
|
var experimentIds = [];
|
|
var experiments = getOptimizelyData('experiments');
|
|
if (experiments) {
|
|
for (var key in experiments) {
|
|
if (experiments.hasOwnProperty(key)) {
|
|
experimentIds.push(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
var state = getOptimizelyData('state');
|
|
if (state) {
|
|
var contexts = [];
|
|
var activeExperiments = state.activeExperiments || [];
|
|
|
|
for (var i = 0; i < experimentIds.length; i++) {
|
|
var experimentId = experimentIds[i];
|
|
var context = {};
|
|
context.experimentId = experimentId;
|
|
context.isActive = helpers.isValueInArray(experimentIds[i], activeExperiments);
|
|
var variationMap = state.variationMap || {};
|
|
context.variationIndex = variationMap[experimentId];
|
|
var variationNamesMap = state.variationNamesMap || {};
|
|
context.variationName = variationNamesMap[experimentId];
|
|
var variationIdsMap = state.variationIdsMap || {};
|
|
if (variationIdsMap[experimentId] && variationIdsMap[experimentId].length === 1) {
|
|
context.variationId = variationIdsMap[experimentId][0];
|
|
}
|
|
|
|
contexts.push({
|
|
schema: 'iglu:com.optimizely/state/jsonschema/1-0-0',
|
|
data: context
|
|
});
|
|
}
|
|
return contexts;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Creates a context from the window['optimizely'].data.variations object
|
|
*
|
|
* @return Array Variation contexts
|
|
*/
|
|
function getOptimizelyVariationContexts() {
|
|
var variations = getOptimizelyData('variations');
|
|
if (variations) {
|
|
var contexts = [];
|
|
|
|
for (var key in variations) {
|
|
if (variations.hasOwnProperty(key)) {
|
|
var context = {};
|
|
context.id = key;
|
|
var variation = variations[key];
|
|
context.name = variation.name;
|
|
context.code = variation.code;
|
|
|
|
contexts.push({
|
|
schema: 'iglu:com.optimizely/variation/jsonschema/1-0-0',
|
|
data: context
|
|
});
|
|
}
|
|
}
|
|
return contexts;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Creates a context from the window['optimizely'].data.visitor object
|
|
*
|
|
* @return object Visitor context
|
|
*/
|
|
function getOptimizelyVisitorContext() {
|
|
var visitor = getOptimizelyData('visitor');
|
|
if (visitor) {
|
|
var context = {};
|
|
context.browser = visitor.browser;
|
|
context.browserVersion = visitor.browserVersion;
|
|
context.device = visitor.device;
|
|
context.deviceType = visitor.deviceType;
|
|
context.ip = visitor.ip;
|
|
var platform = visitor.platform || {};
|
|
context.platformId = platform.id;
|
|
context.platformVersion = platform.version;
|
|
var location = visitor.location || {};
|
|
context.locationCity = location.city;
|
|
context.locationRegion = location.region;
|
|
context.locationCountry = location.country;
|
|
context.mobile = visitor.mobile;
|
|
context.mobileId = visitor.mobileId;
|
|
context.referrer = visitor.referrer;
|
|
context.os = visitor.os;
|
|
|
|
return {
|
|
schema: 'iglu:com.optimizely/visitor/jsonschema/1-0-0',
|
|
data: context
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a context from the window['optimizely'].data.visitor.audiences object
|
|
*
|
|
* @return Array VisitorAudience contexts
|
|
*/
|
|
function getOptimizelyAudienceContexts() {
|
|
var audienceIds = getOptimizelyData('visitor', 'audiences');
|
|
if (audienceIds) {
|
|
var contexts = [];
|
|
|
|
for (var key in audienceIds) {
|
|
if (audienceIds.hasOwnProperty(key)) {
|
|
var context = { id: key, isMember: audienceIds[key] };
|
|
|
|
contexts.push({
|
|
schema: 'iglu:com.optimizely/visitor_audience/jsonschema/1-0-0',
|
|
data: context
|
|
});
|
|
}
|
|
}
|
|
return contexts;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Creates a context from the window['optimizely'].data.visitor.dimensions object
|
|
*
|
|
* @return Array VisitorDimension contexts
|
|
*/
|
|
function getOptimizelyDimensionContexts() {
|
|
var dimensionIds = getOptimizelyData('visitor', 'dimensions');
|
|
if (dimensionIds) {
|
|
var contexts = [];
|
|
|
|
for (var key in dimensionIds) {
|
|
if (dimensionIds.hasOwnProperty(key)) {
|
|
var context = { id: key, value: dimensionIds[key] };
|
|
|
|
contexts.push({
|
|
schema: 'iglu:com.optimizely/visitor_dimension/jsonschema/1-0-0',
|
|
data: context
|
|
});
|
|
}
|
|
}
|
|
return contexts;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
|
|
/**
|
|
* Creates an Optimizely lite context containing only data required to join
|
|
* event to experiment data
|
|
*
|
|
* @returns Array of custom contexts
|
|
*/
|
|
function getOptimizelySummaryContexts() {
|
|
return map(getOptimizelySummary(), function (experiment) {
|
|
return {
|
|
schema: 'iglu:com.optimizely.snowplow/optimizely_summary/jsonschema/1-0-0',
|
|
data: experiment
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Creates an OptimizelyX context containing only data required to join
|
|
* event to experiment data
|
|
*
|
|
* @returns Array of custom contexts
|
|
*/
|
|
function getOptimizelyXSummaryContexts() {
|
|
return map(getOptimizelyXSummary(), function (experiment) {
|
|
return {
|
|
schema: 'iglu:com.optimizely.optimizelyx/summary/jsonschema/1-0-0',
|
|
data: experiment
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Creates a context from the window['augur'] object
|
|
*
|
|
* @return object The IdentityLite context
|
|
*/
|
|
function getAugurIdentityLiteContext() {
|
|
var augur = windowAlias.augur;
|
|
if (augur) {
|
|
var context = { consumer: {}, device: {} };
|
|
var consumer = augur.consumer || {};
|
|
context.consumer.UUID = consumer.UID;
|
|
var device = augur.device || {};
|
|
context.device.ID = device.ID;
|
|
context.device.isBot = device.isBot;
|
|
context.device.isProxied = device.isProxied;
|
|
context.device.isTor = device.isTor;
|
|
var fingerprint = device.fingerprint || {};
|
|
context.device.isIncognito = fingerprint.browserHasIncognitoEnabled;
|
|
|
|
return {
|
|
schema: 'iglu:io.augur.snowplow/identity_lite/jsonschema/1-0-0',
|
|
data: context
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a context from the window['_hawk'] object
|
|
*
|
|
* @return object The Parrable context
|
|
*/
|
|
function getParrableContext() {
|
|
var parrable = window['_hawk'];
|
|
if (parrable) {
|
|
var context = { encryptedId: null, optout: null };
|
|
context['encryptedId'] = parrable.browserid;
|
|
var regex = new RegExp('(?:^|;)\\s?' + "_parrable_hawk_optout".replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1') + '=(.*?)(?:;|$)', 'i'), match = document.cookie.match(regex);
|
|
context['optout'] = (match && decodeURIComponent(match[1])) ? match && decodeURIComponent(match[1]) : "false";
|
|
return {
|
|
schema: 'iglu:com.parrable/encrypted_payload/jsonschema/1-0-0',
|
|
data: context
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Expires current session and starts a new session.
|
|
*/
|
|
function newSession() {
|
|
// If cookies are enabled, base visit count and session ID on the cookies
|
|
var nowTs = Math.round(new Date().getTime() / 1000),
|
|
ses = getSnowplowCookieValue('ses'),
|
|
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];
|
|
|
|
// When cookies are enabled
|
|
if (cookiesDisabled === '0') {
|
|
memorizedSessionId = sessionIdFromCookie;
|
|
|
|
// When cookie/local storage is enabled - make a new session
|
|
if (configStateStorageStrategy != 'none') {
|
|
// New session (aka new visit)
|
|
visitCount++;
|
|
// Update the last visit timestamp
|
|
lastVisitTs = currentVisitTs;
|
|
// Regenerate the session ID
|
|
memorizedSessionId = uuid.v4();
|
|
}
|
|
|
|
memorizedVisitCount = visitCount;
|
|
|
|
// Create a new session cookie
|
|
setSessionCookie()
|
|
|
|
} else {
|
|
memorizedSessionId = uuid.v4();
|
|
memorizedVisitCount++;
|
|
}
|
|
|
|
// Update cookies
|
|
if (configStateStorageStrategy != 'none') {
|
|
setDomainUserIdCookie(_domainUserId, createTs, memorizedVisitCount, nowTs,
|
|
lastVisitTs, memorizedSessionId);
|
|
setSessionCookie();
|
|
}
|
|
|
|
lastEventTime = new Date().getTime();
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
navigatorAlias.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: Math.round(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 = {};
|
|
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 staticContexts Array of custom contexts
|
|
* @param contextCallback Function returning an array of contexts
|
|
*/
|
|
function finalizeContexts(staticContexts, contextCallback) {
|
|
return (staticContexts || []).concat(contextCallback ? contextCallback() : []);
|
|
}
|
|
|
|
/**
|
|
* Log the page view / visit
|
|
*
|
|
* @param customTitle string The user-defined page title to attach to this page view
|
|
* @param context object Custom context relating to the event
|
|
* @param contextCallback Function returning an array of contexts
|
|
* @param tstamp number
|
|
*/
|
|
function logPageView(customTitle, context, contextCallback, tstamp) {
|
|
|
|
refreshUrl();
|
|
if (pageViewSent) { // Do not reset pageViewId if previous events were not page_view
|
|
resetPageView();
|
|
}
|
|
pageViewSent = true;
|
|
|
|
// 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)),
|
|
tstamp);
|
|
|
|
// Send ping (to log that user has stayed on page)
|
|
var now = new Date();
|
|
|
|
if (activityTrackingEnabled && !activityTrackingInstalled) {
|
|
activityTrackingInstalled = true;
|
|
|
|
// Add mousewheel event handler, detect passive event listeners for performance
|
|
var detectPassiveEvents = {
|
|
update: function update() {
|
|
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
|
|
var passive = false;
|
|
var options = Object.defineProperty({}, 'passive', {
|
|
get: function get() {
|
|
passive = true;
|
|
}
|
|
});
|
|
// note: have to set and remove a no-op listener instead of null
|
|
// (which was used previously), becasue Edge v15 throws an error
|
|
// when providing a null callback.
|
|
// https://github.com/rafrex/detect-passive-events/pull/3
|
|
var noop = function noop() {
|
|
};
|
|
window.addEventListener('testPassiveEventSupport', noop, options);
|
|
window.removeEventListener('testPassiveEventSupport', noop, options);
|
|
detectPassiveEvents.hasSupport = passive;
|
|
}
|
|
}
|
|
};
|
|
detectPassiveEvents.update();
|
|
|
|
// Detect available wheel event
|
|
var wheelEvent = "onwheel" in document.createElement("div") ? "wheel" : // Modern browsers support "wheel"
|
|
document.onmousewheel !== undefined ? "mousewheel" : // Webkit and IE support at least "mousewheel"
|
|
"DOMMouseScroll"; // let's assume that remaining browsers are older Firefox
|
|
|
|
if (Object.prototype.hasOwnProperty.call(detectPassiveEvents, 'hasSupport')) {
|
|
helpers.addEventListener(documentAlias, wheelEvent, activityHandler, {passive: true});
|
|
} else {
|
|
helpers.addEventListener(documentAlias, wheelEvent, activityHandler);
|
|
}
|
|
|
|
// 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(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();
|
|
clearInterval(pagePingInterval);
|
|
pagePingInterval = 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 context object Custom context relating to the event
|
|
*/
|
|
function logPagePing(context) {
|
|
refreshUrl();
|
|
var 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
|
|
* @param tstamp number or Timestamp object
|
|
*/
|
|
function logTransaction(orderId, affiliation, total, tax, shipping, city, state, country, currency, context, tstamp) {
|
|
core.trackEcommerceTransaction(orderId, affiliation, total, tax, shipping, city, state, country, currency, addCommonContexts(context), tstamp);
|
|
}
|
|
|
|
/**
|
|
* 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, tstamp) {
|
|
core.trackEcommerceTransactionItem(orderId, sku, name, category, price, quantity, currency, addCommonContexts(context), tstamp);
|
|
}
|
|
|
|
/**
|
|
* Construct a browser prefix
|
|
*
|
|
* E.g: (moz, hidden) -> mozHidden
|
|
*/
|
|
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 configPrerendered == true - we'll never set `isPreRendered` to true and fire immediately,
|
|
// otherwise we need to check if this is just prerendered
|
|
if (!configCountPreRendered) { // true by default
|
|
|
|
for (i = 0; i < prefixes.length; i++) {
|
|
prefix = prefixes[i];
|
|
|
|
// does this browser support the page visibility API? (drop this check along with IE9 and iOS6)
|
|
if (documentAlias[prefixPropertyName(prefix, 'hidden')]) {
|
|
// if pre-rendered, then defer callback until page visibility changes
|
|
if (documentAlias[prefixPropertyName(prefix, 'visibilityState')] === 'prerender') {
|
|
isPreRendered = true;
|
|
}
|
|
break;
|
|
} else if (documentAlias[prefixPropertyName(prefix, 'hidden')] === false) { break }
|
|
}
|
|
}
|
|
|
|
// Implies configCountPreRendered = false
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* Update the returned methods (public facing methods)
|
|
*/
|
|
function updateReturnMethods() {
|
|
if (debug) {
|
|
returnMethods = apiMethods;
|
|
} else {
|
|
returnMethods = safeMethods;
|
|
}
|
|
}
|
|
|
|
/************************************************************
|
|
* Constructor
|
|
************************************************************/
|
|
|
|
/*
|
|
* Initialize tracker
|
|
*/
|
|
updateDomainHash();
|
|
|
|
initializeIdsAndCookies();
|
|
|
|
if (argmap.crossDomainLinker) {
|
|
decorateLinks(argmap.crossDomainLinker);
|
|
}
|
|
|
|
/************************************************************
|
|
* Public data and methods
|
|
************************************************************/
|
|
|
|
/**
|
|
* Get the domain session index also known as current memorized visit count.
|
|
*
|
|
* @return int Domain session index
|
|
*/
|
|
apiMethods.getDomainSessionIndex = function() {
|
|
return memorizedVisitCount;
|
|
};
|
|
|
|
/**
|
|
* Get the page view ID as generated or provided by mutSnowplowState.pageViewId.
|
|
*
|
|
* @return string Page view ID
|
|
*/
|
|
apiMethods.getPageViewId = function () {
|
|
return getPageViewId();
|
|
};
|
|
|
|
/**
|
|
* Expires current session and starts a new session.
|
|
*/
|
|
apiMethods.newSession = newSession;
|
|
|
|
/**
|
|
* Get the cookie name as cookieNamePrefix + basename + . + domain.
|
|
*
|
|
* @return string Cookie name
|
|
*/
|
|
apiMethods.getCookieName = function(basename) {
|
|
return getSnowplowCookieName(basename);
|
|
};
|
|
|
|
/**
|
|
* Get the current user ID (as set previously
|
|
* with setUserId()).
|
|
*
|
|
* @return string Business-defined user ID
|
|
*/
|
|
apiMethods.getUserId = function () {
|
|
return businessUserId;
|
|
};
|
|
|
|
/**
|
|
* Get visitor ID (from first party cookie)
|
|
*
|
|
* @return string Visitor ID in hexits (or null, if not yet known)
|
|
*/
|
|
apiMethods.getDomainUserId = function () {
|
|
return (loadDomainUserIdCookie())[1];
|
|
};
|
|
|
|
/**
|
|
* Get the visitor information (from first party cookie)
|
|
*
|
|
* @return array
|
|
*/
|
|
apiMethods.getDomainUserInfo = function () {
|
|
return loadDomainUserIdCookie();
|
|
};
|
|
|
|
/**
|
|
* Get the user fingerprint
|
|
*
|
|
* @return string The user fingerprint
|
|
*/
|
|
apiMethods.getUserFingerprint = function () {
|
|
return userFingerprint;
|
|
};
|
|
|
|
/**
|
|
* Specify the app ID
|
|
*
|
|
* @param int|string appId
|
|
*/
|
|
apiMethods.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
|
|
*/
|
|
apiMethods.setReferrerUrl = function (url) {
|
|
customReferrer = url;
|
|
};
|
|
|
|
/**
|
|
* Override url
|
|
*
|
|
* @param string url
|
|
*/
|
|
apiMethods.setCustomUrl = function (url) {
|
|
refreshUrl();
|
|
configCustomUrl = resolveRelativeReference(locationHrefAlias, url);
|
|
};
|
|
|
|
/**
|
|
* Override document.title
|
|
*
|
|
* @param string title
|
|
*/
|
|
apiMethods.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
|
|
*/
|
|
apiMethods.discardHashTag = function (enableFilter) {
|
|
configDiscardHashTag = enableFilter;
|
|
};
|
|
|
|
/**
|
|
* Set first-party cookie name prefix
|
|
*
|
|
* @param string cookieNamePrefix
|
|
*/
|
|
apiMethods.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
|
|
*/
|
|
apiMethods.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
|
|
*/
|
|
apiMethods.setCookiePath = function (path) {
|
|
configCookiePath = path;
|
|
updateDomainHash();
|
|
};
|
|
|
|
/**
|
|
* Set visitor cookie timeout (in seconds)
|
|
*
|
|
* @param int timeout
|
|
*/
|
|
apiMethods.setVisitorCookieTimeout = function (timeout) {
|
|
configVisitorCookieTimeout = timeout;
|
|
};
|
|
|
|
/**
|
|
* Set session cookie timeout (in seconds)
|
|
*
|
|
* @param int timeout
|
|
*/
|
|
apiMethods.setSessionCookieTimeout = function (timeout) {
|
|
helpers.warn('setSessionCookieTimeout is deprecated. Instead add a "sessionCookieTimeout" field to the argmap argument of newTracker.')
|
|
configSessionCookieTimeout = timeout;
|
|
};
|
|
|
|
/**
|
|
* @param number seed The seed used for MurmurHash3
|
|
*/
|
|
apiMethods.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
|
|
*/
|
|
apiMethods.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.
|
|
*/
|
|
apiMethods.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
|
|
*/
|
|
apiMethods.crossDomainLinker = function (crossDomainLinkerCriterion) {
|
|
decorateLinks(crossDomainLinkerCriterion);
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
apiMethods.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
|
|
*/
|
|
apiMethods.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
|
|
*/
|
|
apiMethods.enableActivityTracking = function (minimumVisitLength, heartBeatDelay) {
|
|
if (minimumVisitLength === parseInt(minimumVisitLength, 10) &&
|
|
heartBeatDelay === parseInt(heartBeatDelay, 10)) {
|
|
activityTrackingEnabled = true;
|
|
configMinimumVisitTime = new Date().getTime() + minimumVisitLength * 1000;
|
|
configHeartBeatTimer = heartBeatDelay * 1000;
|
|
} else {
|
|
helpers.warn("Activity tracking not enabled, please provide integer values " +
|
|
"for minimumVisitLength and heartBeatDelay.")
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Triggers the activityHandler manually to allow external user defined
|
|
* activity. i.e. While watching a video
|
|
*/
|
|
apiMethods.updatePageActivity = function () {
|
|
activityHandler();
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
apiMethods.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
|
|
*/
|
|
apiMethods.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
|
|
*/
|
|
apiMethods.redirectFile = function (url) {
|
|
if (windowAlias.location.protocol === 'file:') {
|
|
windowAlias.location = url;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Sets the opt out cookie.
|
|
*
|
|
* @param string name of the opt out cookie
|
|
*/
|
|
apiMethods.setOptOutCookie = function (name) {
|
|
configOptOutCookie = name;
|
|
};
|
|
|
|
/**
|
|
* Count sites in pre-rendered state
|
|
*
|
|
* @param bool enable If true, track when in pre-rendered state
|
|
*/
|
|
apiMethods.setCountPreRendered = function (enable) {
|
|
configCountPreRendered = enable;
|
|
};
|
|
|
|
/**
|
|
* Set the business-defined user ID for this user.
|
|
*
|
|
* @param string userId The business-defined user ID
|
|
*/
|
|
apiMethods.setUserId = function(userId) {
|
|
businessUserId = userId;
|
|
};
|
|
|
|
/**
|
|
* Alias for setUserId.
|
|
*
|
|
* @param string userId The business-defined user ID
|
|
*/
|
|
apiMethods.identifyUser = function(userId) {
|
|
setUserId(userId);
|
|
};
|
|
|
|
/**
|
|
* Set the business-defined user ID for this user using the location querystring.
|
|
*
|
|
* @param string queryName Name of a querystring name-value pair
|
|
*/
|
|
apiMethods.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
|
|
*/
|
|
apiMethods.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
|
|
*/
|
|
apiMethods.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
|
|
*/
|
|
apiMethods.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
|
|
*/
|
|
apiMethods.setCollectorUrl = function (rawUrl) {
|
|
configCollectorUrl = asCollectorUrl(rawUrl);
|
|
};
|
|
|
|
/**
|
|
* Specify the platform
|
|
*
|
|
* @param string platform Overrides the default tracking platform
|
|
*/
|
|
apiMethods.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 self-describing event payload
|
|
*
|
|
* @param bool enabled A boolean value indicating if the Base64 encoding for self-describing events should be enabled or not
|
|
*/
|
|
apiMethods.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
|
|
*/
|
|
apiMethods.flushBuffer = function () {
|
|
outQueueManager.executeQueue();
|
|
};
|
|
|
|
/**
|
|
* Add the geolocation context to all events
|
|
*/
|
|
apiMethods.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
|
|
* @param tstamp number or Timestamp object
|
|
*/
|
|
apiMethods.trackPageView = function (customTitle, context, contextCallback, tstamp) {
|
|
trackCallback(function () {
|
|
logPageView(customTitle, context, contextCallback, tstamp);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
* @param tstamp number or Timestamp object
|
|
*/
|
|
apiMethods.trackStructEvent = function (category, action, label, property, value, context, tstamp) {
|
|
trackCallback(function () {
|
|
core.trackStructEvent(category, action, label, property, value, addCommonContexts(context), tstamp);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Track a self-describing event (previously 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
|
|
* @param tstamp number or Timestamp object
|
|
*/
|
|
apiMethods.trackSelfDescribingEvent = function (eventJson, context, tstamp) {
|
|
trackCallback(function () {
|
|
core.trackSelfDescribingEvent(eventJson, addCommonContexts(context), tstamp);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Alias for `trackSelfDescribingEvent`, left for compatibility
|
|
*/
|
|
apiMethods.trackUnstructEvent = function (eventJson, context, tstamp) {
|
|
trackCallback(function () {
|
|
core.trackSelfDescribingEvent(eventJson, addCommonContexts(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 object context Optional. Context relating to the event.
|
|
* @param tstamp number or Timestamp object
|
|
*/
|
|
apiMethods.addTrans = function(orderId, affiliation, total, tax, shipping, city, state, country, currency, context, tstamp) {
|
|
ecommerceTransaction.transaction = {
|
|
orderId: orderId,
|
|
affiliation: affiliation,
|
|
total: total,
|
|
tax: tax,
|
|
shipping: shipping,
|
|
city: city,
|
|
state: state,
|
|
country: country,
|
|
currency: currency,
|
|
context: context,
|
|
tstamp: 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 object context Optional. Context relating to the event.
|
|
* @param tstamp number or Timestamp object
|
|
*/
|
|
apiMethods.addItem = function(orderId, sku, name, category, price, quantity, currency, context, tstamp) {
|
|
ecommerceTransaction.items.push({
|
|
orderId: orderId,
|
|
sku: sku,
|
|
name: name,
|
|
category: category,
|
|
price: price,
|
|
quantity: quantity,
|
|
currency: currency,
|
|
context: context,
|
|
tstamp: tstamp
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Commit the ecommerce transaction
|
|
*
|
|
* This call will send the data specified with addTrans,
|
|
* addItem methods to the tracking server.
|
|
*/
|
|
apiMethods.trackTrans = function() {
|
|
trackCallback(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,
|
|
ecommerceTransaction.transaction.tstamp
|
|
);
|
|
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,
|
|
item.tstamp
|
|
);
|
|
}
|
|
|
|
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
|
|
* @param tstamp number or Timestamp object
|
|
*/
|
|
// TODO: break this into trackLink(destUrl) and trackDownload(destUrl)
|
|
apiMethods.trackLinkClick = function(targetUrl, elementId, elementClasses, elementTarget, elementContent, context, tstamp) {
|
|
trackCallback(function () {
|
|
core.trackLinkClick(targetUrl, elementId, elementClasses, elementTarget, elementContent, addCommonContexts(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 object Custom context relating to the event
|
|
* @param tstamp number or Timestamp object
|
|
*/
|
|
apiMethods.trackAdImpression = function(impressionId, costModel, cost, targetUrl, bannerId, zoneId, advertiserId, campaignId, context, tstamp) {
|
|
trackCallback(function () {
|
|
core.trackAdImpression(impressionId, costModel, cost, targetUrl, bannerId, zoneId, advertiserId, campaignId, addCommonContexts(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 object Custom context relating to the event
|
|
* @param tstamp number or Timestamp object
|
|
*/
|
|
apiMethods.trackAdClick = function(targetUrl, clickId, costModel, cost, bannerId, zoneId, impressionId, advertiserId, campaignId, context, tstamp) {
|
|
trackCallback(function () {
|
|
core.trackAdClick(targetUrl, clickId, costModel, cost, bannerId, zoneId, impressionId, advertiserId, campaignId, addCommonContexts(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 object Custom context relating to the event
|
|
* @param tstamp number or Timestamp object
|
|
*/
|
|
apiMethods.trackAdConversion = function(conversionId, costModel, cost, category, action, property, initialValue, advertiserId, campaignId, context, tstamp) {
|
|
trackCallback(function () {
|
|
core.trackAdConversion(conversionId, costModel, cost, category, action, property, initialValue, advertiserId, campaignId, addCommonContexts(context), tstamp);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
* @param tstamp number or Timestamp object
|
|
*/
|
|
apiMethods.trackSocialInteraction = function(action, network, target, context, tstamp) {
|
|
trackCallback(function () {
|
|
core.trackSocialInteraction(action, network, target, addCommonContexts(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 tstamp number or Timestamp object
|
|
*/
|
|
apiMethods.trackAddToCart = function(sku, name, category, unitPrice, quantity, currency, context, tstamp) {
|
|
trackCallback(function () {
|
|
core.trackAddToCart(sku, name, category, unitPrice, quantity, currency, addCommonContexts(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 tstamp Opinal number or Timestamp object
|
|
*/
|
|
apiMethods.trackRemoveFromCart = function(sku, name, category, unitPrice, quantity, currency, context, tstamp) {
|
|
trackCallback(function () {
|
|
core.trackRemoveFromCart(sku, name, category, unitPrice, quantity, currency, addCommonContexts(context), tstamp);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
* @param tstamp Opinal number or Timestamp object
|
|
*/
|
|
apiMethods.trackSiteSearch = function(terms, filters, totalResults, pageResults, context, tstamp) {
|
|
trackCallback(function () {
|
|
core.trackSiteSearch(terms, filters, totalResults, pageResults, addCommonContexts(context), tstamp);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
* @param tstamp Opinal number or Timestamp object
|
|
*/
|
|
apiMethods.trackTiming = function (category, variable, timing, label, context, tstamp) {
|
|
trackCallback(function () {
|
|
core.trackSelfDescribingEvent({
|
|
schema: 'iglu:com.snowplowanalytics.snowplow/timing/jsonschema/1-0-0',
|
|
data: {
|
|
category: category,
|
|
variable: variable,
|
|
timing: timing,
|
|
label: label
|
|
}
|
|
}, addCommonContexts(context), tstamp)
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Track a consent withdrawn action
|
|
*
|
|
* @param {boolean} all - Indicates user withdraws all consent regardless of context documents.
|
|
* @param {string} [id] - Number associated with document.
|
|
* @param {string} [version] - Document version number.
|
|
* @param {string} [name] - Document name.
|
|
* @param {string} [description] - Document description.
|
|
* @param {array} [context] - Context relating to the event.
|
|
* @param {number|Timestamp} [tstamp] - Number or Timestamp object.
|
|
*/
|
|
apiMethods.trackConsentWithdrawn = function (all, id, version, name, description, context, tstamp) {
|
|
trackCallback(function () {
|
|
core.trackConsentWithdrawn(all, id, version, name, description, addCommonContexts(context), tstamp);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Track a consent granted action
|
|
*
|
|
* @param {string} id - ID number associated with document.
|
|
* @param {string} version - Document version number.
|
|
* @param {string} [name] - Document name.
|
|
* @param {string} [description] - Document description.
|
|
* @param {string} [expiry] - Date-time when consent document(s) expire.
|
|
* @param {array} [context] - Context containing consent documents.
|
|
* @param {Timestamp|number} [tstamp] - number or Timestamp object.
|
|
*/
|
|
apiMethods.trackConsentGranted = function (id, version, name, description, expiry, context, tstamp) {
|
|
trackCallback(function () {
|
|
core.trackConsentGranted(id, version, name, description, expiry, addCommonContexts(context), tstamp);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Track a GA Enhanced Ecommerce Action with all stored
|
|
* Enhanced Ecommerce contexts
|
|
*
|
|
* @param string action
|
|
* @param array context Optional. Context relating to the event.
|
|
* @param tstamp Opinal number or Timestamp object
|
|
*/
|
|
apiMethods.trackEnhancedEcommerceAction = function (action, context, tstamp) {
|
|
var combinedEnhancedEcommerceContexts = enhancedEcommerceContexts.concat(context || []);
|
|
enhancedEcommerceContexts.length = 0;
|
|
|
|
trackCallback(function () {
|
|
core.trackSelfDescribingEvent({
|
|
schema: 'iglu:com.google.analytics.enhanced-ecommerce/action/jsonschema/1-0-0',
|
|
data: {
|
|
action: action
|
|
}
|
|
}, addCommonContexts(combinedEnhancedEcommerceContexts), tstamp);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Adds a GA Enhanced Ecommerce Action Context
|
|
*
|
|
* @param string id
|
|
* @param string affiliation
|
|
* @param number revenue
|
|
* @param number tax
|
|
* @param number shipping
|
|
* @param string coupon
|
|
* @param string list
|
|
* @param integer step
|
|
* @param string option
|
|
* @param string currency
|
|
*/
|
|
apiMethods.addEnhancedEcommerceActionContext = function (id, affiliation, revenue, tax, shipping, coupon, list, step, option, currency) {
|
|
enhancedEcommerceContexts.push({
|
|
schema: 'iglu:com.google.analytics.enhanced-ecommerce/actionFieldObject/jsonschema/1-0-0',
|
|
data: {
|
|
id: id,
|
|
affiliation: affiliation,
|
|
revenue: helpers.parseFloat(revenue),
|
|
tax: helpers.parseFloat(tax),
|
|
shipping: helpers.parseFloat(shipping),
|
|
coupon: coupon,
|
|
list: list,
|
|
step: helpers.parseInt(step),
|
|
option: option,
|
|
currency: currency
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Adds a GA Enhanced Ecommerce Impression Context
|
|
*
|
|
* @param string id
|
|
* @param string name
|
|
* @param string list
|
|
* @param string brand
|
|
* @param string category
|
|
* @param string variant
|
|
* @param integer position
|
|
* @param number price
|
|
* @param string currency
|
|
*/
|
|
apiMethods.addEnhancedEcommerceImpressionContext = function (id, name, list, brand, category, variant, position, price, currency) {
|
|
enhancedEcommerceContexts.push({
|
|
schema: 'iglu:com.google.analytics.enhanced-ecommerce/impressionFieldObject/jsonschema/1-0-0',
|
|
data: {
|
|
id: id,
|
|
name: name,
|
|
list: list,
|
|
brand: brand,
|
|
category: category,
|
|
variant: variant,
|
|
position: helpers.parseInt(position),
|
|
price: helpers.parseFloat(price),
|
|
currency: currency
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Adds a GA Enhanced Ecommerce Product Context
|
|
*
|
|
* @param string id
|
|
* @param string name
|
|
* @param string list
|
|
* @param string brand
|
|
* @param string category
|
|
* @param string variant
|
|
* @param number price
|
|
* @param integer quantity
|
|
* @param string coupon
|
|
* @param integer position
|
|
* @param string currency
|
|
*/
|
|
apiMethods.addEnhancedEcommerceProductContext = function (id, name, list, brand, category, variant, price, quantity, coupon, position, currency) {
|
|
enhancedEcommerceContexts.push({
|
|
schema: 'iglu:com.google.analytics.enhanced-ecommerce/productFieldObject/jsonschema/1-0-0',
|
|
data: {
|
|
id: id,
|
|
name: name,
|
|
list: list,
|
|
brand: brand,
|
|
category: category,
|
|
variant: variant,
|
|
price: helpers.parseFloat(price),
|
|
quantity: helpers.parseInt(quantity),
|
|
coupon: coupon,
|
|
position: helpers.parseInt(position),
|
|
currency: currency
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Adds a GA Enhanced Ecommerce Promo Context
|
|
*
|
|
* @param string id
|
|
* @param string name
|
|
* @param string creative
|
|
* @param string position
|
|
* @param string currency
|
|
*/
|
|
apiMethods.addEnhancedEcommercePromoContext = function (id, name, creative, position, currency) {
|
|
enhancedEcommerceContexts.push({
|
|
schema: 'iglu:com.google.analytics.enhanced-ecommerce/promoFieldObject/jsonschema/1-0-0',
|
|
data: {
|
|
id: id,
|
|
name: name,
|
|
creative: creative,
|
|
position: position,
|
|
currency: currency
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* All provided contexts will be sent with every event
|
|
*
|
|
* @param contexts Array<ContextPrimitive | ConditionalContextProvider>
|
|
*/
|
|
apiMethods.addGlobalContexts = function (contexts) { core.addGlobalContexts(contexts); };
|
|
|
|
/**
|
|
* All provided contexts will no longer be sent with every event
|
|
*
|
|
* @param contexts Array<ContextPrimitive | ConditionalContextProvider>
|
|
*/
|
|
apiMethods.removeGlobalContexts = function (contexts) { core.removeGlobalContexts(contexts); };
|
|
|
|
/**
|
|
* Clear all global contexts that are sent with events
|
|
*/
|
|
apiMethods.clearGlobalContexts = function () { core.clearGlobalContexts(); };
|
|
|
|
/**
|
|
* Enable tracking of unhandled exceptions with custom contexts
|
|
*
|
|
* @param filter Function ErrorEvent => Bool to check whether error should be tracker
|
|
* @param contextsAdder Function ErrorEvent => Array<Context> to add custom contexts with
|
|
* internal state based on particular error
|
|
*/
|
|
apiMethods.enableErrorTracking = function (filter, contextsAdder) {
|
|
errorManager.enableErrorTracking(filter, contextsAdder, addCommonContexts())
|
|
};
|
|
|
|
/**
|
|
* Track unhandled exception.
|
|
* This method supposed to be used inside try/catch block
|
|
*
|
|
* @param message string Message appeared in console
|
|
* @param filename string Source file (not used)
|
|
* @param lineno number Line number
|
|
* @param colno number Column number (not used)
|
|
* @param error Error error object (not present in all browsers)
|
|
* @param contexts Array of custom contexts
|
|
*/
|
|
apiMethods.trackError = function (message, filename, lineno, colno, error, contexts) {
|
|
var enrichedContexts = addCommonContexts(contexts);
|
|
errorManager.trackError(message, filename, lineno, colno, error, enrichedContexts);
|
|
};
|
|
|
|
/**
|
|
* Stop regenerating `pageViewId` (available from `web_page` context)
|
|
*/
|
|
apiMethods.preservePageViewId = function () {
|
|
preservePageViewId = true
|
|
};
|
|
|
|
apiMethods.setDebug = function (isDebug) {
|
|
debug = Boolean(isDebug).valueOf();
|
|
updateReturnMethods();
|
|
};
|
|
|
|
// Create guarded methods from apiMethods,
|
|
// and set returnMethods to apiMethods or safeMethods depending on value of debug
|
|
safeMethods = productionize(apiMethods);
|
|
updateReturnMethods();
|
|
|
|
return returnMethods;
|
|
};
|
|
}());
|