feat(analytics): implement advanced tracking with script-less smart proxy
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 9s
Build & Deploy / 🧪 QA (push) Failing after 1m18s
Build & Deploy / 🏗️ Build (push) Failing after 2m58s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 9s
Build & Deploy / 🧪 QA (push) Failing after 1m18s
Build & Deploy / 🏗️ Build (push) Failing after 2m58s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
- Added Umami Smart Proxy route handler - Refactored Umami adapter to use proxy-based fetch - Implemented TrackedButton, TrackedLink, and ScrollDepthTracker - Integrated event tracking into ContactForm - Enhanced Analytics component with manual pageview and performance tracking
This commit is contained in:
@@ -1,34 +1,59 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { getDefaultAnalytics } from '../utils/analytics';
|
||||
import { getDefaultErrorTracking } from '../utils/error-tracking';
|
||||
import React, { useEffect } from "react";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { ScrollDepthTracker } from "./analytics/ScrollDepthTracker";
|
||||
import { getDefaultAnalytics } from "../utils/analytics";
|
||||
import { getDefaultErrorTracking } from "../utils/error-tracking";
|
||||
|
||||
export const Analytics: React.FC = () => {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// Track pageviews on route change
|
||||
useEffect(() => {
|
||||
if (!pathname) return;
|
||||
|
||||
const analytics = getDefaultAnalytics();
|
||||
const url = `${pathname}${searchParams?.size ? `?${searchParams.toString()}` : ""}`;
|
||||
|
||||
analytics.page(url);
|
||||
}, [pathname, searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const analytics = getDefaultAnalytics();
|
||||
const errorTracking = getDefaultErrorTracking();
|
||||
|
||||
// Track page load performance
|
||||
const trackPageLoad = () => {
|
||||
const perfData = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
||||
if (perfData && typeof perfData.loadEventEnd === 'number' && typeof perfData.startTime === 'number') {
|
||||
// ... existing implementation ...
|
||||
const perfData = performance.getEntriesByType(
|
||||
"navigation",
|
||||
)[0] as PerformanceNavigationTiming;
|
||||
if (
|
||||
perfData &&
|
||||
typeof perfData.loadEventEnd === "number" &&
|
||||
typeof perfData.startTime === "number"
|
||||
) {
|
||||
const loadTime = perfData.loadEventEnd - perfData.startTime;
|
||||
analytics.trackPageLoad(
|
||||
loadTime,
|
||||
window.location.pathname,
|
||||
navigator.userAgent
|
||||
navigator.userAgent,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Track outbound links
|
||||
const trackOutboundLinks = () => {
|
||||
document.querySelectorAll('a[href^="http"]').forEach(link => {
|
||||
document.querySelectorAll('a[href^="http"]').forEach((link) => {
|
||||
const anchor = link as HTMLAnchorElement;
|
||||
if (!anchor.href.includes(window.location.hostname)) {
|
||||
anchor.addEventListener('click', () => {
|
||||
analytics.trackOutboundLink(anchor.href, anchor.textContent?.trim() || 'unknown');
|
||||
anchor.addEventListener("click", () => {
|
||||
analytics.trackOutboundLink(
|
||||
anchor.href,
|
||||
anchor.textContent?.trim() || "unknown",
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -36,7 +61,9 @@ export const Analytics: React.FC = () => {
|
||||
|
||||
// Track search
|
||||
const trackSearch = () => {
|
||||
const searchInput = document.querySelector('input[type="search"]') as HTMLInputElement;
|
||||
const searchInput = document.querySelector(
|
||||
'input[type="search"]',
|
||||
) as HTMLInputElement;
|
||||
if (searchInput) {
|
||||
const handleSearch = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
@@ -44,8 +71,8 @@ export const Analytics: React.FC = () => {
|
||||
analytics.trackSearch(target.value, window.location.pathname);
|
||||
}
|
||||
};
|
||||
searchInput.addEventListener('search', handleSearch);
|
||||
return () => searchInput.removeEventListener('search', handleSearch);
|
||||
searchInput.addEventListener("search", handleSearch);
|
||||
return () => searchInput.removeEventListener("search", handleSearch);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -58,18 +85,37 @@ export const Analytics: React.FC = () => {
|
||||
errorTracking.captureException(event.reason);
|
||||
};
|
||||
|
||||
window.addEventListener('error', handleGlobalError);
|
||||
window.addEventListener('unhandledrejection', handleUnhandledRejection);
|
||||
window.addEventListener("error", handleGlobalError);
|
||||
window.addEventListener("unhandledrejection", handleUnhandledRejection);
|
||||
|
||||
trackPageLoad();
|
||||
trackOutboundLinks();
|
||||
const cleanupSearch = trackSearch();
|
||||
|
||||
return () => {
|
||||
if (cleanupSearch) cleanupSearch();
|
||||
window.removeEventListener('error', handleGlobalError);
|
||||
window.removeEventListener('unhandledrejection', handleUnhandledRejection);
|
||||
};
|
||||
// Initial load tracking
|
||||
if (document.readyState === "complete") {
|
||||
trackPageLoad();
|
||||
trackOutboundLinks();
|
||||
const cleanupSearch = trackSearch();
|
||||
return () => {
|
||||
if (cleanupSearch) cleanupSearch();
|
||||
window.removeEventListener("error", handleGlobalError);
|
||||
window.removeEventListener(
|
||||
"unhandledrejection",
|
||||
handleUnhandledRejection,
|
||||
);
|
||||
};
|
||||
} else {
|
||||
window.addEventListener("load", () => {
|
||||
trackPageLoad();
|
||||
trackOutboundLinks();
|
||||
// search tracking might need to wait for hydration/render
|
||||
});
|
||||
// Fallback/standard cleanup
|
||||
return () => {
|
||||
window.removeEventListener("error", handleGlobalError);
|
||||
window.removeEventListener(
|
||||
"unhandledrejection",
|
||||
handleUnhandledRejection,
|
||||
);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
const analytics = getDefaultAnalytics();
|
||||
@@ -81,9 +127,12 @@ export const Analytics: React.FC = () => {
|
||||
// We use dangerouslySetInnerHTML to inject the script tag from the adapter
|
||||
// This is safe here because the script URLs and IDs come from our own config/env
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: scriptTag }}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<>
|
||||
<ScrollDepthTracker />
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: scriptTag }}
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user