feat(docs): enhance Matomo analytics tracking (#36743)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Evan Rusackas
2025-12-18 13:52:08 -08:00
committed by GitHub
parent 2cf0d7936e
commit 32435bc3e9

View File

@@ -19,6 +19,17 @@
import { useEffect } from 'react';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
// File extensions to track as downloads
const DOWNLOAD_EXTENSIONS = [
'pdf', 'zip', 'tar', 'gz', 'tgz', 'bz2',
'exe', 'dmg', 'pkg', 'deb', 'rpm',
'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
'csv', 'json', 'yaml', 'yml',
];
// Scroll depth milestones to track
const SCROLL_MILESTONES = [25, 50, 75, 100];
export default function Root({ children }) {
const { siteConfig } = useDocusaurusContext();
const { customFields } = siteConfig;
@@ -27,10 +38,9 @@ export default function Root({ children }) {
const { matomoUrl, matomoSiteId } = customFields;
if (typeof window !== 'undefined') {
// Making testing easier, logging debug junk if we're in development
const devMode = window.location.hostname === 'localhost' ? true : false;
const devMode = ['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(window.location.hostname);
// Initialize the _paq array first
// Initialize the _paq array
window._paq = window._paq || [];
// Configure the tracker before loading matomo.js
@@ -39,7 +49,8 @@ export default function Root({ children }) {
window._paq.push(['setTrackerUrl', `${matomoUrl}/matomo.php`]);
window._paq.push(['setSiteId', matomoSiteId]);
// Initial page view is handled by handleRouteChange
// Track downloads with custom extensions
window._paq.push(['setDownloadExtensions', DOWNLOAD_EXTENSIONS.join('|')]);
// Now load the matomo.js script
const script = document.createElement('script');
@@ -47,19 +58,168 @@ export default function Root({ children }) {
script.src = `${matomoUrl}/matomo.js`;
document.head.appendChild(script);
// Helper to track events
const trackEvent = (category, action, name, value) => {
if (devMode) {
console.log('Matomo trackEvent:', { category, action, name, value });
}
window._paq.push(['trackEvent', category, action, name, value]);
};
// Helper to track site search
const trackSiteSearch = (keyword, category, resultsCount) => {
if (devMode) {
console.log('Matomo trackSiteSearch:', { keyword, category, resultsCount });
}
window._paq.push(['trackSiteSearch', keyword, category, resultsCount]);
};
// Track external link clicks using domain as category (vendor-agnostic)
const handleLinkClick = (event) => {
const link = event.target.closest('a');
if (!link) return;
const href = link.getAttribute('href');
if (!href) return;
try {
const url = new URL(href, window.location.origin);
// Skip internal links
if (url.hostname === window.location.hostname) return;
// Use hostname as category for vendor-agnostic tracking
trackEvent('Outbound Link', url.hostname, href);
} catch {
// Invalid URL, skip tracking
}
};
// Track Algolia search queries
const setupAlgoliaTracking = () => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const searchInput = node.querySelector?.('.DocSearch-Input') ||
(node.classList?.contains('DocSearch-Input') ? node : null);
if (searchInput) {
let debounceTimer;
searchInput.addEventListener('input', (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const query = e.target.value.trim();
if (query.length >= 3) {
const results = document.querySelectorAll('.DocSearch-Hit');
trackSiteSearch(query, 'Documentation', results.length);
}
}, 1000);
});
}
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
return observer;
};
// Track video plays
const handleVideoPlay = (event) => {
if (event.target.tagName === 'VIDEO') {
const videoSrc = event.target.currentSrc || event.target.src || 'unknown';
trackEvent('Video', 'Play', videoSrc);
}
};
// Track CTA button clicks
const handleCTAClick = (event) => {
const button = event.target.closest('.get-started-button, .default-button-theme');
if (button) {
const buttonText = button.textContent?.trim() || 'Unknown';
const href = button.getAttribute('href') || '';
trackEvent('CTA', 'Click', `${buttonText} - ${href}`);
}
};
// Track scroll depth
let scrollMilestonesReached = new Set();
const handleScroll = () => {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
if (docHeight <= 0) return;
const scrollPercent = Math.round((scrollTop / docHeight) * 100);
SCROLL_MILESTONES.forEach(milestone => {
if (scrollPercent >= milestone && !scrollMilestonesReached.has(milestone)) {
scrollMilestonesReached.add(milestone);
trackEvent('Scroll Depth', `${milestone}%`, window.location.pathname);
}
});
};
// Reset scroll tracking on route change
const resetScrollTracking = () => {
scrollMilestonesReached = new Set();
};
// Track 404 pages
const track404 = () => {
const is404 = document.querySelector('.theme-doc-404') ||
document.title.toLowerCase().includes('not found') ||
document.querySelector('h1')?.textContent?.toLowerCase().includes('not found');
if (is404) {
trackEvent('Error', '404', window.location.pathname);
if (devMode) {
console.log('Matomo: 404 page detected', window.location.pathname);
}
}
};
// Track copy-to-clipboard events on code blocks
const handleCopy = (event) => {
const codeBlock = event.target.closest('pre, code, .prism-code');
if (codeBlock) {
const codeText = window.getSelection()?.toString() || '';
const codeSnippet = codeText.substring(0, 100) + (codeText.length > 100 ? '...' : '');
trackEvent('Code', 'Copy', `${window.location.pathname}: ${codeSnippet}`);
}
};
// Track color mode preference (as event, no admin config needed)
const trackColorMode = () => {
const colorMode = document.documentElement.getAttribute('data-theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
trackEvent('User Preference', 'Color Mode', colorMode);
};
// Track docs version from URL (as event, no admin config needed)
const trackDocsVersion = () => {
const pathMatch = window.location.pathname.match(/\/docs\/([\d.]+)\//);
const version = pathMatch ? pathMatch[1] : 'latest';
trackEvent('User Preference', 'Docs Version', version);
};
// Handle route changes for SPA
const handleRouteChange = () => {
if (devMode) {
console.log('Route changed to:', window.location.pathname);
}
// Short timeout to ensure the page has fully rendered
// Reset scroll tracking for new page
resetScrollTracking();
setTimeout(() => {
// Get the current page title from the document
const currentTitle = document.title;
const currentPath = window.location.pathname;
// For testing: impersonate real domain - ONLY FOR DEVELOPMENT
// Set custom dimensions before tracking page view
trackColorMode();
trackDocsVersion();
if (devMode) {
console.log('Tracking page view:', currentPath, currentTitle);
window._paq.push(['setDomains', ['superset.apache.org']]);
@@ -74,10 +234,13 @@ export default function Root({ children }) {
window._paq.push(['setReferrerUrl', window.location.href]);
window._paq.push(['setDocumentTitle', currentTitle]);
window._paq.push(['trackPageView']);
}, 100); // Increased delay to ensure page has fully rendered
// Check for 404 after page renders
setTimeout(track404, 500);
}, 100);
};
// Try all possible Docusaurus events - they've changed between versions
// Set up Docusaurus route listeners
const possibleEvents = [
'docusaurus.routeDidUpdate',
'docusaurusRouteDidUpdate',
@@ -85,21 +248,22 @@ export default function Root({ children }) {
];
if (devMode) {
console.log('Setting up Docusaurus route listeners');
console.log('Setting up Matomo tracking with enhanced features');
}
possibleEvents.forEach(eventName => {
document.addEventListener(eventName, () => {
// Store handler references for proper cleanup
const routeHandlers = possibleEvents.map(eventName => {
const handler = () => {
if (devMode) {
console.log(`Docusaurus route update detected via ${eventName}`);
}
handleRouteChange();
});
};
document.addEventListener(eventName, handler);
return { eventName, handler };
});
// Also set up manual history tracking as fallback
if (devMode) {
console.log('Setting up manual history tracking as fallback');
}
// Manual history tracking as fallback
const originalPushState = window.history.pushState;
window.history.pushState = function () {
originalPushState.apply(this, arguments);
@@ -108,19 +272,53 @@ export default function Root({ children }) {
window.addEventListener('popstate', handleRouteChange);
// Set up event listeners
document.addEventListener('click', handleLinkClick);
document.addEventListener('click', handleCTAClick);
document.addEventListener('play', handleVideoPlay, true);
document.addEventListener('copy', handleCopy);
window.addEventListener('scroll', handleScroll, { passive: true });
// Watch for color mode changes
const colorModeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'data-theme') {
trackEvent('User Preference', 'Color Mode Change',
document.documentElement.getAttribute('data-theme'));
}
});
});
colorModeObserver.observe(document.documentElement, { attributes: true });
// Set up Algolia tracking
const algoliaObserver = setupAlgoliaTracking();
// Initial page tracking
handleRouteChange();
// Cleanup
return () => {
// Cleanup listeners
possibleEvents.forEach(eventName => {
document.removeEventListener(eventName, handleRouteChange);
routeHandlers.forEach(({ eventName, handler }) => {
document.removeEventListener(eventName, handler);
});
if (originalPushState) {
window.history.pushState = originalPushState;
window.removeEventListener('popstate', handleRouteChange);
}
document.removeEventListener('click', handleLinkClick);
document.removeEventListener('click', handleCTAClick);
document.removeEventListener('play', handleVideoPlay, true);
document.removeEventListener('copy', handleCopy);
window.removeEventListener('scroll', handleScroll);
if (algoliaObserver) {
algoliaObserver.disconnect();
}
if (colorModeObserver) {
colorModeObserver.disconnect();
}
};
}
}, []);