User Audit
Version 2.1.2
May 5, 2026
Fixed
- Logs index threw
Integrity constraint violation 1052: Column 'dateCreated' in order clause is ambiguousbecause the v2.1.0 LEFT JOIN onto{{%elements}}introduced a seconddateCreatedinto scope and the controller's ORDER BY used the unqualified column.buildQuery()now qualifies every WHERE / ORDER BY reference with the resolved audit-table name. - Logs index also returned a row containing only
elementDateDeleted(and nothing else) becauseaddSelect()on a Yii AR query withselect === nullreplaces the implicitSELECT *instead of appending to it. BothactionIndexandactionExportnow use an explicitselect()that includes<audit_table>.*plus the aliasedelementDateDeleted. - Declared
UserActivityLog::$elementDateDeletedas a public property so AR'spopulateRecord()accepts the aliased column viacanSetProperty().
Version 2.1.1
May 5, 2026
Fixed
- Logs index threw
SQLSTATE[42000] 1064 syntax errorbecause the v2.1.0 elements LEFT JOIN nested{{%user_activity_log}}inside a[[…]]quoting block. Yiis quoter unwraps[[col]]and{{%table}}separately and refuses to nest them, so the literal[[user_activity_log.elementId]]string ended up in the SQL. Resolves the table name withgetRawTableName()first and embeds the raw name. Same fix applied toactionExport.
Version 2.1.0
May 5, 2026
Changed
- Walked back the v2.0 element-index UX. The CP logs page at
/admin/user-auditis back to a hand-rolled filter bar at the top- single-table query + custom table, like v1.x — but with the
status-pill column ported over from v2.0 (login=green,
logout=gray, login_failed=orange, login_blocked=red,
session_expired=blue, custom=fuchsia) and the time column made
click-through to the read-only detail view at
/admin/user-audit/log/<elementId>. The element-index source sidebar on the left is gone.
- single-table query + custom table, like v1.x — but with the
status-pill column ported over from v2.0 (login=green,
logout=gray, login_failed=orange, login_blocked=red,
session_expired=blue, custom=fuchsia) and the time column made
click-through to the read-only detail view at
- Filter bar gained an Include archive checkbox. Off by default — rotated (soft-deleted) entries are hidden so the live activity stays the focus. On → trashed rows show in the list with a Rotated badge next to the timestamp and a dimmed row.
- AuditLog elements are no longer indexed by Crafts search engine
(
defineSearchableAttributes()returns an empty array). ActivityLogService writes are no longer slowed down by a search-index job per row.
Why the walk-back
The v2.0 element-index needed two joins on user_activity_log plus
left-joins on elements and elements_sites per page load, which
became the dominant cost on large audit tables (5+ table scans
where v1.3.2 had one). The custom filter form covers the same
filtering need with single-table queries and stays snappy at 100k+
rows. The element layer underneath stays intact — element rows,
elementId FK, Craft-Trash soft-delete, hard-purge console command
and CSV deleted_at column are unchanged from v2.0.
Notes
- No data migration is required. v2.0 → v2.1 is purely a UI swap on top of the same schema.
Version 2.0.1
May 5, 2026
Fixed
- v2.0 conversion migration crashed with
UnknownMethodException: ...::stdout()when run from the CP web updater.stdout()is acraft\console\Controllermethod, not acraft\db\Migrationmethod — the CLI runner injects one at runtime but the web updater (UpdaterController::actionMigrate) does not. Replaced with a localnote()helper that uses plainecho(captured by both runners) and mirrors toCraft::info()for persistent log. - The crash happened before any audit row was backfilled, so the schema half (elementId column + unique index + FK) was applied but no element rows were created. v2.0.1 re-running the same migration is idempotent — it skips the schema-add steps and proceeds straight to the backfill.
Version 2.0.0
May 5, 2026
Breaking
- The audit log has been promoted to a first-class Craft element
type. Existing audit rows are migrated automatically: the install
migration adds an
elementIdFK to{{%user_activity_log}}and back-fills one element per existing row in 500-row batches. Resumable on interruption — rerun./craft upif it stops mid-way. Plan for upgrade time roughly proportional to row count (~10 ms/row direct insert). ./craft user-audit/purge/runno longer hard-deletes. It now soft-deletes via Crafts element trash (setsdateDeleted). Rotated rows disappear from the default index but remain queryable via the new "Soft-deleted" source and exportable via CSV with the newdeleted_atcolumn.- Hard-delete is now an explicit, console-only operation:
./craft user-audit/purge/hard --before=YYYY-MM-DD [--user-id=N]. Refuses to run without at least one filter, prompts for confirmation interactively, and warns that hard-deleted rows are unrecoverable and will not appear in any subsequent CSV export. - The CSV export now contains an additional trailing
deleted_atcolumn (empty for live rows, ISO timestamp for soft-deleted ones). Downstream importers that hard-coded the column count must adjust.
Added
- Standard Craft element-index UI for
/admin/user-audit: source-sidebar with quick filters (All, by event type, by context, Soft-deleted archive), status pills coloured by event type (login=green, logout=gray, login_failed=orange, login_blocked=red, session_expired=blue, custom=fuchsia), sortable sticky-header table, native search index, ajax pagination, column picker that remembers per-user preference, keyboard navigation. - New read-only detail view at
/admin/user-audit/log/<elementId>, reachable by clicking the title in the index. Shows identity, context, network, user-agent (parsed + raw) and the JSON metadata payload of custom events. Soft-deleted entries display a banner with the rotation timestamp. "Show user trace" button jumps to the per-user dashboard whenuserIdis set. ./craft user-audit/purge/hardconsole command (see Breaking).
Changed
ActivityLogService::log()now writes the element shell first and then the audit row in a single DB transaction, so a save-failure on either side leaves no orphans behind. Public signature unchanged — callers feel nothing.- The legacy hand-rolled filter form, ad-hoc sortable headers and custom pagination on the logs index were removed; their job is handled by the standard Craft element-index layout now.
Notes for v2.0 release planning
- v2.0 ships option α for the detail view (full-page detail reachable via title-click). Slideout option β (Element-Editor-based) was dropped from the v2.0 scope to avoid fighting Crafts new element editor — page-level detail is robust and a follow-up release can iterate on slideout polish.
Version 1.3.2
April 29, 2026
Changed
- Logs list page size lowered from 100 to 50 entries per page, matching Craft's element-index default. The Twig render of 11 columns × per-row macros at 100 rows was the dominant cost on the page; halving it cuts log-list render time roughly in half without losing useful density.
Performance
- User trace page now executes a single roll-up query for the six
headline stats (total, logins 24h/7d, failed 24h/total, blocked
total) via conditional
SUM(CASE WHEN …)aggregation, replacing six separateCOUNT(*)round-trips. The CASE-WHEN form stays portable across MySQL/MariaDB/Postgres. - User trace login heatmap (7 × 24 buckets over 90 days) is now
bucketed at the database level with a single
GROUP BY weekday, hourquery — result set is bounded to ≤168 rows regardless of how active the user is. v1.3.1 pulled every login row of the last 90 days into PHP and counted them in a foreach, which turned the page into a multi-second load for power users with thousands of login events. Driver-specific weekday expressions (WEEKDAY+1on MySQL/MariaDB,EXTRACT(ISODOW)on Postgres) keep the 1=Mon..7=Sun mapping identical to the previous PHP-sideformat('N').
Version 1.3.1
April 27, 2026
Changed
- Email / Login column in the Logs list is now also a link to the
user trace page when the row has a
userId. Failed-login rows without a matched user keep showing the email as plain text.
Version 1.3.0
April 24, 2026
Added
- User trace page at
/admin/user-audit/user/<userId>— drill into the full activity log of a single user. Reachable by clicking the user-id in the User column of the Logs list. - Identity card surfacing user name, email, group memberships and account state (active / suspended / locked). Deleted Craft users remain inspectable via the email + groups snapshot stored on the user's last audit row.
- Headline stat cards: total events, logins (24h / 7d), failed logins (24h / total), blocked attempts (total).
- Last-login and last-failed-login summary lines including IP, device, OS and browser context.
- Login pattern heatmap: 7 (Mon-Sun) × 24 (hours of day) grid showing when this user actually uses the system over the last 90 days. Cell color intensity scales with the bucket count; hover reveals the exact time-slot and count.
- Top-IPs / top-devices / top-browsers boxes (last 90 days).
- Sortable, paginated activity log (50 per page) scoped to the user.
Changed
- User column in the Logs list is now a link when
userIdis set; failed logins without a matching user keep showing a plain—.
Version 1.2.2
April 24, 2026
Fixed
- Logs table distended the whole CP content area wider than the
viewport on narrow windows instead of scrolling inside its own
bounds. The table is now wrapped in a
.ua-table-scrollelement withdisplay: block,width: 100%,min-width: 0andoverflow-x: auto. Themin-width: 0override is the critical piece — Craft's content pane is a flex container, and flex children default tomin-width: autowhich refuses to shrink below their intrinsic content width. Without that override the wrapper kept growing with the table andoverflow-x: autonever triggered. Inner.tableviewoverflow is neutralized inside the wrapper, and.data/.data.fullwidthare set towidth: auto; min-width: 100%so the table keeps its natural column widths and scrolls horizontally inside its own frame.
Version 1.2.1
April 23, 2026
Fixed
- Plugin settings page threw
Twig\Error\SyntaxError("Unexpected endjs tag (expecting closing tag for the namespace tag defined near line 195)") because a JS comment inside a{% js %}block referenced Craft's wrapper tag literally as{% namespace 'settings' %}. Twig parses the body of{% js %}blocks too — it's not a raw region — so the literal braces were read as a real tag opening. Comment rephrased without Twig syntax.
Version 1.2.0
April 23, 2026
Fixed
- Monitor chart stayed blank when every row in the audit log had
client = NULL(the common case on fresh installs where all activity is CP logins — CP requests don't carry a client type). The default query always appendedWHERE context IN (…) AND client IN (…)which silently excluded NULL columns. Now "all options selected" is treated as "no filter on this dimension" and NULL-column rows are included; partial selections still exclude NULLs as operators expect. - Stat cards on top of the Monitor page use the same rule so the header numbers match what's in the chart.
Changed
- Page titles renamed:
- Logs → User audit logs (DE: "User Audit Logs")
- Monitor → User audit monitor (DE: "User Audit Monitor") The subnav items in the CP sidebar keep their short labels (Logs, Monitor) so the navigation stays compact.
- Monitor chart series regain per-line area fills at
fill-opacity: 0.08. Up to five overlapping series stack additively without turning muddy, and on sparse data the faint tint surfaces which series has activity before the stroke alone would show it. - All/None toggle in the filter dropdowns uses a slash separator
(
All / None) instead of a middle dot — reads more clearly as two opposite actions. - German translations consolidated: dead strings from the
removed Active Sessions page pruned, new filter/monitor
strings added. Current state: 96 keys in code = 96 keys in
de/user-audit.php, no missing, no orphans.
Version 1.1.6
April 23, 2026
Added
- Monitor chart is now multi-series: one colored line per event type
(
login,logout,login_failed,login_blocked,session_expired). The line colors match the swatches next to each event checkbox in the filter dropdown, and a legend appears below the chart when more than one series is visible. - Event / context / client checkbox dropdowns gain an All / None toggle at the top — one click to reset a dimension.
- Filter defaults: first-time visits now show every event / context /
client pre-selected (full dataset out of the box). A hidden
_filterssentinel lets the form legitimately submit "none selected" without being mistaken for a fresh page load. - Hover tooltip lists every visible series' count at the hovered bucket with a matching color dot, plus a Σ total when more than one series is shown. A dashed vertical guide line and per-series accent dots highlight the active column.
Fixed
- Nav: main "User Audit" item now stays highlighted (subnav open) while on the Monitor page. Parent URL was pointing at /logs, which made Craft's prefix-match consider /monitor an unrelated top-level URL. Restored to the plugin root.
Version 1.1.5
April 23, 2026
Added
- Monitor: time-range options now include This month, Last 30 days and Last 90 days in addition to the existing 24 h / 48 h / 7 d. This month is calendar-bounded (1st of the current month → today); all other ranges are trailing windows.
- Monitor chart: Y-axis now shows 5 numeric ticks (0 → max event count) so the vertical scale is readable, not just implied.
- Monitor chart: interactive hover tooltip. Moving the cursor across the chart highlights the nearest bucket with an accent dot and shows a floating tooltip with the richer bucket label and event count.
- Event / Context / Client filters on the Monitor are now multi-select checkbox dropdowns — pick any combination, the chart updates accordingly. The range picker stays single-choice.
Changed
- Monitor URL parameter switched from
?hours=Nto?range=<key>(24h/48h/7d/month/30d/90d). Old?hours=URLs are mapped to the nearest new range so existing bookmarks stay valid. - Filter URL parameters are now arrays:
?event[]=login&event[]=logoutetc. Single-value URLs (?event=login) remain accepted.
Fixed
- Smooth-curve chart no longer renders segments below the x-axis when a sudden drop to zero triggered a Catmull-Rom overshoot. Control points are now clamped inside the plot area.
Version 1.1.4
April 23, 2026
Added
- Reset to defaults button at the bottom of the settings page. Refills every form field with its class-declared default without touching saved state — the reset is only persisted when the operator clicks Save afterwards, so it doubles as a safe preview of the defaults.
Version 1.1.3
April 23, 2026
Added
- Subnav under the User Audit control-panel item:
- Logs — the filterable, paginated event list (previously the root).
- Monitor — a dedicated activity dashboard with stat cards and a smooth-curve time-series chart. Clicking the main nav item lands on Logs.
- Monitor view: filterable by event, context and client (same dropdowns as the log list) plus a time-window picker (24 h / 48 h / 7 d). Buckets auto-switch between hourly and daily based on the window.
- Log list: column headers are now sort links (↑ / ↓ / ↕). Sort column and direction are preserved across filter changes and pagination.
Changed
/user-audit/activenow redirects to the Monitor view.
Removed
- The standalone Active Sessions page — its role is absorbed by the Monitor dashboard.
Version 1.1.2
April 23, 2026
Changed
- Breaking: Viewer access is now admin-only by default. The previous
user-audit-viewpermission has been removed. Any CP user who should keep access must either be an admin or be a member of a group listed under the new Access → Allowed user groups setting. - Settings page: new Access section at the top with a user-group picker (stores group UIDs, safe to deploy via project config).
Migration
- On upgrade, non-admin users lose access until the deploying admin opens Settings → Plugins → User Audit and whitelists the relevant CP user group(s). Admins are unaffected.
Version 1.1.1
April 22, 2026
Added
clientcolumn on{{%user_activity_log}}: storespwaorbrowserbased on theX-Reest-Clientrequest header. Indexed, filterable in the CP viewer.userGroupscolumn: comma-separated snapshot of the user's group handles at login time. Searchable.recordClientTypesetting (lightswitch): when off, the column is left NULL and the client filter is hidden.- CP index: settings-button in the top-right, live filter (debounced 350 ms, activates from 2 characters), new Client and Groups columns, extended search across email/groups/IP/browser+version/OS+version/device/ failureReason/eventType/client.
Changed
- Settings page: default strings are now English; German translations live
in
src/translations/de/user-audit.php. - CSV export now includes
clientanduserGroupscolumns.
Fixed
- CSV export previously returned
ERR_INVALID_RESPONSEbecause the stream callable didn't yield. Rewritten as a generator yielding CSV lines.
Version 1.1.0
April 22, 2026
Added
contextcolumn (cp/fe). Control-panel logins are taggedcp, frontend loginsfe, console/custom events stay NULL.- Per-context recording toggles (
recordCpEvents,recordFrontendEvents). Disabled contexts are skipped before the log write, not filtered later, so purge and stats queries stay accurate. - Context filter and badges in the CP viewer.
- Mail subject uses the new translation channel.
Version 1.0.0
April 22, 2026
Added
- Initial release.
- Automatic logging of
login,logout,login_failed,login_blocked,session_expired. - Custom events API (
ActivityLogService::log). - Regex-only user-agent parser (device type, OS, browser) — zero runtime dependencies.
- CP viewer (index, filters, paginated), active-sessions view, CSV export.
- Dashboard widget: 24h logins/logouts/failed + top-5 failing IPs.
- Retention purge console command (
user-audit/purge/run). - Failed-login throttling (sliding-window, per IP and per email).
- Throttle reset console command (
user-audit/throttle/reset). - New-location email alerts (configurable lookback).
- Session-expired endpoint for frontend-initiated entries.
- User-facing recent-activity JSON endpoint.
- Permission
user-audit-viewfor viewer access.