diff --git a/src/Console/Commands/AggregateAndPruneVisitsCommand.php b/src/Console/Commands/AggregateAndPruneVisitsCommand.php index c68bfe3..750cfeb 100644 --- a/src/Console/Commands/AggregateAndPruneVisitsCommand.php +++ b/src/Console/Commands/AggregateAndPruneVisitsCommand.php @@ -47,8 +47,26 @@ class AggregateAndPruneVisitsCommand extends Command $statsByUrl = []; $nextDate = Carbon::parse($date)->addDay()->toDateString(); - ShortUrlVisit::where('visited_at', '>=', $date.' 00:00:00') + DB::table('short_url_visits') + ->where('visited_at', '>=', $date.' 00:00:00') ->where('visited_at', '<', $nextDate.' 00:00:00') + ->select([ + 'short_url_id', + 'is_qr_scan', + 'ip_hash', + 'device_type', + 'browser', + 'operating_system', + 'country', + 'country_code', + 'utm_source', + 'utm_medium', + 'utm_campaign', + 'browser_language', + 'city', + 'referer_host', + ]) + ->orderBy('id') ->chunk(1000, function ($chunk) use (&$statsByUrl): void { foreach ($chunk as $visit) { $urlId = $visit->short_url_id; diff --git a/src/Console/Commands/SyncBufferedCountersCommand.php b/src/Console/Commands/SyncBufferedCountersCommand.php index 04bf777..91ba3f1 100644 --- a/src/Console/Commands/SyncBufferedCountersCommand.php +++ b/src/Console/Commands/SyncBufferedCountersCommand.php @@ -25,9 +25,13 @@ class SyncBufferedCountersCommand extends Command if (Cache::getDefaultDriver() === 'redis' && class_exists(Redis::class)) { $tempKey = "{$dirtyKey}:temp:".time(); try { - Redis::rename($dirtyKey, $tempKey); - $dirtyIds = Redis::smembers($tempKey); - Redis::del($tempKey); + if (Redis::exists($dirtyKey)) { + Redis::rename($dirtyKey, $tempKey); + $dirtyIds = Redis::smembers($tempKey); + Redis::del($tempKey); + } else { + $dirtyIds = []; + } } catch (\Throwable) { // If key does not exist or rename fails, fallback $dirtyIds = []; diff --git a/src/FilamentShortUrlServiceProvider.php b/src/FilamentShortUrlServiceProvider.php index c24c511..4636f1f 100644 --- a/src/FilamentShortUrlServiceProvider.php +++ b/src/FilamentShortUrlServiceProvider.php @@ -71,9 +71,7 @@ class FilamentShortUrlServiceProvider extends PackageServiceProvider $this->app->booted(function (): void { $schedule = $this->app->make(Schedule::class); - if (config('filament-short-url.pruning.enabled', true)) { - $schedule->command('short-url:aggregate-and-prune')->dailyAt('02:00'); - } + $schedule->command('short-url:aggregate-and-prune')->dailyAt('02:00'); if (config('filament-short-url.counter_buffering.enabled', false)) { $schedule->command('short-url:sync-counters')->everyMinute(); diff --git a/src/Http/Controllers/ShortUrlRedirectController.php b/src/Http/Controllers/ShortUrlRedirectController.php index 84e5fea..620467d 100644 --- a/src/Http/Controllers/ShortUrlRedirectController.php +++ b/src/Http/Controllers/ShortUrlRedirectController.php @@ -89,20 +89,8 @@ class ShortUrlRedirectController extends Controller } } - // 4. Resolve Destination URL (evaluating targeting rules) - $destination = $shortUrl->resolveDestinationUrl($request); - - // Forward query parameters if configured - if ($shortUrl->forward_query_params) { - $queryParams = $request->query(); - // Remove routing/auth parameters so they don't leak to destination - unset($queryParams['confirmed'], $queryParams['password']); - - if (! empty($queryParams)) { - $separator = str_contains($destination, '?') ? '&' : '?'; - $destination .= $separator.http_build_query($queryParams); - } - } + // 4. Resolve Destination URL (evaluating targeting rules and forwarding query parameters) + $destination = $this->service->resolveRedirectUrl($shortUrl, $request); // 5. Warning / Intermediate Page Check if ($shortUrl->show_warning_page && ! $request->has('confirmed')) { diff --git a/src/Models/ShortUrl.php b/src/Models/ShortUrl.php index 91da7b2..4c92207 100644 --- a/src/Models/ShortUrl.php +++ b/src/Models/ShortUrl.php @@ -276,7 +276,21 @@ class ShortUrl extends Model public function getRealTimeTotalVisits(): int { - return $this->total_visits; + $buffered = 0; + if (config('filament-short-url.counter_buffering.enabled', false)) { + $prefix = config('filament-short-url.counter_buffering.cache_key_prefix', 'filament-short-url:buffer:'); + $buffered = (int) cache()->get("{$prefix}total:{$this->id}", 0); + } + + if ($this->max_visits !== null) { + $dbVal = (int) DB::table($this->table) + ->where('id', $this->id) + ->value('total_visits'); + + return $dbVal + $buffered; + } + + return $this->total_visits + $buffered; } public function isActive(): bool @@ -285,6 +299,13 @@ class ShortUrl extends Model return false; } + if ($this->single_use) { + $realEnabled = DB::table($this->table)->where('id', $this->id)->value('is_enabled'); + if (! $realEnabled) { + return false; + } + } + // Not yet active if ($this->activated_at && $this->activated_at->isFuture()) { return false; diff --git a/src/Services/GeoIpService.php b/src/Services/GeoIpService.php index d5c4ad9..d167ae7 100644 --- a/src/Services/GeoIpService.php +++ b/src/Services/GeoIpService.php @@ -91,7 +91,7 @@ class GeoIpService if (class_exists(\Locale::class)) { try { - $name = \Locale::getDisplayRegion('-'.$code, 'en'); + $name = \Locale::getDisplayRegion('en-'.$code, 'en'); if ($name && $name !== $code) { return $name; } diff --git a/src/Services/UserAgentParser.php b/src/Services/UserAgentParser.php index d046d03..fa44f2d 100644 --- a/src/Services/UserAgentParser.php +++ b/src/Services/UserAgentParser.php @@ -37,6 +37,8 @@ class UserAgentParser // Order matters — check specific first, generic last $patterns = [ 'Edg/' => 'Edge', + 'EdgiOS' => 'Edge', + 'EdgA' => 'Edge', 'OPR/' => 'Opera', 'Opera' => 'Opera', 'SamsungBrowser' => 'Samsung Browser', @@ -67,6 +69,8 @@ class UserAgentParser { $patterns = [ '/Edg\/([0-9.]+)/', + '/EdgiOS\/([0-9.]+)/', + '/EdgA\/([0-9.]+)/', '/OPR\/([0-9.]+)/', '/SamsungBrowser\/([0-9.]+)/', '/CriOS\/([0-9.]+)/',