Skip to content

Commit f4c6a27

Browse files
committed
chore(docs): add google analytics tracking code
1 parent 2aa1aa8 commit f4c6a27

2 files changed

Lines changed: 430 additions & 0 deletions

File tree

docs/src/theme/Root/index.js

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
/**
2+
* Root theme component - wraps the entire app
3+
* Handles client-side tracking
4+
*/
5+
6+
import React, { useEffect } from 'react';
7+
import { trackSiteSearch, trackCTAClick, trackCopyCode, extractFunctionName } from '../../utils/tracking';
8+
9+
export default function Root({ children }) {
10+
useEffect(() => {
11+
if (typeof window === 'undefined') {
12+
return;
13+
}
14+
15+
if (!window.dataLayer) {
16+
window.dataLayer = [];
17+
}
18+
19+
let lastTrackedQuery = '';
20+
let searchTimeout = null;
21+
22+
const handleDocSearchQuery = (event) => {
23+
if (event.detail && event.detail.query) {
24+
const query = event.detail.query.trim();
25+
if (query && query !== lastTrackedQuery && query.length > 0) {
26+
trackSiteSearch(query);
27+
lastTrackedQuery = query;
28+
}
29+
}
30+
};
31+
32+
document.addEventListener('docsearch:query', handleDocSearchQuery);
33+
34+
const setupInputTracking = (searchInput) => {
35+
if (searchInput.hasAttribute('data-tracking-setup')) {
36+
return;
37+
}
38+
39+
searchInput.setAttribute('data-tracking-setup', 'true');
40+
41+
const handleInput = (e) => {
42+
const query = e.target.value.trim();
43+
44+
if (searchTimeout) {
45+
clearTimeout(searchTimeout);
46+
}
47+
48+
if (query.length > 0 && query !== lastTrackedQuery) {
49+
searchTimeout = setTimeout(() => {
50+
trackSiteSearch(query);
51+
lastTrackedQuery = query;
52+
}, 800);
53+
}
54+
};
55+
56+
const handleKeyDown = (e) => {
57+
if (e.key === 'Enter') {
58+
const query = e.target.value.trim();
59+
if (query && query !== lastTrackedQuery && query.length > 0) {
60+
if (searchTimeout) {
61+
clearTimeout(searchTimeout);
62+
}
63+
trackSiteSearch(query);
64+
lastTrackedQuery = query;
65+
}
66+
}
67+
};
68+
69+
searchInput.addEventListener('input', handleInput);
70+
searchInput.addEventListener('keydown', handleKeyDown);
71+
};
72+
73+
const observer = new MutationObserver(() => {
74+
const searchInput = document.querySelector('.DocSearch-Input');
75+
if (searchInput) {
76+
setupInputTracking(searchInput);
77+
}
78+
});
79+
80+
observer.observe(document.body, {
81+
childList: true,
82+
subtree: true
83+
});
84+
85+
const initialSearchInput = document.querySelector('.DocSearch-Input');
86+
if (initialSearchInput) {
87+
setupInputTracking(initialSearchInput);
88+
}
89+
90+
const handleResultClick = (e) => {
91+
const hitElement = e.target.closest('.DocSearch-Hit');
92+
if (hitElement) {
93+
const searchInput = document.querySelector('.DocSearch-Input');
94+
if (searchInput && searchInput.value) {
95+
const query = searchInput.value.trim();
96+
if (query && query !== lastTrackedQuery && query.length > 0) {
97+
trackSiteSearch(query);
98+
lastTrackedQuery = query;
99+
}
100+
}
101+
}
102+
};
103+
104+
document.addEventListener('click', handleResultClick);
105+
106+
// CTA Click Tracking
107+
// Track clicks on buttons and links that should be treated as CTAs
108+
const handleCTAClick = (e) => {
109+
// Find the clicked element (could be button, link, or child element)
110+
let target = e.target;
111+
112+
// Traverse up to find the actual button/link element
113+
while (target && target !== document.body) {
114+
// Check if it's a button or link with CTA classes
115+
const ctaSelector = 'a.getStarted, a.button, button.button, .button, a.cardButton, .cardButton, .downloadAsset, a.downloadAsset, .assetDownloadLink, a.assetDownloadLink';
116+
const isCTA = target.matches && (
117+
target.matches(ctaSelector) ||
118+
target.closest(ctaSelector)
119+
);
120+
121+
if (isCTA) {
122+
const ctaElement = target.matches(ctaSelector)
123+
? target
124+
: target.closest(ctaSelector);
125+
126+
if (ctaElement) {
127+
let clickUrl = '';
128+
if (ctaElement.href) {
129+
clickUrl = ctaElement.href;
130+
} else if (ctaElement.getAttribute('href')) {
131+
const href = ctaElement.getAttribute('href');
132+
clickUrl = href.startsWith('http') ? href : window.location.origin + href;
133+
} else if (ctaElement.getAttribute('to')) {
134+
// Docusaurus Link component uses 'to' attribute
135+
const to = ctaElement.getAttribute('to');
136+
clickUrl = to.startsWith('http') ? to : window.location.origin + to;
137+
} else {
138+
clickUrl = window.location.href;
139+
}
140+
141+
// Get the text content
142+
let clickText = ctaElement.textContent?.trim() ||
143+
ctaElement.innerText?.trim() ||
144+
ctaElement.getAttribute('aria-label') ||
145+
ctaElement.getAttribute('title') ||
146+
'CTA Click';
147+
148+
// Clean up the text (remove extra whitespace)
149+
clickText = clickText.replace(/\s+/g, ' ').trim();
150+
151+
// Track the CTA click
152+
if (clickUrl && clickText) {
153+
trackCTAClick(clickUrl, clickText);
154+
}
155+
}
156+
break;
157+
}
158+
target = target.parentElement;
159+
}
160+
};
161+
162+
document.addEventListener('click', handleCTAClick);
163+
164+
// Code Copy Tracking
165+
const handleCodeCopyClick = (e) => {
166+
// Find the copy button (Docusaurus uses a button with aria-label containing "copy")
167+
const copyButton = e.target.closest('button[aria-label*="copy" i], button[aria-label*="copier" i]');
168+
169+
if (copyButton) {
170+
const codeBlock = copyButton.closest('div[class*="codeBlock"], .theme-code-block, .prism-code, pre');
171+
172+
if (codeBlock) {
173+
let codeElement = codeBlock.querySelector('code');
174+
175+
if (!codeElement) {
176+
const preElement = codeBlock.querySelector('pre code') || codeBlock.closest('pre')?.querySelector('code');
177+
if (preElement) {
178+
codeElement = preElement;
179+
}
180+
}
181+
182+
if (codeElement) {
183+
// Extract language from class names (recursive search)
184+
const findLanguage = (element) => {
185+
if (!element || element === document.body) return 'unknown';
186+
187+
const classList = Array.from(element.classList);
188+
const langClass = classList.find(cls => cls.startsWith('language-'));
189+
190+
if (langClass) {
191+
return langClass.replace('language-', '');
192+
}
193+
194+
return findLanguage(element.parentElement);
195+
};
196+
197+
// Also check pre element and container for language class
198+
let languageName = findLanguage(codeElement);
199+
if (languageName === 'unknown') {
200+
const preElement = codeElement.closest('pre');
201+
if (preElement) {
202+
languageName = findLanguage(preElement);
203+
}
204+
}
205+
if (languageName === 'unknown') {
206+
languageName = findLanguage(codeBlock);
207+
}
208+
209+
const codeContent = codeElement.textContent || codeElement.innerText || '';
210+
211+
const functionName = extractFunctionName(codeContent, languageName);
212+
213+
trackCopyCode(functionName, languageName);
214+
}
215+
}
216+
}
217+
};
218+
219+
// Also listen for copy events to catch any copy operations
220+
const handleCopyEvent = (e) => {
221+
const selection = window.getSelection();
222+
if (!selection || selection.rangeCount === 0) return;
223+
224+
const range = selection.getRangeAt(0);
225+
let node = range.commonAncestorContainer;
226+
227+
if (node.nodeType === Node.TEXT_NODE) {
228+
node = node.parentElement;
229+
}
230+
231+
const codeElement = node?.closest?.('code');
232+
if (!codeElement) return;
233+
234+
// Extract language
235+
const findLanguage = (element) => {
236+
const classList = Array.from(element.classList);
237+
const langClass = classList.find(cls => cls.startsWith('language-'));
238+
console.log("hello", langClass);
239+
240+
if (langClass) return langClass.replace('language-', '');
241+
242+
const parent = element.parentElement;
243+
if (parent && parent !== document.body) {
244+
return findLanguage(parent);
245+
}
246+
return 'unknown';
247+
};
248+
249+
const languageName = findLanguage(codeElement);
250+
const codeContent = selection.toString() || codeElement.textContent || '';
251+
const functionName = extractFunctionName(codeContent, languageName);
252+
253+
trackCopyCode(functionName, languageName);
254+
};
255+
256+
document.addEventListener('click', handleCodeCopyClick);
257+
document.addEventListener('copy', handleCopyEvent);
258+
259+
// Cleanup
260+
return () => {
261+
document.removeEventListener('docsearch:query', handleDocSearchQuery);
262+
document.removeEventListener('click', handleResultClick);
263+
document.removeEventListener('click', handleCTAClick);
264+
document.removeEventListener('click', handleCodeCopyClick);
265+
document.removeEventListener('copy', handleCopyEvent);
266+
observer.disconnect();
267+
if (searchTimeout) {
268+
clearTimeout(searchTimeout);
269+
}
270+
};
271+
}, []);
272+
273+
return <>{children}</>;
274+
}
275+

0 commit comments

Comments
 (0)