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