Skip to content

Commit

Permalink
Merge pull request #2864 from alphagov/ga4-schema-rework
Browse files Browse the repository at this point in the history
GA4 analytics schema rework
  • Loading branch information
andysellick authored Jul 21, 2022
2 parents 7552874 + b0e7a91 commit 57f187f
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 279 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
useful summary for people upgrading their application, not a replication
of the commit log.

## Unreleased

* GA4 analytics schema rework ([PR #2864](https://github.com/alphagov/govuk_publishing_components/pull/2864))

## 29.15.1

* Removes additional id argument from table_helper ([PR #2862](https://github.com/alphagov/govuk_publishing_components/pull/2862))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// The following modules are imported in a specific order
//= require ./analytics-ga4/gtm-schemas
//= require ./analytics-ga4/pii-remover
//= require ./analytics-ga4/gtm-page-views
//= require ./analytics-ga4/gtm-click-tracking
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};

function GtmClickTracking (module) {
this.module = module
this.trackingTrigger = 'data-gtm-event-name' // elements with this attribute get tracked
this.trackingTrigger = 'data-ga4' // elements with this attribute get tracked
}

GtmClickTracking.prototype.init = function () {
Expand All @@ -18,12 +18,24 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};
if (window.dataLayer) {
var target = this.findTrackingAttributes(event.target)
if (target) {
var data = {
event: 'analytics',
event_name: target.getAttribute('data-gtm-event-name'),
// get entire URL apart from domain
link_url: window.location.href.substring(window.location.origin.length),
ui: JSON.parse(target.getAttribute('data-gtm-attributes')) || {}
var schema = new window.GOVUK.analyticsGA4.Schemas().eventSchema()

try {
var data = target.getAttribute(this.trackingTrigger)
data = JSON.parse(data)
} catch (e) {
// if there's a problem with the config, don't start the tracker
console.error('GA4 configuration error: ' + e.message, window.location)
return
}

schema.event = 'event_data'
// get attributes from the data attribute to send to GA
// only allow it if it already exists in the schema
for (var property in data) {
if (schema.event_data[property]) {
schema.event_data[property] = data[property]
}
}

// Ensure it only tracks aria-expanded in an accordion element, instead of in any child of the clicked element
Expand All @@ -39,15 +51,14 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};
var detailsElement = target.closest('details')

if (ariaExpanded) {
data.ui.text = data.ui.text || target.innerText
data.ui.action = (ariaExpanded === 'false') ? 'opened' : 'closed'
schema.event_data.text = data.text || target.innerText
schema.event_data.action = (ariaExpanded === 'false') ? 'opened' : 'closed'
} else if (detailsElement) {
data.ui.text = data.ui.text || detailsElement.textContent
schema.event_data.text = data.text || detailsElement.textContent
var openAttribute = detailsElement.getAttribute('open')
data.ui.action = (openAttribute == null) ? 'opened' : 'closed'
schema.event_data.action = (openAttribute == null) ? 'opened' : 'closed'
}

window.dataLayer.push(data)
window.dataLayer.push(schema)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,26 @@
sendPageView: function () {
if (window.dataLayer) {
var data = {
event: 'config_ready',
event: 'page_view',
page: {
location: this.getLocation(),
referrer: this.getReferrer(),
title: this.getTitle(),
status_code: this.getStatusCode()
},
publishing: {
status_code: this.getStatusCode(),

document_type: this.getMetaContent('format'),
publishing_app: this.getMetaContent('publishing-app'),
rendering_app: this.getMetaContent('rendering-app'),
schema_name: this.getMetaContent('schema-name'),
content_id: this.getMetaContent('content-id')
},
taxonomy: {
content_id: this.getMetaContent('content-id'),

section: this.getMetaContent('section'),
taxon_slug: this.getMetaContent('taxon-slug'),
taxon_id: this.getMetaContent('taxon-id'),
themes: this.getMetaContent('themes'),
taxon_slugs: this.getMetaContent('taxon-slugs'),
taxon_ids: this.getMetaContent('taxon-ids')
},
content: {
taxon_ids: this.getMetaContent('taxon-ids'),

language: this.getLanguage(),
history: this.getHistory(),
withdrawn: this.getWithDrawn(),
Expand Down Expand Up @@ -65,9 +62,9 @@
// https://github.com/alphagov/static/blob/main/app/views/root/_error_page.html.erb#L32
getStatusCode: function () {
if (window.httpStatusCode) {
return window.httpStatusCode
return window.httpStatusCode.toString()
} else {
return 200
return '200'
}
},

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
;(function (global) {
'use strict'

var GOVUK = global.GOVUK || {}

var Schemas = function () {
this.null = 'n/a'
}

Schemas.prototype.eventSchema = function () {
return {
event: this.null,

event_data: {
event_name: this.null,
type: this.null,
url: this.null,
text: this.null,
index: this.null,
index_total: this.null,
section: this.null,
action: this.null,
external: this.null
}
}
}

GOVUK.analyticsGA4 = GOVUK.analyticsGA4 || {}
GOVUK.analyticsGA4.Schemas = Schemas

global.GOVUK = GOVUK
})(window)
59 changes: 59 additions & 0 deletions docs/analytics-gtm/analytics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Google Analytics 4 and Google Tag Manager

This document describes the use of Google Tag Manager (GTM) with Google Analytics 4 (GA4) on GOV.UK Publishing.

No analytics code is initialised and no data is gathered without user consent.

## General approach

We pass three types of data to GA4.

- page views
- events
- search data

Page views happen when a page loads.

Events happen when a user interacts with certain things, for example clicking on an accordion, tab, or link.

Search data is gathered when users perform a search.

## Data schemas

All of the data sent to GTM is based on a common schema.

```
{
event: '',
page: {},
event_data: {},
search_results: {}
}
```

`event` must have a specific value to activate the right trigger in GTM.

`page` is defined in the [gtm-page-views script](https://github.com/alphagov/govuk_publishing_components/blob/main/app/assets/javascripts/govuk_publishing_components/analytics-ga4/gtm-page-views.js).

`event_data` is defined in the [gtm-schemas script](https://github.com/alphagov/govuk_publishing_components/blob/main/app/assets/javascripts/govuk_publishing_components/analytics-ga4/gtm-schemas.js) and used in the [gtm-click-tracking script](https://github.com/alphagov/govuk_publishing_components/blob/main/app/assets/javascripts/govuk_publishing_components/analytics-ga4/gtm-click-tracking.js).

`search_results` has not been implemented yet.

## How the dataLayer works

GTM's dataLayer has two elements - an array and an object. `window.dataLayer = []` is executed when the page loads.

GOV.UK JavaScript (JS) pushes objects to the dataLayer array when certain things happen e.g. when the page loads, or a user clicks on a certain type of link. Once that happens GTM takes over. It reads the latest object in the array and passes the data found into the dataLayer object. Importantly, it only adds to the object, so data can persist from previous array pushes.

For example:

- an event happens and JS pushes `{ a: 1 }` to the dataLayer array
- GTM adds this to the dataLayer object, which is now `{ a: 1 }`
- another event happens and JS pushes `{ b: 1 }` to the array
- GTM adds this to the dataLayer object, which is now `{ a: 1, b: 1 }`

If data shouldn't persist it can be erased in a following push, for example by sending `{ a: 1, b: false }`, but often this overall behaviour is desirable - for example, page view data will persist in events that happen on that page, providing more context for analysts.

If the data given to GTM contains a recognised `event` attribute value, GTM then sends that data on to GA4.

The dataLayer is recreated when a user navigates to another page, so no data in the dataLayer will persist between pages.
62 changes: 33 additions & 29 deletions docs/analytics-gtm/gtm-click-tracking.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,39 @@
# Google Tag Manager click tracking

This is an experimental script to allow click tracking through Google Tag Manager to be added to any element using data attributes.
This is a script to allow click tracking through Google Tag Manager to be added to any element using data attributes.

## Basic use

```html
<div data-module="gtm-click-tracking">
<div
data-gtm-event-name="anything"
data-gtm-attributes='{"type":"something","index":0,"index-total":1,"section":"n/a","text":"Click me"}'>
<div data-ga4='{"event_name":"select_content", "type":"something", "index":0, "index_total":1, "text":"Click me"}'>
Click me
</div>
</div>
```

The module is initialised on the parent element, but tracking only occurs when clicks happen:

- on child elements that have a `data-gtm-event-name` attribute
- on the parent if it has a `data-gtm-event-name` attribute
- on child elements that have a valid `data-ga4` attribute
- on the parent if it has a valid `data-ga4` attribute

The contents of `data-gtm-attributes` are parsed as a JSON object and included in the information passed to GTM. In the example above, the following would be pushed to the dataLayer. Note that the `link_url` attribute is automatically populated with the current page path.
A valid `data-ga4` attribute is a JSON string that can be parsed into an object, that contains a recognised value for `event_name`. Pushes to the dataLayer that do not include a valid `event_name` attribute will be ignored by Tag Manager.

In the example above, the following would be pushed to the dataLayer. Note that the schema automatically populates empty values, and that attributes not in the schema are ignored.

```
{
'event': 'analytics',
'event_name': 'anything',
'link_url': '/government/publications/cheese',
'ui': {
'event': 'event_data',
'event_data': {
'event_name': 'select_content',
'type': 'something',
'url': 'n/a',
'text': 'Click me',
'index': '0',
'index-total': '1',
'index_total': '1',
'section': 'n/a',
'text': 'Click me'
'action': 'n/a',
'external': 'n/a'
}
}
```
Expand All @@ -46,16 +48,16 @@ This is handled using the following approach.
<div data-module="gtm-click-tracking">
<%
show_all_attributes = {
event_name: "select_content",
type: "accordion",
index: 0,
"index-total": 5,
index_total: 5,
section: "n/a"
}
%>
<%= render 'govuk_publishing_components/components/accordion', {
data_attributes_show_all: {
"gtm-event-name": "select_content",
"gtm-attributes": show_all_attributes.to_json
"ga4": show_all_attributes.to_json
},
items: []
} %>
Expand All @@ -68,22 +70,23 @@ This works as follows.
- `GtmClickTracking` can now be triggered because the accordion is wrapped in the module and its 'Show/hide all' link has a `gtm-event-name` attribute
- `GtmClickTracking` checks for the presence of an `aria-expanded` attribute, either on the clicked element or a child of the clicked element
- if it finds one, it sets the `state` attribute of `ui` to either 'opened' or 'closed' depending on the value of `aria-expanded`
- if `data-gtm-attributes` does not include `text`, `GtmClickTracking` automatically uses the text of the clicked element
- if `data-ga4` does not include `text`, `GtmClickTracking` automatically uses the text of the clicked element

When a user clicks 'Show all sections' the following information is pushed to the dataLayer.

```
{
'event': 'analytics',
'event_name': 'select_content',
'link_url': '/government/publications/cheese',
'ui': {
'event': 'event_data',
'event_data': {
'event_name': 'select_content',
'type': 'accordion',
'url': 'n/a',
'text': 'Show all sections',
'index': '0',
'index-total': '5',
'section': 'n/a',
'text': 'Show all sections',
'state': 'opened'
'action': 'opened',
'external': 'n/a'
}
}
```
Expand All @@ -92,16 +95,17 @@ When a user clicks 'Hide all sections' the following information is pushed to th

```
{
'event': 'analytics',
'event_name': 'select_content',
'link_url': '/government/publications/cheese',
'ui': {
'event': 'event_data',
'event_data': {
'event_name': 'select_content',
'type': 'accordion',
'url': 'n/a',
'text': 'Hide all sections',
'index': '0',
'index-total': '5',
'section': 'n/a',
'text': 'Hide all sections',
'state': 'closed'
'action': 'closed',
'external': 'n/a'
}
}
```
Loading

0 comments on commit 57f187f

Please sign in to comment.