/*! * Matomo - free/libre analytics platform * * JavaScript tracking client * * @link https://piwik.org * @source https://github.com/matomo-org/matomo/blob/master/js/piwik.js * @license https://piwik.org/free-software/bsd/ BSD-3 Clause (also in js/LICENSE.txt) * @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause */ // NOTE: if you change this above Piwik comment block, you must also change `$byteStart` in js/tracker.php // Refer to README.md for build instructions when minifying this file for distribution. /* * Browser [In]Compatibility * - minimum required ECMAScript: ECMA-262, edition 3 * * Incompatible with these (and earlier) versions of: * - IE4 - try..catch and for..in introduced in IE5 * - IE5 - named anonymous functions, array.push, encodeURIComponent, decodeURIComponent, and getElementsByTagName introduced in IE5.5 * - IE6 and 7 - window.JSON introduced in IE8 * - Firefox 1.0 and Netscape 8.x - FF1.5 adds array.indexOf, among other things * - Mozilla 1.7 and Netscape 6.x-7.x * - Netscape 4.8 * - Opera 6 - Error object (and Presto) introduced in Opera 7 * - Opera 7 */ /* startjslint */ /*jslint browser:true, plusplus:true, vars:true, nomen:true, evil:true, regexp: false, bitwise: true, white: true */ /*global window */ /*global unescape */ /*global ActiveXObject */ /*global Blob */ /* NOTE: If you add a new config setting to the tracker, like disableCookies, please add it to plugins/TagManager/Template/Tag/MatomoTag.web.js as well */ /*members Piwik, Matomo, encodeURIComponent, decodeURIComponent, getElementsByTagName, shift, unshift, piwikAsyncInit, matomoAsyncInit, matomoPluginAsyncInit , frameElement, self, hasFocus, createElement, appendChild, characterSet, charset, all, piwik_log, AnalyticsTracker, addEventListener, attachEvent, removeEventListener, detachEvent, disableCookies, setCookieConsentGiven, areCookiesEnabled, getRememberedCookieConsent, rememberCookieConsentGiven, forgetCookieConsentGiven, requireCookieConsent, cookie, domain, readyState, documentElement, doScroll, title, text, contentWindow, postMessage, location, top, onerror, document, referrer, parent, links, href, protocol, name, close, performance, mozPerformance, msPerformance, webkitPerformance, timing, getEntriesByType, connectEnd, requestStart, responseStart, responseEnd, fetchStart, domInteractive, domLoading, domComplete, loadEventStart, loadEventEnd, event, which, button, srcElement, type, target, data, parentNode, tagName, hostname, className, userAgent, cookieEnabled, sendBeacon, platform, mimeTypes, enabledPlugin, javaEnabled, userAgentData, getHighEntropyValues, brands, uaFullVersion, fullVersionList, serviceWorker, ready, then, sync, register, XMLHttpRequest, ActiveXObject, open, setRequestHeader, onreadystatechange, send, readyState, status, getTime, getTimeAlias, setTime, toGMTString, getHours, getMinutes, getSeconds, toLowerCase, toUpperCase, charAt, indexOf, lastIndexOf, split, slice, onload, src, min, round, random, floor, exec, success, trackerUrl, isSendBeacon, xhr, res, width, height, pdf, qt, realp, wma, fla, java, ag, showModalDialog, _rcn, _rck, _refts, _ref, maq_initial_value, maq_opted_in, maq_optout_by_default, maq_url, initialized, hook, getHook, resetUserId, getVisitorId, getVisitorInfo, setUserId, getUserId, setSiteId, getSiteId, setTrackerUrl, getTrackerUrl, appendToTrackingUrl, getRequest, addPlugin, getAttributionInfo, getAttributionCampaignName, getAttributionCampaignKeyword, getAttributionReferrerTimestamp, getAttributionReferrerUrl, setCustomData, getCustomData, setCustomRequestProcessing, setCustomVariable, getCustomVariable, deleteCustomVariable, storeCustomVariablesInCookie, setCustomDimension, getCustomDimension, deleteCustomVariables, deleteCustomDimension, setDownloadExtensions, addDownloadExtensions, removeDownloadExtensions, setDomains, setIgnoreClasses, setRequestMethod, setRequestContentType, setGenerationTimeMs, setPagePerformanceTiming, setReferrerUrl, setCustomUrl, setAPIUrl, setDocumentTitle, setPageViewId, getPageViewId, getPiwikUrl, getMatomoUrl, getCurrentUrl, setExcludedReferrers, getExcludedReferrers, setDownloadClasses, setLinkClasses, setCampaignNameKey, setCampaignKeywordKey, getConsentRequestsQueue, requireConsent, getRememberedConsent, hasRememberedConsent, isConsentRequired, setConsentGiven, rememberConsentGiven, forgetConsentGiven, unload, hasConsent, discardHashTag, alwaysUseSendBeacon, disableAlwaysUseSendBeacon, isUsingAlwaysUseSendBeacon, setCookieNamePrefix, setCookieDomain, setCookiePath, setSecureCookie, setVisitorIdCookie, getCookieDomain, hasCookies, setSessionCookie, setVisitorCookieTimeout, setSessionCookieTimeout, setReferralCookieTimeout, getCookie, getCookiePath, getSessionCookieTimeout, setExcludedQueryParams, setConversionAttributionFirstReferrer, tracker, request, disablePerformanceTracking, maq_confirm_opted_in, doNotTrack, setDoNotTrack, disableCampaignParameters, msDoNotTrack, getValuesFromVisitorIdCookie, enableCrossDomainLinking, disableCrossDomainLinking, isCrossDomainLinkingEnabled, setCrossDomainLinkingTimeout, getCrossDomainLinkingUrlParameter, addListener, enableLinkTracking, disableBrowserFeatureDetection, enableBrowserFeatureDetection, enableJSErrorTracking, setLinkTrackingTimer, getLinkTrackingTimer, enableHeartBeatTimer, disableHeartBeatTimer, killFrame, redirectFile, setCountPreRendered, setVisitStandardLength, trackGoal, trackLink, trackPageView, getNumTrackedPageViews, trackRequest, ping, queueRequest, trackSiteSearch, trackEvent, requests, timeout, enabled, sendRequests, queueRequest, canQueue, pushMultiple, disableQueueRequest,setRequestQueueInterval,interval,getRequestQueue, getJavascriptErrors, unsetPageIsUnloading, setEcommerceView, getEcommerceItems, addEcommerceItem, removeEcommerceItem, clearEcommerceCart, trackEcommerceOrder, trackEcommerceCartUpdate, deleteCookie, deleteCookies, offsetTop, offsetLeft, offsetHeight, offsetWidth, nodeType, defaultView, innerHTML, scrollLeft, scrollTop, currentStyle, getComputedStyle, querySelectorAll, splice, getAttribute, hasAttribute, attributes, nodeName, findContentNodes, findContentNodes, findContentNodesWithinNode, findPieceNode, findTargetNodeNoDefault, findTargetNode, findContentPiece, children, hasNodeCssClass, getAttributeValueFromNode, hasNodeAttributeWithValue, hasNodeAttribute, findNodesByTagName, findMultiple, makeNodesUnique, concat, find, htmlCollectionToArray, offsetParent, value, nodeValue, findNodesHavingAttribute, findFirstNodeHavingAttribute, findFirstNodeHavingAttributeWithValue, getElementsByClassName, findNodesHavingCssClass, findFirstNodeHavingClass, isLinkElement, findParentContentNode, removeDomainIfIsInLink, findContentName, findMediaUrlInNode, toAbsoluteUrl, findContentTarget, getLocation, origin, host, isSameDomain, search, trim, getBoundingClientRect, bottom, right, left, innerWidth, innerHeight, clientWidth, clientHeight, isOrWasNodeInViewport, isNodeVisible, buildInteractionRequestParams, buildImpressionRequestParams, shouldIgnoreInteraction, setHrefAttribute, setAttribute, buildContentBlock, collectContent, setLocation, CONTENT_ATTR, CONTENT_CLASS, LEGACY_CONTENT_CLASS, CONTENT_NAME_ATTR, CONTENT_PIECE_ATTR, CONTENT_PIECE_CLASS, LEGACY_CONTENT_PIECE_CLASS, CONTENT_TARGET_ATTR, CONTENT_TARGET_CLASS, LEGACY_CONTENT_TARGET_CLASS, CONTENT_IGNOREINTERACTION_ATTR, CONTENT_IGNOREINTERACTION_CLASS, LEGACY_CONTENT_IGNOREINTERACTION_CLASS, trackCallbackOnLoad, trackCallbackOnReady, buildContentImpressionsRequests, wasContentImpressionAlreadyTracked, getQuery, getContent, setVisitorId, getContentImpressionsRequestsFromNodes, buildContentInteractionRequestNode, buildContentInteractionRequest, buildContentImpressionRequest, appendContentInteractionToRequestIfPossible, setupInteractionsTracking, trackContentImpressionClickInteraction, internalIsNodeVisible, clearTrackedContentImpressions, getTrackerUrl, trackAllContentImpressions, getTrackedContentImpressions, getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet, contentInteractionTrackingSetupDone, contains, match, pathname, piece, trackContentInteractionNode, trackContentInteractionNode, trackContentImpressionsWithinNode, trackContentImpression, enableTrackOnlyVisibleContent, trackContentInteraction, clearEnableTrackOnlyVisibleContent, logAllContentBlocksOnPage, trackVisibleContentImpressions, isTrackOnlyVisibleContentEnabled, port, isUrlToCurrentDomain, matomoTrackers, isNodeAuthorizedToTriggerInteraction, getConfigDownloadExtensions, disableLinkTracking, substr, setAnyAttribute, max, abs, childNodes, compareDocumentPosition, body, getConfigVisitorCookieTimeout, getRemainingVisitorCookieTimeout, getDomains, getConfigCookiePath, getConfigCookieSameSite, getCustomPagePerformanceTiming, setCookieSameSite, getConfigIdPageView, newVisitor, uuid, createTs, currentVisitTs, "", "\b", "\t", "\n", "\f", "\r", "\"", "\\", apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length, parse, prototype, push, replace, sort, slice, stringify, test, toJSON, toString, valueOf, objectToJSON, addTracker, removeAllAsyncTrackersButFirst, optUserOut, forgetUserOptOut, isUserOptedOut, withCredentials, visibilityState, enableFileTracking */ /*global _paq:true */ /*members push */ /*global Piwik:true */ /*global Matomo:true */ /*members addPlugin, getTracker, getAsyncTracker, getAsyncTrackers, addTracker, trigger, on, off, retryMissedPluginCalls, DOM, onLoad, onReady, isNodeVisible, isOrWasNodeVisible, JSON */ /*global Matomo_Overlay_Client */ /*global AnalyticsTracker:true */ /*members initialize */ /*global define */ /*global console */ /*members amd */ /*members error */ /*members log */ // asynchronous tracker (or proxy) if (typeof _paq !== 'object') { _paq = []; } // Matomo singleton and namespace if (typeof window.Matomo !== 'object') { window.Matomo = window.Piwik = (function () { 'use strict'; /************************************************************ * Private data ************************************************************/ var expireDateTime, /* plugins */ plugins = {}, eventHandlers = {}, /* alias frequently used globals for added minification */ documentAlias = document, navigatorAlias = navigator, screenAlias = screen, windowAlias = window, /* performance timing */ performanceAlias = windowAlias.performance || windowAlias.mozPerformance || windowAlias.msPerformance || windowAlias.webkitPerformance, /* encode */ encodeWrapper = windowAlias.encodeURIComponent, /* decode */ decodeWrapper = windowAlias.decodeURIComponent, /* urldecode */ urldecode = unescape, /* asynchronous tracker */ asyncTrackers = [], /* iterator */ iterator, /* local Matomo */ Matomo, missedPluginTrackerCalls = [], coreConsentCounter = 0, coreHeartBeatCounter = 0, trackerIdCounter = 0, isPageUnloading = false, trackerInstallCheckNonce = ''; /************************************************************ * Private methods ************************************************************/ /** * See https://github.com/matomo-org/matomo/issues/8413 * To prevent Javascript Error: Uncaught URIError: URI malformed when encoding is not UTF-8. Use this method * instead of decodeWrapper if a text could contain any non UTF-8 encoded characters eg * a URL like http://apache.matomo/test.html?%F6%E4%FC or a link like * (encoded iso-8859-1 URL) */ function safeDecodeWrapper(url) { try { return decodeWrapper(url); } catch (e) { return unescape(url); } } /* * Is property defined? */ function isDefined(property) { // workaround https://github.com/douglascrockford/JSLint/commit/24f63ada2f9d7ad65afc90e6d949f631935c2480 var propertyType = typeof property; return propertyType !== 'undefined'; } /* * Is property a function? */ function isFunction(property) { return typeof property === 'function'; } /* * Is property an object? * * @returns {boolean} Returns true if property is null, an Object, or subclass of Object (i.e., an instanceof String, Date, etc.) */ function isObject(property) { return typeof property === 'object'; } /* * Is property a string? */ function isString(property) { return typeof property === 'string' || property instanceof String; } /* * Is property a string? */ function isNumber(property) { return typeof property === 'number' || property instanceof Number; } /* * Is property a string? */ function isNumberOrHasLength(property) { return isDefined(property) && (isNumber(property) || (isString(property) && property.length)); } function isObjectEmpty(property) { if (!property) { return true; } var i; for (i in property) { if (Object.prototype.hasOwnProperty.call(property, i)) { return false; } } return true; } /** * Logs an error in the console. * Note: it does not generate a JavaScript error, so make sure to also generate an error if needed. * @param message */ function logConsoleError(message) { // needed to write it this way for jslint var consoleType = typeof console; if (consoleType !== 'undefined' && console && console.error) { console.error(message); } } /* * apply wrapper * * @param {Array} parameterArray An array comprising either: * [ 'methodName', optional_parameters ] * or: * [ functionObject, optional_parameters ] */ function apply() { var i, j, f, parameterArray, trackerCall; for (i = 0; i < arguments.length; i += 1) { trackerCall = null; if (arguments[i] && arguments[i].slice) { trackerCall = arguments[i].slice(); } parameterArray = arguments[i]; f = parameterArray.shift(); var fParts, context; var isStaticPluginCall = isString(f) && f.indexOf('::') > 0; if (isStaticPluginCall) { // a static method will not be called on a tracker and is not dependent on the existence of a // tracker etc fParts = f.split('::'); context = fParts[0]; f = fParts[1]; if ('object' === typeof Matomo[context] && 'function' === typeof Matomo[context][f]) { Matomo[context][f].apply(Matomo[context], parameterArray); } else if (trackerCall) { // we try to call that method again later as the plugin might not be loaded yet // a plugin can call "Matomo.retryMissedPluginCalls();" once it has been loaded and then the // method call to "Matomo[context][f]" may be executed missedPluginTrackerCalls.push(trackerCall); } } else { for (j = 0; j < asyncTrackers.length; j++) { if (isString(f)) { context = asyncTrackers[j]; var isPluginTrackerCall = f.indexOf('.') > 0; if (isPluginTrackerCall) { fParts = f.split('.'); if (context && 'object' === typeof context[fParts[0]]) { context = context[fParts[0]]; f = fParts[1]; } else if (trackerCall) { // we try to call that method again later as the plugin might not be loaded yet missedPluginTrackerCalls.push(trackerCall); break; } } if (context[f]) { context[f].apply(context, parameterArray); } else { var message = 'The method \'' + f + '\' was not found in "_paq" variable. Please have a look at the Matomo tracker documentation: https://developer.matomo.org/api-reference/tracking-javascript'; logConsoleError(message); if (!isPluginTrackerCall) { // do not trigger an error if it is a call to a plugin as the plugin may just not be // loaded yet etc throw new TypeError(message); } } if (f === 'addTracker') { // addTracker adds an entry to asyncTrackers and would otherwise result in an endless loop break; } if (f === 'setTrackerUrl' || f === 'setSiteId') { // these two methods should be only executed on the first tracker break; } } else { f.apply(asyncTrackers[j], parameterArray); } } } } } /* * Cross-browser helper function to add event handler */ function addEventListener(element, eventType, eventHandler, useCapture) { if (element.addEventListener) { element.addEventListener(eventType, eventHandler, useCapture); return true; } if (element.attachEvent) { return element.attachEvent('on' + eventType, eventHandler); } element['on' + eventType] = eventHandler; } function trackCallbackOnLoad(callback) { if (documentAlias.readyState === 'complete') { callback(); } else if (windowAlias.addEventListener) { windowAlias.addEventListener('load', callback, false); } else if (windowAlias.attachEvent) { windowAlias.attachEvent('onload', callback); } } function trackCallbackOnReady(callback) { var loaded = false; if (documentAlias.attachEvent) { loaded = documentAlias.readyState === 'complete'; } else { loaded = documentAlias.readyState !== 'loading'; } if (loaded) { callback(); return; } var _timer; if (documentAlias.addEventListener) { addEventListener(documentAlias, 'DOMContentLoaded', function ready() { documentAlias.removeEventListener('DOMContentLoaded', ready, false); if (!loaded) { loaded = true; callback(); } }); } else if (documentAlias.attachEvent) { documentAlias.attachEvent('onreadystatechange', function ready() { if (documentAlias.readyState === 'complete') { documentAlias.detachEvent('onreadystatechange', ready); if (!loaded) { loaded = true; callback(); } } }); if (documentAlias.documentElement.doScroll && windowAlias === windowAlias.top) { (function ready() { if (!loaded) { try { documentAlias.documentElement.doScroll('left'); } catch (error) { setTimeout(ready, 0); return; } loaded = true; callback(); } }()); } } // fallback addEventListener(windowAlias, 'load', function () { if (!loaded) { loaded = true; callback(); } }, false); } /* * Call plugin hook methods */ function executePluginMethod(methodName, params, callback) { if (!methodName) { return ''; } var result = '', i, pluginMethod, value, isFunction; for (i in plugins) { if (Object.prototype.hasOwnProperty.call(plugins, i)) { isFunction = plugins[i] && 'function' === typeof plugins[i][methodName]; if (isFunction) { pluginMethod = plugins[i][methodName]; value = pluginMethod(params || {}, callback); if (value) { result += value; } } } } return result; } /* * Handle beforeunload event * * Subject to Safari's "Runaway JavaScript Timer" and * Chrome V8 extension that terminates JS that exhibits * "slow unload", i.e., calling getTime() > 1000 times */ function beforeUnloadHandler(event) { var now; isPageUnloading = true; executePluginMethod('unload'); now = new Date(); var aliasTime = now.getTimeAlias(); if ((expireDateTime - aliasTime) > 3000) { expireDateTime = aliasTime + 3000; } /* * Delay/pause (blocks UI) */ if (expireDateTime) { // the things we do for backwards compatibility... // in ECMA-262 5th ed., we could simply use: // while (Date.now() < expireDateTime) { } do { now = new Date(); } while (now.getTimeAlias() < expireDateTime); } } /* * Load JavaScript file (asynchronously) */ function loadScript(src, onLoad) { var script = documentAlias.createElement('script'); script.type = 'text/javascript'; script.src = src; if (script.readyState) { script.onreadystatechange = function () { var state = this.readyState; if (state === 'loaded' || state === 'complete') { script.onreadystatechange = null; onLoad(); } }; } else { script.onload = onLoad; } documentAlias.getElementsByTagName('head')[0].appendChild(script); } /* * Get page referrer */ function getReferrer() { var referrer = ''; try { referrer = windowAlias.top.document.referrer; } catch (e) { if (windowAlias.parent) { try { referrer = windowAlias.parent.document.referrer; } catch (e2) { referrer = ''; } } } if (referrer === '') { referrer = documentAlias.referrer; } return referrer; } /* * Extract scheme/protocol from URL */ function getProtocolScheme(url) { var e = new RegExp('^([a-z]+):'), matches = e.exec(url); return matches ? matches[1] : null; } /* * Extract hostname from URL */ function getHostName(url) { // scheme : // [username [: password] @] hostame [: port] [/ [path] [? query] [# fragment]] var e = new RegExp('^(?:(?:https?|ftp):)/*(?:[^@]+@)?([^:/#]+)'), matches = e.exec(url); return matches ? matches[1] : url; } function isPositiveNumberString(str) { // !isNaN(str) could be used but does not cover '03' (octal) and '0xA' (hex) // nor negative numbers return (/^[0-9][0-9]*(\.[0-9]+)?$/).test(str); } function filterIn(object, byFunction) { var result = {}, k; for (k in object) { if (object.hasOwnProperty(k) && byFunction(object[k])) { result[k] = object[k]; } } return result; } function onlyPositiveIntegers(data) { var result = {}, k; for (k in data) { if (data.hasOwnProperty(k)) { if (isPositiveNumberString(data[k])) { result[k] = Math.round(data[k]); } else { throw new Error('Parameter "' + k + '" provided value "' + data[k] + '" is not valid. Please provide a numeric value.'); } } } return result; } function queryStringify(data) { var queryString = '', k; for (k in data) { if (data.hasOwnProperty(k)) { queryString += '&' + encodeWrapper(k) + '=' + encodeWrapper(data[k]); } } return queryString; } function stringStartsWith(str, prefix) { str = String(str); return str.lastIndexOf(prefix, 0) === 0; } function stringEndsWith(str, suffix) { str = String(str); return str.indexOf(suffix, str.length - suffix.length) !== -1; } function stringContains(str, needle) { str = String(str); return str.indexOf(needle) !== -1; } function removeCharactersFromEndOfString(str, numCharactersToRemove) { str = String(str); return str.substr(0, str.length - numCharactersToRemove); } /** * We do not check whether URL contains already url parameter, please use removeUrlParameter() if needed * before calling this method. * This method makes sure to append URL parameters before a possible hash. Will escape (encode URI component) * the set name and value */ function addUrlParameter(url, name, value) { url = String(url); if (!value) { value = ''; } var hashPos = url.indexOf('#'); var urlLength = url.length; if (hashPos === -1) { hashPos = urlLength; } var baseUrl = url.substr(0, hashPos); var urlHash = url.substr(hashPos, urlLength - hashPos); if (baseUrl.indexOf('?') === -1) { baseUrl += '?'; } else if (!stringEndsWith(baseUrl, '?')) { baseUrl += '&'; } // nothing to if ends with ? return baseUrl + encodeWrapper(name) + '=' + encodeWrapper(value) + urlHash; } function removeUrlParameter(url, name) { url = String(url); if (url.indexOf('?' + name + '=') === -1 && url.indexOf('&' + name + '=') === -1 && url.indexOf('#' + name + '=') === -1) { // nothing to remove, url does not contain this parameter in query or hash return url; } var urlHash = ''; var hashPos = url.indexOf('#'); if (hashPos !== -1) { urlHash = url.substr(hashPos + 1); url = url.substr(0, hashPos); } var searchPos = url.indexOf('?'); var queryString = ''; var baseUrl = url; if (searchPos > -1) { queryString = url.substr(searchPos + 1); baseUrl = url.substr(0, searchPos); } var filterParams = function (paramsArr) { var param; var i = paramsArr.length - 1; for (i; i >= 0; i--) { param = paramsArr[i].split('=')[0]; if (param === name) { paramsArr.splice(i, 1); } } return paramsArr; }; if (queryString) { var newQueryString = filterParams(queryString.split('&')).join('&'); if (newQueryString) { baseUrl += '?' + newQueryString; } } if (urlHash && urlHash.indexOf('=') > 0) { var hashWithMark = urlHash.charAt(0) === '?'; if (hashWithMark) { urlHash = urlHash.substr(1); } var newHashString = filterParams(urlHash.split('&')).join('&'); if (newHashString) { baseUrl += '#'; if (hashWithMark) { baseUrl += '?'; } baseUrl += newHashString; } } else if (urlHash) { baseUrl += '#' + urlHash; } return baseUrl; } /* * Extract parameter from URL */ function getUrlParameter(url, name) { var regexSearch = "[\\?&#]" + name + "=([^&#]*)"; var regex = new RegExp(regexSearch); var results = regex.exec(url); return results ? safeDecodeWrapper(results[1]) : ''; } function trim(text) { if (text && String(text) === text) { return text.replace(/^\s+|\s+$/g, ''); } return text; } /* * UTF-8 encoding */ function utf8_encode(argString) { return unescape(encodeWrapper(argString)); } /************************************************************ * sha1 * - based on sha1 from http://phpjs.org/functions/sha1:512 (MIT / GPL v2) ************************************************************/ function sha1(str) { // + original by: Webtoolkit.info (http://www.webtoolkit.info/) // + namespaced by: Michael White (http://getsprink.com) // + input by: Brett Zamir (http://brett-zamir.me) // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) // + jslinted by: Anthon Pang (https://matomo.org) var rotate_left = function (n, s) { return (n << s) | (n >>> (32 - s)); }, cvt_hex = function (val) { var strout = '', i, v; for (i = 7; i >= 0; i--) { v = (val >>> (i * 4)) & 0x0f; strout += v.toString(16); } return strout; }, blockstart, i, j, W = [], H0 = 0x67452301, H1 = 0xEFCDAB89, H2 = 0x98BADCFE, H3 = 0x10325476, H4 = 0xC3D2E1F0, A, B, C, D, E, temp, str_len, word_array = []; str = utf8_encode(str); str_len = str.length; for (i = 0; i < str_len - 3; i += 4) { j = str.charCodeAt(i) << 24 | str.charCodeAt(i + 1) << 16 | str.charCodeAt(i + 2) << 8 | str.charCodeAt(i + 3); word_array.push(j); } switch (str_len & 3) { case 0: i = 0x080000000; break; case 1: i = str.charCodeAt(str_len - 1) << 24 | 0x0800000; break; case 2: i = str.charCodeAt(str_len - 2) << 24 | str.charCodeAt(str_len - 1) << 16 | 0x08000; break; case 3: i = str.charCodeAt(str_len - 3) << 24 | str.charCodeAt(str_len - 2) << 16 | str.charCodeAt(str_len - 1) << 8 | 0x80; break; } word_array.push(i); while ((word_array.length & 15) !== 14) { word_array.push(0); } word_array.push(str_len >>> 29); word_array.push((str_len << 3) & 0x0ffffffff); for (blockstart = 0; blockstart < word_array.length; blockstart += 16) { for (i = 0; i < 16; i++) { W[i] = word_array[blockstart + i]; } for (i = 16; i <= 79; i++) { W[i] = rotate_left(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1); } A = H0; B = H1; C = H2; D = H3; E = H4; for (i = 0; i <= 19; i++) { temp = (rotate_left(A, 5) + ((B & C) | (~B & D)) + E + W[i] + 0x5A827999) & 0x0ffffffff; E = D; D = C; C = rotate_left(B, 30); B = A; A = temp; } for (i = 20; i <= 39; i++) { temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff; E = D; D = C; C = rotate_left(B, 30); B = A; A = temp; } for (i = 40; i <= 59; i++) { temp = (rotate_left(A, 5) + ((B & C) | (B & D) | (C & D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff; E = D; D = C; C = rotate_left(B, 30); B = A; A = temp; } for (i = 60; i <= 79; i++) { temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff; E = D; D = C; C = rotate_left(B, 30); B = A; A = temp; } H0 = (H0 + A) & 0x0ffffffff; H1 = (H1 + B) & 0x0ffffffff; H2 = (H2 + C) & 0x0ffffffff; H3 = (H3 + D) & 0x0ffffffff; H4 = (H4 + E) & 0x0ffffffff; } temp = cvt_hex(H0) + cvt_hex(H1) + cvt_hex(H2) + cvt_hex(H3) + cvt_hex(H4); return temp.toLowerCase(); } /************************************************************ * end sha1 ************************************************************/ /* * Fix-up URL when page rendered from search engine cache or translated page */ function urlFixup(hostName, href, referrer) { if (!hostName) { hostName = ''; } if (!href) { href = ''; } if (hostName === 'translate.googleusercontent.com') { // Google if (referrer === '') { referrer = href; } href = getUrlParameter(href, 'u'); hostName = getHostName(href); } else if (hostName === 'cc.bingj.com' || // Bing hostName === 'webcache.googleusercontent.com' || // Google hostName.slice(0, 5) === '74.6.') { // Yahoo (via Inktomi 74.6.0.0/16) href = documentAlias.links[0].href; hostName = getHostName(href); } return [hostName, href, referrer]; } /* * Fix-up domain */ function domainFixup(domain) { var dl = domain.length; // remove trailing '.' if (domain.charAt(--dl) === '.') { domain = domain.slice(0, dl); } // remove leading '*' if (domain.slice(0, 2) === '*.') { domain = domain.slice(1); } if (domain.indexOf('/') !== -1) { domain = domain.substr(0, domain.indexOf('/')); } return domain; } /* * Title fixup */ function titleFixup(title) { title = title && title.text ? title.text : title; if (!isString(title)) { var tmp = documentAlias.getElementsByTagName('title'); if (tmp && isDefined(tmp[0])) { title = tmp[0].text; } } return title; } function getChildrenFromNode(node) { if (!node) { return []; } if (!isDefined(node.children) && isDefined(node.childNodes)) { return node.children; } if (isDefined(node.children)) { return node.children; } return []; } function containsNodeElement(node, containedNode) { if (!node || !containedNode) { return false; } if (node.contains) { return node.contains(containedNode); } if (node === containedNode) { return true; } if (node.compareDocumentPosition) { return !!(node.compareDocumentPosition(containedNode) & 16); } return false; } // Polyfill for IndexOf for IE6-IE8 function indexOfArray(theArray, searchElement) { if (theArray && theArray.indexOf) { return theArray.indexOf(searchElement); } // 1. Let O be the result of calling ToObject passing // the this value as the argument. if (!isDefined(theArray) || theArray === null) { return -1; } if (!theArray.length) { return -1; } var len = theArray.length; if (len === 0) { return -1; } var k = 0; // 9. Repeat, while k < len while (k < len) { // a. Let Pk be ToString(k). // This is implicit for LHS operands of the in operator // b. Let kPresent be the result of calling the // HasProperty internal method of O with argument Pk. // This step can be combined with c // c. If kPresent is true, then // i. Let elementK be the result of calling the Get // internal method of O with the argument ToString(k). // ii. Let same be the result of applying the // Strict Equality Comparison Algorithm to // searchElement and elementK. // iii. If same is true, return k. if (theArray[k] === searchElement) { return k; } k++; } return -1; } /************************************************************ * Element Visiblility ************************************************************/ /** * Author: Jason Farrell * Author URI: http://useallfive.com/ * * Description: Checks if a DOM element is truly visible. * Package URL: https://github.com/UseAllFive/true-visibility * License: MIT (https://github.com/UseAllFive/true-visibility/blob/master/LICENSE.txt) */ function isVisible(node) { if (!node) { return false; } //-- Cross browser method to get style properties: function _getStyle(el, property) { if (windowAlias.getComputedStyle) { return documentAlias.defaultView.getComputedStyle(el,null)[property]; } if (el.currentStyle) { return el.currentStyle[property]; } } function _elementInDocument(element) { element = element.parentNode; while (element) { if (element === documentAlias) { return true; } element = element.parentNode; } return false; } /** * Checks if a DOM element is visible. Takes into * consideration its parents and overflow. * * @param (el) the DOM element to check if is visible * * These params are optional that are sent in recursively, * you typically won't use these: * * @param (t) Top corner position number * @param (r) Right corner position number * @param (b) Bottom corner position number * @param (l) Left corner position number * @param (w) Element width number * @param (h) Element height number */ function _isVisible(el, t, r, b, l, w, h) { var p = el.parentNode, VISIBLE_PADDING = 1; // has to be visible at least one px of the element if (!_elementInDocument(el)) { return false; } //-- Return true for document node if (9 === p.nodeType) { return true; } //-- Return false if our element is invisible if ( '0' === _getStyle(el, 'opacity') || 'none' === _getStyle(el, 'display') || 'hidden' === _getStyle(el, 'visibility') ) { return false; } if (!isDefined(t) || !isDefined(r) || !isDefined(b) || !isDefined(l) || !isDefined(w) || !isDefined(h)) { t = el.offsetTop; l = el.offsetLeft; b = t + el.offsetHeight; r = l + el.offsetWidth; w = el.offsetWidth; h = el.offsetHeight; } if (node === el && (0 === h || 0 === w) && 'hidden' === _getStyle(el, 'overflow')) { return false; } //-- If we have a parent, let's continue: if (p) { //-- Check if the parent can hide its children. if (('hidden' === _getStyle(p, 'overflow') || 'scroll' === _getStyle(p, 'overflow'))) { //-- Only check if the offset is different for the parent if ( //-- If the target element is to the right of the parent elm l + VISIBLE_PADDING > p.offsetWidth + p.scrollLeft || //-- If the target element is to the left of the parent elm l + w - VISIBLE_PADDING < p.scrollLeft || //-- If the target element is under the parent elm t + VISIBLE_PADDING > p.offsetHeight + p.scrollTop || //-- If the target element is above the parent elm t + h - VISIBLE_PADDING < p.scrollTop ) { //-- Our target element is out of bounds: return false; } } //-- Add the offset parent's left/top coords to our element's offset: if (el.offsetParent === p) { l += p.offsetLeft; t += p.offsetTop; } //-- Let's recursively check upwards: return _isVisible(p, t, r, b, l, w, h); } return true; } return _isVisible(node); } /************************************************************ * Query ************************************************************/ var query = { htmlCollectionToArray: function (foundNodes) { var nodes = [], index; if (!foundNodes || !foundNodes.length) { return nodes; } for (index = 0; index < foundNodes.length; index++) { nodes.push(foundNodes[index]); } return nodes; }, find: function (selector) { // we use querySelectorAll only on document, not on nodes because of its unexpected behavior. See for // instance http://stackoverflow.com/questions/11503534/jquery-vs-document-queryselectorall and // http://jsfiddle.net/QdMc5/ and http://ejohn.org/blog/thoughts-on-queryselectorall if (!document.querySelectorAll || !selector) { return []; // we do not support all browsers } var foundNodes = document.querySelectorAll(selector); return this.htmlCollectionToArray(foundNodes); }, findMultiple: function (selectors) { if (!selectors || !selectors.length) { return []; } var index, foundNodes; var nodes = []; for (index = 0; index < selectors.length; index++) { foundNodes = this.find(selectors[index]); nodes = nodes.concat(foundNodes); } nodes = this.makeNodesUnique(nodes); return nodes; }, findNodesByTagName: function (node, tagName) { if (!node || !tagName || !node.getElementsByTagName) { return []; } var foundNodes = node.getElementsByTagName(tagName); return this.htmlCollectionToArray(foundNodes); }, makeNodesUnique: function (nodes) { var copy = [].concat(nodes); nodes.sort(function(n1, n2){ if (n1 === n2) { return 0; } var index1 = indexOfArray(copy, n1); var index2 = indexOfArray(copy, n2); if (index1 === index2) { return 0; } return index1 > index2 ? -1 : 1; }); if (nodes.length <= 1) { return nodes; } var index = 0; var numDuplicates = 0; var duplicates = []; var node; node = nodes[index++]; while (node) { if (node === nodes[index]) { numDuplicates = duplicates.push(index); } node = nodes[index++] || null; } while (numDuplicates--) { nodes.splice(duplicates[numDuplicates], 1); } return nodes; }, getAttributeValueFromNode: function (node, attributeName) { if (!this.hasNodeAttribute(node, attributeName)) { return; } if (node && node.getAttribute) { return node.getAttribute(attributeName); } if (!node || !node.attributes) { return; } var typeOfAttr = (typeof node.attributes[attributeName]); if ('undefined' === typeOfAttr) { return; } if (node.attributes[attributeName].value) { return node.attributes[attributeName].value; // nodeValue is deprecated ie Chrome } if (node.attributes[attributeName].nodeValue) { return node.attributes[attributeName].nodeValue; } var index; var attrs = node.attributes; if (!attrs) { return; } for (index = 0; index < attrs.length; index++) { if (attrs[index].nodeName === attributeName) { return attrs[index].nodeValue; } } return null; }, hasNodeAttributeWithValue: function (node, attributeName) { var value = this.getAttributeValueFromNode(node, attributeName); return !!value; }, hasNodeAttribute: function (node, attributeName) { if (node && node.hasAttribute) { return node.hasAttribute(attributeName); } if (node && node.attributes) { var typeOfAttr = (typeof node.attributes[attributeName]); return 'undefined' !== typeOfAttr; } return false; }, hasNodeCssClass: function (node, klassName) { if (node && klassName && node.className) { var classes = typeof node.className === "string" ? node.className.split(' ') : []; if (-1 !== indexOfArray(classes, klassName)) { return true; } } return false; }, findNodesHavingAttribute: function (nodeToSearch, attributeName, nodes) { if (!nodes) { nodes = []; } if (!nodeToSearch || !attributeName) { return nodes; } var children = getChildrenFromNode(nodeToSearch); if (!children || !children.length) { return nodes; } var index, child; for (index = 0; index < children.length; index++) { child = children[index]; if (this.hasNodeAttribute(child, attributeName)) { nodes.push(child); } nodes = this.findNodesHavingAttribute(child, attributeName, nodes); } return nodes; }, findFirstNodeHavingAttribute: function (node, attributeName) { if (!node || !attributeName) { return; } if (this.hasNodeAttribute(node, attributeName)) { return node; } var nodes = this.findNodesHavingAttribute(node, attributeName); if (nodes && nodes.length) { return nodes[0]; } }, findFirstNodeHavingAttributeWithValue: function (node, attributeName) { if (!node || !attributeName) { return; } if (this.hasNodeAttributeWithValue(node, attributeName)) { return node; } var nodes = this.findNodesHavingAttribute(node, attributeName); if (!nodes || !nodes.length) { return; } var index; for (index = 0; index < nodes.length; index++) { if (this.getAttributeValueFromNode(nodes[index], attributeName)) { return nodes[index]; } } }, findNodesHavingCssClass: function (nodeToSearch, className, nodes) { if (!nodes) { nodes = []; } if (!nodeToSearch || !className) { return nodes; } if (nodeToSearch.getElementsByClassName) { var foundNodes = nodeToSearch.getElementsByClassName(className); return this.htmlCollectionToArray(foundNodes); } var children = getChildrenFromNode(nodeToSearch); if (!children || !children.length) { return []; } var index, child; for (index = 0; index < children.length; index++) { child = children[index]; if (this.hasNodeCssClass(child, className)) { nodes.push(child); } nodes = this.findNodesHavingCssClass(child, className, nodes); } return nodes; }, findFirstNodeHavingClass: function (node, className) { if (!node || !className) { return; } if (this.hasNodeCssClass(node, className)) { return node; } var nodes = this.findNodesHavingCssClass(node, className); if (nodes && nodes.length) { return nodes[0]; } }, isLinkElement: function (node) { if (!node) { return false; } var elementName = String(node.nodeName).toLowerCase(); var linkElementNames = ['a', 'area']; var pos = indexOfArray(linkElementNames, elementName); return pos !== -1; }, setAnyAttribute: function (node, attrName, attrValue) { if (!node || !attrName) { return; } if (node.setAttribute) { node.setAttribute(attrName, attrValue); } else { node[attrName] = attrValue; } } }; /************************************************************ * Content Tracking ************************************************************/ var content = { CONTENT_ATTR: 'data-track-content', CONTENT_CLASS: 'matomoTrackContent', LEGACY_CONTENT_CLASS: 'piwikTrackContent', CONTENT_NAME_ATTR: 'data-content-name', CONTENT_PIECE_ATTR: 'data-content-piece', CONTENT_PIECE_CLASS: 'matomoContentPiece', LEGACY_CONTENT_PIECE_CLASS: 'piwikContentPiece', CONTENT_TARGET_ATTR: 'data-content-target', CONTENT_TARGET_CLASS: 'matomoContentTarget', LEGACY_CONTENT_TARGET_CLASS: 'piwikContentTarget', CONTENT_IGNOREINTERACTION_ATTR: 'data-content-ignoreinteraction', CONTENT_IGNOREINTERACTION_CLASS: 'matomoContentIgnoreInteraction', LEGACY_CONTENT_IGNOREINTERACTION_CLASS: 'piwikContentIgnoreInteraction', location: undefined, findContentNodes: function () { var cssSelector = '.' + this.CONTENT_CLASS; var cssSelector2 = '.' + this.LEGACY_CONTENT_CLASS; var attrSelector = '[' + this.CONTENT_ATTR + ']'; var contentNodes = query.findMultiple([cssSelector, cssSelector2, attrSelector]); return contentNodes; }, findContentNodesWithinNode: function (node) { if (!node) { return []; } // NOTE: we do not use query.findMultiple here as querySelectorAll would most likely not deliver the result we want var nodes1 = query.findNodesHavingCssClass(node, this.CONTENT_CLASS); nodes1 = query.findNodesHavingCssClass(node, this.LEGACY_CONTENT_CLASS, nodes1); var nodes2 = query.findNodesHavingAttribute(node, this.CONTENT_ATTR); if (nodes2 && nodes2.length) { var index; for (index = 0; index < nodes2.length; index++) { nodes1.push(nodes2[index]); } } if (query.hasNodeAttribute(node, this.CONTENT_ATTR)) { nodes1.push(node); } else if (query.hasNodeCssClass(node, this.CONTENT_CLASS)) { nodes1.push(node); } else if (query.hasNodeCssClass(node, this.LEGACY_CONTENT_CLASS)) { nodes1.push(node); } nodes1 = query.makeNodesUnique(nodes1); return nodes1; }, findParentContentNode: function (anyNode) { if (!anyNode) { return; } var node = anyNode; var counter = 0; while (node && node !== documentAlias && node.parentNode) { if (query.hasNodeAttribute(node, this.CONTENT_ATTR)) { return node; } if (query.hasNodeCssClass(node, this.CONTENT_CLASS)) { return node; } if (query.hasNodeCssClass(node, this.LEGACY_CONTENT_CLASS)) { return node; } node = node.parentNode; if (counter > 1000) { break; // prevent loop, should not happen anyway but better we do this } counter++; } }, findPieceNode: function (node) { var contentPiece; contentPiece = query.findFirstNodeHavingAttribute(node, this.CONTENT_PIECE_ATTR); if (!contentPiece) { contentPiece = query.findFirstNodeHavingClass(node, this.CONTENT_PIECE_CLASS); } if (!contentPiece) { contentPiece = query.findFirstNodeHavingClass(node, this.LEGACY_CONTENT_PIECE_CLASS); } if (contentPiece) { return contentPiece; } return node; }, findTargetNodeNoDefault: function (node) { if (!node) { return; } var target = query.findFirstNodeHavingAttributeWithValue(node, this.CONTENT_TARGET_ATTR); if (target) { return target; } target = query.findFirstNodeHavingAttribute(node, this.CONTENT_TARGET_ATTR); if (target) { return target; } target = query.findFirstNodeHavingClass(node, this.CONTENT_TARGET_CLASS); if (target) { return target; } target = query.findFirstNodeHavingClass(node, this.LEGACY_CONTENT_TARGET_CLASS); if (target) { return target; } }, findTargetNode: function (node) { var target = this.findTargetNodeNoDefault(node); if (target) { return target; } return node; }, findContentName: function (node) { if (!node) { return; } var nameNode = query.findFirstNodeHavingAttributeWithValue(node, this.CONTENT_NAME_ATTR); if (nameNode) { return query.getAttributeValueFromNode(nameNode, this.CONTENT_NAME_ATTR); } var contentPiece = this.findContentPiece(node); if (contentPiece) { return this.removeDomainIfIsInLink(contentPiece); } if (query.hasNodeAttributeWithValue(node, 'title')) { return query.getAttributeValueFromNode(node, 'title'); } var clickUrlNode = this.findPieceNode(node); if (query.hasNodeAttributeWithValue(clickUrlNode, 'title')) { return query.getAttributeValueFromNode(clickUrlNode, 'title'); } var targetNode = this.findTargetNode(node); if (query.hasNodeAttributeWithValue(targetNode, 'title')) { return query.getAttributeValueFromNode(targetNode, 'title'); } }, findContentPiece: function (node) { if (!node) { return; } var nameNode = query.findFirstNodeHavingAttributeWithValue(node, this.CONTENT_PIECE_ATTR); if (nameNode) { return query.getAttributeValueFromNode(nameNode, this.CONTENT_PIECE_ATTR); } var contentNode = this.findPieceNode(node); var media = this.findMediaUrlInNode(contentNode); if (media) { return this.toAbsoluteUrl(media); } }, findContentTarget: function (node) { if (!node) { return; } var targetNode = this.findTargetNode(node); if (query.hasNodeAttributeWithValue(targetNode, this.CONTENT_TARGET_ATTR)) { return query.getAttributeValueFromNode(targetNode, this.CONTENT_TARGET_ATTR); } var href; if (query.hasNodeAttributeWithValue(targetNode, 'href')) { href = query.getAttributeValueFromNode(targetNode, 'href'); return this.toAbsoluteUrl(href); } var contentNode = this.findPieceNode(node); if (query.hasNodeAttributeWithValue(contentNode, 'href')) { href = query.getAttributeValueFromNode(contentNode, 'href'); return this.toAbsoluteUrl(href); } }, isSameDomain: function (url) { if (!url || !url.indexOf) { return false; } if (0 === url.indexOf(this.getLocation().origin)) { return true; } var posHost = url.indexOf(this.getLocation().host); if (8 >= posHost && 0 <= posHost) { return true; } return false; }, removeDomainIfIsInLink: function (text) { // we will only remove if domain === location.origin meaning is not an outlink var regexContainsProtocol = '^https?:\/\/[^\/]+'; var regexReplaceDomain = '^.*\/\/[^\/]+'; if (text && text.search && -1 !== text.search(new RegExp(regexContainsProtocol)) && this.isSameDomain(text)) { text = text.replace(new RegExp(regexReplaceDomain), ''); if (!text) { text = '/'; } } return text; }, findMediaUrlInNode: function (node) { if (!node) { return; } var mediaElements = ['img', 'embed', 'video', 'audio']; var elementName = node.nodeName.toLowerCase(); if (-1 !== indexOfArray(mediaElements, elementName) && query.findFirstNodeHavingAttributeWithValue(node, 'src')) { var sourceNode = query.findFirstNodeHavingAttributeWithValue(node, 'src'); return query.getAttributeValueFromNode(sourceNode, 'src'); } if (elementName === 'object' && query.hasNodeAttributeWithValue(node, 'data')) { return query.getAttributeValueFromNode(node, 'data'); } if (elementName === 'object') { var params = query.findNodesByTagName(node, 'param'); if (params && params.length) { var index; for (index = 0; index < params.length; index++) { if ('movie' === query.getAttributeValueFromNode(params[index], 'name') && query.hasNodeAttributeWithValue(params[index], 'value')) { return query.getAttributeValueFromNode(params[index], 'value'); } } } var embed = query.findNodesByTagName(node, 'embed'); if (embed && embed.length) { return this.findMediaUrlInNode(embed[0]); } } }, trim: function (text) { return trim(text); }, isOrWasNodeInViewport: function (node) { if (!node || !node.getBoundingClientRect || node.nodeType !== 1) { return true; } var rect = node.getBoundingClientRect(); var html = documentAlias.documentElement || {}; var wasVisible = rect.top < 0; if (wasVisible && node.offsetTop) { wasVisible = (node.offsetTop + rect.height) > 0; } var docWidth = html.clientWidth; // The clientWidth attribute returns the viewport width excluding the size of a rendered scroll bar if (windowAlias.innerWidth && docWidth > windowAlias.innerWidth) { docWidth = windowAlias.innerWidth; // The innerWidth attribute must return the viewport width including the size of a rendered scroll bar } var docHeight = html.clientHeight; // The clientWidth attribute returns the viewport width excluding the size of a rendered scroll bar if (windowAlias.innerHeight && docHeight > windowAlias.innerHeight) { docHeight = windowAlias.innerHeight; // The innerWidth attribute must return the viewport width including the size of a rendered scroll bar } return ( (rect.bottom > 0 || wasVisible) && rect.right > 0 && rect.left < docWidth && ((rect.top < docHeight) || wasVisible) // rect.top < 0 we assume user has seen all the ones that are above the current viewport ); }, isNodeVisible: function (node) { var isItVisible = isVisible(node); var isInViewport = this.isOrWasNodeInViewport(node); return isItVisible && isInViewport; }, buildInteractionRequestParams: function (interaction, name, piece, target) { var params = ''; if (interaction) { params += 'c_i='+ encodeWrapper(interaction); } if (name) { if (params) { params += '&'; } params += 'c_n='+ encodeWrapper(name); } if (piece) { if (params) { params += '&'; } params += 'c_p='+ encodeWrapper(piece); } if (target) { if (params) { params += '&'; } params += 'c_t='+ encodeWrapper(target); } if (params) { params += '&ca=1'; } return params; }, buildImpressionRequestParams: function (name, piece, target) { var params = 'c_n=' + encodeWrapper(name) + '&c_p=' + encodeWrapper(piece); if (target) { params += '&c_t=' + encodeWrapper(target); } if (params) { params += '&ca=1'; } return params; }, buildContentBlock: function (node) { if (!node) { return; } var name = this.findContentName(node); var piece = this.findContentPiece(node); var target = this.findContentTarget(node); name = this.trim(name); piece = this.trim(piece); target = this.trim(target); return { name: name || 'Unknown', piece: piece || 'Unknown', target: target || '' }; }, collectContent: function (contentNodes) { if (!contentNodes || !contentNodes.length) { return []; } var contents = []; var index, contentBlock; for (index = 0; index < contentNodes.length; index++) { contentBlock = this.buildContentBlock(contentNodes[index]); if (isDefined(contentBlock)) { contents.push(contentBlock); } } return contents; }, setLocation: function (location) { this.location = location; }, getLocation: function () { var locationAlias = this.location || windowAlias.location; if (!locationAlias.origin) { locationAlias.origin = locationAlias.protocol + "//" + locationAlias.hostname + (locationAlias.port ? ':' + locationAlias.port: ''); } return locationAlias; }, toAbsoluteUrl: function (url) { if ((!url || String(url) !== url) && url !== '') { // we only handle strings return url; } if ('' === url) { return this.getLocation().href; } // Eg //example.com/test.jpg if (url.search(/^\/\//) !== -1) { return this.getLocation().protocol + url; } // Eg http://example.com/test.jpg if (url.search(/:\/\//) !== -1) { return url; } // Eg #test.jpg if (0 === url.indexOf('#')) { return this.getLocation().origin + this.getLocation().pathname + url; } // Eg ?x=5 if (0 === url.indexOf('?')) { return this.getLocation().origin + this.getLocation().pathname + url; } // Eg mailto:x@y.z tel:012345, ... market:... sms:..., javascript:... ecmascript: ... and many more if (0 === url.search('^[a-zA-Z]{2,11}:')) { return url; } // Eg /test.jpg if (url.search(/^\//) !== -1) { return this.getLocation().origin + url; } // Eg test.jpg var regexMatchDir = '(.*\/)'; var base = this.getLocation().origin + this.getLocation().pathname.match(new RegExp(regexMatchDir))[0]; return base + url; }, isUrlToCurrentDomain: function (url) { var absoluteUrl = this.toAbsoluteUrl(url); if (!absoluteUrl) { return false; } var origin = this.getLocation().origin; if (origin === absoluteUrl) { return true; } if (0 === String(absoluteUrl).indexOf(origin)) { if (':' === String(absoluteUrl).substr(origin.length, 1)) { return false; // url has port whereas origin has not => different URL } return true; } return false; }, setHrefAttribute: function (node, url) { if (!node || !url) { return; } query.setAnyAttribute(node, 'href', url); }, shouldIgnoreInteraction: function (targetNode) { if (query.hasNodeAttribute(targetNode, this.CONTENT_IGNOREINTERACTION_ATTR)) { return true; } if (query.hasNodeCssClass(targetNode, this.CONTENT_IGNOREINTERACTION_CLASS)) { return true; } if (query.hasNodeCssClass(targetNode, this.LEGACY_CONTENT_IGNOREINTERACTION_CLASS)) { return true; } return false; } }; /************************************************************ * Page Overlay ************************************************************/ function getMatomoUrlForOverlay(trackerUrl, apiUrl) { if (apiUrl) { return apiUrl; } trackerUrl = content.toAbsoluteUrl(trackerUrl); // if eg http://www.example.com/js/tracker.php?version=232323 => http://www.example.com/js/tracker.php if (stringContains(trackerUrl, '?')) { var posQuery = trackerUrl.indexOf('?'); trackerUrl = trackerUrl.slice(0, posQuery); } if (stringEndsWith(trackerUrl, 'matomo.php')) { // if eg without domain or path "matomo.php" => '' trackerUrl = removeCharactersFromEndOfString(trackerUrl, 'matomo.php'.length); } else if (stringEndsWith(trackerUrl, 'piwik.php')) { // if eg without domain or path "piwik.php" => '' trackerUrl = removeCharactersFromEndOfString(trackerUrl, 'piwik.php'.length); } else if (stringEndsWith(trackerUrl, '.php')) { // if eg http://www.example.com/js/matomo.php => http://www.example.com/js/ // or if eg http://www.example.com/tracker.php => http://www.example.com/ var lastSlash = trackerUrl.lastIndexOf('/'); var includeLastSlash = 1; trackerUrl = trackerUrl.slice(0, lastSlash + includeLastSlash); } // if eg http://www.example.com/js/ => http://www.example.com/ (when not minified Matomo JS loaded) if (stringEndsWith(trackerUrl, '/js/')) { trackerUrl = removeCharactersFromEndOfString(trackerUrl, 'js/'.length); } // http://www.example.com/ return trackerUrl; } /* * Check whether this is a page overlay session * * @returns {boolean} * * {@internal side-effect: modifies window.name }} */ function isOverlaySession(configTrackerSiteId) { var windowName = 'Matomo_Overlay'; // check whether we were redirected from the matomo overlay plugin var referrerRegExp = new RegExp('index\\.php\\?module=Overlay&action=startOverlaySession' + '&idSite=([0-9]+)&period=([^&]+)&date=([^&]+)(&segment=[^&]*)?'); var match = referrerRegExp.exec(documentAlias.referrer); if (match) { // check idsite var idsite = match[1]; if (idsite !== String(configTrackerSiteId)) { return false; } // store overlay session info in window name var period = match[2], date = match[3], segment = match[4]; if (!segment) { segment = ''; } else if (segment.indexOf('&segment=') === 0) { segment = segment.substr('&segment='.length); } windowAlias.name = windowName + '###' + period + '###' + date + '###' + segment; } // retrieve and check data from window name var windowNameParts = windowAlias.name.split('###'); return windowNameParts.length === 4 && windowNameParts[0] === windowName; } /* * Inject the script needed for page overlay */ function injectOverlayScripts(configTrackerUrl, configApiUrl, configTrackerSiteId) { var windowNameParts = windowAlias.name.split('###'), period = windowNameParts[1], date = windowNameParts[2], segment = windowNameParts[3], matomoUrl = getMatomoUrlForOverlay(configTrackerUrl, configApiUrl); loadScript( matomoUrl + 'plugins/Overlay/client/client.js?v=1', function () { Matomo_Overlay_Client.initialize(matomoUrl, configTrackerSiteId, period, date, segment); } ); } function isInsideAnIframe () { var frameElement; try { // If the parent window has another origin, then accessing frameElement // throws an Error in IE. see issue #10105. frameElement = windowAlias.frameElement; } catch(e) { // When there was an Error, then we know we are inside an iframe. return true; } if (isDefined(frameElement)) { return (frameElement && String(frameElement.nodeName).toLowerCase() === 'iframe') ? true : false; } try { return windowAlias.self !== windowAlias.top; } catch (e2) { return true; } } /************************************************************ * End Page Overlay ************************************************************/ /* * Matomo Tracker class * * trackerUrl and trackerSiteId are optional arguments to the constructor * * See: Tracker.setTrackerUrl() and Tracker.setSiteId() */ function Tracker(trackerUrl, siteId) { /************************************************************ * Private members ************************************************************/ var /**/ /* * registered test hooks */ registeredHooks = {}, /**/ trackerInstance = this, // constants CONSENT_COOKIE_NAME = 'mtm_consent', COOKIE_CONSENT_COOKIE_NAME = 'mtm_cookie_consent', CONSENT_REMOVED_COOKIE_NAME = 'mtm_consent_removed', // Current URL and Referrer URL locationArray = urlFixup(documentAlias.domain, windowAlias.location.href, getReferrer()), domainAlias = domainFixup(locationArray[0]), locationHrefAlias = safeDecodeWrapper(locationArray[1]), configReferrerUrl = safeDecodeWrapper(locationArray[2]), enableJSErrorTracking = false, defaultRequestMethod = 'GET', // Request method (GET or POST) configRequestMethod = defaultRequestMethod, defaultRequestContentType = 'application/x-www-form-urlencoded; charset=UTF-8', // Request Content-Type header value; applicable when POST request method is used for submitting tracking events configRequestContentType = defaultRequestContentType, // Tracker URL configTrackerUrl = trackerUrl || '', // API URL (only set if it differs from the Tracker URL) configApiUrl = '', // This string is appended to the Tracker URL Request (eg. to send data that is not handled by the existing setters/getters) configAppendToTrackingUrl = '', // setPagePerformanceTiming sets this manually for SPAs customPagePerformanceTiming = '', // Site ID configTrackerSiteId = siteId || '', // User ID configUserId = '', // Visitor UUID visitorUUID = '', // Document URL configCustomUrl, // Document title configTitle = '', // Extensions to be treated as download links configDownloadExtensions = ['7z','aac','apk','arc','arj','asc','asf','asx','avi','azw3','bin','csv','deb','dmg','doc','docx','epub','exe','flv','gif','gz','gzip','hqx','ibooks','jar','jpg','jpeg','js','md5','mobi','mp2','mp3','mp4','mpg','mpeg','mov','movie','msi','msp','odb','odf','odg','ods','odt','ogg','ogv','pdf','phps','png','ppt','pptx','qt','qtm','ra','ram','rar','rpm','rtf','sea','sha','sha256','sha512','sig','sit','tar','tbz','tbz2','bz','bz2','tgz','torrent','txt','wav','wma','wmv','wpd','xls','xlsx','xml','xz','z','zip'], // Hosts or alias(es) to not treat as outlinks configHostsAlias = [domainAlias], // HTML anchor element classes to not track configIgnoreClasses = [], // Referrer URLs that should be excluded configExcludedReferrers = ['.paypal.com'], // Query parameters to be excluded configExcludedQueryParams = [], // HTML anchor element classes to treat as downloads configDownloadClasses = [], // HTML anchor element classes to treat at outlinks configLinkClasses = [], // Maximum delay to wait for web bug image to be fetched (in milliseconds) configTrackerPause = 500, // If enabled, always use sendBeacon if the browser supports it configAlwaysUseSendBeacon = true, // Minimum visit time after initial page view (in milliseconds) configMinimumVisitTime, // Recurring heart beat after initial ping (in milliseconds) configHeartBeatDelay, // alias to circumvent circular function dependency (JSLint requires this) heartBeatPingIfActivityAlias, // Disallow hash tags in URL configDiscardHashTag, // Custom data configCustomData, // Campaign names configCampaignNameParameters = [ 'pk_campaign', 'mtm_campaign', 'piwik_campaign', 'matomo_campaign', 'utm_campaign', 'utm_source', 'utm_medium' ], // Campaign keywords configCampaignKeywordParameters = [ 'pk_kwd', 'mtm_kwd', 'piwik_kwd', 'matomo_kwd', 'utm_term' ], // All known parameters used for campaign tracking, this list will be used when removing campaign parameters from url configCampaignKnownParameters = [ 'mtm_campaign', 'matomo_campaign', 'mtm_cpn', 'pk_campaign', 'piwik_campaign', 'pk_cpn', 'utm_campaign', // campaign name 'mtm_keyword', 'matomo_kwd', 'mtm_kwd', 'pk_keyword', 'piwik_kwd', 'pk_kwd', 'utm_term', // campaign keyword 'mtm_source', 'pk_source', 'utm_source', 'mtm_medium', 'pk_medium', 'utm_medium', 'mtm_content', 'pk_content', 'utm_content', // campaign source 'mtm_cid', 'pk_cid', 'utm_id', 'mtm_clid', // campaign (click) id 'mtm_group', 'pk_group', // campaign group 'mtm_placement', 'pk_placement' // campaign placement ], // First-party cookie name prefix configCookieNamePrefix = '_pk_', // the URL parameter that will store the visitorId if cross domain linking is enabled // pk_vid = visitor ID // first part of this URL parameter will be 16 char visitor Id. // The second part is the 10 char current timestamp and the third and last part will be a 6 characters deviceId // timestamp is needed to prevent reusing the visitorId when the URL is shared. The visitorId will be // only reused if the timestamp is less than 45 seconds old. // deviceId parameter is needed to prevent reusing the visitorId when the URL is shared. The visitorId // will be only reused if the device is still the same when opening the link. // VDI = visitor device identifier configVisitorIdUrlParameter = 'pk_vid', // Cross domain linking, the visitor ID is transmitted only in the 180 seconds following the click. configVisitorIdUrlParameterTimeoutInSeconds = 180, // First-party cookie domain // User agent defaults to origin hostname configCookieDomain, // First-party cookie path // Default is user agent defined. configCookiePath, // Whether to use "Secure" cookies that only work over SSL configCookieIsSecure = false, // Set SameSite attribute for cookies configCookieSameSite = 'Lax', // First-party cookies are disabled configCookiesDisabled = false, // Do Not Track configDoNotTrack, // Count sites which are pre-rendered configCountPreRendered, // Enable Processing and sending campaign parameters to backend. // Setting this parameter to false will cause all campaign parameters to be removed from tracked url parameters and hashes. // Campaigns will also be ignored in referrer attribution detection // If consent is required the value of this config is ignored. configEnableCampaignParameters = true, // Do we attribute the conversion to the first referrer or the most recent referrer? configConversionAttributionFirstReferrer, // Life of the visitor cookie (in milliseconds) configVisitorCookieTimeout = 33955200000, // 13 months (365 days + 28days) // Life of the session cookie (in milliseconds) configSessionCookieTimeout = 1800000, // 30 minutes // Life of the referral cookie (in milliseconds) configReferralCookieTimeout = 15768000000, // 6 months // Is performance tracking enabled configPerformanceTrackingEnabled = true, // will be set to true automatically once the onload event has finished performanceAvailable = false, // indicates if performance metrics for the page view have been sent with a request performanceTracked = false, // Whether Custom Variables scope "visit" should be stored in a cookie during the time of the visit configStoreCustomVariablesInCookie = false, // Custom Variables read from cookie, scope "visit" customVariables = false, configCustomRequestContentProcessing, // Custom Variables, scope "page" customVariablesPage = {}, // Custom Variables, scope "event" customVariablesEvent = {}, // Custom Dimensions (can be any scope) customDimensions = {}, // Custom Variables names and values are each truncated before being sent in the request or recorded in the cookie customVariableMaximumLength = 200, // Ecommerce product view ecommerceProductView = {}, // Ecommerce items ecommerceItems = {}, // Browser features via client-side data collection browserFeatures = {}, // Browser client hints clientHints = {}, clientHintsRequestQueue = [], clientHintsResolved = false, clientHintsResolving = false, // Keeps track of previously tracked content impressions trackedContentImpressions = [], isTrackOnlyVisibleContentEnabled = false, // Guard to prevent empty visits see #6415. If there is a new visitor and the first 2 (or 3 or 4) // tracking requests are at nearly same time (eg trackPageView and trackContentImpression) 2 or more // visits will be created timeNextTrackingRequestCanBeExecutedImmediately = false, // Guard against installing the link tracker more than once per Tracker instance linkTrackingInstalled = false, linkTrackingEnabled = false, crossDomainTrackingEnabled = false, // Guard against installing the activity tracker more than once per Tracker instance heartBeatSetUp = false, // bool used to detect whether this browser window had focus at least once. So far we cannot really // detect this 100% correct for an iframe so whenever Matomo is loaded inside an iframe we presume // the window had focus at least once. hadWindowFocusAtLeastOnce = isInsideAnIframe(), timeWindowLastFocused = null, // Timestamp of last tracker request sent to Matomo lastTrackerRequestTime = null, // Internal state of the pseudo click handler lastButton, lastTarget, // Hash function hash = sha1, // Domain hash value domainHash, configIdPageView, // Boolean indicating that a page view ID has been set manually configIdPageViewSetManually = false, // we measure how many pageviews have been tracked so plugins can use it to eg detect if a // pageview was already tracked or not numTrackedPageviews = 0, configCookiesToDelete = ['id', 'ses', 'cvar', 'ref'], // whether requireConsent() was called or not configConsentRequired = false, // we always have the concept of consent. by default consent is assumed unless the end user removes it, // or unless a matomo user explicitly requires consent (via requireConsent()) configHasConsent = null, // initialized below // holds all pending tracking requests that have not been tracked because we need consent consentRequestsQueue = [], // holds the actual javascript errors if enableJSErrorTracking is on, if the very same error is // happening multiple times, then it will be tracked only once within the same page view javaScriptErrors = [], // a unique ID for this tracker during this request uniqueTrackerId = trackerIdCounter++, // whether a tracking request has been sent yet during this page view hasSentTrackingRequestYet = false, configBrowserFeatureDetection = true, configFileTracking = false; // Document title try { configTitle = documentAlias.title; } catch(e) { configTitle = ''; } /* * Get cookie value */ function getCookie(cookieName) { if (configCookiesDisabled && cookieName !== CONSENT_REMOVED_COOKIE_NAME) { return 0; } var cookiePattern = new RegExp('(^|;)[ ]*' + cookieName + '=([^;]*)'), cookieMatch = cookiePattern.exec(documentAlias.cookie); return cookieMatch ? decodeWrapper(cookieMatch[2]) : 0; } configHasConsent = !getCookie(CONSENT_REMOVED_COOKIE_NAME); /* * Set cookie value */ function setCookie(cookieName, value, msToExpire, path, domain, isSecure, sameSite) { if (configCookiesDisabled && cookieName !== CONSENT_REMOVED_COOKIE_NAME) { return; } var expiryDate; // relative time to expire in milliseconds if (msToExpire) { expiryDate = new Date(); expiryDate.setTime(expiryDate.getTime() + msToExpire); } if (!sameSite) { sameSite = 'Lax'; } documentAlias.cookie = cookieName + '=' + encodeWrapper(value) + (msToExpire ? ';expires=' + expiryDate.toGMTString() : '') + ';path=' + (path || '/') + (domain ? ';domain=' + domain : '') + (isSecure ? ';secure' : '') + ';SameSite=' + sameSite; // check the cookie was actually set if ((!msToExpire || msToExpire >= 0) && getCookie(cookieName) !== String(value)) { var msg = 'There was an error setting cookie `' + cookieName + '`. Please check domain and path.'; logConsoleError(msg); } } /* * Removes hash tag from the URL * Removes ignore_referrer/ignore_referer * Removes configVisitorIdUrlParameter * Removes campaign parameters * * URLs are purified before being recorded in the cookie, * or before being sent as GET parameters */ function purify(url) { var targetPattern, i; // Remove campaign names/keywords from URL if (configEnableCampaignParameters !== true && !configConsentRequired) { for (i = 0; i < configCampaignNameParameters.length; i++) { url = removeUrlParameter(url, configCampaignNameParameters[i]); } for (i = 0; i < configCampaignKeywordParameters.length; i++) { url = removeUrlParameter(url, configCampaignKeywordParameters[i]); } for (i = 0; i < configCampaignKnownParameters.length; i++) { url = removeUrlParameter(url, configCampaignKnownParameters[i]); } } // we need to remove this parameter here, they wouldn't be removed in Matomo tracker otherwise eg // for outlinks or referrers url = removeUrlParameter(url, configVisitorIdUrlParameter); // remove ignore referrer parameter if present url = removeUrlParameter(url, 'ignore_referrer'); url = removeUrlParameter(url, 'ignore_referer'); for (i = 0; i < configExcludedQueryParams.length; i++) { url = removeUrlParameter(url, configExcludedQueryParams[i]); } if (configDiscardHashTag) { targetPattern = new RegExp('#.*'); return url.replace(targetPattern, ''); } return url; } /* * 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) + '://' + getHostName(baseUrl) + url; } baseUrl = purify(baseUrl); i = baseUrl.indexOf('?'); if (i >= 0) { baseUrl = baseUrl.slice(0, i); } i = baseUrl.lastIndexOf('/'); if (i !== baseUrl.length - 1) { baseUrl = baseUrl.slice(0, i + 1); } return baseUrl + url; } function isSameHost (hostName, alias) { var offset; hostName = String(hostName).toLowerCase(); alias = String(alias).toLowerCase(); if (hostName === alias) { return true; } if (alias.slice(0, 1) === '.') { if (hostName === alias.slice(1)) { return true; } offset = hostName.length - alias.length; if ((offset > 0) && (hostName.slice(offset) === alias)) { return true; } } return false; } /* * Extract pathname from URL. element.pathname is actually supported by pretty much all browsers including * IE6 apart from some rare very old ones */ function getPathName(url) { var parser = document.createElement('a'); if (url.indexOf('//') !== 0 && url.indexOf('http') !== 0) { if (url.indexOf('*') === 0) { url = url.substr(1); } if (url.indexOf('.') === 0) { url = url.substr(1); } url = 'http://' + url; } parser.href = content.toAbsoluteUrl(url); if (parser.pathname) { return parser.pathname; } return ''; } function isSitePath (path, pathAlias) { if(!stringStartsWith(pathAlias, '/')) { pathAlias = '/' + pathAlias; } if(!stringStartsWith(path, '/')) { path = '/' + path; } var matchesAnyPath = (pathAlias === '/' || pathAlias === '/*'); if (matchesAnyPath) { return true; } if (path === pathAlias) { return true; } pathAlias = String(pathAlias).toLowerCase(); path = String(path).toLowerCase(); // wildcard path support if(stringEndsWith(pathAlias, '*')) { // remove the final '*' before comparing pathAlias = pathAlias.slice(0, -1); // Note: this is almost duplicated from just few lines above matchesAnyPath = (!pathAlias || pathAlias === '/'); if (matchesAnyPath) { return true; } if (path === pathAlias) { return true; } // wildcard match return path.indexOf(pathAlias) === 0; } // we need to append slashes so /foobarbaz won't match a site /foobar if (!stringEndsWith(path, '/')) { path += '/'; } if (!stringEndsWith(pathAlias, '/')) { pathAlias += '/'; } return path.indexOf(pathAlias) === 0; } /** * Whether the specified domain name and path belong to any of the alias domains (eg. set via setDomains). * * Note: this function is used to determine whether a click on a URL will be considered an "Outlink". * * @param host * @param path * @returns {boolean} */ function isSiteHostPath(host, path) { var i, alias, configAlias, aliasHost, aliasPath; for (i = 0; i < configHostsAlias.length; i++) { aliasHost = domainFixup(configHostsAlias[i]); aliasPath = getPathName(configHostsAlias[i]); if (isSameHost(host, aliasHost) && isSitePath(path, aliasPath)) { return true; } } return false; } /* * Is the host local? (i.e., not an outlink) */ function isSiteHostName(hostName) { var i, alias, offset; for (i = 0; i < configHostsAlias.length; i++) { alias = domainFixup(configHostsAlias[i].toLowerCase()); if (hostName === alias) { return true; } if (alias.slice(0, 1) === '.') { if (hostName === alias.slice(1)) { return true; } offset = hostName.length - alias.length; if ((offset > 0) && (hostName.slice(offset) === alias)) { return true; } } } return false; } /** * Whether the specified referrer url matches one of the configured excluded referrers. * * @param {string} referrerUrl * @returns {boolean} */ function isReferrerExcluded(referrerUrl) { var i, host, path, aliasHost, aliasPath; if (!referrerUrl.length || !configExcludedReferrers.length) { return false; } host = getHostName(referrerUrl); path = getPathName(referrerUrl); // ignore www subdomain if (host.indexOf('www.') === 0) { host = host.substr(4); } for (i = 0; i < configExcludedReferrers.length; i++) { aliasHost = domainFixup(configExcludedReferrers[i]); aliasPath = getPathName(configExcludedReferrers[i]); // ignore www subdomain if (aliasHost.indexOf('www.') === 0) { aliasHost = aliasHost.substr(4); } if (isSameHost(host, aliasHost) && isSitePath(path, aliasPath)) { return true; } } return false; } /* * Checks if the special query parameter was included in the current URL indicating this * is supposed to be a tracking code install test. */ function wasJsTrackingCodeInstallCheckParamProvided() { if (trackerInstallCheckNonce && trackerInstallCheckNonce.length > 0) { return true; } trackerInstallCheckNonce = getUrlParameter(windowAlias.location.href, 'tracker_install_check'); return trackerInstallCheckNonce && trackerInstallCheckNonce.length > 0; } /** * If the query parameter was included in the current URL indicating it's an install check, close the window */ function closeWindowIfJsTrackingCodeInstallCheck() { // If the query parameter indicating this is a test exists if (wasJsTrackingCodeInstallCheckParamProvided() && isObject(windowAlias)) { windowAlias.close(); } } /* * Send image request to Matomo server using GET. * The infamous web bug (or beacon) is a transparent, single pixel (1x1) image */ function getImage(request, callback) { // make sure to actually load an image so callback gets invoked request = request.replace("send_image=0","send_image=1"); var image = new Image(1, 1); image.onload = function () { iterator = 0; // To avoid JSLint warning of empty block if (typeof callback === 'function') { callback({request: request, trackerUrl: configTrackerUrl, success: true}); } }; image.onerror = function () { if (typeof callback === 'function') { callback({request: request, trackerUrl: configTrackerUrl, success: false}); } }; image.src = configTrackerUrl + (configTrackerUrl.indexOf('?') < 0 ? '?' : '&') + request; // If the query parameter indicating this is a test exists, close after first request is sent closeWindowIfJsTrackingCodeInstallCheck(); } function shouldForcePost(request) { if (configRequestMethod === 'POST') { return true; } // we force long single request urls and bulk requests over post return request && (request.length > 2000 || request.indexOf('{"requests"') === 0); } function supportsSendBeacon() { return 'object' === typeof navigatorAlias && 'function' === typeof navigatorAlias.sendBeacon && 'function' === typeof Blob; } function sendPostRequestViaSendBeacon(request, callback, fallbackToGet) { var isSupported = supportsSendBeacon(); if (!isSupported) { return false; } var headers = {type: 'application/x-www-form-urlencoded; charset=UTF-8'}; var success = false; var url = configTrackerUrl; try { var blob = new Blob([request], headers); if (fallbackToGet && !shouldForcePost(request)) { blob = new Blob([], headers); url = url + (url.indexOf('?') < 0 ? '?' : '&') + request; } success = navigatorAlias.sendBeacon(url, blob); // returns true if the user agent is able to successfully queue the data for transfer, // Otherwise it returns false and we need to try the regular way } catch (e) { return false; } if (success && typeof callback === 'function') { callback({request: request, trackerUrl: configTrackerUrl, success: true, isSendBeacon: true}); } // If the query parameter indicating this is a test exists, close after first request is sent closeWindowIfJsTrackingCodeInstallCheck(); return success; } /* * POST request to Matomo server using XMLHttpRequest. */ function sendXmlHttpRequest(request, callback, fallbackToGet) { if (!isDefined(fallbackToGet) || null === fallbackToGet) { fallbackToGet = true; } if (isPageUnloading && sendPostRequestViaSendBeacon(request, callback, fallbackToGet)) { return; } setTimeout(function () { // we execute it with a little delay in case the unload event occurred just after sending this request // this is to avoid the following behaviour: Eg on form submit a tracking request is sent via POST // in this method. Then a few ms later the browser wants to navigate to the new page and the unload // event occurs and the browser cancels the just triggered POST request. This causes or fallback // method to be triggered and we execute the same request again (either as fallbackGet or sendBeacon). // The problem is that we do not know whether the initial POST request was already fully transferred // to the server or not when the onreadystatechange callback is executed and we might execute the // same request a second time. To avoid this, we delay the actual execution of this POST request just // by 50ms which gives it usually enough time to detect the unload event in most cases. if (isPageUnloading && sendPostRequestViaSendBeacon(request, callback, fallbackToGet)) { return; } var sentViaBeacon; try { // we use the progid Microsoft.XMLHTTP because // IE5.5 included MSXML 2.5; the progid MSXML2.XMLHTTP // is pinned to MSXML2.XMLHTTP.3.0 var xhr = windowAlias.XMLHttpRequest ? new windowAlias.XMLHttpRequest() : windowAlias.ActiveXObject ? new ActiveXObject('Microsoft.XMLHTTP') : null; xhr.open('POST', configTrackerUrl, true); // fallback on error xhr.onreadystatechange = function () { if (this.readyState === 4 && !(this.status >= 200 && this.status < 300)) { var sentViaBeacon = isPageUnloading && sendPostRequestViaSendBeacon(request, callback, fallbackToGet); if (!sentViaBeacon && fallbackToGet) { getImage(request, callback); } else if (typeof callback === 'function') { callback({request: request, trackerUrl: configTrackerUrl, success: false, xhr: this}); } } else { if (this.readyState === 4 && (typeof callback === 'function')) { callback({request: request, trackerUrl: configTrackerUrl, success: true, xhr: this}); } } }; xhr.setRequestHeader('Content-Type', configRequestContentType); xhr.withCredentials = true; xhr.send(request); } catch (e) { sentViaBeacon = isPageUnloading && sendPostRequestViaSendBeacon(request, callback, fallbackToGet); if (!sentViaBeacon && fallbackToGet) { getImage(request, callback); } else if (typeof callback === 'function') { callback({request: request, trackerUrl: configTrackerUrl, success: false}); } } // If the query parameter indicating this is a test exists, close after first request is sent closeWindowIfJsTrackingCodeInstallCheck(); }, 50); } function setExpireDateTime(delay) { var now = new Date(); var time = now.getTime() + delay; if (!expireDateTime || time > expireDateTime) { expireDateTime = time; } } function heartBeatOnFocus() { hadWindowFocusAtLeastOnce = true; timeWindowLastFocused = new Date().getTime(); } function hadWindowMinimalFocusToConsiderViewed() { // we ping on blur or unload only if user was active for more than configHeartBeatDelay seconds on // the page otherwise we can assume user was not really on the page and for example only switching // through tabs var now = new Date().getTime(); return !timeWindowLastFocused || (now - timeWindowLastFocused) > configHeartBeatDelay; } function heartBeatOnBlur() { if (hadWindowMinimalFocusToConsiderViewed()) { heartBeatPingIfActivityAlias(); } } function heartBeatOnVisible() { if (documentAlias.visibilityState === 'hidden' && hadWindowMinimalFocusToConsiderViewed()) { heartBeatPingIfActivityAlias(); } else if (documentAlias.visibilityState === 'visible') { timeWindowLastFocused = new Date().getTime(); } } /* * Setup event handlers and timeout for initial heart beat. */ function setUpHeartBeat() { if (heartBeatSetUp || !configHeartBeatDelay ) { return; } heartBeatSetUp = true; addEventListener(windowAlias, 'focus', heartBeatOnFocus); addEventListener(windowAlias, 'blur', heartBeatOnBlur); addEventListener(windowAlias, 'visibilitychange', heartBeatOnVisible); // when using multiple trackers then we need to add this event for each tracker coreHeartBeatCounter++; Matomo.addPlugin('HeartBeat' + coreHeartBeatCounter, { unload: function () { // we can't remove the unload plugin event when disabling heart beat timer but we at least // check if it is still enabled... note: when enabling heart beat, then disabling, then // enabling then this could trigger two requests under circumstances maybe. it's edge case though // we only send the heartbeat if onunload the user spent at least 15seconds since last focus // or the configured heatbeat timer if (heartBeatSetUp && hadWindowMinimalFocusToConsiderViewed()) { heartBeatPingIfActivityAlias(); } } }); } function makeSureThereIsAGapAfterFirstTrackingRequestToPreventMultipleVisitorCreation(callback) { var now = new Date(); var timeNow = now.getTime(); lastTrackerRequestTime = timeNow; if (timeNextTrackingRequestCanBeExecutedImmediately && timeNow < timeNextTrackingRequestCanBeExecutedImmediately) { // we are in the time frame shortly after the first request. we have to delay this request a bit to make sure // a visitor has been created meanwhile. var timeToWait = timeNextTrackingRequestCanBeExecutedImmediately - timeNow; setTimeout(callback, timeToWait); setExpireDateTime(timeToWait + 50); // set timeout is not necessarily executed at timeToWait so delay a bit more timeNextTrackingRequestCanBeExecutedImmediately += 50; // delay next tracking request by further 50ms to next execute them at same time return; } if (timeNextTrackingRequestCanBeExecutedImmediately === false) { // it is the first request, we want to execute this one directly and delay all the next one(s) within a delay. // All requests after this delay can be executed as usual again var delayInMs = 800; timeNextTrackingRequestCanBeExecutedImmediately = timeNow + delayInMs; } callback(); } /* * Check first-party cookies and update the configHasConsent value. Ensures that any * change to the user opt-in/out status in another browser window will be respected. */ function refreshConsentStatus() { if (getCookie(CONSENT_REMOVED_COOKIE_NAME)) { configHasConsent = false; } else if (getCookie(CONSENT_COOKIE_NAME)) { configHasConsent = true; } } function injectBrowserFeaturesAndClientHints(request) { var i, appendix = '', bfAppendix = ''; for (i in browserFeatures) { if (Object.prototype.hasOwnProperty.call(browserFeatures, i)) { bfAppendix += '&' + i + '=' + browserFeatures[i]; } } if (clientHints) { appendix = '&uadata=' + encodeWrapper(windowAlias.JSON.stringify(clientHints)); } if (request instanceof Array) { for (i = 0; i < request.length; i++) { request[i] += appendix + bfAppendix; } } else { request += appendix + bfAppendix; } return request; } function supportsClientHints() { return isDefined(navigatorAlias.userAgentData) && isFunction(navigatorAlias.userAgentData.getHighEntropyValues); } function detectClientHints (callback) { if (clientHintsResolved || clientHintsResolving) { // skip if client hints were already resolved or a previous request already triggered it return; } clientHintsResolving = true; // Initialize with low entropy values that are always available clientHints = { brands: navigatorAlias.userAgentData.brands, platform: navigatorAlias.userAgentData.platform }; // try to gather high entropy values // currently this methods simply returns the requested values through a Promise // In later versions it might require a user permission navigatorAlias.userAgentData.getHighEntropyValues( ['brands', 'model', 'platform', 'platformVersion', 'uaFullVersion', 'fullVersionList'] ).then(function(ua) { var i; if (ua.fullVersionList) { // if fullVersionList is available, brands and uaFullVersion isn't needed delete ua.brands; delete ua.uaFullVersion; } clientHints = ua; clientHintsResolved = true; clientHintsResolving = false; callback(); }, function (message) { clientHintsResolved = true; clientHintsResolving = false; callback(); }); } /* * Send request */ function sendRequest(request, delay, callback) { refreshConsentStatus(); if (!configHasConsent) { consentRequestsQueue.push([request, callback]); return; } if (configBrowserFeatureDetection && !clientHintsResolved && supportsClientHints()) { clientHintsRequestQueue.push([request, callback]); return; } hasSentTrackingRequestYet = true; if (!configDoNotTrack && request) { if (configConsentRequired && configHasConsent) { // send a consent=1 when explicit consent is given for the apache logs request += '&consent=1'; } request = injectBrowserFeaturesAndClientHints(request); makeSureThereIsAGapAfterFirstTrackingRequestToPreventMultipleVisitorCreation(function () { if (configAlwaysUseSendBeacon && sendPostRequestViaSendBeacon(request, callback, true)) { setExpireDateTime(100); return; } if (shouldForcePost(request)) { sendXmlHttpRequest(request, callback); } else { getImage(request, callback); } setExpireDateTime(delay); }); } if (!heartBeatSetUp) { setUpHeartBeat(); // setup window events too, but only once } } function canSendBulkRequest(requests) { if (configDoNotTrack) { return false; } return (requests && requests.length); } function arrayChunk(theArray, chunkSize) { if (!chunkSize || chunkSize >= theArray.length) { return [theArray]; } var index = 0; var arrLength = theArray.length; var chunks = []; for (index; index < arrLength; index += chunkSize) { chunks.push(theArray.slice(index, index + chunkSize)); } return chunks; } /* * Send requests using bulk */ function sendBulkRequest(requests, delay) { if (!canSendBulkRequest(requests)) { return; } if (configBrowserFeatureDetection && !clientHintsResolved && supportsClientHints()) { clientHintsRequestQueue.push([requests, null]); return; } if (!configHasConsent) { consentRequestsQueue.push([requests, null]); return; } hasSentTrackingRequestYet = true; makeSureThereIsAGapAfterFirstTrackingRequestToPreventMultipleVisitorCreation(function () { var chunks = arrayChunk(requests, 50); var i = 0, bulk; for (i; i < chunks.length; i++) { bulk = '{"requests":["?' + injectBrowserFeaturesAndClientHints(chunks[i]).join('","?') + '"],"send_image":0}'; if (configAlwaysUseSendBeacon && sendPostRequestViaSendBeacon(bulk, null, false)) { // makes sure to load the next page faster by not waiting as long // we apply this once we know send beacon works setExpireDateTime(100); } else { sendXmlHttpRequest(bulk, null, false); } } setExpireDateTime(delay); }); } /* * Get cookie name with prefix and domain hash */ function getCookieName(baseName) { // NOTE: If the cookie name is changed, we must also update the MatomoTracker.php which // will attempt to discover first party cookies. eg. See the PHP Client method getVisitorId() return configCookieNamePrefix + baseName + '.' + configTrackerSiteId + '.' + domainHash; } function deleteCookie(cookieName, path, domain) { setCookie(cookieName, '', -129600000, path, domain); } /* * Does browser have cookies enabled (for this site)? */ function hasCookies() { if (configCookiesDisabled) { return '0'; } if(!isDefined(windowAlias.showModalDialog) && isDefined(navigatorAlias.cookieEnabled)) { return navigatorAlias.cookieEnabled ? '1' : '0'; } // for IE we want to actually set the cookie to avoid trigger a warning eg in IE see #11507 var testCookieName = configCookieNamePrefix + 'testcookie'; setCookie(testCookieName, '1', undefined, configCookiePath, configCookieDomain, configCookieIsSecure, configCookieSameSite); var hasCookie = getCookie(testCookieName) === '1' ? '1' : '0'; deleteCookie(testCookieName); return hasCookie; } /* * Update domain hash */ function updateDomainHash() { domainHash = hash((configCookieDomain || domainAlias) + (configCookiePath || '/')).slice(0, 4); // 4 hexits = 16 bits } function processClientHintsQueue () { var i, requestType; for (i = 0; i < clientHintsRequestQueue.length; i++) { requestType = typeof clientHintsRequestQueue[i][0]; if (requestType === 'string') { sendRequest(clientHintsRequestQueue[i][0], configTrackerPause, clientHintsRequestQueue[i][1]); } else if (requestType === 'object') { sendBulkRequest(clientHintsRequestQueue[i][0], configTrackerPause); } } clientHintsRequestQueue = []; } /* * Browser features (plugins, resolution, cookies) */ function detectBrowserFeatures() { // Browser Feature is disabled return empty object if (!configBrowserFeatureDetection) { return {}; } if (supportsClientHints()) { detectClientHints(processClientHintsQueue); } if (isDefined(browserFeatures.res)) { return browserFeatures; } var i, mimeType, pluginMap = { // document types pdf: 'application/pdf', // media players qt: 'video/quicktime', realp: 'audio/x-pn-realaudio-plugin', wma: 'application/x-mplayer2', // interactive multimedia fla: 'application/x-shockwave-flash', // RIA java: 'application/x-java-vm', ag: 'application/x-silverlight' }; // detect browser features except IE < 11 (IE 11 user agent is no longer MSIE) if (!((new RegExp('MSIE')).test(navigatorAlias.userAgent))) { // general plugin detection if (navigatorAlias.mimeTypes && navigatorAlias.mimeTypes.length) { for (i in pluginMap) { if (Object.prototype.hasOwnProperty.call(pluginMap, i)) { mimeType = navigatorAlias.mimeTypes[pluginMap[i]]; browserFeatures[i] = (mimeType && mimeType.enabledPlugin) ? '1' : '0'; } } } // Safari and Opera // IE6/IE7 navigator.javaEnabled can't be aliased, so test directly // on Edge navigator.javaEnabled() always returns `true`, so ignore it if (!((new RegExp('Edge[ /](\\d+[\\.\\d]+)')).test(navigatorAlias.userAgent)) && typeof navigator.javaEnabled !== 'unknown' && isDefined(navigatorAlias.javaEnabled) && navigatorAlias.javaEnabled()) { browserFeatures.java = '1'; } if (!isDefined(windowAlias.showModalDialog) && isDefined(navigatorAlias.cookieEnabled)) { browserFeatures.cookie = navigatorAlias.cookieEnabled ? '1' : '0'; } else { // Eg IE11 ... prevent error when cookieEnabled is requested within modal dialog. see #11507 browserFeatures.cookie = hasCookies(); } } var width = parseInt(screenAlias.width, 10); var height = parseInt(screenAlias.height, 10); browserFeatures.res = parseInt(width, 10) + 'x' + parseInt(height, 10); return browserFeatures; } /* * Inits the custom variables object */ function getCustomVariablesFromCookie() { var cookieName = getCookieName('cvar'), cookie = getCookie(cookieName); if (cookie && cookie.length) { cookie = windowAlias.JSON.parse(cookie); if (isObject(cookie)) { return cookie; } } return {}; } /* * Lazy loads the custom variables from the cookie, only once during this page view */ function loadCustomVariables() { if (customVariables === false) { customVariables = getCustomVariablesFromCookie(); } } /* * Generate a pseudo-unique ID to fingerprint this user * 16 hexits = 64 bits * note: this isn't a RFC4122-compliant UUID */ function generateRandomUuid() { var browserFeatures = detectBrowserFeatures(); return hash( (navigatorAlias.userAgent || '') + (navigatorAlias.platform || '') + windowAlias.JSON.stringify(browserFeatures) + (new Date()).getTime() + Math.random() ).slice(0, 16); } function generateBrowserSpecificId() { var browserFeatures = detectBrowserFeatures(); return hash( (navigatorAlias.userAgent || '') + (navigatorAlias.platform || '') + windowAlias.JSON.stringify(browserFeatures)).slice(0, 6); } function getCurrentTimestampInSeconds() { return Math.floor((new Date()).getTime() / 1000); } function makeCrossDomainDeviceId() { var timestamp = getCurrentTimestampInSeconds(); var browserId = generateBrowserSpecificId(); var deviceId = String(timestamp) + browserId; return deviceId; } function isSameCrossDomainDevice(deviceIdFromUrl) { deviceIdFromUrl = String(deviceIdFromUrl); var thisBrowserId = generateBrowserSpecificId(); var lengthBrowserId = thisBrowserId.length; var browserIdInUrl = deviceIdFromUrl.substr(-1 * lengthBrowserId, lengthBrowserId); var timestampInUrl = parseInt(deviceIdFromUrl.substr(0, deviceIdFromUrl.length - lengthBrowserId), 10); if (timestampInUrl && browserIdInUrl && browserIdInUrl === thisBrowserId) { // we only reuse visitorId when used on same device / browser var currentTimestampInSeconds = getCurrentTimestampInSeconds(); if (configVisitorIdUrlParameterTimeoutInSeconds <= 0) { return true; } if (currentTimestampInSeconds >= timestampInUrl && currentTimestampInSeconds <= (timestampInUrl + configVisitorIdUrlParameterTimeoutInSeconds)) { // we only use visitorId if it was generated max 180 seconds ago return true; } } return false; } function getVisitorIdFromUrl(url) { if (!crossDomainTrackingEnabled) { return ''; } // problem different timezone or when the time on the computer is not set correctly it may re-use // the same visitorId again. therefore we also have a factor like hashed user agent to reduce possible // activation of a visitorId on other device var visitorIdParam = getUrlParameter(url, configVisitorIdUrlParameter); if (!visitorIdParam) { return ''; } visitorIdParam = String(visitorIdParam); var pattern = new RegExp("^[a-zA-Z0-9]+$"); if (visitorIdParam.length === 32 && pattern.test(visitorIdParam)) { var visitorDevice = visitorIdParam.substr(16, 32); if (isSameCrossDomainDevice(visitorDevice)) { var visitorId = visitorIdParam.substr(0, 16); return visitorId; } } return ''; } /* * Load visitor ID cookie */ function loadVisitorIdCookie() { if (!visitorUUID) { // we are using locationHrefAlias and not currentUrl on purpose to for sure get the passed URL parameters // from original URL visitorUUID = getVisitorIdFromUrl(locationHrefAlias); } var now = new Date(), nowTs = Math.round(now.getTime() / 1000), visitorIdCookieName = getCookieName('id'), id = getCookie(visitorIdCookieName), cookieValue, uuid; // Visitor ID cookie found if (id) { cookieValue = id.split('.'); // returning visitor flag cookieValue.unshift('0'); if(visitorUUID.length) { cookieValue[1] = visitorUUID; } return cookieValue; } if(visitorUUID.length) { uuid = visitorUUID; } else if ('0' === hasCookies()){ uuid = ''; } else { uuid = generateRandomUuid(); } // No visitor ID cookie, let's create a new one cookieValue = [ // new visitor '1', // uuid uuid, // creation timestamp - seconds since Unix epoch nowTs ]; return cookieValue; } /** * Loads the Visitor ID cookie and returns a named array of values */ function getValuesFromVisitorIdCookie() { var cookieVisitorIdValue = loadVisitorIdCookie(), newVisitor = cookieVisitorIdValue[0], uuid = cookieVisitorIdValue[1], createTs = cookieVisitorIdValue[2]; return { newVisitor: newVisitor, uuid: uuid, createTs: createTs }; } function getRemainingVisitorCookieTimeout() { var now = new Date(), nowTs = now.getTime(), cookieCreatedTs = getValuesFromVisitorIdCookie().createTs; var createTs = parseInt(cookieCreatedTs, 10); var originalTimeout = (createTs * 1000) + configVisitorCookieTimeout - nowTs; return originalTimeout; } /* * Sets the Visitor ID cookie */ function setVisitorIdCookie(visitorIdCookieValues) { if(!configTrackerSiteId) { // when called before Site ID was set return; } var now = new Date(), nowTs = Math.round(now.getTime() / 1000); if(!isDefined(visitorIdCookieValues)) { visitorIdCookieValues = getValuesFromVisitorIdCookie(); } var cookieValue = visitorIdCookieValues.uuid + '.' + visitorIdCookieValues.createTs + '.'; setCookie(getCookieName('id'), cookieValue, getRemainingVisitorCookieTimeout(), configCookiePath, configCookieDomain, configCookieIsSecure, configCookieSameSite); } /* * Loads the referrer attribution information * * @returns {Array} * 0: campaign name * 1: campaign keyword * 2: timestamp * 3: raw URL */ function loadReferrerAttributionCookie() { // NOTE: if the format of the cookie changes, // we must also update JS tests, PHP tracker, System tests, // and notify other tracking clients (eg. Java) of the changes var cookie = getCookie(getCookieName('ref')); if (cookie.length) { try { cookie = windowAlias.JSON.parse(cookie); if (isObject(cookie)) { return cookie; } } catch (ignore) { // Pre 1.3, this cookie was not JSON encoded } } return [ '', '', 0, '' ]; } function isPossibleToSetCookieOnDomain(domainToTest) { var testCookieName = configCookieNamePrefix + 'testcookie_domain'; var valueToSet = 'testvalue'; setCookie(testCookieName, valueToSet, 10000, null, domainToTest, configCookieIsSecure, configCookieSameSite); if (getCookie(testCookieName) === valueToSet) { deleteCookie(testCookieName, null, domainToTest); return true; } return false; } function deleteCookies() { var savedConfigCookiesDisabled = configCookiesDisabled; // Temporarily allow cookies just to delete the existing ones configCookiesDisabled = false; var index, cookieName; for (index = 0; index < configCookiesToDelete.length; index++) { cookieName = getCookieName(configCookiesToDelete[index]); if (cookieName !== CONSENT_REMOVED_COOKIE_NAME && cookieName !== CONSENT_COOKIE_NAME && 0 !== getCookie(cookieName)) { deleteCookie(cookieName, configCookiePath, configCookieDomain); } } configCookiesDisabled = savedConfigCookiesDisabled; } function setSiteId(siteId) { configTrackerSiteId = siteId; } function sortObjectByKeys(value) { if (!value || !isObject(value)) { return; } // Object.keys(value) is not supported by all browsers, we get the keys manually var keys = []; var key; for (key in value) { if (Object.prototype.hasOwnProperty.call(value, key)) { keys.push(key); } } var normalized = {}; keys.sort(); var len = keys.length; var i; for (i = 0; i < len; i++) { normalized[keys[i]] = value[keys[i]]; } return normalized; } /** * Creates the session cookie */ function setSessionCookie() { setCookie(getCookieName('ses'), '1', configSessionCookieTimeout, configCookiePath, configCookieDomain, configCookieIsSecure, configCookieSameSite); } function generateUniqueId() { var id = ''; var chars = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; var charLen = chars.length; var i; for (i = 0; i < 6; i++) { id += chars.charAt(Math.floor(Math.random() * charLen)); } return id; } function appendAvailablePerformanceMetrics(request) { if (customPagePerformanceTiming !== '') { request += customPagePerformanceTiming; performanceTracked = true; return request; } if (!performanceAlias) { return request; } var performanceData = (typeof performanceAlias.timing === 'object') && performanceAlias.timing ? performanceAlias.timing : undefined; if (!performanceData) { performanceData = (typeof performanceAlias.getEntriesByType === 'function') && performanceAlias.getEntriesByType('navigation') ? performanceAlias.getEntriesByType('navigation')[0] : undefined; } if (!performanceData) { return request; } // note: there might be negative values because of browser bugs see https://github.com/matomo-org/matomo/pull/16516 in this case we ignore the values var timings = ''; if (performanceData.connectEnd && performanceData.fetchStart) { if (performanceData.connectEnd < performanceData.fetchStart) { return request; } timings += '&pf_net=' + Math.round(performanceData.connectEnd - performanceData.fetchStart); } if (performanceData.responseStart && performanceData.requestStart) { if (performanceData.responseStart < performanceData.requestStart) { return request; } timings += '&pf_srv=' + Math.round(performanceData.responseStart - performanceData.requestStart); } if (performanceData.responseStart && performanceData.responseEnd) { if (performanceData.responseEnd < performanceData.responseStart) { return request; } timings += '&pf_tfr=' + Math.round(performanceData.responseEnd - performanceData.responseStart); } if (isDefined(performanceData.domLoading)) { if (performanceData.domInteractive && performanceData.domLoading) { if (performanceData.domInteractive < performanceData.domLoading) { return request; } timings += '&pf_dm1=' + Math.round(performanceData.domInteractive - performanceData.domLoading); } } else { if (performanceData.domInteractive && performanceData.responseEnd) { if (performanceData.domInteractive < performanceData.responseEnd) { return request; } timings += '&pf_dm1=' + Math.round(performanceData.domInteractive - performanceData.responseEnd); } } if (performanceData.domComplete && performanceData.domInteractive) { if (performanceData.domComplete < performanceData.domInteractive) { return request; } timings += '&pf_dm2=' + Math.round(performanceData.domComplete - performanceData.domInteractive); } if (performanceData.loadEventEnd && performanceData.loadEventStart) { if (performanceData.loadEventEnd < performanceData.loadEventStart) { return request; } timings += '&pf_onl=' + Math.round(performanceData.loadEventEnd - performanceData.loadEventStart); } return request + timings; } /** * Returns if the given url contains a parameter to ignore the referrer * e.g. ignore_referer or ignore_referrer * @param url * @returns {boolean} */ function hasIgnoreReferrerParameter(url) { return getUrlParameter(url, 'ignore_referrer') === "1" || getUrlParameter(url, 'ignore_referer') === "1"; } function detectReferrerAttribution() { var i, now = new Date(), nowTs = Math.round(now.getTime() / 1000), referralTs, referralUrl, referralUrlMaxLength = 1024, currentReferrerHostName, originalReferrerHostName, cookieSessionName = getCookieName('ses'), cookieReferrerName = getCookieName('ref'), cookieSessionValue = getCookie(cookieSessionName), attributionCookie = loadReferrerAttributionCookie(), currentUrl = configCustomUrl || locationHrefAlias, campaignNameDetected, campaignKeywordDetected, attributionValues = {}; campaignNameDetected = attributionCookie[0]; campaignKeywordDetected = attributionCookie[1]; referralTs = attributionCookie[2]; referralUrl = attributionCookie[3]; if (!hasIgnoreReferrerParameter(currentUrl) && !cookieSessionValue) { // cookie 'ses' was not found: we consider this the start of a 'session' // Detect the campaign information from the current URL // Only if campaign wasn't previously set // Or if it was set but we must attribute to the most recent one // And if using campaign parameters isn't restricted // Note: we are working on the currentUrl before purify() since we can parse the campaign parameters in the hash tag if ((!configConversionAttributionFirstReferrer || !campaignNameDetected.length) && (configEnableCampaignParameters || configConsentRequired)) { for (i in configCampaignNameParameters) { if (Object.prototype.hasOwnProperty.call(configCampaignNameParameters, i)) { campaignNameDetected = getUrlParameter(currentUrl, configCampaignNameParameters[i]); if (campaignNameDetected.length) { break; } } } for (i in configCampaignKeywordParameters) { if (Object.prototype.hasOwnProperty.call(configCampaignKeywordParameters, i)) { campaignKeywordDetected = getUrlParameter(currentUrl, configCampaignKeywordParameters[i]); if (campaignKeywordDetected.length) { break; } } } } // Store the referrer URL and time in the cookie; // referral URL depends on the first or last referrer attribution currentReferrerHostName = getHostName(configReferrerUrl); originalReferrerHostName = referralUrl.length ? getHostName(referralUrl) : ''; if (currentReferrerHostName.length // there is a referrer && !isSiteHostName(currentReferrerHostName) // domain is not the current domain && !isReferrerExcluded(configReferrerUrl) // referrer is excluded && ( !configConversionAttributionFirstReferrer // attribute to last known referrer || !originalReferrerHostName.length // previously empty || isSiteHostName(originalReferrerHostName) // previously set but in current domain || isReferrerExcluded(referralUrl) // previously set but excluded ) ) { referralUrl = configReferrerUrl; } // Set the referral cookie if we have either a Referrer URL, or detected a Campaign (or both) if (referralUrl.length || campaignNameDetected.length) { referralTs = nowTs; attributionCookie = [ campaignNameDetected, campaignKeywordDetected, referralTs, purify(referralUrl.slice(0, referralUrlMaxLength)) ]; setCookie(cookieReferrerName, windowAlias.JSON.stringify(attributionCookie), configReferralCookieTimeout, configCookiePath, configCookieDomain, configCookieIsSecure, configCookieSameSite); } } if (campaignNameDetected.length) { attributionValues._rcn = encodeWrapper(campaignNameDetected); } if (campaignKeywordDetected.length) { attributionValues._rck = encodeWrapper(campaignKeywordDetected); } attributionValues._refts = referralTs; if (String(referralUrl).length) { attributionValues._ref = encodeWrapper(purify(referralUrl.slice(0, referralUrlMaxLength))); } return attributionValues; } /** * Returns the URL to call matomo.php, * with the standard parameters (plugins, resolution, url, referrer, etc.). * Sends the pageview and browser settings with every request in case of race conditions. */ function getRequest(request, customData, pluginMethod) { var i, now = new Date(), customVariablesCopy = customVariables, cookieCustomVariablesName = getCookieName('cvar'), currentUrl = configCustomUrl || locationHrefAlias, hasIgnoreReferrerParam = hasIgnoreReferrerParameter(currentUrl); if (configCookiesDisabled) { deleteCookies(); } if (configDoNotTrack) { return ''; } var fileRegex = new RegExp('^file://', 'i'); if (!configFileTracking && (windowAlias.location.protocol === 'file:' || fileRegex.test(currentUrl))) { return ''; } // trigger detection of browser feature to ensure a request might not end up in the client hints queue without being processed detectBrowserFeatures(); var cookieVisitorIdValues = getValuesFromVisitorIdCookie(); // send charset if document charset is not utf-8. sometimes encoding // of urls will be the same as this and not utf-8, which will cause problems // do not send charset if it is utf8 since it's assumed by default in Matomo var charSet = documentAlias.characterSet || documentAlias.charset; if (!charSet || charSet.toLowerCase() === 'utf-8') { charSet = null; } // build out the rest of the request request += '&idsite=' + configTrackerSiteId + '&rec=1' + '&r=' + String(Math.random()).slice(2, 8) + // keep the string to a minimum '&h=' + now.getHours() + '&m=' + now.getMinutes() + '&s=' + now.getSeconds() + '&url=' + encodeWrapper(purify(currentUrl)) + (configReferrerUrl.length && !isReferrerExcluded(configReferrerUrl) && !hasIgnoreReferrerParam ? '&urlref=' + encodeWrapper(purify(configReferrerUrl)) : '') + (isNumberOrHasLength(configUserId) ? '&uid=' + encodeWrapper(configUserId) : '') + '&_id=' + cookieVisitorIdValues.uuid + '&_idn=' + cookieVisitorIdValues.newVisitor + // currently unused (charSet ? '&cs=' + encodeWrapper(charSet) : '') + '&send_image=0'; var referrerAttribution = detectReferrerAttribution(); // referrer attribution for (i in referrerAttribution) { if (Object.prototype.hasOwnProperty.call(referrerAttribution, i)) { request += '&' + i + '=' + referrerAttribution[i]; } } var customDimensionIdsAlreadyHandled = []; if (customData) { for (i in customData) { if (Object.prototype.hasOwnProperty.call(customData, i) && /^dimension\d+$/.test(i)) { var index = i.replace('dimension', ''); customDimensionIdsAlreadyHandled.push(parseInt(index, 10)); customDimensionIdsAlreadyHandled.push(String(index)); request += '&' + i + '=' + encodeWrapper(customData[i]); delete customData[i]; } } } if (customData && isObjectEmpty(customData)) { customData = null; // we deleted all keys from custom data } // product page view for (i in ecommerceProductView) { if (Object.prototype.hasOwnProperty.call(ecommerceProductView, i)) { request += '&' + i + '=' + encodeWrapper(ecommerceProductView[i]); } } // custom dimensions for (i in customDimensions) { if (Object.prototype.hasOwnProperty.call(customDimensions, i)) { var isNotSetYet = (-1 === indexOfArray(customDimensionIdsAlreadyHandled, i)); if (isNotSetYet) { request += '&dimension' + i + '=' + encodeWrapper(customDimensions[i]); } } } // custom data if (customData) { request += '&data=' + encodeWrapper(windowAlias.JSON.stringify(customData)); } else if (configCustomData) { request += '&data=' + encodeWrapper(windowAlias.JSON.stringify(configCustomData)); } // Custom Variables, scope "page" function appendCustomVariablesToRequest(customVariables, parameterName) { var customVariablesStringified = windowAlias.JSON.stringify(customVariables); if (customVariablesStringified.length > 2) { return '&' + parameterName + '=' + encodeWrapper(customVariablesStringified); } return ''; } var sortedCustomVarPage = sortObjectByKeys(customVariablesPage); var sortedCustomVarEvent = sortObjectByKeys(customVariablesEvent); request += appendCustomVariablesToRequest(sortedCustomVarPage, 'cvar'); request += appendCustomVariablesToRequest(sortedCustomVarEvent, 'e_cvar'); // Custom Variables, scope "visit" if (customVariables) { request += appendCustomVariablesToRequest(customVariables, '_cvar'); // Don't save deleted custom variables in the cookie for (i in customVariablesCopy) { if (Object.prototype.hasOwnProperty.call(customVariablesCopy, i)) { if (customVariables[i][0] === '' || customVariables[i][1] === '') { delete customVariables[i]; } } } if (configStoreCustomVariablesInCookie) { setCookie(cookieCustomVariablesName, windowAlias.JSON.stringify(customVariables), configSessionCookieTimeout, configCookiePath, configCookieDomain, configCookieIsSecure, configCookieSameSite); } } // performance tracking if (configPerformanceTrackingEnabled && performanceAvailable && !performanceTracked) { request = appendAvailablePerformanceMetrics(request); performanceTracked = true; } if (configIdPageView) { request += '&pv_id=' + configIdPageView; } // update cookies setVisitorIdCookie(cookieVisitorIdValues); setSessionCookie(); // tracker plugin hook request += executePluginMethod(pluginMethod, {tracker: trackerInstance, request: request}); if (configAppendToTrackingUrl.length) { request += '&' + configAppendToTrackingUrl; } if (wasJsTrackingCodeInstallCheckParamProvided()) { request += '&tracker_install_check=' + trackerInstallCheckNonce; } if (isFunction(configCustomRequestContentProcessing)) { request = configCustomRequestContentProcessing(request); } return request; } /* * If there was user activity since the last check, and it's been configHeartBeatDelay seconds * since the last tracker, send a ping request (the heartbeat timeout will be reset by sendRequest). */ heartBeatPingIfActivityAlias = function heartBeatPingIfActivity() { var now = new Date(); now = now.getTime(); if (!lastTrackerRequestTime) { return false; // no tracking request was ever sent so lets not send heartbeat now } if (lastTrackerRequestTime + configHeartBeatDelay <= now) { trackerInstance.ping(); return true; } return false; }; function logEcommerce(orderId, grandTotal, subTotal, tax, shipping, discount) { var request = 'idgoal=0', now = new Date(), items = [], sku, isEcommerceOrder = String(orderId).length; if (isEcommerceOrder) { request += '&ec_id=' + encodeWrapper(orderId); } request += '&revenue=' + grandTotal; if (String(subTotal).length) { request += '&ec_st=' + subTotal; } if (String(tax).length) { request += '&ec_tx=' + tax; } if (String(shipping).length) { request += '&ec_sh=' + shipping; } if (String(discount).length) { request += '&ec_dt=' + discount; } if (ecommerceItems) { // Removing the SKU index in the array before JSON encoding for (sku in ecommerceItems) { if (Object.prototype.hasOwnProperty.call(ecommerceItems, sku)) { // Ensure name and category default to healthy value if (!isDefined(ecommerceItems[sku][1])) { ecommerceItems[sku][1] = ""; } if (!isDefined(ecommerceItems[sku][2])) { ecommerceItems[sku][2] = ""; } // Set price to zero if (!isDefined(ecommerceItems[sku][3]) || String(ecommerceItems[sku][3]).length === 0) { ecommerceItems[sku][3] = 0; } // Set quantity to 1 if (!isDefined(ecommerceItems[sku][4]) || String(ecommerceItems[sku][4]).length === 0) { ecommerceItems[sku][4] = 1; } items.push(ecommerceItems[sku]); } } request += '&ec_items=' + encodeWrapper(windowAlias.JSON.stringify(items)); } request = getRequest(request, configCustomData, 'ecommerce'); sendRequest(request, configTrackerPause); if (isEcommerceOrder) { ecommerceItems = {}; } } function logEcommerceOrder(orderId, grandTotal, subTotal, tax, shipping, discount) { if (String(orderId).length && isDefined(grandTotal)) { logEcommerce(orderId, grandTotal, subTotal, tax, shipping, discount); } } function logEcommerceCartUpdate(grandTotal) { if (isDefined(grandTotal)) { logEcommerce("", grandTotal, "", "", "", ""); } } /* * Log the page view / visit */ function logPageView(customTitle, customData, callback) { if (!configIdPageViewSetManually) { configIdPageView = generateUniqueId(); } var request = getRequest('action_name=' + encodeWrapper(titleFixup(customTitle || configTitle)), customData, 'log'); // append already available performance metrics if they were not already tracked (or appended) if (configPerformanceTrackingEnabled && !performanceTracked) { request = appendAvailablePerformanceMetrics(request); } sendRequest(request, configTrackerPause, callback); } /* * Construct regular expression of classes */ function getClassesRegExp(configClasses, defaultClass) { var i, classesRegExp = '(^| )(piwik[_-]' + defaultClass + '|matomo[_-]' + defaultClass; if (configClasses) { for (i = 0; i < configClasses.length; i++) { classesRegExp += '|' + configClasses[i]; } } classesRegExp += ')( |$)'; return new RegExp(classesRegExp); } function startsUrlWithTrackerUrl(url) { return (configTrackerUrl && url && 0 === String(url).indexOf(configTrackerUrl)); } /* * Link or Download? */ function getLinkType(className, href, isInLink, hasDownloadAttribute) { if (startsUrlWithTrackerUrl(href)) { return 0; } // does class indicate whether it is an (explicit/forced) outlink or a download? var downloadPattern = getClassesRegExp(configDownloadClasses, 'download'), linkPattern = getClassesRegExp(configLinkClasses, 'link'), // does file extension indicate that it is a download? downloadExtensionsPattern = new RegExp('\\.(' + configDownloadExtensions.join('|') + ')([?&#]|$)', 'i'); if (linkPattern.test(className)) { return 'link'; } if (hasDownloadAttribute || downloadPattern.test(className) || downloadExtensionsPattern.test(href)) { return 'download'; } if (isInLink) { return 0; } return 'link'; } function getSourceElement(sourceElement) { var parentElement; parentElement = sourceElement.parentNode; while (parentElement !== null && /* buggy IE5.5 */ isDefined(parentElement)) { if (query.isLinkElement(sourceElement)) { break; } sourceElement = parentElement; parentElement = sourceElement.parentNode; } return sourceElement; } function getLinkIfShouldBeProcessed(sourceElement) { sourceElement = getSourceElement(sourceElement); if (!query.hasNodeAttribute(sourceElement, 'href')) { return; } if (!isDefined(sourceElement.href)) { return; } var href = query.getAttributeValueFromNode(sourceElement, 'href'); var originalSourcePath = sourceElement.pathname || getPathName(sourceElement.href); // browsers, such as Safari, don't downcase hostname and href var originalSourceHostName = sourceElement.hostname || getHostName(sourceElement.href); var sourceHostName = originalSourceHostName.toLowerCase(); var sourceHref = sourceElement.href.replace(originalSourceHostName, sourceHostName); // browsers, such as Safari, don't downcase hostname and href var scriptProtocol = new RegExp('^(javascript|vbscript|jscript|mocha|livescript|ecmascript|mailto|tel):', 'i'); if (!scriptProtocol.test(sourceHref)) { // track outlinks and all downloads var linkType = getLinkType(sourceElement.className, sourceHref, isSiteHostPath(sourceHostName, originalSourcePath), query.hasNodeAttribute(sourceElement, 'download')); if (linkType) { return { type: linkType, href: sourceHref }; } } } function buildContentInteractionRequest(interaction, name, piece, target) { var params = content.buildInteractionRequestParams(interaction, name, piece, target); if (!params) { return; } return getRequest(params, null, 'contentInteraction'); } function isNodeAuthorizedToTriggerInteraction(contentNode, interactedNode) { if (!contentNode || !interactedNode) { return false; } var targetNode = content.findTargetNode(contentNode); if (content.shouldIgnoreInteraction(targetNode)) { // interaction should be ignored return false; } targetNode = content.findTargetNodeNoDefault(contentNode); if (targetNode && !containsNodeElement(targetNode, interactedNode)) { /** * There is a target node defined but the clicked element is not within the target node. example: *
YZ
* * The user clicked in this case on link Z and not on target Y */ return false; } return true; } function getContentInteractionToRequestIfPossible (anyNode, interaction, fallbackTarget) { if (!anyNode) { return; } var contentNode = content.findParentContentNode(anyNode); if (!contentNode) { // we are not within a content block return; } if (!isNodeAuthorizedToTriggerInteraction(contentNode, anyNode)) { return; } var contentBlock = content.buildContentBlock(contentNode); if (!contentBlock) { return; } if (!contentBlock.target && fallbackTarget) { contentBlock.target = fallbackTarget; } return content.buildInteractionRequestParams(interaction, contentBlock.name, contentBlock.piece, contentBlock.target); } function wasContentImpressionAlreadyTracked(contentBlock) { if (!trackedContentImpressions || !trackedContentImpressions.length) { return false; } var index, trackedContent; for (index = 0; index < trackedContentImpressions.length; index++) { trackedContent = trackedContentImpressions[index]; if (trackedContent && trackedContent.name === contentBlock.name && trackedContent.piece === contentBlock.piece && trackedContent.target === contentBlock.target) { return true; } } return false; } function trackContentImpressionClickInteraction (targetNode) { return function (event) { if (!targetNode) { return; } var contentBlock = content.findParentContentNode(targetNode); var interactedElement; if (event) { interactedElement = event.target || event.srcElement; } if (!interactedElement) { interactedElement = targetNode; } if (!isNodeAuthorizedToTriggerInteraction(contentBlock, interactedElement)) { return; } if (!contentBlock) { return false; } var theTargetNode = content.findTargetNode(contentBlock); if (!theTargetNode || content.shouldIgnoreInteraction(theTargetNode)) { return false; } var link = getLinkIfShouldBeProcessed(theTargetNode); if (linkTrackingEnabled && link && link.type) { return link.type; // will be handled via outlink or download. } return trackerInstance.trackContentInteractionNode(interactedElement, 'click'); }; } function setupInteractionsTracking(contentNodes) { if (!contentNodes || !contentNodes.length) { return; } var index, targetNode; for (index = 0; index < contentNodes.length; index++) { targetNode = content.findTargetNode(contentNodes[index]); if (targetNode && !targetNode.contentInteractionTrackingSetupDone) { targetNode.contentInteractionTrackingSetupDone = true; addEventListener(targetNode, 'click', trackContentImpressionClickInteraction(targetNode)); } } } /* * Log all content pieces */ function buildContentImpressionsRequests(contents, contentNodes) { if (!contents || !contents.length) { return []; } var index, request; for (index = 0; index < contents.length; index++) { if (wasContentImpressionAlreadyTracked(contents[index])) { contents.splice(index, 1); index--; } else { trackedContentImpressions.push(contents[index]); } } if (!contents || !contents.length) { return []; } setupInteractionsTracking(contentNodes); var requests = []; for (index = 0; index < contents.length; index++) { request = getRequest( content.buildImpressionRequestParams(contents[index].name, contents[index].piece, contents[index].target), undefined, 'contentImpressions' ); if (request) { requests.push(request); } } return requests; } /* * Log all content pieces */ function getContentImpressionsRequestsFromNodes(contentNodes) { var contents = content.collectContent(contentNodes); return buildContentImpressionsRequests(contents, contentNodes); } /* * Log currently visible content pieces */ function getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet(contentNodes) { if (!contentNodes || !contentNodes.length) { return []; } var index; for (index = 0; index < contentNodes.length; index++) { if (!content.isNodeVisible(contentNodes[index])) { contentNodes.splice(index, 1); index--; } } if (!contentNodes || !contentNodes.length) { return []; } return getContentImpressionsRequestsFromNodes(contentNodes); } function buildContentImpressionRequest(contentName, contentPiece, contentTarget) { var params = content.buildImpressionRequestParams(contentName, contentPiece, contentTarget); return getRequest(params, null, 'contentImpression'); } function buildContentInteractionRequestNode(node, contentInteraction) { if (!node) { return; } var contentNode = content.findParentContentNode(node); var contentBlock = content.buildContentBlock(contentNode); if (!contentBlock) { return; } if (!contentInteraction) { contentInteraction = 'Unknown'; } return buildContentInteractionRequest(contentInteraction, contentBlock.name, contentBlock.piece, contentBlock.target); } function buildEventRequest(category, action, name, value) { return 'e_c=' + encodeWrapper(category) + '&e_a=' + encodeWrapper(action) + (isDefined(name) ? '&e_n=' + encodeWrapper(name) : '') + (isDefined(value) ? '&e_v=' + encodeWrapper(value) : '') + '&ca=1'; } /* * Log the event */ function logEvent(category, action, name, value, customData, callback) { // Category and Action are required parameters if (!isNumberOrHasLength(category) || !isNumberOrHasLength(action)) { logConsoleError('Error while logging event: Parameters `category` and `action` must not be empty or filled with whitespaces'); return false; } var request = getRequest( buildEventRequest(category, action, name, value), customData, 'event' ); sendRequest(request, configTrackerPause, callback); } /* * Log the site search request */ function logSiteSearch(keyword, category, resultsCount, customData) { var request = getRequest('search=' + encodeWrapper(keyword) + (category ? '&search_cat=' + encodeWrapper(category) : '') + (isDefined(resultsCount) ? '&search_count=' + resultsCount : ''), customData, 'sitesearch'); sendRequest(request, configTrackerPause); } /* * Log the goal with the server */ function logGoal(idGoal, customRevenue, customData, callback) { var request = getRequest('idgoal=' + idGoal + (customRevenue ? '&revenue=' + customRevenue : ''), customData, 'goal'); sendRequest(request, configTrackerPause, callback); } /* * Log the link or click with the server */ function logLink(url, linkType, customData, callback, sourceElement) { var linkParams = linkType + '=' + encodeWrapper(purify(url)); var interaction = getContentInteractionToRequestIfPossible(sourceElement, 'click', url); if (interaction) { linkParams += '&' + interaction; } var request = getRequest(linkParams, customData, 'link'); sendRequest(request, configTrackerPause, callback); } /* * Browser prefix */ function prefixPropertyName(prefix, propertyName) { if (prefix !== '') { return prefix + propertyName.charAt(0).toUpperCase() + propertyName.slice(1); } return propertyName; } /* * Check for pre-rendered web pages, and log the page view/link/goal * according to the configuration and/or visibility * * @see http://dvcs.w3.org/hg/webperf/raw-file/tip/specs/PageVisibility/Overview.html */ function trackCallback(callback) { var isPreRendered, i, // Chrome 13, IE10, FF10 prefixes = ['', 'webkit', 'ms', 'moz'], prefix; if (!configCountPreRendered) { for (i = 0; i < prefixes.length; i++) { prefix = prefixes[i]; // does this browser support the page visibility API? if (Object.prototype.hasOwnProperty.call(documentAlias, prefixPropertyName(prefix, 'hidden'))) { // if pre-rendered, then defer callback until page visibility changes if (documentAlias[prefixPropertyName(prefix, 'visibilityState')] === 'prerender') { isPreRendered = true; } break; } } } if (isPreRendered) { // note: the event name doesn't follow the same naming convention as vendor properties addEventListener(documentAlias, prefix + 'visibilitychange', function ready() { documentAlias.removeEventListener(prefix + 'visibilitychange', ready, false); callback(); }); return; } // configCountPreRendered === true || isPreRendered === false callback(); } function getCrossDomainVisitorId() { var visitorId = trackerInstance.getVisitorId(); var deviceId = makeCrossDomainDeviceId(); return visitorId + deviceId; } function replaceHrefForCrossDomainLink(element) { if (!element) { return; } if (!query.hasNodeAttribute(element, 'href')) { return; } var link = query.getAttributeValueFromNode(element, 'href'); if (!link || startsUrlWithTrackerUrl(link)) { return; } if (!trackerInstance.getVisitorId()) { return; // cookies are disabled. } // we need to remove the parameter and add it again if needed to make sure we have latest timestamp // and visitorId (eg userId might be set etc) link = removeUrlParameter(link, configVisitorIdUrlParameter); var crossDomainVisitorId = getCrossDomainVisitorId(); link = addUrlParameter(link, configVisitorIdUrlParameter, crossDomainVisitorId); query.setAnyAttribute(element, 'href', link); } function isLinkToDifferentDomainButSameMatomoWebsite(element) { var targetLink = query.getAttributeValueFromNode(element, 'href'); if (!targetLink) { return false; } targetLink = String(targetLink); var isOutlink = targetLink.indexOf('//') === 0 || targetLink.indexOf('http://') === 0 || targetLink.indexOf('https://') === 0; if (!isOutlink) { return false; } var originalSourcePath = element.pathname || getPathName(element.href); var originalSourceHostName = (element.hostname || getHostName(element.href)).toLowerCase(); if (isSiteHostPath(originalSourceHostName, originalSourcePath)) { // we could also check against config cookie domain but this would require that other website // sets actually same cookie domain and we cannot rely on it. if (!isSameHost(domainAlias, domainFixup(originalSourceHostName))) { return true; } return false; } return false; } /* * Process clicks */ function processClick(sourceElement) { var link = getLinkIfShouldBeProcessed(sourceElement); // not a link to same domain or the same website (as set in setDomains()) if (link && link.type) { link.href = safeDecodeWrapper(link.href); logLink(link.href, link.type, undefined, null, sourceElement); return; } // a link to same domain or the same website (as set in setDomains()) if (crossDomainTrackingEnabled) { // in case the clicked element is within the (for example there is a
within the ) this will get the actual link element sourceElement = getSourceElement(sourceElement); if(isLinkToDifferentDomainButSameMatomoWebsite(sourceElement)) { replaceHrefForCrossDomainLink(sourceElement); } } } function isIE8orOlder() { return documentAlias.all && !documentAlias.addEventListener; } function getKeyCodeFromEvent(event) { // event.which is deprecated https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/which var which = event.which; /** 1 : Left mouse button 2 : Wheel button or middle button 3 : Right mouse button */ var typeOfEventButton = (typeof event.button); if (!which && typeOfEventButton !== 'undefined' ) { /** -1: No button pressed 0 : Main button pressed, usually the left button 1 : Auxiliary button pressed, usually the wheel button or themiddle button (if present) 2 : Secondary button pressed, usually the right button 3 : Fourth button, typically the Browser Back button 4 : Fifth button, typically the Browser Forward button IE8 and earlier has different values: 1 : Left mouse button 2 : Right mouse button 4 : Wheel button or middle button For a left-hand configured mouse, the return values are reversed. We do not take care of that. */ if (isIE8orOlder()) { if (event.button & 1) { which = 1; } else if (event.button & 2) { which = 3; } else if (event.button & 4) { which = 2; } } else { if (event.button === 0 || event.button === '0') { which = 1; } else if (event.button & 1) { which = 2; } else if (event.button & 2) { which = 3; } } } return which; } function getNameOfClickedButton(event) { switch (getKeyCodeFromEvent(event)) { case 1: return 'left'; case 2: return 'middle'; case 3: return 'right'; } } function getTargetElementFromEvent(event) { return event.target || event.srcElement; } function isClickNode(nodeName) { return nodeName === 'A' || nodeName === 'AREA'; } /* * Handle click event */ function clickHandler(enable) { function getLinkTarget(event) { var target = getTargetElementFromEvent(event); var nodeName = target.nodeName; var ignorePattern = getClassesRegExp(configIgnoreClasses, 'ignore'); while (!isClickNode(nodeName) && target && target.parentNode) { target = target.parentNode; nodeName = target.nodeName; } if (target && isClickNode(nodeName) && !ignorePattern.test(target.className)) { return target; } } return function (event) { event = event || windowAlias.event; var target = getLinkTarget(event); if (!target) { return; } var button = getNameOfClickedButton(event); if (event.type === 'click') { var ignoreClick = false; if (enable && button === 'middle') { // if enabled, we track middle clicks via mouseup // some browsers (eg chrome) trigger click and mousedown/up events when middle is clicked, // whereas some do not. This way we make "sure" to track them only once, either in click // (default) or in mouseup (if enable == true) ignoreClick = true; } if (target && !ignoreClick) { processClick(target); } } else if (event.type === 'mousedown') { if (button === 'middle' && target) { lastButton = button; lastTarget = target; } else { lastButton = lastTarget = null; } } else if (event.type === 'mouseup') { if (button === lastButton && target === lastTarget) { processClick(target); } lastButton = lastTarget = null; } else if (event.type === 'contextmenu') { processClick(target); } }; } /* * Add click listener to a DOM element */ function addClickListener(element, enable, useCapture) { var enableType = typeof enable; if (enableType === 'undefined') { enable = true; } addEventListener(element, 'click', clickHandler(enable), useCapture); if (enable) { addEventListener(element, 'mouseup', clickHandler(enable), useCapture); addEventListener(element, 'mousedown', clickHandler(enable), useCapture); addEventListener(element, 'contextmenu', clickHandler(enable), useCapture); } } function enableTrackOnlyVisibleContent (checkOnScroll, timeIntervalInMs, tracker) { if (isTrackOnlyVisibleContentEnabled) { // already enabled, do not register intervals again return true; } isTrackOnlyVisibleContentEnabled = true; var didScroll = false; var events, index; function setDidScroll() { didScroll = true; } trackCallbackOnLoad(function () { function checkContent(intervalInMs) { setTimeout(function () { if (!isTrackOnlyVisibleContentEnabled) { return; // the tests stopped tracking only visible content } didScroll = false; tracker.trackVisibleContentImpressions(); checkContent(intervalInMs); }, intervalInMs); } function checkContentIfDidScroll(intervalInMs) { setTimeout(function () { if (!isTrackOnlyVisibleContentEnabled) { return; // the tests stopped tracking only visible content } if (didScroll) { didScroll = false; tracker.trackVisibleContentImpressions(); } checkContentIfDidScroll(intervalInMs); }, intervalInMs); } if (checkOnScroll) { // scroll event is executed after each pixel, so we make sure not to // execute event too often. otherwise FPS goes down a lot! events = ['scroll', 'resize']; for (index = 0; index < events.length; index++) { if (documentAlias.addEventListener) { documentAlias.addEventListener(events[index], setDidScroll, false); } else { windowAlias.attachEvent('on' + events[index], setDidScroll); } } checkContentIfDidScroll(100); } if (timeIntervalInMs && timeIntervalInMs > 0) { timeIntervalInMs = parseInt(timeIntervalInMs, 10); checkContent(timeIntervalInMs); } }); } /**/ /* * Register a test hook. Using eval() permits access to otherwise * privileged members. */ function registerHook(hookName, userHook) { var hookObj = null; if (isString(hookName) && !isDefined(registeredHooks[hookName]) && userHook) { if (isObject(userHook)) { hookObj = userHook; } else if (isString(userHook)) { try { eval('hookObj =' + userHook); } catch (ignore) { } } registeredHooks[hookName] = hookObj; } return hookObj; } /**/ var requestQueue = { enabled: true, requests: [], timeout: null, interval: 2500, sendRequests: function () { var requestsToTrack = this.requests; this.requests = []; if (requestsToTrack.length === 1) { sendRequest(requestsToTrack[0], configTrackerPause); } else { sendBulkRequest(requestsToTrack, configTrackerPause); } }, canQueue: function () { return !isPageUnloading && this.enabled; }, pushMultiple: function (requests) { if (!this.canQueue()) { sendBulkRequest(requests, configTrackerPause); return; } var i; for (i = 0; i < requests.length; i++) { this.push(requests[i]); } }, push: function (requestUrl) { if (!requestUrl) { return; } if (!this.canQueue()) { // we don't queue as we need to ensure the request will be sent when the page is unloading... sendRequest(requestUrl, configTrackerPause); return; } requestQueue.requests.push(requestUrl); if (this.timeout) { clearTimeout(this.timeout); this.timeout = null; } // we always extend by another 2.5 seconds after receiving a tracking request this.timeout = setTimeout(function () { requestQueue.timeout = null; requestQueue.sendRequests(); }, requestQueue.interval); var trackerQueueId = 'RequestQueue' + uniqueTrackerId; if (!Object.prototype.hasOwnProperty.call(plugins, trackerQueueId)) { // we setup one unload handler per tracker... // Matomo.addPlugin might not be defined at this point, we add the plugin directly also to make // JSLint happy. plugins[trackerQueueId] = { unload: function () { if (requestQueue.timeout) { clearTimeout(requestQueue.timeout); } requestQueue.sendRequests(); } }; } } }; /************************************************************ * Constructor ************************************************************/ /* * initialize tracker */ updateDomainHash(); /**/ /* * initialize test plugin */ executePluginMethod('run', null, registerHook); /**/ /************************************************************ * Public data and methods ************************************************************/ /**/ /* * Test hook accessors */ this.hook = registeredHooks; this.getHook = function (hookName) { return registeredHooks[hookName]; }; this.getQuery = function () { return query; }; this.getContent = function () { return content; }; this.isUsingAlwaysUseSendBeacon = function () { return configAlwaysUseSendBeacon; }; this.buildContentImpressionRequest = buildContentImpressionRequest; this.buildContentInteractionRequest = buildContentInteractionRequest; this.buildContentInteractionRequestNode = buildContentInteractionRequestNode; this.getContentImpressionsRequestsFromNodes = getContentImpressionsRequestsFromNodes; this.getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet = getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet; this.trackCallbackOnLoad = trackCallbackOnLoad; this.trackCallbackOnReady = trackCallbackOnReady; this.buildContentImpressionsRequests = buildContentImpressionsRequests; this.wasContentImpressionAlreadyTracked = wasContentImpressionAlreadyTracked; this.appendContentInteractionToRequestIfPossible = getContentInteractionToRequestIfPossible; this.setupInteractionsTracking = setupInteractionsTracking; this.trackContentImpressionClickInteraction = trackContentImpressionClickInteraction; this.internalIsNodeVisible = isVisible; this.isNodeAuthorizedToTriggerInteraction = isNodeAuthorizedToTriggerInteraction; this.getDomains = function () { return configHostsAlias; }; this.getExcludedReferrers = function () { return configExcludedReferrers; }; this.getConfigIdPageView = function () { return configIdPageView; }; this.getConfigDownloadExtensions = function () { return configDownloadExtensions; }; this.enableTrackOnlyVisibleContent = function (checkOnScroll, timeIntervalInMs) { return enableTrackOnlyVisibleContent(checkOnScroll, timeIntervalInMs, this); }; this.clearTrackedContentImpressions = function () { trackedContentImpressions = []; }; this.getTrackedContentImpressions = function () { return trackedContentImpressions; }; this.clearEnableTrackOnlyVisibleContent = function () { isTrackOnlyVisibleContentEnabled = false; }; this.disableLinkTracking = function () { linkTrackingInstalled = false; linkTrackingEnabled = false; }; this.getConfigVisitorCookieTimeout = function () { return configVisitorCookieTimeout; }; this.getConfigCookieSameSite = function () { return configCookieSameSite; }; this.getCustomPagePerformanceTiming = function () { return customPagePerformanceTiming; }; this.removeAllAsyncTrackersButFirst = function () { var firstTracker = asyncTrackers[0]; asyncTrackers = [firstTracker]; }; this.getConsentRequestsQueue = function () { var i, requests = []; for (i=0; i < consentRequestsQueue.length; i++){ requests.push(consentRequestsQueue[i][0]); } return requests; }; this.getRequestQueue = function () { return requestQueue; }; this.getJavascriptErrors = function () { return javaScriptErrors; }; this.unsetPageIsUnloading = function () { isPageUnloading = false; }; this.getRemainingVisitorCookieTimeout = getRemainingVisitorCookieTimeout; /**/ this.hasConsent = function () { return configHasConsent; }; /** * Get the visitor information (from first party cookie) * * @returns {Array} */ this.getVisitorInfo = function () { if (!getCookie(getCookieName('id'))) { setVisitorIdCookie(); } // Note: in a new method, we could return also return getValuesFromVisitorIdCookie() // which returns named parameters rather than returning integer indexed array return loadVisitorIdCookie(); }; /** * Get visitor ID (from first party cookie) * * @returns {string} Visitor ID in hexits (or empty string, if not yet known) */ this.getVisitorId = function () { return this.getVisitorInfo()[1]; }; /** * Get the Attribution information, which is an array that contains * the Referrer used to reach the site as well as the campaign name and keyword * It is useful only when used in conjunction with Tracker API function setAttributionInfo() * To access specific data point, you should use the other functions getAttributionReferrer* and getAttributionCampaign* * * @returns {Array} Attribution array, Example use: * 1) Call windowAlias.JSON.stringify(matomoTracker.getAttributionInfo()) * 2) Pass this json encoded string to the Tracking API (php or java client): setAttributionInfo() */ this.getAttributionInfo = function () { return loadReferrerAttributionCookie(); }; /** * Get the Campaign name that was parsed from the landing page URL when the visitor * landed on the site originally * * @returns {string} */ this.getAttributionCampaignName = function () { return loadReferrerAttributionCookie()[0]; }; /** * Get the Campaign keyword that was parsed from the landing page URL when the visitor * landed on the site originally * * @returns {string} */ this.getAttributionCampaignKeyword = function () { return loadReferrerAttributionCookie()[1]; }; /** * Get the time at which the referrer (used for Goal Attribution) was detected * * @returns {int} Timestamp or 0 if no referrer currently set */ this.getAttributionReferrerTimestamp = function () { return loadReferrerAttributionCookie()[2]; }; /** * Get the full referrer URL that will be used for Goal Attribution * * @returns {string} Raw URL, or empty string '' if no referrer currently set */ this.getAttributionReferrerUrl = function () { return loadReferrerAttributionCookie()[3]; }; /** * Specify the Matomo tracking URL * * @param {string} trackerUrl */ this.setTrackerUrl = function (trackerUrl) { configTrackerUrl = trackerUrl; }; /** * Returns the Matomo tracking URL * @returns {string} */ this.getTrackerUrl = function () { return configTrackerUrl; }; /** * Returns the Matomo server URL. * * @returns {string} */ this.getMatomoUrl = function () { return getMatomoUrlForOverlay(this.getTrackerUrl(), configApiUrl); }; /** * Returns the Matomo server URL. * @deprecated since Matomo 4.0.0 use `getMatomoUrl()` instead. * @returns {string} */ this.getPiwikUrl = function () { return this.getMatomoUrl(); }; /** * Adds a new tracker. All sent requests will be also sent to the given siteId and matomoUrl. * * @param {string} matomoUrl The tracker URL of the current tracker instance * @param {int|string} siteId * @returns {Tracker} */ this.addTracker = function (matomoUrl, siteId) { if (!isDefined(matomoUrl) || null === matomoUrl) { matomoUrl = this.getTrackerUrl(); } var tracker = new Tracker(matomoUrl, siteId); asyncTrackers.push(tracker); Matomo.trigger('TrackerAdded', [this]); return tracker; }; /** * Returns the site ID * * @returns {int} */ this.getSiteId = function() { return configTrackerSiteId; }; /** * Specify the site ID * * @param {int|string} siteId */ this.setSiteId = function (siteId) { setSiteId(siteId); }; /** * Clears the User ID */ this.resetUserId = function() { configUserId = ''; }; /** * Sets a User ID to this user (such as an email address or a username) * * @param {string} userId User ID */ this.setUserId = function (userId) { if (isNumberOrHasLength(userId)) { configUserId = userId; } }; /** * Sets a Visitor ID to this visitor. Should be a 16 digit hex string. * The visitorId won't be persisted in a cookie or something similar and needs to be set every time. * * @param {string} visitorId Visitor ID */ this.setVisitorId = function (visitorId) { var validation = /[0-9A-Fa-f]{16}/g; if (isString(visitorId) && validation.test(visitorId)) { visitorUUID = visitorId; } else { logConsoleError('Invalid visitorId set' + visitorId); } }; /** * Gets the User ID if set. * * @returns {string} User ID */ this.getUserId = function() { return configUserId; }; /** * Pass custom data to the server * * Examples: * tracker.setCustomData(object); * tracker.setCustomData(key, value); * * @param {*} key_or_obj * @param {*} opt_value */ this.setCustomData = function (key_or_obj, opt_value) { if (isObject(key_or_obj)) { configCustomData = key_or_obj; } else { if (!configCustomData) { configCustomData = {}; } configCustomData[key_or_obj] = opt_value; } }; /** * Get custom data * * @returns {*} */ this.getCustomData = function () { return configCustomData; }; /** * Configure function with custom request content processing logic. * It gets called after request content in form of query parameters string has been prepared and before request content gets sent. * * Examples: * tracker.setCustomRequestProcessing(function(request){ * var pairs = request.split('&'); * var result = {}; * pairs.forEach(function(pair) { * pair = pair.split('='); * result[pair[0]] = decodeURIComponent(pair[1] || ''); * }); * return JSON.stringify(result); * }); * * @param {Function} customRequestContentProcessingLogic */ this.setCustomRequestProcessing = function (customRequestContentProcessingLogic) { configCustomRequestContentProcessing = customRequestContentProcessingLogic; }; /** * Appends the specified query string to the matomo.php?... Tracking API URL * * @param {string} queryString eg. 'lat=140&long=100' */ this.appendToTrackingUrl = function (queryString) { configAppendToTrackingUrl = queryString; }; /** * Returns the query string for the current HTTP Tracking API request. * Matomo would prepend the hostname and path to Matomo: http://example.org/matomo/matomo.php? * prior to sending the request. * * @param request eg. "param=value¶m2=value2" */ this.getRequest = function (request) { return getRequest(request); }; /** * Add plugin defined by a name and a callback function. * The callback function will be called whenever a tracking request is sent. * This can be used to append data to the tracking request, or execute other custom logic. * * @param {string} pluginName * @param {Object} pluginObj */ this.addPlugin = function (pluginName, pluginObj) { plugins[pluginName] = pluginObj; }; /** * Set Custom Dimensions. Set Custom Dimensions will not be cleared after a tracked pageview and will * be sent along all following tracking requests. It is possible to remove/clear a value via `deleteCustomDimension`. * * @param {int} customDimensionId A Custom Dimension index * @param {string} value */ this.setCustomDimension = function (customDimensionId, value) { customDimensionId = parseInt(customDimensionId, 10); if (customDimensionId > 0) { if (!isDefined(value)) { value = ''; } if (!isString(value)) { value = String(value); } customDimensions[customDimensionId] = value; } }; /** * Get a stored value for a specific Custom Dimension index. * * @param {int} customDimensionId A Custom Dimension index */ this.getCustomDimension = function (customDimensionId) { customDimensionId = parseInt(customDimensionId, 10); if (customDimensionId > 0 && Object.prototype.hasOwnProperty.call(customDimensions, customDimensionId)) { return customDimensions[customDimensionId]; } }; /** * Delete a custom dimension. * * @param {int} customDimensionId Custom dimension Id */ this.deleteCustomDimension = function (customDimensionId) { customDimensionId = parseInt(customDimensionId, 10); if (customDimensionId > 0) { delete customDimensions[customDimensionId]; } }; /** * Set custom variable within this visit * * @param {int} index Custom variable slot ID from 1-5 * @param {string} name * @param {string} value * @param {string} scope Scope of Custom Variable: * - "visit" will store the name/value in the visit and will persist it in the cookie for the duration of the visit, * - "page" will store the name/value in the next page view tracked. * - "event" will store the name/value in the next event tracked. */ this.setCustomVariable = function (index, name, value, scope) { var toRecord; if (!isDefined(scope)) { scope = 'visit'; } if (!isDefined(name)) { return; } if (!isDefined(value)) { value = ""; } if (index > 0) { name = !isString(name) ? String(name) : name; value = !isString(value) ? String(value) : value; toRecord = [name.slice(0, customVariableMaximumLength), value.slice(0, customVariableMaximumLength)]; // numeric scope is there for GA compatibility if (scope === 'visit' || scope === 2) { loadCustomVariables(); customVariables[index] = toRecord; } else if (scope === 'page' || scope === 3) { customVariablesPage[index] = toRecord; } else if (scope === 'event') { /* GA does not have 'event' scope but we do */ customVariablesEvent[index] = toRecord; } } }; /** * Get custom variable * * @param {int} index Custom variable slot ID from 1-5 * @param {string} scope Scope of Custom Variable: "visit" or "page" or "event" */ this.getCustomVariable = function (index, scope) { var cvar; if (!isDefined(scope)) { scope = "visit"; } if (scope === "page" || scope === 3) { cvar = customVariablesPage[index]; } else if (scope === "event") { cvar = customVariablesEvent[index]; } else if (scope === "visit" || scope === 2) { loadCustomVariables(); cvar = customVariables[index]; } if (!isDefined(cvar) || (cvar && cvar[0] === '')) { return false; } return cvar; }; /** * Delete custom variable * * @param {int} index Custom variable slot ID from 1-5 * @param {string} scope */ this.deleteCustomVariable = function (index, scope) { // Only delete if it was there already if (this.getCustomVariable(index, scope)) { this.setCustomVariable(index, '', '', scope); } }; /** * Deletes all custom variables for a certain scope. * * @param {string} scope */ this.deleteCustomVariables = function (scope) { if (scope === "page" || scope === 3) { customVariablesPage = {}; } else if (scope === "event") { customVariablesEvent = {}; } else if (scope === "visit" || scope === 2) { customVariables = {}; } }; /** * When called then the Custom Variables of scope "visit" will be stored (persisted) in a first party cookie * for the duration of the visit. This is useful if you want to call getCustomVariable later in the visit. * * By default, Custom Variables of scope "visit" are not stored on the visitor's computer. */ this.storeCustomVariablesInCookie = function () { configStoreCustomVariablesInCookie = true; }; /** * Set delay for link tracking (in milliseconds) * * @param {int} delay Delay [ms] */ this.setLinkTrackingTimer = function (delay) { configTrackerPause = delay; }; /** * Get delay for link tracking (in milliseconds) * * @returns {int} Delay [ms] */ this.getLinkTrackingTimer = function () { return configTrackerPause; }; /** * Set list of file extensions to be recognized as downloads * * @param {string|Array} extensions */ this.setDownloadExtensions = function (extensions) { if(isString(extensions)) { extensions = extensions.split('|'); } configDownloadExtensions = extensions; }; /** * Specify additional file extensions to be recognized as downloads * * @param {string|Array} extensions for example 'custom' or ['custom1','custom2','custom3'] */ this.addDownloadExtensions = function (extensions) { var i; if(isString(extensions)) { extensions = extensions.split('|'); } for (i=0; i < extensions.length; i++) { configDownloadExtensions.push(extensions[i]); } }; /** * Removes specified file extensions from the list of recognized downloads * * @param {string|Array} extensions for example 'custom' or ['custom1','custom2','custom3'] */ this.removeDownloadExtensions = function (extensions) { var i, newExtensions = []; if(isString(extensions)) { extensions = extensions.split('|'); } for (i=0; i < configDownloadExtensions.length; i++) { if (indexOfArray(extensions, configDownloadExtensions[i]) === -1) { newExtensions.push(configDownloadExtensions[i]); } } configDownloadExtensions = newExtensions; }; /** * Set array of domains to be treated as local. Also supports path, eg '.matomo.org/subsite1'. In this * case all links that don't go to '*.matomo.org/subsite1/ *' would be treated as outlinks. * For example a link to 'matomo.org/' or 'matomo.org/subsite2' both would be treated as outlinks. * * Also supports page wildcard, eg 'matomo.org/index*'. In this case all links * that don't go to matomo.org/index* would be treated as outlinks. * * The current domain will be added automatically if no given host alias contains a path and if no host * alias is already given for the current host alias. Say you are on "example.org" and set * "hostAlias = ['example.com', 'example.org/test']" then the current "example.org" domain will not be * added as there is already a more restrictive hostAlias 'example.org/test' given. We also do not add * it automatically if there was any other host specifying any path like * "['example.com', 'example2.com/test']". In this case we would also not add the current * domain "example.org" automatically as the "path" feature is used. As soon as someone uses the path * feature, for Matomo JS Tracker to work correctly in all cases, one needs to specify all hosts * manually. * * @param {string|Array} hostsAlias */ this.setDomains = function (hostsAlias) { configHostsAlias = isString(hostsAlias) ? [hostsAlias] : hostsAlias; var hasDomainAliasAlready = false, i = 0, alias; for (i; i < configHostsAlias.length; i++) { alias = String(configHostsAlias[i]); if (isSameHost(domainAlias, domainFixup(alias))) { hasDomainAliasAlready = true; break; } var pathName = getPathName(alias); if (pathName && pathName !== '/' && pathName !== '/*') { hasDomainAliasAlready = true; break; } } // The current domain will be added automatically if no given host alias contains a path // and if no host alias is already given for the current host alias. if (!hasDomainAliasAlready) { /** * eg if domainAlias = 'matomo.org' and someone set hostsAlias = ['matomo.org/foo'] then we should * not add matomo.org as it would increase the allowed scope. */ configHostsAlias.push(domainAlias); } }; /** * Set array of domains to be excluded as referrer. Also supports path, eg '.matomo.org/subsite1'. In this * case all referrers that don't match '*.matomo.org/subsite1/ *' would still be used as referrer. * For example 'matomo.org/' or 'matomo.org/subsite2' would both be used as referrer. * * Also supports page wildcard, eg 'matomo.org/index*'. In this case all referrers * that don't match matomo.org/index* would still be treated as referrer. * * Domains added with setDomains will automatically be excluded as referrers. * * @param {string|Array} excludedReferrers */ this.setExcludedReferrers = function(excludedReferrers) { configExcludedReferrers = isString(excludedReferrers) ? [excludedReferrers] : excludedReferrers; }; /** * Enables cross domain linking. By default, the visitor ID that identifies a unique visitor is stored in * the browser's first party cookies. This means the cookie can only be accessed by pages on the same domain. * If you own multiple domains and would like to track all the actions and pageviews of a specific visitor * into the same visit, you may enable cross domain linking. Whenever a user clicks on a link it will append * a URL parameter pk_vid to the clicked URL which consists of these parts: 16 char visitorId, a 10 character * current timestamp and the last 6 characters are an id based on the userAgent to identify the users device). * This way the current visitorId is forwarded to the page of the different domain. * * On the different domain, the Matomo tracker will recognize the set visitorId from the URL parameter and * reuse this parameter if the page was loaded within 45 seconds. If cross domain linking was not enabled, * it would create a new visit on that page because we wouldn't be able to access the previously created * cookie. By enabling cross domain linking you can track several different domains into one website and * won't lose for example the original referrer. * * To make cross domain linking work you need to set which domains should be considered as your domains by * calling the method "setDomains()" first. We will add the URL parameter to links that go to a * different domain but only if the domain was previously set with "setDomains()" to make sure not to append * the URL parameters when a link actually goes to a third-party URL. */ this.enableCrossDomainLinking = function () { crossDomainTrackingEnabled = true; }; /** * Disable cross domain linking if it was previously enabled. See enableCrossDomainLinking(); */ this.disableCrossDomainLinking = function () { crossDomainTrackingEnabled = false; }; /** * Detect whether cross domain linking is enabled or not. See enableCrossDomainLinking(); * @returns {boolean} */ this.isCrossDomainLinkingEnabled = function () { return crossDomainTrackingEnabled; }; /** * By default, the two visits across domains will be linked together * when the link is click and the page is loaded within 180 seconds. * @param timeout in seconds */ this.setCrossDomainLinkingTimeout = function (timeout) { configVisitorIdUrlParameterTimeoutInSeconds = timeout; }; /** * Returns the query parameter appended to link URLs so cross domain visits * can be detected. * * If your application creates links dynamically, then you'll have to add this * query parameter manually to those links (since the JavaScript tracker cannot * detect when those links are added). * * Eg: * * var url = 'http://myotherdomain.com/?' + matomoTracker.getCrossDomainLinkingUrlParameter(); * $element.append(''); */ this.getCrossDomainLinkingUrlParameter = function () { return encodeWrapper(configVisitorIdUrlParameter) + '=' + encodeWrapper(getCrossDomainVisitorId()); }; /** * Set array of classes to be ignored if present in link * * @param {string|Array} ignoreClasses */ this.setIgnoreClasses = function (ignoreClasses) { configIgnoreClasses = isString(ignoreClasses) ? [ignoreClasses] : ignoreClasses; }; /** * Set request method. If you specify GET then it will automatically disable sendBeacon. * * @param {string} method GET or POST; default is GET */ this.setRequestMethod = function (method) { if (method) { configRequestMethod = String(method).toUpperCase(); } else { configRequestMethod = defaultRequestMethod; } if (configRequestMethod === 'GET') { // send beacon always sends a POST request so we have to disable it to make GET work this.disableAlwaysUseSendBeacon(); } }; /** * Set request Content-Type header value, applicable when POST request method is used for submitting tracking events. * See XMLHttpRequest Level 2 spec, section 4.7.2 for invalid headers * @link http://dvcs.w3.org/hg/xhr/raw-file/tip/Overview.html * * @param {string} requestContentType; default is 'application/x-www-form-urlencoded; charset=UTF-8' */ this.setRequestContentType = function (requestContentType) { configRequestContentType = requestContentType || defaultRequestContentType; }; /** * Removed since Matomo 4 * @param generationTime */ this.setGenerationTimeMs = function(generationTime) { logConsoleError('setGenerationTimeMs is no longer supported since Matomo 4. The call will be ignored. The replacement is setPagePerformanceTiming.'); }; /** * Replace setGenerationTimeMs with this more generic function * Use in SPA * @param networkTimeInMs * @param serverTimeInMs * @param transferTimeInMs * @param domProcessingTimeInMs * @param domCompletionTimeInMs * @param onloadTimeInMs */ this.setPagePerformanceTiming = function( networkTimeInMs, serverTimeInMs, transferTimeInMs, domProcessingTimeInMs, domCompletionTimeInMs, onloadTimeInMs ) { /*members pf_net, pf_srv, pf_tfr, pf_dm1, pf_dm2, pf_onl */ var data = { pf_net: networkTimeInMs, pf_srv: serverTimeInMs, pf_tfr: transferTimeInMs, pf_dm1: domProcessingTimeInMs, pf_dm2: domCompletionTimeInMs, pf_onl: onloadTimeInMs }; try { data = filterIn(data, isDefined); data = onlyPositiveIntegers(data); customPagePerformanceTiming = queryStringify(data); if (customPagePerformanceTiming === '') { logConsoleError('setPagePerformanceTiming() called without parameters. This function needs to be called with at least one performance parameter.'); return; } performanceTracked = false; // to ensure the values are sent (again) performanceAvailable = true; // so appendAvailablePerformanceMetrics will be called directly // Otherwise performanceAvailable will be set when the pageload finished, but there is no need // to wait for that, when the values are set manually. } catch (error) { logConsoleError('setPagePerformanceTiming: ' + error.toString()); } }; /** * Override referrer * * @param {string} url */ this.setReferrerUrl = function (url) { configReferrerUrl = url; }; /** * Override url * * @param {string} url */ this.setCustomUrl = function (url) { configCustomUrl = resolveRelativeReference(locationHrefAlias, url); }; /** * Returns the current url of the page that is currently being visited. If a custom URL was set, the * previously defined custom URL will be returned. */ this.getCurrentUrl = function () { return configCustomUrl || locationHrefAlias; }; /** * Override document.title * * @param {string} title */ this.setDocumentTitle = function (title) { configTitle = title; }; /** * Override PageView id for every use of logPageView(). Do not use this if you call trackPageView() * multiple times during tracking (if, for example, you are tracking a single page application). * * @param {string} pageView */ this.setPageViewId = function (pageView) { configIdPageView = pageView; configIdPageViewSetManually = true; }; /** * Returns the PageView id. If the id was manually set using setPageViewId(), that id will be returned. * If the id was not set manually, the id that was automatically generated in last trackPageView() will be * returned. If there was no last page view, this will be undefined. * * @returns {string} */ this.getPageViewId = function () { return configIdPageView; }; /** * Set the URL of the Matomo API. It is used for Page Overlay. * This method should only be called when the API URL differs from the tracker URL. * * @param {string} apiUrl */ this.setAPIUrl = function (apiUrl) { configApiUrl = apiUrl; }; /** * Set array of classes to be treated as downloads * * @param {string|Array} downloadClasses */ this.setDownloadClasses = function (downloadClasses) { configDownloadClasses = isString(downloadClasses) ? [downloadClasses] : downloadClasses; }; /** * Set array of classes to be treated as outlinks * * @param {string|Array} linkClasses */ this.setLinkClasses = function (linkClasses) { configLinkClasses = isString(linkClasses) ? [linkClasses] : linkClasses; }; /** * Set array of campaign name parameters * * @see https://matomo.org/faq/how-to/faq_120 * @param {string|Array} campaignNames */ this.setCampaignNameKey = function (campaignNames) { configCampaignNameParameters = isString(campaignNames) ? [campaignNames] : campaignNames; }; /** * Set array of campaign keyword parameters * * @see https://matomo.org/faq/how-to/faq_120 * @param {string|Array} campaignKeywords */ this.setCampaignKeywordKey = function (campaignKeywords) { configCampaignKeywordParameters = isString(campaignKeywords) ? [campaignKeywords] : campaignKeywords; }; /** * Strip hash tag (or anchor) from URL * Note: this can be done in the Matomo>Settings>Websites on a per-website basis * * @deprecated * @param {boolean} enableFilter */ this.discardHashTag = function (enableFilter) { configDiscardHashTag = enableFilter; }; /** * Set first-party cookie name prefix * * @param {string} cookieNamePrefix */ this.setCookieNamePrefix = function (cookieNamePrefix) { configCookieNamePrefix = cookieNamePrefix; // Re-init the Custom Variables cookie if (customVariables) { customVariables = getCustomVariablesFromCookie(); } }; /** * Set first-party cookie domain * * @param {string} domain */ this.setCookieDomain = function (domain) { var domainFixed = domainFixup(domain); if (!configCookiesDisabled && !isPossibleToSetCookieOnDomain(domainFixed)) { logConsoleError('Can\'t write cookie on domain ' + domain); } else { configCookieDomain = domainFixed; updateDomainHash(); } }; /** * Set an array of query parameters to be excluded if in the url * * @param {string|Array} excludedQueryParams 'uid' or ['uid', 'sid'] */ this.setExcludedQueryParams = function (excludedQueryParams) { configExcludedQueryParams = isString(excludedQueryParams) ? [excludedQueryParams] : excludedQueryParams; }; /** * Get first-party cookie domain */ this.getCookieDomain = function () { return configCookieDomain; }; /** * Detect if cookies are enabled and supported by browser. */ this.hasCookies = function () { return '1' === hasCookies(); }; /** * Set a first-party cookie for the duration of the session. * * @param {string} cookieName * @param {string} cookieValue * @param {int} msToExpire Defaults to session cookie timeout */ this.setSessionCookie = function (cookieName, cookieValue, msToExpire) { if (!cookieName) { throw new Error('Missing cookie name'); } if (!isDefined(msToExpire)) { msToExpire = configSessionCookieTimeout; } configCookiesToDelete.push(cookieName); setCookie(getCookieName(cookieName), cookieValue, msToExpire, configCookiePath, configCookieDomain, configCookieIsSecure, configCookieSameSite); }; /** * Get first-party cookie value. * * Returns null if cookies are disabled or if no cookie could be found for this name. * * @param {string} cookieName */ this.getCookie = function (cookieName) { var cookieValue = getCookie(getCookieName(cookieName)); if (cookieValue === 0) { return null; } return cookieValue; }; /** * Set first-party cookie path. * * @param {string} path Cookie path */ this.setCookiePath = function (path) { configCookiePath = path; updateDomainHash(); }; /** * Get first-party cookie path. * * @returns {string} Cookie path */ this.getCookiePath = function () { return configCookiePath; }; /** * Set visitor cookie timeout (in seconds) * Defaults to 13 months (timeout=33955200) * * @param {int} timeout */ this.setVisitorCookieTimeout = function (timeout) { configVisitorCookieTimeout = timeout * 1000; }; /** * Set session cookie timeout (in seconds). * Defaults to 30 minutes (timeout=1800) * * @param {int} timeout */ this.setSessionCookieTimeout = function (timeout) { configSessionCookieTimeout = timeout * 1000; }; /** * Get session cookie timeout (in seconds). */ this.getSessionCookieTimeout = function () { return configSessionCookieTimeout; }; /** * Set referral cookie timeout (in seconds). * Defaults to 6 months (15768000000) * * @param {int} timeout */ this.setReferralCookieTimeout = function (timeout) { configReferralCookieTimeout = timeout * 1000; }; /** * Set conversion attribution to first referrer and campaign * * @param {boolean} enable If true, use first referrer (and first campaign) * if false, use the last referrer (or campaign) */ this.setConversionAttributionFirstReferrer = function (enable) { configConversionAttributionFirstReferrer = enable; }; /** * Enable the Secure cookie flag on all first party cookies. * This should be used when your website is only available under HTTPS * so that all tracking cookies are always sent over secure connection. * * Warning: If your site is available under http and https, * setting this might lead to duplicate or incomplete visits. * * @param {boolean} enable */ this.setSecureCookie = function (enable) { if(enable && location.protocol !== 'https:') { logConsoleError("Error in setSecureCookie: You cannot use `Secure` on http."); return; } configCookieIsSecure = enable; }; /** * Set the SameSite attribute for cookies to a custom value. * You might want to use this if your site is running in an iframe since * then it will only be able to access the cookies if SameSite is set to 'None'. * * * Warning: * Sets CookieIsSecure to true on None, because None will only work with Secure; cookies * If your site is available under http and https, * using "None" might lead to duplicate or incomplete visits. * * @param {string} sameSite Either Lax, None or Strict */ this.setCookieSameSite = function (sameSite) { sameSite = String(sameSite); sameSite = sameSite.charAt(0).toUpperCase() + sameSite.toLowerCase().slice(1); if (sameSite !== 'None' && sameSite !== 'Lax' && sameSite !== 'Strict') { logConsoleError('Ignored value for sameSite. Please use either Lax, None, or Strict.'); return; } if (sameSite === 'None') { if (location.protocol === 'https:') { this.setSecureCookie(true); } else { logConsoleError('sameSite=None cannot be used on http, reverted to sameSite=Lax.'); sameSite = 'Lax'; } } configCookieSameSite = sameSite; }; /** * Disables all cookies from being set * * Existing cookies will be deleted on the next call to track */ this.disableCookies = function () { configCookiesDisabled = true; if (configTrackerSiteId) { deleteCookies(); } }; /** * Detects if cookies are enabled or not * @returns {boolean} */ this.areCookiesEnabled = function () { return !configCookiesDisabled; }; /** * Enables cookies if they were disabled previously. */ this.setCookieConsentGiven = function () { if (configCookiesDisabled && !configDoNotTrack) { configCookiesDisabled = false; if (!configBrowserFeatureDetection) { this.enableBrowserFeatureDetection(); } if (configTrackerSiteId && hasSentTrackingRequestYet) { setVisitorIdCookie(); // sets attribution cookie, and updates visitorId in the backend // because hasSentTrackingRequestYet=true we assume there might not be another tracking // request within this page view so we trigger one ourselves. // if no tracking request has been sent yet, we don't set the attribution cookie cause Matomo // sets the cookie only when there is a tracking request. It'll be set if the user sends // a tracking request afterwards var request = getRequest('ping=1', null, 'ping'); sendRequest(request, configTrackerPause); } } }; /** * When called, no cookies will be set until you have called `setCookieConsentGiven()` * unless consent was given previously AND you called {@link rememberCookieConsentGiven()} when the user * gave consent. * * This may be useful when you want to implement for example a popup to ask for cookie consent. * Once the user has given consent, you should call {@link setCookieConsentGiven()} * or {@link rememberCookieConsentGiven()}. * * If you require tracking consent for example because you are tracking personal data and GDPR applies to you, * then have a look at `_paq.push(['requireConsent'])` instead. * * If the user has already given consent in the past, you can either decide to not call `requireCookieConsent` at all * or call `_paq.push(['setCookieConsentGiven'])` on each page view at any time after calling `requireCookieConsent`. * * When the user gives you the consent to set cookies, you can also call `_paq.push(['rememberCookieConsentGiven', optionalTimeoutInHours])` * and for the duration while the cookie consent is remembered, any call to `requireCoookieConsent` will be automatically ignored * until you call `forgetCookieConsentGiven`. * `forgetCookieConsentGiven` needs to be called when the user removes consent for using cookies. This means if you call `rememberCookieConsentGiven` at the * time the user gives you consent, you do not need to ever call `_paq.push(['setCookieConsentGiven'])` as the consent * will be detected automatically through cookies. */ this.requireCookieConsent = function() { if (this.getRememberedCookieConsent()) { return false; } this.disableCookies(); return true; }; /** * If the user has given cookie consent previously and this consent was remembered, it will return the number * in milliseconds since 1970/01/01 which is the date when the user has given cookie consent. Please note that * the returned time depends on the users local time which may not always be correct. * * @returns {number|string} */ this.getRememberedCookieConsent = function () { return getCookie(COOKIE_CONSENT_COOKIE_NAME); }; /** * Calling this method will remove any previously given cookie consent and it disables cookies for subsequent * page views. You may call this method if the user removes cookie consent manually, or if you * want to re-ask for cookie consent after a specific time period. */ this.forgetCookieConsentGiven = function () { deleteCookie(COOKIE_CONSENT_COOKIE_NAME, configCookiePath, configCookieDomain); this.disableCookies(); }; /** * Calling this method will remember that the user has given cookie consent across multiple requests by setting * a cookie named "mtm_cookie_consent". You can optionally define the lifetime of that cookie in hours * using a parameter. * * When you call this method, we imply that the user has given cookie consent for this page view, and will also * imply consent for all future page views unless the cookie expires or the user * deletes all their cookies. Remembering cookie consent means even if you call {@link disableCookies()}, * then cookies will still be enabled and it won't disable cookies since the user has given consent for cookies. * * Please note that this feature requires you to set the `cookieDomain` and `cookiePath` correctly. Please * also note that when you call this method, consent will be implied for all sites that match the configured * cookieDomain and cookiePath. Depending on your website structure, you may need to restrict or widen the * scope of the cookie domain/path to ensure the consent is applied to the sites you want. * * @param {int} hoursToExpire After how many hours the cookie consent should expire. By default the consent is valid * for 30 years unless cookies are deleted by the user or the browser prior to this */ this.rememberCookieConsentGiven = function (hoursToExpire) { if (hoursToExpire) { hoursToExpire = hoursToExpire * 60 * 60 * 1000; } else { hoursToExpire = 30 * 365 * 24 * 60 * 60 * 1000; } this.setCookieConsentGiven(); var now = new Date().getTime(); setCookie(COOKIE_CONSENT_COOKIE_NAME, now, hoursToExpire, configCookiePath, configCookieDomain, configCookieIsSecure, configCookieSameSite); }; /** * One off cookies clearing. Useful to call this when you know for sure a new visitor is using the same browser, * it maybe helps to "reset" tracking cookies to prevent data reuse for different users. */ this.deleteCookies = function () { deleteCookies(); }; /** * Handle do-not-track requests * * @param {boolean} enable If true, don't track if user agent sends 'do-not-track' header */ this.setDoNotTrack = function (enable) { var dnt = navigatorAlias.doNotTrack || navigatorAlias.msDoNotTrack; configDoNotTrack = enable && (dnt === 'yes' || dnt === '1'); // do not track also disables cookies and deletes existing cookies if (configDoNotTrack) { this.disableCookies(); } }; /** * Prevent campaign parameters being sent to the tracker, unless consent given. */ this.disableCampaignParameters = function () { configEnableCampaignParameters = false; }; /** * Enables send beacon usage instead of regular XHR which reduces the link tracking time to a minimum * of 100ms instead of 500ms (default). This means when a user clicks for example on an outlink, the * navigation to this page will happen 400ms faster. * In case you are setting a callback method when issuing a tracking request, the callback method will * be executed as soon as the tracking request was sent through "sendBeacon" and not after the tracking * request finished as it is not possible to find out when the request finished. * Send beacon will only be used if the browser actually supports it. */ this.alwaysUseSendBeacon = function () { configAlwaysUseSendBeacon = true; }; /** * Disables send beacon usage instead and instead enables using regular XHR when possible. This makes * callbacks work and also tracking requests will appear in the browser developer tools console. */ this.disableAlwaysUseSendBeacon = function () { configAlwaysUseSendBeacon = false; }; /** * Add click listener to a specific link element. * When clicked, Matomo will log the click automatically. * * @param {Element} element * @param {boolean} enable If false, do not use pseudo click-handler (middle click + context menu) */ this.addListener = function (element, enable) { addClickListener(element, enable, false); }; /** * Install link tracker. * * If you change the DOM of your website or web application Matomo will automatically detect links * that were added newly. * * 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 {boolean} enable Defaults to true. * * If "true", use pseudo click-handler (treat middle click and open contextmenu as * left click). A right click (or any click that opens the context menu) on a link * will be tracked as clicked even if "Open in new tab" is not selected. * * If "false" (default), nothing will be tracked on open context menu or middle click. * The context menu is usually opened to open a link / download in a new tab * therefore you can get more accurate results by treat it as a click but it can lead * to wrong click numbers. */ this.enableLinkTracking = function (enable) { if (linkTrackingEnabled) { return; } linkTrackingEnabled = true; var self = this; trackCallbackOnReady(function () { linkTrackingInstalled = true; var element = documentAlias.body; addClickListener(element, enable, true); }); }; /** * Enable tracking of uncatched JavaScript errors * * If enabled, uncaught JavaScript Errors will be tracked as an event by defining a * window.onerror handler. If a window.onerror handler is already defined we will make * sure to call this previously registered error handler after tracking the error. * * By default we return false in the window.onerror handler to make sure the error still * appears in the browser's console etc. Note: Some older browsers might behave differently * so it could happen that an actual JavaScript error will be suppressed. * If a window.onerror handler was registered we will return the result of this handler. * * Make sure not to overwrite the window.onerror handler after enabling the JS error * tracking as the error tracking won't work otherwise. To capture all JS errors we * recommend to include the Matomo JavaScript tracker in the HTML as early as possible. * If possible directly in before loading any other JavaScript. */ this.enableJSErrorTracking = function () { if (enableJSErrorTracking) { return; } enableJSErrorTracking = true; var onError = windowAlias.onerror; windowAlias.onerror = function (message, url, linenumber, column, error) { trackCallback(function () { var category = 'JavaScript Errors'; var action = url + ':' + linenumber; if (column) { action += ':' + column; } if (indexOfArray(javaScriptErrors, category + action + message) === -1) { javaScriptErrors.push(category + action + message); logEvent(category, action, message); } }); if (onError) { return onError(message, url, linenumber, column, error); } return false; }; }; /** * Disable automatic performance tracking */ this.disablePerformanceTracking = function () { configPerformanceTrackingEnabled = false; }; /** * Set heartbeat (in seconds) * * @param {int} heartBeatDelayInSeconds Defaults to 15s. Cannot be lower than 5. */ this.enableHeartBeatTimer = function (heartBeatDelayInSeconds) { heartBeatDelayInSeconds = Math.max(heartBeatDelayInSeconds || 15, 5); configHeartBeatDelay = heartBeatDelayInSeconds * 1000; // if a tracking request has already been sent, start the heart beat timeout if (lastTrackerRequestTime !== null) { setUpHeartBeat(); } }; /** * Disable heartbeat if it was previously activated. */ this.disableHeartBeatTimer = function () { if (configHeartBeatDelay || heartBeatSetUp) { if (windowAlias.removeEventListener) { windowAlias.removeEventListener('focus', heartBeatOnFocus); windowAlias.removeEventListener('blur', heartBeatOnBlur); windowAlias.removeEventListener('visibilitychange', heartBeatOnVisible); } else if (windowAlias.detachEvent) { windowAlias.detachEvent('onfocus', heartBeatOnFocus); windowAlias.detachEvent('onblur', heartBeatOnBlur); windowAlias.detachEvent('visibilitychange', heartBeatOnVisible); } } configHeartBeatDelay = null; heartBeatSetUp = false; }; /** * Frame buster */ this.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 */ this.redirectFile = function (url) { if (windowAlias.location.protocol === 'file:') { windowAlias.location = url; } }; /** * Count sites in pre-rendered state * * @param {boolean} enable If true, track when in pre-rendered state */ this.setCountPreRendered = function (enable) { configCountPreRendered = enable; }; /** * Trigger a goal * * @param {int|string} idGoal * @param {int|float} customRevenue * @param {*} customData * @param {Function} callback */ this.trackGoal = function (idGoal, customRevenue, customData, callback) { trackCallback(function () { logGoal(idGoal, customRevenue, customData, callback); }); }; /** * Manually log a click from your own code * * @param {string} sourceUrl * @param {string} linkType * @param {*} customData * @param {Function} callback */ this.trackLink = function (sourceUrl, linkType, customData, callback) { trackCallback(function () { logLink(sourceUrl, linkType, customData, callback); }); }; /** * Get the number of page views that have been tracked so far within the currently loaded page. */ this.getNumTrackedPageViews = function () { return numTrackedPageviews; }; /** * Log visit to this page * * @param {string} customTitle * @param {*} customData * @param {Function} callback */ this.trackPageView = function (customTitle, customData, callback) { trackedContentImpressions = []; consentRequestsQueue = []; javaScriptErrors = []; if (isOverlaySession(configTrackerSiteId)) { trackCallback(function () { injectOverlayScripts(configTrackerUrl, configApiUrl, configTrackerSiteId); }); } else { trackCallback(function () { numTrackedPageviews++; logPageView(customTitle, customData, callback); }); } }; this.disableBrowserFeatureDetection = function () { configBrowserFeatureDetection = false; browserFeatures = {}; if (supportsClientHints()) { // ensure already queue requests are still processed processClientHintsQueue(); } }; this.enableBrowserFeatureDetection = function () { configBrowserFeatureDetection = true; detectBrowserFeatures(); }; /** * Scans the entire DOM for all content blocks and tracks all impressions once the DOM ready event has * been triggered. * * If you only want to track visible content impressions have a look at `trackVisibleContentImpressions()`. * We do not track an impression of the same content block twice if you call this method multiple times * unless `trackPageView()` is called meanwhile. This is useful for single page applications. */ this.trackAllContentImpressions = function () { if (isOverlaySession(configTrackerSiteId)) { return; } trackCallback(function () { trackCallbackOnReady(function () { // we have to wait till DOM ready var contentNodes = content.findContentNodes(); var requests = getContentImpressionsRequestsFromNodes(contentNodes); requestQueue.pushMultiple(requests); }); }); }; /** * Scans the entire DOM for all content blocks as soon as the page is loaded. It tracks an impression * only if a content block is actually visible. Meaning it is not hidden and the content is or was at * some point in the viewport. * * If you want to track all content blocks have a look at `trackAllContentImpressions()`. * We do not track an impression of the same content block twice if you call this method multiple times * unless `trackPageView()` is called meanwhile. This is useful for single page applications. * * Once you have called this method you can no longer change `checkOnScroll` or `timeIntervalInMs`. * * If you do want to only track visible content blocks but not want us to perform any automatic checks * as they can slow down your frames per second you can call `trackVisibleContentImpressions()` or * `trackContentImpressionsWithinNode()` manually at any time to rescan the entire DOM for newly * visible content blocks. * o Call `trackVisibleContentImpressions(false, 0)` to initially track only visible content impressions * o Call `trackVisibleContentImpressions()` at any time again to rescan the entire DOM for newly visible content blocks or * o Call `trackContentImpressionsWithinNode(node)` at any time to rescan only a part of the DOM for newly visible content blocks * * @param {boolean} [checkOnScroll=true] Optional, you can disable rescanning the entire DOM automatically * after each scroll event by passing the value `false`. If enabled, * we check whether a previously hidden content blocks became visible * after a scroll and if so track the impression. * Note: If a content block is placed within a scrollable element * (`overflow: scroll`), we can currently not detect when this block * becomes visible. * @param {int} [timeIntervalInMs=750] Optional, you can define an interval to rescan the entire DOM * for new impressions every X milliseconds by passing * for instance `timeIntervalInMs=500` (rescan DOM every 500ms). * Rescanning the entire DOM and detecting the visible state of content * blocks can take a while depending on the browser and amount of content. * In case your frames per second goes down you might want to increase * this value or disable it by passing the value `0`. */ this.trackVisibleContentImpressions = function (checkOnScroll, timeIntervalInMs) { if (isOverlaySession(configTrackerSiteId)) { return; } if (!isDefined(checkOnScroll)) { checkOnScroll = true; } if (!isDefined(timeIntervalInMs)) { timeIntervalInMs = 750; } enableTrackOnlyVisibleContent(checkOnScroll, timeIntervalInMs, this); trackCallback(function () { trackCallbackOnLoad(function () { // we have to wait till CSS parsed and applied var contentNodes = content.findContentNodes(); var requests = getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet(contentNodes); requestQueue.pushMultiple(requests); }); }); }; /** * Tracks a content impression using the specified values. You should not call this method too often * as each call causes an XHR tracking request and can slow down your site or your server. * * @param {string} contentName For instance "Ad Sale". * @param {string} [contentPiece='Unknown'] For instance a path to an image or the text of a text ad. * @param {string} [contentTarget] For instance the URL of a landing page. */ this.trackContentImpression = function (contentName, contentPiece, contentTarget) { if (isOverlaySession(configTrackerSiteId)) { return; } contentName = trim(contentName); contentPiece = trim(contentPiece); contentTarget = trim(contentTarget); if (!contentName) { return; } contentPiece = contentPiece || 'Unknown'; trackCallback(function () { var request = buildContentImpressionRequest(contentName, contentPiece, contentTarget); requestQueue.push(request); }); }; /** * Scans the given DOM node and its children for content blocks and tracks an impression for them if * no impression was already tracked for it. If you have called `trackVisibleContentImpressions()` * upfront only visible content blocks will be tracked. You can use this method if you, for instance, * dynamically add an element using JavaScript to your DOM after we have tracked the initial impressions. * * @param {Element} domNode */ this.trackContentImpressionsWithinNode = function (domNode) { if (isOverlaySession(configTrackerSiteId) || !domNode) { return; } trackCallback(function () { if (isTrackOnlyVisibleContentEnabled) { trackCallbackOnLoad(function () { // we have to wait till CSS parsed and applied var contentNodes = content.findContentNodesWithinNode(domNode); var requests = getCurrentlyVisibleContentImpressionsRequestsIfNotTrackedYet(contentNodes); requestQueue.pushMultiple(requests); }); } else { trackCallbackOnReady(function () { // we have to wait till DOM ready var contentNodes = content.findContentNodesWithinNode(domNode); var requests = getContentImpressionsRequestsFromNodes(contentNodes); requestQueue.pushMultiple(requests); }); } }); }; /** * Tracks a content interaction using the specified values. You should use this method only in conjunction * with `trackContentImpression()`. The specified `contentName` and `contentPiece` has to be exactly the * same as the ones that were used in `trackContentImpression()`. Otherwise the interaction will not count. * * @param {string} contentInteraction The type of interaction that happened. For instance 'click' or 'submit'. * @param {string} contentName The name of the content. For instance "Ad Sale". * @param {string} [contentPiece='Unknown'] The actual content. For instance a path to an image or the text of a text ad. * @param {string} [contentTarget] For instance the URL of a landing page. */ this.trackContentInteraction = function (contentInteraction, contentName, contentPiece, contentTarget) { if (isOverlaySession(configTrackerSiteId)) { return; } contentInteraction = trim(contentInteraction); contentName = trim(contentName); contentPiece = trim(contentPiece); contentTarget = trim(contentTarget); if (!contentInteraction || !contentName) { return; } contentPiece = contentPiece || 'Unknown'; trackCallback(function () { var request = buildContentInteractionRequest(contentInteraction, contentName, contentPiece, contentTarget); if (request) { requestQueue.push(request); } }); }; /** * Tracks an interaction with the given DOM node / content block. * * By default we track interactions on click but sometimes you might want to track interactions yourself. * For instance you might want to track an interaction manually on a double click or a form submit. * Make sure to disable the automatic interaction tracking in this case by specifying either the CSS * class `matomoContentIgnoreInteraction` or the attribute `data-content-ignoreinteraction`. * * @param {Element} domNode This element itself or any of its parent elements has to be a content block * element. Meaning one of those has to have a `matomoTrackContent` CSS class or * a `data-track-content` attribute. * @param {string} [contentInteraction='Unknown] The name of the interaction that happened. For instance * 'click', 'formSubmit', 'DblClick', ... */ this.trackContentInteractionNode = function (domNode, contentInteraction) { if (isOverlaySession(configTrackerSiteId) || !domNode) { return; } var theRequest = null; trackCallback(function () { theRequest = buildContentInteractionRequestNode(domNode, contentInteraction); if (theRequest) { requestQueue.push(theRequest); } }); //note: return value is only for tests... will only work if dom is already ready... return theRequest; }; /** * Useful to debug content tracking. This method will log all detected content blocks to console * (if the browser supports the console). It will list the detected name, piece, and target of each * content block. */ this.logAllContentBlocksOnPage = function () { var contentNodes = content.findContentNodes(); var contents = content.collectContent(contentNodes); // needed to write it this way for jslint var consoleType = typeof console; if (consoleType !== 'undefined' && console && console.log) { console.log(contents); } }; /** * Records an event * * @param {string} category The Event Category (Videos, Music, Games...) * @param {string} action The Event's Action (Play, Pause, Duration, Add Playlist, Downloaded, Clicked...) * @param {string} name (optional) The Event's object Name (a particular Movie name, or Song name, or File name...) * @param {float} value (optional) The Event's value * @param {Function} callback * @param {*} customData */ this.trackEvent = function (category, action, name, value, customData, callback) { trackCallback(function () { logEvent(category, action, name, value, customData, callback); }); }; /** * Log special pageview: Internal search * * @param {string} keyword * @param {string} category * @param {int} resultsCount * @param {*} customData */ this.trackSiteSearch = function (keyword, category, resultsCount, customData) { trackedContentImpressions = []; trackCallback(function () { logSiteSearch(keyword, category, resultsCount, customData); }); }; /** * Used to record that the current page view is an item (product) page view, or a Ecommerce Category page view. * This must be called before trackPageView() on the product/category page. * * On a category page, you can set the parameter category, and set the other parameters to empty string or false * * Tracking Product/Category page views will allow Matomo to report on Product & Categories * conversion rates (Conversion rate = Ecommerce orders containing this product or category / Visits to the product or category) * * @param {string} sku Item's SKU code being viewed * @param {string} name Item's Name being viewed * @param {string} category Category page being viewed. On an Item's page, this is the item's category * @param {float} price Item's display price, not use in standard Matomo reports, but output in API product reports. */ this.setEcommerceView = function (sku, name, category, price) { ecommerceProductView = {}; if (isNumberOrHasLength(category)) { category = String(category); } if (!isDefined(category) || category === null || category === false || !category.length) { category = ""; } else if (category instanceof Array) { category = windowAlias.JSON.stringify(category); } var param = '_pkc'; ecommerceProductView[param] = category; if (isDefined(price) && price !== null && price !== false && String(price).length) { param = '_pkp'; ecommerceProductView[param] = price; } // On a category page, do not track Product name not defined if (!isNumberOrHasLength(sku) && !isNumberOrHasLength(name)) { return; } if (isNumberOrHasLength(sku)) { param = '_pks'; ecommerceProductView[param] = sku; } if (!isNumberOrHasLength(name)) { name = ""; } param = '_pkn'; ecommerceProductView[param] = name; }; /** * Returns the list of ecommerce items that will be sent when a cart update or order is tracked. * The returned value is read-only, modifications will not change what will be tracked. Use * addEcommerceItem/removeEcommerceItem/clearEcommerceCart to modify what items will be tracked. * * Note: the cart will be cleared after an order. * * @returns {Array} */ this.getEcommerceItems = function () { return JSON.parse(JSON.stringify(ecommerceItems)); }; /** * Adds an item (product) that is in the current Cart or in the Ecommerce order. * This function is called for every item (product) in the Cart or the Order. * The only required parameter is sku. * The items are deleted from this JavaScript object when the Ecommerce order is tracked via the method trackEcommerceOrder. * * If there is already a saved item for the given sku, it will be updated with the * new information. * * @param {string} sku (required) Item's SKU Code. This is the unique identifier for the product. * @param {string} name (optional) Item's name * @param {string} category (optional) Item's category, or array of up to 5 categories * @param {float} price (optional) Item's price. If not specified, will default to 0 * @param {float} quantity (optional) Item's quantity. If not specified, will default to 1 */ this.addEcommerceItem = function (sku, name, category, price, quantity) { if (isNumberOrHasLength(sku)) { ecommerceItems[sku] = [ String(sku), name, category, price, quantity ]; } }; /** * Removes a single ecommerce item by SKU from the current cart. * * @param {string} sku (required) Item's SKU Code. This is the unique identifier for the product. */ this.removeEcommerceItem = function (sku) { if (isNumberOrHasLength(sku)) { sku = String(sku); delete ecommerceItems[sku]; } }; /** * Clears the current cart, removing all saved ecommerce items. Call this method to manually clear * the cart before sending an ecommerce order. */ this.clearEcommerceCart = function () { ecommerceItems = {}; }; /** * Tracks an Ecommerce order. * If the Ecommerce order contains items (products), you must call first the addEcommerceItem() for each item in the order. * All revenues (grandTotal, subTotal, tax, shipping, discount) will be individually summed and reported in Matomo reports. * Parameters orderId and grandTotal are required. For others, you can set to false if you don't need to specify them. * After calling this method, items added to the cart will be removed from this JavaScript object. * * @param {string|int} orderId (required) Unique Order ID. * This will be used to count this order only once in the event the order page is reloaded several times. * orderId must be unique for each transaction, even on different days, or the transaction will not be recorded by Matomo. * @param {float} grandTotal (required) Grand Total revenue of the transaction (including tax, shipping, etc.) * @param {float} subTotal (optional) Sub total amount, typically the sum of items prices for all items in this order (before Tax and Shipping costs are applied) * @param {float} tax (optional) Tax amount for this order * @param {float} shipping (optional) Shipping amount for this order * @param {float} discount (optional) Discounted amount in this order */ this.trackEcommerceOrder = function (orderId, grandTotal, subTotal, tax, shipping, discount) { logEcommerceOrder(orderId, grandTotal, subTotal, tax, shipping, discount); }; /** * Tracks a Cart Update (add item, remove item, update item). * On every Cart update, you must call addEcommerceItem() for each item (product) in the cart, including the items that haven't been updated since the last cart update. * Then you can call this function with the Cart grandTotal (typically the sum of all items' prices) * Calling this method does not remove from this JavaScript object the items that were added to the cart via addEcommerceItem * * @param {float} grandTotal (required) Items (products) amount in the Cart */ this.trackEcommerceCartUpdate = function (grandTotal) { logEcommerceCartUpdate(grandTotal); }; /** * Sends a tracking request with custom request parameters. * Matomo will prepend the hostname and path to Matomo, as well as all other needed tracking request * parameters prior to sending the request. Useful eg if you track custom dimensions via a plugin. * * @param request eg. "param=value¶m2=value2" * @param customData * @param callback * @param pluginMethod */ this.trackRequest = function (request, customData, callback, pluginMethod) { trackCallback(function () { var fullRequest = getRequest(request, customData, pluginMethod); sendRequest(fullRequest, configTrackerPause, callback); }); }; /** * Sends a ping request. * * Ping requests do not track new actions. If they are sent within the standard visit length, they will * extend the existing visit and the current last action for the visit. If after the standard visit * length, ping requests will create a new visit using the last action in the last known visit. */ this.ping = function () { this.trackRequest('ping=1', null, null, 'ping'); }; /** * Disables sending requests queued */ this.disableQueueRequest = function () { requestQueue.enabled = false; }; /** * Defines after how many ms a queued requests will be executed after the request was queued initially. * The higher the value the more tracking requests can be send together at once. */ this.setRequestQueueInterval = function (interval) { if (interval < 1000) { throw new Error('Request queue interval needs to be at least 1000ms'); } requestQueue.interval = interval; }; /** * Won't send the tracking request directly but wait for a short time to possibly send this tracking request * along with other tracking requests in one go. This can reduce the number of requests send to your server. * If the page unloads (user navigates to another page or closes the browser), then all remaining queued * requests will be sent immediately so that no tracking request gets lost. * Note: Any queued request may not be possible to be replayed in case a POST request is sent. Only queue * requests that don't have to be replayed. * * @param request eg. "param=value¶m2=value2" * @param isFullRequest whether request is a full tracking request or not. If true, we don't call * call getRequest() before pushing to the queue. */ this.queueRequest = function (request, isFullRequest) { trackCallback(function () { var fullRequest = isFullRequest ? request : getRequest(request); requestQueue.push(fullRequest); }); }; /** * Returns whether consent is required or not. * * @returns {boolean} */ this.isConsentRequired = function() { return configConsentRequired; }; /** * If the user has given consent previously and this consent was remembered, it will return the number * in milliseconds since 1970/01/01 which is the date when the user has given consent. Please note that * the returned time depends on the users local time which may not always be correct. * * @returns {number|string} */ this.getRememberedConsent = function () { var value = getCookie(CONSENT_COOKIE_NAME); if (getCookie(CONSENT_REMOVED_COOKIE_NAME)) { // if for some reason the consent_removed cookie is also set with the consent cookie, the // consent_removed cookie overrides the consent one, and we make sure to delete the consent // cookie. if (value) { deleteCookie(CONSENT_COOKIE_NAME, configCookiePath, configCookieDomain); } return null; } if (!value || value === 0) { return null; } return value; }; /** * Detects whether the user has given consent previously. * * @returns {boolean} */ this.hasRememberedConsent = function () { return !!this.getRememberedConsent(); }; /** * When called, no tracking request will be sent to the Matomo server until you have called `setConsentGiven()` * unless consent was given previously AND you called {@link rememberConsentGiven()} when the user gave their * consent. * * This may be useful when you want to implement for example a popup to ask for consent before tracking the user. * Once the user has given consent, you should call {@link setConsentGiven()} or {@link rememberConsentGiven()}. * * If you require consent for tracking personal data for example, you should first call * `_paq.push(['requireConsent'])`. * * If the user has already given consent in the past, you can either decide to not call `requireConsent` at all * or call `_paq.push(['setConsentGiven'])` on each page view at any time after calling `requireConsent`. * * When the user gives you the consent to track data, you can also call `_paq.push(['rememberConsentGiven', optionalTimeoutInHours])` * and for the duration while the consent is remembered, any call to `requireConsent` will be automatically ignored until you call `forgetConsentGiven`. * `forgetConsentGiven` needs to be called when the user removes consent for tracking. This means if you call `rememberConsentGiven` at the * time the user gives you consent, you do not need to ever call `_paq.push(['setConsentGiven'])`. */ this.requireConsent = function () { configConsentRequired = true; configHasConsent = this.hasRememberedConsent(); if (!configHasConsent) { // we won't call this.disableCookies() since we don't want to delete any cookies just yet // user might call `setConsentGiven` next configCookiesDisabled = true; } // Matomo.addPlugin might not be defined at this point, we add the plugin directly also to make JSLint happy // We also want to make sure to define an unload listener for each tracker, not only one tracker. coreConsentCounter++; plugins['CoreConsent' + coreConsentCounter] = { unload: function () { if (!configHasConsent) { // we want to make sure to remove all previously set cookies again deleteCookies(); } } }; }; /** * Call this method once the user has given consent. This will cause all tracking requests from this * page view to be sent. Please note that the given consent won't be remembered across page views. If you * want to remember consent across page views, call {@link rememberConsentGiven()} instead. * * It will also automatically enable cookies if they were disabled previously. * * @param {boolean} [setCookieConsent=true] Internal parameter. Defines whether cookies should be enabled or not. */ this.setConsentGiven = function (setCookieConsent) { configHasConsent = true; if (!configBrowserFeatureDetection) { this.enableBrowserFeatureDetection(); } deleteCookie(CONSENT_REMOVED_COOKIE_NAME, configCookiePath, configCookieDomain); var i, requestType; for (i = 0; i < consentRequestsQueue.length; i++) { requestType = typeof consentRequestsQueue[i][0]; if (requestType === 'string') { sendRequest(consentRequestsQueue[i][0], configTrackerPause, consentRequestsQueue[i][1]); } else if (requestType === 'object') { sendBulkRequest(consentRequestsQueue[i][0], configTrackerPause); } } consentRequestsQueue = []; // we need to enable cookies after sending the previous requests as it will make sure that we send // a ping request if needed. Cookies are only set once we call `getRequest`. Above only calls sendRequest // meaning no cookies will be created unless we called enableCookies after at least one request has been sent. // this will cause a ping request to be sent that sets the cookies and also updates the newly generated visitorId // on the server. // If the user calls setConsentGiven before sending any tracking request (which usually is the case) then // nothing will need to be done as it only enables cookies and the next tracking request will set the cookies // etc. if (!isDefined(setCookieConsent) || setCookieConsent) { this.setCookieConsentGiven(); } }; /** * Calling this method will remember that the user has given consent across multiple requests by setting * a cookie. You can optionally define the lifetime of that cookie in hours using a parameter. * * When you call this method, we imply that the user has given consent for this page view, and will also * imply consent for all future page views unless the cookie expires (if timeout defined) or the user * deletes all their cookies. This means even if you call {@link requireConsent()}, then all requests * will still be tracked. * * Please note that this feature requires you to set the `cookieDomain` and `cookiePath` correctly and requires * that you do not disable cookies. Please also note that when you call this method, consent will be implied * for all sites that match the configured cookieDomain and cookiePath. Depending on your website structure, * you may need to restrict or widen the scope of the cookie domain/path to ensure the consent is applied * to the sites you want. * * @param {int} hoursToExpire After how many hours the consent should expire. By default the consent is valid * for 30 years unless cookies are deleted by the user or the browser prior to this */ this.rememberConsentGiven = function (hoursToExpire) { if (hoursToExpire) { hoursToExpire = hoursToExpire * 60 * 60 * 1000; } else { hoursToExpire = 30 * 365 * 24 * 60 * 60 * 1000; } var setCookieConsent = true; // we currently always enable cookies if we remember consent cause we don't store across requests whether // cookies should be automatically enabled or not. this.setConsentGiven(setCookieConsent); var now = new Date().getTime(); setCookie(CONSENT_COOKIE_NAME, now, hoursToExpire, configCookiePath, configCookieDomain, configCookieIsSecure, configCookieSameSite); }; /** * Calling this method will remove any previously given consent and during this page view no request * will be sent anymore ({@link requireConsent()}) will be called automatically to ensure the removed * consent will be enforced. You may call this method if the user removes consent manually, or if you * want to re-ask for consent after a specific time period. You can optionally define the lifetime of * the CONSENT_REMOVED_COOKIE_NAME cookie in hours using a parameter. * * @param {int} hoursToExpire After how many hours the CONSENT_REMOVED_COOKIE_NAME cookie should expire. * By default the consent is valid for 30 years unless cookies are deleted by the user or the browser * prior to this */ this.forgetConsentGiven = function (hoursToExpire) { if (hoursToExpire) { hoursToExpire = hoursToExpire * 60 * 60 * 1000; } else { hoursToExpire = 30 * 365 * 24 * 60 * 60 * 1000; } deleteCookie(CONSENT_COOKIE_NAME, configCookiePath, configCookieDomain); setCookie(CONSENT_REMOVED_COOKIE_NAME, new Date().getTime(), hoursToExpire, configCookiePath, configCookieDomain, configCookieIsSecure, configCookieSameSite); this.forgetCookieConsentGiven(); this.requireConsent(); }; /** * Returns true if user is opted out, false if otherwise. * * @returns {boolean} */ this.isUserOptedOut = function () { return !configHasConsent; }; /** * Alias for forgetConsentGiven(). After calling this function, the user will no longer be tracked, * (even if they come back to the site). */ this.optUserOut = this.forgetConsentGiven; /** * Alias for rememberConsentGiven(). After calling this function, the current user will be tracked. */ this.forgetUserOptOut = function () { // we can't automatically enable cookies here as we don't know if user actually gave consent for cookies this.setConsentGiven(false); }; /** * enable protocol file: format tracking */ this.enableFileTracking = function () { configFileTracking = true; }; /** * Mark performance metrics as available, once onload event has finished */ trackCallbackOnLoad(function(){ setTimeout(function(){ performanceAvailable = true; }, 0); }); Matomo.trigger('TrackerSetup', [this]); Matomo.addPlugin('TrackerVisitorIdCookie' + uniqueTrackerId, { // if no tracking request was sent we refresh the visitor id cookie on page unload unload: function () { if (supportsClientHints() && !clientHintsResolved) { clientHintsResolved = true; processClientHintsQueue(); // ensure possible queued request are sent out } if (!hasSentTrackingRequestYet) { setVisitorIdCookie(); // this will set the referrer attribution cookie detectReferrerAttribution(); } } }); } function TrackerProxy() { return { push: apply }; } /** * Applies the given methods in the given order if they are present in paq. * * @param {Array} paq * @param {Array} methodsToApply an array containing method names in the order that they should be applied * eg ['setSiteId', 'setTrackerUrl'] * @returns {Array} the modified paq array with the methods that were already applied set to undefined */ function applyMethodsInOrder(paq, methodsToApply) { var appliedMethods = {}; var index, iterator; for (index = 0; index < methodsToApply.length; index++) { var methodNameToApply = methodsToApply[index]; appliedMethods[methodNameToApply] = 1; for (iterator = 0; iterator < paq.length; iterator++) { if (paq[iterator] && paq[iterator][0]) { var methodName = paq[iterator][0]; if (methodNameToApply === methodName) { apply(paq[iterator]); delete paq[iterator]; if (appliedMethods[methodName] > 1 && methodName !== "addTracker" && methodName !== "enableLinkTracking") { logConsoleError('The method ' + methodName + ' is registered more than once in "_paq" variable. Only the last call has an effect. Please have a look at the multiple Matomo trackers documentation: https://developer.matomo.org/guides/tracking-javascript-guide#multiple-piwik-trackers'); } appliedMethods[methodName]++; } } } } return paq; } /************************************************************ * Constructor ************************************************************/ var applyFirst = ['addTracker', 'enableFileTracking', 'forgetCookieConsentGiven', 'requireCookieConsent', 'disableBrowserFeatureDetection', 'disableCampaignParameters', 'disableCookies', 'setTrackerUrl', 'setAPIUrl', 'enableCrossDomainLinking', 'setCrossDomainLinkingTimeout', 'setSessionCookieTimeout', 'setVisitorCookieTimeout', 'setCookieNamePrefix', 'setCookieSameSite', 'setSecureCookie', 'setCookiePath', 'setCookieDomain', 'setDomains', 'setUserId', 'setVisitorId', 'setSiteId', 'alwaysUseSendBeacon', 'disableAlwaysUseSendBeacon', 'enableLinkTracking', 'setCookieConsentGiven', 'requireConsent', 'setConsentGiven', 'disablePerformanceTracking', 'setPagePerformanceTiming', 'setExcludedQueryParams', 'setExcludedReferrers']; function createFirstTracker(matomoUrl, siteId) { var tracker = new Tracker(matomoUrl, siteId); asyncTrackers.push(tracker); _paq = applyMethodsInOrder(_paq, applyFirst); // apply the queue of actions for (iterator = 0; iterator < _paq.length; iterator++) { if (_paq[iterator]) { apply(_paq[iterator]); } } // replace initialization array with proxy object _paq = new TrackerProxy(); Matomo.trigger('TrackerAdded', [tracker]); return tracker; } /************************************************************ * Proxy object * - this allows the caller to continue push()'ing to _paq * after the Tracker has been initialized and loaded ************************************************************/ // initialize the Matomo singleton addEventListener(windowAlias, 'beforeunload', beforeUnloadHandler, false); addEventListener(windowAlias, 'visibilitychange', function () { // if unloaded, return if (isPageUnloading) { return; } // if not visible if (documentAlias.visibilityState === 'hidden') { executePluginMethod('unload'); } }, false); addEventListener(windowAlias, 'online', function () { if (isDefined(navigatorAlias.serviceWorker)) { navigatorAlias.serviceWorker.ready.then(function(swRegistration) { if (swRegistration && swRegistration.sync) { return swRegistration.sync.register('matomoSync'); } }, function() { // handle (but ignore) failed promise, see https://github.com/matomo-org/matomo/issues/17454 }); } }, false); addEventListener(windowAlias,'message', function(e) { if (!e || !e.origin) { return; } var tracker, i, matomoHost; var originHost = getHostName(e.origin); var trackers = Matomo.getAsyncTrackers(); for (i = 0; i < trackers.length; i++) { matomoHost = getHostName(trackers[i].getMatomoUrl()); // find the matching tracker if (matomoHost === originHost) { tracker = trackers[i]; break; } } if (!tracker) { // no matching tracker // Don't accept the message unless it came from the expected origin return; } var data = null; try { data = JSON.parse(e.data); } catch (ex) { return; } if (!data) { return; } function postMessageToCorrectFrame(postMessage){ // Find the iframe with the right URL to send it back to var iframes = documentAlias.getElementsByTagName('iframe'); for (i = 0; i < iframes.length; i++) { var iframe = iframes[i]; var iframeHost = getHostName(iframe.src); if (iframe.contentWindow && isDefined(iframe.contentWindow.postMessage) && iframeHost === originHost) { var jsonMessage = JSON.stringify(postMessage); iframe.contentWindow.postMessage(jsonMessage, e.origin); } } } // This listener can process two kinds of messages // 1) maq_initial_value => sent by optout iframe when it finishes loading. Passes the value of the third // party opt-out cookie (if set) - we need to use this and any first-party cookies that are present to // initialise the configHasConsent value and send back the result so that the display can be updated. // 2) maq_opted_in => sent by optout iframe when the user changes their optout setting. We need to update // our first-party cookie. if (isDefined(data.maq_initial_value)) { // Make a message to tell the optout iframe about the current state postMessageToCorrectFrame({ maq_opted_in: data.maq_initial_value && tracker.hasConsent(), maq_url: tracker.getMatomoUrl(), maq_optout_by_default: tracker.isConsentRequired() }); } else if (isDefined(data.maq_opted_in)) { // perform the opt in or opt out... trackers = Matomo.getAsyncTrackers(); for (i = 0; i < trackers.length; i++) { tracker = trackers[i]; if (data.maq_opted_in) { tracker.rememberConsentGiven(); } else { tracker.forgetConsentGiven(); } } // Make a message to tell the optout iframe about the current state postMessageToCorrectFrame({ maq_confirm_opted_in: tracker.hasConsent(), maq_url: tracker.getMatomoUrl(), maq_optout_by_default: tracker.isConsentRequired() }); } }, false); Date.prototype.getTimeAlias = Date.prototype.getTime; /************************************************************ * Public data and methods ************************************************************/ Matomo = { initialized: false, JSON: windowAlias.JSON, /** * DOM Document related methods */ DOM: { /** * Adds an event listener to the given element. * @param element * @param eventType * @param eventHandler * @param useCapture Optional */ addEventListener: function (element, eventType, eventHandler, useCapture) { var captureType = typeof useCapture; if (captureType === 'undefined') { useCapture = false; } addEventListener(element, eventType, eventHandler, useCapture); }, /** * Specify a function to execute when the DOM is fully loaded. * * If the DOM is already loaded, the function will be executed immediately. * * @param {Function} callback */ onLoad: trackCallbackOnLoad, /** * Specify a function to execute when the DOM is ready. * * If the DOM is already ready, the function will be executed immediately. * * @param {Function} callback */ onReady: trackCallbackOnReady, /** * Detect whether a node is visible right now. */ isNodeVisible: isVisible, /** * Detect whether a node has been visible at some point */ isOrWasNodeVisible: content.isNodeVisible }, /** * Listen to an event and invoke the handler when a the event is triggered. * * @param {string} event * @param {Function} handler */ on: function (event, handler) { if (!eventHandlers[event]) { eventHandlers[event] = []; } eventHandlers[event].push(handler); }, /** * Remove a handler to no longer listen to the event. Must pass the same handler that was used when * attaching the event via ".on". * @param {string} event * @param {Function} handler */ off: function (event, handler) { if (!eventHandlers[event]) { return; } var i = 0; for (i; i < eventHandlers[event].length; i++) { if (eventHandlers[event][i] === handler) { eventHandlers[event].splice(i, 1); } } }, /** * Triggers the given event and passes the parameters to all handlers. * * @param {string} event * @param {Array} extraParameters * @param {Object} context If given the handler will be executed in this context */ trigger: function (event, extraParameters, context) { if (!eventHandlers[event]) { return; } var i = 0; for (i; i < eventHandlers[event].length; i++) { eventHandlers[event][i].apply(context || windowAlias, extraParameters); } }, /** * Add plugin * * @param {string} pluginName * @param {Object} pluginObj */ addPlugin: function (pluginName, pluginObj) { plugins[pluginName] = pluginObj; }, /** * Get Tracker (factory method) * * @param {string} matomoUrl * @param {int|string} siteId * @returns {Tracker} */ getTracker: function (matomoUrl, siteId) { if (!isDefined(siteId)) { siteId = this.getAsyncTracker().getSiteId(); } if (!isDefined(matomoUrl)) { matomoUrl = this.getAsyncTracker().getTrackerUrl(); } return new Tracker(matomoUrl, siteId); }, /** * Get all created async trackers * * @returns {Tracker[]} */ getAsyncTrackers: function () { return asyncTrackers; }, /** * Adds a new tracker. All sent requests will be also sent to the given siteId and matomoUrl. * If matomoUrl is not set, current url will be used. * * @param {null|string} matomoUrl If null, will reuse the same tracker URL of the current tracker instance * @param {int|string} siteId * @returns {Tracker} */ addTracker: function (matomoUrl, siteId) { var tracker; if (!asyncTrackers.length) { tracker = createFirstTracker(matomoUrl, siteId); } else { tracker = asyncTrackers[0].addTracker(matomoUrl, siteId); } return tracker; }, /** * Get internal asynchronous tracker object. * * If no parameters are given, it returns the internal asynchronous tracker object. If a matomoUrl and idSite * is given, it will try to find an optional * * @param {string} matomoUrl * @param {int|string} siteId * @returns {Tracker} */ getAsyncTracker: function (matomoUrl, siteId) { var firstTracker; if (asyncTrackers && asyncTrackers.length && asyncTrackers[0]) { firstTracker = asyncTrackers[0]; } else { return createFirstTracker(matomoUrl, siteId); } if (!siteId && !matomoUrl) { // for BC and by default we just return the initially created tracker return firstTracker; } // we look for another tracker created via `addTracker` method if ((!isDefined(siteId) || null === siteId) && firstTracker) { siteId = firstTracker.getSiteId(); } if ((!isDefined(matomoUrl) || null === matomoUrl) && firstTracker) { matomoUrl = firstTracker.getTrackerUrl(); } var tracker, i = 0; for (i; i < asyncTrackers.length; i++) { tracker = asyncTrackers[i]; if (tracker && String(tracker.getSiteId()) === String(siteId) && tracker.getTrackerUrl() === matomoUrl) { return tracker; } } }, /** * When calling plugin methods via "_paq.push(['...'])" and the plugin is loaded separately because * matomo.js is not writable then there is a chance that first matomo.js is loaded and later the plugin. * In this case we would have already executed all "_paq.push" methods and they would not have succeeded * because the plugin will be loaded only later. In this case, once a plugin is loaded, it should call * "Matomo.retryMissedPluginCalls()" so they will be executed after all. */ retryMissedPluginCalls: function () { var missedCalls = missedPluginTrackerCalls; missedPluginTrackerCalls = []; var i = 0; for (i; i < missedCalls.length; i++) { apply(missedCalls[i]); } } }; // Expose Matomo as an AMD module if (typeof define === 'function' && define.amd) { define('piwik', [], function () { return Matomo; }); define('matomo', [], function () { return Matomo; }); } return Matomo; }()); } /*!! pluginTrackerHook */ (function () { 'use strict'; function hasPaqConfiguration() { if ('object' !== typeof _paq) { return false; } // needed to write it this way for jslint var lengthType = typeof _paq.length; if ('undefined' === lengthType) { return false; } return !!_paq.length; } if (window && 'object' === typeof window.matomoPluginAsyncInit && window.matomoPluginAsyncInit.length) { var i = 0; for (i; i < window.matomoPluginAsyncInit.length; i++) { if (typeof window.matomoPluginAsyncInit[i] === 'function') { window.matomoPluginAsyncInit[i](); } } } if (window && window.piwikAsyncInit) { window.piwikAsyncInit(); } if (window && window.matomoAsyncInit) { window.matomoAsyncInit(); } if (!window.Matomo.getAsyncTrackers().length) { // we only create an initial tracker when no other async tracker has been created yet in matomoAsyncInit() if (hasPaqConfiguration()) { // we only create an initial tracker if there is a configuration for it via _paq. Otherwise // Matomo.getAsyncTrackers() would return unconfigured trackers window.Matomo.addTracker(); } else { _paq = {push: function (args) { // needed to write it this way for jslint var consoleType = typeof console; if (consoleType !== 'undefined' && console && console.error) { console.error('_paq.push() was used but Matomo tracker was not initialized before the matomo.js file was loaded. Make sure to configure the tracker via _paq.push before loading matomo.js. Alternatively, you can create a tracker via Matomo.addTracker() manually and then use _paq.push but it may not fully work as tracker methods may not be executed in the correct order.', args); } }}; } } window.Matomo.trigger('MatomoInitialized', []); window.Matomo.initialized = true; }()); /*jslint sloppy: true */ (function () { var jsTrackerType = (typeof window.AnalyticsTracker); if (jsTrackerType === 'undefined') { window.AnalyticsTracker = window.Matomo; } }()); /*jslint sloppy: false */ /************************************************************ * Deprecated functionality below * Legacy piwik.js compatibility ftw ************************************************************/ /* * Matomo globals * * var piwik_install_tracker, piwik_tracker_pause, piwik_download_extensions, piwik_hosts_alias, piwik_ignore_classes; */ /*global piwik_log:true */ /*global piwik_track:true */ /** * Track page visit * * @param {string} documentTitle * @param {int|string} siteId * @param {string} matomoUrl * @param {*} customData */ if (typeof window.piwik_log !== 'function') { window.piwik_log = function (documentTitle, siteId, matomoUrl, customData) { 'use strict'; function getOption(optionName) { try { if (window['piwik_' + optionName]) { return window['piwik_' + optionName]; } } catch (ignore) { } return; // undefined } // instantiate the tracker var option, matomoTracker = window.Matomo.getTracker(matomoUrl, siteId); // initialize tracker matomoTracker.setDocumentTitle(documentTitle); matomoTracker.setCustomData(customData); // handle Matomo globals option = getOption('tracker_pause'); if (option) { matomoTracker.setLinkTrackingTimer(option); } option = getOption('download_extensions'); if (option) { matomoTracker.setDownloadExtensions(option); } option = getOption('hosts_alias'); if (option) { matomoTracker.setDomains(option); } option = getOption('ignore_classes'); if (option) { matomoTracker.setIgnoreClasses(option); } // track this page view matomoTracker.trackPageView(); // default is to install the link tracker if (getOption('install_tracker')) { /** * Track click manually (function is defined below) * * @param {string} sourceUrl * @param {int|string} siteId * @param {string} matomoUrl * @param {string} linkType */ piwik_track = function (sourceUrl, siteId, matomoUrl, linkType) { matomoTracker.setSiteId(siteId); matomoTracker.setTrackerUrl(matomoUrl); matomoTracker.trackLink(sourceUrl, linkType); }; // set-up link tracking matomoTracker.enableLinkTracking(); } }; } /*! @license-end */