Loading...
Nextjs14-Custom-Tracking-Script
Author Cloudapp
E.G.

Next.js 14 - Benutzerdefiniertes Client-seitiges End-to-End-Tracking erstellen

20. August 2024
Inhaltsverzeichnis

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/fingerprintjs

Anpassen 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 push

nach 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:

Wenn Ihnen gefallen hat was Sie gelesen haben oder wenn es Ihnen sogar geholfen hat, dann würden wir uns über einen "Clap" 👏 oder einen neuen Follower auf unseren Medium Account sehr freuen.

Oder folgen Sie uns auf Twitter -> Cloudapp.dev

Verwandte Artikel