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

