Matomo vs uBlock Origin

~ 3 min
blocker
matomo
stats
tracking

After Ackee got an update and stopped working I wanted to search for an alternative to get some stats on my statically rendered site. As no server is used, I need some 3rd party service.

I don’t want to spy on people, nor set cookies and annoy people with consent banners if they only want to read a damn blog post. The goal is just get a feel for the traffic on the site.
This is important to mention as the next steps could sound a bit nefarious otherwise.
Data collected on this site is 100% anonymous and GDPR compliant.

Since Matomo is the de facto way to go, I spun up the Matomo server with my trusted docker-traefik setup and was up and running in no time.
( I’ll share the config files if anyone is interested at the bottom. )

Then I quickly copied the JS tracker code in my main html template and thought that was it. Wrong.

The problem defaults.

So turns out that Matomo, being widely used is of course included in many Ad-Blocker lists and therefore my stats did not work. Lets see why:
Basically all ad blockers work with lists. Those lists include pattern that if matched will be filtered out. Let’s take a look at the default Matomo tracking code:

<script type="text/javascript">
  var _paq = (window._paq = window._paq || [])
  /* tracker methods like "setCustomDimension" should be called before "trackPageView" */
  _paq.push(['trackPageView'])
  _paq.push(['enableLinkTracking'])
  ;(function () {
    var u = '//stats.nicco.io/'
    _paq.push(['setTrackerUrl', u + 'matomo.php'])
    _paq.push(['setSiteId', '1'])
    var d = document,
      g = d.createElement('script'),
      s = d.getElementsByTagName('script')[0]
    g.type = 'text/javascript'
    g.async = true
    g.src = u + 'matomo.js'
    s.parentNode.insertBefore(g, s)
  })()
</script>

We can see that my stats server is stats.nicco.io. And we also can see that the tracking script is loaded by matomo.js, which then sends the details to matomo.php. Well that is of course incredibly easy to block, and it is as you can see below:

Part of the EasyList Filter

Part of the EasyList Filter

That won’t work, and since most of the people that visit this site are probably developers which probably have some kind of ad blocker installed.

Solution time

So after a short Ecosia search I landed on the blog of Christian Mochow that wrote a blog post on this issue. I got the solution from his article.

Luckily Apache has the famous Rewrite module, which will solve all our problems. I bet most of you already know where this is headed.

We can create a .htaccess file in the root of our Matomo installation folder, to cloak our requests.

# .htaccess
RewriteEngine On
RewriteRule ^unicorn matomo.js
RewriteRule ^rainbow matomo.php

Now if we request https://stats.nicco.io/unicorn we actually get the response for https://stats.nicco.io/matomo.js and the same for rainbow and matomo.php.

// Replace in the client

_paq.push(['setTrackerUrl', u + 'matomo.php']) // Before
_paq.push(['setTrackerUrl', u + 'rainbow']) // After

g.src = u + 'matomo.js' // Before
g.src = u + 'unicorn' // After

Awesome!

I had to create a minuscule Dockerfile as the Rewrite module is not enabled per default in the standard Matomo docker image.

# Dockerfile
FROM matomo
RUN a2enmod rewrite

Responsible Usage

Now as you can see it’s incredibly easy to mask tracking stuff, and I bet there are a lot of people doing this in the wild. It is important to respect the privacy of your users and you should never store more data than you need and in the best case don’t store data at all.

Anonymize as much as possible! Matomo makes this easy. You can effortlessly delete 2 bytes of each ip address (half of the info), enforce strict no cookie tracking and automatically delete data after x days. Please do ❤️

Config Files

The Dockerfile and the .htaccess files are shown above.

# docker-compose.yml
version: '3.7'

networks:
  traefik:
    external: true

services:
  db:
    image: mariadb
    command: --max-allowed-packet=64MB
    restart: unless-stopped
    volumes:
      - ./data/db:/var/lib/mysql
    env_file: .env

  app:
    build: .
    restart: unless-stopped
    links:
      - db
    volumes:
      - ./data/matomo:/var/www/html
      - ./.htaccess:/var/www/html/.htaccess
    env_file: .env
    labels:
      - traefik.enable=true
      - traefik.docker.network=traefik
      - traefik.port=80
      - traefik.backend=matomo
      - 'traefik.frontend.rule=Host:stats.nicco.io;'
    networks:
      - traefik
      - default
# .env
MYSQL_DATABASE=matomo
MYSQL_USER=matomo
MYSQL_PASSWORD=<random bytes>
MYSQL_RANDOM_ROOT_PASSWORD=yes

MATOMO_DATABASE_HOST=db
MATOMO_DATABASE_ADAPTER=mysql

MATOMO_DATABASE_DBNAME=matomo
MATOMO_DATABASE_USERNAME=matomo
MATOMO_DATABASE_PASSWORD=<random bytes>

See the code for this website.