"use client"; import { useEffect, useRef } from "react"; import { usePathname } from "next/navigation"; // Admin-Routen und API-Routen tracken wir nicht const EXCLUDED_PREFIXES = ["/admin", "/api", "/_next"]; /** * PageTracker – Client-seitige Komponente für Web-Analytics * * Wird in app/layout.tsx eingebunden (global, einmalig) * - Erfasst Seitenaufrufe * - Misst Verweildauer via pagehide-Event * - Verwendet sessionStorage für Session-ID (nicht persistent) * - Sendet Daten an /api/analytics/track */ export function PageTracker() { const pathname = usePathname(); const viewIdRef = useRef(null); const startTimeRef = useRef(Date.now()); /** * Gibt oder erstellt eine Session-ID aus sessionStorage * Diese ID ist Tab-gebunden (nicht persistent über Reload) */ function getSessionId(): string { let sid = sessionStorage.getItem("_mpv_sid"); if (!sid) { // UUID v4 via Crypto API (kein externe Library) sid = crypto.randomUUID(); sessionStorage.setItem("_mpv_sid", sid); } return sid; } useEffect(() => { // Skip ausgeschlossene Routen if (EXCLUDED_PREFIXES.some((p) => pathname.startsWith(p))) return; startTimeRef.current = Date.now(); viewIdRef.current = null; // ──── Seitenaufruf tracken ──────────────────────────────────────────── fetch("/api/analytics/track", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: pathname, session_id: getSessionId(), referrer: typeof document !== "undefined" ? document.referrer || undefined : undefined, }), }) .then((r) => r.json()) .then((d) => { viewIdRef.current = d.view_id ?? null; }) .catch(() => { // Tracking-Fehler nie nach oben propagieren }); // ──── Verweildauer beim Verlassen der Seite senden ──────────────────── function sendDuration() { if (!viewIdRef.current) return; const ms = Date.now() - startTimeRef.current; // sendBeacon ist zuverlässiger als fetch bei pagehide (safari, mobile) // Blob mit application/json damit req.json() im Handler funktioniert const blob = new Blob( [ JSON.stringify({ path: pathname, session_id: getSessionId(), view_id: viewIdRef.current, duration_ms: ms, }), ], { type: "application/json" } ); navigator.sendBeacon("/api/analytics/track", blob); } // ──── Phone-Click Tracking ──────────────────────────────────────── function trackPhoneClick(event: Event) { const target = event.target as HTMLElement; const link = target.closest('a[href^="tel:"]'); if (!link) return; const phoneHref = link.getAttribute("href"); const phoneNumber = phoneHref?.replace("tel:", "").trim(); if (!phoneNumber) return; const sourceElement = link.getAttribute("data-source-element") || link.closest("[data-source-element]")?.getAttribute("data-source-element") || "unknown"; fetch("/api/analytics/track-phone-click", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone_number: phoneNumber, source_page: pathname, source_element: sourceElement, session_id: getSessionId(), }), }).catch(() => { // Fehler ignorieren }); } // pagehide: zuverlässiger als beforeunload (Safari, Mobile) window.addEventListener("pagehide", sendDuration); // Phone-Click Tracking document.addEventListener("click", trackPhoneClick); return () => { document.removeEventListener("click", trackPhoneClick); window.removeEventListener("pagehide", sendDuration); // Auch beim Route-Wechsel (SPA) die Verweildauer senden sendDuration(); }; }, [pathname]); return null; // Kein Markup }