Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Activity Timeline Laravel Package

lara-zeus/activity-timeline

A Laravel/Filament package from Lara Zeus that adds a clean activity timeline to your admin panel, making it easy to display, browse, and review model or user activity in chronological order. Simple to install and customize.

View on GitHub
Deep Wiki
Context7

Getting Started

Prerequisite: this package is a renderer, not a recorder. It displays rows from spatie/laravel-activitylog. Adopt both or neither.

composer require lara-zeus/activity-timeline spatie/laravel-activitylog
php artisan vendor:publish --provider="Spatie\Activitylog\ActivitylogServiceProvider" --tag="activitylog-migrations"
php artisan migrate

Then, on the panel provider:

->plugins([
    \LaraZeus\ActivityTimeline\ActivityTimelinePlugin::make(),
])

and rebuild panel assets:

php artisan filament:assets

Publish the plugin config/views only when you actually need to change markup or defaults:

php artisan vendor:publish --tag="activity-timeline-config"
php artisan vendor:publish --tag="activity-timeline-views"

Where to look first, in this order:

  1. The plugin's config/activity-timeline.php — density/compact defaults, date grouping, timestamp format.
  2. The published Blade views — this is where the "lightweight, customizable" claim actually lives.
  3. The Spatie side: LogsActivity trait and getActivitylogOptions() on the model. If this is missing, the timeline renders empty and you will spend an hour debugging a UI problem that is a data problem.

First use case — do this one only: tag your single most-opened resource (orders, tickets, accounts) with LogsActivity, then mount the timeline on that resource's View page. That exercises the record-scoped path you will use ~90% of the time, produces an immediate demo, and defers the harder decisions (panel-wide feeds, cross-tenant access policy) until you have seen real data in the widget.

Minimal model setup:

use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions;

class Order extends Model
{
    use LogsActivity;

    public function getActivitylogOptions(): LogOptions
    {
        return LogOptions::defaults()
            ->logOnly(['status', 'total', 'assigned_to'])
            ->logOnlyDirty()
            ->dontLogEmptyChanges()
            ->useLogName('order');
    }
}

logOnly() is not a convenience — it is your field-visibility boundary. See Gotchas.


Implementation Patterns

Pattern 1: Record-scoped history tab (the default)

Register the timeline on the resource's View page or as a dedicated tab. This is your bread-and-butter placement and should be the first thing you ship for every audited resource. Every downstream question — "who changed this?", "when did it move to shipped?" — resolves here.

public static function getWidgets(): array
{
    return [
        OrderTimeline::class,
    ];
}

If you need per-placement configuration (density, heading, subject scoping), the package exposes fluent setters on the timeline class or component; confirm exact method names against the README of the version you pin — the API surface is small but has shifted across releases.

Pattern 2: Infolist entry on a custom page

The infolist embedding is the most under-used capability and the reason the package exists beyond "another Filament widget." Use it for entity overview pages that are not resources — an account dashboard, a settings page, a bespoke ops screen. You get a chronological feed without spinning up a full resource.

Pattern 3: Compact vs. full, deliberately per placement

5.0.3 added compact. Treat it as a product lever:

Placement Density Reason
Dedicated history tab Full Vertical space is free; scanability matters
Sidebar widget Compact Space-constrained; recency beats detail
Infolist entry / modal Compact Embedding context, not a destination
Relation manager Full Users are already in "investigate" mode

Pick one per placement up front. Do not toggle on viewport width — that produces two bad experiences instead of one good one.

Pattern 4: Relation-manager / cross-record feeds

For "activity across all orders for this customer" or similar scoped feeds, mount the timeline against a query scoped by subject. Watch the query: a feed that spans many subjects is where N+1 bites.

Pattern 5: Panel-wide audit dashboard (ship last, or never)

Technically the same component. Politically a different product: it exposes cross-record and potentially cross-tenant activity. Gate it behind an explicit policy and a permission before you write the page. This is the one placement where "it works in staging" is not evidence it is safe.

Integration workflow that actually holds up

  1. Instrument first, render second. Add LogsActivity + getActivitylogOptions() to the model, make a change, confirm a row lands in activity_log via tinker. Only then mount the widget.
  2. Set expectations on history depth. Timelines start empty at adoption and fill forward. There is no backfill. Say this to stakeholders before the demo, not after.
  3. Schedule retention at adoption, not at first slowness. activity_log never prunes itself. Add a scheduled command with a configurable window (e.g. 180 days) and an index on (subject_type, subject_id, created_at) in the same PR that ships the timeline.
  4. Log names are your filter. useLogName('order') gives you a cheap way to scope feeds and later to query selectively. Decide a naming convention before you instrument the second model.

Gotchas and Tips

The timeline is empty and the UI is fine. Nine times out of ten, the model is not logging, logOnly() excludes the attributes you changed, or logOnlyDirty() correctly suppressed a no-op save. Verify with Activity::all() in tinker before touching the view.

hidden() is not a privacy control. Activity log stores old/new attribute values verbatim. A field hidden from the form is still fully visible in the timeline if it is in logOnly(['*']). Audit every getActivitylogOptions() for password hashes, tokens, internal notes, PII, and margins. This is a product decision, not a refactor — route it to whoever owns data governance.

logOnly(['*']) is the tempting footgun. It logs the entire model snapshot on every change, which inflates the table, exposes every column, and makes the timeline unreadable. Enumerate fields. The friction is the point.

N+1 on any feed spanning multiple subjects. The renderer will touch causer and subject per row. On a record-scoped tab this is fine (one subject). On a relation manager or a global feed, eager-load both or watch the page crawl.

No pruning, ever. activity_log grows monotonically. Decide the retention window at adoption and implement it with a scheduled command, plus an index. compact does not help query cost; it only reduces vertical space.

Filament minor-version coupling. The plugin tracks Filament's release line (5.0.3 pins against 5.7.0), and Filament minors have historically moved view internals. Keep the panel and the plugin on the same minor. When you bump Filament, bump this in the same PR and re-run filament:assets — stale assets after an upgrade produce blank or unstyled timelines that look like a bug in the package.

Republishing views freezes your fork of the markup. Once you vendor:publish the views, upstream markup fixes no longer reach you. Publish only if you need it, and note in your upgrade checklist that published views must be re-diffed on each release.

Asset cache is the first debugging step. Blank timeline, unstyled timeline, or layout that ignores config: run php artisan filament:assets, then php artisan optimize:clear. Most "the package is broken" reports end here.

Global vs. record scope is a security boundary. A panel-wide dashboard built on the same component exposes other users' and other tenants' activity. Add a policy before the page, not after the first support ticket asking why a customer can see another customer's order history.

Extension points, in order of safety:

  1. Config (density, date grouping, timestamp format) — safest, upgrade-friendly.
  2. Fluent setters per placement — safe, no fork.
  3. Published views for markup changes — creates a maintenance surface; do it last, and keep the diff small.

Continuity risk is real and should shape design. 10 stars, 0 dependents, single maintainer. Keep your usage wrapped in a thin local class or component (e.g. App\Filament\Components\OrderTimeline) rather than scattering the package's class names across resources. When the package stalls, replacing the renderer with a Blade partial is then a single-file change instead of a grep across the panel.

Not a compliance answer. A chronological feed over a mutable table is not an immutable audit trail. Fine for internal visibility and support triage; do not hand it to a questionnaire without review.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package