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

112 lines
4.3 KiB
PHP

<?php
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,
) {}
/**
* 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,
): ?ShortUrlVisit {
$ip = ClientIpExtractor::getIp($request);
$ipHash = hash('sha256', $ip);
$ua = $request->userAgent() ?? '';
$parsed = $this->uaParser->parse($ua);
// Don't track bots — they inflate stats and waste API calls
if ($parsed['device_type'] === 'robot') {
return null;
}
$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 = ! 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->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();
$shortUrl->incrementVisits($isUnique);
return $visit;
}
}