Skip to content

Commit

Permalink
Improve GA4 search tracking
Browse files Browse the repository at this point in the history
Now that we have collected a decent amount of tracking data on search
across GOV.UK, we've come across a couple of trip hazards with the
existing tracking. In particular:
- suppressing the event if the keyword hasn't changed is a bit brittle
  (and subtly buggy as we've forgotten to normalise the initial keyword
  in the JS component state)
- Relying on separate tracking in Finder Frontend for filter application
  has led to duplication and unreliable behaviour (for example, as we
  only track clicks of the filter button, we do not get events when a
  user submits the form through other means such as the Enter key or the
  main search field's submit button)

This change updates the behaviour of the `Ga4SearchTracker` module as
follows:
- update the tracking to _always_ fire a `search` event when the form is
  submitted
- make the action of the `search` event dynamic
- update the logic to establish whether the user is searching vs
  filtering by checking which fields the user has interacted with
  (instead of checking if the keyword has changed), and set the event's
  action property accordingly
  • Loading branch information
csutter committed Jan 22, 2025
1 parent dd14a7d commit 71e4a53
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 69 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* Use component wrapper on contextual footer ([PR #4562](https://github.com/alphagov/govuk_publishing_components/pull/4562))
* Update Govspeak "Warning Text" component styles ([PR #4487](https://github.com/alphagov/govuk_publishing_components/pull/4487))
* Make "Add another" component styles more specific ([PR #4579](https://github.com/alphagov/govuk_publishing_components/pull/4579))
* Improve GA4 search tracking ([PR #4582](https://github.com/alphagov/govuk_publishing_components/pull/4582))

## 49.1.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};
class Ga4SearchTracker {
constructor ($module) {
this.$module = $module
this.$searchInput = this.$module.querySelector('input[type="search"]')
this.$searchInput = $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

// At the beginning, the user has not yet interacted with any form fields
this.triggeredAction = 'unchanged'
}

init () {
Expand All @@ -22,7 +25,11 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};
return
}

this.initialKeywords = this.$searchInput.value
// These event handlers do not send any tracking data, they only set internal state. They are
// added here so if the user hasn't given consent yet but does so later, we do not end up with
// inconsistent module state.
this.$module.addEventListener('change', event => this.setTriggeredAction(event))
this.$module.addEventListener('input', event => this.setTriggeredAction(event))

if (window.GOVUK.getConsentCookie() && window.GOVUK.getConsentCookie().usage) {
this.startModule()
Expand All @@ -35,16 +42,24 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};
this.$module.addEventListener('submit', event => this.trackSearch(event))
}

setTriggeredAction (event) {
if (event.target.type === 'search') {
this.triggeredAction = 'search'
} else if (this.triggeredAction !== 'search') {
// The 'search' action always takes precedence over the 'filter' action, so only set the
// action to 'filter' if it is not already 'search'.
this.triggeredAction = 'filter'
}
}

trackSearch () {
// The original search input may have been removed from the DOM by the autocomplete component
// if it is used, so make sure we are tracking the correct input
this.$searchInput = this.$module.querySelector('input[type="search"]')

if (this.skipTracking()) return

const data = {
event_name: 'search',
action: 'search',
action: this.triggeredAction,

type: this.type,
section: this.section,
Expand Down Expand Up @@ -80,12 +95,6 @@ window.GOVUK.Modules = window.GOVUK.Modules || {};
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

Expand Down
7 changes: 6 additions & 1 deletion docs/analytics-ga4/trackers/ga4-search-tracker.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ fields:
>
```

When the form is submitted, a `search` event with the will be tracked containing:
When the form is submitted, a `search` event will be tracked containing:
- an action according to the user's interaction with the form:
- `search` if they have interacted with the search term field
- `filter` if they have interacted with any other field in the form (but not the search term
field)
- `unchanged` otherwise, for example if they have just (re-)submitted the form
- 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
describe('Google Analytics search tracking', () => {
'use strict'

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

const html = `
Expand All @@ -16,10 +16,19 @@ describe('Google Analytics search tracking', () => {
data-ga4-search-index-section-count="89"
>
<input type="search" name="keyword" value="initial value">
<input type="text" name="foo" value="bar">
<button type="submit">Search</button>
</form>
`

const commonEventProps = {
type: 'site search',
section: 'section',
url: '/search',
index_section: '19',
index_section_count: '89'
}

beforeAll(() => {
GOVUK.analyticsGa4 = GOVUK.analyticsGa4 || {}
GOVUK.analyticsGa4.vars = GOVUK.analyticsGa4.vars || {}
Expand All @@ -36,7 +45,8 @@ describe('Google Analytics search tracking', () => {
fixture.innerHTML = html

form = fixture.querySelector('form')
input = form.querySelector('input')
searchInput = form.querySelector('input[name="keyword"]')
filterInput = form.querySelector('input[name="foo"]')

sendSpy = spyOn(GOVUK.analyticsGa4.core, 'applySchemaAndSendData')
setItemSpy = spyOn(window.sessionStorage, 'setItem')
Expand All @@ -52,7 +62,7 @@ describe('Google Analytics search tracking', () => {
beforeEach(() => {
GOVUK.setConsentCookie({ usage: true })

form.removeChild(input)
form.removeChild(searchInput)
ga4SearchTracker = new GOVUK.Modules.Ga4SearchTracker(form)
ga4SearchTracker.init()
})
Expand Down Expand Up @@ -83,103 +93,96 @@ describe('Google Analytics search tracking', () => {
ga4SearchTracker.init()
})

it('tracks search events when the input changes', () => {
input.value = 'new value'
it('tracks search events as `search` action when the keyword input changes', () => {
searchInput.value = 'new value'
GOVUK.triggerEvent(searchInput, 'input', { target: searchInput })
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'
text: 'new value',
...commonEventProps
},
'event_data'
)
})

it('does not track search events when the input does not change', () => {
it('tracks search events as `filter` action when non-keyword input changes', () => {
filterInput.value = 'new value'
GOVUK.triggerEvent(filterInput, 'input', { target: filterInput })
GOVUK.triggerEvent(form, 'submit')

expect(sendSpy).not.toHaveBeenCalled()
expect(sendSpy).toHaveBeenCalledWith(
{
event_name: 'search',
action: 'filter',
text: 'initial value',
...commonEventProps
},
'event_data'
)
})

it('tracks search events as `unchanged` action when nothing changes', () => {
GOVUK.triggerEvent(form, 'submit')

expect(sendSpy).toHaveBeenCalledWith(
{
event_name: 'search',
action: 'unchanged',
text: 'initial value',
...commonEventProps
},
'event_data'
)
})

it('includes autocomplete information if present', () => {
input.dataset.autocompleteTriggerInput = 'i want to'
input.dataset.autocompleteSuggestions = 'i want to fish|i want to dance|i want to sleep'
input.dataset.autocompleteSuggestionsCount = '3'
input.dataset.autocompleteAccepted = 'true'
searchInput.dataset.autocompleteTriggerInput = 'i want to'
searchInput.dataset.autocompleteSuggestions = 'i want to fish|i want to dance|i want to sleep'
searchInput.dataset.autocompleteSuggestionsCount = '3'
searchInput.dataset.autocompleteAccepted = 'true'

input.value = 'i want to fish'
searchInput.value = 'i want to fish'
GOVUK.triggerEvent(searchInput, 'input', { target: searchInput })
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: 'i want to fish',
tool_name: 'autocomplete',
length: 3,
autocomplete_input: 'i want to',
autocomplete_suggestions: 'i want to fish|i want to dance|i want to sleep'
autocomplete_suggestions: 'i want to fish|i want to dance|i want to sleep',
...commonEventProps
},
'event_data'
)
})

it('persists usage of autocomplete so that the next page knows it was used', () => {
input.dataset.autocompleteTriggerInput = 'i want to remember'
input.dataset.autocompleteAccepted = 'true'
input.value = 'i want to remember'
searchInput.dataset.autocompleteTriggerInput = 'i want to remember'
searchInput.dataset.autocompleteAccepted = 'true'
searchInput.value = 'i want to remember'

GOVUK.triggerEvent(form, 'submit')
expect(setItemSpy).toHaveBeenCalledWith('searchAutocompleteAccepted', 'true')
})

it('sets tool_name to null if the user has not accepted a suggestion', () => {
input.dataset.autocompleteTriggerInput = 'i want to'
input.dataset.autocompleteSuggestions = 'i want to fish|i want to dance|i want to sleep'
input.dataset.autocompleteSuggestionsCount = '3'
searchInput.dataset.autocompleteTriggerInput = 'i want to'
searchInput.dataset.autocompleteSuggestions = 'i want to fish|i want to dance|i want to sleep'
searchInput.dataset.autocompleteSuggestionsCount = '3'

input.value = 'i want to fish'
searchInput.value = 'i want to fish'
GOVUK.triggerEvent(searchInput, 'input', { target: searchInput })
GOVUK.triggerEvent(form, 'submit')

expect(sendSpy.calls.mostRecent().args[0].tool_name).toBeNull()
})
})

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 71e4a53

Please sign in to comment.