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