debian-mirror-gitlab/snowplow-javascript-tracker/src/js/tracker.js
2020-04-13 14:06:41 +05:30

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;
};
}());