Set up and configure Ranetrace's privacy-first website analytics with bot detection, the opt-in human-verification beacon, path filtering, and custom request filters.
php artisan boost:add-skill ranetrace/ranetrace-laravel
Save this content to: .claude/skills/ranetrace-analytics/SKILL.md
# Ranetrace website analytics
## When to use this skill
Use this skill when setting up website analytics, configuring bot detection, turning on the human-verification beacon, excluding paths from tracking, or implementing custom request filters.
## How it works
The `TrackPageVisit` middleware is auto-registered on the `web` middleware group when both `RANETRACE_ENABLED` and `RANETRACE_WEBSITE_ANALYTICS_ENABLED` are `true`. No manual middleware registration is needed.
Analytics is privacy-first: no cookies, no fingerprinting, no consent banner, and an optional beacon that sends one opaque token and nothing else. Visitors are identified only by **salted, one-way HMAC hashes**: a user-agent hash and a daily-rotating session-id hash (IP + user agent + date, keyed by a per-install salt), never raw identifiers and never across sites. The raw IP and user agent are used transiently on the server and are not sent (unless `debug.preserve_user_agent` is enabled for local debugging).
## Configuration
```php
// config/ranetrace.php
'website_analytics' => [
'enabled' => env('RANETRACE_WEBSITE_ANALYTICS_ENABLED', false),
'queue' => env('RANETRACE_WEBSITE_ANALYTICS_QUEUE', true),
'queue_name' => env('RANETRACE_WEBSITE_ANALYTICS_QUEUE_NAME', null), // unset: the connection's default queue
'timeout' => env('RANETRACE_WEBSITE_ANALYTICS_TIMEOUT', 10),
'excluded_paths' => [
'horizon', 'nova', 'telescope', 'admin', 'filament', 'api', 'debugbar',
'storage', 'livewire', '_debugbar', 'up', 'sanctum', '_ignition',
],
'request_filter' => null, // Custom filter class (FQCN)
'user_agent' => [
'min_length' => env('RANETRACE_WEBSITE_ANALYTICS_UA_MIN_LENGTH', 10),
'max_length' => env('RANETRACE_WEBSITE_ANALYTICS_UA_MAX_LENGTH', 1000),
],
'throttle_seconds' => env('RANETRACE_WEBSITE_ANALYTICS_THROTTLE_SECONDS', 30),
'min_human_score' => env('RANETRACE_WEBSITE_ANALYTICS_MIN_HUMAN_SCORE', 70),
'bot_detection' => [
'require_client_hints' => env('RANETRACE_WEBSITE_ANALYTICS_REQUIRE_CLIENT_HINTS', true),
'modern_browser_min_version' => env('RANETRACE_WEBSITE_ANALYTICS_MODERN_BROWSER_MIN_VERSION', 100),
],
'beacon' => [
'enabled' => env('RANETRACE_WEBSITE_ANALYTICS_BEACON_ENABLED', false),
'wait_seconds' => env('RANETRACE_WEBSITE_ANALYTICS_BEACON_WAIT_SECONDS', 15),
'delay_ms' => env('RANETRACE_WEBSITE_ANALYTICS_BEACON_DELAY_MS', 0),
'throttle' => env('RANETRACE_WEBSITE_ANALYTICS_BEACON_THROTTLE', '120,1'),
],
],
```
## Human-verification beacon
Every filter above reads what a request claims about itself, which an HTTP client can lie about. The beacon asks the other side to prove it: a real browser that ran JavaScript and had the page visible posts one opaque token back, and the visit is reported as verified or not.
Opt in with one env var:
```env
RANETRACE_WEBSITE_ANALYTICS_BEACON_ENABLED=true
```
Then keep `@ranetraceErrorTracking` just before `</body>` in your layout. That directive renders the beacon as well as the JavaScript error script, so a layout that already has it needs nothing else, and a layout that does not needs one line.
How it works: the capture middleware mints a token for the view, hands it to the page, and dispatches the page visit job with a delay of `wait_seconds`. In the browser the beacon starts at once: after one painted animation frame and `delay_ms` (default 0) it checks the page is visible and posts the token to `POST ranetrace/analytics/verify` (rate limited by `beacon.throttle`, in Laravel's `requests,minutes` form). The endpoint leaves a mark in Ranetrace's own cache store. When the job runs it reads that mark and ships the visit with `verified_human` true or false. A visit is never lost to a missing beacon: it is only reported unverified. Ranetrace's traffic pages then show verified visits, and how many were kept out.
Two constraints:
- **It needs a real queue connection.** On a connection that runs jobs at once (`QUEUE_CONNECTION=sync`, `deferred` or `background`), and with `RANETRACE_WEBSITE_ANALYTICS_QUEUE=false`, the visit cannot be held while the beacon answers, so it is sent with no `verified_human` field at all and the page gets no beacon. A job that runs before its hold ended (a `failover` connection falling through to a target that ignores delays, such as `deferred`) sends the visit without the flag as well. The beacon only verifies anything on a real queue connection.
- **Keep it off behind a full-page cache** (Cloudflare cache-everything, a static export). The token is printed into the HTML, so a cached page hands one token to many visitors.
A page that is hidden when the beacon is due (a background tab, a prerender) is waited on, and the beacon posts once it is shown. Leaving the page posts too, once it has been visible, so a non-zero `delay_ms` never drops a visitor who leaves before it elapses. Prerenders and background tabs are filtered by the visibility check, not by the delay, which is why `delay_ms` defaults to 0: a delay is a minimum time on page. One gap remains by design: a page opened in a background tab and first looked at after `wait_seconds` is reported unverified, because the visit job has already run.
`wait_seconds` is how long the visit is held before it is reported; the mark outlives it by a minute so a busy queue running the job late does not read a beacon that did arrive as absent.
## Excluded paths
The `excluded_paths` config array matches the **first URL segment**. To exclude `/admin/users`, add `'admin'` (this excludes all `/admin/*` routes).
The array replaces the default, it does not merge with it: copy the default list from the published config and append your own first segments, for example `'webhooks'` and `'health'`.
## Custom request filters
For advanced filtering logic, implement the `RequestFilter` contract:
```php
use Illuminate\Http\Request;
use Ranetrace\Laravel\Analytics\Contracts\RequestFilter;
class MyRequestFilter implements RequestFilter
{
public function shouldSkip(Request $request): bool
{
// Skip tracking for internal API consumers
if ($request->header('X-Internal-Client')) {
return true;
}
// Skip tracking for specific IP ranges
if (str_starts_with($request->ip(), '10.0.')) {
return true;
}
return false;
}
}
```
Register in config:
```php
'request_filter' => \App\Analytics\MyRequestFilter::class,
```
## Bot detection
The middleware uses a multi-layer bot detection system:
1. **CrawlerDetect library**: comprehensive crawler detection
2. **Extra bot patterns**: additional bots not caught by CrawlerDetect (ChatGPT, Claude, social media crawlers, SEO bots, headless browsers)
3. **Suspicious user agent patterns**: filters curl, wget, python-requests, etc.
4. **Client hint consistency check**: a user agent claiming Chrome or Edge 100 or newer while sending none of the `Sec-Fetch-Site`, `Sec-Fetch-Mode`, `Sec-Fetch-Dest` or `Sec-CH-UA` headers every real Chromium build emits is rejected as a spoofed HTTP client. Set `RANETRACE_WEBSITE_ANALYTICS_REQUIRE_CLIENT_HINTS=false` to switch the check off, or `RANETRACE_WEBSITE_ANALYTICS_MODERN_BROWSER_MIN_VERSION` (default 100) to move the version floor. Older Chromium builds and non-Chromium browsers are never touched by it
5. **Human probability scoring**: analyzes HTTP headers and patterns to score request likelihood of being human (a `Sec-CH-UA` header adds to that score)
6. **Minimum score threshold**: a request scoring below `RANETRACE_WEBSITE_ANALYTICS_MIN_HUMAN_SCORE` (default 70) is not captured. Lower it towards 50 if legitimate visitors on uncommon browsers or behind header-stripping proxies go missing
7. **Header validation**: requires `Accept-Language` and meaningful `Accept` headers
8. **Human-verification beacon** (opt-in, off by default): the layers above read what a request claims about itself; the beacon asks a real browser to answer. See *Human-verification beacon* above
## Throttling
Requests from the same IP and the same browser to the same path are throttled to prevent duplicate tracking. Default: 30 seconds between tracked visits per IP, user agent and path. The user agent is part of the key so that people behind one shared IP (an office, a mobile carrier) are counted separately, while a reload by one visitor still counts once. Configure with `RANETRACE_WEBSITE_ANALYTICS_THROTTLE_SECONDS`.
## What gets captured
Each page visit includes:
- Path, referrer, and UTM parameters (source, medium, campaign, term, content)
- Device type (mobile, tablet, desktop, console); an iPad on iPadOS 13 or later is counted as desktop, because its browser sends a Mac user agent
- Browser detection (Chrome, Firefox, Safari, Edge, Opera, etc.)
- Privacy-safe user agent hash and daily-rotating session ID hash
- Human probability score
- `verified_human`, only when the beacon is enabled
## Testing
```bash
php artisan ranetrace:test-analytics
```
Use this skill when setting up website analytics, configuring bot detection, turning on the human-verification beacon, excluding paths from tracking, or implementing custom request filters.
The TrackPageVisit middleware is auto-registered on the web middleware group when both RANETRACE_ENABLED and RANETRACE_WEBSITE_ANALYTICS_ENABLED are true. No manual middleware registration is needed.
Analytics is privacy-first: no cookies, no fingerprinting, no consent banner, and an optional beacon that sends one opaque token and nothing else. Visitors are identified only by salted, one-way HMAC hashes: a user-agent hash and a daily-rotating session-id hash (IP + user agent + date, keyed by a per-install salt), never raw identifiers and never across sites. The raw IP and user agent are used transiently on the server and are not sent (unless debug.preserve_user_agent is enabled for local debugging).
// config/ranetrace.php
'website_analytics' => [
'enabled' => env('RANETRACE_WEBSITE_ANALYTICS_ENABLED', false),
'queue' => env('RANETRACE_WEBSITE_ANALYTICS_QUEUE', true),
'queue_name' => env('RANETRACE_WEBSITE_ANALYTICS_QUEUE_NAME', null), // unset: the connection's default queue
'timeout' => env('RANETRACE_WEBSITE_ANALYTICS_TIMEOUT', 10),
'excluded_paths' => [
'horizon', 'nova', 'telescope', 'admin', 'filament', 'api', 'debugbar',
'storage', 'livewire', '_debugbar', 'up', 'sanctum', '_ignition',
],
'request_filter' => null, // Custom filter class (FQCN)
'user_agent' => [
'min_length' => env('RANETRACE_WEBSITE_ANALYTICS_UA_MIN_LENGTH', 10),
'max_length' => env('RANETRACE_WEBSITE_ANALYTICS_UA_MAX_LENGTH', 1000),
],
'throttle_seconds' => env('RANETRACE_WEBSITE_ANALYTICS_THROTTLE_SECONDS', 30),
'min_human_score' => env('RANETRACE_WEBSITE_ANALYTICS_MIN_HUMAN_SCORE', 70),
'bot_detection' => [
'require_client_hints' => env('RANETRACE_WEBSITE_ANALYTICS_REQUIRE_CLIENT_HINTS', true),
'modern_browser_min_version' => env('RANETRACE_WEBSITE_ANALYTICS_MODERN_BROWSER_MIN_VERSION', 100),
],
'beacon' => [
'enabled' => env('RANETRACE_WEBSITE_ANALYTICS_BEACON_ENABLED', false),
'wait_seconds' => env('RANETRACE_WEBSITE_ANALYTICS_BEACON_WAIT_SECONDS', 15),
'delay_ms' => env('RANETRACE_WEBSITE_ANALYTICS_BEACON_DELAY_MS', 0),
'throttle' => env('RANETRACE_WEBSITE_ANALYTICS_BEACON_THROTTLE', '120,1'),
],
],
Every filter above reads what a request claims about itself, which an HTTP client can lie about. The beacon asks the other side to prove it: a real browser that ran JavaScript and had the page visible posts one opaque token back, and the visit is reported as verified or not.
Opt in with one env var:
RANETRACE_WEBSITE_ANALYTICS_BEACON_ENABLED=true
Then keep @ranetraceErrorTracking just before </body> in your layout. That directive renders the beacon as well as the JavaScript error script, so a layout that already has it needs nothing else, and a layout that does not needs one line.
How it works: the capture middleware mints a token for the view, hands it to the page, and dispatches the page visit job with a delay of wait_seconds. In the browser the beacon starts at once: after one painted animation frame and delay_ms (default 0) it checks the page is visible and posts the token to POST ranetrace/analytics/verify (rate limited by beacon.throttle, in Laravel's requests,minutes form). The endpoint leaves a mark in Ranetrace's own cache store. When the job runs it reads that mark and ships the visit with verified_human true or false. A visit is never lost to a missing beacon: it is only reported unverified. Ranetrace's traffic pages then show verified visits, and how many were kept out.
Two constraints:
QUEUE_CONNECTION=sync, deferred or background), and with RANETRACE_WEBSITE_ANALYTICS_QUEUE=false, the visit cannot be held while the beacon answers, so it is sent with no verified_human field at all and the page gets no beacon. A job that runs before its hold ended (a failover connection falling through to a target that ignores delays, such as deferred) sends the visit without the flag as well. The beacon only verifies anything on a real queue connection.A page that is hidden when the beacon is due (a background tab, a prerender) is waited on, and the beacon posts once it is shown. Leaving the page posts too, once it has been visible, so a non-zero delay_ms never drops a visitor who leaves before it elapses. Prerenders and background tabs are filtered by the visibility check, not by the delay, which is why delay_ms defaults to 0: a delay is a minimum time on page. One gap remains by design: a page opened in a background tab and first looked at after wait_seconds is reported unverified, because the visit job has already run.
wait_seconds is how long the visit is held before it is reported; the mark outlives it by a minute so a busy queue running the job late does not read a beacon that did arrive as absent.
The excluded_paths config array matches the first URL segment. To exclude /admin/users, add 'admin' (this excludes all /admin/* routes).
The array replaces the default, it does not merge with it: copy the default list from the published config and append your own first segments, for example 'webhooks' and 'health'.
For advanced filtering logic, implement the RequestFilter contract:
use Illuminate\Http\Request;
use Ranetrace\Laravel\Analytics\Contracts\RequestFilter;
class MyRequestFilter implements RequestFilter
{
public function shouldSkip(Request $request): bool
{
// Skip tracking for internal API consumers
if ($request->header('X-Internal-Client')) {
return true;
}
// Skip tracking for specific IP ranges
if (str_starts_with($request->ip(), '10.0.')) {
return true;
}
return false;
}
}
Register in config:
'request_filter' => \App\Analytics\MyRequestFilter::class,
The middleware uses a multi-layer bot detection system:
Sec-Fetch-Site, Sec-Fetch-Mode, Sec-Fetch-Dest or Sec-CH-UA headers every real Chromium build emits is rejected as a spoofed HTTP client. Set RANETRACE_WEBSITE_ANALYTICS_REQUIRE_CLIENT_HINTS=false to switch the check off, or RANETRACE_WEBSITE_ANALYTICS_MODERN_BROWSER_MIN_VERSION (default 100) to move the version floor. Older Chromium builds and non-Chromium browsers are never touched by itSec-CH-UA header adds to that score)RANETRACE_WEBSITE_ANALYTICS_MIN_HUMAN_SCORE (default 70) is not captured. Lower it towards 50 if legitimate visitors on uncommon browsers or behind header-stripping proxies go missingAccept-Language and meaningful Accept headersRequests from the same IP and the same browser to the same path are throttled to prevent duplicate tracking. Default: 30 seconds between tracked visits per IP, user agent and path. The user agent is part of the key so that people behind one shared IP (an office, a mobile carrier) are counted separately, while a reload by one visitor still counts once. Configure with RANETRACE_WEBSITE_ANALYTICS_THROTTLE_SECONDS.
Each page visit includes:
verified_human, only when the beacon is enabledphp artisan ranetrace:test-analytics
How can I help you explore Laravel packages today?