jeffersongoncalves/laravel-page-visits
A lightweight Laravel package for tracking page visits in your app. Record visits to routes and pages to see traffic patterns and popular content, without relying on external analytics services. Simple to install, configure, and extend.
Install and publish:
composer require <vendor>/page-visits
php artisan vendor:publish --tag="page-visits-config"
php artisan vendor:publish --tag="page-visits-migrations"
php artisan migrate
On 1.3.0 the migration set includes the new response_time_stats column on page_visit_daily_stats, so a fresh install and an upgrade converge on the same schema.
Look first at three places: the published config (retention window, timezone, tracking toggles), the page_visit_daily_stats migration (now the contract for what your dashboards can read), and the page-visits:aggregate-and-prune command (the only writer of that table).
Schedule the command — nothing aggregates on its own:
// routes/console.php or bootstrap/app.php
Schedule::command('page-visits:aggregate-and-prune')->dailyAt('00:15');
First use case: a daily traffic panel that reads count next to response_time_stats->p95. One row per day gives you visits and latency in the same query, which is the whole point of the release — no second metrics store, no extra table joins.
Read the JSON blob through a cast, not raw. Add an array (or a small DTO cast) for response_time_stats on the daily-stat model so $stat->response_time_stats['p95'] works and nulls stay null. If you swap the model via config, keep the cast or every consumer breaks.
Treat the column as nullable-by-design. A day with visits but no timed requests is a legitimate null, not a data bug. Guard at the edges:
$stats = $day->response_time_stats;
$p95 = $stats['p95'] ?? null;
Drive alerting off p95, not avg. avg hides the tail that users feel. A cheap regression check compares today's p95 against a rolling baseline of the prior 7 days and posts to your alert channel when it crosses a threshold and count is above a floor — otherwise low-traffic days produce noisy spikes.
Keep a derived column for anything you sort or filter on. JSON columns are painful to index across engines. If you build a "slowest days" report, add your own generated/stored column for p95 (or cache the computed report) rather than scanning JSON on every request.
Backfill deliberately. Existing daily rows keep response_time_stats = null until they are re-aggregated. Re-run the command for historical dates before the retention window closes; once raw visits are pruned, the latency data for those days is unrecoverable and the null is permanent.
Pair it with your own instrumentation when you need more than "page visits". If your middleware only times page GETs, API and asset responses won't appear in the percentiles. Either extend the timed surface or accept that these numbers describe HTML page loads only — state which in the dashboard so nobody compares them to APM latency.
Expose via a thin resource. Wrap the metrics in an API resource or view model so the percentage keys are renamed once (p95 → p95_ms) and consumers never hardcode the package's key names.
vendor:publish --tag="page-visits-migrations" leaves the column missing and the command throws on write. Publish first, then migrate, then run the command once by hand to confirm.PERCENTILE_CONT; implementations usually fall back to nearest-rank or interpolation. Confirm which one you're getting — the p99 especially will differ from your APM tool's number, and you don't want to explain that during an incident.count is not, the null is correct and the bug is in your middleware, not the aggregator.RefreshDatabase, freeze time with Carbon::setTestNow(), seed visits, run the command via $this->artisan(...), then assert both the visit count and the shape of response_time_stats — including the all-null case, which is the one that regresses silently.How can I help you explore Laravel packages today?