From 6ba279dc78f33e7f2d0e3db10bd58ede57983211 Mon Sep 17 00:00:00 2001 From: Karl Baker Date: Wed, 23 Dec 2020 12:32:12 +0000 Subject: [PATCH] Add cookieless A/A test This commit adds the cookieless A/A test to government-frontend, which fires on every request. The purpose of the cookieless A/A test is to test an approach with our CDN which does not use cookies for A/B testing, and is something that we intend to use to A/B test the cookie consent banner should the A/A test be successful; as the cookie consent banner is presented up until users either consent to or decline cookies, we need to be able to understand which variant a user falls in before they have consented to cookies. The data involved in this is the variant of the test only - no other data is being sent to analytics, and data that is generally sent by default has been masked by hardcoding the various properties. This test has been approved by IA on the basis set out above, for collecting only variant data. In terms of the changes in this commit, there is _some_ duplication with the cookieless-tracker.js file, which has been largely copied from _Static_ but scoped only to send the data that we need to send. This approach has been verified with @DilwoarH. --- .../javascripts/modules/cookieless-tracker.js | 116 ++++++++++++++++++ .../javascripts/modules/track-variant.js | 32 +++++ app/controllers/application_controller.rb | 2 + .../concerns/cookieless_testable.rb | 31 +++++ app/views/layouts/application.html.erb | 5 + spec/javascripts/track-variant-spec.js | 61 +++++++++ .../content_items_controller_test.rb | 20 +++ 7 files changed, 267 insertions(+) create mode 100644 app/assets/javascripts/modules/cookieless-tracker.js create mode 100644 app/assets/javascripts/modules/track-variant.js create mode 100644 app/controllers/concerns/cookieless_testable.rb create mode 100644 spec/javascripts/track-variant-spec.js diff --git a/app/assets/javascripts/modules/cookieless-tracker.js b/app/assets/javascripts/modules/cookieless-tracker.js new file mode 100644 index 000000000..25604ae64 --- /dev/null +++ b/app/assets/javascripts/modules/cookieless-tracker.js @@ -0,0 +1,116 @@ +window.GOVUK = window.GOVUK || {} +window.GOVUK.Modules = window.GOVUK.Modules || {}; + +(function (Modules) { + var CookielessTracker = function (trackingId, fieldsObject) { + var trackerName = fieldsObject.name + '.' + + function configureProfile () { + // https://developers.google.com/analytics/devguides/collection/analyticsjs/command-queue-reference#create + sendToGa('create', trackingId, fieldsObject) + } + + function anonymizeIp () { + // https://developers.google.com/analytics/devguides/collection/analyticsjs/advanced#anonymizeip + sendToGa(trackerName + 'set', 'anonymizeIp', true) + } + + function disableAdFeatures () { + // https://developers.google.com/analytics/devguides/collection/analyticsjs/field-reference#allowAdFeatures + sendToGa(trackerName + 'set', 'allowAdFeatures', false) + } + + function stripTitlePII () { + sendToGa(trackerName + 'set', 'title', '') + } + + function stripLocationPII () { + sendToGa(trackerName + 'set', 'location', '') + } + + function load () { + /* eslint-disable */ + (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', 'https://www.google-analytics.com/analytics.js', 'ga') + /* eslint-enable */ + } + + // Support legacy cookieDomain param + if (typeof fieldsObject === 'string') { + fieldsObject = { cookieDomain: fieldsObject } + } + + load() + configureProfile() + anonymizeIp() + disableAdFeatures() + stripTitlePII() + stripLocationPII() + } + + CookielessTracker.load = function () { + /* eslint-disable */ + (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', 'https://www.google-analytics.com/analytics.js', 'ga') + /* eslint-enable */ + } + + // https://developers.google.com/analytics/devguides/collection/analyticsjs/events + CookielessTracker.prototype.trackEvent = function (category, action, options) { + options = options || {} + var value + var trackerName = '' + var evt = { + hitType: 'event', + eventCategory: category, + eventAction: action + } + + // Label is optional + if (typeof options.label === 'string') { + evt.eventLabel = options.label + delete options.label + } + + // Value is optional, but when used must be an + // integer, otherwise the event will be invalid + // and not logged + if (options.value || options.value === 0) { + value = parseInt(options.value, 10) + if (typeof value === 'number' && !isNaN(value)) { + options.eventValue = value + } + delete options.value + } + + // trackerName is optional + if (typeof options.trackerName === 'string') { + trackerName = options.trackerName + '.' + delete options.trackerName + } + + // Prevents an event from affecting bounce rate + // https://developers.google.com/analytics/devguides/collection/analyticsjs/events#implementation + if (options.nonInteraction) { + options.nonInteraction = 1 + } + + if (typeof options === 'object') { + $.extend(evt, options) + } + + sendToGa(trackerName + 'send', evt) + } + + function sendToGa () { + if (typeof window.ga === 'function') { + window.ga.apply(window, arguments) + } + } + + Modules.CookielessTracker = CookielessTracker +})(window.GOVUK.Modules) diff --git a/app/assets/javascripts/modules/track-variant.js b/app/assets/javascripts/modules/track-variant.js new file mode 100644 index 000000000..11dcbc469 --- /dev/null +++ b/app/assets/javascripts/modules/track-variant.js @@ -0,0 +1,32 @@ +window.GOVUK.Modules = window.GOVUK.Modules || {}; + +(function (Modules) { + 'use strict' + + Modules.TrackVariant = function () { + this.start = function ($element) { + var element = $element[0] + + if (window.GOVUK.cookie('cookies_preferences_set') !== 'true') { + var variant = element.getAttribute('content') + + if (variant === undefined) { + return + } + + var cookielessTracker = new GOVUK.Modules.CookielessTracker('UA-26179049-29', { + name: 'CookielessTracker', + storage: 'none', + clientId: '0' + }) + + cookielessTracker.trackEvent('cookieless', 'hit', { + trackerName: 'CookielessTracker', + label: variant, + javaEnabled: false, + language: '' + }) + } + } + } +})(window.GOVUK.Modules) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 41e929d77..69976c1bd 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -3,6 +3,8 @@ class ApplicationController < ActionController::Base # For APIs, you may want to use :null_session instead. protect_from_forgery except: :service_sign_in_options + include CookielessTestable + if ENV["BASIC_AUTH_USERNAME"] http_basic_authenticate_with( name: ENV.fetch("BASIC_AUTH_USERNAME"), diff --git a/app/controllers/concerns/cookieless_testable.rb b/app/controllers/concerns/cookieless_testable.rb new file mode 100644 index 000000000..ee957fab9 --- /dev/null +++ b/app/controllers/concerns/cookieless_testable.rb @@ -0,0 +1,31 @@ +module CookielessTestable + extend ActiveSupport::Concern + + CUSTOM_DIMENSION = 49 + + def self.included(base) + base.helper_method( + :cookieless_variant, + ) + base.after_action :set_test_response_header + end + + def cookieless_variant + @cookieless_variant ||= cookieless_test.requested_variant(request.headers) + end + +private + + def cookieless_test + @cookieless_test ||= GovukAbTesting::AbTest.new( + "CookielessAATest", + dimension: CUSTOM_DIMENSION, + allowed_variants: %w[A B Z], + control_variant: "Z", + ) + end + + def set_test_response_header + cookieless_variant.configure_response(response) + end +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 312c8dcd3..bc7c9ac1e 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -26,6 +26,11 @@ <% if @content_item.description %> <% end %> + + <%= cookieless_variant.analytics_meta_tag.html_safe %> + <% unless cookieless_variant.variant?('Z') %> + + <% end %> <%= yield :extra_head_content %> diff --git a/spec/javascripts/track-variant-spec.js b/spec/javascripts/track-variant-spec.js new file mode 100644 index 000000000..91be96462 --- /dev/null +++ b/spec/javascripts/track-variant-spec.js @@ -0,0 +1,61 @@ +describe('Test variant tracker', function () { + 'use strict' + + var tracker, + element, + FakeCookielessTracker, + gaSpy + + beforeEach(function () { + GOVUK.cookie('cookies_preferences_set', null) + gaSpy = jasmine.createSpyObj('initGa', ['send']) + + FakeCookielessTracker = function (trackingId, fieldsObject) {} + FakeCookielessTracker.prototype.trackEvent = function (category, action, options) { + gaSpy.send(category, action, options) + } + + GOVUK.Modules.CookielessTracker = FakeCookielessTracker + + tracker = new GOVUK.Modules.TrackVariant() + }) + + afterEach(function () { + GOVUK.Modules.CookielessTracker = null + }) + + it('tracks A variant', function () { + element = $('') + + tracker.start(element) + + expect(gaSpy.send).toHaveBeenCalledWith('cookieless', 'hit', { + trackerName: 'CookielessTracker', + label: 'A', + javaEnabled: false, + language: '' + }) + }) + + it('tracks B variant', function () { + element = $('') + + tracker.start(element) + + expect(gaSpy.send).toHaveBeenCalledWith('cookieless', 'hit', { + trackerName: 'CookielessTracker', + label: 'B', + javaEnabled: false, + language: '' + }) + }) + + it('does not track variant if cookie is set', function () { + GOVUK.cookie('cookies_preferences_set', true) + element = $('') + + tracker.start(element) + + expect(gaSpy.send).not.toHaveBeenCalled() + }) +}) diff --git a/test/controllers/content_items_controller_test.rb b/test/controllers/content_items_controller_test.rb index e0b2ed42c..8995e9ac2 100644 --- a/test/controllers/content_items_controller_test.rb +++ b/test/controllers/content_items_controller_test.rb @@ -385,6 +385,26 @@ class ContentItemsControllerTest < ActionController::TestCase assert_select ".gem-c-contextual-footer", false end + %w[A B].each do |test_variant| + test "record cookieless hit when in variant #{test_variant}" do + with_variant CookielessAATest: test_variant.to_s do + content_item = content_store_has_schema_example("case_study", "case_study") + + get :show, params: { path: path_for(content_item) } + assert_select "meta[data-module=track-variant][content=#{test_variant}]" + end + end + end + + test "not record cookieless hit when in variant Z" do + with_variant CookielessAATest: "Z" do + content_item = content_store_has_schema_example("case_study", "case_study") + + get :show, params: { path: path_for(content_item) } + assert_select "meta[data-module=track-variant]", false + end + end + def path_for(content_item, locale = nil) base_path = content_item["base_path"].sub(/^\//, "") base_path.gsub!(/\.#{locale}$/, "") if locale