Engineering Blog Article

Recording my own web analytics in Next.js

It is very easy, and you should do it too!

Published OnJune 22, 2024
Updated OnJune 25, 2024
Read time6 mins

I value everyone. Every single person (out of, maybe 10 people) who view my portfolio and/or websites regularly deserve to be recognized, so I have started thinking about web analytics integrations more and more these days. The most important, of course, is analytics for my portfolio. I started using Vercel Analytics for it, and it has served me quite well.

I use Vercel to deploy all of my many, many projects. The developer experience (DX) is truly great. Everything from CI/CD pipelines, domains assignment/management (Vercel also issues SSL certificates really fast), rollbacks, etc work flawlessly. So, I thought, why not extend my usage to Analytics as well? The UI is great and it tracks information pretty well, although I find it hard to believe that the majority of my viewers come from Belgium and not the United States.

However, as a SWE, I always wonder after finding a good tool, what if I just "made it myself". I did it with link-in-bios, having created many themes in the past year (some of them are on

), URL shortener services (check out
Trimr
for that), and currently making my own form creator service. Point being, it is fun to make your own free alternative to these services, and these projects are great learning experiences as well.

So today I set out to make my own analytics script for my latest link-in-bio website (check out the

). It turned out to be much easier than I had anticipated. I found out that there is a library called
ua-parser-js
, which is very well maintained and is fantastic for getting information about the user's browser, OS, etc. But that's only part of a page view or analytics 'event' payload. So, I turned to the Next.js (Next) documentation.

That's arguably the part where it gets the least straightforward. Especially with Next 13+ and the advent of the App Router, almost every development decision comes down to you having to take either of two avenues - the server avenue, or the client avenue. By this, of course, I mean whether the component in consideration (and this includes your layouts and pages) will be a server component (i.e., a React Server Component or RSC) or if it will be a client component. There are intricacies to this distinction, so it's not as simple as just saying one is rendered on the server and one is rendered on the client, but I'll leave that for you to understand in detail by either following Vercel's CEO Guillermo Rauch (very active on Twitter/X, actually) or by reading articles such as

. Point being, whether you use the
"use client"
or
"use server"
(technically,
use-server
is used implicitly, so you mostly only need to explicitly use the client directive when you need to) directive also dictates what kinds of APIs you are able to use without the Next compiler yeeting an error at you.

Regarding my use-case, I was concerned with getting access to as much information about the user and the request for the page as possible. The first question was where to access this - in

layout.tsx
or in
page.tsx
. I went ahead with implementing it in
page.tsx
.

Now, I needed access to headers. As I found out, that is quite straightforward with the

headers()
function that is available to server components. First, you call the function like so,

const request_headers = headers();

And then, you access information from it like so,

const userAgent = request_headers.get("user-agent") || ""; const ip = request_headers.get("x-forwarded-for") || ""; const referrer = request_headers.get("referer") || ""; const language = request_headers.get("accept-language") || "";

Pretty straightforward so far. I asked GPT-4o what other kinds of information would be useful to include in an analytics event (I am an analytics newbie), and it mentioned geo-location information, which I figured was an obvious addition, because I also wanted to see if my link-in-bio was as popular amongst Belgian people as my portfolio. I researched how I may get access to this information, and some resources said that I could use the

navigator
object and API to get co-ordinates and then use a third-party API to parse the latitude and longitude that the navigator spits out and then find the city and country of the user. Sounds cool! So I added all of that to my tracking script as well.

But oh! The build failed. Well, a server component cannot access the navigator object because that is a browser/client-side API. Similarly, attempts to access the user's device resolution also created errors because that requires access to the

window
object which is, you guessed it, only available to the client.

So I was back to the drawing board. I was thinking of changing my

page.tsx
to a client component, moving the analytics recording to a route handler (for security purposes so that my Firebase credentials do not become part of the public domain) and making POST requests to it from the client component, and so on. But doing that would mean no straightforward way to access headers, which are still the most crucial part of the data collection. And even after abstracting the logic in a server route handler, the user will be able to see
POST
requests being made from the client to the server and the data being passed on. While that is not an issue since the code is public anyway, it was not an elegant workaround since the point of Next.js is being able to abstract these processes and leverage new React features to do away with boring and conventional client-server interactions and encapsulate them better.

Then, I figured that since I am getting easy access to the user's IP address, I could just use that to find the user's location, since the Navigator method required using a third-party API as well. The only difference would be that instead of using latitude and longitude to find locations, I would be using IP addresses. A bit more abstract, but still elegant. I found an API which did not require sign ups, auth tokens, or API keys and had a very generous free tier. I was up and running in no time. I removed all references to the

navigator
object, and added a quick
GET
request to the IP lookup API in my tracking script. I was able to get the city and country names out of the fairly succinct JSON response.

Another thing I remembered later on was to write an early return statement that would not record any analytics event (or try to access the IP lookup API) if the GET request for the index route was coming from

localhost:3000
. This was partly because I was getting errors otherwise, and partly because I did not want my Firestore collection to become a hot mess of a million
localhost:3000
entries, which are inevitable with how development goes, and especially with HMR (hot module replacement, i.e., the feature which reloads your dev server every time you make a change).

Implementing Firestore was the easiest part of it. I have a lot of experience using it in the past, with my Gen AI chatbot Xzayvian (currently under a major overhaul),

,
Trimr
, and more. I created project in the Firebase console, added the config to
firebase.config.ts
in the utilities folder of the app, and added all made all environment variables server-only, since doing away with the Navigator API meant that I could make my entire app with server components and do asynchronous data fetching right in my
page.tsx
file.

Check out the final version of the file, which includes the tracking script,

.

This was part 1 of my two posts about it. In the second post, I will discuss how I structured the data in Firestore, and how I created an admin-style dashboard to view the analytics.