diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b666a4f94..235990d29b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-search-tracker.js b/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-search-tracker.js index 034965d27a..72014ec599 100644 --- a/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-search-tracker.js +++ b/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-search-tracker.js @@ -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 () { @@ -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() @@ -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, @@ -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 diff --git a/docs/analytics-ga4/trackers/ga4-search-tracker.md b/docs/analytics-ga4/trackers/ga4-search-tracker.md index 59e9305b19..bc5b7f3b22 100644 --- a/docs/analytics-ga4/trackers/ga4-search-tracker.md +++ b/docs/analytics-ga4/trackers/ga4-search-tracker.md @@ -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 diff --git a/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-search-tracker.spec.js b/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-search-tracker.spec.js index 50182c5998..a185d8e075 100644 --- a/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-search-tracker.spec.js +++ b/spec/javascripts/govuk_publishing_components/analytics-ga4/ga4-search-tracker.spec.js @@ -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 = ` @@ -16,10 +16,19 @@ describe('Google Analytics search tracking', () => { data-ga4-search-index-section-count="89" > + ` + 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 || {} @@ -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') @@ -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() }) @@ -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' - ) - }) - }) })