diff --git a/packages/@ourworldindata/grapher/src/core/GrapherAnalytics.ts b/packages/@ourworldindata/grapher/src/core/GrapherAnalytics.ts index d557334786a..3720bab652d 100644 --- a/packages/@ourworldindata/grapher/src/core/GrapherAnalytics.ts +++ b/packages/@ourworldindata/grapher/src/core/GrapherAnalytics.ts @@ -19,6 +19,9 @@ export enum EventCategory { KeyboardShortcut = "owid.keyboard_shortcut", SiteClick = "owid.site_click", SiteError = "owid.site_error", + SiteSearchClick = "owid.site_search_click", + SiteSearchFilterClick = "owid.site_search_filter_click", + SiteInstantSearchClick = "owid.site_instantsearch_click", } enum EventAction { @@ -38,7 +41,7 @@ type countrySelectorEvent = interface GAEvent { event: EventCategory eventAction?: string - eventContext?: string + eventContext?: string | Record eventTarget?: string grapherPath?: string } diff --git a/packages/@ourworldindata/utils/src/Util.ts b/packages/@ourworldindata/utils/src/Util.ts index 0fcea199c28..880efa71643 100644 --- a/packages/@ourworldindata/utils/src/Util.ts +++ b/packages/@ourworldindata/utils/src/Util.ts @@ -1831,3 +1831,14 @@ export function cartesian(matrix: T[][]): T[][] { [[]] ) } + +export function isElementHidden(element: Element | null): boolean { + if (!element) return false + const computedStyle = window.getComputedStyle(element) + if ( + computedStyle.display === "none" || + computedStyle.visibility === "hidden" + ) + return true + return isElementHidden(element.parentElement) +} diff --git a/packages/@ourworldindata/utils/src/index.ts b/packages/@ourworldindata/utils/src/index.ts index f14c7459a2d..577f58ad587 100644 --- a/packages/@ourworldindata/utils/src/index.ts +++ b/packages/@ourworldindata/utils/src/index.ts @@ -118,6 +118,7 @@ export { checkIsDataInsight, checkIsAuthor, cartesian, + isElementHidden, } from "./Util.js" export { diff --git a/site/SiteAnalytics.ts b/site/SiteAnalytics.ts index 6039a412092..d8837ddb625 100644 --- a/site/SiteAnalytics.ts +++ b/site/SiteAnalytics.ts @@ -1,4 +1,5 @@ import { GrapherAnalytics, EventCategory } from "@ourworldindata/grapher" +import { SearchCategoryFilter } from "./search/searchTypes.js" export class SiteAnalytics extends GrapherAnalytics { logCountryProfileSearch(country: string) { @@ -23,4 +24,50 @@ export class SiteAnalytics extends GrapherAnalytics { eventContext: query, }) } + + logSearchClick({ + query, + position, + url, + positionInSection, + filter, + }: { + query: string + position: string + positionInSection: string + url: string + filter: SearchCategoryFilter + }) { + this.logToGA({ + event: EventCategory.SiteSearchClick, + eventAction: "click", + eventContext: { query, position, positionInSection, filter }, + eventTarget: url, + }) + } + + logInstantSearchClick({ + query, + url, + position, + }: { + query: string + url: string + position: string + }) { + this.logToGA({ + event: EventCategory.SiteInstantSearchClick, + eventAction: "click", + eventContext: { query, position }, + eventTarget: url, + }) + } + + logSearchFilterClick({ key }: { key: string }) { + this.logToGA({ + event: EventCategory.SiteSearchFilterClick, + eventAction: "click", + eventContext: key, + }) + } } diff --git a/site/search/Autocomplete.tsx b/site/search/Autocomplete.tsx index 6a5ae89cd91..9fe5271441d 100644 --- a/site/search/Autocomplete.tsx +++ b/site/search/Autocomplete.tsx @@ -26,6 +26,9 @@ import { parseIndexName, } from "./searchClient.js" import { queryParamsToStr } from "@ourworldindata/utils" +import { SiteAnalytics } from "../SiteAnalytics.js" + +const siteAnalytics = new SiteAnalytics() type BaseItem = Record @@ -107,6 +110,11 @@ const AlgoliaSource: AutocompleteSource = { sourceId: "autocomplete", onSelect({ navigator, item, state }) { const itemUrl = prependSubdirectoryToAlgoliaItemUrl(item) + siteAnalytics.logInstantSearchClick({ + query: state.query, + url: itemUrl, + position: String(state.activeItemId), + }) navigator.navigate({ itemUrl, item, state }) }, getItemUrl({ item }) { diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index 1482fe17ec5..e11b8ac7df6 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -6,6 +6,7 @@ import { getWindowQueryParams, get, mapValues, + isElementHidden, } from "@ourworldindata/utils" import { InstantSearch, @@ -42,7 +43,7 @@ import { faHeartBroken, faSearch } from "@fortawesome/free-solid-svg-icons" import { DEFAULT_SEARCH_PLACEHOLDER, getIndexName, - logSiteSearchClick, + logSiteSearchClickToAlgoliaInsights, } from "./searchClient.js" import { PreferenceType, @@ -52,6 +53,9 @@ import { DEFAULT_GRAPHER_HEIGHT, DEFAULT_GRAPHER_WIDTH, } from "@ourworldindata/grapher" +import { SiteAnalytics } from "../SiteAnalytics.js" + +const siteAnalytics = new SiteAnalytics() function PagesHit({ hit }: { hit: IPageHit }) { return ( @@ -254,6 +258,7 @@ interface SearchResultsProps { activeCategoryFilter: SearchCategoryFilter isHidden: boolean handleCategoryFilterClick: (x: SearchCategoryFilter) => void + query: string } const SearchResults = (props: SearchResultsProps) => { @@ -281,22 +286,48 @@ const SearchResults = (props: SearchResultsProps) => { const objectId = target.getAttribute( "data-algolia-object-id" ) - const position = target.getAttribute( + + const allVisibleHits = Array.from( + document.querySelectorAll( + ".search-results .ais-Hits-item a" + ) + ).filter((e) => !isElementHidden(e)) + + // starts from 1 at the top of the page + const globalPosition = allVisibleHits.indexOf(target) + 1 + // starts from 1 in each section + const positionInSection = target.getAttribute( "data-algolia-position" ) const index = target.getAttribute("data-algolia-index") - if (objectId && position && index) { - logSiteSearchClick({ + const href = target.getAttribute("href") + const query = props.query + + if ( + objectId && + positionInSection && + index && + href && + query + ) { + logSiteSearchClickToAlgoliaInsights({ index, queryID, objectIDs: [objectId], - positions: [parseInt(position)], + positions: [parseInt(positionInSection)], + }) + siteAnalytics.logSearchClick({ + query, + position: String(globalPosition), + positionInSection, + url: href, + filter: activeCategoryFilter, }) } } } }, - [queryID] + [queryID, activeCategoryFilter, props.query] ) useEffect(() => { document.addEventListener("click", handleHitClick) @@ -474,6 +505,7 @@ export class InstantSearchContainer extends React.Component { behavior: "smooth", }) } + siteAnalytics.logSearchFilterClick({ key }) this.setActiveCategoryFilter(key) } @@ -524,6 +556,7 @@ export class InstantSearchContainer extends React.Component { { } } -export const logSiteSearchClick = ( +export const logSiteSearchClickToAlgoliaInsights = ( event: Omit ) => { const client = getInsightsClient()