chrickell/laraprints

Track page views and element clicks in Laravel applications. Ships with a Vue 3 + Tailwind dashboard component.

Maintainers

Package info

github.com/Chrickell/laraprints

pkg:composer/chrickell/laraprints

Statistics

Installs: 21

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.1 2026-03-28 03:28 UTC

This package is auto-updated.

Last update: 2026-03-28 03:29:54 UTC


README

Track page views and element clicks across your Laravel application. Stores data asynchronously via queued jobs, and ships with a ready-to-use Vue 3 dashboard component you can drop into any Inertia or Vue-powered page.

What it does

  • Page view tracking — Middleware captures every request (or only those you choose) and stores path, referrer, query params, device type, session ID, and optional user ID.
  • Click tracking — A lightweight frontend composable attaches a global click listener and posts events to a built-in API endpoint.
  • Analytics dashboard — Two JSON endpoints aggregate your data by date range. A self-contained Vue 3 UI with date range filtering, trend charts, sortable tables, and device breakdowns — publish it and embed it into any page in your app.
  • Horizon-style authorization — Access to the dashboard is gated by a viewLaraprints gate. Publish a one-file service provider, add your email addresses, done.
  • Subdomain / multi-app tracking — Point any subdomain app at the primary app's database via a named connection so all analytics land in one place and the dashboard shows a unified view.
  • Async by design — Both page view and click jobs implement ShouldQueue. Nothing blocks the response.
  • Auto-pruningMassPrunable on both models. Schedule model:prune and configure a retention window.

Requirements

PHP 8.1+
Laravel 10, 11, or 12
Frontend scaffold Laravel + Vue/Inertia starter kit
Queue Redis (via queue:work or Laravel Horizon)

Frontend scaffold

The dashboard component and click tracking composable are built for the Vue 3 + Inertia Laravel starter kit. This is the scaffold you get when you select Vue during laravel new. React and Livewire scaffold support is planned for a future release.

The backend — middleware, API endpoints, models, and queue jobs — works with any frontend or no frontend at all.

Queue

A real queue driver is required in production. Jobs are dispatched asynchronously and will not run without a worker.

Using queue:work on Laravel Forge:

Set QUEUE_CONNECTION=redis in your .env, then configure a queue worker daemon in Forge under your site's Queue tab.

Using Laravel Horizon:

Horizon gives you a UI for monitoring your queues and is the recommended approach for Redis. Install it separately:

composer require laravel/horizon
php artisan horizon:install

Then run Horizon as a daemon process on Forge (or your server) instead of queue:work.

Local development:

Set QUEUE_CONNECTION=sync in your .env to process jobs immediately without a worker running.

Installation

composer require chrickell/laraprints
php artisan laraprints:install

The install command publishes the authorization provider, config file, and Vue components, then runs migrations. The service provider is auto-discovered.

Quickstart

After running laraprints:install, three things remain:

# 1. Add your email to the published authorization provider
#    app/Providers/LaraprintsServiceProvider.php → $emails = ['you@example.com']

# 2. Add the tracking middleware to the routes you want to track
#    routes/web.php → Route::middleware('track.requests')->group(...)

# 3. Set up click tracking in your frontend JS entry point
#    setupClickTracking({ inertia: true })

That's it. The AnalyticsDashboard Vue component is published to resources/js/vendor/laraprints/ and ready to embed in any Inertia page.

Install command flags

Flag Description
--no-migrate Skip running migrations (useful if you manage migrations separately)
--no-components Skip publishing Vue components (backend-only installs)
# Backend only, no frontend components, no migrations
php artisan laraprints:install --no-migrate --no-components

Manual publishing (if needed)

php artisan vendor:publish --tag=laraprints-provider     # authorization gate
php artisan vendor:publish --tag=laraprints-config       # config file
php artisan vendor:publish --tag=laraprints-components   # Vue components
php artisan vendor:publish --tag=laraprints-migrations   # migrations only

Page View Tracking

The TrackPageViews middleware intercepts incoming requests and dispatches a queued StorePageView job. Nothing is written synchronously — the response is not affected.

Register the middleware

Option A — on specific route groups (recommended):

// routes/web.php
Route::middleware('track.requests')->group(function () {
    Route::get('/', HomeController::class);
    Route::get('/about', AboutController::class);
    // only these routes are tracked
});

Option B — globally via bootstrap/app.php (Laravel 11+):

->withMiddleware(function (Middleware $middleware) {
    $middleware->append(\Chrickell\Laraprints\Http\Middleware\TrackPageViews::class);
})

Option C — automatically on boot:

Set requests.auto_register_middleware => true in config/laraprints.php and the middleware is pushed onto the global stack automatically when the package boots.

What gets recorded

Each page view stores:

Column Description
domain Hostname of the request (e.g. example.com) — useful when tracking multiple apps into one database
session_id Laravel session ID
visit_id UUID, persisted in session across requests (groups a browsing session)
user_id Authenticated user's ID (nullable, omitted when store_user_id is false)
device_type desktop, mobile, or unknown (derived from User-Agent)
country_code Two-letter country code (nullable)
method HTTP method
current_path Request path
current_params URL query parameters as JSON (nullable)
referrer_path Referring URL path (nullable)
referrer_params Referrer query params as JSON (nullable)
viewed_at Timestamp

Configuration — requests section

'requests' => [
    'enabled'                  => true,
    'auto_register_middleware' => false,
    'methods'                  => ['GET'],
    'track_page_views'         => true,
    'excluded_paths'           => ['api/*', '_debugbar/*', 'livewire/*', 'telescope/*', 'horizon/*'],
    'exclude_assets'           => true,
    'ignore_bots'              => true,
    'only_authenticated'       => false,
    'exclude_admins'           => false,
    'store_user_id'            => true,
    'store_referrer'           => true,
    'store_params'             => true,
    'session_key'              => 'laraprints_visit_id',
],
Key Default Description
enabled true Master toggle. Set false to disable all request tracking without removing middleware
auto_register_middleware false Push middleware onto the global stack automatically on boot
methods ['GET'] HTTP methods to track. Add 'POST' etc. to capture form submissions
track_page_views true Whether to persist to the database. Set false to run the middleware without storing
excluded_paths see above Paths that are never tracked. Supports * wildcards
exclude_assets true Skip requests for static assets (images, CSS, JS, fonts) based on file extension
ignore_bots true Skip requests from known bots and crawlers
only_authenticated false Only track logged-in users. Guest visits are silently skipped
exclude_admins false Skip admin users. true checks $user->is_admin. Pass a callable for custom logic
store_user_id true Store the authenticated user's ID. Set false for anonymized analytics
store_referrer true Capture and store the referrer URL path and query params
store_params true Capture and store the current URL's query params
session_key 'laraprints_visit_id' The session key used to persist the per-visit UUID

exclude_admins examples:

// Checks $user->is_admin attribute
'exclude_admins' => true,

// Custom callable — works with any role/permission system
'exclude_admins' => fn ($user) => $user->hasRole('admin'),
'exclude_admins' => fn ($user) => $user->role === 'superuser',

Click Tracking

Clicks are tracked from the frontend via a lightweight JavaScript composable. It attaches a global listener to the document, detects interactions with buttons, links, inputs, and elements with data-event-click, and posts the event to a built-in API endpoint.

Endpoint

POST /api/clicks

Rate limited to 60 requests per minute per IP. The path is configurable under clicks.route.

Frontend setup

The JS composable is published by laraprints:install. Call setupClickTracking() once when your Vue app mounts — typically in app.js or app.ts:

import { setupClickTracking } from '@/vendor/laraprints/composables/useAnalyticsTracking'

setupClickTracking()

With Inertia

When using Inertia, share the tracking IDs from the server so that server-side page view records and client-side click records can be correlated by the same session_id and visit_id:

In HandleInertiaRequests.php:

public function share(Request $request): array
{
    return array_merge(parent::share($request), [
        'tracking_session_id' => $request->session()->getId(),
        'tracking_visit_id'   => $request->session()->get(
            config('laraprints.requests.session_key', 'laraprints_visit_id')
        ),
    ]);
}

Then call with the inertia flag:

setupClickTracking({ inertia: true })

When inertia: true, the composable reads tracking_session_id and tracking_visit_id from Inertia's shared page props instead of generating its own IDs from sessionStorage.

What gets recorded

Each click event stores:

Column Description
domain Hostname of the request — useful when tracking multiple apps into one database
session_id Session ID shared with the server-side page view record
visit_id Visit UUID shared with the server-side page view record
user_id Authenticated user's ID (nullable, omitted when store_user_id is false)
element HTML tag name of the clicked element (e.g. button, a)
element_class CSS classes on the element (nullable)
element_id id attribute of the element (nullable)
element_style Inline style attribute (nullable)
path Page path where the click occurred
clicked_at Timestamp

Configuration — clicks section

Key Default Description
enabled true Master toggle. Setting false also prevents the click route from being registered
route '/api/clicks' Endpoint path. Change this if it conflicts with your own routes
only_authenticated false Return 401 for unauthenticated click submissions
store_user_id true Store the authenticated user's ID on each click

Analytics Dashboard

The dashboard is a Vue 3 component — it is not a standalone page hosted by this package. You embed it into an Inertia page (or any Vue-mounted Blade view) in your own application. The package registers the JSON API endpoints that power it.

Vue + Inertia only (for now). The dashboard component requires the Vue 3 + Inertia Laravel starter kit. React and Livewire scaffold support is coming in a future release. The backend API endpoints work with any frontend.

Dashboard API endpoints

These are registered automatically when dashboard.enabled is true. Both require the viewLaraprints gate.

GET /laraprints/page-views?start=YYYY-MM-DD&end=YYYY-MM-DD
GET /laraprints/clicks?start=YYYY-MM-DD&end=YYYY-MM-DD

Both start and end are optional. The default range is the last 30 days.

Embedding the dashboard

The AnalyticsDashboard component is published to resources/js/vendor/laraprints/ by laraprints:install.

Inertia page (resources/js/Pages/Analytics.vue):

<script setup>
import AnalyticsDashboard from '@/vendor/laraprints/components/AnalyticsDashboard.vue'
</script>

<template>
  <AnalyticsDashboard />
</template>

Blade with a Vue mount point:

<div id="laraprints-app"></div>

@vite(['resources/js/laraprints.js'])
// resources/js/laraprints.js
import { createApp } from 'vue'
import AnalyticsDashboard from '@/vendor/laraprints/components/AnalyticsDashboard.vue'

createApp(AnalyticsDashboard).mount('#laraprints-app')

Protecting the route

// routes/web.php
Route::get('/laraprints', fn () => inertia('Laraprints'))
    ->middleware(['auth', 'can:viewLaraprints'])
    ->name('laraprints');

AnalyticsDashboard props

Prop Default Description
baseUrl '/laraprints' The URL prefix for the data API. Match this to dashboard.route_prefix in your config if changed
<AnalyticsDashboard base-url="/my-custom-prefix" />

Dashboard features

  • Date range selector — Today, last 7 days, last 30 days, last 90 days, or a custom date range
  • Stat cards — total page views, unique sessions, total clicks, mobile traffic percentage
  • Page Views tab:
    • Trend chart (views over time)
    • Sortable top-25 pages table with desktop/mobile split
    • Device breakdown with proportion bars (desktop / mobile / unknown)
    • Top-15 referrers table
  • Clicks tab:
    • Trend chart (clicks over time)
    • Top-25 clicked pages
    • Top-25 clicked elements with <tag>, .class, #id shown
  • Loading skeletons, error banners with retry buttons, and empty states throughout

Configuration — dashboard section

Key Default Description
enabled true Master toggle. false prevents the API routes from being registered
route_prefix 'laraprints' URL prefix for the data endpoints
middleware ['web'] Middleware wrapping the routes (session, CSRF, etc.). The viewLaraprints gate is always enforced on top of this

Direct model access

Both models are available for custom queries and integrations beyond the dashboard:

use Chrickell\Laraprints\Models\PageView;
use Chrickell\Laraprints\Models\Click;

// All views for a date range
$views = PageView::byDateRange('2025-01-01', '2025-12-31')->get();

// Aggregated stats: path + device_type + count
$stats = PageView::getAnalytics('2025-06-01', '2025-06-30');

// Raw click data for a specific page
$clicks = Click::where('path', '/checkout')->latest('clicked_at')->get();

Authorization

Dashboard access is controlled by the viewLaraprints gate — the same pattern Laravel Horizon uses.

Publish the authorization provider

laraprints:install publishes this automatically. To publish manually:

php artisan vendor:publish --tag=laraprints-provider

This creates app/Providers/LaraprintsServiceProvider.php. The package auto-discovers and registers it on every boot — no manual step in config/app.php or bootstrap/providers.php required.

// app/Providers/LaraprintsServiceProvider.php

class LaraprintsServiceProvider extends LaraprintsApplicationServiceProvider
{
    protected array $emails = [
        'you@example.com',
    ];

    protected function gate(): void
    {
        Gate::define('viewLaraprints', function ($user) {
            return in_array($user->email, $this->emails);
        });
    }
}

Add the email addresses of users who should have access to the $emails array. That's it.

Customizing the gate

The gate() method is yours to replace with any logic you need:

// Allow anyone in the local environment
protected function gate(): void
{
    Gate::define('viewLaraprints', function ($user) {
        return app()->environment('local') || in_array($user->email, $this->emails);
    });
}

// Check a role via spatie/laravel-permission
protected function gate(): void
{
    Gate::define('viewLaraprints', function ($user) {
        return $user->hasRole('admin');
    });
}

// Check a model attribute
protected function gate(): void
{
    Gate::define('viewLaraprints', function ($user) {
        return $user->is_admin === true;
    });
}

Default behavior (before publishing)

If you haven't published the provider yet, a fallback gate is registered automatically:

  • Local environment → access granted
  • All other environments → access denied (403)

This means the dashboard works out of the box during local development, and is safely locked down in production until you publish the provider and configure it.

Subdomain / Multi-App Tracking

By default, Laraprints uses your application's default database connection. You can point it at any named connection from config/database.php:

'database' => [
    'connection' => 'mysql', // any connection name from config/database.php
],

A common pattern is running an admin subdomain (e.g. admin.example.com) as a separate Laravel installation while writing analytics to the primary application's database, so all data lands in one place and the dashboard shows a unified view.

Setup

  1. In the primary app — run the Laraprints migrations as normal. They create the page_views and clicks tables.

  2. In your config/database.php on the subdomain app — add a named connection that points to the primary app's database:

    'connections' => [
        // ... your other connections ...
        'primary' => [
            'driver'    => 'mysql',
            'host'      => env('PRIMARY_DB_HOST', '127.0.0.1'),
            'database'  => env('PRIMARY_DB_DATABASE', 'myapp'),
            'username'  => env('PRIMARY_DB_USERNAME', 'forge'),
            'password'  => env('PRIMARY_DB_PASSWORD', ''),
            'charset'   => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci',
            'prefix'    => '',
            'strict'    => true,
            'engine'    => null,
        ],
    ],
  3. In the subdomain app's config/laraprints.php — set the connection name:

    'database' => [
        'connection' => 'primary',
    ],
  4. Do not run Laraprints migrations on the subdomain app. The tables already exist on the primary connection and the subdomain app will write directly to them.

Queue Configuration

Both StorePageView and StoreClick implement ShouldQueue. A queue worker must be running in production:

php artisan queue:work

During local development, set QUEUE_CONNECTION=sync in your .env to process jobs immediately without a worker.

Configuration — queue section

Key Default Description
connection null Queue connection name ('redis', 'database', 'sqs', etc.). null uses the application default
requests_queue null Queue name for page view jobs. null uses the default queue
clicks_queue null Queue name for click jobs. null uses the default queue

Data Pruning

Add model:prune to your scheduler to automatically delete old records:

// routes/console.php (Laravel 11+)
Schedule::command('model:prune')->daily();

// app/Console/Kernel.php (Laravel 10)
$schedule->command('model:prune')->daily();

Configure the retention window in config/laraprints.php:

'pruning' => [
    'page_views_after_days' => 90, // delete page_views older than 90 days
    'clicks_after_days'     => 90, // delete clicks older than 90 days
],

Both values default to null (keep forever). Set a value to enable pruning.

You can also prune manually:

php artisan model:prune --model="Chrickell\Laraprints\Models\PageView"
php artisan model:prune --model="Chrickell\Laraprints\Models\Click"

Publishing Assets

Tag What it publishes
laraprints-provider app/Providers/LaraprintsServiceProvider.php — authorization gate
laraprints-config config/laraprints.php — all configuration options
laraprints-migrations database/migrations/ — table definitions
laraprints-components resources/js/vendor/laraprints/ — Vue components and composable
# Publish everything at once
php artisan vendor:publish --provider="Chrickell\Laraprints\LaraprintsServiceProvider"

Testing

composer test

License

MIT — see LICENSE.

laraprints

laraprints

laraprints