Today I came across a little goodie that I wanted to share with you.
I've been using Plausible Analytics for a few projects and clients as a privacy-friendly alternative to Google Analytics.
Plausible happens to also have a tiny tracking script (< 1KB) and they have a neat way of keeping their script small while also shipping a lot of functionality.
1. File Extensions to Opt-In to Features
Plausible's base tracking script is hosted at https://plausible.io/js/script.js
. You include it through a simple <script>
tag on your website.
With the base tracking script, Plausible will take care of tracking your pageviews.
If you want Plausible to also track clicks on outbound links, you add the .outbound-links
extension to the script URL:
script.outbound-links.js
You can combine this with their other script extensions, such as tracking file downloads:
script.outbound-links.file-downloads.js
I was curious to see how they implemented this and was delighted by their simple and elegant solution:
2. How Plausible Implements Script Extensions
Inside the Plausible repository, there is a folder for the tracking script.
The tracking script looks like a normal JavaScript file but it's actually a Handlebars template that contains several if-statements.
2.1. Handlebars Templates
For example, here is some code that tracks outbound links:
customEvents.js
{{#if outbound_links}}if (isOutboundLink(link)) {return sendLinkClickEvent(event, link, { name: 'Outbound Link: Click', props: { url: link.href } })}{{/if}}
The code above lives in a file called customEvents.js
and is included in the main tracking script with the following Handlebars code:
plausible.js
{{#if (any outbound_links file_downloads tagged_events)}}{{> customEvents}}{{/if}}
I love this! No need for CommonJS or ES Modules for such a tiny script.
2.2 Building 1024 Different Combinations
I mentioned earlier that you can combine the script extensions. You might request script.outbound-links.js
or script.outbound-links.file-downloads.js
.
This means that Plausible has to build 1024 different combinations of the script.
They achieve this with a custom build script that is only about 30 lines of code.
Here is a small excerpt where you can see how they build each possible combination of script extensions:
compile.js
const base_variants = ['hash','outbound-links','exclusions','compat','local','manual','file-downloads','pageview-props','tagged-events','revenue',]const variants = [...g.clone.powerSet(base_variants)].filter((a) => a.length > 0).map((a) => a.sort())variants.map((variant) => {const options = variant.map((variant) => variant.replace('-', '_')).reduce((acc, curr) => ((acc[curr] = true), acc), {})compilefile(relPath('src/plausible.js'), relPath(`../priv/tracker/js/plausible.${variant.join('.')}.js`), options)})
If you take a look at the build output, you can see that all 1024 scripts physically exist on the disk:
- plausible.pageview-props.js
- plausible.pageview-props.revenue.js
- plausible.pageview-props.revenue.tagged-events.js
- plausible.pageview-props.tagged-events.js
- plausible.revenue.js
- plausible.revenue.tagged-events.js
- plausible.tagged-events.js
- Etc.
2.3. Allowing Script Extensions in Any Order
Plausible allows you to request the script extensions in any order.
For example, both of these links work and have the same content:
But: Only plausible.file-downloads.outbound-links.js
physically exists on the
disk.
And it makes sense:
All possible combinations with script extensions in any order can't feasibly be built. For 10 extensions, they would have to build 3,628,800 different script files.
So they simply intercept the request for the script on the server and reorder the extensions before serving the script. You can see this in the server code.
Final Thoughts
I had a good time taking this small excursion into Plausible's codebase. It was really nice and refreshing to see such a simple, practical solution to a problem.
It reminds me of the days when there weren't 10 compilation steps between writing code and shipping it to the browser.
I wonder if they miss the tooling that they have to give up for this simplicity (you can't run ESLint or TypeScript on a Handlebars template) but I imagine that their script is (still) small enough that they don't need it.