PHPackages                             rpillz/laravel-visitor - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. rpillz/laravel-visitor

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

rpillz/laravel-visitor
======================

Minimalist analytics tracker for Laravel — records page visits, logs to a database, and displays reports in Filament.

1.1(1mo ago)020[1 PRs](https://github.com/RPillz/laravel-visitor/pulls)MITPHPPHP ^8.4CI passing

Since Jun 23Pushed 1mo agoCompare

[ Source](https://github.com/RPillz/laravel-visitor)[ Packagist](https://packagist.org/packages/rpillz/laravel-visitor)[ Docs](https://github.com/rpillz/laravel-visitor)[ GitHub Sponsors](https://github.com/RPillz)[ RSS](/packages/rpillz-laravel-visitor/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (2)Dependencies (34)Versions (4)Used By (0)

Laravel Visitor
===============

[](#laravel-visitor)

[![Latest Version on Packagist](https://camo.githubusercontent.com/8a6997aae3fb468b53e147429e63f7820abafa6ef0e2b7c0d67ef4740ae4f853/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f7270696c6c7a2f6c61726176656c2d76697369746f722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/rpillz/laravel-visitor)[![GitHub Tests Action Status](https://github.com/rpillz/laravel-visitor/actions/workflows/run-tests.yml/badge.svg)](https://github.com/rpillz/laravel-visitor/actions?query=workflow%3Arun-tests+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/3f7f28f1c7dd796a74b2a2accf4353c728c15985536838d777e713f3451dc3b9/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f7270696c6c7a2f6c61726176656c2d76697369746f722e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/rpillz/laravel-visitor)

Minimalist page-visit analytics for Laravel. Records visits to an isolated SQLite database, resolves country and device info in the background, and surfaces reports through a Filament admin panel plugin — with zero impact on page load times.

Fun fact; this started as a quick way to count visitors. I was blown away to discover tens-of-thousands of bots and crawlers visiting my site *every day*! This quickly package evolved to be more about blocking unwanted bots... while still keeping tabs on the real(?) visitors.

Features
--------

[](#features)

- All tracking runs on a **queued job** — never blocks a request
- **Separate database connection** (SQLite by default) keeps analytics data out of your main DB
- Resolves **country &amp; city** from a local MaxMind GeoLite2 database (no external API calls)
- Detects **device type, browser, and OS** from the User-Agent string
- **Anonymous by default** — no user IDs or IPs stored without opt-in
- User ID tracking opt-in, overridable per-call via `Visitor::anonymous()`
- **Bot tracking** — records bots with their name and header fingerprint; unidentified non-browser requests labelled automatically
- **Probe path blocking** — auto-blocks scanners hitting known attack paths (wp-admin, .env, etc.)
- **404 rate-limit blocking** — auto-blocks IPs that rack up too many 404s in a short window
- **Request rate limiting** — auto-blocks fingerprints that exceed a per-minute request threshold
- **Header fingerprinting** — tracks and blocks bots that rotate IPs
- **Verified crawler passthrough** — search engines (Google, Bing, etc.) verified via rDNS or published IP lists bypass auto-blocking
- **Bot name blocking** — blocks known scrapers and AI crawlers by name, even when IP-verified
- **Unverified bot blocking** — blocks any crawler that cannot prove its identity
- **Agent allow-list** — exempt specific User-Agent substrings (uptime monitors, internal crawlers) from bot-name blocking
- **Managed robots.txt** — optionally serve `/robots.txt` with `Disallow` rules via config
- **Database-driven ignore/block list** — block by IP, user ID, user agent wildcard, or header fingerprint via Filament UI; soft-ignore or hard-block (403 returned); temporary or permanent
- **Block logging** — records blocked requests for auditing
- **Filament v5 plugin** with an analytics dashboard and bot management: stats overview, visits chart, top pages, referrers, device breakdown, bot stats, bot list, and ignore/block list management

Requirements
------------

[](#requirements)

- PHP 8.4+
- Laravel 11+
- Filament 5+ (only required for the admin panel plugin)

Installation
------------

[](#installation)

```
composer require rpillz/laravel-visitor
```

Run the install command:

```
php artisan visitor:install
```

This publishes the config file, publishes the migration, creates the SQLite database file if needed, and runs the migration. You can also do these steps manually:

```
php artisan vendor:publish --tag="visitor-config"
php artisan vendor:publish --tag="visitor-migrations"
php artisan migrate
```

### Database connection

[](#database-connection)

By default the package registers a `visitor` SQLite connection automatically, writing to `storage/app/visitor.sqlite`. No changes to your `database.php` are needed unless you want to point it at a different database:

```
// config/database.php
'visitor' => [
    'driver' => 'sqlite',
    'database' => storage_path('app/analytics.sqlite'),
],
```

Or set the `VISITOR_DB_CONNECTION` environment variable to use an existing named connection from your app.

### Remote database (libSQL / Turso)

[](#remote-database-libsql--turso)

To store visit data in a remote [Turso](https://turso.tech) database or any libSQL-compatible endpoint, install the Turso driver:

```
composer require tursodatabase/turso-driver-laravel
```

Then configure your `.env`:

```
# Remote-only (Turso cloud — no local file)
VISITOR_DB_DRIVER=libsql
VISITOR_DB_URL=libsql+wss://your-database.turso.io
VISITOR_DB_AUTH_TOKEN=your-auth-token

# Embedded replica (local SQLite file kept in sync with the remote)
VISITOR_DB_DRIVER=libsql
VISITOR_DB_URL=libsql+wss://your-database.turso.io
VISITOR_DB_AUTH_TOKEN=your-auth-token
VISITOR_DB_DATABASE=/absolute/path/to/local/replica.sqlite
```

The package auto-registers the connection — no changes to `config/database.php` are needed unless you have a naming conflict (see below).

> **Note:** The Turso driver requires the `libsql` PHP extension. See the [turso-driver-laravel documentation](https://github.com/tursodatabase/turso-driver-laravel) for installation instructions.

#### Naming conflicts

[](#naming-conflicts)

The Turso driver resolves connection config from `database.connections.libsql`. If your app already uses that key for another database, set `VISITOR_DB_CONNECTION` to a unique name and define the connection manually in your `config/database.php`:

```
// config/database.php
'visitor_remote' => [
    'driver'    => 'libsql',
    'url'       => env('VISITOR_DB_URL'),
    'authToken' => env('VISITOR_DB_AUTH_TOKEN'),
    'prefix'    => '',
],
```

```
VISITOR_DB_CONNECTION=visitor_remote
```

### GeoIP setup

[](#geoip-setup)

Country and city resolution uses a local MaxMind GeoLite2 database. Download the free `GeoLite2-City.mmdb` file from [MaxMind](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data) (free account required) and place it at:

```
storage/app/geoip/GeoLite2-City.mmdb

```

Override the path via `VISITOR_GEOIP_DATABASE` or in the config. Geo resolution is disabled by default — enable it with `VISITOR_GEOIP_ENABLED=true`. It is silently skipped if the database file is absent.

Usage
-----

[](#usage)

### Automatic tracking via middleware

[](#automatic-tracking-via-middleware)

By default the package appends `visitor.track` to Laravel's `web` middleware group automatically, so all web routes are tracked with no extra configuration.

To disable auto-tracking and apply the middleware selectively, set `auto_track` to `false` in `config/visitor.php` (or `VISITOR_AUTO_TRACK=false` in your `.env`):

```
// config/visitor.php
'auto_track' => false,
```

Then apply the alias to specific route groups:

```
// routes/web.php
Route::middleware('visitor.track')->group(function () {
    Route::get('/', HomeController::class);
    // ...
});
```

Tracking fires in the middleware's `terminate()` method — after the response is sent to the browser.

### Manual tracking

[](#manual-tracking)

Use the `Visitor` facade anywhere in your code:

```
use RPillz\LaravelVisitor\Facades\Visitor;

Visitor::track($request);
```

### Anonymous tracking

[](#anonymous-tracking)

By default (`anonymous = true`), no user IDs are ever stored. If you've enabled user ID storage globally (`anonymous = false`), you can force a specific call to skip it:

```
Visitor::anonymous()->track($request);
```

To enable user ID storage globally, set this in `config/visitor.php`:

```
'anonymous' => false,
```

### Multi-tenant support

[](#multi-tenant-support)

If your app serves multiple tenants, you can route each tenant's visit data and ignore list to a separate database connection. Register a resolver once at boot and the package calls it lazily on every request — no per-request wiring needed:

```
// AppServiceProvider::boot()
use RPillz\LaravelVisitor\LaravelVisitor;

LaravelVisitor::resolveConnectionUsing(function () {
    return tenant() ? 'tenant_' . tenant()->id : config('visitor.connection', 'visitor');
});
```

Both the `visits` table and the `visitor_ignores` table (including the Filament ignore list UI) will use whichever connection the resolver returns for the current request.

If no resolver is registered the package behaves exactly as normal, falling back to `config('visitor.connection', 'visitor')`.

#### Per-tenant database setup

[](#per-tenant-database-setup)

You are responsible for registering each tenant's connection in `config/database.php` (or dynamically via `config([...])`) and running the package migrations against it before tracking begins. The package ships two migration stubs — `create_visits_table` and `create_visitor_ignores_table` — that you can run against each tenant connection as part of your tenant-provisioning flow.

#### Per-call connection override

[](#per-call-connection-override)

To route a single tracking call to a specific connection without a global resolver:

```
Visitor::setConnection('tenant_42')->track($request);
```

This takes priority over any registered resolver for that call only.

### Pruning old records

[](#pruning-old-records)

Schedule the prune command to keep your database tidy:

```
// routes/console.php
Schedule::command('visitor:prune')->daily();
```

The default retention period is 90 days. Override per-run:

```
php artisan visitor:prune --days=90
```

Blocking &amp; Bot Protection
-----------------------------

[](#blocking--bot-protection)

The middleware runs active blocking logic **before** the request reaches your application, so malicious scanners and repeat offenders are rejected at the edge with no application overhead.

### Probe path blocking

[](#probe-path-blocking)

Requests hitting known scanner paths (wp-admin, .env, phpinfo, etc.) are automatically blocked and the requesting IP is added to the block list. Blocked requests receive a 404 response so scanners get no information about your stack.

Configure the paths and block duration in `config/visitor.php`:

```
'block_probes' => true, // set false to disable entirely

'probe_paths' => [
    'wp-admin*',
    'wp-login*',
    '.env*',
    'phpinfo*',
    'xmlrpc.php',
    // add your own patterns — supports * and ? wildcards
],

'probe_block_duration' => env('VISITOR_PROBE_BLOCK_DURATION', 60*24*3), // minutes, null = permanent
```

The default block duration is 3 days (4320 minutes). Set to `null` for a permanent block, or use `VISITOR_PROBE_BLOCK_DURATION` to override. Set `VISITOR_BLOCK_PROBES=false` to disable probe blocking without touching the config file.

### 404 rate-limit blocking

[](#404-rate-limit-blocking)

IPs that generate too many 404 responses in a short window are automatically blocked. This catches scanners that don't match any specific probe path but are obviously enumerating your routes.

```
'probe_404' => [
    'threshold' => env('VISITOR_PROBE_404_THRESHOLD', 5), // 404s before blocking
    'window'    => env('VISITOR_PROBE_404_WINDOW', 3),    // rolling window in minutes
],
```

Once the threshold is exceeded, further requests from that IP return 429 until the block expires.

### Header fingerprinting

[](#header-fingerprinting)

The middleware computes a lightweight fingerprint from the request's HTTP headers. This fingerprint is stored alongside each visit and is used when auto-blocking — so a scanner that rotates IPs is still caught and blocked by its fingerprint.

Manual blocks via the Filament **Bot List** resource also prefer fingerprint-based blocks over user-agent wildcards when a fingerprint is available.

### Verified crawlers

[](#verified-crawlers)

When `verified_crawlers` is enabled, the middleware attempts to verify a crawler using two methods:

- **rDNS** — the bot's IP reverse-resolves to a hostname that forward-resolves back to the same IP, with a suffix matching a known search engine domain (Googlebot, Bingbot, DuckDuckBot, Yandex, Baidu, Applebot, Qwant, PetalSearch). These domains are hardcoded and do not need configuration.
- **IP lists** — the bot's IP is checked against CIDR prefix lists published by crawler operators. The default list covers ClaudeBot, BingBot, Meta crawlers, GPTBot, Perplexity, and others.

Verified bots bypass probe-path and 404-rate blocking and fingerprint rate limiting. Their visits are stored with `is_verified = true`.

```
'verified_crawlers' => [
    'enabled'   => env('VISITOR_VERIFIED_CRAWLERS', true),
    'cache_ttl' => env('VISITOR_CRAWLER_CACHE_TTL', 1440), // minutes per IP
    'ip_lists'  => [
        'https://raw.githubusercontent.com/hexydec/ip-ranges/main/output/crawlers.json',
    ],
],
```

Verification results are cached per IP for `cache_ttl` minutes. IP list responses are cached for 24 hours.

### Bot name blocking

[](#bot-name-blocking)

Even verified crawlers can be blocked by name. `block_verified_bots` lists bot names (resolved from the User-Agent) that are rejected regardless of verification status. `block_unverified_bots` rejects any crawler that identifies itself by name but cannot be verified:

```
// Blocked by name, even if IP-verified
'block_verified_bots' => [
    'ClaudeBot', 'GPTBot', 'PerplexityBot', 'Amazonbot', 'CCBot', 'Bytespider',
    'Meta-WebIndexer', 'Meta-ExternalAds', 'Meta-ExternalAgent',
    'Semrush', 'Ahrefs', 'DotBot', 'MJ12bot', 'Diffbot', 'PetalBot', 'Scrapy',
],

// Block any crawler that cannot prove its identity (default: true)
'block_unverified_bots' => env('VISITOR_BLOCK_UNVERIFIED_BOTS', true),
```

Search engines (Googlebot, Bingbot, etc.) are not in the `block_verified_bots` list by default — leave them unblocked so they continue to index your site.

### Allowing specific agents

[](#allowing-specific-agents)

Uptime monitors, internal crawlers, and other trusted services may be detected as bots but publish no IP list and have no rDNS to verify against. Add a substring of their User-Agent to `allow_agents` to let them through regardless of verification status:

```
'allow_agents' => [
    'Phare',           // Phare uptime monitor
    'UptimeRobot',     // UptimeRobot
],
```

Each value is matched as a case-sensitive substring of the User-Agent string. An agent in this list bypasses both `block_verified_bots` and `block_unverified_bots` — it is never blocked by bot-name checks, though explicit IP/fingerprint/user-agent blocks in the ignore list still apply.

### Request rate limiting

[](#request-rate-limiting)

The fingerprint rate limiter counts every request from a given header fingerprint within a rolling window, catching high-volume scrapers that only hit valid pages (and would never trigger the 404 limiter):

```
'rate_limit' => [
    'enabled'    => env('VISITOR_RATE_LIMIT', true),
    'threshold'  => env('VISITOR_RATE_LIMIT_THRESHOLD', 60), // requests per window
    'window'     => env('VISITOR_RATE_LIMIT_WINDOW', 1),     // minutes
    'auto_block' => env('VISITOR_RATE_LIMIT_AUTO_BLOCK', true),
],
```

When `auto_block` is true, a fingerprint that exceeds the threshold is written to the block list so subsequent requests are caught by `isBlocked()` immediately — no further rate-limiter checks needed.

### robots.txt

[](#robotstxt)

The package can serve `GET /robots.txt` with `Disallow: /` entries for each listed User-agent. Useful for discouraging commercial SEO crawlers and AI scrapers that do respect `robots.txt`:

```
'robots_txt' => [
    'enabled' => env('VISITOR_ROBOTS_TXT', false),
    'disallow' => [
        'ClaudeBot', 'Amazonbot', 'meta-externalagent',
        'meta-webindexer', 'meta-externalads',
        'GPTBot', 'Google-Extended', 'PerplexityBot', 'CCBot',
    ],
],
```

Leave `enabled` as `false` (the default) if your application already manages its own `robots.txt`. If you enable this, add `robots.txt` to `exclude_paths` so those hits are not recorded in your visit analytics.

Bots that ignore `robots.txt` are exactly the kind of traffic the probe-path, 404-rate, and fingerprint-rate blocking are designed to catch.

### Block logging

[](#block-logging)

Blocked requests are recorded by default for auditing. Disable if you do not need the log:

```
'log_blocks' => env('VISITOR_LOG_BLOCKS', true),
```

When enabled, blocked requests are dispatched to the queue and stored as visit records with `is_blocked = true`. The `Visit` model's default global scope excludes these from all normal queries, so they never appear in analytics — only in the raw table.

Ignore List &amp; Block List
----------------------------

[](#ignore-list--block-list)

The ignore/block list controls what happens to a visitor:

- **Ignore** (tracking skipped) — the visit is silently not recorded; the request proceeds normally
- **Block** (`is_blocked = true`) — the request is rejected with a 403 before reaching your application

Entries can target:

TypeMatches`ip`Exact IP address`user_id`Authenticated user ID`user_agent`User-Agent string (supports `*` and `?` wildcards)`header_fingerprint`Computed header fingerprint hashEntries can be **permanent** or **temporary** (`expires_at`). Automatic blocks (from probe detection and 404 rate limiting) are flagged `is_automatic = true` and are distinct from manually added entries.

### Managing the list

[](#managing-the-list)

When the Filament plugin is registered, an **Ignore List** resource appears in the Analytics navigation group. From there you can add, edit, or remove entries.

**When an ignore entry is added**, all existing visit records matching that value are deleted immediately. Future visits are silently skipped.

**When a block entry is added**, the IP, user agent, or fingerprint is rejected at the middleware with a 403 — no application code runs at all.

The list is loaded from the database and cached for 5 minutes (`visitor.ignore_list.`). The cache is flushed automatically whenever an entry is added or removed.

Filament Plugin
---------------

[](#filament-plugin)

Register the plugin in your Filament panel provider:

```
use RPillz\LaravelVisitor\Filament\VisitorPlugin;

public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->plugins([
            VisitorPlugin::make(),
        ]);
}
```

This adds an **Analytics** page to your panel at `/your-panel/analytics` with the following widgets and resources:

### Analytics dashboard widgets

[](#analytics-dashboard-widgets)

WidgetDescriptionOverview StatsTotal visits, unique visitors (by session), today's countVisits ChartLine chart of visits over time — filter by 7, 30, or 90 daysTop PagesMost-visited paths ranked by visit countTop ReferrersReferring domains ranked by visit countDevices &amp; BrowsersBreakdown of device type, browser, and OSBot StatsTotal bot visits, today's count, and verified crawler count (hidden when `track_bots = false`)Top BotsTable of bots ranked by visit count with verified status (hidden when `track_bots = false`)### Resources

[](#resources)

ResourceDescriptionBot ListAll tracked bots grouped by name and fingerprint; one-click block action per entryIgnore ListFull ignore/block list management — add entries by IP, user ID, user agent, or fingerprintThe **Bot List** block action uses the header fingerprint when one is recorded, falling back to a wildcard user-agent rule (`*BotName*`) when not.

Configuration
-------------

[](#configuration)

> **Horizon users:** `TrackVisitJob` is a high-frequency, low-importance job that will flood your Horizon dashboard. Add it to the `silenced` array in `config/horizon.php`:
>
> ```
> 'silenced' => [
>     \RPillz\LaravelVisitor\Jobs\TrackVisitJob::class,
> ],
> ```

```
// config/visitor.php

return [
    // Database connection for visit records
    'connection' => env('VISITOR_DB_CONNECTION', 'visitor'),

    // Queue connection and name for the tracking job
    'queue' => [
        'connection' => env('VISITOR_QUEUE_CONNECTION', null),
        'name'       => env('VISITOR_QUEUE_NAME', 'default'),
    ],

    // Automatically append visitor.track to the web middleware group
    'auto_track' => env('VISITOR_AUTO_TRACK', true),

    // Paths to exclude from tracking (supports * and ? wildcards)
    'exclude_paths' => [
        'admin*', '_debugbar*', 'horizon*', 'telescope*', 'livewire*', '_ignition*',
    ],

    // Track bot/crawler visits (stores bot_name, fingerprint, is_verified)
    'track_bots' => env('VISITOR_TRACK_BOTS', true),

    // Skip requests from these IPs
    'exclude_ips' => [],

    // Only track these HTTP methods
    'track_methods' => ['GET'],

    // Never store the authenticated user ID
    'anonymous' => true,

    // Never store IP addresses (also skips country/city resolution)
    'store_ip' => env('VISITOR_STORE_IP', false),

    // Prevent duplicate records for the same session+path within a rolling window
    'deduplication' => [
        'enabled' => env('VISITOR_DEDUP_ENABLED', true),
        'window'  => env('VISITOR_DEDUP_WINDOW', 30), // minutes
    ],

    // Local MaxMind GeoLite2 database for country/city resolution (disabled by default)
    'geoip' => [
        'enabled'  => env('VISITOR_GEOIP_ENABLED', false),
        'database' => env('VISITOR_GEOIP_DATABASE', storage_path('app/geoip/GeoLite2-City.mmdb')),
    ],

    // Bot names (resolved from User-Agent) to block even when IP-verified
    'block_verified_bots' => [
        'ClaudeBot', 'GPTBot', 'PerplexityBot', 'Amazonbot', 'CCBot', 'Bytespider',
        'Meta-WebIndexer', 'Meta-ExternalAds', 'Meta-ExternalAgent',
        'Semrush', 'Ahrefs', 'DotBot', 'MJ12bot', 'Diffbot', 'PetalBot', 'Scrapy',
    ],

    // Block any crawler that cannot be verified via rDNS or a published IP list
    'block_unverified_bots' => env('VISITOR_BLOCK_UNVERIFIED_BOTS', true),

    // User-Agent substrings that bypass block_verified_bots and block_unverified_bots
    'allow_agents' => [
        // 'Phare',
    ],

    // Fingerprint-based rate limiter — catches high-volume scrapers that hit only valid pages
    'rate_limit' => [
        'enabled'    => env('VISITOR_RATE_LIMIT', true),
        'threshold'  => env('VISITOR_RATE_LIMIT_THRESHOLD', 60), // requests per window
        'window'     => env('VISITOR_RATE_LIMIT_WINDOW', 1),     // minutes
        'auto_block' => env('VISITOR_RATE_LIMIT_AUTO_BLOCK', true),
    ],

    // Serve GET /robots.txt with Disallow entries for each listed User-agent
    'robots_txt' => [
        'enabled' => env('VISITOR_ROBOTS_TXT', false),
        'disallow' => [
            'ClaudeBot', 'Amazonbot', 'meta-externalagent',
            'meta-webindexer', 'meta-externalads',
            'GPTBot', 'Google-Extended', 'PerplexityBot', 'CCBot',
        ],
    ],

    // Auto-block IPs and fingerprints that hit probe paths; return 404
    'block_probes' => env('VISITOR_BLOCK_PROBES', true),

    // Record blocked requests as visits with is_blocked=true (excluded from analytics)
    'log_blocks' => env('VISITOR_LOG_BLOCKS', true),

    // Paths treated as probe/scanner activity (supports wildcards)
    'probe_paths' => [
        'wp-admin*', 'wp-login*', '.env*', 'phpinfo*', 'xmlrpc.php',
    ],

    // How long auto-blocks last (minutes); null = permanent
    'probe_block_duration' => env('VISITOR_PROBE_BLOCK_DURATION', 60 * 24 * 3), // 3 days

    // Auto-block IPs that hit this many 404s within the window
    'probe_404' => [
        'threshold' => env('VISITOR_PROBE_404_THRESHOLD', 5),
        'window'    => env('VISITOR_PROBE_404_WINDOW', 3), // minutes
    ],

    // Verify legitimate search engine bots via rDNS and published IP lists
    'verified_crawlers' => [
        'enabled'   => env('VISITOR_VERIFIED_CRAWLERS', true),
        'cache_ttl' => env('VISITOR_CRAWLER_CACHE_TTL', 1440), // minutes per IP
        'ip_lists'  => [
            'https://raw.githubusercontent.com/hexydec/ip-ranges/main/output/crawlers.json',
        ],
    ],

    // Retention period for visit records
    'pruning' => [
        'enabled' => true,
        'days'    => env('VISITOR_PRUNE_DAYS', 90),
    ],
];
```

What Gets Recorded
------------------

[](#what-gets-recorded)

Each visit record stores:

ColumnDescription`url`Full URL`path`URL path (indexed)`query`Query string`referrer`Full referrer URL`referrer_domain`Referrer domain only (indexed)`ip_address`Visitor IP (nullable — off by default)`country`ISO 3166-1 alpha-2 country code`city`City name`device_type``desktop`, `mobile`, or `tablet``browser`Browser name`os`Operating system`user_agent`Raw User-Agent string`header_fingerprint`Hash of request headers for bot fingerprinting`bot_name`Bot/crawler name (null for human visits)`is_blocked``true` when the record is a logged blocked request`is_verified``true` for rDNS-verified crawlers (Google, Bing, etc.)`is_user``true` when an authenticated user was present`user_id`Auth user ID (nullable — off by default)`session_id`Session ID for unique visitor counting (indexed)`created_at`Timestamp (indexed)Blocked visit records (`is_blocked = true`) are excluded from all normal queries via a global scope on the `Visit` model. They are only visible in the raw table or when explicitly calling `withoutGlobalScope('exclude_blocked')`.

GDPR Considerations
-------------------

[](#gdpr-considerations)

By default this package does not track personal data, but does have the option to store IP addresses, Geolocation, and user IDs, which are personal data under GDPR. Depending on your jurisdiction and use case you may need user consent before tracking, or you may want to avoid storing personal data altogether.

### Default behaviour (consent-free)

[](#default-behaviour-consent-free)

Out of the box, `anonymous = true` and `store_ip = false`, so no personal data is stored. Records contain only path, referrer domain, device type, browser, OS, and session ID — none of which are personal data on their own. No consent mechanism is required.

If you want to opt in to storing user IDs and IP addresses (for richer analytics), set these in your `.env`:

```
VISITOR_STORE_IP=true
```

```
// config/visitor.php
'anonymous' => false,
'store_ip' => true,
```

When storing personal data you are responsible for obtaining user consent and disclosing this in your privacy policy.

### Right to erasure

[](#right-to-erasure)

To delete all visit records linked to a specific user (GDPR Article 17):

```
# By user ID
php artisan visitor:forget {userId}

# By session ID (for anonymous visitors)
php artisan visitor:forget --session={sessionId}

# By IP address
php artisan visitor:forget --ip={ipAddress}
```

Safe to call from your app's user deletion flow:

```
Artisan::call('visitor:forget', ['userId' => $user->id, '--force' => true]);
```

### Data retention

[](#data-retention)

Set a retention period and schedule the prune command so old records are automatically removed:

```
// config/visitor.php
'pruning' => ['enabled' => true, 'days' => 90],
```

```
// routes/console.php
Schedule::command('visitor:prune')->daily();
```

Testing
-------

[](#testing)

```
composer test
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [RPillz](https://github.com/RPillz)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

42

—

FairBetter than 88% of packages

Maintenance91

Actively maintained with recent releases

Popularity9

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity53

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

2

Last Release

44d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/9386244?v=4)[Ryan](/maintainers/RPillz)[@RPillz](https://github.com/RPillz)

---

Top Contributors

[![RPillz](https://avatars.githubusercontent.com/u/9386244?v=4)](https://github.com/RPillz "RPillz (4 commits)")

---

Tags

laravelanalyticsfilamentlaravel-visitorRPillz

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/rpillz-laravel-visitor/health.svg)

```
[![Health](https://phpackages.com/badges/rpillz-laravel-visitor/health.svg)](https://phpackages.com/packages/rpillz-laravel-visitor)
```

###  Alternatives

[codewithdennis/filament-select-tree

The multi-level select field enables you to make single selections from a predefined list of options that are organized into multiple levels or depths.

329575.9k33](/packages/codewithdennis-filament-select-tree)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3914.8k](/packages/rawilk-profile-filament-plugin)[tapp/filament-form-builder

User facing form builder using Filament components

133.5k5](/packages/tapp-filament-form-builder)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
