From 0e6af81c17a94bb6354558f9983b9c3289bd7293 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Tue, 23 May 2017 12:30:51 -0700 Subject: [PATCH 01/71] Release version 2.3.3 --- CHANGELOG.md | 4 +++ autotrack.js | 90 +++++++++++++++++++++++++----------------------- autotrack.js.map | 2 +- lib/constants.js | 2 +- package.json | 2 +- 5 files changed, 53 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2a3e55..92b1f4f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ This document lists the changes between each minor and patch versions. For changes between major versions, see the [Upgrade Reference](/docs/upgrading.md) +### 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/autotrack.js b/autotrack.js index af62613f..df03e45c 100644 --- a/autotrack.js +++ b/autotrack.js @@ -1,60 +1,62 @@ (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)))} +var y=Object.assign||function(a,b){for(var c=[],d=1;d>b/4).toString(16):"10000000-1000-4000-8000-100000000000".replace(/[018]/g,wa)}; +function F(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 G={T:1,U:2,V:3,X:4,Y:5,Z:6,$:7,aa:8,ba:9,W:10},H=Object.keys(G).length; +function I(a,b){a.set("\x26_av","2.3.3");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))}};K.prototype.remove=function(){var a=this;Object.keys(this.b).forEach(function(b){a.b[b].j()})};F("eventTracker",K); +function ya(a,b){var c=this;I(a,G.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.i={},this.h={},sa(function(){c.a.elements&&c.observeElements(c.a.elements)}))}f=ya.prototype; +f.observeElements=function(a){var b=this;a=L(this,a);this.items=this.items.concat(a.items);this.i=y({},a.i,this.i);this.h=y({},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(){})}; +f.unobserveElements=function(a){var b=[],c=[];this.items.forEach(function(d){a.some(function(a){a=za(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=L(this,b),e=L(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()}; +f.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 L(a,b){var c=[],d={},e={};b.length&&b.forEach(function(b){b=za(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}}f.M=function(a){for(var b=0,c;c=a[b];b++){for(var d=0,e;e=c.removedNodes[d];d++)M(this,e,this.L);for(d=0;e=c.addedNodes[d];d++)M(this,e,this.K)}}; +function M(a,b,c){1==b.nodeType&&b.id in a.i&&c(b.id);for(var d=0,e;e=b.childNodes[d];d++)M(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(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=C());d&&(e.isExpired=!0);b.a.set(e)}}; +U.prototype.j=function(){w(this.f,"sendHitTask",this.b);this.a.j();delete T[this.f.get("trackingId")]};var Fa=30;function W(a,b){I(a,G.W);window.addEventListener&&(this.b=y({increaseThreshold:20,sessionTimeout:Fa,fieldsObj:{}},b),this.f=a,this.c=Ha(this),this.g=ta(this.g.bind(this),500),this.o=this.o.bind(this),this.a=R(a.get("trackingId"),"plugins/max-scroll-tracker"),this.m=Ga(a,this.b.sessionTimeout,this.b.timeZone),v(a,"set",this.o),Ia(this))} +function Ia(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&&(Ea(this.a),this.a.set({sessionId:b}));if(this.m.isExpired(this.a.get().sessionId))Ea(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",x(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={};(B(c)?c:(e[c]=d,e)).page&&(c=b.c,b.c=Ha(b),b.c!=c&&Ia(b))}}; +function Ha(a){a=t(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);w(this.f,"set",this.o)};F("maxScrollTracker",W);var Ja={};function Ka(a,b){I(a,G.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=[],La(this)))} +function La(a){a.a.definitions.forEach(function(b){if(b.name&&b.dimensionIndex){var c=Na(b);a.b.set("dimension"+b.dimensionIndex,c);Oa(a,b)}})}function Na(a){var b;a.items.forEach(function(a){Pa(a.media).matches&&(b=a)});return b?b.name:"(not set)"} +function Oa(a,b){b.items.forEach(function(c){c=Pa(c.media);var d=ta(function(){var c=Na(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})})}Ka.prototype.remove=function(){for(var a=0,b;b=this.c[a];a++)b.fa.removeListener(b.da)}; +Ka.prototype.changeTemplate=function(a,b){return a+" \x3d\x3e "+b};F("mediaQueryTracker",Ka);function Pa(a){return Ja[a]||(Ja[a]=window.matchMedia(a))}function X(a,b){I(a,G.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)}))} +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()};F("outboundFormTracker",X); +function Y(a,b){var c=this;I(a,G.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){}} +this.f.send("event",x(e,d,this.f,this.a.hitFilter,b,a))}};Y.prototype.shouldTrackOutboundLink=function(a,b){a=a.getAttribute("href")||a.getAttribute("xlink:href");b=b(a);return b.hostname!=location.hostname&&"http"==b.protocol.slice(0,4)};Y.prototype.remove=function(){var a=this;Object.keys(this.b).forEach(function(b){a.b[b].j()})};F("outboundLinkTracker",Y);var Z=C(); +function Qa(a,b){var c=this;I(a,G.$);document.visibilityState&&(this.a=y({sessionTimeout:Fa,visibleThreshold:5E3,sendInitialPageview:!1,fieldsObj:{}},b),this.b=a,this.g=document.visibilityState,this.m=null,this.o=!1,this.v=this.v.bind(this),this.s=this.s.bind(this),this.G=this.G.bind(this),this.N=this.N.bind(this),this.c=R(a.get("trackingId"),"plugins/page-visibility-tracker"),Ba(this.c,this.N),this.f=Ga(a,this.a.sessionTimeout,this.a.timeZone),v(a,"set",this.v),window.addEventListener("unload",this.G), +document.addEventListener("visibilitychange",this.s),va(this.b,function(){if("visible"==document.visibilityState)c.a.sendInitialPageview&&(Ra(c,{ea:!0}),c.o=!0),c.c.set({time:+new Date,state:"visible",pageId:Z,sessionId:V(c.f)});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=Qa.prototype; +f.s=function(){var a=this;if("visible"==document.visibilityState||"hidden"==document.visibilityState){var b=Sa(this),c={time:+new Date,state:document.visibilityState,pageId:Z,sessionId:V(this.f)};"visible"==document.visibilityState&&this.a.sendInitialPageview&&!this.o&&(Ra(this),this.o=!0);"hidden"==document.visibilityState&&this.m&&clearTimeout(this.m);this.f.isExpired(b.sessionId)?(Ea(this.c),"hidden"==this.g&&"visible"==document.visibilityState&&(clearTimeout(this.m),this.m=setTimeout(function(){a.c.set(c); +Ra(a,{hitTime:c.time})},this.a.visibleThreshold))):(b.pageId==Z&&"visible"==b.state&&Ta(this,b),this.c.set(c));this.g=document.visibilityState}};function Sa(a){var b=a.c.get();"visible"==a.g&&"hidden"==b.state&&b.pageId!=Z&&(b.state="visible",b.pageId=Z,a.c.set(b));return b} +function Ta(a,b,c){c=(c?c:{}).hitTime;var d={hitTime:c},d=(d?d:{}).hitTime;(b=b.time?(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 Ra(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.g&&b.s();a(c,d)}};f.N=function(a,b){a.time!=b.time&&(b.pageId!=Z||"visible"!=b.state||this.f.isExpired(b.sessionId)||Ta(this,b,{hitTime:a.time}))}; +f.G=function(){"hidden"!=this.g&&this.s()};f.remove=function(){this.c.j();this.f.j();w(this.b,"set",this.v);window.removeEventListener("unload",this.G);document.removeEventListener("visibilitychange",this.s)};F("pageVisibilityTracker",Qa); +function Ua(a,b){I(a,G.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=Ua.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 Va(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} 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/arrayfromiterable] "," [synthetic:es6/util/makeiterator] "," [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","NULL_DIMENSION","urlFieldsFilter","userCleanedFieldsObj","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","Array","isArray","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","defaultPrevented","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.3.3';\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, 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 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 = 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/lib/constants.js b/lib/constants.js index b790d854..820a8066 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -15,7 +15,7 @@ */ -export const VERSION = '2.3.2'; +export const VERSION = '2.3.3'; export const DEV_ID = 'i5iSjo'; export const VERSION_PARAM = '_av'; diff --git a/package.json b/package.json index 061976b1..f491bb94 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "autotrack", - "version": "2.3.2", + "version": "2.3.3", "description": "Automatic and enhanced Google Analytics tracking for common user interactions on the web", "main": "lib", "bin": "./bin/autotrack", From 5af4f28f843c1d8aa816c8fff85e3601b4be67d6 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Tue, 30 May 2017 16:59:33 -0700 Subject: [PATCH 02/71] Remove site search mentions --- docs/plugins/clean-url-tracker.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugins/clean-url-tracker.md b/docs/plugins/clean-url-tracker.md index 08123ed7..8322805e 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 @@ -92,7 +92,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.

From 2fac034ff373473bdf81170efc1020c4a66cb72e Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Thu, 1 Jun 2017 16:55:08 -0700 Subject: [PATCH 03/71] Update the analytics.js test files --- test/analytics.js | 71 ++++++++++++++++---------------- test/analytics_debug.js | 89 +++++++++++++++++++++-------------------- 2 files changed, 83 insertions(+), 77 deletions(-) diff --git a/test/analytics.js b/test/analytics.js index 730b66c4..e361479c 100644 --- a/test/analytics.js +++ b/test/analytics.js @@ -1,45 +1,48 @@ -(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\x3c/script>')):(c=M.createElement("script"), +c.type="text/javascript",c.async=!0,c.src=a,d&&(c.onload=d),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){return(a=a.match("(?:&|#|\\?)"+K(b).replace(/([.*+?^=!:${}()|\[\]\/\\])/g,"\\$1")+"=([^&#]*)"))&&2==a.length?a[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)} +a=a.replace(/^http:/,"https:");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=j54","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){d.F&&(c=a.get(c),void 0!=c&&c!=d.defaultValue&&("boolean"==typeof c&&(c*=1),b.push(d.F+"="+K(""+c))))});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)}; +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=a.get(Na);if(b=h(b).pending_experiments){var c=[];for(d in b)b.hasOwnProperty(d)&&b[d]&&c.push(encodeURIComponent(d)+"."+encodeURIComponent(b[d]));var d=c.join("!")}else d=void 0;d&&a.set(m,d,!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){a=a.get(b);return void 0==a?"":""+a},R=function(a,b){a=a.get(b);return void 0==a||""===a?0:1*a};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={})}}; +var ld=S("usage","_u"),Gd=S("_um");S("forceSSL",void 0,void 0,function(){return Ba},function(a,b,c){J(34);Ba=!!c});var ed=S("_j1","jid"),ia=S("_j2","gjid");cb("\\&(.*)",function(a){var b=new bb(a[0],a[1]),c=yc(a[0].substring(1));c&&(b.Z=function(a){return a.get(c)},b.o=function(a,b,g,ca){a.set(c,g,ca)},b.F=void 0);return b}); +var Qb=T("_oot"),dd=S("previewTask"),Rb=S("checkProtocolTask"),md=S("validationTask"),Sb=S("checkStorageTask"),Uc=S("historyImportTask"),Tb=S("samplerTask"),Vb=S("_rlt"),Wb=S("buildHitTask"),Xb=S("sendHitTask"),Vc=S("ceTask"),zd=S("devIdTask"),Cd=S("timingTask"),Ld=S("displayFeaturesTask"),oa=S("customTask"),V=T("name"),Q=T("clientId","cid"),n=T("clientIdTime"),Ad=S("userId","uid"),Na=T("trackingId","tid"),U=T("cookieName",void 0,"_ga"),W=T("cookieDomain"),Yb=T("cookiePath",void 0,"/"),Zb=T("cookieExpires", +void 0,63072E3),$b=T("legacyCookieDomain"),Wc=T("legacyHistoryImport",void 0,!0),ac=T("storage",void 0,"cookie"),bc=T("allowLinker",void 0,!1),cc=T("allowAnchor",void 0,!0),Ka=T("sampleRate","sf",100),dc=T("siteSpeedSampleRate",void 0,1),ec=T("alwaysSendReferrer",void 0,!1),I=T("_gid","_gid"),ja=T("_ge"),la=T("_gcn"),gd=S("transportUrl"),Md=S("_r","_r");function X(a,b,c,d){b[a]=function(){try{return d&&J(d),c.apply(this,arguments)}catch(e){throw ge("exc",a,e&&e.name),e;}}};var Od=function(a,b,c){this.V=a;this.fa=b;this.$=!1;this.oa=c;this.ea=1},Ed=function(a,b,c){if(a.fa&&a.$)return 0;a.$=!0;if(b){if(a.oa&&R(b,a.oa))return R(b,a.oa);if(0==b.get(dc))return 0}if(0==a.V)return 0;void 0===c&&(c=Bd());return 0==c%a.V?Math.floor(c/a.V)%a.ea+1:0};function fc(){var a,b;if((b=(b=O.navigator)?b.plugins:null)&&b.length)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=0b.length)J(12);else{for(var c= +[],d=0;d=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){if(null==a)var c=a=1;else c=La(a),a=La(D(a,".")?a.substring(1):"."+a);for(var d=0;d=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)){if("string"===typeof arguments[0]){var b=arguments[0];var c=[].slice.call(arguments,1)}else 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={},Ed(this.ra,this.b)&&da(this.b.get(Na)))}}; 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=[];0a.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 +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 index 9b1a14c9..12b8db2d 100644 --- a/test/analytics_debug.js +++ b/test/analytics_debug.js @@ -1,17 +1,17 @@ -(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\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,d&&(c.onload=d),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){return(a=a.match("(?:&|#|\\?)"+ +P(b).replace(/([.*+?^=!:${}()|\[\]\/\\])/g,"\\$1")+"=([^&#]*)"))&&2==a.length?a[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 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){d.i&&(c=a.get(c),void 0!=c&&c!=d.defaultValue&&("boolean"==typeof c&&(c*=1),b.push(d.i+"="+P(""+c))))});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)}; +c+1:1;b=a.get(U);delete h(b).pending_experiments;a.set(Nb,Aa,!0)}function td(a){(Q.gaData=Q.gaData||{}).expId&&a.set(zd,(Q.gaData=Q.gaData||{}).expId);(Q.gaData=Q.gaData||{}).expVar&&a.set(Ad,(Q.gaData=Q.gaData||{}).expVar);var b=a.get(U);if(b=h(b).pending_experiments){var c=[];for(d in b)b.hasOwnProperty(d)&&b[d]&&c.push(encodeURIComponent(d)+"."+encodeURIComponent(b[d]));var d=c.join("!")}else d=void 0;d&&a.set(m,d,!0)}function nc(a){Ec(a)} +function Ud(){if(Q.navigator&&"preview"==Q.navigator.loadPurpose)throw N("Navigator in preview mode. Aborting hit."),"abort";}function ve(a){var b=Q.gaDevIds;ga(b)&&0!=b.length&&a.set("&did",b.join(","),!0)}function ya(a){if(!a.get(U))throw O("Tracking ID not set. Aborting hit."),"abort";};var ae=function(){return Math.round(2147483647*Math.random())},be=function(){try{var a=new Uint32Array(1);Q.crypto.getRandomValues(a);return a[0]&2147483647}catch(b){return ae()}};function qc(a){var b=jc(a,rc);500<=b&&(F(15),N("Exceeded maximum number of hits for this page. Try reducing the number of hits being sent."));var c=V(a,Ma);if("transaction"!=c&&"item"!=c){var c=jc(a,uc),d=(new Date).getTime(),e=jc(a,vc);0==e&&a.set(vc,d);e=Math.round(2*(d-e)/1E3);0=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){a=a.get(b);return void 0==a?"":""+a},jc=function(a,b){a=a.get(b);return void 0==a||""===a?0:1*a};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;c=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=0b.length)F(12);else{for(var c=[],d=0;d=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){if(null==a)var c=a=1;else c=ic(a),a=ic(H(a,".")?a.substring(1):"."+a);for(var d=0;d=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.send=function(a){L("Send start: "+(0==Z.h?-1:(new Date).getTime()-Z.h));if(1>arguments.length)O("No hit type specified. Aborting hit.");else{if("string"===typeof arguments[0]){var b=arguments[0];var c=[].slice.call(arguments,1)}else 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={},ye(this.ua,this.a)&&fa(this.a.get(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=[];0a.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=[];0>21:b;return b};})(window); \ No newline at end of file +J("Running analytics_debug.js. This script is intended for testing and debugging only.");var sa=Z.P,ta=Q[Fc];ta&&ta.r?sa():A(sa);A(function(){jf.H(["provide","render",Aa])});function ic(a){var b=1,c;if(a)for(b=0,c=a.length-1;0<=c;c--){var d=a.charCodeAt(c);b=(b<<6&268435455)+d+(d<<14);d=b&266338304;b=0!=d?b^d>>21:b}return b};})(window); \ No newline at end of file From bdbc90a6d8306e5fa06387793dc68ff2ba49676f Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Thu, 1 Jun 2017 16:57:23 -0700 Subject: [PATCH 04/71] Increase the visible threshold to reduce flakiness --- test/e2e/page-visibility-tracker-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/page-visibility-tracker-test.js b/test/e2e/page-visibility-tracker-test.js index b0909c87..28db68cc 100644 --- a/test/e2e/page-visibility-tracker-test.js +++ b/test/e2e/page-visibility-tracker-test.js @@ -25,7 +25,7 @@ import pkg from '../../package.json'; const SESSION_TIMEOUT_IN_MILLISECONDS = 3000; // 3 seconds const SESSION_TIMEOUT_IN_MINUTES = (1/60) * 3; // 3 seconds -const VISIBLE_THRESHOLD = 2000; // 2 seconds +const VISIBLE_THRESHOLD = 4000; // 4 seconds const DEFAULT_TRACKER_FIELDS = { From d3649b4c3423da6e9dab0f912e37809de0f9b143 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Thu, 1 Jun 2017 16:52:53 -0700 Subject: [PATCH 05/71] Add a siteSearchQueryParams config option --- lib/externs/clean-url-tracker.js | 1 + lib/plugins/clean-url-tracker.js | 36 +- test/e2e/clean-url-tracker-test.js | 157 +------- test/unit/plugins/clean-url-tracker-test.js | 398 ++++++++++++++++++++ 4 files changed, 434 insertions(+), 158 deletions(-) create mode 100644 test/unit/plugins/clean-url-tracker-test.js diff --git a/lib/externs/clean-url-tracker.js b/lib/externs/clean-url-tracker.js index 7a4fe5d3..5f9dc886 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), + * siteSearchQueryParams: (Array|unefined), * queryDimensionIndex: (number|undefined), * indexFilename: (string|undefined), * trailingSlash: (string|undefined), diff --git a/lib/plugins/clean-url-tracker.js b/lib/plugins/clean-url-tracker.js index c92bee9b..49262fae 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, + // siteSearchQueryParams: 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.stripNonSiteSearchParams(url.search) : url.search), }; if (fieldsObj.location) { cleanedFieldsObj.location = fieldsObj.location; @@ -157,16 +159,44 @@ 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 A whitelist of URL params. + * @param {string} search The URL search string (starting with '?'). + * @return {string} The query string + */ + stripNonSiteSearchParams(searchString) { + if (Array.isArray(this.opts.siteSearchQueryParams)) { + const foundParams = []; + searchString.slice(1).split('&').forEach((kv) => { + const [key, value] = kv.split('='); + if (this.opts.siteSearchQueryParams.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/test/e2e/clean-url-tracker-test.js b/test/e2e/clean-url-tracker-test.js index b2d324f3..26cb146f 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, + siteSearchQueryParams: ['q', 's'], queryDimensionIndex: 1, indexFilename: 'index.html', trailingSlash: 'remove', diff --git a/test/unit/plugins/clean-url-tracker-test.js b/test/unit/plugins/clean-url-tracker-test.js new file mode 100644 index 00000000..ef182c28 --- /dev/null +++ b/test/unit/plugins/clean-url-tracker-test.js @@ -0,0 +1,398 @@ +/** + * Copyright 2017 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 assert from 'assert'; +import sinon from 'sinon'; +import * as constants from '../../../lib/constants'; +import '../../../lib/plugins/clean-url-tracker'; + + +const DEFAULT_TRACKER_FIELDS = { + trackingId: 'UA-12345-1', + cookieDomain: 'auto', + siteSpeedSampleRate: 0, +}; + + +describe('CleanUrlTracker', () => { + let tracker; + let CleanUrlTracker; + + beforeEach((done) => { + localStorage.clear(); + window.ga('create', DEFAULT_TRACKER_FIELDS); + window.ga((t) => { + tracker = t; + CleanUrlTracker = window.gaplugins.CleanUrlTracker; + done(); + }); + }); + + afterEach(() => { + localStorage.clear(); + window.ga('remove'); + }); + + describe('constructor', () => { + it('stores the tracker on the instance', () => { + const cut = new CleanUrlTracker(tracker); + assert.strictEqual(tracker, cut.tracker); + + cut.remove(); + }); + + it('merges the passed options with the defaults', () => { + if (!document.visibilityState) this.skip(); + + let cut = new CleanUrlTracker(tracker); + + assert.deepEqual(cut.opts, {}); + cut.remove(); + + const fn = () => {}; + const opts = { + stripQuery: true, + siteSearchQueryParams: ['q', 's'], + queryDimensionIndex: 1, + indexFilename: 'index.html', + trailingSlash: 'remove', + urlFilter: fn, + }; + cut = new CleanUrlTracker(tracker, opts); + assert.deepEqual(cut.opts, opts); + cut.remove(); + }); + + it('overrides the tracker\'s get method', () => { + tracker.set('location', 'https://example.com/test?foo=bar'); + assert(!tracker.get('page')); + + const originalTrackerGet = tracker.get; + const cut = new CleanUrlTracker(tracker, { + stripQuery: true, + }); + + assert.notStrictEqual(tracker.get, originalTrackerGet); + assert.strictEqual(tracker.get('page'), '/test'); + + cut.remove(); + }); + + it('overrides the tracker\'s buildHitTask function', () => { + const originalTrackerGet = tracker.get('buildHitTask'); + const cut = new CleanUrlTracker(tracker); + assert.notStrictEqual(tracker.get('buildHitTask'), originalTrackerGet); + + const spy = sinon.spy(cut, 'cleanUrlFields'); + tracker.send('pageview'); + + assert(spy.calledOnce); + + cut.remove(); + }); + }); + + describe('stripNonSiteSearchParams', () => { + it('returns a URL search string with only site search params', () => { + const cut = new CleanUrlTracker(tracker, { + siteSearchQueryParams: ['q', 's'], + }); + + assert.strictEqual( + cut.stripNonSiteSearchParams('?gclid=foo'), ''); + assert.strictEqual( + cut.stripNonSiteSearchParams('?gclid=foo&q=1'), '?q=1'); + assert.strictEqual( + cut.stripNonSiteSearchParams('?q=1&gclid=foo'), '?q=1'); + assert.strictEqual( + cut.stripNonSiteSearchParams('?gclid=foo&q=1&s=2'), + '?q=1&s=2'); + assert.strictEqual( + cut.stripNonSiteSearchParams('?q=1&gclid=foo&s=2'), '?q=1&s=2'); + assert.strictEqual( + cut.stripNonSiteSearchParams('?q=1&s=2&gclid=foo'), '?q=1&s=2'); + + cut.remove(); + }); + + it('does not modify URL encoded keys or values', () => { + const cut = new CleanUrlTracker(tracker, { + siteSearchQueryParams: ['q', 's'], + }); + + assert.strictEqual( + cut.stripNonSiteSearchParams('?gclid=foo&q=1%202'), '?q=1%202'); + + cut.remove(); + }); + + it('works with empty or missing param values', () => { + const cut = new CleanUrlTracker(tracker, { + siteSearchQueryParams: ['q', 's'], + }); + + assert.strictEqual( + cut.stripNonSiteSearchParams('?q=1&s=2&gclid=foo'), '?q=1&s=2'); + assert.strictEqual( + cut.stripNonSiteSearchParams('?q&s=&gclid=foo'), ''); + + cut.remove(); + }); + + it('works when the siteSearchQueryParams option is not set', () => { + const cut = new CleanUrlTracker(tracker); + + assert.strictEqual(cut.stripNonSiteSearchParams('?utm_source=foo'), ''); + assert.strictEqual( + cut.stripNonSiteSearchParams('?utm_source=foo&q=1'), ''); + assert.strictEqual( + cut.stripNonSiteSearchParams('?utm_source=foo&q=1&s=2'), ''); + + cut.remove(); + }); + }); + + describe('cleanUrlFields', () => { + it('returns a fieldsObj with the URL fields set', () => { + const cut = new CleanUrlTracker(tracker); + const ret = cut.cleanUrlFields({ + location: 'https://example.com/test?foo=bar', + }); + + assert(typeof ret == 'object'); + assert(ret.hasOwnProperty('location')); + assert(ret.hasOwnProperty('page')); + + cut.remove(); + }); + + it('sets the page field but does not modify the path by default', () => { + const cut = new CleanUrlTracker(tracker); + const location = 'https://example.com/foo/bar?q=qux&b=baz#hash'; + + assert.deepEqual(cut.cleanUrlFields({location}), { + location, + page: '/foo/bar?q=qux&b=baz', + }); + + cut.remove(); + }); + + it('supports removing the query string from the URL path', () => { + const cut = new CleanUrlTracker(tracker, { + stripQuery: true, + }); + const location = 'https://example.com/foo/bar?q=qux&b=baz#hash'; + + assert.deepEqual(cut.cleanUrlFields({location}), { + location, + page: '/foo/bar', + }); + + cut.remove(); + }); + + it('optionally returns the query string as a custom dimension', () => { + const cut = new CleanUrlTracker(tracker, { + stripQuery: true, + queryDimensionIndex: 1, + }); + const location = 'https://example.com/foo/bar?q=qux&b=baz#hash'; + + assert.deepEqual(cut.cleanUrlFields({location}), { + location, + page: '/foo/bar', + dimension1: 'q=qux&b=baz', + }); + + cut.remove(); + }); + + it('returns the null dimensions when no query string is found', () => { + const cut = new CleanUrlTracker(tracker, { + stripQuery: true, + queryDimensionIndex: 1, + }); + const location = 'https://example.com/foo/bar'; + + assert.deepEqual(cut.cleanUrlFields({location}), { + location, + page: '/foo/bar', + dimension1: constants.NULL_DIMENSION, + }); + + cut.remove(); + }); + + it('does not set a dimension if strip query is false', () => { + const cut = new CleanUrlTracker(tracker, { + stripQuery: false, + queryDimensionIndex: 1, + }); + const location = 'https://example.com/foo/bar?q=qux&b=baz#hash'; + + assert.deepEqual(cut.cleanUrlFields({location}), { + location, + page: '/foo/bar?q=qux&b=baz', + }); + + cut.remove(); + }); + + it('supports removing index filenames', () => { + const cut = new CleanUrlTracker(tracker, { + indexFilename: 'index.html', + }); + const location = + 'https://example.com/foo/bar/index.html?q=qux&b=baz#hash'; + + assert.deepEqual(cut.cleanUrlFields({location}), { + location, + page: '/foo/bar/?q=qux&b=baz', + }); + + cut.remove(); + }); + + it('only removes index filenames at the end of the URL after a slash', + () => { + const cut = new CleanUrlTracker(tracker, { + indexFilename: 'index.html', + }); + const location = 'https://example.com/noindex.html'; + + assert.deepEqual(cut.cleanUrlFields({location}), { + location, + page: '/noindex.html', + }); + + cut.remove(); + }); + + it('supports stripping trailing slashes', () => { + const cut = new CleanUrlTracker(tracker, { + trailingSlash: 'remove', + }); + const location = 'https://example.com/foo/bar/'; + + assert.deepEqual(cut.cleanUrlFields({location}), { + location, + page: '/foo/bar', + }); + + cut.remove(); + }); + + it('supports adding trailing slashes to non-filename URLs', () => { + const cut = new CleanUrlTracker(tracker, { + stripQuery: true, + trailingSlash: 'add', + }); + const location = 'https://example.com/foo/bar?q=qux&b=baz#hash'; + + assert.deepEqual(cut.cleanUrlFields({location}), { + location, + page: '/foo/bar/', + }); + + assert.deepEqual(cut.cleanUrlFields({ + location, + page: '/foo/bar.html', + }), { + location, + page: '/foo/bar.html', + }); + + cut.remove(); + }); + + it('supports programmatically filtering URL fields', () => { + const cut = new CleanUrlTracker(tracker, { + 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; + }, + }); + const location = 'https://example.com/foo/bar?q=qux&b=baz#hash'; + + assert.deepEqual(cut.cleanUrlFields({location}), { + location: 'https://example.io/foo/bar?q=qux&b=baz#hash', + page: '/foo/bar', + }); + + cut.remove(); + }); + + it('works with many options in conjunction with each other', () => { + const cut = new CleanUrlTracker(tracker, { + stripQuery: true, + queryDimensionIndex: 1, + indexFilename: 'index.html', + trailingSlash: 'remove', + urlFieldsFilter: (fieldsObj, parseUrl) => { + const url = parseUrl(fieldsObj.location); + if (url.hostname == 'example.com') { + fieldsObj.location = + `${url.protocol}//example.io` + + `${url.pathname}${url.search}${url.hash}`; + } + return fieldsObj; + }, + }); + const location = + 'https://example.com/path/to/index.html?q=qux&b=baz#hash'; + + assert.deepEqual(cut.cleanUrlFields({location}), { + location: 'https://example.io/path/to/index.html?q=qux&b=baz#hash', + page: '/path/to', + dimension1: 'q=qux&b=baz', + }); + + cut.remove(); + }); + }); + + describe('remove', () => { + it('restores the tracker\'s get method', () => { + const originalTrackerGet = tracker.get; + const cut = new CleanUrlTracker(tracker); + + assert.notStrictEqual(tracker.get, originalTrackerGet); + + cut.remove(); + + assert.strictEqual(tracker.get, originalTrackerGet); + }); + + it('restores the tracker\'s buildHitTask function', () => { + const originalTrackerGet = tracker.get('buildHitTask'); + const cut = new CleanUrlTracker(tracker); + assert.notStrictEqual(tracker.get('buildHitTask'), originalTrackerGet); + + cut.remove(); + + assert.strictEqual(tracker.get('buildHitTask'), originalTrackerGet); + }); + }); +}); From 9f7359a76575f1b1b5d3a6a18871528c50c82910 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Thu, 1 Jun 2017 17:50:08 -0700 Subject: [PATCH 06/71] Rename siteSearchQueryParams To queryParamsWhitelist. --- lib/externs/clean-url-tracker.js | 2 +- lib/plugins/clean-url-tracker.js | 10 +++--- test/e2e/clean-url-tracker-test.js | 2 +- test/unit/plugins/clean-url-tracker-test.js | 39 +++++++++++---------- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/lib/externs/clean-url-tracker.js b/lib/externs/clean-url-tracker.js index 5f9dc886..01f5e29e 100644 --- a/lib/externs/clean-url-tracker.js +++ b/lib/externs/clean-url-tracker.js @@ -2,7 +2,7 @@ * Public options for the CleanUrlTracker. * @typedef {{ * stripQuery: (boolean|undefined), - * siteSearchQueryParams: (Array|unefined), + * queryParamsWhitelist: (Array|unefined), * queryDimensionIndex: (number|undefined), * indexFilename: (string|undefined), * trailingSlash: (string|undefined), diff --git a/lib/plugins/clean-url-tracker.js b/lib/plugins/clean-url-tracker.js index 49262fae..b76e7b2b 100644 --- a/lib/plugins/clean-url-tracker.js +++ b/lib/plugins/clean-url-tracker.js @@ -42,7 +42,7 @@ class CleanUrlTracker { /** @type {CleanUrlTrackerOpts} */ const defaultOpts = { // stripQuery: undefined, - // siteSearchQueryParams: undefined, + // queryParamsWhitelist: undefined, // queryDimensionIndex: undefined, // indexFilename: undefined, // trailingSlash: undefined, @@ -142,7 +142,7 @@ class CleanUrlTracker { /** @type {!FieldsObj} */ const cleanedFieldsObj = { page: pathname + (this.opts.stripQuery ? - this.stripNonSiteSearchParams(url.search) : url.search), + this.stripNonWhitelistedQueryParams(url.search) : url.search), }; if (fieldsObj.location) { cleanedFieldsObj.location = fieldsObj.location; @@ -180,12 +180,12 @@ class CleanUrlTracker { * @param {string} search The URL search string (starting with '?'). * @return {string} The query string */ - stripNonSiteSearchParams(searchString) { - if (Array.isArray(this.opts.siteSearchQueryParams)) { + stripNonWhitelistedQueryParams(searchString) { + if (Array.isArray(this.opts.queryParamsWhitelist)) { const foundParams = []; searchString.slice(1).split('&').forEach((kv) => { const [key, value] = kv.split('='); - if (this.opts.siteSearchQueryParams.indexOf(key) > -1 && value) { + if (this.opts.queryParamsWhitelist.indexOf(key) > -1 && value) { foundParams.push([key, value]); } }); diff --git a/test/e2e/clean-url-tracker-test.js b/test/e2e/clean-url-tracker-test.js index 26cb146f..301f8006 100644 --- a/test/e2e/clean-url-tracker-test.js +++ b/test/e2e/clean-url-tracker-test.js @@ -176,7 +176,7 @@ describe('cleanUrlTracker', function() { function requireCleanUrlTracker_multipleOpts() { ga('require', 'cleanUrlTracker', { stripQuery: true, - siteSearchQueryParams: ['q', 's'], + queryParamsWhitelist: ['q', 's'], queryDimensionIndex: 1, indexFilename: 'index.html', trailingSlash: 'remove', diff --git a/test/unit/plugins/clean-url-tracker-test.js b/test/unit/plugins/clean-url-tracker-test.js index ef182c28..7d7d196f 100644 --- a/test/unit/plugins/clean-url-tracker-test.js +++ b/test/unit/plugins/clean-url-tracker-test.js @@ -66,7 +66,7 @@ describe('CleanUrlTracker', () => { const fn = () => {}; const opts = { stripQuery: true, - siteSearchQueryParams: ['q', 's'], + queryParamsWhitelist: ['q', 's'], queryDimensionIndex: 1, indexFilename: 'index.html', trailingSlash: 'remove', @@ -106,61 +106,62 @@ describe('CleanUrlTracker', () => { }); }); - describe('stripNonSiteSearchParams', () => { - it('returns a URL search string with only site search params', () => { + describe('stripNonWhitelistedQueryParams', () => { + it('returns a URL search string with only whitelisted params', () => { const cut = new CleanUrlTracker(tracker, { - siteSearchQueryParams: ['q', 's'], + queryParamsWhitelist: ['q', 's'], }); assert.strictEqual( - cut.stripNonSiteSearchParams('?gclid=foo'), ''); + cut.stripNonWhitelistedQueryParams('?gclid=foo'), ''); assert.strictEqual( - cut.stripNonSiteSearchParams('?gclid=foo&q=1'), '?q=1'); + cut.stripNonWhitelistedQueryParams('?gclid=foo&q=1'), '?q=1'); assert.strictEqual( - cut.stripNonSiteSearchParams('?q=1&gclid=foo'), '?q=1'); + cut.stripNonWhitelistedQueryParams('?q=1&gclid=foo'), '?q=1'); assert.strictEqual( - cut.stripNonSiteSearchParams('?gclid=foo&q=1&s=2'), + cut.stripNonWhitelistedQueryParams('?gclid=foo&q=1&s=2'), '?q=1&s=2'); assert.strictEqual( - cut.stripNonSiteSearchParams('?q=1&gclid=foo&s=2'), '?q=1&s=2'); + cut.stripNonWhitelistedQueryParams('?q=1&gclid=foo&s=2'), '?q=1&s=2'); assert.strictEqual( - cut.stripNonSiteSearchParams('?q=1&s=2&gclid=foo'), '?q=1&s=2'); + cut.stripNonWhitelistedQueryParams('?q=1&s=2&gclid=foo'), '?q=1&s=2'); cut.remove(); }); it('does not modify URL encoded keys or values', () => { const cut = new CleanUrlTracker(tracker, { - siteSearchQueryParams: ['q', 's'], + queryParamsWhitelist: ['q', 's'], }); assert.strictEqual( - cut.stripNonSiteSearchParams('?gclid=foo&q=1%202'), '?q=1%202'); + cut.stripNonWhitelistedQueryParams('?gclid=foo&q=1%202'), '?q=1%202'); cut.remove(); }); it('works with empty or missing param values', () => { const cut = new CleanUrlTracker(tracker, { - siteSearchQueryParams: ['q', 's'], + queryParamsWhitelist: ['q', 's'], }); assert.strictEqual( - cut.stripNonSiteSearchParams('?q=1&s=2&gclid=foo'), '?q=1&s=2'); + cut.stripNonWhitelistedQueryParams('?q=1&s=2&gclid=foo'), '?q=1&s=2'); assert.strictEqual( - cut.stripNonSiteSearchParams('?q&s=&gclid=foo'), ''); + cut.stripNonWhitelistedQueryParams('?q&s=&gclid=foo'), ''); cut.remove(); }); - it('works when the siteSearchQueryParams option is not set', () => { + it('works when the queryParamsWhitelist option is not set', () => { const cut = new CleanUrlTracker(tracker); - assert.strictEqual(cut.stripNonSiteSearchParams('?utm_source=foo'), ''); assert.strictEqual( - cut.stripNonSiteSearchParams('?utm_source=foo&q=1'), ''); + cut.stripNonWhitelistedQueryParams('?utm_source=foo'), ''); assert.strictEqual( - cut.stripNonSiteSearchParams('?utm_source=foo&q=1&s=2'), ''); + cut.stripNonWhitelistedQueryParams('?utm_source=foo&q=1'), ''); + assert.strictEqual( + cut.stripNonWhitelistedQueryParams('?utm_source=foo&q=1&s=2'), ''); cut.remove(); }); From 432629febb4bba55adc40e1bee230622aea0c763 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Thu, 1 Jun 2017 18:15:20 -0700 Subject: [PATCH 07/71] Fix closure compiler warnings --- lib/externs/clean-url-tracker.js | 2 +- lib/plugins/clean-url-tracker.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/externs/clean-url-tracker.js b/lib/externs/clean-url-tracker.js index 01f5e29e..3392cd1d 100644 --- a/lib/externs/clean-url-tracker.js +++ b/lib/externs/clean-url-tracker.js @@ -2,7 +2,7 @@ * Public options for the CleanUrlTracker. * @typedef {{ * stripQuery: (boolean|undefined), - * queryParamsWhitelist: (Array|unefined), + * queryParamsWhitelist: (Array|undefined), * queryDimensionIndex: (number|undefined), * indexFilename: (string|undefined), * trailingSlash: (string|undefined), diff --git a/lib/plugins/clean-url-tracker.js b/lib/plugins/clean-url-tracker.js index b76e7b2b..fac86637 100644 --- a/lib/plugins/clean-url-tracker.js +++ b/lib/plugins/clean-url-tracker.js @@ -176,8 +176,7 @@ class CleanUrlTracker { /** * Accpets a raw URL search string and returns a new search string containing * only the site search params (if they exist). - * @param {string} searchString A whitelist of URL params. - * @param {string} search The URL search string (starting with '?'). + * @param {string} searchString The URL search string (starting with '?'). * @return {string} The query string */ stripNonWhitelistedQueryParams(searchString) { From a4ac81cea1af62c51b0380cf537b01fb53ddaf56 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Thu, 1 Jun 2017 18:15:49 -0700 Subject: [PATCH 08/71] Add new option documentation --- docs/plugins/clean-url-tracker.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/plugins/clean-url-tracker.md b/docs/plugins/clean-url-tracker.md index 8322805e..3aab98a1 100644 --- a/docs/plugins/clean-url-tracker.md +++ b/docs/plugins/clean-url-tracker.md @@ -65,6 +65,13 @@ The following table outlines all possible configuration options for the `cleanUr Default: false + + queryParamWhitelist + Array + + An array of query params not to strip. This is most commonly used in conjunction with site search, as shown in the queryParamWhitelist example below. + + queryDimensionIndex number @@ -154,6 +161,26 @@ 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. From 5e13e6541038c1ed1ed07003918b8e3539e6c51d Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Thu, 1 Jun 2017 18:36:46 -0700 Subject: [PATCH 09/71] Remove missed compat check --- test/unit/plugins/clean-url-tracker-test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/unit/plugins/clean-url-tracker-test.js b/test/unit/plugins/clean-url-tracker-test.js index 7d7d196f..e51b4fff 100644 --- a/test/unit/plugins/clean-url-tracker-test.js +++ b/test/unit/plugins/clean-url-tracker-test.js @@ -56,8 +56,6 @@ describe('CleanUrlTracker', () => { }); it('merges the passed options with the defaults', () => { - if (!document.visibilityState) this.skip(); - let cut = new CleanUrlTracker(tracker); assert.deepEqual(cut.opts, {}); From 3ebc2543310d611f97939669fff38bb9822beb37 Mon Sep 17 00:00:00 2001 From: Brent Clark Date: Thu, 1 Jun 2017 22:36:24 -0500 Subject: [PATCH 10/71] Fixed typo --- docs/plugins/event-tracker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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`. From 55c6d0e678a63bfd1dac0a7ab27dd4047d95bcc8 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Thu, 1 Jun 2017 22:46:12 -0700 Subject: [PATCH 11/71] Fix documentation spelling error --- docs/plugins/clean-url-tracker.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/plugins/clean-url-tracker.md b/docs/plugins/clean-url-tracker.md index 3aab98a1..7cf9655a 100644 --- a/docs/plugins/clean-url-tracker.md +++ b/docs/plugins/clean-url-tracker.md @@ -66,10 +66,10 @@ The following table outlines all possible configuration options for the `cleanUr - queryParamWhitelist + queryParamsWhitelist Array - An array of query params not to strip. This is most commonly used in conjunction with site search, as shown in the queryParamWhitelist example below. + 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. From 7b354a531a63b9cb0d308dd0ba5c44c2778884b7 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Fri, 2 Jun 2017 12:38:35 -0700 Subject: [PATCH 12/71] Release version 2.4.0 --- CHANGELOG.md | 4 ++ autotrack.js | 122 +++++++++++++++++++++++------------------------ autotrack.js.map | 2 +- lib/constants.js | 2 +- package.json | 2 +- 5 files changed, 68 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92b1f4f0..03b26a3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ 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.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] diff --git a/autotrack.js b/autotrack.js index df03e45c..58926d52 100644 --- a/autotrack.js +++ b/autotrack.js @@ -1,62 +1,62 @@ -(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 b>b/4).toString(16):"10000000-1000-4000-8000-100000000000".replace(/[018]/g,wa)}; -function F(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 G={T:1,U:2,V:3,X:4,Y:5,Z:6,$:7,aa:8,ba:9,W:10},H=Object.keys(G).length; -function I(a,b){a.set("\x26_av","2.3.3");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))}};K.prototype.remove=function(){var a=this;Object.keys(this.b).forEach(function(b){a.b[b].j()})};F("eventTracker",K); -function ya(a,b){var c=this;I(a,G.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.i={},this.h={},sa(function(){c.a.elements&&c.observeElements(c.a.elements)}))}f=ya.prototype; -f.observeElements=function(a){var b=this;a=L(this,a);this.items=this.items.concat(a.items);this.i=y({},a.i,this.i);this.h=y({},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(){})}; -f.unobserveElements=function(a){var b=[],c=[];this.items.forEach(function(d){a.some(function(a){a=za(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=L(this,b),e=L(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()}; -f.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 L(a,b){var c=[],d={},e={};b.length&&b.forEach(function(b){b=za(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}}f.M=function(a){for(var b=0,c;c=a[b];b++){for(var d=0,e;e=c.removedNodes[d];d++)M(this,e,this.L);for(d=0;e=c.addedNodes[d];d++)M(this,e,this.K)}}; -function M(a,b,c){1==b.nodeType&&b.id in a.i&&c(b.id);for(var d=0,e;e=b.childNodes[d];d++)M(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(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=C());d&&(e.isExpired=!0);b.a.set(e)}}; -U.prototype.j=function(){w(this.f,"sendHitTask",this.b);this.a.j();delete T[this.f.get("trackingId")]};var Fa=30;function W(a,b){I(a,G.W);window.addEventListener&&(this.b=y({increaseThreshold:20,sessionTimeout:Fa,fieldsObj:{}},b),this.f=a,this.c=Ha(this),this.g=ta(this.g.bind(this),500),this.o=this.o.bind(this),this.a=R(a.get("trackingId"),"plugins/max-scroll-tracker"),this.m=Ga(a,this.b.sessionTimeout,this.b.timeZone),v(a,"set",this.o),Ia(this))} -function Ia(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&&(Ea(this.a),this.a.set({sessionId:b}));if(this.m.isExpired(this.a.get().sessionId))Ea(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",x(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={};(B(c)?c:(e[c]=d,e)).page&&(c=b.c,b.c=Ha(b),b.c!=c&&Ia(b))}}; -function Ha(a){a=t(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);w(this.f,"set",this.o)};F("maxScrollTracker",W);var Ja={};function Ka(a,b){I(a,G.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=[],La(this)))} -function La(a){a.a.definitions.forEach(function(b){if(b.name&&b.dimensionIndex){var c=Na(b);a.b.set("dimension"+b.dimensionIndex,c);Oa(a,b)}})}function Na(a){var b;a.items.forEach(function(a){Pa(a.media).matches&&(b=a)});return b?b.name:"(not set)"} -function Oa(a,b){b.items.forEach(function(c){c=Pa(c.media);var d=ta(function(){var c=Na(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})})}Ka.prototype.remove=function(){for(var a=0,b;b=this.c[a];a++)b.fa.removeListener(b.da)}; -Ka.prototype.changeTemplate=function(a,b){return a+" \x3d\x3e "+b};F("mediaQueryTracker",Ka);function Pa(a){return Ja[a]||(Ja[a]=window.matchMedia(a))}function X(a,b){I(a,G.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()};F("outboundFormTracker",X); -function Y(a,b){var c=this;I(a,G.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=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 Ra(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.g&&b.s();a(c,d)}};f.N=function(a,b){a.time!=b.time&&(b.pageId!=Z||"visible"!=b.state||this.f.isExpired(b.sessionId)||Ta(this,b,{hitTime:a.time}))}; -f.G=function(){"hidden"!=this.g&&this.s()};f.remove=function(){this.c.j();this.f.j();w(this.b,"set",this.v);window.removeEventListener("unload",this.G);document.removeEventListener("visibilitychange",this.s)};F("pageVisibilityTracker",Qa); -function Ua(a,b){I(a,G.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=Ua.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 Va(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){}Va(this)};F("socialWidgetTracker",Ua); -function Wa(a,b){I(a,G.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=Wa.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.0");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=A(b,c),d=z({},this.a.fieldsObj,c);this.f.send(c.hitType||"event",y({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=z({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)}))}f=za.prototype; +f.observeElements=function(a){var b=this;a=M(this,a);this.items=this.items.concat(a.items);this.i=z({},a.i,this.i);this.h=z({},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(){})}; +f.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()}; +f.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}}f.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)} +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(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=D());d&&(e.isExpired=!0);b.a.set(e)}}; +U.prototype.j=function(){x(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=z({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),w(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",y(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={};(C(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);x(this.f,"set",this.o)};G("maxScrollTracker",W);var La={};function Ma(a,b){J(a,H.X);window.matchMedia&&(this.a=z({changeTemplate:this.changeTemplate,changeTimeout:1E3,fieldsObj:{}},b),C(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",y(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=z({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=z({},this.a.fieldsObj,A(b,this.a.attributePrefix));this.b.send("event",y(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=z({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]=q(a,c.a.linkSelector,c.c)}))} +Y.prototype.c=function(a,b){if(this.a.shouldTrackOutboundLink(b,u)){var c=b.getAttribute("href")||b.getAttribute("xlink:href"),d=u(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=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",y(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",y(d,a.a.fieldsObj,a.b,a.a.hitFilter))}f.v=function(a){var b=this;return function(c,d){var e={},e=C(c)?c:(e[c]=d,e);e.page&&e.page!==b.b.get("page")&&"visible"==b.g&&b.s();a(c,d)}};f.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}))}; +f.G=function(){"hidden"!=this.g&&this.s()};f.remove=function(){this.c.j();this.f.j();x(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=z({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=Wa.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 Xa(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",y(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",y(b,this.a.fieldsObj,this.b,this.a.hitFilter,a.target,a))}};f.B=function(a){this.b.send("social",y({transport:"beacon",socialNetwork:"Facebook",socialAction:"like",socialTarget:a},this.a.fieldsObj,this.b,this.a.hitFilter))}; +f.F=function(a){this.b.send("social",y({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){}Xa(this)};G("socialWidgetTracker",Wa); +function Ya(a,b){J(a,H.ba);history.pushState&&window.addEventListener&&(this.a=z({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),w(history,"pushState",this.H),w(history,"replaceState",this.I),window.addEventListener("popstate",this.C))}f=Ya.prototype; +f.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.3';\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, 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 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 = 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 +{"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","defaultPrevented","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.0';\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 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 = 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/lib/constants.js b/lib/constants.js index 820a8066..8273cae7 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -15,7 +15,7 @@ */ -export const VERSION = '2.3.3'; +export const VERSION = '2.4.0'; export const DEV_ID = 'i5iSjo'; export const VERSION_PARAM = '_av'; diff --git a/package.json b/package.json index f491bb94..1ca8acb8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "autotrack", - "version": "2.3.3", + "version": "2.4.0", "description": "Automatic and enhanced Google Analytics tracking for common user interactions on the web", "main": "lib", "bin": "./bin/autotrack", From 7e29fb98650edbf0e5d3a7862dd67051c266b089 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Wed, 7 Jun 2017 14:38:43 -0700 Subject: [PATCH 13/71] Fix outboundLinkTracker bugs in Safari --- lib/plugins/outbound-link-tracker.js | 30 +++++++++++++------- test/e2e/fixtures/outbound-form-tracker.html | 15 ++++++---- test/e2e/fixtures/outbound-link-tracker.html | 14 ++++----- test/e2e/ga.js | 2 +- test/e2e/outbound-form-tracker-test.js | 12 ++++---- test/e2e/outbound-link-tracker-test.js | 20 ++++++------- test/e2e/wdio.conf.js | 10 +++---- 7 files changed, 57 insertions(+), 46 deletions(-) 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/test/e2e/fixtures/outbound-form-tracker.html b/test/e2e/fixtures/outbound-form-tracker.html index 88b9a011..5f2c4497 100644 --- a/test/e2e/fixtures/outbound-form-tracker.html +++ b/test/e2e/fixtures/outbound-form-tracker.html @@ -9,29 +9,32 @@ -
+
+ action="https://example.com/?q=outbound-submit-with-class">
-
+
@@ -44,7 +47,7 @@ if (shadowHost.attachShadow) { shadowHost.attachShadow({mode: 'open'}); shadowHost.shadowRoot.innerHTML = - '' + + '' + ' ' + '
'; } diff --git a/test/e2e/fixtures/outbound-link-tracker.html b/test/e2e/fixtures/outbound-link-tracker.html index 6c025ab2..c6722f35 100644 --- a/test/e2e/fixtures/outbound-link-tracker.html +++ b/test/e2e/fixtures/outbound-link-tracker.html @@ -11,14 +11,14 @@ + href="https://example.com/?q=outbound-link"> Outbound link + href="https://example.com/?q=outbound-link-with-class"> Outbound link with Class @@ -38,7 +38,7 @@ Declarative Attributes @@ -46,7 +46,7 @@ Declarative Attributes @@ -61,7 +61,7 @@ viewBox="0 0 100 20" width="100" height="20"> - + SVG Link @@ -77,7 +77,7 @@ id="area-link" shape="rect" coords="0,0,20,20" - href="https://example.com/area-link"> + href="https://example.com/?q=area-link"> @@ -86,7 +86,7 @@ if (shadowHost.attachShadow) { shadowHost.attachShadow({mode: 'open'}); shadowHost.shadowRoot.innerHTML = - 'Shadow Link'; + 'Shadow Link'; } diff --git a/test/e2e/ga.js b/test/e2e/ga.js index fd961a38..8bf25574 100644 --- a/test/e2e/ga.js +++ b/test/e2e/ga.js @@ -54,7 +54,7 @@ export function logHitData(testId) { oldSendHitTask(model); - if ('sendBeacon' in navigator) { + if (typeof navigator.sendBeacon == 'function') { navigator.sendBeacon(`/collect/${testId}`, hitPayload); } else { const beacon = new Image(); diff --git a/test/e2e/outbound-form-tracker-test.js b/test/e2e/outbound-form-tracker-test.js index ea278ef7..d6a42cf3 100644 --- a/test/e2e/outbound-form-tracker-test.js +++ b/test/e2e/outbound-form-tracker-test.js @@ -61,7 +61,7 @@ describe('outboundFormTracker', function() { const hits = log.getHits(); assert.strictEqual(hits[0].ec, 'Outbound Form'); assert.strictEqual(hits[0].ea, 'submit'); - assert.strictEqual(hits[0].el, 'https://example.com/outbound-submit'); + assert.strictEqual(hits[0].el, 'https://example.com/?q=outbound-submit'); }); it('does not send events on local form submits', () => { @@ -74,7 +74,7 @@ describe('outboundFormTracker', function() { it('navigates to the proper outbound location on submit', () => { browser.execute(ga.run, 'require', 'outboundFormTracker'); browser.click('#outbound-submit'); - browser.waitUntil(urlMatches('https://example.com/outbound-submit')); + browser.waitUntil(urlMatches('https://example.com/?q=outbound-submit')); }); it('navigates to the proper local location on submit', () => { @@ -108,7 +108,7 @@ describe('outboundFormTracker', function() { assert.strictEqual(hits[0].ec, 'Outbound Form'); assert.strictEqual(hits[0].ea, 'submit'); assert.strictEqual( - hits[0].el, 'https://example.com/outbound-submit-with-class'); + hits[0].el, 'https://example.com/?q=outbound-submit-with-class'); }); it('supports customizing what is considered an outbound form', () => { @@ -136,7 +136,7 @@ describe('outboundFormTracker', function() { const hits = log.getHits(); assert.strictEqual(hits[0].ec, 'External Form'); assert.strictEqual(hits[0].ea, 'send'); - assert.strictEqual(hits[0].el, 'https://example.com/outbound-submit'); + assert.strictEqual(hits[0].el, 'https://example.com/?q=outbound-submit'); assert.strictEqual(hits[0].ni, '1'); }); @@ -186,7 +186,7 @@ describe('outboundFormTracker', function() { const hits = log.getHits(); assert.strictEqual(hits[0].ec, 'Outbound Form'); assert.strictEqual(hits[0].ea, 'submit'); - assert.strictEqual(hits[0].el, 'https://example.com/shadow-host'); + assert.strictEqual(hits[0].el, 'https://example.com/?q=shadow-host'); }); it('includes usage params with all hits', () => { @@ -261,7 +261,7 @@ function requireOutboundFormTracker_shouldTrackOutboundForm() { function requireOutboundFormTracker_hitFilter() { ga('require', 'outboundFormTracker', { hitFilter: (model, form, event) => { - if (form.action == 'https://example.com/outbound-submit') { + if (form.action == 'https://example.com/?q=outbound-submit') { model.set('eventLabel', '/outbound-submit', true); } model.set('dimension1', event.type); diff --git a/test/e2e/outbound-link-tracker-test.js b/test/e2e/outbound-link-tracker-test.js index fe3ed1fd..916f69b9 100644 --- a/test/e2e/outbound-link-tracker-test.js +++ b/test/e2e/outbound-link-tracker-test.js @@ -61,7 +61,7 @@ describe('outboundLinkTracker', function() { const hits = log.getHits(); assert.strictEqual(hits[0].ec, 'Outbound Link'); assert.strictEqual(hits[0].ea, 'click'); - assert.strictEqual(hits[0].el, 'https://example.com/outbound-link'); + assert.strictEqual(hits[0].el, 'https://example.com/?q=outbound-link'); }); it('does not send events on local link clicks', () => { @@ -82,7 +82,7 @@ describe('outboundLinkTracker', function() { it('navigates to the proper location on outbound clicks', () => { browser.execute(ga.run, 'require', 'outboundLinkTracker'); browser.click('#outbound-link'); - browser.waitUntil(urlMatches('https://example.com/outbound-link')); + browser.waitUntil(urlMatches('https://example.com/?q=outbound-link')); }); it('navigates to the proper location on local clicks', () => { @@ -99,7 +99,7 @@ describe('outboundLinkTracker', function() { const hits = log.getHits(); assert.strictEqual(hits[0].ec, 'Outbound Link'); assert.strictEqual(hits[0].ea, 'click'); - assert.strictEqual(hits[0].el, 'https://example.com/svg-link'); + assert.strictEqual(hits[0].el, 'https://example.com/?q=svg-link'); }); it('works with links', function() { @@ -112,7 +112,7 @@ describe('outboundLinkTracker', function() { const hits = log.getHits(); assert.strictEqual(hits[0].ec, 'Outbound Link'); assert.strictEqual(hits[0].ea, 'click'); - assert.strictEqual(hits[0].el, 'https://example.com/area-link'); + assert.strictEqual(hits[0].el, 'https://example.com/?q=area-link'); }); it('supports events other than click', () => { @@ -133,11 +133,11 @@ describe('outboundLinkTracker', function() { const hits = log.getHits(); assert.strictEqual(hits[0].ec, 'Outbound Link'); assert.strictEqual(hits[0].ea, 'mousedown'); - assert.strictEqual(hits[0].el, 'https://example.com/outbound-link'); + assert.strictEqual(hits[0].el, 'https://example.com/?q=outbound-link'); if (browserSupportsRightClick()) { assert.strictEqual(hits[1].ec, 'Outbound Link'); assert.strictEqual(hits[1].ea, 'contextmenu'); - assert.strictEqual(hits[1].el, 'https://example.com/outbound-link'); + assert.strictEqual(hits[1].el, 'https://example.com/?q=outbound-link'); } }); @@ -164,7 +164,7 @@ describe('outboundLinkTracker', function() { assert.strictEqual(hits[0].ec, 'Outbound Link'); assert.strictEqual(hits[0].ea, 'click'); assert.strictEqual(hits[0].el, - 'https://example.com/outbound-link-with-class'); + 'https://example.com/?q=outbound-link-with-class'); }); it('supports customizing what is considered an outbound link', () => { @@ -192,7 +192,7 @@ describe('outboundLinkTracker', function() { const hits = log.getHits(); assert.strictEqual(hits[0].ec, 'External Link'); assert.strictEqual(hits[0].ea, 'tap'); - assert.strictEqual(hits[0].el, 'https://example.com/outbound-link'); + assert.strictEqual(hits[0].el, 'https://example.com/?q=outbound-link'); assert.strictEqual(hits[0].ni, '1'); }); @@ -242,7 +242,7 @@ describe('outboundLinkTracker', function() { const hits = log.getHits(); assert.strictEqual(hits[0].ec, 'Outbound Link'); assert.strictEqual(hits[0].ea, 'click'); - assert.strictEqual(hits[0].el, 'https://example.com/shadow-host'); + assert.strictEqual(hits[0].el, 'https://example.com/?q=shadow-host'); }); it('includes usage params with all hits', () => { @@ -343,7 +343,7 @@ function requireOutboundLinkTracker_shouldTrackOutboundLink() { function requireOutboundLinkTracker_hitFilter() { ga('require', 'outboundLinkTracker', { hitFilter: (model, link, event) => { - if (link.href == 'https://example.com/outbound-link') { + if (link.href == 'https://example.com/?q=outbound-link') { model.set('eventLabel', '/outbound-link', true); } model.set('dimension1', event.type, true); diff --git a/test/e2e/wdio.conf.js b/test/e2e/wdio.conf.js index a8839c07..42463e65 100644 --- a/test/e2e/wdio.conf.js +++ b/test/e2e/wdio.conf.js @@ -51,11 +51,11 @@ const getCapabilities = () => { // platform: 'OS X 10.11', // version: 'latest', // }, - // { - // browserName: 'safari', - // platform: 'OS X 10.12', - // version: '10.0', - // }, + { + browserName: 'safari', + platform: 'OS X 10.12', + version: '10.0', + }, // { // browserName: 'safari', // platform: 'OS X 10.11', From eda5cdcb340c508f48a323a22b1c971326afefa9 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Wed, 7 Jun 2017 15:27:14 -0700 Subject: [PATCH 14/71] Release version 2.4.1 --- CHANGELOG.md | 4 ++ README.md | 2 +- autotrack.js | 95 ++++++++++++++++++++++++------------------------ autotrack.js.map | 2 +- lib/constants.js | 2 +- package.json | 2 +- 6 files changed, 56 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03b26a3f..b8737543 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ 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] diff --git a/README.md b/README.md index b509ecb4..04cc6f24 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,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: diff --git a/autotrack.js b/autotrack.js index 58926d52..b86811c4 100644 --- a/autotrack.js +++ b/autotrack.js @@ -1,62 +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 l(){l=function(){};k.Symbol||(k.Symbol=ba)}var ca=0;function ba(a){return"jscomp_symbol_"+(a||"")+ca++} +(function(){var g,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 l(){l=function(){};k.Symbol||(k.Symbol=ba)}var ca=0;function ba(a){return"jscomp_symbol_"+(a||"")+ca++} function m(){l();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 da(this)}});m=function(){}}function da(a){var b=0;return ea(function(){return b>b/4).toString(16):"10000000-1000-4000-8000-100000000000".replace(/[018]/g,wa)}; +function u(a){a=a&&"."!=a?a:location.href;if(t[a])return t[a];r.href=a;if("."==a.charAt(0)||"/"==a.charAt(0))return u(r.href);var b="80"==r.port||"443"==r.port?"":r.port,b="0"==b?"":b,c=r.host.replace(na,"");return t[a]={hash:r.hash,host:c,hostname:r.hostname,href:r.href,origin:r.origin?r.origin:r.protocol+"//"+c,pathname:"/"==r.pathname.charAt(0)?r.pathname:"/"+r.pathname,port:b,protocol:r.protocol,search:r.search}}var w=[]; +function oa(a,b){var c=this;this.context=a;this.P=b;this.f=(this.c=/Task$/.test(b))?a.get(b):a[b];this.b=[];this.a=[];this.g=function(a){for(var b=[],d=0;d>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.0");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=A(b,c),d=z({},this.a.fieldsObj,c);this.f.send(c.hitType||"event",y({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=z({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)}))}f=za.prototype; -f.observeElements=function(a){var b=this;a=M(this,a);this.items=this.items.concat(a.items);this.i=z({},a.i,this.i);this.h=z({},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(){})}; -f.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()}; -f.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}}f.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)}}; +location:b.location},a.b&&(c[a.b]=b[a.b]),c):d}function ya(a,b){if(Array.isArray(a.a.queryParamsWhitelist)){var c=[];b.slice(1).split("\x26").forEach(function(b){var d=fa(b.split("\x3d"));b=d.next().value;d=d.next().value;-1b.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)} -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=0=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=D());d&&(e.isExpired=!0);b.a.set(e)}}; -U.prototype.j=function(){x(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=z({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),w(a,"set",this.o),Ka(this))} +function Ea(){if(null!=Q)return Q;try{window.localStorage.setItem("autotrack","autotrack"),window.localStorage.removeItem("autotrack"),Q=!0}catch(a){Q=!1}return Q}R.prototype.get=function(){if(this.l)return this.l;if(Ea())try{this.l=Fa(window.localStorage.getItem(this.b))}catch(a){}return this.l=A({},this.w,this.l)};R.prototype.set=function(a){this.l=A({},this.w,this.l,a);if(Ea())try{var b=JSON.stringify(this.l);window.localStorage.setItem(this.b,b)}catch(c){}}; +function Ga(a){a.l={};if(Ea())try{window.localStorage.removeItem(a.b)}catch(b){}}R.prototype.j=function(){delete O[this.b];Object.keys(O).length||(window.removeEventListener("storage",Da),P=!1)};function Da(a){var b=O[a.key];if(b){var c=A({},b.w,Fa(a.oldValue));a=A({},b.w,Fa(a.newValue));b.l=a;b.ca("externalSet",a,c)}}function Fa(a){var b={};if(a)try{b=JSON.parse(a)}catch(c){}return b}var T={}; +function U(a,b,c){this.f=a;this.timeout=b||Ha;this.timeZone=c;this.b=this.b.bind(this);x(a,"sendHitTask",this.b);try{this.c=new Intl.DateTimeFormat("en-US",{timeZone:this.timeZone})}catch(d){}this.a=S(a.get("trackingId"),"session",{hitTime:0,isExpired:!1});this.a.get().id||this.a.set({id:E()})}function Ia(a,b,c){var d=a.get("trackingId");return T[d]?T[d]:T[d]=new U(a,b,c)}function V(a){return a.a.get().id} +U.prototype.isExpired=function(a){a=void 0===a?V(this):a;if(a!=V(this))return!0;a=this.a.get();if(a.isExpired)return!0;var b=a.hitTime;return b&&(a=new Date,b=new Date(b),a-b>6E4*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",y(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={};(C(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);x(this.f,"set",this.o)};G("maxScrollTracker",W);var La={};function Ma(a,b){J(a,H.X);window.matchMedia&&(this.a=z({changeTemplate:this.changeTemplate,changeTimeout:1E3,fieldsObj:{}},b),C(this.a.definitions)&&(b=this.a.definitions,this.a.definitions=Array.isArray(b)?b:[b],this.b=a,this.c=[],Oa(this)))} +{};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",y(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=z({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=z({},this.a.fieldsObj,A(b,this.a.attributePrefix));this.b.send("event",y(c,d,this.b,this.a.hitFilter,b,a))}}; +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=z({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]=q(a,c.a.linkSelector,c.c)}))} -Y.prototype.c=function(a,b){if(this.a.shouldTrackOutboundLink(b,u)){var c=b.getAttribute("href")||b.getAttribute("xlink:href"),d=u(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=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",y(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",y(d,a.a.fieldsObj,a.b,a.a.hitFilter))}f.v=function(a){var b=this;return function(c,d){var e={},e=C(c)?c:(e[c]=d,e);e.page&&e.page!==b.b.get("page")&&"visible"==b.g&&b.s();a(c,d)}};f.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}))}; -f.G=function(){"hidden"!=this.g&&this.s()};f.remove=function(){this.c.j();this.f.j();x(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=z({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=Wa.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 Xa(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",y(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",y(b,this.a.fieldsObj,this.b,this.a.hitFilter,a.target,a))}};f.B=function(a){this.b.send("social",y({transport:"beacon",socialNetwork:"Facebook",socialAction:"like",socialTarget:a},this.a.fieldsObj,this.b,this.a.hitFilter))}; -f.F=function(a){this.b.send("social",y({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){}Xa(this)};G("socialWidgetTracker",Wa); -function Ya(a,b){J(a,H.ba);history.pushState&&window.addEventListener&&(this.a=z({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),w(history,"pushState",this.H),w(history,"replaceState",this.I),window.addEventListener("popstate",this.C))}f=Ya.prototype; -f.H=function(a){var b=this;return function(c){for(var d=[],e=0;e=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.4.0';\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 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 = 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 +{"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/lib/constants.js b/lib/constants.js index 8273cae7..b7149d34 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -15,7 +15,7 @@ */ -export const VERSION = '2.4.0'; +export const VERSION = '2.4.1'; export const DEV_ID = 'i5iSjo'; export const VERSION_PARAM = '_av'; diff --git a/package.json b/package.json index 1ca8acb8..3e86bd38 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "autotrack", - "version": "2.4.0", + "version": "2.4.1", "description": "Automatic and enhanced Google Analytics tracking for common user interactions on the web", "main": "lib", "bin": "./bin/autotrack", From f53366e1a78ecf262fa60f15037748fe3f313b90 Mon Sep 17 00:00:00 2001 From: Young Chen Date: Tue, 14 Nov 2017 14:53:25 +0800 Subject: [PATCH 15/71] Update clean-url-tracker.md fixed typo --- docs/plugins/clean-url-tracker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/clean-url-tracker.md b/docs/plugins/clean-url-tracker.md index 7cf9655a..617e4453 100644 --- a/docs/plugins/clean-url-tracker.md +++ b/docs/plugins/clean-url-tracker.md @@ -183,7 +183,7 @@ These options combined will allow you to keep all unwanted query params out of y ### 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/`: From 647b2ae78fa7bda67e25c6e5f4fa6181b91ecc7f Mon Sep 17 00:00:00 2001 From: Kevin Jalbert Date: Mon, 27 Nov 2017 20:49:34 -0500 Subject: [PATCH 16/71] Fix MaxScrollTracker interactive events docs The "Making scroll events interactive beyond 50%" documentation was incorrect as it was setting the "nonInteraction" property to "true", which in turn does not make the next events interactive. Fix the value, and also corrected the comment. --- docs/plugins/max-scroll-tracker.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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); } }, }); From 5c5bf4015089a8440f974e28cd7834a4c5df57e4 Mon Sep 17 00:00:00 2001 From: Philipp Scheit Date: Thu, 4 Jan 2018 10:59:32 +0100 Subject: [PATCH 17/71] Missing trailing - in attribute prefix without this - i get an error from google-analytics that says "EventCategory" is not a valid set property (notice the big E instead of e) --- docs/common-options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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-' }); ``` From 19610cb8f13ad24fc3c89d4cdd1dfae690a22c2e Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Wed, 7 Feb 2018 23:41:21 -0800 Subject: [PATCH 18/71] Fix lint errors from updating google style --- lib/plugins/social-widget-tracker.js | 8 ++++---- lib/session.js | 2 +- lib/store.js | 8 ++++---- lib/utilities.js | 4 ++-- test/e2e/max-scroll-tracker-test.js | 2 +- test/e2e/page-visibility-tracker-test.js | 2 +- test/e2e/server.js | 2 +- test/unit/session-test.js | 2 +- test/unit/store-test.js | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/plugins/social-widget-tracker.js b/lib/plugins/social-widget-tracker.js index 1bd6c92c..a8a002af 100644 --- a/lib/plugins/social-widget-tracker.js +++ b/lib/plugins/social-widget-tracker.js @@ -89,7 +89,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 +104,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 +117,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 +130,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. } } diff --git a/lib/session.js b/lib/session.js index 44ade8a4..fb44f10a 100644 --- a/lib/session.js +++ b/lib/session.js @@ -85,7 +85,7 @@ export default class Session { try { this.dateTimeFormatter = new Intl.DateTimeFormat('en-US', {timeZone: this.timeZone}); - } catch(err) { + } catch (err) { // Do nothing. } diff --git a/lib/store.js b/lib/store.js index 3dec04b2..b9eadd8f 100644 --- a/lib/store.js +++ b/lib/store.js @@ -130,7 +130,7 @@ export default class Store extends EventEmitter { if (Store.isSupported_()) { try { this.cache_ = parse(Store.get_(this.key_)); - } catch(err) { + } catch (err) { // Do nothing. } } @@ -149,7 +149,7 @@ export default class Store extends EventEmitter { if (Store.isSupported_()) { try { Store.set_(this.key_, JSON.stringify(this.cache_)); - } catch(err) { + } catch (err) { // Do nothing. } } @@ -163,7 +163,7 @@ export default class Store extends EventEmitter { if (Store.isSupported_()) { try { Store.clear_(this.key_); - } catch(err) { + } catch (err) { // Do nothing. } } @@ -229,7 +229,7 @@ function parse(source) { if (source) { try { data = /** @type {!Object} */ (JSON.parse(source)); - } catch(err) { + } catch (err) { // Do nothing. } } diff --git a/lib/utilities.js b/lib/utilities.js index d1d39022..19f04945 100644 --- a/lib/utilities.js +++ b/lib/utilities.js @@ -212,7 +212,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 +261,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/test/e2e/max-scroll-tracker-test.js b/test/e2e/max-scroll-tracker-test.js index 296651ab..69d1ce9f 100644 --- a/test/e2e/max-scroll-tracker-test.js +++ b/test/e2e/max-scroll-tracker-test.js @@ -389,7 +389,7 @@ function setStoreData(key, value) { window.localStorage.setItem(key, newValue); window.dispatchEvent( new StorageEvent('storage', {key, oldValue, newValue})); - } catch(err) { + } catch (err) { // Do nothing } }, key, value); diff --git a/test/e2e/page-visibility-tracker-test.js b/test/e2e/page-visibility-tracker-test.js index 28db68cc..b41be91f 100644 --- a/test/e2e/page-visibility-tracker-test.js +++ b/test/e2e/page-visibility-tracker-test.js @@ -1032,7 +1032,7 @@ function setStoreData(key, value) { window.localStorage.setItem(key, newValue); window.dispatchEvent( new StorageEvent('storage', {key, oldValue, newValue})); - } catch(err) { + } catch (err) { // Do nothing } }, key, value); diff --git a/test/e2e/server.js b/test/e2e/server.js index d66a3f9d..186e46ac 100644 --- a/test/e2e/server.js +++ b/test/e2e/server.js @@ -86,7 +86,7 @@ export function getHitLogs(testId) { let contents; try { contents = fs.readFileSync(logFile, 'utf-8'); - } catch(e) { + } catch (e) { process.stderr.write(e + '\n'); } return contents.trim().split('\n') diff --git a/test/unit/session-test.js b/test/unit/session-test.js index f58d4ff1..0ae00897 100644 --- a/test/unit/session-test.js +++ b/test/unit/session-test.js @@ -133,7 +133,7 @@ describe('Session', () => { new Intl.DateTimeFormat('en-US', { timeZone: 'America/Los_Angeles', }).format(new Date()); - } catch(err) { + } catch (err) { // Skip this test in browsers that don't support time zones. return this.skip(); } diff --git a/test/unit/store-test.js b/test/unit/store-test.js index 39dadf2c..e91d3c77 100644 --- a/test/unit/store-test.js +++ b/test/unit/store-test.js @@ -182,7 +182,7 @@ describe('Store', () => { // Feature detect event constructor support, skip otherwise. try { new StorageEvent('storage', {}); - } catch(err) { + } catch (err) { this.skip(); } From 14d90daa8716f3df4ec7ed63fc3f0a7c9d639f9b Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Sun, 5 Aug 2018 18:39:06 -0700 Subject: [PATCH 19/71] Add an initial IdleQueue implementation --- lib/idle-queue.js | 190 +++++++++++++++++ lib/utilities.js | 66 ++++++ test/unit/idle-queue-test.js | 387 +++++++++++++++++++++++++++++++++++ 3 files changed, 643 insertions(+) create mode 100644 lib/idle-queue.js create mode 100644 test/unit/idle-queue-test.js diff --git a/lib/idle-queue.js b/lib/idle-queue.js new file mode 100644 index 00000000..867cedd4 --- /dev/null +++ b/lib/idle-queue.js @@ -0,0 +1,190 @@ +/** + * 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 MethodChain from './method-chain'; +import {cIC, isSafari, queueMicrotask, rIC, uid} from './utilities'; + +const instances = {}; + +/** + * A class wraps a queue of requestIdleCallback functions for two reasons: + * 1. So other callers can know whether or not the queue is empty. + *. 2. So we can provide some guarantees that the queued functions will + *. run in unload-type situations. + */ +export default class IdleQueue { + /** + * Gets an existing instance for the passed arguments or creates a new + * instance if one doesn't exist. + * @param {!Tracker} tracker An analytics.js tracker object. + * @return {Session} The Session instance. + */ + static getOrCreate(tracker) { + // Don't create multiple instances for the same property. + const trackingId = tracker.get('trackingId'); + if (!instances[trackingId]) { + instances[trackingId] = new IdleQueue(tracker); + } + return instances[trackingId]; + } + + /** + * @param {!Tracker} tracker An analytics.js tracker object. + */ + constructor(tracker) { + this.tracker_ = tracker; + this.idleCallbacks_ = {}; + + this.pendingIdleCallbacks = 0; + this.beforeSendCallbacks_ = []; + + // Bind methods + this.onVisibilityChange_ = this.onVisibilityChange_.bind(this); + this.trackerSendOverride_ = this.trackerSendOverride_.bind(this); + + MethodChain.add(this.tracker_, 'send', this.trackerSendOverride_); + addEventListener('visibilitychange', this.onVisibilityChange_, 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.runCallbacksImmediately_, true); + } + } + + /** + * @param {!Function} callback + */ + addCallback(callback) { + const id = uid(); + const state = { + time: Date.now(), + visibilityState: document.visibilityState, + }; + const entry = this.idleCallbacks_[id] = {callback, state}; + const wrappedCallback = () => { + callback(state); + this.onIdleCalbackRun_(id); + }; + + ++this.pendingIdleCallbacks; + + if (document.visibilityState === 'hidden') { + queueMicrotask(wrappedCallback); + } else { + // Schedule the callback in the idle queue and store its handle (so it + // can be cancelled later if needed). + entry.handle = rIC(wrappedCallback); + } + } + + /** + * Destroys the instance by unregistering all added event listeners and + * removing any overridden methods. + */ + destroy() { + MethodChain.remove(this.tracker_, 'send', this.trackerSendOverride_); + removeEventListener('visibilitychange', this.onVisibilityChange_, 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()) { + removeEventListener('beforeunload', this.runCallbacksImmediately_, true); + } + } + + /** + * Runs all added callbacks as microtasks. + */ + runCallbacksImmediately_() { + if (this.pendingIdleCallbacks > 0) { + Object.keys(this.idleCallbacks_).forEach((id) => { + const {handle, callback, state} = this.idleCallbacks_[id]; + + if (handle) { + cIC(handle); + } + + // console.log('running as microtask', id, callback.toString()); + queueMicrotask(() => callback(state)); + }); + this.idleCallbacks_ = {}; + } + } + + /** + * Deletes a callback from the stored set and decremets the pending count. + * @param {number} id + */ + onIdleCalbackRun_(id) { + delete this.idleCallbacks_[id]; + --this.pendingIdleCallbacks; + + if (this.pendingIdleCallbacks === 0 && + this.trackerSendOverride_ !== null) { + this.removeTrackerSendOverride_(); + } + } + + /** + * A callback for the `visibilitychange` event that runs all pending + * callbacks immediately if the document's visibility state is hidden. + */ + onVisibilityChange_() { + if (document.visibilityState === 'hidden') { + this.runCallbacksImmediately_(); + } + } + + /** + * Generates an override for the `tracker.send()` method. + * @param {!Function} originalMethod + * @return {!Function} + */ + trackerSendOverride_(originalMethod) { + return (...args) => { + if (this.pendingIdleCallbacks === 0) { + this.removeTrackerSendOverride_(); + originalMethod(...args); + } else { + this.beforeSendCallbacks_.push(() => { + originalMethod(...args); + }); + } + }; + } + + /** + * Restores the `tracker.send()` override function. + */ + removeTrackerSendOverride_() { + MethodChain.remove(this.tracker_, 'send', this.trackerSendOverride_); + this.trackerSendOverride_ = null; + + this.beforeSendCallbacks_.forEach((callback) => callback()); + this.beforeSendCallbacks_ = []; + } +} diff --git a/lib/utilities.js b/lib/utilities.js index 19f04945..27d254a8 100644 --- a/lib/utilities.js +++ b/lib/utilities.js @@ -18,6 +18,10 @@ import {getAttributes} from 'dom-utils'; import MethodChain from './method-chain'; +let uid_ = 0; +const isSafari_ = !!(typeof safari === 'object' && safari.pushNotification); +const supportsRequestIdleCallback_ = typeof requestIdleCallback === 'function'; + /** * Accepts default and user override fields and an optional tracker, hit @@ -183,6 +187,31 @@ export function deferUntilPluginsLoaded(tracker, fn) { } } +export const queueMicrotask = (() => { + if (typeof Promise !== 'undefined') { + return (microtask) => { + Promise.resolve().then(microtask); + }; + } else { + return () => { + let i = 0; + let microtaskQueue = []; + const observer = new MutationObserver(() => { + microtaskQueue.forEach((microtask) => microtask()); + microtaskQueue = []; + }); + const node = document.createTextNode(''); + observer.observe(node, {characterData: true}); + + return (microtask) => { + microtaskQueue.push(microtask); + + // Trigger a mutation observer callback, which is a microtask. + node.data = ++i % 2; + }; + }; + } +})(); /** * A small shim of Object.assign that aims for brevity over spec-compliant @@ -257,6 +286,43 @@ export function now() { } +/** + * Returns a unique number for this page. + * @return {number} + */ +export function uid() { + return ++uid_; +} + + +/** + * Returns whether or not the current browser is Safari. + * @return {boolean} + */ +export function isSafari() { + return isSafari_; +} + +/** + * The native `requestIdleCallback()` function or `setTimeout()` if the + * browser doesn't support it. + * @param {!Function} callback + * @return {number} + */ +export const rIC = (callback) => supportsRequestIdleCallback_ ? + requestIdleCallback(callback) : setTimeout(callback, 0); + + +/** + * The native `cancelIdleCallback()` function or `clearTimeout()` if the + * browser doesn't support it. + * @param {number} handle + * @return {undefined} + */ +export const cIC = (handle) => supportsRequestIdleCallback_ ? + cancelIdleCallback(handle) : clearTimeout(handle); + + /*eslint-disable */ // https://gist.github.com/jed/982883 /** @param {?=} a */ diff --git a/test/unit/idle-queue-test.js b/test/unit/idle-queue-test.js new file mode 100644 index 00000000..e83847f8 --- /dev/null +++ b/test/unit/idle-queue-test.js @@ -0,0 +1,387 @@ +/** + * Copyright 2017 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 {dispatch} from 'dom-utils'; +import IdleQueue from '../../lib/idle-queue'; +import {isSafari, queueMicrotask, rIC} from '../../lib/utilities'; + +/* + * A wrapper around `sinon.stub()` for properties that supports non-existent + * own properties (sinon doesn't). + */ +const stubProperty = (obj, prop, value) => { + if (!obj.hasOwnProperty(prop)) { + return { + value: (value) => { + Object.defineProperty(obj, prop, {value, configurable: true}); + }, + }; + } else { + return sandbox.stub(obj, prop); + } +}; + + +const getFields = (overrides = {}) => { + return Object.assign({}, { + trackingId: 'UA-12345-1', + cookieDomain: 'auto', + siteSpeedSampleRate: 0, + }, overrides); +}; + +const sandbox = sinon.createSandbox(); +let tracker; +let hits; + +describe('IdleQueue', () => { + beforeEach((done) => { + sandbox.restore(); + + hits = []; + window.ga('create', getFields()); + window.ga((t) => { + tracker = t; + const originalSendHitTask = tracker.get('sendHitTask'); + tracker.set('sendHitTask', (model) => { + const query = {}; + const hitPayload = model.get('hitPayload'); + hitPayload.split('&').forEach((entry) => { + const [key, value] = entry.split('='); + query[decodeURIComponent(key)] = decodeURIComponent(value); + }); + + hits.push(query); + originalSendHitTask(model); + }); + + done(); + }); + }); + + afterEach(() => { + sandbox.restore(); + window.ga('remove'); + }); + + describe('constructor', () => { + it('adds a lifecycle event listeners to window', () => { + sandbox.spy(window, 'addEventListener'); + + const queue = new IdleQueue(tracker); + + if (isSafari()) { + assert(window.addEventListener.calledTwice); + assert(window.addEventListener.calledWith( + 'beforeunload', sinon.match.func, true)); + } else { + assert(window.addEventListener.calledOnce); + } + assert(window.addEventListener.calledWith( + 'visibilitychange', sinon.match.func, true)); + + queue.destroy(); + }); + + it('overrides and defers the tracker.send method', (done) => { + const originalSendMethod = tracker.send; + const queue = new IdleQueue(tracker); + + // Creating the queue should have overridden the `send()` method. + assert.notEqual(originalSendMethod, tracker.send); + + queue.addCallback(() => { + tracker.set('dimension1', 'A'); + tracker.set('dimension2', 'A'); + }); + queue.addCallback(() => { + tracker.set('dimension2', 'B'); + tracker.set('dimension3', 'B'); + }); + // This normally runs sync, but when creating the idle queue it + // automatically queues the first `send()` call to allow plugin creation + // logic to finish before any hits are sent. + tracker.send('pageview'); + tracker.send('pageview'); + + rIC(() => { + // The queue dimension set should be found on this hit + assert.strictEqual(hits.length, 2); + assert.strictEqual(hits[0].cd1, 'A'); + assert.strictEqual(hits[0].cd2, 'B'); + assert.strictEqual(hits[0].cd3, 'B'); + assert.strictEqual(hits[1].cd1, 'A'); + assert.strictEqual(hits[1].cd2, 'B'); + assert.strictEqual(hits[1].cd3, 'B'); + + // The send method should now be restored. + assert.strictEqual(originalSendMethod, tracker.send); + + tracker.set('dimension3', 'C'); + tracker.send('pageview'); + + // Since the `send()` command has been restored, it should run sync. + assert.strictEqual(hits.length, 3); + assert.strictEqual(hits[2].cd1, 'A'); + assert.strictEqual(hits[2].cd2, 'B'); + assert.strictEqual(hits[2].cd3, 'C'); + + queue.destroy(); + done(); + }); + }); + + it('immediately restores the send method if the queue is empty', () => { + const originalSendMethod = tracker.send; + const queue = new IdleQueue(tracker); + + // Creating the queue should have overridden the `send()` method. + assert.notEqual(originalSendMethod, tracker.send); + + tracker.set('dimension1', 'A'); + + // Since there's nothing in the idle queue, this should happen sync + // and the overridden method should be immediately restored. + tracker.send('pageview'); + + assert.strictEqual(originalSendMethod, tracker.send); + assert.strictEqual(hits.length, 1); + assert.strictEqual(hits[0].cd1, 'A'); + + queue.destroy(); + }); + }); + + describe('addCallback', () => { + it('queues a task to run (when idle if supported)', (done) => { + stubProperty(document, 'visibilityState').value('visible'); + + const spy = sandbox.spy(); + const queue = new IdleQueue(tracker); + + queue.addCallback(spy); + + assert(spy.notCalled); + rIC(() => { + assert(spy.calledOnce); + + queue.destroy(); + done(); + }); + }); + + it('runs the callback as a microtask when in the hidden state', (done) => { + stubProperty(document, 'visibilityState').value('hidden'); + + const spy = sandbox.spy(); + const queue = new IdleQueue(tracker); + + queue.addCallback(spy); + + queueMicrotask(() => { + assert(spy.calledOnce); + + queue.destroy(); + done(); + }); + }); + + it('runs tasks in order', async () => { + const testQueueOrder = (visibilityState) => { + return new Promise((resolve) => { + stubProperty(document, 'visibilityState').value(visibilityState); + + const spy1 = sandbox.spy(); + const spy2 = sandbox.spy(); + const spy3 = sandbox.spy(); + const queue = new IdleQueue(tracker); + + queue.addCallback(spy1); + queue.addCallback(spy2); + queue.addCallback(spy3); + + assert(spy1.notCalled); + assert(spy2.notCalled); + assert(spy3.notCalled); + + rIC(() => { + assert(spy1.calledOnce); + assert(spy1.calledBefore(spy2)); + + assert(spy2.calledOnce); + assert(spy2.calledBefore(spy3)); + + assert(spy3.calledOnce); + + queue.destroy(); + resolve(); + }); + }); + }; + + await testQueueOrder('visible'); + await testQueueOrder('hidden'); + }); + + it('runs nested tasks in order', async () => { + const testQueueOrder = (visibilityState) => { + return new Promise((resolve) => { + stubProperty(document, 'visibilityState').value(visibilityState); + + const spy1 = sandbox.spy(); + const spy2 = sandbox.spy(); + const spy3 = sandbox.spy(); + const spy4 = sandbox.spy(); + const spy5 = sandbox.spy(); + const spy6 = sandbox.spy(); + + const queue = new IdleQueue(tracker); + + queue.addCallback(spy1); + queue.addCallback(() => { + queue.addCallback(() => { + spy4(); + queue.addCallback(spy6); + }); + spy2(); + }); + queue.addCallback(() => { + queue.addCallback(spy5); + spy3(); + }); + + // Nest the `rIC()` calls to ensure nested tasks are run. + rIC(() => { + rIC(() => { + rIC(() => { + assert(spy1.calledOnce); + assert(spy1.calledBefore(spy2)); + assert(spy2.calledOnce); + assert(spy2.calledBefore(spy3)); + assert(spy3.calledOnce); + assert(spy3.calledBefore(spy4)); + assert(spy4.calledOnce); + assert(spy4.calledBefore(spy5)); + assert(spy5.calledOnce); + + queue.destroy(); + resolve(); + }); + }); + }); + }); + }; + + await testQueueOrder('visible'); + await testQueueOrder('hidden'); + }); + + it('handles changes in visibilityState while the queue is pending', + (done) => { + stubProperty(document, 'visibilityState').value('visible'); + + const spy1 = sandbox.spy(); + const spy2 = sandbox.spy(); + const spy3 = sandbox.spy(); + const queue = new IdleQueue(tracker); + + queue.addCallback(spy1); + queue.addCallback(spy2); + assert(spy1.notCalled); + assert(spy2.notCalled); + + stubProperty(document, 'visibilityState').value('hidden'); + dispatch(document, 'visibilitychange', self); + + queueMicrotask(() => { + assert(spy1.calledOnce); + assert(spy2.calledOnce); + assert(spy3.notCalled); + + queue.addCallback(spy3); + + queueMicrotask(() => { + assert(spy3.calledOnce); + + queue.destroy(); + done(); + }); + }); + }); + + it('does not run queued tasks twice after a visibilitychange', (done) => { + stubProperty(document, 'visibilityState').value('visible'); + + const spy1 = sandbox.spy(); + const spy2 = sandbox.spy(); + const queue = new IdleQueue(tracker); + + queue.addCallback(spy1); + queue.addCallback(spy2); + assert(spy1.notCalled); + assert(spy2.notCalled); + + stubProperty(document, 'visibilityState').value('hidden'); + dispatch(document, 'visibilitychange', self); + + queueMicrotask(() => { + assert(spy1.calledOnce); + assert(spy2.calledOnce); + + // Wait until the next idle point to assert the tasks weren't re-called. + rIC(() => { + assert(spy1.calledOnce); + assert(spy2.calledOnce); + + queue.destroy(); + done(); + }); + }); + }); + }); + + describe('destroy', () => { + it('removes all added listeners', () => { + sandbox.spy(self, 'removeEventListener'); + + const queue = new IdleQueue(tracker); + assert(self.removeEventListener.notCalled); + + queue.destroy(); + + if (isSafari()) { + assert(self.removeEventListener.calledTwice); + assert(self.removeEventListener.calledWith( + 'beforeunload', sinon.match.func, true)); + } else { + assert(self.removeEventListener.calledOnce); + } + assert(self.removeEventListener.calledWith( + 'visibilitychange', sinon.match.func, true)); + }); + + it('reverts overridden methods', () => { + const originalSendMethod = tracker.send; + + const queue = new IdleQueue(tracker); + assert.notEqual(originalSendMethod, tracker.send); + + queue.destroy(); + assert.strictEqual(originalSendMethod, tracker.send); + }); + }); +}); From dece7be8e133ec7243a3185b8efe5e619c224302 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Sun, 5 Aug 2018 09:36:14 -0700 Subject: [PATCH 20/71] Update dependencies --- package.json | 67 ++++++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index 3e86bd38..57085760 100644 --- a/package.json +++ b/package.json @@ -30,45 +30,44 @@ }, "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", + "rollup": "^0.64.1", + "rollup-plugin-memory": "^3.0.0", + "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" } } From 5ba09e2d102031a36cd6a76bd261aa2d4a2e1b90 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Sun, 5 Aug 2018 09:57:40 -0700 Subject: [PATCH 21/71] Update the gulpfile to work with Gulp 4 --- gulpfile.js | 145 +++++++++++++++++++++++++++------------------------ package.json | 8 +-- 2 files changed, 81 insertions(+), 72 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 7ad181ee..3e4272f9 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'); @@ -52,7 +48,7 @@ const isProd = () => { }; -gulp.task('javascript', () => { +gulp.task('js:lib', () => { if (isProd()) { return build('autotrack.js').then(({code, map}) => { fs.outputFileSync('autotrack.js', code, 'utf-8'); @@ -66,29 +62,36 @@ gulp.task('javascript', () => { }); } else { return rollup({ - entry: './lib/index.js', + input: './lib/index.js', plugins: [ nodeResolve(), babel({ babelrc: false, plugins: ['external-helpers'], - presets: [['es2015', {modules: false}]], + presets: [['env', { + modules: false, + // Note: uncomment when developing for easier debugging. + // targets: { + // browsers: ['last 2 Chrome versions'], + // }, + }]], }), ], }).then((bundle) => { return bundle.write({ - dest: 'autotrack.js', + file: 'autotrack.js', format: 'iife', - sourceMap: true, + sourcemap: true, }); }); } }); -gulp.task('javascript:unit', ((compiler) => { +gulp.task('js:test', ((compiler) => { const createCompiler = () => { return webpack({ + mode: 'development', entry: glob.sync('./test/unit/**/*-test.js'), output: { path: path.resolve(__dirname, 'test/unit'), @@ -98,18 +101,21 @@ 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: /\.js$/, + exclude: /node_modules\/(?!(dom-utils)\/).*/, + use: { + loader: 'babel-loader', + options: { + babelrc: false, + cacheDirectory: true, + presets: [['env', {modules: false}]], + }, + }, }, - }], + ], }, }); }; @@ -123,9 +129,12 @@ gulp.task('javascript:unit', ((compiler) => { })()); +gulp.task('js', gulp.parallel('js:lib', 'js:test')); + + gulp.task('lint', () => { return gulp.src([ - 'gulpfile.babel.js', + 'gulpfile.js', 'bin/autotrack', 'bin/*.js', 'lib/*.js', @@ -140,37 +149,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', gulp.series('serve', (done) => { const opts = { username: process.env.SAUCE_USERNAME, accessKey: process.env.SAUCE_ACCESS_KEY, @@ -189,33 +188,41 @@ gulp.task('tunnel', ['serve'], (done) => { done(); } }); -}); +})); -gulp.task('serve', ['javascript', 'javascript:unit'], (done) => { - server.start(done); - process.on('exit', server.stop.bind(server)); -}); +gulp.task('test:e2e', gulp.series('js', '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', gulp.series('js', (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('watch', ['serve'], () => { - gulp.watch('./lib/**/*.js', ['javascript']); - gulp.watch([ - './lib/**/*.js', - './test/unit/**/*-test.js', - ], ['javascript:unit']); -}); +gulp.task('watch', gulp.series('serve', () => { + gulp.watch('./lib/**/*.js', gulp.series('js:lib')); + gulp.watch(['./lib/**/*.js', './test/unit/**/*-test.js'], + gulp.series('js:test')); +})); diff --git a/package.json b/package.json index 57085760..2e37c8ba 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,11 @@ "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", + "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", From 8a7cb87f90979732b4deb234d965b2d6be236acb Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Sun, 5 Aug 2018 19:45:58 -0700 Subject: [PATCH 22/71] Upgrade test libs and move them out of the bundle --- test/unit/event-emitter-test.js | 2 -- test/unit/index.html | 21 ++++++++++-- test/unit/method-chain-test.js | 34 +++++++++---------- test/unit/plugins/clean-url-tracker-test.js | 2 -- .../plugins/page-visibility-tracker-test.js | 2 -- test/unit/session-test.js | 2 -- test/unit/store-test.js | 4 +-- test/unit/utilities-test.js | 15 ++++++-- 8 files changed, 47 insertions(+), 35 deletions(-) diff --git a/test/unit/event-emitter-test.js b/test/unit/event-emitter-test.js index c538c2c7..12b6fb0a 100644 --- a/test/unit/event-emitter-test.js +++ b/test/unit/event-emitter-test.js @@ -15,8 +15,6 @@ */ -import assert from 'assert'; -import sinon from 'sinon'; import EventEmitter from '../../lib/event-emitter'; diff --git a/test/unit/index.html b/test/unit/index.html index 94f4bf9f..51db90ab 100644 --- a/test/unit/index.html +++ b/test/unit/index.html @@ -3,12 +3,27 @@ Mocha Tests - + +
- - + + + + + + + + + + diff --git a/test/unit/method-chain-test.js b/test/unit/method-chain-test.js index 4d900d5e..f948e9dd 100644 --- a/test/unit/method-chain-test.js +++ b/test/unit/method-chain-test.js @@ -15,8 +15,6 @@ */ -import assert from 'assert'; -import sinon from 'sinon'; import MethodChain from '../../lib/method-chain'; @@ -295,10 +293,10 @@ describe('MethodChain', () => { assert(spy3.calledOnce); assert(tracker.get('page'), '/foo'); - trackerSetSpy.reset(); - spy1.reset(); - spy2.reset(); - spy3.reset(); + trackerSetSpy.resetHistory(); + spy1.resetHistory(); + spy2.resetHistory(); + spy3.resetHistory(); MethodChain.remove(tracker, 'set', overrideMethod1); tracker.set('page', '/bar'); @@ -310,10 +308,10 @@ describe('MethodChain', () => { assert(spy3.calledOnce); assert(tracker.get('page'), '/bar'); - trackerSetSpy.reset(); - spy1.reset(); - spy2.reset(); - spy3.reset(); + trackerSetSpy.resetHistory(); + spy1.resetHistory(); + spy2.resetHistory(); + spy3.resetHistory(); MethodChain.remove(tracker, 'set', overrideMethod2); MethodChain.remove(tracker, 'set', overrideMethod3); @@ -363,10 +361,10 @@ describe('MethodChain', () => { assert(spy2.calledOnce); assert(spy3.calledOnce); - trackerBuildHitTaskSpy.reset(); - spy1.reset(); - spy2.reset(); - spy3.reset(); + trackerBuildHitTaskSpy.resetHistory(); + spy1.resetHistory(); + spy2.resetHistory(); + spy3.resetHistory(); MethodChain.remove(tracker, 'buildHitTask', overrideMethod1); tracker.send('pageview', '/bar'); @@ -377,10 +375,10 @@ describe('MethodChain', () => { assert(spy2.calledOnce); assert(spy3.calledOnce); - trackerBuildHitTaskSpy.reset(); - spy1.reset(); - spy2.reset(); - spy3.reset(); + trackerBuildHitTaskSpy.resetHistory(); + spy1.resetHistory(); + spy2.resetHistory(); + spy3.resetHistory(); MethodChain.remove(tracker, 'buildHitTask', overrideMethod2); MethodChain.remove(tracker, 'buildHitTask', overrideMethod3); diff --git a/test/unit/plugins/clean-url-tracker-test.js b/test/unit/plugins/clean-url-tracker-test.js index e51b4fff..5585eb9d 100644 --- a/test/unit/plugins/clean-url-tracker-test.js +++ b/test/unit/plugins/clean-url-tracker-test.js @@ -15,8 +15,6 @@ */ -import assert from 'assert'; -import sinon from 'sinon'; import * as constants from '../../../lib/constants'; import '../../../lib/plugins/clean-url-tracker'; diff --git a/test/unit/plugins/page-visibility-tracker-test.js b/test/unit/plugins/page-visibility-tracker-test.js index 824492e6..83d8076f 100644 --- a/test/unit/plugins/page-visibility-tracker-test.js +++ b/test/unit/plugins/page-visibility-tracker-test.js @@ -15,8 +15,6 @@ */ -import assert from 'assert'; -import sinon from 'sinon'; import '../../../lib/plugins/page-visibility-tracker'; diff --git a/test/unit/session-test.js b/test/unit/session-test.js index 0ae00897..e5afaf44 100644 --- a/test/unit/session-test.js +++ b/test/unit/session-test.js @@ -15,8 +15,6 @@ */ -import assert from 'assert'; -import sinon from 'sinon'; import Session from '../../lib/session'; import {now} from '../../lib/utilities'; diff --git a/test/unit/store-test.js b/test/unit/store-test.js index e91d3c77..9948f601 100644 --- a/test/unit/store-test.js +++ b/test/unit/store-test.js @@ -15,8 +15,6 @@ */ -import assert from 'assert'; -import sinon from 'sinon'; import Store from '../../lib/store'; @@ -110,7 +108,7 @@ describe('Store', () => { }); it('returns the cached data if localStorage is not supported', () => { - sinon.stub(Store, 'isSupported_', () => false); + sinon.stub(Store, 'isSupported_').returns(false); const store1 = Store.getOrCreate( 'UA-12345-1', 'ns1', {default: true, foo: 1}); diff --git a/test/unit/utilities-test.js b/test/unit/utilities-test.js index 39ace39c..e22f36d7 100644 --- a/test/unit/utilities-test.js +++ b/test/unit/utilities-test.js @@ -15,8 +15,6 @@ */ -import assert from 'assert'; -import qs from 'querystring'; import * as utilities from '../../lib/utilities'; @@ -26,19 +24,29 @@ const DEFAULT_FIELDS = { siteSpeedSampleRate: 0, }; +const sandbox = sinon.createSandbox(); describe('utilities', () => { let tracker; let hits; beforeEach((done) => { + sandbox.restore(); + hits = []; window.ga('create', DEFAULT_FIELDS); window.ga((t) => { tracker = t; const originalSendHitTask = tracker.get('sendHitTask'); tracker.set('sendHitTask', (model) => { - hits.push(qs.parse(model.get('hitPayload'))); + const query = {}; + const hitPayload = model.get('hitPayload'); + hitPayload.split('&').forEach((entry) => { + const [key, value] = entry.split('='); + query[decodeURIComponent(key)] = decodeURIComponent(value); + }); + + hits.push(query); originalSendHitTask(model); }); @@ -47,6 +55,7 @@ describe('utilities', () => { }); afterEach(() => { + sandbox.restore(); window.ga('remove'); }); From bdb833fde65454b66cf3455059122339eb64b15e Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Sun, 5 Aug 2018 19:46:52 -0700 Subject: [PATCH 23/71] Update .eslintrc to use Google JS style --- .eslintrc | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 + } + } + ] } From d44822957699f7757b8dfb3fc84c0916b1cf02b8 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Sun, 5 Aug 2018 19:49:04 -0700 Subject: [PATCH 24/71] Remove unused getEventCount method --- lib/event-emitter.js | 12 ------------ test/unit/event-emitter-test.js | 14 -------------- 2 files changed, 26 deletions(-) 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/test/unit/event-emitter-test.js b/test/unit/event-emitter-test.js index 12b6fb0a..a9d6361e 100644 --- a/test/unit/event-emitter-test.js +++ b/test/unit/event-emitter-test.js @@ -121,18 +121,4 @@ describe('EventEmitter', () => { assert(spy3.calledOnce); }); }); - - describe('getEventCount', () => { - it('returns the total number of registered event handlers', () => { - const emitter = new EventEmitter(); - const spy1 = sinon.spy(); - const spy2 = sinon.spy(); - const spy3 = sinon.spy(); - emitter.on('foo', spy1); - emitter.on('bar', spy2); - emitter.on('bar', spy3); - - assert.strictEqual(emitter.getEventCount(), 3); - }); - }); }); From 5bcc8cb1dfaa9ee993c1c70e3b232462e8ceca02 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Sun, 5 Aug 2018 19:58:57 -0700 Subject: [PATCH 25/71] Optimize the MethodChain.remove static method --- lib/method-chain.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) 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); From 4a8b9ad54384fad675e01c8e5598b3488333a130 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Tue, 7 Aug 2018 11:22:18 -0700 Subject: [PATCH 26/71] Add timestamp support to Store --- lib/externs/store.js | 8 +++ lib/session.js | 6 +- lib/store.js | 55 +++++++++++----- test/unit/store-test.js | 141 ++++++++++++++++++++++++++++------------ 4 files changed, 150 insertions(+), 60 deletions(-) create mode 100644 lib/externs/store.js 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/session.js b/lib/session.js index fb44f10a..7172c4be 100644 --- a/lib/session.js +++ b/lib/session.js @@ -94,8 +94,10 @@ export default class Session { hitTime: 0, isExpired: false, }; - this.store = Store.getOrCreate( - tracker.get('trackingId'), 'session', defaultProps); + this.store = Store.getOrCreate(tracker.get('trackingId'), 'session', { + defaults: defaultProps, + timestampKey: 'hitTime', + }); // Ensure the session has an ID. if (!this.store.get().id) { diff --git a/lib/store.js b/lib/store.js index b9eadd8f..eca82975 100644 --- a/lib/store.js +++ b/lib/store.js @@ -37,15 +37,15 @@ 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); + instances[key] = new Store(key, opts); if (!isListening) initStorageListener(); } return instances[key]; @@ -104,12 +104,13 @@ 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. @@ -121,31 +122,48 @@ export default class Store extends EventEmitter { * and in sync with activity in other windows via the `storage` event). * TODO(philipwalton): Implement schema migrations if/when a new * schema version is introduced. + * @param {{bypassCache: (boolean)}=} param1 + * bypassCache: If true, cached data will be ignored. * @return {!Object} The stored data merged with the defaults. */ - get() { - if (this.cache_) { + get({bypassCache = false} = {}) { + if (this.cache_ && !bypassCache) { return this.cache_; - } else { - if (Store.isSupported_()) { - try { - this.cache_ = parse(Store.get_(this.key_)); - } catch (err) { - // Do nothing. - } + } + if (Store.isSupported_()) { + try { + this.cache_ = parse(Store.get_(this.key_)); + } catch (err) { + // Do nothing. } - return this.cache_ = assign({}, this.defaults_, this.cache_); } + return this.cache_ = assign({}, this.defaults_, this.cache_); } /** * 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); + 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 set. + // 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.get({bypassCache: true}); + if (typeof oldData[timestampKey] === 'number' && + oldData[timestampKey] > newData[timestampKey]) { + return; + } + } else { + oldData = this.get(); + } + + this.cache_ = assign(oldData, newData); if (Store.isSupported_()) { try { Store.set_(this.key_, JSON.stringify(this.cache_)); @@ -160,6 +178,7 @@ export default class Store extends EventEmitter { */ clear() { this.cache_ = {}; + if (Store.isSupported_()) { try { Store.clear_(this.key_); diff --git a/test/unit/store-test.js b/test/unit/store-test.js index 9948f601..bda229b6 100644 --- a/test/unit/store-test.js +++ b/test/unit/store-test.js @@ -14,13 +14,39 @@ * limitations under the License. */ - import Store from '../../lib/store'; +const sandbox = sinon.createSandbox(); + +// TODO(philipwalton): remove once dom=utils supports +// using Object.defineProperty on events. +const dispatchStorageEvent = ({key, oldValue, newValue}) => { + let event; + try { + event = new StorageEvent('storage', {key, oldValue, newValue}); + } catch (err) { + event = document.createEvent('StorageEvent'); + event.initEvent('storage'); + Object.defineProperties(event, { + key: {value: key}, + newValue: {value: newValue}, + oldValue: {value: oldValue}, + }); + } + window.dispatchEvent(event); +}; + + describe('Store', () => { - beforeEach(() => localStorage.clear()); - afterEach(() => localStorage.clear()); + beforeEach(() => { + sandbox.restore(); + localStorage.clear(); + }); + afterEach(() => { + sandbox.restore(); + localStorage.clear(); + }); describe('static getOrCreate', () => { it('creates a localStorage key from the tracking ID and namespace', () => { @@ -47,7 +73,7 @@ describe('Store', () => { }); it('adds a single event listener for the storage event', () => { - sinon.spy(window, 'addEventListener'); + sandbox.spy(window, 'addEventListener'); const store1 = Store.getOrCreate('UA-12345-1', 'ns1'); const store2 = Store.getOrCreate('UA-67890-1', 'ns2'); @@ -56,8 +82,6 @@ describe('Store', () => { store1.destroy(); store2.destroy(); - - window.addEventListener.restore(); }); }); @@ -77,10 +101,12 @@ describe('Store', () => { }); it('merges the stored data with the defaults', () => { - const store1 = Store.getOrCreate( - 'UA-12345-1', 'ns1', {default: true, foo: 1}); - const store2 = Store.getOrCreate( - 'UA-67890-1', 'ns2', {default: true, qux: 2}); + const store1 = Store.getOrCreate('UA-12345-1', 'ns1', { + defaults: {default: true, foo: 1}, + }); + const store2 = Store.getOrCreate('UA-67890-1', 'ns2', { + defaults: {default: true, qux: 2}, + }); localStorage.setItem(store1.key_, JSON.stringify({foo: 12, bar: 34})); localStorage.setItem(store2.key_, JSON.stringify({qux: 56, baz: 78})); @@ -93,10 +119,12 @@ describe('Store', () => { }); it('returns the cached data if the store read errors', () => { - const store1 = Store.getOrCreate( - 'UA-12345-1', 'ns1', {default: true, foo: 1}); - const store2 = Store.getOrCreate( - 'UA-67890-1', 'ns2', {default: true, qux: 2}); + const store1 = Store.getOrCreate('UA-12345-1', 'ns1', { + defaults: {default: true, foo: 1}, + }); + const store2 = Store.getOrCreate('UA-67890-1', 'ns2', { + defaults: {default: true, qux: 2}, + }); localStorage.setItem(store1.key_, 'bad data'); @@ -108,12 +136,14 @@ describe('Store', () => { }); it('returns the cached data if localStorage is not supported', () => { - sinon.stub(Store, 'isSupported_').returns(false); + sandbox.stub(Store, 'isSupported_').returns(false); - const store1 = Store.getOrCreate( - 'UA-12345-1', 'ns1', {default: true, foo: 1}); - const store2 = Store.getOrCreate( - 'UA-67890-1', 'ns2', {default: true, qux: 2}); + const store1 = Store.getOrCreate('UA-12345-1', 'ns1', { + defaults: {default: true, foo: 1}, + }); + const store2 = Store.getOrCreate('UA-67890-1', 'ns2', { + defaults: {default: true, qux: 2}, + }); store1.set({bar: 3}); store2.set({baz: 4}); @@ -121,7 +151,6 @@ describe('Store', () => { assert.deepEqual(store1.get(), {default: true, foo: 1, bar: 3}); assert.deepEqual(store2.get(), {default: true, qux: 2, baz: 4}); - Store.isSupported_.restore(); store1.destroy(); store2.destroy(); }); @@ -164,45 +193,58 @@ describe('Store', () => { const store1 = Store.getOrCreate('UA-12345-1', 'ns1'); const store2 = Store.getOrCreate('UA-67890-1', 'ns2'); - sinon.stub(Store, 'set_').throws(); + sandbox.stub(Store, 'set_').throws(); store1.set({foo: 12, bar: 34}); store2.set({qux: 56, baz: 78}); assert.deepEqual(store1.cache_, {foo: 12, bar: 34}); assert.deepEqual(store2.cache_, {qux: 56, baz: 78}); - Store.set_.restore(); store1.destroy(); store2.destroy(); }); - it('updates the cache of other stores in other tabs', function() { - // Feature detect event constructor support, skip otherwise. - try { - new StorageEvent('storage', {}); - } catch (err) { - this.skip(); - } + it('handles cases where the new data is older than the old data', () => { + const store1 = Store.getOrCreate('UA-12345-1', 'ns1'); + const store2 = Store.getOrCreate('UA-67890-1', 'ns2', { + timestampKey: 'time', + }); + + store1.set({time: 1000, value: 'A'}); + store2.set({time: 1000, value: 'A'}); + assert.deepEqual(store1.cache_, {time: 1000, value: 'A'}); + assert.deepEqual(store2.cache_, {time: 1000, value: 'A'}); + + store1.set({time: 999, value: 'B'}); + store2.set({time: 999, value: 'B'}); + + assert.deepEqual(store1.cache_, {time: 999, value: 'B'}); + + // No data should have been written because the stored time is newer. + assert.deepEqual(store2.cache_, {time: 1000, value: 'A'}); + + store1.destroy(); + store2.destroy(); + }); + + it('updates the cache of other stores in other tabs', () => { const store1 = Store.getOrCreate('UA-12345-1', 'ns1'); const store2 = Store.getOrCreate('UA-67890-1', 'ns2'); // Simulate a storage event, meaning a `set()` call was made in // another tab. - const storageEvent1 = new StorageEvent('storage', { + dispatchStorageEvent({ key: store1.key_, oldValue: '', newValue: JSON.stringify({foo: 12, bar: 34}), }); - const storageEvent2 = new StorageEvent('storage', { + dispatchStorageEvent({ key: store2.key_, oldValue: '', newValue: JSON.stringify({qux: 56, baz: 78}), }); - window.dispatchEvent(storageEvent1); - window.dispatchEvent(storageEvent2); - assert.deepEqual(store1.cache_, {foo: 12, bar: 34}); assert.deepEqual(store2.cache_, {qux: 56, baz: 78}); @@ -237,7 +279,7 @@ describe('Store', () => { it('clears the cache even if the localStorage clear fails', () => { const store1 = Store.getOrCreate('UA-12345-1', 'ns1'); const store2 = Store.getOrCreate('UA-67890-1', 'ns2'); - sinon.stub(Store, 'clear_').throws(); + sandbox.stub(Store, 'clear_').throws(); store1.set({foo: 12, bar: 34}); store2.set({qux: 56, baz: 78}); @@ -251,7 +293,6 @@ describe('Store', () => { assert.deepEqual(store1.get(), {}); assert.deepEqual(store2.get(), {}); - Store.clear_.restore(); store1.destroy(); store2.destroy(); }); @@ -276,8 +317,8 @@ describe('Store', () => { it('removes the storage listener when the last instance is destroyed', () => { - sinon.spy(window, 'addEventListener'); - sinon.spy(window, 'removeEventListener'); + sandbox.spy(window, 'addEventListener'); + sandbox.spy(window, 'removeEventListener'); const store1 = Store.getOrCreate('UA-12345-1', 'ns1'); const store2 = Store.getOrCreate('UA-67890-1', 'ns2'); @@ -291,9 +332,29 @@ describe('Store', () => { store2.destroy(); assert(window.removeEventListener.calledOnce); assert(window.removeEventListener.alwaysCalledWith(listener)); + }); + }); + + describe('[[events]]', () => { + describe('externalSet', () => { + it('is invoked when the stored data is updated in another tab', () => { + const spy = sandbox.spy(); + const store = Store.getOrCreate('UA-12345-1', 'ns'); - window.addEventListener.restore(); - window.removeEventListener.restore(); + store.on('externalSet', spy); + + dispatchStorageEvent({ + key: 'autotrack:UA-12345-1:ns', + oldValue: JSON.stringify({data: 'foo'}), + newValue: JSON.stringify({data: 'bar'}), + }); + + assert(spy.calledOnce); + assert.deepEqual(spy.firstCall.args[0], {data: 'bar'}); + assert.deepEqual(spy.firstCall.args[1], {data: 'foo'}); + + store.destroy(); + }); }); }); }); From 15650a03e98aa1b892cc3aae23377504481372c1 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Thu, 9 Aug 2018 13:09:12 -0700 Subject: [PATCH 27/71] Ensure queue callbacks are removed on destroy --- lib/idle-queue.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/idle-queue.js b/lib/idle-queue.js index 867cedd4..e6552611 100644 --- a/lib/idle-queue.js +++ b/lib/idle-queue.js @@ -66,7 +66,7 @@ export default class IdleQueue { // 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.runCallbacksImmediately_, true); + addEventListener('beforeunload', this.processCallbacksImmediately_, true); } } @@ -101,6 +101,7 @@ export default class IdleQueue { * removing any overridden methods. */ destroy() { + this.processCallbacksImmediately_({destroy: true}); MethodChain.remove(this.tracker_, 'send', this.trackerSendOverride_); removeEventListener('visibilitychange', this.onVisibilityChange_, true); @@ -112,14 +113,19 @@ export default class IdleQueue { // NOTE: we only add this to Safari because adding it to Firefox would // prevent the page from being eligible for bfcache. if (isSafari()) { - removeEventListener('beforeunload', this.runCallbacksImmediately_, true); + removeEventListener( + 'beforeunload', this.processCallbacksImmediately_, true); } } /** - * Runs all added callbacks as microtasks. + * Loops through each added callbacks, cancels the `requestIdleCallback` + * function, and (unless the destroy flag is `true`), runs each callback + * in a microtask. + * @param {{destroy: (boolean)}=} param1 + * destroy: When true the callbacks are removed and not run. */ - runCallbacksImmediately_() { + processCallbacksImmediately_({destroy = false} = {}) { if (this.pendingIdleCallbacks > 0) { Object.keys(this.idleCallbacks_).forEach((id) => { const {handle, callback, state} = this.idleCallbacks_[id]; @@ -128,8 +134,9 @@ export default class IdleQueue { cIC(handle); } - // console.log('running as microtask', id, callback.toString()); - queueMicrotask(() => callback(state)); + if (!destroy) { + queueMicrotask(() => callback(state)); + } }); this.idleCallbacks_ = {}; } @@ -155,7 +162,7 @@ export default class IdleQueue { */ onVisibilityChange_() { if (document.visibilityState === 'hidden') { - this.runCallbacksImmediately_(); + this.processCallbacksImmediately_(); } } From 41eabdc12869089746bcf2acc85773d9e87f0b15 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Fri, 10 Aug 2018 18:15:40 -0700 Subject: [PATCH 28/71] Remove tracker logic from IdleQueue --- lib/idle-queue.js | 166 ++++------ lib/tracker-queue.js | 111 +++++++ lib/utilities.js | 55 ++-- test/unit/idle-queue-test.js | 555 ++++++++++++++++++-------------- test/unit/tracker-queue-test.js | 171 ++++++++++ 5 files changed, 705 insertions(+), 353 deletions(-) create mode 100644 lib/tracker-queue.js create mode 100644 test/unit/tracker-queue-test.js diff --git a/lib/idle-queue.js b/lib/idle-queue.js index e6552611..a84a67c0 100644 --- a/lib/idle-queue.js +++ b/lib/idle-queue.js @@ -14,10 +14,7 @@ * limitations under the License. */ -import MethodChain from './method-chain'; -import {cIC, isSafari, queueMicrotask, rIC, uid} from './utilities'; - -const instances = {}; +import {cIC, isSafari, queueMicrotask, rIC} from './utilities'; /** * A class wraps a queue of requestIdleCallback functions for two reasons: @@ -27,35 +24,18 @@ const instances = {}; */ export default class IdleQueue { /** - * Gets an existing instance for the passed arguments or creates a new - * instance if one doesn't exist. - * @param {!Tracker} tracker An analytics.js tracker object. - * @return {Session} The Session instance. + * Creates the IdleQueue instance and adds lifecycle event listeners to + * run the queue if the page is hidden (with fallback behavior for Safari). */ - static getOrCreate(tracker) { - // Don't create multiple instances for the same property. - const trackingId = tracker.get('trackingId'); - if (!instances[trackingId]) { - instances[trackingId] = new IdleQueue(tracker); - } - return instances[trackingId]; - } - - /** - * @param {!Tracker} tracker An analytics.js tracker object. - */ - constructor(tracker) { - this.tracker_ = tracker; - this.idleCallbacks_ = {}; - - this.pendingIdleCallbacks = 0; - this.beforeSendCallbacks_ = []; + constructor() { + this.idleCallbackHandle_ = null; + this.callbackQueue_ = []; // Bind methods this.onVisibilityChange_ = this.onVisibilityChange_.bind(this); - this.trackerSendOverride_ = this.trackerSendOverride_.bind(this); + this.onBeforeUnload_ = this.onBeforeUnload_.bind(this); + this.processCallbacks_ = this.processCallbacks_.bind(this); - MethodChain.add(this.tracker_, 'send', this.trackerSendOverride_); addEventListener('visibilitychange', this.onVisibilityChange_, true); // Safari does not reliably fire the `pagehide` or `visibilitychange` @@ -66,7 +46,7 @@ export default class IdleQueue { // 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.processCallbacksImmediately_, true); + addEventListener('beforeunload', this.onBeforeUnload_, true); } } @@ -74,26 +54,13 @@ export default class IdleQueue { * @param {!Function} callback */ addCallback(callback) { - const id = uid(); const state = { time: Date.now(), visibilityState: document.visibilityState, }; - const entry = this.idleCallbacks_[id] = {callback, state}; - const wrappedCallback = () => { - callback(state); - this.onIdleCalbackRun_(id); - }; - ++this.pendingIdleCallbacks; - - if (document.visibilityState === 'hidden') { - queueMicrotask(wrappedCallback); - } else { - // Schedule the callback in the idle queue and store its handle (so it - // can be cancelled later if needed). - entry.handle = rIC(wrappedCallback); - } + this.callbackQueue_.push({state, callback}); + this.scheduleCallbackProcessing_(); } /** @@ -101,8 +68,9 @@ export default class IdleQueue { * removing any overridden methods. */ destroy() { - this.processCallbacksImmediately_({destroy: true}); - MethodChain.remove(this.tracker_, 'send', this.trackerSendOverride_); + this.callbackQueue_ = []; + this.cancelScheduledCallbackProcessing_(); + removeEventListener('visibilitychange', this.onVisibilityChange_, true); // Safari does not reliably fire the `pagehide` or `visibilitychange` @@ -114,84 +82,82 @@ export default class IdleQueue { // prevent the page from being eligible for bfcache. if (isSafari()) { removeEventListener( - 'beforeunload', this.processCallbacksImmediately_, true); + 'beforeunload', this.onBeforeUnload_, true); } } /** - * Loops through each added callbacks, cancels the `requestIdleCallback` - * function, and (unless the destroy flag is `true`), runs each callback - * in a microtask. - * @param {{destroy: (boolean)}=} param1 - * destroy: When true the callbacks are removed and not run. + * Schedules the callback queue to be processed. If the document is in the + * hidden state, they queue is scheduled as a microtask so it can be run + * in cases where a macrotask couldn't (like if the page is unloading). If + * the document is in the visible state, `requestIdleCallback` is used. */ - processCallbacksImmediately_({destroy = false} = {}) { - if (this.pendingIdleCallbacks > 0) { - Object.keys(this.idleCallbacks_).forEach((id) => { - const {handle, callback, state} = this.idleCallbacks_[id]; - - if (handle) { - cIC(handle); - } - - if (!destroy) { - queueMicrotask(() => callback(state)); - } - }); - this.idleCallbacks_ = {}; + scheduleCallbackProcessing_() { + if (document.visibilityState === 'hidden') { + queueMicrotask(this.processCallbacks_); + } else { + if (!this.idleCallbackHandle_) { + this.idleCallbackHandle_ = rIC(this.processCallbacks_); + } } } /** - * Deletes a callback from the stored set and decremets the pending count. - * @param {number} id + * Processes as many callbacks in the queue as it can before reaching the + * deadline. If no deadline is passed, it will process all callbacks + * immediately. If an `IdleDeadline` object is passed (as is with + * `requestIdleCallback`) then the callbacks are processed until there's + * no time remaining. + * @param {IdleDeadline|undefined} deadline */ - onIdleCalbackRun_(id) { - delete this.idleCallbacks_[id]; - --this.pendingIdleCallbacks; + processCallbacks_(deadline) { + this.cancelScheduledCallbackProcessing_(); - if (this.pendingIdleCallbacks === 0 && - this.trackerSendOverride_ !== null) { - this.removeTrackerSendOverride_(); + // Process callbacks until there's none left or the deadline has passed. + while (this.callbackQueue_.length > 0 && deadlineNotPassed(deadline)) { + const {callback, state} = this.callbackQueue_.shift(); + callback(state); + } + + if (this.callbackQueue_.length > 0) { + this.scheduleCallbackProcessing_(); } } + /** + * Cancels any scheduled idle callback and removes the handler (if set). + */ + cancelScheduledCallbackProcessing_() { + cIC(this.idleCallbackHandle_); + this.idleCallbackHandle_ = null; + } + /** * A callback for the `visibilitychange` event that runs all pending * callbacks immediately if the document's visibility state is hidden. */ onVisibilityChange_() { if (document.visibilityState === 'hidden') { - this.processCallbacksImmediately_(); + this.processCallbacks_(); } } /** - * Generates an override for the `tracker.send()` method. - * @param {!Function} originalMethod - * @return {!Function} + * A callback for the `beforeunload` event than runs all pending callbacks + * immediately. The reason this is used instead of adding `processCallbacks_` + * directly is we can't invoke `processCallbacks_` with an `Event` object. */ - trackerSendOverride_(originalMethod) { - return (...args) => { - if (this.pendingIdleCallbacks === 0) { - this.removeTrackerSendOverride_(); - originalMethod(...args); - } else { - this.beforeSendCallbacks_.push(() => { - originalMethod(...args); - }); - } - }; - } - - /** - * Restores the `tracker.send()` override function. - */ - removeTrackerSendOverride_() { - MethodChain.remove(this.tracker_, 'send', this.trackerSendOverride_); - this.trackerSendOverride_ = null; - - this.beforeSendCallbacks_.forEach((callback) => callback()); - this.beforeSendCallbacks_ = []; + onBeforeUnload_() { + this.processCallbacks_(); } } + +/** + * Returns true if there's no deadline or if there is a deadline but it has + * not passed. + * @param {IdleDeadline|undefined} deadline + * @return {boolean} + */ +const deadlineNotPassed = (deadline) => { + return !deadline || deadline.timeRemaining() > 0; +}; diff --git a/lib/tracker-queue.js b/lib/tracker-queue.js new file mode 100644 index 00000000..91b77687 --- /dev/null +++ b/lib/tracker-queue.js @@ -0,0 +1,111 @@ +/** + * 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 MethodChain from './method-chain'; +import IdleQueue from './idle-queue'; + +const instances = {}; + +/** + * A class wraps a queue of requestIdleCallback functions for two reasons: + * 1. So other callers can know whether or not the queue is empty. + *. 2. So we can provide some guarantees that the queued functions will + *. run in unload-type situations. + */ +export default class TrackerQueue extends IdleQueue { + /** + * Gets an existing instance for the passed arguments or creates a new + * instance if one doesn't exist. + * @param {!Tracker} tracker An analytics.js tracker object. + * @return {Session} The Session instance. + */ + static getOrCreate(tracker) { + // Don't create multiple instances for the same property. + const trackingId = tracker.get('trackingId'); + if (!instances[trackingId]) { + instances[trackingId] = new IdleQueue(); + } + return instances[trackingId]; + } + + /** + * @param {!Tracker} tracker An analytics.js tracker object. + */ + constructor(tracker) { + super(); + + this.tracker_ = tracker; + this.beforeSendCallbackQueue_ = []; + + // Bind methods + this.trackerSendOverride_ = this.trackerSendOverride_.bind(this); + + MethodChain.add(this.tracker_, 'send', this.trackerSendOverride_); + } + + /** + * Adds logic to the superclass method to remove the tracker.send override + * the first time the queue is empty. + * @param {...*} args The arguments to be passed to the handler. + */ + processCallbacks_(...args) { + super.processCallbacks_(...args); + + if (this.callbackQueue_.length === 0 && + this.trackerSendOverride_ !== null) { + this.removeTrackerSendOverride_(); + } + } + + /** + * Destroys the instance by unregistering all added event listeners and + * removing any overridden methods. + */ + destroy() { + super.destroy(); + MethodChain.remove(this.tracker_, 'send', this.trackerSendOverride_); + } + + /** + * Generates an override for the `tracker.send()` method. + * @param {!Function} originalMethod + * @return {!Function} + */ + trackerSendOverride_(originalMethod) { + return (...args) => { + if (this.callbackQueue_.length === 0) { + this.removeTrackerSendOverride_(); + originalMethod(...args); + } else { + this.beforeSendCallbackQueue_.push(() => { + originalMethod(...args); + }); + } + }; + } + + /** + * Restores the `tracker.send()` override function. + */ + removeTrackerSendOverride_() { + MethodChain.remove(this.tracker_, 'send', this.trackerSendOverride_); + this.trackerSendOverride_ = null; + + this.beforeSendCallbackQueue_.forEach((callback) => callback()); + this.beforeSendCallbackQueue_ = []; + } +} + diff --git a/lib/utilities.js b/lib/utilities.js index 27d254a8..7e67634c 100644 --- a/lib/utilities.js +++ b/lib/utilities.js @@ -18,7 +18,6 @@ import {getAttributes} from 'dom-utils'; import MethodChain from './method-chain'; -let uid_ = 0; const isSafari_ = !!(typeof safari === 'object' && safari.pushNotification); const supportsRequestIdleCallback_ = typeof requestIdleCallback === 'function'; @@ -286,15 +285,6 @@ export function now() { } -/** - * Returns a unique number for this page. - * @return {number} - */ -export function uid() { - return ++uid_; -} - - /** * Returns whether or not the current browser is Safari. * @return {boolean} @@ -304,23 +294,50 @@ export function isSafari() { } /** - * The native `requestIdleCallback()` function or `setTimeout()` if the - * browser doesn't support it. + * A minimal shim for the requestIdleCallback function. This accepts a + * callback function and runs it at the next idle period, passing in an + * object with a `timeRemaining()` method. + * @param {!Function} callback + * @return {number} + */ +const requestIdleCallbackShim = (callback) => { + let startTime = +new Date; + return setTimeout(() => { + callback({ + didTimeout: false, + timeRemaining: () => Math.max(0, 50 - (new Date - startTime)), + }); + }, 0); +}; + + +/** + * A minimal shim for the cancelIdleCallback function. This accepts a + * handle identifying the idle callback to cancel. + * @param {number} handle + */ +const cancelIdleCallbackShim = (handle) => { + clearTimeout(handle); +}; + + +/** + * The native `requestIdleCallback()` function or `cancelIdleCallbackShim()` + *.if the browser doesn't support it. * @param {!Function} callback * @return {number} */ -export const rIC = (callback) => supportsRequestIdleCallback_ ? - requestIdleCallback(callback) : setTimeout(callback, 0); +export const rIC = supportsRequestIdleCallback_ ? + requestIdleCallback : requestIdleCallbackShim; /** - * The native `cancelIdleCallback()` function or `clearTimeout()` if the - * browser doesn't support it. + * The native `cancelIdleCallback()` function or `cancelIdleCallbackShim()` + * if the browser doesn't support it. * @param {number} handle - * @return {undefined} */ -export const cIC = (handle) => supportsRequestIdleCallback_ ? - cancelIdleCallback(handle) : clearTimeout(handle); +export const cIC = supportsRequestIdleCallback_ ? + cancelIdleCallback : cancelIdleCallbackShim; /*eslint-disable */ diff --git a/test/unit/idle-queue-test.js b/test/unit/idle-queue-test.js index e83847f8..8b1701ea 100644 --- a/test/unit/idle-queue-test.js +++ b/test/unit/idle-queue-test.js @@ -19,11 +19,16 @@ import {dispatch} from 'dom-utils'; import IdleQueue from '../../lib/idle-queue'; import {isSafari, queueMicrotask, rIC} from '../../lib/utilities'; -/* - * A wrapper around `sinon.stub()` for properties that supports non-existent - * own properties (sinon doesn't). +const sandbox = sinon.createSandbox(); + +/** + * A wrapper around `sinon.stub()` that supports non-existent own properties. + * @param {!Object} obj + * @param {string} prop + * @param {*} value + * @return {{value: !Function}} */ -const stubProperty = (obj, prop, value) => { +export const stubProperty = (obj, prop, value) => { if (!obj.hasOwnProperty(prop)) { return { value: (value) => { @@ -35,202 +40,228 @@ const stubProperty = (obj, prop, value) => { } }; +export const blockingSpy = (ms) => { + return sandbox.stub().callsFake(() => { + const startTime = performance.now(); + while (performance.now() - startTime < ms) { + // Do nothing. + } + }); +}; -const getFields = (overrides = {}) => { - return Object.assign({}, { - trackingId: 'UA-12345-1', - cookieDomain: 'auto', - siteSpeedSampleRate: 0, - }, overrides); +export const when = async (fn, intervalMillis = 100, retries = 20) => { + for (let i = 0; i < retries; i++) { + const result = await fn(); + if (result) { + return; + } + await new Promise((resolve) => setTimeout(resolve, intervalMillis)); + } + throw new Error(`${fn} didn't return true after ${retries} retries.`); }; -const sandbox = sinon.createSandbox(); -let tracker; -let hits; +const nextMicroTask = () => new Promise((res) => queueMicrotask(res)); +const nextIdleCallback = () => new Promise((res) => rIC(res)); describe('IdleQueue', () => { - beforeEach((done) => { + beforeEach(() => { sandbox.restore(); - - hits = []; - window.ga('create', getFields()); - window.ga((t) => { - tracker = t; - const originalSendHitTask = tracker.get('sendHitTask'); - tracker.set('sendHitTask', (model) => { - const query = {}; - const hitPayload = model.get('hitPayload'); - hitPayload.split('&').forEach((entry) => { - const [key, value] = entry.split('='); - query[decodeURIComponent(key)] = decodeURIComponent(value); - }); - - hits.push(query); - originalSendHitTask(model); - }); - - done(); - }); }); afterEach(() => { sandbox.restore(); - window.ga('remove'); }); describe('constructor', () => { - it('adds a lifecycle event listeners to window', () => { + it('adds lifecycle event listeners that process callbacks immediately', + () => { sandbox.spy(window, 'addEventListener'); - const queue = new IdleQueue(tracker); + const queue = new IdleQueue(); + + assert(window.addEventListener.calledWith( + 'visibilitychange', sinon.match.func, true)); if (isSafari()) { - assert(window.addEventListener.calledTwice); assert(window.addEventListener.calledWith( 'beforeunload', sinon.match.func, true)); } else { - assert(window.addEventListener.calledOnce); + assert(!window.addEventListener.calledWith( + 'beforeunload', sinon.match.func, true)); } - assert(window.addEventListener.calledWith( - 'visibilitychange', sinon.match.func, true)); - queue.destroy(); - }); + stubProperty(document, 'visibilityState').value('visible'); - it('overrides and defers the tracker.send method', (done) => { - const originalSendMethod = tracker.send; - const queue = new IdleQueue(tracker); + const spy1 = sinon.spy(); + const spy2 = sinon.spy(); - // Creating the queue should have overridden the `send()` method. - assert.notEqual(originalSendMethod, tracker.send); + queue.addCallback(spy1); + queue.addCallback(spy2); + dispatch(window, 'beforeunload'); - queue.addCallback(() => { - tracker.set('dimension1', 'A'); - tracker.set('dimension2', 'A'); - }); - queue.addCallback(() => { - tracker.set('dimension2', 'B'); - tracker.set('dimension3', 'B'); - }); - // This normally runs sync, but when creating the idle queue it - // automatically queues the first `send()` call to allow plugin creation - // logic to finish before any hits are sent. - tracker.send('pageview'); - tracker.send('pageview'); + if (isSafari()) { + assert(spy1.calledOnce); + assert(spy2.calledOnce); + } else { + assert(spy1.notCalled); + assert(spy2.notCalled); + } - rIC(() => { - // The queue dimension set should be found on this hit - assert.strictEqual(hits.length, 2); - assert.strictEqual(hits[0].cd1, 'A'); - assert.strictEqual(hits[0].cd2, 'B'); - assert.strictEqual(hits[0].cd3, 'B'); - assert.strictEqual(hits[1].cd1, 'A'); - assert.strictEqual(hits[1].cd2, 'B'); - assert.strictEqual(hits[1].cd3, 'B'); - - // The send method should now be restored. - assert.strictEqual(originalSendMethod, tracker.send); - - tracker.set('dimension3', 'C'); - tracker.send('pageview'); - - // Since the `send()` command has been restored, it should run sync. - assert.strictEqual(hits.length, 3); - assert.strictEqual(hits[2].cd1, 'A'); - assert.strictEqual(hits[2].cd2, 'B'); - assert.strictEqual(hits[2].cd3, 'C'); + const spy3 = sinon.spy(); + const spy4 = sinon.spy(); + const spy5 = sinon.spy(); - queue.destroy(); - done(); - }); + queue.addCallback(spy3); + queue.addCallback(spy4); + queue.addCallback(spy5); + + stubProperty(document, 'visibilityState').value('hidden'); + dispatch(document, 'visibilitychange'); + + assert(spy1.calledOnce); + assert(spy2.calledOnce); + assert(spy3.calledOnce); + + queue.destroy(); }); + }); + + describe('addCallback', () => { + it('queues a task to run when idle', async () => { + stubProperty(document, 'visibilityState').value('visible'); - it('immediately restores the send method if the queue is empty', () => { - const originalSendMethod = tracker.send; - const queue = new IdleQueue(tracker); + const spy1 = sinon.spy(); + const spy2 = sinon.spy(); + const spy3 = sinon.spy(); - // Creating the queue should have overridden the `send()` method. - assert.notEqual(originalSendMethod, tracker.send); + const queue = new IdleQueue(); - tracker.set('dimension1', 'A'); + queue.addCallback(spy1); + queue.addCallback(spy2); + queue.addCallback(spy3); - // Since there's nothing in the idle queue, this should happen sync - // and the overridden method should be immediately restored. - tracker.send('pageview'); + assert(spy1.notCalled); + assert(spy2.notCalled); + assert(spy3.notCalled); - assert.strictEqual(originalSendMethod, tracker.send); - assert.strictEqual(hits.length, 1); - assert.strictEqual(hits[0].cd1, 'A'); + await nextIdleCallback(); + + // At this point at least one of the spies should have been called, but + // not necessarily all of them (it depends on the idle time remaining). + assert(spy1.calledOnce); + + await when(() => spy3.calledOnce); + + assert(spy1.calledOnce); + assert(spy2.calledOnce); + assert(spy3.calledOnce); queue.destroy(); }); - }); - describe('addCallback', () => { - it('queues a task to run (when idle if supported)', (done) => { + it('waits until the next idle period if all tasks cannot finish', + async () => { stubProperty(document, 'visibilityState').value('visible'); - const spy = sandbox.spy(); - const queue = new IdleQueue(tracker); + const spy1 = blockingSpy(5); + const spy2 = blockingSpy(45); + const spy3 = blockingSpy(5); + const spy4 = blockingSpy(5); + const rICSpy = sinon.spy(); - queue.addCallback(spy); + const queue = new IdleQueue(); - assert(spy.notCalled); - rIC(() => { - assert(spy.calledOnce); + queue.addCallback(spy1); + queue.addCallback(spy2); + queue.addCallback(spy3); + queue.addCallback(spy4); - queue.destroy(); - done(); - }); + // rICSpy is queued after the 4 spies above, + // but it should run at some point between them. + rIC(rICSpy); + + assert(spy1.notCalled); + assert(spy2.notCalled); + assert(spy3.notCalled); + assert(spy4.notCalled); + assert(rICSpy.notCalled); + + await nextIdleCallback(); + + // At this point the one set of idle callbacks should have been called, + // but any that couldn't finished within the time remaining should have + // been queued for the next idle time. + assert(spy1.calledOnce); + assert(rICSpy.calledOnce); + assert(spy4.notCalled); + + await when(() => spy4.calledOnce); + + assert(spy1.calledOnce); + assert(spy2.calledOnce); + assert(spy3.calledOnce); + assert(spy4.calledOnce); + + assert(rICSpy.calledOnce); + assert(rICSpy.calledAfter(spy1)); + assert(rICSpy.calledBefore(spy4)); + + queue.destroy(); }); - it('runs the callback as a microtask when in the hidden state', (done) => { + it('runs the callback as a microtask when in the hidden state', + async () => { stubProperty(document, 'visibilityState').value('hidden'); - const spy = sandbox.spy(); - const queue = new IdleQueue(tracker); + const spy1 = sinon.spy(); + const spy2 = sinon.spy(); + const spy3 = sinon.spy(); - queue.addCallback(spy); + const queue = new IdleQueue(); - queueMicrotask(() => { - assert(spy.calledOnce); + queue.addCallback(spy1); + queue.addCallback(spy2); + queue.addCallback(spy3); - queue.destroy(); - done(); - }); + assert(spy1.notCalled); + assert(spy2.notCalled); + assert(spy3.notCalled); + + await nextMicroTask(); + + assert(spy1.calledOnce); + assert(spy2.calledOnce); + assert(spy3.calledOnce); + + queue.destroy(); }); it('runs tasks in order', async () => { - const testQueueOrder = (visibilityState) => { - return new Promise((resolve) => { - stubProperty(document, 'visibilityState').value(visibilityState); - - const spy1 = sandbox.spy(); - const spy2 = sandbox.spy(); - const spy3 = sandbox.spy(); - const queue = new IdleQueue(tracker); + const testQueueOrder = async (visibilityState) => { + stubProperty(document, 'visibilityState').value(visibilityState); - queue.addCallback(spy1); - queue.addCallback(spy2); - queue.addCallback(spy3); + const spy1 = sinon.spy(); + const spy2 = sinon.spy(); + const spy3 = sinon.spy(); + const queue = new IdleQueue(); - assert(spy1.notCalled); - assert(spy2.notCalled); - assert(spy3.notCalled); + queue.addCallback(spy1); + queue.addCallback(spy2); + queue.addCallback(spy3); - rIC(() => { - assert(spy1.calledOnce); - assert(spy1.calledBefore(spy2)); + assert(spy1.notCalled); + assert(spy2.notCalled); + assert(spy3.notCalled); - assert(spy2.calledOnce); - assert(spy2.calledBefore(spy3)); + await when(() => spy3.calledOnce); - assert(spy3.calledOnce); + assert(spy1.calledOnce); + assert(spy1.calledBefore(spy2)); + assert(spy2.calledOnce); + assert(spy2.calledBefore(spy3)); + assert(spy3.calledOnce); - queue.destroy(); - resolve(); - }); - }); + queue.destroy(); }; await testQueueOrder('visible'); @@ -238,119 +269,184 @@ describe('IdleQueue', () => { }); it('runs nested tasks in order', async () => { - const testQueueOrder = (visibilityState) => { - return new Promise((resolve) => { - stubProperty(document, 'visibilityState').value(visibilityState); + const testQueueOrder = async (visibilityState) => { + stubProperty(document, 'visibilityState').value(visibilityState); - const spy1 = sandbox.spy(); - const spy2 = sandbox.spy(); - const spy3 = sandbox.spy(); - const spy4 = sandbox.spy(); - const spy5 = sandbox.spy(); - const spy6 = sandbox.spy(); + const spy1 = sinon.spy(); + const spy2 = sinon.spy(); + const spy3 = sinon.spy(); + const spy4 = sinon.spy(); + const spy5 = sinon.spy(); + const spy6 = sinon.spy(); - const queue = new IdleQueue(tracker); + const queue = new IdleQueue(); - queue.addCallback(spy1); + queue.addCallback(spy1); + queue.addCallback(() => { queue.addCallback(() => { - queue.addCallback(() => { - spy4(); - queue.addCallback(spy6); - }); - spy2(); - }); - queue.addCallback(() => { - queue.addCallback(spy5); - spy3(); - }); - - // Nest the `rIC()` calls to ensure nested tasks are run. - rIC(() => { - rIC(() => { - rIC(() => { - assert(spy1.calledOnce); - assert(spy1.calledBefore(spy2)); - assert(spy2.calledOnce); - assert(spy2.calledBefore(spy3)); - assert(spy3.calledOnce); - assert(spy3.calledBefore(spy4)); - assert(spy4.calledOnce); - assert(spy4.calledBefore(spy5)); - assert(spy5.calledOnce); - - queue.destroy(); - resolve(); - }); - }); + spy4(); + queue.addCallback(spy6); }); + spy2(); + }); + queue.addCallback(() => { + queue.addCallback(spy5); + spy3(); }); + + await when(() => spy6.calledOnce); + + assert(spy1.calledOnce); + assert(spy1.calledBefore(spy2)); + assert(spy2.calledOnce); + assert(spy2.calledBefore(spy3)); + assert(spy3.calledOnce); + assert(spy3.calledBefore(spy4)); + assert(spy4.calledOnce); + assert(spy4.calledBefore(spy5)); + assert(spy5.calledOnce); + assert(spy5.calledBefore(spy6)); + assert(spy6.calledOnce); + + queue.destroy(); }; await testQueueOrder('visible'); await testQueueOrder('hidden'); }); - it('handles changes in visibilityState while the queue is pending', - (done) => { + it('runs nested tasks in order across idle periods', async () => { stubProperty(document, 'visibilityState').value('visible'); - const spy1 = sandbox.spy(); - const spy2 = sandbox.spy(); - const spy3 = sandbox.spy(); - const queue = new IdleQueue(tracker); + const spy1 = blockingSpy(5); + const spy2 = blockingSpy(45); + const spy3 = blockingSpy(5); + const spy4 = blockingSpy(45); + const spy5 = blockingSpy(5); + const spy6 = blockingSpy(45); + + const queue = new IdleQueue(); queue.addCallback(spy1); - queue.addCallback(spy2); - assert(spy1.notCalled); - assert(spy2.notCalled); + queue.addCallback(() => { + queue.addCallback(() => { + spy4(); + queue.addCallback(spy6); + }); + spy2(); + }); + queue.addCallback(() => { + queue.addCallback(spy5); + spy3(); + }); - stubProperty(document, 'visibilityState').value('hidden'); - dispatch(document, 'visibilitychange', self); + await when(() => spy6.calledOnce); - queueMicrotask(() => { - assert(spy1.calledOnce); - assert(spy2.calledOnce); - assert(spy3.notCalled); + assert(spy1.calledOnce); + assert(spy1.calledBefore(spy2)); + assert(spy2.calledOnce); + assert(spy2.calledBefore(spy3)); + assert(spy3.calledOnce); + assert(spy3.calledBefore(spy4)); + assert(spy4.calledOnce); + assert(spy4.calledBefore(spy5)); + assert(spy5.calledOnce); + assert(spy5.calledBefore(spy6)); + assert(spy6.calledOnce); - queue.addCallback(spy3); + queue.destroy(); + }); - queueMicrotask(() => { - assert(spy3.calledOnce); + it('handles changes in visibilityState while the queue is pending', + async () => { + stubProperty(document, 'visibilityState').value('visible'); + + const spy1 = blockingSpy(5); + const spy2 = blockingSpy(45); + const spy3 = blockingSpy(5); + const spy4 = blockingSpy(45); + const spy5 = blockingSpy(5); + const spy6 = blockingSpy(45); - queue.destroy(); - done(); + const queue = new IdleQueue(); + + queue.addCallback(spy1); + queue.addCallback(() => { + queue.addCallback(() => { + spy4(); + queue.addCallback(spy6); }); + spy2(); + }); + queue.addCallback(() => { + queue.addCallback(spy5); + spy3(); }); + + // This should run at some point in the middle of the 6 spies running. + // Ensure that the remaining spies are called immediately. + rIC(() => { + assert(spy1.calledOnce); + assert(spy6.notCalled); + + dispatch(window, 'beforeunload'); + + if (isSafari()) { + assert(spy6.calledOnce); + } else { + assert(spy6.notCalled); + } + + stubProperty(document, 'visibilityState').value('hidden'); + dispatch(document, 'visibilitychange'); + + assert(spy6.calledOnce); + }); + + await when(() => spy6.calledOnce); + + assert(spy1.calledOnce); + assert(spy1.calledBefore(spy2)); + assert(spy2.calledOnce); + assert(spy2.calledBefore(spy3)); + assert(spy3.calledOnce); + assert(spy3.calledBefore(spy4)); + assert(spy4.calledOnce); + assert(spy4.calledBefore(spy5)); + assert(spy5.calledOnce); + assert(spy5.calledBefore(spy6)); + assert(spy6.calledOnce); + + queue.destroy(); }); - it('does not run queued tasks twice after a visibilitychange', (done) => { + it('does not run queued tasks twice after a visibilitychange', async () => { stubProperty(document, 'visibilityState').value('visible'); - const spy1 = sandbox.spy(); - const spy2 = sandbox.spy(); - const queue = new IdleQueue(tracker); + const spy1 = sinon.spy(); + const spy2 = sinon.spy(); + const queue = new IdleQueue(); queue.addCallback(spy1); queue.addCallback(spy2); assert(spy1.notCalled); assert(spy2.notCalled); + dispatch(window, 'beforeunload'); + stubProperty(document, 'visibilityState').value('hidden'); - dispatch(document, 'visibilitychange', self); + dispatch(document, 'visibilitychange'); - queueMicrotask(() => { - assert(spy1.calledOnce); - assert(spy2.calledOnce); + assert(spy1.calledOnce); + assert(spy2.calledOnce); - // Wait until the next idle point to assert the tasks weren't re-called. - rIC(() => { - assert(spy1.calledOnce); - assert(spy2.calledOnce); + // Wait until the next idle point to assert the tasks weren't re-called. + await nextIdleCallback(); - queue.destroy(); - done(); - }); - }); + assert(spy1.calledOnce); + assert(spy2.calledOnce); + + queue.destroy(); }); }); @@ -358,30 +454,21 @@ describe('IdleQueue', () => { it('removes all added listeners', () => { sandbox.spy(self, 'removeEventListener'); - const queue = new IdleQueue(tracker); + const queue = new IdleQueue(); assert(self.removeEventListener.notCalled); queue.destroy(); + assert(self.removeEventListener.calledWith( + 'visibilitychange', sinon.match.func, true)); + if (isSafari()) { - assert(self.removeEventListener.calledTwice); - assert(self.removeEventListener.calledWith( + assert(window.removeEventListener.calledWith( 'beforeunload', sinon.match.func, true)); } else { - assert(self.removeEventListener.calledOnce); + assert(!window.removeEventListener.calledWith( + 'beforeunload', sinon.match.func, true)); } - assert(self.removeEventListener.calledWith( - 'visibilitychange', sinon.match.func, true)); - }); - - it('reverts overridden methods', () => { - const originalSendMethod = tracker.send; - - const queue = new IdleQueue(tracker); - assert.notEqual(originalSendMethod, tracker.send); - - queue.destroy(); - assert.strictEqual(originalSendMethod, tracker.send); }); }); }); diff --git a/test/unit/tracker-queue-test.js b/test/unit/tracker-queue-test.js new file mode 100644 index 00000000..2709cb50 --- /dev/null +++ b/test/unit/tracker-queue-test.js @@ -0,0 +1,171 @@ +/** + * Copyright 2017 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 {blockingSpy, stubProperty, when} from './idle-queue-test'; +import TrackerQueue from '../../lib/tracker-queue'; + +const sandbox = sinon.createSandbox(); +let tracker; +let hits; + +const getFields = (overrides = {}) => { + return Object.assign({}, { + trackingId: 'UA-12345-1', + cookieDomain: 'auto', + siteSpeedSampleRate: 0, + }, overrides); +}; + +describe('TrackerQueue', () => { + beforeEach((done) => { + sandbox.restore(); + + hits = []; + window.ga('create', getFields()); + window.ga((t) => { + tracker = t; + const originalSendHitTask = tracker.get('sendHitTask'); + tracker.set('sendHitTask', (model) => { + const query = {}; + const hitPayload = model.get('hitPayload'); + hitPayload.split('&').forEach((entry) => { + const [key, value] = entry.split('='); + query[decodeURIComponent(key)] = decodeURIComponent(value); + }); + + hits.push(query); + originalSendHitTask(model); + }); + + done(); + }); + }); + + afterEach(() => { + sandbox.restore(); + window.ga('remove'); + }); + + describe('static getOrCreate', () => { + it('does not create more than one instance per tracking ID', () => { + const queue1 = TrackerQueue.getOrCreate(tracker); + const queue2 = TrackerQueue.getOrCreate(tracker); + + assert.strictEqual(queue1, queue2); + + queue1.destroy(); + queue2.destroy(); // Not really needed. + }); + }); + + describe('constructor', () => { + it('overrides and defers the tracker.send method', async () => { + stubProperty(document, 'visibilityState').value('visible'); + + const originalSendMethod = tracker.send; + const queue = new TrackerQueue(tracker); + + // Creating the queue should have overridden the `send()` method. + assert.notEqual(originalSendMethod, tracker.send); + + const spy1 = blockingSpy(5); + const spy2 = blockingSpy(45); + const spy3 = blockingSpy(5); + const spy4 = blockingSpy(45); + + // Add blocking spies to the beginning of the queue to ensure it + // requests additional idle callbacks. Otherwise the callbacks will be + // invoked sync and it won't test that tracker.send was really deferred. + queue.addCallback(spy1); + queue.addCallback(spy2); + queue.addCallback(spy3); + queue.addCallback(spy4); + + queue.addCallback(() => { + tracker.set('dimension1', 'A'); + tracker.set('dimension2', 'A'); + }); + queue.addCallback(() => { + tracker.set('dimension2', 'B'); + tracker.set('dimension3', 'B'); + }); + + // This normally runs sync, but when creating the idle queue it + // automatically queues the first `send()` call to allow plugin creation + // logic to finish before any hits are sent. + tracker.send('pageview'); + tracker.send('pageview'); + + await when(() => hits.length == 2); + + // The queue dimension set should be found on this hit + assert.strictEqual(hits.length, 2); + assert.strictEqual(hits[0].cd1, 'A'); + assert.strictEqual(hits[0].cd2, 'B'); + assert.strictEqual(hits[0].cd3, 'B'); + assert.strictEqual(hits[1].cd1, 'A'); + assert.strictEqual(hits[1].cd2, 'B'); + assert.strictEqual(hits[1].cd3, 'B'); + + // The send method should now be restored. + assert.strictEqual(originalSendMethod, tracker.send); + + tracker.set('dimension3', 'C'); + tracker.send('pageview'); + + // Since the `send()` command has been restored, it should run sync. + assert.strictEqual(hits.length, 3); + assert.strictEqual(hits[2].cd1, 'A'); + assert.strictEqual(hits[2].cd2, 'B'); + assert.strictEqual(hits[2].cd3, 'C'); + + queue.destroy(); + }); + + it('immediately restores the send method if the queue is empty', () => { + const originalSendMethod = tracker.send; + const queue = new TrackerQueue(tracker); + + // Creating the queue should have overridden the `send()` method. + assert.notEqual(originalSendMethod, tracker.send); + + tracker.set('dimension1', 'A'); + + // Since there's nothing in the idle queue, this should happen sync + // and the overridden method should be immediately restored. + tracker.send('pageview'); + + assert.strictEqual(originalSendMethod, tracker.send); + assert.strictEqual(hits.length, 1); + assert.strictEqual(hits[0].cd1, 'A'); + + queue.destroy(); + }); + }); + + describe('destroy', () => { + it('reverts overridden methods', () => { + const originalSendMethod = tracker.send; + + const queue = new TrackerQueue(tracker); + assert.notEqual(originalSendMethod, tracker.send); + + queue.destroy(); + assert.strictEqual(originalSendMethod, tracker.send); + }); + }); +}); From 336830e4d65bbaa9883d2271e30433984cbccbfe Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Sat, 11 Aug 2018 11:20:45 -0700 Subject: [PATCH 29/71] Simplify Idle and Tracker queue method names Also add support for adding an array of tasks. --- lib/idle-queue.js | 68 ++++++++++++--------- lib/tracker-queue.js | 16 ++--- test/unit/idle-queue-test.js | 105 +++++++++++++++++++------------- test/unit/tracker-queue-test.js | 14 ++--- 4 files changed, 118 insertions(+), 85 deletions(-) diff --git a/lib/idle-queue.js b/lib/idle-queue.js index a84a67c0..77d843f6 100644 --- a/lib/idle-queue.js +++ b/lib/idle-queue.js @@ -29,12 +29,12 @@ export default class IdleQueue { */ constructor() { this.idleCallbackHandle_ = null; - this.callbackQueue_ = []; + this.taskQueue_ = []; // Bind methods this.onVisibilityChange_ = this.onVisibilityChange_.bind(this); this.onBeforeUnload_ = this.onBeforeUnload_.bind(this); - this.processCallbacks_ = this.processCallbacks_.bind(this); + this.processTasks_ = this.processTasks_.bind(this); addEventListener('visibilitychange', this.onVisibilityChange_, true); @@ -51,16 +51,22 @@ export default class IdleQueue { } /** - * @param {!Function} callback + * @param {!Array|!Function} tasks */ - addCallback(callback) { + add(tasks) { + // Support single functions or arrays of functions. + if (typeof tasks === 'function') tasks = [tasks]; + const state = { time: Date.now(), visibilityState: document.visibilityState, }; - this.callbackQueue_.push({state, callback}); - this.scheduleCallbackProcessing_(); + for (const task of tasks) { + this.taskQueue_.push({state, task}); + } + + this.scheduleTaskProcessing_(); } /** @@ -68,8 +74,8 @@ export default class IdleQueue { * removing any overridden methods. */ destroy() { - this.callbackQueue_ = []; - this.cancelScheduledCallbackProcessing_(); + this.taskQueue_ = []; + this.cancelScheduledTaskProcessing_(); removeEventListener('visibilitychange', this.onVisibilityChange_, true); @@ -87,47 +93,53 @@ export default class IdleQueue { } /** - * Schedules the callback queue to be processed. If the document is in the + * Schedules the task queue to be processed. If the document is in the * hidden state, they queue is scheduled as a microtask so it can be run * in cases where a macrotask couldn't (like if the page is unloading). If * the document is in the visible state, `requestIdleCallback` is used. */ - scheduleCallbackProcessing_() { + scheduleTaskProcessing_() { if (document.visibilityState === 'hidden') { - queueMicrotask(this.processCallbacks_); + queueMicrotask(this.processTasks_); } else { if (!this.idleCallbackHandle_) { - this.idleCallbackHandle_ = rIC(this.processCallbacks_); + this.idleCallbackHandle_ = rIC(this.processTasks_); } } } /** - * Processes as many callbacks in the queue as it can before reaching the - * deadline. If no deadline is passed, it will process all callbacks + * Processes as many tasks in the queue as it can before reaching the + * deadline. If no deadline is passed, it will process all tasks * immediately. If an `IdleDeadline` object is passed (as is with - * `requestIdleCallback`) then the callbacks are processed until there's + * `requestIdleCallback`) then the tasks are processed until there's * no time remaining. * @param {IdleDeadline|undefined} deadline */ - processCallbacks_(deadline) { - this.cancelScheduledCallbackProcessing_(); + processTasks_(deadline) { + this.cancelScheduledTaskProcessing_(); + + // Process tasks until there's none left or the deadline has passed. + while (this.taskQueue_.length > 0 && deadlineNotPassed(deadline)) { + const {task, state} = this.taskQueue_.shift(); + + // Expose the current state to external code. + this.state_ = this; - // Process callbacks until there's none left or the deadline has passed. - while (this.callbackQueue_.length > 0 && deadlineNotPassed(deadline)) { - const {callback, state} = this.callbackQueue_.shift(); - callback(state); + task(state); } + // State should only be exposed while tasks are processing. + this.state_ = null; - if (this.callbackQueue_.length > 0) { - this.scheduleCallbackProcessing_(); + if (this.taskQueue_.length > 0) { + this.scheduleTaskProcessing_(); } } /** * Cancels any scheduled idle callback and removes the handler (if set). */ - cancelScheduledCallbackProcessing_() { + cancelScheduledTaskProcessing_() { cIC(this.idleCallbackHandle_); this.idleCallbackHandle_ = null; } @@ -138,17 +150,17 @@ export default class IdleQueue { */ onVisibilityChange_() { if (document.visibilityState === 'hidden') { - this.processCallbacks_(); + this.processTasks_(); } } /** * A callback for the `beforeunload` event than runs all pending callbacks - * immediately. The reason this is used instead of adding `processCallbacks_` - * directly is we can't invoke `processCallbacks_` with an `Event` object. + * immediately. The reason this is used instead of adding `processTasks_` + * directly is we can't invoke `processTasks_` with an `Event` object. */ onBeforeUnload_() { - this.processCallbacks_(); + this.processTasks_(); } } diff --git a/lib/tracker-queue.js b/lib/tracker-queue.js index 91b77687..7bd43d11 100644 --- a/lib/tracker-queue.js +++ b/lib/tracker-queue.js @@ -48,7 +48,7 @@ export default class TrackerQueue extends IdleQueue { super(); this.tracker_ = tracker; - this.beforeSendCallbackQueue_ = []; + this.deferredSendCallbacks_ = []; // Bind methods this.trackerSendOverride_ = this.trackerSendOverride_.bind(this); @@ -61,10 +61,10 @@ export default class TrackerQueue extends IdleQueue { * the first time the queue is empty. * @param {...*} args The arguments to be passed to the handler. */ - processCallbacks_(...args) { - super.processCallbacks_(...args); + processTasks_(...args) { + super.processTasks_(...args); - if (this.callbackQueue_.length === 0 && + if (this.taskQueue_.length === 0 && this.trackerSendOverride_ !== null) { this.removeTrackerSendOverride_(); } @@ -86,11 +86,11 @@ export default class TrackerQueue extends IdleQueue { */ trackerSendOverride_(originalMethod) { return (...args) => { - if (this.callbackQueue_.length === 0) { + if (this.taskQueue_.length === 0) { this.removeTrackerSendOverride_(); originalMethod(...args); } else { - this.beforeSendCallbackQueue_.push(() => { + this.deferredSendCallbacks_.push(() => { originalMethod(...args); }); } @@ -104,8 +104,8 @@ export default class TrackerQueue extends IdleQueue { MethodChain.remove(this.tracker_, 'send', this.trackerSendOverride_); this.trackerSendOverride_ = null; - this.beforeSendCallbackQueue_.forEach((callback) => callback()); - this.beforeSendCallbackQueue_ = []; + this.deferredSendCallbacks_.forEach((callback) => callback()); + this.deferredSendCallbacks_ = []; } } diff --git a/test/unit/idle-queue-test.js b/test/unit/idle-queue-test.js index 8b1701ea..e2df9742 100644 --- a/test/unit/idle-queue-test.js +++ b/test/unit/idle-queue-test.js @@ -73,8 +73,7 @@ describe('IdleQueue', () => { }); describe('constructor', () => { - it('adds lifecycle event listeners that process callbacks immediately', - () => { + it('adds lifecycle event listeners that process tasks immediately', () => { sandbox.spy(window, 'addEventListener'); const queue = new IdleQueue(); @@ -95,8 +94,8 @@ describe('IdleQueue', () => { const spy1 = sinon.spy(); const spy2 = sinon.spy(); - queue.addCallback(spy1); - queue.addCallback(spy2); + queue.add(spy1); + queue.add(spy2); dispatch(window, 'beforeunload'); if (isSafari()) { @@ -111,9 +110,9 @@ describe('IdleQueue', () => { const spy4 = sinon.spy(); const spy5 = sinon.spy(); - queue.addCallback(spy3); - queue.addCallback(spy4); - queue.addCallback(spy5); + queue.add(spy3); + queue.add(spy4); + queue.add(spy5); stubProperty(document, 'visibilityState').value('hidden'); dispatch(document, 'visibilitychange'); @@ -126,7 +125,7 @@ describe('IdleQueue', () => { }); }); - describe('addCallback', () => { + describe('add', () => { it('queues a task to run when idle', async () => { stubProperty(document, 'visibilityState').value('visible'); @@ -136,9 +135,39 @@ describe('IdleQueue', () => { const queue = new IdleQueue(); - queue.addCallback(spy1); - queue.addCallback(spy2); - queue.addCallback(spy3); + queue.add(spy1); + queue.add(spy2); + queue.add(spy3); + + assert(spy1.notCalled); + assert(spy2.notCalled); + assert(spy3.notCalled); + + await nextIdleCallback(); + + // At this point at least one of the spies should have been called, but + // not necessarily all of them (it depends on the idle time remaining). + assert(spy1.calledOnce); + + await when(() => spy3.calledOnce); + + assert(spy1.calledOnce); + assert(spy2.calledOnce); + assert(spy3.calledOnce); + + queue.destroy(); + }); + + it('supports passing an array of tasks', async () => { + stubProperty(document, 'visibilityState').value('visible'); + + const spy1 = sinon.spy(); + const spy2 = sinon.spy(); + const spy3 = sinon.spy(); + + const queue = new IdleQueue(); + + queue.add([spy1, spy2, spy3]); assert(spy1.notCalled); assert(spy2.notCalled); @@ -171,10 +200,7 @@ describe('IdleQueue', () => { const queue = new IdleQueue(); - queue.addCallback(spy1); - queue.addCallback(spy2); - queue.addCallback(spy3); - queue.addCallback(spy4); + queue.add([spy1, spy2, spy3, spy4]); // rICSpy is queued after the 4 spies above, // but it should run at some point between them. @@ -209,7 +235,7 @@ describe('IdleQueue', () => { queue.destroy(); }); - it('runs the callback as a microtask when in the hidden state', + it('runs the task as a microtask when in the hidden state', async () => { stubProperty(document, 'visibilityState').value('hidden'); @@ -219,9 +245,7 @@ describe('IdleQueue', () => { const queue = new IdleQueue(); - queue.addCallback(spy1); - queue.addCallback(spy2); - queue.addCallback(spy3); + queue.add([spy1, spy2, spy3]); assert(spy1.notCalled); assert(spy2.notCalled); @@ -245,9 +269,7 @@ describe('IdleQueue', () => { const spy3 = sinon.spy(); const queue = new IdleQueue(); - queue.addCallback(spy1); - queue.addCallback(spy2); - queue.addCallback(spy3); + queue.add([spy1, spy2, spy3]); assert(spy1.notCalled); assert(spy2.notCalled); @@ -281,16 +303,16 @@ describe('IdleQueue', () => { const queue = new IdleQueue(); - queue.addCallback(spy1); - queue.addCallback(() => { - queue.addCallback(() => { + queue.add(spy1); + queue.add(() => { + queue.add(() => { spy4(); - queue.addCallback(spy6); + queue.add(spy6); }); spy2(); }); - queue.addCallback(() => { - queue.addCallback(spy5); + queue.add(() => { + queue.add(spy5); spy3(); }); @@ -327,16 +349,16 @@ describe('IdleQueue', () => { const queue = new IdleQueue(); - queue.addCallback(spy1); - queue.addCallback(() => { - queue.addCallback(() => { + queue.add(spy1); + queue.add(() => { + queue.add(() => { spy4(); - queue.addCallback(spy6); + queue.add(spy6); }); spy2(); }); - queue.addCallback(() => { - queue.addCallback(spy5); + queue.add(() => { + queue.add(spy5); spy3(); }); @@ -370,16 +392,16 @@ describe('IdleQueue', () => { const queue = new IdleQueue(); - queue.addCallback(spy1); - queue.addCallback(() => { - queue.addCallback(() => { + queue.add(spy1); + queue.add(() => { + queue.add(() => { spy4(); - queue.addCallback(spy6); + queue.add(spy6); }); spy2(); }); - queue.addCallback(() => { - queue.addCallback(spy5); + queue.add(() => { + queue.add(spy5); spy3(); }); @@ -427,8 +449,7 @@ describe('IdleQueue', () => { const spy2 = sinon.spy(); const queue = new IdleQueue(); - queue.addCallback(spy1); - queue.addCallback(spy2); + queue.add([spy1, spy2]); assert(spy1.notCalled); assert(spy2.notCalled); diff --git a/test/unit/tracker-queue-test.js b/test/unit/tracker-queue-test.js index 2709cb50..8f164c02 100644 --- a/test/unit/tracker-queue-test.js +++ b/test/unit/tracker-queue-test.js @@ -88,18 +88,18 @@ describe('TrackerQueue', () => { const spy4 = blockingSpy(45); // Add blocking spies to the beginning of the queue to ensure it - // requests additional idle callbacks. Otherwise the callbacks will be + // requests additional idle callbacks. Otherwise the tasks will be // invoked sync and it won't test that tracker.send was really deferred. - queue.addCallback(spy1); - queue.addCallback(spy2); - queue.addCallback(spy3); - queue.addCallback(spy4); + queue.add(spy1); + queue.add(spy2); + queue.add(spy3); + queue.add(spy4); - queue.addCallback(() => { + queue.add(() => { tracker.set('dimension1', 'A'); tracker.set('dimension2', 'A'); }); - queue.addCallback(() => { + queue.add(() => { tracker.set('dimension2', 'B'); tracker.set('dimension3', 'B'); }); From 3906b780a082d3713ed8bd2d735acb204f86f720 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Sun, 12 Aug 2018 23:25:05 -0700 Subject: [PATCH 30/71] Add an isProcessing flag and simplify logic --- lib/idle-queue.js | 47 ++++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/lib/idle-queue.js b/lib/idle-queue.js index 77d843f6..843c3eb6 100644 --- a/lib/idle-queue.js +++ b/lib/idle-queue.js @@ -30,6 +30,7 @@ export default class IdleQueue { constructor() { this.idleCallbackHandle_ = null; this.taskQueue_ = []; + this.isProcessing_ = false; // Bind methods this.onVisibilityChange_ = this.onVisibilityChange_.bind(this); @@ -110,29 +111,33 @@ export default class IdleQueue { /** * Processes as many tasks in the queue as it can before reaching the - * deadline. If no deadline is passed, it will process all tasks - * immediately. If an `IdleDeadline` object is passed (as is with - * `requestIdleCallback`) then the tasks are processed until there's - * no time remaining. + * deadline. If no deadline is passed, it will process all tasks. + * If an `IdleDeadline` object is passed (as is with `requestIdleCallback`) + * then the tasks are processed until there's no time remaining, at which + * we yield to input or other script and wait until the next idle time. * @param {IdleDeadline|undefined} deadline */ processTasks_(deadline) { this.cancelScheduledTaskProcessing_(); - // Process tasks until there's none left or the deadline has passed. - while (this.taskQueue_.length > 0 && deadlineNotPassed(deadline)) { - const {task, state} = this.taskQueue_.shift(); + if (!this.isProcessing_) { + this.isProcessing_ = true; - // Expose the current state to external code. - this.state_ = this; + // Process tasks until there's none left or we need to yield to input. + while (this.taskQueue_.length > 0 && !shouldYield(deadline)) { + const {task, state} = this.taskQueue_.shift(); - task(state); - } - // State should only be exposed while tasks are processing. - this.state_ = null; + // Expose the current state to external code. + // this.state_ = this; + + task(state); + } + // this.state_ = null; + this.isProcessing_ = false; - if (this.taskQueue_.length > 0) { - this.scheduleTaskProcessing_(); + if (this.taskQueue_.length > 0) { + this.scheduleTaskProcessing_(); + } } } @@ -165,11 +170,15 @@ export default class IdleQueue { } /** - * Returns true if there's no deadline or if there is a deadline but it has - * not passed. + * Returns true if the IdleDealine object exists and shows no time remaining. + * Otherwise returns false. * @param {IdleDeadline|undefined} deadline * @return {boolean} */ -const deadlineNotPassed = (deadline) => { - return !deadline || deadline.timeRemaining() > 0; +const shouldYield = (deadline) => { + if (!deadline) { + return false; + } else { + return deadline.timeRemaining() === 0; + } }; From daaab2a056d9276b544a40bd5bd2431582de4e43 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Sun, 12 Aug 2018 23:44:36 -0700 Subject: [PATCH 31/71] Clean up tests and use sandbox --- test/unit/idle-queue-test.js | 77 +++++++++++++++--------------------- 1 file changed, 32 insertions(+), 45 deletions(-) diff --git a/test/unit/idle-queue-test.js b/test/unit/idle-queue-test.js index e2df9742..31b6e476 100644 --- a/test/unit/idle-queue-test.js +++ b/test/unit/idle-queue-test.js @@ -28,7 +28,7 @@ const sandbox = sinon.createSandbox(); * @param {*} value * @return {{value: !Function}} */ -export const stubProperty = (obj, prop, value) => { +const stubProperty = (obj, prop, value) => { if (!obj.hasOwnProperty(prop)) { return { value: (value) => { @@ -40,7 +40,7 @@ export const stubProperty = (obj, prop, value) => { } }; -export const blockingSpy = (ms) => { +const blockingSpy = (ms) => { return sandbox.stub().callsFake(() => { const startTime = performance.now(); while (performance.now() - startTime < ms) { @@ -49,7 +49,7 @@ export const blockingSpy = (ms) => { }); }; -export const when = async (fn, intervalMillis = 100, retries = 20) => { +const when = async (fn, intervalMillis = 100, retries = 20) => { for (let i = 0; i < retries; i++) { const result = await fn(); if (result) { @@ -66,6 +66,7 @@ const nextIdleCallback = () => new Promise((res) => rIC(res)); describe('IdleQueue', () => { beforeEach(() => { sandbox.restore(); + stubProperty(document, 'visibilityState').value('visible'); }); afterEach(() => { @@ -89,10 +90,8 @@ describe('IdleQueue', () => { 'beforeunload', sinon.match.func, true)); } - stubProperty(document, 'visibilityState').value('visible'); - - const spy1 = sinon.spy(); - const spy2 = sinon.spy(); + const spy1 = sandbox.spy(); + const spy2 = sandbox.spy(); queue.add(spy1); queue.add(spy2); @@ -106,9 +105,9 @@ describe('IdleQueue', () => { assert(spy2.notCalled); } - const spy3 = sinon.spy(); - const spy4 = sinon.spy(); - const spy5 = sinon.spy(); + const spy3 = sandbox.spy(); + const spy4 = sandbox.spy(); + const spy5 = sandbox.spy(); queue.add(spy3); queue.add(spy4); @@ -117,9 +116,9 @@ describe('IdleQueue', () => { stubProperty(document, 'visibilityState').value('hidden'); dispatch(document, 'visibilitychange'); - assert(spy1.calledOnce); - assert(spy2.calledOnce); assert(spy3.calledOnce); + assert(spy4.calledOnce); + assert(spy5.calledOnce); queue.destroy(); }); @@ -127,11 +126,9 @@ describe('IdleQueue', () => { describe('add', () => { it('queues a task to run when idle', async () => { - stubProperty(document, 'visibilityState').value('visible'); - - const spy1 = sinon.spy(); - const spy2 = sinon.spy(); - const spy3 = sinon.spy(); + const spy1 = sandbox.spy(); + const spy2 = sandbox.spy(); + const spy3 = sandbox.spy(); const queue = new IdleQueue(); @@ -159,11 +156,9 @@ describe('IdleQueue', () => { }); it('supports passing an array of tasks', async () => { - stubProperty(document, 'visibilityState').value('visible'); - - const spy1 = sinon.spy(); - const spy2 = sinon.spy(); - const spy3 = sinon.spy(); + const spy1 = sandbox.spy(); + const spy2 = sandbox.spy(); + const spy3 = sandbox.spy(); const queue = new IdleQueue(); @@ -190,13 +185,11 @@ describe('IdleQueue', () => { it('waits until the next idle period if all tasks cannot finish', async () => { - stubProperty(document, 'visibilityState').value('visible'); - const spy1 = blockingSpy(5); const spy2 = blockingSpy(45); const spy3 = blockingSpy(5); const spy4 = blockingSpy(5); - const rICSpy = sinon.spy(); + const rICSpy = sandbox.spy(); const queue = new IdleQueue(); @@ -239,9 +232,9 @@ describe('IdleQueue', () => { async () => { stubProperty(document, 'visibilityState').value('hidden'); - const spy1 = sinon.spy(); - const spy2 = sinon.spy(); - const spy3 = sinon.spy(); + const spy1 = sandbox.spy(); + const spy2 = sandbox.spy(); + const spy3 = sandbox.spy(); const queue = new IdleQueue(); @@ -264,9 +257,9 @@ describe('IdleQueue', () => { const testQueueOrder = async (visibilityState) => { stubProperty(document, 'visibilityState').value(visibilityState); - const spy1 = sinon.spy(); - const spy2 = sinon.spy(); - const spy3 = sinon.spy(); + const spy1 = sandbox.spy(); + const spy2 = sandbox.spy(); + const spy3 = sandbox.spy(); const queue = new IdleQueue(); queue.add([spy1, spy2, spy3]); @@ -294,12 +287,12 @@ describe('IdleQueue', () => { const testQueueOrder = async (visibilityState) => { stubProperty(document, 'visibilityState').value(visibilityState); - const spy1 = sinon.spy(); - const spy2 = sinon.spy(); - const spy3 = sinon.spy(); - const spy4 = sinon.spy(); - const spy5 = sinon.spy(); - const spy6 = sinon.spy(); + const spy1 = sandbox.spy(); + const spy2 = sandbox.spy(); + const spy3 = sandbox.spy(); + const spy4 = sandbox.spy(); + const spy5 = sandbox.spy(); + const spy6 = sandbox.spy(); const queue = new IdleQueue(); @@ -338,8 +331,6 @@ describe('IdleQueue', () => { }); it('runs nested tasks in order across idle periods', async () => { - stubProperty(document, 'visibilityState').value('visible'); - const spy1 = blockingSpy(5); const spy2 = blockingSpy(45); const spy3 = blockingSpy(5); @@ -381,8 +372,6 @@ describe('IdleQueue', () => { it('handles changes in visibilityState while the queue is pending', async () => { - stubProperty(document, 'visibilityState').value('visible'); - const spy1 = blockingSpy(5); const spy2 = blockingSpy(45); const spy3 = blockingSpy(5); @@ -443,10 +432,8 @@ describe('IdleQueue', () => { }); it('does not run queued tasks twice after a visibilitychange', async () => { - stubProperty(document, 'visibilityState').value('visible'); - - const spy1 = sinon.spy(); - const spy2 = sinon.spy(); + const spy1 = sandbox.spy(); + const spy2 = sandbox.spy(); const queue = new IdleQueue(); queue.add([spy1, spy2]); From 7dc97500a169db027af9479cd24ce471c6b14c76 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Sun, 12 Aug 2018 23:45:39 -0700 Subject: [PATCH 32/71] Remove tracker method overrides --- lib/tracker-queue.js | 99 +++--------------------- test/unit/tracker-queue-test.js | 133 ++++---------------------------- 2 files changed, 25 insertions(+), 207 deletions(-) diff --git a/lib/tracker-queue.js b/lib/tracker-queue.js index 7bd43d11..11e1faa6 100644 --- a/lib/tracker-queue.js +++ b/lib/tracker-queue.js @@ -14,98 +14,23 @@ * limitations under the License. */ -import MethodChain from './method-chain'; import IdleQueue from './idle-queue'; + const instances = {}; /** - * A class wraps a queue of requestIdleCallback functions for two reasons: - * 1. So other callers can know whether or not the queue is empty. - *. 2. So we can provide some guarantees that the queued functions will - *. run in unload-type situations. + * Gets an existing instance for the passed arguments or creates a new + * instance if one doesn't exist. + * @param {!Tracker} tracker An analytics.js tracker object. + * @return {Session} The Session instance. */ -export default class TrackerQueue extends IdleQueue { - /** - * Gets an existing instance for the passed arguments or creates a new - * instance if one doesn't exist. - * @param {!Tracker} tracker An analytics.js tracker object. - * @return {Session} The Session instance. - */ - static getOrCreate(tracker) { - // Don't create multiple instances for the same property. - const trackingId = tracker.get('trackingId'); - if (!instances[trackingId]) { - instances[trackingId] = new IdleQueue(); - } - return instances[trackingId]; - } - - /** - * @param {!Tracker} tracker An analytics.js tracker object. - */ - constructor(tracker) { - super(); - - this.tracker_ = tracker; - this.deferredSendCallbacks_ = []; - - // Bind methods - this.trackerSendOverride_ = this.trackerSendOverride_.bind(this); - - MethodChain.add(this.tracker_, 'send', this.trackerSendOverride_); - } - - /** - * Adds logic to the superclass method to remove the tracker.send override - * the first time the queue is empty. - * @param {...*} args The arguments to be passed to the handler. - */ - processTasks_(...args) { - super.processTasks_(...args); - - if (this.taskQueue_.length === 0 && - this.trackerSendOverride_ !== null) { - this.removeTrackerSendOverride_(); - } - } - - /** - * Destroys the instance by unregistering all added event listeners and - * removing any overridden methods. - */ - destroy() { - super.destroy(); - MethodChain.remove(this.tracker_, 'send', this.trackerSendOverride_); - } - - /** - * Generates an override for the `tracker.send()` method. - * @param {!Function} originalMethod - * @return {!Function} - */ - trackerSendOverride_(originalMethod) { - return (...args) => { - if (this.taskQueue_.length === 0) { - this.removeTrackerSendOverride_(); - originalMethod(...args); - } else { - this.deferredSendCallbacks_.push(() => { - originalMethod(...args); - }); - } - }; - } - - /** - * Restores the `tracker.send()` override function. - */ - removeTrackerSendOverride_() { - MethodChain.remove(this.tracker_, 'send', this.trackerSendOverride_); - this.trackerSendOverride_ = null; - - this.deferredSendCallbacks_.forEach((callback) => callback()); - this.deferredSendCallbacks_ = []; +export const getOrCreateTrackerQueue = (tracker) => { + // Don't create multiple instances for the same property. + const trackingId = tracker.get('trackingId'); + if (!instances[trackingId]) { + instances[trackingId] = new IdleQueue(); } -} + return instances[trackingId]; +}; diff --git a/test/unit/tracker-queue-test.js b/test/unit/tracker-queue-test.js index 8f164c02..a64a3e7b 100644 --- a/test/unit/tracker-queue-test.js +++ b/test/unit/tracker-queue-test.js @@ -14,13 +14,11 @@ * limitations under the License. */ - -import {blockingSpy, stubProperty, when} from './idle-queue-test'; -import TrackerQueue from '../../lib/tracker-queue'; + import IdleQueue from '../../lib/idle-queue'; +import {getOrCreateTrackerQueue} from '../../lib/tracker-queue'; const sandbox = sinon.createSandbox(); let tracker; -let hits; const getFields = (overrides = {}) => { return Object.assign({}, { @@ -30,27 +28,13 @@ const getFields = (overrides = {}) => { }, overrides); }; -describe('TrackerQueue', () => { +describe('getOrCreateTrackerQueue', () => { beforeEach((done) => { sandbox.restore(); - hits = []; window.ga('create', getFields()); window.ga((t) => { tracker = t; - const originalSendHitTask = tracker.get('sendHitTask'); - tracker.set('sendHitTask', (model) => { - const query = {}; - const hitPayload = model.get('hitPayload'); - hitPayload.split('&').forEach((entry) => { - const [key, value] = entry.split('='); - query[decodeURIComponent(key)] = decodeURIComponent(value); - }); - - hits.push(query); - originalSendHitTask(model); - }); - done(); }); }); @@ -60,112 +44,21 @@ describe('TrackerQueue', () => { window.ga('remove'); }); - describe('static getOrCreate', () => { - it('does not create more than one instance per tracking ID', () => { - const queue1 = TrackerQueue.getOrCreate(tracker); - const queue2 = TrackerQueue.getOrCreate(tracker); + it('creates an instance of IdleQueue for the passed tracker', () => { + const queue = getOrCreateTrackerQueue(tracker); - assert.strictEqual(queue1, queue2); + assert(queue instanceof IdleQueue); - queue1.destroy(); - queue2.destroy(); // Not really needed. - }); + queue.destroy(); }); - describe('constructor', () => { - it('overrides and defers the tracker.send method', async () => { - stubProperty(document, 'visibilityState').value('visible'); - - const originalSendMethod = tracker.send; - const queue = new TrackerQueue(tracker); - - // Creating the queue should have overridden the `send()` method. - assert.notEqual(originalSendMethod, tracker.send); - - const spy1 = blockingSpy(5); - const spy2 = blockingSpy(45); - const spy3 = blockingSpy(5); - const spy4 = blockingSpy(45); - - // Add blocking spies to the beginning of the queue to ensure it - // requests additional idle callbacks. Otherwise the tasks will be - // invoked sync and it won't test that tracker.send was really deferred. - queue.add(spy1); - queue.add(spy2); - queue.add(spy3); - queue.add(spy4); - - queue.add(() => { - tracker.set('dimension1', 'A'); - tracker.set('dimension2', 'A'); - }); - queue.add(() => { - tracker.set('dimension2', 'B'); - tracker.set('dimension3', 'B'); - }); - - // This normally runs sync, but when creating the idle queue it - // automatically queues the first `send()` call to allow plugin creation - // logic to finish before any hits are sent. - tracker.send('pageview'); - tracker.send('pageview'); - - await when(() => hits.length == 2); - - // The queue dimension set should be found on this hit - assert.strictEqual(hits.length, 2); - assert.strictEqual(hits[0].cd1, 'A'); - assert.strictEqual(hits[0].cd2, 'B'); - assert.strictEqual(hits[0].cd3, 'B'); - assert.strictEqual(hits[1].cd1, 'A'); - assert.strictEqual(hits[1].cd2, 'B'); - assert.strictEqual(hits[1].cd3, 'B'); - - // The send method should now be restored. - assert.strictEqual(originalSendMethod, tracker.send); - - tracker.set('dimension3', 'C'); - tracker.send('pageview'); + it('does not create more than one instance per tracking ID', () => { + const queue1 = getOrCreateTrackerQueue(tracker); + const queue2 = getOrCreateTrackerQueue(tracker); - // Since the `send()` command has been restored, it should run sync. - assert.strictEqual(hits.length, 3); - assert.strictEqual(hits[2].cd1, 'A'); - assert.strictEqual(hits[2].cd2, 'B'); - assert.strictEqual(hits[2].cd3, 'C'); + assert.strictEqual(queue1, queue2); - queue.destroy(); - }); - - it('immediately restores the send method if the queue is empty', () => { - const originalSendMethod = tracker.send; - const queue = new TrackerQueue(tracker); - - // Creating the queue should have overridden the `send()` method. - assert.notEqual(originalSendMethod, tracker.send); - - tracker.set('dimension1', 'A'); - - // Since there's nothing in the idle queue, this should happen sync - // and the overridden method should be immediately restored. - tracker.send('pageview'); - - assert.strictEqual(originalSendMethod, tracker.send); - assert.strictEqual(hits.length, 1); - assert.strictEqual(hits[0].cd1, 'A'); - - queue.destroy(); - }); - }); - - describe('destroy', () => { - it('reverts overridden methods', () => { - const originalSendMethod = tracker.send; - - const queue = new TrackerQueue(tracker); - assert.notEqual(originalSendMethod, tracker.send); - - queue.destroy(); - assert.strictEqual(originalSendMethod, tracker.send); - }); + queue1.destroy(); + queue2.destroy(); // Not really needed. }); }); From d6025378fbde60cfbbe2d60525332cb48bbc3920 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Sun, 12 Aug 2018 23:47:46 -0700 Subject: [PATCH 33/71] Defer expensive initializing until accessed --- lib/session.js | 81 ++++++++++++++++++++++++--------------- test/unit/session-test.js | 68 ++++++++++++++++++-------------- 2 files changed, 89 insertions(+), 60 deletions(-) diff --git a/lib/session.js b/lib/session.js index 7172c4be..8dd02829 100644 --- a/lib/session.js +++ b/lib/session.js @@ -72,44 +72,45 @@ export default class Session { this.timeout = timeout || Session.DEFAULT_TIMEOUT; this.timeZone = timeZone; + // This will be set lazily since it's expensive to create. + this.dateTimeFormatter = null; + // Binds methods. this.sendHitTaskOverride = this.sendHitTaskOverride.bind(this); // 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', { - defaults: defaultProps, - timestampKey: 'hitTime', - }); - - // Ensure the session has an ID. - if (!this.store.get().id) { - this.store.set(/** @type {SessionStoreData} */ ({id: uuid()})); + /** + * @return {!Store} + */ + get store() { + if (!this.store_) { + /** @type {SessionStoreData} */ + const defaultProps = { + hitTime: 0, + isExpired: false, + }; + this.store_ = Store.getOrCreate( + this.tracker.get('trackingId'), 'session', { + defaults: defaultProps, + timestampKey: 'hitTime', + }); + + // Ensure the session has an ID. + if (!this.store_.get().id) { + this.store_.set(/** @type {SessionStoreData} */ ({id: uuid()})); + } } + return this.store_; } /** * Returns the ID of the current session. * @return {string} */ - getId() { + get id() { return this.store.get().id; } @@ -129,11 +130,11 @@ 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(); @@ -169,11 +170,12 @@ export default class Session { * @return {boolean} */ datesAreDifferentInTimezone(d1, d2) { - if (!this.dateTimeFormatter) { - return false; + const dateTimeFormatter = getOrCreateDateTimeFormatter(this.timeZone); + + if (dateTimeFormatter) { + return dateTimeFormatter.format(d1) != dateTimeFormatter.format(d2); } else { - return this.dateTimeFormatter.format(d1) - != this.dateTimeFormatter.format(d2); + return false; } } @@ -221,3 +223,20 @@ export default class Session { Session.DEFAULT_TIMEOUT = 30; // minutes + + +const dateTimeFormatterCache_ = {}; +const getOrCreateDateTimeFormatter = (timeZone) => { + if (!(timeZone in dateTimeFormatterCache_)) { + // Some browser doesn't support various features of the + // `Intl.DateTimeFormat` API, so we have to try/catch it. + try { + dateTimeFormatterCache_[timeZone] = + new Intl.DateTimeFormat('en-US', {timeZone}); + } catch (err) { + dateTimeFormatterCache_[timeZone] = null; + } + } + return dateTimeFormatterCache_[timeZone]; +}; + diff --git a/test/unit/session-test.js b/test/unit/session-test.js index e5afaf44..b39ea160 100644 --- a/test/unit/session-test.js +++ b/test/unit/session-test.js @@ -16,7 +16,6 @@ import Session from '../../lib/session'; -import {now} from '../../lib/utilities'; const TRACKING_ID = 'UA-12345-1'; @@ -56,7 +55,7 @@ describe('Session', () => { it('stores a unique ID', () => { const session = new Session(tracker); - assert(session.getId()); + assert(session.id); session.destroy(); }); @@ -66,7 +65,7 @@ describe('Session', () => { 'autotrack:UA-12345-1:session', JSON.stringify({id: 'foo'})); const session = new Session(tracker); - assert.strictEqual(session.getId(), 'foo'); + assert.strictEqual(session.id, 'foo'); session.destroy(); }); @@ -102,11 +101,11 @@ describe('Session', () => { }); }); - describe('getId', () => { + describe('get id', () => { it('returns the stored ID', () => { const session = new Session(tracker); - assert(session.getId()); + assert(session.id); session.destroy(); }); @@ -114,23 +113,29 @@ describe('Session', () => { describe('isExpired', () => { it('returns true if the last hit was too long ago', () => { + const clock = sinon.useFakeTimers({now: 1e12}); + const session = new Session(tracker); + tracker.send('pageview'); + assert(!session.isExpired()); - session.store.set({hitTime: now() - (60 * MINUTES)}); + clock.tick(15 * MINUTES); + assert(!session.isExpired()); + clock.tick(60 * MINUTES); assert(session.isExpired()); - session.store.set({hitTime: now() - (15 * MINUTES)}); - assert(!session.isExpired()); - session.destroy(); + clock.restore(); }); it('returns true if a new day has started', function() { + const clock = sinon.useFakeTimers({now: 1e12}); + try { new Intl.DateTimeFormat('en-US', { timeZone: 'America/Los_Angeles', - }).format(new Date()); + }); } catch (err) { // Skip this test in browsers that don't support time zones. return this.skip(); @@ -139,10 +144,11 @@ describe('Session', () => { const dateTimeFormatStub = stubDateTimeFormat(); dateTimeFormatStub.onCall(0).returns('9/15/1982'); dateTimeFormatStub.onCall(1).returns('9/14/1982'); - dateTimeFormatStub.returns('9/14/1982'); const session = new Session(tracker, 30, 'America/Los_Angeles'); - session.store.set({hitTime: now() - (15 * MINUTES)}); + tracker.send('pageview'); + + clock.tick(15 * MINUTES); // The stubs above should return difference dates for now vs the last // hit, so even though 30 minutes hasn't passed, the session has expired. @@ -153,6 +159,7 @@ describe('Session', () => { session.destroy(); restoreDateTimeFormat(); + clock.restore(); }); it('returns true if the previous hit ended the session', () => { @@ -168,7 +175,6 @@ describe('Session', () => { it('does not error in browsers with no time zone support', () => { const session = new Session(tracker, 30, 'America/Los_Angeles'); - session.store.set({hitTime: now()}); assert.doesNotThrow(() => session.isExpired()); @@ -177,7 +183,6 @@ describe('Session', () => { it('accepts an optional session ID', () => { const session = new Session(tracker); - session.store.set({hitTime: now()}); assert(!session.isExpired()); assert(session.isExpired('old-id')); @@ -188,47 +193,52 @@ describe('Session', () => { describe('sendHitTaskHook', () => { it('logs the time of the last hit', () => { - const session = new Session(tracker); + const clock = sinon.useFakeTimers({now: 1e12}); - const timeBeforePageview = now(); + const session = new Session(tracker); tracker.send('pageview'); - let lastHitTime = session.store.get().hitTime; - assert(lastHitTime >= timeBeforePageview); - const timeBeforeTimingHit = now(); + assert(session.store.get().hitTime, 1e12); + + clock.tick(10 * MINUTES); + tracker.send('timing', 'foo', 'bar', 1000); - lastHitTime = session.store.get().hitTime; - assert(lastHitTime >= timeBeforeTimingHit); + assert(session.store.get().hitTime, 1e12 + (10 * MINUTES)); session.destroy(); + clock.restore(); }); it('updates the session ID if the session has expired', () => { + const clock = sinon.useFakeTimers({now: 1e12}); + const session = new Session(tracker); - const id = session.getId(); - session.store.set({hitTime: now() - (60 * MINUTES)}); + const id = session.id; + tracker.send('pageview'); + + clock.tick(60 * MINUTES); - assert.strictEqual(id, session.getId()); + assert.strictEqual(id, session.id); // Start a new session by sending a hit, which should generate a new ID. tracker.send('pageview'); - assert.notStrictEqual(id, session.getId()); + assert.notStrictEqual(id, session.id); session.destroy(); + clock.restore(); }); it('updates the session ID if sessionControl was set to start', () => { const session = new Session(tracker); - const id = session.getId(); - session.store.set({hitTime: now()}); + const id = session.id; - assert.strictEqual(id, session.getId()); + assert.strictEqual(id, session.id); // Start a new session via the sessionControl field. tracker.send('pageview', {sessionControl: 'start'}); - assert.notStrictEqual(id, session.getId()); + assert.notStrictEqual(id, session.id); session.destroy(); }); From 8b1e6c457850b8a616ac789859095c7a6479f1d6 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Mon, 13 Aug 2018 21:49:09 -0700 Subject: [PATCH 34/71] Add missing jsdoc comment --- lib/utilities.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/utilities.js b/lib/utilities.js index 7e67634c..679a8f4c 100644 --- a/lib/utilities.js +++ b/lib/utilities.js @@ -186,6 +186,11 @@ export function deferUntilPluginsLoaded(tracker, fn) { } } +/** + * Queues a function to be run in the next microtask. If the browser supports + * Promises, those are used. Otherwise it falls back to MutationObserver. + * @param {!Function} microtask + */ export const queueMicrotask = (() => { if (typeof Promise !== 'undefined') { return (microtask) => { From b2bc274696bd473b9856ed33eb44e18793224e80 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Mon, 13 Aug 2018 22:07:14 -0700 Subject: [PATCH 35/71] Add a defineIdleProperties utility method --- lib/session.js | 76 +++++++++++++++++------------------------------- lib/store.js | 29 +++++++++++------- lib/utilities.js | 43 +++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 60 deletions(-) diff --git a/lib/session.js b/lib/session.js index 8dd02829..fc00191a 100644 --- a/lib/session.js +++ b/lib/session.js @@ -17,7 +17,7 @@ import MethodChain from './method-chain'; import Store from './store'; -import {now, uuid} from './utilities'; +import {defineIdleProperties, now, uuid} from './utilities'; const SECONDS = 1000; @@ -72,40 +72,36 @@ export default class Session { this.timeout = timeout || Session.DEFAULT_TIMEOUT; this.timeZone = timeZone; - // This will be set lazily since it's expensive to create. - this.dateTimeFormatter = null; - // Binds methods. this.sendHitTaskOverride = this.sendHitTaskOverride.bind(this); + // Idleize these as they're expensive to create if not needed. + defineIdleProperties(this, { + store: () => { + /** @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.get().id) { + store.set(/** @type {SessionStoreData} */ ({id: uuid()})); + } + return store; + }, + dateTimeFormatter: () => { + return new Intl.DateTimeFormat('en-US', {timeZone: this.timeZone}); + }, + }) + // Overrides into the trackers sendHitTask method. MethodChain.add(tracker, 'sendHitTask', this.sendHitTaskOverride); } - /** - * @return {!Store} - */ - get store() { - if (!this.store_) { - /** @type {SessionStoreData} */ - const defaultProps = { - hitTime: 0, - isExpired: false, - }; - this.store_ = Store.getOrCreate( - this.tracker.get('trackingId'), 'session', { - defaults: defaultProps, - timestampKey: 'hitTime', - }); - - // Ensure the session has an ID. - if (!this.store_.get().id) { - this.store_.set(/** @type {SessionStoreData} */ ({id: uuid()})); - } - } - return this.store_; - } - /** * Returns the ID of the current session. * @return {string} @@ -170,10 +166,9 @@ export default class Session { * @return {boolean} */ datesAreDifferentInTimezone(d1, d2) { - const dateTimeFormatter = getOrCreateDateTimeFormatter(this.timeZone); - - if (dateTimeFormatter) { - return dateTimeFormatter.format(d1) != dateTimeFormatter.format(d2); + if (this.dateTimeFormatter) { + return this.dateTimeFormatter.format(d1) != + this.dateTimeFormatter.format(d2); } else { return false; } @@ -223,20 +218,3 @@ export default class Session { Session.DEFAULT_TIMEOUT = 30; // minutes - - -const dateTimeFormatterCache_ = {}; -const getOrCreateDateTimeFormatter = (timeZone) => { - if (!(timeZone in dateTimeFormatterCache_)) { - // Some browser doesn't support various features of the - // `Intl.DateTimeFormat` API, so we have to try/catch it. - try { - dateTimeFormatterCache_[timeZone] = - new Intl.DateTimeFormat('en-US', {timeZone}); - } catch (err) { - dateTimeFormatterCache_[timeZone] = null; - } - } - return dateTimeFormatterCache_[timeZone]; -}; - diff --git a/lib/store.js b/lib/store.js index eca82975..5352e813 100644 --- a/lib/store.js +++ b/lib/store.js @@ -16,7 +16,7 @@ import EventEmitter from './event-emitter'; -import {assign} from './utilities'; +import {assign, defineIdleProperties} from './utilities'; const AUTOTRACK_PREFIX = 'autotrack'; @@ -112,8 +112,9 @@ export default class Store extends EventEmitter { this.defaults_ = opts.defaults || {}; this.timestampKey_ = opts.timestampKey; - /** @type {?Object} */ - this.cache_ = null; // Will be set after the first get. + defineIdleProperties(this, { + cache_: () => this.read(), + }); } /** @@ -122,22 +123,28 @@ export default class Store extends EventEmitter { * and in sync with activity in other windows via the `storage` event). * TODO(philipwalton): Implement schema migrations if/when a new * schema version is introduced. - * @param {{bypassCache: (boolean)}=} param1 - * bypassCache: If true, cached data will be ignored. * @return {!Object} The stored data merged with the defaults. */ - get({bypassCache = false} = {}) { - if (this.cache_ && !bypassCache) { - return this.cache_; + get() { + if (!this.cache_) { + this.cache_ = this.read(); } + return assign({}, this.defaults_, this.cache_); + } + + /** + * Reads the data stored in localStorage for this store. This method ignores + * the cache. + * @return {Object|undefined} + */ + read() { if (Store.isSupported_()) { try { - this.cache_ = parse(Store.get_(this.key_)); + return parse(Store.get_(this.key_)); } catch (err) { // Do nothing. } } - return this.cache_ = assign({}, this.defaults_, this.cache_); } /** @@ -154,7 +161,7 @@ export default class Store extends EventEmitter { // one tab get queue before but run after tasks in another tab. let oldData; if (timestampKey && typeof newData[timestampKey] === 'number') { - oldData = this.get({bypassCache: true}); + oldData = this.read() || {}; if (typeof oldData[timestampKey] === 'number' && oldData[timestampKey] > newData[timestampKey]) { return; diff --git a/lib/utilities.js b/lib/utilities.js index 679a8f4c..aa69850b 100644 --- a/lib/utilities.js +++ b/lib/utilities.js @@ -345,6 +345,49 @@ export const cIC = supportsRequestIdleCallback_ ? cancelIdleCallback : cancelIdleCallbackShim; +/** + * Defines a getter and setter on on a property that idly runs and initializer. + * If the property is referred and the initializer has not yet run, it is + * run immediately and the value returned. The setter allows the value to be + * assign, and the property is configurable so a new idle property can be + * defined at any time. + * @param {!Object} obj + * @param {string} prop + * @param {!Function} init The initialization function whose return value + * is the initial value of the property. + */ +const defineIdleProperty = (obj, prop, init) => { + let value; + const handle = rIC(() => { + value = init(); + }); + Object.defineProperty(obj, prop, { + configurable: true, + get: () => { + if (value === undefined) { + cIC(handle); + value = init(); + } + return value; + }, + set: (newValue) => { + value = newValue; + } + }); +}; + +/** + * Invokes `defineIdleProperty` for each entry in the property object passed. + * @param {!Object} obj + * @param {!Object} props + */ +export const defineIdleProperties = (obj, props) => { + Object.keys(props).forEach((prop) => { + const init = props[prop]; + defineIdleProperty(obj, prop, init); + }); +} + /*eslint-disable */ // https://gist.github.com/jed/982883 /** @param {?=} a */ From 9edd84b676e9f698ee9c4a992b907adab10d868f Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Tue, 14 Aug 2018 19:06:59 -0700 Subject: [PATCH 36/71] Move shared test helpers into their own file --- test/unit/helpers.js | 32 ++++++++++++++++++++++++++++++++ test/unit/idle-queue-test.js | 18 +++--------------- 2 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 test/unit/helpers.js diff --git a/test/unit/helpers.js b/test/unit/helpers.js new file mode 100644 index 00000000..44f7601b --- /dev/null +++ b/test/unit/helpers.js @@ -0,0 +1,32 @@ +/** + * 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 {queueMicrotask, rIC} from '../../lib/utilities'; + + +export const when = async (fn, intervalMillis = 100, retries = 20) => { + for (let i = 0; i < retries; i++) { + const result = await fn(); + if (result) { + return; + } + await new Promise((resolve) => setTimeout(resolve, intervalMillis)); + } + throw new Error(`${fn} didn't return true after ${retries} retries.`); +}; + +export const nextMicroTask = () => new Promise((res) => queueMicrotask(res)); +export const nextIdleCallback = () => new Promise((res) => rIC(res)); diff --git a/test/unit/idle-queue-test.js b/test/unit/idle-queue-test.js index 31b6e476..4067d59b 100644 --- a/test/unit/idle-queue-test.js +++ b/test/unit/idle-queue-test.js @@ -16,8 +16,10 @@ import {dispatch} from 'dom-utils'; +import {nextMicroTask, nextIdleCallback, when} from './helpers'; import IdleQueue from '../../lib/idle-queue'; -import {isSafari, queueMicrotask, rIC} from '../../lib/utilities'; +import {isSafari, rIC} from '../../lib/utilities'; + const sandbox = sinon.createSandbox(); @@ -49,20 +51,6 @@ const blockingSpy = (ms) => { }); }; -const when = async (fn, intervalMillis = 100, retries = 20) => { - for (let i = 0; i < retries; i++) { - const result = await fn(); - if (result) { - return; - } - await new Promise((resolve) => setTimeout(resolve, intervalMillis)); - } - throw new Error(`${fn} didn't return true after ${retries} retries.`); -}; - -const nextMicroTask = () => new Promise((res) => queueMicrotask(res)); -const nextIdleCallback = () => new Promise((res) => rIC(res)); - describe('IdleQueue', () => { beforeEach(() => { sandbox.restore(); From bbb00eee67df3b767c97c8357009cc4c95f92cab Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Tue, 14 Aug 2018 19:07:56 -0700 Subject: [PATCH 37/71] Add tests for defineIdleProperties --- lib/utilities.js | 6 +- test/unit/utilities-test.js | 137 ++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 3 deletions(-) diff --git a/lib/utilities.js b/lib/utilities.js index aa69850b..9df7ed5f 100644 --- a/lib/utilities.js +++ b/lib/utilities.js @@ -356,7 +356,7 @@ export const cIC = supportsRequestIdleCallback_ ? * @param {!Function} init The initialization function whose return value * is the initial value of the property. */ -const defineIdleProperty = (obj, prop, init) => { +export const defineIdleProperty = (obj, prop, init) => { let value; const handle = rIC(() => { value = init(); @@ -372,7 +372,7 @@ const defineIdleProperty = (obj, prop, init) => { }, set: (newValue) => { value = newValue; - } + }, }); }; @@ -386,7 +386,7 @@ export const defineIdleProperties = (obj, props) => { const init = props[prop]; defineIdleProperty(obj, prop, init); }); -} +}; /*eslint-disable */ // https://gist.github.com/jed/982883 diff --git a/test/unit/utilities-test.js b/test/unit/utilities-test.js index e22f36d7..088b459a 100644 --- a/test/unit/utilities-test.js +++ b/test/unit/utilities-test.js @@ -16,6 +16,7 @@ import * as utilities from '../../lib/utilities'; +import {nextIdleCallback} from './helpers'; const DEFAULT_FIELDS = { @@ -120,4 +121,140 @@ describe('utilities', () => { assert.strictEqual(order[2], 3); }); }); + + describe('defineIdleProperty', () => { + it('defines a getter whose value is idly initialized', async () => { + const obj = {}; + + const init = sandbox.stub().returns('expensiveValue'); + utilities.defineIdleProperty(obj, 'expensiveProp', init); + + assert(init.notCalled); + + await nextIdleCallback(); + + assert(init.calledOnce); + assert.strictEqual(obj.expensiveProp, 'expensiveValue'); + }); + + it('initilizes immediately if the property is accessed', async () => { + const obj = {}; + + const init = sandbox.stub().returns('expensiveValue'); + utilities.defineIdleProperty(obj, 'expensiveProp', init); + + assert(init.notCalled); + + assert.strictEqual(obj.expensiveProp, 'expensiveValue'); + assert(init.calledOnce); + }); + + it('does not initialize the property more than once', async () => { + const obj = {}; + + const init = sandbox.stub().returns('expensiveValue'); + utilities.defineIdleProperty(obj, 'expensiveProp', init); + + assert(init.notCalled); + + obj.expensiveProp; + obj.expensiveProp; + obj.expensiveProp; + assert(init.calledOnce); + }); + + it('lets the property be set', () => { + const obj = {}; + + const init = sandbox.stub().returns('expensiveValue'); + utilities.defineIdleProperty(obj, 'expensiveProp', init); + + assert(init.notCalled); + + assert.strictEqual(obj.expensiveProp, 'expensiveValue'); + assert(init.calledOnce); + + obj.expensiveProp = 'newValue'; + assert.strictEqual(obj.expensiveProp, 'newValue'); + }); + + it('lets the property be re-idly-defined', () => { + sandbox.spy(Object, 'defineProperty'); + + const obj = {}; + + const init1 = sandbox.stub().returns('expensiveValue'); + const init2 = sandbox.stub().returns('newExpensiveValue'); + utilities.defineIdleProperty(obj, 'expensiveProp', init1); + + assert(Object.defineProperty.calledOnce); + assert(Object.defineProperty.firstCall.calledWith(obj, 'expensiveProp', + sinon.match({ + configurable: true, + get: sinon.match.func, + set: sinon.match.func, + }))); + + assert(init1.notCalled); + + assert.strictEqual(obj.expensiveProp, 'expensiveValue'); + assert(init1.calledOnce); + + utilities.defineIdleProperty(obj, 'expensiveProp', init2); + + assert(Object.defineProperty.calledTwice); + assert(Object.defineProperty.secondCall.calledWith(obj, 'expensiveProp', + sinon.match({ + configurable: true, + get: sinon.match.func, + set: sinon.match.func, + }))); + + assert(init2.notCalled); + + assert.strictEqual(obj.expensiveProp, 'newExpensiveValue'); + assert(init2.calledOnce); + }); + }); + + describe('defineIdleProperties', () => { + it('calls defineIdleProperty for each passed prop', async () => { + sandbox.spy(Object, 'defineProperty'); + + const obj = {}; + + const init1 = sandbox.stub().returns('value1'); + const init2 = sandbox.stub().returns('value2'); + + utilities.defineIdleProperties(obj, { + prop1: init1, + prop2: init2, + }); + + assert(Object.defineProperty.calledTwice); + assert(Object.defineProperty.firstCall.calledWith(obj, 'prop1', + sinon.match({ + configurable: true, + get: sinon.match.func, + set: sinon.match.func, + }))); + assert(Object.defineProperty.secondCall.calledWith(obj, 'prop2', + sinon.match({ + configurable: true, + get: sinon.match.func, + set: sinon.match.func, + }))); + + assert(init1.notCalled); + assert(init2.notCalled); + + assert.strictEqual(obj.prop1, 'value1'); + assert(init2.notCalled); + + await nextIdleCallback(); + + assert.strictEqual(obj.prop1, 'value1'); + assert.strictEqual(obj.prop2, 'value2'); + }); + }); }); From 8fde15d666123856a0ab0d2dfaa6cac45e76c5b2 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Thu, 16 Aug 2018 23:30:50 -0700 Subject: [PATCH 38/71] Refactor defineIdleProperties into IdleValue --- lib/idle-value.js | 70 ++++++++++++++++++ lib/utilities.js | 45 +----------- test/unit/idle-value-test.js | 135 ++++++++++++++++++++++++++++++++++ test/unit/utilities-test.js | 137 ----------------------------------- 4 files changed, 206 insertions(+), 181 deletions(-) create mode 100644 lib/idle-value.js create mode 100644 test/unit/idle-value-test.js diff --git a/lib/idle-value.js b/lib/idle-value.js new file mode 100644 index 00000000..7b987ac9 --- /dev/null +++ b/lib/idle-value.js @@ -0,0 +1,70 @@ +/** + * Copyright 2016 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 {cIC, rIC} from './utilities'; + +/** + * A class that wraps a value that is initialied when idle. + */ +export default class IdleValue { + /** + * Accepts a function to initialize the value of a variable when idle. + * @param {function():?} init + */ + constructor(init) { + this.init_ = init; + + /** @type (?|undefined) */ + this.value_; + + this.idleHandle_ = rIC(() => { + this.value_ = this.init_(); + }); + } + + /** + * Returns the value if it's already been initialized. If it hasn't then the + * initializer function is run immediately and the pending idle callback + * is cancelled. + * @return {?} + */ + get() { + if (this.value_ === undefined) { + this.cancleIdleInit_(); + this.value_ = this.init_(); + } + return this.value_; + } + + /** + * @param {?} newValue + */ + set(newValue) { + this.cancleIdleInit_(); + this.value_ = newValue; + } + + /** + * Cancels any scheduled requestIdleCallback and resets the handle. + */ + cancleIdleInit_() { + if (this.idleHandle_) { + cIC(this.idleHandle_); + this.idleHandle_ = null; + } + } +} diff --git a/lib/utilities.js b/lib/utilities.js index 9df7ed5f..41d21c52 100644 --- a/lib/utilities.js +++ b/lib/utilities.js @@ -211,7 +211,7 @@ export const queueMicrotask = (() => { microtaskQueue.push(microtask); // Trigger a mutation observer callback, which is a microtask. - node.data = ++i % 2; + node.data = String(++i % 2); }; }; } @@ -345,49 +345,6 @@ export const cIC = supportsRequestIdleCallback_ ? cancelIdleCallback : cancelIdleCallbackShim; -/** - * Defines a getter and setter on on a property that idly runs and initializer. - * If the property is referred and the initializer has not yet run, it is - * run immediately and the value returned. The setter allows the value to be - * assign, and the property is configurable so a new idle property can be - * defined at any time. - * @param {!Object} obj - * @param {string} prop - * @param {!Function} init The initialization function whose return value - * is the initial value of the property. - */ -export const defineIdleProperty = (obj, prop, init) => { - let value; - const handle = rIC(() => { - value = init(); - }); - Object.defineProperty(obj, prop, { - configurable: true, - get: () => { - if (value === undefined) { - cIC(handle); - value = init(); - } - return value; - }, - set: (newValue) => { - value = newValue; - }, - }); -}; - -/** - * Invokes `defineIdleProperty` for each entry in the property object passed. - * @param {!Object} obj - * @param {!Object} props - */ -export const defineIdleProperties = (obj, props) => { - Object.keys(props).forEach((prop) => { - const init = props[prop]; - defineIdleProperty(obj, prop, init); - }); -}; - /*eslint-disable */ // https://gist.github.com/jed/982883 /** @param {?=} a */ diff --git a/test/unit/idle-value-test.js b/test/unit/idle-value-test.js new file mode 100644 index 00000000..5b118b25 --- /dev/null +++ b/test/unit/idle-value-test.js @@ -0,0 +1,135 @@ +/** + * Copyright 2017 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 IdleValue from '../../lib/idle-value'; +import {nextIdleCallback} from './helpers'; + + +const sandbox = sinon.createSandbox(); + +describe('IdleValue', () => { + beforeEach(() => { + sandbox.restore(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('IdleValue', () => { + describe('constructor', () => { + it('initializes a value when idle', async () => { + const initStub = sandbox.stub().returns('42'); + new IdleValue(initStub); + + assert(initStub.notCalled); + + await nextIdleCallback(); + + assert(initStub.calledOnce); + }); + }); + + describe('get', () => { + it('returns the value immediately when already initialized', async () => { + const initStub = sandbox.stub().returns('42'); + const idleVal = new IdleValue(initStub); + + await nextIdleCallback(); + assert(initStub.calledOnce); + + const val = idleVal.get(); + + assert.strictEqual(val, '42'); + }); + + it('runs the init function immediately if the value not yet set', () => { + const initStub = sandbox.stub().returns('42'); + const idleVal = new IdleValue(initStub); + + assert(initStub.notCalled); + + const val = idleVal.get(); + assert.strictEqual(val, '42'); + assert(initStub.calledOnce); + }); + + it('cancels the idle request if run before idle', async () => { + const initStub = sandbox.stub().returns('42'); + const idleVal = new IdleValue(initStub); + + const val = idleVal.get(); + assert(initStub.calledOnce); + assert.strictEqual(val, '42'); + + await nextIdleCallback(); + + // Assert the init function wasn't called again. + assert(initStub.calledOnce); + }); + + it('does not initialize the value more than once', async () => { + const initStub = sandbox.stub().returns('42'); + const idleVal = new IdleValue(initStub); + + let val = idleVal.get(); + assert.strictEqual(val, '42'); + assert(initStub.calledOnce); + + val = idleVal.get(); + assert.strictEqual(val, '42'); + assert(initStub.calledOnce); + + await nextIdleCallback(); + + val = idleVal.get(); + assert.strictEqual(val, '42'); + assert(initStub.calledOnce); + }); + }); + + describe('set', () => { + it('updates the value', () => { + const initStub = sandbox.stub().returns('42'); + const idleVal = new IdleValue(initStub); + + let val = idleVal.get(); + assert.strictEqual(val, '42'); + + idleVal.set('43'); + + val = idleVal.get(); + assert.strictEqual(val, '43'); + }); + + it('cancels the idle request if run before idle', async () => { + const initStub = sandbox.stub().returns('42'); + const idleVal = new IdleValue(initStub); + + idleVal.set('43'); + assert(initStub.notCalled); + + let val = idleVal.get(); + assert.strictEqual(val, '43'); + assert(initStub.notCalled); + + await nextIdleCallback(); + + assert(initStub.notCalled); + }); + }); + }); +}); diff --git a/test/unit/utilities-test.js b/test/unit/utilities-test.js index 088b459a..e22f36d7 100644 --- a/test/unit/utilities-test.js +++ b/test/unit/utilities-test.js @@ -16,7 +16,6 @@ import * as utilities from '../../lib/utilities'; -import {nextIdleCallback} from './helpers'; const DEFAULT_FIELDS = { @@ -121,140 +120,4 @@ describe('utilities', () => { assert.strictEqual(order[2], 3); }); }); - - describe('defineIdleProperty', () => { - it('defines a getter whose value is idly initialized', async () => { - const obj = {}; - - const init = sandbox.stub().returns('expensiveValue'); - utilities.defineIdleProperty(obj, 'expensiveProp', init); - - assert(init.notCalled); - - await nextIdleCallback(); - - assert(init.calledOnce); - assert.strictEqual(obj.expensiveProp, 'expensiveValue'); - }); - - it('initilizes immediately if the property is accessed', async () => { - const obj = {}; - - const init = sandbox.stub().returns('expensiveValue'); - utilities.defineIdleProperty(obj, 'expensiveProp', init); - - assert(init.notCalled); - - assert.strictEqual(obj.expensiveProp, 'expensiveValue'); - assert(init.calledOnce); - }); - - it('does not initialize the property more than once', async () => { - const obj = {}; - - const init = sandbox.stub().returns('expensiveValue'); - utilities.defineIdleProperty(obj, 'expensiveProp', init); - - assert(init.notCalled); - - obj.expensiveProp; - obj.expensiveProp; - obj.expensiveProp; - assert(init.calledOnce); - }); - - it('lets the property be set', () => { - const obj = {}; - - const init = sandbox.stub().returns('expensiveValue'); - utilities.defineIdleProperty(obj, 'expensiveProp', init); - - assert(init.notCalled); - - assert.strictEqual(obj.expensiveProp, 'expensiveValue'); - assert(init.calledOnce); - - obj.expensiveProp = 'newValue'; - assert.strictEqual(obj.expensiveProp, 'newValue'); - }); - - it('lets the property be re-idly-defined', () => { - sandbox.spy(Object, 'defineProperty'); - - const obj = {}; - - const init1 = sandbox.stub().returns('expensiveValue'); - const init2 = sandbox.stub().returns('newExpensiveValue'); - utilities.defineIdleProperty(obj, 'expensiveProp', init1); - - assert(Object.defineProperty.calledOnce); - assert(Object.defineProperty.firstCall.calledWith(obj, 'expensiveProp', - sinon.match({ - configurable: true, - get: sinon.match.func, - set: sinon.match.func, - }))); - - assert(init1.notCalled); - - assert.strictEqual(obj.expensiveProp, 'expensiveValue'); - assert(init1.calledOnce); - - utilities.defineIdleProperty(obj, 'expensiveProp', init2); - - assert(Object.defineProperty.calledTwice); - assert(Object.defineProperty.secondCall.calledWith(obj, 'expensiveProp', - sinon.match({ - configurable: true, - get: sinon.match.func, - set: sinon.match.func, - }))); - - assert(init2.notCalled); - - assert.strictEqual(obj.expensiveProp, 'newExpensiveValue'); - assert(init2.calledOnce); - }); - }); - - describe('defineIdleProperties', () => { - it('calls defineIdleProperty for each passed prop', async () => { - sandbox.spy(Object, 'defineProperty'); - - const obj = {}; - - const init1 = sandbox.stub().returns('value1'); - const init2 = sandbox.stub().returns('value2'); - - utilities.defineIdleProperties(obj, { - prop1: init1, - prop2: init2, - }); - - assert(Object.defineProperty.calledTwice); - assert(Object.defineProperty.firstCall.calledWith(obj, 'prop1', - sinon.match({ - configurable: true, - get: sinon.match.func, - set: sinon.match.func, - }))); - assert(Object.defineProperty.secondCall.calledWith(obj, 'prop2', - sinon.match({ - configurable: true, - get: sinon.match.func, - set: sinon.match.func, - }))); - - assert(init1.notCalled); - assert(init2.notCalled); - - assert.strictEqual(obj.prop1, 'value1'); - assert(init2.notCalled); - - await nextIdleCallback(); - - assert.strictEqual(obj.prop1, 'value1'); - assert.strictEqual(obj.prop2, 'value2'); - }); - }); }); From 90d67646b1cd262605597efb43e418f85b6a8b7a Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Thu, 16 Aug 2018 23:41:35 -0700 Subject: [PATCH 39/71] Update Session and Store to use IdleValue --- lib/session.js | 73 +++++++++++--------- lib/store.js | 62 +++++++++-------- test/unit/store-test.js | 146 +++++++++++++++++++++++++--------------- 3 files changed, 163 insertions(+), 118 deletions(-) diff --git a/lib/session.js b/lib/session.js index fc00191a..28bee769 100644 --- a/lib/session.js +++ b/lib/session.js @@ -15,9 +15,10 @@ */ +import IdleValue from './idle-value'; import MethodChain from './method-chain'; import Store from './store'; -import {defineIdleProperties, now, uuid} from './utilities'; +import {now, uuid} from './utilities'; const SECONDS = 1000; @@ -75,39 +76,49 @@ export default class Session { // Binds methods. this.sendHitTaskOverride = this.sendHitTaskOverride.bind(this); - // Idleize these as they're expensive to create if not needed. - defineIdleProperties(this, { - store: () => { - /** @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.get().id) { - store.set(/** @type {SessionStoreData} */ ({id: uuid()})); - } - return store; - }, - dateTimeFormatter: () => { - return new Intl.DateTimeFormat('en-US', {timeZone: this.timeZone}); - }, - }) + // 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(() => { + return new Intl.DateTimeFormat('en-US', {timeZone: this.timeZone}); + }); // Overrides into the trackers sendHitTask method. MethodChain.add(tracker, 'sendHitTask', this.sendHitTaskOverride); } + /** @return {!Store} */ + get store_() { + return this.idleStore_.get(); + } + + /** @return {!Intl.DateTimeFormat} */ + get dateTimeFormatter_() { + return this.idleDateTimeFormatter_.get(); + } + /** * Returns the ID of the current session. * @return {string} */ get id() { - return this.store.get().id; + return this.store_.data.id; } /** @@ -133,7 +144,7 @@ export default class Session { 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. @@ -166,9 +177,9 @@ export default class Session { * @return {boolean} */ datesAreDifferentInTimezone(d1, d2) { - if (this.dateTimeFormatter) { - return this.dateTimeFormatter.format(d1) != - this.dateTimeFormatter.format(d2); + if (this.dateTimeFormatter_) { + return this.dateTimeFormatter_.format(d1) != + this.dateTimeFormatter_.format(d2); } else { return false; } @@ -191,7 +202,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; @@ -200,7 +211,7 @@ export default class Session { if (sessionWillEnd) { sessionData.isExpired = true; } - this.store.set(sessionData); + this.store_.update(sessionData); }; } @@ -211,7 +222,7 @@ export default class Session { */ destroy() { MethodChain.remove(this.tracker, 'sendHitTask', this.sendHitTaskOverride); - this.store.destroy(); + this.store_.destroy(); delete instances[this.tracker.get('trackingId')]; } } diff --git a/lib/store.js b/lib/store.js index 5352e813..f081b447 100644 --- a/lib/store.js +++ b/lib/store.js @@ -16,7 +16,9 @@ import EventEmitter from './event-emitter'; -import {assign, defineIdleProperties} from './utilities'; +import {assign} from './utilities'; + +import IdleValue from './idle-value'; const AUTOTRACK_PREFIX = 'autotrack'; @@ -112,9 +114,7 @@ export default class Store extends EventEmitter { this.defaults_ = opts.defaults || {}; this.timestampKey_ = opts.timestampKey; - defineIdleProperties(this, { - cache_: () => this.read(), - }); + this.cache_ = new IdleValue(() => this.read_()); } /** @@ -125,26 +125,8 @@ export default class Store extends EventEmitter { * schema version is introduced. * @return {!Object} The stored data merged with the defaults. */ - get() { - if (!this.cache_) { - this.cache_ = this.read(); - } - return assign({}, this.defaults_, this.cache_); - } - - /** - * 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. - } - } + get data() { + return assign({}, this.defaults_, this.cache_.get()); } /** @@ -152,28 +134,29 @@ export default class Store extends EventEmitter { * merging it with the existing data. * @param {!Object} newData The data to save. */ - set(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 set. + // 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() || {}; + oldData = this.read_() || {}; if (typeof oldData[timestampKey] === 'number' && oldData[timestampKey] > newData[timestampKey]) { return; } } else { - oldData = this.get(); + oldData = this.data; } - this.cache_ = assign(oldData, newData); + const newCache = assign(oldData, newData); + this.cache_.set(newCache); if (Store.isSupported_()) { try { - Store.set_(this.key_, JSON.stringify(this.cache_)); + Store.set_(this.key_, JSON.stringify(newCache)); } catch (err) { // Do nothing. } @@ -184,7 +167,7 @@ export default class Store extends EventEmitter { * Clears the data in localStorage for the current store. */ clear() { - this.cache_ = {}; + this.cache_.set({}); if (Store.isSupported_()) { try { @@ -206,6 +189,21 @@ export default class Store extends EventEmitter { 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. + } + } + } } @@ -239,7 +237,7 @@ function storageListener(event) { const oldData = assign({}, store.defaults_, parse(event.oldValue)); const newData = assign({}, store.defaults_, parse(event.newValue)); - store.cache_ = newData; + store.cache_.set(newData); store.emit('externalSet', newData, oldData); } } diff --git a/test/unit/store-test.js b/test/unit/store-test.js index bda229b6..7a6d8b2e 100644 --- a/test/unit/store-test.js +++ b/test/unit/store-test.js @@ -85,7 +85,7 @@ describe('Store', () => { }); }); - describe('get', () => { + describe('get data', () => { it('reads data from localStorage for the store key', () => { const store1 = Store.getOrCreate('UA-12345-1', 'ns1'); const store2 = Store.getOrCreate('UA-67890-1', 'ns2'); @@ -93,8 +93,8 @@ describe('Store', () => { localStorage.setItem(store1.key_, JSON.stringify({foo: 12, bar: 34})); localStorage.setItem(store2.key_, JSON.stringify({qux: 56, baz: 78})); - assert.deepEqual(store1.get(), {foo: 12, bar: 34}); - assert.deepEqual(store2.get(), {qux: 56, baz: 78}); + assert.deepEqual(store1.data, {foo: 12, bar: 34}); + assert.deepEqual(store2.data, {qux: 56, baz: 78}); store1.destroy(); store2.destroy(); @@ -111,8 +111,8 @@ describe('Store', () => { localStorage.setItem(store1.key_, JSON.stringify({foo: 12, bar: 34})); localStorage.setItem(store2.key_, JSON.stringify({qux: 56, baz: 78})); - assert.deepEqual(store1.get(), {default: true, foo: 12, bar: 34}); - assert.deepEqual(store2.get(), {default: true, qux: 56, baz: 78}); + assert.deepEqual(store1.data, {default: true, foo: 12, bar: 34}); + assert.deepEqual(store2.data, {default: true, qux: 56, baz: 78}); store1.destroy(); store2.destroy(); @@ -126,10 +126,10 @@ describe('Store', () => { defaults: {default: true, qux: 2}, }); - localStorage.setItem(store1.key_, 'bad data'); + localStorage.setItem('autotrack:UA-12345-1:ns1', 'bad data'); - assert.deepEqual(store1.get(), {default: true, foo: 1}); - assert.deepEqual(store2.get(), {default: true, qux: 2}); + assert.deepEqual(store1.data, {default: true, foo: 1}); + assert.deepEqual(store2.data, {default: true, qux: 2}); store1.destroy(); store2.destroy(); @@ -145,45 +145,49 @@ describe('Store', () => { defaults: {default: true, qux: 2}, }); - store1.set({bar: 3}); - store2.set({baz: 4}); + store1.update({bar: 3}); + store2.update({baz: 4}); - assert.deepEqual(store1.get(), {default: true, foo: 1, bar: 3}); - assert.deepEqual(store2.get(), {default: true, qux: 2, baz: 4}); + assert.deepEqual(store1.data, {default: true, foo: 1, bar: 3}); + assert.deepEqual(store2.data, {default: true, qux: 2, baz: 4}); store1.destroy(); store2.destroy(); }); }); - describe('set', () => { + describe('update', () => { it('writes data to localStorage for the store key', () => { const store1 = Store.getOrCreate('UA-12345-1', 'ns1'); const store2 = Store.getOrCreate('UA-67890-1', 'ns2'); - store1.set({foo: 12, bar: 34}); - store2.set({qux: 56, baz: 78}); + store1.update({foo: 12, bar: 34}); + store2.update({qux: 56, baz: 78}); assert.deepEqual( - JSON.parse(localStorage.getItem(store1.key_)), + JSON.parse(localStorage.getItem('autotrack:UA-12345-1:ns1')), {foo: 12, bar: 34}); assert.deepEqual( - JSON.parse(localStorage.getItem(store2.key_)), + JSON.parse(localStorage.getItem('autotrack:UA-67890-1:ns2')), {qux: 56, baz: 78}); store1.destroy(); store2.destroy(); }); - it('stores the updated data in the local cache', () => { + it('stores the updated data in the local cache to quicker reads', () => { const store1 = Store.getOrCreate('UA-12345-1', 'ns1'); const store2 = Store.getOrCreate('UA-67890-1', 'ns2'); - store1.set({foo: 12, bar: 34}); - store2.set({qux: 56, baz: 78}); + store1.update({foo: 12, bar: 34}); + store2.update({qux: 56, baz: 78}); - assert.deepEqual(store1.cache_, {foo: 12, bar: 34}); - assert.deepEqual(store2.cache_, {qux: 56, baz: 78}); + sandbox.spy(localStorage, 'getItem'); + + assert.deepEqual(store1.data, {foo: 12, bar: 34}); + assert.deepEqual(store2.data, {qux: 56, baz: 78}); + + assert(localStorage.getItem.notCalled); store1.destroy(); store2.destroy(); @@ -193,12 +197,23 @@ describe('Store', () => { const store1 = Store.getOrCreate('UA-12345-1', 'ns1'); const store2 = Store.getOrCreate('UA-67890-1', 'ns2'); - sandbox.stub(Store, 'set_').throws(); - store1.set({foo: 12, bar: 34}); - store2.set({qux: 56, baz: 78}); + sandbox.stub(localStorage, 'setItem').throws(); + + store1.update({foo: 12, bar: 34}); + store2.update({qux: 56, baz: 78}); + + // No write should have happened. + assert.strictEqual( + localStorage.getItem('autotrack:UA-12345-1:ns1'), null); + assert.strictEqual( + localStorage.getItem('autotrack:UA-67890-1:ns2'), null); + + sandbox.spy(localStorage, 'getItem'); + assert.deepEqual(store1.data, {foo: 12, bar: 34}); + assert.deepEqual(store2.data, {qux: 56, baz: 78}); - assert.deepEqual(store1.cache_, {foo: 12, bar: 34}); - assert.deepEqual(store2.cache_, {qux: 56, baz: 78}); + // The `.data getter`should read from cache. + assert(localStorage.getItem.notCalled); store1.destroy(); store2.destroy(); @@ -210,19 +225,28 @@ describe('Store', () => { timestampKey: 'time', }); - store1.set({time: 1000, value: 'A'}); - store2.set({time: 1000, value: 'A'}); + store1.update({time: 1000, value: 'A'}); + store2.update({time: 1000, value: 'A'}); - assert.deepEqual(store1.cache_, {time: 1000, value: 'A'}); - assert.deepEqual(store2.cache_, {time: 1000, value: 'A'}); + assert.deepEqual( + JSON.parse(localStorage.getItem('autotrack:UA-12345-1:ns1')), + {time: 1000, value: 'A'}); - store1.set({time: 999, value: 'B'}); - store2.set({time: 999, value: 'B'}); + assert.deepEqual( + JSON.parse(localStorage.getItem('autotrack:UA-67890-1:ns2')), + {time: 1000, value: 'A'}); - assert.deepEqual(store1.cache_, {time: 999, value: 'B'}); + store1.update({time: 999, value: 'B'}); + store2.update({time: 999, value: 'B'}); + + assert.deepEqual( + JSON.parse(localStorage.getItem('autotrack:UA-12345-1:ns1')), + {time: 999, value: 'B'}); // No data should have been written because the stored time is newer. - assert.deepEqual(store2.cache_, {time: 1000, value: 'A'}); + assert.deepEqual( + JSON.parse(localStorage.getItem('autotrack:UA-67890-1:ns2')), + {time: 1000, value: 'A'}); store1.destroy(); store2.destroy(); @@ -235,18 +259,22 @@ describe('Store', () => { // Simulate a storage event, meaning a `set()` call was made in // another tab. dispatchStorageEvent({ - key: store1.key_, + key: 'autotrack:UA-12345-1:ns1', oldValue: '', newValue: JSON.stringify({foo: 12, bar: 34}), }); dispatchStorageEvent({ - key: store2.key_, + key: 'autotrack:UA-67890-1:ns2', oldValue: '', newValue: JSON.stringify({qux: 56, baz: 78}), }); - assert.deepEqual(store1.cache_, {foo: 12, bar: 34}); - assert.deepEqual(store2.cache_, {qux: 56, baz: 78}); + sandbox.spy(localStorage, 'getItem'); + + assert.deepEqual(store1.data, {foo: 12, bar: 34}); + assert.deepEqual(store2.data, {qux: 56, baz: 78}); + + assert(localStorage.getItem.notCalled); store1.destroy(); store2.destroy(); @@ -256,21 +284,26 @@ describe('Store', () => { describe('clear', () => { it('removes the key from localStorage', () => { const store1 = Store.getOrCreate('UA-12345-1', 'ns1'); - const store2 = Store.getOrCreate('UA-67890-1', 'ns2'); + const store2 = Store.getOrCreate('UA-67890-1', 'ns2', { + defaults: {qux: 1}, + }); - store1.set({foo: 12, bar: 34}); - store2.set({qux: 56, baz: 78}); + store1.update({foo: 12, bar: 34}); + store2.update({qux: 56, baz: 78}); - assert.deepEqual(store1.get(), {foo: 12, bar: 34}); - assert.deepEqual(store2.get(), {qux: 56, baz: 78}); + assert.deepEqual(store1.data, {foo: 12, bar: 34}); + assert.deepEqual(store2.data, {qux: 56, baz: 78}); store1.clear(); store2.clear(); - assert.deepEqual(store1.get(), {}); - assert.deepEqual(store2.get(), {}); - assert.strictEqual(localStorage.getItem(store1.key_), null); - assert.strictEqual(localStorage.getItem(store2.key_), null); + assert.deepEqual(store1.data, {}); + assert.deepEqual(store2.data, {qux: 1}); + + assert.strictEqual( + localStorage.getItem('autotrack:UA-12345-1:ns1'), null); + assert.strictEqual( + localStorage.getItem('autotrack:UA-67890-1:ns2'), null); store1.destroy(); store2.destroy(); @@ -278,20 +311,23 @@ describe('Store', () => { it('clears the cache even if the localStorage clear fails', () => { const store1 = Store.getOrCreate('UA-12345-1', 'ns1'); - const store2 = Store.getOrCreate('UA-67890-1', 'ns2'); + const store2 = Store.getOrCreate('UA-67890-1', 'ns2', { + defaults: {qux: 1}, + }); + sandbox.stub(Store, 'clear_').throws(); - store1.set({foo: 12, bar: 34}); - store2.set({qux: 56, baz: 78}); + store1.update({foo: 12, bar: 34}); + store2.update({qux: 56, baz: 78}); - assert.deepEqual(store1.get(), {foo: 12, bar: 34}); - assert.deepEqual(store2.get(), {qux: 56, baz: 78}); + assert.deepEqual(store1.data, {foo: 12, bar: 34}); + assert.deepEqual(store2.data, {qux: 56, baz: 78}); store1.clear(); store2.clear(); - assert.deepEqual(store1.get(), {}); - assert.deepEqual(store2.get(), {}); + assert.deepEqual(store1.data, {}); + assert.deepEqual(store2.data, {qux: 1}); store1.destroy(); store2.destroy(); From ed2b306b44c34d7f371f6d05b28f14caceb6d074 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Thu, 16 Aug 2018 23:43:39 -0700 Subject: [PATCH 40/71] Fix closure compiler warnings --- lib/externs/request-idle-callback.js | 76 ++++++++++++++++++++++++++++ lib/externs/utilities.js | 2 + lib/externs/window.js | 19 +++++++ lib/idle-queue.js | 8 ++- lib/tracker-queue.js | 2 +- 5 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 lib/externs/request-idle-callback.js create mode 100644 lib/externs/utilities.js create mode 100644 lib/externs/window.js 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/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/idle-queue.js b/lib/idle-queue.js index 843c3eb6..c5697f94 100644 --- a/lib/idle-queue.js +++ b/lib/idle-queue.js @@ -53,6 +53,7 @@ export default class IdleQueue { /** * @param {!Array|!Function} tasks + * @return {!IdleQueue} */ add(tasks) { // Support single functions or arrays of functions. @@ -68,6 +69,9 @@ export default class IdleQueue { } this.scheduleTaskProcessing_(); + + // For chaining. + return this; } /** @@ -115,9 +119,9 @@ export default class IdleQueue { * If an `IdleDeadline` object is passed (as is with `requestIdleCallback`) * then the tasks are processed until there's no time remaining, at which * we yield to input or other script and wait until the next idle time. - * @param {IdleDeadline|undefined} deadline + * @param {!IdleDeadline=} deadline */ - processTasks_(deadline) { + processTasks_(deadline = undefined) { this.cancelScheduledTaskProcessing_(); if (!this.isProcessing_) { diff --git a/lib/tracker-queue.js b/lib/tracker-queue.js index 11e1faa6..5d134b40 100644 --- a/lib/tracker-queue.js +++ b/lib/tracker-queue.js @@ -23,7 +23,7 @@ const instances = {}; * Gets an existing instance for the passed arguments or creates a new * instance if one doesn't exist. * @param {!Tracker} tracker An analytics.js tracker object. - * @return {Session} The Session instance. + * @return {!IdleQueue} */ export const getOrCreateTrackerQueue = (tracker) => { // Don't create multiple instances for the same property. From 8b813eca2491ae658ee20f339f5039a370e3ad60 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Fri, 17 Aug 2018 12:32:47 -0700 Subject: [PATCH 41/71] Add proper reference counting --- lib/session.js | 27 +++++++---- lib/store.js | 29 +++++++++--- lib/tracker-queue.js | 55 +++++++++++++++++----- test/unit/session-test.js | 79 +++++++++++++++++++++----------- test/unit/store-test.js | 70 ++++++++++++++++++++-------- test/unit/tracker-queue-test.js | 81 +++++++++++++++++++++++++++------ 6 files changed, 257 insertions(+), 84 deletions(-) diff --git a/lib/session.js b/lib/session.js index 28bee769..e451c3e9 100644 --- a/lib/session.js +++ b/lib/session.js @@ -48,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; } /** @@ -221,9 +226,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 f081b447..1f725f7a 100644 --- a/lib/store.js +++ b/lib/store.js @@ -46,11 +46,20 @@ export default class Store extends EventEmitter { 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, opts); - 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; } /** @@ -184,8 +193,14 @@ 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(); } } @@ -232,7 +247,7 @@ function removeStorageListener() { * @param {!Event} event The DOM event. */ function storageListener(event) { - const store = instances[event.key]; + const store = instances[event.key].value; if (store) { const oldData = assign({}, store.defaults_, parse(event.oldValue)); const newData = assign({}, store.defaults_, parse(event.newValue)); diff --git a/lib/tracker-queue.js b/lib/tracker-queue.js index 5d134b40..0be1f600 100644 --- a/lib/tracker-queue.js +++ b/lib/tracker-queue.js @@ -20,17 +20,50 @@ import IdleQueue from './idle-queue'; const instances = {}; /** - * Gets an existing instance for the passed arguments or creates a new - * instance if one doesn't exist. - * @param {!Tracker} tracker An analytics.js tracker object. - * @return {!IdleQueue} + * A class that enforces a unique IdleQueue per tracker. */ -export const getOrCreateTrackerQueue = (tracker) => { - // Don't create multiple instances for the same property. - const trackingId = tracker.get('trackingId'); - if (!instances[trackingId]) { - instances[trackingId] = new IdleQueue(); +export default class TrackerQueue extends IdleQueue { + /** + * Gets an existing instance for the passed tracker or creates a new + * instance if one doesn't exist. + * @param {!Tracker} tracker An analytics.js tracker object. + * @return {!TrackerQueue} + */ + static getOrCreate(tracker) { + // Don't create multiple instances for the same tracker. + const trackingId = tracker.get('trackingId'); + + if (!(trackingId in instances)) { + instances[trackingId] = { + references: 0, + value: new TrackerQueue(tracker), + }; + } + + ++instances[trackingId].references; + return instances[trackingId].value; + } + + /** + * @param {!Tracker} tracker] + */ + constructor(tracker) { + super(); + this.tracker = tracker; } - return instances[trackingId]; -}; + /** + * Removes a reference from the instances map. If no more references exist + * for this instance, destroy it. + */ + destroy() { + const trackingId = this.tracker.get('trackingId'); + + --instances[trackingId].references; + + if (instances[trackingId].references === 0) { + super.destroy(); + delete instances[trackingId]; + } + } +} diff --git a/test/unit/session-test.js b/test/unit/session-test.js index b39ea160..7f1a91c2 100644 --- a/test/unit/session-test.js +++ b/test/unit/session-test.js @@ -47,31 +47,31 @@ describe('Session', () => { assert.strictEqual(session1, session2); session1.destroy(); - session2.destroy(); // Not really needed. + session2.destroy(); }); }); describe('constructor', () => { - it('stores a unique ID', () => { - const session = new Session(tracker); + xit('stores a unique ID', () => { + const session = Session.getOrCreate(tracker); assert(session.id); session.destroy(); }); - it('reuses a stored ID if found', () => { + xit('reuses a stored ID if found', () => { localStorage.setItem( 'autotrack:UA-12345-1:session', JSON.stringify({id: 'foo'})); - const session = new Session(tracker); + const session = Session.getOrCreate(tracker); assert.strictEqual(session.id, 'foo'); session.destroy(); }); - it('sets the passed args on the instance', () => { - const session = new Session(tracker, 123, 'America/Los_Angeles'); + xit('sets the passed args on the instance', () => { + const session = Session.getOrCreate(tracker, 123, 'America/Los_Angeles'); assert.strictEqual(session.tracker, tracker); assert.strictEqual(session.timeout, 123); @@ -80,8 +80,8 @@ describe('Session', () => { session.destroy(); }); - it('uses the default timeout if not set', () => { - const session = new Session(tracker); + xit('uses the default timeout if not set', () => { + const session = Session.getOrCreate(tracker); assert.strictEqual(session.tracker, tracker); assert.strictEqual(session.timeout, DEFAULT_TIMEOUT); @@ -90,8 +90,8 @@ describe('Session', () => { session.destroy(); }); - it('adds a listener for storage changes', () => { - const session = new Session(tracker); + xit('adds a listener for storage changes', () => { + const session = Session.getOrCreate(tracker); assert.strictEqual( session.store.storageDidChangeInAnotherWindow, @@ -103,7 +103,7 @@ describe('Session', () => { describe('get id', () => { it('returns the stored ID', () => { - const session = new Session(tracker); + const session = Session.getOrCreate(tracker); assert(session.id); @@ -115,7 +115,7 @@ describe('Session', () => { it('returns true if the last hit was too long ago', () => { const clock = sinon.useFakeTimers({now: 1e12}); - const session = new Session(tracker); + const session = Session.getOrCreate(tracker); tracker.send('pageview'); assert(!session.isExpired()); @@ -145,7 +145,7 @@ describe('Session', () => { dateTimeFormatStub.onCall(0).returns('9/15/1982'); dateTimeFormatStub.onCall(1).returns('9/14/1982'); - const session = new Session(tracker, 30, 'America/Los_Angeles'); + const session = Session.getOrCreate(tracker, 30, 'America/Los_Angeles'); tracker.send('pageview'); clock.tick(15 * MINUTES); @@ -163,7 +163,7 @@ describe('Session', () => { }); it('returns true if the previous hit ended the session', () => { - const session = new Session(tracker); + const session = Session.getOrCreate(tracker); tracker.send('pageview'); tracker.send('event', 'cat', 'act', {sessionControl: 'end'}); @@ -174,7 +174,7 @@ describe('Session', () => { }); it('does not error in browsers with no time zone support', () => { - const session = new Session(tracker, 30, 'America/Los_Angeles'); + const session = Session.getOrCreate(tracker, 30, 'America/Los_Angeles'); assert.doesNotThrow(() => session.isExpired()); @@ -182,7 +182,7 @@ describe('Session', () => { }); it('accepts an optional session ID', () => { - const session = new Session(tracker); + const session = Session.getOrCreate(tracker); assert(!session.isExpired()); assert(session.isExpired('old-id')); @@ -191,11 +191,11 @@ describe('Session', () => { }); }); - describe('sendHitTaskHook', () => { + xdescribe('sendHitTaskHook', () => { it('logs the time of the last hit', () => { const clock = sinon.useFakeTimers({now: 1e12}); - const session = new Session(tracker); + const session = Session.getOrCreate(tracker); tracker.send('pageview'); assert(session.store.get().hitTime, 1e12); @@ -212,7 +212,7 @@ describe('Session', () => { it('updates the session ID if the session has expired', () => { const clock = sinon.useFakeTimers({now: 1e12}); - const session = new Session(tracker); + const session = Session.getOrCreate(tracker); const id = session.id; tracker.send('pageview'); @@ -230,7 +230,7 @@ describe('Session', () => { }); it('updates the session ID if sessionControl was set to start', () => { - const session = new Session(tracker); + const session = Session.getOrCreate(tracker); const id = session.id; assert.strictEqual(id, session.id); @@ -245,20 +245,47 @@ describe('Session', () => { }); describe('destroy', () => { - it('removes the instance from the global store', () => { + it('releases the reference to the instance', () => { const session1 = Session.getOrCreate(tracker); const session2 = Session.getOrCreate(tracker); assert.strictEqual(session1, session2); session1.destroy(); - session2.destroy(); - const session3 = new Session(tracker); - assert.notStrictEqual(session3, session1); - assert.notStrictEqual(session3, session2); + // session2 still has a reference, so this shouldn't create a new one + const session3 = Session.getOrCreate(tracker); + assert.strictEqual(session2, session3); + session2.destroy(); session3.destroy(); + + // All the references should be released, so a new one should be created. + const session4 = Session.getOrCreate(tracker); + assert.notStrictEqual(session3, session4); + + session4.destroy(); + }); + + it('clears the store if no more references exist', () => { + const session1 = Session.getOrCreate(tracker); + const session2 = Session.getOrCreate(tracker); + + assert.strictEqual(session1, session2); + + // Force the session to write store data. + tracker.send('pageview'); + + session1.destroy(); + + // A reference still exists, so the store shouldn't be cleared. + assert.notStrictEqual( + localStorage.getItem('autotrack:UA-12345-1:session'), null); + + session2.destroy(); + + assert.strictEqual( + localStorage.getItem('autotrack:UA-12345-1:session'), null); }); }); }); diff --git a/test/unit/store-test.js b/test/unit/store-test.js index 7a6d8b2e..46af2a87 100644 --- a/test/unit/store-test.js +++ b/test/unit/store-test.js @@ -41,25 +41,12 @@ const dispatchStorageEvent = ({key, oldValue, newValue}) => { describe('Store', () => { beforeEach(() => { sandbox.restore(); - localStorage.clear(); }); afterEach(() => { sandbox.restore(); - localStorage.clear(); }); describe('static getOrCreate', () => { - it('creates a localStorage key from the tracking ID and namespace', () => { - const store1 = Store.getOrCreate('UA-12345-1', 'ns1'); - assert.strictEqual(store1.key_, 'autotrack:UA-12345-1:ns1'); - - const store2 = Store.getOrCreate('UA-67890-1', 'ns2'); - assert.strictEqual(store2.key_, 'autotrack:UA-67890-1:ns2'); - - store1.destroy(); - store2.destroy(); - }); - it('does not create multiple instances for the same key', () => { const store1 = Store.getOrCreate('UA-12345-1', 'ns1'); const store2 = Store.getOrCreate('UA-67890-1', 'ns2'); @@ -70,6 +57,7 @@ describe('Store', () => { store1.destroy(); store2.destroy(); + store3.destroy(); }); it('adds a single event listener for the storage event', () => { @@ -335,24 +323,68 @@ describe('Store', () => { }); describe('destroy', () => { - it('removes the instance from the global store', () => { + it('releases the reference to the instance', () => { const store1 = Store.getOrCreate('UA-12345-1', 'ns1'); const store2 = Store.getOrCreate('UA-12345-1', 'ns1'); assert.strictEqual(store1, store2); store1.destroy(); - store2.destroy(); + // store2 still has a reference, so this shouldn't create a new one const store3 = Store.getOrCreate('UA-12345-1', 'ns1'); - assert.notStrictEqual(store3, store1); - assert.notStrictEqual(store3, store2); + assert.strictEqual(store2, store3); + store2.destroy(); store3.destroy(); + + // All the references should be released, so a new one should be created. + const store4 = Store.getOrCreate('UA-12345-1', 'ns1'); + assert.notStrictEqual(store3, store4); + + store4.destroy(); + }); + + it('clears the localStorage entry if no more references exist', () => { + const store1 = Store.getOrCreate('UA-12345-1', 'ns1'); + const store2 = Store.getOrCreate('UA-12345-1', 'ns1'); + const store3 = Store.getOrCreate('UA-67890-1', 'ns2'); + const store4 = Store.getOrCreate('UA-67890-1', 'ns2'); + + assert.strictEqual(store1, store2); + assert.strictEqual(store3, store4); + + store1.update({stuff: 1}); + store3.update({things: 2}); + + assert.notStrictEqual( + localStorage.getItem('autotrack:UA-12345-1:ns1'), null); + + assert.notStrictEqual( + localStorage.getItem('autotrack:UA-67890-1:ns2'), null); + + // This shouldn't clear the stores since other references exist. + store1.destroy(); + store3.destroy(); + + assert.notStrictEqual( + localStorage.getItem('autotrack:UA-12345-1:ns1'), null); + + assert.notStrictEqual( + localStorage.getItem('autotrack:UA-67890-1:ns2'), null); + + // This *should* clear the stores because no other references exist. + store2.destroy(); + store4.destroy(); + + assert.strictEqual( + localStorage.getItem('autotrack:UA-12345-1:ns1'), null); + + assert.strictEqual( + localStorage.getItem('autotrack:UA-67890-1:ns2'), null); }); - it('removes the storage listener when the last instance is destroyed', - () => { + it('removes the storage listener when all instances are destroyed', () => { sandbox.spy(window, 'addEventListener'); sandbox.spy(window, 'removeEventListener'); diff --git a/test/unit/tracker-queue-test.js b/test/unit/tracker-queue-test.js index a64a3e7b..28a09762 100644 --- a/test/unit/tracker-queue-test.js +++ b/test/unit/tracker-queue-test.js @@ -14,8 +14,8 @@ * limitations under the License. */ - import IdleQueue from '../../lib/idle-queue'; -import {getOrCreateTrackerQueue} from '../../lib/tracker-queue'; +import IdleQueue from '../../lib/idle-queue'; +import TrackerQueue from '../../lib/tracker-queue'; const sandbox = sinon.createSandbox(); let tracker; @@ -28,7 +28,7 @@ const getFields = (overrides = {}) => { }, overrides); }; -describe('getOrCreateTrackerQueue', () => { +describe('TrackerQueue', () => { beforeEach((done) => { sandbox.restore(); @@ -44,21 +44,76 @@ describe('getOrCreateTrackerQueue', () => { window.ga('remove'); }); - it('creates an instance of IdleQueue for the passed tracker', () => { - const queue = getOrCreateTrackerQueue(tracker); + describe('static getOrCreate', () => { + it('creates an instance of for the passed tracker', () => { + const queue = TrackerQueue.getOrCreate(tracker); - assert(queue instanceof IdleQueue); + assert(queue instanceof TrackerQueue); - queue.destroy(); + queue.destroy(); + }); + + it('creates an instance that extends IdleQueue', () => { + const queue = TrackerQueue.getOrCreate(tracker); + + assert(queue instanceof IdleQueue); + + queue.destroy(); + }); + + it('does not create more than one instance per tracking ID', () => { + const queue1 = TrackerQueue.getOrCreate(tracker); + const queue2 = TrackerQueue.getOrCreate(tracker); + + assert.strictEqual(queue1, queue2); + + queue1.destroy(); + queue2.destroy(); // Not really needed. + }); }); - it('does not create more than one instance per tracking ID', () => { - const queue1 = getOrCreateTrackerQueue(tracker); - const queue2 = getOrCreateTrackerQueue(tracker); + describe('destroy', () => { + it('releases the reference to the instance', () => { + const queue1 = TrackerQueue.getOrCreate(tracker); + const queue2 = TrackerQueue.getOrCreate(tracker); + + assert.strictEqual(queue1, queue2); + + queue1.destroy(); + + // queue2 still has a reference, so this shouldn't create a new one + const queue3 = TrackerQueue.getOrCreate(tracker); + assert.strictEqual(queue2, queue3); - assert.strictEqual(queue1, queue2); + queue2.destroy(); + queue3.destroy(); - queue1.destroy(); - queue2.destroy(); // Not really needed. + // All the references should be released, so a new one should be created. + const queue4 = TrackerQueue.getOrCreate(tracker); + assert.notStrictEqual(queue3, queue4); + + queue4.destroy(); + }); + + it('destroys the instance if no more references exist', () => { + sandbox.spy(IdleQueue.prototype, 'destroy'); + + const queue1 = TrackerQueue.getOrCreate(tracker); + const queue2 = TrackerQueue.getOrCreate(tracker); + + assert.strictEqual(queue1, queue2); + + // Force the queue to write store data. + tracker.send('pageview'); + + queue1.destroy(); + + assert(IdleQueue.prototype.destroy.notCalled); + + queue2.destroy(); + + assert(IdleQueue.prototype.destroy.calledOnce); + assert(IdleQueue.prototype.destroy.calledOn(queue2)); + }); }); }); From 3eefed4ab69cd60d86794604332a19cb4e2a5ef6 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Fri, 17 Aug 2018 13:14:54 -0700 Subject: [PATCH 42/71] Update pageVisibilityTracker to use TrackerQueue --- lib/plugins/page-visibility-tracker.js | 361 +++++++++++++---------- test/e2e/page-visibility-tracker-test.js | 263 +++++++++++------ 2 files changed, 366 insertions(+), 258 deletions(-) diff --git a/lib/plugins/page-visibility-tracker.js b/lib/plugins/page-visibility-tracker.js index 829f4cff..eb7fee41 100644 --- a/lib/plugins/page-visibility-tracker.js +++ b/lib/plugins/page-visibility-tracker.js @@ -20,9 +20,10 @@ 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, isSafari, + now, uuid} from '../utilities'; const HIDDEN = 'hidden'; @@ -63,51 +64,67 @@ 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. - this.store = Store.getOrCreate( - tracker.get('trackingId'), 'plugins/page-visibility-tracker'); + // 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); + } + + this.store = Store.getOrCreate(tracker.get('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(tracker).add(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 +143,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.add(({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}); + }, 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 +234,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 +250,92 @@ 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.add(() => { + /** @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.add(() => { /** @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. + * Sends a pageview, optionally calculating an offset if time is passed. + * @param {{pageviewTime: (number), isPageLoad: (boolean|undefined)}} param1 */ - 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)); - } + sendPageview({pageviewTime, isPageLoad}) { + this.queue.add(() => { + /** @type {FieldsObj} */ + const defaultFields = { + transport: 'beacon', + queueTime: now() - pageviewTime, + }; - /** - * Sends a pageview, optionally calculating an offset if hitTime 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. - */ - 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; - } + 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)); + }); } /** @@ -309,7 +350,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 +358,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 +381,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 +407,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/test/e2e/page-visibility-tracker-test.js b/test/e2e/page-visibility-tracker-test.js index b41be91f..2c3c5120 100644 --- a/test/e2e/page-visibility-tracker-test.js +++ b/test/e2e/page-visibility-tracker-test.js @@ -64,41 +64,54 @@ describe('pageVisibilityTracker', function() { if (!browserSupportsTabs()) return this.skip(); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); - openNewTab(); - + browser.execute(ga.run, 'send', 'pageview'); browser.waitUntil(log.hitCountEquals(1)); + openNewTab(); + browser.waitUntil(log.hitCountEquals(2)); + const hits = log.getHits(); - assert.strictEqual(hits[0].ec, 'Page Visibility'); - assert.strictEqual(hits[0].ea, 'track'); + assert.strictEqual(hits[0].t, 'pageview'); + assert.strictEqual(hits[1].ec, 'Page Visibility'); + assert.strictEqual(hits[1].ea, 'track'); }); it('tracks the elapsed time a page was visible', function() { if (!browserSupportsTabs()) return this.skip(); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); - browser.pause(2000); - openNewTab(); - + browser.execute(ga.run, 'send', 'pageview'); browser.waitUntil(log.hitCountEquals(1)); + browser.pause(1500); + openNewTab(); + browser.waitUntil(log.hitCountEquals(2)); + const hits = log.getHits(); - assert(Number(hits[0].ev) >= 2); + assert.strictEqual(hits[0].t, 'pageview'); + assert.strictEqual(hits[1].ec, 'Page Visibility'); + assert.strictEqual(hits[1].ea, 'track'); + assert(Number(hits[1].ev) >= 2); }); it('sends events as nonInteraction by default', function() { if (!browserSupportsTabs()) return this.skip(); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); + browser.execute(ga.run, 'send', 'pageview'); + browser.waitUntil(log.hitCountEquals(1)); + openNewTab(); + browser.waitUntil(log.hitCountEquals(2)); + closeAllButFirstTab(); openNewTab(); - - browser.waitUntil(log.hitCountEquals(2)); + browser.waitUntil(log.hitCountEquals(3)); const hits = log.getHits(); - assert.strictEqual(hits[0].ni, '1'); + assert.strictEqual(hits[0].t, 'pageview'); assert.strictEqual(hits[1].ni, '1'); + assert.strictEqual(hits[2].ni, '1'); }); it('uses a custom metric if specified', function() { @@ -108,15 +121,18 @@ describe('pageVisibilityTracker', function() { visibleThreshold: 0, visibleMetricIndex: 1, }); + browser.execute(ga.run, 'send', 'pageview'); + browser.waitUntil(log.hitCountEquals(1)); - browser.pause(1000); + browser.pause(500); openNewTab(); - - browser.waitUntil(log.hitCountEquals(1)); + browser.waitUntil(log.hitCountEquals(2)); const hits = log.getHits(); - assert(Number(hits[0].ev) >= 1); - assert(Number(hits[0].cm1) >= 1); + + assert.strictEqual(hits[0].t, 'pageview'); + assert(Number(hits[1].ev) >= 1); + assert(Number(hits[1].cm1) >= 1); }); it('does not send any hidden events if the session has expired', function() { @@ -126,6 +142,8 @@ describe('pageVisibilityTracker', function() { visibleThreshold: 0, sessionTimeout: SESSION_TIMEOUT_IN_MINUTES, }); + browser.execute(ga.run, 'send', 'pageview'); + browser.waitUntil(log.hitCountEquals(1)); expireSession(); log.removeHits(); @@ -139,6 +157,8 @@ describe('pageVisibilityTracker', function() { if (!browserSupportsTabs()) return this.skip(); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); + browser.execute(ga.run, 'send', 'pageview'); + browser.waitUntil(log.hitCountEquals(1)); expireSession(); log.removeHits(); @@ -158,7 +178,11 @@ describe('pageVisibilityTracker', function() { if (!browserSupportsTabs()) return this.skip(); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); + browser.execute(ga.run, 'send', 'pageview'); + browser.waitUntil(log.hitCountEquals(1)); + expireSession(); + log.removeHits(); openNewWindow('/test/e2e/fixtures/autotrack.html?window=1'); browser.execute(ga.run, 'create', DEFAULT_TRACKER_FIELDS); @@ -176,12 +200,15 @@ describe('pageVisibilityTracker', function() { sessionTimeout: SESSION_TIMEOUT_IN_MINUTES, }); browser.execute(ga.run, 'send', 'pageview'); + browser.waitUntil(log.hitCountEquals(1)); browser.pause(SESSION_TIMEOUT_IN_MILLISECONDS / 3); browser.execute(ga.run, 'send', 'event', 'Uncategorized', 'unimportant'); + browser.waitUntil(log.hitCountEquals(2)); browser.pause(SESSION_TIMEOUT_IN_MILLISECONDS / 3); browser.execute(ga.run, 'send', 'event', 'Uncategorized', 'unimportant'); + browser.waitUntil(log.hitCountEquals(3)); browser.pause(SESSION_TIMEOUT_IN_MILLISECONDS / 3); openNewTab(); @@ -204,8 +231,11 @@ describe('pageVisibilityTracker', function() { browser.execute(ga.run, 'require', 'pageVisibilityTracker', { visibleThreshold: VISIBLE_THRESHOLD, - sessionTimeout: SESSION_TIMEOUT_IN_MINUTES, }); + browser.execute(ga.run, 'send', 'pageview'); + browser.waitUntil(log.hitCountEquals(1)); + log.removeHits(); + openNewTab(); closeAllButFirstTab(); openNewTab(); @@ -228,8 +258,9 @@ describe('pageVisibilityTracker', function() { browser.execute(ga.run, 'require', 'pageVisibilityTracker', { visibleThreshold: VISIBLE_THRESHOLD, - sessionTimeout: SESSION_TIMEOUT_IN_MINUTES, }); + browser.execute(ga.run, 'send', 'pageview'); + browser.waitUntil(log.hitCountEquals(1)); expireSession(); log.removeHits(); @@ -237,12 +268,9 @@ describe('pageVisibilityTracker', function() { openNewTab(); closeAllButFirstTab(); const start = Date.now(); - browser.waitUntil(log.hitCountEquals(1)); const end = Date.now(); - // Expects non-pageview hits queued to be sent after the session has timed - // out to include a pageview immediately before them. const hits = log.getHits(); assert.strictEqual(hits[0].t, 'pageview'); assert(hits[0].qt >= VISIBLE_THRESHOLD); @@ -258,13 +286,13 @@ describe('pageVisibilityTracker', function() { }; browser.execute(ga.run, 'require', 'pageVisibilityTracker', opts); + browser.waitUntil(log.hitCountEquals(1)); browser.pause(500); openNewTab('/test/e2e/fixtures/autotrack.html?tab=2'); browser.execute(ga.run, 'create', DEFAULT_TRACKER_FIELDS); browser.execute(ga.logHitData, testId); browser.execute(ga.run, 'require', 'pageVisibilityTracker', opts); - browser.waitUntil(log.hitCountEquals(3)); const hits = log.getHits(); @@ -282,8 +310,11 @@ describe('pageVisibilityTracker', function() { if (!browserSupportsTabs()) return this.skip(); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); + browser.execute(ga.run, 'send', 'pageview'); + browser.waitUntil(log.hitCountEquals(1)); corruptSession(); + log.removeHits(); openNewWindow('/test/e2e/fixtures/autotrack.html?window=1'); browser.execute(ga.run, 'create', DEFAULT_TRACKER_FIELDS); @@ -315,6 +346,7 @@ describe('pageVisibilityTracker', function() { }; browser.execute(ga.run, 'require', 'pageVisibilityTracker', opts); + browser.waitUntil(log.hitCountEquals(1)); browser.pause(500); openNewTabInBackground('/test/e2e/fixtures' + @@ -343,6 +375,7 @@ describe('pageVisibilityTracker', function() { }; browser.execute(ga.run, 'require', 'pageVisibilityTracker', opts); + browser.waitUntil(log.hitCountEquals(1)); browser.pause(500); const backgroundTab = openNewTabInBackground('/test/e2e/fixtures' + @@ -382,15 +415,12 @@ describe('pageVisibilityTracker', function() { }; browser.execute(ga.run, 'require', 'pageVisibilityTracker', opts); + browser.waitUntil(log.hitCountEquals(1)); browser.pause(500); const backgroundTab = openNewTabInBackground('/test/e2e/fixtures' + '/page-visibility-tracker-pageload.html?testId=' + testId); - browser.execute(ga.run, 'create', DEFAULT_TRACKER_FIELDS); - browser.execute(ga.logHitData, testId); - browser.execute(ga.run, 'require', 'pageVisibilityTracker', opts); - browser.waitUntil(log.hitCountEquals(2)); expireSession(); @@ -420,31 +450,36 @@ describe('pageVisibilityTracker', function() { const tab1 = browser.getCurrentTabId(); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); + browser.execute(ga.run, 'send', 'pageview'); + browser.waitUntil(log.hitCountEquals(1)); const window1 = openNewWindow('/test/e2e/fixtures/autotrack.html?window=1'); browser.execute(ga.run, 'create', DEFAULT_TRACKER_FIELDS); browser.execute(ga.logHitData, testId); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); + browser.waitUntil(log.hitCountEquals(2)); browser.close(tab1); // Close window1 and switch to tab1. - openNewTab(); - browser.waitUntil(log.hitCountEquals(3)); + openNewTab(); + browser.waitUntil(log.hitCountEquals(4)); + // Use the references to make the linter happy. assert(tab1 && window1); const hits = log.getHits(); + assert.strictEqual(hits[0].t, 'pageview'); // window1 change:visible - assert(hits[0].dl.endsWith('tab=1')); - assert.strictEqual(hits[0].ea, 'track'); - // window1 change:hidden - assert(hits[1].dl.endsWith('window=1')); + assert(hits[1].dl.endsWith('tab=1')); assert.strictEqual(hits[1].ea, 'track'); - // tab1 url change to tab=1a - assert(hits[2].dl.endsWith('tab=1')); + // window1 change:hidden + assert(hits[2].dl.endsWith('window=1')); assert.strictEqual(hits[2].ea, 'track'); + // tab1 url change to tab=1a + assert(hits[3].dl.endsWith('tab=1')); + assert.strictEqual(hits[3].ea, 'track'); }); it('reports visibility if the page path changes on a visible page', @@ -453,19 +488,23 @@ describe('pageVisibilityTracker', function() { browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); browser.execute(ga.run, 'send', 'pageview'); - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(1)); + browser.pause(500); // Simulate a URL change on the tracker. browser.execute(ga.run, 'set', 'page', '/test/e2e/fixtures/autotrack.html?tab=1a'); + browser.waitUntil(log.hitCountEquals(2)); + browser.execute(ga.run, 'send', 'pageview'); - browser.pause(randomInteger(500, 2000)); + browser.pause(500); // Simulate another URL change on the tracker. browser.execute(ga.run, 'set', 'page', '/test/e2e/fixtures/autotrack.html?tab=1b'); - browser.execute(ga.run, 'send', 'pageview'); + browser.waitUntil(log.hitCountEquals(4)); + browser.execute(ga.run, 'send', 'pageview'); browser.waitUntil(log.hitCountEquals(5)); const hits = log.getHits(); @@ -474,6 +513,7 @@ describe('pageVisibilityTracker', function() { assert.strictEqual(hits[0].t, 'pageview'); // Tab 1 url change to tab=1a assert(hits[1].dl.endsWith('tab=1')); + assert(!hits[1].dp); assert.strictEqual(hits[1].ea, 'track'); assert(Number(hits[1].ev) > 0); // Pageview @@ -495,23 +535,26 @@ describe('pageVisibilityTracker', function() { const tab1 = browser.getCurrentTabId(); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); browser.execute(ga.run, 'send', 'pageview'); - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(1)); + browser.pause(500); const tab2 = openNewTab('/test/e2e/fixtures/blank.html'); - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(2)); + browser.pause(500); const tab3 = openNewTab('/test/e2e/fixtures/autotrack.html?tab=3'); browser.execute(ga.run, 'create', DEFAULT_TRACKER_FIELDS); browser.execute(ga.logHitData, testId); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); browser.execute(ga.run, 'send', 'pageview'); - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(3)); + browser.pause(500); browser.close(tab2); // Close tab3 and go to tab2. - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(4)); + browser.pause(500); browser.close(tab1); // Close tab2 and go to tab1. - browser.waitUntil(log.hitCountEquals(4)); // Use the references to make the linter happy. assert(tab1 && tab2 && tab3); @@ -544,30 +587,35 @@ describe('pageVisibilityTracker', function() { const tab1 = browser.getCurrentTabId(); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); browser.execute(ga.run, 'send', 'pageview'); - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(1)); + browser.pause(500); const tab2 = openNewTab('/test/e2e/fixtures/autotrack.html?tab=2'); browser.execute(ga.run, 'create', DEFAULT_TRACKER_FIELDS); browser.execute(ga.logHitData, testId); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); browser.execute(ga.run, 'send', 'pageview'); - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(3)); + browser.pause(500); const tab3 = openNewTab('/test/e2e/fixtures/blank.html'); - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(4)); + browser.pause(500); const tab4 = openNewTab('/test/e2e/fixtures/autotrack.html?tab=4'); browser.execute(ga.run, 'create', DEFAULT_TRACKER_FIELDS); browser.execute(ga.logHitData, testId); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); browser.execute(ga.run, 'send', 'pageview'); - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(5)); + browser.pause(500); browser.close(tab3); // Close tab4 and go to tab3. - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(6)); + browser.pause(500); browser.close(tab2); // Close tab3 and go to tab2. - browser.pause(randomInteger(500, 2000)); + browser.pause(500); browser.close(tab1); // Close tab2 and go to tab1. browser.waitUntil(log.hitCountEquals(7)); @@ -615,45 +663,51 @@ describe('pageVisibilityTracker', function() { const tab1 = browser.getCurrentTabId(); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); browser.execute(ga.run, 'send', 'pageview'); - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(1)); + browser.pause(500); const tab2 = openNewTab('/test/e2e/fixtures/blank.html'); - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(2)); + browser.pause(500); const tab3 = openNewTab('/test/e2e/fixtures/autotrack.html?tab=3'); browser.execute(ga.run, 'create', DEFAULT_TRACKER_FIELDS); browser.execute(ga.logHitData, testId); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); browser.execute(ga.run, 'send', 'pageview'); - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(3)); + browser.pause(500); const window1 = openNewWindow('/test/e2e/fixtures/autotrack.html?window=1'); browser.execute(ga.run, 'create', DEFAULT_TRACKER_FIELDS); browser.execute(ga.logHitData, testId); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); browser.execute(ga.run, 'send', 'pageview'); - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(5)); + browser.pause(500); const window2 = openNewWindow('/test/e2e/fixtures/autotrack.html?window=2'); browser.execute(ga.run, 'create', DEFAULT_TRACKER_FIELDS); browser.execute(ga.logHitData, testId); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); browser.execute(ga.run, 'send', 'pageview'); - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(7)); + browser.pause(500); browser.close(tab3); // Close window2 and switch to tab3. - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(8)); + browser.pause(500); browser.close(window1); // Close tab3 and go to window1. - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(9)); + browser.pause(500); browser.close(tab2); // Close window1 and go to tab2. - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(10)); + browser.pause(500); browser.close(tab1); // Close tab2 and go to tab1. - browser.waitUntil(log.hitCountEquals(10)); - // Use the references to make the linter happy. assert(tab1 && tab2 && tab3 && window1 && window2); @@ -670,20 +724,45 @@ describe('pageVisibilityTracker', function() { // tab3 pageview (change:visible) assert(hits[2].dl.endsWith('tab=3')); assert.strictEqual(hits[2].t, 'pageview'); - // window1 change:visible - assert(hits[3].dl.endsWith('tab=3')); - assert.strictEqual(hits[3].ea, 'track'); - assert(Number(hits[3].ev) > 0); - // window1 pageview - assert(hits[4].dl.endsWith('window=1')); - assert.strictEqual(hits[4].t, 'pageview'); - // window2 change:visible - assert(hits[5].dl.endsWith('window=1')); - assert.strictEqual(hits[5].ea, 'track'); - assert(Number(hits[5].ev) > 0); - // window2 pageview - assert(hits[6].dl.endsWith('window=2')); - assert.strictEqual(hits[6].t, 'pageview'); + + // The following can race, so we check both orders. + try { + // window1 change:visible + assert(hits[3].dl.endsWith('tab=3')); + assert.strictEqual(hits[3].ea, 'track'); + assert(Number(hits[3].ev) > 0); + // window1 pageview + assert(hits[4].dl.endsWith('window=1')); + assert.strictEqual(hits[4].t, 'pageview'); + } catch (err) { + // window1 pageview + assert(hits[3].dl.endsWith('window=1')); + assert.strictEqual(hits[3].t, 'pageview'); + // window1 change:visible + assert(hits[4].dl.endsWith('tab=3')); + assert.strictEqual(hits[4].ea, 'track'); + assert(Number(hits[4].ev) > 0); + } + + // The following can race, so we check both orders. + try { + // window2 change:visible + assert(hits[5].dl.endsWith('window=1')); + assert.strictEqual(hits[5].ea, 'track'); + assert(Number(hits[5].ev) > 0); + // window2 pageview + assert(hits[6].dl.endsWith('window=2')); + assert.strictEqual(hits[6].t, 'pageview'); + } catch (err) { + // window2 pageview + assert(hits[5].dl.endsWith('window=2')); + assert.strictEqual(hits[5].t, 'pageview'); + // window2 change:visible + assert(hits[6].dl.endsWith('window=1')); + assert.strictEqual(hits[6].ea, 'track'); + assert(Number(hits[6].ev) > 0); + } + // window2 change:hidden assert(hits[7].dl.endsWith('window=2')); assert.strictEqual(hits[7].ea, 'track'); @@ -707,40 +786,40 @@ describe('pageVisibilityTracker', function() { const tab1 = browser.getCurrentTabId(); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); browser.execute(ga.run, 'send', 'pageview'); - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(1)); + browser.pause(500); const tab2 = openNewTab('/test/e2e/fixtures/blank.html'); - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(2)); + browser.pause(500); const tab3 = openNewTab('/test/e2e/fixtures/autotrack.html?tab=3'); browser.execute(ga.run, 'create', DEFAULT_TRACKER_FIELDS); browser.execute(ga.logHitData, testId); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); browser.execute(ga.run, 'send', 'pageview'); - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(3)); + browser.pause(500); const tab4 = openNewTab('/test/e2e/fixtures/autotrack.html?tab=4'); browser.execute(ga.run, 'create', DEFAULT_TRACKER_FIELDS); browser.execute(ga.logHitData, testId); browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); browser.execute(ga.run, 'send', 'pageview'); - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(5)); // Manually expire session 1 expireSession(); - browser.pause(randomInteger(500, 2000)); browser.close(tab3); // Close tab4 and go to tab3. - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(6)); + browser.pause(500); browser.close(tab2); // Close tab3 and go to tab2. - browser.pause(randomInteger(500, 2000)); + browser.waitUntil(log.hitCountEquals(7)); browser.close(tab1); // Close tab2 and go to tab1. - // browser.waitUntil(log.hitCountEquals(11)); - browser.pause(1000); - // Use the references to make the linter happy. assert(tab1 && tab2 && tab3 && tab4); @@ -791,6 +870,7 @@ describe('pageVisibilityTracker', function() { }, }); browser.execute(ga.run, 'send', 'pageview'); + browser.waitUntil(log.hitCountEquals(1)); openNewTab(); browser.waitUntil(log.hitCountEquals(2)); @@ -807,6 +887,7 @@ describe('pageVisibilityTracker', function() { browser.execute(requirePageVisibilityTracker_hitFilter); browser.execute(ga.run, 'send', 'pageview'); + browser.waitUntil(log.hitCountEquals(1)); openNewTab(); browser.waitUntil(log.hitCountEquals(2)); @@ -835,6 +916,7 @@ describe('pageVisibilityTracker', function() { it('destroys all bound events and functionality', () => { browser.execute(ga.run, 'require', 'pageVisibilityTracker', TEST_OPTS); browser.execute(ga.run, 'send', 'pageview'); + browser.waitUntil(log.hitCountEquals(1)); log.removeHits(); browser.execute(ga.run, 'pageVisibilityTracker:remove'); @@ -843,6 +925,7 @@ describe('pageVisibilityTracker', function() { log.assertNoHitsReceived(); }); }); + /* */ }); @@ -997,7 +1080,10 @@ function clearStorage() { * Manually expires the session. */ function expireSession() { - setStoreData('autotrack:UA-12345-1:session', {isExpired: true}); + setStoreData('autotrack:UA-12345-1:session', { + isExpired: true, + hitTime: +new Date, + }); } @@ -1053,14 +1139,3 @@ function requirePageVisibilityTracker_hitFilter() { }, }); } - - -/** - * Randomly picks an interger between the two passed values (inclusively). - * @param {number} min The lowest value to pick. - * @param {number} max The lowest value to pick. - * @return {number} The random integer picked. - */ -function randomInteger(min, max) { - return Math.floor(Math.random() * (max - min)) + min; -} From e613e54fe3277a0558eb7c402039c31110545194 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Fri, 17 Aug 2018 13:15:38 -0700 Subject: [PATCH 43/71] Update maxScrollTracker to use TrackerQueue --- lib/plugins/max-scroll-tracker.js | 160 ++++++++++++++++-------------- 1 file changed, 88 insertions(+), 72 deletions(-) diff --git a/lib/plugins/max-scroll-tracker.js b/lib/plugins/max-scroll-tracker.js index b1697113..c346e2b1 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,27 +48,33 @@ 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.init = this.init.bind(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(); + this.store = Store.getOrCreate( tracker.get('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(tracker).add(this.init); + } + /** + * Idly initializes the rest of the plugin instance initialization logic. + */ + init() { this.listenForMaxScrollChanges(); } @@ -82,7 +86,7 @@ class MaxScrollTracker { listenForMaxScrollChanges() { const maxScrollPercentage = this.getMaxScrollPercentageForCurrentPage(); if (maxScrollPercentage < 100) { - window.addEventListener('scroll', this.handleScroll); + addEventListener('scroll', this.handleScroll); } } @@ -91,53 +95,55 @@ 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() { - const pageHeight = getPageHeight(); - 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.add(({time}) => { + const pageHeight = getPageHeight(); + 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.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 +177,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.add(() => { + /** @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 +209,11 @@ class MaxScrollTracker { * @param {number} maxScrollPercentage */ setMaxScrollPercentageForCurrentPage(maxScrollPercentage) { - this.store.set({ - [this.pagePath]: maxScrollPercentage, - sessionId: this.session.getId(), + this.queue.add(() => { + this.store.update({ + [this.pagePath]: maxScrollPercentage, + sessionId: this.session.id, + }); }); } @@ -209,12 +222,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 +239,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); } From be29604183d00f3d9fcf505464793ee1851a7d02 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Fri, 17 Aug 2018 13:16:17 -0700 Subject: [PATCH 44/71] Update eventTracker to use TrackerQueue --- lib/plugins/event-tracker.js | 40 ++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/lib/plugins/event-tracker.js b/lib/plugins/event-tracker.js index a7bf2282..4f16f0e7 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); } /** @@ -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.add(({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(); }); From b75615abef345e59b263f30a05e85f848617dd85 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Fri, 17 Aug 2018 14:54:32 -0700 Subject: [PATCH 45/71] Update impressionTracker to use TrackerQueue --- lib/plugins/impression-tracker.js | 250 +++++++++++++++------------- test/e2e/impression-tracker-test.js | 3 + 2 files changed, 135 insertions(+), 118 deletions(-) diff --git a/lib/plugins/impression-tracker.js b/lib/plugins/impression-tracker.js index 2f93e22a..38543b08 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); + // 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.add(() => { + 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,76 @@ 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.add(() => { + 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.add(() => { + Object.keys(this.thresholdMap).forEach((key) => { + this.thresholdMap[key].disconnect(); + }); - this.mutationObserver.disconnect(); - this.mutationObserver = null; + this.mutationObserver.disconnect(); + this.mutationObserver = null; - this.items = []; - this.elementMap = {}; - this.thresholdMap = {}; + this.items = []; + this.elementMap = {}; + this.thresholdMap = {}; + }); } /** @@ -261,47 +269,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.add(({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.add(() => { + 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 +351,7 @@ class ImpressionTracker { * @private */ remove() { + this.queue.destroy(); this.unobserveAllElements(); } } diff --git a/test/e2e/impression-tracker-test.js b/test/e2e/impression-tracker-test.js index 6305a489..f1a128a1 100644 --- a/test/e2e/impression-tracker-test.js +++ b/test/e2e/impression-tracker-test.js @@ -50,6 +50,9 @@ const elementIdsByDomOrder = [ 'attrs', 'attrs-1', 'attrs-2', + 'fixture', + 'fixture-1', + 'fixture-2', ]; From 220bd716ab8dbf234d6ddc349fdd74f199189b0d Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Fri, 17 Aug 2018 19:18:52 -0700 Subject: [PATCH 46/71] Update mediaQueryTracker to use TrackerQueue --- lib/plugins/media-query-tracker.js | 37 +++++++++++++----- test/e2e/media-query-tracker-test.js | 58 ++++++++++++++++++---------- 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/lib/plugins/media-query-tracker.js b/lib/plugins/media-query-tracker.js index c832df78..3a201538 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); + 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.add(({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/test/e2e/media-query-tracker-test.js b/test/e2e/media-query-tracker-test.js index acbb62c0..7a005009 100644 --- a/test/e2e/media-query-tracker-test.js +++ b/test/e2e/media-query-tracker-test.js @@ -96,28 +96,30 @@ describe('mediaQueryTracker', function() { browser.setViewportSize({width: 400, height: 400}, false); browser.waitUntil(log.hitCountEquals(2)); - const hits = log.getHits(); - assert.strictEqual(hits[0].ec, 'Width'); + const hits = log.getHits().sort(sortHitDataByEventCategory); + + assert.strictEqual(hits[0].ec, 'Height'); assert.strictEqual(hits[0].ea, 'change'); - assert.strictEqual(hits[0].el, 'lg => sm'); - assert.strictEqual(hits[1].ec, 'Height'); + assert.strictEqual(hits[0].el, 'md => sm'); + assert.strictEqual(hits[1].ec, 'Width'); assert.strictEqual(hits[1].ea, 'change'); - assert.strictEqual(hits[1].el, 'md => sm'); + assert.strictEqual(hits[1].el, 'lg => sm'); }); + it('sends events as nonInteraction by default', () => { browser.execute(ga.run, 'require', 'mediaQueryTracker', opts); browser.setViewportSize({width: 400, height: 400}, false); browser.waitUntil(log.hitCountEquals(2)); - const hits = log.getHits(); - assert.strictEqual(hits[0].ec, 'Width'); + const hits = log.getHits().sort(sortHitDataByEventCategory); + assert.strictEqual(hits[0].ec, 'Height'); assert.strictEqual(hits[0].ea, 'change'); - assert.strictEqual(hits[0].el, 'lg => sm'); + assert.strictEqual(hits[0].el, 'md => sm'); assert.strictEqual(hits[0].ni, '1'); - assert.strictEqual(hits[1].ec, 'Height'); + assert.strictEqual(hits[1].ec, 'Width'); assert.strictEqual(hits[1].ea, 'change'); - assert.strictEqual(hits[1].el, 'md => sm'); + assert.strictEqual(hits[1].el, 'lg => sm'); assert.strictEqual(hits[1].ni, '1'); }); @@ -163,9 +165,9 @@ describe('mediaQueryTracker', function() { browser.setViewportSize({width: 400, height: 400}, false); browser.waitUntil(log.hitCountEquals(2)); - const hits = log.getHits(); - assert.strictEqual(hits[0].el, 'lg:sm'); - assert.strictEqual(hits[1].el, 'md:sm'); + const hits = log.getHits().sort(sortHitDataByEventCategory); + assert.strictEqual(hits[0].el, 'md:sm'); + assert.strictEqual(hits[1].el, 'lg:sm'); }); it('supports customizing any field via the fieldsObj', () => { @@ -179,14 +181,14 @@ describe('mediaQueryTracker', function() { browser.setViewportSize({width: 400, height: 400}, false); browser.waitUntil(log.hitCountEquals(2)); - const hits = log.getHits(); - assert.strictEqual(hits[0].ec, 'Width'); + const hits = log.getHits().sort(sortHitDataByEventCategory); + assert.strictEqual(hits[0].ec, 'Height'); assert.strictEqual(hits[0].ea, 'change'); - assert.strictEqual(hits[0].el, 'lg => sm'); + assert.strictEqual(hits[0].el, 'md => sm'); assert.strictEqual(hits[0].ni, '0'); - assert.strictEqual(hits[1].ec, 'Height'); + assert.strictEqual(hits[1].ec, 'Width'); assert.strictEqual(hits[1].ea, 'change'); - assert.strictEqual(hits[1].el, 'md => sm'); + assert.strictEqual(hits[1].el, 'lg => sm'); assert.strictEqual(hits[1].ni, '0'); }); @@ -224,9 +226,9 @@ describe('mediaQueryTracker', function() { browser.setViewportSize({width: 400, height: 400}, false); browser.waitUntil(log.hitCountEquals(2)); - const hits = log.getHits(); - assert.strictEqual(hits[0].ec, 'Width'); - assert.strictEqual(hits[1].ec, 'Height'); + const hits = log.getHits().sort(sortHitDataByEventCategory); + assert.strictEqual(hits[0].ec, 'Height'); + assert.strictEqual(hits[1].ec, 'Width'); log.removeHits(); browser.execute(ga.run, 'mediaQueryTracker:remove'); @@ -313,3 +315,17 @@ function requireMediaQueryTracker_hitFilter() { }); } + +/** + * A comparison function that sorts hits by the `ec` param. + * This is needed because the code wdio is injecting into the page to + * calculate the time seems to often be off for a few milliseconds. + * (This doesn't seem to happen when using the browser normally.) + * @param {Object} a The first hit to compare. + * @param {Object} b The second hit to compare. + * @return {number} A negative number if `a` should appear first in the sorted + * array, and a positive number if `b` should appear first. + */ +function sortHitDataByEventCategory(a, b) { + return a.ec < b.ec ? -1 : 1; +} From ed040946404e431081cb3ec2cf9e9efcc96efcdb Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Fri, 17 Aug 2018 20:53:39 -0700 Subject: [PATCH 47/71] Update socialWidgetTracker to use TrackerQueue --- lib/plugins/social-widget-tracker.js | 126 +++++++++++++++------------ 1 file changed, 72 insertions(+), 54 deletions(-) diff --git a/lib/plugins/social-widget-tracker.js b/lib/plugins/social-widget-tracker.js index a8a002af..0eb59eb0 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); + 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.add(() => { + if (window.FB) this.addFacebookEventHandlers(); + if (window.twttr) this.addTwitterEventHandlers(); + }); } /** @@ -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.add(({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.add(({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.add(({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.add(({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); } } From 79aad71aa5803aa8ad4ba377e09d19a9f377d1c3 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Fri, 17 Aug 2018 21:13:01 -0700 Subject: [PATCH 48/71] Update urlChangeTracker to use TrackerQueue --- lib/plugins/url-change-tracker.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/plugins/url-change-tracker.js b/lib/plugins/url-change-tracker.js index ae925855..c5218c90 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); + // Binds methods. this.pushStateOverride = this.pushStateOverride.bind(this); this.replaceStateOverride = this.replaceStateOverride.bind(this); @@ -107,9 +110,9 @@ 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(() => { + this.queue.add(({time}) => { const oldPath = this.path; const newPath = getPath(); @@ -123,12 +126,15 @@ class UrlChangeTracker { if (historyDidUpdate || this.opts.trackReplaceState) { /** @type {FieldsObj} */ - const defaultFields = {transport: 'beacon'}; + const defaultFields = { + transport: 'beacon', + queueTime: now() - time, + }; this.tracker.send('pageview', createFieldsObj(defaultFields, this.opts.fieldsObj, this.tracker, this.opts.hitFilter)); } } - }, 0); + }); } /** @@ -146,6 +152,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); From 9f732db050d385d2f317f381bc65d174374be5d1 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Fri, 17 Aug 2018 21:14:02 -0700 Subject: [PATCH 49/71] Use the now() utility instead of Date.now() --- lib/idle-queue.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/idle-queue.js b/lib/idle-queue.js index c5697f94..8330a6e7 100644 --- a/lib/idle-queue.js +++ b/lib/idle-queue.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import {cIC, isSafari, queueMicrotask, rIC} from './utilities'; +import {cIC, isSafari, now, queueMicrotask, rIC} from './utilities'; /** * A class wraps a queue of requestIdleCallback functions for two reasons: @@ -60,7 +60,7 @@ export default class IdleQueue { if (typeof tasks === 'function') tasks = [tasks]; const state = { - time: Date.now(), + time: now(), visibilityState: document.visibilityState, }; From e9ac57d6926a751e99f92dff4cd3014f3006cbb6 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Fri, 17 Aug 2018 21:29:34 -0700 Subject: [PATCH 50/71] Remove the local analytics.js test files --- test/analytics.js | 48 ------------ test/analytics_debug.js | 78 ------------------- test/e2e/fixtures/autotrack-rename.html | 2 +- test/e2e/fixtures/autotrack-reorder.html | 2 +- test/e2e/fixtures/autotrack.html | 2 +- test/e2e/fixtures/blank.html | 1 + test/e2e/fixtures/event-tracker.html | 2 +- test/e2e/fixtures/impression-tracker.html | 2 +- test/e2e/fixtures/max-scroll-tracker.html | 2 +- test/e2e/fixtures/outbound-form-tracker.html | 2 +- test/e2e/fixtures/outbound-link-tracker.html | 2 +- .../page-visibility-tracker-pageload.html | 2 +- test/e2e/fixtures/social-widget-tracker.html | 2 +- test/e2e/fixtures/url-change-tracker.html | 2 +- test/unit/index.html | 2 +- 15 files changed, 13 insertions(+), 138 deletions(-) delete mode 100644 test/analytics.js delete mode 100644 test/analytics_debug.js diff --git a/test/analytics.js b/test/analytics.js deleted file mode 100644 index e361479c..00000000 --- a/test/analytics.js +++ /dev/null @@ -1,48 +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,d&&(c.onload=d),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){return(a=a.match("(?:&|#|\\?)"+K(b).replace(/([.*+?^=!:${}()|\[\]\/\\])/g,"\\$1")+"=([^&#]*)"))&&2==a.length?a[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; -a=a.replace(/^http:/,"https:");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=j54","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){d.F&&(c=a.get(c),void 0!=c&&c!=d.defaultValue&&("boolean"==typeof c&&(c*=1),b.push(d.F+"="+K(""+c))))});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=a.get(Na);if(b=h(b).pending_experiments){var c=[];for(d in b)b.hasOwnProperty(d)&&b[d]&&c.push(encodeURIComponent(d)+"."+encodeURIComponent(b[d]));var d=c.join("!")}else d=void 0;d&&a.set(m,d,!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){a=a.get(b);return void 0==a?"":""+a},R=function(a,b){a=a.get(b);return void 0==a||""===a?0:1*a};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=0b.length)J(12);else{for(var c= -[],d=0;d=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){if(null==a)var c=a=1;else c=La(a),a=La(D(a,".")?a.substring(1):"."+a);for(var d=0;d=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)){if("string"===typeof arguments[0]){var b=arguments[0];var c=[].slice.call(arguments,1)}else 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={},Ed(this.ra,this.b)&&da(this.b.get(Na)))}}; -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"),sync: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)),c=c.palindrome?r:q,c=(c=M.cookie.replace(/^|(; +)/g,";").match(c))?c.sort().join("").substring(1):void 0,d.la=c,d.qa=E(a.b.get(kb)||"","gclid"));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);b=A.get(a)||[];for(var c=0;ca.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 12b8db2d..00000000 --- a/test/analytics_debug.js +++ /dev/null @@ -1,78 +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,d&&(c.onload=d),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){return(a=a.match("(?:&|#|\\?)"+ -P(b).replace(/([.*+?^=!:${}()|\[\]\/\\])/g,"\\$1")+"=([^&#]*)"))&&2==a.length?a[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;a=a.replace(/^http:/,"https:");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=j54d","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){d.i&&(c=a.get(c),void 0!=c&&c!=d.defaultValue&&("boolean"==typeof c&&(c*=1),b.push(d.i+"="+P(""+c))))});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){a=a.get(b);return void 0==a?"":""+a},jc=function(a,b){a=a.get(b);return void 0==a||""===a?0:1*a};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"),qa=W("customTask"),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),la=X("_gid","_gid"),ma=X("_ge"),na=X("_gcn"),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,b,c){this.Z=a;this.ja=b;this.fa=!1;this.ra=c;this.ia=1},ye=function(a,b,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;if((b=(b=Q.navigator)?b.plugins:null)&&b.length)for(var c=0;c=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=0b.length)F(12);else{for(var c=[],d=0;d=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){if(null==a)var c=a=1;else c=ic(a),a=ic(H(a,".")?a.substring(1):"."+a);for(var d=0;d=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{if("string"===typeof arguments[0]){var b=arguments[0];var c=[].slice.call(arguments,1)}else 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={},ye(this.ua,this.a)&&fa(this.a.get(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);var 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"),sync: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)),c=c.palindrome?r:q,c=(c=I.cookie.replace(/^|(; +)/g,";").match(c))?c.sort().join("").substring(1):void 0,d.oa=c,d.ta=aa(a.a.get(Pa)||"","gclid"));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);b=B.get(a)||[];for(var c=0;ca.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/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 @@ - +