---
name: ranetrace-analytics
description: 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.
---

# 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
```
