From e29aee21fda8733053964041a9238b1f65685414 Mon Sep 17 00:00:00 2001 From: jalel Date: Wed, 29 Apr 2026 00:35:19 +0100 Subject: [PATCH 1/2] feat: integrate ghdj/laravel-visitor-tracker Wire the ghdj/laravel-visitor-tracker package (^1.1) into the app to provide first-party visitor analytics for dev-tools.online. Integration: - Require ghdj/laravel-visitor-tracker ^1.1 (composer.json/lock). - Append the package's TrackVisitor middleware to the web group in bootstrap/app.php so all web requests are recorded. - Add the /tools/visitor-tracker route and sitemap entry, served by a new visitorTracker() action on ToolController plus a public-facing Blade view under resources/views/tools/visitor-tracker.blade.php. - Rely on the package's loadMigrationsFrom for the visitors/visits schema; published copies are intentionally not committed. Dashboard auth (env-driven via config/visitor-tracker.php): - Local: token-based auth so the dashboard is gated by a shared secret. - Production: allow_unprotected enabled because the dashboard route is fronted by Cloudflare Access, which performs the actual auth. Operations: - Add cron/migrate.php as the OVH cron entrypoint that bootstraps Laravel and runs migrate --force, so package migrations apply on shared hosting without shell access. - .gitignore: ignore database/*.sqlite, *.sqlite-journal and bootstrap/cache/*.php to keep local artifacts out of the repo. - deploy.yml: strip database/*.sqlite* and bootstrap/cache/*.php from the build before SFTP so dev artifacts never reach production. --- .github/workflows/deploy.yml | 9 + .gitignore | 3 + app/Http/Controllers/ToolController.php | 33 ++ bootstrap/app.php | 4 +- composer.json | 1 + composer.lock | 79 ++++- config/visitor-tracker.php | 323 ++++++++++++++++++ cron/migrate.php | 34 ++ .../views/tools/visitor-tracker.blade.php | 299 ++++++++++++++++ routes/web.php | 2 + 10 files changed, 785 insertions(+), 2 deletions(-) create mode 100644 config/visitor-tracker.php create mode 100644 cron/migrate.php create mode 100644 resources/views/tools/visitor-tracker.blade.php diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2c048c4..10c5af9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -69,6 +69,15 @@ jobs: rm -f .env.example rm -f deploy.sh + # Never ship a local sqlite — would overwrite production data + rm -f database/*.sqlite database/*.sqlite-journal + + # Force the server to re-cache config/routes after deploy + rm -f bootstrap/cache/config.php + rm -f bootstrap/cache/routes-v7.php + rm -f bootstrap/cache/services.php + rm -f bootstrap/cache/packages.php + - name: Deploy via SFTP uses: wlixcc/SFTP-Deploy-Action@v1.2.4 with: diff --git a/.gitignore b/.gitignore index a7e49f9..931efa7 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,9 @@ /storage/*.key /storage/pail /vendor +/bootstrap/cache/*.php +/database/*.sqlite +/database/*.sqlite-journal Homestead.json Homestead.yaml Thumbs.db diff --git a/app/Http/Controllers/ToolController.php b/app/Http/Controllers/ToolController.php index d0a6454..e1653b3 100644 --- a/app/Http/Controllers/ToolController.php +++ b/app/Http/Controllers/ToolController.php @@ -147,6 +147,12 @@ public function index(): View 'route' => 'tools.diff', 'icon' => 'diff', ], + [ + 'name' => 'Visitor Tracker', + 'description' => 'Server-side visitor analytics for Laravel applications', + 'route' => 'tools.visitor-tracker', + 'icon' => 'chart', + ], ]; return view('home', compact('tools')); @@ -266,4 +272,31 @@ public function diff(): View { return view('tools.diff'); } + + public function visitorTracker(): View + { + $stats = app(\Ghdj\VisitorTracker\Services\StatisticsService::class); + $parser = new \Ghdj\VisitorTracker\Services\UserAgentParser(); + $botDetector = new \Ghdj\VisitorTracker\Services\BotDetector(); + + $userAgent = request()->userAgent(); + $parsedUA = $parser->parse($userAgent); + $isBot = $botDetector->isBot($userAgent); + $botName = $isBot ? $botDetector->getBotName($userAgent) : null; + $botCategory = $isBot ? $botDetector->getBotCategory($userAgent) : null; + + return view('tools.visitor-tracker', [ + 'summary' => $stats->summary(), + 'browsers' => $stats->browserStats(5), + 'platforms' => $stats->platformStats(5), + 'devices' => $stats->deviceStats(), + 'topPages' => $stats->mostVisitedPages(5), + 'userAgent' => $userAgent, + 'parsedUA' => $parsedUA, + 'isBot' => $isBot, + 'botName' => $botName, + 'botCategory' => $botCategory, + 'visitorIp' => request()->ip(), + ]); + } } diff --git a/bootstrap/app.php b/bootstrap/app.php index c3928c5..8e7cb9e 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -12,7 +12,9 @@ health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - // + $middleware->web(append: [ + \Ghdj\VisitorTracker\Middleware\TrackVisitor::class, + ]); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/composer.json b/composer.json index 681cfab..4d695fc 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "require": { "php": "^8.2", "doctrine/sql-formatter": "^1.5", + "ghdj/laravel-visitor-tracker": "^1.1", "laravel/framework": "^12.0", "laravel/tinker": "^2.10.1", "league/commonmark": "^2.8", diff --git a/composer.lock b/composer.lock index 34ee7ce..9c9e109 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6bb720e3c979f64f44a17196c295bcb9", + "content-hash": "0a858fe21191d8c1887ca6ad34dbbee9", "packages": [ { "name": "brick/math", @@ -634,6 +634,83 @@ ], "time": "2023-10-12T05:21:21+00:00" }, + { + "name": "ghdj/laravel-visitor-tracker", + "version": "v1.1.0", + "source": { + "type": "git", + "url": "https://github.com/GhDj/laravel-visitor-tracker.git", + "reference": "b4b28d3a22d46b39798d53dc47f5a3672633a873" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GhDj/laravel-visitor-tracker/zipball/b4b28d3a22d46b39798d53dc47f5a3672633a873", + "reference": "b4b28d3a22d46b39798d53dc47f5a3672633a873", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.0|^12.0", + "illuminate/database": "^10.0|^11.0|^12.0", + "illuminate/http": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "php": "^8.1" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.0", + "laravel/pint": "^1.0", + "nunomaduro/collision": "^7.0|^8.0", + "orchestra/testbench": "^8.0|^9.0|^10.0", + "pestphp/pest": "^2.34|^3.0", + "pestphp/pest-plugin-laravel": "^2.3|^3.0", + "phpstan/phpstan": "^1.10|^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "VisitorTracker": "Ghdj\\VisitorTracker\\Facades\\VisitorTracker" + }, + "providers": [ + "Ghdj\\VisitorTracker\\VisitorTrackerServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Helpers/helpers.php" + ], + "psr-4": { + "Ghdj\\VisitorTracker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "ghdj", + "role": "Developer" + } + ], + "description": "A comprehensive visitor tracking package for Laravel applications with analytics, geolocation, and bot detection - zero external dependencies", + "homepage": "https://github.com/ghdj/laravel-visitor-tracker", + "keywords": [ + "analytics", + "bot-detection", + "geolocation", + "laravel", + "statistics", + "tracking", + "visitor", + "zero-dependency" + ], + "support": { + "issues": "https://github.com/GhDj/laravel-visitor-tracker/issues", + "source": "https://github.com/GhDj/laravel-visitor-tracker/tree/v1.1.0" + }, + "time": "2026-04-28T11:53:44+00:00" + }, { "name": "graham-campbell/result-type", "version": "v1.1.3", diff --git a/config/visitor-tracker.php b/config/visitor-tracker.php new file mode 100644 index 0000000..97c245e --- /dev/null +++ b/config/visitor-tracker.php @@ -0,0 +1,323 @@ + env('VISITOR_TRACKER_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Database Tables + |-------------------------------------------------------------------------- + | + | Customize the table names used by the package. + | + */ + 'tables' => [ + 'visitors' => 'visitors', + 'visits' => 'visits', + ], + + /* + |-------------------------------------------------------------------------- + | Cookie Settings + |-------------------------------------------------------------------------- + | + | Settings for the visitor identification cookie. + | + */ + 'cookie' => [ + 'name' => 'visitor_tracker', + 'expiration' => 60 * 24 * 365, // 1 year in minutes + ], + + /* + |-------------------------------------------------------------------------- + | Tracking Exclusions + |-------------------------------------------------------------------------- + | + | Define what should be excluded from tracking. + | + */ + 'exclude' => [ + /* + |---------------------------------------------------------------------- + | Excluded Paths + |---------------------------------------------------------------------- + | + | Path patterns to exclude from tracking. Supports wildcards (*). + | + */ + 'paths' => [ + 'api/*', + 'admin/*', + '_debugbar/*', + 'telescope/*', + 'horizon/*', + 'livewire/*', + 'sanctum/*', + ], + + /* + |---------------------------------------------------------------------- + | Excluded HTTP Methods + |---------------------------------------------------------------------- + | + | HTTP methods to exclude from tracking. + | + */ + 'methods' => [ + 'OPTIONS', + 'HEAD', + ], + + /* + |---------------------------------------------------------------------- + | Excluded Status Codes + |---------------------------------------------------------------------- + | + | Response status codes to exclude from tracking. + | + */ + 'status_codes' => [ + 301, + 302, + 307, + 308, + 404, + 500, + ], + + /* + |---------------------------------------------------------------------- + | Excluded IPs + |---------------------------------------------------------------------- + | + | IP addresses to exclude from tracking. Supports CIDR notation. + | + */ + 'ips' => [ + // '127.0.0.1', + // '192.168.0.0/16', + ], + + /* + |---------------------------------------------------------------------- + | Excluded User Agents + |---------------------------------------------------------------------- + | + | User agent patterns to exclude (case-insensitive partial match). + | + */ + 'user_agents' => [ + // 'curl', + // 'postman', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Bot Detection + |-------------------------------------------------------------------------- + | + | Configuration for detecting and handling bots/crawlers. + | + */ + 'bots' => [ + 'track' => false, // Set to true to track bot visits + 'detect' => true, // Enable bot detection + + // Additional bot patterns (merged with built-in patterns) + 'additional_patterns' => [ + // 'custom-bot', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Agent Parser + |-------------------------------------------------------------------------- + | + | Configuration for the native user agent parser. + | + */ + 'parser' => [ + // Additional browser patterns (merged with built-in patterns) + 'additional_browsers' => [ + // 'CustomBrowser' => '/CustomBrowser\/([0-9.]+)/', + ], + + // Additional platform patterns (merged with built-in patterns) + 'additional_platforms' => [ + // 'CustomOS' => '/CustomOS ([0-9.]+)/', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Geolocation + |-------------------------------------------------------------------------- + | + | Configuration for IP geolocation services. + | Uses Laravel's HTTP client - no external packages required. + | + */ + 'geolocation' => [ + 'enabled' => env('VISITOR_TRACKER_GEOLOCATION', true), + 'provider' => env('VISITOR_TRACKER_GEO_PROVIDER', 'ip-api'), // ip-api, ipinfo, ipapi + 'api_key' => env('VISITOR_TRACKER_GEO_API_KEY'), + 'cache_days' => 7, + 'timeout' => 5, // HTTP request timeout in seconds + ], + + /* + |-------------------------------------------------------------------------- + | Privacy & GDPR + |-------------------------------------------------------------------------- + | + | Privacy-related settings for compliance. + | + */ + 'privacy' => [ + /* + |---------------------------------------------------------------------- + | GDPR Safe Mode + |---------------------------------------------------------------------- + | + | When enabled, the tracker will NOT collect any personal data, + | allowing you to track anonymous aggregate statistics without + | requiring user consent under GDPR. + | + | This mode disables: + | - IP address storage (not even anonymized) + | - User ID association + | - Persistent cookies (uses session-only identification) + | - Full user agent storage + | - Precise geolocation (city, region, coordinates) + | + | Only collected: + | - Page view counts (aggregate) + | - Browser name (Chrome, Firefox, etc.) + | - Platform name (Windows, macOS, etc.) + | - Device type (mobile, desktop, tablet) + | - Country (broad location only) + | - Referrer domain + | + */ + 'gdpr_safe_mode' => env('VISITOR_TRACKER_GDPR_SAFE', false), + + 'anonymize_ip' => env('VISITOR_TRACKER_ANONYMIZE_IP', false), + 'respect_dnt' => env('VISITOR_TRACKER_RESPECT_DNT', true), // Do Not Track header + ], + + /* + |-------------------------------------------------------------------------- + | Data Retention + |-------------------------------------------------------------------------- + | + | How long to keep visitor data. Set to null for indefinite retention. + | + */ + 'retention' => [ + 'days' => env('VISITOR_TRACKER_RETENTION_DAYS', 90), + ], + + /* + |-------------------------------------------------------------------------- + | Queue + |-------------------------------------------------------------------------- + | + | Enable queue for async tracking to improve performance. + | + */ + 'queue' => [ + 'enabled' => env('VISITOR_TRACKER_QUEUE', false), + 'connection' => env('VISITOR_TRACKER_QUEUE_CONNECTION', 'default'), + 'queue' => env('VISITOR_TRACKER_QUEUE_NAME', 'default'), + ], + + /* + |-------------------------------------------------------------------------- + | Cache + |-------------------------------------------------------------------------- + | + | Cache settings for statistics. + | + */ + 'cache' => [ + 'enabled' => true, + 'ttl' => 60, // Cache TTL in minutes + 'prefix' => 'visitor_tracker_', + ], + + /* + |-------------------------------------------------------------------------- + | Online Threshold + |-------------------------------------------------------------------------- + | + | Minutes of inactivity before a visitor is considered offline. + | + */ + 'online_threshold' => 5, + + /* + |-------------------------------------------------------------------------- + | API Routes + |-------------------------------------------------------------------------- + | + | Enable built-in API routes for statistics. + | + */ + 'api' => [ + 'enabled' => false, + 'prefix' => 'api/visitor-tracker', + 'middleware' => ['api', 'auth:sanctum'], + ], + + /* + |-------------------------------------------------------------------------- + | Dashboard + |-------------------------------------------------------------------------- + | + | Enable built-in dashboard routes. Run `php artisan visitor-tracker:install-dashboard` + | to enable the dashboard and publish the necessary files. + | + | IMPORTANT: Always protect your dashboard with at least one of these methods: + | - token: Secret token for sites without authentication + | - middleware: Laravel auth middleware for sites with authentication + | - gate: Laravel Gate for role-based access control + | + */ + 'dashboard' => [ + 'enabled' => env('VISITOR_TRACKER_DASHBOARD_ENABLED', true), + 'prefix' => 'admin/visitor-tracker', + + /* + |---------------------------------------------------------------------- + | Secret Token Authentication + |---------------------------------------------------------------------- + | + | For sites WITHOUT user authentication, use a secret token to protect + | the dashboard. Store this in your .env file (never commit it!). + | + | In local: set VISITOR_TRACKER_TOKEN to require it. + | In production: leave VISITOR_TRACKER_TOKEN unset — Cloudflare Access + | gates dev-tools.online/admin/* at the edge, and we set + | VISITOR_TRACKER_ALLOW_UNPROTECTED=true on the server so the + | package's boot-time guard doesn't trip. + */ + 'token' => env('VISITOR_TRACKER_TOKEN'), + + 'middleware' => ['web'], + + 'gate' => null, + + 'allow_unprotected' => env('VISITOR_TRACKER_ALLOW_UNPROTECTED', false), + ], +]; diff --git a/cron/migrate.php b/cron/migrate.php new file mode 100644 index 0000000..9706754 --- /dev/null +++ b/cron/migrate.php @@ -0,0 +1,34 @@ + + * - Frequency: hourly is plenty (migrations are infrequent and idempotent) + * + * Output (Artisan::output()) is echoed and OVH captures it in the task's + * execution log inside the OVH manager. Migrations are idempotent — running + * this on an empty migration queue is a no-op. + */ + +$root = dirname(__DIR__); +chdir($root); + +require $root.'/vendor/autoload.php'; + +/** @var \Illuminate\Foundation\Application $app */ +$app = require $root.'/bootstrap/app.php'; + +/** @var \Illuminate\Contracts\Console\Kernel $kernel */ +$kernel = $app->make(\Illuminate\Contracts\Console\Kernel::class); + +$status = $kernel->call('migrate', ['--force' => true]); + +echo $kernel->output(); + +exit($status); diff --git a/resources/views/tools/visitor-tracker.blade.php b/resources/views/tools/visitor-tracker.blade.php new file mode 100644 index 0000000..1f15265 --- /dev/null +++ b/resources/views/tools/visitor-tracker.blade.php @@ -0,0 +1,299 @@ +@extends('layouts.app') + +@section('title', 'Laravel Visitor Tracker - Server-Side Analytics Demo | Dev Tools') +@section('meta_description', 'Laravel Visitor Tracker - A server-side analytics package for Laravel with bot detection, device parsing, and GDPR compliance. Unblockable by ad blockers.') +@section('meta_keywords', 'laravel visitor tracker, laravel analytics, server-side tracking, bot detection, user agent parser, gdpr analytics, laravel package') + +@push('schema') + +@endpush + +@section('content') +
+
+
+

Laravel Visitor Tracker

+

Server-side analytics for Laravel - Unblockable by ad blockers

+
+ ← Back +
+ + + + + +
+

Live Statistics from dev-tools.online

+

Real data collected by this package running on this site:

+ +
+
+

{{ number_format($summary['total_visitors']) }}

+

Total Visitors

+
+
+

{{ number_format($summary['total_page_views']) }}

+

Page Views

+
+
+

{{ number_format($summary['online_visitors']) }}

+

Online Now

+
+
+

{{ number_format($summary['today_visitors']) }}

+

Today

+
+
+
+ +
+ +
+

Your Browser Detected

+

This is what the package detects about your current visit:

+ +
+
+ Browser + {{ $parsedUA['browser'] ?? 'Unknown' }} {{ $parsedUA['browser_version'] ?? '' }} +
+
+ Platform + {{ $parsedUA['platform'] ?? 'Unknown' }} {{ $parsedUA['platform_version'] ?? '' }} +
+
+ Device Type + {{ $parsedUA['device_type'] ?? 'Unknown' }} +
+
+ Is Bot? + + {{ $isBot ? 'Yes' . ($botName ? " ({$botName})" : '') : 'No (Human)' }} + +
+ @if($botCategory) +
+ Bot Category + {{ str_replace('_', ' ', $botCategory) }} +
+ @endif +
+ IP Address + {{ $visitorIp }} +
+
+ +
+

User Agent:

+

{{ $userAgent }}

+
+
+ + +
+
+

Browser Stats

+
+ @php $totalBrowsers = $browsers->sum('count'); @endphp + @forelse($browsers as $browser) +
+
+ {{ $browser->browser ?? 'Unknown' }} + {{ $totalBrowsers > 0 ? round($browser->count / $totalBrowsers * 100, 1) : 0 }}% +
+
+
+
+
+ @empty +

No data yet

+ @endforelse +
+
+ +
+

Device Types

+
+ @php + $totalDevices = $devices->sum('count'); + $deviceColors = ['desktop' => 'bg-purple-600', 'mobile' => 'bg-green-600', 'tablet' => 'bg-yellow-500']; + @endphp + @forelse($devices as $device) +
+
+ {{ $device->device_type ?? 'Unknown' }} + {{ number_format($device->count) }} ({{ $totalDevices > 0 ? round($device->count / $totalDevices * 100, 1) : 0 }}%) +
+
+
+
+
+ @empty +

No data yet

+ @endforelse +
+
+
+
+ + + @if($topPages->isNotEmpty()) +
+

Top Pages

+
+ + + + + + + + + + @foreach($topPages as $page) + + + + + + @endforeach + +
PathViewsUnique
/{{ $page->path }}{{ number_format($page->visits) }}{{ number_format($page->unique_visitors) }}
+
+
+ @endif + + +
+

Package Features

+
+
+
+ +
+
+

Unblockable

+

Server-side tracking can't be blocked by ad blockers

+
+
+
+
+ +
+
+

Native Detection

+

100+ bot patterns, browser/device parsing - no dependencies

+
+
+
+
+ +
+
+

GDPR Compliant

+

GDPR Safe Mode, IP anonymization, DNT support

+
+
+
+
+ +
+
+

Zero Dependencies

+

Only uses Laravel's built-in features

+
+
+
+
+ +
+
+

Geolocation

+

IP-based location via free APIs (optional)

+
+
+
+
+ +
+
+

Built-in Dashboard

+

Tailwind CSS dashboard with token auth

+
+
+
+
+ + +
+

Quick Installation

+
+
+

1. Install via Composer:

+
composer require ghdj/laravel-visitor-tracker
+
+
+

2. Publish config and run migrations:

+
php artisan vendor:publish --tag="visitor-tracker-config"
+php artisan migrate
+
+
+

3. Add middleware to your routes:

+
// In bootstrap/app.php (Laravel 11+)
+->withMiddleware(function (Middleware $middleware) {
+    $middleware->web(append: [
+        \Ghdj\VisitorTracker\Middleware\TrackVisitor::class,
+    ]);
+})
+
+
+

4. Use it:

+
use Ghdj\VisitorTracker\Facades\VisitorTracker;
+
+// Get statistics
+$stats = VisitorTracker::stats()->summary();
+$online = VisitorTracker::stats()->onlineVisitors();
+$topPages = VisitorTracker::stats()->mostVisitedPages(10);
+
+
+
+ + +
+

Laravel Visitor Tracker v1.0.0 - MIT License

+

+ Documentation + · + Packagist +

+
+
+@endsection diff --git a/routes/web.php b/routes/web.php index 63e04f2..68c5470 100644 --- a/routes/web.php +++ b/routes/web.php @@ -29,6 +29,7 @@ Route::get('/jwt', [ToolController::class, 'jwt'])->name('jwt'); Route::get('/timestamp', [ToolController::class, 'timestamp'])->name('timestamp'); Route::get('/diff', [ToolController::class, 'diff'])->name('diff'); + Route::get('/visitor-tracker', [ToolController::class, 'visitorTracker'])->name('visitor-tracker'); }); // Static Pages @@ -62,6 +63,7 @@ ['loc' => route('tools.jwt'), 'priority' => '0.8', 'changefreq' => 'monthly'], ['loc' => route('tools.timestamp'), 'priority' => '0.8', 'changefreq' => 'monthly'], ['loc' => route('tools.diff'), 'priority' => '0.8', 'changefreq' => 'monthly'], + ['loc' => route('tools.visitor-tracker'), 'priority' => '0.9', 'changefreq' => 'monthly'], ['loc' => route('about'), 'priority' => '0.5', 'changefreq' => 'monthly'], ['loc' => route('privacy'), 'priority' => '0.3', 'changefreq' => 'yearly'], ]; From 47bc7712b6653e523ef328c4314a841adb8dc114 Mon Sep 17 00:00:00 2001 From: jalel Date: Wed, 29 Apr 2026 00:47:16 +0100 Subject: [PATCH 2/2] ci: disable visitor tracker dashboard in tests workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The package's service provider throws a RuntimeException during package:discover when the dashboard is enabled but no auth method is configured. CI has no env vars set, so composer install fails before tests can run. Disable the dashboard for the tests job — the suite doesn't exercise it. --- .github/workflows/tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 08480d4..a1418b0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,6 +16,9 @@ jobs: name: PHP ${{ matrix.php }} + env: + VISITOR_TRACKER_DASHBOARD_ENABLED: false + steps: - name: Checkout code uses: actions/checkout@v4