Skip to content

Commit

Permalink
🎉 fix up data catalog types and add basic analytics
Browse files Browse the repository at this point in the history
  • Loading branch information
ikesau committed Sep 9, 2024
1 parent 65fc6f0 commit a1668ee
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 33 deletions.
2 changes: 2 additions & 0 deletions packages/@ourworldindata/grapher/src/core/GrapherAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export enum EventCategory {
SiteInstantSearchClick = "owid.site_instantsearch_click",
SiteFormSubmit = "owid.site_form_submit",
DetailOnDemand = "owid.detail_on_demand",
DataCatalogSearch = "owid.data_catalog_search",
DataCatalogResultClick = "owid.data_catalog_result_click",
}

enum EventAction {
Expand Down
16 changes: 16 additions & 0 deletions site/DataCatalog/DataCatalog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ import {
SMALL_BREAKPOINT_MEDIA_QUERY,
TOUCH_DEVICE_MEDIA_QUERY,
} from "../SiteConstants.js"
import { SiteAnalytics } from "../SiteAnalytics.js"

const analytics = new SiteAnalytics()

const DataCatalogSearchInput = ({
value,
Expand Down Expand Up @@ -429,6 +432,12 @@ const DataCatalogRibbon = ({
>
<ChartHit
hit={hit}
onClick={() => {
analytics.logDataCatalogResultClick(
hit,
"ribbon"
)
}}
searchQueryRegionsMatches={selectedCountries}
/>
</li>
Expand Down Expand Up @@ -554,6 +563,12 @@ const DataCatalogResults = ({
key={hit.objectID}
>
<ChartHit
onClick={() => {
analytics.logDataCatalogResultClick(
hit,
"search"
)
}}
hit={hit}
searchQueryRegionsMatches={selectedCountries}
/>
Expand Down Expand Up @@ -836,6 +851,7 @@ export const DataCatalog = ({
}

syncDataCatalogURL(stateAsUrl)
analytics.logDataCatalogSearch(state)
if (cache.current[cacheKey].has(stateAsUrl)) return

setIsLoading(true)
Expand Down
85 changes: 53 additions & 32 deletions site/DataCatalog/DataCatalogUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { SearchResponse } from "instantsearch.js"
import {
HitAttributeHighlightResult,

Check warning on line 2 in site/DataCatalog/DataCatalogUtils.ts

View workflow job for this annotation

GitHub Actions / eslint

'HitAttributeHighlightResult' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 2 in site/DataCatalog/DataCatalogUtils.ts

View workflow job for this annotation

GitHub Actions / eslint

'HitAttributeHighlightResult' is defined but never used. Allowed unused vars must match /^_/u
HitAttributeSnippetResult,

Check warning on line 3 in site/DataCatalog/DataCatalogUtils.ts

View workflow job for this annotation

GitHub Actions / eslint

'HitAttributeSnippetResult' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 3 in site/DataCatalog/DataCatalogUtils.ts

View workflow job for this annotation

GitHub Actions / eslint

'HitAttributeSnippetResult' is defined but never used. Allowed unused vars must match /^_/u
HitHighlightResult,
SearchForFacetValuesResponse,
SearchResponse,
} from "instantsearch.js"
import { getIndexName } from "../search/searchClient.js"
import { IChartHit, SearchIndexName } from "../search/searchTypes.js"
import { SearchIndexName } from "../search/searchTypes.js"
import { TagGraphRoot } from "@ourworldindata/types"
import { DataCatalogState, dataCatalogStateToUrl } from "./DataCatalogState.js"
import { countriesByName, Region } from "@ourworldindata/utils"
Expand All @@ -11,26 +17,48 @@ import { SearchClient } from "algoliasearch"
*/
const CHARTS_INDEX = getIndexName(SearchIndexName.Charts)

const DATA_CATALOG_ATTRIBUTES = [
"title",
"slug",
"availableEntities",
"variantName",
]

/**
* Types
*/
export type DataCatalogHit = {
// This is a type that algolia doesn't export but is necessary to work with the algolia client
// Effectively the same as Awaited<ReturnType<SearchClient["search"]>>, but generic
type MultipleQueriesResponse<TObject> = {
results: Array<SearchResponse<TObject> | SearchForFacetValuesResponse>
}

// This is the type for the hits that we get back from algolia when we search
// response.results[0].hits is an array of these
export type IDataCatalogHit = {
title: string
slug: string
availableEntities: string[]
objectID: string
variantName: string | null
__position: number
_highlightResult?: HitHighlightResult
_snippetResult?: HitHighlightResult
}

// SearchResponse adds the extra fields from Algolia: page, nbHits, etc
export type DataCatalogSearchResult = SearchResponse<IDataCatalogHit>

// We add a title field to the SearchResponse for the ribbons
export type DataCatalogRibbonResult = SearchResponse<IDataCatalogHit> & {
title: string
}

export type DataCatalogCache = {
ribbons: Map<string, DataCatalogRibbonResult[]>
search: Map<string, DataCatalogSearchResult>
}

export type DataCatalogSearchResult = SearchResponse<IChartHit>

export type DataCatalogRibbonResult = SearchResponse<IChartHit> & {
title: string
}

/**
* Utils
*/
Expand Down Expand Up @@ -128,14 +156,9 @@ export function dataCatalogStateToAlgoliaQueries(
const facetFilters = [[`tags:${topic}`], ...countryFacetFilters]
return {
indexName: CHARTS_INDEX,
attributesToRetrieve: DATA_CATALOG_ATTRIBUTES,
query: state.query,
facetFilters: facetFilters,
attributesToRetrieve: [
"title",
"slug",
"availableEntities",
"variantName",
],
hitsPerPage: 4,
facets: ["tags"],
page: state.page,
Expand All @@ -153,14 +176,9 @@ export function dataCatalogStateToAlgoliaQuery(state: DataCatalogState) {
return [
{
indexName: CHARTS_INDEX,
attributesToRetrieve: DATA_CATALOG_ATTRIBUTES,
query: state.query,
facetFilters: facetFilters,
attributesToRetrieve: [
"title",
"slug",
"availableEntities",
"variantName",
],
highlightPostTag: "</mark>",
highlightPreTag: "<mark>",
facets: ["tags"],
Expand All @@ -172,22 +190,25 @@ export function dataCatalogStateToAlgoliaQuery(state: DataCatalogState) {
}

export function formatAlgoliaRibbonsResponse(
response: any,
response: MultipleQueriesResponse<IDataCatalogHit>,
ribbonTopics: string[]
): DataCatalogRibbonResult[] {
return response.results.map(
(res: SearchResponse<DataCatalogHit>, i: number) => ({
...res,
title: ribbonTopics[i],
})
)
return response.results.map((res, i: number) => ({
...(res as SearchResponse<IDataCatalogHit>),
title: ribbonTopics[i],
}))
}

export function formatAlgoliaSearchResponse(
response: any
response: MultipleQueriesResponse<IDataCatalogHit>
): DataCatalogSearchResult {
const result = response.results[0] as SearchResponse<IDataCatalogHit>
return {
...response.results[0],
...result,
hits: result.hits.map((hit, i) => ({
...hit,
__position: i + 1,
})),
}
}

Expand All @@ -206,7 +227,7 @@ export async function queryRibbonsWithCache(
topicsForRibbons
)
return searchClient
.search<DataCatalogRibbonResult>(searchParams)
.search<IDataCatalogHit>(searchParams)
.then((response) =>
formatAlgoliaRibbonsResponse(response, topicsForRibbons)
)
Expand All @@ -222,7 +243,7 @@ export async function querySearchWithCache(
): Promise<void> {
const searchParams = dataCatalogStateToAlgoliaQuery(state)
return searchClient
.search<DataCatalogHit>(searchParams)
.search<IDataCatalogHit>(searchParams)
.then(formatAlgoliaSearchResponse)
.then((formatted) => {
cache.current.search.set(dataCatalogStateToUrl(state), formatted)
Expand Down
29 changes: 29 additions & 0 deletions site/SiteAnalytics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { GrapherAnalytics, EventCategory } from "@ourworldindata/grapher"
import { SearchCategoryFilter } from "./search/searchTypes.js"
import { DataCatalogState } from "./DataCatalog/DataCatalogState.js"
import { IDataCatalogHit } from "./DataCatalog/DataCatalogUtils.js"

export class SiteAnalytics extends GrapherAnalytics {
logCountryProfileSearch(country: string) {
Expand Down Expand Up @@ -89,4 +91,31 @@ export class SiteAnalytics extends GrapherAnalytics {
eventTarget: id,
})
}

logDataCatalogSearch(state: DataCatalogState) {
this.logToGA({
event: EventCategory.DataCatalogSearch,
eventAction: "search",
eventContext: JSON.stringify({
...state,
topics: Array.from(state.topics),
selectedCountryNames: Array.from(state.selectedCountryNames),
}),
})
}

logDataCatalogResultClick(
hit: IDataCatalogHit,
source: "ribbon" | "search"
) {
this.logToGA({
event: EventCategory.DataCatalogResultClick,
eventAction: "click",
eventContext: JSON.stringify({
position: hit.__position,
source,
}),
eventTarget: hit.slug,
})
}
}
8 changes: 7 additions & 1 deletion site/search/ChartHit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@ import {
DEFAULT_GRAPHER_WIDTH,
} from "@ourworldindata/grapher"
import { Highlight } from "react-instantsearch"
import { IDataCatalogHit } from "../DataCatalog/DataCatalogUtils.js"

export function ChartHit({
hit,
searchQueryRegionsMatches,
onClick,
}: {
hit: IChartHit
hit: IChartHit | IDataCatalogHit
searchQueryRegionsMatches?: Region[] | undefined
// Search uses a global onClick handler to track analytics
// But the data catalog passes a function to this component explicitly
onClick?: () => void
}) {
const [imgLoaded, setImgLoaded] = useState(false)
const [imgError, setImgError] = useState(false)
Expand Down Expand Up @@ -60,6 +65,7 @@ export function ChartHit({
<a
href={`${BAKED_GRAPHER_URL}/${hit.slug}${queryStr}`}
className="chart-hit"
onClick={onClick}
data-algolia-index={getIndexName(SearchIndexName.Charts)}
data-algolia-object-id={hit.objectID}
data-algolia-position={hit.__position}
Expand Down

0 comments on commit a1668ee

Please sign in to comment.