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

Footprints Laravel Package

kyranb/footprints

Kyranb/footprints is a lightweight Laravel package for tracking user activity. Add a trait to your models, record footprints through a polymorphic relationship, and query activity history with familiar Eloquent syntax—useful for audits and recent-activity feeds.

View on GitHub
Deep Wiki
Context7

Getting Started

Footprints is a lightweight activity/visit tracker for Laravel: it records who did what to which model, from which IP, into a single footprints table. It is deliberately narrow — think "recently viewed" lists, per-user activity feeds, and coarse audit trails — not a full diffing audit log.

  1. Install and publish

    composer require kyranb/footprints
    php artisan vendor:publish --provider="KyranB\Footprints\FootprintsServiceProvider"
    php artisan migrate
    

    Publishing gives you config/footprints.php and the migration that creates the footprints table (user_id, ip_address, model, action, timestamps, plus a UUID column). Read those two files first — they define almost everything the package does.

  2. Wire up tracking. There are two independent entry points, and you'll usually want both:

    • Request middleware — register the package's footprint middleware (globally or on a route group) so page visits are logged. This is what gives you "users viewed X" data.
    • Model trait — add the package's footprint trait to the Eloquent models you care about so create/update/delete events are captured automatically. (Confirm the exact trait namespace against the README of the version you install; it has been renamed across major versions.)
  3. First useful use case. A "Recently viewed" panel on a dashboard:

    Footprint::where('user_id', auth()->id())
        ->latest()
        ->limit(10)
        ->get();
    

    That single query is the payoff of the package: a denormalised, cheap-to-read activity stream you don't have to build yourself.

  4. Verify it works before building on it — hit a route as an authenticated user, then check the table:

    php artisan tinker --execute="dump(\KyranB\Footprints\Footprint::latest()->first()->toArray());"
    

Note on 3.2: this release only adds Laravel 13 support (PR #93). There are no API changes, no new config keys, and no deprecations. If you're on 3.1.x, upgrading is a composer update with no code changes. If you're starting fresh on Laravel 13, 3.2 is the first version you can use — check the package's PHP requirement against your app's PHP version, since Laravel 13's PHP floor is the binding constraint.

Implementation Patterns

Middleware for page views, trait for state changes. Keep the two concerns separate. The middleware answers "what did this user look at?"; the trait answers "what did this user change?". Mixing them into one mental model leads to duplicate rows — a GET /posts/5 plus a model-touch can log twice. Decide per model whether you want visit tracking, mutation tracking, or both.

Scope the middleware, don't go global. Registering it globally means every asset request, health check, and webhook is a candidate for a row. Apply it to a route group instead:

Route::middleware(['auth', 'footprints'])->group(function () {
    Route::get('/dashboard', DashboardController::class);
    Route::resource('posts', PostController::class)->only('show');
});

This also fixes ordering: your auth middleware must run before the footprint middleware, or user_id will be null on requests that are actually authenticated.

Build read models on top, not queries in views. Footprints is a write-heavy table with a simple shape; wrap it in a small repository or query scope so your controllers stay clean and you have one place to add indexes or caching later:

class RecentActivity
{
    public function forUser(User $user, int $limit = 20): Collection
    {
        return Footprint::query()
            ->where('user_id', $user->id)
            ->where('created_at', '>=', now()->subDays(30))
            ->latest()
            ->limit($limit)
            ->get();
    }
}

Enrich at write time via the model, not the table. If you need context the package doesn't store (tenant ID, impersonation flag, request ID), don't hack the migration. Either extend the published migration with nullable columns and populate them from a Footprint::creating listener, or keep a side table keyed by the footprint's UUID. Extending the published migration is fine — you own it after publishing.

Pair with a real audit log when correctness matters. For anything an auditor or a compliance team will read, run spatie/laravel-activitylog (or Laravel's own Auditable patterns) alongside Footprints. Use Footprints for the UX surface, the audit log for the record of truth. Trying to make Footprints carry old/new value diffs is a dead end — those columns don't exist and the package's design doesn't accommodate them.

Admin drill-down pattern. Because model is stored as a class string, you can reconstruct links cheaply:

$footprint->model;                    // e.g. App\Models\Post
$footprint->model::find($footprint->model_id ?? null);

If your version doesn't persist a subject key, resolve by parsing the request path stored alongside the record, or extend the table with model_id. Verify which of the two you have before shipping a "jump to record" link.

Multi-guard apps. The package assumes a single authenticated user. If you have separate admin and web guards, either normalise to one user_id column with an added guard column, or run two footprint tables via separate model bindings. Don't silently log both guards into one column — you'll get ID collisions with no way to disambiguate retrospectively.

Gotchas and Tips

The table grows without bound. This is the number-one production issue with Footprints. A busy app generates millions of rows. Before you ship: add a composite index on (user_id, created_at) and on (model, action), and schedule pruning. Laravel's MassPrunable on a subclassed Footprint model works well:

public function prunable(): Builder
{
    return static::where('created_at', '<=', now()->subDays(90));
}

IP addresses are personal data. Under GDPR/UK GDPR an IP is personal data. If you enable IP recording, document the retention period, disclose it in your privacy policy, and prefer truncating the last octet if you only need coarse geo/abuse signal. Many teams set ip_address to null and lose nothing they actually use.

Middleware order is the classic bug. Null user_id on rows you know came from logged-in users almost always means the footprint middleware is running before auth (or in the web group ahead of session start). Check app/Http/Kernel.php / bootstrap/app.php ordering first.

Non-HTTP contexts write nothing useful. Queue workers, artisan commands, and scheduled jobs have no request, so no IP and often no user. If you need footprint rows from jobs, set the causer explicitly (Auth::login() in tests, or a Footprint::creating hook reading a context object) — otherwise you'll get silent gaps that look like broken tracking.

Tests and seeders pollute the table. Factories that create models with the trait will fire footprints, and RefreshDatabase will happily churn thousands of rows and slow your suite. Disable recording in the testing environment via a config flag, or wrap the trait's behaviour in a check on app()->runningUnitTests(). Do this on day one, not after the suite hits two minutes.

config:cache bites during rollout. Changing config/footprints.php on a cached production deploy does nothing until php artisan config:cache runs again. Put it in your deploy script.

Only GET-like requests are typically tracked by the middleware. POST/PUT/DELETE submissions won't appear as "visits" — that's by design, and mutation tracking is the trait's job. Don't chase this as a bug.

No built-in diffing, no rollback, no actor resolution beyond the authenticated user. Records are append-only facts, not state. If you need "what did this row look like before?", this package cannot answer it.

Extend through the container, not the vendor directory. The package registers its model and middleware bindings in the service provider; if you need a custom Footprint model (extra scopes, pruning, relationships), publish and subclass it and rebind in a your own provider rather than editing vendor/. This survives upgrades — including minor ones like 3.2.

Upgrading to 3.2 is a no-op for application code, but it does widen the framework constraint. If you're currently pinned to Laravel 12 and not ready to move, stay on 3.1.7.2; the constraint change in 3.2 is the only behavioural surface, and it's a compatibility widening, not a break.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console