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

Analytics Laravel Package

andreaselia/analytics

andreaselia/analytics is a lightweight, privacy-friendly analytics package for Laravel. It tracks page views, unique visitors, referrers, browsers, devices, and countries in your own database, with a built-in dashboard for quick insights and no external service required.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require andreaselia/analytics
php artisan vendor:publish --provider="AndreasElia\Analytics\AnalyticsServiceProvider"
php artisan migrate

The package is deliberately small: a migration for the page-view table, a tracking middleware, a config file, and a Blade dashboard route. Publishing gives you the config and the migration so you own both and can edit them freely.

Where to look first (in this order)

  1. config/analytics.php — the entire behaviour of the package is driven from here: whether tracking is on, which middleware guards the dashboard, which paths to ignore, and where the dashboard lives.
  2. src/Http/Middleware/ — the tracking middleware itself. Read it before you rely on it; it tells you exactly which columns get written (path, referrer, user agent, IP, session identifier, timestamp) and how uniqueness is derived.
  3. The published migration — confirms the table name and indexes. If you are going to query the data directly, this is your schema.
  4. The routes file — confirms the dashboard URI and route name so you can link to it and protect it.

First use case

A marketing site, docs site, or small SaaS landing page where you want "how many people hit /pricing this week" without shipping a third-party script, dealing with cookie banners, or paying for Plausible. Install, migrate, visit /analytics, confirm rows are landing. That's the whole loop.

Do not treat it as a Google Analytics replacement. It is a first-party page-view counter with a nice-enough dashboard — closer to a self-hosted hit counter than an analytics platform. That framing keeps expectations sane.

Verify immediately after install

php artisan route:list | grep -i analytic   # confirm dashboard URI + name
# hit a public page, then:
php artisan tinker --execute="dump(DB::table('page_views')->latest('id')->first());"

If that row doesn't appear, the problem is almost always middleware placement or a cached response — see Gotchas.

Implementation Patterns

Attach tracking to a route group, not globally

Global registration in the web group is convenient but noisy — it will happily count /horizon, /telescope, health checks, and your own dashboard refreshes. Prefer explicit opt-in:

use AndreasElia\Analytics\Http\Middleware\Analytics;

Route::middleware(['web', Analytics::class])->group(function () {
    Route::get('/', HomeController::class);
    Route::get('/pricing', PricingController::class);
    Route::get('/docs/{page?}', DocsController::class);
});

This keeps the dashboard numbers meaningful and makes it obvious in code review which surfaces are instrumented.

Deny-list the boring traffic

Even with scoped groups you'll want a belt-and-braces filter for bots, prefetchers, and internal tools:

// config/analytics.php
'ignore' => [
    'admin/*',
    'horizon/*',
    'telescope/*',
    'analytics*',
    'up',
],

If the config only supports static patterns, wrap the middleware with your own and short-circuit on $request->user()?->isInternal() or a User-Agent regex — the package middleware is a normal class, so subclassing or composing it is trivial.

Lock down the dashboard

The dashboard is a plain route. Put it behind auth plus a gate, and never rely on obscurity:

// config/analytics.php
'middleware' => ['web', 'auth', 'can:viewAnalytics'],

Remember to re-run php artisan route:cache (or clear it) after changing this — route middleware is baked into the cache.

Read the table directly for anything custom

The built-in dashboard is intentionally minimal. The real day-to-day value is that the data is in your database, queryable with anything:

// Top pages, last 7 days
DB::table('page_views')
    ->selectRaw('path, COUNT(*) as views, COUNT(DISTINCT visitor_id) as visitors')
    ->where('created_at', '>=', now()->subDays(7))
    ->groupBy('path')
    ->orderByDesc('views')
    ->limit(20)
    ->get();

// Referrers, excluding internal navigation
DB::table('page_views')
    ->selectRaw('referrer, COUNT(*) as hits')
    ->whereNotNull('referrer')
    ->where('referrer', 'not like', '%'.parse_url(config('app.url'), PHP_URL_HOST).'%')
    ->groupBy('referrer')
    ->orderByDesc('hits')
    ->limit(10)
    ->get();

Wrap these in a small AnalyticsReport service and you can surface them in your existing admin panel (Filament/Nova/Blade) instead of linking out to a separate dashboard.

Pair it with a real analytics tool, not instead of one

Use this package for the numbers you want inside your own admin UI (per-page counts, referrer breakdown, a "views this week" widget), and keep GA4/Plausible/Fathom for funnel and campaign work. The overlap is small and the combination is much cheaper than trying to make a page-view counter answer acquisition questions.

Prune on a schedule

Rows accumulate fast on anything with real traffic. Either add a Prunable model or a scheduled delete:

// routes/console.php
Schedule::call(fn () => DB::table('page_views')
    ->where('created_at', '<', now()->subMonths(12))
    ->delete())->daily();

Twelve months is usually plenty for a hit counter; shorter if you're in the EU and haven't thought about retention.

Opt out in local and test environments

Set the tracking flag off in .env for local and testing, and drop the middleware in TestCase:

protected function setUp(): void
{
    parent::setUp();
    $this->withoutMiddleware(\AndreasElia\Analytics\Http\Middleware\Analytics::class);
}

Otherwise your feature tests silently insert analytics rows and your dev machine inflates your own stats.

Gotchas and Tips

Middleware order and the session. Uniqueness typically depends on a session or cookie identifier, so the tracking middleware must run after StartSession — i.e. inside the web group or a group that includes it. Register it in api or as global middleware and you'll get a fresh "visitor" on every request, or a null identifier.

Response caching kills it. If a route is cached by spatie/laravel-responsecache, Cloudflare, or a full-page CDN rule, Laravel never executes the request and nothing is recorded. Either exclude tracked routes from caching or push the hits into your edge/CDN logs instead. This is the single most common "it stopped working" cause.

Bots and prefetch inflate everything. Server-side tracking cannot distinguish a human from curl, a link-preview crawler, or Chrome's speculative prefetch. Expect your numbers to sit 20–60% above what a JS-based tool reports. Filter obvious user agents if precision matters; otherwise accept it and use the data for relative trends, not absolute truth.

Single-page apps are undercounted. Inertia, Livewire wire:navigate, and any client-side router only trigger one server request for the initial load. Every subsequent navigation is invisible. Either add a lightweight fetch-based ping on route change or don't point this package at an SPA and expect a page-view chart.

Writes are synchronous per request. One INSERT per page view is fine up to a few hundred requests per second and then becomes a real cost. Add indexes on created_at and path early, and if you outgrow the default, either sample (track 1 in N requests) or move the write to a queued job — accepting that the dashboard will lag by a few seconds.

Privacy is your problem, not the package's. IP addresses and user agents are personal data under GDPR. If you're EU-facing: anonymize or hash the IP before storage, document retention, and make sure the page-view table is in your privacy policy. The package gives you raw data, not a compliance story.

Migration collisions. If you already have a page_views table, or you run a multi-tenant setup on a separate connection, edit the published migration before php artisan migrate rather than rolling back afterwards. Similarly, if you're on Postgres and want to partition the table later, decide the primary key shape now.

Publishing the vendor assets is not always obvious. If vendor:publish shows nothing, list the available tags/providers with php artisan vendor:publish and pick the entry for this package interactively — the exact tag name varies between releases, and guessing wastes ten minutes.

Extension points are deliberately thin. There's no event system worth building on in most versions. When you need extra columns (tenant ID, campaign parameters, A/B bucket), the pragmatic move is to publish the migration, add the columns yourself, and copy the middleware into app/Http/Middleware so you control it. Forking three files is cheaper than fighting a package designed to stay small.

Last release is recent, stars are modest. 198 stars and a single maintainer means the API is stable but the community surface is small — expect to read src/ rather than find a Stack Overflow answer. Budget half an hour for that read; it pays for itself the first time something silently stops recording.

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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
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
cgv/localized-resource