MBO-Tech-IT-Webseite/modules/03-analytics/files/components/analytics/PageTracker.tsx

129 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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<string | null>(null);
const startTimeRef = useRef<number>(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
}