Max Schmitt

October 26 2023

How Plausible Analytics Keeps a Tiny (< 1KB) Tracking Script

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 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 features.

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 is sprinkled with 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

Now 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 more 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.