mlstephane/laravel-analytics

Privacy-first analytics package for Laravel: track visitors and user actions without cookies, with a built-in dashboard, pageviews, sessions, bounce rate and visit duration.

Maintainers

Package info

github.com/MltStephane/laravel-analytics

pkg:composer/mlstephane/laravel-analytics

Transparency log

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

1.0.2 2026-08-11 23:40 UTC

This package is auto-updated.

Last update: 2026-08-12 15:14:32 UTC


README

Latest Version on Packagist Total Downloads PHP Version Software License

Privacy-first analytics and observability for Laravel: track visitors and user actions — pageviews, sessions, referrers, environments, bounce rate and visit duration — and read everything on a built-in dashboard. No cookies, no personal data, no IP addresses stored. Inspired by Umami.

Features

  • Dashboard with visitors, pageviews, pages/visit, bounce rate, average visit duration, a time-series chart (24h/7d/30d/90d), top pages, sources, browsers, OS, devices, countries and events — aggregated SQL, server-rendered SVG chart, no external assets.
  • Clean DX facade: Analytics::track() / Analytics::pageview() from PHP, a one-line Blade directive, and a lightweight vanilla JS tracker (~2 Ko gzipped, window.analytics.track(), SPA-aware auto pageviews, data-analytics attributes).
  • Privacy-first: visitor identified by a client-side uuid kept in localStorage, no cookies, no fingerprinting, no IP stored, optional Do-Not-Track support.
  • Sessions with landing page, referrer domain, UTM parameters, bounce flag and duration (30 min inactivity window).
  • Secure public collection endpoint: POST-only, domain allow-list, per-IP rate limiting, bot detection (device-detector), strictly validated and normalized payloads.
  • Pluggable geolocation through a LocationResolver contract (country / region / city).
  • Data retention command: php artisan analytics:prune.

Requirements

  • PHP 8.2+
  • Laravel 11, 12 or 13

Installation

You can install the package via composer:

composer require mltstephane/laravel-analytics

The service provider and the Analytics facade are auto-discovered. Migrations are loaded automatically by the package — just run:

php artisan migrate

Publishing the config or the views is optional:

php artisan vendor:publish --provider="MltStephane\LaravelAnalytics\AnalyticsServiceProvider" --tag="laravel-config"
php artisan vendor:publish --provider="MltStephane\LaravelAnalytics\AnalyticsServiceProvider" --tag="laravel-views"

Usage

Quick start

Add the tracker to your layout <head>:

@analytics

That's it: pageviews are sent automatically. Open the dashboard at /analytics and watch the data arrive.

Tracking events from the browser

The tracker exposes a small global API:

// Custom event
window.analytics.track('signup', { plan: 'pro' });

// Manual pageview
window.analytics.pageview('/blog/my-post', 'My post');

// Attach a stable id to the visitor (max 64 chars)
window.analytics.identify('user-42');

You can also track clicks declaratively with data attributes:

<button data-analytics="signup" data-analytics-plan="pro">Sign up</button>

Any extra data-analytics-* attribute is sent as event data.

Pageviews are tracked automatically on load and on SPA navigation (pushState, popstate, hashchange). Set data-auto-track="false" on the directive output to disable automatic pageviews, and the browser's Do-Not-Track flag is honored by default (tracker.respect_do_not_track).

Tracking events from PHP

Use the Analytics facade:

use MltStephane\LaravelAnalytics\Facades\Analytics;

Analytics::track('purchase', ['qty' => 2, 'total' => 99.99]);
Analytics::pageview('/blog/my-post');

Server-side events are attached to a single shared visitor (config server.visitor_uuid).

Tracking pageviews with a middleware

Register the alias and apply it to the routes you want tracked server-side:

// routes/web.php
Route::middleware('analytics.track-pageview')->get('/blog/{post}', [PostController::class, 'show']);

Event data limits

Event data is normalized server-side (Umami-like rules):

Data type Limit
Properties max 50 per event
Strings max 500 chars
Arrays converted to string, max 500 chars
Numbers rounded to 4 decimals
Event name max 50 chars

Dashboard

The dashboard lives at /analytics (configurable prefix and middleware — default ['web', 'auth']) with periods 24h, 7d, 30d, 90d:

  • Stat cards: unique visitors, pageviews, pages/visit, bounce rate, average visit duration.
  • Time series chart (hourly buckets on 24h, daily otherwise): pageviews + unique visitors per bucket.
  • Top pages (url, pageviews, visitors), top sources (referrer domain or (direct)).
  • Environments: browsers, operating systems, device types (desktop / mobile / tablet).
  • Top countries (unique visitors) and top custom events (occurrences).
  • Last 20 events with type badge, detail and visitor browser.

Geolocation (custom driver)

Country / region / city are resolved through a pluggable resolver. Implement the contract and point the config to your class:

use MltStephane\LaravelAnalytics\Contracts\LocationResolver;

class IpApiResolver implements LocationResolver
{
    public function resolve(string $ip): ?array
    {
        // return ['country' => 'FR', 'region' => 'Île-de-France', 'city' => 'Paris'] or null
    }
}
// config/analytics.php
'geolocation' => [
    'driver' => \App\Support\IpApiResolver::class,
],

The IP is only used for the lookup and is never stored.

Data retention

php artisan analytics:prune

Deletes events older than prune.days (default 365, in chunks), then orphaned sessions and visitors.

Configuration

Key Default Description
enabled true Master switch — no route is registered when false
collect.uri analytics/collect POST endpoint used by the tracker
collect.domains [] Allowed referrer/origin domains. Empty = host of app.url, * = any domain
collect.ignore_bots true Skip bot user agents (device-detector)
collect.ignore_paths [] Regex list (preg_match) of paths that are not tracked
collect.ignore_ips [] IPs that are never tracked (private/monitoring)
collect.rate_limit 60 Max requests/minute/IP to the collection endpoint
collect.session_timeout 30 Minutes of inactivity before a visitor starts a new session
dashboard.enabled true Enable the dashboard routes
dashboard.prefix analytics Dashboard URL prefix
dashboard.middleware ['web', 'auth'] Dashboard middleware stack
tracker.auto_track true Automatic pageviews from the JS tracker
tracker.script_path js/tracker.js URL path serving the tracker script. Use a neutral filename: blockers (Brave, uBlock) block any URL ending in analytics.js
tracker.respect_do_not_track true Honor the browser Do-Not-Track flag
geolocation.driver null Class implementing LocationResolver
prune.days 365 Event retention in days
server.visitor_uuid server Visitor uuid used by facade/middleware events

Security model

  • The collection endpoint is not CSRF-protected by design: it is guarded by a domain allow-list (Origin/Referer) and a per-IP rate limit, and it only stores normalized, bounded data.
  • All inputs are strictly validated (422 on failure). No user string is ever interpolated into SQL — all queries are parameterized or use internal constants.
  • The Analytics manager never throws toward the calling request: failures are logged and null is returned.

Testing

composer test
composer fix

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security related issues, please report them privately instead of opening a public issue.

Credits

License

The MIT License (MIT). Please see License File for more information.