Skip to content

Max Niederman

Web analytics from scratch - Part 1: Page Views

Tutorial, Analytics, Web Development1 min read

Google Analytics is used in nearly 70% of websites1 for a good reason. It's a very powerful tool, and more importantly, it's free and easy to use, but there's a downside: Google owns and uses all of your data. "Just use Matamo or Open Web Analytics!" I hear you say, and you're right, that's the easiest solution, but I think we can all agree it's no fun.

In this series we'll be creating a simple website analytics service from scratch. This part will be about creating a basic API to keep track of raw page views, but more statistics will obviously be added later.

1 According to builtwith.com.

Setting up a Web Server

First we'll need to setup a basic web server. I'll be using Fastify, but it shouldn't be too difficult to use Express or another framework.

app.ts
1import fastify from "fastify";
2
3const app = fastify({ logger: true });
4
5// Routes
6app.get("/", async (req, reply) => {
7 hello: "world";
8});
9
10const port = Number(process.env.PORT) || 3000;
11app
12 .listen(port, "0.0.0.0")
13 .then(() => console.log(`Listening on port ${port}`));

Now that we have a web server, we can add a route the client will use when it loads the page:

routes/index.ts
1import { FastifyPluginCallback } from "fastify";
2
3const plugin: FastifyPluginCallback = async (fastify, opts, done) => {
4 fastify.post("/tracker", async (req, reply) => {
5 if (typeof req.body !== "object") return;
6
7 // Record client data
8
9 return { success: true, message: "Recorded analytics data" };
10 });
11
12 done();
13};
14
15export default plugin;

And then register it in app.ts:

app.ts
1import fastify from "fastify";
2import routes from "./routes";
3
4const app = fastify({ logger: true });
5app.register(routes);
6
7const port = Number(process.env.PORT) || 3000;
8app
9 .listen(port, "0.0.0.0")
10 .then(() => console.log(`Listening on port ${port}`));

Storing data

Now that the client can send us information, we'll need a way to store it. Since we won't be storing personal data as per the GDPR and CCPA, we'll essentially just be storing a bunch of counters. I decided Redis would be perfect for this.

With Fastify, we can share a single Redis client using the fastify-redis plugin:

app.ts
1import fastifyRedis from "fastify-redis";
2
3app.register(fastifyRedis, {
4 host: process.env.REDIS_HOST || "127.0.0.1",
5 port: Number(process.env.REDIS_PORT) || 6379,
6});

Then we can keep track of views like this:

routes/index.ts
1fastify.post("/tracker", async (req, reply) => {
2 if (typeof req.body !== "object") return;
3
4 await fastify.redis.incr("views");
5
6 return { success: true, message: "Recorded analytics data" };
7});

If we then call this endpoint a few times we can see the count in our redis console like this:

> GET views
"3"

Pageviews

We'll obviously want to store more data, like views per-page. First, we can add a resource property to the tracker endpoint:

routes/index.ts
1fastify.post("/tracker", async (req, reply) => {
2 if (typeof req.body !== "object") return;
3 const url = new URL(req.body.url);
4
5 await redis.hincrby(`${url.hostname}:resources`, url.pathname, 1);
6
7 return { success: true, message: "Recorded analytics data" };
8});

Stay tuned for the next part of this series, in which we'll be creating a client that can log this information, and endpoints to get analytics data once it's recorded.

© 2020 by Max Niederman. All rights reserved.
Built with this Tech