9am / 9am.github.io

9am Blog 🕘 ㍡
https://9am.github.io/
MIT License
3 stars 0 forks source link

Dynamic Hits Counter with Deno #15

Open 9am opened 1 year ago

9am commented 1 year ago
How to build a dynamic image API that works as a hits counter powered by Deno Deploy and Deno KV
metronome hits
9am commented 1 year ago

Deno has been there for a while, it supports pretty much everything a Node.js developer needs: testing, linting, formatting, TypeScript, and even JSX(without installing a ton of packages). It's backward compatible with Node.js and npm. So if you work heavily on Node.js, it is worth a try to switch to Deno.

But what I love most is Deno Deploy. It's super easy to build a serverless application, and the deployment is lightning-fast. It's a perfect platform to try some ideas or an MVP. I've been invited to try Deno KV Beta in Deno Deploy recently (It's in open beta now), so I got this idea to build a generated image API that works as a hits counter.

I'll not bother to show you how to set up the project and Deno Deploy, cuz it's pretty easy thanks to the thorough Documatation and the well-tuned UX. In this article, I'll talk about some trouble I encountered.

deno-logo

The hits: n in the upper comment is what I come up with, check out the project page.


𐄡

## The Idea For the service, from the user side, it's pretty straightforward: > Embed a `` into the web pages, and it'll show the hits number as a `` will do. ### Wait... why do we need a `` as a counter? > There are some situations in which we want to count and show the hits, but we can not include any javascript to send a request, like in Github README, issue, etc... And I'm using Github issue to serve my blog, well, maybe it's just me, but it's nice way to show me the number of reads. To achieve that, we'll expose an HTTP API that returns an image with the hits in it. And also use the `GET` request as an increasing signal of the counter. The counter needs to be recorded under different keys of `referer`, which are the web pages that embed the `` so that we count hits for different resources. > ![idea](https://github.com/9am/9am.github.io/assets/1435457/f3101064-0e9d-4354-92cb-715988d3cb3c)

𐄡

## The Troubles ### 1. Can't get the right `Referer` We want to save the hits number in the key of different `Referer` so that we can give the right number when the image pops out. > | key(referer) | value | > |:------------:|:-----:| > | `https://a.com/x.html` | 3 | > | `https://b.com` | 4 | > | `https://c.com?query=search` | 5 | > | ... | ... | Lucky for us, the information can be found in `HTTP headers`. Origin, path, and query string will be included. **The problem is that which one of them are included is depended on the [referrer policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#directives). Every page can have its own `referer policy` settings in different ways**. > For example, if a page `https://a.com/x.html` has a `meta` like this: ``. The server will get the referer as `https://a.com`. Or even worse, with a setting of `Referrer-Policy: no-referrer`, there'll be no referer value at all. #### The solution: 1. For places we can embed the `` like this, we add a `referrerpolicy` to override the default setting. `no-referrer-when-downgrade` means the referer will be `${origin}${path}${query}` if the page protocol is not `downgrade` to HTTP. `` 2. For markdown, we have no choice but to add an explicit param `referer` for the API. `![alt](https://hits/api?referer={value})`

𐄡

### 2. CDN or Proxy server cache image request For a normal page, the API works fine. But in the GitHub markdown, the counter just doesn't update. Because GitHub uses a [`camo` proxy server](https://github.com/atmos/camo) for the image embedded in the markdown content. > A `![](imglink)` turns into ``. So the next time we open the page with our counter, the proxy server returns the cached image, and **the counter failed to increase cuz no request was received at all**. #### The solution: A `Cache-Control` response header was my first thought, but adding a `Cache-Control: no-cache` didn't work. I started to look for solutions from other projects that may encounter the same issue. Thanks to [github-readme-stats](https://github.com/anuraghazra/github-readme-stats), I finally took their way to bust the cache: > `Cache-Control: max-age=1, s-maxage=1` > The `1` second in there works like a `throttle` so that we can take advantage of the cache.

𐄡

### 3. The design of `KEYs` I have plans to save more information about the hits, like date, geo, browser, device, etc. So I can think of 2 ways of organizing the data. #### Option 1: | key(referer) | value | |:------------:|:-----:| | `a.com` | `{ total: 3, 2023-09-01: 3, 2023-09-02: 1, geo-cn: 4, geo-us: 1 ...}` | | ... | ... | ##### Pros: 1. Keys are easy to understand. 2. No need to query DB multiple times for each type of data. #### Option 2: | key(referer) | key(total) | key(date) | key(geo) | value | |:------------:|:----------:|:---------:|:--------:|:-----:| | `a.com` | `total` | - | - | 5 | `a.com` | - | `2023-09-01` | - | 4 | | `a.com` | - | `2023-09-02` | - | 1 | | `a.com` | - | - | `us` | 3 | | `a.com` | - | - | `cn` | 2 | | ... | ... | ... | ... | ... | ##### Pros: 1. A native `sum` mutation can be used to increase the counter. 2. Take advantage of the native `selector` to filter and aggregate the result. 3. Only a small amount of selected data will be returned to the server side, no need to filter the result again. e.g. find hits for the last 7 days: `db.list({ prefix: [referer, 'date'], start: '2023-09-13' })` **I took the 2nd solution for now until I become more experienced with KV DBs.** > hits > > A trend for the last 7 days

𐄡

### 4. The access right control There is a request limitation on Deno Deploy and Deno KV, and I'm not ready to provide the API as a public service. So to prevent unknown request or even attack, an `ALLOW_REFERER` env was there to filter the input. This can not prevent a vicious attack, but enough for an MVP. Maybe someday in the future, I'll make it a free service. > [Deno Deploy & KV Price](https://deno.com/deploy/pricing)

𐄡

### 5. An eye-catching renderer Well, it's not a trouble for me. I've been exploring a bitmap font display recently. It's fun to apply it here. I'll explain the details in another article. > ![bitmap-font](https://github.com/9am/9am.github.io/assets/1435457/9dba1ea3-0db6-487e-966c-1cef7647068b)

𐄡

## Closing thoughts For the last few months, the fear of uncertainty has overwhelmed me. New things keep coming into my life, some are good, some are not. It's hard to find time to explore new things and write. But strangely, I found the peace coming back the moment I started writing. I'm thankful for the things I love and the people who love me. At the end of the day, that's what keeps me going!
--- > ## @9am 🕘 > * Read more [articles](https://9am.github.io) at [9am.github.io](https://9am.github.io) > * Find other [things](https://www.npmjs.com/search?q=%409am) I built on [GitHub](https://github.com/9am) and [NPM](https://www.npmjs.com/~9am) > * Contact me via [email](mailto:tech.9am@gmail.com) > * [![Creative Commons License](https://i.creativecommons.org/l/by-nc-nd/4.0/80x15.png)](http://creativecommons.org/licenses/by-nc-nd/4.0/)