Files
filament-short-url/src/Services/ShortUrlTracker.php

143 lines
5.5 KiB
PHP

<?php
/**
* @author Bartek Janczak <barek122@gmail.com>
* @copyright 2026 Bartek Janczak
* @license Custom Source-Available License (see LICENSE file)
*/
namespace Bjanczak\FilamentShortUrl\Services;
use Bjanczak\FilamentShortUrl\Models\ShortUrl;
use Bjanczak\FilamentShortUrl\Models\ShortUrlVisit;
use Illuminate\Http\Request;
/**
* Records a visit to a ShortUrl, collecting all enabled tracking fields.
*
* @internal
*/
class ShortUrlTracker
{
public function __construct(
private readonly UserAgentParser $uaParser,
private readonly GeoIpService $geoIp,
private readonly ProxyDetectionService $proxyDetector,
) {}
/**
* Record a visit and return the created ShortUrlVisit model.
*
* Returns null if the visit was from a bot/crawler (we don't track those).
*/
public function record(
ShortUrl $shortUrl,
Request $request,
?string $preResolvedCountryCode = null,
?string $preResolvedCity = null,
?string $utmSource = null,
?string $utmMedium = null,
?string $utmCampaign = null,
?string $utmTerm = null,
?string $utmContent = null,
bool $isQrScan = false,
?string $browserLanguage = null,
): ?ShortUrlVisit {
$ip = ClientIpExtractor::getIp($request);
$ipHash = hash('sha256', $ip);
$ua = $request->userAgent() ?? '';
$parsed = $this->uaParser->parse($ua);
// Run bot & proxy/VPN detection
$isBot = $parsed['device_type'] === 'robot';
$isProxy = false;
if (! $isBot) {
$detection = $this->proxyDetector->detect($ip);
$isBot = (bool) $detection['is_bot'];
$isProxy = (bool) $detection['is_proxy'];
}
$geo = config('filament-short-url.geo_ip.enabled', true)
? $this->geoIp->resolve($ip, $preResolvedCountryCode, $preResolvedCity)
: ['country' => null, 'country_code' => null, 'city' => null];
// Determine uniqueness: first time this IP hash visits this URL.
// We check BEFORE insert to avoid a self-referential race.
$isUnique = false;
if (config('filament-short-url.counter_buffering.enabled', false)) {
$uniqueCacheKey = "filament-short-url:unique-visit:{$shortUrl->id}:{$ipHash}";
if (cache()->add($uniqueCacheKey, true, 86400)) {
$isUnique = ! ShortUrlVisit::where('short_url_id', $shortUrl->id)
->where('ip_hash', $ipHash)
->exists();
}
} else {
$isUnique = ! ShortUrlVisit::where('short_url_id', $shortUrl->id)
->where('ip_hash', $ipHash)
->exists();
}
$visit = new ShortUrlVisit;
$visit->short_url_id = $shortUrl->id;
$visit->visited_at = now();
$visit->ip_hash = $ipHash;
$visit->ip_address = $shortUrl->track_ip_address ? $ip : null;
$visit->browser = $shortUrl->track_browser ? $parsed['browser'] : null;
$visit->browser_version = $shortUrl->track_browser_version ? $parsed['browser_version'] : null;
$visit->operating_system = $shortUrl->track_operating_system ? $parsed['operating_system'] : null;
$visit->operating_system_version = $shortUrl->track_operating_system_version
? $parsed['operating_system_version'] : null;
$visit->device_type = $shortUrl->track_device_type ? $parsed['device_type'] : null;
$visit->country = $geo['country'];
$visit->country_code = $geo['country_code'];
$visit->city = $geo['city'] ?? null;
$visit->is_bot = $isBot;
$visit->is_proxy = $isProxy;
$visit->is_qr_scan = $isQrScan;
$visit->browser_language = $shortUrl->track_browser_language ? $browserLanguage : null;
$visit->utm_source = $utmSource;
$visit->utm_medium = $utmMedium;
$visit->utm_campaign = $utmCampaign;
$visit->utm_term = $utmTerm;
$visit->utm_content = $utmContent;
// Clean & normalize referer URL to host domain
if ($shortUrl->track_referer_url && $referer = $request->header('Referer')) {
$visit->referer_url = $referer;
$refererHost = parse_url($referer, PHP_URL_HOST);
if ($refererHost) {
$refererHost = preg_replace('/^www\./', '', strtolower($refererHost));
if (str_contains($refererHost, 'facebook.com')) {
$refererHost = 'facebook.com';
} elseif (str_contains($refererHost, 'linkedin.com')) {
$refererHost = 'linkedin.com';
} elseif (str_contains($refererHost, 'twitter.com') || str_contains($refererHost, 't.co')) {
$refererHost = 'twitter.com';
} elseif (str_contains($refererHost, 'google.')) {
$refererHost = 'google.com';
} elseif (str_contains($refererHost, 'instagram.com')) {
$refererHost = 'instagram.com';
} elseif (str_contains($refererHost, 'youtube.com')) {
$refererHost = 'youtube.com';
}
$visit->referer_host = $refererHost;
} else {
$visit->referer_host = 'Direct';
}
} else {
$visit->referer_host = 'Direct';
}
$visit->save();
// Increment stats only if it's NOT a bot or proxy/VPN to keep analytics clean
if (! $isBot && ! $isProxy) {
$shortUrl->incrementVisits($isUnique, $isQrScan);
}
return $visit;
}
}