Skip to content

Commit

Permalink
Merge pull request #4354 from alphagov/ga4search
Browse files Browse the repository at this point in the history
Add new custom analytics tracker for search
  • Loading branch information
csutter authored Nov 1, 2024
2 parents 92ee587 + 80728ed commit cb67325
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
## Unreleased

* Chart component accessibility improvements ([PR #4344](https://github.com/alphagov/govuk_publishing_components/pull/4344))
* Add new custom analytics tracker for search and use in layout super nav header ([PR #4354](https://github.com/alphagov/govuk_publishing_components/pull/4354))

## 44.9.1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
//= require ./analytics-ga4/ga4-auto-tracker
//= require ./analytics-ga4/ga4-smart-answer-results-tracker
//= require ./analytics-ga4/ga4-scroll-tracker
//= require ./analytics-ga4/ga4-search-tracker
//= require ./analytics-ga4/ga4-video-tracker
//= require ./analytics-ga4/ga4-focus-loss-tracker
//= require ./analytics-ga4/init-ga4
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
window.GOVUK = window.GOVUK || {}
window.GOVUK.Modules = window.GOVUK.Modules || {};
(function (Modules) {
'use strict'
// Tracks interactions with search forms, including on the all content finder ("site search"),
// homepage, and the layout super navigation header search.
class Ga4SearchTracker {
constructor ($module) {
this.$module = $module
this.$searchInput = this.$module.querySelector('input[type="search"]')

this.type = this.$module.dataset.ga4SearchType
this.url = this.$module.dataset.ga4SearchUrl
this.section = this.$module.dataset.ga4SearchSection
this.indexSection = this.$module.dataset.ga4SearchIndexSection
this.indexSectionCount = this.$module.dataset.ga4SearchIndexSectionCount
}

init () {
if (!this.$searchInput) {
console.warn('Ga4SearchTracker: Module added to element without child search input')
return
}

this.initialKeywords = this.$searchInput.value

if (window.GOVUK.getConsentCookie() && window.GOVUK.getConsentCookie().usage) {
this.startModule()
} else {
window.addEventListener('cookie-consent', () => this.startModule())
}
}

startModule () {
this.$module.addEventListener('submit', event => this.trackSearch(event))
}

trackSearch () {
if (this.skipTracking()) return

const data = {
event_name: 'search',
action: 'search',

type: this.type,
section: this.section,
url: this.url,
index_section: this.indexSection,
index_section_count: this.indexSectionCount,
text: this.searchTerm()
}

window.GOVUK.analyticsGa4.core.applySchemaAndSendData(data, 'event_data')
}

skipTracking () {
// Skip tracking for those events that we do not want to track: where the search term is
// present, but has not changed from its initial value
return this.searchTerm() !== '' && this.searchTerm() === this.initialKeywords
}

searchTerm () {
const { standardiseSearchTerm } = window.GOVUK.analyticsGa4.core.trackFunctions

// `standardiseSearchTerm` returns undefined for empty strings, whereas we actively want an
// empty string as part of search events (undefined would not overwrite the current value in
// the analytics state)
return standardiseSearchTerm(this.$searchInput.value) || ''
}
}

Modules.Ga4SearchTracker = Ga4SearchTracker
})(window.GOVUK.Modules)
Original file line number Diff line number Diff line change
Expand Up @@ -303,18 +303,12 @@
class: "gem-c-layout-super-navigation-header__search-form",
id: "search",
data: {
module: "ga4-form-tracker",
ga4_form: {
event_name: "search",
type: "header menu bar",
section: "Search GOV.UK",
action: "search",
url: "/search/all",
index_section: 3,
index_section_count: 3,
},
ga4_form_include_text: "",
ga4_form_no_answer_undefined: "",
module: "ga4-search-tracker",
ga4_search_type: "header menu bar",
ga4_search_url: "/search/all",
ga4_search_section: "Search GOV.UK",
ga4_search_index_section: 3,
ga4_search_index_section_count: 3,
},
action: absolute_links_helper.make_url_absolute("/search/all"),
method: "get",
Expand Down
4 changes: 4 additions & 0 deletions docs/analytics-ga4/trackers.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ The [print intent tracker](trackers/ga4-print-intent-tracker.md) tracks if the p

The [scroll tracker](trackers/ga4-scroll-tracker.md) tracks how much of a page has been viewed.

## Search tracker

The [search tracker](trackers/ga4-search-tracker.md) tracks user interaction with site search boxes in the header, on the homepage, and on the "all content" finder.

## Smart answer results tracker

The [smart answer results tracker](trackers/ga4-smart-answer-results-tracker.md) has been built specifically to track the Cost of Living smart answer.
Expand Down
31 changes: 31 additions & 0 deletions docs/analytics-ga4/trackers/ga4-search-tracker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Google Analytics 4 search tracker

This module allows us to consistently track usage of search across the several different search
fields from which a user can initiate a search on GOV.UK:
- the layout super navigation header
- the homepage
- the search page itself

It is not used by the legacy UI for finders, which triggers search events on user input (rather than
waiting for form submission) as part of its "live search" functionality.

## How it works
Annotate a `<form>` element containing a search field (`<input type="search">`, for example the
`search` or `search_with_autocomplete` publishing components) with the module and its required data
fields:

```html
<form
data-module="ga4-search-tracker"
data-ga4-search-type="site search"
data-ga4-search-url="/search"
data-ga4-search-section="section"
data-ga4-search-index-section="19"
data-ga4-search-index-section-count="89"
>
```

When the form is submitted, a `search` event with the will be tracked containing:
- the type, URL, section, index section, and index section count fields based on the data attributes
outlined above
- the state (text) of the search field contained within
11 changes: 1 addition & 10 deletions spec/components/layout_super_navigation_header_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -173,15 +173,6 @@ def component_name
assert_select 'a[data-ga4-link=\'{"event_name":"navigation","type":"header menu bar","index_section":1,"index_link":16,"index_section_count":3,"index_total":16,"section":"Services and information"}\']'
assert_select 'a[data-ga4-link=\'{"event_name":"navigation","type":"header menu bar","index_section":2,"index_link":1,"index_section_count":3,"index_total":6,"section":"Government activity"}\']'
assert_select 'a[data-ga4-link=\'{"event_name":"navigation","type":"header menu bar","index_section":2,"index_link":6,"index_section_count":3,"index_total":6,"section":"Government activity"}\']'
form_attributes = {
event_name: "search",
type: "header menu bar",
section: "Search GOV.UK",
action: "search",
url: "/search/all",
index_section: 3,
index_section_count: 3,
}.to_json
assert_select "form[data-ga4-form=\'#{form_attributes}\']"
assert_select "form[data-module='ga4-search-tracker']"
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/* eslint-env jasmine */

describe('Google Analytics search tracking', () => {
'use strict'

let fixture, form, input, sendSpy, ga4SearchTracker
const GOVUK = window.GOVUK

const html = `
<form
data-module="ga4-search-tracker"
data-ga4-search-type="site search"
data-ga4-search-url="/search"
data-ga4-search-section="section"
data-ga4-search-index-section="19"
data-ga4-search-index-section-count="89"
>
<input type="search" name="keyword" value="initial value">
<button type="submit">Search</button>
</form>
`

beforeAll(() => {
GOVUK.analyticsGa4 = GOVUK.analyticsGa4 || {}
GOVUK.analyticsGa4.vars = GOVUK.analyticsGa4.vars || {}
GOVUK.analyticsGa4.vars.gem_version = 'aVersion'
})

afterAll(() => {
window.dataLayer = []
})

beforeEach(() => {
fixture = document.createElement('div')
document.body.appendChild(fixture)
fixture.innerHTML = html

form = fixture.querySelector('form')
input = form.querySelector('input')

sendSpy = spyOn(GOVUK.analyticsGa4.core, 'applySchemaAndSendData')

ga4SearchTracker = new GOVUK.Modules.Ga4SearchTracker(form)
})

afterEach(() => {
fixture.remove()
})

describe('if the search field is missing', () => {
beforeEach(() => {
GOVUK.setConsentCookie({ usage: true })

form.removeChild(input)
ga4SearchTracker = new GOVUK.Modules.Ga4SearchTracker(form)
ga4SearchTracker.init()
})

it('does not track search events', () => {
GOVUK.triggerEvent(form, 'submit')

expect(sendSpy).not.toHaveBeenCalled()
})
})

describe('when usage tracking is declined', () => {
beforeEach(() => {
GOVUK.setConsentCookie({ usage: false })
ga4SearchTracker.init()
})

it('does not track search events', () => {
GOVUK.triggerEvent(form, 'submit')

expect(sendSpy).not.toHaveBeenCalled()
})
})

describe('when usage tracking is accepted', () => {
beforeEach(() => {
GOVUK.setConsentCookie({ usage: true })
ga4SearchTracker.init()
})

it('tracks search events when the input changes', () => {
input.value = 'new value'
GOVUK.triggerEvent(form, 'submit')

expect(sendSpy).toHaveBeenCalledWith(
{
event_name: 'search',
action: 'search',
type: 'site search',
section: 'section',
url: '/search',
index_section: '19',
index_section_count: '89',
text: 'new value'
},
'event_data'
)
})

it('does not track search events when the input does not change', () => {
GOVUK.triggerEvent(form, 'submit')

expect(sendSpy).not.toHaveBeenCalled()
})
})

describe('when the input is originally empty', () => {
beforeEach(() => {
GOVUK.setConsentCookie({ usage: true })
input.value = ''
ga4SearchTracker.init()
})

it('tracks search events even if the (empty) input is unchanged', () => {
GOVUK.triggerEvent(form, 'submit')

expect(sendSpy).toHaveBeenCalledWith(
{
event_name: 'search',
action: 'search',
type: 'site search',
section: 'section',
url: '/search',
index_section: '19',
index_section_count: '89',
text: ''
},
'event_data'
)
})
})
})

0 comments on commit cb67325

Please sign in to comment.