Inhaltsverzeichnis
- NPM Paket fürs Fingerprinting
- Anpassen des Prisma Schema
- Synchronisieren der Schema Änderungen
- Neuer Tracking Hook wird erstellt
- Neue Komponente für die Integration des Hooks
- Import der neuen Komponente ins Rootlayout
- Layout.tsx — Kompletter Code
- Speichern von Tracking-Daten in der Postgres-DB über eine benutzerdefinierte Route
- Tracking Route — Kompletter Code
- Cloudapp-dev und bevor Sie uns verlassen
In dieser Story zeige ich Ihnen, wie Sie einen benutzerdefinierten Hook für die Seitenverfolgung erstellen und ihn problemlos in Ihr vorhandenes Next.js 14-Projekt integrieren können. Die verfolgten Daten werden über das ORM Prisma und eine benutzerdefinierte API-Route in einer Neon.tech Postgres-Datenbank gespeichert.
Hier ist das GitHub Repo mit dem gesamten Code und darunter der Link zur Beispielwebsite.
Beispielseite für mit integrierter Datenerfassung -> https://nextjs14-kafka-tracking.vercel.app/
NPM Paket fürs Fingerprinting
Wenn Sie die erste Geschichte gelesen haben, haben Sie die benötigten NPM-Pakete bereits installiert.
npm i @fingerprintjs/fingerprintjsAnpassen des Prisma Schema
Hinzufügen neuer Felder in die Datei schema.prisma
model tracking {
id Int @id @default(autoincrement())
country String?
city String?
region String?
url String?
nexturl String?
ip String?
pathname String?
mobile String?
platform String?
useragent String?
referer String?
fetchsite String?
created_at DateTime?
sessionId String?
userId String?
pageViewId String?
addedOn DateTime @default(now())
}Synchronisieren der Schema Änderungen
npx prisma db pushnach Schemaänderungen, so dass die Änderung mit der zugrunde liegenden DB synchronisiert wurde.
Neuer Tracking Hook wird erstellt
Jetzt sind wir bereit für die Erstellung unseres neuen Hooks, bei dem wir das neue npm-Paket „fingerprintjs“ verwenden, mit dem wir eine UserID definieren.
//src/lib/hooks/usePageTracking.ts
"use client";
import { useEffect, useState } from "react";
import FingerprintJS from "@fingerprintjs/fingerprintjs";
const apikey = process.env.API_KEY;
// Define the type for the page view data
interface PageViewData {
pageViewID: string;
sessionID: string;
userID: string;
url: string;
referrer: string;
timestamp: string;
country: string;
}
// Define the type for the time spent data
interface TimeSpentData {
sessionID: string;
userID: string;
timeSpent: number;
url: string;
}
function createSessionID(): string {
return "session-" + Math.random().toString(36).substr(2, 16);
}
function createPageViewID(): string {
return "view-" + Math.random().toString(36).substr(2, 16);
}
function getSessionID(): string {
let sessionId = localStorage.getItem("session_id");
if (!sessionId) {
sessionId = createSessionID();
localStorage.setItem("session_id", sessionId);
}
return sessionId;
}
async function sendToPrisma(message: any) {
// Pushing tracking Info direct to Postgres
await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/user/tracking`, {
method: "POST",
body: JSON.stringify(message),
headers: new Headers({
"Content-Type": "application/json" || "",
"x-api-key": apikey || "",
}),
});
}
async function getUserID(): Promise<string> {
const fpPromise = FingerprintJS.load();
const fp = await fpPromise;
const result = await fp.get();
return result.visitorId; // This is the anonymous user ID
}
async function getUserCountry(): Promise<string> {
try {
const response = await fetch("https://ipapi.co/json/");
const data = await response.json();
return data.country_name || "Unknown";
} catch (error) {
console.error("Error fetching country:", error);
return "Unknown";
}
}
async function capturePageView(userID: string): Promise<void> {
const pageViewID = createPageViewID();
const sessionID = getSessionID();
const url = window.location.href;
const referrer = document.referrer;
const timestamp = new Date().toISOString();
const country = await getUserCountry();
const pageViewData: PageViewData = {
pageViewID: pageViewID,
sessionID: sessionID,
userID: userID,
url: url,
referrer: referrer,
timestamp: timestamp,
country: country,
};
sendDataToServer(pageViewData);
}
function sendDataToServer(data: PageViewData | TimeSpentData): void {
const trackdata: any = data;
const message = {
country: trackdata.country,
// city: request.geo?.city,
// region: request.geo?.region,
// pathname: request.nextUrl.pathname,
url: trackdata.url,
// nexturl: request.headers.get("next-url"),
// mobile: request.headers.get("sec-ch-ua-mobile"),
// platform: request.headers.get("sec-ch-ua-platform"),
// useragent: request.headers.get("user-agent"),
referer: trackdata.referrer,
userId: trackdata.userID,
pageViewId: trackdata.pageViewID,
sessionId: trackdata.sessionID,
created_at: trackdata.timestamp,
};
sendToPrisma(message);
}
export default function usePageTracking(): void {
const [userID, setUserID] = useState<string | null>(null);
useEffect(() => {
// Initialize user ID and start tracking
const initializeTracking = async () => {
const id = await getUserID();
setUserID(id);
await capturePageView(id);
};
initializeTracking();
const pageLoadTime = Date.now();
const handleBeforeUnload = () => {
if (userID) {
const timeSpent = Date.now() - pageLoadTime;
const timeSpentData: TimeSpentData = {
sessionID: getSessionID(),
userID: userID,
timeSpent: timeSpent,
url: window.location.href,
};
sendDataToServer(timeSpentData);
}
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
};
}, [userID]);
}Neue Komponente für die Integration des Hooks
Nun erstellen wir eine neue Komponente, die den neuen Hook verwendet, den wir dann in unsere layout.tsx importieren
//src/components/analytics/customTracking.tsx
"use client";
import usePageTracking from "@/lib/hooks/usePageTracking";
const PageTracker: React.FC = () => {
usePageTracking(); // Hook is called here
return null; // This component doesn't need to render anything
};
export default PageTracker;Import der neuen Komponente ins Rootlayout
Jetzt importieren wir die Komponente src/app/[locale]/layout.tsx
// Custom Tracking Script/Hooks
import CustomTracking from "@/components/analytics/customTracking";und unterhalb {children} wird die Komponente eingebunden.
{/* Custom Tracking Hook */}
<CustomTracking />Layout.tsx — Kompletter Code
import type { Metadata } from "next";
import { Urbanist } from "next/font/google";
import { draftMode } from "next/headers";
import "instantsearch.css/themes/satellite-min.css";
import "@/app/globals.css";
import Header from "@/components/header/header.component";
import Footer from "@/components/footer/footer.component";
import { Providers } from "@/components/header/providers";
import getAllNavitemsForHome from "@/components/header/navbar.menuitems.component";
import getAllFooteritemsForHome from "@/components/footer/footer.menuitems.component";
import ExitDraftModeLink from "@/components/header/draftmode/ExitDraftModeLink.component";
import { locales } from "@/app/i18n/settings";
//Contentful Client
import { client } from "@/lib/client";
// Custom Tracking Script/Hooks
import CustomTracking from "@/components/analytics/customTracking";
// Auth AD B2C
import { NextAuthProvider } from "@/app/providers";
const urbanist = Urbanist({ subsets: ["latin"], variable: "--font-urbanist" });
export async function generateStaticParams() {
return locales.map((lng) => ({ lng }));
}
export const metadata: Metadata = {
title: "Example Blog",
description: "Your Example Blog Description",
icons: {
icon: [
{ rel: "icon", url: "/favicons/favicon-16x16.png", sizes: "16x16" },
new URL("/favicons/favicon-16x16.png", process.env.NEXT_PUBLIC_BASE_URL),
{ rel: "icon", url: "/favicons/favicon-32x32.png", sizes: "32x32" },
new URL("/favicons/favicon-32x32.png", process.env.NEXT_PUBLIC_BASE_URL),
],
shortcut: [{ rel: "shortcut icon", url: "/favicons/favicon.ico" }],
apple: [
{
url: "/favicons/apple-touch-icon.png",
sizes: "180x180",
type: "image/png",
},
],
},
};
type LayoutProps = {
children: React.ReactNode;
params: { locale: string };
};
export default async function RootLayout({ children, params }: LayoutProps) {
const locale = params.locale;
const htmlLang = locale === "en-US" ? "en" : "de";
const headerdata = await getAllNavitemsForHome(locale);
const footerdata = await getAllFooteritemsForHome(locale);
// Get the landing page data for the logo
const { isEnabled } = draftMode();
const landingPageData = await client.pageLanding({
locale,
preview: isEnabled,
slug: "/",
});
const page = landingPageData.pageLandingCollection?.items[0];
const logourl = page?.logo?.url || "";
return (
<html lang={htmlLang} suppressHydrationWarning>
<head></head>
<body>
<main className={`${urbanist.variable} font-sans dark:bg-gray-900`}>
<NextAuthProvider>
<Providers>
<Header showBar={true} menuItems={headerdata} logourl={logourl} />
{draftMode().isEnabled && (
<p className="bg-emerald-400 py-4 px-[6vw]">
Draft mode is on! <ExitDraftModeLink className="underline" />
</p>
)}
{children}
{/* Custom Tracking Hook */}
<CustomTracking />
<Footer footerItems={footerdata} />
</Providers>
</NextAuthProvider>
</main>
</body>
</html>
);
}Speichern von Tracking-Daten in der Postgres-DB über eine benutzerdefinierte Route
Wir verwenden die neue Route aus dem zuvor erstellen Hook
async function sendToPrisma(message: any) {
// Pushing tracking Info direct to Postgres
await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/user/tracking`, {
method: "POST",
body: JSON.stringify(message),
headers: new Headers({
"Content-Type": "application/json" || "",
"x-api-key": apikey || "",
}),
});
}Tracking Route — Kompletter Code
Die Route ruft die Daten ab und speichert sie in der entsprechenden „Tracking“-Tabelle. Da wir Prisma verwenden, ist das ganz einfach.
//src/app/api/user/tracking/route.ts
import prisma from "@/lib/prisma";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const {
country,
city,
region,
pathname,
url,
nexturl,
ip,
mobile,
platform,
useragent,
referer,
fetchsite,
created_at,
userId,
pageViewId,
sessionId,
} = await req.json();
let data = await prisma.tracking.create({
data: {
country: country,
city: city,
region: region,
pathname: pathname,
url: url,
nexturl: nexturl,
ip: ip,
mobile: mobile,
platform: platform,
useragent: useragent,
referer: referer,
fetchsite: fetchsite,
created_at: created_at,
userId: userId,
pageViewId: pageViewId,
sessionId: sessionId,
},
});
return NextResponse.json({
data,
});
}Cloudapp-dev und bevor Sie uns verlassen
Danke, dass Sie bis zum Ende gelesen haben. Noch eine Bitte bevor Sie gehen:

