88 lines
2.8 KiB
TypeScript
88 lines
2.8 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { createServiceClient } from "@/lib/supabase";
|
|
import { anonymizeIp, isBot, parseDevice, parseBrowser, parseOs } from "@/lib/analytics";
|
|
|
|
export const runtime = "nodejs";
|
|
export const dynamic = "force-dynamic";
|
|
|
|
interface TrackBody {
|
|
path: string;
|
|
session_id: string;
|
|
referrer?: string;
|
|
view_id?: string; // Für duration_ms-Updates
|
|
duration_ms?: number; // Nur bei pagehide-Updates
|
|
}
|
|
|
|
/**
|
|
* POST /api/analytics/track
|
|
*
|
|
* Insert-Modus: { path, session_id, referrer? }
|
|
* → DB INSERT, view_id zurückgeben
|
|
*
|
|
* Update-Modus: { view_id, duration_ms }
|
|
* → DB UPDATE duration_ms
|
|
*
|
|
* Bot-Anfragen: Keine DB-Eintrag, aber 200 OK zurückgeben
|
|
*/
|
|
export async function POST(req: NextRequest) {
|
|
try {
|
|
const body: TrackBody = await req.json();
|
|
|
|
// Minimale Validierung
|
|
if (!body.path || !body.session_id) {
|
|
return NextResponse.json({ ok: false }, { status: 400 });
|
|
}
|
|
|
|
const ua = req.headers.get("user-agent") ?? "";
|
|
|
|
// ──── Bot-Filter: Keine DB-Einträge für Bots ────────────────────────────
|
|
if (isBot(ua)) {
|
|
return NextResponse.json({ ok: true, bot: true });
|
|
}
|
|
|
|
const db = createServiceClient();
|
|
|
|
// ──── Update-Modus: Verweildauer setzen ────────────────────────────────
|
|
if (body.view_id && body.duration_ms !== undefined) {
|
|
await db
|
|
.from("page_views")
|
|
.update({ duration_ms: body.duration_ms })
|
|
.eq("id", body.view_id)
|
|
.is("duration_ms", null); // Idempotent: nur updaten wenn noch nicht gesetzt
|
|
return NextResponse.json({ ok: true });
|
|
}
|
|
|
|
// ──── Insert-Modus: Neuen Seitenaufruf anlegen ─────────────────────────
|
|
const rawIp =
|
|
req.headers.get("x-forwarded-for")?.split(",")[0].trim() ??
|
|
req.headers.get("x-real-ip") ??
|
|
"0.0.0.0";
|
|
|
|
const { data, error } = await db
|
|
.from("page_views")
|
|
.insert({
|
|
path: body.path,
|
|
session_id: body.session_id,
|
|
referrer: body.referrer ?? null,
|
|
ip_anon: anonymizeIp(rawIp),
|
|
device_type: parseDevice(ua),
|
|
browser: parseBrowser(ua),
|
|
os: parseOs(ua),
|
|
is_bot: false,
|
|
})
|
|
.select("id")
|
|
.single();
|
|
|
|
if (error) {
|
|
console.error("[analytics/track] DB error:", error);
|
|
return NextResponse.json({ ok: false }, { status: 500 });
|
|
}
|
|
|
|
// view_id zurückgeben für späteren duration_ms-Update
|
|
return NextResponse.json({ ok: true, view_id: data.id });
|
|
} catch (err) {
|
|
console.error("[analytics/track] Error:", err);
|
|
return NextResponse.json({ ok: false }, { status: 500 });
|
|
}
|
|
}
|