JB SiteAudit releases
A WordPress audit log that records what changed, not merely that something did. Plugin agnostic, quiet by default, WCAG 2.2 AA.
by Jeremiah Beasley · github.com/jeremiahbeasley/jb-siteaudit · website
Install
The author publishes release zips, so WP-CLI can install straight from GitHub:
wp plugin install https://github.com/jeremiahbeasley/jb-siteaudit/releases/download/v1.2.1/jb-siteaudit-1.2.1.zipReadme
JB SiteAudit
A WordPress audit log that records what changed, not merely that something did.
Most activity logs tell you a field was touched. This one tells you the field was
blogdescription, that it held "Grouping test", and that it now holds something
else — and it says so in a sentence a person can read.
- Plugin agnostic by design. It hooks WordPress's own storage layer — post types, post meta, options, terms, users — so a plugin installed next year is covered the day it arrives, with no code written for it.
- Quiet. Machinery that WordPress and plugins write about themselves is filtered out at source. On one real site, 22 of 28 recorded rows were noise; they are no longer recorded at all.
- Accessible. WCAG 2.2 Level A and AA, including reflow at 320px, verified with a real browser driving real key presses.
Requires WordPress 6.0+ and PHP 7.4+.
What it records
| Area | Examples |
|---|---|
| Content | Any post type: published, edited, trashed, restored, deleted. Title, body, web address and parent changes, each with before and after |
| Custom fields | Added, changed and removed — including the value a removed field held |
| Settings | Any option, with the previous and new value |
| Users | Created (with role and email address), role changes, profile edits, deletion, sign-in and sign-out |
| Categories and tags | Created, renamed, re-described, re-parented, deleted |
| Media | Uploaded and deleted |
| Plugins and themes | Activated, deactivated, switched, and updated — with the version before and after |
| Plugin data tables | Changes inside a plugin's own tables, attributed to the plugin that owns them |
What it deliberately does not record
This is an audit log of decisions people make, not a copy of your database.
- Submitted records — form entries, orders, donations, subscribers. Those are what the public sends in, not what a signed-in person changed.
- Visitor analytics — view counters, hit tables, crawl results.
- Credentials. Any option or field whose name suggests a password, token, API key, secret, salt or licence is recorded as (value not shown). The detail screen withholds it too — it must never become the place a secret is printed.
- Machine bookkeeping — transients, locks, cached version stamps, dismissed notices, install dates, and the settings a plugin rewrites during its own update.
The four screens
Activity Log — the entries, most recent first. Knock-on changes are nested under the action that caused them, so one editor save that writes forty custom fields is one row you can expand, not forty rows.
Entry detail — the complete before and after values (the list truncates at 90 characters), the exact setting or field name, checksums for content too large to store in full, where the change was made from, and a link to open the changed item.
Coverage — computed live for this site: for every active plugin, does it register post types, own tables, write options, have an adapter — and therefore is it fully covered, settings-only, watched by fingerprint, or not covered. Installing a plugin updates this with no code change.
Settings — retention, size caps, ignore lists, and the table watcher switch.
Installation
Download the release zip and install it through Plugins → Add New → Upload,
or clone into wp-content/plugins/:
git clone https://github.com/jeremiahbeasley/jb-siteaudit.git
Activate it. The log table is created on activation and the menu appears as
JB SiteAudit. Access requires the jbsa_view_log capability, granted to
administrators on activation.
Settings
| Setting | Default | What it does |
|---|---|---|
retention_days |
90 | How long entries are kept. 0 keeps everything |
max_detail_bytes |
20000 | Largest single value stored in full |
watch_tables |
on | Watch plugin-owned tables for changes |
checksum_max_bytes |
52428800 | Above this a table is tracked by row count only, because a checksum would read too much of it to be affordable on a request |
ignored_option_keys |
— | Settings never recorded |
ignored_meta_keys |
— | Custom fields never recorded |
ignored_post_types |
— | Content types never recorded |
ignored_tables |
— | Data tables never watched |
client_ip_header |
— | Empty trusts REMOTE_ADDR. Set to a server key such as HTTP_X_FORWARDED_FOR only when a trusted proxy sets it |
For developers
Record your own events
Any plugin can write to the log without depending on this one. If JB SiteAudit is not installed the action simply goes nowhere.
do_action( 'jbsa_log', array(
'event_type' => 'entry_checked_in',
'object_type' => 'forminator_entry',
'object_id' => 42,
'object_name' => 'Jane Doe',
'summary' => 'Checked in at the desk',
) );
Filters
| Filter | Purpose |
|---|---|
jbsa_ignored_post_types |
Content types to skip entirely |
jbsa_ignored_meta_keys |
Custom fields to skip entirely |
jbsa_ignored_option_keys |
Settings to skip entirely |
jbsa_skip_option |
Decide per option name — receives ( bool $skip, string $name ) |
jbsa_classify_table |
Classify a table as core, machine or watch — receives ( string $class, string $name ) |
Example — stop recording a noisy option your plugin writes on every page load:
add_filter( 'jbsa_skip_option', function ( $skip, $name ) {
return $skip || 'myplugin_last_ping' === $name;
}, 10, 2 );
Why a table watcher exists
Roughly three quarters of the most-used WordPress plugins store their settings in post types or options, so hooking core storage covers them for free. The rest own their tables, and no core hook can see a plugin write to its own table.
That gap cannot be closed by a shipped list of supported plugins, for a reason that was measured rather than assumed: Forminator, Code Snippets, Redirection and Elementor create no tables at installation and create them lazily on first use. Any precomputed compatibility list understates reality the moment a feature is used.
So the watcher discovers non-core tables at runtime from the site's own prefix, classifies them, attributes them to the owning plugin by searching active plugin code for the table suffix, and fingerprints them. Where a table has a column a person would recognise, changes are named — "added logo_url", "edited colour". Where it has only numeric row ids, the entry says how many records moved and states plainly that contents were not recorded, rather than printing row numbers and pretending that is information.
See docs/COVERAGE.md for the measured results.
Accessibility
The interface targets WCAG 2.2 Level A and AA, and is tested rather than asserted. A Playwright suite signs in to a real WordPress install and drives the interface with real key presses:
- The entry list is an ARIA treegrid with roving tabindex — one tab stop for the whole grid, Up and Down between rows, Left and Right within a row, and Left and Right on the first column to collapse, expand and move to the parent.
- Reflow at 320px on all four screens, with no horizontal page scrolling. Wide tables scroll inside their own container.
- Status is never carried by colour alone.
- Focus is visible and never obscured.
Automated scanners are not treated as sufficient: they check attributes and cannot catch an interaction failure.
Privacy and data protection
The log stores the acting user's login and ID, their IP address, and a description of what changed. Because it can hold personal data:
- Set
retention_daysto match your retention policy. The default is 90 days. - Where a proxy sits in front of the site, set
client_ip_headerso the recorded address is the real client and not the proxy. - Values that look like credentials are never stored, only noted as withheld.
- Client-supplied values, including the user agent, are sanitised before they are stored.
Coding standards
Checked with the official Plugin Check tool — the same one WordPress.org runs on submission — at zero errors.
The warnings that remain are direct database access, which is inherent to a
plugin that owns a table and reads it live. Each significant case carries a
written justification in the source: table names come from $wpdb->prefix, sort
columns are whitelisted, every user value is bound as a placeholder, and results
are deliberately not cached, because a stale audit log would misrepresent the
record.
See docs/DEVELOPERS.md if you intend to contribute.
Licence
GPL-2.0-or-later. See LICENSE.
Copyright © Jeremiah Beasley.
Read the full README on GitHub →