Reslab Activity Log
A lightweight, privacy-aware audit log plugin for WordPress.
by Reslab · github.com/andrewchuev/reslab-activity-log · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/andrewchuev/reslab-activity-log/archive/refs/heads/main.zipReadme
Reslab Activity Log
A lightweight, privacy-aware audit log plugin for WordPress. Tracks every meaningful change on your site — authentication, content, users, plugins, WooCommerce orders, and more — and stores it in a dedicated database table for maximum performance.
Features
Core tracking
| Area | Events |
|---|---|
| Authentication | Login, logout, failed login attempts |
| Content | Post creation, publishing, trashing, deletion; slug/title diffs |
| Users | Registration, profile changes (email, name, password, role changes), deletion |
| Plugins & Themes | Activation, deactivation, installation, updates, theme switches |
| Settings | Changes to key wp_options keys (site URL, admin email, permalinks, etc.) |
| Navigation | Menu updates |
WooCommerce integration
- Order status transitions — HPOS-compatible (
woocommerce_order_status_changed) - Product price and stock changes with before/after diff
- Coupon application and removal
- Refund creation (amount, reason, refunded_by)
Polylang integration
- Language assignments logged when a post is saved via Polylang (
pll_save_post)
Security alerts (both opt-in, disabled by default)
Two independent hourly WP-Cron checks, each with its own threshold/window:
- Brute-force — too many
login_failedevents from the same IP. - Mass deletion — one user logging an unusually large number of
deletedevents in a short window (compromised/malicious account bulk-deleting content, orders, or users).
Alerts are emailed to the admin; optionally also POSTed as JSON to a webhook URL (Slack/Discord incoming webhooks, Zapier, Make, n8n, or any custom endpoint) — see reslab_al_alert_webhook_url below. Duplicate alerts within the same window are suppressed via transient. A reslab_al_alert_{$type} action hook (bruteforce or mass_deletion) fires for custom PHP integrations beyond a generic webhook.
Privacy & GDPR
- IP anonymisation — enabled by default; mask last IPv4 octet (e.g.
192.168.1.0) - Email hashing — user email stored as SHA-256 hash, never plain text
- Configurable retention — auto-delete entries older than N days (default: 30)
- Archive before purge (opt-in) — save a gzip CSV snapshot of entries before the nightly purge deletes them; downloadable from Settings (nonce + capability gated, no public URL)
- Custom capabilities —
reslab_al_view_log/reslab_al_clear_log/reslab_al_manage_settingsfor granular role access;reslab_al_viewable_object_typesfilter further restricts which event types a role sees - Full uninstall —
uninstall.phpdrops the table, options, transients, archive files, and capabilities
Admin UI
- Located at Tools → Activity Log (not polluting the main menu)
- Filter by action, object type, user, IP address, date range, or free-text search over event details
- Events fired within the same request are grouped into one row (with a
+Nbadge), instead of one row per hook - Inline before/after diff, collapsed by default (
<details>/<summary>) - Export CSV with active filters applied, or pull events via the read-only REST API (
GET /wp-json/reslab-al/v1/events, Application-Password authenticated) for external monitoring/SIEM tools - Screen Options for per-page count
- Tools → Activity Log Settings — retention (+ archiving), IP anonymisation, both alert types, webhook URL, and "last ran" status for every background job
Requirements
| Requirement | Version |
|---|---|
| WordPress | 6.0+ |
| PHP | 8.0+ |
| WooCommerce (optional) | 7.0+ |
| Polylang (optional) | 3.0+ |
Installation
# Via Composer (if registered)
composer require reslab/activity-log
# Or manually — copy to your plugins directory
cp -r reslab-activity-log /path/to/wp-content/plugins/
Then activate via Plugins → Installed Plugins.
Configuration
Trusted proxy (Cloudflare / reverse proxy)
Add to wp-config.php:
define( 'RESLAB_AL_TRUSTED_PROXIES', '10.0.0.1' ); // IP of your proxy
Or use the filter:
add_filter( 'reslab_al_trusted_proxies', function( array $ips ): array {
$ips[] = '10.0.0.1';
return $ips;
} );
Without this, only REMOTE_ADDR is used — no IP spoofing possible from forged headers.
Granting log access to editors or shop managers
Preferred: let the plugin grant it itself on activation/upgrade, no role-editor plugin needed —
add_filter( 'reslab_al_default_roles', function ( array $roles ): array {
$roles[] = 'shop_manager';
return $roles;
} );
Or, for a one-off/ad-hoc grant instead:
// Run once, e.g. in a one-time migration or via a role-editor plugin.
$role = get_role( 'shop_manager' );
$role->add_cap( 'reslab_al_view_log' ); // can view
// $role->add_cap( 'reslab_al_clear_log' ); // can also clear
Pair this with reslab_al_viewable_object_types to limit what that role sees once it can view the log at all — e.g. a shop manager who shouldn't see user-management or plugin/theme events:
add_filter( 'reslab_al_viewable_object_types', function ( array $types, int $user_id ): array {
$user = get_userdata( $user_id );
if ( $user && in_array( 'shop_manager', $user->roles, true ) ) {
return [ 'order', 'coupon', 'product' ];
}
return $types; // unrestricted for everyone else
}, 10, 2 );
File structure
reslab-activity-log/
├── reslab-activity-log.php # Plugin header, activation/deactivation, bootstrap
├── uninstall.php # Full cleanup on plugin deletion
├── readme.txt # WordPress.org readme
├── languages/ # Translation files (.po/.mo)
├── assets/css/admin.css # Admin list table + settings page styles
└── includes/
├── class-tracker.php # Core WordPress hook interceptors
├── class-tracker-woocommerce.php # WooCommerce-specific hooks (loaded only if WC active)
├── class-list-table.php # WP_List_Table UI + Admin page wrapper
├── class-cron.php # Log rotation, purge, brute-force/mass-deletion alerts (WP-Cron)
├── class-settings.php # Settings page (Tools → Activity Log Settings)
└── class-rest-api.php # Read-only REST API (/wp-json/reslab-al/v1/events)
Dev-only, not shipped in a release build (see Development): tests/, vendor/, composer.json, composer.lock, phpunit.xml.dist, .wordpress-org/ (wordpress.org listing assets — banners/icons/screenshots, not plugin code), .gitignore.
Database
The plugin creates a single table {prefix}reslab_activity_log (renamed from
{prefix}activity_log in 1.1.0 — earlier installs are migrated automatically
via RENAME TABLE on the first request after upgrading):
CREATE TABLE wp_reslab_activity_log (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
user_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
ip_address VARCHAR(45) NOT NULL DEFAULT '',
action VARCHAR(50) NOT NULL DEFAULT '',
object_type VARCHAR(50) NOT NULL DEFAULT '',
object_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
context LONGTEXT,
request_id VARCHAR(20) NOT NULL DEFAULT '',
PRIMARY KEY (id),
KEY idx_created_at (created_at),
KEY idx_user_id (user_id),
KEY idx_action (action),
KEY idx_object_type (object_type),
KEY idx_action_created (action, created_at),
KEY idx_request_id (request_id)
);
request_id (added in 1.3.0) is a short random ID shared by every event logged within the same HTTP/cron request — it's what lets the admin UI collapse a single editorial save (which can fire several hooks: status transition, content diff, Polylang language assignment) into one row instead of 3-6 near-duplicate ones.
Hooks reference
Filters
| Filter | Description |
|---|---|
reslab_al_trusted_proxies |
Array of trusted proxy IP addresses for forwarded-header IP resolution |
reslab_al_viewable_object_types |
array $types, int $user_id — restrict which object_type values the current user can see in the log, export, and REST API. Empty (default) = unrestricted. |
reslab_al_default_roles |
array $roles — roles granted reslab_al_view_log / reslab_al_clear_log / reslab_al_manage_settings on activation and on every schema upgrade. Defaults to [ 'administrator' ]. |
Actions
| Action | Description |
|---|---|
reslab_al_alert_bruteforce |
array $payload — fires after a brute-force alert email is sent. $payload has ip, attempts, window_hours, log_url. |
reslab_al_alert_mass_deletion |
array $payload — fires after a mass-deletion alert email is sent. $payload has user_id, user_login, deletions, window_hours, log_url. |
Use these for integrations a generic webhook POST can't handle (a Telegram bot API call, a signed Slack SDK request, writing to another system, etc.). For simpler cases, set reslab_al_alert_webhook_url in Settings instead — no code required.
REST API
GET /wp-json/reslab-al/v1/events — read-only, paginated (page, per_page, max 200), authenticated via WP Application Passwords, gated by reslab_al_view_log. Accepts the same filter_action / filter_object_type / filter_user / filter_date_from / filter_date_to / filter_ip / filter_search query params as the admin screen, and respects reslab_al_viewable_object_types. Response includes X-WP-Total / X-WP-TotalPages headers.
Security
- All database queries use
$wpdb->prepare()or$wpdb->insert()with format arrays - All output is escaped with
esc_html(),esc_attr(),esc_url(), orwp_kses() - Forms protected with
wp_nonce_field()/check_admin_referer() - Capability checks on every admin action (
reslab_al_view_log,reslab_al_clear_log,reslab_al_manage_settings) - IP resolution only trusts forwarded headers from explicitly whitelisted proxy IPs
- CSV export neutralises cell values that would otherwise be interpreted as spreadsheet formulas (CSV/Formula injection)
- Archive downloads use random filenames + a directory-listing-blocking
index.phpstub, and are only ever served through a nonce +reslab_al_view_log-gated handler — never a public/guessable URL - REST endpoint requires
reslab_al_view_logvia WordPress's standard Application Passwords auth; no custom API-key scheme to get wrong
Development
Integration tests run against a real WordPress + MySQL install (via wp-phpunit/wp-phpunit), including WooCommerce loaded the same way it would be as an active plugin — see tests/README.md for setup and how to run them.
tests/, vendor/, composer.json/composer.lock, and phpunit.xml.dist are dev-only and are never included in a release build (the WordPress.org submission ZIP / SVN trunk should only contain the runtime plugin files listed under File structure above, plus readme.txt).
Changelog
1.4.1
- Fixed: successful logins and logouts were logged with the acting user recorded as "Guest" (
user_id = 0) —wp_login/wp_logoutfire before WordPress considers that user "current" for the request, soget_current_user_id()was still 0 at that point. The tracker now uses the user WordPress itself passes to those hooks. - Fixed: user-deletion events always logged an empty
login—deleted_userfires after the row is already gone fromwp_users, so re-fetching by ID always failed. Now uses theWP_Userobject core passes as the hook's third argument (added in WP 5.5). - Fixed: the REST API endpoint returned a
_doing_it_wrong()notice (wpdb::prepare()called with no placeholders) when hit with no filters active. - Fixed: the log table's
CREATE TABLESQL had inconsistent internal whitespace between column names and types.dbDelta()'s column-definition parser is picky about exactly one space there; the padding made it think every padded column had changed, reissuing 8ALTER TABLE ... CHANGE COLUMNstatements on every version-gated upgrade check — never actually a no-op like the code's own comments assumed. Harmless functionally, but unnecessary DDL against a potentially large log table. - Added:
reslab_al_default_rolesfilter — controls which roles get the plugin's capabilities on activation/upgrade (previously hardcoded toadministratoronly). - Changed: the schema-upgrade lock (
reslab_al_maybe_upgrade_table()) now uses an atomicadd_option()instead of aget_transient()/set_transient()pair, closing a check-then-act race under concurrent requests. - Internal: added a full PHPUnit integration test suite (
tests/, dev-only, see Development below) exercising every tracker, the cron jobs, list-table filtering/grouping, the REST API, and settings sanitization against a real WordPress + MySQL install.
1.4.0
- Added: Mass Deletion Alerts — a second, independent anomaly check (off by default) for one user deleting an unusual number of objects in a short window
- Added: optional webhook URL — alerts are POSTed as JSON alongside email (Slack/Discord/Zapier/Make/n8n); new
reslab_al_alert_{$type}action hook for custom integrations - Added: "Archive before purge" — gzip CSV snapshot of entries saved before the nightly purge deletes them, downloadable from Settings
- Added: read-only REST API (
GET /wp-json/reslab-al/v1/events) for external monitoring/SIEM tools