justin991q / matomo-server-side-tracking
Matomo server-side tracking module for Laravel
Package info
gitlab.com/Justin991q/laravel-matomo-server-side-tracking
pkg:composer/justin991q/matomo-server-side-tracking
Requires
- php: ^8.3
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- illuminate/view: ^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.5|^11.0|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A Laravel module for server-side Matomo tracking and client-side JavaScript injection.
Requirements
- PHP 8.3+
- Laravel 11, 12 or 13
Installation
The package isn't published on Packagist, add one of the following repositories to your composer.json first.
GitLab package registry
Tagged releases are published to the GitLab Composer package registry, which is accessed at the group level:
"repositories": [
{
"type": "composer",
"url": "https://gitlab.com/api/v4/group/<group_id>/-/packages/composer/packages.json"
}
]
The registry requires authentication, use a personal access token (read_api scope) or a deploy token (read_package_registry scope):
composer config gitlab-token.gitlab.com <token>
Git repository
"repositories": [
{
"type": "vcs",
"url": "https://gitlab.com/Justin991q/laravel-matomo-server-side-tracking.git"
}
]
Then require it:
composer require justin991q/matomo-server-side-tracking
The MatomoServiceProvider and the Matomo facade alias are registered automatically via package discovery.
Configuration
Add the following variables to your .env file:
MATOMO_ENABLED=true
MATOMO_URL=https://stats.example.com
MATOMO_SITE_ID=1
MATOMO_TOKEN_AUTH=your-token-here
Core
| Variable | Required | Default | Description |
|---|---|---|---|
MATOMO_ENABLED | No | false | Enable or disable all tracking |
MATOMO_URL | Yes | — | Base URL of your Matomo instance |
MATOMO_SITE_ID | No | 1 | Matomo site ID |
MATOMO_TOKEN_AUTH | Yes* | — | Auth token — required for server-side IP forwarding (cip) |
MATOMO_TIMEOUT | No | 5 | HTTP request timeout in seconds |
MATOMO_JS_TRACK_PAGE_VIEW | No | true | Whether the JS snippet calls trackPageView |
MATOMO_TOKEN_AUTHis required for the server-side tracker to forward the visitor's real IP. Without it, all requests will appear to originate from your server.
When
MATOMO_ENABLED=trueandMATOMO_URLis empty, tracking throws aMatomoException.
Queue
Tracking requests are dispatched to a queue by default so they never block the HTTP response.
| Variable | Required | Default | Description |
|---|---|---|---|
MATOMO_QUEUE | No | true | Dispatch tracking requests via the queue |
MATOMO_QUEUE_NAME | No | matomo | Queue name for tracking jobs |
MATOMO_QUEUE_CONNECTION | No | QUEUE_CONNECTION | Queue connection (defaults to the app's default connection) |
To process the queue, run a worker targeting the matomo queue:
php artisan queue:work --queue=matomo
To send tracking requests synchronously instead (blocking, no worker required):
MATOMO_QUEUE=false
When the queue is enabled, all tracking methods always return true immediately. Tracking is best-effort: the SendTrackingRequest job catches all exceptions, so failed requests are dropped and never retried or marked as failed.
Request headers (user agent, language, referrer) can contain malformed UTF-8, which can't be JSON encoded into the queue payload. Invalid byte sequences in all string parameters are replaced with ? before the request is queued or sent.
Exclusions
| Variable | Required | Default | Description |
|---|---|---|---|
MATOMO_EXCLUDED_IPS | No | — | Comma-separated IP patterns to exclude (supports * wildcard) |
MATOMO_EXCLUDED_PAGES | No | _tt*,telescope/*,admin/* | Comma-separated path patterns to exclude (supports * wildcard) |
MATOMO_FORCE_SERVER_SIDE_ROUTES | No | — | Comma-separated route name patterns that always track server-side, even when the JS cookie is present (supports * wildcard) |
MATOMO_EXCLUDED_IPS=192.168.1.*,10.0.0.1
MATOMO_EXCLUDED_PAGES=_tt*,telescope/*,admin/*,health
MATOMO_FORCE_SERVER_SIDE_ROUTES=files.*,downloads.show
Avoiding Duplicate Page Views
When both the TrackPageView middleware and the JS snippet are active, page views are recorded twice — once server-side and once by the browser.
Option A — Server-side owns all page view tracking (recommended):
Set MATOMO_JS_TRACK_PAGE_VIEW=false. The JS snippet still loads and calls enableLinkTracking, so outbound link clicks and downloads continue to be tracked. Only trackPageView is removed from the JS snippet.
MATOMO_JS_TRACK_PAGE_VIEW=false
Option B — JS owns page view tracking, server-side is fallback:
Leave MATOMO_JS_TRACK_PAGE_VIEW=true (the default). The middleware checks for the Matomo cookie (_pk_id.*) set by the JS tracker. If the cookie is present, the browser has already tracked the page view and the middleware skips. If the cookie is absent (ad-blocker, first visit before JS runs), the middleware tracks as a fallback.
This is read directly from $_COOKIE so it works regardless of Laravel's cookie encryption configuration.
Usage
Client-Side JavaScript (Blade view)
Include the tracking snippet in your layout. It renders nothing when MATOMO_ENABLED is false:
@include('matomo::tracking')
Pass a CSP nonce to add it to the <script> tag:
@include('matomo::tracking', ['csp' => app('csp-nonce')])
The snippet sets the authenticated user's email as the Matomo user ID when a session is active. The server-side tracker does the same (uid) for trackPageView and trackRequest.
Server-Side Tracking
Inject MatomoTracker or use the Matomo facade.
Track a page view:
use Justin991q\MatomoServerSideTracking\MatomoTracker;
app(MatomoTracker::class)->trackPageView($request, url: 'https://example.com/page', actionName: 'Page Title');
url defaults to the request's full URL and actionName to the request path.
Track an event:
use Justin991q\MatomoServerSideTracking\Facades\Matomo;
Matomo::trackEvent('Downloads', 'click', 'report.pdf', 1.0);
Events don't take a request, so no visitor data (IP, user agent, visitor ID) is sent unless you pass it via $options.
Track from a Laravel Request (auto-extracts IP, user agent, language, user ID, visitor ID):
Matomo::trackRequest($request, 'Optional Page Title');
All methods accept an $options array as the final argument for any additional Matomo Tracking API parameters:
Matomo::trackPageView($request, options: [
'dimension1' => 'premium',
'gt_ms' => 250,
]);
When the queue is disabled, methods return bool — true on success, false on failure (connection error, HTTP 5xx). Failures are silent and will never throw or break the application. When the queue is enabled, methods always return true.
Visitor ID Resolution
The server-side tracker resolves visitor IDs in this order:
- Reads the Matomo cookie (
_pk_id_{site_id}_*) from$_COOKIE. PHP normalises the dots in the original cookie name (_pk_id.1.abc) to underscores, so this is read directly from the PHP superglobal. - Falls back to an MD5 hash of the visitor's IP address.
Force Server-Side Tracking on Specific Routes
Use MATOMO_FORCE_SERVER_SIDE_ROUTES to always track certain routes server-side, even when the JS cookie is present. This is useful for routes that are critical to track regardless of ad-blockers or duplicate-prevention logic:
MATOMO_FORCE_SERVER_SIDE_ROUTES=files.*,downloads.show
Supports * wildcards matching against the Laravel route name. Unnamed routes are never matched.
Middleware
TrackPageView automatically tracks all non-JSON GET requests when MATOMO_ENABLED=true. Register it in your route groups or global middleware stack:
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\Justin991q\MatomoServerSideTracking\Middleware\TrackPageView::class);
})
The middleware skips:
- Non-
GETrequests - JSON requests (
Accept: application/json) - Requests from excluded IPs (
MATOMO_EXCLUDED_IPS) - Requests matching excluded page patterns (
MATOMO_EXCLUDED_PAGES) - Requests with the JS tracker cookie, unless
MATOMO_JS_TRACK_PAGE_VIEW=falseor the route matchesMATOMO_FORCE_SERVER_SIDE_ROUTES(see Avoiding Duplicate Page Views)
Module Structure
├── config/
│ └── matomo.php
├── resources/
│ └── views/
│ └── tracking.blade.php # Client-side JS snippet
├── Exceptions/
│ └── MatomoException.php
├── Facades/
│ └── Matomo.php
├── Jobs/
│ └── SendTrackingRequest.php # Queued HTTP dispatch
├── Middleware/
│ └── TrackPageView.php
├── tests/
│ ├── MatomoServerSideTrackingTest.php
│ └── TestCase.php # Testbench standalone, the host app's TestCase otherwise
├── MatomoServiceProvider.php
├── MatomoTracker.php
└── composer.json
Testing
composer install
composer test
The GitLab CI pipeline runs the test suite on PHP 8.3, 8.4 and 8.5 against Laravel 11 (PHP 8.3 and 8.4 only), 12 and 13.
Releasing
Push a semver tag, after the tests pass the pipeline publishes it to the GitLab package registry:
git tag v1.0.0
git push origin v1.0.0
License
The MIT License (MIT). See LICENSE for details.