Skip to content

Max Niederman

Web analytics from scratch - Part 2: The Client

Tutorial, Analytics, Web Development1 min read

This is Part 2 of this series. You can read the first part here

Tracker Client

Now that we have a server which can keep track of our analytics data, we need to add a client which will tell the server the page has been loaded. This is extremely simple since we only have to send a single request with two JSON properties when the page loads:

1var analytics = async (apiBase) => {
2 await fetch(`${apiBase}/tracker`, {
3 method: "POST",
4 headers: {
5 "Content-Type": "application/json",
6 },
7 body: JSON.stringify({ url: location.href }),
8 });
9};

Then we can just load and call it like this:

1<script src="analytics.js"></script>
2<script>
3 analytics("http://yourserver.com");
4</script>

And that's really all it takes to implement our super-simple analytics on the client side. This has the added benefit that the bundle size of the client is negligible (<250B unminified).

Getting Analytics Data

We have a way to collect and store analytics data, but now we need a way to get it.

We can already look at the data using redis-cli like this:

redis-cli
> HGETALL <YOUR_HOST>:resources
1) "/"
2) "42"
3) "/index.html"
4) "7"

However, this isn't a great solution for quickly looking at analytics data. We want some way to present this data to the user nicely. We can do this by adding an endpoint to our API to get this data:

routes/index.ts
1fastify.get("/data", async (req, reply) => {
2 const resources = await fastify.redis.hgetall(`${req.query?.host}:resource`);
3 return { resources };
4});

Now if we call this by making an HTTP request with a host query parameter and get back some JSON like this:

1{
2 "/": "42",
3 "/index.html": "7"
4}

Now you might notice that the numbers are, in fact, strings. This is because Redis stores everything as a string, so we'll need to cast it to a number:

routes/index.ts
1// Helper function to cast Object values
2const castValues = <T>(
3 object: Record<string, unknown>,
4 castFn: (value: unknown) => T
5): Record<string, T> =>
6 Object.fromEntries(
7 Object.entries(object).map(([key, val]) => [key, castFn(val)])
8 );
9
10fastify.get("/data", async (req, reply) => {
11 const resources = await fastify.redis.hgetall(`${req.query?.host}:resource`);
12
13 // Cast values to numbers
14 return { resources: castValues(resources, Number) };
15});

Now it'll reply this:

1{
2 "/": 42,
3 "/index.html": 7
4}

In the next part of this series, we'll add more statistics and work on a visual dashboard.

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