/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ 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; useEffect(() => { const { matomoUrl, matomoSiteId } = customFields; if (typeof window !== 'undefined') { const devMode = ['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(window.location.hostname); // Initialize the _paq array window._paq = window._paq || []; // Configure the tracker before loading matomo.js window._paq.push(['enableHeartBeatTimer']); window._paq.push(['enableLinkTracking']); window._paq.push(['setTrackerUrl', `${matomoUrl}/matomo.php`]); window._paq.push(['setSiteId', matomoSiteId]); // Track downloads with custom extensions window._paq.push(['setDownloadExtensions', DOWNLOAD_EXTENSIONS.join('|')]); // Now load the matomo.js script const script = document.createElement('script'); script.async = true; 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]); }; // Helper to track page views const trackPageView = (url, title) => { if (devMode) { console.log('Matomo trackPageView:', { url, title }); } window._paq.push(['trackPageView']); }; // 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); } // Reset scroll tracking for new page resetScrollTracking(); setTimeout(() => { const currentTitle = document.title; const currentPath = window.location.pathname; // Set custom dimensions before tracking page view trackColorMode(); trackDocsVersion(); if (devMode) { window._paq.push(['setDomains', ['superset.apache.org']]); window._paq.push([ 'setCustomUrl', 'https://superset.apache.org' + currentPath, ]); } else { window._paq.push(['setCustomUrl', currentPath]); } window._paq.push(['setReferrerUrl', window.location.href]); window._paq.push(['setDocumentTitle', currentTitle]); trackPageView(currentPath, currentTitle); // Check for 404 after page renders setTimeout(track404, 500); }, 100); }; // Set up Docusaurus route listeners const possibleEvents = [ 'docusaurus.routeDidUpdate', 'docusaurusRouteDidUpdate', 'routeDidUpdate', ]; if (devMode) { console.log('Setting up Matomo tracking with enhanced features'); } // 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 }; }); // Manual history tracking as fallback const originalPushState = window.history.pushState; window.history.pushState = function () { originalPushState.apply(this, arguments); handleRouteChange(); }; 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 () => { 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(); } }; } }, []); return children; }