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/.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 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 +
+ + +
+ + + GitHub + + + + Packagist + +
+ + +
+

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'], ];