A security, correctness and interface release, and a major version: the UI now
requires authentication by default, exports answer to their owner, unusable
parameters are refused rather than reinterpreted, and some public API is gone.
Upgrading from 2.x? The database and models are unchanged — update the
package, then read UPGRADING.md.
composer require muhammadsadeeq/laravel-activitylog-ui:"^3.0"Recommended after upgrading
Spatie indexes the log by subject and causer. This UI lists it newest first and
groups it by event over a date range, neither of which was indexed. On 204,963
activities that took the first page from 350ms to 21ms:
php artisan vendor:publish --tag="activitylog-ui-migrations"
php artisan migrateOpt-in, because activity_log is Spatie's table and indexing an established one
locks it while it runs.
Breaking Changes
authorization.enablednow defaults totrue. Previously the UI shipped fully public — a default install served the entire audit log to unauthenticated visitors. Installs that published the config keep their own value and are unaffected; installs that never published it will now require a logged-in user. SetACTIVITYLOG_UI_AUTHORIZATION=falseto restore the old behaviour, and see the authorization section of the README for the full precedence order.access.allowed_usersandaccess.allowed_rolesare now actually enforced whenauthorization.enabledisfalse. The middleware that enforces them was previously only registered when authorization was enabled, so those lists had no effect at all in that combination.route.middlewareno longer replaces the authentication and access middleware; it replaces the base stack only, and the security middleware is appended afterwards.AnalyticsService::getUserActivityProfile()returnsfirst_activityandlast_activityas ISO 8601 strings rather than Carbon instances. The default HTTP response is unchanged; direct PHP callers and applications usingCarbon::serializeUsing()will see the difference.- Out-of-range and unusable request parameters now return
422instead of being silently clamped or dropped.per_page=999999used to become 100 and an unusablecauser_idbecame no causer filter at all, each answering200as though nothing had changed — and a dropped filter shows more of the audit log than was asked for. Affectspage,per_page,anchor_id,causer_id,subject_id,event_typesand the single-value filters, on the list, analytics and export endpoints. Omitted or empty parameters still use the default. See UPGRADING.md. - Exports are now scoped to the user who created them. Both the download and the progress endpoint refuse anyone else, and job ids are random rather than
uniqid(). Shared export URLs will stop working across users. - Removed
ActivitylogService::searchWithSuggestions()and thesearch()controller action it served. The action had no route, so nothing could reach it, but it was a second implementation of the suggestions feature carrying the same email exposure fixed below. UsegetSearchSuggestions(). - Removed
Activity::hasMonotonicKey(). It existed to switch pagination anchoring off entirely for UUID and ULID keys; the anchor now compares(created_at, key), which reduces the key to a tiebreak within one timestamp and makes anchoring worth doing for those keys rather than disabling it. A row inserted at the anchor's exact timestamp with a lower random key can still join the pinned set; an auto-incrementing key has no such window. - The activity list endpoint requires
anchor_idandanchor_timetogether. Sending one without the other returns422rather than being applied as half an anchor.anchor_timenow carries microseconds; send back the value you were given rather than reformatting it. Clients using the shipped UI are unaffected.
Changed
- The UI no longer loads the Tailwind Play CDN, Chart.js or a webfont at page load — roughly 630KB of third-party assets. It ships one stylesheet served on a versioned route by the package itself (about 6KB over the wire), with no build step and nothing to publish. Chart.js is fetched only when a chart is drawn.
- The activity table was reworked for scanning: the date is a heading per day rather than a column on every row, rows are about half as tall, the record acted on is the primary line with the description beneath it only when it differs from the event, and below 960px rows stack into blocks instead of scrolling the page sideways.
- Activity listings are ordered by
created_atwith the primary key as a tiebreak, rather than by the key alone. Backdated or imported activities previously appeared out of chronological order. - Trend chart lines are coloured by which event they represent, using the same mapping as the badges and event bars, instead of by their position in the series list. The line labelled "Updated" could previously be drawn in the "created" green while its badge elsewhere on the page stayed blue.
Fixed
- Fixed the filter option caches returning
__PHP_Incomplete_Classand taking the dashboard down (#12). They now store plain arrays, every read is validated before use so a bad entry is discarded and rebuilt, and the keys are versioned so an upgrade cannot read what an older release wrote. Reported by @djemmal-nour-el-islam, who also identifiedgetEventTypesWithStyling()as affected. - Fixed the UI ignoring a custom
activitylog.activity_model(#9). The table, connection and key metadata now come from the configured model, including causers that live on a different connection from the log itself. Reported by @fbmfbm. - Fixed four of the usability problems in #10, reported by @femto-code: "Load More" no longer jumps back to the top of the page, the repeated "Loaded N activities" toasts are gone, the causer name is configurable through
ui.causer_name_attributesinstead of showing "Unknown", and the timeline hint can be dismissed. The mobile layout quirks are addressed by the interface rework, which stacks each row into a block below 960px rather than scrolling the page sideways. - Fixed pagination hiding activities. The anchor that pins later pages filtered on the primary key while the listing was ordered by
created_at, and the two disagree as soon as anything is backdated or imported. On a log of 204,963 activities this made 44,523 of them — a fifth — unreachable on every page after the first, while the footer went on reporting the full count. - Fixed the filter-option single-flight never running. It called a method that did not exist; the resulting
Errorwas caught by a guard meant for an unhealthy cache store, so every cold read logged a warning and ran the full scan the lock was added to prevent.performance.filter_lock_waitandperformance.filter_lock_ttlare now read. - Fixed
/api/search/suggestionsloading the entire activity table into memory, which stopped it answering at all on a large log, and publishing causer email addresses regardless offilters.expose_causer_email. - Fixed
AnalyticsService::getUserActivityProfile()loading a causer's complete history to produce six summary figures. For a causer with 3,342 activities it took 8.3 seconds; it now takes 186ms and the numbers are unchanged. - Fixed a causer or subject id of
0being validated and then silently dropped, which listed the entire log as though no filter had been applied. - Fixed a queued export taking its owner from the caller's own request body, so a posted
owner_iddecided who the finished file belonged to. - Fixed the trend chart accumulating a Chart.js instance on every re-render, including on every theme toggle, because the instance was held in Alpine's reactive state and
destroy()could not reach the animation loop through the proxy. - Fixed the analytics view serving figures for the previously selected filters. Changing filters while the table was on screen was recorded but never fetched, and returning to analytics counted as already loaded.
- Fixed the delete-saved-view dialog declaring
aria-modalwithout trapping focus, so Tab left the dialog for controls a screen reader was no longer announcing. - Fixed resolution of a custom
activitylog.activity_modelrunning full validation and construction on every key lookup — hundreds of times per page. Key metadata is now settled once per class, while the table and connection are still asked of a freshly built model on every call, so a model that picks either per tenant is followed rather than frozen. - Fixed the pagination anchor being truncated to whole seconds. On a log whose
created_atstores sub-second precision the anchor was not the anchoring row's own timestamp, so every row sharing that second was excluded from later pages. - Fixed search suggestions failing to find a literal
%or_on SQLite, which has no LIKE escape character unless one is named. - Fixed a slow analytics request for the previous filters overwriting the figures of a faster one for the current filters when the two overlapped.
Added
php artisan activitylog-ui:clear-cachefor clearing the filter option caches by hand.analytics.max_chart_series— how many event types the trend chart draws before the remainder are summed into a single "Other" series. Defaults to 6.performance.filter_lock_waitandperformance.filter_lock_ttl— control the single-flight lock that stops every in-flight request rebuilding the filter caches at once when they expire.- A publishable migration adding the indexes this UI's queries need, under the
activitylog-ui-migrationstag. Spatie indexes the log by subject and causer; this UI lists it newest first and groups it by event over a date range, neither of which was indexed. On 204,963 activities the first page goes from 350ms to 21ms. Opt-in, becauseactivity_logis Spatie's table and indexing an established one locks it while it runs. - Trend chart datasets include the raw
eventname alongside the display label.
Thanks to @djemmal-nour-el-islam (#12), @fbmfbm (#9), @femto-code (#10) and
@keatliang2005 (#13) for the reports.