Init
This commit is contained in:
13 files changed
+10287
No files matched your search
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Mattitja\Analytics;
|
||||
|
||||
use Illuminate\Routing\Router;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Mattitja\Analytics\Middleware\AnalyticsMiddleware;
|
||||
use Mattitja\Analytics\Services\AnalyticsClient;
|
||||
|
||||
class AnalyticsServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services in the container.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->mergeConfigFrom(
|
||||
__DIR__.'/../config/mattitja-analytics.php',
|
||||
'mattitja-analytics'
|
||||
);
|
||||
|
||||
$this->app->singleton(AnalyticsClient::class, function ($app) {
|
||||
return new AnalyticsClient;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
$this->publishes([
|
||||
__DIR__.'/../config/mattitja-analytics.php' => config_path('mattitja-analytics.php'),
|
||||
], 'mattitja-analytics-config');
|
||||
|
||||
$this->registerMiddleware();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the analytics middleware.
|
||||
*/
|
||||
private function registerMiddleware(): void
|
||||
{
|
||||
$router = $this->app->make(Router::class);
|
||||
|
||||
$router->aliasMiddleware('mattitja-analytics', AnalyticsMiddleware::class);
|
||||
$router->pushMiddlewareToGroup('web', AnalyticsMiddleware::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace Mattitja\Analytics\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Mattitja\Analytics\Services\AnalyticsClient;
|
||||
|
||||
class AnalyticsMiddleware
|
||||
{
|
||||
/**
|
||||
* Common bot user agent patterns for detection.
|
||||
*/
|
||||
private const array BOT_PATTERNS = [
|
||||
'bot',
|
||||
'crawler',
|
||||
'spider',
|
||||
'scraper',
|
||||
'curl',
|
||||
'wget',
|
||||
'googlebot',
|
||||
'bingbot',
|
||||
'facebookexternalhit',
|
||||
'twitterbot',
|
||||
];
|
||||
|
||||
/**
|
||||
* Handle an incoming request and capture analytics data.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
if ($this->shouldCaptureAnalytics($request)) {
|
||||
$data = $this->buildAnalyticsData($request);
|
||||
$this->sendAnalytics($data);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if analytics should be captured for this request.
|
||||
*/
|
||||
protected function shouldCaptureAnalytics(Request $request): bool
|
||||
{
|
||||
return $this->isAnalyticsEnabled()
|
||||
&& $this->hasSiteKey()
|
||||
&& ! $this->isIgnoredPath($request)
|
||||
&& ! $this->isIgnoredIp($request)
|
||||
&& ! $this->shouldIgnoreBot($request)
|
||||
&& ! $this->respectsDoNotTrack($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if analytics tracking is enabled.
|
||||
*/
|
||||
private function isAnalyticsEnabled(): bool
|
||||
{
|
||||
return config('mattitja-analytics.enabled', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if site key is configured.
|
||||
*/
|
||||
private function hasSiteKey(): bool
|
||||
{
|
||||
return ! empty(config('mattitja-analytics.site_key'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if bot traffic should be ignored.
|
||||
*/
|
||||
private function shouldIgnoreBot(Request $request): bool
|
||||
{
|
||||
return $this->isBot($request) && ! config('mattitja-analytics.capture_bots', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the request path should be ignored.
|
||||
*/
|
||||
protected function isIgnoredPath(Request $request): bool
|
||||
{
|
||||
$ignoredPaths = config('mattitja-analytics.ignore_paths', []);
|
||||
$path = $request->path();
|
||||
|
||||
foreach ($ignoredPaths as $pattern) {
|
||||
if (fnmatch($pattern, $path)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the request IP should be ignored.
|
||||
*/
|
||||
protected function isIgnoredIp(Request $request): bool
|
||||
{
|
||||
$ignoredIps = config('mattitja-analytics.ignore_ips', []);
|
||||
|
||||
return in_array($request->ip(), $ignoredIps, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if the request is from a bot.
|
||||
*/
|
||||
protected function isBot(Request $request): bool
|
||||
{
|
||||
$userAgent = $request->userAgent() ?? '';
|
||||
|
||||
foreach (self::BOT_PATTERNS as $pattern) {
|
||||
if (stripos($userAgent, $pattern) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the request respects Do Not Track header.
|
||||
*/
|
||||
protected function respectsDoNotTrack(Request $request): bool
|
||||
{
|
||||
if (! config('mattitja-analytics.respect_dnt', true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $request->header('DNT') === '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build analytics data payload from request.
|
||||
*/
|
||||
protected function buildAnalyticsData(Request $request): array
|
||||
{
|
||||
$data = [
|
||||
'k' => config('mattitja-analytics.site_key'),
|
||||
'u' => $request->fullUrl(),
|
||||
'ip' => $request->ip(),
|
||||
'ua' => $request->userAgent() ?? '',
|
||||
];
|
||||
|
||||
$referrer = $request->header('referer');
|
||||
if (! empty($referrer)) {
|
||||
$data['r'] = $referrer;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send analytics data to the API.
|
||||
*/
|
||||
protected function sendAnalytics(array $data): void
|
||||
{
|
||||
try {
|
||||
app(AnalyticsClient::class)->send($data);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to send analytics data', [
|
||||
'error' => $e->getMessage(),
|
||||
'data' => $data,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Mattitja\Analytics\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class AnalyticsClient
|
||||
{
|
||||
/**
|
||||
* The Mattitja Analytics API endpoint.
|
||||
*/
|
||||
private const string ENDPOINT = 'https://analytics.mattitja.cloud/api/send';
|
||||
|
||||
/**
|
||||
* HTTP request timeout in seconds.
|
||||
*/
|
||||
private const int TIMEOUT = 5;
|
||||
|
||||
/**
|
||||
* Required fields for analytics data.
|
||||
*/
|
||||
private const array REQUIRED_FIELDS = ['k', 'u', 'ip', 'ua'];
|
||||
|
||||
/**
|
||||
* Send analytics data to the Mattitja Analytics API.
|
||||
*
|
||||
* @param array $data Analytics data payload
|
||||
*
|
||||
* @throws InvalidArgumentException If required fields are missing
|
||||
*/
|
||||
public function send(array $data): void
|
||||
{
|
||||
$this->validateData($data);
|
||||
|
||||
$cleanData = $this->cleanData($data);
|
||||
|
||||
try {
|
||||
Http::timeout(self::TIMEOUT)
|
||||
->post(self::ENDPOINT, $cleanData);
|
||||
} catch (\Exception) {
|
||||
// Fail silently to not break user experience
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that all required fields are present and not empty.
|
||||
*
|
||||
* @param array $data Analytics data to validate
|
||||
*
|
||||
* @throws InvalidArgumentException If required fields are missing
|
||||
*/
|
||||
protected function validateData(array $data): void
|
||||
{
|
||||
foreach (self::REQUIRED_FIELDS as $field) {
|
||||
if (! isset($data[$field])) {
|
||||
throw new InvalidArgumentException("Required field '{$field}' is missing");
|
||||
}
|
||||
|
||||
// User agent can be empty string, but other fields cannot
|
||||
if ($field !== 'ua' && empty($data[$field])) {
|
||||
throw new InvalidArgumentException("Required field '{$field}' cannot be empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean analytics data by removing empty optional fields.
|
||||
*
|
||||
* @param array $data Raw analytics data
|
||||
* @return array Cleaned analytics data
|
||||
*/
|
||||
protected function cleanData(array $data): array
|
||||
{
|
||||
// Remove empty referrer URLs
|
||||
if (isset($data['r']) && empty($data['r'])) {
|
||||
unset($data['r']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user