Skip to main content

Client-side tracking

The client-side tracker sees what the server cannot: how long someone stayed, what they clicked, their screen and browser, and the page-performance timings Matomo's own report is built from. One Blade directive renders it.

<head>
{{-- ... --}}
@matomoScript
</head>

It records a page view on every page it is on, so leave the server-side page view middleware, matomo.track, off those pages: the two together count each view twice.

That is the whole integration. The directive renders an empty string unless the package is enabled, js.enabled is true and the instance is configured — so it is safe to drop into a layout that also serves local development and CI, with no @if around it. It renders nothing either on a page that tracking.environments or tracking.except_routes excludes; which other gate rules reach matomo.js is on the gate's page.

What it emits​

By default: a dns-prefetch link for the Matomo origin, the _paq bootstrap, the asynchronous loader for matomo.js, and a <noscript> fallback pixel so a visitor without JavaScript still produces a page view.

The order of the _paq commands is deliberate, because Matomo applies some of them only if they arrive before the page view:

  1. disableCookies — when privacy.cookieless is true (the default).
  2. The consent call for your privacy.consent posture, if any.
  3. setDoNotTrack — when privacy.honor_dnt is true (the default).
  4. setCustomRequestProcessing, which redacts the names in privacy.redact.query_params and the token of a password-reset link from every request — when redaction is on (the default).
  5. Any statically configured custom dimensions.
  6. disablePerformanceTracking — only when js.performance is false.
  7. trackPageView.
  8. Content-impression scanning, if content tracking is on.
  9. enableLinkTracking — when js.enable_link_tracking is true (the default), then the heartbeat timer — when js.heartbeat is above zero.
  10. The tracker URL and site id, then the loader. It inserts matomo.js at once, or, on a page the browser prerenders, when the reader opens it, so a prerender nobody opens is not counted. See Prefetched and prerendered pages.
  11. The soft-navigation listeners, if spa.enabled.

Every value the package embeds in that script is JSON-encoded as a JavaScript string literal, or HTML-escaped when it lands in an attribute.

The settings​

'js' => [
'enabled' => env('MATOMO_JS_ENABLED', true),
'host' => env('MATOMO_JS_HOST'), // separate asset host, e.g. a Matomo Cloud CDN
'tag_manager' => null, // full MTM container URL
'enable_link_tracking' => true,
'performance' => true,
'custom_dimensions' => [],
'content_tracking' => false,
'heartbeat' => 15, // enableHeartBeatTimer seconds; 0 to disable
'noscript' => true,
'dns_prefetch' => true,
],

enabled turns the client tracker off as a whole, and it reads from the environment so that a deployment whose config file is template-managed can still say so — MATOMO_JS_ENABLED=false. That matters for the failure mode a config edit has there: the next template sync reverts it silently, so the switch reads "off" until one day it does not, and nothing announces the change.

It governs both doors that put tracking into a page:

DirectiveGoverned byNotes
@matomoScriptjs.enabledthe tracker itself
@matomoNoscriptjs.enableda tracking pixel that works with JavaScript off
@matomoWebVitalsweb_vitals.enabledits own switch, and it ships off
@matomoOptOut—not tracking; it is the opt-out control and keeps working

The table is here because the list is easy to get wrong from memory: a consuming project's guard named two of the four, and the two it missed were the <noscript> pixel and the Web Vitals reporter — both real doors. If you are auditing which directives a codebase uses, read the registered list rather than writing it out.

heartbeat is what makes time-on-page meaningful. Without it Matomo can only measure the interval between two page views, so the last page of every visit counts as zero seconds. Fifteen seconds is a reasonable default; 0 disables it.

enable_link_tracking records outbound links and file downloads automatically. It is pushed after the page view, as Matomo requires.

noscript renders a tracking pixel for visitors without JavaScript, for a request the tracking gate lets through. The browser requests the pixel from Matomo directly, so consent under full, an opt-out and an excluded route are checked when the page is rendered. The pixel names the page itself in url, redacted like a server-side hit, and sends no Referer: without url, Matomo takes the page from that header, and the browser would send the full address with its query.

One caveat if you validate your markup, and it is the only reason the pixel can be split out: inside <head>, the HTML specification allows a <noscript> element to contain only <link>, <style> and <meta>. An <img> there is a parse error. Browsers recover from it — the parser's "after head" handling pushes the following head elements back where they belong — so the practical effect is a validator complaint, not a broken page, and only for visitors with JavaScript switched off.

If you would rather have neither, place the pixel in the <body>, where an <img> is legal. It is a two-step change:

// config/matomo-analytics.php
'js' => [
'noscript' => false, // stop @matomoScript emitting the pixel in <head>
],
<body>
{{-- ...your page... --}}
@matomoNoscript
</body>

@matomoNoscript renders exactly the same pixel and, like every other part, renders nothing at all when tracking is inactive. Leaving noscript at true and also placing the directive would emit it twice, so pick one.

dns_prefetch emits a resolver hint for the Matomo origin — and a second one for the asset host when js.host points somewhere else.

host loads matomo.js from a different origin (typically a Matomo Cloud CDN) while tracking stays on your own subdomain. See Matomo Cloud.

Content-Security-Policy nonce​

Pass your nonce and it is applied to every script tag the directive renders:

@matomoScript($cspNonce)

Tag Manager instead of the tracker​

Set js.tag_manager to a container URL and the directive renders the Matomo Tag Manager bootstrap (_mtm) instead of the direct tracker:

'js' => [
'tag_manager' => 'https://analytics.example.com/js/container_AbCdEf12.js',
],

Everything the container does is configured in Matomo's Tag Manager UI rather than here, and soft-navigation tracking becomes Tag Manager's job — it has its own history-change trigger. The container is loaded the way matomo.js is: at once, or, on a prerendered page, when the reader opens it.

Four of the js settings still apply, because they are not _paq pushes and have nothing to bootstrap against:

SettingUnder Tag Manager
js.enabledStill the master switch. MATOMO_JS_ENABLED=false renders nothing at all, container included.
js.noscriptStill emits the <noscript> pixel. This is deliberate: a visitor without JavaScript never loads the container either, so the pixel is the only thing that counts them. It hits matomo.php directly rather than going through the container — set 'noscript' => false if you would rather have no second path.
js.dns_prefetchStill emits the <link rel="dns-prefetch"> hints.
js.hostStill chooses the host those hints name. It no longer selects where a script is loaded from — the container URL is js.tag_manager in full.

The remaining six — enable_link_tracking, performance, custom_dimensions, content_tracking, heartbeat and the tracker half of js.host — are _paq pushes, and under Tag Manager there is no _paq bootstrap to push them onto. Configure those behaviors in the container instead.

Server-side tracking is unaffected either way.

The opt-out control​

@matomoOptOut

This renders Matomo's JavaScript opt-out: an empty <div> and the script from your Matomo instance that fills it with the opt-out text and checkbox. It renders nothing when the instance is not configured. Under a strict content security policy, pass the nonce, as for the other directives: @matomoOptOut($nonce).

The opt-out is a first-party cookie, mtm_consent_removed, which the script writes through the tracker on the page. matomo.js writes it with cookies disabled as well and stops tracking once it is there. Matomo's older iframe opt-out set its cookie on the Matomo domain instead, which a browser that blocks third-party cookies never keeps when Matomo runs on another site.

The tracking gate reads the same cookie, so an opt-out through this control stops server-side tracking as well: page views, beacons and the hits your code sends. The package exempts the cookie from EncryptCookies, because JavaScript writes it in plain text. The first-party opt-out cookie described in consent and redaction stays the way to opt a visitor out from your own server code.

Also on the client side​