Basic analytics with Vercel Postgres - Drizzle - Astro
TL;DR
Full code can be found on GitHub, live data can be seen on my website.
Why?
Since Vercel’s analytics pricing is a bit too expensive for my use case (where I hit the limit of 2,500 requests per month), and I didn’t like using Google Analytics (not a big fan of Google), I decided to build my own analytics dashboard. Databases was something I didn’t work with much before directly, so I decided to use an ORM, Drizzle, which is quite lightweight and easy to use.
Setting up the database
I used Vercel Postgres as my database and Astro as my frontend framework.
Vercel Postgres is basically a wrapper around Neon, a serverless SQL server provider.
Setting up the database is pretty straightforward, I just followed the docs and got my database up and running in no time.
Once my database was up and running, the time came to set up the database schema.
The schema is pretty simple, it consists of a table called page_views
with the following columns:
url
: the URL of the viewed pagedate
: the data of the page view
The SQL query to create this table is the following:
Setting up Drizzle
To set up the Drizzle client, I first created a pool
using the @vercel/postgres
createPool function.
Once the pool is created, I can instantiate the Drizzle client with the pool.
In the client I also create an export of the PageViews table, which is a Drizzle table that represents the page_views
table in the database.
Filling up the database
Now that the database is set up, it’s time to fill it up with data. Since I was using Google Analytics before this, I had a lot of data already available. Porting this data to my own database required a bit of work:
- Export the data from Google Analytics by downloading a CSV file
- Create an API route in Astro
- Inside the API route, convert the CSV file to JSON with csv-parser
- Once converted, insert the data into the database using the Drizzle client
The code for the API route is the following:
After a few seconds or minutes (depending on the size of your CSV and amount of views), the database should be filled up with data.
Tracking new views
Now the database is filled up with the historic data, but I’m not tracking new views yet.
To do this, I created a new API route that inserts a new row into the database every time a page is viewed.
This API route is called when the astro:page-load
event is triggered on the document (see Astro docs on View Transitions).
This event is triggered whenever a new page is loaded when you’re using View Transitions.
I use the isbot
package to check if the page is loaded by a crawler/bot to prevent bots from being tracked, and when running the site in development mode, I return an error to prevent the database from being filled up with development views.
The code for the API route is the following:
Creating the dashboard
Now I have my historic and new data coming into my database, it’s time to create the dashboard.
I created a (publicly available) route called /page-views
that shows the dashboard.
This is a .astro
page that uses the Drizzle client to query the database and show the data.
There’s a few things going on here:
- A search input that allows you to search for a specific page
- A dropdown with some predefined time ranges (past day, past week, past month, past year, all time)
- A bar chart that shows the amount of views per page
- Pagination buttons to go to the next or previous page of results, with a maximum of 10 results per page
The code to handle the query to the database looks like this:
The totalViewsQuery
is used to get the total amount of views and unique URLs, and the viewsQuery
is used to get the data for the bar chart.
The total views are used to provide the pagination at the bottom of the page.
I left out the code for the search input and dropdown, but it’s pretty straightforward and can be found on GitHub if you’re interested (see links at bottom of the article).
The bar chart is created using recharts
and the code looks like this:
Getting page views for a specific page
The last thing I wanted to do was to get the page views for a specific page. This is done by creating a new API route that returns the amount of views for a specific page. I decided to execute the request to the API route on the client side, so I don’t block the rendering of my blog pages just to calculate the view count for this blog post. To accomodate this (together with a loading skeleton), I decided to use React (since I’m most familiar with it) combined with Tanstack Query. The code for this looks like this:
The code for this API route is the following:
Conclusion
Now I own my own analytics data, I can choose how the visualize it myself, what to track and what not to track. Of course I’m not storing IP addresses or any other personal data, so I’m not violating any privacy laws. I’m also not tracking any data that I don’t need, so I’m not wasting any resources.
The only downside I encountered so far with the Vercel Postgres database, is the cold starts. Since the database is serverless, it needs to start up when it’s not used for a while, so the first request after a while takes a bit longer (up to 1.5 seconds).
I’m pretty happy with the result, and I hope you learned something from this post! This gave me the opportunity to learn more about SQL and Drizzle, which is another thing I can check off my list.
There’s still room for improvement on the UI side, and the tracking data could also be improved by tracking things like the referrer etc.
Full code can be found on GitHub, live data can be seen on my website.