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 Log Laravel Package

escarter/activity-log

A lightweight Laravel package for tracking user activity. Log model events and custom actions to the database, then query recent activity to build audit trails, dashboards, or admin feeds with minimal configuration. Works with Eloquent models and is simple to install and extend.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Pin and install. composer require escarter/activity-log:1.0.1. This is a 1-star package with no described changes in the v1.0.1 release notes beyond a changelog link — treat it as untriaged. Pin the exact version, and before upgrading later, run git log v1.0.0..v1.0.1 (or read the GitHub compare view) to see whether the migration stub, config keys, or trait method signatures moved.

  2. Publish and migrate before writing any application code.

    php artisan vendor:publish --provider="Escarter\\ActivityLog\\ActivityLogServiceProvider"
    php artisan migrate
    

    Confirm the service provider path in the README first — on a low-traffic package the namespace in the docs is the thing most likely to be stale. If vendor:publish finds nothing, run php artisan vendor:publish interactively and pick the tag.

  3. Read three files, in this order. This package's docs are thin enough that the source is the documentation, and it's small enough to read in fifteen minutes:

    • the published config — table name, activity model class, enabled/disabled switch, and whether a causer resolver is configurable;
    • the migration — confirm the column names you'll actually query (subject_type/subject_id, causer_type/causer_id, event, properties, batch_uuid, created_at) and which indexes ship by default;
    • the trait and the fluent logger class — these define your whole public API.
  4. First use case: audit exactly one model. Pick the model where support or compliance already asks "who changed this?" — an Invoice, Order, or User. Add the trait, enable only created and updated (skip deleted until you need it), and make one real update through your app.

  5. Verify the write, not the intent. Drop into tinker:

    ActivityLog::where('subject_id', $invoice->id)->latest()->first();
    

    You want: correct subject_type, a non-null causer_id, an event that matches what happened, and a properties payload you can actually render in a UI. If any of the four is wrong, fix it now — before the trait is on ten models.

  6. Only then add a second model. One model proves the plumbing works; the second proves your attribute allow-lists are correct and that you aren't accidentally logging timestamps or tokens.

Implementation Patterns

Trait + per-model options method. The core pattern is: use LogsActivity on the model, plus a method on the model that declares which events fire and which attributes are captured. Keep this explicit per model. Resist a global "log everything" default — the first time it writes a password hash or a remember_token into a queryable table, you have an incident, not a bug.

Allow-list attributes, not deny-list. Deny-lists rot: a new api_secret column is added, nobody updates the exclusion, and it lands in the audit table. Write the allow-list positively (identifiers, status fields, money fields, owner fields) and add to it deliberately.

Store diffs for anything a human will read. The value of old/new pairs is that your UI can render status: pending → paid instead of a JSON blob. For high-churn columns (last_seen_at, counters, updated_at) — exclude them, or you generate a row per request and the table becomes both expensive and unreadable.

Resolve the causer explicitly outside HTTP. Auth-based causer resolution works in a controller and silently fails everywhere else. In queued jobs, console commands, and scheduled tasks there is no auth user, so either:

  • wrap the operation in an auth context (Auth::loginUsingId($requesterId) around the write, then restore), or
  • call the fluent logger with an explicit causer model.

The rule that matters: attribute the change to the person who requested it, not the worker that executed it. Pass the requester's ID into the job payload and thread it through.

Manual/fluent logging for non-model events. Logins, exports, permission grants, CSV downloads, third-party webhook receipts — none of these are Eloquent writes, so the trait can't help. Use the fluent builder and name the events yourself:

activity()
    ->performedOn($report)
    ->causedBy($user)
    ->withProperties(['format' => 'csv', 'rows' => 12000])
    ->event('exported')
    ->log('report exported');

This is also the right tool inside service classes where putting the trait on the model would be too coarse (you want to log this specific domain action, not every save).

Batch multi-step operations. Wrap a unit of work so all its rows share a batch_uuid, and render them as one timeline entry. Without batching, a single checkout produces nine unconnected rows and the audit trail becomes noise. Batch boundaries should match your business transactions, not your database transactions.

Build a query layer, not controller queries. Put the activity model behind a repository or query scope with named filters: by subject, by causer, by event, by date range, and by log name if the package supports named logs. Always paginate. Separating "billing" / "auth" / "admin" into named logs at write time is far cheaper than filtering by string in views later.

Subclass the activity model via config. The cleanest extension point is pointing the config at your own App\Models\Activity extends PackageActivity. That gets you accessors (causer_avatar), casts (properties => array), and relationships without forking vendor code. Do this rather than patching vendor/.

Retention on day one, not after the incident. Register whatever prune command the package ships (or write one) in routes/console.php / the scheduler with an explicit window from config. The table grows monotonically and nothing else in your app will ever clean it up.

Testing. Assert on the table for the contract — assertDatabaseHas('activity_log', ['subject_id' => $order->id, 'event' => 'updated']) — and use freezeTime() so created_at assertions are stable. Critically: assert causer_id is not null in your critical flows. A test suite that passes with null causers is worse than no test, because it certifies the bug.

Gotchas and Tips

  • Silent no-ops are the default failure mode. When "nothing is logged," check in this order: (1) logging isn't disabled in config, (2) php artisan config:clear — a stale config cache in a queue worker is the classic culprit, (3) the table exists, (4) the trait is actually on the model you think it is, (5) the event is in your enabled list. Only then suspect the package.

  • causer_id null is the #1 data-quality bug. It doesn't throw, it doesn't log a warning, and it's invisible until security asks who made a change and you have "System." Guard the critical paths with tests and add a nullable-causer dashboard metric if the volume is high.

  • Morph maps break history. If your app registers a morph map, rows store the alias, not the FQCN. Renaming an alias later orphans every historical row — the data is still there, but $activity->subject returns null. Decide the aliasing strategy before you accumulate history, and treat aliases as immutable once written.

  • The same problem applies to subject_type on namespace moves. Refactoring App\Models\Order to App\Domain\Sales\Order silently breaks every old row. A stable morph alias from day one is the only real fix; retrofitting one means a data migration.

  • Transactions and queues. If the package defers writes (afterCommit, queued listener), verify explicitly whether a rolled-back transaction still produces a log row. For anything compliance-adjacent, write a throwaway test that wraps an update in DB::transaction() and throws — then check the table. Don't assume.

  • Indexes you actually need: (subject_type, subject_id), (causer_type, causer_id), and created_at. Check what the shipped migration provides and add a follow-up migration for the gaps. Large properties JSON blobs hurt write throughput and full-table scans alike — trim the payload rather than indexing it, unless you know you need MySQL 8 / Postgres JSON indexes.

  • v1.0.1 is opaque — diff the stubs. The release notes are a changelog link, nothing more. Patch releases on a 1.0 line routinely carry added columns and renamed config keys. Publish to a temp path and diff against your live copies:

    php artisan vendor:publish --force --tag=activity-log-migrations
    git diff config/ database/migrations/
    

    Then write the column addition as your own migration. Never --force over a live config file in production.

  • Debugging in one command. php artisan tinker, filter to a single subject ID, order by created_at, dd() the first row. This immediately separates "nothing written" (package/config/trait problem) from "written but wrong" (attribute list or causer problem). Those have completely different fixes and people waste hours conflating them.

  • Attribute lists rot. Every time a model gains a credential-ish column, revisit its allow-list. Put a comment in the migration or a note in the model. This is the maintenance cost you're accepting in exchange for not hand-rolling observers.

  • Extension points worth knowing: custom activity model via config (accessors, casts, relationships), named logs (separation of concerns at write time), and custom event strings (so views never string-match on a description field). All three let you stay on the vendor package through real product requirements — which matters a lot on a 1-star dependency where you may eventually need to fork.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo