Guide

Drupal views being modified — change-tracking your views

A content editor opens the homepage and the "Latest articles" block is empty. Or it shows the wrong content type. Or the pager is gone. Or rows that were sorted by date are now sorted by node ID. Nobody deployed anything.

1. Problem

A content editor opens the homepage and the "Latest articles" block is empty. Or it shows the wrong content type. Or the pager is gone. Or rows that were sorted by date are now sorted by node ID. Nobody deployed anything. Nobody filed a ticket. The only thing anyone can say is: "It worked yesterday."

You go to /admin/structure/views, open the view, and stare at the configuration. Something is different — but Drupal doesn't tell you what changed, when it changed, or who changed it. The view's last-modified timestamp is updated. That's it. No diff. No actor. No reason.

If you've ever Googled "drupal views changed who edited", "drupal track view modifications", or "drupal view audit trail" while a stakeholder pings you on Slack, this is the failure mode. Views are configuration entities. Configuration entities are mutable from the UI by anyone with the administer views permission. And by default, Drupal keeps no history of who touched them.

This guide is about catching view modifications the moment they happen, using the views.change signal, the supporting config.import, auth.login_success, and http.request signals around /admin/structure/views, and the log trail that ties them together.

2. Impact

A Drupal view often is the page. The homepage feed, the news listing, the team directory, the search results, the admin moderation queue — they're all views. When a view's filter, sort, display, or row plugin changes, the visible behaviour of the site changes with it. There is no staging gate. There is no review step. The save button writes straight to the active configuration store.

The realistic damage:

  • A filter on status = published is removed and unpublished drafts leak onto the public site.
  • A contextual filter is changed and the view returns content for the wrong taxonomy term — including content from another section of the business.
  • An access plugin is switched from "Permission: view published content" to "None", exposing an admin listing publicly.
  • A pager is disabled and the view dumps 4,000 nodes onto a single page, blowing out memory and tanking response time.
  • Someone exports the view to code, edits in a sandbox, re-imports — and silently overwrites another editor's tweaks.

None of these throw a PHP error. None of these trip uptime monitoring. The site is up. The page renders. The output is just wrong, and the only person who knows what changed is the person who changed it — assuming they remember.

3. Why It’s Hard to Spot

Drupal's UI is honest about a lot of things. View modifications are not one of them.

The Views module shows you a "last modified" timestamp on the listing page. It does not show you who modified it. There is no built-in revisions table for configuration entities — node_revision exists, views_view_revision does not. The audit trail simply isn't there.

Watchdog (the dblog channel) records very little of this. By default, a view save writes one bland entry: "View: has been updated." No diff. No display granularity. No indication of which filter changed. If watchdog has been silenced or rotated, even that disappears.

Configuration synchronization is supposed to help, but only if the team uses it. Most Drupal sites in the wild allow admin users to edit views directly in production, with the active config store as the source of truth. That means changes never pass through config/sync and never show up in a Git diff.

Uptime monitoring won't catch it because the view still returns 200. APM won't catch it because there's no exception. Cache invalidation will hide the change for an hour. By the time the editor notices the wrong content on the homepage, the modification could be six hours old and nobody remembers being logged in.

This is the textbook silent failure: a high-impact change with no surface, no alert, no diff.

4. Cause

The views.change signal fires when Drupal's configuration system commits a write to a view configuration entity. Views are stored as views.view. config objects. Internally, every save flows through Drupal\Core\Config\ConfigFactory::save() and dispatches a ConfigCrudEvent with one of three operations: create, update, or delete.

Each views.change event captures:

  • Actor — the user ID and username from the active session at the moment of save.
  • View name — the machine name of the view (e.g. frontpage, content, taxonomy_term).
  • Operation — created, updated, or deleted.
  • Displays affected — which displays inside the view (page_1, block_2, attachment_1) had their config touched.
  • Originating request — typically a POST to /admin/structure/views/view/ or a config import via Drush.

When the change comes through the UI, you'll see a matching http.request signal for POST /admin/structure/views/... from the same session. When the change comes from drush config:import or a deployment, you'll see a config.import signal instead, with no UI session attached. Correlating views.change with the nearest preceding auth.login_success for the same user gives you a complete chain: who logged in, from where, and what they then modified.

This is the mechanism. The signal is not "Drupal sometimes changes views." The signal is a structured event tied to a specific config write, a specific actor, and a specific HTTP transaction.

5. Solution

5.1 Diagnose (logs first)

You're looking for a views.change event close to the moment the breakage was first reported, then walking backwards through the supporting signals to identify the actor.

Step 1 — Confirm a view was modified at all.

If you're shipping logs through the Logystera Drupal agent, filter for event_type=views.change against the affected entity. If you're on the host directly, the dblog channel and PHP-FPM logs are the starting points:

# Drush dblog query — last 50 view-related entries
drush watchdog:show --type=views --count=50

# Or via SQL if dblog is the watchdog backend
mysql -e "SELECT wid, FROM_UNIXTIME(timestamp), uid, message, variables \
  FROM watchdog WHERE type='views' ORDER BY wid DESC LIMIT 50;" drupal

The views watchdog type produces messages like View: frontpage has been updated. The uid column gives you the actor. This is what surfaces as the views.change signal.

Step 2 — Tie the change to an HTTP request.

Every UI-driven view save is a POST to /admin/structure/views/view/ (or /ajax for inline edits). Search the web server access log:

# Apache / nginx access log — POSTs to the views admin
grep -E 'POST /admin/structure/views' /var/log/nginx/access.log \
  | awk '{print $1, $4, $7, $9}'

# Same idea, last 200 lines, with response code
tail -n 200000 /var/log/nginx/access.log \
  | grep -E 'POST /admin/structure/views' \
  | tail -n 50

Each POST is a candidate http.request signal. Cross-reference the timestamp with the watchdog views.change entry. The IP and user agent on that line are the same session that produced the config write.

Step 3 — Identify the human.

Find the auth.login_success event for that session. Drupal logs successful logins through the user watchdog type:

drush watchdog:show --type=user --severity=Notice --count=100 \
  | grep -i 'session opened'

Or directly:

SELECT FROM_UNIXTIME(timestamp), uid, hostname, message
FROM watchdog
WHERE type='user' AND message LIKE 'Session opened%'
ORDER BY wid DESC LIMIT 50;

The auth.login_success signal feeding Logystera carries the same uid, hostname, and timestamp. Match the login session that overlaps the http.request POST and you have the actor.

Step 4 — Rule out a config import.

If the views.change event has no matching http.request to /admin/structure/views, the change came from CLI. Look for config.import signals or, on the host:

grep -E 'config:import|Config import' /var/log/syslog /var/log/drush.log

A deployment, a drush cim, or a script run as the www-data shell account will leave traces here, not in the web access log. This distinction matters: a UI edit is a person; a config.import is a pipeline. The fix path is different.

Step 5 — Diff the actual change.

Drupal stores the active view config in the config table (or the sync directory if file-based). Export both versions and compare:

# Export current view config to YAML
drush config:get views.view.frontpage > /tmp/frontpage.now.yml

# Compare against the version in your sync directory (Git-tracked)
diff /tmp/frontpage.now.yml ../config/sync/views.view.frontpage.yml

If you don't have config/sync populated, restore the previous version from a database backup, dump it, and diff. The diff is the real story — filter changes, sort changes, display changes, all line by line.

5.2 Root Causes

(see root causes inline in 5.3 Fix)

5.3 Fix

The fix depends on what the diff shows and who made the change. Three realistic root causes, in order of frequency:

1. Editor used the UI in production, broke a filter or sort. This produces a views.change event with a matching UI http.request and a recognisable auth.login_success. The fix is to revert the view configuration. If config/sync is the canonical source, run:

drush config:import --partial --source=../config/sync \
  --diff views.view.frontpage
drush cache:rebuild

If sync isn't maintained, restore the view from a backup or rebuild the changed display by hand. Then audit the editor's permissions — administer views is broader than most editor roles need.

2. A drush cim overwrote local edits. This shows up as a config.import signal with no preceding UI http.request for that view. Multiple views may have changed at once. The fix is to export production config first (drush cex), reconcile against the deployment payload, and re-import the merged result. Long-term, stop allowing UI edits to views that are tracked in Git, or move those views into config-readonly enforcement (e.g. the Config Readonly module).

3. A privilege escalation or compromised account. This is the case where auth.login_success precedes a burst of views.change and config.import events from an unfamiliar IP, often outside business hours. Treat as an incident. Lock the account, rotate the affected user's credentials, force-logout all sessions (drush user:logout), audit recent wp.state_change-equivalent config writes (config.* events), and restore views from the last known-good backup.

For all three, after restoring the view, clear render and views caches:

drush cache:rebuild
drush views:dev:invalidate-caches  # if devel_views is installed

5.4 Verify

The signal you're verifying against is views.change. After the restore:

  • Confirm the view renders correctly. Load the page, log in as an editor, browse the displays you care about.
  • Watch for the absence of new unexpected views.change events. In Logystera, set the filter for event_type=views.change AND view_name= and confirm no new entries appear in a 30-minute window of normal traffic.
  • On the host, tail the watchdog:
drush watchdog:tail --type=views

If no new View: has been updated. lines appear during a normal editor session that isn't touching that view, the fix is stable.

  • Re-run the diff:
drush config:get views.view.frontpage > /tmp/frontpage.verify.yml
diff /tmp/frontpage.verify.yml ../config/sync/views.view.frontpage.yml

A clean diff (no output) means the active store matches the canonical version.

  • Healthy looks like: zero unexpected views.change signals, http.request POSTs to /admin/structure/views only from known editor accounts during business hours, and config.import signals only during scheduled deployment windows.

If a views.change event fires for the same view again within an hour with no corresponding deployment, you haven't fixed the root cause — you've fixed the symptom. Go back to step 3 of diagnosis and find the actor.

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 views.change.

The hard part of this failure mode isn't fixing it. The hard part is knowing it happened.

Drupal does not alert you when a view changes. It does not alert you when a view is deleted. It does not alert you when a config import overwrites an active edit. Watchdog records a one-line note that nobody reads, and dblog rotates it out within days. By the time a user complains about the homepage, the audit trail is already thin.

This is exactly the type of issue that surfaces as the views.change signal, which Logystera detects and alerts on early. Every view save — UI, Drush, or config import — emits a structured event with the actor, the view machine name, the operation, and the affected displays. Correlation with auth.login_success ties it to a human. Correlation with http.request ties it to a session. Correlation with config.import separates pipeline changes from people changes. The signal is the thing that turns a silent config write into an alert before an editor notices the homepage is wrong.

You don't have to write a custom module. You don't have to bolt on the Config Log module and hope someone reads it. The detection is in the signal pipeline; the work is in deciding what's normal — production view edits during business hours by known editors — and what isn't.

The point isn't tooling. The point is: a view modification on a production Drupal site is a high-impact event with no native audit trail. It deserves an alert.

7. Related Silent Failures

These failures sit close to views.change in signal space. If you're investigating one, you should be aware of the others.

  • Configuration drift between environmentsconfig.import and config.export events that don't match a deployment, indicating manual edits in production that diverge from staging.
  • Privilege escalation through admin rolesauth.login_success followed by a burst of views.change and other config.* writes from an unfamiliar IP or an account that doesn't usually touch admin paths.
  • Permissions changes affecting access pluginsconfig.change on user.role.* that quietly grants administer views, administer permissions, or access content overview to a role that shouldn't have it.
  • Block placement and theme region overridesconfig.change on block.block.*, similar mechanic to views: a single save changes what users see, with no diff in the UI.
  • Module install/uninstall via UIsystem.modules.installed or system.modules.uninstalled events that ship with their own config writes and frequently take views and blocks down with them.

Each of these has the same shape: an admin-path config write, no exception, no 5xx, no native alert. The defence is the same — log the signal, correlate with the actor, alert on the unexpected.

See what's actually happening in your Drupal system

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.