diff --git a/__tests__/search.test.js b/__tests__/search.test.js index 5a24bd1703..6ee4ffa20c 100644 --- a/__tests__/search.test.js +++ b/__tests__/search.test.js @@ -176,14 +176,16 @@ describe('Site search', () => { expect(GoogleTagManagerDataLayer).toEqual( expect.arrayContaining([ expect.objectContaining({ - ecommerce: { - impressions: [] - }, event: 'site_search', - eventDetails: { + event_data: { action: 'no result', - category: 'site search', - label: 'lorem ipsum' + text: 'lorem ipsum' + } + }), + expect.objectContaining({ + event: 'view_item_list', + ecommerce: { + items: [] } }) ]) @@ -204,16 +206,24 @@ describe('Site search', () => { ) // Find layer that has the impressions to test. - const impressions = GoogleTagManagerDataLayer.filter( + const items = GoogleTagManagerDataLayer.filter( (layer) => layer.ecommerce - ).map((layer) => layer.ecommerce.impressions)[0] + ).map((layer) => layer.ecommerce.items)[0] - expect(impressions.length).toEqual($searchOptions.length) + expect(items.length).toEqual($searchOptions.length) expect(GoogleTagManagerDataLayer).toEqual( expect.arrayContaining([ expect.objectContaining({ + event: 'site_search', + event_data: { + action: 'results', + text: 'g' + } + }), + expect.objectContaining({ + event: 'view_item_list', ecommerce: { - impressions: expect.arrayContaining([ + items: expect.arrayContaining([ expect.objectContaining({ name: expect.any(String), category: expect.any(String), @@ -221,12 +231,6 @@ describe('Site search', () => { position: expect.any(Number) }) ]) - }, - event: 'site_search', - eventDetails: { - action: 'results', - category: 'site search', - label: 'g' } }) ]) @@ -257,27 +261,28 @@ describe('Site search', () => { expect(GoogleTagManagerDataLayer).toEqual( expect.arrayContaining([ expect.objectContaining({ - ecommerce: { - click: { - actionField: { - list: 'g' - }, - products: expect.arrayContaining([ - expect.objectContaining({ - name: expect.any(String), - category: expect.any(String), - list: 'g', - position: 2 - }) - ]) - } - }, event: 'site_search', - eventDetails: { + event_data: { action: 'click', - category: 'site search', - label: expect.stringContaining('g |') + text: expect.stringContaining('g'), + section: expect.any(String) } + }), + expect.objectContaining({ + ecommerce: null + }), + expect.objectContaining({ + ecommerce: { + items: expect.arrayContaining([ + expect.objectContaining({ + name: expect.any(String), + category: expect.any(String), + list: 'g', + position: 2 + }) + ]) + }, + event: 'select_item' }) ]) ) @@ -298,8 +303,8 @@ describe('Site search', () => { expect(GoogleTagManagerDataLayer).toEqual( expect.arrayContaining([ expect.objectContaining({ - eventDetails: expect.objectContaining({ - label: '[REDACTED EMAIL]' + event_data: expect.objectContaining({ + text: '[REDACTED EMAIL]' }) }) ]) @@ -321,8 +326,8 @@ describe('Site search', () => { expect(GoogleTagManagerDataLayer).toEqual( expect.arrayContaining([ expect.objectContaining({ - eventDetails: expect.objectContaining({ - label: '[REDACTED NUMBER]' + event_data: expect.objectContaining({ + text: '[REDACTED NUMBER]' }) }) ]) diff --git a/src/javascripts/components/analytics.mjs b/src/javascripts/components/analytics.mjs index d23fee9503..18613d8a75 100644 --- a/src/javascripts/components/analytics.mjs +++ b/src/javascripts/components/analytics.mjs @@ -57,3 +57,23 @@ export function stripPossiblePII(string) { string = string.replace(/[0-9]+/g, '[REDACTED NUMBER]') return string } + +/** + * Translate list of search results to + * format compatiable with GA4 ecommerce + * `items` attribute + * + * @param {Array} searchResults - Array of search results + * @param {string} searchTerm - Search string entered by user + * @returns {Array} items - Array of `items` + */ +export function translateToItems(searchResults, searchTerm) { + const items = searchResults.map((result, key) => ({ + name: result.title, + category: result.section, + list: searchTerm, // Used to match an searchTerm with results + position: key + 1 + })) + + return items +} diff --git a/src/javascripts/components/search.tracking.mjs b/src/javascripts/components/search.tracking.mjs index 0cf9730dc3..82de216669 100644 --- a/src/javascripts/components/search.tracking.mjs +++ b/src/javascripts/components/search.tracking.mjs @@ -1,4 +1,8 @@ -import { addToDataLayer, stripPossiblePII } from './analytics.mjs' +import { + addToDataLayer, + stripPossiblePII, + translateToItems +} from './analytics.mjs' /** * Track confirmed autocomplete result @@ -13,28 +17,30 @@ export function trackConfirm(searchQuery, searchResults, result) { } const searchTerm = stripPossiblePII(searchQuery) - const products = searchResults - .map((result, key) => ({ - name: result.title, - category: result.section, - list: searchTerm, // Used to match an searchTerm with results - position: key + 1 - })) - // Only return the product that matches what was clicked - .filter((product) => product.name === result.title) + + // Only return the product that matches what was clicked + const items = translateToItems(searchResults, searchTerm).filter( + (product) => product.name === result.title + ) addToDataLayer({ event: 'site_search', - eventDetails: { - category: 'site search', + event_data: { action: 'click', - label: `${searchTerm} | ${result.title}` - }, + text: searchTerm, + section: result.title + } + }) + + // Each time the ecommerce object is pushed to the dataLayer, + // it needs to be nullified first. Nullifying the ecommerce + // object clears it and prevents multiple ecommerce events on a + // page from affecting each other. + addToDataLayer({ ecommerce: null }) + addToDataLayer({ + event: 'select_item', ecommerce: { - click: { - actionField: { list: searchTerm }, - products - } + items } }) } @@ -51,25 +57,26 @@ export function trackSearchResults(searchQuery, searchResults) { } const searchTerm = stripPossiblePII(searchQuery) - const hasResults = searchResults.length > 0 - // Impressions is Google Analytics lingo for what people have seen. - const impressions = searchResults.map((result, key) => ({ - name: result.title, - category: result.section, - list: searchTerm, // Used to match an searchTerm with results - position: key + 1 - })) + const items = translateToItems(searchResults, searchTerm) addToDataLayer({ event: 'site_search', - eventDetails: { - category: 'site search', + event_data: { action: hasResults ? 'results' : 'no result', - label: searchTerm - }, + text: searchTerm + } + }) + + // Each time the ecommerce object is pushed to the dataLayer, + // it needs to be nullified first. Nullifying the ecommerce + // object clears it and prevents multiple ecommerce events on a + // page from affecting each other. + addToDataLayer({ ecommerce: null }) + addToDataLayer({ + event: 'view_item_list', ecommerce: { - impressions + items } }) }