mediasoep / maintenance-reporter
Reports a WordPress site's environment and maintenance events to SOILboard.
Requires
- php: ^8.1
Requires (Dev)
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^9.6
- szepeviktor/phpstan-wordpress: ^2.0
- wp-phpunit/wp-phpunit: ^7.1
- yoast/phpunit-polyfills: ^2.0 || ^3.0 || ^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Reports a WordPress site's environment and maintenance events to SOILboard: the WordPress, PHP and database versions, the active theme, every installed plugin with its latest known version, site health, and events such as plugin and core updates, activations, theme switches, failed updates, stalled cron and PHP fatals. It also activates and checks the site's SOILboard licence.
It is the reporter inside SOIL and inside the standalone soilboard
plugin for sites without SOIL. It uses WordPress APIs only: no framework, no runtime dependencies.
Requirements
- PHP 8.1 or newer
- WordPress 6.2 or newer
- A SOILboard licence key registered for the site's url
Installation
composer require mediasoep/maintenance-reporter
Inside a WordPress plugin, prefix the package (for example with Strauss) so two plugins shipping different versions of it never collide on one install.
Usage
Boot it once from your plugin, as early as plugins_loaded:
use MediaSoep\MaintenanceReporter\Agent;
use MediaSoep\MaintenanceReporter\Config;
$agent = Agent::boot(new Config(
optionPrefix: 'myplugin',
agentName: 'myplugin',
agentVersion: '1.0.0',
dashboardUrl: 'https://www.soilboard.nl',
extraSnapshotFields: static fn (): array => [],
enabled: static fn (): bool => wp_get_environment_type() === 'production',
pluginFile: __FILE__,
));
$result = $agent->license()->activate('XXXXX-XXXXX-XXXXX-XXXXX-XXXXX');
// ['status' => 'valid', …], or 'missing', 'invalid', 'expired', 'revoked', 'not_license_site', 'error'
$agent->license()->deactivate();
// ['status' => 'deactivated'] clears the key; 'failed' and 'error' keep it
From then on the agent records events as they happen and sends them, with a snapshot of the site
whenever it changed, to {dashboardUrl}/api/v1/maintenance/events. A flush runs a minute after an
event, every hour from cron, and at the end of the first request a new host version runs.
| Part | What it does |
|---|---|
Agent::boot(Config) | Builds every collaborator and hooks it into WordPress, also when booted after init. |
$agent->license() | activate(), check(), deactivate(), key(), information(), hasValidLicense(), downloadQueryArgs() for an update checker. |
$agent->recorder() | record(string $type, array $data) for events of your own. |
$agent->reporter() | flush(bool $force = false) to send now. |
Agent::clearCron(Config) | For your plugin's deactivation hook. |
Configuration
| Parameter | Meaning |
|---|---|
optionPrefix | Prefix of everything the package stores: options, transients, cron events and hooks all start with {optionPrefix}_, so an uninstaller can delete by prefix. |
agentName, agentVersion | Reported to SOILboard as agent: {name, version} and with licence activations (at most 32 characters each). |
dashboardUrl | SOILboard's base url. |
extraSnapshotFields | Top-level fields your plugin adds to the snapshot. They are hashed with it, but never override a field of the package's own. |
enabled | Whether to report at all. Return false on staging and local copies, so they never report as the live site; such a copy does not run the daily licence check either. |
pluginFile | Your main plugin file. |
checkIntervalMinutes | Minimum minutes between two snapshot collections (default 45). |
log | fn (string $level, string $message, array $context); by default messages go to error_log() while WP_DEBUG is on. |
registerLicenseHooks | Whether the package hooks the daily licence check itself (default true). |
fallbackLicensePrefix | Closure answering the option prefix of another plugin's licence in the same shape (soil for SOIL before 5.15), or null. While the host has no key of its own, LicenseClient uses that key and that licence's status, and its checks write the status there, so both plugins show one status whoever checked last; borrowsLicense() says so. The other licence's key is never written or deactivated, and a key the host fails to activate leaves that status alone. Hosts without a fallback (SOIL itself) are unaffected. |
The remaining parameters (endpoint, maxEvents, chunkSize, maxBodyBytes, flushDelay,
invalidWarningThreshold, bufferLockTtl, bufferLockWait, requestTimeout) tune delivery and have
defaults that suit any site.
Multisite
The licence is network wide: only the main site activates, checks or deactivates it, and a subsite
asking gets not_license_site without a request. Every site reports its own snapshot and events
under its own blog_id.
Updates from SOILboard
SOILboard can ask a site to run updates (a maintenance run). The site pulls; SOILboard never sends a command:
- SOILboard wakes the site with a bare
POSTto its home url (the snapshot'supdates.wake_url) and the headerX-SOILboard-Wake: 1. The package answers202atplugins_loadedand closes the connection. It is no REST route or admin-ajax call, so SOIL's REST lock and Hide My WP leave it alone; a firewall must let the header through. Anyone may send it: it only makes the site ask. - At
wp_loadedthe main site asks{dashboardUrl}/api/v1/maintenance/update-requests/nextwith its licence, and runs what it gets in the same request. The request runs as cron (DOING_CRON) from the wake on, as WordPress' own automatic updates do, so premium plugins that hook their updater in only in wp-admin or cron offer their updates here too. It never asks without a wake: a site SOILboard cannot wake fails at once and visibly, instead of updating at some moment nobody watches. - WordPress' own automatic updater runs, with filters at
PHP_INT_MAXthat allow exactly the requested items for this run only: they win over the site's own auto-update choices and over a plugin that turns the updater off, but not overDISALLOW_FILE_MODSorAUTOMATIC_UPDATER_DISABLED. Only updates WordPress already offers can run; a request names items, never a package or url. - The site reports
running, thendone,failedorrefusedwith a result per item to…/update-requests/{id}, with the one-time token that came with the request, so only the site that fetched it can say how it went. Then it sends its events (actorsoilboard) with a fresh snapshot.
The snapshot's updates field says whether the site can update itself and which core and theme
updates WordPress offers it.
Commands from SOILboard
The answer to the fetch after a wake can also carry commands: [{id, name, payload}], with or without an
update request. They run first, in order; each one that ran is reported to
…/maintenance/site-commands/done with its id, and SOILboard hands out one it never heard back about
again on a later fetch. A name the site does not know is skipped.
| Command | What it does |
|---|---|
report | Sends the full report now, its snapshot included even when nothing changed. Kept until a snapshot is sent, so a flush that fails leaves the next one to send it. |
A host or any other plugin adds its own command on the action soilboard/maintenance_reporter/commands, the same on every
host whatever its prefix, which is passed the SiteCommands:
add_action('soilboard/maintenance_reporter/commands', function (MediaSoep\MaintenanceReporter\SiteCommands $commands): void {
$commands->register('my-plugin/clear-cache', function (array $payload): void {
// …
});
});
Prefix your own names, so they never meet one the package adds later.
Failure behaviour
Reporting never throws into WordPress. A failed recording, flush or logger is caught and logged; an event that cannot be buffered is dropped and counted, and the count is reported. An "invalid" or "missing" answer from SOILboard backs off for a day instead of retrying every hour.
Development
The tests run against a real WordPress, installed into a database whose name must contain test
(maintenance_reporter_tests by default, see tests/database.php). Point WP_CORE_DIR at any
WordPress install for the core files.
composer install
composer test # single site
composer test:multisite # network
composer analyse # PHPStan, PHP 8.1 rules
License
GPL-2.0-or-later.