Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🏗️ Build (push) Failing after 14s
Build & Deploy / 🧪 QA (push) Failing after 1m48s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
145 lines
4.2 KiB
TypeScript
145 lines
4.2 KiB
TypeScript
"use client";
|
|
|
|
import React, { useEffect, Suspense } from "react";
|
|
import { useSafePathname, useSafeSearchParams } from "./analytics/useSafePathname";
|
|
import { ScrollDepthTracker } from "./analytics/ScrollDepthTracker";
|
|
import { getDefaultAnalytics } from "../utils/analytics";
|
|
import { getDefaultErrorTracking } from "../utils/error-tracking";
|
|
|
|
const AnalyticsInner: React.FC = () => {
|
|
const pathname = useSafePathname();
|
|
const searchParams = useSafeSearchParams();
|
|
|
|
// 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 = () => {
|
|
// ... 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,
|
|
);
|
|
}
|
|
};
|
|
|
|
// Track outbound links
|
|
const trackOutboundLinks = () => {
|
|
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",
|
|
);
|
|
});
|
|
}
|
|
});
|
|
};
|
|
|
|
// Track search
|
|
const trackSearch = () => {
|
|
const searchInput = document.querySelector(
|
|
'input[type="search"]',
|
|
) as HTMLInputElement;
|
|
if (searchInput) {
|
|
const handleSearch = (e: Event) => {
|
|
const target = e.target as HTMLInputElement;
|
|
if (target.value) {
|
|
analytics.trackSearch(target.value, window.location.pathname);
|
|
}
|
|
};
|
|
searchInput.addEventListener("search", handleSearch);
|
|
return () => searchInput.removeEventListener("search", handleSearch);
|
|
}
|
|
};
|
|
|
|
// Global error handler for error tracking
|
|
const handleGlobalError = (event: ErrorEvent) => {
|
|
errorTracking.captureException(event.error || event.message);
|
|
};
|
|
|
|
const handleUnhandledRejection = (event: PromiseRejectionEvent) => {
|
|
errorTracking.captureException(event.reason);
|
|
};
|
|
|
|
window.addEventListener("error", handleGlobalError);
|
|
window.addEventListener("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();
|
|
const adapter = analytics.getAdapter();
|
|
const scriptTag = adapter.getScriptTag ? adapter.getScriptTag() : null;
|
|
|
|
return (
|
|
<>
|
|
<ScrollDepthTracker />
|
|
{scriptTag && (
|
|
<div
|
|
dangerouslySetInnerHTML={{ __html: scriptTag }}
|
|
style={{ display: "none" }}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const Analytics: React.FC = () => {
|
|
return (
|
|
<Suspense fallback={null}>
|
|
<AnalyticsInner />
|
|
</Suspense>
|
|
);
|
|
};
|