Skip to content

Commit

Permalink
Add cookieless A/A test
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Karl Baker committed Jan 15, 2021
1 parent 79bdd29 commit 6ba279d
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 0 deletions.
116 changes: 116 additions & 0 deletions app/assets/javascripts/modules/cookieless-tracker.js
Original file line number Diff line number Diff line change
@@ -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)
32 changes: 32 additions & 0 deletions app/assets/javascripts/modules/track-variant.js
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 2 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
31 changes: 31 additions & 0 deletions app/controllers/concerns/cookieless_testable.rb
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
<% if @content_item.description %>
<meta name="description" content="<%= strip_tags(@content_item.description) %>" />
<% end %>

<%= cookieless_variant.analytics_meta_tag.html_safe %>
<% unless cookieless_variant.variant?('Z') %>
<meta name="Cookieless-Variant" content="<%= cookieless_variant.variant_name %>" data-module="track-variant">
<% end %>

<%= yield :extra_head_content %>
</head>
Expand Down
61 changes: 61 additions & 0 deletions spec/javascripts/track-variant-spec.js
Original file line number Diff line number Diff line change
@@ -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 = $('<meta name="Cookieless-Variant" content="A" data-module="track-variant">')

tracker.start(element)

expect(gaSpy.send).toHaveBeenCalledWith('cookieless', 'hit', {
trackerName: 'CookielessTracker',
label: 'A',
javaEnabled: false,
language: ''
})
})

it('tracks B variant', function () {
element = $('<meta name="Cookieless-Variant" content="B" data-module="track-variant">')

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 = $('<meta name="Cookieless-Variant" content="Z" data-module="track-variant">')

tracker.start(element)

expect(gaSpy.send).not.toHaveBeenCalled()
})
})
20 changes: 20 additions & 0 deletions test/controllers/content_items_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 6ba279d

Please sign in to comment.