Guide

Tracking "what changed" on WordPress vs Drupal — two metric models, one diagnostic question

You run both WordPress and Drupal — an agency portfolio, a multi-CMS publisher stack, marketing on WP and the customer portal on Drupal — and at 14:47 UTC something broke.

1. Problem

You run both WordPress and Drupal — an agency portfolio, a multi-CMS publisher stack, marketing on WP and the customer portal on Drupal — and at 14:47 UTC something broke. PagerDuty is paging on a 5xx spike, support is forwarding "site is slow" tickets, and the only useful question in the war room is the one nobody can answer in less than fifteen minutes:

What changed in the last hour?

On WP you'd grep plugin activations. On Drupal you'd look at config_import and module installs. But you have eight WP installs and three Drupal sites, two audit-trail conventions, and the on-call keeps asking "is this WP or Drupal?" because the answer changes which dashboard they open. This is the textbook "wordpress vs drupal how to track plugin update caused incident" scenario, and it surfaces — on WordPress — as wp_state_changes_total plus wp_environment_changes_total, and — on Drupal — as drupal_deployment_events_total plus drupal_config_import_total plus drupal_module_changes_total. Same diagnostic question, two metric models, one dashboard to unify them.

The trap: each platform's audit trail is internally consistent, but they don't map 1:1. WordPress splits "what changed" across two metrics. Drupal collapses it across three. Ask a question shaped like one platform on the other and you'll miss the change that caused the incident.

2. Impact

In a war room, the cost of the change-tracking gap is measured in minutes-to-suspect. A unified change view turns "what changed in the last hour" into a 10-second answer. Without it, the on-call is doing parallel platform-specific archaeology while the page count climbs.

For an agency running 30 WP sites and 5 Drupal sites: an unattributed 14:47 plugin auto-update on a single high-traffic WP install can break checkout for 90 minutes before someone notices that WooCommerce 9.4.2 activated at 14:46 — the wp_state_changes_total counter went up by 1 at exactly the right minute, and nobody was watching. The Drupal equivalent is a drush cim from CI that imported a stale core.extension.yml and disabled a critical module — drupal_module_changes_total ticks once, drupal_config_import_total ticks once, and /admin/reports/dblog is the only place anyone looks. Two platforms, two blind spots, one outage.

Post-incident review carries a quieter cost. When timeline reconstruction means stitching WP option_value diffs with Drupal config audit rows and two phpinfo() snapshots, the postmortem either takes a day or skips "what changed" entirely. For multi-CMS hosts, the change-tracking asymmetry is the single biggest reason incidents get blamed on "infrastructure" when they were CMS-level config drift.

3. Why It’s Hard to Spot

The asymmetry between WordPress and Drupal change-tracking is not arbitrary — it reflects how each platform models change — and that's exactly what makes it hard to monitor uniformly.

WordPress treats change as two separable streams. Plugin and theme activations are app-level events: activate_plugin, deactivate_plugin, switch_theme — they fire WP hooks and write to wp_options. PHP version, WP core version, MySQL version are environment-level facts: they don't fire hooks; they're observed at request time. WP doesn't unify them because internally they aren't unified. The Logystera WP plugin reflects this with two metrics: wp_state_changes_total for app-level transitions and wp_environment_changes_total for runtime shifts.

Drupal treats change as a configuration system, full stop. Modules, themes, blocks, view modes, fields, and most "settings" live in YAML config files synced via drush cim/drush cex. There is no separate "plugin activation" event because activation is a config import. The Drupal agent reflects this by emitting drupal_deployment_events_total for deploy/install events, drupal_config_import_total for the drush cim operation, and drupal_module_changes_total for the subset of config changes that toggle modules in core.extension.yml.

A WP engineer looking at a Drupal incident asks "where's the plugin activation log?" and finds nothing — the equivalent event is buried in a core.extension.yml diff. A Drupal engineer looking at a WP incident asks "where's the config diff?" and finds nothing — there is no config-as-code equivalent. Each platform's audit trail is invisible from the other's mental model. Standard tools make this worse: New Relic groups everything under "deployments," Datadog gives you "events." Neither knows wp_state_changes_total and drupal_module_changes_total are answering the same question, so neither lets you ask it once.

4. Cause

The diagnostic question — "what changed in the last hour?" — is universal. The signals that answer it are platform-specific, and the mapping is the load-bearing thing.

On WordPress, the Logystera plugin hooks activated_plugin, deactivated_plugin, switch_theme, and upgrader_process_complete. Each fires wp.state_change with event_type=plugin_activated (or plugin_deactivated, theme_switched, plugin_updated). The processor counts these into wp_state_changes_total, labelled by entity_id, event_type, target (plugin/theme slug), and version. Separately, on every request the plugin captures PHP_VERSION, $wp_version, and the MySQL server version. When any change between requests it emits wp.environment_change → counted into wp_environment_changes_total, labelled by component and from/to versions.

On Drupal, the agent hooks ConfigImporter events, \Drupal::moduleHandler()->install()/uninstall(), and hook_update_N execution. The aggregate "something was deployed" signal is drupal_deployment_events_total (event_type: deploy_started/completed/hook_update_n_run). The narrower "config changed" signal is drupal_config_import_total, fired once per drush cim, labelled by config_count and import_source. The narrowest "modules toggled" signal is drupal_module_changes_total, fired only when core.extension.yml diffs install or uninstall a module, labelled by module, action, and version.

Same diagnostic question. WordPress answers in 2 metrics; Drupal in 3.

5. Solution

5.1 Diagnose (logs first)

The diagnosis path is mechanical: pivot off incident time, pull change events from both platforms in parallel, time-correlate, unify into one timeline.

1. WordPress — pull wp_state_changes_total and wp_environment_changes_total for the incident window.

If you're using the WP plugin's local audit log (default at wp-content/uploads/logystera/audit.log) you can grep directly:

# All app-level state changes in the 14:00–15:00 window
grep -E '"event_type":"plugin_(activated|deactivated|updated)|theme_switched"' \
    /var/www/wp-site/wp-content/uploads/logystera/audit.log \
  | grep '"timestamp":"2026-04-27T14:'
# → surfaces every wp.state_change → counted into wp_state_changes_total
# Environment-level changes (PHP/WP/MySQL versions) in the same window
grep '"signal":"wp.environment_change"' \
    /var/www/wp-site/wp-content/uploads/logystera/audit.log \
  | grep '"timestamp":"2026-04-27T14:'
# → surfaces wp.environment_change → counted into wp_environment_changes_total

The line you want looks like this — the target and version are the diagnostic keys:

{"signal":"wp.state_change","event_type":"plugin_activated",
 "target":"woocommerce","version":"9.4.2","timestamp":"2026-04-27T14:46:11Z"}

That 2026-04-27T14:46:11Z activation 36 seconds before the 14:47 5xx spike is the entire postmortem.

2. Drupal — pull drupal_deployment_events_total, drupal_config_import_total, drupal_module_changes_total for the same window.

# Drupal deployment events (deploys, hook_update_N) in the window
drush -r /var/www/drupal watchdog:show --type=deploy --count=50 \
  | grep "2026-04-27 14:"
# → surfaces drupal_deployment_events_total

# Config imports (drush cim invocations)
drush -r /var/www/drupal watchdog:show --type=config --severity=Notice \
  | grep -i "config import" | grep "2026-04-27 14:"
# → surfaces drupal_config_import_total

# Module install/uninstall events from core.extension.yml diffs
drush -r /var/www/drupal watchdog:show --type=system --count=100 \
  | grep -iE "module (installed|uninstalled)" | grep "2026-04-27 14:"
# → surfaces drupal_module_changes_total

3. Time-correlate across both platforms with the most recent change window.

The point of a unified change view: you don't need to remember which platform you're on — you ask the same question of both.

# Cross-platform "what changed in the last hour" — single shell pipeline
START="2026-04-27T14:00"
END="2026-04-27T15:00"

# WP side
for site in /var/www/wp-*; do
  echo "=== $(basename $site) ==="
  grep -E '"signal":"wp\.(state_change|environment_change)"' \
       "$site/wp-content/uploads/logystera/audit.log" \
    | grep -E "\"timestamp\":\"${START}|${END}\""
done

# Drupal side
for site in /var/www/drupal-*; do
  echo "=== $(basename $site) ==="
  drush -r "$site" watchdog:show --count=200 \
    | grep -E "config import|module installed|module uninstalled|hook_update_N" \
    | grep "2026-04-27 14:"
done

If the first 5xx lines up with a wp.state_change and there's nothing on the Drupal side, the change is on WP. If it lines up with a drupal_module_changes_total tick and nothing on WP, it's Drupal. The discipline is asking both questions every time, not guessing platform first.

5.2 Root Causes

Each cause maps to a specific signal. Prioritized by frequency in mixed estates.

  • WP plugin auto-update broke a hot path — WP's background updater fired upgrader_process_complete and a major-version bump introduced a fatal. Produces wp_state_changes_total{event_type="plugin_updated"}, often immediately followed by wp_php_fatal_total or 5xx spike. The smoking gun is the target and version labels.
  • WP theme switch from staging tooling — a deploy script or misclicked admin promoted a staging theme. Produces wp_state_changes_total{event_type="theme_switched"} and a cascade of CSS-asset 404s.
  • WP PHP version bump from the host — managed host silently rolled PHP 8.2 → 8.3 or back. Produces wp_environment_changes_total{component="php"} with from/to labels. Surfaces hours later as fatals from contrib plugins not tested on the new runtime.
  • Drupal drush cim from CI pulled a stale config tree — long-lived feature branch deployed with core.extension.yml older than prod, uninstalling modules. Produces drupal_config_import_total with high config_count, plus drupal_module_changes_total{action="uninstalled"} per affected module.
  • Drupal hook_update_N failed mid-deploydrush updatedb crashed partway. Produces drupal_deployment_events_total{event_type="hook_update_n_run"} followed by deploy_failed. Site is half-migrated — the worst kind.
  • Drupal contrib module toggled via UI — somebody clicked through /admin/modules instead of going through config. Produces drupal_module_changes_total{action="installed"} without a paired drupal_config_import_total — that mismatch is itself diagnostic.
  • Cross-platform: same upstream change deployed on both stacks — a CI pipeline rolled a shared Composer dependency at the same time. You'll see wp_state_changes_total and drupal_deployment_events_total increment within the same minute. Without a unified dashboard this looks like two separate incidents.

5.3 Fix

Match the fix to which signal told you the truth, not to a guess about which platform is "more likely" at fault.

Cause A — WP plugin update: roll back, disable auto-updates for the plugin, pin the version in deployment config.

wp plugin install woocommerce --version=9.4.1 --force --activate
wp config set AUTOMATIC_UPDATER_DISABLED true --raw

Cause B — WP theme switch: revert via WP-CLI; do not click through /wp-admin/themes.php — the broken theme loads its own admin head.

wp theme activate previous-theme-slug

Cause C — WP PHP version drift: pin PHP version in the hosting console (WP Engine: User Portal → Site → PHP). Self-hosted: assert php -v in the deploy pipeline.

Cause D — Drupal stale cim: roll back, then re-export config from prod before the next attempt: drush cex && git commit && git push. Never drush cim from a branch unsynced with prod's exported config.

Cause E — Drupal hook_update_N half-failed: check key_value for the failing module's schema version. Mark the update complete only after verifying the migration ran. Do not re-run blindly.

SELECT name, value FROM key_value
 WHERE collection = 'system.schema' AND name = 'failing_module';

Cause F — Drupal manual UI module change: force a re-sync. drush cex -y to capture current state, review diff, decide whether intentional, then commit or drush cim -y to revert.

Cause G — Cross-platform simultaneous deploy: if both stacks ticked at the same minute, treat it as one incident. Roll back the upstream change once, not twice.

5.4 Verify

After the fix you're looking for two things to hold simultaneously across both platforms: change-event signals settle back to baseline and the downstream error signals (wp_request_errors_total, drupal_server_errors_total) return to their pre-incident floor.

# WP: no new wp.state_change or wp.environment_change for 30 min
tail -F /var/www/wp-*/wp-content/uploads/logystera/audit.log \
  | grep -E '"signal":"wp\.(state_change|environment_change)"'

# Drupal: no new module/config/deploy events for 30 min
drush -r /var/www/drupal watchdog:show --tail \
  | grep -iE "config import|module (installed|uninstalled)|hook_update_N"

Baselines differ by metric:

  • wp_state_changes_total0–3 events/site/week (legitimate plugin/theme updates in maintenance windows). Non-zero outside your window is anomalous.
  • wp_environment_changes_total0/month unless actively migrating PHP. Any unscheduled increment is anomalous.
  • drupal_deployment_events_total — matches your deploy cadence. 4-deploys-a-week ≈ 16/month; 4-deploys-a-day ≈ 480/month.
  • drupal_config_import_total — one event per deploy, no more. Two without a paired drupal_deployment_events_total means somebody ran drush cim manually on prod.
  • drupal_module_changes_total0 unless a deploy is in progress. Module changes outside deploy windows always warrant investigation.

If wp_request_errors_total settles but wp_state_changes_total is still firing every few minutes, you reverted the symptom plugin but missed an upstream automation re-activating it. Stop the automation before you call it resolved.

6. How to Catch This Early

Fixing it is straightforward once you know the cause. The hard part is knowing it happened at all.

This issue surfaces as wp_state_changes_total (WordPress) paired with drupal_deployment_events_total (Drupal) — two metrics, one diagnostic question.

Everything you just did manually — grep WP audit logs, run drush watchdog:show against three Drupal sites, time-correlate across two timestamp formats, build a unified timeline by hand — Logystera does automatically. The WP plugin and the Drupal agent each emit their platform-native change signals, and the unified dashboard renders them on a shared time axis. The "wordpress drupal change audit trail comparison" exists exactly once: as a single Logystera entity-group panel.

!Logystera dashboard — wp_state_changes_total and drupal_deployment_events_total over time Cross-platform change events, last 24h — WP wp_state_changes_total (top) and Drupal drupal_deployment_events_total (bottom) on a shared time axis. Spike at 14:46, immediately before the 14:47 5xx incident.

The dashboard pattern is one panel per platform, side-by-side, on the same time axis. The asymmetry stays — WP shows two stacked series, Drupal shows three — but the question you ask of them is identical: is there a vertical line within 5 minutes of the incident?

The rule that fires is id 511 — Unattributed change in CMS estate, severity warning, threshold any change event outside declared maintenance windows. Maintenance windows are declared per-entity-group; events inside are silenced, events outside trigger.

!Logystera alert — Unattributed change in CMS estate Warning alert fires within 60s of any wp_state_changes_total or drupal_deployment_events_total event outside the declared maintenance window, including the affected entity, signal name, and target.

The alert payload includes the platform (so on-call knows which runbook to open), the signal name, the target/module/component label, version from/to, and entity name. That's enough to skip the "is this WP or Drupal?" question and go straight to the right rollback command.

The fix is simple once you know the problem. The hard part is knowing it happened at all — and across two platforms with asymmetric audit-trail conventions, "knowing" used to mean fifteen minutes of parallel grepping. Logystera turns "what changed in the last hour" from a war-room scramble into a 60-second notification with the platform, the signal, and the target that proves it.

7. Related Silent Failures

  • wp_php_fatal_total immediately after wp_state_changes_total — the canonical "plugin update broke the site" cascade. Covered in WordPress 500 error after plugin update.
  • drupal_server_errors_total after drupal_module_changes_total — the Drupal equivalent. A drush cim uninstalls a module another module depended on; 5xx follows within seconds.
  • wp_environment_changes_total without wp_state_changes_total — silent host-driven PHP/MySQL version bump. No plugin/theme touched, but the runtime changed. Surfaces hours later as deprecation-driven fatals.
  • drupal_config_import_total without drupal_deployment_events_total — somebody ran drush cim on prod by hand. The mismatch is itself the alert.
  • Cross-platform deploy correlation — one Composer dependency bump produces simultaneous wp_state_changes_total and drupal_deployment_events_total. Without a unified view this looks like two unrelated incidents — which is how shared-library regressions stay un-root-caused for days.

See what's actually happening in your systems

Connect your site. Logystera starts monitoring within minutes.

Logystera Logystera
Monitoring for WordPress and Drupal sites. Install a plugin or module to catch silent failures — cron stalls, failed emails, login attacks, PHP errors — before users report them.
Company
Copyright © 2026 Logystera. All rights reserved.