diff --git a/.eslintrc b/.eslintrc index fafebc61..878aa407 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,10 +5,23 @@ "node": true, }, "parserOptions": { + "ecmaVersion": 2017, "sourceType": "module", }, "extends": [ "eslint:recommended", "google", ], + "globals": { + "safari": false + }, + "overrides": [ + { + "files": ["test/**/*"], + "globals": { + "assert": false, + "sinon": false + } + } + ] } diff --git a/.travis.yml b/.travis.yml index 20657e14..f00c9185 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ --- language: node_js node_js: - - "6" + - "node" # If a valid commit range exits, check that it has changes to code files. before_install: diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2a3e55..b8737543 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ This document lists the changes between each minor and patch versions. For changes between major versions, see the [Upgrade Reference](/docs/upgrading.md) +### 2.4.1 (2017-06-07) + +- Fix a bug in Safari where `outboundLinkTracker` doesn't work with the back button [#185] + +### 2.4.0 (2017-06-02) + +- Add a `queryParamsWhitelist` option to the `cleanUrlTracker` plugin [#181] + +### 2.3.3 (2017-05-23) + +- Fix a bug where, in rare cases, visibility times were being tracked cross-session [#177] + ### 2.3.2 (2017-04-10) - Fix incorrect plugin usage attribution on the initial pageview sent by the `pageVisibilityTracker` if other plugins are required after it [#169] diff --git a/README.md b/README.md index b509ecb4..16864295 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# This library is no longer actively maintained + # Autotrack [![Build Status](https://travis-ci.org/googleanalytics/autotrack.svg?branch=master)](https://travis-ci.org/googleanalytics/autotrack) - [Overview](#overview) @@ -21,7 +23,7 @@ Autotrack was created to solve this problem. It provides default tracking for th ## Plugins -The `autotrack.js` file in this repository is small (7K gzipped) and comes with all plugins included. You can use it as is, or you can create a [custom build](#custom-builds) that only includes the plugins you want to make it even smaller. +The `autotrack.js` file in this repository is small (8K gzipped) and comes with all plugins included. You can use it as is, or you can create a [custom build](#custom-builds) that only includes the plugins you want to make it even smaller. The following table briefly explains what each plugin does; you can click on the plugin name to see the full documentation and usage instructions: @@ -72,7 +74,7 @@ The following table briefly explains what each plugin does; you can click on the -**Disclaimer:** autotrack is maintained by members of the Google Analytics developer platform team and is primarily intended for a developer audience. It is not an official Google Analytics product and does not qualify for Google Analytics 360 support. Developers who choose to use this library are responsible for ensuring that their implementation meets the requirements of the [Google Analytics Terms of Service](https://www.google.com/analytics/terms/us.html) and the legal obligations of their respective country. +**Disclaimer:** autotrack is maintained by members of the Google Analytics developer platform team and is primarily intended for a developer audience. It is not an official Google Analytics product and does not qualify for Google Analytics 360 support. Developers who choose to use this library are responsible for ensuring that their implementation meets the requirements of the [Google Analytics Terms of Service](https://marketingplatform.google.com/about/analytics/terms/us/) and the legal obligations of their respective country. ## Installation and usage @@ -110,7 +112,7 @@ Of course, you'll have to make the following modifications to the above code to ### Loading autotrack via npm -If you use npm and a module loader that understands [ES2015 imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) (e.g. [Webpack](https://webpack.js.org/), [Rollup](http://rollupjs.org/), or [SystemJS](https://github.com/systemjs/systemjs)), you can include autotrack in your build by importing it as you would any other npm module: +If you use npm and a module loader that understands [ES2015 imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) (e.g. [Webpack](https://webpack.js.org/), [Rollup](https://rollupjs.org/), or [SystemJS](https://github.com/systemjs/systemjs)), you can include autotrack in your build by importing it as you would any other npm module: ```sh npm install autotrack @@ -277,7 +279,7 @@ The following translations have been graciously provided by the community. Pleas If you discover issues with a particular translation, please file them with the appropriate repository. To submit your own translation, follow these steps: 1. Fork this repository. -2. Update the settings of your fork to [allow issues](http://programmers.stackexchange.com/questions/179468/forking-a-repo-on-github-but-allowing-new-issues-on-the-fork). +2. Update the settings of your fork to [allow issues](https://softwareengineering.stackexchange.com/questions/179468/forking-a-repo-on-github-but-allowing-new-issues-on-the-fork). 3. Remove all non-documentation files. 4. Update the documentation files with your translated versions. 5. Submit a pull request to this repository that adds a link to your fork to the above list. diff --git a/autotrack.js b/autotrack.js index af62613f..b86811c4 100644 --- a/autotrack.js +++ b/autotrack.js @@ -1,60 +1,63 @@ -(function(){var f,aa="function"==typeof Object.defineProperties?Object.defineProperty:function(a,b,c){if(c.get||c.set)throw new TypeError("ES3 does not support getters and setters.");a!=Array.prototype&&a!=Object.prototype&&(a[b]=c.value)},k="undefined"!=typeof window&&window===this?this:"undefined"!=typeof global&&null!=global?global:this;function ba(){ba=function(){};k.Symbol||(k.Symbol=ca)}var da=0;function ca(a){return"jscomp_symbol_"+(a||"")+da++} -function l(){ba();var a=k.Symbol.iterator;a||(a=k.Symbol.iterator=k.Symbol("iterator"));"function"!=typeof Array.prototype[a]&&aa(Array.prototype,a,{configurable:!0,writable:!0,value:function(){return ea(this)}});l=function(){}}function ea(a){var b=0;return fa(function(){return bwindow.gaDevIds.indexOf("i5iSjo")&&window.gaDevIds.push("i5iSjo");window[c]("provide",a,b);window.gaplugins=window.gaplugins||{};window.gaplugins[a.charAt(0).toUpperCase()+a.slice(1)]=b}var F={T:1,U:2,V:3,X:4,Y:5,Z:6,$:7,aa:8,ba:9,W:10},G=Object.keys(F).length; -function H(a,b){a.set("\x26_av","2.3.2");var c=a.get("\x26_au"),c=parseInt(c||"0",16).toString(2);if(c.lengthb.getAttribute(c+"on").split(/\s*,\s*/).indexOf(a.type))){var c=z(b,c),d=y({},this.a.fieldsObj,c);this.f.send(c.hitType||"event",x({transport:"beacon"},d,this.f,this.a.hitFilter,b,a))}};J.prototype.remove=function(){var a=this;Object.keys(this.b).forEach(function(b){a.b[b].j()})};C("eventTracker",J); -function xa(a,b){var c=this;H(a,F.V);window.IntersectionObserver&&window.MutationObserver&&(this.a=y({rootMargin:"0px",fieldsObj:{},attributePrefix:"ga-"},b),this.c=a,this.M=this.M.bind(this),this.O=this.O.bind(this),this.K=this.K.bind(this),this.L=this.L.bind(this),this.b=null,this.items=[],this.h={},this.g={},sa(function(){c.a.elements&&c.observeElements(c.a.elements)}))}f=xa.prototype; -f.observeElements=function(a){var b=this;a=K(this,a);this.items=this.items.concat(a.items);this.h=y({},a.h,this.h);this.g=y({},a.g,this.g);a.items.forEach(function(a){var c=b.g[a.threshold]=b.g[a.threshold]||new IntersectionObserver(b.O,{rootMargin:b.a.rootMargin,threshold:[+a.threshold]});(a=b.h[a.id]||(b.h[a.id]=document.getElementById(a.id)))&&c.observe(a)});this.b||(this.b=new MutationObserver(this.M),this.b.observe(document.body,{childList:!0,subtree:!0}));requestAnimationFrame(function(){})}; -f.unobserveElements=function(a){var b=[],c=[];this.items.forEach(function(d){a.some(function(a){a=ya(a);return a.id===d.id&&a.threshold===d.threshold&&a.trackFirstImpressionOnly===d.trackFirstImpressionOnly})?c.push(d):b.push(d)});if(b.length){var d=K(this,b),e=K(this,c);this.items=d.items;this.h=d.h;this.g=d.g;c.forEach(function(a){if(!d.h[a.id]){var b=e.g[a.threshold],c=e.h[a.id];c&&b.unobserve(c);d.g[a.threshold]||e.g[a.threshold].disconnect()}})}else this.unobserveAllElements()}; -f.unobserveAllElements=function(){var a=this;Object.keys(this.g).forEach(function(b){a.g[b].disconnect()});this.b.disconnect();this.b=null;this.items=[];this.h={};this.g={}};function K(a,b){var c=[],d={},e={};b.length&&b.forEach(function(b){b=ya(b);c.push(b);e[b.id]=a.h[b.id]||null;d[b.threshold]=a.g[b.threshold]||null});return{items:c,h:e,g:d}}f.M=function(a){for(var b=0,c;c=a[b];b++){for(var d=0,e;e=c.removedNodes[d];d++)L(this,e,this.L);for(d=0;e=c.addedNodes[d];d++)L(this,e,this.K)}}; -function L(a,b,c){1==b.nodeType&&b.id in a.h&&c(b.id);for(var d=0,e;e=b.childNodes[d];d++)L(a,e,c)} -f.O=function(a){for(var b=[],c=0,d;d=a[c];c++)for(var e=0,h;h=this.items[e];e++){var g;if(g=d.target.id===h.id)(g=h.threshold)?g=d.intersectionRatio>=g:(g=d.intersectionRect,g=06E4*this.timeout||this.c&&this.c.format(b)!=this.c.format(c))?!0:!1}; -T.prototype.b=function(a){var b=this;return function(c){a(c);var d=b.a.get(),e=b.isExpired(d);c=c.get("sessionControl");d.hitTime=+new Date;if("start"==c||e)d.isExpired=!1;"end"==c&&(d.isExpired=!0);b.a.set(d)}};T.prototype.j=function(){w(this.f,"sendHitTask",this.b);this.a.j();delete Da[this.f.get("trackingId")]};var U=30; -function V(a,b){H(a,F.W);window.addEventListener&&(this.a=y({increaseThreshold:20,sessionTimeout:U,fieldsObj:{}},b),this.c=a,this.b=Ea(this),this.f=ta(this.f.bind(this),500),this.m=this.m.bind(this),this.i=Q(a.get("trackingId"),"plugins/max-scroll-tracker"),this.s=new T(a,this.a.sessionTimeout,this.a.timeZone),v(a,"set",this.m),Fa(this))}function Fa(a){100>(a.i.get()[a.b]||0)&&window.addEventListener("scroll",a.f)} -V.prototype.f=function(){var a=document.documentElement,b=document.body,a=Math.min(100,Math.max(0,Math.round(window.pageYOffset/(Math.max(a.offsetHeight,a.scrollHeight,b.offsetHeight,b.scrollHeight)-window.innerHeight)*100)));if(this.s.isExpired())Ca(this.i);else if(b=this.i.get()[this.b]||0,a>b&&(100!=a&&100!=b||window.removeEventListener("scroll",this.f),b=a-b,100==a||b>=this.a.increaseThreshold)){var c={};this.i.set((c[this.b]=a,c));a={transport:"beacon",eventCategory:"Max Scroll",eventAction:"increase", -eventValue:b,eventLabel:String(a),nonInteraction:!0};this.a.maxScrollMetricIndex&&(a["metric"+this.a.maxScrollMetricIndex]=b);this.c.send("event",x(a,this.a.fieldsObj,this.c,this.a.hitFilter))}};V.prototype.m=function(a){var b=this;return function(c,d){a(c,d);var e={};(B(c)?c:(e[c]=d,e)).page&&(c=b.b,b.b=Ea(b),b.b!=c&&Fa(b))}};function Ea(a){a=t(a.c.get("page")||a.c.get("location"));return a.pathname+a.search} -V.prototype.remove=function(){this.s.j();window.removeEventListener("scroll",this.f);w(this.c,"set",this.m)};C("maxScrollTracker",V);var Ga={};function W(a,b){H(a,F.X);window.matchMedia&&(this.a=y({changeTemplate:this.changeTemplate,changeTimeout:1E3,fieldsObj:{}},b),B(this.a.definitions)&&(b=this.a.definitions,this.a.definitions=Array.isArray(b)?b:[b],this.b=a,this.c=[],Ha(this)))} -function Ha(a){a.a.definitions.forEach(function(b){if(b.name&&b.dimensionIndex){var c=Ja(b);a.b.set("dimension"+b.dimensionIndex,c);Ka(a,b)}})}function Ja(a){var b;a.items.forEach(function(a){La(a.media).matches&&(b=a)});return b?b.name:"(not set)"} -function Ka(a,b){b.items.forEach(function(c){c=La(c.media);var d=ta(function(){var c=Ja(b),d=a.b.get("dimension"+b.dimensionIndex);c!==d&&(a.b.set("dimension"+b.dimensionIndex,c),c={transport:"beacon",eventCategory:b.name,eventAction:"change",eventLabel:a.a.changeTemplate(d,c),nonInteraction:!0},a.b.send("event",x(c,a.a.fieldsObj,a.b,a.a.hitFilter)))},a.a.changeTimeout);c.addListener(d);a.c.push({fa:c,da:d})})}W.prototype.remove=function(){for(var a=0,b;b=this.c[a];a++)b.fa.removeListener(b.da)}; -W.prototype.changeTemplate=function(a,b){return a+" \x3d\x3e "+b};C("mediaQueryTracker",W);function La(a){return Ga[a]||(Ga[a]=window.matchMedia(a))}function X(a,b){H(a,F.Y);window.addEventListener&&(this.a=y({formSelector:"form",shouldTrackOutboundForm:this.shouldTrackOutboundForm,fieldsObj:{},attributePrefix:"ga-"},b),this.b=a,this.c=p("submit",this.a.formSelector,this.f.bind(this)))} -X.prototype.f=function(a,b){var c={transport:"beacon",eventCategory:"Outbound Form",eventAction:"submit",eventLabel:t(b.action).href};if(this.a.shouldTrackOutboundForm(b,t)){navigator.sendBeacon||(a.preventDefault(),c.hitCallback=ua(function(){b.submit()}));var d=y({},this.a.fieldsObj,z(b,this.a.attributePrefix));this.b.send("event",x(c,d,this.b,this.a.hitFilter,b,a))}}; -X.prototype.shouldTrackOutboundForm=function(a,b){a=b(a.action);return a.hostname!=location.hostname&&"http"==a.protocol.slice(0,4)};X.prototype.remove=function(){this.c.j()};C("outboundFormTracker",X); -function Y(a,b){var c=this;H(a,F.Z);window.addEventListener&&(this.a=y({events:["click"],linkSelector:"a, area",shouldTrackOutboundLink:this.shouldTrackOutboundLink,fieldsObj:{},attributePrefix:"ga-"},b),this.f=a,this.c=this.c.bind(this),this.b={},this.a.events.forEach(function(a){c.b[a]=p(a,c.a.linkSelector,c.c)}))} -Y.prototype.c=function(a,b){if(this.a.shouldTrackOutboundLink(b,t)){var c=b.getAttribute("href")||b.getAttribute("xlink:href"),d=t(c),e={transport:"beacon",eventCategory:"Outbound Link",eventAction:a.type,eventLabel:d.href};navigator.sendBeacon||"click"!=a.type||"_blank"==b.target||a.metaKey||a.ctrlKey||a.shiftKey||a.altKey||1>b/4).toString(16):"10000000-1000-4000-8000-100000000000".replace(/[018]/g,Ma)}(); -function Na(a,b){var c=this;H(a,F.$);document.visibilityState&&(this.a=y({sessionTimeout:U,visibleThreshold:5E3,sendInitialPageview:!1,fieldsObj:{}},b),this.b=a,this.i=this.f=null,this.s=!1,this.v=this.v.bind(this),this.o=this.o.bind(this),this.G=this.G.bind(this),this.N=this.N.bind(this),this.c=Q(a.get("trackingId"),"plugins/page-visibility-tracker"),Aa(this.c,this.N),this.m=new T(a,this.a.sessionTimeout,this.a.timeZone),v(a,"set",this.v),window.addEventListener("unload",this.G),document.addEventListener("visibilitychange", -this.o),this.o(),va(this.b,function(){if("visible"==document.visibilityState)c.a.sendInitialPageview&&(Oa(c,{ea:!0}),c.s=!0);else if(c.a.sendInitialPageview&&c.a.pageLoadsMetricIndex){var a={},a=(a.transport="beacon",a.eventCategory="Page Visibility",a.eventAction="page load",a.eventLabel="(not set)",a["metric"+c.a.pageLoadsMetricIndex]=1,a.nonInteraction=!0,a);c.b.send("event",x(a,c.a.fieldsObj,c.b,c.a.hitFilter))}}))}f=Na.prototype; -f.o=function(){var a=this;if("visible"==document.visibilityState||"hidden"==document.visibilityState){var b=Pa(this,this.c.get()),c={time:+new Date,state:document.visibilityState,pageId:Z};this.f&&"visible"==document.visibilityState&&this.a.sendInitialPageview&&!this.s&&(Oa(this),this.s=!0);this.i&&"hidden"==document.visibilityState&&clearTimeout(this.i);this.m.isExpired()?"hidden"==this.f&&"visible"==document.visibilityState?(clearTimeout(this.i),this.i=setTimeout(function(){a.c.set(c);Oa(a,{hitTime:c.time})}, -this.a.visibleThreshold)):"hidden"==document.visibilityState&&Ca(this.c):(b.pageId==Z&&"visible"==b.state&&Qa(this,b),this.c.set(c));this.f=document.visibilityState}};function Pa(a,b){"visible"==a.f&&"hidden"==b.state&&b.pageId!=Z&&(b.state="visible",b.pageId=Z,a.c.set(b));return b} -function Qa(a,b,c){c=(c?c:{}).hitTime;var d={hitTime:c},d=(d?d:{}).hitTime;(b=b.time&&!a.m.isExpired()?(d||+new Date)-b.time:0)&&b>=a.a.visibleThreshold&&(b=Math.round(b/1E3),d={transport:"beacon",nonInteraction:!0,eventCategory:"Page Visibility",eventAction:"track",eventValue:b,eventLabel:"(not set)"},c&&(d.queueTime=+new Date-c),a.a.visibleMetricIndex&&(d["metric"+a.a.visibleMetricIndex]=b),a.b.send("event",x(d,a.a.fieldsObj,a.b,a.a.hitFilter)))} -function Oa(a,b){var c=b?b:{};b=c.hitTime;var c=c.ea,d={transport:"beacon"};b&&(d.queueTime=+new Date-b);c&&a.a.pageLoadsMetricIndex&&(d["metric"+a.a.pageLoadsMetricIndex]=1);a.b.send("pageview",x(d,a.a.fieldsObj,a.b,a.a.hitFilter))}f.v=function(a){var b=this;return function(c,d){var e={},e=B(c)?c:(e[c]=d,e);e.page&&e.page!==b.b.get("page")&&"visible"==b.f&&b.o();a(c,d)}};f.N=function(a,b){a.time!=b.time&&b.pageId==Z&&"visible"==b.state&&Qa(this,b,{hitTime:a.time})}; -f.G=function(){"hidden"!=this.f&&this.o()};f.remove=function(){this.c.j();this.m.j();w(this.b,"set",this.v);window.removeEventListener("unload",this.G);document.removeEventListener("visibilitychange",this.o)};C("pageVisibilityTracker",Na); -function Ra(a,b){H(a,F.aa);window.addEventListener&&(this.a=y({fieldsObj:{},hitFilter:null},b),this.b=a,this.u=this.u.bind(this),this.J=this.J.bind(this),this.D=this.D.bind(this),this.A=this.A.bind(this),this.B=this.B.bind(this),this.F=this.F.bind(this),"complete"!=document.readyState?window.addEventListener("load",this.u):this.u())}f=Ra.prototype; -f.u=function(){if(window.FB)try{window.FB.Event.subscribe("edge.create",this.B),window.FB.Event.subscribe("edge.remove",this.F)}catch(a){}window.twttr&&this.J()};f.J=function(){var a=this;try{window.twttr.ready(function(){window.twttr.events.bind("tweet",a.D);window.twttr.events.bind("follow",a.A)})}catch(b){}};function Sa(a){try{window.twttr.ready(function(){window.twttr.events.unbind("tweet",a.D);window.twttr.events.unbind("follow",a.A)})}catch(b){}} -f.D=function(a){if("tweet"==a.region){var b={transport:"beacon",socialNetwork:"Twitter",socialAction:"tweet",socialTarget:a.data.url||a.target.getAttribute("data-url")||location.href};this.b.send("social",x(b,this.a.fieldsObj,this.b,this.a.hitFilter,a.target,a))}}; -f.A=function(a){if("follow"==a.region){var b={transport:"beacon",socialNetwork:"Twitter",socialAction:"follow",socialTarget:a.data.screen_name||a.target.getAttribute("data-screen-name")};this.b.send("social",x(b,this.a.fieldsObj,this.b,this.a.hitFilter,a.target,a))}};f.B=function(a){this.b.send("social",x({transport:"beacon",socialNetwork:"Facebook",socialAction:"like",socialTarget:a},this.a.fieldsObj,this.b,this.a.hitFilter))}; -f.F=function(a){this.b.send("social",x({transport:"beacon",socialNetwork:"Facebook",socialAction:"unlike",socialTarget:a},this.a.fieldsObj,this.b,this.a.hitFilter))};f.remove=function(){window.removeEventListener("load",this.u);try{window.FB.Event.unsubscribe("edge.create",this.B),window.FB.Event.unsubscribe("edge.remove",this.F)}catch(a){}Sa(this)};C("socialWidgetTracker",Ra); -function Ta(a,b){H(a,F.ba);history.pushState&&window.addEventListener&&(this.a=y({shouldTrackUrlChange:this.shouldTrackUrlChange,trackReplaceState:!1,fieldsObj:{},hitFilter:null},b),this.b=a,this.c=location.pathname+location.search,this.H=this.H.bind(this),this.I=this.I.bind(this),this.C=this.C.bind(this),v(history,"pushState",this.H),v(history,"replaceState",this.I),window.addEventListener("popstate",this.C))}f=Ta.prototype; -f.H=function(a){var b=this;return function(c){for(var d=[],e=0;e>b/4).toString(16):"10000000-1000-4000-8000-100000000000".replace(/[018]/g,wa)}; +function G(a,b){var c=window.GoogleAnalyticsObject||"ga";window[c]=window[c]||function(a){for(var b=[],d=0;dwindow.gaDevIds.indexOf("i5iSjo")&&window.gaDevIds.push("i5iSjo");window[c]("provide",a,b);window.gaplugins=window.gaplugins||{};window.gaplugins[a.charAt(0).toUpperCase()+a.slice(1)]=b}var H={T:1,U:2,V:3,X:4,Y:5,Z:6,$:7,aa:8,ba:9,W:10},I=Object.keys(H).length; +function J(a,b){a.set("\x26_av","2.4.1");var c=a.get("\x26_au"),c=parseInt(c||"0",16).toString(2);if(c.lengthb.getAttribute(c+"on").split(/\s*,\s*/).indexOf(a.type))){var c=B(b,c),d=A({},this.a.fieldsObj,c);this.f.send(c.hitType||"event",z({transport:"beacon"},d,this.f,this.a.hitFilter,b,a))}};L.prototype.remove=function(){var a=this;Object.keys(this.b).forEach(function(b){a.b[b].j()})};G("eventTracker",L); +function za(a,b){var c=this;J(a,H.V);window.IntersectionObserver&&window.MutationObserver&&(this.a=A({rootMargin:"0px",fieldsObj:{},attributePrefix:"ga-"},b),this.c=a,this.M=this.M.bind(this),this.O=this.O.bind(this),this.K=this.K.bind(this),this.L=this.L.bind(this),this.b=null,this.items=[],this.i={},this.h={},sa(function(){c.a.elements&&c.observeElements(c.a.elements)}))}g=za.prototype; +g.observeElements=function(a){var b=this;a=M(this,a);this.items=this.items.concat(a.items);this.i=A({},a.i,this.i);this.h=A({},a.h,this.h);a.items.forEach(function(a){var c=b.h[a.threshold]=b.h[a.threshold]||new IntersectionObserver(b.O,{rootMargin:b.a.rootMargin,threshold:[+a.threshold]});(a=b.i[a.id]||(b.i[a.id]=document.getElementById(a.id)))&&c.observe(a)});this.b||(this.b=new MutationObserver(this.M),this.b.observe(document.body,{childList:!0,subtree:!0}));requestAnimationFrame(function(){})}; +g.unobserveElements=function(a){var b=[],c=[];this.items.forEach(function(d){a.some(function(a){a=Aa(a);return a.id===d.id&&a.threshold===d.threshold&&a.trackFirstImpressionOnly===d.trackFirstImpressionOnly})?c.push(d):b.push(d)});if(b.length){var d=M(this,b),e=M(this,c);this.items=d.items;this.i=d.i;this.h=d.h;c.forEach(function(a){if(!d.i[a.id]){var b=e.h[a.threshold],c=e.i[a.id];c&&b.unobserve(c);d.h[a.threshold]||e.h[a.threshold].disconnect()}})}else this.unobserveAllElements()}; +g.unobserveAllElements=function(){var a=this;Object.keys(this.h).forEach(function(b){a.h[b].disconnect()});this.b.disconnect();this.b=null;this.items=[];this.i={};this.h={}};function M(a,b){var c=[],d={},e={};b.length&&b.forEach(function(b){b=Aa(b);c.push(b);e[b.id]=a.i[b.id]||null;d[b.threshold]=a.h[b.threshold]||null});return{items:c,i:e,h:d}}g.M=function(a){for(var b=0,c;c=a[b];b++){for(var d=0,e;e=c.removedNodes[d];d++)N(this,e,this.L);for(d=0;e=c.addedNodes[d];d++)N(this,e,this.K)}}; +function N(a,b,c){1==b.nodeType&&b.id in a.i&&c(b.id);for(var d=0,e;e=b.childNodes[d];d++)N(a,e,c)} +g.O=function(a){for(var b=[],c=0,d;d=a[c];c++)for(var e=0,h;h=this.items[e];e++){var f;if(f=d.target.id===h.id)(f=h.threshold)?f=d.intersectionRatio>=f:(f=d.intersectionRect,f=06E4*this.timeout||this.c&&this.c.format(a)!=this.c.format(b))?!0:!1};U.prototype.b=function(a){var b=this;return function(c){a(c);var d=c.get("sessionControl");c="start"==d||b.isExpired();var d="end"==d,e=b.a.get();e.hitTime=+new Date;c&&(e.isExpired=!1,e.id=E());d&&(e.isExpired=!0);b.a.set(e)}}; +U.prototype.j=function(){y(this.f,"sendHitTask",this.b);this.a.j();delete T[this.f.get("trackingId")]};var Ha=30;function W(a,b){J(a,H.W);window.addEventListener&&(this.b=A({increaseThreshold:20,sessionTimeout:Ha,fieldsObj:{}},b),this.f=a,this.c=Ja(this),this.g=ta(this.g.bind(this),500),this.o=this.o.bind(this),this.a=S(a.get("trackingId"),"plugins/max-scroll-tracker"),this.m=Ia(a,this.b.sessionTimeout,this.b.timeZone),x(a,"set",this.o),Ka(this))} +function Ka(a){100>(a.a.get()[a.c]||0)&&window.addEventListener("scroll",a.g)} +W.prototype.g=function(){var a=document.documentElement,b=document.body,a=Math.min(100,Math.max(0,Math.round(window.pageYOffset/(Math.max(a.offsetHeight,a.scrollHeight,b.offsetHeight,b.scrollHeight)-window.innerHeight)*100))),b=V(this.m);b!=this.a.get().sessionId&&(Ga(this.a),this.a.set({sessionId:b}));if(this.m.isExpired(this.a.get().sessionId))Ga(this.a);else if(b=this.a.get()[this.c]||0,a>b&&(100!=a&&100!=b||window.removeEventListener("scroll",this.g),b=a-b,100==a||b>=this.b.increaseThreshold)){var c= +{};this.a.set((c[this.c]=a,c.sessionId=V(this.m),c));a={transport:"beacon",eventCategory:"Max Scroll",eventAction:"increase",eventValue:b,eventLabel:String(a),nonInteraction:!0};this.b.maxScrollMetricIndex&&(a["metric"+this.b.maxScrollMetricIndex]=b);this.f.send("event",z(a,this.b.fieldsObj,this.f,this.b.hitFilter))}};W.prototype.o=function(a){var b=this;return function(c,d){a(c,d);var e={};(D(c)?c:(e[c]=d,e)).page&&(c=b.c,b.c=Ja(b),b.c!=c&&Ka(b))}}; +function Ja(a){a=u(a.f.get("page")||a.f.get("location"));return a.pathname+a.search}W.prototype.remove=function(){this.m.j();window.removeEventListener("scroll",this.g);y(this.f,"set",this.o)};G("maxScrollTracker",W);var La={};function Ma(a,b){J(a,H.X);window.matchMedia&&(this.a=A({changeTemplate:this.changeTemplate,changeTimeout:1E3,fieldsObj:{}},b),D(this.a.definitions)&&(b=this.a.definitions,this.a.definitions=Array.isArray(b)?b:[b],this.b=a,this.c=[],Oa(this)))} +function Oa(a){a.a.definitions.forEach(function(b){if(b.name&&b.dimensionIndex){var c=Pa(b);a.b.set("dimension"+b.dimensionIndex,c);Qa(a,b)}})}function Pa(a){var b;a.items.forEach(function(a){Ra(a.media).matches&&(b=a)});return b?b.name:"(not set)"} +function Qa(a,b){b.items.forEach(function(c){c=Ra(c.media);var d=ta(function(){var c=Pa(b),d=a.b.get("dimension"+b.dimensionIndex);c!==d&&(a.b.set("dimension"+b.dimensionIndex,c),c={transport:"beacon",eventCategory:b.name,eventAction:"change",eventLabel:a.a.changeTemplate(d,c),nonInteraction:!0},a.b.send("event",z(c,a.a.fieldsObj,a.b,a.a.hitFilter)))},a.a.changeTimeout);c.addListener(d);a.c.push({fa:c,da:d})})}Ma.prototype.remove=function(){for(var a=0,b;b=this.c[a];a++)b.fa.removeListener(b.da)}; +Ma.prototype.changeTemplate=function(a,b){return a+" \x3d\x3e "+b};G("mediaQueryTracker",Ma);function Ra(a){return La[a]||(La[a]=window.matchMedia(a))}function X(a,b){J(a,H.Y);window.addEventListener&&(this.a=A({formSelector:"form",shouldTrackOutboundForm:this.shouldTrackOutboundForm,fieldsObj:{},attributePrefix:"ga-"},b),this.b=a,this.c=q("submit",this.a.formSelector,this.f.bind(this)))} +X.prototype.f=function(a,b){var c={transport:"beacon",eventCategory:"Outbound Form",eventAction:"submit",eventLabel:u(b.action).href};if(this.a.shouldTrackOutboundForm(b,u)){navigator.sendBeacon||(a.preventDefault(),c.hitCallback=ua(function(){b.submit()}));var d=A({},this.a.fieldsObj,B(b,this.a.attributePrefix));this.b.send("event",z(c,d,this.b,this.a.hitFilter,b,a))}}; +X.prototype.shouldTrackOutboundForm=function(a,b){a=b(a.action);return a.hostname!=location.hostname&&"http"==a.protocol.slice(0,4)};X.prototype.remove=function(){this.c.j()};G("outboundFormTracker",X); +function Y(a,b){var c=this;J(a,H.Z);window.addEventListener&&(this.a=A({events:["click"],linkSelector:"a, area",shouldTrackOutboundLink:this.shouldTrackOutboundLink,fieldsObj:{},attributePrefix:"ga-"},b),this.c=a,this.f=this.f.bind(this),this.b={},this.a.events.forEach(function(a){c.b[a]=q(a,c.a.linkSelector,c.f)}))} +Y.prototype.f=function(a,b){var c=this;if(this.a.shouldTrackOutboundLink(b,u)){var d=b.getAttribute("href")||b.getAttribute("xlink:href"),e=u(d),e={transport:"beacon",eventCategory:"Outbound Link",eventAction:a.type,eventLabel:e.href},h=A({},this.a.fieldsObj,B(b,this.a.attributePrefix)),f=z(e,h,this.c,this.a.hitFilter,b,a);if(navigator.sendBeacon||"click"!=a.type||"_blank"==b.target||a.metaKey||a.ctrlKey||a.shiftKey||a.altKey||1=a.a.visibleThreshold&&(b=Math.round(b/1E3),d={transport:"beacon",nonInteraction:!0,eventCategory:"Page Visibility",eventAction:"track",eventValue:b,eventLabel:"(not set)"},c&&(d.queueTime=+new Date-c),a.a.visibleMetricIndex&&(d["metric"+a.a.visibleMetricIndex]=b),a.b.send("event",z(d,a.a.fieldsObj,a.b,a.a.hitFilter)))} +function Ta(a,b){var c=b?b:{};b=c.hitTime;var c=c.ea,d={transport:"beacon"};b&&(d.queueTime=+new Date-b);c&&a.a.pageLoadsMetricIndex&&(d["metric"+a.a.pageLoadsMetricIndex]=1);a.b.send("pageview",z(d,a.a.fieldsObj,a.b,a.a.hitFilter))}g.v=function(a){var b=this;return function(c,d){var e={},e=D(c)?c:(e[c]=d,e);e.page&&e.page!==b.b.get("page")&&"visible"==b.g&&b.s();a(c,d)}};g.N=function(a,b){a.time!=b.time&&(b.pageId!=Z||"visible"!=b.state||this.f.isExpired(b.sessionId)||Va(this,b,{hitTime:a.time}))}; +g.G=function(){"hidden"!=this.g&&this.s()};g.remove=function(){this.c.j();this.f.j();y(this.b,"set",this.v);window.removeEventListener("unload",this.G);document.removeEventListener("visibilitychange",this.s)};G("pageVisibilityTracker",Sa); +function Wa(a,b){J(a,H.aa);window.addEventListener&&(this.a=A({fieldsObj:{},hitFilter:null},b),this.b=a,this.u=this.u.bind(this),this.J=this.J.bind(this),this.D=this.D.bind(this),this.A=this.A.bind(this),this.B=this.B.bind(this),this.F=this.F.bind(this),"complete"!=document.readyState?window.addEventListener("load",this.u):this.u())}g=Wa.prototype; +g.u=function(){if(window.FB)try{window.FB.Event.subscribe("edge.create",this.B),window.FB.Event.subscribe("edge.remove",this.F)}catch(a){}window.twttr&&this.J()};g.J=function(){var a=this;try{window.twttr.ready(function(){window.twttr.events.bind("tweet",a.D);window.twttr.events.bind("follow",a.A)})}catch(b){}};function Xa(a){try{window.twttr.ready(function(){window.twttr.events.unbind("tweet",a.D);window.twttr.events.unbind("follow",a.A)})}catch(b){}} +g.D=function(a){if("tweet"==a.region){var b={transport:"beacon",socialNetwork:"Twitter",socialAction:"tweet",socialTarget:a.data.url||a.target.getAttribute("data-url")||location.href};this.b.send("social",z(b,this.a.fieldsObj,this.b,this.a.hitFilter,a.target,a))}}; +g.A=function(a){if("follow"==a.region){var b={transport:"beacon",socialNetwork:"Twitter",socialAction:"follow",socialTarget:a.data.screen_name||a.target.getAttribute("data-screen-name")};this.b.send("social",z(b,this.a.fieldsObj,this.b,this.a.hitFilter,a.target,a))}};g.B=function(a){this.b.send("social",z({transport:"beacon",socialNetwork:"Facebook",socialAction:"like",socialTarget:a},this.a.fieldsObj,this.b,this.a.hitFilter))}; +g.F=function(a){this.b.send("social",z({transport:"beacon",socialNetwork:"Facebook",socialAction:"unlike",socialTarget:a},this.a.fieldsObj,this.b,this.a.hitFilter))};g.remove=function(){window.removeEventListener("load",this.u);try{window.FB.Event.unsubscribe("edge.create",this.B),window.FB.Event.unsubscribe("edge.remove",this.F)}catch(a){}Xa(this)};G("socialWidgetTracker",Wa); +function Ya(a,b){J(a,H.ba);history.pushState&&window.addEventListener&&(this.a=A({shouldTrackUrlChange:this.shouldTrackUrlChange,trackReplaceState:!1,fieldsObj:{},hitFilter:null},b),this.b=a,this.c=location.pathname+location.search,this.H=this.H.bind(this),this.I=this.I.bind(this),this.C=this.C.bind(this),x(history,"pushState",this.H),x(history,"replaceState",this.I),window.addEventListener("popstate",this.C))}g=Ya.prototype; +g.H=function(a){var b=this;return function(c){for(var d=[],e=0;e} test A DOM element, a CSS\n * selector, or an array of DOM elements or CSS selectors to match against.\n * @return {boolean} True of any part of the test matches.\n */\nexport default function matches(element, test) {\n // Validate input.\n if (element && element.nodeType == 1 && test) {\n // if test is a string or DOM element test it.\n if (typeof test == 'string' || test.nodeType == 1) {\n return element == test ||\n matchesSelector(element, /** @type {string} */ (test));\n } else if ('length' in test) {\n // if it has a length property iterate over the items\n // and return true if any match.\n for (let i = 0, item; item = test[i]; i++) {\n if (element == item || matchesSelector(element, item)) return true;\n }\n }\n }\n // Still here? Return false\n return false;\n}\n\n\n/**\n * Tests whether a DOM element matches a selector. This polyfills the native\n * Element.prototype.matches method across browsers.\n * @param {!Element} element The DOM element to test.\n * @param {string} selector The CSS selector to test element against.\n * @return {boolean} True if the selector matches.\n */\nfunction matchesSelector(element, selector) {\n if (typeof selector != 'string') return false;\n if (nativeMatches) return nativeMatches.call(element, selector);\n const nodes = element.parentNode.querySelectorAll(selector);\n for (let i = 0, node; node = nodes[i]; i++) {\n if (node == element) return true;\n }\n return false;\n}\n",null,null,null,null,null,null,null,"/**\n * Returns an array of a DOM element's parent elements.\n * @param {!Element} element The DOM element whose parents to get.\n * @return {!Array} An array of all parent elemets, or an empty array if no\n * parent elements are found.\n */\nexport default function parents(element) {\n const list = [];\n while (element && element.parentNode && element.parentNode.nodeType == 1) {\n element = /** @type {!Element} */ (element.parentNode);\n list.push(element);\n }\n return list;\n}\n","import closest from './closest';\nimport matches from './matches';\n\n/**\n * Delegates the handling of events for an element matching a selector to an\n * ancestor of the matching element.\n * @param {!Node} ancestor The ancestor element to add the listener to.\n * @param {string} eventType The event type to listen to.\n * @param {string} selector A CSS selector to match against child elements.\n * @param {!Function} callback A function to run any time the event happens.\n * @param {Object=} opts A configuration options object. The available options:\n * - useCapture: If true, bind to the event capture phase.\n * - deep: If true, delegate into shadow trees.\n * @return {Object} The delegate object. It contains a destroy method.\n */\nexport default function delegate(\n ancestor, eventType, selector, callback, opts = {}) {\n // Defines the event listener.\n const listener = function(event) {\n let delegateTarget;\n\n // If opts.composed is true and the event originated from inside a Shadow\n // tree, check the composed path nodes.\n if (opts.composed && typeof event.composedPath == 'function') {\n const composedPath = event.composedPath();\n for (let i = 0, node; node = composedPath[i]; i++) {\n if (node.nodeType == 1 && matches(node, selector)) {\n delegateTarget = node;\n }\n }\n } else {\n // Otherwise check the parents.\n delegateTarget = closest(event.target, selector, true);\n }\n\n if (delegateTarget) {\n callback.call(delegateTarget, event, delegateTarget);\n }\n };\n\n ancestor.addEventListener(eventType, listener, opts.useCapture);\n\n return {\n destroy: function() {\n ancestor.removeEventListener(eventType, listener, opts.useCapture);\n },\n };\n}\n","import matches from './matches';\nimport parents from './parents';\n\n/**\n * Gets the closest parent element that matches the passed selector.\n * @param {Element} element The element whose parents to check.\n * @param {string} selector The CSS selector to match against.\n * @param {boolean=} shouldCheckSelf True if the selector should test against\n * the passed element itself.\n * @return {Element|undefined} The matching element or undefined.\n */\nexport default function closest(element, selector, shouldCheckSelf = false) {\n if (!(element && element.nodeType == 1 && selector)) return;\n const parentElements =\n (shouldCheckSelf ? [element] : []).concat(parents(element));\n\n for (let i = 0, parent; parent = parentElements[i]; i++) {\n if (matches(parent, selector)) return parent;\n }\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {delegate} from 'dom-utils';\nimport provide from '../provide';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj, getAttributeFields} from '../utilities';\n\n\n/**\n * Class for the `eventTracker` analytics.js plugin.\n * @implements {EventTrackerPublicInterface}\n */\nclass EventTracker {\n /**\n * Registers declarative event tracking.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?EventTrackerOpts} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.EVENT_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!window.addEventListener) return;\n\n /** @type {EventTrackerOpts} */\n const defaultOpts = {\n events: ['click'],\n fieldsObj: {},\n attributePrefix: 'ga-',\n // hitFilter: undefined,\n };\n\n this.opts = /** @type {EventTrackerOpts} */ (assign(defaultOpts, opts));\n\n this.tracker = tracker;\n\n // Binds methods.\n this.handleEvents = this.handleEvents.bind(this);\n\n const selector = '[' + this.opts.attributePrefix + 'on]';\n\n // Creates a mapping of events to their delegates\n this.delegates = {};\n this.opts.events.forEach((event) => {\n this.delegates[event] = delegate(document, event, selector,\n this.handleEvents, {composed: true, useCapture: true});\n });\n }\n\n /**\n * Handles all events on elements with event attributes.\n * @param {Event} event The DOM click event.\n * @param {Element} element The delegated DOM element target.\n */\n handleEvents(event, element) {\n const prefix = this.opts.attributePrefix;\n const events = element.getAttribute(prefix + 'on').split(/\\s*,\\s*/);\n\n // Ensures the type matches one of the events specified on the element.\n if (events.indexOf(event.type) < 0) return;\n\n /** @type {FieldsObj} */\n const defaultFields = {transport: 'beacon'};\n const attributeFields = getAttributeFields(element, prefix);\n const userFields = assign({}, this.opts.fieldsObj, attributeFields);\n const hitType = attributeFields.hitType || 'event';\n\n this.tracker.send(hitType, createFieldsObj(defaultFields,\n userFields, this.tracker, this.opts.hitFilter, element, event));\n }\n\n /**\n * Removes all event listeners and instance properties.\n */\n remove() {\n Object.keys(this.delegates).forEach((key) => {\n this.delegates[key].destroy();\n });\n }\n}\n\n\nprovide('eventTracker', EventTracker);\n","/**\n * Gets all attributes of an element as a plain JavaScriot object.\n * @param {Element} element The element whose attributes to get.\n * @return {!Object} An object whose keys are the attribute keys and whose\n * values are the attribute values. If no attributes exist, an empty\n * object is returned.\n */\nexport default function getAttributes(element) {\n const attrs = {};\n\n // Validate input.\n if (!(element && element.nodeType == 1)) return attrs;\n\n // Return an empty object if there are no attributes.\n const map = element.attributes;\n if (map.length === 0) return {};\n\n for (let i = 0, attr; attr = map[i]; i++) {\n attrs[attr.name] = attr.value;\n }\n return attrs;\n}\n","const HTTP_PORT = '80';\nconst HTTPS_PORT = '443';\nconst DEFAULT_PORT = RegExp(':(' + HTTP_PORT + '|' + HTTPS_PORT + ')$');\n\n\nconst a = document.createElement('a');\nconst cache = {};\n\n\n/**\n * Parses the given url and returns an object mimicing a `Location` object.\n * @param {string} url The url to parse.\n * @return {!Object} An object with the same properties as a `Location`.\n */\nexport default function parseUrl(url) {\n // All falsy values (as well as \".\") should map to the current URL.\n url = (!url || url == '.') ? location.href : url;\n\n if (cache[url]) return cache[url];\n\n a.href = url;\n\n // When parsing file relative paths (e.g. `../index.html`), IE will correctly\n // resolve the `href` property but will keep the `..` in the `path` property.\n // It will also not include the `host` or `hostname` properties. Furthermore,\n // IE will sometimes return no protocol or just a colon, especially for things\n // like relative protocol URLs (e.g. \"//google.com\").\n // To workaround all of these issues, we reparse with the full URL from the\n // `href` property.\n if (url.charAt(0) == '.' || url.charAt(0) == '/') return parseUrl(a.href);\n\n // Don't include default ports.\n let port = (a.port == HTTP_PORT || a.port == HTTPS_PORT) ? '' : a.port;\n\n // PhantomJS sets the port to \"0\" when using the file: protocol.\n port = port == '0' ? '' : port;\n\n // Sometimes IE incorrectly includes a port for default ports\n // (e.g. `:80` or `:443`) even when no port is specified in the URL.\n // http://bit.ly/1rQNoMg\n const host = a.host.replace(DEFAULT_PORT, '');\n\n // Not all browser support `origin` so we have to build it.\n const origin = a.origin ? a.origin : a.protocol + '//' + host;\n\n // Sometimes IE doesn't include the leading slash for pathname.\n // http://bit.ly/1rQNoMg\n const pathname = a.pathname.charAt(0) == '/' ? a.pathname : '/' + a.pathname;\n\n return cache[url] = {\n hash: a.hash,\n host: host,\n hostname: a.hostname,\n href: a.href,\n origin: origin,\n pathname: pathname,\n port: port,\n protocol: a.protocol,\n search: a.search,\n };\n}\n","/**\n * Copyright 2017 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\n/**\n * @fileoverview\n * The functions exported by this module make it easier (and safer) to override\n * foreign object methods (in a modular way) and respond to or modify their\n * invocation. The primary feature is the ability to override a method without\n * worrying if it's already been overridden somewhere else in the codebase. It\n * also allows for safe restoring of an overridden method by only fully\n * restoring a method once all overrides have been removed.\n */\n\n\nconst instances = [];\n\n\n/**\n * A class that wraps a foreign object method and emit events before and\n * after the original method is called.\n */\nexport default class MethodChain {\n /**\n * Adds the passed override method to the list of method chain overrides.\n * @param {!Object} context The object containing the method to chain.\n * @param {string} methodName The name of the method on the object.\n * @param {!Function} methodOverride The override method to add.\n */\n static add(context, methodName, methodOverride) {\n getOrCreateMethodChain(context, methodName).add(methodOverride);\n }\n\n /**\n * Removes a method chain added via `add()`. If the override is the\n * only override added, the original method is restored.\n * @param {!Object} context The object containing the method to unchain.\n * @param {string} methodName The name of the method on the object.\n * @param {!Function} methodOverride The override method to remove.\n */\n static remove(context, methodName, methodOverride) {\n getOrCreateMethodChain(context, methodName).remove(methodOverride);\n }\n\n /**\n * Wraps a foreign object method and overrides it. Also stores a reference\n * to the original method so it can be restored later.\n * @param {!Object} context The object containing the method.\n * @param {string} methodName The name of the method on the object.\n */\n constructor(context, methodName) {\n this.context = context;\n this.methodName = methodName;\n this.isTask = /Task$/.test(methodName);\n\n this.originalMethodReference = this.isTask ?\n context.get(methodName) : context[methodName];\n\n this.methodChain = [];\n this.boundMethodChain = [];\n\n // Wraps the original method.\n this.wrappedMethod = (...args) => {\n const lastBoundMethod =\n this.boundMethodChain[this.boundMethodChain.length - 1];\n\n return lastBoundMethod(...args);\n };\n\n // Override original method with the wrapped one.\n if (this.isTask) {\n context.set(methodName, this.wrappedMethod);\n } else {\n context[methodName] = this.wrappedMethod;\n }\n }\n\n /**\n * Adds a method to the method chain.\n * @param {!Function} overrideMethod The override method to add.\n */\n add(overrideMethod) {\n this.methodChain.push(overrideMethod);\n this.rebindMethodChain();\n }\n\n /**\n * Removes a method from the method chain and restores the prior order.\n * @param {!Function} overrideMethod The override method to remove.\n */\n remove(overrideMethod) {\n const index = this.methodChain.indexOf(overrideMethod);\n if (index > -1) {\n this.methodChain.splice(index, 1);\n if (this.methodChain.length > 0) {\n this.rebindMethodChain();\n } else {\n this.destroy();\n }\n }\n }\n\n /**\n * Loops through the method chain array and recreates the bound method\n * chain array. This is necessary any time a method is added or removed\n * to ensure proper original method context and order.\n */\n rebindMethodChain() {\n this.boundMethodChain = [];\n for (let method, i = 0; method = this.methodChain[i]; i++) {\n const previousMethod = this.boundMethodChain[i - 1] ||\n this.originalMethodReference.bind(this.context);\n this.boundMethodChain.push(method(previousMethod));\n }\n }\n\n /**\n * Calls super and destroys the instance if no registered handlers remain.\n */\n destroy() {\n const index = instances.indexOf(this);\n if (index > -1) {\n instances.splice(index, 1);\n if (this.isTask) {\n this.context.set(this.methodName, this.originalMethodReference);\n } else {\n this.context[this.methodName] = this.originalMethodReference;\n }\n }\n }\n}\n\n\n/**\n * Gets a MethodChain instance for the passed object and method. If the method\n * has already been wrapped via an existing MethodChain instance, that\n * instance is returned.\n * @param {!Object} context The object containing the method.\n * @param {string} methodName The name of the method on the object.\n * @return {!MethodChain}\n */\nfunction getOrCreateMethodChain(context, methodName) {\n let methodChain = instances\n .filter((h) => h.context == context && h.methodName == methodName)[0];\n\n if (!methodChain) {\n methodChain = new MethodChain(context, methodName);\n instances.push(methodChain);\n }\n return methodChain;\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {getAttributes} from 'dom-utils';\nimport MethodChain from './method-chain';\n\n\n/**\n * Accepts default and user override fields and an optional tracker, hit\n * filter, and target element and returns a single object that can be used in\n * `ga('send', ...)` commands.\n * @param {FieldsObj} defaultFields The default fields to return.\n * @param {FieldsObj} userFields Fields set by the user to override the\n * defaults.\n * @param {Tracker=} tracker The tracker object to apply the hit filter to.\n * @param {Function=} hitFilter A filter function that gets\n * called with the tracker model right before the `buildHitTask`. It can\n * be used to modify the model for the current hit only.\n * @param {Element=} target If the hit originated from an interaction\n * with a DOM element, hitFilter is invoked with that element as the\n * second argument.\n * @param {(Event|TwttrEvent)=} event If the hit originated via a DOM event,\n * hitFilter is invoked with that event as the third argument.\n * @return {!FieldsObj} The final fields object.\n */\nexport function createFieldsObj(\n defaultFields, userFields, tracker = undefined,\n hitFilter = undefined, target = undefined, event = undefined) {\n if (typeof hitFilter == 'function') {\n const originalBuildHitTask = tracker.get('buildHitTask');\n return {\n buildHitTask: (/** @type {!Model} */ model) => {\n model.set(defaultFields, null, true);\n model.set(userFields, null, true);\n hitFilter(model, target, event);\n originalBuildHitTask(model);\n },\n };\n } else {\n return assign({}, defaultFields, userFields);\n }\n}\n\n\n/**\n * Retrieves the attributes from an DOM element and returns a fields object\n * for all attributes matching the passed prefix string.\n * @param {Element} element The DOM element to get attributes from.\n * @param {string} prefix An attribute prefix. Only the attributes matching\n * the prefix will be returned on the fields object.\n * @return {FieldsObj} An object of analytics.js fields and values\n */\nexport function getAttributeFields(element, prefix) {\n const attributes = getAttributes(element);\n const attributeFields = {};\n\n Object.keys(attributes).forEach(function(attribute) {\n // The `on` prefix is used for event handling but isn't a field.\n if (attribute.indexOf(prefix) === 0 && attribute != prefix + 'on') {\n let value = attributes[attribute];\n\n // Detects Boolean value strings.\n if (value == 'true') value = true;\n if (value == 'false') value = false;\n\n const field = camelCase(attribute.slice(prefix.length));\n attributeFields[field] = value;\n }\n });\n\n return attributeFields;\n}\n\n\n/**\n * Accepts a function to be invoked once the DOM is ready. If the DOM is\n * already ready, the callback is invoked immediately.\n * @param {!Function} callback The ready callback.\n */\nexport function domReady(callback) {\n if (document.readyState == 'loading') {\n document.addEventListener('DOMContentLoaded', function fn() {\n document.removeEventListener('DOMContentLoaded', fn);\n callback();\n });\n } else {\n callback();\n }\n}\n\n\n/**\n * Returns a function, that, as long as it continues to be called, will not\n * actually run. The function will only run after it stops being called for\n * `wait` milliseconds.\n * @param {!Function} fn The function to debounce.\n * @param {number} wait The debounce wait timeout in ms.\n * @return {!Function} The debounced function.\n */\nexport function debounce(fn, wait) {\n let timeout;\n return function(...args) {\n clearTimeout(timeout);\n timeout = setTimeout(() => fn(...args), wait);\n };\n}\n\n\n/**\n * Accepts a function and returns a wrapped version of the function that is\n * expected to be called elsewhere in the system. If it's not called\n * elsewhere after the timeout period, it's called regardless. The wrapper\n * function also prevents the callback from being called more than once.\n * @param {!Function} callback The function to call.\n * @param {number=} wait How many milliseconds to wait before invoking\n * the callback.\n * @return {!Function} The wrapped version of the passed function.\n */\nexport function withTimeout(callback, wait = 2000) {\n let called = false;\n const fn = function() {\n if (!called) {\n called = true;\n callback();\n }\n };\n setTimeout(fn, wait);\n return fn;\n}\n\n// Maps trackers to queue by tracking ID.\nconst queueMap = {};\n\n/**\n * Queues a function for execution in the next call stack, or immediately\n * before any send commands are executed on the tracker. This allows\n * autotrack plugins to defer running commands until after all other plugins\n * are required but before any other hits are sent.\n * @param {!Tracker} tracker\n * @param {!Function} fn\n */\nexport function deferUntilPluginsLoaded(tracker, fn) {\n const trackingId = tracker.get('trackingId');\n const ref = queueMap[trackingId] = queueMap[trackingId] || {};\n\n const processQueue = () => {\n clearTimeout(ref.timeout);\n if (ref.send) {\n MethodChain.remove(tracker, 'send', ref.send);\n }\n delete queueMap[trackingId];\n\n ref.queue.forEach((fn) => fn());\n };\n\n clearTimeout(ref.timeout);\n ref.timeout = setTimeout(processQueue, 0);\n ref.queue = ref.queue || [];\n ref.queue.push(fn);\n\n if (!ref.send) {\n ref.send = (originalMethod) => {\n return (...args) => {\n processQueue();\n originalMethod(...args);\n };\n };\n MethodChain.add(tracker, 'send', ref.send);\n }\n}\n\n\n/**\n * A small shim of Object.assign that aims for brevity over spec-compliant\n * handling all the edge cases.\n * @param {!Object} target The target object to assign to.\n * @param {...?Object} sources Additional objects who properties should be\n * assigned to target. Non-objects are converted to objects.\n * @return {!Object} The modified target object.\n */\nexport const assign = Object.assign || function(target, ...sources) {\n for (let i = 0, len = sources.length; i < len; i++) {\n const source = Object(sources[i]);\n for (let key in source) {\n if (Object.prototype.hasOwnProperty.call(source, key)) {\n target[key] = source[key];\n }\n }\n }\n return target;\n};\n\n\n/**\n * Accepts a string containing hyphen or underscore word separators and\n * converts it to camelCase.\n * @param {string} str The string to camelCase.\n * @return {string} The camelCased version of the string.\n */\nexport function camelCase(str) {\n return str.replace(/[\\-\\_]+(\\w?)/g, function(match, p1) {\n return p1.toUpperCase();\n });\n}\n\n\n/**\n * Capitalizes the first letter of a string.\n * @param {string} str The input string.\n * @return {string} The capitalized string\n */\nexport function capitalize(str) {\n return str.charAt(0).toUpperCase() + str.slice(1);\n}\n\n\n/**\n * Indicates whether the passed variable is a JavaScript object.\n * @param {*} value The input variable to test.\n * @return {boolean} Whether or not the test is an object.\n */\nexport function isObject(value) {\n return typeof value == 'object' && value !== null;\n}\n\n\n/**\n * Accepts a value that may or may not be an array. If it is not an array,\n * it is returned as the first item in a single-item array.\n * @param {*} value The value to convert to an array if it is not.\n * @return {!Array} The array-ified value.\n */\nexport function toArray(value) {\n return Array.isArray(value) ? value : [value];\n}\n\n\n/**\n * @return {number} The current date timestamp\n */\nexport function now() {\n return +new Date();\n}\n\n\n/*eslint-disable */\n// https://gist.github.com/jed/982883\n/** @param {?=} a */\nexport const uuid = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)};\n/*eslint-enable */\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {DEV_ID} from './constants';\nimport {capitalize} from './utilities';\n\n\n/**\n * Provides a plugin for use with analytics.js, accounting for the possibility\n * that the global command queue has been renamed or not yet defined.\n * @param {string} pluginName The plugin name identifier.\n * @param {Function} pluginConstructor The plugin constructor function.\n */\nexport default function provide(pluginName, pluginConstructor) {\n const gaAlias = window.GoogleAnalyticsObject || 'ga';\n window[gaAlias] = window[gaAlias] || function(...args) {\n (window[gaAlias].q = window[gaAlias].q || []).push(args);\n };\n\n // Adds the autotrack dev ID if not already included.\n window.gaDevIds = window.gaDevIds || [];\n if (window.gaDevIds.indexOf(DEV_ID) < 0) {\n window.gaDevIds.push(DEV_ID);\n }\n\n // Formally provides the plugin for use with analytics.js.\n window[gaAlias]('provide', pluginName, pluginConstructor);\n\n // Registers the plugin on the global gaplugins object.\n window.gaplugins = window.gaplugins || {};\n window.gaplugins[capitalize(pluginName)] = pluginConstructor;\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nexport const VERSION = '2.3.2';\nexport const DEV_ID = 'i5iSjo';\n\nexport const VERSION_PARAM = '_av';\nexport const USAGE_PARAM = '_au';\n\nexport const NULL_DIMENSION = '(not set)';\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {USAGE_PARAM, VERSION, VERSION_PARAM} from './constants';\n\n\nexport const plugins = {\n CLEAN_URL_TRACKER: 1,\n EVENT_TRACKER: 2,\n IMPRESSION_TRACKER: 3,\n MEDIA_QUERY_TRACKER: 4,\n OUTBOUND_FORM_TRACKER: 5,\n OUTBOUND_LINK_TRACKER: 6,\n PAGE_VISIBILITY_TRACKER: 7,\n SOCIAL_WIDGET_TRACKER: 8,\n URL_CHANGE_TRACKER: 9,\n MAX_SCROLL_TRACKER: 10,\n};\n\n\nconst PLUGIN_COUNT = Object.keys(plugins).length;\n\n\n/**\n * Tracks the usage of the passed plugin by encoding a value into a usage\n * string sent with all hits for the passed tracker.\n * @param {!Tracker} tracker The analytics.js tracker object.\n * @param {number} plugin The plugin enum.\n */\nexport function trackUsage(tracker, plugin) {\n trackVersion(tracker);\n trackPlugin(tracker, plugin);\n}\n\n\n/**\n * Converts a hexadecimal string to a binary string.\n * @param {string} hex A hexadecimal numeric string.\n * @return {string} a binary numeric string.\n */\nfunction convertHexToBin(hex) {\n return parseInt(hex || '0', 16).toString(2);\n}\n\n\n/**\n * Converts a binary string to a hexadecimal string.\n * @param {string} bin A binary numeric string.\n * @return {string} a hexadecimal numeric string.\n */\nfunction convertBinToHex(bin) {\n return parseInt(bin || '0', 2).toString(16);\n}\n\n\n/**\n * Adds leading zeros to a string if it's less than a minimum length.\n * @param {string} str A string to pad.\n * @param {number} len The minimum length of the string\n * @return {string} The padded string.\n */\nfunction padZeros(str, len) {\n if (str.length < len) {\n let toAdd = len - str.length;\n while (toAdd) {\n str = '0' + str;\n toAdd--;\n }\n }\n return str;\n}\n\n\n/**\n * Accepts a binary numeric string and flips the digit from 0 to 1 at the\n * specified index.\n * @param {string} str The binary numeric string.\n * @param {number} index The index to flip the bit.\n * @return {string} The new binary string with the bit flipped on\n */\nfunction flipBitOn(str, index) {\n return str.substr(0, index) + 1 + str.substr(index + 1);\n}\n\n\n/**\n * Accepts a tracker and a plugin index and flips the bit at the specified\n * index on the tracker's usage parameter.\n * @param {Object} tracker An analytics.js tracker.\n * @param {number} pluginIndex The index of the plugin in the global list.\n */\nfunction trackPlugin(tracker, pluginIndex) {\n const usageHex = tracker.get('&' + USAGE_PARAM);\n let usageBin = padZeros(convertHexToBin(usageHex), PLUGIN_COUNT);\n\n // Flip the bit of the plugin being tracked.\n usageBin = flipBitOn(usageBin, PLUGIN_COUNT - pluginIndex);\n\n // Stores the modified usage string back on the tracker.\n tracker.set('&' + USAGE_PARAM, convertBinToHex(usageBin));\n}\n\n\n/**\n * Accepts a tracker and adds the current version to the version param.\n * @param {Object} tracker An analytics.js tracker.\n */\nfunction trackVersion(tracker) {\n tracker.set('&' + VERSION_PARAM, VERSION);\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {parseUrl} from 'dom-utils';\nimport {NULL_DIMENSION} from '../constants';\nimport MethodChain from '../method-chain';\nimport provide from '../provide';\nimport {plugins, trackUsage} from '../usage';\nimport {assign} from '../utilities';\n\n\n/**\n * Class for the `cleanUrlTracker` analytics.js plugin.\n * @implements {CleanUrlTrackerPublicInterface}\n */\nclass CleanUrlTracker {\n /**\n * Registers clean URL tracking on a tracker object. The clean URL tracker\n * removes query parameters from the page value reported to Google Analytics.\n * It also helps to prevent tracking similar URLs, e.g. sometimes ending a\n * URL with a slash and sometimes not.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?CleanUrlTrackerOpts} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.CLEAN_URL_TRACKER);\n\n /** @type {CleanUrlTrackerOpts} */\n const defaultOpts = {\n // stripQuery: undefined,\n // queryDimensionIndex: undefined,\n // indexFilename: undefined,\n // trailingSlash: undefined,\n // urlFilter: undefined,\n };\n this.opts = /** @type {CleanUrlTrackerOpts} */ (assign(defaultOpts, opts));\n\n this.tracker = tracker;\n\n /** @type {string|null} */\n this.queryDimension = this.opts.stripQuery &&\n this.opts.queryDimensionIndex ?\n `dimension${this.opts.queryDimensionIndex}` : null;\n\n // Binds methods to `this`.\n this.trackerGetOverride = this.trackerGetOverride.bind(this);\n this.buildHitTaskOverride = this.buildHitTaskOverride.bind(this);\n\n // Override built-in tracker method to watch for changes.\n MethodChain.add(tracker, 'get', this.trackerGetOverride);\n MethodChain.add(tracker, 'buildHitTask', this.buildHitTaskOverride);\n }\n\n /**\n * Ensures reads of the tracker object by other plugins always see the\n * \"cleaned\" versions of all URL fields.\n * @param {function(string):*} originalMethod A reference to the overridden\n * method.\n * @return {function(string):*}\n */\n trackerGetOverride(originalMethod) {\n return (field) => {\n if (field == 'page' || field == this.queryDimension) {\n const fieldsObj = /** @type {!FieldsObj} */ ({\n location: originalMethod('location'),\n page: originalMethod('page'),\n });\n const cleanedFieldsObj = this.cleanUrlFields(fieldsObj);\n return cleanedFieldsObj[field];\n } else {\n return originalMethod(field);\n }\n };\n }\n\n /**\n * Cleans URL fields passed in a send command.\n * @param {function(!Model)} originalMethod A reference to the\n * overridden method.\n * @return {function(!Model)}\n */\n buildHitTaskOverride(originalMethod) {\n return (model) => {\n const cleanedFieldsObj = this.cleanUrlFields({\n location: model.get('location'),\n page: model.get('page'),\n });\n model.set(cleanedFieldsObj, null, true);\n originalMethod(model);\n };\n }\n\n /**\n * Accepts of fields object containing URL fields and returns a new\n * fields object with the URLs \"cleaned\" according to the tracker options.\n * @param {!FieldsObj} fieldsObj\n * @return {!FieldsObj}\n */\n cleanUrlFields(fieldsObj) {\n const url = parseUrl(\n /** @type {string} */ (fieldsObj.page || fieldsObj.location));\n\n let pathname = url.pathname;\n\n // If an index filename was provided, remove it if it appears at the end\n // of the URL.\n if (this.opts.indexFilename) {\n const parts = pathname.split('/');\n if (this.opts.indexFilename == parts[parts.length - 1]) {\n parts[parts.length - 1] = '';\n pathname = parts.join('/');\n }\n }\n\n // Ensure the URL ends with or doesn't end with slash based on the\n // `trailingSlash` option. Note that filename URLs should never contain\n // a trailing slash.\n if (this.opts.trailingSlash == 'remove') {\n pathname = pathname.replace(/\\/+$/, '');\n } else if (this.opts.trailingSlash == 'add') {\n const isFilename = /\\.\\w+$/.test(pathname);\n if (!isFilename && pathname.substr(-1) != '/') {\n pathname = pathname + '/';\n }\n }\n\n /** @type {!FieldsObj} */\n const cleanedFieldsObj = {\n page: pathname + (!this.opts.stripQuery ? url.search : ''),\n };\n if (fieldsObj.location) {\n cleanedFieldsObj.location = fieldsObj.location;\n }\n if (this.queryDimension) {\n cleanedFieldsObj[this.queryDimension] =\n url.search.slice(1) || NULL_DIMENSION;\n }\n\n // Apply the `urlFieldsFilter()` option if passed.\n if (typeof this.opts.urlFieldsFilter == 'function') {\n /** @type {!FieldsObj} */\n const userCleanedFieldsObj =\n this.opts.urlFieldsFilter(cleanedFieldsObj, parseUrl);\n\n // Ensure only the URL fields are returned.\n return {\n page: userCleanedFieldsObj.page,\n location: userCleanedFieldsObj.location,\n [this.queryDimension]: userCleanedFieldsObj[this.queryDimension],\n };\n } else {\n return cleanedFieldsObj;\n }\n }\n\n /**\n * Restores all overridden tasks and methods.\n */\n remove() {\n MethodChain.remove(this.tracker, 'get', this.trackerGetOverride);\n MethodChain.remove(this.tracker, 'buildHitTask', this.buildHitTaskOverride);\n }\n}\n\n\nprovide('cleanUrlTracker', CleanUrlTracker);\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport provide from '../provide';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj,\n domReady, getAttributeFields} from '../utilities';\n\n\n/**\n * Class for the `impressionTracker` analytics.js plugin.\n * @implements {ImpressionTrackerPublicInterface}\n */\nclass ImpressionTracker {\n /**\n * Registers impression tracking.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?ImpressionTrackerOpts} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.IMPRESSION_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!(window.IntersectionObserver && window.MutationObserver)) return;\n\n /** type {ImpressionTrackerOpts} */\n const defaultOptions = {\n // elements: undefined,\n rootMargin: '0px',\n fieldsObj: {},\n attributePrefix: 'ga-',\n // hitFilter: undefined,\n };\n\n this.opts = /** type {ImpressionTrackerOpts} */ (\n assign(defaultOptions, opts));\n\n this.tracker = tracker;\n\n // Binds methods.\n this.handleDomMutations = this.handleDomMutations.bind(this);\n this.handleIntersectionChanges = this.handleIntersectionChanges.bind(this);\n this.handleDomElementAdded = this.handleDomElementAdded.bind(this);\n this.handleDomElementRemoved = this.handleDomElementRemoved.bind(this);\n\n /** @type {MutationObserver} */\n this.mutationObserver = null;\n\n // The primary list of elements to observe. Each item contains the\n // element ID, threshold, and whether it's currently in-view.\n this.items = [];\n\n // A map of element IDs in the `items` array to DOM elements in the\n // document. The presence of a key indicates that the element ID is in the\n // `items` array, and the presence of an element value indicates that the\n // element is in the DOM.\n this.elementMap = {};\n\n // A map of threshold values. Each threshold is mapped to an\n // IntersectionObserver instance specific to that threshold.\n this.thresholdMap = {};\n\n // Once the DOM is ready, start observing for changes (if present).\n domReady(() => {\n if (this.opts.elements) {\n this.observeElements(this.opts.elements);\n }\n });\n }\n\n /**\n * Starts observing the passed elements for impressions.\n * @param {Array} elements\n */\n observeElements(elements) {\n const data = this.deriveDataFromElements(elements);\n\n // Merge the new data with the data already on the plugin instance.\n this.items = this.items.concat(data.items);\n this.elementMap = assign({}, data.elementMap, this.elementMap);\n this.thresholdMap = assign({}, data.thresholdMap, this.thresholdMap);\n\n // Observe each new item.\n data.items.forEach((item) => {\n const observer = this.thresholdMap[item.threshold] =\n (this.thresholdMap[item.threshold] || new IntersectionObserver(\n this.handleIntersectionChanges, {\n rootMargin: this.opts.rootMargin,\n threshold: [+item.threshold],\n }));\n\n const element = this.elementMap[item.id] ||\n (this.elementMap[item.id] = document.getElementById(item.id));\n\n if (element) {\n observer.observe(element);\n }\n });\n\n if (!this.mutationObserver) {\n this.mutationObserver = new MutationObserver(this.handleDomMutations);\n this.mutationObserver.observe(document.body, {\n childList: true,\n subtree: true,\n });\n }\n\n // TODO(philipwalton): Remove temporary hack to force a new frame\n // immediately after adding observers.\n // https://bugs.chromium.org/p/chromium/issues/detail?id=612323\n requestAnimationFrame(() => {});\n }\n\n /**\n * Stops observing the passed elements for impressions.\n * @param {Array} elements\n * @return {undefined}\n */\n unobserveElements(elements) {\n const itemsToKeep = [];\n const itemsToRemove = [];\n\n this.items.forEach((item) => {\n const itemInItems = elements.some((element) => {\n const itemToRemove = getItemFromElement(element);\n return itemToRemove.id === item.id &&\n itemToRemove.threshold === item.threshold &&\n itemToRemove.trackFirstImpressionOnly ===\n item.trackFirstImpressionOnly;\n });\n if (itemInItems) {\n itemsToRemove.push(item);\n } else {\n itemsToKeep.push(item);\n }\n });\n\n // If there are no items to keep, run the `unobserveAllElements` logic.\n if (!itemsToKeep.length) {\n this.unobserveAllElements();\n } else {\n const dataToKeep = this.deriveDataFromElements(itemsToKeep);\n const dataToRemove = this.deriveDataFromElements(itemsToRemove);\n\n this.items = dataToKeep.items;\n this.elementMap = dataToKeep.elementMap;\n this.thresholdMap = dataToKeep.thresholdMap;\n\n // Unobserve removed elements.\n itemsToRemove.forEach((item) => {\n if (!dataToKeep.elementMap[item.id]) {\n const observer = dataToRemove.thresholdMap[item.threshold];\n const element = dataToRemove.elementMap[item.id];\n\n if (element) {\n observer.unobserve(element);\n }\n\n // Disconnect unneeded threshold observers.\n if (!dataToKeep.thresholdMap[item.threshold]) {\n dataToRemove.thresholdMap[item.threshold].disconnect();\n }\n }\n });\n }\n }\n\n /**\n * Stops observing all currently observed elements.\n */\n unobserveAllElements() {\n Object.keys(this.thresholdMap).forEach((key) => {\n this.thresholdMap[key].disconnect();\n });\n\n this.mutationObserver.disconnect();\n this.mutationObserver = null;\n\n this.items = [];\n this.elementMap = {};\n this.thresholdMap = {};\n }\n\n /**\n * Loops through each of the passed elements and creates a map of element IDs,\n * threshold values, and a list of \"items\" (which contains each element's\n * `threshold` and `trackFirstImpressionOnly` property).\n * @param {Array} elements A list of elements to derive item data from.\n * @return {Object} An object with the properties `items`, `elementMap`\n * and `threshold`.\n */\n deriveDataFromElements(elements) {\n const items = [];\n const thresholdMap = {};\n const elementMap = {};\n\n if (elements.length) {\n elements.forEach((element) => {\n const item = getItemFromElement(element);\n\n items.push(item);\n elementMap[item.id] = this.elementMap[item.id] || null;\n thresholdMap[item.threshold] =\n this.thresholdMap[item.threshold] || null;\n });\n }\n\n return {items, elementMap, thresholdMap};\n }\n\n /**\n * Handles nodes being added or removed from the DOM. This function is passed\n * as the callback to `this.mutationObserver`.\n * @param {Array} mutations A list of `MutationRecord` instances\n */\n handleDomMutations(mutations) {\n for (let i = 0, mutation; mutation = mutations[i]; i++) {\n // Handles removed elements.\n for (let k = 0, removedEl; removedEl = mutation.removedNodes[k]; k++) {\n this.walkNodeTree(removedEl, this.handleDomElementRemoved);\n }\n // Handles added elements.\n for (let j = 0, addedEl; addedEl = mutation.addedNodes[j]; j++) {\n this.walkNodeTree(addedEl, this.handleDomElementAdded);\n }\n }\n }\n\n /**\n * Iterates through all descendents of a DOM node and invokes the passed\n * callback if any of them match an elememt in `elementMap`.\n * @param {Node} node The DOM node to walk.\n * @param {Function} callback A function to be invoked if a match is found.\n */\n walkNodeTree(node, callback) {\n if (node.nodeType == 1 && node.id in this.elementMap) {\n callback(node.id);\n }\n for (let i = 0, child; child = node.childNodes[i]; i++) {\n this.walkNodeTree(child, callback);\n }\n }\n\n /**\n * Handles intersection changes. This function is passed as the callback to\n * `this.intersectionObserver`\n * @param {Array} records A list of `IntersectionObserverEntry` records.\n */\n handleIntersectionChanges(records) {\n const itemsToRemove = [];\n for (let i = 0, record; record = records[i]; i++) {\n for (let j = 0, item; item = this.items[j]; j++) {\n if (record.target.id !== item.id) continue;\n\n if (isTargetVisible(item.threshold, record)) {\n this.handleImpression(item.id);\n\n if (item.trackFirstImpressionOnly) {\n itemsToRemove.push(item);\n }\n }\n }\n }\n if (itemsToRemove.length) {\n this.unobserveElements(itemsToRemove);\n }\n }\n\n /**\n * Sends a hit to Google Analytics with the impression data.\n * @param {string} id The ID of the element making the impression.\n */\n handleImpression(id) {\n const element = document.getElementById(id);\n\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n eventCategory: 'Viewport',\n eventAction: 'impression',\n eventLabel: id,\n nonInteraction: true,\n };\n\n /** @type {FieldsObj} */\n const userFields = assign({}, this.opts.fieldsObj,\n getAttributeFields(element, this.opts.attributePrefix));\n\n this.tracker.send('event', createFieldsObj(defaultFields,\n userFields, this.tracker, this.opts.hitFilter, element));\n }\n\n /**\n * Handles an element in the items array being added to the DOM.\n * @param {string} id The ID of the element that was added.\n */\n handleDomElementAdded(id) {\n const element = this.elementMap[id] = document.getElementById(id);\n this.items.forEach((item) => {\n if (id == item.id) {\n this.thresholdMap[item.threshold].observe(element);\n }\n });\n }\n\n /**\n * Handles an element currently being observed for intersections being\n * removed from the DOM.\n * @param {string} id The ID of the element that was removed.\n */\n handleDomElementRemoved(id) {\n const element = this.elementMap[id];\n this.items.forEach((item) => {\n if (id == item.id) {\n this.thresholdMap[item.threshold].unobserve(element);\n }\n });\n\n this.elementMap[id] = null;\n }\n\n /**\n * Removes all listeners and observers.\n * @private\n */\n remove() {\n this.unobserveAllElements();\n }\n}\n\n\nprovide('impressionTracker', ImpressionTracker);\n\n\n/**\n * Detects whether or not an intersection record represents a visible target\n * given a particular threshold.\n * @param {number} threshold The threshold the target is visible above.\n * @param {IntersectionObserverEntry} record The most recent record entry.\n * @return {boolean} True if the target is visible.\n */\nfunction isTargetVisible(threshold, record) {\n if (threshold === 0) {\n const i = record.intersectionRect;\n return i.top > 0 || i.bottom > 0 || i.left > 0 || i.right > 0;\n } else {\n return record.intersectionRatio >= threshold;\n }\n}\n\n\n/**\n * Creates an item by merging the passed element with the item defaults.\n * If the passed element is just a string, that string is treated as\n * the item ID.\n * @param {!ImpressionTrackerElementsItem|string} element The element to\n * convert to an item.\n * @return {!ImpressionTrackerElementsItem} The item object.\n */\nfunction getItemFromElement(element) {\n /** @type {ImpressionTrackerElementsItem} */\n const defaultOpts = {\n threshold: 0,\n trackFirstImpressionOnly: true,\n };\n\n if (typeof element == 'string') {\n element = /** @type {!ImpressionTrackerElementsItem} */ ({id: element});\n }\n\n return assign(defaultOpts, element);\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\n/**\n * An simple reimplementation of the native Node.js EventEmitter class.\n * The goal of this implementation is to be as small as possible.\n */\nexport default class EventEmitter {\n /**\n * Creates the event registry.\n */\n constructor() {\n this.registry_ = {};\n }\n\n /**\n * Adds a handler function to the registry for the passed event.\n * @param {string} event The event name.\n * @param {!Function} fn The handler to be invoked when the passed\n * event is emitted.\n */\n on(event, fn) {\n this.getRegistry_(event).push(fn);\n }\n\n /**\n * Removes a handler function from the registry for the passed event.\n * @param {string=} event The event name.\n * @param {Function=} fn The handler to be removed.\n */\n off(event = undefined, fn = undefined) {\n if (event && fn) {\n const eventRegistry = this.getRegistry_(event);\n const handlerIndex = eventRegistry.indexOf(fn);\n if (handlerIndex > -1) {\n eventRegistry.splice(handlerIndex, 1);\n }\n } else {\n this.registry_ = {};\n }\n }\n\n /**\n * Runs all registered handlers for the passed event with the optional args.\n * @param {string} event The event name.\n * @param {...*} args The arguments to be passed to the handler.\n */\n emit(event, ...args) {\n this.getRegistry_(event).forEach((fn) => fn(...args));\n }\n\n /**\n * Returns the total number of event handlers currently registered.\n * @return {number}\n */\n getEventCount() {\n let eventCount = 0;\n Object.keys(this.registry_).forEach((event) => {\n eventCount += this.getRegistry_(event).length;\n });\n return eventCount;\n }\n\n /**\n * Returns an array of handlers associated with the passed event name.\n * If no handlers have been registered, an empty array is returned.\n * @private\n * @param {string} event The event name.\n * @return {!Array} An array of handler functions.\n */\n getRegistry_(event) {\n return this.registry_[event] = (this.registry_[event] || []);\n }\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport EventEmitter from './event-emitter';\nimport {assign} from './utilities';\n\n\nconst AUTOTRACK_PREFIX = 'autotrack';\nconst instances = {};\nlet isListening = false;\n\n\n/** @type {boolean|undefined} */\nlet browserSupportsLocalStorage;\n\n\n/**\n * A storage object to simplify interacting with localStorage.\n */\nexport default class Store extends EventEmitter {\n /**\n * Gets an existing instance for the passed arguements or creates a new\n * instance if one doesn't exist.\n * @param {string} trackingId The tracking ID for the GA property.\n * @param {string} namespace A namespace unique to this store.\n * @param {Object=} defaults An optional object of key/value defaults.\n * @return {Store} The Store instance.\n */\n static getOrCreate(trackingId, namespace, defaults) {\n const key = [AUTOTRACK_PREFIX, trackingId, namespace].join(':');\n\n // Don't create multiple instances for the same tracking Id and namespace.\n if (!instances[key]) {\n instances[key] = new Store(key, defaults);\n if (!isListening) initStorageListener();\n }\n return instances[key];\n }\n\n /**\n * Returns true if the browser supports and can successfully write to\n * localStorage. The results is cached so this method can be invoked many\n * times with no extra performance cost.\n * @private\n * @return {boolean}\n */\n static isSupported_() {\n if (browserSupportsLocalStorage != null) {\n return browserSupportsLocalStorage;\n }\n\n try {\n window.localStorage.setItem(AUTOTRACK_PREFIX, AUTOTRACK_PREFIX);\n window.localStorage.removeItem(AUTOTRACK_PREFIX);\n browserSupportsLocalStorage = true;\n } catch (err) {\n browserSupportsLocalStorage = false;\n }\n return browserSupportsLocalStorage;\n }\n\n /**\n * Wraps the native localStorage method for each stubbing in tests.\n * @private\n * @param {string} key The store key.\n * @return {string|null} The stored value.\n */\n static get_(key) {\n return window.localStorage.getItem(key);\n }\n\n /**\n * Wraps the native localStorage method for each stubbing in tests.\n * @private\n * @param {string} key The store key.\n * @param {string} value The value to store.\n */\n static set_(key, value) {\n window.localStorage.setItem(key, value);\n }\n\n /**\n * Wraps the native localStorage method for each stubbing in tests.\n * @private\n * @param {string} key The store key.\n */\n static clear_(key) {\n window.localStorage.removeItem(key);\n }\n\n /**\n * @param {string} key A key unique to this store.\n * @param {Object=} defaults An optional object of key/value defaults.\n */\n constructor(key, defaults = {}) {\n super();\n this.key_ = key;\n this.defaults_ = defaults;\n\n /** @type {?Object} */\n this.cache_ = null; // Will be set after the first get.\n }\n\n /**\n * Gets the data stored in localStorage for this store. If the cache is\n * already populated, return it as is (since it's always kept up-to-date\n * and in sync with activity in other windows via the `storage` event).\n * TODO(philipwalton): Implement schema migrations if/when a new\n * schema version is introduced.\n * @return {!Object} The stored data merged with the defaults.\n */\n get() {\n if (this.cache_) {\n return this.cache_;\n } else {\n if (Store.isSupported_()) {\n try {\n this.cache_ = parse(Store.get_(this.key_));\n } catch(err) {\n // Do nothing.\n }\n }\n return this.cache_ = assign({}, this.defaults_, this.cache_);\n }\n }\n\n /**\n * Saves the passed data object to localStorage,\n * merging it with the existing data.\n * @param {Object} newData The data to save.\n */\n set(newData) {\n this.cache_ = assign({}, this.defaults_, this.cache_, newData);\n\n if (Store.isSupported_()) {\n try {\n Store.set_(this.key_, JSON.stringify(this.cache_));\n } catch(err) {\n // Do nothing.\n }\n }\n }\n\n /**\n * Clears the data in localStorage for the current store.\n */\n clear() {\n this.cache_ = {};\n if (Store.isSupported_()) {\n try {\n Store.clear_(this.key_);\n } catch(err) {\n // Do nothing.\n }\n }\n }\n\n /**\n * Removes the store instance for the global instances map. If this is the\n * last store instance, the storage listener is also removed.\n * Note: this does not erase the stored data. Use `clear()` for that.\n */\n destroy() {\n delete instances[this.key_];\n if (!Object.keys(instances).length) {\n removeStorageListener();\n }\n }\n}\n\n\n/**\n * Adds a single storage event listener and flips the global `isListening`\n * flag so multiple events aren't added.\n */\nfunction initStorageListener() {\n window.addEventListener('storage', storageListener);\n isListening = true;\n}\n\n\n/**\n * Removes the storage event listener and flips the global `isListening`\n * flag so it can be re-added later.\n */\nfunction removeStorageListener() {\n window.removeEventListener('storage', storageListener);\n isListening = false;\n}\n\n\n/**\n * The global storage event listener.\n * @param {!Event} event The DOM event.\n */\nfunction storageListener(event) {\n const store = instances[event.key];\n if (store) {\n const oldData = assign({}, store.defaults_, parse(event.oldValue));\n const newData = assign({}, store.defaults_, parse(event.newValue));\n\n store.cache_ = newData;\n store.emit('externalSet', newData, oldData);\n }\n}\n\n\n/**\n * Parses a source string as JSON\n * @param {string|null} source\n * @return {!Object} The JSON object.\n */\nfunction parse(source) {\n let data = {};\n if (source) {\n try {\n data = /** @type {!Object} */ (JSON.parse(source));\n } catch(err) {\n // Do nothing.\n }\n }\n return data;\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport MethodChain from './method-chain';\nimport Store from './store';\nimport {now} from './utilities';\n\n\nconst SECONDS = 1000;\nconst MINUTES = 60 * SECONDS;\n\n\nconst instances = {};\n\n\n/**\n * A session management class that helps track session boundaries\n * across multiple open tabs/windows.\n */\nexport default class Session {\n /**\n * Gets an existing instance for the passed arguments or creates a new\n * instance if one doesn't exist.\n * @param {!Tracker} tracker An analytics.js tracker object.\n * @param {number} timeout The session timeout (in minutes). This value\n * should match what's set in the \"Session settings\" section of the\n * Google Analytics admin.\n * @param {string=} timeZone The optional IANA time zone of the view. This\n * value should match what's set in the \"View settings\" section of the\n * Google Analytics admin. (Note: this assumes all views for the property\n * use the same time zone. If that's not true, it's better not to use\n * this feature).\n * @return {Session} The Session instance.\n */\n static getOrCreate(tracker, timeout, timeZone) {\n // Don't create multiple instances for the same property.\n const trackingId = tracker.get('trackingId');\n if (instances[trackingId]) {\n return instances[trackingId];\n } else {\n return instances[trackingId] = new Session(tracker, timeout, timeZone);\n }\n }\n\n /**\n * @param {!Tracker} tracker An analytics.js tracker object.\n * @param {number} timeout The session timeout (in minutes). This value\n * should match what's set in the \"Session settings\" section of the\n * Google Analytics admin.\n * @param {string=} timeZone The optional IANA time zone of the view. This\n * value should match what's set in the \"View settings\" section of the\n * Google Analytics admin. (Note: this assumes all views for the property\n * use the same time zone. If that's not true, it's better not to use\n * this feature).\n */\n constructor(tracker, timeout, timeZone) {\n this.tracker = tracker;\n this.timeout = timeout || Session.DEFAULT_TIMEOUT;\n this.timeZone = timeZone;\n\n // Binds methods.\n this.sendHitTaskOverride = this.sendHitTaskOverride.bind(this);\n\n // Overrides into the trackers sendHitTask method.\n MethodChain.add(tracker, 'sendHitTask', this.sendHitTaskOverride);\n\n // Some browser doesn't support various features of the\n // `Intl.DateTimeFormat` API, so we have to try/catch it. Consequently,\n // this allows us to assume the presence of `this.dateTimeFormatter` means\n // it works in the current browser.\n try {\n this.dateTimeFormatter =\n new Intl.DateTimeFormat('en-US', {timeZone: this.timeZone});\n } catch(err) {\n // Do nothing.\n }\n\n // Creates the session store and adds change listeners.\n /** @type {SessionStoreData} */\n const defaultProps = {\n hitTime: 0,\n isExpired: false,\n };\n this.store = Store.getOrCreate(\n tracker.get('trackingId'), 'session', defaultProps);\n }\n\n /**\n * Accepts a tracker object and returns whether or not the session for that\n * tracker has expired. A session can expire for two reasons:\n * - More than 30 minutes has elapsed since the previous hit\n * was sent (The 30 minutes number is the Google Analytics default, but\n * it can be modified in GA admin \"Session settings\").\n * - A new day has started since the previous hit, in the\n * specified time zone (should correspond to the time zone of the\n * property's views).\n *\n * Note: since real session boundaries are determined at processing time,\n * this is just a best guess rather than a source of truth.\n *\n * @param {SessionStoreData=} sessionData An optional sessionData object\n * which avoids an additional localStorage read if the data is known to\n * be fresh.\n * @return {boolean} True if the session has expired.\n */\n isExpired(sessionData = this.store.get()) {\n // True if the sessionControl field was set to 'end' on the previous hit.\n if (sessionData.isExpired) return true;\n\n const currentDate = new Date();\n const oldHitTime = sessionData.hitTime;\n const oldHitDate = oldHitTime && new Date(oldHitTime);\n\n if (oldHitTime) {\n if (currentDate - oldHitDate > (this.timeout * MINUTES)) {\n // If more time has elapsed than the session expiry time,\n // the session has expired.\n return true;\n } else if (this.datesAreDifferentInTimezone(currentDate, oldHitDate)) {\n // A new day has started since the previous hit, which means the\n // session has expired.\n return true;\n }\n }\n\n // For all other cases return false.\n return false;\n }\n\n /**\n * Returns true if (and only if) the timezone date formatting is supported\n * in the current browser and if the two dates are diffinitiabely not the\n * same date in the session timezone. Anything short of this returns false.\n * @param {!Date} d1\n * @param {!Date} d2\n * @return {boolean}\n */\n datesAreDifferentInTimezone(d1, d2) {\n if (!this.dateTimeFormatter) {\n return false;\n } else {\n return this.dateTimeFormatter.format(d1)\n != this.dateTimeFormatter.format(d2);\n }\n }\n\n /**\n * Keeps track of when the previous hit was sent to determine if a session\n * has expired. Also inspects the `sessionControl` field to handles\n * expiration accordingly.\n * @param {function(!Model)} originalMethod A reference to the overridden\n * method.\n * @return {function(!Model)}\n */\n sendHitTaskOverride(originalMethod) {\n return (model) => {\n originalMethod(model);\n\n const sessionData = this.store.get();\n const isSessionExpired = this.isExpired(sessionData);\n const sessionControl = model.get('sessionControl');\n\n const sessionWillStart = sessionControl == 'start' || isSessionExpired;\n const sessionWillEnd = sessionControl == 'end';\n\n // Update the stored session data.\n sessionData.hitTime = now();\n if (sessionWillStart) {\n sessionData.isExpired = false;\n }\n if (sessionWillEnd) {\n sessionData.isExpired = true;\n }\n this.store.set(sessionData);\n };\n }\n\n /**\n * Restores the tracker's original `sendHitTask` to the state before\n * session control was initialized and removes this instance from the global\n * store.\n */\n destroy() {\n MethodChain.remove(this.tracker, 'sendHitTask', this.sendHitTaskOverride);\n this.store.destroy();\n delete instances[this.tracker.get('trackingId')];\n }\n}\n\n\nSession.DEFAULT_TIMEOUT = 30; // minutes\n","/**\n * Copyright 2017 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {parseUrl} from 'dom-utils';\nimport MethodChain from '../method-chain';\nimport provide from '../provide';\nimport Session from '../session';\nimport Store from '../store';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj, debounce, isObject} from '../utilities';\n\n\n/**\n * Class for the `maxScrollQueryTracker` analytics.js plugin.\n * @implements {MaxScrollTrackerPublicInterface}\n */\nclass MaxScrollTracker {\n /**\n * Registers outbound link tracking on tracker object.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?Object} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.MAX_SCROLL_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!window.addEventListener) return;\n\n /** @type {MaxScrollTrackerOpts} */\n const defaultOpts = {\n increaseThreshold: 20,\n sessionTimeout: Session.DEFAULT_TIMEOUT,\n // timeZone: undefined,\n // maxScrollMetricIndex: undefined,\n fieldsObj: {},\n // hitFilter: undefined\n };\n\n this.opts = /** @type {MaxScrollTrackerOpts} */ (\n assign(defaultOpts, opts));\n\n this.tracker = tracker;\n this.pagePath = this.getPagePath();\n\n // Binds methods to `this`.\n this.handleScroll = debounce(this.handleScroll.bind(this), 500);\n this.trackerSetOverride = this.trackerSetOverride.bind(this);\n\n // Creates the store and binds storage change events.\n this.store = Store.getOrCreate(\n tracker.get('trackingId'), 'plugins/max-scroll-tracker');\n\n // Creates the session and binds session events.\n this.session = new Session(\n tracker, this.opts.sessionTimeout, this.opts.timeZone);\n\n // Override the built-in tracker.set method to watch for changes.\n MethodChain.add(tracker, 'set', this.trackerSetOverride);\n\n this.listenForMaxScrollChanges();\n }\n\n\n /**\n * Adds a scroll event listener if the max scroll percentage for the\n * current page isn't already at 100%.\n */\n listenForMaxScrollChanges() {\n const maxScrollPercentage = this.getMaxScrollPercentageForCurrentPage();\n if (maxScrollPercentage < 100) {\n window.addEventListener('scroll', this.handleScroll);\n }\n }\n\n\n /**\n * Removes an added scroll listener.\n */\n stopListeningForMaxScrollChanges() {\n window.removeEventListener('scroll', this.handleScroll);\n }\n\n\n /**\n * Handles the scroll event. If the current scroll percentage is greater\n * that the stored scroll event by at least the specified increase threshold,\n * send an event with the increase amount.\n */\n handleScroll() {\n const pageHeight = getPageHeight();\n const scrollPos = window.pageYOffset; // scrollY isn't supported in IE.\n const windowHeight = window.innerHeight;\n\n // Ensure scrollPercentage is an integer between 0 and 100.\n const scrollPercentage = Math.min(100, Math.max(0,\n Math.round(100 * (scrollPos / (pageHeight - windowHeight)))));\n\n // If the session has expired, clear old scroll data and send no events.\n if (this.session.isExpired()) {\n this.store.clear();\n } else {\n const maxScrollPercentage = this.getMaxScrollPercentageForCurrentPage();\n\n if (scrollPercentage > maxScrollPercentage) {\n if (scrollPercentage == 100 || maxScrollPercentage == 100) {\n this.stopListeningForMaxScrollChanges();\n }\n const increaseAmount = scrollPercentage - maxScrollPercentage;\n if (scrollPercentage == 100 ||\n increaseAmount >= this.opts.increaseThreshold) {\n this.setMaxScrollPercentageForCurrentPage(scrollPercentage);\n this.sendMaxScrollEvent(increaseAmount, scrollPercentage);\n }\n }\n }\n }\n\n /**\n * Detects changes to the tracker object and triggers an update if the page\n * field has changed.\n * @param {function((Object|string), (string|undefined))} originalMethod\n * A reference to the overridden method.\n * @return {function((Object|string), (string|undefined))}\n */\n trackerSetOverride(originalMethod) {\n return (field, value) => {\n originalMethod(field, value);\n\n /** @type {!FieldsObj} */\n const fields = isObject(field) ? field : {[field]: value};\n if (fields.page) {\n const lastPagePath = this.pagePath;\n this.pagePath = this.getPagePath();\n\n if (this.pagePath != lastPagePath) {\n // Since event listeners for the same function are never added twice,\n // we don't need to worry about whether we're already listening. We\n // can just add the event listener again.\n this.listenForMaxScrollChanges();\n }\n }\n };\n }\n\n /**\n * Sends an event for the increased max scroll percentage amount.\n * @param {number} increaseAmount\n * @param {number} scrollPercentage\n */\n sendMaxScrollEvent(increaseAmount, scrollPercentage) {\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n eventCategory: 'Max Scroll',\n eventAction: 'increase',\n eventValue: increaseAmount,\n eventLabel: String(scrollPercentage),\n nonInteraction: true,\n };\n\n // If a custom metric was specified, set it equal to the event value.\n if (this.opts.maxScrollMetricIndex) {\n defaultFields['metric' + this.opts.maxScrollMetricIndex] = increaseAmount;\n }\n\n this.tracker.send('event',\n createFieldsObj(defaultFields, this.opts.fieldsObj,\n this.tracker, this.opts.hitFilter));\n }\n\n /**\n * Stores the current max scroll percentage for the current page.\n * @param {number} maxScrollPercentage\n */\n setMaxScrollPercentageForCurrentPage(maxScrollPercentage) {\n this.store.set({[this.pagePath]: maxScrollPercentage});\n }\n\n /**\n * Gets the stored max scroll percentage for the current page.\n * @return {number}\n */\n getMaxScrollPercentageForCurrentPage() {\n return this.store.get()[this.pagePath] || 0;\n }\n\n /**\n * Gets the page path from the tracker object.\n * @return {number}\n */\n getPagePath() {\n const url = parseUrl(\n this.tracker.get('page') || this.tracker.get('location'));\n return url.pathname + url.search;\n }\n\n /**\n * Removes all event listeners and restores overridden methods.\n */\n remove() {\n this.session.destroy();\n this.stopListeningForMaxScrollChanges();\n MethodChain.remove(this.tracker, 'set', this.trackerSetOverride);\n }\n}\n\n\nprovide('maxScrollTracker', MaxScrollTracker);\n\n\n/**\n * Gets the maximum height of the page including scrollable area.\n * @return {number}\n */\nfunction getPageHeight() {\n const html = document.documentElement;\n const body = document.body;\n return Math.max(html.offsetHeight, html.scrollHeight,\n body.offsetHeight, body.scrollHeight);\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {NULL_DIMENSION} from '../constants';\nimport provide from '../provide';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj,\n debounce, isObject, toArray} from '../utilities';\n\n\n/**\n * Declares the MediaQueryList instance cache.\n */\nconst mediaMap = {};\n\n\n/**\n * Class for the `mediaQueryTracker` analytics.js plugin.\n * @implements {MediaQueryTrackerPublicInterface}\n */\nclass MediaQueryTracker {\n /**\n * Registers media query tracking.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?Object} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.MEDIA_QUERY_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!window.matchMedia) return;\n\n /** @type {MediaQueryTrackerOpts} */\n const defaultOpts = {\n // definitions: unefined,\n changeTemplate: this.changeTemplate,\n changeTimeout: 1000,\n fieldsObj: {},\n // hitFilter: undefined,\n };\n\n this.opts = /** @type {MediaQueryTrackerOpts} */ (\n assign(defaultOpts, opts));\n\n // Exits early if media query data doesn't exist.\n if (!isObject(this.opts.definitions)) return;\n\n this.opts.definitions = toArray(this.opts.definitions);\n this.tracker = tracker;\n this.changeListeners = [];\n\n this.processMediaQueries();\n }\n\n /**\n * Loops through each media query definition, sets the custom dimenion data,\n * and adds the change listeners.\n */\n processMediaQueries() {\n this.opts.definitions.forEach((definition) => {\n // Only processes definitions with a name and index.\n if (definition.name && definition.dimensionIndex) {\n const mediaName = this.getMatchName(definition);\n this.tracker.set('dimension' + definition.dimensionIndex, mediaName);\n\n this.addChangeListeners(definition);\n }\n });\n }\n\n /**\n * Takes a definition object and return the name of the matching media item.\n * If no match is found, the NULL_DIMENSION value is returned.\n * @param {Object} definition A set of named media queries associated\n * with a single custom dimension.\n * @return {string} The name of the matched media or NULL_DIMENSION.\n */\n getMatchName(definition) {\n let match;\n\n definition.items.forEach((item) => {\n if (getMediaList(item.media).matches) {\n match = item;\n }\n });\n return match ? match.name : NULL_DIMENSION;\n }\n\n /**\n * Adds change listeners to each media query in the definition list.\n * Debounces the changes to prevent unnecessary hits from being sent.\n * @param {Object} definition A set of named media queries associated\n * with a single custom dimension\n */\n addChangeListeners(definition) {\n definition.items.forEach((item) => {\n const mql = getMediaList(item.media);\n const fn = debounce(() => {\n this.handleChanges(definition);\n }, this.opts.changeTimeout);\n\n mql.addListener(fn);\n this.changeListeners.push({mql, fn});\n });\n }\n\n /**\n * Handles changes to the matched media. When the new value differs from\n * the old value, a change event is sent.\n * @param {Object} definition A set of named media queries associated\n * with a single custom dimension\n */\n handleChanges(definition) {\n const newValue = this.getMatchName(definition);\n const oldValue = this.tracker.get('dimension' + definition.dimensionIndex);\n\n if (newValue !== oldValue) {\n this.tracker.set('dimension' + definition.dimensionIndex, newValue);\n\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n eventCategory: definition.name,\n eventAction: 'change',\n eventLabel: this.opts.changeTemplate(oldValue, newValue),\n nonInteraction: true,\n };\n this.tracker.send('event', createFieldsObj(defaultFields,\n this.opts.fieldsObj, this.tracker, this.opts.hitFilter));\n }\n }\n\n /**\n * Removes all event listeners and instance properties.\n */\n remove() {\n for (let i = 0, listener; listener = this.changeListeners[i]; i++) {\n listener.mql.removeListener(listener.fn);\n }\n }\n\n /**\n * Sets the default formatting of the change event label.\n * This can be overridden by setting the `changeTemplate` option.\n * @param {string} oldValue The value of the media query prior to the change.\n * @param {string} newValue The value of the media query after the change.\n * @return {string} The formatted event label.\n */\n changeTemplate(oldValue, newValue) {\n return oldValue + ' => ' + newValue;\n }\n}\n\n\nprovide('mediaQueryTracker', MediaQueryTracker);\n\n\n/**\n * Accepts a media query and returns a MediaQueryList object.\n * Caches the values to avoid multiple unnecessary instances.\n * @param {string} media A media query value.\n * @return {MediaQueryList} The matched media.\n */\nfunction getMediaList(media) {\n return mediaMap[media] || (mediaMap[media] = window.matchMedia(media));\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {delegate, parseUrl} from 'dom-utils';\nimport provide from '../provide';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj,\n getAttributeFields, withTimeout} from '../utilities';\n\n\n/**\n * Class for the `outboundFormTracker` analytics.js plugin.\n * @implements {OutboundFormTrackerPublicInterface}\n */\nclass OutboundFormTracker {\n /**\n * Registers outbound form tracking.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?Object} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.OUTBOUND_FORM_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!window.addEventListener) return;\n\n /** @type {OutboundFormTrackerOpts} */\n const defaultOpts = {\n formSelector: 'form',\n shouldTrackOutboundForm: this.shouldTrackOutboundForm,\n fieldsObj: {},\n attributePrefix: 'ga-',\n // hitFilter: undefined\n };\n\n this.opts = /** @type {OutboundFormTrackerOpts} */ (\n assign(defaultOpts, opts));\n\n this.tracker = tracker;\n\n this.delegate = delegate(document, 'submit', this.opts.formSelector,\n this.handleFormSubmits.bind(this), {composed: true, useCapture: true});\n }\n\n /**\n * Handles all submits on form elements. A form submit is considered outbound\n * if its action attribute starts with http and does not contain\n * location.hostname.\n * When the beacon transport method is not available, the event's default\n * action is prevented and re-emitted after the hit is sent.\n * @param {Event} event The DOM submit event.\n * @param {Element} form The delegated event target.\n */\n handleFormSubmits(event, form) {\n const action = parseUrl(form.action).href;\n\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n eventCategory: 'Outbound Form',\n eventAction: 'submit',\n eventLabel: action,\n };\n\n if (this.opts.shouldTrackOutboundForm(form, parseUrl)) {\n if (!navigator.sendBeacon) {\n // Stops the submit and waits until the hit is complete (with timeout)\n // for browsers that don't support beacon.\n event.preventDefault();\n defaultFields.hitCallback = withTimeout(function() {\n form.submit();\n });\n }\n\n const userFields = assign({}, this.opts.fieldsObj,\n getAttributeFields(form, this.opts.attributePrefix));\n\n this.tracker.send('event', createFieldsObj(\n defaultFields, userFields,\n this.tracker, this.opts.hitFilter, form, event));\n }\n }\n\n /**\n * Determines whether or not the tracker should send a hit when a form is\n * submitted. By default, forms with an action attribute that starts with\n * \"http\" and doesn't contain the current hostname are tracked.\n * @param {Element} form The form that was submitted.\n * @param {Function} parseUrlFn A cross-browser utility method for url\n * parsing (note: renamed to disambiguate when compiling).\n * @return {boolean} Whether or not the form should be tracked.\n */\n shouldTrackOutboundForm(form, parseUrlFn) {\n const url = parseUrlFn(form.action);\n return url.hostname != location.hostname &&\n url.protocol.slice(0, 4) == 'http';\n }\n\n /**\n * Removes all event listeners and instance properties.\n */\n remove() {\n this.delegate.destroy();\n }\n}\n\n\nprovide('outboundFormTracker', OutboundFormTracker);\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {delegate, parseUrl} from 'dom-utils';\nimport provide from '../provide';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj,\n getAttributeFields, withTimeout} from '../utilities';\n\n\n/**\n * Class for the `outboundLinkTracker` analytics.js plugin.\n * @implements {OutboundLinkTrackerPublicInterface}\n */\nclass OutboundLinkTracker {\n /**\n * Registers outbound link tracking on a tracker object.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?Object} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.OUTBOUND_LINK_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!window.addEventListener) return;\n\n /** @type {OutboundLinkTrackerOpts} */\n const defaultOpts = {\n events: ['click'],\n linkSelector: 'a, area',\n shouldTrackOutboundLink: this.shouldTrackOutboundLink,\n fieldsObj: {},\n attributePrefix: 'ga-',\n // hitFilter: undefined,\n };\n\n this.opts = /** @type {OutboundLinkTrackerOpts} */ (\n assign(defaultOpts, opts));\n\n this.tracker = tracker;\n\n // Binds methods.\n this.handleLinkInteractions = this.handleLinkInteractions.bind(this);\n\n // Creates a mapping of events to their delegates\n this.delegates = {};\n this.opts.events.forEach((event) => {\n this.delegates[event] = delegate(document, event, this.opts.linkSelector,\n this.handleLinkInteractions, {composed: true, useCapture: true});\n });\n }\n\n /**\n * Handles all interactions on link elements. A link is considered an outbound\n * link if its hostname property does not match location.hostname. When the\n * beacon transport method is not available, the links target is set to\n * \"_blank\" to ensure the hit can be sent.\n * @param {Event} event The DOM click event.\n * @param {Element} link The delegated event target.\n */\n handleLinkInteractions(event, link) {\n if (this.opts.shouldTrackOutboundLink(link, parseUrl)) {\n const href = link.getAttribute('href') || link.getAttribute('xlink:href');\n const url = parseUrl(href);\n\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n eventCategory: 'Outbound Link',\n eventAction: event.type,\n eventLabel: url.href,\n };\n\n if (!navigator.sendBeacon &&\n linkClickWillUnloadCurrentPage(event, link)) {\n // Adds a new event handler at the last minute to minimize the chances\n // that another event handler for this click will run after this logic.\n window.addEventListener('click', function(event) {\n // Checks to make sure another event handler hasn't already prevented\n // the default action. If it has the custom redirect isn't needed.\n if (!event.defaultPrevented) {\n // Stops the click and waits until the hit is complete (with\n // timeout) for browsers that don't support beacon.\n event.preventDefault();\n defaultFields.hitCallback = withTimeout(function() {\n location.href = href;\n });\n }\n });\n }\n\n /** @type {FieldsObj} */\n const userFields = assign({}, this.opts.fieldsObj,\n getAttributeFields(link, this.opts.attributePrefix));\n\n this.tracker.send('event',\n createFieldsObj(defaultFields, userFields,\n this.tracker, this.opts.hitFilter, link, event));\n }\n }\n\n /**\n * Determines whether or not the tracker should send a hit when a link is\n * clicked. By default links with a hostname property not equal to the current\n * hostname are tracked.\n * @param {Element} link The link that was clicked on.\n * @param {Function} parseUrlFn A cross-browser utility method for url\n * parsing (note: renamed to disambiguate when compiling).\n * @return {boolean} Whether or not the link should be tracked.\n */\n shouldTrackOutboundLink(link, parseUrlFn) {\n const href = link.getAttribute('href') || link.getAttribute('xlink:href');\n const url = parseUrlFn(href);\n return url.hostname != location.hostname &&\n url.protocol.slice(0, 4) == 'http';\n }\n\n /**\n * Removes all event listeners and instance properties.\n */\n remove() {\n Object.keys(this.delegates).forEach((key) => {\n this.delegates[key].destroy();\n });\n }\n}\n\n\nprovide('outboundLinkTracker', OutboundLinkTracker);\n\n\n/**\n * Determines if a link click event will cause the current page to upload.\n * Note: most link clicks *will* cause the current page to unload because they\n * initiate a page navigation. The most common reason a link click won't cause\n * the page to unload is if the clicked was to open the link in a new tab.\n * @param {Event} event The DOM event.\n * @param {Element} link The link element clicked on.\n * @return {boolean} True if the current page will be unloaded.\n */\nfunction linkClickWillUnloadCurrentPage(event, link) {\n return !(\n // The event type can be customized; we only care about clicks here.\n event.type != 'click' ||\n // Links with target=\"_blank\" set will open in a new window/tab.\n link.target == '_blank' ||\n // On mac, command clicking will open a link in a new tab. Control\n // clicking does this on windows.\n event.metaKey || event.ctrlKey ||\n // Shift clicking in Chrome/Firefox opens the link in a new window\n // In Safari it adds the URL to a favorites list.\n event.shiftKey ||\n // On Mac, clicking with the option key is used to download a resouce.\n event.altKey ||\n // Middle mouse button clicks (which == 2) are used to open a link\n // in a new tab, and right clicks (which == 3) on Firefox trigger\n // a click event.\n event.which > 1);\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {NULL_DIMENSION} from '../constants';\nimport MethodChain from '../method-chain';\nimport provide from '../provide';\nimport Session from '../session';\nimport Store from '../store';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj, deferUntilPluginsLoaded,\n isObject, now, uuid} from '../utilities';\n\n\nconst HIDDEN = 'hidden';\nconst VISIBLE = 'visible';\nconst PAGE_ID = uuid();\nconst SECONDS = 1000;\n\n\n/**\n * Class for the `pageVisibilityTracker` analytics.js plugin.\n * @implements {PageVisibilityTrackerPublicInterface}\n */\nclass PageVisibilityTracker {\n /**\n * Registers outbound link tracking on tracker object.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?Object} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.PAGE_VISIBILITY_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!document.visibilityState) return;\n\n /** @type {PageVisibilityTrackerOpts} */\n const defaultOpts = {\n sessionTimeout: Session.DEFAULT_TIMEOUT,\n visibleThreshold: 5 * SECONDS,\n // timeZone: undefined,\n sendInitialPageview: false,\n // pageLoadsMetricIndex: undefined,\n // visibleMetricIndex: undefined,\n fieldsObj: {},\n // hitFilter: undefined\n };\n\n this.opts = /** @type {PageVisibilityTrackerOpts} */ (\n assign(defaultOpts, opts));\n\n this.tracker = tracker;\n this.lastPageState = null;\n this.visibleThresholdTimeout_ = null;\n this.isInitialPageviewSent_ = false;\n\n // Binds methods to `this`.\n this.trackerSetOverride = this.trackerSetOverride.bind(this);\n this.handleChange = this.handleChange.bind(this);\n this.handleWindowUnload = this.handleWindowUnload.bind(this);\n this.handleExternalStoreSet = this.handleExternalStoreSet.bind(this);\n\n // Creates the store and binds storage change events.\n this.store = Store.getOrCreate(\n tracker.get('trackingId'), 'plugins/page-visibility-tracker');\n this.store.on('externalSet', this.handleExternalStoreSet);\n\n // Creates the session and binds session events.\n this.session = new Session(\n tracker, this.opts.sessionTimeout, this.opts.timeZone);\n\n // Override the built-in tracker.set method to watch for changes.\n MethodChain.add(tracker, 'set', this.trackerSetOverride);\n\n window.addEventListener('unload', this.handleWindowUnload);\n document.addEventListener('visibilitychange', this.handleChange);\n this.handleChange();\n\n // Postpone sending any hits until the next call stack, which allows all\n // autotrack plugins to be required sync before any hits are sent.\n deferUntilPluginsLoaded(this.tracker, () => {\n if (document.visibilityState == VISIBLE) {\n if (this.opts.sendInitialPageview) {\n this.sendPageview({isPageLoad: true});\n this.isInitialPageviewSent_ = true;\n }\n } else {\n if (this.opts.sendInitialPageview && this.opts.pageLoadsMetricIndex) {\n this.sendPageLoad();\n }\n }\n });\n }\n\n /**\n * Inspects the last visibility state change data and determines if a\n * visibility event needs to be tracked based on the current visibility\n * state and whether or not the session has expired. If the session has\n * expired, a change to `visible` will trigger an additional pageview.\n * This method also sends as the event value (and optionally a custom metric)\n * the elapsed time between this event and the previously reported change\n * in the same session, allowing you to more accurately determine when users\n * were actually looking at your page versus when it was in the background.\n */\n handleChange() {\n if (!(document.visibilityState == VISIBLE ||\n document.visibilityState == HIDDEN)) {\n return;\n }\n\n const lastStoredChange = this.validateChangeData(this.store.get());\n\n /** @type {PageVisibilityStoreData} */\n const change = {\n time: now(),\n state: document.visibilityState,\n pageId: PAGE_ID,\n };\n\n // If the visibilityState has changed to visible and the initial pageview\n // has not been sent (and the `sendInitialPageview` option is `true`).\n // Send the initial pageview now.\n if (this.lastPageState &&\n document.visibilityState == VISIBLE &&\n this.opts.sendInitialPageview && !this.isInitialPageviewSent_) {\n this.sendPageview();\n this.isInitialPageviewSent_ = true;\n }\n\n // If the visibilityState has changed to hidden, clear any scheduled\n // pageviews waiting for the visibleThreshold timeout.\n if (this.visibleThresholdTimeout_ && document.visibilityState == HIDDEN) {\n clearTimeout(this.visibleThresholdTimeout_);\n }\n\n if (this.session.isExpired()) {\n if (this.lastPageState == HIDDEN &&\n document.visibilityState == VISIBLE) {\n // If the session has expired, changes from hidden to visible should\n // be considered a new pageview rather than a visibility event.\n // This behavior ensures all sessions contain a pageview so\n // session-level page dimensions and metrics (e.g. ga:landingPagePath\n // and ga:entrances) are correct.\n // Also, in order to prevent false positives, we add a small timeout\n // that is cleared if the visibilityState changes to hidden shortly\n // after the change to visible. This can happen if a user is quickly\n // switching through their open tabs but not actually interacting with\n // and of them. It can also happen when a user goes to a tab just to\n // immediately close it. Such cases should not be considered pageviews.\n clearTimeout(this.visibleThresholdTimeout_);\n this.visibleThresholdTimeout_ = setTimeout(() => {\n this.store.set(change);\n this.sendPageview({hitTime: change.time});\n }, this.opts.visibleThreshold);\n } else if (document.visibilityState == HIDDEN) {\n // Hidden events should never be sent if a session has expired (if\n // they are, they'll likely start a new session with just this event).\n this.store.clear();\n }\n } else {\n if (lastStoredChange.pageId == PAGE_ID &&\n lastStoredChange.state == VISIBLE) {\n this.sendPageVisibilityEvent(lastStoredChange);\n }\n this.store.set(change);\n }\n\n this.lastPageState = document.visibilityState;\n }\n\n /**\n * Retroactively updates the stored change data in cases where it's known to\n * be out of sync.\n * This plugin keeps track of each visiblity change and stores the last one\n * in localStorage. LocalStorage is used to handle situations where the user\n * has multiple page open at the same time and we don't want to\n * double-report page visibility in those cases.\n * However, a problem can occur if a user closes a page when one or more\n * visible pages are still open. In such cases it's impossible to know\n * which of the remaining pages the user will interact with next.\n * To solve this problem we wait for the next change on any page and then\n * retroactively update the stored data to reflect the current page as being\n * the page on which the last change event occured and measure visibility\n * from that point.\n * @param {PageVisibilityStoreData} lastStoredChange\n * @return {PageVisibilityStoreData}\n */\n validateChangeData(lastStoredChange) {\n if (this.lastPageState == VISIBLE &&\n lastStoredChange.state == HIDDEN &&\n lastStoredChange.pageId != PAGE_ID) {\n lastStoredChange.state = VISIBLE;\n lastStoredChange.pageId = PAGE_ID;\n this.store.set(lastStoredChange);\n }\n return lastStoredChange;\n }\n\n /**\n * Sends a Page Visibility event to track the time this page was in the\n * visible state (assuming it was in that state long enough to meet the\n * threshold).\n * @param {PageVisibilityStoreData} lastStoredChange\n * @param {{hitTime: (number|undefined)}=} param1\n * - hitTime: A hit timestap used to help ensure original order in cases\n * where the send is delayed.\n */\n sendPageVisibilityEvent(lastStoredChange, {hitTime} = {}) {\n const delta = this.getTimeSinceLastStoredChange(\n lastStoredChange, {hitTime});\n\n // If the detla is greater than the visibileThreshold, report it.\n if (delta && delta >= this.opts.visibleThreshold) {\n const deltaInSeconds = Math.round(delta / SECONDS);\n\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n nonInteraction: true,\n eventCategory: 'Page Visibility',\n eventAction: 'track',\n eventValue: deltaInSeconds,\n eventLabel: NULL_DIMENSION,\n };\n\n if (hitTime) {\n defaultFields.queueTime = now() - hitTime;\n }\n\n // If a custom metric was specified, set it equal to the event value.\n if (this.opts.visibleMetricIndex) {\n defaultFields['metric' + this.opts.visibleMetricIndex] = deltaInSeconds;\n }\n\n this.tracker.send('event',\n createFieldsObj(defaultFields, this.opts.fieldsObj,\n this.tracker, this.opts.hitFilter));\n }\n }\n\n /**\n * Sends a page load event.\n */\n sendPageLoad() {\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n eventCategory: 'Page Visibility',\n eventAction: 'page load',\n eventLabel: NULL_DIMENSION,\n ['metric' + this.opts.pageLoadsMetricIndex]: 1,\n nonInteraction: true,\n };\n this.tracker.send('event',\n createFieldsObj(defaultFields, this.opts.fieldsObj,\n this.tracker, this.opts.hitFilter));\n }\n\n /**\n * Sends a pageview, optionally calculating an offset if hitTime is passed.\n * @param {{\n * hitTime: (number|undefined),\n * isPageLoad: (boolean|undefined)\n * }=} param1\n * hitTime: The timestamp of the current hit.\n * isPageLoad: True if this pageview was also a page load.\n */\n sendPageview({hitTime, isPageLoad} = {}) {\n /** @type {FieldsObj} */\n const defaultFields = {transport: 'beacon'};\n if (hitTime) {\n defaultFields.queueTime = now() - hitTime;\n }\n if (isPageLoad && this.opts.pageLoadsMetricIndex) {\n defaultFields['metric' + this.opts.pageLoadsMetricIndex] = 1;\n }\n\n this.tracker.send('pageview',\n createFieldsObj(defaultFields, this.opts.fieldsObj,\n this.tracker, this.opts.hitFilter));\n }\n\n /**\n * Detects changes to the tracker object and triggers an update if the page\n * field has changed.\n * @param {function((Object|string), (string|undefined))} originalMethod\n * A reference to the overridden method.\n * @return {function((Object|string), (string|undefined))}\n */\n trackerSetOverride(originalMethod) {\n return (field, value) => {\n /** @type {!FieldsObj} */\n const fields = isObject(field) ? field : {[field]: value};\n if (fields.page && fields.page !== this.tracker.get('page')) {\n if (this.lastPageState == VISIBLE) {\n this.handleChange();\n }\n }\n originalMethod(field, value);\n };\n }\n\n /**\n * Calculates the time since the last visibility change event in the current\n * session. If the session has expired the reported time is zero.\n * @param {PageVisibilityStoreData} lastStoredChange\n * @param {{hitTime: (number|undefined)}=} param1\n * hitTime: The time of the current hit (defaults to now).\n * @return {number} The time (in ms) since the last change.\n */\n getTimeSinceLastStoredChange(lastStoredChange, {hitTime} = {}) {\n return lastStoredChange.time && !this.session.isExpired() ?\n (hitTime || now()) - lastStoredChange.time : 0;\n }\n\n /**\n * Handles responding to the `storage` event.\n * The code on this page needs to be informed when other tabs or windows are\n * updating the stored page visibility state data. This method checks to see\n * if a hidden state is stored when there are still visible tabs open, which\n * can happen if multiple windows are open at the same time.\n * @param {PageVisibilityStoreData} newData\n * @param {PageVisibilityStoreData} oldData\n */\n handleExternalStoreSet(newData, oldData) {\n // If the change times are the same, then the previous write only\n // updated the active page ID. It didn't enter a new state and thus no\n // hits should be sent.\n if (newData.time == oldData.time) return;\n\n // Page Visibility events must be sent by the tracker on the page\n // where the original event occurred. So if a change happens on another\n // page, but this page is where the previous change event occurred, then\n // this page is the one that needs to send the event (so all dimension\n // data is correct).\n if (oldData.pageId == PAGE_ID &&\n oldData.state == VISIBLE) {\n this.sendPageVisibilityEvent(oldData, {hitTime: newData.time});\n }\n }\n\n /**\n * Handles responding to the `unload` event.\n * Since some browsers don't emit a `visibilitychange` event in all cases\n * where a page might be unloaded, it's necessary to hook into the `unload`\n * event to ensure the correct state is always stored.\n */\n handleWindowUnload() {\n // If the stored visibility state isn't hidden when the unload event\n // fires, it means the visibilitychange event didn't fire as the document\n // was being unloaded, so we invoke it manually.\n if (this.lastPageState != HIDDEN) {\n this.handleChange();\n }\n }\n\n /**\n * Removes all event listeners and restores overridden methods.\n */\n remove() {\n this.store.destroy();\n this.session.destroy();\n MethodChain.remove(this.tracker, 'set', this.trackerSetOverride);\n window.removeEventListener('unload', this.handleWindowUnload);\n document.removeEventListener('visibilitychange', this.handleChange);\n }\n}\n\n\nprovide('pageVisibilityTracker', PageVisibilityTracker);\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport provide from '../provide';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj} from '../utilities';\n\n\n/**\n * Class for the `socialWidgetTracker` analytics.js plugin.\n * @implements {SocialWidgetTrackerPublicInterface}\n */\nclass SocialWidgetTracker {\n /**\n * Registers social tracking on tracker object.\n * Supports both declarative social tracking via HTML attributes as well as\n * tracking for social events when using official Twitter or Facebook widgets.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?Object} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.SOCIAL_WIDGET_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!window.addEventListener) return;\n\n /** @type {SocialWidgetTrackerOpts} */\n const defaultOpts = {\n fieldsObj: {},\n hitFilter: null,\n };\n\n this.opts = /** @type {SocialWidgetTrackerOpts} */ (\n assign(defaultOpts, opts));\n\n this.tracker = tracker;\n\n // Binds methods to `this`.\n this.addWidgetListeners = this.addWidgetListeners.bind(this);\n this.addTwitterEventHandlers = this.addTwitterEventHandlers.bind(this);\n this.handleTweetEvents = this.handleTweetEvents.bind(this);\n this.handleFollowEvents = this.handleFollowEvents.bind(this);\n this.handleLikeEvents = this.handleLikeEvents.bind(this);\n this.handleUnlikeEvents = this.handleUnlikeEvents.bind(this);\n\n if (document.readyState != 'complete') {\n // Adds the widget listeners after the window's `load` event fires.\n // If loading widgets using the officially recommended snippets, they\n // will be available at `window.load`. If not users can call the\n // `addWidgetListeners` method manually.\n window.addEventListener('load', this.addWidgetListeners);\n } else {\n this.addWidgetListeners();\n }\n }\n\n\n /**\n * Invokes the methods to add Facebook and Twitter widget event listeners.\n * Ensures the respective global namespaces are present before adding.\n */\n addWidgetListeners() {\n if (window.FB) this.addFacebookEventHandlers();\n if (window.twttr) this.addTwitterEventHandlers();\n }\n\n /**\n * Adds event handlers for the \"tweet\" and \"follow\" events emitted by the\n * official tweet and follow buttons. Note: this does not capture tweet or\n * follow events emitted by other Twitter widgets (tweet, timeline, etc.).\n */\n addTwitterEventHandlers() {\n try {\n window.twttr.ready(() => {\n window.twttr.events.bind('tweet', this.handleTweetEvents);\n window.twttr.events.bind('follow', this.handleFollowEvents);\n });\n } catch(err) {\n // Do nothing.\n }\n }\n\n /**\n * Removes event handlers for the \"tweet\" and \"follow\" events emitted by the\n * official tweet and follow buttons.\n */\n removeTwitterEventHandlers() {\n try {\n window.twttr.ready(() => {\n window.twttr.events.unbind('tweet', this.handleTweetEvents);\n window.twttr.events.unbind('follow', this.handleFollowEvents);\n });\n } catch(err) {\n // Do nothing.\n }\n }\n\n /**\n * Adds event handlers for the \"like\" and \"unlike\" events emitted by the\n * official Facebook like button.\n */\n addFacebookEventHandlers() {\n try {\n window.FB.Event.subscribe('edge.create', this.handleLikeEvents);\n window.FB.Event.subscribe('edge.remove', this.handleUnlikeEvents);\n } catch(err) {\n // Do nothing.\n }\n }\n\n /**\n * Removes event handlers for the \"like\" and \"unlike\" events emitted by the\n * official Facebook like button.\n */\n removeFacebookEventHandlers() {\n try {\n window.FB.Event.unsubscribe('edge.create', this.handleLikeEvents);\n window.FB.Event.unsubscribe('edge.remove', this.handleUnlikeEvents);\n } catch(err) {\n // Do nothing.\n }\n }\n\n /**\n * Handles `tweet` events emitted by the Twitter JS SDK.\n * @param {TwttrEvent} event The Twitter event object passed to the handler.\n */\n handleTweetEvents(event) {\n // Ignores tweets from widgets that aren't the tweet button.\n if (event.region != 'tweet') return;\n\n const url = event.data.url || event.target.getAttribute('data-url') ||\n location.href;\n\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n socialNetwork: 'Twitter',\n socialAction: 'tweet',\n socialTarget: url,\n };\n this.tracker.send('social',\n createFieldsObj(defaultFields, this.opts.fieldsObj,\n this.tracker, this.opts.hitFilter, event.target, event));\n }\n\n /**\n * Handles `follow` events emitted by the Twitter JS SDK.\n * @param {TwttrEvent} event The Twitter event object passed to the handler.\n */\n handleFollowEvents(event) {\n // Ignore follows from widgets that aren't the follow button.\n if (event.region != 'follow') return;\n\n const screenName = event.data.screen_name ||\n event.target.getAttribute('data-screen-name');\n\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n socialNetwork: 'Twitter',\n socialAction: 'follow',\n socialTarget: screenName,\n };\n this.tracker.send('social',\n createFieldsObj(defaultFields, this.opts.fieldsObj,\n this.tracker, this.opts.hitFilter, event.target, event));\n }\n\n /**\n * Handles `like` events emitted by the Facebook JS SDK.\n * @param {string} url The URL corresponding to the like event.\n */\n handleLikeEvents(url) {\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n socialNetwork: 'Facebook',\n socialAction: 'like',\n socialTarget: url,\n };\n this.tracker.send('social', createFieldsObj(defaultFields,\n this.opts.fieldsObj, this.tracker, this.opts.hitFilter));\n }\n\n /**\n * Handles `unlike` events emitted by the Facebook JS SDK.\n * @param {string} url The URL corresponding to the unlike event.\n */\n handleUnlikeEvents(url) {\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n socialNetwork: 'Facebook',\n socialAction: 'unlike',\n socialTarget: url,\n };\n this.tracker.send('social', createFieldsObj(defaultFields,\n this.opts.fieldsObj, this.tracker, this.opts.hitFilter));\n }\n\n /**\n * Removes all event listeners and instance properties.\n */\n remove() {\n window.removeEventListener('load', this.addWidgetListeners);\n this.removeFacebookEventHandlers();\n this.removeTwitterEventHandlers();\n }\n}\n\n\nprovide('socialWidgetTracker', SocialWidgetTracker);\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport MethodChain from '../method-chain';\nimport provide from '../provide';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj} from '../utilities';\n\n\n/**\n * Class for the `urlChangeTracker` analytics.js plugin.\n * @implements {UrlChangeTrackerPublicInterface}\n */\nclass UrlChangeTracker {\n /**\n * Adds handler for the history API methods\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?Object} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.URL_CHANGE_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!history.pushState || !window.addEventListener) return;\n\n /** @type {UrlChangeTrackerOpts} */\n const defaultOpts = {\n shouldTrackUrlChange: this.shouldTrackUrlChange,\n trackReplaceState: false,\n fieldsObj: {},\n hitFilter: null,\n };\n\n this.opts = /** @type {UrlChangeTrackerOpts} */ (assign(defaultOpts, opts));\n\n this.tracker = tracker;\n\n // Sets the initial page field.\n // Don't set this on the tracker yet so campaign data can be retreived\n // from the location field.\n this.path = getPath();\n\n // Binds methods.\n this.pushStateOverride = this.pushStateOverride.bind(this);\n this.replaceStateOverride = this.replaceStateOverride.bind(this);\n this.handlePopState = this.handlePopState.bind(this);\n\n // Watches for history changes.\n MethodChain.add(history, 'pushState', this.pushStateOverride);\n MethodChain.add(history, 'replaceState', this.replaceStateOverride);\n window.addEventListener('popstate', this.handlePopState);\n }\n\n /**\n * Handles invocations of the native `history.pushState` and calls\n * `handleUrlChange()` indicating that the history updated.\n * @param {!Function} originalMethod A reference to the overridden method.\n * @return {!Function}\n */\n pushStateOverride(originalMethod) {\n return (...args) => {\n originalMethod(...args);\n this.handleUrlChange(true);\n };\n }\n\n /**\n * Handles invocations of the native `history.replaceState` and calls\n * `handleUrlChange()` indicating that history was replaced.\n * @param {!Function} originalMethod A reference to the overridden method.\n * @return {!Function}\n */\n replaceStateOverride(originalMethod) {\n return (...args) => {\n originalMethod(...args);\n this.handleUrlChange(false);\n };\n }\n\n /**\n * Handles responding to the popstate event and calls\n * `handleUrlChange()` indicating that history was updated.\n */\n handlePopState() {\n this.handleUrlChange(true);\n }\n\n /**\n * Updates the page and title fields on the tracker and sends a pageview\n * if a new history entry was created.\n * @param {boolean} historyDidUpdate True if the history was changed via\n * `pushState()` or the `popstate` event. False if the history was just\n * modified via `replaceState()`.\n */\n handleUrlChange(historyDidUpdate) {\n // Calls the update logic asychronously to help ensure that app logic\n // responding to the URL change happens prior to this.\n setTimeout(() => {\n const oldPath = this.path;\n const newPath = getPath();\n\n if (oldPath != newPath &&\n this.opts.shouldTrackUrlChange.call(this, newPath, oldPath)) {\n this.path = newPath;\n this.tracker.set({\n page: newPath,\n title: document.title,\n });\n\n if (historyDidUpdate || this.opts.trackReplaceState) {\n /** @type {FieldsObj} */\n const defaultFields = {transport: 'beacon'};\n this.tracker.send('pageview', createFieldsObj(defaultFields,\n this.opts.fieldsObj, this.tracker, this.opts.hitFilter));\n }\n }\n }, 0);\n }\n\n /**\n * Determines whether or not the tracker should send a hit with the new page\n * data. This default implementation can be overrided in the config options.\n * @param {string} newPath The path after the URL change.\n * @param {string} oldPath The path prior to the URL change.\n * @return {boolean} Whether or not the URL change should be tracked.\n */\n shouldTrackUrlChange(newPath, oldPath) {\n return !!(newPath && oldPath);\n }\n\n /**\n * Removes all event listeners and restores overridden methods.\n */\n remove() {\n MethodChain.remove(history, 'pushState', this.pushStateOverride);\n MethodChain.remove(history, 'replaceState', this.replaceStateOverride);\n window.removeEventListener('popstate', this.handlePopState);\n }\n}\n\n\nprovide('urlChangeTracker', UrlChangeTracker);\n\n\n/**\n * @return {string} The path value of the current URL.\n */\nfunction getPath() {\n return location.pathname + location.search;\n}\n"]} \ No newline at end of file +{"version":3,"sources":["node_modules/dom-utils/lib/matches.js"," [synthetic:util/defineproperty] "," [synthetic:util/global] "," [synthetic:es6/symbol] "," [synthetic:es6/util/makeiterator] "," [synthetic:es6/util/arrayfromiterable] "," [synthetic:es6/util/arrayfromiterator] "," [synthetic:es6/util/inherits] ","node_modules/dom-utils/lib/parents.js","node_modules/dom-utils/lib/delegate.js","node_modules/dom-utils/lib/closest.js","lib/plugins/event-tracker.js","node_modules/dom-utils/lib/get-attributes.js","node_modules/dom-utils/lib/parse-url.js","lib/method-chain.js","lib/utilities.js","lib/provide.js","lib/constants.js","lib/usage.js","lib/plugins/clean-url-tracker.js","lib/plugins/impression-tracker.js","lib/event-emitter.js","lib/store.js","lib/session.js","lib/plugins/max-scroll-tracker.js","lib/plugins/media-query-tracker.js","lib/plugins/outbound-form-tracker.js","lib/plugins/outbound-link-tracker.js","lib/plugins/page-visibility-tracker.js","lib/plugins/social-widget-tracker.js","lib/plugins/url-change-tracker.js"],"names":["$jscomp.defineProperty","$jscomp.global","$jscomp.initSymbol","$jscomp.Symbol","$jscomp.symbolCounter_","$jscomp.SYMBOL_PREFIX","$jscomp.arrayIterator","$jscomp.initSymbolIterator","$jscomp.iteratorPrototype","proto","window","Element","prototype","nativeMatches","matches","matchesSelector","webkitMatchesSelector","mozMatchesSelector","msMatchesSelector","oMatchesSelector","element","test","nodeType","i","item","selector","call","nodes","parentNode","querySelectorAll","node","parents","list","push","delegate","eventType","callback","listener","event","delegateTarget","opts","composed","composedPath","target","parentElements","concat","parent","document","useCapture","ancestor","addEventListener","destroy","removeEventListener","getAttributes","attrs","map","attributes","length","attr","name","value","DEFAULT_PORT","a","createElement","cache","parseUrl","url","location","href","charAt","port","HTTP_PORT","HTTPS_PORT","host","replace","hash","hostname","origin","protocol","pathname","search","instances","constructor","MethodChain","context","methodName","originalMethodReference","isTask","get","methodChain","boundMethodChain","wrappedMethod","this.wrappedMethod","lastBoundMethod","$jscomp.arrayFromIterable","args","set","add","methodOverride","getOrCreateMethodChain","rebindMethodChain","remove","index","indexOf","splice","method","previousMethod","bind","filter","h","createFieldsObj","defaultFields","userFields","tracker","hitFilter","originalBuildHitTask","buildHitTask","model","assign","getAttributeFields","prefix","attributeFields","Object","keys","forEach","attribute","field","camelCase","slice","domReady","readyState","fn","debounce","wait","timeout","clearTimeout","setTimeout","withTimeout","called","queueMap","deferUntilPluginsLoaded","processQueue","ref","send","MethodChain.remove","trackingId","queue","ref.send","originalMethod","MethodChain.add","len","sources","source","key","hasOwnProperty","str","match","p1","toUpperCase","isObject","uuid","b","toString","Math","random","provide","pluginName","pluginConstructor","gaAlias","GoogleAnalyticsObject","q","gaDevIds","DEV_ID","gaplugins","plugins","CLEAN_URL_TRACKER","EVENT_TRACKER","IMPRESSION_TRACKER","MEDIA_QUERY_TRACKER","OUTBOUND_FORM_TRACKER","OUTBOUND_LINK_TRACKER","PAGE_VISIBILITY_TRACKER","SOCIAL_WIDGET_TRACKER","URL_CHANGE_TRACKER","MAX_SCROLL_TRACKER","PLUGIN_COUNT","trackUsage","plugin","VERSION","usageHex","parseInt","toAdd","usageBin","substr","CleanUrlTracker","defaultOpts","queryDimension","stripQuery","queryDimensionIndex","trackerGetOverride","buildHitTaskOverride","fieldsObj","page","cleanUrlFields","cleanedFieldsObj","indexFilename","parts","split","join","trailingSlash","isFilename","stripNonWhitelistedQueryParams","NULL_DIMENSION","urlFieldsFilter","userCleanedFieldsObj","returnValue","searchString","Array","isArray","queryParamsWhitelist","foundParams","kv","$jscomp.makeIterator","EventTracker","events","attributePrefix","handleEvents","delegates","getAttribute","type","hitType","transport","ImpressionTracker","IntersectionObserver","MutationObserver","defaultOptions","rootMargin","handleDomMutations","handleIntersectionChanges","handleDomElementAdded","handleDomElementRemoved","mutationObserver","items","elementMap","thresholdMap","elements","observeElements","ImpressionTracker.prototype","?.prototype","data","deriveDataFromElements","observer","threshold","id","getElementById","observe","body","childList","subtree","requestAnimationFrame","unobserveElements","itemsToKeep","itemsToRemove","some","itemInItems","itemToRemove","getItemFromElement","trackFirstImpressionOnly","dataToKeep","dataToRemove","unobserve","disconnect","unobserveAllElements","mutations","mutation","k","removedEl","removedNodes","walkNodeTree","j","addedEl","addedNodes","child","childNodes","records","record","intersectionRatio","intersectionRect","top","bottom","left","right","eventCategory","eventAction","eventLabel","nonInteraction","handleImpression","EventEmitter","registry_","on","getRegistry_","emit","isListening","browserSupportsLocalStorage","Store","defaults","key_","defaults_","cache_","$jscomp.inherits","getOrCreate","namespace","AUTOTRACK_PREFIX","storageListener","isSupported_","localStorage","setItem","removeItem","err","Store.isSupported_","parse","getItem","newData","JSON","stringify","clear","store","oldData","oldValue","newValue","Session","timeZone","Session.DEFAULT_TIMEOUT","sendHitTaskOverride","dateTimeFormatter","Intl","DateTimeFormat","Store.getOrCreate","defaultProps","hitTime","isExpired","getId","sessionData","oldHitTime","currentDate","Date","oldHitDate","MINUTES","datesAreDifferentInTimezone","format","sessionControl","sessionWillStart","sessionWillEnd","MaxScrollTracker","increaseThreshold","sessionTimeout","pagePath","getPagePath","handleScroll","trackerSetOverride","session","Session.getOrCreate","listenForMaxScrollChanges","getMaxScrollPercentageForCurrentPage","html","documentElement","scrollPercentage","min","max","round","pageYOffset","pageHeight","offsetHeight","scrollHeight","innerHeight","sessionId","maxScrollPercentage","stopListeningForMaxScrollChanges","increaseAmount","setMaxScrollPercentageForCurrentPage","eventValue","String","sendMaxScrollEvent","maxScrollMetricIndex","fields","lastPagePath","mediaMap","MediaQueryTracker","matchMedia","changeTemplate","changeTimeout","definitions","changeListeners","processMediaQueries","definition","dimensionIndex","mediaName","getMatchName","addChangeListeners","getMediaList","media","mql","handleChanges","addListener","removeListener","OutboundFormTracker","formSelector","shouldTrackOutboundForm","handleFormSubmits","form","action","navigator","sendBeacon","preventDefault","hitCallback","submit","parseUrlFn","OutboundLinkTracker","linkSelector","shouldTrackOutboundLink","handleLinkInteractions","link","metaKey","ctrlKey","shiftKey","altKey","which","clickHandler","defaultPrevented","oldHitCallback","PAGE_ID","PageVisibilityTracker","visibilityState","visibleThreshold","sendInitialPageview","lastPageState","visibleThresholdTimeout_","isInitialPageviewSent_","handleChange","handleWindowUnload","handleExternalStoreSet","VISIBLE","sendPageview","isPageLoad","time","state","pageId","pageLoadsMetricIndex","sendPageLoad","PageVisibilityTracker.prototype","HIDDEN","lastStoredChange","getAndValidateChangeData","change","sendPageVisibilityEvent","delta","deltaInSeconds","SECONDS$1","queueTime","visibleMetricIndex","PageVisibilityTracker_prototype$trackerSetOverride","SocialWidgetTracker","addWidgetListeners","addTwitterEventHandlers","handleTweetEvents","handleFollowEvents","handleLikeEvents","handleUnlikeEvents","SocialWidgetTracker.prototype","FB","Event","subscribe","addFacebookEventHandlers","twttr","ready","removeTwitterEventHandlers","unbind","region","socialNetwork","socialAction","socialTarget","screen_name","unsubscribe","removeFacebookEventHandlers","UrlChangeTracker","history","pushState","shouldTrackUrlChange","trackReplaceState","path","pushStateOverride","replaceStateOverride","handlePopState","UrlChangeTracker.prototype","handleUrlChange","historyDidUpdate","oldPath","newPath","title"],"mappings":"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","file":"","sourcesContent":["const proto = window.Element.prototype;\nconst nativeMatches = proto.matches ||\n proto.matchesSelector ||\n proto.webkitMatchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector;\n\n\n/**\n * Tests if a DOM elements matches any of the test DOM elements or selectors.\n * @param {Element} element The DOM element to test.\n * @param {Element|string|Array} test A DOM element, a CSS\n * selector, or an array of DOM elements or CSS selectors to match against.\n * @return {boolean} True of any part of the test matches.\n */\nexport default function matches(element, test) {\n // Validate input.\n if (element && element.nodeType == 1 && test) {\n // if test is a string or DOM element test it.\n if (typeof test == 'string' || test.nodeType == 1) {\n return element == test ||\n matchesSelector(element, /** @type {string} */ (test));\n } else if ('length' in test) {\n // if it has a length property iterate over the items\n // and return true if any match.\n for (let i = 0, item; item = test[i]; i++) {\n if (element == item || matchesSelector(element, item)) return true;\n }\n }\n }\n // Still here? Return false\n return false;\n}\n\n\n/**\n * Tests whether a DOM element matches a selector. This polyfills the native\n * Element.prototype.matches method across browsers.\n * @param {!Element} element The DOM element to test.\n * @param {string} selector The CSS selector to test element against.\n * @return {boolean} True if the selector matches.\n */\nfunction matchesSelector(element, selector) {\n if (typeof selector != 'string') return false;\n if (nativeMatches) return nativeMatches.call(element, selector);\n const nodes = element.parentNode.querySelectorAll(selector);\n for (let i = 0, node; node = nodes[i]; i++) {\n if (node == element) return true;\n }\n return false;\n}\n",null,null,null,null,null,null,null,"/**\n * Returns an array of a DOM element's parent elements.\n * @param {!Element} element The DOM element whose parents to get.\n * @return {!Array} An array of all parent elemets, or an empty array if no\n * parent elements are found.\n */\nexport default function parents(element) {\n const list = [];\n while (element && element.parentNode && element.parentNode.nodeType == 1) {\n element = /** @type {!Element} */ (element.parentNode);\n list.push(element);\n }\n return list;\n}\n","import closest from './closest';\nimport matches from './matches';\n\n/**\n * Delegates the handling of events for an element matching a selector to an\n * ancestor of the matching element.\n * @param {!Node} ancestor The ancestor element to add the listener to.\n * @param {string} eventType The event type to listen to.\n * @param {string} selector A CSS selector to match against child elements.\n * @param {!Function} callback A function to run any time the event happens.\n * @param {Object=} opts A configuration options object. The available options:\n * - useCapture: If true, bind to the event capture phase.\n * - deep: If true, delegate into shadow trees.\n * @return {Object} The delegate object. It contains a destroy method.\n */\nexport default function delegate(\n ancestor, eventType, selector, callback, opts = {}) {\n // Defines the event listener.\n const listener = function(event) {\n let delegateTarget;\n\n // If opts.composed is true and the event originated from inside a Shadow\n // tree, check the composed path nodes.\n if (opts.composed && typeof event.composedPath == 'function') {\n const composedPath = event.composedPath();\n for (let i = 0, node; node = composedPath[i]; i++) {\n if (node.nodeType == 1 && matches(node, selector)) {\n delegateTarget = node;\n }\n }\n } else {\n // Otherwise check the parents.\n delegateTarget = closest(event.target, selector, true);\n }\n\n if (delegateTarget) {\n callback.call(delegateTarget, event, delegateTarget);\n }\n };\n\n ancestor.addEventListener(eventType, listener, opts.useCapture);\n\n return {\n destroy: function() {\n ancestor.removeEventListener(eventType, listener, opts.useCapture);\n },\n };\n}\n","import matches from './matches';\nimport parents from './parents';\n\n/**\n * Gets the closest parent element that matches the passed selector.\n * @param {Element} element The element whose parents to check.\n * @param {string} selector The CSS selector to match against.\n * @param {boolean=} shouldCheckSelf True if the selector should test against\n * the passed element itself.\n * @return {Element|undefined} The matching element or undefined.\n */\nexport default function closest(element, selector, shouldCheckSelf = false) {\n if (!(element && element.nodeType == 1 && selector)) return;\n const parentElements =\n (shouldCheckSelf ? [element] : []).concat(parents(element));\n\n for (let i = 0, parent; parent = parentElements[i]; i++) {\n if (matches(parent, selector)) return parent;\n }\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {delegate} from 'dom-utils';\nimport provide from '../provide';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj, getAttributeFields} from '../utilities';\n\n\n/**\n * Class for the `eventTracker` analytics.js plugin.\n * @implements {EventTrackerPublicInterface}\n */\nclass EventTracker {\n /**\n * Registers declarative event tracking.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?EventTrackerOpts} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.EVENT_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!window.addEventListener) return;\n\n /** @type {EventTrackerOpts} */\n const defaultOpts = {\n events: ['click'],\n fieldsObj: {},\n attributePrefix: 'ga-',\n // hitFilter: undefined,\n };\n\n this.opts = /** @type {EventTrackerOpts} */ (assign(defaultOpts, opts));\n\n this.tracker = tracker;\n\n // Binds methods.\n this.handleEvents = this.handleEvents.bind(this);\n\n const selector = '[' + this.opts.attributePrefix + 'on]';\n\n // Creates a mapping of events to their delegates\n this.delegates = {};\n this.opts.events.forEach((event) => {\n this.delegates[event] = delegate(document, event, selector,\n this.handleEvents, {composed: true, useCapture: true});\n });\n }\n\n /**\n * Handles all events on elements with event attributes.\n * @param {Event} event The DOM click event.\n * @param {Element} element The delegated DOM element target.\n */\n handleEvents(event, element) {\n const prefix = this.opts.attributePrefix;\n const events = element.getAttribute(prefix + 'on').split(/\\s*,\\s*/);\n\n // Ensures the type matches one of the events specified on the element.\n if (events.indexOf(event.type) < 0) return;\n\n /** @type {FieldsObj} */\n const defaultFields = {transport: 'beacon'};\n const attributeFields = getAttributeFields(element, prefix);\n const userFields = assign({}, this.opts.fieldsObj, attributeFields);\n const hitType = attributeFields.hitType || 'event';\n\n this.tracker.send(hitType, createFieldsObj(defaultFields,\n userFields, this.tracker, this.opts.hitFilter, element, event));\n }\n\n /**\n * Removes all event listeners and instance properties.\n */\n remove() {\n Object.keys(this.delegates).forEach((key) => {\n this.delegates[key].destroy();\n });\n }\n}\n\n\nprovide('eventTracker', EventTracker);\n","/**\n * Gets all attributes of an element as a plain JavaScriot object.\n * @param {Element} element The element whose attributes to get.\n * @return {!Object} An object whose keys are the attribute keys and whose\n * values are the attribute values. If no attributes exist, an empty\n * object is returned.\n */\nexport default function getAttributes(element) {\n const attrs = {};\n\n // Validate input.\n if (!(element && element.nodeType == 1)) return attrs;\n\n // Return an empty object if there are no attributes.\n const map = element.attributes;\n if (map.length === 0) return {};\n\n for (let i = 0, attr; attr = map[i]; i++) {\n attrs[attr.name] = attr.value;\n }\n return attrs;\n}\n","const HTTP_PORT = '80';\nconst HTTPS_PORT = '443';\nconst DEFAULT_PORT = RegExp(':(' + HTTP_PORT + '|' + HTTPS_PORT + ')$');\n\n\nconst a = document.createElement('a');\nconst cache = {};\n\n\n/**\n * Parses the given url and returns an object mimicing a `Location` object.\n * @param {string} url The url to parse.\n * @return {!Object} An object with the same properties as a `Location`.\n */\nexport default function parseUrl(url) {\n // All falsy values (as well as \".\") should map to the current URL.\n url = (!url || url == '.') ? location.href : url;\n\n if (cache[url]) return cache[url];\n\n a.href = url;\n\n // When parsing file relative paths (e.g. `../index.html`), IE will correctly\n // resolve the `href` property but will keep the `..` in the `path` property.\n // It will also not include the `host` or `hostname` properties. Furthermore,\n // IE will sometimes return no protocol or just a colon, especially for things\n // like relative protocol URLs (e.g. \"//google.com\").\n // To workaround all of these issues, we reparse with the full URL from the\n // `href` property.\n if (url.charAt(0) == '.' || url.charAt(0) == '/') return parseUrl(a.href);\n\n // Don't include default ports.\n let port = (a.port == HTTP_PORT || a.port == HTTPS_PORT) ? '' : a.port;\n\n // PhantomJS sets the port to \"0\" when using the file: protocol.\n port = port == '0' ? '' : port;\n\n // Sometimes IE incorrectly includes a port for default ports\n // (e.g. `:80` or `:443`) even when no port is specified in the URL.\n // http://bit.ly/1rQNoMg\n const host = a.host.replace(DEFAULT_PORT, '');\n\n // Not all browser support `origin` so we have to build it.\n const origin = a.origin ? a.origin : a.protocol + '//' + host;\n\n // Sometimes IE doesn't include the leading slash for pathname.\n // http://bit.ly/1rQNoMg\n const pathname = a.pathname.charAt(0) == '/' ? a.pathname : '/' + a.pathname;\n\n return cache[url] = {\n hash: a.hash,\n host: host,\n hostname: a.hostname,\n href: a.href,\n origin: origin,\n pathname: pathname,\n port: port,\n protocol: a.protocol,\n search: a.search,\n };\n}\n","/**\n * Copyright 2017 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\n/**\n * @fileoverview\n * The functions exported by this module make it easier (and safer) to override\n * foreign object methods (in a modular way) and respond to or modify their\n * invocation. The primary feature is the ability to override a method without\n * worrying if it's already been overridden somewhere else in the codebase. It\n * also allows for safe restoring of an overridden method by only fully\n * restoring a method once all overrides have been removed.\n */\n\n\nconst instances = [];\n\n\n/**\n * A class that wraps a foreign object method and emit events before and\n * after the original method is called.\n */\nexport default class MethodChain {\n /**\n * Adds the passed override method to the list of method chain overrides.\n * @param {!Object} context The object containing the method to chain.\n * @param {string} methodName The name of the method on the object.\n * @param {!Function} methodOverride The override method to add.\n */\n static add(context, methodName, methodOverride) {\n getOrCreateMethodChain(context, methodName).add(methodOverride);\n }\n\n /**\n * Removes a method chain added via `add()`. If the override is the\n * only override added, the original method is restored.\n * @param {!Object} context The object containing the method to unchain.\n * @param {string} methodName The name of the method on the object.\n * @param {!Function} methodOverride The override method to remove.\n */\n static remove(context, methodName, methodOverride) {\n getOrCreateMethodChain(context, methodName).remove(methodOverride);\n }\n\n /**\n * Wraps a foreign object method and overrides it. Also stores a reference\n * to the original method so it can be restored later.\n * @param {!Object} context The object containing the method.\n * @param {string} methodName The name of the method on the object.\n */\n constructor(context, methodName) {\n this.context = context;\n this.methodName = methodName;\n this.isTask = /Task$/.test(methodName);\n\n this.originalMethodReference = this.isTask ?\n context.get(methodName) : context[methodName];\n\n this.methodChain = [];\n this.boundMethodChain = [];\n\n // Wraps the original method.\n this.wrappedMethod = (...args) => {\n const lastBoundMethod =\n this.boundMethodChain[this.boundMethodChain.length - 1];\n\n return lastBoundMethod(...args);\n };\n\n // Override original method with the wrapped one.\n if (this.isTask) {\n context.set(methodName, this.wrappedMethod);\n } else {\n context[methodName] = this.wrappedMethod;\n }\n }\n\n /**\n * Adds a method to the method chain.\n * @param {!Function} overrideMethod The override method to add.\n */\n add(overrideMethod) {\n this.methodChain.push(overrideMethod);\n this.rebindMethodChain();\n }\n\n /**\n * Removes a method from the method chain and restores the prior order.\n * @param {!Function} overrideMethod The override method to remove.\n */\n remove(overrideMethod) {\n const index = this.methodChain.indexOf(overrideMethod);\n if (index > -1) {\n this.methodChain.splice(index, 1);\n if (this.methodChain.length > 0) {\n this.rebindMethodChain();\n } else {\n this.destroy();\n }\n }\n }\n\n /**\n * Loops through the method chain array and recreates the bound method\n * chain array. This is necessary any time a method is added or removed\n * to ensure proper original method context and order.\n */\n rebindMethodChain() {\n this.boundMethodChain = [];\n for (let method, i = 0; method = this.methodChain[i]; i++) {\n const previousMethod = this.boundMethodChain[i - 1] ||\n this.originalMethodReference.bind(this.context);\n this.boundMethodChain.push(method(previousMethod));\n }\n }\n\n /**\n * Calls super and destroys the instance if no registered handlers remain.\n */\n destroy() {\n const index = instances.indexOf(this);\n if (index > -1) {\n instances.splice(index, 1);\n if (this.isTask) {\n this.context.set(this.methodName, this.originalMethodReference);\n } else {\n this.context[this.methodName] = this.originalMethodReference;\n }\n }\n }\n}\n\n\n/**\n * Gets a MethodChain instance for the passed object and method. If the method\n * has already been wrapped via an existing MethodChain instance, that\n * instance is returned.\n * @param {!Object} context The object containing the method.\n * @param {string} methodName The name of the method on the object.\n * @return {!MethodChain}\n */\nfunction getOrCreateMethodChain(context, methodName) {\n let methodChain = instances\n .filter((h) => h.context == context && h.methodName == methodName)[0];\n\n if (!methodChain) {\n methodChain = new MethodChain(context, methodName);\n instances.push(methodChain);\n }\n return methodChain;\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {getAttributes} from 'dom-utils';\nimport MethodChain from './method-chain';\n\n\n/**\n * Accepts default and user override fields and an optional tracker, hit\n * filter, and target element and returns a single object that can be used in\n * `ga('send', ...)` commands.\n * @param {FieldsObj} defaultFields The default fields to return.\n * @param {FieldsObj} userFields Fields set by the user to override the\n * defaults.\n * @param {Tracker=} tracker The tracker object to apply the hit filter to.\n * @param {Function=} hitFilter A filter function that gets\n * called with the tracker model right before the `buildHitTask`. It can\n * be used to modify the model for the current hit only.\n * @param {Element=} target If the hit originated from an interaction\n * with a DOM element, hitFilter is invoked with that element as the\n * second argument.\n * @param {(Event|TwttrEvent)=} event If the hit originated via a DOM event,\n * hitFilter is invoked with that event as the third argument.\n * @return {!FieldsObj} The final fields object.\n */\nexport function createFieldsObj(\n defaultFields, userFields, tracker = undefined,\n hitFilter = undefined, target = undefined, event = undefined) {\n if (typeof hitFilter == 'function') {\n const originalBuildHitTask = tracker.get('buildHitTask');\n return {\n buildHitTask: (/** @type {!Model} */ model) => {\n model.set(defaultFields, null, true);\n model.set(userFields, null, true);\n hitFilter(model, target, event);\n originalBuildHitTask(model);\n },\n };\n } else {\n return assign({}, defaultFields, userFields);\n }\n}\n\n\n/**\n * Retrieves the attributes from an DOM element and returns a fields object\n * for all attributes matching the passed prefix string.\n * @param {Element} element The DOM element to get attributes from.\n * @param {string} prefix An attribute prefix. Only the attributes matching\n * the prefix will be returned on the fields object.\n * @return {FieldsObj} An object of analytics.js fields and values\n */\nexport function getAttributeFields(element, prefix) {\n const attributes = getAttributes(element);\n const attributeFields = {};\n\n Object.keys(attributes).forEach(function(attribute) {\n // The `on` prefix is used for event handling but isn't a field.\n if (attribute.indexOf(prefix) === 0 && attribute != prefix + 'on') {\n let value = attributes[attribute];\n\n // Detects Boolean value strings.\n if (value == 'true') value = true;\n if (value == 'false') value = false;\n\n const field = camelCase(attribute.slice(prefix.length));\n attributeFields[field] = value;\n }\n });\n\n return attributeFields;\n}\n\n\n/**\n * Accepts a function to be invoked once the DOM is ready. If the DOM is\n * already ready, the callback is invoked immediately.\n * @param {!Function} callback The ready callback.\n */\nexport function domReady(callback) {\n if (document.readyState == 'loading') {\n document.addEventListener('DOMContentLoaded', function fn() {\n document.removeEventListener('DOMContentLoaded', fn);\n callback();\n });\n } else {\n callback();\n }\n}\n\n\n/**\n * Returns a function, that, as long as it continues to be called, will not\n * actually run. The function will only run after it stops being called for\n * `wait` milliseconds.\n * @param {!Function} fn The function to debounce.\n * @param {number} wait The debounce wait timeout in ms.\n * @return {!Function} The debounced function.\n */\nexport function debounce(fn, wait) {\n let timeout;\n return function(...args) {\n clearTimeout(timeout);\n timeout = setTimeout(() => fn(...args), wait);\n };\n}\n\n\n/**\n * Accepts a function and returns a wrapped version of the function that is\n * expected to be called elsewhere in the system. If it's not called\n * elsewhere after the timeout period, it's called regardless. The wrapper\n * function also prevents the callback from being called more than once.\n * @param {!Function} callback The function to call.\n * @param {number=} wait How many milliseconds to wait before invoking\n * the callback.\n * @return {!Function} The wrapped version of the passed function.\n */\nexport function withTimeout(callback, wait = 2000) {\n let called = false;\n const fn = function() {\n if (!called) {\n called = true;\n callback();\n }\n };\n setTimeout(fn, wait);\n return fn;\n}\n\n// Maps trackers to queue by tracking ID.\nconst queueMap = {};\n\n/**\n * Queues a function for execution in the next call stack, or immediately\n * before any send commands are executed on the tracker. This allows\n * autotrack plugins to defer running commands until after all other plugins\n * are required but before any other hits are sent.\n * @param {!Tracker} tracker\n * @param {!Function} fn\n */\nexport function deferUntilPluginsLoaded(tracker, fn) {\n const trackingId = tracker.get('trackingId');\n const ref = queueMap[trackingId] = queueMap[trackingId] || {};\n\n const processQueue = () => {\n clearTimeout(ref.timeout);\n if (ref.send) {\n MethodChain.remove(tracker, 'send', ref.send);\n }\n delete queueMap[trackingId];\n\n ref.queue.forEach((fn) => fn());\n };\n\n clearTimeout(ref.timeout);\n ref.timeout = setTimeout(processQueue, 0);\n ref.queue = ref.queue || [];\n ref.queue.push(fn);\n\n if (!ref.send) {\n ref.send = (originalMethod) => {\n return (...args) => {\n processQueue();\n originalMethod(...args);\n };\n };\n MethodChain.add(tracker, 'send', ref.send);\n }\n}\n\n\n/**\n * A small shim of Object.assign that aims for brevity over spec-compliant\n * handling all the edge cases.\n * @param {!Object} target The target object to assign to.\n * @param {...?Object} sources Additional objects who properties should be\n * assigned to target. Non-objects are converted to objects.\n * @return {!Object} The modified target object.\n */\nexport const assign = Object.assign || function(target, ...sources) {\n for (let i = 0, len = sources.length; i < len; i++) {\n const source = Object(sources[i]);\n for (let key in source) {\n if (Object.prototype.hasOwnProperty.call(source, key)) {\n target[key] = source[key];\n }\n }\n }\n return target;\n};\n\n\n/**\n * Accepts a string containing hyphen or underscore word separators and\n * converts it to camelCase.\n * @param {string} str The string to camelCase.\n * @return {string} The camelCased version of the string.\n */\nexport function camelCase(str) {\n return str.replace(/[\\-\\_]+(\\w?)/g, function(match, p1) {\n return p1.toUpperCase();\n });\n}\n\n\n/**\n * Capitalizes the first letter of a string.\n * @param {string} str The input string.\n * @return {string} The capitalized string\n */\nexport function capitalize(str) {\n return str.charAt(0).toUpperCase() + str.slice(1);\n}\n\n\n/**\n * Indicates whether the passed variable is a JavaScript object.\n * @param {*} value The input variable to test.\n * @return {boolean} Whether or not the test is an object.\n */\nexport function isObject(value) {\n return typeof value == 'object' && value !== null;\n}\n\n\n/**\n * Accepts a value that may or may not be an array. If it is not an array,\n * it is returned as the first item in a single-item array.\n * @param {*} value The value to convert to an array if it is not.\n * @return {!Array} The array-ified value.\n */\nexport function toArray(value) {\n return Array.isArray(value) ? value : [value];\n}\n\n\n/**\n * @return {number} The current date timestamp\n */\nexport function now() {\n return +new Date();\n}\n\n\n/*eslint-disable */\n// https://gist.github.com/jed/982883\n/** @param {?=} a */\nexport const uuid = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)};\n/*eslint-enable */\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {DEV_ID} from './constants';\nimport {capitalize} from './utilities';\n\n\n/**\n * Provides a plugin for use with analytics.js, accounting for the possibility\n * that the global command queue has been renamed or not yet defined.\n * @param {string} pluginName The plugin name identifier.\n * @param {Function} pluginConstructor The plugin constructor function.\n */\nexport default function provide(pluginName, pluginConstructor) {\n const gaAlias = window.GoogleAnalyticsObject || 'ga';\n window[gaAlias] = window[gaAlias] || function(...args) {\n (window[gaAlias].q = window[gaAlias].q || []).push(args);\n };\n\n // Adds the autotrack dev ID if not already included.\n window.gaDevIds = window.gaDevIds || [];\n if (window.gaDevIds.indexOf(DEV_ID) < 0) {\n window.gaDevIds.push(DEV_ID);\n }\n\n // Formally provides the plugin for use with analytics.js.\n window[gaAlias]('provide', pluginName, pluginConstructor);\n\n // Registers the plugin on the global gaplugins object.\n window.gaplugins = window.gaplugins || {};\n window.gaplugins[capitalize(pluginName)] = pluginConstructor;\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nexport const VERSION = '2.4.1';\nexport const DEV_ID = 'i5iSjo';\n\nexport const VERSION_PARAM = '_av';\nexport const USAGE_PARAM = '_au';\n\nexport const NULL_DIMENSION = '(not set)';\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {USAGE_PARAM, VERSION, VERSION_PARAM} from './constants';\n\n\nexport const plugins = {\n CLEAN_URL_TRACKER: 1,\n EVENT_TRACKER: 2,\n IMPRESSION_TRACKER: 3,\n MEDIA_QUERY_TRACKER: 4,\n OUTBOUND_FORM_TRACKER: 5,\n OUTBOUND_LINK_TRACKER: 6,\n PAGE_VISIBILITY_TRACKER: 7,\n SOCIAL_WIDGET_TRACKER: 8,\n URL_CHANGE_TRACKER: 9,\n MAX_SCROLL_TRACKER: 10,\n};\n\n\nconst PLUGIN_COUNT = Object.keys(plugins).length;\n\n\n/**\n * Tracks the usage of the passed plugin by encoding a value into a usage\n * string sent with all hits for the passed tracker.\n * @param {!Tracker} tracker The analytics.js tracker object.\n * @param {number} plugin The plugin enum.\n */\nexport function trackUsage(tracker, plugin) {\n trackVersion(tracker);\n trackPlugin(tracker, plugin);\n}\n\n\n/**\n * Converts a hexadecimal string to a binary string.\n * @param {string} hex A hexadecimal numeric string.\n * @return {string} a binary numeric string.\n */\nfunction convertHexToBin(hex) {\n return parseInt(hex || '0', 16).toString(2);\n}\n\n\n/**\n * Converts a binary string to a hexadecimal string.\n * @param {string} bin A binary numeric string.\n * @return {string} a hexadecimal numeric string.\n */\nfunction convertBinToHex(bin) {\n return parseInt(bin || '0', 2).toString(16);\n}\n\n\n/**\n * Adds leading zeros to a string if it's less than a minimum length.\n * @param {string} str A string to pad.\n * @param {number} len The minimum length of the string\n * @return {string} The padded string.\n */\nfunction padZeros(str, len) {\n if (str.length < len) {\n let toAdd = len - str.length;\n while (toAdd) {\n str = '0' + str;\n toAdd--;\n }\n }\n return str;\n}\n\n\n/**\n * Accepts a binary numeric string and flips the digit from 0 to 1 at the\n * specified index.\n * @param {string} str The binary numeric string.\n * @param {number} index The index to flip the bit.\n * @return {string} The new binary string with the bit flipped on\n */\nfunction flipBitOn(str, index) {\n return str.substr(0, index) + 1 + str.substr(index + 1);\n}\n\n\n/**\n * Accepts a tracker and a plugin index and flips the bit at the specified\n * index on the tracker's usage parameter.\n * @param {Object} tracker An analytics.js tracker.\n * @param {number} pluginIndex The index of the plugin in the global list.\n */\nfunction trackPlugin(tracker, pluginIndex) {\n const usageHex = tracker.get('&' + USAGE_PARAM);\n let usageBin = padZeros(convertHexToBin(usageHex), PLUGIN_COUNT);\n\n // Flip the bit of the plugin being tracked.\n usageBin = flipBitOn(usageBin, PLUGIN_COUNT - pluginIndex);\n\n // Stores the modified usage string back on the tracker.\n tracker.set('&' + USAGE_PARAM, convertBinToHex(usageBin));\n}\n\n\n/**\n * Accepts a tracker and adds the current version to the version param.\n * @param {Object} tracker An analytics.js tracker.\n */\nfunction trackVersion(tracker) {\n tracker.set('&' + VERSION_PARAM, VERSION);\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {parseUrl} from 'dom-utils';\nimport {NULL_DIMENSION} from '../constants';\nimport MethodChain from '../method-chain';\nimport provide from '../provide';\nimport {plugins, trackUsage} from '../usage';\nimport {assign} from '../utilities';\n\n\n/**\n * Class for the `cleanUrlTracker` analytics.js plugin.\n * @implements {CleanUrlTrackerPublicInterface}\n */\nclass CleanUrlTracker {\n /**\n * Registers clean URL tracking on a tracker object. The clean URL tracker\n * removes query parameters from the page value reported to Google Analytics.\n * It also helps to prevent tracking similar URLs, e.g. sometimes ending a\n * URL with a slash and sometimes not.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?CleanUrlTrackerOpts} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.CLEAN_URL_TRACKER);\n\n /** @type {CleanUrlTrackerOpts} */\n const defaultOpts = {\n // stripQuery: undefined,\n // queryParamsWhitelist: undefined,\n // queryDimensionIndex: undefined,\n // indexFilename: undefined,\n // trailingSlash: undefined,\n // urlFilter: undefined,\n };\n this.opts = /** @type {CleanUrlTrackerOpts} */ (assign(defaultOpts, opts));\n\n this.tracker = tracker;\n\n /** @type {string|null} */\n this.queryDimension = this.opts.stripQuery &&\n this.opts.queryDimensionIndex ?\n `dimension${this.opts.queryDimensionIndex}` : null;\n\n // Binds methods to `this`.\n this.trackerGetOverride = this.trackerGetOverride.bind(this);\n this.buildHitTaskOverride = this.buildHitTaskOverride.bind(this);\n\n // Override built-in tracker method to watch for changes.\n MethodChain.add(tracker, 'get', this.trackerGetOverride);\n MethodChain.add(tracker, 'buildHitTask', this.buildHitTaskOverride);\n }\n\n /**\n * Ensures reads of the tracker object by other plugins always see the\n * \"cleaned\" versions of all URL fields.\n * @param {function(string):*} originalMethod A reference to the overridden\n * method.\n * @return {function(string):*}\n */\n trackerGetOverride(originalMethod) {\n return (field) => {\n if (field == 'page' || field == this.queryDimension) {\n const fieldsObj = /** @type {!FieldsObj} */ ({\n location: originalMethod('location'),\n page: originalMethod('page'),\n });\n const cleanedFieldsObj = this.cleanUrlFields(fieldsObj);\n return cleanedFieldsObj[field];\n } else {\n return originalMethod(field);\n }\n };\n }\n\n /**\n * Cleans URL fields passed in a send command.\n * @param {function(!Model)} originalMethod A reference to the\n * overridden method.\n * @return {function(!Model)}\n */\n buildHitTaskOverride(originalMethod) {\n return (model) => {\n const cleanedFieldsObj = this.cleanUrlFields({\n location: model.get('location'),\n page: model.get('page'),\n });\n model.set(cleanedFieldsObj, null, true);\n originalMethod(model);\n };\n }\n\n /**\n * Accepts of fields object containing URL fields and returns a new\n * fields object with the URLs \"cleaned\" according to the tracker options.\n * @param {!FieldsObj} fieldsObj\n * @return {!FieldsObj}\n */\n cleanUrlFields(fieldsObj) {\n const url = parseUrl(\n /** @type {string} */ (fieldsObj.page || fieldsObj.location));\n\n let pathname = url.pathname;\n\n // If an index filename was provided, remove it if it appears at the end\n // of the URL.\n if (this.opts.indexFilename) {\n const parts = pathname.split('/');\n if (this.opts.indexFilename == parts[parts.length - 1]) {\n parts[parts.length - 1] = '';\n pathname = parts.join('/');\n }\n }\n\n // Ensure the URL ends with or doesn't end with slash based on the\n // `trailingSlash` option. Note that filename URLs should never contain\n // a trailing slash.\n if (this.opts.trailingSlash == 'remove') {\n pathname = pathname.replace(/\\/+$/, '');\n } else if (this.opts.trailingSlash == 'add') {\n const isFilename = /\\.\\w+$/.test(pathname);\n if (!isFilename && pathname.substr(-1) != '/') {\n pathname = pathname + '/';\n }\n }\n\n /** @type {!FieldsObj} */\n const cleanedFieldsObj = {\n page: pathname + (this.opts.stripQuery ?\n this.stripNonWhitelistedQueryParams(url.search) : url.search),\n };\n if (fieldsObj.location) {\n cleanedFieldsObj.location = fieldsObj.location;\n }\n if (this.queryDimension) {\n cleanedFieldsObj[this.queryDimension] =\n url.search.slice(1) || NULL_DIMENSION;\n }\n\n // Apply the `urlFieldsFilter()` option if passed.\n if (typeof this.opts.urlFieldsFilter == 'function') {\n /** @type {!FieldsObj} */\n const userCleanedFieldsObj =\n this.opts.urlFieldsFilter(cleanedFieldsObj, parseUrl);\n\n // Ensure only the URL fields are returned.\n const returnValue = {\n page: userCleanedFieldsObj.page,\n location: userCleanedFieldsObj.location,\n };\n if (this.queryDimension) {\n returnValue[this.queryDimension] =\n userCleanedFieldsObj[this.queryDimension];\n }\n return returnValue;\n } else {\n return cleanedFieldsObj;\n }\n }\n\n /**\n * Accpets a raw URL search string and returns a new search string containing\n * only the site search params (if they exist).\n * @param {string} searchString The URL search string (starting with '?').\n * @return {string} The query string\n */\n stripNonWhitelistedQueryParams(searchString) {\n if (Array.isArray(this.opts.queryParamsWhitelist)) {\n const foundParams = [];\n searchString.slice(1).split('&').forEach((kv) => {\n const [key, value] = kv.split('=');\n if (this.opts.queryParamsWhitelist.indexOf(key) > -1 && value) {\n foundParams.push([key, value]);\n }\n });\n\n return foundParams.length ?\n '?' + foundParams.map((kv) => kv.join('=')).join('&') : '';\n } else {\n return '';\n }\n }\n\n /**\n * Restores all overridden tasks and methods.\n */\n remove() {\n MethodChain.remove(this.tracker, 'get', this.trackerGetOverride);\n MethodChain.remove(this.tracker, 'buildHitTask', this.buildHitTaskOverride);\n }\n}\n\n\nprovide('cleanUrlTracker', CleanUrlTracker);\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport provide from '../provide';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj,\n domReady, getAttributeFields} from '../utilities';\n\n\n/**\n * Class for the `impressionTracker` analytics.js plugin.\n * @implements {ImpressionTrackerPublicInterface}\n */\nclass ImpressionTracker {\n /**\n * Registers impression tracking.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?ImpressionTrackerOpts} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.IMPRESSION_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!(window.IntersectionObserver && window.MutationObserver)) return;\n\n /** type {ImpressionTrackerOpts} */\n const defaultOptions = {\n // elements: undefined,\n rootMargin: '0px',\n fieldsObj: {},\n attributePrefix: 'ga-',\n // hitFilter: undefined,\n };\n\n this.opts = /** type {ImpressionTrackerOpts} */ (\n assign(defaultOptions, opts));\n\n this.tracker = tracker;\n\n // Binds methods.\n this.handleDomMutations = this.handleDomMutations.bind(this);\n this.handleIntersectionChanges = this.handleIntersectionChanges.bind(this);\n this.handleDomElementAdded = this.handleDomElementAdded.bind(this);\n this.handleDomElementRemoved = this.handleDomElementRemoved.bind(this);\n\n /** @type {MutationObserver} */\n this.mutationObserver = null;\n\n // The primary list of elements to observe. Each item contains the\n // element ID, threshold, and whether it's currently in-view.\n this.items = [];\n\n // A map of element IDs in the `items` array to DOM elements in the\n // document. The presence of a key indicates that the element ID is in the\n // `items` array, and the presence of an element value indicates that the\n // element is in the DOM.\n this.elementMap = {};\n\n // A map of threshold values. Each threshold is mapped to an\n // IntersectionObserver instance specific to that threshold.\n this.thresholdMap = {};\n\n // Once the DOM is ready, start observing for changes (if present).\n domReady(() => {\n if (this.opts.elements) {\n this.observeElements(this.opts.elements);\n }\n });\n }\n\n /**\n * Starts observing the passed elements for impressions.\n * @param {Array} elements\n */\n observeElements(elements) {\n const data = this.deriveDataFromElements(elements);\n\n // Merge the new data with the data already on the plugin instance.\n this.items = this.items.concat(data.items);\n this.elementMap = assign({}, data.elementMap, this.elementMap);\n this.thresholdMap = assign({}, data.thresholdMap, this.thresholdMap);\n\n // Observe each new item.\n data.items.forEach((item) => {\n const observer = this.thresholdMap[item.threshold] =\n (this.thresholdMap[item.threshold] || new IntersectionObserver(\n this.handleIntersectionChanges, {\n rootMargin: this.opts.rootMargin,\n threshold: [+item.threshold],\n }));\n\n const element = this.elementMap[item.id] ||\n (this.elementMap[item.id] = document.getElementById(item.id));\n\n if (element) {\n observer.observe(element);\n }\n });\n\n if (!this.mutationObserver) {\n this.mutationObserver = new MutationObserver(this.handleDomMutations);\n this.mutationObserver.observe(document.body, {\n childList: true,\n subtree: true,\n });\n }\n\n // TODO(philipwalton): Remove temporary hack to force a new frame\n // immediately after adding observers.\n // https://bugs.chromium.org/p/chromium/issues/detail?id=612323\n requestAnimationFrame(() => {});\n }\n\n /**\n * Stops observing the passed elements for impressions.\n * @param {Array} elements\n * @return {undefined}\n */\n unobserveElements(elements) {\n const itemsToKeep = [];\n const itemsToRemove = [];\n\n this.items.forEach((item) => {\n const itemInItems = elements.some((element) => {\n const itemToRemove = getItemFromElement(element);\n return itemToRemove.id === item.id &&\n itemToRemove.threshold === item.threshold &&\n itemToRemove.trackFirstImpressionOnly ===\n item.trackFirstImpressionOnly;\n });\n if (itemInItems) {\n itemsToRemove.push(item);\n } else {\n itemsToKeep.push(item);\n }\n });\n\n // If there are no items to keep, run the `unobserveAllElements` logic.\n if (!itemsToKeep.length) {\n this.unobserveAllElements();\n } else {\n const dataToKeep = this.deriveDataFromElements(itemsToKeep);\n const dataToRemove = this.deriveDataFromElements(itemsToRemove);\n\n this.items = dataToKeep.items;\n this.elementMap = dataToKeep.elementMap;\n this.thresholdMap = dataToKeep.thresholdMap;\n\n // Unobserve removed elements.\n itemsToRemove.forEach((item) => {\n if (!dataToKeep.elementMap[item.id]) {\n const observer = dataToRemove.thresholdMap[item.threshold];\n const element = dataToRemove.elementMap[item.id];\n\n if (element) {\n observer.unobserve(element);\n }\n\n // Disconnect unneeded threshold observers.\n if (!dataToKeep.thresholdMap[item.threshold]) {\n dataToRemove.thresholdMap[item.threshold].disconnect();\n }\n }\n });\n }\n }\n\n /**\n * Stops observing all currently observed elements.\n */\n unobserveAllElements() {\n Object.keys(this.thresholdMap).forEach((key) => {\n this.thresholdMap[key].disconnect();\n });\n\n this.mutationObserver.disconnect();\n this.mutationObserver = null;\n\n this.items = [];\n this.elementMap = {};\n this.thresholdMap = {};\n }\n\n /**\n * Loops through each of the passed elements and creates a map of element IDs,\n * threshold values, and a list of \"items\" (which contains each element's\n * `threshold` and `trackFirstImpressionOnly` property).\n * @param {Array} elements A list of elements to derive item data from.\n * @return {Object} An object with the properties `items`, `elementMap`\n * and `threshold`.\n */\n deriveDataFromElements(elements) {\n const items = [];\n const thresholdMap = {};\n const elementMap = {};\n\n if (elements.length) {\n elements.forEach((element) => {\n const item = getItemFromElement(element);\n\n items.push(item);\n elementMap[item.id] = this.elementMap[item.id] || null;\n thresholdMap[item.threshold] =\n this.thresholdMap[item.threshold] || null;\n });\n }\n\n return {items, elementMap, thresholdMap};\n }\n\n /**\n * Handles nodes being added or removed from the DOM. This function is passed\n * as the callback to `this.mutationObserver`.\n * @param {Array} mutations A list of `MutationRecord` instances\n */\n handleDomMutations(mutations) {\n for (let i = 0, mutation; mutation = mutations[i]; i++) {\n // Handles removed elements.\n for (let k = 0, removedEl; removedEl = mutation.removedNodes[k]; k++) {\n this.walkNodeTree(removedEl, this.handleDomElementRemoved);\n }\n // Handles added elements.\n for (let j = 0, addedEl; addedEl = mutation.addedNodes[j]; j++) {\n this.walkNodeTree(addedEl, this.handleDomElementAdded);\n }\n }\n }\n\n /**\n * Iterates through all descendents of a DOM node and invokes the passed\n * callback if any of them match an elememt in `elementMap`.\n * @param {Node} node The DOM node to walk.\n * @param {Function} callback A function to be invoked if a match is found.\n */\n walkNodeTree(node, callback) {\n if (node.nodeType == 1 && node.id in this.elementMap) {\n callback(node.id);\n }\n for (let i = 0, child; child = node.childNodes[i]; i++) {\n this.walkNodeTree(child, callback);\n }\n }\n\n /**\n * Handles intersection changes. This function is passed as the callback to\n * `this.intersectionObserver`\n * @param {Array} records A list of `IntersectionObserverEntry` records.\n */\n handleIntersectionChanges(records) {\n const itemsToRemove = [];\n for (let i = 0, record; record = records[i]; i++) {\n for (let j = 0, item; item = this.items[j]; j++) {\n if (record.target.id !== item.id) continue;\n\n if (isTargetVisible(item.threshold, record)) {\n this.handleImpression(item.id);\n\n if (item.trackFirstImpressionOnly) {\n itemsToRemove.push(item);\n }\n }\n }\n }\n if (itemsToRemove.length) {\n this.unobserveElements(itemsToRemove);\n }\n }\n\n /**\n * Sends a hit to Google Analytics with the impression data.\n * @param {string} id The ID of the element making the impression.\n */\n handleImpression(id) {\n const element = document.getElementById(id);\n\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n eventCategory: 'Viewport',\n eventAction: 'impression',\n eventLabel: id,\n nonInteraction: true,\n };\n\n /** @type {FieldsObj} */\n const userFields = assign({}, this.opts.fieldsObj,\n getAttributeFields(element, this.opts.attributePrefix));\n\n this.tracker.send('event', createFieldsObj(defaultFields,\n userFields, this.tracker, this.opts.hitFilter, element));\n }\n\n /**\n * Handles an element in the items array being added to the DOM.\n * @param {string} id The ID of the element that was added.\n */\n handleDomElementAdded(id) {\n const element = this.elementMap[id] = document.getElementById(id);\n this.items.forEach((item) => {\n if (id == item.id) {\n this.thresholdMap[item.threshold].observe(element);\n }\n });\n }\n\n /**\n * Handles an element currently being observed for intersections being\n * removed from the DOM.\n * @param {string} id The ID of the element that was removed.\n */\n handleDomElementRemoved(id) {\n const element = this.elementMap[id];\n this.items.forEach((item) => {\n if (id == item.id) {\n this.thresholdMap[item.threshold].unobserve(element);\n }\n });\n\n this.elementMap[id] = null;\n }\n\n /**\n * Removes all listeners and observers.\n * @private\n */\n remove() {\n this.unobserveAllElements();\n }\n}\n\n\nprovide('impressionTracker', ImpressionTracker);\n\n\n/**\n * Detects whether or not an intersection record represents a visible target\n * given a particular threshold.\n * @param {number} threshold The threshold the target is visible above.\n * @param {IntersectionObserverEntry} record The most recent record entry.\n * @return {boolean} True if the target is visible.\n */\nfunction isTargetVisible(threshold, record) {\n if (threshold === 0) {\n const i = record.intersectionRect;\n return i.top > 0 || i.bottom > 0 || i.left > 0 || i.right > 0;\n } else {\n return record.intersectionRatio >= threshold;\n }\n}\n\n\n/**\n * Creates an item by merging the passed element with the item defaults.\n * If the passed element is just a string, that string is treated as\n * the item ID.\n * @param {!ImpressionTrackerElementsItem|string} element The element to\n * convert to an item.\n * @return {!ImpressionTrackerElementsItem} The item object.\n */\nfunction getItemFromElement(element) {\n /** @type {ImpressionTrackerElementsItem} */\n const defaultOpts = {\n threshold: 0,\n trackFirstImpressionOnly: true,\n };\n\n if (typeof element == 'string') {\n element = /** @type {!ImpressionTrackerElementsItem} */ ({id: element});\n }\n\n return assign(defaultOpts, element);\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\n/**\n * An simple reimplementation of the native Node.js EventEmitter class.\n * The goal of this implementation is to be as small as possible.\n */\nexport default class EventEmitter {\n /**\n * Creates the event registry.\n */\n constructor() {\n this.registry_ = {};\n }\n\n /**\n * Adds a handler function to the registry for the passed event.\n * @param {string} event The event name.\n * @param {!Function} fn The handler to be invoked when the passed\n * event is emitted.\n */\n on(event, fn) {\n this.getRegistry_(event).push(fn);\n }\n\n /**\n * Removes a handler function from the registry for the passed event.\n * @param {string=} event The event name.\n * @param {Function=} fn The handler to be removed.\n */\n off(event = undefined, fn = undefined) {\n if (event && fn) {\n const eventRegistry = this.getRegistry_(event);\n const handlerIndex = eventRegistry.indexOf(fn);\n if (handlerIndex > -1) {\n eventRegistry.splice(handlerIndex, 1);\n }\n } else {\n this.registry_ = {};\n }\n }\n\n /**\n * Runs all registered handlers for the passed event with the optional args.\n * @param {string} event The event name.\n * @param {...*} args The arguments to be passed to the handler.\n */\n emit(event, ...args) {\n this.getRegistry_(event).forEach((fn) => fn(...args));\n }\n\n /**\n * Returns the total number of event handlers currently registered.\n * @return {number}\n */\n getEventCount() {\n let eventCount = 0;\n Object.keys(this.registry_).forEach((event) => {\n eventCount += this.getRegistry_(event).length;\n });\n return eventCount;\n }\n\n /**\n * Returns an array of handlers associated with the passed event name.\n * If no handlers have been registered, an empty array is returned.\n * @private\n * @param {string} event The event name.\n * @return {!Array} An array of handler functions.\n */\n getRegistry_(event) {\n return this.registry_[event] = (this.registry_[event] || []);\n }\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport EventEmitter from './event-emitter';\nimport {assign} from './utilities';\n\n\nconst AUTOTRACK_PREFIX = 'autotrack';\nconst instances = {};\nlet isListening = false;\n\n\n/** @type {boolean|undefined} */\nlet browserSupportsLocalStorage;\n\n\n/**\n * A storage object to simplify interacting with localStorage.\n */\nexport default class Store extends EventEmitter {\n /**\n * Gets an existing instance for the passed arguements or creates a new\n * instance if one doesn't exist.\n * @param {string} trackingId The tracking ID for the GA property.\n * @param {string} namespace A namespace unique to this store.\n * @param {Object=} defaults An optional object of key/value defaults.\n * @return {Store} The Store instance.\n */\n static getOrCreate(trackingId, namespace, defaults) {\n const key = [AUTOTRACK_PREFIX, trackingId, namespace].join(':');\n\n // Don't create multiple instances for the same tracking Id and namespace.\n if (!instances[key]) {\n instances[key] = new Store(key, defaults);\n if (!isListening) initStorageListener();\n }\n return instances[key];\n }\n\n /**\n * Returns true if the browser supports and can successfully write to\n * localStorage. The results is cached so this method can be invoked many\n * times with no extra performance cost.\n * @private\n * @return {boolean}\n */\n static isSupported_() {\n if (browserSupportsLocalStorage != null) {\n return browserSupportsLocalStorage;\n }\n\n try {\n window.localStorage.setItem(AUTOTRACK_PREFIX, AUTOTRACK_PREFIX);\n window.localStorage.removeItem(AUTOTRACK_PREFIX);\n browserSupportsLocalStorage = true;\n } catch (err) {\n browserSupportsLocalStorage = false;\n }\n return browserSupportsLocalStorage;\n }\n\n /**\n * Wraps the native localStorage method for each stubbing in tests.\n * @private\n * @param {string} key The store key.\n * @return {string|null} The stored value.\n */\n static get_(key) {\n return window.localStorage.getItem(key);\n }\n\n /**\n * Wraps the native localStorage method for each stubbing in tests.\n * @private\n * @param {string} key The store key.\n * @param {string} value The value to store.\n */\n static set_(key, value) {\n window.localStorage.setItem(key, value);\n }\n\n /**\n * Wraps the native localStorage method for each stubbing in tests.\n * @private\n * @param {string} key The store key.\n */\n static clear_(key) {\n window.localStorage.removeItem(key);\n }\n\n /**\n * @param {string} key A key unique to this store.\n * @param {Object=} defaults An optional object of key/value defaults.\n */\n constructor(key, defaults = {}) {\n super();\n this.key_ = key;\n this.defaults_ = defaults;\n\n /** @type {?Object} */\n this.cache_ = null; // Will be set after the first get.\n }\n\n /**\n * Gets the data stored in localStorage for this store. If the cache is\n * already populated, return it as is (since it's always kept up-to-date\n * and in sync with activity in other windows via the `storage` event).\n * TODO(philipwalton): Implement schema migrations if/when a new\n * schema version is introduced.\n * @return {!Object} The stored data merged with the defaults.\n */\n get() {\n if (this.cache_) {\n return this.cache_;\n } else {\n if (Store.isSupported_()) {\n try {\n this.cache_ = parse(Store.get_(this.key_));\n } catch(err) {\n // Do nothing.\n }\n }\n return this.cache_ = assign({}, this.defaults_, this.cache_);\n }\n }\n\n /**\n * Saves the passed data object to localStorage,\n * merging it with the existing data.\n * @param {Object} newData The data to save.\n */\n set(newData) {\n this.cache_ = assign({}, this.defaults_, this.cache_, newData);\n\n if (Store.isSupported_()) {\n try {\n Store.set_(this.key_, JSON.stringify(this.cache_));\n } catch(err) {\n // Do nothing.\n }\n }\n }\n\n /**\n * Clears the data in localStorage for the current store.\n */\n clear() {\n this.cache_ = {};\n if (Store.isSupported_()) {\n try {\n Store.clear_(this.key_);\n } catch(err) {\n // Do nothing.\n }\n }\n }\n\n /**\n * Removes the store instance for the global instances map. If this is the\n * last store instance, the storage listener is also removed.\n * Note: this does not erase the stored data. Use `clear()` for that.\n */\n destroy() {\n delete instances[this.key_];\n if (!Object.keys(instances).length) {\n removeStorageListener();\n }\n }\n}\n\n\n/**\n * Adds a single storage event listener and flips the global `isListening`\n * flag so multiple events aren't added.\n */\nfunction initStorageListener() {\n window.addEventListener('storage', storageListener);\n isListening = true;\n}\n\n\n/**\n * Removes the storage event listener and flips the global `isListening`\n * flag so it can be re-added later.\n */\nfunction removeStorageListener() {\n window.removeEventListener('storage', storageListener);\n isListening = false;\n}\n\n\n/**\n * The global storage event listener.\n * @param {!Event} event The DOM event.\n */\nfunction storageListener(event) {\n const store = instances[event.key];\n if (store) {\n const oldData = assign({}, store.defaults_, parse(event.oldValue));\n const newData = assign({}, store.defaults_, parse(event.newValue));\n\n store.cache_ = newData;\n store.emit('externalSet', newData, oldData);\n }\n}\n\n\n/**\n * Parses a source string as JSON\n * @param {string|null} source\n * @return {!Object} The JSON object.\n */\nfunction parse(source) {\n let data = {};\n if (source) {\n try {\n data = /** @type {!Object} */ (JSON.parse(source));\n } catch(err) {\n // Do nothing.\n }\n }\n return data;\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport MethodChain from './method-chain';\nimport Store from './store';\nimport {now, uuid} from './utilities';\n\n\nconst SECONDS = 1000;\nconst MINUTES = 60 * SECONDS;\n\n\nconst instances = {};\n\n\n/**\n * A session management class that helps track session boundaries\n * across multiple open tabs/windows.\n */\nexport default class Session {\n /**\n * Gets an existing instance for the passed arguments or creates a new\n * instance if one doesn't exist.\n * @param {!Tracker} tracker An analytics.js tracker object.\n * @param {number} timeout The session timeout (in minutes). This value\n * should match what's set in the \"Session settings\" section of the\n * Google Analytics admin.\n * @param {string=} timeZone The optional IANA time zone of the view. This\n * value should match what's set in the \"View settings\" section of the\n * Google Analytics admin. (Note: this assumes all views for the property\n * use the same time zone. If that's not true, it's better not to use\n * this feature).\n * @return {Session} The Session instance.\n */\n static getOrCreate(tracker, timeout, timeZone) {\n // Don't create multiple instances for the same property.\n const trackingId = tracker.get('trackingId');\n if (instances[trackingId]) {\n return instances[trackingId];\n } else {\n return instances[trackingId] = new Session(tracker, timeout, timeZone);\n }\n }\n\n /**\n * @param {!Tracker} tracker An analytics.js tracker object.\n * @param {number} timeout The session timeout (in minutes). This value\n * should match what's set in the \"Session settings\" section of the\n * Google Analytics admin.\n * @param {string=} timeZone The optional IANA time zone of the view. This\n * value should match what's set in the \"View settings\" section of the\n * Google Analytics admin. (Note: this assumes all views for the property\n * use the same time zone. If that's not true, it's better not to use\n * this feature).\n */\n constructor(tracker, timeout, timeZone) {\n this.tracker = tracker;\n this.timeout = timeout || Session.DEFAULT_TIMEOUT;\n this.timeZone = timeZone;\n\n // Binds methods.\n this.sendHitTaskOverride = this.sendHitTaskOverride.bind(this);\n\n // Overrides into the trackers sendHitTask method.\n MethodChain.add(tracker, 'sendHitTask', this.sendHitTaskOverride);\n\n // Some browser doesn't support various features of the\n // `Intl.DateTimeFormat` API, so we have to try/catch it. Consequently,\n // this allows us to assume the presence of `this.dateTimeFormatter` means\n // it works in the current browser.\n try {\n this.dateTimeFormatter =\n new Intl.DateTimeFormat('en-US', {timeZone: this.timeZone});\n } catch(err) {\n // Do nothing.\n }\n\n /** @type {SessionStoreData} */\n const defaultProps = {\n hitTime: 0,\n isExpired: false,\n };\n this.store = Store.getOrCreate(\n tracker.get('trackingId'), 'session', defaultProps);\n\n // Ensure the session has an ID.\n if (!this.store.get().id) {\n this.store.set(/** @type {SessionStoreData} */ ({id: uuid()}));\n }\n }\n\n /**\n * Returns the ID of the current session.\n * @return {string}\n */\n getId() {\n return this.store.get().id;\n }\n\n /**\n * Accepts a session ID and returns true if the specified session has\n * evidentially expired. A session can expire for two reasons:\n * - More than 30 minutes has elapsed since the previous hit\n * was sent (The 30 minutes number is the Google Analytics default, but\n * it can be modified in GA admin \"Session settings\").\n * - A new day has started since the previous hit, in the\n * specified time zone (should correspond to the time zone of the\n * property's views).\n *\n * Note: since real session boundaries are determined at processing time,\n * this is just a best guess rather than a source of truth.\n *\n * @param {string} id The ID of a session to check for expiry.\n * @return {boolean} True if the session has not exp\n */\n isExpired(id = this.getId()) {\n // If a session ID is passed and it doesn't match the current ID,\n // assume it's from an expired session. If no ID is passed, assume the ID\n // of the current session.\n if (id != this.getId()) return true;\n\n /** @type {SessionStoreData} */\n const sessionData = this.store.get();\n\n // `isExpired` will be `true` if the sessionControl field was set to\n // 'end' on the previous hit.\n if (sessionData.isExpired) return true;\n\n const oldHitTime = sessionData.hitTime;\n\n // Only consider a session expired if previous hit time data exists, and\n // the previous hit time is greater than that session timeout period or\n // the hits occurred on different days in the session timezone.\n if (oldHitTime) {\n const currentDate = new Date();\n const oldHitDate = new Date(oldHitTime);\n if (currentDate - oldHitDate > (this.timeout * MINUTES) ||\n this.datesAreDifferentInTimezone(currentDate, oldHitDate)) {\n return true;\n }\n }\n\n // For all other cases return false.\n return false;\n }\n\n /**\n * Returns true if (and only if) the timezone date formatting is supported\n * in the current browser and if the two dates are definitively not the\n * same date in the session timezone. Anything short of this returns false.\n * @param {!Date} d1\n * @param {!Date} d2\n * @return {boolean}\n */\n datesAreDifferentInTimezone(d1, d2) {\n if (!this.dateTimeFormatter) {\n return false;\n } else {\n return this.dateTimeFormatter.format(d1)\n != this.dateTimeFormatter.format(d2);\n }\n }\n\n /**\n * Keeps track of when the previous hit was sent to determine if a session\n * has expired. Also inspects the `sessionControl` field to handles\n * expiration accordingly.\n * @param {function(!Model)} originalMethod A reference to the overridden\n * method.\n * @return {function(!Model)}\n */\n sendHitTaskOverride(originalMethod) {\n return (model) => {\n originalMethod(model);\n\n const sessionControl = model.get('sessionControl');\n const sessionWillStart = sessionControl == 'start' || this.isExpired();\n const sessionWillEnd = sessionControl == 'end';\n\n /** @type {SessionStoreData} */\n const sessionData = this.store.get();\n sessionData.hitTime = now();\n if (sessionWillStart) {\n sessionData.isExpired = false;\n sessionData.id = uuid();\n }\n if (sessionWillEnd) {\n sessionData.isExpired = true;\n }\n this.store.set(sessionData);\n };\n }\n\n /**\n * Restores the tracker's original `sendHitTask` to the state before\n * session control was initialized and removes this instance from the global\n * store.\n */\n destroy() {\n MethodChain.remove(this.tracker, 'sendHitTask', this.sendHitTaskOverride);\n this.store.destroy();\n delete instances[this.tracker.get('trackingId')];\n }\n}\n\n\nSession.DEFAULT_TIMEOUT = 30; // minutes\n","/**\n * Copyright 2017 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {parseUrl} from 'dom-utils';\nimport MethodChain from '../method-chain';\nimport provide from '../provide';\nimport Session from '../session';\nimport Store from '../store';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj, debounce, isObject} from '../utilities';\n\n\n/**\n * Class for the `maxScrollQueryTracker` analytics.js plugin.\n * @implements {MaxScrollTrackerPublicInterface}\n */\nclass MaxScrollTracker {\n /**\n * Registers outbound link tracking on tracker object.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?Object} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.MAX_SCROLL_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!window.addEventListener) return;\n\n /** @type {MaxScrollTrackerOpts} */\n const defaultOpts = {\n increaseThreshold: 20,\n sessionTimeout: Session.DEFAULT_TIMEOUT,\n // timeZone: undefined,\n // maxScrollMetricIndex: undefined,\n fieldsObj: {},\n // hitFilter: undefined\n };\n\n this.opts = /** @type {MaxScrollTrackerOpts} */ (\n assign(defaultOpts, opts));\n\n this.tracker = tracker;\n this.pagePath = this.getPagePath();\n\n // Binds methods to `this`.\n this.handleScroll = debounce(this.handleScroll.bind(this), 500);\n this.trackerSetOverride = this.trackerSetOverride.bind(this);\n\n // Creates the store and binds storage change events.\n this.store = Store.getOrCreate(\n tracker.get('trackingId'), 'plugins/max-scroll-tracker');\n\n // Creates the session and binds session events.\n this.session = Session.getOrCreate(\n tracker, this.opts.sessionTimeout, this.opts.timeZone);\n\n // Override the built-in tracker.set method to watch for changes.\n MethodChain.add(tracker, 'set', this.trackerSetOverride);\n\n this.listenForMaxScrollChanges();\n }\n\n\n /**\n * Adds a scroll event listener if the max scroll percentage for the\n * current page isn't already at 100%.\n */\n listenForMaxScrollChanges() {\n const maxScrollPercentage = this.getMaxScrollPercentageForCurrentPage();\n if (maxScrollPercentage < 100) {\n window.addEventListener('scroll', this.handleScroll);\n }\n }\n\n\n /**\n * Removes an added scroll listener.\n */\n stopListeningForMaxScrollChanges() {\n window.removeEventListener('scroll', this.handleScroll);\n }\n\n\n /**\n * Handles the scroll event. If the current scroll percentage is greater\n * that the stored scroll event by at least the specified increase threshold,\n * send an event with the increase amount.\n */\n handleScroll() {\n const pageHeight = getPageHeight();\n const scrollPos = window.pageYOffset; // scrollY isn't supported in IE.\n const windowHeight = window.innerHeight;\n\n // Ensure scrollPercentage is an integer between 0 and 100.\n const scrollPercentage = Math.min(100, Math.max(0,\n Math.round(100 * (scrollPos / (pageHeight - windowHeight)))));\n\n // If the max scroll data gets out of the sync with the session data\n // (for whatever reason), clear it.\n const sessionId = this.session.getId();\n if (sessionId != this.store.get().sessionId) {\n this.store.clear();\n this.store.set({sessionId});\n }\n\n // If the session has expired, clear the stored data and don't send any\n // events (since they'd start a new session). Note: this check is needed,\n // in addition to the above check, to handle cases where the session IDs\n // got out of sync, but the session didn't expire.\n if (this.session.isExpired(this.store.get().sessionId)) {\n this.store.clear();\n } else {\n const maxScrollPercentage = this.getMaxScrollPercentageForCurrentPage();\n\n if (scrollPercentage > maxScrollPercentage) {\n if (scrollPercentage == 100 || maxScrollPercentage == 100) {\n this.stopListeningForMaxScrollChanges();\n }\n const increaseAmount = scrollPercentage - maxScrollPercentage;\n if (scrollPercentage == 100 ||\n increaseAmount >= this.opts.increaseThreshold) {\n this.setMaxScrollPercentageForCurrentPage(scrollPercentage);\n this.sendMaxScrollEvent(increaseAmount, scrollPercentage);\n }\n }\n }\n }\n\n /**\n * Detects changes to the tracker object and triggers an update if the page\n * field has changed.\n * @param {function((Object|string), (string|undefined))} originalMethod\n * A reference to the overridden method.\n * @return {function((Object|string), (string|undefined))}\n */\n trackerSetOverride(originalMethod) {\n return (field, value) => {\n originalMethod(field, value);\n\n /** @type {!FieldsObj} */\n const fields = isObject(field) ? field : {[field]: value};\n if (fields.page) {\n const lastPagePath = this.pagePath;\n this.pagePath = this.getPagePath();\n\n if (this.pagePath != lastPagePath) {\n // Since event listeners for the same function are never added twice,\n // we don't need to worry about whether we're already listening. We\n // can just add the event listener again.\n this.listenForMaxScrollChanges();\n }\n }\n };\n }\n\n /**\n * Sends an event for the increased max scroll percentage amount.\n * @param {number} increaseAmount\n * @param {number} scrollPercentage\n */\n sendMaxScrollEvent(increaseAmount, scrollPercentage) {\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n eventCategory: 'Max Scroll',\n eventAction: 'increase',\n eventValue: increaseAmount,\n eventLabel: String(scrollPercentage),\n nonInteraction: true,\n };\n\n // If a custom metric was specified, set it equal to the event value.\n if (this.opts.maxScrollMetricIndex) {\n defaultFields['metric' + this.opts.maxScrollMetricIndex] = increaseAmount;\n }\n\n this.tracker.send('event',\n createFieldsObj(defaultFields, this.opts.fieldsObj,\n this.tracker, this.opts.hitFilter));\n }\n\n /**\n * Stores the current max scroll percentage for the current page.\n * @param {number} maxScrollPercentage\n */\n setMaxScrollPercentageForCurrentPage(maxScrollPercentage) {\n this.store.set({\n [this.pagePath]: maxScrollPercentage,\n sessionId: this.session.getId(),\n });\n }\n\n /**\n * Gets the stored max scroll percentage for the current page.\n * @return {number}\n */\n getMaxScrollPercentageForCurrentPage() {\n return this.store.get()[this.pagePath] || 0;\n }\n\n /**\n * Gets the page path from the tracker object.\n * @return {number}\n */\n getPagePath() {\n const url = parseUrl(\n this.tracker.get('page') || this.tracker.get('location'));\n return url.pathname + url.search;\n }\n\n /**\n * Removes all event listeners and restores overridden methods.\n */\n remove() {\n this.session.destroy();\n this.stopListeningForMaxScrollChanges();\n MethodChain.remove(this.tracker, 'set', this.trackerSetOverride);\n }\n}\n\n\nprovide('maxScrollTracker', MaxScrollTracker);\n\n\n/**\n * Gets the maximum height of the page including scrollable area.\n * @return {number}\n */\nfunction getPageHeight() {\n const html = document.documentElement;\n const body = document.body;\n return Math.max(html.offsetHeight, html.scrollHeight,\n body.offsetHeight, body.scrollHeight);\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {NULL_DIMENSION} from '../constants';\nimport provide from '../provide';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj,\n debounce, isObject, toArray} from '../utilities';\n\n\n/**\n * Declares the MediaQueryList instance cache.\n */\nconst mediaMap = {};\n\n\n/**\n * Class for the `mediaQueryTracker` analytics.js plugin.\n * @implements {MediaQueryTrackerPublicInterface}\n */\nclass MediaQueryTracker {\n /**\n * Registers media query tracking.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?Object} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.MEDIA_QUERY_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!window.matchMedia) return;\n\n /** @type {MediaQueryTrackerOpts} */\n const defaultOpts = {\n // definitions: unefined,\n changeTemplate: this.changeTemplate,\n changeTimeout: 1000,\n fieldsObj: {},\n // hitFilter: undefined,\n };\n\n this.opts = /** @type {MediaQueryTrackerOpts} */ (\n assign(defaultOpts, opts));\n\n // Exits early if media query data doesn't exist.\n if (!isObject(this.opts.definitions)) return;\n\n this.opts.definitions = toArray(this.opts.definitions);\n this.tracker = tracker;\n this.changeListeners = [];\n\n this.processMediaQueries();\n }\n\n /**\n * Loops through each media query definition, sets the custom dimenion data,\n * and adds the change listeners.\n */\n processMediaQueries() {\n this.opts.definitions.forEach((definition) => {\n // Only processes definitions with a name and index.\n if (definition.name && definition.dimensionIndex) {\n const mediaName = this.getMatchName(definition);\n this.tracker.set('dimension' + definition.dimensionIndex, mediaName);\n\n this.addChangeListeners(definition);\n }\n });\n }\n\n /**\n * Takes a definition object and return the name of the matching media item.\n * If no match is found, the NULL_DIMENSION value is returned.\n * @param {Object} definition A set of named media queries associated\n * with a single custom dimension.\n * @return {string} The name of the matched media or NULL_DIMENSION.\n */\n getMatchName(definition) {\n let match;\n\n definition.items.forEach((item) => {\n if (getMediaList(item.media).matches) {\n match = item;\n }\n });\n return match ? match.name : NULL_DIMENSION;\n }\n\n /**\n * Adds change listeners to each media query in the definition list.\n * Debounces the changes to prevent unnecessary hits from being sent.\n * @param {Object} definition A set of named media queries associated\n * with a single custom dimension\n */\n addChangeListeners(definition) {\n definition.items.forEach((item) => {\n const mql = getMediaList(item.media);\n const fn = debounce(() => {\n this.handleChanges(definition);\n }, this.opts.changeTimeout);\n\n mql.addListener(fn);\n this.changeListeners.push({mql, fn});\n });\n }\n\n /**\n * Handles changes to the matched media. When the new value differs from\n * the old value, a change event is sent.\n * @param {Object} definition A set of named media queries associated\n * with a single custom dimension\n */\n handleChanges(definition) {\n const newValue = this.getMatchName(definition);\n const oldValue = this.tracker.get('dimension' + definition.dimensionIndex);\n\n if (newValue !== oldValue) {\n this.tracker.set('dimension' + definition.dimensionIndex, newValue);\n\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n eventCategory: definition.name,\n eventAction: 'change',\n eventLabel: this.opts.changeTemplate(oldValue, newValue),\n nonInteraction: true,\n };\n this.tracker.send('event', createFieldsObj(defaultFields,\n this.opts.fieldsObj, this.tracker, this.opts.hitFilter));\n }\n }\n\n /**\n * Removes all event listeners and instance properties.\n */\n remove() {\n for (let i = 0, listener; listener = this.changeListeners[i]; i++) {\n listener.mql.removeListener(listener.fn);\n }\n }\n\n /**\n * Sets the default formatting of the change event label.\n * This can be overridden by setting the `changeTemplate` option.\n * @param {string} oldValue The value of the media query prior to the change.\n * @param {string} newValue The value of the media query after the change.\n * @return {string} The formatted event label.\n */\n changeTemplate(oldValue, newValue) {\n return oldValue + ' => ' + newValue;\n }\n}\n\n\nprovide('mediaQueryTracker', MediaQueryTracker);\n\n\n/**\n * Accepts a media query and returns a MediaQueryList object.\n * Caches the values to avoid multiple unnecessary instances.\n * @param {string} media A media query value.\n * @return {MediaQueryList} The matched media.\n */\nfunction getMediaList(media) {\n return mediaMap[media] || (mediaMap[media] = window.matchMedia(media));\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {delegate, parseUrl} from 'dom-utils';\nimport provide from '../provide';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj,\n getAttributeFields, withTimeout} from '../utilities';\n\n\n/**\n * Class for the `outboundFormTracker` analytics.js plugin.\n * @implements {OutboundFormTrackerPublicInterface}\n */\nclass OutboundFormTracker {\n /**\n * Registers outbound form tracking.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?Object} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.OUTBOUND_FORM_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!window.addEventListener) return;\n\n /** @type {OutboundFormTrackerOpts} */\n const defaultOpts = {\n formSelector: 'form',\n shouldTrackOutboundForm: this.shouldTrackOutboundForm,\n fieldsObj: {},\n attributePrefix: 'ga-',\n // hitFilter: undefined\n };\n\n this.opts = /** @type {OutboundFormTrackerOpts} */ (\n assign(defaultOpts, opts));\n\n this.tracker = tracker;\n\n this.delegate = delegate(document, 'submit', this.opts.formSelector,\n this.handleFormSubmits.bind(this), {composed: true, useCapture: true});\n }\n\n /**\n * Handles all submits on form elements. A form submit is considered outbound\n * if its action attribute starts with http and does not contain\n * location.hostname.\n * When the beacon transport method is not available, the event's default\n * action is prevented and re-emitted after the hit is sent.\n * @param {Event} event The DOM submit event.\n * @param {Element} form The delegated event target.\n */\n handleFormSubmits(event, form) {\n const action = parseUrl(form.action).href;\n\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n eventCategory: 'Outbound Form',\n eventAction: 'submit',\n eventLabel: action,\n };\n\n if (this.opts.shouldTrackOutboundForm(form, parseUrl)) {\n if (!navigator.sendBeacon) {\n // Stops the submit and waits until the hit is complete (with timeout)\n // for browsers that don't support beacon.\n event.preventDefault();\n defaultFields.hitCallback = withTimeout(function() {\n form.submit();\n });\n }\n\n const userFields = assign({}, this.opts.fieldsObj,\n getAttributeFields(form, this.opts.attributePrefix));\n\n this.tracker.send('event', createFieldsObj(\n defaultFields, userFields,\n this.tracker, this.opts.hitFilter, form, event));\n }\n }\n\n /**\n * Determines whether or not the tracker should send a hit when a form is\n * submitted. By default, forms with an action attribute that starts with\n * \"http\" and doesn't contain the current hostname are tracked.\n * @param {Element} form The form that was submitted.\n * @param {Function} parseUrlFn A cross-browser utility method for url\n * parsing (note: renamed to disambiguate when compiling).\n * @return {boolean} Whether or not the form should be tracked.\n */\n shouldTrackOutboundForm(form, parseUrlFn) {\n const url = parseUrlFn(form.action);\n return url.hostname != location.hostname &&\n url.protocol.slice(0, 4) == 'http';\n }\n\n /**\n * Removes all event listeners and instance properties.\n */\n remove() {\n this.delegate.destroy();\n }\n}\n\n\nprovide('outboundFormTracker', OutboundFormTracker);\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {delegate, parseUrl} from 'dom-utils';\nimport provide from '../provide';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj,\n getAttributeFields, withTimeout} from '../utilities';\n\n\n/**\n * Class for the `outboundLinkTracker` analytics.js plugin.\n * @implements {OutboundLinkTrackerPublicInterface}\n */\nclass OutboundLinkTracker {\n /**\n * Registers outbound link tracking on a tracker object.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?Object} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.OUTBOUND_LINK_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!window.addEventListener) return;\n\n /** @type {OutboundLinkTrackerOpts} */\n const defaultOpts = {\n events: ['click'],\n linkSelector: 'a, area',\n shouldTrackOutboundLink: this.shouldTrackOutboundLink,\n fieldsObj: {},\n attributePrefix: 'ga-',\n // hitFilter: undefined,\n };\n\n this.opts = /** @type {OutboundLinkTrackerOpts} */ (\n assign(defaultOpts, opts));\n\n this.tracker = tracker;\n\n // Binds methods.\n this.handleLinkInteractions = this.handleLinkInteractions.bind(this);\n\n // Creates a mapping of events to their delegates\n this.delegates = {};\n this.opts.events.forEach((event) => {\n this.delegates[event] = delegate(document, event, this.opts.linkSelector,\n this.handleLinkInteractions, {composed: true, useCapture: true});\n });\n }\n\n /**\n * Handles all interactions on link elements. A link is considered an outbound\n * link if its hostname property does not match location.hostname. When the\n * beacon transport method is not available, the links target is set to\n * \"_blank\" to ensure the hit can be sent.\n * @param {Event} event The DOM click event.\n * @param {Element} link The delegated event target.\n */\n handleLinkInteractions(event, link) {\n if (this.opts.shouldTrackOutboundLink(link, parseUrl)) {\n const href = link.getAttribute('href') || link.getAttribute('xlink:href');\n const url = parseUrl(href);\n\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n eventCategory: 'Outbound Link',\n eventAction: event.type,\n eventLabel: url.href,\n };\n\n /** @type {FieldsObj} */\n const userFields = assign({}, this.opts.fieldsObj,\n getAttributeFields(link, this.opts.attributePrefix));\n\n const fieldsObj = createFieldsObj(defaultFields, userFields,\n this.tracker, this.opts.hitFilter, link, event);\n\n if (!navigator.sendBeacon &&\n linkClickWillUnloadCurrentPage(event, link)) {\n // Adds a new event handler at the last minute to minimize the chances\n // that another event handler for this click will run after this logic.\n const clickHandler = () => {\n window.removeEventListener('click', clickHandler);\n\n // Checks to make sure another event handler hasn't already prevented\n // the default action. If it has the custom redirect isn't needed.\n if (!event.defaultPrevented) {\n // Stops the click and waits until the hit is complete (with\n // timeout) for browsers that don't support beacon.\n event.preventDefault();\n\n const oldHitCallback = fieldsObj.hitCallback;\n fieldsObj.hitCallback = withTimeout(function() {\n if (typeof oldHitCallback == 'function') oldHitCallback();\n location.href = href;\n });\n }\n this.tracker.send('event', fieldsObj);\n };\n window.addEventListener('click', clickHandler);\n } else {\n this.tracker.send('event', fieldsObj);\n }\n }\n }\n\n /**\n * Determines whether or not the tracker should send a hit when a link is\n * clicked. By default links with a hostname property not equal to the current\n * hostname are tracked.\n * @param {Element} link The link that was clicked on.\n * @param {Function} parseUrlFn A cross-browser utility method for url\n * parsing (note: renamed to disambiguate when compiling).\n * @return {boolean} Whether or not the link should be tracked.\n */\n shouldTrackOutboundLink(link, parseUrlFn) {\n const href = link.getAttribute('href') || link.getAttribute('xlink:href');\n const url = parseUrlFn(href);\n return url.hostname != location.hostname &&\n url.protocol.slice(0, 4) == 'http';\n }\n\n /**\n * Removes all event listeners and instance properties.\n */\n remove() {\n Object.keys(this.delegates).forEach((key) => {\n this.delegates[key].destroy();\n });\n }\n}\n\n\nprovide('outboundLinkTracker', OutboundLinkTracker);\n\n\n/**\n * Determines if a link click event will cause the current page to upload.\n * Note: most link clicks *will* cause the current page to unload because they\n * initiate a page navigation. The most common reason a link click won't cause\n * the page to unload is if the clicked was to open the link in a new tab.\n * @param {Event} event The DOM event.\n * @param {Element} link The link element clicked on.\n * @return {boolean} True if the current page will be unloaded.\n */\nfunction linkClickWillUnloadCurrentPage(event, link) {\n return !(\n // The event type can be customized; we only care about clicks here.\n event.type != 'click' ||\n // Links with target=\"_blank\" set will open in a new window/tab.\n link.target == '_blank' ||\n // On mac, command clicking will open a link in a new tab. Control\n // clicking does this on windows.\n event.metaKey || event.ctrlKey ||\n // Shift clicking in Chrome/Firefox opens the link in a new window\n // In Safari it adds the URL to a favorites list.\n event.shiftKey ||\n // On Mac, clicking with the option key is used to download a resouce.\n event.altKey ||\n // Middle mouse button clicks (which == 2) are used to open a link\n // in a new tab, and right clicks (which == 3) on Firefox trigger\n // a click event.\n event.which > 1);\n}\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport {NULL_DIMENSION} from '../constants';\nimport MethodChain from '../method-chain';\nimport provide from '../provide';\nimport Session from '../session';\nimport Store from '../store';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj, deferUntilPluginsLoaded,\n isObject, now, uuid} from '../utilities';\n\n\nconst HIDDEN = 'hidden';\nconst VISIBLE = 'visible';\nconst PAGE_ID = uuid();\nconst SECONDS = 1000;\n\n\n/**\n * Class for the `pageVisibilityTracker` analytics.js plugin.\n * @implements {PageVisibilityTrackerPublicInterface}\n */\nclass PageVisibilityTracker {\n /**\n * Registers outbound link tracking on tracker object.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?Object} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.PAGE_VISIBILITY_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!document.visibilityState) return;\n\n /** @type {PageVisibilityTrackerOpts} */\n const defaultOpts = {\n sessionTimeout: Session.DEFAULT_TIMEOUT,\n visibleThreshold: 5 * SECONDS,\n // timeZone: undefined,\n sendInitialPageview: false,\n // pageLoadsMetricIndex: undefined,\n // visibleMetricIndex: undefined,\n fieldsObj: {},\n // hitFilter: undefined\n };\n\n this.opts = /** @type {PageVisibilityTrackerOpts} */ (\n assign(defaultOpts, opts));\n\n this.tracker = tracker;\n this.lastPageState = document.visibilityState;\n this.visibleThresholdTimeout_ = null;\n this.isInitialPageviewSent_ = false;\n\n // Binds methods to `this`.\n this.trackerSetOverride = this.trackerSetOverride.bind(this);\n this.handleChange = this.handleChange.bind(this);\n this.handleWindowUnload = this.handleWindowUnload.bind(this);\n this.handleExternalStoreSet = this.handleExternalStoreSet.bind(this);\n\n // Creates the store and binds storage change events.\n this.store = Store.getOrCreate(\n tracker.get('trackingId'), 'plugins/page-visibility-tracker');\n this.store.on('externalSet', this.handleExternalStoreSet);\n\n // Creates the session and binds session events.\n this.session = Session.getOrCreate(\n tracker, this.opts.sessionTimeout, this.opts.timeZone);\n\n // Override the built-in tracker.set method to watch for changes.\n MethodChain.add(tracker, 'set', this.trackerSetOverride);\n\n window.addEventListener('unload', this.handleWindowUnload);\n document.addEventListener('visibilitychange', this.handleChange);\n\n // Postpone sending any hits until the next call stack, which allows all\n // autotrack plugins to be required sync before any hits are sent.\n deferUntilPluginsLoaded(this.tracker, () => {\n if (document.visibilityState == VISIBLE) {\n if (this.opts.sendInitialPageview) {\n this.sendPageview({isPageLoad: true});\n this.isInitialPageviewSent_ = true;\n }\n this.store.set(/** @type {PageVisibilityStoreData} */ ({\n time: now(),\n state: VISIBLE,\n pageId: PAGE_ID,\n sessionId: this.session.getId(),\n }));\n } else {\n if (this.opts.sendInitialPageview && this.opts.pageLoadsMetricIndex) {\n this.sendPageLoad();\n }\n }\n });\n }\n\n /**\n * Inspects the last visibility state change data and determines if a\n * visibility event needs to be tracked based on the current visibility\n * state and whether or not the session has expired. If the session has\n * expired, a change to `visible` will trigger an additional pageview.\n * This method also sends as the event value (and optionally a custom metric)\n * the elapsed time between this event and the previously reported change\n * in the same session, allowing you to more accurately determine when users\n * were actually looking at your page versus when it was in the background.\n */\n handleChange() {\n if (!(document.visibilityState == VISIBLE ||\n document.visibilityState == HIDDEN)) {\n return;\n }\n\n const lastStoredChange = this.getAndValidateChangeData();\n\n /** @type {PageVisibilityStoreData} */\n const change = {\n time: now(),\n state: document.visibilityState,\n pageId: PAGE_ID,\n sessionId: this.session.getId(),\n };\n\n // If the visibilityState has changed to visible and the initial pageview\n // has not been sent (and the `sendInitialPageview` option is `true`).\n // Send the initial pageview now.\n if (document.visibilityState == VISIBLE &&\n this.opts.sendInitialPageview && !this.isInitialPageviewSent_) {\n this.sendPageview();\n this.isInitialPageviewSent_ = true;\n }\n\n // If the visibilityState has changed to hidden, clear any scheduled\n // pageviews waiting for the visibleThreshold timeout.\n if (document.visibilityState == HIDDEN && this.visibleThresholdTimeout_) {\n clearTimeout(this.visibleThresholdTimeout_);\n }\n\n if (this.session.isExpired(lastStoredChange.sessionId)) {\n this.store.clear();\n if (this.lastPageState == HIDDEN &&\n document.visibilityState == VISIBLE) {\n // If the session has expired, changes from hidden to visible should\n // be considered a new pageview rather than a visibility event.\n // This behavior ensures all sessions contain a pageview so\n // session-level page dimensions and metrics (e.g. ga:landingPagePath\n // and ga:entrances) are correct.\n // Also, in order to prevent false positives, we add a small timeout\n // that is cleared if the visibilityState changes to hidden shortly\n // after the change to visible. This can happen if a user is quickly\n // switching through their open tabs but not actually interacting with\n // and of them. It can also happen when a user goes to a tab just to\n // immediately close it. Such cases should not be considered pageviews.\n clearTimeout(this.visibleThresholdTimeout_);\n this.visibleThresholdTimeout_ = setTimeout(() => {\n this.store.set(change);\n this.sendPageview({hitTime: change.time});\n }, this.opts.visibleThreshold);\n }\n } else {\n if (lastStoredChange.pageId == PAGE_ID &&\n lastStoredChange.state == VISIBLE) {\n this.sendPageVisibilityEvent(lastStoredChange);\n }\n this.store.set(change);\n }\n\n this.lastPageState = document.visibilityState;\n }\n\n /**\n * Retroactively updates the stored change data in cases where it's known to\n * be out of sync.\n * This plugin keeps track of each visiblity change and stores the last one\n * in localStorage. LocalStorage is used to handle situations where the user\n * has multiple page open at the same time and we don't want to\n * double-report page visibility in those cases.\n * However, a problem can occur if a user closes a page when one or more\n * visible pages are still open. In such cases it's impossible to know\n * which of the remaining pages the user will interact with next.\n * To solve this problem we wait for the next change on any page and then\n * retroactively update the stored data to reflect the current page as being\n * the page on which the last change event occured and measure visibility\n * from that point.\n * @return {!PageVisibilityStoreData}\n */\n getAndValidateChangeData() {\n const lastStoredChange =\n /** @type {PageVisibilityStoreData} */ (this.store.get());\n\n if (this.lastPageState == VISIBLE &&\n lastStoredChange.state == HIDDEN &&\n lastStoredChange.pageId != PAGE_ID) {\n lastStoredChange.state = VISIBLE;\n lastStoredChange.pageId = PAGE_ID;\n this.store.set(lastStoredChange);\n }\n return lastStoredChange;\n }\n\n /**\n * Sends a Page Visibility event to track the time this page was in the\n * visible state (assuming it was in that state long enough to meet the\n * threshold).\n * @param {!PageVisibilityStoreData} lastStoredChange\n * @param {{hitTime: (number|undefined)}=} param1\n * - hitTime: A hit timestap used to help ensure original order in cases\n * where the send is delayed.\n */\n sendPageVisibilityEvent(lastStoredChange, {hitTime} = {}) {\n const delta = this.getTimeSinceLastStoredChange(\n lastStoredChange, {hitTime});\n\n // If the detla is greater than the visibileThreshold, report it.\n if (delta && delta >= this.opts.visibleThreshold) {\n const deltaInSeconds = Math.round(delta / SECONDS);\n\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n nonInteraction: true,\n eventCategory: 'Page Visibility',\n eventAction: 'track',\n eventValue: deltaInSeconds,\n eventLabel: NULL_DIMENSION,\n };\n\n if (hitTime) {\n defaultFields.queueTime = now() - hitTime;\n }\n\n // If a custom metric was specified, set it equal to the event value.\n if (this.opts.visibleMetricIndex) {\n defaultFields['metric' + this.opts.visibleMetricIndex] = deltaInSeconds;\n }\n\n this.tracker.send('event',\n createFieldsObj(defaultFields, this.opts.fieldsObj,\n this.tracker, this.opts.hitFilter));\n }\n }\n\n /**\n * Sends a page load event.\n */\n sendPageLoad() {\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n eventCategory: 'Page Visibility',\n eventAction: 'page load',\n eventLabel: NULL_DIMENSION,\n ['metric' + this.opts.pageLoadsMetricIndex]: 1,\n nonInteraction: true,\n };\n this.tracker.send('event',\n createFieldsObj(defaultFields, this.opts.fieldsObj,\n this.tracker, this.opts.hitFilter));\n }\n\n /**\n * Sends a pageview, optionally calculating an offset if hitTime is passed.\n * @param {{\n * hitTime: (number|undefined),\n * isPageLoad: (boolean|undefined)\n * }=} param1\n * hitTime: The timestamp of the current hit.\n * isPageLoad: True if this pageview was also a page load.\n */\n sendPageview({hitTime, isPageLoad} = {}) {\n /** @type {FieldsObj} */\n const defaultFields = {transport: 'beacon'};\n if (hitTime) {\n defaultFields.queueTime = now() - hitTime;\n }\n if (isPageLoad && this.opts.pageLoadsMetricIndex) {\n defaultFields['metric' + this.opts.pageLoadsMetricIndex] = 1;\n }\n\n this.tracker.send('pageview',\n createFieldsObj(defaultFields, this.opts.fieldsObj,\n this.tracker, this.opts.hitFilter));\n }\n\n /**\n * Detects changes to the tracker object and triggers an update if the page\n * field has changed.\n * @param {function((Object|string), (string|undefined))} originalMethod\n * A reference to the overridden method.\n * @return {function((Object|string), (string|undefined))}\n */\n trackerSetOverride(originalMethod) {\n return (field, value) => {\n /** @type {!FieldsObj} */\n const fields = isObject(field) ? field : {[field]: value};\n if (fields.page && fields.page !== this.tracker.get('page')) {\n if (this.lastPageState == VISIBLE) {\n this.handleChange();\n }\n }\n originalMethod(field, value);\n };\n }\n\n /**\n * Calculates the time since the last visibility change event in the current\n * session. If the session has expired the reported time is zero.\n * @param {PageVisibilityStoreData} lastStoredChange\n * @param {{hitTime: (number|undefined)}=} param1\n * hitTime: The time of the current hit (defaults to now).\n * @return {number} The time (in ms) since the last change.\n */\n getTimeSinceLastStoredChange(lastStoredChange, {hitTime} = {}) {\n return lastStoredChange.time ?\n (hitTime || now()) - lastStoredChange.time : 0;\n }\n\n /**\n * Handles responding to the `storage` event.\n * The code on this page needs to be informed when other tabs or windows are\n * updating the stored page visibility state data. This method checks to see\n * if a hidden state is stored when there are still visible tabs open, which\n * can happen if multiple windows are open at the same time.\n * @param {PageVisibilityStoreData} newData\n * @param {PageVisibilityStoreData} oldData\n */\n handleExternalStoreSet(newData, oldData) {\n // If the change times are the same, then the previous write only\n // updated the active page ID. It didn't enter a new state and thus no\n // hits should be sent.\n if (newData.time == oldData.time) return;\n\n // Page Visibility events must be sent by the tracker on the page\n // where the original event occurred. So if a change happens on another\n // page, but this page is where the previous change event occurred, then\n // this page is the one that needs to send the event (so all dimension\n // data is correct).\n if (oldData.pageId == PAGE_ID &&\n oldData.state == VISIBLE &&\n !this.session.isExpired(oldData.sessionId)) {\n this.sendPageVisibilityEvent(oldData, {hitTime: newData.time});\n }\n }\n\n /**\n * Handles responding to the `unload` event.\n * Since some browsers don't emit a `visibilitychange` event in all cases\n * where a page might be unloaded, it's necessary to hook into the `unload`\n * event to ensure the correct state is always stored.\n */\n handleWindowUnload() {\n // If the stored visibility state isn't hidden when the unload event\n // fires, it means the visibilitychange event didn't fire as the document\n // was being unloaded, so we invoke it manually.\n if (this.lastPageState != HIDDEN) {\n this.handleChange();\n }\n }\n\n /**\n * Removes all event listeners and restores overridden methods.\n */\n remove() {\n this.store.destroy();\n this.session.destroy();\n MethodChain.remove(this.tracker, 'set', this.trackerSetOverride);\n window.removeEventListener('unload', this.handleWindowUnload);\n document.removeEventListener('visibilitychange', this.handleChange);\n }\n}\n\n\nprovide('pageVisibilityTracker', PageVisibilityTracker);\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport provide from '../provide';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj} from '../utilities';\n\n\n/**\n * Class for the `socialWidgetTracker` analytics.js plugin.\n * @implements {SocialWidgetTrackerPublicInterface}\n */\nclass SocialWidgetTracker {\n /**\n * Registers social tracking on tracker object.\n * Supports both declarative social tracking via HTML attributes as well as\n * tracking for social events when using official Twitter or Facebook widgets.\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?Object} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.SOCIAL_WIDGET_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!window.addEventListener) return;\n\n /** @type {SocialWidgetTrackerOpts} */\n const defaultOpts = {\n fieldsObj: {},\n hitFilter: null,\n };\n\n this.opts = /** @type {SocialWidgetTrackerOpts} */ (\n assign(defaultOpts, opts));\n\n this.tracker = tracker;\n\n // Binds methods to `this`.\n this.addWidgetListeners = this.addWidgetListeners.bind(this);\n this.addTwitterEventHandlers = this.addTwitterEventHandlers.bind(this);\n this.handleTweetEvents = this.handleTweetEvents.bind(this);\n this.handleFollowEvents = this.handleFollowEvents.bind(this);\n this.handleLikeEvents = this.handleLikeEvents.bind(this);\n this.handleUnlikeEvents = this.handleUnlikeEvents.bind(this);\n\n if (document.readyState != 'complete') {\n // Adds the widget listeners after the window's `load` event fires.\n // If loading widgets using the officially recommended snippets, they\n // will be available at `window.load`. If not users can call the\n // `addWidgetListeners` method manually.\n window.addEventListener('load', this.addWidgetListeners);\n } else {\n this.addWidgetListeners();\n }\n }\n\n\n /**\n * Invokes the methods to add Facebook and Twitter widget event listeners.\n * Ensures the respective global namespaces are present before adding.\n */\n addWidgetListeners() {\n if (window.FB) this.addFacebookEventHandlers();\n if (window.twttr) this.addTwitterEventHandlers();\n }\n\n /**\n * Adds event handlers for the \"tweet\" and \"follow\" events emitted by the\n * official tweet and follow buttons. Note: this does not capture tweet or\n * follow events emitted by other Twitter widgets (tweet, timeline, etc.).\n */\n addTwitterEventHandlers() {\n try {\n window.twttr.ready(() => {\n window.twttr.events.bind('tweet', this.handleTweetEvents);\n window.twttr.events.bind('follow', this.handleFollowEvents);\n });\n } catch(err) {\n // Do nothing.\n }\n }\n\n /**\n * Removes event handlers for the \"tweet\" and \"follow\" events emitted by the\n * official tweet and follow buttons.\n */\n removeTwitterEventHandlers() {\n try {\n window.twttr.ready(() => {\n window.twttr.events.unbind('tweet', this.handleTweetEvents);\n window.twttr.events.unbind('follow', this.handleFollowEvents);\n });\n } catch(err) {\n // Do nothing.\n }\n }\n\n /**\n * Adds event handlers for the \"like\" and \"unlike\" events emitted by the\n * official Facebook like button.\n */\n addFacebookEventHandlers() {\n try {\n window.FB.Event.subscribe('edge.create', this.handleLikeEvents);\n window.FB.Event.subscribe('edge.remove', this.handleUnlikeEvents);\n } catch(err) {\n // Do nothing.\n }\n }\n\n /**\n * Removes event handlers for the \"like\" and \"unlike\" events emitted by the\n * official Facebook like button.\n */\n removeFacebookEventHandlers() {\n try {\n window.FB.Event.unsubscribe('edge.create', this.handleLikeEvents);\n window.FB.Event.unsubscribe('edge.remove', this.handleUnlikeEvents);\n } catch(err) {\n // Do nothing.\n }\n }\n\n /**\n * Handles `tweet` events emitted by the Twitter JS SDK.\n * @param {TwttrEvent} event The Twitter event object passed to the handler.\n */\n handleTweetEvents(event) {\n // Ignores tweets from widgets that aren't the tweet button.\n if (event.region != 'tweet') return;\n\n const url = event.data.url || event.target.getAttribute('data-url') ||\n location.href;\n\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n socialNetwork: 'Twitter',\n socialAction: 'tweet',\n socialTarget: url,\n };\n this.tracker.send('social',\n createFieldsObj(defaultFields, this.opts.fieldsObj,\n this.tracker, this.opts.hitFilter, event.target, event));\n }\n\n /**\n * Handles `follow` events emitted by the Twitter JS SDK.\n * @param {TwttrEvent} event The Twitter event object passed to the handler.\n */\n handleFollowEvents(event) {\n // Ignore follows from widgets that aren't the follow button.\n if (event.region != 'follow') return;\n\n const screenName = event.data.screen_name ||\n event.target.getAttribute('data-screen-name');\n\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n socialNetwork: 'Twitter',\n socialAction: 'follow',\n socialTarget: screenName,\n };\n this.tracker.send('social',\n createFieldsObj(defaultFields, this.opts.fieldsObj,\n this.tracker, this.opts.hitFilter, event.target, event));\n }\n\n /**\n * Handles `like` events emitted by the Facebook JS SDK.\n * @param {string} url The URL corresponding to the like event.\n */\n handleLikeEvents(url) {\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n socialNetwork: 'Facebook',\n socialAction: 'like',\n socialTarget: url,\n };\n this.tracker.send('social', createFieldsObj(defaultFields,\n this.opts.fieldsObj, this.tracker, this.opts.hitFilter));\n }\n\n /**\n * Handles `unlike` events emitted by the Facebook JS SDK.\n * @param {string} url The URL corresponding to the unlike event.\n */\n handleUnlikeEvents(url) {\n /** @type {FieldsObj} */\n const defaultFields = {\n transport: 'beacon',\n socialNetwork: 'Facebook',\n socialAction: 'unlike',\n socialTarget: url,\n };\n this.tracker.send('social', createFieldsObj(defaultFields,\n this.opts.fieldsObj, this.tracker, this.opts.hitFilter));\n }\n\n /**\n * Removes all event listeners and instance properties.\n */\n remove() {\n window.removeEventListener('load', this.addWidgetListeners);\n this.removeFacebookEventHandlers();\n this.removeTwitterEventHandlers();\n }\n}\n\n\nprovide('socialWidgetTracker', SocialWidgetTracker);\n","/**\n * Copyright 2016 Google Inc. All Rights Reserved.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\nimport MethodChain from '../method-chain';\nimport provide from '../provide';\nimport {plugins, trackUsage} from '../usage';\nimport {assign, createFieldsObj} from '../utilities';\n\n\n/**\n * Class for the `urlChangeTracker` analytics.js plugin.\n * @implements {UrlChangeTrackerPublicInterface}\n */\nclass UrlChangeTracker {\n /**\n * Adds handler for the history API methods\n * @param {!Tracker} tracker Passed internally by analytics.js\n * @param {?Object} opts Passed by the require command.\n */\n constructor(tracker, opts) {\n trackUsage(tracker, plugins.URL_CHANGE_TRACKER);\n\n // Feature detects to prevent errors in unsupporting browsers.\n if (!history.pushState || !window.addEventListener) return;\n\n /** @type {UrlChangeTrackerOpts} */\n const defaultOpts = {\n shouldTrackUrlChange: this.shouldTrackUrlChange,\n trackReplaceState: false,\n fieldsObj: {},\n hitFilter: null,\n };\n\n this.opts = /** @type {UrlChangeTrackerOpts} */ (assign(defaultOpts, opts));\n\n this.tracker = tracker;\n\n // Sets the initial page field.\n // Don't set this on the tracker yet so campaign data can be retreived\n // from the location field.\n this.path = getPath();\n\n // Binds methods.\n this.pushStateOverride = this.pushStateOverride.bind(this);\n this.replaceStateOverride = this.replaceStateOverride.bind(this);\n this.handlePopState = this.handlePopState.bind(this);\n\n // Watches for history changes.\n MethodChain.add(history, 'pushState', this.pushStateOverride);\n MethodChain.add(history, 'replaceState', this.replaceStateOverride);\n window.addEventListener('popstate', this.handlePopState);\n }\n\n /**\n * Handles invocations of the native `history.pushState` and calls\n * `handleUrlChange()` indicating that the history updated.\n * @param {!Function} originalMethod A reference to the overridden method.\n * @return {!Function}\n */\n pushStateOverride(originalMethod) {\n return (...args) => {\n originalMethod(...args);\n this.handleUrlChange(true);\n };\n }\n\n /**\n * Handles invocations of the native `history.replaceState` and calls\n * `handleUrlChange()` indicating that history was replaced.\n * @param {!Function} originalMethod A reference to the overridden method.\n * @return {!Function}\n */\n replaceStateOverride(originalMethod) {\n return (...args) => {\n originalMethod(...args);\n this.handleUrlChange(false);\n };\n }\n\n /**\n * Handles responding to the popstate event and calls\n * `handleUrlChange()` indicating that history was updated.\n */\n handlePopState() {\n this.handleUrlChange(true);\n }\n\n /**\n * Updates the page and title fields on the tracker and sends a pageview\n * if a new history entry was created.\n * @param {boolean} historyDidUpdate True if the history was changed via\n * `pushState()` or the `popstate` event. False if the history was just\n * modified via `replaceState()`.\n */\n handleUrlChange(historyDidUpdate) {\n // Calls the update logic asychronously to help ensure that app logic\n // responding to the URL change happens prior to this.\n setTimeout(() => {\n const oldPath = this.path;\n const newPath = getPath();\n\n if (oldPath != newPath &&\n this.opts.shouldTrackUrlChange.call(this, newPath, oldPath)) {\n this.path = newPath;\n this.tracker.set({\n page: newPath,\n title: document.title,\n });\n\n if (historyDidUpdate || this.opts.trackReplaceState) {\n /** @type {FieldsObj} */\n const defaultFields = {transport: 'beacon'};\n this.tracker.send('pageview', createFieldsObj(defaultFields,\n this.opts.fieldsObj, this.tracker, this.opts.hitFilter));\n }\n }\n }, 0);\n }\n\n /**\n * Determines whether or not the tracker should send a hit with the new page\n * data. This default implementation can be overrided in the config options.\n * @param {string} newPath The path after the URL change.\n * @param {string} oldPath The path prior to the URL change.\n * @return {boolean} Whether or not the URL change should be tracked.\n */\n shouldTrackUrlChange(newPath, oldPath) {\n return !!(newPath && oldPath);\n }\n\n /**\n * Removes all event listeners and restores overridden methods.\n */\n remove() {\n MethodChain.remove(history, 'pushState', this.pushStateOverride);\n MethodChain.remove(history, 'replaceState', this.replaceStateOverride);\n window.removeEventListener('popstate', this.handlePopState);\n }\n}\n\n\nprovide('urlChangeTracker', UrlChangeTracker);\n\n\n/**\n * @return {string} The path value of the current URL.\n */\nfunction getPath() {\n return location.pathname + location.search;\n}\n"]} \ No newline at end of file diff --git a/bin/build.js b/bin/build.js index 1c59a8a8..6624e670 100644 --- a/bin/build.js +++ b/bin/build.js @@ -17,13 +17,13 @@ /* eslint-env node */ /* eslint require-jsdoc: "off" */ - +/* eslint no-throw-literal: "off" */ const fs = require('fs-extra'); const glob = require('glob'); const {compile}= require('google-closure-compiler-js'); const {rollup} = require('rollup'); -const memory = require('rollup-plugin-memory'); +const virtual = require('rollup-plugin-virtual'); const nodeResolve = require('rollup-plugin-node-resolve'); const path = require('path'); const {SourceMapGenerator, SourceMapConsumer} = require('source-map'); @@ -34,91 +34,89 @@ const kebabCase = (str) => { }; -module.exports = (output, autotrackPlugins = []) => { - const entryPath = path.resolve(__dirname, '../lib/index.js'); - const entry = autotrackPlugins.length === 0 ? entryPath : { - path: entryPath, - contents: autotrackPlugins - .map((plugin) => `import './plugins/${kebabCase(plugin)}';`) - .join('\n'), - }; - const plugins = [nodeResolve()]; - if (autotrackPlugins.length) plugins.push(memory()); - - return new Promise((resolve, reject) => { - rollup({entry, plugins}).then((bundle) => { - try { - const rollupResult = bundle.generate({ - format: 'es', - dest: output, - sourceMap: true, - }); - - const externsDir = path.resolve(__dirname, '../lib/externs'); - const externs = glob.sync(path.join(externsDir, '*.js')) - .reduce((acc, cur) => acc + fs.readFileSync(cur, 'utf-8'), ''); - - const closureFlags = { - jsCode: [{ - src: rollupResult.code, - path: path.basename(output), - }], - compilationLevel: 'ADVANCED', - useTypesForOptimization: true, - outputWrapper: - '(function(){%output%})();\n' + - `//# sourceMappingURL=${path.basename(output)}.map`, - assumeFunctionWrapper: true, - rewritePolyfills: false, - warningLevel: 'VERBOSE', - createSourceMap: true, - externs: [{src: externs}], - }; - - const closureResult = compile(closureFlags); - - if (closureResult.errors.length || closureResult.warnings.length) { - const rollupMap = new SourceMapConsumer(rollupResult.map); - - // Remap errors from the closure compiler output to the original - // files before rollup bundled them. - const remap = (type) => (item) => { - let {line, column, source} = rollupMap.originalPositionFor({ - line: item.lineNo, - column: item.charNo, - }); - source = path.relative('.', path.resolve(__dirname, '..', source)); - return {type, line, column, source, desc: item.description}; - }; - - reject({ - errors: [ - ...closureResult.errors.map(remap('error')), - ...closureResult.warnings.map(remap('warning')), - ], - }); - } else { - // Currently, closure compiler doesn't support applying its generated - // source map to an existing source map, so we do it manually. - const fromMap = JSON.parse(closureResult.sourceMap); - const toMap = rollupResult.map; - - const generator = SourceMapGenerator.fromSourceMap( - new SourceMapConsumer(fromMap)); - - generator.applySourceMap( - new SourceMapConsumer(toMap), path.basename(output)); - - const sourceMap = generator.toString(); - - resolve({ - code: closureResult.compiledCode, - map: sourceMap, - }); - } - } catch(err) { - reject(err); - } - }).catch(reject); +module.exports = async (output, autotrackPlugins = []) => { + const input = path.resolve(__dirname, '../lib/index.js'); + + const plugins = []; + if (autotrackPlugins.length) { + const pluginPath = path.resolve(__dirname, '../lib/plugins'); + + // Generate the input file based on the autotrack plugins to bundle. + plugins.push(virtual({ + [input]: autotrackPlugins + .map((plugin) => `import '${pluginPath}/${kebabCase(plugin)}';`) + .join('\n'), + })); + } + plugins.push(nodeResolve()); + + const bundle = await rollup({input, plugins}); + const rollupResult = await bundle.generate({ + format: 'es', + dest: output, + sourcemap: true, // Note: lowercase "m" in sourcemap. }); + + const externsDir = path.resolve(__dirname, '../lib/externs'); + const externs = glob.sync(path.join(externsDir, '*.js')) + .reduce((acc, cur) => acc + fs.readFileSync(cur, 'utf-8'), ''); + + const closureFlags = { + jsCode: [{ + src: rollupResult.code, + path: path.basename(output), + }], + compilationLevel: 'ADVANCED', + useTypesForOptimization: true, + outputWrapper: + '(function(){%output%})();\n' + + `//# sourceMappingURL=${path.basename(output)}.map`, + assumeFunctionWrapper: true, + rewritePolyfills: false, + warningLevel: 'VERBOSE', + createSourceMap: true, + externs: [{src: externs}], + }; + + const closureResult = compile(closureFlags); + + if (closureResult.errors.length || closureResult.warnings.length) { + const rollupMap = await new SourceMapConsumer(rollupResult.map); + + // Remap errors from the closure compiler output to the original + // files before rollup bundled them. + const remap = (type) => (item) => { + let {line, column, source} = rollupMap.originalPositionFor({ + line: item.lineNo, + column: item.charNo, + }); + source = path.relative('.', path.resolve(__dirname, '..', source)); + return {type, line, column, source, desc: item.description}; + }; + + throw { + errors: [ + ...closureResult.errors.map(remap('error')), + ...closureResult.warnings.map(remap('warning')), + ], + }; + } else { + // Currently, closure compiler doesn't support applying its generated + // source map to an existing source map, so we do it manually. + const fromMap = JSON.parse(closureResult.sourceMap); + const toMap = rollupResult.map; + + const generator = SourceMapGenerator.fromSourceMap( + await new SourceMapConsumer(fromMap)); + + generator.applySourceMap( + await new SourceMapConsumer(toMap), path.basename(output)); + + const sourceMap = generator.toString(); + + return { + code: closureResult.compiledCode, + map: sourceMap, + }; + } }; diff --git a/docs/common-options.md b/docs/common-options.md index 058870f7..e7b0f06f 100644 --- a/docs/common-options.md +++ b/docs/common-options.md @@ -74,7 +74,7 @@ ga('require', 'eventTracker', { ```js ga('require', 'impressionTracker', { elements: ['cta'], - attributePrefix: 'data-ga' + attributePrefix: 'data-ga-' }); ``` diff --git a/docs/plugins/clean-url-tracker.md b/docs/plugins/clean-url-tracker.md index 08123ed7..617e4453 100644 --- a/docs/plugins/clean-url-tracker.md +++ b/docs/plugins/clean-url-tracker.md @@ -37,7 +37,7 @@ The `cleanUrlTracker` plugin helps you do this. It lets you specify a preference The `cleanUrlPlugin` works by intercepting each hit as it's being sent and modifying the [`page`](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#page) field based on the rules specified by the configuration [options](#options). The plugin also intercepts calls to [`tracker.get()`] that reference the `page` field, so other plugins that use `page` data get the cleaned versions instead of the original versions. -**Note:** while the `cleanUrlTracker` plugin does modify the `page` field value for each hit, it never modifies the [`location`](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#location) field. This allows campaign and site search data encoded in the full URL to be preserved. +**Note:** while the `cleanUrlTracker` plugin does modify the `page` field value for each hit, it never modifies the [`location`](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#location) field. This allows campaign (e.g. `utm` params) and adwords (e.g. `glclid`) data encoded in the full URL to be preserved. ## Usage @@ -65,6 +65,13 @@ The following table outlines all possible configuration options for the `cleanUr Default: false + + queryParamsWhitelist + Array + + An array of query params not to strip. This is most commonly used in conjunction with site search, as shown in the queryParamsWhitelist example below. + + queryDimensionIndex number @@ -92,7 +99,7 @@ The following table outlines all possible configuration options for the `cleanUr

A function that is passed a fieldsObj (containing the location and page fields and optionally the custom dimension field set via queryDimensionIndex) as its first argument and a parseUrl utility function (which returns a Location-like object) as its second argument.

The urlFieldsFilter function must return a fieldsObj (either the passed one or a new one), and the returned fields will be sent with all hits. Non-URL fields set on the fieldsObj are ignored.

-

Warning: be careful when modifying the location field as it's used to determine many session-level dimensions in Google Analytics (e.g. utm campaign data, site search, hostname, etc.). Unless you need to update the hostname, it's usually better to only modify the page field.

+

Warning: be careful when modifying the location field as it's used to determine many session-level dimensions in Google Analytics (e.g. utm campaign data, adwords identifiers, hostname, etc.). Unless you need to update the hostname, it's usually better to only modify the page field.

@@ -154,9 +161,29 @@ And given those four URLs, the following fields would be sent to Google Analytic } ``` +### Using the `queryParamsWhitelist` option + +Unlike campaign (e.g. `utm` params) and adwords (e.g. `glclid`) data, [Site Search](https://support.google.com/analytics/answer/1012264) data is not inferred by Google Analytics from the `location` field when the `page` field is present, so any site search query params *must not* be stripped from the `page` field. + +You can preserve individual query params via the `queryParamsWhitelist` option: + +```js +ga('require', 'cleanUrlTracker', { + stripQuery: true, + queryParamsWhitelist: ['q'], +}); +``` + +Note that *not* stripping site search params from your URLs means those params will still show up in your page reports. If you don't want this to happen you can update your view's [Site Search setup](https://support.google.com/analytics/answer/1012264) as follows: + +1. Specify the same parameter(s) you set in the `queryParamsWhitelist` option. +2. Check the "Strip query parameters out of URL" box. + +These options combined will allow you to keep all unwanted query params out of your page reports and still use site search. + ### Using the `urlFieldsFilter` option -If the available configuration options are not sufficient for your needs, you can use the `urlFieldsFilter` option to arbirarily modify the URL fields sent to Google Analytics. +If the available configuration options are not sufficient for your needs, you can use the `urlFieldsFilter` option to arbitrarily modify the URL fields sent to Google Analytics. The following example passes the same options as the basic example above, but in addition it removes user-specific IDs from the page path, e.g. `/users/18542823` becomes `/users/`: diff --git a/docs/plugins/event-tracker.md b/docs/plugins/event-tracker.md index 7f860395..557d7732 100644 --- a/docs/plugins/event-tracker.md +++ b/docs/plugins/event-tracker.md @@ -18,7 +18,7 @@ ga('require', 'eventTracker', options); ### Modifying the HTML -To add declarative interaction tracking to a DOM element, you start by adding a `ga-on` attribute (assuming the default `'ga-'` attribute prefix) and setting its value to a comma-separated list of DOM events you want to track (note: all events specified in the attribute most also be present in the [`events`](#options) configuration option). When any of the specified events is detected, a hit is sent to Google Analytics with the corresponding attribute values present on the element. +To add declarative interaction tracking to a DOM element, you start by adding a `ga-on` attribute (assuming the default `'ga-'` attribute prefix) and setting its value to a comma-separated list of DOM events you want to track (note: all events specified in the attribute must also be present in the [`events`](#options) configuration option). When any of the specified events is detected, a hit is sent to Google Analytics with the corresponding attribute values present on the element. Any valid [analytics.js field](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference) can be set declaratively as an attribute. The attribute name can be determined by combining the [`attributePrefix`](#options) option with the [kebab-cased](https://en.wikipedia.org/wiki/Letter_case#Special_case_styles) version of the field name. For example, if you want to set the [`eventCategory`](https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#eventCategory) field and you're using the default `attributePrefix` of `'ga-'`, you would use the attribute name `ga-event-category`. diff --git a/docs/plugins/max-scroll-tracker.md b/docs/plugins/max-scroll-tracker.md index 2ee513c6..f6eebb12 100644 --- a/docs/plugins/max-scroll-tracker.md +++ b/docs/plugins/max-scroll-tracker.md @@ -174,8 +174,8 @@ ga('require', 'maxScrollTracker', { hitFilter: function(model) { var scrollPercentage = model.get('eventLabel'); if (scrollPercentage > 50) { - // Sets the nonInteractive field to `true` for the current hit. - model.set('nonInteraction', true, true); + // Sets the nonInteractive field to `false` for the current hit. + model.set('nonInteraction', false, true); } }, }); diff --git a/gulpfile.js b/gulpfile.js index 7ad181ee..b0f41008 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -14,10 +14,6 @@ * limitations under the License. */ - -require('babel-register')({presets: ['es2015']}); - - const {spawn} = require('child_process'); const fs = require('fs-extra'); const eslint = require('gulp-eslint'); @@ -30,7 +26,6 @@ const path = require('path'); const {rollup} = require('rollup'); const nodeResolve = require('rollup-plugin-node-resolve'); const babel = require('rollup-plugin-babel'); -const runSequence = require('run-sequence'); const sauceConnectLauncher = require('sauce-connect-launcher'); const seleniumServerJar = require('selenium-server-standalone-jar'); const webpack = require('webpack'); @@ -52,7 +47,7 @@ const isProd = () => { }; -gulp.task('javascript', () => { +gulp.task('js:lib', async () => { if (isProd()) { return build('autotrack.js').then(({code, map}) => { fs.outputFileSync('autotrack.js', code, 'utf-8'); @@ -65,31 +60,51 @@ gulp.task('javascript', () => { throw new Error('failed to build autotrack.js'); }); } else { - return rollup({ - entry: './lib/index.js', - plugins: [ - nodeResolve(), - babel({ - babelrc: false, - plugins: ['external-helpers'], - presets: [['es2015', {modules: false}]], - }), - ], - }).then((bundle) => { - return bundle.write({ - dest: 'autotrack.js', - format: 'iife', - sourceMap: true, - }); + const plugins = [nodeResolve()]; + + // At the moment this first conditional is a no-op, but after this issue + // is resolved we can switch to using the closure compiler plugin: + // https://github.com/ampproject/rollup-plugin-closure-compiler/issues/42 + if (isProd()) { + // const compiler = require('@ampproject/rollup-plugin-closure-compiler'); + // plugins.push(compiler({ + // compilation_level: 'ADVANCED', + // warning_level: 'VERBOSE', + // language_out: 'ES5', + // output_wrapper: '(function(){%output%})();', + // assume_function_wrapper: true, + // use_types_for_optimization: true, + // rewrite_polyfills: false, + // externs: glob.sync('lib/externs/*.js'), + // })); + } else { + // Note: remove babel() when developing for easier debugging. + plugins.push(babel({ + babelrc: false, + plugins: ['external-helpers'], + presets: [['env', {modules: false}]], + })); + } + + const bundle = await rollup({ + input: './lib/index.js', + plugins, + }); + + await bundle.write({ + file: 'autotrack.js', + format: 'es', + sourcemap: true, }); } }); -gulp.task('javascript:unit', ((compiler) => { +gulp.task('js:test', ((compiler) => { const createCompiler = () => { return webpack({ - entry: glob.sync('./test/unit/**/*-test.js'), + mode: 'development', + entry: ['babel-polyfill', ...glob.sync('./test/unit/**/*-test.js')], output: { path: path.resolve(__dirname, 'test/unit'), filename: 'index.js', @@ -98,18 +113,23 @@ gulp.task('javascript:unit', ((compiler) => { cache: {}, performance: {hints: false}, module: { - loaders: [{ - test: /\.js$/, - exclude: /node_modules\/(?!(dom-utils)\/).*/, - loader: 'babel-loader', - query: { - babelrc: false, - cacheDirectory: false, - presets: [ - ['es2015', {'modules': false}], - ], + // Note: comment this rule out when testing for easier debugging. + rules: [ + { + test: /\.m?js$/, + use: { + loader: 'babel-loader', + options: { + babelrc: false, + cacheDirectory: true, + presets: [['env', { + modules: false, + useBuiltIns: true, + }]], + }, + }, }, - }], + ], }, }); }; @@ -123,11 +143,14 @@ gulp.task('javascript:unit', ((compiler) => { })()); +gulp.task('js', gulp.series('js:lib', 'js:test')); + + gulp.task('lint', () => { return gulp.src([ - 'gulpfile.babel.js', + 'gulpfile.js', 'bin/autotrack', - 'bin/*.js', + 'bin/build.js', 'lib/*.js', 'lib/plugins/*.js', 'test/e2e/*.js', @@ -140,37 +163,27 @@ gulp.task('lint', () => { }); -gulp.task('test:e2e', ['javascript', 'lint', 'tunnel', 'selenium'], () => { - const stopServers = () => { - // TODO(philipwalton): re-add this logic to close the tunnel once this is - // fixed: https://github.com/bermi/sauce-connect-launcher/issues/116 - // process.on('exit', sshTunnel.close.bind(sshTunnel)); - sshTunnel.close(); - server.stop(); - if (!process.env.CI) { - seleniumServer.kill(); - } - }; - return gulp.src('./test/e2e/wdio.conf.js') - .pipe(webdriver()) - .on('end', stopServers); -}); - +gulp.task('selenium', (done) => { + // Don't start the selenium server on CI. + if (process.env.CI) return done(); -gulp.task('test:unit', ['javascript', 'javascript:unit'], (done) => { - spawn( - './node_modules/.bin/easy-sauce', - ['-c', 'test/unit/easy-sauce-config.json'], - {stdio: [0, 1, 2]}).on('end', done); + seleniumServer = spawn('java', ['-jar', seleniumServerJar.path]); + seleniumServer.stderr.on('data', (data) => { + if (data.indexOf('Selenium Server is up and running') > -1) { + done(); + } + }); + process.on('exit', seleniumServer.kill.bind(seleniumServer)); }); -gulp.task('test', (done) => { - runSequence('test:e2e', 'test:unit', done); -}); +gulp.task('serve', gulp.series('js', (done) => { + server.start(done); + process.on('exit', server.stop.bind(server)); +})); -gulp.task('tunnel', ['serve'], (done) => { +gulp.task('tunnel', (done) => { const opts = { username: process.env.SAUCE_USERNAME, accessKey: process.env.SAUCE_ACCESS_KEY, @@ -192,30 +205,48 @@ gulp.task('tunnel', ['serve'], (done) => { }); -gulp.task('serve', ['javascript', 'javascript:unit'], (done) => { - server.start(done); - process.on('exit', server.stop.bind(server)); -}); - +gulp.task('test:e2e', gulp.series( + 'lint', 'js', 'serve', 'tunnel', 'selenium', () => { + const stopServers = () => { + // TODO(philipwalton): re-add this logic to close the tunnel once this is + // fixed: https://github.com/bermi/sauce-connect-launcher/issues/116 + // process.on('exit', sshTunnel.close.bind(sshTunnel)); + sshTunnel.close(); + server.stop(); + if (!process.env.CI) { + seleniumServer.kill(); + } + }; + return gulp.src('./test/e2e/wdio.conf.js') + .pipe(webdriver()) + .on('end', stopServers); +})); -gulp.task('selenium', (done) => { - // Don't start the selenium server on CI. - if (process.env.CI) return done(); - seleniumServer = spawn('java', ['-jar', seleniumServerJar.path]); - seleniumServer.stderr.on('data', (data) => { - if (data.indexOf('Selenium Server is up and running') > -1) { +gulp.task('test:unit', gulp.series('lint', 'js', (done) => { + const easySauceProcess = spawn('./node_modules/.bin/easy-sauce', + ['-c', 'test/unit/easy-sauce-config.json'], + {stdio: [0, 1, 2]}); + + easySauceProcess + .on('error', (err) => done(err)) + .on('exit', (code, signal) => { + if (code > 0) { + return done(new Error(`Process exited with code ${code}`)); + } + if (signal) { + return done(new Error(`Process exited with signal ${signal}`)); + } done(); - } - }); - process.on('exit', seleniumServer.kill.bind(seleniumServer)); -}); + }); +})); -gulp.task('watch', ['serve'], () => { - gulp.watch('./lib/**/*.js', ['javascript']); - gulp.watch([ - './lib/**/*.js', - './test/unit/**/*-test.js', - ], ['javascript:unit']); -}); +gulp.task('test', gulp.series('test:e2e', 'test:unit')); + + +gulp.task('watch', gulp.series('serve', () => { + gulp.watch('./lib/**/*.js', gulp.series('js:lib')); + gulp.watch(['./lib/**/*.js', './test/unit/**/*.js', '!./test/unit/index.js'], + gulp.series('js:test')); +})); diff --git a/lib/constants.js b/lib/constants.js index b790d854..b7149d34 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -15,7 +15,7 @@ */ -export const VERSION = '2.3.2'; +export const VERSION = '2.4.1'; export const DEV_ID = 'i5iSjo'; export const VERSION_PARAM = '_av'; diff --git a/lib/event-emitter.js b/lib/event-emitter.js index 6b8bfa87..196f18b7 100644 --- a/lib/event-emitter.js +++ b/lib/event-emitter.js @@ -63,18 +63,6 @@ export default class EventEmitter { this.getRegistry_(event).forEach((fn) => fn(...args)); } - /** - * Returns the total number of event handlers currently registered. - * @return {number} - */ - getEventCount() { - let eventCount = 0; - Object.keys(this.registry_).forEach((event) => { - eventCount += this.getRegistry_(event).length; - }); - return eventCount; - } - /** * Returns an array of handlers associated with the passed event name. * If no handlers have been registered, an empty array is returned. diff --git a/lib/externs/clean-url-tracker.js b/lib/externs/clean-url-tracker.js index 7a4fe5d3..3392cd1d 100644 --- a/lib/externs/clean-url-tracker.js +++ b/lib/externs/clean-url-tracker.js @@ -2,6 +2,7 @@ * Public options for the CleanUrlTracker. * @typedef {{ * stripQuery: (boolean|undefined), + * queryParamsWhitelist: (Array|undefined), * queryDimensionIndex: (number|undefined), * indexFilename: (string|undefined), * trailingSlash: (string|undefined), diff --git a/lib/externs/request-idle-callback.js b/lib/externs/request-idle-callback.js new file mode 100644 index 00000000..f4344f94 --- /dev/null +++ b/lib/externs/request-idle-callback.js @@ -0,0 +1,76 @@ +/* + * Copyright 2015 The Closure Compiler Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * @fileoverview Definitions for cooperative scheduling of background tasks in + * the browser. This spec is still very likely to change. + * + * @see https://w3c.github.io/requestidlecallback/ + * @see https://developers.google.com/web/updates/2015/08/27/using-requestidlecallback?hl=en + * @externs + */ + + +/** + * @typedef {{ + * timeout: (number|undefined) + * }} + */ +var IdleCallbackOptions; + + +/** + * Schedules a callback to run when the browser is idle. + * @param {function(!IdleDeadline)} callback Called when the browser is idle. + * @param {number|IdleCallbackOptions=} opt_options If set, gives the browser a time in ms by which + * it must execute the callback. No timeout enforced otherwise. + * @return {number} A handle that can be used to cancel the scheduled callback. + */ +function requestIdleCallback(callback, opt_options) {} + + +/** + * Cancels a callback scheduled to run when the browser is idle. + * @param {number} handle The handle returned by `requestIdleCallback` for + * the scheduled callback to cancel. + * @return {undefined} + */ +function cancelIdleCallback(handle) {} + + + +/** + * An interface for an object passed into the callback for + * `requestIdleCallback` that remains up-to-date on the amount of idle + * time left in the current time slice. + * @interface + */ +function IdleDeadline() {} + + +/** + * @return {number} The amount of idle time (milliseconds) remaining in the + * current time slice. Will always be positive or 0. + */ +IdleDeadline.prototype.timeRemaining = function() {}; + + +/** + * Whether the callback was forced to run due to a timeout. Specifically, + * whether the callback was invoked by the idle callback timeout algorithm: + * https://w3c.github.io/requestidlecallback/#dfn-invoke-idle-callback-timeout-algorithm + * @type {boolean} + */ +IdleDeadline.prototype.didTimeout; diff --git a/lib/externs/store.js b/lib/externs/store.js new file mode 100644 index 00000000..eea18e48 --- /dev/null +++ b/lib/externs/store.js @@ -0,0 +1,8 @@ +/** + * Store options data schema. + * @typedef {{ + * timestampKey: (string|undefined), + * defaults: (Object|undefined), + * }} + */ +var StoreOpts; diff --git a/lib/externs/utilities.js b/lib/externs/utilities.js new file mode 100644 index 00000000..9dcda993 --- /dev/null +++ b/lib/externs/utilities.js @@ -0,0 +1,2 @@ +var safari; +safari.pushNotification; diff --git a/lib/externs/window.js b/lib/externs/window.js new file mode 100644 index 00000000..2c31463e --- /dev/null +++ b/lib/externs/window.js @@ -0,0 +1,19 @@ +/** + * @param {string} type + * @param {EventListener|function(!Event):(boolean|undefined)} listener + * @param {(boolean|!AddEventListenerOptions)=} opt_options + * @return {undefined} + * @see https://dom.spec.whatwg.org/#dom-eventtarget-addeventlistener + */ +function addEventListener(type, listener, opt_options) { +}; + +/** + * @param {string} type + * @param {EventListener|function(!Event):(boolean|undefined)} listener + * @param {(boolean|!EventListenerOptions)=} opt_options + * @return {undefined} + * @see https://dom.spec.whatwg.org/#dom-eventtarget-removeeventlistener + */ +function removeEventListener(type, listener, opt_options) { +}; diff --git a/lib/method-chain.js b/lib/method-chain.js index 81c220a8..55abd7ec 100644 --- a/lib/method-chain.js +++ b/lib/method-chain.js @@ -46,13 +46,17 @@ export default class MethodChain { /** * Removes a method chain added via `add()`. If the override is the - * only override added, the original method is restored. + * only override added, the original method is restored. If the method + * chain does not exist, nothing happens. * @param {!Object} context The object containing the method to unchain. * @param {string} methodName The name of the method on the object. * @param {!Function} methodOverride The override method to remove. */ static remove(context, methodName, methodOverride) { - getOrCreateMethodChain(context, methodName).remove(methodOverride); + let methodChain = getMethodChain(context, methodName); + if (methodChain) { + methodChain.remove(methodOverride); + } } /** @@ -144,6 +148,18 @@ export default class MethodChain { } +/** + * Gets a MethodChain instance for the passed object and method. + * @param {!Object} context The object containing the method. + * @param {string} methodName The name of the method on the object. + * @return {!MethodChain|undefined} + */ +function getMethodChain(context, methodName) { + return instances + .filter((h) => h.context == context && h.methodName == methodName)[0]; +} + + /** * Gets a MethodChain instance for the passed object and method. If the method * has already been wrapped via an existing MethodChain instance, that @@ -153,8 +169,7 @@ export default class MethodChain { * @return {!MethodChain} */ function getOrCreateMethodChain(context, methodName) { - let methodChain = instances - .filter((h) => h.context == context && h.methodName == methodName)[0]; + let methodChain = getMethodChain(context, methodName); if (!methodChain) { methodChain = new MethodChain(context, methodName); diff --git a/lib/plugins/clean-url-tracker.js b/lib/plugins/clean-url-tracker.js index c92bee9b..fac86637 100644 --- a/lib/plugins/clean-url-tracker.js +++ b/lib/plugins/clean-url-tracker.js @@ -42,6 +42,7 @@ class CleanUrlTracker { /** @type {CleanUrlTrackerOpts} */ const defaultOpts = { // stripQuery: undefined, + // queryParamsWhitelist: undefined, // queryDimensionIndex: undefined, // indexFilename: undefined, // trailingSlash: undefined, @@ -140,7 +141,8 @@ class CleanUrlTracker { /** @type {!FieldsObj} */ const cleanedFieldsObj = { - page: pathname + (!this.opts.stripQuery ? url.search : ''), + page: pathname + (this.opts.stripQuery ? + this.stripNonWhitelistedQueryParams(url.search) : url.search), }; if (fieldsObj.location) { cleanedFieldsObj.location = fieldsObj.location; @@ -157,16 +159,43 @@ class CleanUrlTracker { this.opts.urlFieldsFilter(cleanedFieldsObj, parseUrl); // Ensure only the URL fields are returned. - return { + const returnValue = { page: userCleanedFieldsObj.page, location: userCleanedFieldsObj.location, - [this.queryDimension]: userCleanedFieldsObj[this.queryDimension], }; + if (this.queryDimension) { + returnValue[this.queryDimension] = + userCleanedFieldsObj[this.queryDimension]; + } + return returnValue; } else { return cleanedFieldsObj; } } + /** + * Accpets a raw URL search string and returns a new search string containing + * only the site search params (if they exist). + * @param {string} searchString The URL search string (starting with '?'). + * @return {string} The query string + */ + stripNonWhitelistedQueryParams(searchString) { + if (Array.isArray(this.opts.queryParamsWhitelist)) { + const foundParams = []; + searchString.slice(1).split('&').forEach((kv) => { + const [key, value] = kv.split('='); + if (this.opts.queryParamsWhitelist.indexOf(key) > -1 && value) { + foundParams.push([key, value]); + } + }); + + return foundParams.length ? + '?' + foundParams.map((kv) => kv.join('=')).join('&') : ''; + } else { + return ''; + } + } + /** * Restores all overridden tasks and methods. */ diff --git a/lib/plugins/event-tracker.js b/lib/plugins/event-tracker.js index a7bf2282..90e61564 100644 --- a/lib/plugins/event-tracker.js +++ b/lib/plugins/event-tracker.js @@ -17,8 +17,9 @@ import {delegate} from 'dom-utils'; import provide from '../provide'; +import TrackerQueue from '../tracker-queue'; import {plugins, trackUsage} from '../usage'; -import {assign, createFieldsObj, getAttributeFields} from '../utilities'; +import {assign, createFieldsObj, getAttributeFields, now} from '../utilities'; /** @@ -34,9 +35,6 @@ class EventTracker { constructor(tracker, opts) { trackUsage(tracker, plugins.EVENT_TRACKER); - // Feature detects to prevent errors in unsupporting browsers. - if (!window.addEventListener) return; - /** @type {EventTrackerOpts} */ const defaultOpts = { events: ['click'], @@ -46,20 +44,19 @@ class EventTracker { }; this.opts = /** @type {EventTrackerOpts} */ (assign(defaultOpts, opts)); - this.tracker = tracker; // Binds methods. this.handleEvents = this.handleEvents.bind(this); const selector = '[' + this.opts.attributePrefix + 'on]'; - - // Creates a mapping of events to their delegates this.delegates = {}; this.opts.events.forEach((event) => { this.delegates[event] = delegate(document, event, selector, this.handleEvents, {composed: true, useCapture: true}); }); + + this.queue = TrackerQueue.getOrCreate(tracker.get('trackingId')); } /** @@ -68,26 +65,33 @@ class EventTracker { * @param {Element} element The delegated DOM element target. */ handleEvents(event, element) { - const prefix = this.opts.attributePrefix; - const events = element.getAttribute(prefix + 'on').split(/\s*,\s*/); + this.queue.pushTask(({time}) => { + const prefix = this.opts.attributePrefix; + const events = element.getAttribute(prefix + 'on').split(/\s*,\s*/); + + // Ensures the type matches one of the events specified on the element. + if (events.indexOf(event.type) < 0) return; - // Ensures the type matches one of the events specified on the element. - if (events.indexOf(event.type) < 0) return; + /** @type {FieldsObj} */ + const defaultFields = { + transport: 'beacon', + queueTime: now() - time, + }; - /** @type {FieldsObj} */ - const defaultFields = {transport: 'beacon'}; - const attributeFields = getAttributeFields(element, prefix); - const userFields = assign({}, this.opts.fieldsObj, attributeFields); - const hitType = attributeFields.hitType || 'event'; + const attributeFields = getAttributeFields(element, prefix); + const userFields = assign({}, this.opts.fieldsObj, attributeFields); + const hitType = attributeFields.hitType || 'event'; - this.tracker.send(hitType, createFieldsObj(defaultFields, - userFields, this.tracker, this.opts.hitFilter, element, event)); + this.tracker.send(hitType, createFieldsObj(defaultFields, + userFields, this.tracker, this.opts.hitFilter, element, event)); + }); } /** * Removes all event listeners and instance properties. */ remove() { + this.queue.destroy(); Object.keys(this.delegates).forEach((key) => { this.delegates[key].destroy(); }); diff --git a/lib/plugins/impression-tracker.js b/lib/plugins/impression-tracker.js index 2f93e22a..50e0d742 100644 --- a/lib/plugins/impression-tracker.js +++ b/lib/plugins/impression-tracker.js @@ -16,9 +16,10 @@ import provide from '../provide'; +import TrackerQueue from '../tracker-queue'; import {plugins, trackUsage} from '../usage'; import {assign, createFieldsObj, - domReady, getAttributeFields} from '../utilities'; + domReady, getAttributeFields, now} from '../utilities'; /** @@ -74,6 +75,8 @@ class ImpressionTracker { // IntersectionObserver instance specific to that threshold. this.thresholdMap = {}; + this.queue = TrackerQueue.getOrCreate(tracker.get('trackingId')); + // Once the DOM is ready, start observing for changes (if present). domReady(() => { if (this.opts.elements) { @@ -87,42 +90,39 @@ class ImpressionTracker { * @param {Array} elements */ observeElements(elements) { - const data = this.deriveDataFromElements(elements); - - // Merge the new data with the data already on the plugin instance. - this.items = this.items.concat(data.items); - this.elementMap = assign({}, data.elementMap, this.elementMap); - this.thresholdMap = assign({}, data.thresholdMap, this.thresholdMap); - - // Observe each new item. - data.items.forEach((item) => { - const observer = this.thresholdMap[item.threshold] = - (this.thresholdMap[item.threshold] || new IntersectionObserver( - this.handleIntersectionChanges, { - rootMargin: this.opts.rootMargin, - threshold: [+item.threshold], - })); - - const element = this.elementMap[item.id] || - (this.elementMap[item.id] = document.getElementById(item.id)); - - if (element) { - observer.observe(element); - } - }); - - if (!this.mutationObserver) { - this.mutationObserver = new MutationObserver(this.handleDomMutations); - this.mutationObserver.observe(document.body, { - childList: true, - subtree: true, + this.queue.pushTask(() => { + const data = this.deriveDataFromElements(elements); + + // Merge the new data with the data already on the plugin instance. + this.items = this.items.concat(data.items); + this.elementMap = assign({}, data.elementMap, this.elementMap); + this.thresholdMap = assign({}, data.thresholdMap, this.thresholdMap); + + // Observe each new item. + data.items.forEach((item) => { + const observer = this.thresholdMap[item.threshold] = + (this.thresholdMap[item.threshold] || new IntersectionObserver( + this.handleIntersectionChanges, { + rootMargin: this.opts.rootMargin, + threshold: [+item.threshold], + })); + + const element = this.elementMap[item.id] || + (this.elementMap[item.id] = document.getElementById(item.id)); + + if (element) { + observer.observe(element); + } }); - } - // TODO(philipwalton): Remove temporary hack to force a new frame - // immediately after adding observers. - // https://bugs.chromium.org/p/chromium/issues/detail?id=612323 - requestAnimationFrame(() => {}); + if (!this.mutationObserver) { + this.mutationObserver = new MutationObserver(this.handleDomMutations); + this.mutationObserver.observe(document.body, { + childList: true, + subtree: true, + }); + } + }); } /** @@ -131,68 +131,78 @@ class ImpressionTracker { * @return {undefined} */ unobserveElements(elements) { - const itemsToKeep = []; - const itemsToRemove = []; - - this.items.forEach((item) => { - const itemInItems = elements.some((element) => { - const itemToRemove = getItemFromElement(element); - return itemToRemove.id === item.id && - itemToRemove.threshold === item.threshold && - itemToRemove.trackFirstImpressionOnly === - item.trackFirstImpressionOnly; + // Since observing elements is queued, unobserving must be queued also or + // we risk this running before the observing. + this.queue.pushTask(() => { + const itemsToKeep = []; + const itemsToRemove = []; + + this.items.forEach((item) => { + const itemInItems = elements.some((element) => { + const itemToRemove = getItemFromElement(element); + return itemToRemove.id === item.id && + itemToRemove.threshold === item.threshold && + itemToRemove.trackFirstImpressionOnly === + item.trackFirstImpressionOnly; + }); + if (itemInItems) { + itemsToRemove.push(item); + } else { + itemsToKeep.push(item); + } }); - if (itemInItems) { - itemsToRemove.push(item); + + // If there are no items to keep, run the `unobserveAllElements` logic. + if (!itemsToKeep.length) { + this.unobserveAllElements(); } else { - itemsToKeep.push(item); + const dataToKeep = this.deriveDataFromElements(itemsToKeep); + const dataToRemove = this.deriveDataFromElements(itemsToRemove); + + this.items = dataToKeep.items; + this.elementMap = dataToKeep.elementMap; + this.thresholdMap = dataToKeep.thresholdMap; + + // Unobserve removed elements. + itemsToRemove.forEach((item) => { + if (!dataToKeep.elementMap[item.id]) { + const observer = dataToRemove.thresholdMap[item.threshold]; + const element = dataToRemove.elementMap[item.id]; + + if (element) { + observer.unobserve(element); + } + + // Disconnect unneeded threshold observers. + if (!dataToKeep.thresholdMap[item.threshold]) { + dataToRemove.thresholdMap[item.threshold].disconnect(); + } + } + }); } }); - - // If there are no items to keep, run the `unobserveAllElements` logic. - if (!itemsToKeep.length) { - this.unobserveAllElements(); - } else { - const dataToKeep = this.deriveDataFromElements(itemsToKeep); - const dataToRemove = this.deriveDataFromElements(itemsToRemove); - - this.items = dataToKeep.items; - this.elementMap = dataToKeep.elementMap; - this.thresholdMap = dataToKeep.thresholdMap; - - // Unobserve removed elements. - itemsToRemove.forEach((item) => { - if (!dataToKeep.elementMap[item.id]) { - const observer = dataToRemove.thresholdMap[item.threshold]; - const element = dataToRemove.elementMap[item.id]; - - if (element) { - observer.unobserve(element); - } - - // Disconnect unneeded threshold observers. - if (!dataToKeep.thresholdMap[item.threshold]) { - dataToRemove.thresholdMap[item.threshold].disconnect(); - } - } - }); - } } /** * Stops observing all currently observed elements. */ unobserveAllElements() { - Object.keys(this.thresholdMap).forEach((key) => { - this.thresholdMap[key].disconnect(); - }); + // Since observing elements is queued, unobserving must be queued also or + // we risk this running before the observing. + this.queue.pushTask(() => { + Object.keys(this.thresholdMap).forEach((key) => { + this.thresholdMap[key].disconnect(); + }); - this.mutationObserver.disconnect(); - this.mutationObserver = null; + if (this.mutationObserver) { + this.mutationObserver.disconnect(); + this.mutationObserver = null; + } - this.items = []; - this.elementMap = {}; - this.thresholdMap = {}; + this.items = []; + this.elementMap = {}; + this.thresholdMap = {}; + }); } /** @@ -261,47 +271,52 @@ class ImpressionTracker { * @param {Array} records A list of `IntersectionObserverEntry` records. */ handleIntersectionChanges(records) { - const itemsToRemove = []; - for (let i = 0, record; record = records[i]; i++) { - for (let j = 0, item; item = this.items[j]; j++) { - if (record.target.id !== item.id) continue; - - if (isTargetVisible(item.threshold, record)) { - this.handleImpression(item.id); - - if (item.trackFirstImpressionOnly) { - itemsToRemove.push(item); + this.queue.pushTask(({time}) => { + const itemsToRemove = []; + for (let i = 0, record; record = records[i]; i++) { + for (let j = 0, item; item = this.items[j]; j++) { + if (record.target.id !== item.id) continue; + + if (isTargetVisible(item.threshold, record)) { + this.handleImpression({id: item.id, impressionTime: time}); + + if (item.trackFirstImpressionOnly) { + itemsToRemove.push(item); + } } } } - } - if (itemsToRemove.length) { - this.unobserveElements(itemsToRemove); - } + if (itemsToRemove.length) { + this.unobserveElements(itemsToRemove); + } + }); } /** * Sends a hit to Google Analytics with the impression data. - * @param {string} id The ID of the element making the impression. + * @param {{id: (string), impressionTime: (number)}} param1 */ - handleImpression(id) { - const element = document.getElementById(id); - - /** @type {FieldsObj} */ - const defaultFields = { - transport: 'beacon', - eventCategory: 'Viewport', - eventAction: 'impression', - eventLabel: id, - nonInteraction: true, - }; - - /** @type {FieldsObj} */ - const userFields = assign({}, this.opts.fieldsObj, - getAttributeFields(element, this.opts.attributePrefix)); - - this.tracker.send('event', createFieldsObj(defaultFields, - userFields, this.tracker, this.opts.hitFilter, element)); + handleImpression({id, impressionTime}) { + this.queue.pushTask(() => { + const element = document.getElementById(id); + + /** @type {FieldsObj} */ + const defaultFields = { + transport: 'beacon', + eventCategory: 'Viewport', + eventAction: 'impression', + eventLabel: id, + nonInteraction: true, + queueTime: now() - impressionTime, + }; + + /** @type {FieldsObj} */ + const userFields = assign({}, this.opts.fieldsObj, + getAttributeFields(element, this.opts.attributePrefix)); + + this.tracker.send('event', createFieldsObj(defaultFields, + userFields, this.tracker, this.opts.hitFilter, element)); + }); } /** @@ -338,6 +353,7 @@ class ImpressionTracker { * @private */ remove() { + this.queue.destroy(); this.unobserveAllElements(); } } diff --git a/lib/plugins/max-scroll-tracker.js b/lib/plugins/max-scroll-tracker.js index b1697113..ed83784e 100644 --- a/lib/plugins/max-scroll-tracker.js +++ b/lib/plugins/max-scroll-tracker.js @@ -20,8 +20,9 @@ import MethodChain from '../method-chain'; import provide from '../provide'; import Session from '../session'; import Store from '../store'; +import TrackerQueue from '../tracker-queue'; import {plugins, trackUsage} from '../usage'; -import {assign, createFieldsObj, debounce, isObject} from '../utilities'; +import {assign, createFieldsObj, debounce, isObject, now} from '../utilities'; /** @@ -37,9 +38,6 @@ class MaxScrollTracker { constructor(tracker, opts) { trackUsage(tracker, plugins.MAX_SCROLL_TRACKER); - // Feature detects to prevent errors in unsupporting browsers. - if (!window.addEventListener) return; - /** @type {MaxScrollTrackerOpts} */ const defaultOpts = { increaseThreshold: 20, @@ -50,31 +48,32 @@ class MaxScrollTracker { // hitFilter: undefined }; - this.opts = /** @type {MaxScrollTrackerOpts} */ ( - assign(defaultOpts, opts)); - + this.opts = /** @type {MaxScrollTrackerOpts} */ (assign(defaultOpts, opts)); this.tracker = tracker; - this.pagePath = this.getPagePath(); // Binds methods to `this`. this.handleScroll = debounce(this.handleScroll.bind(this), 500); this.trackerSetOverride = this.trackerSetOverride.bind(this); - // Creates the store and binds storage change events. + // Override the built-in tracker.set method to watch for changes. + MethodChain.add(tracker, 'set', this.trackerSetOverride); + + this.pagePath = this.getPagePath(); + + const trackingId = tracker.get('trackingId'); + this.store = Store.getOrCreate( - tracker.get('trackingId'), 'plugins/max-scroll-tracker'); + trackingId, 'plugins/max-scroll-tracker'); - // Creates the session and binds session events. this.session = Session.getOrCreate( tracker, this.opts.sessionTimeout, this.opts.timeZone); - // Override the built-in tracker.set method to watch for changes. - MethodChain.add(tracker, 'set', this.trackerSetOverride); + // Queue the rest of the initialization of the plugin idly. + this.queue = TrackerQueue.getOrCreate(trackingId); this.listenForMaxScrollChanges(); } - /** * Adds a scroll event listener if the max scroll percentage for the * current page isn't already at 100%. @@ -82,7 +81,7 @@ class MaxScrollTracker { listenForMaxScrollChanges() { const maxScrollPercentage = this.getMaxScrollPercentageForCurrentPage(); if (maxScrollPercentage < 100) { - window.addEventListener('scroll', this.handleScroll); + addEventListener('scroll', this.handleScroll); } } @@ -91,13 +90,13 @@ class MaxScrollTracker { * Removes an added scroll listener. */ stopListeningForMaxScrollChanges() { - window.removeEventListener('scroll', this.handleScroll); + removeEventListener('scroll', this.handleScroll); } /** * Handles the scroll event. If the current scroll percentage is greater - * that the stored scroll event by at least the specified increase threshold, + * than the stored scroll event by at least the specified increase threshold, * send an event with the increase amount. */ handleScroll() { @@ -105,39 +104,41 @@ class MaxScrollTracker { const scrollPos = window.pageYOffset; // scrollY isn't supported in IE. const windowHeight = window.innerHeight; - // Ensure scrollPercentage is an integer between 0 and 100. - const scrollPercentage = Math.min(100, Math.max(0, - Math.round(100 * (scrollPos / (pageHeight - windowHeight))))); - - // If the max scroll data gets out of the sync with the session data - // (for whatever reason), clear it. - const sessionId = this.session.getId(); - if (sessionId != this.store.get().sessionId) { - this.store.clear(); - this.store.set({sessionId}); - } + this.queue.pushTask(({time}) => { + // Ensure scrollPercentage is an integer between 0 and 100. + const scrollPercentage = Math.min(100, Math.max(0, + Math.round(100 * (scrollPos / (pageHeight - windowHeight))))); + + // If the max scroll data gets out of the sync with the session data + // (for whatever reason), clear it. + const sessionId = this.session.id; + if (sessionId != this.store.data.sessionId) { + this.store.clear(); + this.store.update({sessionId}); + } - // If the session has expired, clear the stored data and don't send any - // events (since they'd start a new session). Note: this check is needed, - // in addition to the above check, to handle cases where the session IDs - // got out of sync, but the session didn't expire. - if (this.session.isExpired(this.store.get().sessionId)) { - this.store.clear(); - } else { - const maxScrollPercentage = this.getMaxScrollPercentageForCurrentPage(); - - if (scrollPercentage > maxScrollPercentage) { - if (scrollPercentage == 100 || maxScrollPercentage == 100) { - this.stopListeningForMaxScrollChanges(); - } - const increaseAmount = scrollPercentage - maxScrollPercentage; - if (scrollPercentage == 100 || - increaseAmount >= this.opts.increaseThreshold) { - this.setMaxScrollPercentageForCurrentPage(scrollPercentage); - this.sendMaxScrollEvent(increaseAmount, scrollPercentage); + // If the session has expired, clear the stored data and don't send any + // events (since they'd start a new session). Note: this check is needed, + // in addition to the above check, to handle cases where the session IDs + // got out of sync, but the session didn't expire. + if (this.session.isExpired(this.store.data.sessionId)) { + this.store.clear(); + } else { + const maxScrollPercentage = this.getMaxScrollPercentageForCurrentPage(); + + if (scrollPercentage > maxScrollPercentage) { + if (scrollPercentage == 100 || maxScrollPercentage == 100) { + this.stopListeningForMaxScrollChanges(); + } + const increaseAmount = scrollPercentage - maxScrollPercentage; + if (scrollPercentage == 100 || + increaseAmount >= this.opts.increaseThreshold) { + this.setMaxScrollPercentageForCurrentPage(scrollPercentage); + this.sendMaxScrollEvent(increaseAmount, scrollPercentage, time); + } } } - } + }); } /** @@ -171,26 +172,31 @@ class MaxScrollTracker { * Sends an event for the increased max scroll percentage amount. * @param {number} increaseAmount * @param {number} scrollPercentage + * @param {number} scrollTimestamp */ - sendMaxScrollEvent(increaseAmount, scrollPercentage) { - /** @type {FieldsObj} */ - const defaultFields = { - transport: 'beacon', - eventCategory: 'Max Scroll', - eventAction: 'increase', - eventValue: increaseAmount, - eventLabel: String(scrollPercentage), - nonInteraction: true, - }; - - // If a custom metric was specified, set it equal to the event value. - if (this.opts.maxScrollMetricIndex) { - defaultFields['metric' + this.opts.maxScrollMetricIndex] = increaseAmount; - } + sendMaxScrollEvent(increaseAmount, scrollPercentage, scrollTimestamp) { + this.queue.pushTask(() => { + /** @type {FieldsObj} */ + const defaultFields = { + transport: 'beacon', + eventCategory: 'Max Scroll', + eventAction: 'increase', + eventValue: increaseAmount, + eventLabel: String(scrollPercentage), + nonInteraction: true, + queueTime: now() - scrollTimestamp, + }; + + // If a custom metric was specified, set it equal to the event value. + if (this.opts.maxScrollMetricIndex) { + defaultFields['metric' + this.opts.maxScrollMetricIndex] = + increaseAmount; + } - this.tracker.send('event', - createFieldsObj(defaultFields, this.opts.fieldsObj, - this.tracker, this.opts.hitFilter)); + this.tracker.send('event', + createFieldsObj(defaultFields, this.opts.fieldsObj, + this.tracker, this.opts.hitFilter)); + }); } /** @@ -198,9 +204,9 @@ class MaxScrollTracker { * @param {number} maxScrollPercentage */ setMaxScrollPercentageForCurrentPage(maxScrollPercentage) { - this.store.set({ + this.store.update({ [this.pagePath]: maxScrollPercentage, - sessionId: this.session.getId(), + sessionId: this.session.id, }); } @@ -209,12 +215,12 @@ class MaxScrollTracker { * @return {number} */ getMaxScrollPercentageForCurrentPage() { - return this.store.get()[this.pagePath] || 0; + return this.store.data[this.pagePath] || 0; } /** * Gets the page path from the tracker object. - * @return {number} + * @return {string} */ getPagePath() { const url = parseUrl( @@ -226,7 +232,10 @@ class MaxScrollTracker { * Removes all event listeners and restores overridden methods. */ remove() { + this.queue.destroy(); + this.store.destroy(); this.session.destroy(); + this.stopListeningForMaxScrollChanges(); MethodChain.remove(this.tracker, 'set', this.trackerSetOverride); } diff --git a/lib/plugins/media-query-tracker.js b/lib/plugins/media-query-tracker.js index c832df78..a3456f33 100644 --- a/lib/plugins/media-query-tracker.js +++ b/lib/plugins/media-query-tracker.js @@ -17,9 +17,10 @@ import {NULL_DIMENSION} from '../constants'; import provide from '../provide'; +import TrackerQueue from '../tracker-queue'; import {plugins, trackUsage} from '../usage'; import {assign, createFieldsObj, - debounce, isObject, toArray} from '../utilities'; + debounce, isObject, now, toArray} from '../utilities'; /** @@ -41,10 +42,7 @@ class MediaQueryTracker { constructor(tracker, opts) { trackUsage(tracker, plugins.MEDIA_QUERY_TRACKER); - // Feature detects to prevent errors in unsupporting browsers. - if (!window.matchMedia) return; - - /** @type {MediaQueryTrackerOpts} */ + /** @type {!MediaQueryTrackerOpts} */ const defaultOpts = { // definitions: unefined, changeTemplate: this.changeTemplate, @@ -53,7 +51,7 @@ class MediaQueryTracker { // hitFilter: undefined, }; - this.opts = /** @type {MediaQueryTrackerOpts} */ ( + this.opts = /** @type {!MediaQueryTrackerOpts} */ ( assign(defaultOpts, opts)); // Exits early if media query data doesn't exist. @@ -63,6 +61,8 @@ class MediaQueryTracker { this.tracker = tracker; this.changeListeners = []; + this.queue = TrackerQueue.getOrCreate(tracker.get('trackingId')); + this.processMediaQueries(); } @@ -85,7 +85,7 @@ class MediaQueryTracker { /** * Takes a definition object and return the name of the matching media item. * If no match is found, the NULL_DIMENSION value is returned. - * @param {Object} definition A set of named media queries associated + * @param {!Object} definition A set of named media queries associated * with a single custom dimension. * @return {string} The name of the matched media or NULL_DIMENSION. */ @@ -103,7 +103,7 @@ class MediaQueryTracker { /** * Adds change listeners to each media query in the definition list. * Debounces the changes to prevent unnecessary hits from being sent. - * @param {Object} definition A set of named media queries associated + * @param {!Object} definition A set of named media queries associated * with a single custom dimension */ addChangeListeners(definition) { @@ -121,7 +121,7 @@ class MediaQueryTracker { /** * Handles changes to the matched media. When the new value differs from * the old value, a change event is sent. - * @param {Object} definition A set of named media queries associated + * @param {!Object} definition A set of named media queries associated * with a single custom dimension */ handleChanges(definition) { @@ -130,7 +130,20 @@ class MediaQueryTracker { if (newValue !== oldValue) { this.tracker.set('dimension' + definition.dimensionIndex, newValue); + this.sendChangeEvent({definition, oldValue, newValue}); + } + } + /** + * Sends a change event. + * @param {{ + * definition: (!Object), + * oldValue: (string), + * newValue: (string), + * }} param1 + */ + sendChangeEvent({definition, oldValue, newValue}) { + this.queue.pushTask(({time}) => { /** @type {FieldsObj} */ const defaultFields = { transport: 'beacon', @@ -138,16 +151,20 @@ class MediaQueryTracker { eventAction: 'change', eventLabel: this.opts.changeTemplate(oldValue, newValue), nonInteraction: true, + queueTime: now() - time, }; + this.tracker.send('event', createFieldsObj(defaultFields, this.opts.fieldsObj, this.tracker, this.opts.hitFilter)); - } + }); } + /** * Removes all event listeners and instance properties. */ remove() { + this.queue.destroy(); for (let i = 0, listener; listener = this.changeListeners[i]; i++) { listener.mql.removeListener(listener.fn); } diff --git a/lib/plugins/outbound-link-tracker.js b/lib/plugins/outbound-link-tracker.js index cf1cc4d2..91b601d4 100644 --- a/lib/plugins/outbound-link-tracker.js +++ b/lib/plugins/outbound-link-tracker.js @@ -85,31 +85,39 @@ class OutboundLinkTracker { eventLabel: url.href, }; + /** @type {FieldsObj} */ + const userFields = assign({}, this.opts.fieldsObj, + getAttributeFields(link, this.opts.attributePrefix)); + + const fieldsObj = createFieldsObj(defaultFields, userFields, + this.tracker, this.opts.hitFilter, link, event); + if (!navigator.sendBeacon && linkClickWillUnloadCurrentPage(event, link)) { // Adds a new event handler at the last minute to minimize the chances // that another event handler for this click will run after this logic. - window.addEventListener('click', function(event) { + const clickHandler = () => { + window.removeEventListener('click', clickHandler); + // Checks to make sure another event handler hasn't already prevented // the default action. If it has the custom redirect isn't needed. if (!event.defaultPrevented) { // Stops the click and waits until the hit is complete (with // timeout) for browsers that don't support beacon. event.preventDefault(); - defaultFields.hitCallback = withTimeout(function() { + + const oldHitCallback = fieldsObj.hitCallback; + fieldsObj.hitCallback = withTimeout(function() { + if (typeof oldHitCallback == 'function') oldHitCallback(); location.href = href; }); } - }); + this.tracker.send('event', fieldsObj); + }; + window.addEventListener('click', clickHandler); + } else { + this.tracker.send('event', fieldsObj); } - - /** @type {FieldsObj} */ - const userFields = assign({}, this.opts.fieldsObj, - getAttributeFields(link, this.opts.attributePrefix)); - - this.tracker.send('event', - createFieldsObj(defaultFields, userFields, - this.tracker, this.opts.hitFilter, link, event)); } } diff --git a/lib/plugins/page-visibility-tracker.js b/lib/plugins/page-visibility-tracker.js index 829f4cff..2212f960 100644 --- a/lib/plugins/page-visibility-tracker.js +++ b/lib/plugins/page-visibility-tracker.js @@ -20,9 +20,9 @@ import MethodChain from '../method-chain'; import provide from '../provide'; import Session from '../session'; import Store from '../store'; +import TrackerQueue from '../tracker-queue'; import {plugins, trackUsage} from '../usage'; -import {assign, createFieldsObj, deferUntilPluginsLoaded, - isObject, now, uuid} from '../utilities'; +import {assign, createFieldsObj, isObject, now, uuid} from '../utilities'; const HIDDEN = 'hidden'; @@ -31,6 +31,9 @@ const PAGE_ID = uuid(); const SECONDS = 1000; +const isSafari_ = !!(typeof safari === 'object' && safari.pushNotification); + + /** * Class for the `pageVisibilityTracker` analytics.js plugin. * @implements {PageVisibilityTrackerPublicInterface} @@ -63,51 +66,70 @@ class PageVisibilityTracker { assign(defaultOpts, opts)); this.tracker = tracker; - this.lastPageState = document.visibilityState; + + this.lastPageVisibilityState = document.visibilityState; this.visibleThresholdTimeout_ = null; this.isInitialPageviewSent_ = false; // Binds methods to `this`. + this.init = this.init.bind(this); this.trackerSetOverride = this.trackerSetOverride.bind(this); this.handleChange = this.handleChange.bind(this); - this.handleWindowUnload = this.handleWindowUnload.bind(this); + this.handleBeforeUnload = this.handleBeforeUnload.bind(this); this.handleExternalStoreSet = this.handleExternalStoreSet.bind(this); - // Creates the store and binds storage change events. + // Override the built-in tracker.set method to watch for changes. + MethodChain.add(tracker, 'set', this.trackerSetOverride); + + addEventListener('visibilitychange', this.handleChange, true); + + // Safari does not reliably fire the `pagehide` or `visibilitychange` + // events when closing a tab, so we have to use `beforeunload` with a + // timeout to check whether the default action was prevented. + // - https://bugs.webkit.org/show_bug.cgi?id=151610 + // - https://bugs.webkit.org/show_bug.cgi?id=151234 + // NOTE: we only add this to Safari because adding it to Firefox would + // prevent the page from being eligible for bfcache. + if (isSafari_) { + addEventListener('beforeunload', this.handleChange, true); + } + + const trackingId = tracker.get('trackingId'); + this.store = Store.getOrCreate( - tracker.get('trackingId'), 'plugins/page-visibility-tracker'); + trackingId, 'plugins/page-visibility-tracker', {timestampKey: 'time'}); + this.store.on('externalSet', this.handleExternalStoreSet); - // Creates the session and binds session events. this.session = Session.getOrCreate( tracker, this.opts.sessionTimeout, this.opts.timeZone); - // Override the built-in tracker.set method to watch for changes. - MethodChain.add(tracker, 'set', this.trackerSetOverride); - - window.addEventListener('unload', this.handleWindowUnload); - document.addEventListener('visibilitychange', this.handleChange); + // Queue the rest of the initialization of the plugin idly. + this.queue = TrackerQueue.getOrCreate(trackingId); + this.queue.pushTask(this.init); + } - // Postpone sending any hits until the next call stack, which allows all - // autotrack plugins to be required sync before any hits are sent. - deferUntilPluginsLoaded(this.tracker, () => { - if (document.visibilityState == VISIBLE) { - if (this.opts.sendInitialPageview) { - this.sendPageview({isPageLoad: true}); - this.isInitialPageviewSent_ = true; - } - this.store.set(/** @type {PageVisibilityStoreData} */ ({ - time: now(), - state: VISIBLE, - pageId: PAGE_ID, - sessionId: this.session.getId(), - })); - } else { - if (this.opts.sendInitialPageview && this.opts.pageLoadsMetricIndex) { - this.sendPageLoad(); - } + /** + * Idly initializes the rest of the plugin instance initialization logic. + * @param {{visibilityState: (string), time: (number)}} param1 + */ + init({visibilityState, time}) { + if (visibilityState == VISIBLE) { + if (this.opts.sendInitialPageview) { + this.sendPageview({pageviewTime: time, isPageLoad: true}); + this.isInitialPageviewSent_ = true; } - }); + this.store.update(/** @type {PageVisibilityStoreData} */ ({ + time: time, + state: VISIBLE, + pageId: PAGE_ID, + sessionId: this.session.id, + })); + } else { + if (this.opts.sendInitialPageview && this.opts.pageLoadsMetricIndex) { + this.sendPageLoad({pageLoadTime: time}); + } + } } /** @@ -126,61 +148,77 @@ class PageVisibilityTracker { return; } - const lastStoredChange = this.getAndValidateChangeData(); - - /** @type {PageVisibilityStoreData} */ - const change = { - time: now(), - state: document.visibilityState, - pageId: PAGE_ID, - sessionId: this.session.getId(), - }; - - // If the visibilityState has changed to visible and the initial pageview - // has not been sent (and the `sendInitialPageview` option is `true`). - // Send the initial pageview now. - if (document.visibilityState == VISIBLE && - this.opts.sendInitialPageview && !this.isInitialPageviewSent_) { - this.sendPageview(); - this.isInitialPageviewSent_ = true; - } - // If the visibilityState has changed to hidden, clear any scheduled // pageviews waiting for the visibleThreshold timeout. - if (document.visibilityState == HIDDEN && this.visibleThresholdTimeout_) { + if (document.visibilityState == HIDDEN) { clearTimeout(this.visibleThresholdTimeout_); } - if (this.session.isExpired(lastStoredChange.sessionId)) { - this.store.clear(); - if (this.lastPageState == HIDDEN && - document.visibilityState == VISIBLE) { - // If the session has expired, changes from hidden to visible should - // be considered a new pageview rather than a visibility event. - // This behavior ensures all sessions contain a pageview so - // session-level page dimensions and metrics (e.g. ga:landingPagePath - // and ga:entrances) are correct. - // Also, in order to prevent false positives, we add a small timeout - // that is cleared if the visibilityState changes to hidden shortly - // after the change to visible. This can happen if a user is quickly - // switching through their open tabs but not actually interacting with - // and of them. It can also happen when a user goes to a tab just to - // immediately close it. Such cases should not be considered pageviews. - clearTimeout(this.visibleThresholdTimeout_); - this.visibleThresholdTimeout_ = setTimeout(() => { - this.store.set(change); - this.sendPageview({hitTime: change.time}); - }, this.opts.visibleThreshold); - } - } else { - if (lastStoredChange.pageId == PAGE_ID && - lastStoredChange.state == VISIBLE) { - this.sendPageVisibilityEvent(lastStoredChange); + // In some cases this method is invoked immediately before any + // `tracker.set()`` calls will change the tracker's page field, but since + // the Page Visibility event is idly queued we have to store the page at + // the time right before the change. + const page = this.tracker.get('page'); + + this.queue.pushTask(({visibilityState, time}) => { + const lastStoredChange = this.getAndValidateChangeData(); + + /** @type {PageVisibilityStoreData} */ + const change = { + time: time, + state: visibilityState, + pageId: PAGE_ID, + sessionId: this.session.id, + }; + + if (this.session.isExpired(lastStoredChange.sessionId)) { + this.store.clear(); + + if (this.lastPageVisibilityState == HIDDEN && + visibilityState == VISIBLE) { + // If the session has expired, changes from hidden to visible should + // be considered a new pageview rather than a visibility event. + // This behavior ensures all sessions contain a pageview so + // session-level page dimensions and metrics (e.g. ga:landingPagePath + // and ga:entrances) are correct. + // Also, in order to prevent false positives, we add a small timeout + // that is cleared if the visibilityState changes to hidden shortly + // after the change to visible. This can happen if a user is quickly + // switching through their open tabs but not actually interacting + // with any of them. It can also happen when a user goes to a tab + // just to immediately close it. Such cases should not be considered + // pageviews. + clearTimeout(this.visibleThresholdTimeout_); + + this.visibleThresholdTimeout_ = setTimeout(() => { + this.store.update(change); + this.sendPageview({pageviewTime: time, sessionDidExpire: true}); + }, this.opts.visibleThreshold); + } + } else { + this.store.update(change); + + // If the visibilityState has changed to visible and the initial + // pageview has not been sent (and the `sendInitialPageview` option + // is `true`). Send the initial pageview now. + // Otherwise, track the time the page has been visible if the last + // recorded change was for the current page. + if (visibilityState == VISIBLE && + this.opts.sendInitialPageview && !this.isInitialPageviewSent_) { + this.sendPageview({pageviewTime: time}); + this.isInitialPageviewSent_ = true; + } else if (lastStoredChange.pageId == PAGE_ID && + lastStoredChange.state == VISIBLE) { + this.sendPageVisibilityEvent({ + startTime: lastStoredChange.time, + endTime: time, + page: page, + }); + } } - this.store.set(change); - } - this.lastPageState = document.visibilityState; + this.lastPageVisibilityState = visibilityState; + }); } /** @@ -201,14 +239,14 @@ class PageVisibilityTracker { */ getAndValidateChangeData() { const lastStoredChange = - /** @type {PageVisibilityStoreData} */ (this.store.get()); + /** @type {PageVisibilityStoreData} */ (this.store.data); - if (this.lastPageState == VISIBLE && + if (this.lastPageVisibilityState == VISIBLE && lastStoredChange.state == HIDDEN && lastStoredChange.pageId != PAGE_ID) { lastStoredChange.state = VISIBLE; lastStoredChange.pageId = PAGE_ID; - this.store.set(lastStoredChange); + this.store.update(lastStoredChange); } return lastStoredChange; } @@ -217,84 +255,102 @@ class PageVisibilityTracker { * Sends a Page Visibility event to track the time this page was in the * visible state (assuming it was in that state long enough to meet the * threshold). - * @param {!PageVisibilityStoreData} lastStoredChange - * @param {{hitTime: (number|undefined)}=} param1 - * - hitTime: A hit timestap used to help ensure original order in cases - * where the send is delayed. + * @param {{ + * startTime: (number|undefined), + * endTime: (number|undefined), + * page: (string|undefined), + * }} param1 */ - sendPageVisibilityEvent(lastStoredChange, {hitTime} = {}) { - const delta = this.getTimeSinceLastStoredChange( - lastStoredChange, {hitTime}); + sendPageVisibilityEvent({startTime, endTime, page}) { + const delta = endTime - startTime; // If the detla is greater than the visibileThreshold, report it. if (delta && delta >= this.opts.visibleThreshold) { const deltaInSeconds = Math.round(delta / SECONDS); + this.queue.pushTask(() => { + /** @type {FieldsObj} */ + const defaultFields = { + transport: 'beacon', + nonInteraction: true, + eventCategory: 'Page Visibility', + eventAction: 'track', + eventValue: deltaInSeconds, + eventLabel: NULL_DIMENSION, + queueTime: now() - endTime, + }; + + // `lastVisiblePage` can be an empty string. + if (typeof page == 'string') { + defaultFields.page = page; + } + + // If a custom metric was specified, set it equal to the event value. + if (this.opts.visibleMetricIndex) { + defaultFields['metric' + this.opts.visibleMetricIndex] = + deltaInSeconds; + } + + this.tracker.send('event', + createFieldsObj(defaultFields, this.opts.fieldsObj, + this.tracker, this.opts.hitFilter)); + }); + } + } + + /** + * Sends a page load event. + * @param {{pageLoadTime: (number)}} param1 + */ + sendPageLoad({pageLoadTime}) { + this.queue.pushTask(() => { /** @type {FieldsObj} */ const defaultFields = { transport: 'beacon', - nonInteraction: true, eventCategory: 'Page Visibility', - eventAction: 'track', - eventValue: deltaInSeconds, + eventAction: 'page load', eventLabel: NULL_DIMENSION, + ['metric' + this.opts.pageLoadsMetricIndex]: 1, + nonInteraction: true, + queueTime: pageLoadTime ? now() - pageLoadTime : undefined, }; - if (hitTime) { - defaultFields.queueTime = now() - hitTime; - } - - // If a custom metric was specified, set it equal to the event value. - if (this.opts.visibleMetricIndex) { - defaultFields['metric' + this.opts.visibleMetricIndex] = deltaInSeconds; - } - this.tracker.send('event', createFieldsObj(defaultFields, this.opts.fieldsObj, this.tracker, this.opts.hitFilter)); - } - } - - /** - * Sends a page load event. - */ - sendPageLoad() { - /** @type {FieldsObj} */ - const defaultFields = { - transport: 'beacon', - eventCategory: 'Page Visibility', - eventAction: 'page load', - eventLabel: NULL_DIMENSION, - ['metric' + this.opts.pageLoadsMetricIndex]: 1, - nonInteraction: true, - }; - this.tracker.send('event', - createFieldsObj(defaultFields, this.opts.fieldsObj, - this.tracker, this.opts.hitFilter)); + }); } /** - * Sends a pageview, optionally calculating an offset if hitTime is passed. + * Sends a pageview, optionally calculating an offset if time is passed. * @param {{ - * hitTime: (number|undefined), - * isPageLoad: (boolean|undefined) - * }=} param1 - * hitTime: The timestamp of the current hit. - * isPageLoad: True if this pageview was also a page load. + * pageviewTime: (number), + * isPageLoad: (boolean|undefined), + * sessionDidExpire: (boolean|undefined), + * }} param1 */ - sendPageview({hitTime, isPageLoad} = {}) { - /** @type {FieldsObj} */ - const defaultFields = {transport: 'beacon'}; - if (hitTime) { - defaultFields.queueTime = now() - hitTime; - } - if (isPageLoad && this.opts.pageLoadsMetricIndex) { - defaultFields['metric' + this.opts.pageLoadsMetricIndex] = 1; - } + sendPageview({pageviewTime, isPageLoad, sessionDidExpire}) { + this.queue.pushTask(() => { + /** @type {FieldsObj} */ + const defaultFields = { + transport: 'beacon', + queueTime: now() - pageviewTime, + }; + + if (isPageLoad && this.opts.pageLoadsMetricIndex) { + defaultFields['metric' + this.opts.pageLoadsMetricIndex] = 1; + } - this.tracker.send('pageview', - createFieldsObj(defaultFields, this.opts.fieldsObj, - this.tracker, this.opts.hitFilter)); + this.tracker.send('pageview', + createFieldsObj(defaultFields, this.opts.fieldsObj, + this.tracker, this.opts.hitFilter)); + + // If the session expired, sending a new pageview will generate a new + // session ID. We need to make sure the store has that updated ID. + if (sessionDidExpire) { + this.store.update({sessionId: this.session.id}); + } + }); } /** @@ -309,7 +365,7 @@ class PageVisibilityTracker { /** @type {!FieldsObj} */ const fields = isObject(field) ? field : {[field]: value}; if (fields.page && fields.page !== this.tracker.get('page')) { - if (this.lastPageState == VISIBLE) { + if (this.lastPageVisibilityState == VISIBLE) { this.handleChange(); } } @@ -317,27 +373,14 @@ class PageVisibilityTracker { }; } - /** - * Calculates the time since the last visibility change event in the current - * session. If the session has expired the reported time is zero. - * @param {PageVisibilityStoreData} lastStoredChange - * @param {{hitTime: (number|undefined)}=} param1 - * hitTime: The time of the current hit (defaults to now). - * @return {number} The time (in ms) since the last change. - */ - getTimeSinceLastStoredChange(lastStoredChange, {hitTime} = {}) { - return lastStoredChange.time ? - (hitTime || now()) - lastStoredChange.time : 0; - } - /** * Handles responding to the `storage` event. * The code on this page needs to be informed when other tabs or windows are * updating the stored page visibility state data. This method checks to see * if a hidden state is stored when there are still visible tabs open, which * can happen if multiple windows are open at the same time. - * @param {PageVisibilityStoreData} newData - * @param {PageVisibilityStoreData} oldData + * @param {!PageVisibilityStoreData} newData + * @param {!PageVisibilityStoreData} oldData */ handleExternalStoreSet(newData, oldData) { // If the change times are the same, then the previous write only @@ -353,21 +396,24 @@ class PageVisibilityTracker { if (oldData.pageId == PAGE_ID && oldData.state == VISIBLE && !this.session.isExpired(oldData.sessionId)) { - this.sendPageVisibilityEvent(oldData, {hitTime: newData.time}); + this.sendPageVisibilityEvent({ + startTime: oldData.time, + endTime: newData.time, + }); } } /** - * Handles responding to the `unload` event. + * Handles responding to the `beforeunload` event. * Since some browsers don't emit a `visibilitychange` event in all cases - * where a page might be unloaded, it's necessary to hook into the `unload` - * event to ensure the correct state is always stored. + * where a page might be unloaded, it's necessary to hook into the + * `beforeunload` event to ensure the correct state is always stored. */ - handleWindowUnload() { - // If the stored visibility state isn't hidden when the unload event + handleBeforeUnload() { + // If the stored visibility state isn't hidden when the beforeunload event // fires, it means the visibilitychange event didn't fire as the document // was being unloaded, so we invoke it manually. - if (this.lastPageState != HIDDEN) { + if (this.lastPageVisibilityState != HIDDEN) { this.handleChange(); } } @@ -376,11 +422,13 @@ class PageVisibilityTracker { * Removes all event listeners and restores overridden methods. */ remove() { + this.queue.destroy(); this.store.destroy(); this.session.destroy(); + MethodChain.remove(this.tracker, 'set', this.trackerSetOverride); - window.removeEventListener('unload', this.handleWindowUnload); - document.removeEventListener('visibilitychange', this.handleChange); + removeEventListener('beforeunload', this.handleBeforeUnload, true); + removeEventListener('visibilitychange', this.handleChange, true); } } diff --git a/lib/plugins/social-widget-tracker.js b/lib/plugins/social-widget-tracker.js index 1bd6c92c..66d75ce1 100644 --- a/lib/plugins/social-widget-tracker.js +++ b/lib/plugins/social-widget-tracker.js @@ -16,8 +16,9 @@ import provide from '../provide'; +import TrackerQueue from '../tracker-queue'; import {plugins, trackUsage} from '../usage'; -import {assign, createFieldsObj} from '../utilities'; +import {assign, createFieldsObj, now} from '../utilities'; /** @@ -57,6 +58,8 @@ class SocialWidgetTracker { this.handleLikeEvents = this.handleLikeEvents.bind(this); this.handleUnlikeEvents = this.handleUnlikeEvents.bind(this); + this.queue = TrackerQueue.getOrCreate(tracker.get('trackingId')); + if (document.readyState != 'complete') { // Adds the widget listeners after the window's `load` event fires. // If loading widgets using the officially recommended snippets, they @@ -74,8 +77,10 @@ class SocialWidgetTracker { * Ensures the respective global namespaces are present before adding. */ addWidgetListeners() { - if (window.FB) this.addFacebookEventHandlers(); - if (window.twttr) this.addTwitterEventHandlers(); + this.queue.pushTask(() => { + if (window.FB) this.addFacebookEventHandlers(); + if (window.twttr) this.addTwitterEventHandlers(); + }); } /** @@ -89,7 +94,7 @@ class SocialWidgetTracker { window.twttr.events.bind('tweet', this.handleTweetEvents); window.twttr.events.bind('follow', this.handleFollowEvents); }); - } catch(err) { + } catch (err) { // Do nothing. } } @@ -104,7 +109,7 @@ class SocialWidgetTracker { window.twttr.events.unbind('tweet', this.handleTweetEvents); window.twttr.events.unbind('follow', this.handleFollowEvents); }); - } catch(err) { + } catch (err) { // Do nothing. } } @@ -117,7 +122,7 @@ class SocialWidgetTracker { try { window.FB.Event.subscribe('edge.create', this.handleLikeEvents); window.FB.Event.subscribe('edge.remove', this.handleUnlikeEvents); - } catch(err) { + } catch (err) { // Do nothing. } } @@ -130,7 +135,7 @@ class SocialWidgetTracker { try { window.FB.Event.unsubscribe('edge.create', this.handleLikeEvents); window.FB.Event.unsubscribe('edge.remove', this.handleUnlikeEvents); - } catch(err) { + } catch (err) { // Do nothing. } } @@ -140,22 +145,25 @@ class SocialWidgetTracker { * @param {TwttrEvent} event The Twitter event object passed to the handler. */ handleTweetEvents(event) { - // Ignores tweets from widgets that aren't the tweet button. - if (event.region != 'tweet') return; - - const url = event.data.url || event.target.getAttribute('data-url') || - location.href; - - /** @type {FieldsObj} */ - const defaultFields = { - transport: 'beacon', - socialNetwork: 'Twitter', - socialAction: 'tweet', - socialTarget: url, - }; - this.tracker.send('social', - createFieldsObj(defaultFields, this.opts.fieldsObj, - this.tracker, this.opts.hitFilter, event.target, event)); + this.queue.pushTask(({time}) => { + // Ignores tweets from widgets that aren't the tweet button. + if (event.region != 'tweet') return; + + const url = event.data.url || event.target.getAttribute('data-url') || + location.href; + + /** @type {FieldsObj} */ + const defaultFields = { + transport: 'beacon', + socialNetwork: 'Twitter', + socialAction: 'tweet', + socialTarget: url, + queueTime: now() - time, + }; + this.tracker.send('social', + createFieldsObj(defaultFields, this.opts.fieldsObj, + this.tracker, this.opts.hitFilter, event.target, event)); + }); } /** @@ -163,22 +171,25 @@ class SocialWidgetTracker { * @param {TwttrEvent} event The Twitter event object passed to the handler. */ handleFollowEvents(event) { - // Ignore follows from widgets that aren't the follow button. - if (event.region != 'follow') return; - - const screenName = event.data.screen_name || - event.target.getAttribute('data-screen-name'); - - /** @type {FieldsObj} */ - const defaultFields = { - transport: 'beacon', - socialNetwork: 'Twitter', - socialAction: 'follow', - socialTarget: screenName, - }; - this.tracker.send('social', - createFieldsObj(defaultFields, this.opts.fieldsObj, - this.tracker, this.opts.hitFilter, event.target, event)); + this.queue.pushTask(({time}) => { + // Ignore follows from widgets that aren't the follow button. + if (event.region != 'follow') return; + + const screenName = event.data.screen_name || + event.target.getAttribute('data-screen-name'); + + /** @type {FieldsObj} */ + const defaultFields = { + transport: 'beacon', + socialNetwork: 'Twitter', + socialAction: 'follow', + socialTarget: screenName, + queueTime: now() - time, + }; + this.tracker.send('social', + createFieldsObj(defaultFields, this.opts.fieldsObj, + this.tracker, this.opts.hitFilter, event.target, event)); + }); } /** @@ -186,15 +197,18 @@ class SocialWidgetTracker { * @param {string} url The URL corresponding to the like event. */ handleLikeEvents(url) { - /** @type {FieldsObj} */ - const defaultFields = { - transport: 'beacon', - socialNetwork: 'Facebook', - socialAction: 'like', - socialTarget: url, - }; - this.tracker.send('social', createFieldsObj(defaultFields, - this.opts.fieldsObj, this.tracker, this.opts.hitFilter)); + this.queue.pushTask(({time}) => { + /** @type {FieldsObj} */ + const defaultFields = { + transport: 'beacon', + socialNetwork: 'Facebook', + socialAction: 'like', + socialTarget: url, + queueTime: now() - time, + }; + this.tracker.send('social', createFieldsObj(defaultFields, + this.opts.fieldsObj, this.tracker, this.opts.hitFilter)); + }); } /** @@ -202,24 +216,28 @@ class SocialWidgetTracker { * @param {string} url The URL corresponding to the unlike event. */ handleUnlikeEvents(url) { - /** @type {FieldsObj} */ - const defaultFields = { - transport: 'beacon', - socialNetwork: 'Facebook', - socialAction: 'unlike', - socialTarget: url, - }; - this.tracker.send('social', createFieldsObj(defaultFields, - this.opts.fieldsObj, this.tracker, this.opts.hitFilter)); + this.queue.pushTask(({time}) => { + /** @type {FieldsObj} */ + const defaultFields = { + transport: 'beacon', + socialNetwork: 'Facebook', + socialAction: 'unlike', + socialTarget: url, + queueTime: now() - time, + }; + this.tracker.send('social', createFieldsObj(defaultFields, + this.opts.fieldsObj, this.tracker, this.opts.hitFilter)); + }); } /** * Removes all event listeners and instance properties. */ remove() { - window.removeEventListener('load', this.addWidgetListeners); + this.queue.destroy(); this.removeFacebookEventHandlers(); this.removeTwitterEventHandlers(); + window.removeEventListener('load', this.addWidgetListeners); } } diff --git a/lib/plugins/url-change-tracker.js b/lib/plugins/url-change-tracker.js index ae925855..8bbaccb6 100644 --- a/lib/plugins/url-change-tracker.js +++ b/lib/plugins/url-change-tracker.js @@ -17,8 +17,9 @@ import MethodChain from '../method-chain'; import provide from '../provide'; +import TrackerQueue from '../tracker-queue'; import {plugins, trackUsage} from '../usage'; -import {assign, createFieldsObj} from '../utilities'; +import {assign, createFieldsObj, now} from '../utilities'; /** @@ -54,6 +55,8 @@ class UrlChangeTracker { // from the location field. this.path = getPath(); + this.queue = TrackerQueue.getOrCreate(tracker.get('trackingId')); + // Binds methods. this.pushStateOverride = this.pushStateOverride.bind(this); this.replaceStateOverride = this.replaceStateOverride.bind(this); @@ -107,7 +110,7 @@ class UrlChangeTracker { * modified via `replaceState()`. */ handleUrlChange(historyDidUpdate) { - // Calls the update logic asychronously to help ensure that app logic + // Call the update logic asychronously to help ensure that app logic // responding to the URL change happens prior to this. setTimeout(() => { const oldPath = this.path; @@ -116,21 +119,44 @@ class UrlChangeTracker { if (oldPath != newPath && this.opts.shouldTrackUrlChange.call(this, newPath, oldPath)) { this.path = newPath; - this.tracker.set({ + + /** @type {FieldsObj} */ + const newFields = { page: newPath, title: document.title, - }); + }; + + this.tracker.set(newFields); if (historyDidUpdate || this.opts.trackReplaceState) { - /** @type {FieldsObj} */ - const defaultFields = {transport: 'beacon'}; - this.tracker.send('pageview', createFieldsObj(defaultFields, - this.opts.fieldsObj, this.tracker, this.opts.hitFilter)); + // Pass the new fields here in addition to setting them above + // on the off-chance that another URL change happens before this + // one gets sent. + this.sendPageview(newFields); } } }, 0); } + /** + * Sends a pageview hit when idle. + * @param {!FieldsObj} fieldsObj + */ + sendPageview(fieldsObj) { + this.queue.pushTask(({time}) => { + /** @type {FieldsObj} */ + const defaultFields = { + transport: 'beacon', + page: fieldsObj.page, + title: fieldsObj.title, + queueTime: now() - time, + }; + + this.tracker.send('pageview', createFieldsObj(defaultFields, + this.opts.fieldsObj, this.tracker, this.opts.hitFilter)); + }); + } + /** * Determines whether or not the tracker should send a hit with the new page * data. This default implementation can be overrided in the config options. @@ -146,6 +172,7 @@ class UrlChangeTracker { * Removes all event listeners and restores overridden methods. */ remove() { + this.queue.destroy(); MethodChain.remove(history, 'pushState', this.pushStateOverride); MethodChain.remove(history, 'replaceState', this.replaceStateOverride); window.removeEventListener('popstate', this.handlePopState); diff --git a/lib/session.js b/lib/session.js index 44ade8a4..93e9fe29 100644 --- a/lib/session.js +++ b/lib/session.js @@ -15,6 +15,7 @@ */ +import {IdleValue} from 'idlize/IdleValue.mjs'; import MethodChain from './method-chain'; import Store from './store'; import {now, uuid} from './utilities'; @@ -47,13 +48,18 @@ export default class Session { * @return {Session} The Session instance. */ static getOrCreate(tracker, timeout, timeZone) { - // Don't create multiple instances for the same property. + // Don't create multiple instances for the same tracker. const trackingId = tracker.get('trackingId'); - if (instances[trackingId]) { - return instances[trackingId]; - } else { - return instances[trackingId] = new Session(tracker, timeout, timeZone); + + if (!(trackingId in instances)) { + instances[trackingId] = { + references: 0, + value: new Session(tracker, timeout, timeZone), + }; } + + ++instances[trackingId].references; + return instances[trackingId].value; } /** @@ -75,40 +81,57 @@ export default class Session { // Binds methods. this.sendHitTaskOverride = this.sendHitTaskOverride.bind(this); + // Initialize the store idly since it can be expensive. + this.idleStore_ = new IdleValue(() => { + /** @type {SessionStoreData} */ + const defaultProps = { + hitTime: 0, + isExpired: false, + }; + const store = Store.getOrCreate(tracker.get('trackingId'), 'session', { + defaults: defaultProps, + timestampKey: 'hitTime', + }); + // Ensure the session has an ID. + if (!store.data.id) { + store.update(/** @type {SessionStoreData} */ ({id: uuid()})); + } + return store; + }); + + // Initialize the DateTimeFormat object idly since it can be expensive. + this.idleDateTimeFormatter_ = new IdleValue(() => { + if (this.timeZone) { + try { + return new Intl.DateTimeFormat('en-US', {timeZone: this.timeZone}); + } catch (err) { + // Do nothing. + } + } + // Return null (not undefined) so the init function isn't re-run. + return null; + }); + // Overrides into the trackers sendHitTask method. MethodChain.add(tracker, 'sendHitTask', this.sendHitTaskOverride); + } - // Some browser doesn't support various features of the - // `Intl.DateTimeFormat` API, so we have to try/catch it. Consequently, - // this allows us to assume the presence of `this.dateTimeFormatter` means - // it works in the current browser. - try { - this.dateTimeFormatter = - new Intl.DateTimeFormat('en-US', {timeZone: this.timeZone}); - } catch(err) { - // Do nothing. - } - - /** @type {SessionStoreData} */ - const defaultProps = { - hitTime: 0, - isExpired: false, - }; - this.store = Store.getOrCreate( - tracker.get('trackingId'), 'session', defaultProps); + /** @return {!Store} */ + get store_() { + return this.idleStore_.getValue(); + } - // Ensure the session has an ID. - if (!this.store.get().id) { - this.store.set(/** @type {SessionStoreData} */ ({id: uuid()})); - } + /** @return {!Intl.DateTimeFormat} */ + get dateTimeFormatter_() { + return this.idleDateTimeFormatter_.getValue(); } /** * Returns the ID of the current session. * @return {string} */ - getId() { - return this.store.get().id; + get id() { + return this.store_.data.id; } /** @@ -127,14 +150,14 @@ export default class Session { * @param {string} id The ID of a session to check for expiry. * @return {boolean} True if the session has not exp */ - isExpired(id = this.getId()) { + isExpired(id = this.id) { // If a session ID is passed and it doesn't match the current ID, // assume it's from an expired session. If no ID is passed, assume the ID // of the current session. - if (id != this.getId()) return true; + if (id != this.id) return true; /** @type {SessionStoreData} */ - const sessionData = this.store.get(); + const sessionData = this.store_.data; // `isExpired` will be `true` if the sessionControl field was set to // 'end' on the previous hit. @@ -167,11 +190,11 @@ export default class Session { * @return {boolean} */ datesAreDifferentInTimezone(d1, d2) { - if (!this.dateTimeFormatter) { - return false; + if (this.dateTimeFormatter_) { + return this.dateTimeFormatter_.format(d1) != + this.dateTimeFormatter_.format(d2); } else { - return this.dateTimeFormatter.format(d1) - != this.dateTimeFormatter.format(d2); + return false; } } @@ -192,7 +215,7 @@ export default class Session { const sessionWillEnd = sessionControl == 'end'; /** @type {SessionStoreData} */ - const sessionData = this.store.get(); + const sessionData = this.store_.data; sessionData.hitTime = now(); if (sessionWillStart) { sessionData.isExpired = false; @@ -201,7 +224,7 @@ export default class Session { if (sessionWillEnd) { sessionData.isExpired = true; } - this.store.set(sessionData); + this.store_.update(sessionData); }; } @@ -211,9 +234,15 @@ export default class Session { * store. */ destroy() { - MethodChain.remove(this.tracker, 'sendHitTask', this.sendHitTaskOverride); - this.store.destroy(); - delete instances[this.tracker.get('trackingId')]; + const trackingId = this.tracker.get('trackingId'); + + --instances[trackingId].references; + + if (instances[trackingId].references === 0) { + MethodChain.remove(this.tracker, 'sendHitTask', this.sendHitTaskOverride); + this.store_.destroy(); + delete instances[trackingId]; + } } } diff --git a/lib/store.js b/lib/store.js index 3dec04b2..7d865b1e 100644 --- a/lib/store.js +++ b/lib/store.js @@ -15,6 +15,7 @@ */ +import {IdleValue} from 'idlize/IdleValue.mjs'; import EventEmitter from './event-emitter'; import {assign} from './utilities'; @@ -37,18 +38,27 @@ export default class Store extends EventEmitter { * instance if one doesn't exist. * @param {string} trackingId The tracking ID for the GA property. * @param {string} namespace A namespace unique to this store. - * @param {Object=} defaults An optional object of key/value defaults. + * @param {StoreOpts=} opts * @return {Store} The Store instance. */ - static getOrCreate(trackingId, namespace, defaults) { + static getOrCreate(trackingId, namespace, opts = {}) { const key = [AUTOTRACK_PREFIX, trackingId, namespace].join(':'); // Don't create multiple instances for the same tracking Id and namespace. - if (!instances[key]) { - instances[key] = new Store(key, defaults); - if (!isListening) initStorageListener(); + if (!(key in instances)) { + instances[key] = { + references: 0, + value: new Store(key, opts), + }; } - return instances[key]; + + // Only add a single storage listener. + if (!isListening) { + initStorageListener(); + } + + ++instances[key].references; + return instances[key].value; } /** @@ -104,15 +114,15 @@ export default class Store extends EventEmitter { /** * @param {string} key A key unique to this store. - * @param {Object=} defaults An optional object of key/value defaults. + * @param {StoreOpts=} opts */ - constructor(key, defaults = {}) { + constructor(key, opts = {}) { super(); this.key_ = key; - this.defaults_ = defaults; + this.defaults_ = opts.defaults || {}; + this.timestampKey_ = opts.timestampKey; - /** @type {?Object} */ - this.cache_ = null; // Will be set after the first get. + this.cache_ = new IdleValue(() => this.read_()); } /** @@ -123,33 +133,39 @@ export default class Store extends EventEmitter { * schema version is introduced. * @return {!Object} The stored data merged with the defaults. */ - get() { - if (this.cache_) { - return this.cache_; - } else { - if (Store.isSupported_()) { - try { - this.cache_ = parse(Store.get_(this.key_)); - } catch(err) { - // Do nothing. - } - } - return this.cache_ = assign({}, this.defaults_, this.cache_); - } + get data() { + return assign({}, this.defaults_, this.cache_.getValue()); } /** * Saves the passed data object to localStorage, * merging it with the existing data. - * @param {Object} newData The data to save. + * @param {!Object} newData The data to save. */ - set(newData) { - this.cache_ = assign({}, this.defaults_, this.cache_, newData); + update(newData) { + const timestampKey = this.timestampKey_; + // When using a timestamp key, we need to ensure that the stored data + // isn't newer than the data we're about to update. + // This can happen if plugins are using an IdleQueue and tasks in + // one tab get queue before but run after tasks in another tab. + let oldData; + if (timestampKey && typeof newData[timestampKey] === 'number') { + oldData = this.read_() || {}; + if (typeof oldData[timestampKey] === 'number' && + oldData[timestampKey] > newData[timestampKey]) { + return; + } + } else { + oldData = this.data; + } + + const newCache = assign(oldData, newData); + this.cache_.setValue(newCache); if (Store.isSupported_()) { try { - Store.set_(this.key_, JSON.stringify(this.cache_)); - } catch(err) { + Store.set_(this.key_, JSON.stringify(newCache)); + } catch (err) { // Do nothing. } } @@ -159,11 +175,12 @@ export default class Store extends EventEmitter { * Clears the data in localStorage for the current store. */ clear() { - this.cache_ = {}; + this.cache_.setValue({}); + if (Store.isSupported_()) { try { Store.clear_(this.key_); - } catch(err) { + } catch (err) { // Do nothing. } } @@ -175,11 +192,32 @@ export default class Store extends EventEmitter { * Note: this does not erase the stored data. Use `clear()` for that. */ destroy() { - delete instances[this.key_]; - if (!Object.keys(instances).length) { + --instances[this.key_].references; + + if (instances[this.key_].references === 0) { + this.clear(); + delete instances[this.key_]; + } + + if (Object.keys(instances).length === 0) { removeStorageListener(); } } + + /** + * Reads the data stored in localStorage for this store. This method ignores + * the cache. + * @return {Object|undefined} + */ + read_() { + if (Store.isSupported_()) { + try { + return parse(Store.get_(this.key_)); + } catch (err) { + // Do nothing. + } + } + } } @@ -208,12 +246,13 @@ function removeStorageListener() { * @param {!Event} event The DOM event. */ function storageListener(event) { - const store = instances[event.key]; - if (store) { + // Only care about storage events for keys matching stores in instances. + if (event.key in instances) { + const store = instances[event.key].value; const oldData = assign({}, store.defaults_, parse(event.oldValue)); const newData = assign({}, store.defaults_, parse(event.newValue)); - store.cache_ = newData; + store.cache_.setValue(newData); store.emit('externalSet', newData, oldData); } } @@ -229,7 +268,7 @@ function parse(source) { if (source) { try { data = /** @type {!Object} */ (JSON.parse(source)); - } catch(err) { + } catch (err) { // Do nothing. } } diff --git a/lib/tracker-queue.js b/lib/tracker-queue.js new file mode 100644 index 00000000..5359f45a --- /dev/null +++ b/lib/tracker-queue.js @@ -0,0 +1,74 @@ +/** + * Copyright 2018 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {IdleQueue} from 'idlize/IdleQueue.mjs'; + + +const instances = {}; + +/** + * A class that enforces a unique IdleQueue per tracking ID. + */ +export default class TrackerQueue extends IdleQueue { + /** + * Gets an existing instance for the passed tracking ID or creates a new + * instance if one doesn't exist. + * @param {string} trackingId An analytics.js tracking ID. + * @return {!TrackerQueue} + */ + static getOrCreate(trackingId) { + // Don't create multiple instances for the same tracking ID. + if (!(trackingId in instances)) { + instances[trackingId] = { + references: 0, + value: new TrackerQueue(trackingId), + }; + } + + ++instances[trackingId].references; + return instances[trackingId].value; + } + + /** + * @param {string} trackingId + */ + constructor(trackingId) { + // If an idle callback is being run in between frame rendering, it'll + // have an initial `timeRemaining()` value <= 16ms. If it's run when + // no frames are being rendered, it'll have an initial + // `timeRemaining()` <= 50ms. Since all the tasks queued by autotrack + // are non-critial and non-UI-related, we do not want our tasks to + // interfere with frame rendering, and therefore by default we pick a + // `defaultMinTaskTime` value > 16ms, so tasks are always processed + // outside of frame rendering. + super({defaultMinTaskTime: 25, ensureTasksRun: true}); + + this.trackingId_ = trackingId; + } + + /** + * Removes a reference from the instances map. If no more references exist + * for this instance, destroy it. + */ + destroy() { + --instances[this.trackingId_].references; + + if (instances[this.trackingId_].references === 0) { + super.destroy(); + delete instances[this.trackingId_]; + } + } +} diff --git a/lib/utilities.js b/lib/utilities.js index d1d39022..90dad411 100644 --- a/lib/utilities.js +++ b/lib/utilities.js @@ -16,7 +16,6 @@ import {getAttributes} from 'dom-utils'; -import MethodChain from './method-chain'; /** @@ -142,47 +141,6 @@ export function withTimeout(callback, wait = 2000) { return fn; } -// Maps trackers to queue by tracking ID. -const queueMap = {}; - -/** - * Queues a function for execution in the next call stack, or immediately - * before any send commands are executed on the tracker. This allows - * autotrack plugins to defer running commands until after all other plugins - * are required but before any other hits are sent. - * @param {!Tracker} tracker - * @param {!Function} fn - */ -export function deferUntilPluginsLoaded(tracker, fn) { - const trackingId = tracker.get('trackingId'); - const ref = queueMap[trackingId] = queueMap[trackingId] || {}; - - const processQueue = () => { - clearTimeout(ref.timeout); - if (ref.send) { - MethodChain.remove(tracker, 'send', ref.send); - } - delete queueMap[trackingId]; - - ref.queue.forEach((fn) => fn()); - }; - - clearTimeout(ref.timeout); - ref.timeout = setTimeout(processQueue, 0); - ref.queue = ref.queue || []; - ref.queue.push(fn); - - if (!ref.send) { - ref.send = (originalMethod) => { - return (...args) => { - processQueue(); - originalMethod(...args); - }; - }; - MethodChain.add(tracker, 'send', ref.send); - } -} - /** * A small shim of Object.assign that aims for brevity over spec-compliant @@ -212,7 +170,7 @@ export const assign = Object.assign || function(target, ...sources) { * @return {string} The camelCased version of the string. */ export function camelCase(str) { - return str.replace(/[\-\_]+(\w?)/g, function(match, p1) { + return str.replace(/[-_]+(\w?)/g, function(match, p1) { return p1.toUpperCase(); }); } @@ -261,4 +219,4 @@ export function now() { // https://gist.github.com/jed/982883 /** @param {?=} a */ export const uuid = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}; -/*eslint-enable */ +/* eslint-enable */ diff --git a/package.json b/package.json index 061976b1..33ad3f59 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,16 @@ { "name": "autotrack", - "version": "2.3.2", + "version": "2.4.1", "description": "Automatic and enhanced Google Analytics tracking for common user interactions on the web", "main": "lib", "bin": "./bin/autotrack", "scripts": { - "build": "gulp build", - "start": "gulp watch", - "test": "gulp test" + "build": "node -r esm ./node_modules/.bin/gulp js:lib", + "lint": "node -r esm ./node_modules/.bin/gulp lint", + "start": "node -r esm ./node_modules/.bin/gulp watch", + "test": "node -r esm ./node_modules/.bin/gulp test", + "selenium": "node -r esm ./node_modules/.bin/gulp selenium", + "wdio": "node -r esm ./node_modules/.bin/wdio ./test/e2e/wdio.conf.js" }, "repository": { "type": "git", @@ -30,45 +33,45 @@ }, "homepage": "https://github.com/googleanalytics/autotrack#readme", "dependencies": { - "chalk": "^1.1.3", + "chalk": "^2.4.1", "dom-utils": "^0.9.0", - "fs-extra": "^3.0.1", - "glob": "^7.1.1", - "google-closure-compiler-js": "^20170423.0.0", - "gzip-size": "^3.0.0", - "rollup": "^0.41.4", - "rollup-plugin-memory": "^2.0.0", - "rollup-plugin-node-resolve": "^3.0.0", - "source-map": "^0.5.6" + "fs-extra": "^7.0.0", + "glob": "^7.1.2", + "google-closure-compiler-js": "^20180610.0.0", + "gzip-size": "^5.0.0", + "idlize": "^0.1.0", + "rollup": "^0.64.1", + "rollup-plugin-virtual": "^1.0.1", + "rollup-plugin-node-resolve": "^3.3.0", + "source-map": "^0.7.3" }, "devDependencies": { - "babel-core": "^6.22.1", - "babel-loader": "^7.0.0", + "babel-core": "^6.26.3", + "babel-loader": "^7.1.5", "babel-plugin-external-helpers": "^6.22.0", - "babel-preset-es2015": "^6.22.0", - "babel-register": "^6.22.0", - "easy-sauce": "^0.4.1", - "eslint": "^3.14.0", - "eslint-config-google": "^0.7.1", - "express": "^4.14.0", - "gulp": "^3.9.1", - "gulp-eslint": "^3.0.1", + "babel-preset-env": "^1.7.0", + "babel-register": "^6.26.0", + "easy-sauce": "^0.4.2", + "eslint": "^5.4.0", + "eslint-config-google": "^0.9.1", + "esm": "^3.0.77", + "express": "^4.16.3", + "gulp": "^4.0.0", + "gulp-eslint": "^5.0.0", "gulp-util": "^3.0.8", "gulp-webdriver": "^2.0.3", - "intersection-observer": "^0.2.1", - "mocha": "^3.2.0", - "ngrok": "^2.2.5", - "rollup-plugin-babel": "^2.7.1", - "run-sequence": "^1.2.2", - "sauce-connect-launcher": "^1.2.0", - "selenium-server-standalone-jar": "^3.0.1", - "serve-static": "^1.11.1", - "sinon": "sinonjs/sinon#v2.0.0-pre.2", - "source-map-support": "^0.4.10", - "uuid": "^3.0.1", - "wdio-mocha-framework": "^0.5.8", - "wdio-sauce-service": "^0.4.0", - "webdriverio": "^4.6.1", - "webpack": "2.6.0" + "intersection-observer": "^0.5.0", + "ngrok": "^3.0.1", + "rollup-plugin-babel": "^3.0.7", + "run-sequence": "^2.2.1", + "sauce-connect-launcher": "^1.2.4", + "selenium-server-standalone-jar": "^3.13.0", + "serve-static": "^1.13.2", + "source-map-support": "^0.5.8", + "uuid": "^3.3.2", + "wdio-mocha-framework": "^0.6.2", + "wdio-sauce-service": "^0.4.10", + "webdriverio": "^4.13.2", + "webpack": "^4.16.5" } } diff --git a/test/analytics.js b/test/analytics.js deleted file mode 100644 index 730b66c4..00000000 --- a/test/analytics.js +++ /dev/null @@ -1,45 +0,0 @@ -(function(){var $c=function(a){this.w=a||[]};$c.prototype.set=function(a){this.w[a]=!0};$c.prototype.encode=function(){for(var a=[],b=0;b\x3c/script>')):(c=M.createElement("script"),c.type="text/javascript",c.async=!0,c.src=a,b&&(c.id=b),a=M.getElementsByTagName("script")[0],a.parentNode.insertBefore(c,a)))},Ud=function(){return"https:"==M.location.protocol},E=function(a,b){var c= -a.match("(?:&|#|\\?)"+K(b).replace(/([.*+?^=!:${}()|\[\]\/\\])/g,"\\$1")+"=([^&#]*)");return c&&2==c.length?c[1]:""},xa=function(){var a=""+M.location.hostname;return 0==a.indexOf("www.")?a.substring(4):a},ya=function(a){var b=M.referrer;if(/^https?:\/\//i.test(b)){if(a)return b;a="//"+M.location.hostname;var c=b.indexOf(a);if(5==c||6==c)if(a=b.charAt(c+a.length),"/"==a||"?"==a||""==a||":"==a)return;return b}},za=function(a,b){if(1==b.length&&null!=b[0]&&"object"===typeof b[0])return b[0];for(var c= -{},d=Math.min(a.length+1,b.length),e=0;e=b.length)wc(a,b,c);else if(8192>=b.length)x(a,b,c)||wd(a,b,c)||wc(a,b,c);else throw ge("len",b.length),new Da(b.length);},wc=function(a,b,c){var d=ta(a+"?"+b);d.onload=d.onerror=function(){d.onload=null;d.onerror=null;c()}},wd=function(a,b,c){var d=O.XMLHttpRequest;if(!d)return!1;var e=new d;if(!("withCredentials"in e))return!1; -e.open("POST",a,!0);e.withCredentials=!0;e.setRequestHeader("Content-Type","text/plain");e.onreadystatechange=function(){4==e.readyState&&(c(),e=null)};e.send(b);return!0},x=function(a,b,c){return O.navigator.sendBeacon?O.navigator.sendBeacon(a,b)?(c(),!0):!1:!1},ge=function(a,b,c){1<=100*Math.random()||G("?")||(a=["t=error","_e="+a,"_v=j47","sr=1"],b&&a.push("_f="+b),c&&a.push("_m="+K(c.substring(0,100))),a.push("aip=1"),a.push("z="+hd()),wc(oc()+"/collect",a.join("&"),ua))};var h=function(a){var b=O.gaData=O.gaData||{};return b[a]=b[a]||{}};var Ha=function(){this.M=[]};Ha.prototype.add=function(a){this.M.push(a)};Ha.prototype.D=function(a){try{for(var b=0;b=100*R(a,Ka))throw"abort";}function Ma(a){if(G(P(a,Na)))throw"abort";}function Oa(){var a=M.location.protocol;if("http:"!=a&&"https:"!=a)throw"abort";} -function Pa(a){try{O.navigator.sendBeacon?J(42):O.XMLHttpRequest&&"withCredentials"in new O.XMLHttpRequest&&J(40)}catch(c){}a.set(ld,Td(a),!0);a.set(Ac,R(a,Ac)+1);var b=[];Qa.map(function(c,d){if(d.F){var e=a.get(c);void 0!=e&&e!=d.defaultValue&&("boolean"==typeof e&&(e*=1),b.push(d.F+"="+K(""+e)))}});b.push("z="+Bd());a.set(Ra,b.join("&"),!0)} -function Sa(a){var b=P(a,gd)||oc()+"/collect",c=P(a,fa);!c&&a.get(Vd)&&(c="beacon");if(c){var d=P(a,Ra),e=a.get(Ia),e=e||ua;"image"==c?wc(b,d,e):"xhr"==c&&wd(b,d,e)||"beacon"==c&&x(b,d,e)||ba(b,d,e)}else ba(b,P(a,Ra),a.get(Ia));b=a.get(Na);b=h(b);c=b.hitcount;b.hitcount=c?c+1:1;b=a.get(Na);delete h(b).pending_experiments;a.set(Ia,ua,!0)} -function Hc(a){(O.gaData=O.gaData||{}).expId&&a.set(Nc,(O.gaData=O.gaData||{}).expId);(O.gaData=O.gaData||{}).expVar&&a.set(Oc,(O.gaData=O.gaData||{}).expVar);var b;var c=a.get(Na);if(c=h(c).pending_experiments){var d=[];for(b in c)c.hasOwnProperty(b)&&c[b]&&d.push(encodeURIComponent(b)+"."+encodeURIComponent(c[b]));b=d.join("!")}else b=void 0;b&&a.set(m,b,!0)}function cd(){if(O.navigator&&"preview"==O.navigator.loadPurpose)throw"abort";} -function yd(a){var b=O.gaDevIds;ka(b)&&0!=b.length&&a.set("&did",b.join(","),!0)}function vb(a){if(!a.get(Na))throw"abort";};var hd=function(){return Math.round(2147483647*Math.random())},Bd=function(){try{var a=new Uint32Array(1);O.crypto.getRandomValues(a);return a[0]&2147483647}catch(b){return hd()}};function Ta(a){var b=R(a,Ua);500<=b&&J(15);var c=P(a,Va);if("transaction"!=c&&"item"!=c){var c=R(a,Wa),d=(new Date).getTime(),e=R(a,Xa);0==e&&a.set(Xa,d);e=Math.round(2*(d-e)/1E3);0=c)throw"abort";a.set(Wa,--c)}a.set(Ua,++b)};var Ya=function(){this.data=new ee},Qa=new ee,Za=[];Ya.prototype.get=function(a){var b=$a(a),c=this.data.get(a);b&&void 0==c&&(c=ea(b.defaultValue)?b.defaultValue():b.defaultValue);return b&&b.Z?b.Z(this,a,c):c};var P=function(a,b){var c=a.get(b);return void 0==c?"":""+c},R=function(a,b){var c=a.get(b);return void 0==c||""===c?0:1*c};Ya.prototype.set=function(a,b,c){if(a)if("object"==typeof a)for(var d in a)a.hasOwnProperty(d)&&ab(this,d,a[d],c);else ab(this,a,b,c)}; -var ab=function(a,b,c,d){if(void 0!=c)switch(b){case Na:wb.test(c)}var e=$a(b);e&&e.o?e.o(a,b,c,d):a.data.set(b,c,d)},bb=function(a,b,c,d,e){this.name=a;this.F=b;this.Z=d;this.o=e;this.defaultValue=c},$a=function(a){var b=Qa.get(a);if(!b)for(var c=0;c=b?!1:!0},gc=function(a){var b={};if(Ec(b)||Fc(b)){var c=b[Eb];void 0==c||Infinity==c||isNaN(c)||(0c)a[b]=void 0},Fd=function(a){return function(b){if("pageview"==b.get(Va)&&!a.I){a.I=!0;var c= -aa(b);b=0=a&&d.push({hash:ca[0],R:e[g],O:ca})}if(0!=d.length)return 1==d.length?d[0]:Zc(b,d)||Zc(c,d)||Zc(null,d)||d[0]}function Zc(a,b){var c,d;null==a?c=d=1:(c=La(a),d=La(D(a,".")?a.substring(1):"."+a));for(var e=0;ed.length)){c=[];for(var e=0;e=ca[0]||0>=ca[1]?"":ca.join("x");a.set(rb,c);a.set(tb,fc());a.set(ob,M.characterSet||M.charset);a.set(sb,b&&"function"=== -typeof b.javaEnabled&&b.javaEnabled()||!1);a.set(nb,(b&&(b.language||b.browserLanguage)||"").toLowerCase());if(d&&a.get(cc)&&(b=M.location.hash)){b=b.split(/[?&#]+/);d=[];for(c=0;carguments.length)){var b,c;"string"===typeof arguments[0]?(b=arguments[0],c=[].slice.call(arguments,1)):(b=arguments[0]&&arguments[0][Va],c=arguments);b&&(c=za(qc[b]||[],c),c[Va]=b,this.b.set(c,void 0,!0),this.filters.D(this.b),this.b.data.m={})}}; -pc.prototype.ma=function(a,b){var c=this;u(a,c,b)||(v(a,function(){u(a,c,b)}),y(String(c.get(V)),a,void 0,b,!0))};var rc=function(a){if("prerender"==M.visibilityState)return!1;a();return!0},z=function(a){if(!rc(a)){J(16);var b=!1,c=function(){if(!b&&rc(a)){b=!0;var d=c,e=M;e.removeEventListener?e.removeEventListener("visibilitychange",d,!1):e.detachEvent&&e.detachEvent("onvisibilitychange",d)}};L(M,"visibilitychange",c)}};var td=/^(?:(\w+)\.)?(?:(\w+):)?(\w+)$/,sc=function(a){if(ea(a[0]))this.u=a[0];else{var b=td.exec(a[0]);null!=b&&4==b.length&&(this.c=b[1]||"t0",this.K=b[2]||"",this.C=b[3],this.a=[].slice.call(a,1),this.K||(this.A="create"==this.C,this.i="require"==this.C,this.g="provide"==this.C,this.ba="remove"==this.C),this.i&&(3<=this.a.length?(this.X=this.a[1],this.W=this.a[2]):this.a[1]&&(qa(this.a[1])?this.X=this.a[1]:this.W=this.a[1])));b=a[1];a=a[2];if(!this.C)throw"abort";if(this.i&&(!qa(b)||""==b))throw"abort"; -if(this.g&&(!qa(b)||""==b||!ea(a)))throw"abort";if(ud(this.c)||ud(this.K))throw"abort";if(this.g&&"t0"!=this.c)throw"abort";}};function ud(a){return 0<=a.indexOf(".")||0<=a.indexOf(":")};var Yd,Zd,$d,A;Yd=new ee;$d=new ee;A=new ee;Zd={ec:45,ecommerce:46,linkid:47}; -var u=function(a,b,c){b==N||b.get(V);var d=Yd.get(a);if(!ea(d))return!1;b.plugins_=b.plugins_||new ee;if(b.plugins_.get(a))return!0;b.plugins_.set(a,new d(b,c||{}));return!0},y=function(a,b,c,d,e){if(!ea(Yd.get(b))&&!$d.get(b)){Zd.hasOwnProperty(b)&&J(Zd[b]);if(p.test(b)){J(52);a=N.j(a);if(!a)return!0;c=d||{};d={id:b,B:c.dataLayer||"dataLayer",ia:!!a.get("anonymizeIp"),na:e,G:!1};a.get(">m")==b&&(d.G=!0);var g=String(a.get("name"));"t0"!=g&&(d.target=g);G(String(a.get("trackingId")))||(d.ja=String(a.get(Q)), -d.ka=Number(a.get(n)),a=c.palindrome?r:q,a=(a=M.cookie.replace(/^|(; +)/g,";").match(a))?a.sort().join("").substring(1):void 0,d.la=a);a=d.B;c=(new Date).getTime();O[a]=O[a]||[];c={"gtm.start":c};e||(c.event="gtm.js");O[a].push(c);c=t(d)}!c&&Zd.hasOwnProperty(b)?(J(39),c=b+".js"):J(43);c&&(c&&0<=c.indexOf("/")||(c=(Ba||Ud()?"https:":"http:")+"//www.google-analytics.com/plugins/ua/"+c),d=ae(c),a=d.protocol,c=M.location.protocol,("https:"==a||a==c||("http:"!=a?0:"http:"==c))&&B(d)&&(wa(d.url,void 0, -e),$d.set(b,!0)))}},v=function(a,b){var c=A.get(a)||[];c.push(b);A.set(a,c)},C=function(a,b){Yd.set(a,b);for(var c=A.get(a)||[],d=0;da.split("/")[0].indexOf(":")&&(a=ca+e[2].substring(0,e[2].lastIndexOf("/"))+"/"+ -a);c.href=a;d=b(c);return{protocol:(c.protocol||"").toLowerCase(),host:d[0],port:d[1],path:d[2],query:c.search||"",url:a||""}};var Z={ga:function(){Z.f=[]}};Z.ga();Z.D=function(a){var b=Z.J.apply(Z,arguments),b=Z.f.concat(b);for(Z.f=[];0c;c++){var d=b[c].src;if(d&&0==d.indexOf("https://www.google-analytics.com/analytics")){J(33); -b=!0;break a}}b=!1}b&&(Ba=!0)}Ud()||Ba||!Ed(new Od(1E4))||(J(36),Ba=!0);(O.gaplugins=O.gaplugins||{}).Linker=Dc;b=Dc.prototype;C("linker",Dc);X("decorate",b,b.ca,20);X("autoLink",b,b.S,25);C("displayfeatures",fd);C("adfeatures",fd);a=a&&a.q;ka(a)?Z.D.apply(N,a):J(50)}};N.da=function(){for(var a=N.getAll(),b=0;b>21:b;return b};})(window); \ No newline at end of file diff --git a/test/analytics_debug.js b/test/analytics_debug.js deleted file mode 100644 index 9b1a14c9..00000000 --- a/test/analytics_debug.js +++ /dev/null @@ -1,75 +0,0 @@ -(function(){var ec=function(a){this.B=a||[]};ec.prototype.set=function(a){this.B[a]=!0};ec.prototype.encode=function(){for(var a=[],b=0;b\x3c/script>'):J("URL uses invalid characters. Dropping request for: %s", -a)):(c=I.createElement("script"),c.type="text/javascript",c.async=!0,c.src=a,b&&(c.id=b),a=I.getElementsByTagName("script")[0],a.parentNode.insertBefore(c,a)))},df=function(){return"https:"==I.location.protocol},aa=function(a,b){var c=a.match("(?:&|#|\\?)"+P(b).replace(/([.*+?^=!:${}()|\[\]\/\\])/g,"\\$1")+"=([^&#]*)");return c&&2==c.length?c[1]:""},Wb=function(){var a=""+I.location.hostname;return 0==a.indexOf("www.")?a.substring(4):a},Xb=function(a){var b=I.referrer;if(/^https?:\/\//i.test(b)){if(a)return b; -a="//"+I.location.hostname;var c=b.indexOf(a);if(5==c||6==c)if(a=b.charAt(c+a.length),"/"==a||"?"==a||""==a||":"==a)return;return b}},Yb=function(a,b){if(1==b.length&&null!=b[0]&&"object"===typeof b[0])return b[0];for(var c={},d=Math.min(a.length+1,b.length),e=0;e"),b.push([f,"(&"+e+")",Ba(d)]))}}b.sort();Xd(b)} -function Xd(a){for(var b=[],c=0;cb[d]?a[c][d].length:b[d]);for(c=0;c=b.length)id(a,b,c),Ia(b);else if(8192>=b.length)u(a,b,c)||te(a,b,c)||id(a,b,c),Ia(b);else throw O("Payload size is too large (%s). Max allowed is %s.",b.length,8192),fc("len",b.length),new bc(b.length);},id=function(a,b,c){var d=za(a+"?"+b);d.onload=d.onerror=function(){d.onload=null;d.onerror=null;c()}},te=function(a,b,c){var d= -Q.XMLHttpRequest;if(!d)return!1;var e=new d;if(!("withCredentials"in e))return!1;e.open("POST",a,!0);e.withCredentials=!0;e.setRequestHeader("Content-Type","text/plain");e.onreadystatechange=function(){4==e.readyState&&(c(),e=null)};e.send(b);return!0},u=function(a,b,c){return Q.navigator.sendBeacon?Q.navigator.sendBeacon(a,b)?(c(),!0):!1:!1},fc=function(a,b,c){O("Error: type=%s method=%s message=%s account=%s",arguments);if(!(1<=100*Math.random()||K("?"))){var d=["t=error","_e="+a,"_v=j47d","sr=1"]; -b&&d.push("_f="+b);c&&d.push("_m="+P(c.substring(0,100)));d.push("aip=1");d.push("z="+ae());id(hd()+"/collect",d.join("&"),Aa)}};var h=function(a){var b=Q.gaData=Q.gaData||{};return b[a]=b[a]||{}};var gc=function(){this.m=[]};gc.prototype.add=function(a){this.m.push(a)};gc.prototype.H=function(a){L("\nExecuting "+this.m.length+" filters:");try{for(var b=0;b=100*jc(a,Db))throw N("User has been sampled out. Aborting hit."),"abort";}function kc(a){if(K(V(a,U)))throw N("User has opted out of tracking. Aborting hit."),"abort";}function lc(){var a=I.location.protocol;if("http:"!=a&&"https:"!=a)throw N("Unallowed document protocol. Aborting hit."),"abort";} -function mc(a){try{Q.navigator.sendBeacon?F(42):Q.XMLHttpRequest&&"withCredentials"in new Q.XMLHttpRequest&&F(40)}catch(c){}a.set(oc,cf(a),!0);a.set(md,jc(a,md)+1);var b=[];Ka.map(function(c,d){if(d.i){var e=a.get(c);void 0!=e&&e!=d.defaultValue&&("boolean"==typeof e&&(e*=1),b.push(d.i+"="+P(""+e)))}});b.push("z="+be());a.set(Na,b.join("&"),!0)} -function pc(a){var b=V(a,ob)||hd()+"/collect",c=V(a,ha);!c&&a.get(Oe)&&(c="beacon");if(c){var d=V(a,Na),e=a.get(Nb);8192=c)throw N("Exceeded rate limit for sending hits. Aborting hit."),"abort";a.set(uc,--c)}a.set(rc,++b)};var wc=function(){this.data=new ef;this.data.debug=!0},Ka=new ef,xc=[];wc.prototype.get=function(a){var b=yc(a),c=this.data.get(a);b&&void 0==c&&(c=t(b.defaultValue)?b.defaultValue():b.defaultValue);return b&&b.v?b.v(this,a,c):c};var V=function(a,b){var c=a.get(b);return void 0==c?"":""+c},jc=function(a,b){var c=a.get(b);return void 0==c||""===c?0:1*c};wc.prototype.set=function(a,b,c){if(a)if("object"==typeof a)for(var d in a)a.hasOwnProperty(d)&&zc(this,d,a[d],c);else zc(this,a,b,c)}; -var zc=function(a,b,c,d){La(b,c);var e=yc(b);e&&e.w?e.w(a,b,c,d):a.data.set(b,c,d);e||N("Set called on unknown field: %s.",b)},Ac=function(a,b,c,d,e){this.name=a;this.i=b;this.v=d;this.w=e;this.defaultValue=c},yc=function(a){var b=Ka.get(a);if(!b)for(var c=0;c "+c),b.v=function(a){return a.get(c)},b.w=function(a,b,f,ea){a.set(c,f,ea)},b.i=void 0);return b}); -var Ob=X("_oot"),Vd=W("previewTask"),Pb=W("checkProtocolTask"),xd=W("validationTask"),Qb=W("checkStorageTask"),Gd=W("historyImportTask"),Rb=W("samplerTask"),Tb=W("_rlt"),Ub=W("buildHitTask"),Vb=W("sendHitTask"),Hd=W("ceTask"),we=W("devIdTask"),oe=W("timingTask"),Ce=W("displayFeaturesTask"),T=X("name"),R=X("clientId","cid"),n=X("clientIdTime"),xe=W("userId","uid"),U=X("trackingId","tid"),ub=X("cookieName",void 0,"_ga"),S=X("cookieDomain"),vb=X("cookiePath",void 0,"/"),Cb=X("cookieExpires",void 0,63072E3), -wb=X("legacyCookieDomain"),Id=X("legacyHistoryImport",void 0,!0),xb=X("storage",void 0,"cookie"),Kb=X("allowLinker",void 0,!1),Lb=X("allowAnchor",void 0,!0),Db=X("sampleRate","sf",100),Eb=X("siteSpeedSampleRate",void 0,1),Mb=X("alwaysSendReferrer",void 0,!1),ac=[T,U,R,n,xe,ub,S,vb,Cb,wb,Id,Kb,Lb,Db,Eb,Mb,xb],ob=W("transportUrl"),De=W("_r","_r");function Y(a,b,c,d){b[a]=function(){try{return d&&F(d),c.apply(this,arguments)}catch(e){throw fc("exc",a,e&&e.name),e;}}};var Ie=function(a){this.Z=a;this.ja=void 0;this.fa=!1;this.ra=void 0;this.ia=1},ye=function(a,b){var c;if(a.ja&&a.fa)return 0;a.fa=!0;if(b){if(a.ra&&jc(b,a.ra))return jc(b,a.ra);if(0==b.get(Eb))return 0}if(0==a.Z)return 0;void 0===c&&(c=be());return 0==c%a.Z?Math.floor(c/a.Z)%a.ia+1:0};function Qc(){var a,b,c;if((c=(c=Q.navigator)?c.plugins:null)&&c.length)for(var d=0;d=b?(L("Site speed data not sent - visitor sampled out"),!1):!0},Sc=function(a){var b={};if(qd(b)||rd(b)){var c=b[Ic];void 0==c||Infinity==c||isNaN(c)?L("Site speed data not sent - unsupported browser"):0c)a[b]=void 0},ze=function(a){return function(b){if("pageview"==b.get(Ma)&&!a.L){a.L=!0;var c=ba(b);b=0=a&&d.push({hash:ea[0],T:e[f],ea:ea})}if(0!=d.length)return 1==d.length?d[0]:Ld(b,d)||Ld(c,d)||Ld(null,d)||d[0]}function Ld(a,b){var c,d;null==a?c=d=1:(c=ic(a),d=ic(H(a,".")?a.substring(1):"."+a));for(var e=0;ed.length)){c=[];for(var e=0;e=ea[0]||0>=ea[1]?"":ea.join("x");a.set(Ya,c);a.set(Za,Qc());a.set(Ua,I.characterSet|| -I.charset);a.set(Ib,b&&"function"===typeof b.javaEnabled&&b.javaEnabled()||!1);a.set(Ta,(b&&(b.language||b.browserLanguage)||"").toLowerCase());if(d&&a.get(Lb)&&(b=I.location.hash)){b=b.split(/[?&#]+/);d=[];for(c=0;carguments.length)O("No hit type specified. Aborting hit.");else{var b,c;"string"===typeof arguments[0]?(b=arguments[0],c=[].slice.call(arguments,1)):(b=arguments[0]&&arguments[0][Ma],c=arguments);b?(c=Yb(bd[b]||[],c),c[Ma]=b,this.a.set(c,void 0,!0),this.filters.H(this.a),L("Send finished: "+(0==Z.h?-1:(new Date).getTime()-Z.h)),this.a.data.u={}):O("No hit type specified. Aborting hit.")}}; -ad.prototype.pa=function(a,b){var c=this;x(a,c,b)||(y(a,function(){x(a,c,b)}),z(String(c.get(T)),a,void 0,b,!0))};var cd=function(a){if("prerender"==I.visibilityState)return!1;a();return!0},A=function(a){if(!cd(a)){F(16);var b=!1,c=function(){if(!b&&cd(a)){b=!0;var d=c,e=I;e.removeEventListener?e.removeEventListener("visibilitychange",d,!1):e.detachEvent&&e.detachEvent("onvisibilitychange",d)}};Ca(I,"visibilitychange",c)}};var qe=/^(?:(\w+)\.)?(?:(\w+):)?(\w+)$/,se=function(a){this.G=a;if(t(a[0]))this.s=a[0];else{var b=qe.exec(a[0]);null!=b&&4==b.length&&(this.c=b[1]||"t0",this.I=b[2]||"",this.A=b[3],this.b=[].slice.call(a,1),this.I||(this.D="create"==this.A,this.g="require"==this.A,this.f="provide"==this.A,this.$="remove"==this.A),this.g&&(3<=this.b.length?(this.da=this.b[1],this.ba=this.b[2]):this.b[1]&&(G(this.b[1])?this.da=this.b[1]:this.ba=this.b[1])));var b=a[1],c=a[2];if(!this.A)throw O("Invalid command: "+a), -"abort";if(this.g&&(!G(b)||""==b))throw O("Invalid require command.",a),"abort";if(this.f&&(!G(b)||""==b||!t(c)))throw O("Invalid provide command.",a),"abort";if(re(this.c)||re(this.I))throw O('Target name and plugin names should not contain "." or ":"'),"abort";if(this.f&&"t0"!=this.c)throw O("Provide command should not be preceeded by a tracker name."),"abort";}};function re(a){return 0<=a.indexOf(".")||0<=a.indexOf(":")};var Re,Se,Te,B;Re=new ef;Te=new ef;B=new ef;Se={ec:45,ecommerce:46,linkid:47}; -var x=function(a,b,c){var d=b==Z?Fc:b.get(T),e=Re.get(a);if(!t(e))return N("Waiting on require of %s to be fulfilled.",a),!1;b.plugins_=b.plugins_||new ef;if(b.plugins_.get(a))return O("Command ignored. Plugin %s has already been required on tracker %s.",a,d),!0;b.plugins_.set(a,new e(b,c||{}));N("Plugin %s intialized on tracker %s.",a,d);return!0},z=function(a,b,c,d,e){if(!t(Re.get(b))&&!Te.get(b)){Se.hasOwnProperty(b)&&F(Se[b]);if(p.test(b)){F(52);a=Z.O(a);if(!a)return!0;c=d||{};d={id:b,F:c.dataLayer|| -"dataLayer",la:!!a.get("anonymizeIp"),qa:e,J:!1};a.get(">m")==b&&(d.J=!0,O("Infinite loop detected. Tracker trying to load the container (%s) that created it. Ignoring require statement.",b));var f=String(a.get("name"));"t0"!=f&&(d.target=f);K(String(a.get("trackingId")))||(d.ma=String(a.get(R)),d.na=Number(a.get(n)),a=c.palindrome?r:q,a=(a=I.cookie.replace(/^|(; +)/g,";").match(a))?a.sort().join("").substring(1):void 0,d.oa=a);a=d.F;c=(new Date).getTime();Q[a]=Q[a]||[];c={"gtm.start":c};e||(c.event= -"gtm.js");Q[a].push(c);c=w(d)}!c&&Se.hasOwnProperty(b)?(F(39),c=b+".js"):F(43);c?(c&&0<=c.indexOf("/")||(c=($b||df()?"https:":"http:")+"//www.google-analytics.com/plugins/ua/"+c),d=Ue(c),a=d.protocol,c=I.location.protocol,("https:"==a||a==c||("http:"!=a?0:"http:"==c))&&C(d)?(N("Loading resource for plugin: "+b),Ea(d.url,void 0,e),Te.set(b,!0)):O("Error loading resource for plugin %s: Refusing to load url: %s",b,d.url)):N("No plugin url set for %s.",b)}},y=function(a,b){var c=B.get(a)||[];c.push(b); -B.set(a,c)},D=function(a,b){Re.set(a,b);for(var c=B.get(a)||[],d=0;da.split("/")[0].indexOf(":")&&(a=ea+e[2].substring(0,e[2].lastIndexOf("/"))+"/"+a);c.href=a;d=b(c);return{protocol:(c.protocol||"").toLowerCase(),host:d[0],port:d[1], -path:d[2],query:c.search||"",url:a||""}};var jf={ka:function(){jf.j=[]}};jf.ka();jf.H=function(a){var b=jf.N.apply(jf,arguments),b=jf.j.concat(b);for(jf.j=[];0c;c++){var d=b[c].src;if(d&&0==d.indexOf("https://www.google-analytics.com/analytics")){F(33);b=!0;break a}}b=!1}b&&(L("Analytics.js is secure, forcing SSL for all hits."),$b=!0)}df()||$b||!ye(new Ie(1E4))||(L("Sending all Hits by SSL"),F(36),$b=!0);(Q.gaplugins=Q.gaplugins||{}).Linker=pd;b=pd.prototype;D("linker",pd);Y("decorate",b,b.S, -20);Y("autoLink",b,b.U,25);D("displayfeatures",$d);D("adfeatures",$d);a=a&&a.q;ga(a)?jf.H.apply(Z,a):F(50)}ge()}; -Z.ga=function(){for(var a=Z.getAll(),b=0;b>21:b;return b};})(window); \ No newline at end of file diff --git a/test/e2e/clean-url-tracker-test.js b/test/e2e/clean-url-tracker-test.js index b2d324f3..301f8006 100644 --- a/test/e2e/clean-url-tracker-test.js +++ b/test/e2e/clean-url-tracker-test.js @@ -66,68 +66,6 @@ describe('cleanUrlTracker', function() { assert.strictEqual(hits[0].dp, '/foo/bar?q=qux&b=baz'); }); - it('supports removing the query string from the URL path', () => { - const url = 'https://example.com/foo/bar?q=qux&b=baz#hash'; - browser.execute(ga.run, 'set', 'location', url); - browser.execute(ga.run, 'require', 'cleanUrlTracker', { - stripQuery: true, - }); - browser.execute(ga.run, 'send', 'pageview'); - browser.waitUntil(log.hitCountEquals(1)); - - const hits = log.getHits(); - assert.strictEqual(hits[0].dl, url); - assert.strictEqual(hits[0].dp, '/foo/bar'); - }); - - it('optionally adds the query string as a custom dimension', () => { - const url = 'https://example.com/foo/bar?q=qux&b=baz#hash'; - browser.execute(ga.run, 'set', 'location', url); - browser.execute(ga.run, 'require', 'cleanUrlTracker', { - stripQuery: true, - queryDimensionIndex: 1, - }); - browser.execute(ga.run, 'send', 'pageview'); - browser.waitUntil(log.hitCountEquals(1)); - - const hits = log.getHits(); - assert.strictEqual(hits[0].dl, url); - assert.strictEqual(hits[0].dp, '/foo/bar'); - assert.strictEqual(hits[0].cd1, 'q=qux&b=baz'); - }); - - it('adds the null dimensions when no query string is found', () => { - const url = 'https://example.com/foo/bar'; - browser.execute(ga.run, 'set', 'location', url); - browser.execute(ga.run, 'require', 'cleanUrlTracker', { - stripQuery: true, - queryDimensionIndex: 1, - }); - browser.execute(ga.run, 'send', 'pageview'); - browser.waitUntil(log.hitCountEquals(1)); - - const hits = log.getHits(); - assert.strictEqual(hits[0].dl, url); - assert.strictEqual(hits[0].dp, '/foo/bar'); - assert.strictEqual(hits[0].cd1, constants.NULL_DIMENSION); - }); - - it('does not set a dimension if strip query is false', () => { - const url = 'https://example.com/foo/bar?q=qux&b=baz#hash'; - browser.execute(ga.run, 'set', 'location', url); - browser.execute(ga.run, 'require', 'cleanUrlTracker', { - stripQuery: false, - queryDimensionIndex: 1, - }); - browser.execute(ga.run, 'send', 'pageview'); - browser.waitUntil(log.hitCountEquals(1)); - - const hits = log.getHits(); - assert.strictEqual(hits[0].dl, url); - assert.strictEqual(hits[0].dp, '/foo/bar?q=qux&b=baz'); - assert.strictEqual(hits[0].cd1, undefined); - }); - it('cleans URLs in all hits, not just the initial pageview', () => { const url = 'https://example.com/foo/bar?q=qux&b=baz#hash'; browser.execute(ga.run, 'set', 'location', url); @@ -176,76 +114,6 @@ describe('cleanUrlTracker', function() { assert.strictEqual(hits[1].cd1, 'query=new'); }); - it('supports removing index filenames', () => { - const url = 'https://example.com/foo/bar/index.html?q=qux&b=baz#hash'; - browser.execute(ga.run, 'set', 'location', url); - browser.execute(ga.run, 'require', 'cleanUrlTracker', { - indexFilename: 'index.html', - }); - browser.execute(ga.run, 'send', 'pageview'); - browser.waitUntil(log.hitCountEquals(1)); - - const hits = log.getHits(); - assert.strictEqual(hits[0].dp, '/foo/bar/?q=qux&b=baz'); - }); - - it('only removes index filenames at the end of the URL after a slash', () => { - const url = 'https://example.com/noindex.html'; - browser.execute(ga.run, 'set', 'location', url); - browser.execute(ga.run, 'require', 'cleanUrlTracker', { - indexFilename: 'index.html', - }); - browser.execute(ga.run, 'send', 'pageview'); - browser.waitUntil(log.hitCountEquals(1)); - - const hits = log.getHits(); - assert.strictEqual(hits[0].dp, '/noindex.html'); - }); - - it('supports stripping trailing slashes', () => { - const url = 'https://example.com/foo/bar/'; - browser.execute(ga.run, 'set', 'location', url); - browser.execute(ga.run, 'require', 'cleanUrlTracker', { - trailingSlash: 'remove', - }); - browser.execute(ga.run, 'send', 'pageview'); - browser.waitUntil(log.hitCountEquals(1)); - - const hits = log.getHits(); - assert.strictEqual(hits[0].dp, '/foo/bar'); - }); - - it('supports adding trailing slashes to non-filename URLs', () => { - const url = 'https://example.com/foo/bar?q=qux&b=baz#hash'; - browser.execute(ga.run, 'set', 'location', url); - browser.execute(ga.run, 'require', 'cleanUrlTracker', { - stripQuery: true, - queryDimensionIndex: 1, - trailingSlash: 'add', - }); - browser.execute(ga.run, 'send', 'pageview'); - browser.execute(ga.run, 'set', 'page', '/foo/bar.html'); - browser.execute(ga.run, 'send', 'pageview'); - browser.waitUntil(log.hitCountEquals(2)); - - const hits = log.getHits(); - assert.strictEqual(hits[0].dp, '/foo/bar/'); - assert.strictEqual(hits[1].dp, '/foo/bar.html'); - }); - - it('supports generically filtering all URL fields', () => { - const url = 'https://example.com/foo/bar?q=qux&b=baz#hash'; - browser.execute(ga.run, 'set', 'location', url); - browser.execute(requireCleanUrlTracker_urlFieldsFilter); - browser.execute(ga.run, 'send', 'pageview'); - browser.waitUntil(log.hitCountEquals(1)); - - const hits = log.getHits(); - assert.strictEqual(hits[0].dl, - 'https://example.io/foo/bar?q=qux&b=baz#hash'); - assert.strictEqual(hits[0].dp, '/foo/bar'); - }); - it('works with many options in conjunction with each other', () => { const url = 'https://example.com/path/to/index.html?q=qux&b=baz#hash'; browser.execute(ga.run, 'set', 'location', url); @@ -256,7 +124,7 @@ describe('cleanUrlTracker', function() { const hits = log.getHits(); assert.strictEqual(hits[0].dl, 'https://example.io/path/to/index.html?q=qux&b=baz#hash'); - assert.strictEqual(hits[0].dp, '/path/to'); + assert.strictEqual(hits[0].dp, '/path/to?q=qux'); assert.strictEqual(hits[0].cd1, 'q=qux&b=baz'); }); @@ -300,28 +168,6 @@ describe('cleanUrlTracker', function() { }); -/** - * Since function objects can't be passed via parameters from server to - * client, this one-off function must be used to set the value for - * `urlFieldsFilter`. - */ -function requireCleanUrlTracker_urlFieldsFilter() { - ga('require', 'cleanUrlTracker', { - urlFieldsFilter: (fieldsObj, parseUrl) => { - fieldsObj.page = parseUrl(fieldsObj.location).pathname; - - const url = parseUrl(fieldsObj.location); - if (url.hostname == 'example.com') { - fieldsObj.location = - `${url.protocol}//example.io` + - `${url.pathname}${url.search}${url.hash}`; - } - return fieldsObj; - }, - }); -} - - /** * Since function objects can't be passed via parameters from server to * client, this one-off function must be used to set the value for @@ -330,6 +176,7 @@ function requireCleanUrlTracker_urlFieldsFilter() { function requireCleanUrlTracker_multipleOpts() { ga('require', 'cleanUrlTracker', { stripQuery: true, + queryParamsWhitelist: ['q', 's'], queryDimensionIndex: 1, indexFilename: 'index.html', trailingSlash: 'remove', diff --git a/test/e2e/fixtures/autotrack-rename.html b/test/e2e/fixtures/autotrack-rename.html index 43764d51..90fb48c1 100644 --- a/test/e2e/fixtures/autotrack-rename.html +++ b/test/e2e/fixtures/autotrack-rename.html @@ -7,7 +7,7 @@ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','//www.google-analytics.com/analytics.js','_ga'); - + diff --git a/test/e2e/fixtures/autotrack-reorder.html b/test/e2e/fixtures/autotrack-reorder.html index f48e3f67..4e6fc092 100644 --- a/test/e2e/fixtures/autotrack-reorder.html +++ b/test/e2e/fixtures/autotrack-reorder.html @@ -5,7 +5,7 @@ - + diff --git a/test/e2e/fixtures/autotrack.html b/test/e2e/fixtures/autotrack.html index 6d35f9dc..e2d6413a 100644 --- a/test/e2e/fixtures/autotrack.html +++ b/test/e2e/fixtures/autotrack.html @@ -4,7 +4,7 @@ - + diff --git a/test/e2e/fixtures/blank.html b/test/e2e/fixtures/blank.html index e69de29b..8b137891 100644 --- a/test/e2e/fixtures/blank.html +++ b/test/e2e/fixtures/blank.html @@ -0,0 +1 @@ + diff --git a/test/e2e/fixtures/event-tracker.html b/test/e2e/fixtures/event-tracker.html index 7aa16075..42ab7292 100644 --- a/test/e2e/fixtures/event-tracker.html +++ b/test/e2e/fixtures/event-tracker.html @@ -4,7 +4,7 @@ - + diff --git a/test/e2e/fixtures/impression-tracker.html b/test/e2e/fixtures/impression-tracker.html index bc31b2a5..64f6db49 100644 --- a/test/e2e/fixtures/impression-tracker.html +++ b/test/e2e/fixtures/impression-tracker.html @@ -7,7 +7,7 @@ - +