Skip to main content

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' => 'none', // none | cookie | full
ValueThe snippet emitsUse when
noneNo consent callThe cookieless default — nothing to consent to
cookierequireCookieConsentYou track with cookies and gate only the cookies on consent
fullrequireConsentYou 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 EncryptCookies middleware. 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 returns OptOut::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.