Search by

laravel-gtm / route-hits

DGarbs51

Record who hits which routes in your Laravel apps, with a small dashboard.

Package info

github.com/laravel-gtm/route-hits

pkg:composer/laravel-gtm/route-hits

Statistics

Installs: 16

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.1 2026-09-16 22:53 UTC

This package is auto-updated.

Last update: 2026-09-16 22:55:40 UTC


README

Records which authenticated users (by email) view which routes in your Laravel apps, from which IP, and shows a small dashboard of the results. Drop it into any internal tool. Point several apps at one shared database to compare them.

Install

Published on Packagist as laravel-gtm/route-hits. Source lives at github.com/laravel-gtm/route-hits.

1. Require and migrate

composer require laravel-gtm/route-hits
php artisan migrate

The service provider is auto-discovered. Nothing to register.

To develop against a local checkout instead, add a path repository to the application's composer.json before requiring:

"repositories": [
    { "type": "path", "url": "../route-hits" }
]

2. Set environment variables

Minimum for a standalone app: nothing. APP_NAME is used as the app label and hits go to the default database.

To share one route_hits table across several apps, add a connection to each app's config/database.php (for example tracking) pointing at the shared database, then in each app's .env:

ROUTE_HITS_APP=billing            # unique per app
ROUTE_HITS_CONNECTION=tracking    # same value in every app

Run php artisan migrate from one app only. The migration reads ROUTE_HITS_CONNECTION, so it creates the table in the shared database.

3. Gate the dashboard

Open in local. Everywhere else, define the gate in app/Providers/AppServiceProvider.php:

use App\Models\User;
use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::define('viewRouteHits', fn (User $user) => str_ends_with($user->email, '@laravel.com'));
}

4. Create the queue

Outside local, hits are dispatched to the route_tracking queue. On Laravel Cloud, open the environment, click Add compute, then Managed queue, name it route_tracking, standard type, smallest Flex size. Deploy. Cloud sets QUEUE_CONNECTION=cloud for you.

Elsewhere, make sure a worker listens on that queue:

php artisan queue:work --queue=route_tracking,default

5. Make sure the scheduler runs

Pruning is registered in the scheduler. Laravel Cloud has a scheduler toggle on the environment. Elsewhere, php artisan schedule:run must run every minute via cron.

6. Verify

Load any page as a logged-in user, then visit /route-hits. Your app should appear with one hit. If nothing appears, check the queue worker is processing route_tracking and that the table exists on the configured connection:

php artisan queue:work --queue=route_tracking --once
php artisan tinker --execute="echo LaravelGtm\RouteHits\RouteHit::count();"

Configure

Everything is driven by environment variables. Publish the config only if you need more.

Variable Default Purpose
ROUTE_HITS_APP APP_NAME Name stored on every hit. Distinguishes apps in a shared table.
ROUTE_HITS_CONNECTION app default Database connection holding route_hits. Set the same value in every app to share one table.
ROUTE_HITS_QUEUE route_tracking Queue name for the recording job. Ignored in local, which uses the default queue.
ROUTE_HITS_RETENTION_DAYS 90 Hits older than this are pruned daily.
ROUTE_HITS_PATH route-hits Dashboard URI prefix.

Queues and Laravel Cloud

Outside local, the recording job is dispatched to the route_tracking queue on your default queue connection. On Laravel Cloud, create a managed queue named route_tracking in each environment. Cloud sets QUEUE_CONNECTION=cloud on deploy, so nothing else is needed.

Managed standard queues deliver at least once. Each hit's id is a SHA-1 of app, email, method, path, IP, and the UTC second, computed at dispatch. The job inserts with insertOrIgnore, so a replayed or double-fired job never creates a duplicate row. Two genuine hits by the same user to the same path within the same second collapse into one. The job retries 3 times with a 10 second backoff.

In local the job goes to the default queue, so QUEUE_CONNECTION=sync records inline and any local worker picks it up without a --queue flag.

What gets recorded

Successful GET requests through the web middleware group. Skipped: non-GET, XHR, Livewire, non-2xx responses, and the dashboard itself. Each row stores app, user email (null for guests), method, route name, route URI pattern, actual path, IP, and timestamp. Only the email is stored, since user IDs differ between apps.

Dashboard

Visit /route-hits. It shows whatever is in the connected database: apps ranked by page views, and per app the top routes, top users, and views per day. Range toggle for 7, 30, or 90 days.

Access is open in the local environment. Everywhere else, define a gate:

Gate::define('viewRouteHits', fn (User $user) => $user->isAdmin());

Pruning

Uses Laravel's Prunable. The package schedules model:prune --model=LaravelGtm\RouteHits\RouteHit daily. Make sure php artisan schedule:run is running.

Development

Setup

git clone git@github.com:laravel-gtm/route-hits.git
cd route-hits
composer install
npm install

Requires PHP 8.4+ and Node for the Tailwind build. No database server: tests and the dev server use SQLite.

Layout

config/route-hits.php          env-driven config, merged into the host app
database/migrations/           route_hits table, honours ROUTE_HITS_CONNECTION
routes/web.php                 dashboard routes, prefixed by ROUTE_HITS_PATH
src/RouteHitsServiceProvider   wires middleware, routes, views, scheduler
src/TrackRouteHit              web middleware; decides what to record, builds the hit and its id
src/RecordRouteHit             queued job; insertOrIgnore on the hit
src/RouteHit                   Eloquent model, Prunable
src/Authorize                  dashboard gate middleware
src/DashboardController        aggregate queries for index and per-app pages
resources/views/               Blade: index, show, and anonymous components
resources/css/app.css          Tailwind source
resources/dist/route-hits.css  compiled CSS, committed, served by the package
tests/                         Pest tests on Orchestra Testbench
testbench.yaml                 config for the local dev server

Request flow: TrackRouteHit::terminate runs after the response is sent, filters the request, builds the hit array, hashes it into an id, dispatches RecordRouteHit. The job inserts the row. The dashboard only reads aggregates.

Tests

composer test

Tests run against in-memory SQLite through Testbench. tests/TestCase.php registers the provider, sets route-hits.app to billing, and defines a few routes to hit. Add new tests to tests/TrackRouteHitTest.php or a sibling file; tests/Pest.php binds every file in tests/ to that base case.

Run the dashboard locally

touch /tmp/route-hits.sqlite
vendor/bin/testbench migrate
vendor/bin/testbench serve

Open http://127.0.0.1:8000/route-hits. testbench.yaml sets APP_ENV=local, so the gate is open and hits process on the sync queue. Seed some data first:

vendor/bin/testbench tinker --execute='
foreach (["billing" => 120, "crm" => 60, "hr" => 25] as $app => $n) {
    for ($i = 0; $i < $n; $i++) {
        $hit = [
            "app" => $app,
            "email" => ["devon@example.com", "sam@example.com", null][rand(0, 2)],
            "method" => "GET",
            "route_name" => null,
            "uri" => $uri = ["invoices/{id}", "dashboard", "reports"][rand(0, 2)],
            "path" => "/".str_replace("{id}", rand(1, 99), $uri),
            "ip" => "10.0.0.".rand(1, 20),
            "created_at" => now()->utc()->subMinutes(rand(0, 29 * 24 * 60))->startOfSecond(),
        ];
        LaravelGtm\RouteHits\RouteHit::query()->insertOrIgnore(["id" => LaravelGtm\RouteHits\TrackRouteHit::key($hit), ...$hit]);
    }
}'

Editing views

Tailwind classes are compiled from resources/views. After changing any Blade file:

npm run build

Commit the regenerated resources/dist/route-hits.css. Host apps never run a build.

Testing inside a real app

Add a path repository to the app's composer.json and require the package:

"repositories": [{ "type": "path", "url": "../route-hits" }]
composer require laravel-gtm/route-hits:@dev

Composer symlinks the checkout, so edits apply immediately. Run php artisan migrate in the app.

Releasing

Packagist tracks Git tags. Bump nothing in composer.json; the tag is the version.

git tag v1.0.0
git push origin v1.0.0

License

MIT. See LICENSE.md.