Skip to main content

Hits without matomo.js

A page that tracks on the server and loads no matomo.js still has things only the browser sees: a click on a button, an outlink or a download, a search that runs in the page, the time a reader keeps a page open. The facade can record all of them (event(), outlink(), download(), siteSearch(), ping()), but something has to carry them from the browser to the server. The hit beacon is that route, so you do not build one of your own.

Turn it on​

// config/matomo-analytics.php
'hit_beacon' => [
'enabled' => true,
],

Put the directive next to your tracking snippet:

@matomoHitBeacon

It defines window.matomoHit(type, data), and every call sends one hit to the route. Pass your CSP nonce as the argument (@matomoHitBeacon($nonce)) if your policy needs one.

What a page can send​

typeFieldsRecorded as
eventcategory, action, optional name and valueMatomo::event()
outlinklink, the absolute URL that was clickedMatomo::outlink()
downloadlink, the absolute URL of the fileMatomo::download()
searchkeyword, optional category and countMatomo::siteSearch()
pingnoneMatomo::ping(), the heartbeat that measures time on the page
matomoHit('event', { category: 'Docs', action: 'copy', name: 'install command' });
matomoHit('outlink', { link: 'https://github.com/pushery/matomo-analytics-for-laravel' });
matomoHit('download', { link: 'https://example.com/files/guide.pdf' });
matomoHit('search', { keyword: 'queue', count: 3 });
setInterval(function () { matomoHit('ping'); }, 15000);

A download sent as download lands in Matomo's Downloads report. Sent as an event, it would leave that report empty.

The helper adds the page's own address as url, and the hit is filed under that page. The route takes a url from this application's origin only; one from anywhere else is dropped, and the hit is filed under the request it arrived on.

What the route accepts​

The route is public by necessity, so every field is treated as untrusted:

  • The request has to come from your own pages. Its Origin header must be this application's origin, and without Origin the browser's Sec-Fetch-Site has to say same-origin. Another site cannot make its visitors send hits to your Matomo; a client that calls the route directly, with neither header, meets the rate limit.
  • Text is bounded. A category, action, name, keyword or search category is at most 255 characters, and a link at most 2,048. A link has to be an absolute http or https URL.
  • Numbers have to be finite, and a result count a whole number of zero or more.
  • event_categories limits what a page may send. Empty, the default, allows every category, the way matomo.js does. Name categories to accept only those.
  • The throttle is 60,1: sixty requests a minute per client address, and per /64 for IPv6. A heartbeat every fifteen seconds uses four of them.

A payload the route will not record is answered with 422, a request from another origin with 403, and the route answers 404 while the feature is off.

Everything recorded goes through the tracking gate, like a hit your code sends: Do-Not-Track, the opt-out cookie, bots and excluded routes all apply, the excluded routes against the page the hit names.

Sessions and CSRF​

The route sits outside every middleware group, because sendBeacon() carries no CSRF token. No session is started there, so tracking.track_authenticated and tracking.except_abilities see a guest. Name ['web'] in hit_beacon.middleware if you need those two rules, and exempt the route from CSRF on your side.