Skip to main content

Command reference

CommandPurpose
matomo:installPublish the config and print setup hints.
matomo:testSend a test hit and report connectivity.
matomo:flushDrain the batch buffer once. Scheduled automatically in batch mode.
matomo:workLong-running daemon that drains the batch buffer.
matomo:load-simSimulate load through the buffer and flush pipeline and report throughput.
matomo:replayRe-queue dead-lettered hits into the buffer.
matomo:reportFetch a Reporting API method and print the JSON result.
matomo:forgetErase or export a data subject's data for GDPR requests.
matomo:annotateAdd 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
OptionEffect
--onceDrain the buffer once and exit.
--max-runs=NStop after N passes. 0 means run continuously.
--max-time=NStop after roughly N seconds. 0 means no limit.
--memory=NStop 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
OptionEffect
--hits=NSynthetic 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
OptionEffect
--listShow the dead-letter queue without changing anything.
--limit=NReplay at most N entries. 0 means all.
--pruneDiscard the dead-letter queue without replaying.
--prune-older-than=NDelete 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 optionEffect
methodThe 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

php artisan matomo:forget "[email protected]"
php artisan matomo:forget "[email protected]" --force
php artisan matomo:forget "[email protected]" --export
php artisan matomo:forget "[email protected]" --site=all
Argument or optionEffect
segmentSegment identifying the data subject, for example [email protected].
--site=Site id to search. Defaults to the configured one; all searches every site.
--exportExport the data subject's data instead of deleting it.
--forceSkip 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 optionEffect
noteThe annotation text. Omit it with --release.
--date=Annotation date, YYYY-MM-DD. Defaults to today.
--releaseAnnotate a deployment. Gated by annotations.release.
--app-version=The version for --release. Falls back to config('app.version').
--starredStar 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.