Consent and redaction
The default posture is the private one: no cookies, Do-Not-Track and Sec-GPC
honored, URLs scrubbed before they leave your application, and bots excluded. You opt
into more identification, never out of it.
'privacy' => [
'honor_dnt' => true, // skip on DNT:1 / Sec-GPC:1
'cookieless' => true, // JS: disableCookies before trackPageView
'consent' => 'none', // none|cookie|full; any other value is read as full
],
Cookieless by default
privacy.cookieless pushes disableCookies before the page view, so the client-side
tracker sets none. Server-side identification is cookieless by construction — see
how a visitor is identified.
With no cookies and a rotating, salted identifier, there is nothing stored on the visitor's device and no cross-site identifier — which is what makes a consent banner unnecessary in the default configuration. Whether that holds for your jurisdiction and your configuration is a question for your own legal advice, not for this page; what the package guarantees is the technical posture.
Do-Not-Track
honor_dnt covers both signals — the legacy DNT: 1 header and Sec-GPC: 1, the
Global Privacy Control. It is checked server-side, in the
tracking gate, before anything about identity is evaluated. The
client-side tracker is told about it too, via setDoNotTrack.
Honoring it server-side is the part that matters: a DNT header cannot be respected
by JavaScript that has already run.
Consent postures
'consent' => 'none', // none | cookie | full
| Value | The snippet emits | Use when |
|---|---|---|
none | No consent call | The cookieless default — nothing to consent to |
cookie | requireCookieConsent | You track with cookies and gate only the cookies on consent |
full | requireConsent | You gate all tracking on consent |
With cookie or full, Matomo holds hits back until your consent manager calls
setCookieConsentGiven or setConsentGiven. Wiring that call is your consent
manager's job — the package emits the requirement, not the grant.
Any other value is read as full. A typo such as 'Full' or 'required', or a boolean,
therefore asks for consent rather than tracking without it. php artisan matomo:test
names such a value, since the tracker records nothing until consent is granted.
Under full the server side waits as well. The tracking gate lets a server-side hit through
only when the request carries mtm_consent, the cookie in which matomo.js remembers a consent,
so your consent manager has to call rememberConsentGiven rather than setConsentGiven alone.
Until then the page views, the beacons and the hits from your code are refused as no_consent,
and the <noscript> pixel is left out of the page, because a visitor without JavaScript cannot
give that consent.
A job or a command has no visitor and no cookie, and is not asked: send from there only what
you may. none and cookie leave server-side tracking alone, because it sets no cookie.
A publishable privacy-policy partial
php artisan vendor:publish --tag=matomo-analytics-views
That publishes a small partial describing the cookieless Matomo setup, which you can include in your privacy policy and edit freely:
@include('matomo-analytics::privacy-policy')
@include('matomo-analytics::privacy-policy', ['heading' => 'Analytics'])
Read it before you publish it. It describes the package's default configuration, so if you changed the defaults — enabled cookies, stopped anonymizing — the text needs changing too.
The partial calls the IP addresses anonymized. For every hit the package sends from your
server, that is anonymize_ip, on by default: an IPv4 address leaves your application with
its last octet zeroed (a /24), an IPv6 address with only its first 48 bits
(details). A page that loads matomo.js sends
its hits from the browser straight to Matomo, so for those Matomo's own IP anonymization
decides. Turn it on there as well if your policy says the addresses are anonymized.
The paragraph ships in English, German, Spanish, French, Italian, Dutch and Portuguese.
A regional locale such as pt_BR, de_AT or fr-CA reads its language, which Laravel's
own lookup never falls back to. To change the wording, publish the lang files with
--tag=matomo-analytics-lang; a file you add for the region itself is read first.
URL redaction
On by default. Secrets and PII are stripped out of tracked URLs before they reach Matomo:
?token=abc123&page=2 -> ?token=REDACTED&page=2
#access_token=eyJ…&token_type=bearer -> #access_token=REDACTED&token_type=bearer
'privacy' => [
'redact' => [
'enabled' => true,
'replacement' => 'REDACTED',
'query_params' => [
'token', 'api_key', 'apikey', 'api-key', 'access_token', 'refresh_token',
'id_token', 'jwt', 'code', 'state', 'auth', 'auth_token', 'password',
'passwd', 'pwd', 'secret', 'client_secret', 'signature', 'sig', '_token',
'session', 'session_id', 'sessionid', 'email',
'X-Amz-Signature', 'X-Amz-Credential', 'X-Amz-Security-Token',
'X-Goog-Signature', 'X-Goog-Credential',
],
'patterns' => [],
'keys' => ['url', 'urlref', 'link', 'download', 'c_t', 'c_p'],
],
],
Seven things worth understanding about it.
The key survives, the value does not. A redacted parameter keeps its name and
loses its value, so a report still tells you "a password-reset link was opened"
without the token being in your analytics, your Matomo access logs, or a dashboard
someone shares in a meeting. Parameter matching is case-insensitive, and it covers
the array forms token[]=, token[0]= and token[a][b]= with the brackets written
out or percent-encoded as %5B and %5D. Laravel reports the URL of a request in
the encoded form. A listed name counts only as the parameter's own name, never as a
key inside another parameter: user[password]= is left alone, and so is
filter[state]=open, where the name describes the page. The patterns example below
catches a secret nested that way.
A token in a path is covered by the route that carries it. Laravel's reset links carry the
token in the path, as in /reset-password/{token}?email=…, and so does the confirmation page of
a magic link, /magic-link/verify/{token}, where no parameter name reaches it. Every route the
application registers whose URI names a parameter from path_params has that segment redacted,
on the server and in the browser, in the page address and in the referrer of the page that
follows. The list holds token by default, and a config you published earlier reads the
shipped list. The token of the route password.reset, and of the paths the starter kits use,
reset-password/{token} and password/reset/{token}, is redacted whatever the list says; only
enabled => false switches it off. The address in ?email= is redacted by the email entry in
query_params.
A presigned storage URL loses its signature. Storage::temporaryUrl() on an
S3-compatible disk, such as S3, R2, MinIO or Spaces, writes X-Amz-Signature and
X-Amz-Credential into the URL, and X-Amz-Security-Token as well when the application
signs with role credentials, as on Vapor or EC2. GCS writes X-Goog-Signature and
X-Goog-Credential.
Anyone holding such a URL can download the file until it expires, and matomo.js reports
one as a download or an outlink when a visitor clicks it, or as the piece of a content block
that shows it. Those names are redacted; the algorithm, date and lifetime stay, so a report
still shows that the link was a presigned one.
The fragment is read like the query. OAuth, OpenID Connect and magic-link flows
put their tokens after the #, and the page-view and Web Vitals beacons send the
page's whole address, fragment included. A parameter there is redacted the same way.
patterns handles what a parameter list cannot. Any regular expression here is
applied to the URL. This is how you scrub an identifier that lives in a path segment
rather than a query parameter, or a secret nested inside another parameter:
'patterns' => [
'/\b[\w.+-]+@[\w-]+\.[\w.-]+\b/', // email addresses anywhere in the URL
'#/invoices/\d+#', // numeric invoice ids in the path
'/(?<=password%5D=|password\]=)[^&#]*/i', // a password nested as user[password]
],
keys decides which payload fields are scrubbed. By default the page URL, the
referrer, link and download targets, and the target and piece of a tracked content block
(c_t, c_p). matomo.js fills the piece with the address of the image, video or audio in
the block when it carries no data-content-piece, so a piece is often a URL.
The referrer is included, and that is worth knowing in both directions: the parameters of
an incoming URL are scrubbed, while its host is left intact — which is what keeps referrer
reports, and the AI Assistants acquisition channel, working. urlref
stands for both copies of it that matomo.js sends: the referrer of the page, and _ref, the
one it keeps in a cookie for conversion attribution and adds to every request.
The client-side tracker is redacted in the browser. A page that loads matomo.js
sends its hits from the browser straight to Matomo, so @matomoScript installs the
redaction as the tracker's request processing: before a request leaves the page, the
names in query_params are redacted in the fields keys lists. patterns are PHP
regular expressions and apply to server-side hits and the beacons only. A Tag Manager
container configures its own tracker, and nothing is installed there. Matomo keeps a
single request-processing function, so a setCustomRequestProcessing of your own
replaces this one when it runs after the snippet, and is replaced by it when it runs
before.
Redaction is not a substitute for not putting secrets in URLs. It is the safety net for the ones that are already there — a password-reset link, a signed download URL, an OAuth callback.
Server-side opt-out
Matomo's own opt-out widget sets a cookie on the Matomo domain, which your server-side tracker cannot see. So an application that tracks server-side needs a first-party cookie, and the gate honors one:
use MatomoAnalytics\Privacy\OptOut;
return back()->withCookie(OptOut::enable()); // stop tracking this browser
return back()->withCookie(OptOut::disable()); // opt back in
'privacy' => [
'opt_out' => [
'respect' => true,
'cookie' => 'matomo_opt_out',
],
],
enable() returns a permanent cookie; disable() returns the forget-cookie that
clears it. Attach either to a response from your own opt-out control.
Set it server-side. These are Laravel cookies, so they pass through the
EncryptCookiesmiddleware. A plaintext cookie of the same name written by client-side JavaScript is dropped as invalid and will not opt anyone out. If you want a JavaScript control, have it hit a small endpoint that returnsOptOut::enable().
@matomoOptOut is the control a visitor
uses in the browser. Its cookie, mtm_consent_removed, stops matomo.js, and the gate reads it
as well, so one click opts a visitor out of both sides. privacy.opt_out.respect switches
both cookies off together.