sarfraznawaz2005/visitlog
Laravel package for logging and analyzing website visits. It records page views with visitor details like IP, browser, OS, device, referrer, and country, supports route exclusions via middleware, and helps track unique visitors, online users, and traffic stats.
Install with composer require sarfraznawaz2005/visitlog (v6 requires Laravel 13 — see Gotchas if you're still on 10/11/12).
Publish and run the migration, then publish the config so you can see what's actually being recorded:
php artisan vendor:publish --provider="Sarfraznawaz2005\VisitLog\VisitLogServiceProvider"
php artisan migrate
Look at config/visitlog.php first. It controls whether logging is enabled, the table name, and which paths are excluded. Most "why isn't it logging / why is it logging too much" questions are answered there.
Register the middleware on the web group. On Laravel 11+/13 that's bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\Sarfraznawaz2005\VisitLog\Middleware\VisitLog::class,
]);
})
On older skeletons it went into the web array in app/Http/Kernel.php.
First use case: browse a page, then confirm a row landed:
use Sarfraznawaz2005\VisitLog\Models\VisitLog;
VisitLog::latest()->first();
If that returns a row with IP, URL, user agent and timestamps, you're wired up correctly and can move on to querying it.
Global vs. selective tracking. Appending to the web group logs every browser request on the site — the right default for a marketing site or blog. For an app where you only care about a few pages, register it as a route middleware alias and attach it to specific route groups instead, so you don't write rows for every Livewire poll and asset request.
Capture user_id. The middleware runs inside the web group, so auth()->id() is available when it's appended (i.e. after StartSession/Authenticate). If you register it earlier or globally, you'll silently log null users — a common cause of "all my visits are anonymous."
Build your own read layer. Don't scatter VisitLog:: calls through controllers. Create a small VisitLogRepository or dedicated AnalyticsController with the queries you actually need:
VisitLog::query()
->whereBetween('created_at', [$from, $to])
->selectRaw('DATE(created_at) as day, COUNT(*) as visits')
->groupBy('day')
->orderBy('day')
->get();
Because the package stores denormalized columns (browser, OS, country, referrer), most reporting is plain groupBy work — no joins required.
Custom model for domain logic. If you need relations (user(), scopes like scopeHuman(), accessors), extend the package model rather than editing vendor code, and point your app at your subclass:
class Visit extends \Sarfraznawaz2005\VisitLog\Models\VisitLog
{
protected $table = 'visit_logs';
public function user()
{
return $this->belongsTo(User::class);
}
public function scopeHuman($q)
{
return $q->where('browser', '!=', 'Unknown');
}
}
Defer the write. The middleware writes synchronously on the request path. For high-traffic apps, either narrow the middleware to a subset of routes or dispatch a queued job from a terminate()-style listener so the insert happens after the response is flushed.
Retention as a scheduled task. Visit logs grow monotonically. Add a command and schedule it:
VisitLog::where('created_at', '<', now()->subDays(90))->delete();
Delete in chunks (->chunkById(1000, ...)) once the table is large, and make sure created_at is indexed (add your own migration if the published one doesn't).
Testing. Flip visitlog.enabled to false in phpunit.xml so your test suite doesn't write rows through every feature test, then override it per-test when you're asserting logging behaviour.
composer why-not sarfraznawaz2005/visitlog 6.0.0 before upgrading to see exactly what's blocking you.vendor:publish will happily overwrite your edited config/visitlog.php. On upgrade, diff the package's config against yours rather than blindly republishing, and if you've already published migrations, leave them alone — check the package's migrations/ directory for new columns and write your own additive migration instead.enabled, the table name, or the exclusion list, run php artisan config:cache (or config:clear) — otherwise your changes appear to do nothing in production only./horizon, /telescope, sitemaps and any JSON/API endpoints via the exclusion config, and prune on a schedule from day one.TrustProxies. Behind a load balancer or Cloudflare, the logged IP will be your proxy's unless you configure trusted proxies in bootstrap/app.php. This invalidates any geo or unique-visitor numbers you derive.scopeHuman) in your reporting rather than expecting the middleware to do it.How can I help you explore Laravel packages today?