/* * 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= 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 */ apiMethods.addGlobalContexts = function (contexts) { core.addGlobalContexts(contexts); }; /** * All provided contexts will no longer be sent with every event * * @param contexts Array */ 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 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; }; }());