Server-side tracking
Server-side tracking speaks Matomo's Tracking API directly from PHP. It sees things the JavaScript tracker never will — a webhook, a queued job, an API-only client, a visitor with JavaScript disabled — and it cannot be blocked by an ad blocker.
Hits are gathered during the request and delivered out of band, so a call to the
facade costs you the time it takes to build an array. Nothing is sent on the request
path unless you explicitly choose sync mode.
The facade
use MatomoAnalytics\Facades\Matomo;
Matomo::pageView('Pricing');
Matomo::event('Subscription', 'created', 'pro', 49.00);
Matomo::siteSearch('invoices', category: 'docs', count: 7);
Matomo::goal(3, revenue: 49.00);
Matomo::download('https://example.com/whitepaper.pdf');
Matomo::outlink('https://partner.example.com');
Matomo::ping();
Every tracking method returns the tracker, so calls chain — flush() is the exception
and returns nothing, being the end of a chain rather than a link in one. Each tracking
call builds a typed hit object and hands it to the transmission layer; nothing about the
call site changes when you later switch from queue to batch.
The remaining hit types have their own pages:
ecommerce, site search, and
content impressions and interactions.
Matomo::track() takes a hit object directly, which is how you attach
custom dimensions or raw Tracking-API parameters.
For the complete method list, see the
Tracker contract.
Automatic page views
Attach the middleware and page views are recorded with no per-route code:
// routes/web.php
Route::middleware('matomo.track')->group(function () {
// ...
});
Or register it on the whole web group by setting middleware.auto to true.
What it records is deliberately narrow, because a page view should mean a person looked at a page:
| Setting | Default | Effect |
|---|---|---|
middleware.only_get | true | Only GET requests are tracked. |
middleware.only_successful | true | Only delivered pages are tracked: 2xx and 304. A redirect is not one -- the page it lands on is tracked on its own request. |
middleware.skip_livewire | true | Livewire update requests are skipped — they are not navigations. |
middleware.strip_query | false | Drop the query string from the tracked URL entirely. |
middleware.performance | false | Stamp the server generation time onto the page view. |
The page title is resolved from the response, so a tracked page view carries the
same title a visitor saw in their browser tab. It is looked for in the first 64 KB —
the spec puts <title> in <head> — and a response without one falls back to the
route name and then to the path.
The hit is queued after the response has been sent, from the middleware's
terminate(). The gate, the payload build and the buffer write are all off the
critical path, and in sync mode so is the request to Matomo — which is the mode
where it mattered: 24.08ms of request time against 0.096ms in queue, measured
against an instance answering in 20ms. The one number that has to be read earlier,
the server generation time behind middleware.performance, is taken before the
response leaves.
middleware.strip_query is the blunt way to keep query strings out of your
analytics. The precise way is URL redaction,
which is on by default and strips secrets and PII while keeping the parameters that
carry meaning.
middleware.performance is covered in page performance.
The middleware still runs through the tracking gate, so bots, opted-out visitors and excluded routes are filtered before a hit is built.
Site search and AI chatbot middleware
Two more middleware aliases exist, each documented on its own page:
matomo.search— track a search route's query automatically. See site search.matomo.chatbots— record AI-assistant page fetches as bot telemetry. See AI assistants.
How a visitor is identified
By default a visitor gets a cookieless identifier: a salted hash, derived per request and rotated daily, that yields the 16-character hex visitor id Matomo expects. No cookie is set, and nothing survives the rotation window, which is what makes the default posture consent-free.
'visitor' => [
'rotate' => 'daily', // daily|weekly|never
'user_id' => null, // 'auth' to attach the authenticated user id, or null
],
rotate trades identification accuracy against retention. daily means a returning
visitor tomorrow is a new visitor; weekly widens the window; never keeps one
stable identifier for a browser. Pick the shortest window that still answers your
questions.
user_id => 'auth' attaches the authenticated user's id as the Matomo User ID, so
a logged-in person's sessions join up across devices regardless of the rotation
window. It is off by default: linking every hit to a known person is a deliberate
step with a lawful basis behind it, not a starting point.
Which IP is used
With a token configured, the package forwards the client IP as cip. It comes from
ip_header when you set one, and from Laravel's request IP otherwise. The gate's
except_ips check resolves the address the same way, so an exclusion list and the
IP that reaches Matomo always agree.
A forwarding header is a chain — X-Forwarded-For carries client, proxy1, proxy2 — and the package reads the client from the front of it. A port, a bracket
pair around an IPv6 address and a zone id are stripped for the same reason: each is
written next to an address without being part of one. If the header holds no address
at all, its value is passed on untouched rather than cut into something that looks
like one.
'anonymize_ip' => true,
'ip_header' => env('MATOMO_IP_HEADER'), // e.g. CF-Connecting-IP behind Cloudflare
ip_headeris trusted without verification. Set it only when your origin is reachable exclusively through that proxy. If the origin can be reached directly, a client can send the header themselves and both poisoncipand slip pastexcept_ips. The safer configuration is to leave itnulland configure Laravel's ownTrustProxiesmiddleware, which the package's IP resolution then follows.
anonymize_ip truncates the address before it is sent, and is on by default.
Matomo can also anonymize server-side; doing it here means the full address never
leaves your application. Turn it off only if you have a basis for storing complete
addresses.