dp-0/user-activity
A lightweight Laravel package for tracking user activity. Record actions, query activity history, and add simple audit-style timelines to your app using familiar Eloquent models. Easy to install and integrate.
Install and publish everything first. This is a small, archived package with a single maintainer history, so treat the published files as your source of truth rather than the README.
composer require dp-0/user-activity
php artisan vendor:publish --provider="Dp0\UserActivity\UserActivityServiceProvider"
php artisan migrate
Publishing typically gives you three things worth reading before you write any code:
config/user-activity.php — toggles for which events are recorded, retention, and (likely) the model/table names.user_id and created_at are indexed.Where to look first: the service provider's boot() method. Packages in this niche usually register listeners on Illuminate\Auth\Events\Login / Logout / Failed. That determines whether you get activity tracking for free or have to wire it yourself.
First use case — login/logout auditing. Add the package's trait to your User model:
use Dp0\UserActivity\Traits\HasActivity; // verify exact namespace from the package
class User extends Authenticatable
{
use HasActivity;
}
Then render it immediately somewhere cheap — a "Last login" column in your admin users table — to confirm data is actually flowing before you build anything on top:
$user->activities()->latest()->first(); // adjust relation name to the published trait
If rows appear after a real login, the plumbing works and everything below is just shaping.
Auth-event tracking (the default path). Once the trait and config are in place, Login/Logout are usually recorded automatically via the provider's listeners. Don't add middleware for these — you'll get duplicates. Reserve middleware for page visits.
Route middleware for page views. The typical pattern is a web-group middleware that logs the request for authenticated users:
Route::middleware(['web', 'auth', 'log.activity'])->group(function () {
// app routes
});
Scope this deliberately. Applying it to your whole web group is the fastest way to fill the table with favicon, Livewire polling, and AJAX noise.
Custom events are the highest-value usage. Generic page-view logging is low signal; domain events are not. Wrap the package's logging call in your own domain events so the tracking stays a side effect of business logic rather than a routing concern:
// app/Events/InvoicePaid.php listener
UserActivity::log($user, 'invoice.paid', ['invoice_id' => $invoice->id]);
Or, cleaner, use a model observer and keep controllers untouched.
Querying and reporting. Treat the activity table as an append-only log and query it through scopes rather than raw model calls:
Activity::forUser($user->id)
->where('created_at', '>=', now()->subDays(30))
->where('action', 'login')
->count();
For any dashboard, build one or two named scopes (forUser, ofAction, recent) in your own codebase rather than scattering where() calls — this insulates you from the package if you later fork it.
Admin integration. The natural day-one feature is a paginated activity feed on a user detail page, plus a "last active" indicator. Both are cheap: ->latest()->paginate(50) and a cached max(created_at).
Multi-guard applications. If you have admin and web guards, verify which guard the listeners resolve. Most packages of this vintage hardcode Auth::user(); if so, you'll need to override the listener registration in your own EventServiceProvider and skip the package's auto-registration for that guard.
The package is archived — audit it before you commit. Last release was July 2023 and it's marked as archived, which means no Laravel 11/12 support, no security patches, and possibly a hard illuminate/support version constraint that will block your upgrade path. Check composer.json constraints first; if they're capped at ^8|^9|^10, budget for a fork. For a 12-star package the surface area is small — copying the trait, migration, and one listener into app/ is often a better long-term decision than depending on it.
No index on created_at is the most likely performance bug. The shipped migration probably indexes user_id at most. Add a composite index yourself:
$table->index(['user_id', 'created_at']);
$table->index(['action', 'created_at']);
Without it, every "activity over the last 30 days" query becomes a table scan once you're past a few hundred thousand rows.
Write amplification on hot paths. Every logged request is an INSERT plus whatever indices exist. For anything beyond a low-traffic internal app, don't log page views synchronously — either disable them in config and log only domain events, or wrap logging in a queued job. Also consider the package's retention/pruning option if one exists; if not, schedule your own Activity::where('created_at', '<', now()->subMonths(6))->delete().
IP and User-Agent capture behind a load balancer. If the package records ip_address, it will record your proxy's IP unless TrustProxies is configured. Verify with a real request from behind your balancer, not locally.
Privacy obligations. Activity logs containing IPs, user agents, and page paths are personal data under GDPR. Decide retention up front, document it, and make sure the log is covered by your deletion/export flow — deleting a User row will not cascade to the activity table unless the FK was defined with cascadeOnDelete. Check the migration; if it wasn't, add it.
Duplicate rows after testing. If you wire the package's listeners and register your own Login listener, or add the middleware to both the web group and individual routes, you'll get double entries. Symptom: exact-duplicate rows with millisecond-adjacent timestamps. Debug by temporarily logging from a single call site rather than by reading the package source.
Config caching masks config changes. Standard Laravel trap but worth repeating here: after editing config/user-activity.php, run php artisan config:clear. A stale cached config that leaves logging "enabled" is a common cause of mystery test failures.
Extension points to prefer. Use the config file to swap the model class (if supported) rather than editing vendor code, and keep your own ActivityRepository or query scopes in app/ as the seam. That way, if/when you replace the package, your application code doesn't move — only the repository implementation does. Given the archived status, assume replacement is a matter of when, not if.
How can I help you explore Laravel packages today?