Skip to content

Commit 575b351

Browse files
author
Yuraima Estevez
authored
[polaris.shopify.com] Capture search data with google analytics (#8602)
<!-- ☝️How to write a good PR title: - Prefix it with [ComponentName] (if applicable), for example: [Button] - Start with a verb, for example: Add, Delete, Improve, Fix… - Give as much context as necessary and as little as possible - Prefix it with [WIP] while it’s a work in progress --> 📣 Still in draft because I want to figure out how to set up a dev environment in google analytics. I don't think that's necessary for this and I think it will take more time than I want to invest in something that we only need for a week or two but just calling it out. ### WHY are these changes introduced? We want to capture search data for about 2 weeks to help establish a baseline for our search feature on polaris.shopify.com <!-- Context about the problem that’s being addressed. --> ### WHAT is this pull request doing? * adding a new utility to the GlobalSearch to send a `customSearch` event to google analytics * `customSearch` tracks the `searchTerm`, `rank`, and `selectedResult` whenever a user selects a search result or exits the search modal ⚠️ This isn't the DRYest or most sophisticated code... but this is a very limited test we're doing in order to establish a baseline for a future (but not directly related) project. It didn't seem like a good investment to refactor the GlobalSearch for such a small need. I can rip this code out once we get the data we're hoping to capture. <!-- ℹ️ Delete the following for small / trivial changes --> ### How to 🎩 Test the following scenarios: * click on a provided search result * use the keyboard to select a search result * type out a search query and then exit the modal without selecting any result For all of these scenarios you need to check the google `dataLayer` object in the dev console. selecting a search result should add the `searchTerm`, `rank`, and `selectedResult` to the `customSearch` object in the `dataLay` <img width="438" alt="Screenshot 2023-03-08 at 4 55 00 PM" src="https://user-images.githubusercontent.com/4642404/223869996-95876d42-4052-439f-ac50-8e44a52e3a24.png"> closing the modal without selecting a result will create an object with `rank` equal to `0` and an empty `selectedResult` <img width="228" alt="Screenshot 2023-03-08 at 4 56 17 PM" src="https://user-images.githubusercontent.com/4642404/223870206-f4c80202-e19f-4c90-b989-2f5f02849097.png"> ### 🎩 checklist - [ ] Tested on [mobile](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting.md#cross-browser-testing) - [ ] Tested on [multiple browsers](https://help.shopify.com/en/manual/shopify-admin/supported-browsers) - [ ] Tested for [accessibility](https://github.com/Shopify/polaris/blob/main/documentation/Accessibility%20testing.md) - [ ] Updated the component's `README.md` with documentation changes - [ ] [Tophatted documentation](https://github.com/Shopify/polaris/blob/main/documentation/Tophatting%20documentation.md) changes in the style guide
1 parent 515a62f commit 575b351

File tree

5 files changed

+155
-20
lines changed

5 files changed

+155
-20
lines changed

.changeset/swift-cycles-happen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'polaris.shopify.com': patch
3+
---
4+
5+
Added search tracking using google analytics

polaris.shopify.com/src/components/GlobalSearch/GlobalSearch.tsx

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,22 @@ function scrollToTop() {
4343
overflowEl?.scrollTo({top: 0, behavior: 'smooth'});
4444
}
4545

46+
function captureSearchEvent(
47+
searchTerm: string,
48+
resultRank: number,
49+
selectedResult?: string,
50+
) {
51+
const eventParams = {
52+
searchTerm,
53+
resultRank,
54+
selectedResult,
55+
};
56+
57+
if (searchTerm) {
58+
window.gtag('event', 'customSearch', eventParams);
59+
}
60+
}
61+
4662
function scrollIntoView() {
4763
const overflowEl = document.querySelector(`.${styles.ResultsInner}`);
4864
const highlightedEl = document.querySelector(
@@ -152,6 +168,7 @@ function GlobalSearch() {
152168
if (resultsInRenderedOrder.length > 0) {
153169
setIsOpen(false);
154170
const url = resultsInRenderedOrder[currentResultIndex].url;
171+
captureSearchEvent(searchTerm, currentResultIndex + 1, url);
155172
router.push(url);
156173
}
157174
break;
@@ -171,7 +188,14 @@ function GlobalSearch() {
171188
Search <span className={styles.KeyboardShortcutHint}>/</span>
172189
</button>
173190

174-
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
191+
<Dialog
192+
open={isOpen}
193+
onClose={() => {
194+
setIsOpen(false);
195+
// on close we want to capture that no search result was selected
196+
captureSearchEvent(searchTerm, 0);
197+
}}
198+
>
175199
<div className={styles.PreventBackgroundInteractions}></div>
176200
<div className="dark-mode styles-for-site-but-not-polaris-examples">
177201
<Dialog.Panel className={styles.Results}>
@@ -213,6 +237,8 @@ function GlobalSearch() {
213237
<SearchResults
214238
searchResults={searchResults}
215239
currentItemId={currentItemId}
240+
searchTerm={searchTerm}
241+
resultsInRenderedOrder={resultsInRenderedOrder}
216242
/>
217243
)}
218244
</div>
@@ -226,9 +252,13 @@ function GlobalSearch() {
226252
function SearchResults({
227253
searchResults,
228254
currentItemId,
255+
searchTerm,
256+
resultsInRenderedOrder,
229257
}: {
230258
searchResults: GroupedSearchResults;
231259
currentItemId: string;
260+
searchTerm?: string;
261+
resultsInRenderedOrder: SearchResults;
232262
}) {
233263
return (
234264
<>
@@ -243,6 +273,12 @@ function SearchResults({
243273
if (!meta.foundations) return null;
244274
const {title, description, icon, category} =
245275
meta.foundations;
276+
const resultIndex = resultsInRenderedOrder.findIndex(
277+
(r) => {
278+
return r.id === id;
279+
},
280+
);
281+
const rank = resultIndex + 1; // zero-indexed
246282
return (
247283
<SearchContext.Provider
248284
key={title}
@@ -252,6 +288,10 @@ function SearchResults({
252288
title={title}
253289
description={description}
254290
url={url}
291+
customOnClick={() =>
292+
searchTerm &&
293+
captureSearchEvent(searchTerm, rank, url)
294+
}
255295
renderPreview={() => (
256296
<FoundationsThumbnail
257297
icon={icon}
@@ -273,6 +313,12 @@ function SearchResults({
273313
{results.map(({id, url, meta}) => {
274314
if (!meta.patterns) return null;
275315
const {title, description, previewImg} = meta.patterns;
316+
const resultIndex = resultsInRenderedOrder.findIndex(
317+
(r) => {
318+
return r.id === id;
319+
},
320+
);
321+
const rank = resultIndex + 1;
276322
return (
277323
<SearchContext.Provider
278324
key={id}
@@ -282,6 +328,10 @@ function SearchResults({
282328
url={url}
283329
description={description}
284330
title={title}
331+
customOnClick={() =>
332+
searchTerm &&
333+
captureSearchEvent(searchTerm, rank, url)
334+
}
285335
renderPreview={() => (
286336
<PatternThumbnailPreview
287337
alt={title}
@@ -304,6 +354,12 @@ function SearchResults({
304354
{results.map(({id, url, meta}) => {
305355
if (!meta.components) return null;
306356
const {title, description, status, group} = meta.components;
357+
const resultIndex = resultsInRenderedOrder.findIndex(
358+
(r) => {
359+
return r.id === id;
360+
},
361+
);
362+
const rank = resultIndex + 1;
307363
return (
308364
<SearchContext.Provider
309365
key={id}
@@ -314,6 +370,10 @@ function SearchResults({
314370
description={description}
315371
title={title}
316372
status={status}
373+
customOnClick={() =>
374+
searchTerm &&
375+
captureSearchEvent(searchTerm, rank, url)
376+
}
317377
renderPreview={() => (
318378
<ComponentThumbnail title={title} group={group} />
319379
)}
@@ -342,12 +402,24 @@ function SearchResults({
342402
{results.map(({id, meta}) => {
343403
if (!meta.tokens) return null;
344404
const {token, category} = meta.tokens;
405+
const resultIndex = resultsInRenderedOrder.findIndex(
406+
(r) => {
407+
return r.id === id;
408+
},
409+
);
410+
const rank = resultIndex + 1;
345411
return (
346412
<SearchContext.Provider
347413
key={id}
348414
value={{currentItemId, id}}
349415
>
350-
<TokenList.Item category={category} token={token} />
416+
<TokenList.Item
417+
category={category}
418+
token={token}
419+
customOnClick={captureSearchEvent}
420+
searchTerm={searchTerm}
421+
rank={rank}
422+
/>
351423
</SearchContext.Provider>
352424
);
353425
})}
@@ -363,12 +435,23 @@ function SearchResults({
363435
{results.map(({id, meta}) => {
364436
if (!meta.icons) return null;
365437
const {icon} = meta.icons;
438+
const resultIndex = resultsInRenderedOrder.findIndex(
439+
(r) => {
440+
return r.id === id;
441+
},
442+
);
443+
const rank = resultIndex + 1;
366444
return (
367445
<SearchContext.Provider
368446
key={id}
369447
value={{currentItemId, id}}
370448
>
371-
<IconGrid.Item icon={icon} />
449+
<IconGrid.Item
450+
icon={icon}
451+
customOnClick={captureSearchEvent}
452+
searchTerm={searchTerm}
453+
rank={rank}
454+
/>
372455
</SearchContext.Provider>
373456
);
374457
})}

polaris.shopify.com/src/components/Grid/Grid.tsx

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -66,25 +66,39 @@ export interface GridItemProps {
6666
deepLinks?: {url: string; text: string}[];
6767
renderPreview?: () => React.ReactNode;
6868
status?: Status;
69+
customOnClick?: React.MouseEventHandler<HTMLAnchorElement>;
70+
searchQuery?: string;
71+
rank?: number;
6972
}
7073

7174
export const GridItem = forwardRef(
7275
(
73-
{as = 'li', title, description, url, deepLinks, renderPreview, status},
76+
{
77+
as = 'li',
78+
title,
79+
description,
80+
url,
81+
deepLinks,
82+
renderPreview,
83+
status,
84+
customOnClick,
85+
},
7486
ref,
7587
) => {
7688
const searchAttributes = useGlobalSearchResult();
7789
return (
7890
<Box as={as} ref={ref} className={styles.GridItem} {...searchAttributes}>
79-
<Link href={url} className={styles.Text}>
80-
<SearchResultHighlight />
81-
{renderPreview && (
82-
<div className={styles.Preview}>{renderPreview()}</div>
83-
)}
84-
<h4>
85-
{title} {status && <StatusBadge status={status} />}
86-
</h4>
87-
<p>{stripMarkdownLinks(description || '')}</p>
91+
<Link legacyBehavior href={url} className={styles.Text}>
92+
<a onClick={customOnClick}>
93+
<SearchResultHighlight />
94+
{renderPreview && (
95+
<div className={styles.Preview}>{renderPreview()}</div>
96+
)}
97+
<h4>
98+
{title} {status && <StatusBadge status={status} />}
99+
</h4>
100+
<p>{stripMarkdownLinks(description || '')}</p>
101+
</a>
88102
</Link>
89103
{deepLinks && (
90104
<ul className={styles.DeepLinks}>

polaris.shopify.com/src/components/IconGrid/IconGrid.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,26 @@ interface IconGridItemProps {
2727
icon: IconType;
2828
query?: string;
2929
activeIcon?: string;
30+
customOnClick?: Function;
31+
rank?: number;
32+
searchTerm?: string;
3033
}
3134

32-
function IconGridItem({icon, activeIcon, query}: IconGridItemProps) {
35+
function IconGridItem({
36+
icon,
37+
activeIcon,
38+
query,
39+
customOnClick,
40+
rank,
41+
searchTerm,
42+
}: IconGridItemProps) {
3343
const {id, name} = icon;
3444
const searchAttributes = useGlobalSearchResult();
3545

3646
return (
3747
<li key={id}>
3848
<Link
49+
legacyBehavior
3950
href={{
4051
pathname: '/icons',
4152
query: {
@@ -51,9 +62,16 @@ function IconGridItem({icon, activeIcon, query}: IconGridItemProps) {
5162
id={icon.id}
5263
{...searchAttributes}
5364
>
54-
<SearchResultHighlight />
55-
<Icon source={(polarisIcons as any)[id]} />
56-
<p>{name}</p>
65+
<a
66+
onClick={() =>
67+
customOnClick &&
68+
customOnClick(searchTerm, rank, `/icons?icon=${id}`)
69+
}
70+
>
71+
<SearchResultHighlight />
72+
<Icon source={(polarisIcons as any)[id]} />
73+
<p>{name}</p>
74+
</a>
5775
</Link>
5876
</li>
5977
);

polaris.shopify.com/src/components/TokenList/TokenList.tsx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,18 +124,29 @@ function getFigmaUsageForToken(
124124
interface TokenListItemProps {
125125
category: string;
126126
token: TokenPropertiesWithName;
127+
customOnClick?: Function;
128+
searchTerm?: string;
129+
rank?: number;
127130
}
128131

129132
function TokenListItem({
130133
category,
131134
token: {name, value, description},
135+
customOnClick,
136+
searchTerm,
137+
rank,
132138
}: TokenListItemProps) {
133139
const figmaUsage = getFigmaUsageForToken(name, value);
134140
const tokenNameWithPrefix = `--p-${name}`;
135141
const [copy, didJustCopy] = useCopyToClipboard(tokenNameWithPrefix);
136142

137143
const searchAttributes = useGlobalSearchResult();
138144
const isClickableSearchResult = !!searchAttributes?.tabIndex;
145+
const url = `/tokens/${category}#${searchAttributes?.id}`;
146+
147+
const customOnClickHandler = () => {
148+
customOnClick && customOnClick(searchTerm, rank, url);
149+
};
139150

140151
return (
141152
<TokenListContext.Consumer>
@@ -152,11 +163,12 @@ function TokenListItem({
152163
<TokenPreview name={name} value={value} />
153164
{isClickableSearchResult && (
154165
<Link
155-
href={`/tokens/${category}#${searchAttributes?.id}`}
166+
href={url}
156167
className={styles.ClickableItemLink}
157168
tabIndex={-1}
169+
legacyBehavior
158170
>
159-
View token
171+
<a onClick={customOnClickHandler}>View token</a>
160172
</Link>
161173
)}
162174
</td>
@@ -177,7 +189,10 @@ function TokenListItem({
177189
)}
178190
>
179191
<button
180-
onClick={copy}
192+
onClick={() => {
193+
copy();
194+
customOnClickHandler();
195+
}}
181196
tabIndex={searchAttributes?.tabIndex}
182197
>
183198
<Icon source={ClipboardMinor} width={14} height={14} />

0 commit comments

Comments
 (0)