ads/statistics
ads/statistics is a Laravel package that simplifies recording and querying application statistics. It offers a clean API for logging events, aggregating counts, and generating reports, making it easy to add lightweight analytics to your app without external services.
The package is a Laravel-side tracking layer: it records user interactions (page views, actions, requests) and funnels exceptions/errors into a persisted store for post-hoc debugging in production. With only 3 stars and zero dependents, treat it as a small, early-stage module — read the source before relying on it in a critical path.
Minimal setup sequence:
Install via Composer
composer require ads/statistics
If the package does not yet support Laravel auto-discovery (common at this maturity level), register the service provider manually in config/app.php and, if it ships one, the facade alias.
Publish configuration and migrations
php artisan vendor:publish --provider="Ads\Statistics\StatisticsServiceProvider"
php artisan migrate
Expect a config file (typically config/statistics.php) and one or more tables for interaction logs and error records. Check database/migrations/ in the vendor folder to see the exact table names — the README may lag behind the schema.
Confirm the store is reachable Hit any route, then inspect the interactions table:
php artisan tinker
>>> DB::table('statistics')->latest()->first();
If nothing is written, the tracking hook is not yet wired (likely a middleware or a model observer you must enable).
First meaningful use case: production error visibility.
The package's strongest day-one value is not analytics — it is capturing exceptions with request context (user, URL, payload) so you can debug without SSH access. Wire it into app/Exceptions/Handler.php (or bootstrap/app.php on Laravel 11+) so report() hands the throwable to the package:
public function report(Throwable $e): void
{
app(\Ads\Statistics\Contracts\ErrorLogger::class)->capture($e, request());
parent::report($e);
}
Adjust the class/contract name to whatever the package actually exposes — inspect src/ first.
Where to look first in the repo: the service provider (what gets bound and when), the config file (retention, enabled flags, queue settings), and any middleware class (that's where request-level tracking usually lives).
The typical integration is a global or web group middleware that logs each request. Register it explicitly if auto-registration is absent:
// bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\Ads\Statistics\Http\Middleware\TrackInteraction::class);
})
Scope it deliberately — tracking every asset/API request inflates the table fast. Prefer the web group, or a route middleware applied to the routes you actually care about:
Route::middleware(['web', 'statistics.track'])->group(function () {
// customer-facing pages
});
For business-level insight ("user invited a teammate", "invoice generated"), don't rely on URL matching. Fire explicit events and let the package's listener/logging API record them:
event(new \Ads\Statistics\Events\InteractionRecorded(
type: 'invoice.generated',
userId: auth()->id(),
meta: ['invoice_id' => $invoice->id, 'amount' => $invoice->total],
));
This decouples your business code from HTTP concerns and survives route refactors.
Wrap sensitive operations so failures are attributable:
try {
$payment->charge();
} catch (\Throwable $e) {
Statistics::error($e, ['flow' => 'checkout', 'order' => $order->id]);
throw $e;
}
This is the pattern to reach for in jobs and webhooks where there's no HTTP request to auto-track.
Interaction and error writes should never block a response. If the config exposes a queue toggle, enable it and point it at a dedicated queue:
// config/statistics.php
'queue' => [
'enabled' => env('STATISTICS_QUEUE', true),
'connection' => env('STATISTICS_QUEUE_CONNECTION', 'redis'),
'queue' => 'statistics',
],
Run a worker for that queue in every environment, including production.
Don't treat this as a replacement for Sentry/Telescope. Use it as a durable, queryable app-level log you can join against your own users table. A useful pattern is a dashboard query that groups errors by URL and user over the last 24h:
DB::table('statistics_errors')
->select('url', DB::raw('count(*) as hits'), DB::raw('max(created_at) as last_seen'))
->where('created_at', '>=', now()->subDay())
->groupBy('url')
->orderByDesc('hits')
->get();
Table growth is the number one pitfall. Request-level tracking writes a row per page view; on a moderately trafficked app that's millions of rows in weeks. Before shipping:
// routes/console.php
Schedule::call(fn () => DB::table('statistics')->where('created_at', '<', now()->subDays(30))->delete())->daily();
created_at and user_id in the migration; add them if they're missing.PII and compliance. Request payloads and error context frequently contain passwords, tokens, and personal data. Audit what gets serialized — check the config for a except/redact list, and if none exists, add one before the first production deploy. This package predates any guarantee of GDPR hygiene.
Queue workers silently drop data. If queueing is on and no worker is running for the statistics queue, everything appears to work — no errors, no rows. Add a health check that queries the latest row's created_at and alerts if it's stale.
Recursive error logging. Logging an exception that itself throws (DB down, bad serialization) can loop or lose the original error. Wrap your handler integration in a try/catch that falls back to Log::error().
Config caching. After editing config/statistics.php run php artisan config:clear in local and config:cache on deploy. On older package versions, changes made without a fresh cache can appear ignored, which reads as a bug in the package.
Testing. Disable tracking in your test suite so assertions aren't polluted and the DB stays fast:
// phpunit.xml
<env name="STATISTICS_ENABLED" value="false"/>
Verify the config flag name against the published config — it may be STATISTICS_TRACKING or similar.
Extension points to inspect in source: the bound contract/interface (swap the implementation in your provider for custom persistence), the event class fired on each interaction (listen to it for real-time alerting), and the migration files (copy them into your app and modify — for a package with no dependents, vendor migration changes are unlikely to be preserved across updates anyway).
Vendor lock-in risk. With zero dependents and a small star count, upstream maintenance is uncertain. If you adopt it, either fork it into your own namespace or write a thin abstraction (InteractionLogger interface) in your app that delegates to it — that keeps a future swap to a first-party solution to a one-file change.
How can I help you explore Laravel packages today?