Command reference
| Command | Purpose |
|---|---|
matomo:install | Publish the config and print setup hints. |
matomo:test | Send a test hit and report connectivity. |
matomo:flush | Drain the batch buffer once. Scheduled automatically in batch mode. |
matomo:work | Long-running daemon that drains the batch buffer. |
matomo:load-sim | Simulate load through the buffer and flush pipeline and report throughput. |
matomo:replay | Re-queue dead-lettered hits into the buffer. |
matomo:report | Fetch a Reporting API method and print the JSON result. |
matomo:forget | Erase or export a data subject's data for GDPR requests. |
matomo:annotate | Add an annotation, or a deploy marker, to the reports timeline. |
matomo:install
php artisan matomo:install
Publishes config/matomo-analytics.php and prints the environment variables to set. Run
once after installing. Publishing is optional — see
configuration.
matomo:test
php artisan matomo:test
Sends one real hit to the configured tracking endpoint and reports the HTTP status and the URL it used. Fails with a clear message when the package is not configured, when the host is unreachable, and when Matomo answers with an error status.
This is the first thing to run when data is not arriving: it bypasses the queue, the buffer and the gate, so a pass narrows the problem to delivery and a failure narrows it to configuration.
It reports the backlog
Two numbers, both printed only when they are not zero:
Buffer: 412 hit(s) waiting to be flushed.
Dead letters: 3 batch(es) gave up and are parked — inspect them, then `matomo:replay`.
Nothing else in the package exposes either one. matomo:flush reports the pass it just
made, and the buffer's own size() had no caller outside the load simulator — so "the
buffer grows and never drains" was a symptom the troubleshooting
guide named and nothing here could show you.
The buffer count is asked for in batch mode only, because nothing writes to the buffer in
the other modes. Neither count can fail the command: a store that will not answer — no
migration run yet, an unreachable Redis, a spool that does not exist — leaves the line out
rather than replacing the connectivity answer you came for.
It also names settings your application cannot see
A setting this version ships can be missing from the configuration your application actually reads, and nothing fails when it is. The package falls back to its own default, which is usually even the right value — so the setting you deliberately changed is the one that quietly does not apply.
matomo:test compares the settings this version ships against the configuration the running
application resolved, and lists every path that is missing:
2 setting(s) this version ships are not visible to the running application:
matomo-analytics.batch.dead_letter.retention_days
matomo-analytics.resilience.never_throw
The advice that follows depends on which of the two causes applies, and the command works out which:
- The configuration is cached and the cache predates the keys. Rebuild it with
php artisan config:cache. - Your published copy is older than the package. Publish again with
--force, or add the listed lines to your copy.
A list counts as one setting rather than being walked into, so trimming entries from
privacy.redact.keys is not reported as missing settings.
This is advisory and never fatal. The command is asked when something is already wrong, and turning a diagnostic into a failure removes the diagnosis.
matomo:flush
php artisan matomo:flush
Drains the batch buffer once and reports how many hits were delivered. In batch mode the
package schedules this every minute, so you normally do not run it by hand — but your
scheduler has to be running for that to happen.
The overlap lock expires after 10 minutes, not after Laravel's default of a day. A run killed outright — SIGKILL, an OOM, a container replaced mid-flush — cannot release the lock it took, so the bound is what limits the damage: the drain resumes at the next scheduled minute after at most ten, instead of standing still until tomorrow.
It exits non-zero when a drain is stuck. Once the consecutive-failure count reaches
resilience.reporting.report_after_attempts, the command reports the failure and exits
with a failure status, so a cron that mails on non-zero output tells you about a backlog
that is not moving. A normal pass — including one that finds nothing to send — exits 0.
⚠️ That exit code does not reach Laravel's scheduler by default. The package registers
this command with runInBackground(), so schedule:run never waits on an HTTP call to
Matomo — and Laravel raises a scheduled command's non-zero exit only for a foreground
event. A background one dispatches no ScheduledTaskFailed and never reaches your exception
handler, so a stuck drain is invisible to an error tracker.
Two ways to see it. Set MATOMO_SCHEDULE_BACKGROUND=false to run both scheduled commands in
the foreground, which restores the exit code and costs you the wait; or listen for the
TrackingFailed and HitsDeadLettered events, which fire either way. Running
the command from your own cron entry, rather than through the scheduler, also gives you the
status directly.
Configuring the scheduled events yourself
The message is a separate problem from the exit code, and the switch above does not buy it
back: Laravel redirects a scheduled command's output to $event->output in both modes, so
what matomo:flush prints goes to the null device either way.
Because the package registers the two events itself, everything Laravel offers for a scheduled
task — sendOutputTo(), emailOutputOnFailure(), onFailure(), pingOnFailure() — used to be
out of reach for exactly these two. configureSchedule() hands you each event before it is
registered:
use Illuminate\Console\Scheduling\Event;
use MatomoAnalytics\MatomoAnalyticsServiceProvider;
// in a service provider's register()
MatomoAnalyticsServiceProvider::configureSchedule(function (Event $event): void {
$event->sendOutputTo(storage_path('logs/matomo-schedule.log'))
->pingOnFailure('https://monitor.example.com/matomo');
});
It runs last, after the package has applied run_in_background and the overlap bound, so
anything you set there wins. Register it before the scheduler resolves — a service provider's
register() is the reliable place.
matomo:work
php artisan matomo:work
php artisan matomo:work --once
php artisan matomo:work --max-time=3600 --memory=256
| Option | Effect |
|---|---|
--once | Drain the buffer once and exit. |
--max-runs=N | Stop after N passes. 0 means run continuously. |
--max-time=N | Stop after roughly N seconds. 0 means no limit. |
--memory=N | Stop once memory use exceeds N megabytes. 0 means no limit. |
The long-running alternative to the scheduled flush, for high-volume file or Redis
spools. It sleeps batch.flush_interval seconds between passes.
--max-time and --memory exist so a supervisor can recycle the process before it grows
unbounded, the same pattern queue:work uses. Run one instance: the atomic buffer
claims keep concurrent runs from double-sending, but they do not make a second instance
useful.
It says when it is losing hits, and only then. A pass that delivered nothing while dead-lettering something — the shape a wrong host, site id or token makes — prints the reason and makes the command exit non-zero when it eventually stops. So does a buffer it could not read at all. A pass that delivered hits prints the count; an idle pass prints nothing, so a daemon on a one-minute cadence does not fill a log with lines nobody reads.
That is new. It used to call the flusher, discard the answer and return success unconditionally: three buffered hits against a Matomo answering 400 gave exit 0 and no output, and the same three against a Matomo answering 200 gave exit 0 and no output — byte-identical. Under a supervisor, a daemon losing every hit looked exactly like a healthy one.
It shuts down between passes, not wherever the signal lands. SIGTERM and SIGINT are
trapped, so a supervisor restart or a deploy lets the flush in progress finish and then
exits — the same contract queue:work offers. Without it a batch claimed at the moment of
the signal would sit unacknowledged until its claim expired, which is a needless delay on
every ordinary restart. Signal handling is skipped on a host without ext-pcntl, which this
package does not require; there the process still stops, just not gracefully.
matomo:load-sim
php artisan matomo:load-sim --hits=100000 --driver=redis
php artisan matomo:load-sim --hits=10000 --against=real
| Option | Effect |
|---|---|
--hits=N | Synthetic hits to enqueue and drain. Default 1000. |
--driver= | Buffer driver to exercise: array, database, redis or file. Defaults to the configured one. |
--batch= | Bulk batch size. Defaults to batch.size. |
--against= | fake discards the sends and measures the client; real sends to the configured Matomo. Default fake. |
Reports enqueue throughput, flush throughput, the exact Bulk request count and peak memory. It runs the real pipeline, so the numbers describe your deployment rather than a model of it. See scaling.
It stops early and warns if a flush pass makes no progress, which is what you see when
--against=real points at an unreachable endpoint.
matomo:replay
php artisan matomo:replay --list
php artisan matomo:replay
php artisan matomo:replay --limit=100
php artisan matomo:replay --prune
php artisan matomo:replay --prune-older-than=30
| Option | Effect |
|---|---|
--list | Show the dead-letter queue without changing anything. |
--limit=N | Replay at most N entries. 0 means all. |
--prune | Discard the dead-letter queue without replaying. |
--prune-older-than=N | Delete entries that failed more than N days ago. Entries with no recorded failure time are never deleted. |
Always --list first. The listing shows each entry's hit count, delivery attempts, error
and failure time — enough to tell a misconfiguration from a payload problem before you
push the hits back into the buffer. See reliability.
matomo:report
php artisan matomo:report VisitsSummary.get
php artisan matomo:report Actions.getPageUrls --period=month --date=2026-01
php artisan matomo:report VisitsSummary.get --segment="deviceType==smartphone"
| Argument or option | Effect |
|---|---|
method | The Reporting API method, for example VisitsSummary.get. |
--period= | day, week, month, year or range. |
--date= | today, yesterday, a YYYY-MM-DD date, or lastN. |
--segment= | A Matomo segment definition. |
Prints the decoded JSON result, or fails with Matomo's own error message. The quickest way to confirm the read side works and that your token has view access.
matomo:forget
| Argument or option | Effect |
|---|---|
segment | Segment identifying the data subject, for example [email protected]. |
--site= | Site id to search. Defaults to the configured one; all searches every site. |
--export | Export the data subject's data instead of deleting it. |
--force | Skip the confirmation prompt. |
Previews the match count and asks before deleting. Needs a token with admin access.
See GDPR requests, and consider --site=all for a genuine
erasure request.
matomo:annotate
php artisan matomo:annotate "Maintenance window"
php artisan matomo:annotate --release --app-version=1.4.0
| Argument or option | Effect |
|---|---|
note | The annotation text. Omit it with --release. |
--date= | Annotation date, YYYY-MM-DD. Defaults to today. |
--release | Annotate a deployment. Gated by annotations.release. |
--app-version= | The version for --release. Falls back to config('app.version'). |
--starred | Star the annotation. |
--site= | Site id. Defaults to the configured one. |
--release is a no-op — reported, and exiting successfully — unless annotations.release
is enabled, so it is safe to run unconditionally in a deploy pipeline. The flag is
--app-version, not --version, which Symfony reserves. See
release annotations.