From a5580a782864e67eb745f960b6786a5764d65cb1 Mon Sep 17 00:00:00 2001 From: Paul Hayes Date: Tue, 17 Feb 2015 14:13:46 +0000 Subject: [PATCH 01/24] Create analytics trackers with common interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Create classic and universal analytics trackers that can load their tracking code, track custom events, track page views – virtual and otherwise, and set custom dimensions/variables * Create a GOVUKTracker that let’s us track to both classic and universal simultaneously --- app/assets/javascripts/analytics.js | 3 + .../google-analytics-classic-tracker.js | 74 +++++++++++++++ .../google-analytics-universal-tracker.js | 79 +++++++++++++++ app/assets/javascripts/analytics/tracker.js | 32 +++++++ .../google-analytics-classic-tracker-spec.js | 91 ++++++++++++++++++ ...google-analytics-universal-tracker-spec.js | 95 +++++++++++++++++++ spec/javascripts/analytics/tracker-spec.js | 45 +++++++++ 7 files changed, 419 insertions(+) create mode 100644 app/assets/javascripts/analytics/google-analytics-classic-tracker.js create mode 100644 app/assets/javascripts/analytics/google-analytics-universal-tracker.js create mode 100644 app/assets/javascripts/analytics/tracker.js create mode 100644 spec/javascripts/analytics/google-analytics-classic-tracker-spec.js create mode 100644 spec/javascripts/analytics/google-analytics-universal-tracker-spec.js create mode 100644 spec/javascripts/analytics/tracker-spec.js diff --git a/app/assets/javascripts/analytics.js b/app/assets/javascripts/analytics.js index 9406213bc..9a8c9ff71 100644 --- a/app/assets/javascripts/analytics.js +++ b/app/assets/javascripts/analytics.js @@ -1,3 +1,6 @@ +//= require analytics/google-analytics-universal-tracker +//= require analytics/google-analytics-classic-tracker +//= require analytics/tracker //= require analytics/tracking //= require analytics/print-tracking //= require analytics/print-intent diff --git a/app/assets/javascripts/analytics/google-analytics-classic-tracker.js b/app/assets/javascripts/analytics/google-analytics-classic-tracker.js new file mode 100644 index 000000000..6d1daca5f --- /dev/null +++ b/app/assets/javascripts/analytics/google-analytics-classic-tracker.js @@ -0,0 +1,74 @@ +(function() { + "use strict"; + window.GOVUK = window.GOVUK || {}; + window.GOVUK.Analytics = window.GOVUK.Analytics || {}; + + GOVUK.Analytics.GoogleAnalyticsClassicTracker = function(id, cookieDomain) { + configureProfile(id, cookieDomain); + allowCrossDomainTracking(); + anonymizeIp(); + + function configureProfile(id, cookieDomain) { + _gaq.push(['_setAccount', id]); + // TODO: Check that this is acceptable + _gaq.push(['_setDomainName', cookieDomain]); + } + + function allowCrossDomainTracking() { + _gaq.push(['_setAllowLinker', true]); + } + + // https://developers.google.com/analytics/devguides/collection/gajs/methods/gaJSApi_gat#_gat._anonymizeIp + function anonymizeIp() { + _gaq.push(['_gat._anonymizeIp']); + } + }; + + GOVUK.Analytics.GoogleAnalyticsClassicTracker.load = function() { + var ga = document.createElement('script'), + s = document.getElementsByTagName('script')[0]; + + ga.async = true; + ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; + s.parentNode.insertBefore(ga, s); + }; + + // https://developers.google.com/analytics/devguides/collection/gajs/asyncMigrationExamples#VirtualPageviews + GOVUK.Analytics.GoogleAnalyticsClassicTracker.prototype.trackPageview = function(path) { + var pageview = ['_trackPageview']; + + if (typeof path === "string") { + pageview.push(path); + } + + _gaq.push(pageview); + }; + + // https://developers.google.com/analytics/devguides/collection/gajs/eventTrackerGuide + GOVUK.Analytics.GoogleAnalyticsClassicTracker.prototype.trackEvent = function(category, action, label, value) { + var evt = ["_trackEvent", category, action]; + + // Label is optional + if (typeof label === "string") { + evt.push(label); + } + + // Value is optional, but when used must be an + // integer, otherwise the event will be invalid + // and not logged + if (value) { + value = parseInt(value, 10); + if (typeof value === "number" && !isNaN(value)) { + evt.push(value); + } + } + + _gaq.push(evt); + }; + + // https://developers.google.com/analytics/devguides/collection/gajs/gaTrackingCustomVariables + GOVUK.Analytics.GoogleAnalyticsClassicTracker.prototype.setCustomVariable = function(index, value, name, scope) { + _gaq.push(['_setCustomVar', index, name, String(value), scope]); + }; + +})(); diff --git a/app/assets/javascripts/analytics/google-analytics-universal-tracker.js b/app/assets/javascripts/analytics/google-analytics-universal-tracker.js new file mode 100644 index 000000000..46f0a982b --- /dev/null +++ b/app/assets/javascripts/analytics/google-analytics-universal-tracker.js @@ -0,0 +1,79 @@ +(function() { + "use strict"; + window.GOVUK = window.GOVUK || {}; + window.GOVUK.Analytics = window.GOVUK.Analytics || {}; + + GOVUK.Analytics.GoogleAnalyticsUniversalTracker = function(id, cookieDomain) { + configureProfile(id, cookieDomain); + allowCrossDomainTracking(); + anonymizeIp(); + + function configureProfile(id, cookieDomain) { + ga('create', id, {'cookieDomain': cookieDomain}); + } + + function allowCrossDomainTracking() { + //TODO: Does Universal need this? Classic has it. + } + + function anonymizeIp() { + // https://developers.google.com/analytics/devguides/collection/analyticsjs/advanced#anonymizeip + ga('set', 'anonymizeIp', true); + } + }; + + GOVUK.Analytics.GoogleAnalyticsUniversalTracker.load = function() { + (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ + (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), + 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'); + }; + + // https://developers.google.com/analytics/devguides/collection/analyticsjs/pages + GOVUK.Analytics.GoogleAnalyticsUniversalTracker.prototype.trackPageview = function(path, title) { + if (typeof path === "string") { + var pageviewObject = { + page: path + }; + + if (typeof title === "string") { + pageviewObject.title = title; + } + ga('send', 'pageview', pageviewObject); + } else { + ga('send', 'pageview'); + } + }; + + // https://developers.google.com/analytics/devguides/collection/analyticsjs/events + GOVUK.Analytics.GoogleAnalyticsUniversalTracker.prototype.trackEvent = function(category, action, label, value) { + var evt = { + hitType: 'event', + eventCategory: category, + eventAction: action + }; + + // Label is optional + if (typeof label === "string") { + evt.eventLabel = label; + } + + // Value is optional, but when used must be an + // integer, otherwise the event will be invalid + // and not logged + if (value) { + value = parseInt(value, 10); + if (typeof value === "number" && !isNaN(value)) { + evt.eventValue = value; + } + } + + ga('send', evt); + }; + + // https://developers.google.com/analytics/devguides/collection/analyticsjs/custom-dims-mets + GOVUK.Analytics.GoogleAnalyticsUniversalTracker.prototype.setDimension = function(index, value) { + ga('set', 'dimension' + index, String(value)); + }; + +})(); diff --git a/app/assets/javascripts/analytics/tracker.js b/app/assets/javascripts/analytics/tracker.js new file mode 100644 index 000000000..0f20c03e2 --- /dev/null +++ b/app/assets/javascripts/analytics/tracker.js @@ -0,0 +1,32 @@ +(function() { + "use strict"; + window.GOVUK = window.GOVUK || {}; + window.GOVUK.Analytics = window.GOVUK.Analytics || {}; + + GOVUK.Analytics.Tracker = function(universalId, classicId) { + this.universal = new GOVUK.Analytics.GoogleAnalyticsUniversalTracker(universalId, '.www.gov.uk'); + this.classic = new GOVUK.Analytics.GoogleAnalyticsClassicTracker(classicId, '.www.gov.uk'); + this.trackPageview(); + }; + + GOVUK.Analytics.Tracker.load = function() { + GOVUK.Analytics.GoogleAnalyticsClassicTracker.load(); + GOVUK.Analytics.GoogleAnalyticsUniversalTracker.load(); + }; + + GOVUK.Analytics.Tracker.prototype.trackPageview = function(path, title) { + this.classic.trackPageview(path); + this.universal.trackPageview(path, title); + } + + GOVUK.Analytics.Tracker.prototype.trackEvent = function(category, action, label, value) { + this.classic.trackEvent(category, action, label, value); + this.universal.trackEvent(category, action, label, value); + } + + GOVUK.Analytics.Tracker.prototype.setDimension = function(index, value, name) { + var PAGE_LEVEL_SCOPE = 3; + this.universal.setDimension(index, value); + this.classic.setCustomVariable(index, value, name, PAGE_LEVEL_SCOPE); + }; +})(); diff --git a/spec/javascripts/analytics/google-analytics-classic-tracker-spec.js b/spec/javascripts/analytics/google-analytics-classic-tracker-spec.js new file mode 100644 index 000000000..f8d592148 --- /dev/null +++ b/spec/javascripts/analytics/google-analytics-classic-tracker-spec.js @@ -0,0 +1,91 @@ +describe("GOVUK.Analytics.GoogleAnalyticsClassicTracker", function() { + var classic; + + beforeEach(function() { + window._gaq = []; + classic = new GOVUK.Analytics.GoogleAnalyticsClassicTracker('id', 'cookie-domain.com'); + }); + + it('can load the libraries needed to run classic Google Analytics', function() { + delete window._gaq; + GOVUK.Analytics.GoogleAnalyticsClassicTracker.load(); + expect($('head script[async][src*="google-analytics.com/ga.js"]').length).toBe(1); + }); + + describe('when created', function() { + it('configures a Google tracker using the provided profile ID and cookie domain', function() { + expect(window._gaq[0]).toEqual(['_setAccount', 'id']); + expect(window._gaq[1]).toEqual(['_setDomainName', 'cookie-domain.com']); + }); + + it('allows cross site linking', function() { + expect(window._gaq[2]).toEqual(['_setAllowLinker', true]); + }); + + it('anonymises the IP', function() { + expect(window._gaq[3]).toEqual(['_gat._anonymizeIp']); + }); + }); + + describe('when pageviews are tracked', function() { + beforeEach(function() { + // reset queue after setup + window._gaq = []; + }); + + it('sends them to Google Analytics', function() { + classic.trackPageview(); + expect(window._gaq[0]).toEqual(['_trackPageview']); + }); + + it('can track a virtual pageview', function() { + classic.trackPageview('/nicholas-page'); + expect(window._gaq[0]).toEqual(['_trackPageview', '/nicholas-page']); + }); + }); + + describe('when events are tracked', function() { + beforeEach(function() { + // reset queue after setup + window._gaq = []; + }); + + it('sends them to Google Analytics', function() { + classic.trackEvent('category', 'action', 'label'); + expect(window._gaq[0]).toEqual(['_trackEvent', 'category', 'action', 'label']); + }); + + it('the label is optional', function() { + classic.trackEvent('category', 'action'); + expect(window._gaq[0]).toEqual(['_trackEvent', 'category', 'action']); + }); + + it('only sends values if they are parseable as numbers', function() { + classic.trackEvent('category', 'action', 'label', '10'); + expect(window._gaq[0]).toEqual(['_trackEvent', 'category', 'action', 'label', 10]); + + classic.trackEvent('category', 'action', 'label', 10); + expect(window._gaq[1]).toEqual(['_trackEvent', 'category', 'action', 'label', 10]); + + classic.trackEvent('category', 'action', 'label', 'not a number'); + expect(window._gaq[2]).toEqual(['_trackEvent', 'category', 'action', 'label']); + }); + }); + + describe('when setting a custom variable', function() { + beforeEach(function() { + // reset queue after setup + window._gaq = []; + }); + + it('sends the variable to Google Analytics', function() { + classic.setCustomVariable(1, 'value', 'name', 10); + expect(window._gaq[0]).toEqual(['_setCustomVar', 1, 'name', 'value', 10]); + }); + + it('coerces the value to a string', function() { + classic.setCustomVariable(1, 100, 'name', 10); + expect(window._gaq[0]).toEqual(['_setCustomVar', 1, 'name', '100', 10]); + }); + }); +}); diff --git a/spec/javascripts/analytics/google-analytics-universal-tracker-spec.js b/spec/javascripts/analytics/google-analytics-universal-tracker-spec.js new file mode 100644 index 000000000..d8ecf55bc --- /dev/null +++ b/spec/javascripts/analytics/google-analytics-universal-tracker-spec.js @@ -0,0 +1,95 @@ +describe("GOVUK.Analytics.GoogleAnalyticsUniversalTracker", function() { + var universal; + + beforeEach(function() { + window.ga = function() {}; + spyOn(window, 'ga'); + universal = new GOVUK.Analytics.GoogleAnalyticsUniversalTracker('id', 'cookie-domain.com'); + }); + + it('can load the libraries needed to run universal Google Analytics', function() { + delete window.ga; + GOVUK.Analytics.GoogleAnalyticsUniversalTracker.load(); + expect($('head script[async][src="//www.google-analytics.com/analytics.js"]').length).toBe(1); + expect(typeof window.ga).toBe('function'); + + window.ga('send message'); + expect(window.ga.q[0]).toEqual(jasmine.any(Object)); + }); + + describe('when created', function() { + var setupArguments; + + beforeEach(function() { + setupArguments = window.ga.calls.allArgs(); + }); + + it('configures a Google tracker using the provided profile ID and cookie domain', function() { + expect(setupArguments[0]).toEqual(['create', 'id', {'cookieDomain': 'cookie-domain.com'}]); + }); + + it('anonymises the IP', function() { + expect(setupArguments[1]).toEqual(['set', 'anonymizeIp', true]); + }); + }); + + describe('when pageviews are tracked', function() { + it('sends them to Google Analytics', function() { + universal.trackPageview(); + expect(window.ga.calls.mostRecent().args).toEqual(['send', 'pageview']); + }); + + it('can track a virtual pageview', function() { + universal.trackPageview('/nicholas-page'); + expect(window.ga.calls.mostRecent().args).toEqual(['send', 'pageview', {page: '/nicholas-page'}]); + }); + + it('can track a virtual pageview with a custom title', function() { + universal.trackPageview('/nicholas-page', 'Nicholas Page'); + expect(window.ga.calls.mostRecent().args).toEqual(['send', 'pageview', {page: '/nicholas-page', title: 'Nicholas Page'}]); + }); + }); + + describe('when events are tracked', function() { + function eventObjectFromSpy() { + return window.ga.calls.mostRecent().args[1]; + } + + it('sends them to Google Analytics', function() { + universal.trackEvent('category', 'action', 'label'); + expect(window.ga.calls.mostRecent().args).toEqual( + ['send', {hitType: 'event', eventCategory: 'category', eventAction: 'action', eventLabel: 'label'}] + ); + }); + + it('the label is optional', function() { + universal.trackEvent('category', 'action'); + expect(window.ga.calls.mostRecent().args).toEqual( + ['send', {hitType: 'event', eventCategory: 'category', eventAction: 'action'}] + ); + }); + + it('only sends values if they are parseable as numbers', function() { + universal.trackEvent('category', 'action', 'label', '10'); + expect(eventObjectFromSpy()['eventValue']).toEqual(10); + + universal.trackEvent('category', 'action', 'label', 10); + expect(eventObjectFromSpy()['eventValue']).toEqual(10); + + universal.trackEvent('category', 'action', 'label', 'not a number'); + expect(eventObjectFromSpy()['eventValue']).toEqual(undefined); + }); + }); + + describe('when setting a custom dimension', function() { + it('sends the dimension to Google Analytics with the specified index and value', function() { + universal.setDimension(1, 'value'); + expect(window.ga.calls.mostRecent().args).toEqual(['set', 'dimension1', 'value']); + }); + + it('coerces the value to a string', function() { + universal.setDimension(1, 10); + expect(window.ga.calls.mostRecent().args).toEqual(['set', 'dimension1', '10']); + }); + }); +}); diff --git a/spec/javascripts/analytics/tracker-spec.js b/spec/javascripts/analytics/tracker-spec.js new file mode 100644 index 000000000..415737889 --- /dev/null +++ b/spec/javascripts/analytics/tracker-spec.js @@ -0,0 +1,45 @@ +describe("GOVUK.Analytics.Tracker", function() { + var tracker; + + beforeEach(function() { + window._gaq = []; + window.ga = function() {}; + spyOn(window, 'ga'); + tracker = new GOVUK.Analytics.Tracker('universal-id', 'classic-id'); + }); + + describe('when created', function() { + it('configures classic and universal trackers', function() { + var universalSetupArguments = window.ga.calls.allArgs(); + expect(window._gaq[0]).toEqual(['_setAccount', 'classic-id']); + expect(window._gaq[1]).toEqual(['_setDomainName', '.www.gov.uk']); + expect(universalSetupArguments[0]).toEqual(['create', 'universal-id', {'cookieDomain': '.www.gov.uk'}]); + }); + + it('tracks a pageview in both classic and universal', function() { + var universalSetupArguments = window.ga.calls.allArgs(); + expect(window._gaq[4]).toEqual(['_trackPageview']); + expect(universalSetupArguments[2]).toEqual(['send', 'pageview']); + }); + }); + + describe('when tracking pageviews, events and custom dimensions', function() { + it('tracks in both classic and universal', function() { + window._gaq = []; + tracker.trackPageview('/path', 'Title'); + expect(window._gaq[0]).toEqual(['_trackPageview', '/path']); + expect(window.ga.calls.mostRecent().args).toEqual(['send', 'pageview', {page: '/path', title: 'Title'}]); + + window._gaq = []; + tracker.trackEvent('category', 'action'); + expect(window._gaq[0]).toEqual(['_trackEvent', 'category', 'action']); + expect(window.ga.calls.mostRecent().args).toEqual(['send', {hitType: 'event', eventCategory: 'category', eventAction: 'action'}]); + + window._gaq = []; + tracker.setDimension(1, 'value', 'name'); + expect(window._gaq[0]).toEqual(['_setCustomVar', 1, 'name', 'value', 3]); + expect(window.ga.calls.mostRecent().args).toEqual(['set', 'dimension1', 'value']); + }); + }); + +}); From 417e688305b1e9538f171874351db6ec962d96d8 Mon Sep 17 00:00:00 2001 From: Paul Hayes Date: Thu, 19 Feb 2015 10:38:06 +0000 Subject: [PATCH 02/24] Shorten naming of trackers Cleanup method declaration and improve readability. --- .../analytics/google-analytics-classic-tracker.js | 11 ++++++----- .../analytics/google-analytics-universal-tracker.js | 11 ++++++----- app/assets/javascripts/analytics/tracker.js | 12 +++++++----- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/analytics/google-analytics-classic-tracker.js b/app/assets/javascripts/analytics/google-analytics-classic-tracker.js index 6d1daca5f..804b710c1 100644 --- a/app/assets/javascripts/analytics/google-analytics-classic-tracker.js +++ b/app/assets/javascripts/analytics/google-analytics-classic-tracker.js @@ -3,7 +3,7 @@ window.GOVUK = window.GOVUK || {}; window.GOVUK.Analytics = window.GOVUK.Analytics || {}; - GOVUK.Analytics.GoogleAnalyticsClassicTracker = function(id, cookieDomain) { + var GoogleAnalyticsClassicTracker = function(id, cookieDomain) { configureProfile(id, cookieDomain); allowCrossDomainTracking(); anonymizeIp(); @@ -24,7 +24,7 @@ } }; - GOVUK.Analytics.GoogleAnalyticsClassicTracker.load = function() { + GoogleAnalyticsClassicTracker.load = function() { var ga = document.createElement('script'), s = document.getElementsByTagName('script')[0]; @@ -34,7 +34,7 @@ }; // https://developers.google.com/analytics/devguides/collection/gajs/asyncMigrationExamples#VirtualPageviews - GOVUK.Analytics.GoogleAnalyticsClassicTracker.prototype.trackPageview = function(path) { + GoogleAnalyticsClassicTracker.prototype.trackPageview = function(path) { var pageview = ['_trackPageview']; if (typeof path === "string") { @@ -45,7 +45,7 @@ }; // https://developers.google.com/analytics/devguides/collection/gajs/eventTrackerGuide - GOVUK.Analytics.GoogleAnalyticsClassicTracker.prototype.trackEvent = function(category, action, label, value) { + GoogleAnalyticsClassicTracker.prototype.trackEvent = function(category, action, label, value) { var evt = ["_trackEvent", category, action]; // Label is optional @@ -67,8 +67,9 @@ }; // https://developers.google.com/analytics/devguides/collection/gajs/gaTrackingCustomVariables - GOVUK.Analytics.GoogleAnalyticsClassicTracker.prototype.setCustomVariable = function(index, value, name, scope) { + GoogleAnalyticsClassicTracker.prototype.setCustomVariable = function(index, value, name, scope) { _gaq.push(['_setCustomVar', index, name, String(value), scope]); }; + GOVUK.Analytics.GoogleAnalyticsClassicTracker = GoogleAnalyticsClassicTracker; })(); diff --git a/app/assets/javascripts/analytics/google-analytics-universal-tracker.js b/app/assets/javascripts/analytics/google-analytics-universal-tracker.js index 46f0a982b..a3e15dcea 100644 --- a/app/assets/javascripts/analytics/google-analytics-universal-tracker.js +++ b/app/assets/javascripts/analytics/google-analytics-universal-tracker.js @@ -3,7 +3,7 @@ window.GOVUK = window.GOVUK || {}; window.GOVUK.Analytics = window.GOVUK.Analytics || {}; - GOVUK.Analytics.GoogleAnalyticsUniversalTracker = function(id, cookieDomain) { + var GoogleAnalyticsUniversalTracker = function(id, cookieDomain) { configureProfile(id, cookieDomain); allowCrossDomainTracking(); anonymizeIp(); @@ -22,7 +22,7 @@ } }; - GOVUK.Analytics.GoogleAnalyticsUniversalTracker.load = function() { + GoogleAnalyticsUniversalTracker.load = function() { (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) @@ -30,7 +30,7 @@ }; // https://developers.google.com/analytics/devguides/collection/analyticsjs/pages - GOVUK.Analytics.GoogleAnalyticsUniversalTracker.prototype.trackPageview = function(path, title) { + GoogleAnalyticsUniversalTracker.prototype.trackPageview = function(path, title) { if (typeof path === "string") { var pageviewObject = { page: path @@ -46,7 +46,7 @@ }; // https://developers.google.com/analytics/devguides/collection/analyticsjs/events - GOVUK.Analytics.GoogleAnalyticsUniversalTracker.prototype.trackEvent = function(category, action, label, value) { + GoogleAnalyticsUniversalTracker.prototype.trackEvent = function(category, action, label, value) { var evt = { hitType: 'event', eventCategory: category, @@ -72,8 +72,9 @@ }; // https://developers.google.com/analytics/devguides/collection/analyticsjs/custom-dims-mets - GOVUK.Analytics.GoogleAnalyticsUniversalTracker.prototype.setDimension = function(index, value) { + GoogleAnalyticsUniversalTracker.prototype.setDimension = function(index, value) { ga('set', 'dimension' + index, String(value)); }; + GOVUK.Analytics.GoogleAnalyticsUniversalTracker = GoogleAnalyticsUniversalTracker; })(); diff --git a/app/assets/javascripts/analytics/tracker.js b/app/assets/javascripts/analytics/tracker.js index 0f20c03e2..18bd2c8be 100644 --- a/app/assets/javascripts/analytics/tracker.js +++ b/app/assets/javascripts/analytics/tracker.js @@ -3,30 +3,32 @@ window.GOVUK = window.GOVUK || {}; window.GOVUK.Analytics = window.GOVUK.Analytics || {}; - GOVUK.Analytics.Tracker = function(universalId, classicId) { + var Tracker = function(universalId, classicId) { this.universal = new GOVUK.Analytics.GoogleAnalyticsUniversalTracker(universalId, '.www.gov.uk'); this.classic = new GOVUK.Analytics.GoogleAnalyticsClassicTracker(classicId, '.www.gov.uk'); this.trackPageview(); }; - GOVUK.Analytics.Tracker.load = function() { + Tracker.load = function() { GOVUK.Analytics.GoogleAnalyticsClassicTracker.load(); GOVUK.Analytics.GoogleAnalyticsUniversalTracker.load(); }; - GOVUK.Analytics.Tracker.prototype.trackPageview = function(path, title) { + Tracker.prototype.trackPageview = function(path, title) { this.classic.trackPageview(path); this.universal.trackPageview(path, title); } - GOVUK.Analytics.Tracker.prototype.trackEvent = function(category, action, label, value) { + Tracker.prototype.trackEvent = function(category, action, label, value) { this.classic.trackEvent(category, action, label, value); this.universal.trackEvent(category, action, label, value); } - GOVUK.Analytics.Tracker.prototype.setDimension = function(index, value, name) { + Tracker.prototype.setDimension = function(index, value, name) { var PAGE_LEVEL_SCOPE = 3; this.universal.setDimension(index, value); this.classic.setCustomVariable(index, value, name, PAGE_LEVEL_SCOPE); }; + + GOVUK.Analytics.Tracker = Tracker; })(); From 222edbda2c5f112e256114d554081d85520db14b Mon Sep 17 00:00:00 2001 From: Paul Hayes Date: Thu, 19 Feb 2015 10:39:01 +0000 Subject: [PATCH 03/24] Ensure that the GA queue exists for classic Avoid pushing to an undefined array by creating that array when the tracker is created. --- .../javascripts/analytics/google-analytics-classic-tracker.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/analytics/google-analytics-classic-tracker.js b/app/assets/javascripts/analytics/google-analytics-classic-tracker.js index 804b710c1..a46966101 100644 --- a/app/assets/javascripts/analytics/google-analytics-classic-tracker.js +++ b/app/assets/javascripts/analytics/google-analytics-classic-tracker.js @@ -4,6 +4,7 @@ window.GOVUK.Analytics = window.GOVUK.Analytics || {}; var GoogleAnalyticsClassicTracker = function(id, cookieDomain) { + window._gaq = window._gaq || []; configureProfile(id, cookieDomain); allowCrossDomainTracking(); anonymizeIp(); From bf793cd45e1b77da60ab0a21df1077b3a57d1e5a Mon Sep 17 00:00:00 2001 From: Paul Hayes Date: Thu, 19 Feb 2015 11:02:28 +0000 Subject: [PATCH 04/24] Set pixel density and http status code dimensions When starting a tracker, set custom dimensions before the initial pageview is tracked. --- app/assets/javascripts/analytics/tracker.js | 31 +++++++++++++++++---- spec/javascripts/analytics/tracker-spec.js | 22 ++++++++++++--- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/analytics/tracker.js b/app/assets/javascripts/analytics/tracker.js index 18bd2c8be..7b46ff1df 100644 --- a/app/assets/javascripts/analytics/tracker.js +++ b/app/assets/javascripts/analytics/tracker.js @@ -4,9 +4,29 @@ window.GOVUK.Analytics = window.GOVUK.Analytics || {}; var Tracker = function(universalId, classicId) { - this.universal = new GOVUK.Analytics.GoogleAnalyticsUniversalTracker(universalId, '.www.gov.uk'); - this.classic = new GOVUK.Analytics.GoogleAnalyticsClassicTracker(classicId, '.www.gov.uk'); - this.trackPageview(); + var self = this; + self.universal = new GOVUK.Analytics.GoogleAnalyticsUniversalTracker(universalId, '.www.gov.uk'); + self.classic = new GOVUK.Analytics.GoogleAnalyticsClassicTracker(classicId, '.www.gov.uk'); + + setPixelDensityDimension(); + setHTTPStatusCodeDimension(); + + self.trackPageview(); + + function setPixelDensityDimension() { + var pixelRatioDimensionIndex = 11; + + if (window.devicePixelRatio) { + self.setDimension(pixelRatioDimensionIndex, window.devicePixelRatio, 'Pixel Ratio', 2); + } + } + + function setHTTPStatusCodeDimension() { + var statusCode = window.httpStatusCode || 200, + statusCodeDimensionIndex = 15; + + self.setDimension(statusCodeDimensionIndex, statusCode, 'httpStatusCode'); + } }; Tracker.load = function() { @@ -24,10 +44,11 @@ this.universal.trackEvent(category, action, label, value); } - Tracker.prototype.setDimension = function(index, value, name) { + Tracker.prototype.setDimension = function(index, value, name, scope) { var PAGE_LEVEL_SCOPE = 3; + scope = scope || PAGE_LEVEL_SCOPE; this.universal.setDimension(index, value); - this.classic.setCustomVariable(index, value, name, PAGE_LEVEL_SCOPE); + this.classic.setCustomVariable(index, value, name, scope); }; GOVUK.Analytics.Tracker = Tracker; diff --git a/spec/javascripts/analytics/tracker-spec.js b/spec/javascripts/analytics/tracker-spec.js index 415737889..5ca5ef0e5 100644 --- a/spec/javascripts/analytics/tracker-spec.js +++ b/spec/javascripts/analytics/tracker-spec.js @@ -9,17 +9,31 @@ describe("GOVUK.Analytics.Tracker", function() { }); describe('when created', function() { + var universalSetupArguments; + + beforeEach(function() { + universalSetupArguments = window.ga.calls.allArgs(); + }); + it('configures classic and universal trackers', function() { - var universalSetupArguments = window.ga.calls.allArgs(); expect(window._gaq[0]).toEqual(['_setAccount', 'classic-id']); expect(window._gaq[1]).toEqual(['_setDomainName', '.www.gov.uk']); expect(universalSetupArguments[0]).toEqual(['create', 'universal-id', {'cookieDomain': '.www.gov.uk'}]); }); + it('sets the device pixel ratio', function() { + expect(window._gaq[4][2]).toEqual('Pixel Ratio'); + expect(universalSetupArguments[2][1]).toEqual('dimension11'); + }); + + it('sets the HTTP status code', function() { + expect(window._gaq[5][2]).toEqual('httpStatusCode'); + expect(universalSetupArguments[3][1]).toEqual('dimension15'); + }); + it('tracks a pageview in both classic and universal', function() { - var universalSetupArguments = window.ga.calls.allArgs(); - expect(window._gaq[4]).toEqual(['_trackPageview']); - expect(universalSetupArguments[2]).toEqual(['send', 'pageview']); + expect(window._gaq[6]).toEqual(['_trackPageview']); + expect(universalSetupArguments[4]).toEqual(['send', 'pageview']); }); }); From d2556c30fe5b1fdbc2484092c8b0404d4f6df7b9 Mon Sep 17 00:00:00 2001 From: Paul Hayes Date: Thu, 19 Feb 2015 12:20:26 +0000 Subject: [PATCH 05/24] Shim next page parameters cookie into universal A cookie is sometimes set by apps to declare the GA parameters that should run on the subsequent page. These declare the actual methods to call in classic analytics. This is a temporary shim to ensure these are applied to both classic and universal before updating apps. * Pull cookie code from current analytics snippet * Modify to go through setDimension rather than directly to GA * Parse index and scope variables as integers --- app/assets/javascripts/analytics/tracker.js | 28 +++++++++++++++++++++ spec/javascripts/analytics/tracker-spec.js | 14 +++++++++++ 2 files changed, 42 insertions(+) diff --git a/app/assets/javascripts/analytics/tracker.js b/app/assets/javascripts/analytics/tracker.js index 7b46ff1df..0426e0b85 100644 --- a/app/assets/javascripts/analytics/tracker.js +++ b/app/assets/javascripts/analytics/tracker.js @@ -10,6 +10,7 @@ setPixelDensityDimension(); setHTTPStatusCodeDimension(); + shimNextPageParams(); self.trackPageview(); @@ -27,6 +28,24 @@ self.setDimension(statusCodeDimensionIndex, statusCode, 'httpStatusCode'); } + + function shimNextPageParams() { + // A cookie is sometimes set by apps to declare the GA parameters that + // should run on the subsequent page. These declare the actual methods + // to call in classic analytics. This is a temporary shim to ensure these + // are applied to both classic and universal before updating apps. + if (GOVUK.cookie && GOVUK.cookie('ga_nextpage_params') !== null){ + var classicParams = GOVUK.cookie('ga_nextpage_params').split(','); + + if (classicParams[0] == "_setCustomVar") { + // index, value, name, scope + self.setDimension(classicParams[1], classicParams[3], classicParams[2], classicParams[4]); + } + + // Delete cookie + GOVUK.cookie('ga_nextpage_params', null); + } + } }; Tracker.load = function() { @@ -47,6 +66,15 @@ Tracker.prototype.setDimension = function(index, value, name, scope) { var PAGE_LEVEL_SCOPE = 3; scope = scope || PAGE_LEVEL_SCOPE; + + if (typeof index !== "number") { + index = parseInt(index, 10); + } + + if (typeof scope !== "number") { + scope = parseInt(scope, 10); + } + this.universal.setDimension(index, value); this.classic.setCustomVariable(index, value, name, scope); }; diff --git a/spec/javascripts/analytics/tracker-spec.js b/spec/javascripts/analytics/tracker-spec.js index 5ca5ef0e5..c8c67bc29 100644 --- a/spec/javascripts/analytics/tracker-spec.js +++ b/spec/javascripts/analytics/tracker-spec.js @@ -35,6 +35,20 @@ describe("GOVUK.Analytics.Tracker", function() { expect(window._gaq[6]).toEqual(['_trackPageview']); expect(universalSetupArguments[4]).toEqual(['send', 'pageview']); }); + + describe('when there is a cookie with next page parameters set', function() { + it('sets them as a dimension', function() { + window.ga.calls.reset(); + window._gaq = []; + spyOn(GOVUK, 'cookie').and.returnValue("_setCustomVar,21,name,value,3"); + tracker = new GOVUK.Analytics.Tracker('universal-id', 'classic-id'); + universalSetupArguments = window.ga.calls.allArgs(); + + expect(window._gaq[6]).toEqual(['_setCustomVar', 21, 'name', 'value', 3]); + expect(universalSetupArguments[4]).toEqual(['set', 'dimension21', 'value']); + }); + }); + }); describe('when tracking pageviews, events and custom dimensions', function() { From 4f1710d22337dc14e850c6bd1e948aaceb6c21b0 Mon Sep 17 00:00:00 2001 From: Paul Hayes Date: Thu, 19 Feb 2015 13:35:45 +0000 Subject: [PATCH 06/24] Remove analytics tracking code from template Analytics and the loading of the libraries is now within a cacheable, compressible JS file. Avoid repeating this javascript on every page. * Include a comment indicating that slimmer is still inserting custom variables --- app/views/root/_google_analytics.html.erb | 52 +++-------------------- 1 file changed, 5 insertions(+), 47 deletions(-) diff --git a/app/views/root/_google_analytics.html.erb b/app/views/root/_google_analytics.html.erb index f92a98fd2..1ba3289fa 100644 --- a/app/views/root/_google_analytics.html.erb +++ b/app/views/root/_google_analytics.html.erb @@ -1,51 +1,9 @@ - - - -<%# Universal analytics %> - From 2d585d055b62ace19a9783fc78a6d8d6a14e209c Mon Sep 17 00:00:00 2001 From: Paul Hayes Date: Thu, 19 Feb 2015 15:10:42 +0000 Subject: [PATCH 07/24] Intercept injected custom vars, send to universal Slimmer inserts classic custom variables into the ga-params script tag, we need to intercept these and track them in both universal and classic. (Slimmer is being left alone for the time being) * Intercept the classic analytics queue _gaq before initialising analytics * Use this queue after initialisation to setup custom variables before the initial pageview * Move the google_analytics include to load before the application javascript so the slimmer variables can be distinguished from those added later * Compress the javascript within ga-params element --- app/assets/javascripts/analytics/tracker.js | 36 +++++++++++++++++++-- app/views/root/_google_analytics.html.erb | 4 +-- app/views/root/_javascript.html.erb | 2 +- spec/javascripts/analytics/tracker-spec.js | 23 +++++++++++++ 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/analytics/tracker.js b/app/assets/javascripts/analytics/tracker.js index 0426e0b85..49545ec35 100644 --- a/app/assets/javascripts/analytics/tracker.js +++ b/app/assets/javascripts/analytics/tracker.js @@ -4,16 +4,33 @@ window.GOVUK.Analytics = window.GOVUK.Analytics || {}; var Tracker = function(universalId, classicId) { - var self = this; + var self = this, + classicQueue; + + classicQueue = getClassicAnalyticsQueue(); + resetClassicAnalyticsQueue(); + self.universal = new GOVUK.Analytics.GoogleAnalyticsUniversalTracker(universalId, '.www.gov.uk'); self.classic = new GOVUK.Analytics.GoogleAnalyticsClassicTracker(classicId, '.www.gov.uk'); setPixelDensityDimension(); setHTTPStatusCodeDimension(); shimNextPageParams(); + shimClassicAnalyticsQueue(classicQueue); self.trackPageview(); + function getClassicAnalyticsQueue() { + // Slimmer inserts custom variables into the ga-params script tag + // https://github.com/alphagov/slimmer/blob/master/lib/slimmer/processors/google_analytics_configurator.rb + // Pickout these variables before continuing + return (window._gaq && window._gaq.length) > 0 ? window._gaq.slice() : []; + } + + function resetClassicAnalyticsQueue() { + window._gaq = []; + } + function setPixelDensityDimension() { var pixelRatioDimensionIndex = 11; @@ -38,14 +55,27 @@ var classicParams = GOVUK.cookie('ga_nextpage_params').split(','); if (classicParams[0] == "_setCustomVar") { - // index, value, name, scope - self.setDimension(classicParams[1], classicParams[3], classicParams[2], classicParams[4]); + setDimensionFromCustomVariable(classicParams); } // Delete cookie GOVUK.cookie('ga_nextpage_params', null); } } + + function shimClassicAnalyticsQueue(queue) { + $.each(queue, function(index, classicParams) { + if (classicParams[0] == "_setCustomVar") { + setDimensionFromCustomVariable(classicParams); + } + }); + } + + function setDimensionFromCustomVariable(customVar) { + // index, value, name, scope + self.setDimension(customVar[1], customVar[3], customVar[2], customVar[4]); + } + }; Tracker.load = function() { diff --git a/app/views/root/_google_analytics.html.erb b/app/views/root/_google_analytics.html.erb index 1ba3289fa..95b5df949 100644 --- a/app/views/root/_google_analytics.html.erb +++ b/app/views/root/_google_analytics.html.erb @@ -1,7 +1,5 @@