Loading...
Custom Tracking part2
Author Cloudapp
E.G.

Next.js 14 - Custom Client-Side Tracking - Part 2 (new data attributes)

August 22, 2024
Table of Contents

In this follow-up story, I will show you how to add new attributes to your tracking data set. We will add the UserAgent, Pathname, Mobile, Platform, Region, and City. All data will be saved to a Neon.tech Postgres DB via Prisma ORM, as in the story before.

Here is the GitHub repo with the entire code. Below, you will find the link to the example page.

Example page hosted on Vercel -> https://nextjs14-kafka-tracking.vercel.app/

Prisma Schema for new attributes

We added a new table, “custom tracking,” to our schema.prisma so that this table will be created within our Postgres DB Service neon.tech. We need to run the below command to synchronize our schema with the DB

npx prisma db push

New schema.prisma File

We added new attributes like city, region, pathname, useragent, platform, and mobile.

model customtracking {
  id         Int       @id @default(autoincrement())
  country    String?
  city       String?
  region     String?
  url        String?
  pathname   String?
  mobile     Boolean?
  platform   String?
  useragent  String?
  referer    String?
  created_at DateTime?
  sessionId  String?
  userId     String?
  pageViewId String?
  addedOn    DateTime  @default(now())
}

Modifying existing Tracking Hook

We will adapt the tracking hook we created in the last story.

Modified usePageTracking Hook

The main logic sits in the usePageTracking Hook. The complete code is visible below.

//src/lib/hooks/usePageTracking.ts
"use client";
import { useEffect, useState } from "react";
import FingerprintJS from "@fingerprintjs/fingerprintjs";

// Define the type for the page view data
interface PageViewData {
  pageViewID: string;
  sessionID: string;
  userID: string;
  url: string;
  referrer: string;
  timestamp: string;
  country: string;
  city: string;
  region: string;
  pathname: string;
  userAgent: string;
  platform: string;
  isMobileDevice: boolean;
}

// Define the type for the time spent data
interface TimeSpentData {
  sessionID: string;
  userID: string;
  timeSpent: number;
  url: string;
  pathname: string;
  userAgent: string;
  platform: string;
  isMobileDevice: boolean;
  country?: string;
  city?: string;
  region?: 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/customtracking`, {
    method: "POST",
    body: JSON.stringify(message),
  });
}

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 getUserLocation(): Promise<{
  country: string;
  city: string;
  region: string;
}> {
  try {
    const response = await fetch("https://ipapi.co/json/");
    const data = await response.json();
    return {
      country: data.country_name || "Unknown",
      city: data.city || "Unknown",
      region: data.region || "Unknown",
    };
  } catch (error) {
    console.error("Error fetching location:", error);
    return {
      country: "Unknown",
      city: "Unknown",
      region: "Unknown",
    };
  }
}

function getPlatform(): string {
  return navigator.platform || "Unknown";
}

function isMobileDevice(): boolean {
  const userAgent =
    navigator.userAgent ||
    navigator.vendor ||
    (window as Window & { opera?: string }).opera;

  // Check if the user agent matches typical mobile devices
  if (/android/i.test(userAgent || "")) {
    return true;
  }

  if (
    /iPad|iPhone|iPod/.test(userAgent || "") &&
    !(window as Window & { MSStream?: unknown }).MSStream
  ) {
    return true;
  }

  return false;
}

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 { city, region } = await getUserLocation();
  const pathname = window.location.pathname;
  const userAgent = navigator.userAgent;
  const platform = getPlatform();
  const mobileDevice = isMobileDevice();

  const pageViewData: PageViewData = {
    pageViewID: pageViewID,
    sessionID: sessionID,
    userID: userID,
    url: url,
    referrer: referrer,
    timestamp: timestamp,
    country: country,
    city: city,
    region: region,
    pathname: pathname,
    userAgent: userAgent,
    platform: platform,
    isMobileDevice: mobileDevice,
  };

  sendDataToServer(pageViewData);
}

function sendDataToServer(data: PageViewData | TimeSpentData): void {
  const trackdata: any = data;

  const message = {
    country: trackdata.country,
    city: trackdata.city,
    region: trackdata.region,
    pathname: trackdata.pathname,
    url: trackdata.url,
    mobile: trackdata.isMobileDevice,
    platform: trackdata.platform,
    useragent: trackdata.userAgent,
    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 = async () => {
      if (userID) {
        const timeSpent = Date.now() - pageLoadTime;
        const url = window.location.href;
        const pathname = window.location.pathname;
        const userAgent = navigator.userAgent;
        const platform = getPlatform();
        const mobileDevice = isMobileDevice();
        const { country, city, region } = await getUserLocation(); // get location before unload

        const timeSpentData: TimeSpentData = {
          sessionID: getSessionID(),
          userID: userID,
          timeSpent: timeSpent,
          url: url,
          pathname: pathname,
          userAgent: userAgent,
          platform: platform,
          isMobileDevice: mobileDevice,
          country: country,
          city: city,
          region: region,
        };

        sendDataToServer(timeSpentData);
      }
    };

    window.addEventListener("beforeunload", handleBeforeUnload);

    return () => {
      window.removeEventListener("beforeunload", handleBeforeUnload);
    };
  }, [userID]);
}

To extend the script to track the userAgent, pathname, mobile, platform and city, we'll update the usePageTracking hook accordingly.

Explanation of the Additions:

  • The userAgent is retrieved from the navigator.userAgent object. This string contains information about the browser, operating system, and device.

  • The pathname is obtained from window.location.pathname, which gives you the path of the URL relative to the domain.

  • The platform is retrieved using navigator.platform. This provides the operating system or environment the user is on, such as Win32 for Windows, MacIntel for macOS, etc.

  • The isMobileDevice function checks if the user's device is likely to be a mobile device based on the userAgent string. It checks for common mobile device identifiers like android, iPhone, iPad, etc. Returns true if a mobile device is detected and false otherwise.

  • The getUserLocation function now returns an object containing country, city and

    region by using the ipapi.co API. This data is retrieved using the API response and includes fallback values of Unknown if the data isn't available.

You can extend the Hook even more, if you would like to track additional attributes.

New Route for writing data to Neon.tech table “customtracking”

Since we created a new table, “custom tracking,” we will create a new API Route as well so that we can clearly separate the logic.

import prisma from "@/lib/prisma";
import { NextResponse } from "next/server";

export async function POST(req: Request) {
  const {
    country,
    city,
    region,
    pathname,
    url,
    mobile,
    platform,
    useragent,
    referer,
    created_at,
    userId,
    pageViewId,
    sessionId,
  } = await req.json();

  let data = await prisma.customtracking.create({
    data: {
      country: country,
      city: city,
      region: region,
      pathname: pathname,
      url: url,
      mobile: mobile,
      platform: platform,
      useragent: useragent,
      referer: referer,
      created_at: created_at,
      userId: userId,
      pageViewId: pageViewId,
      sessionId: sessionId,
    },
  });

  return NextResponse.json({
    data,
  });
}

That’s it. Now we have quite complete tracking of our users on the page. Please don’t forget to respect GDPR rules and activate tracking only if you are allowed to.

Cloudapp-dev, and before you leave us

Thank you for reading until the end. Before you go:

Please consider clapping and following the writer! 👏 on our Medium Account

Or follow us on twitter -> Cloudapp.dev

Related articles