Init
This commit is contained in:
13 files changed
+10287
No files matched your search
@@ -0,0 +1,4 @@
|
|||||||
|
.idea
|
||||||
|
.phpunit.cache
|
||||||
|
build
|
||||||
|
vendor
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Mattitja Analytics for Laravel
|
||||||
|
|
||||||
|
A Laravel middleware package for sending analytics data to the Mattitja Analytics API. The package automatically registers middleware for web routes and sends data synchronously to the analytics service.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer require mattitjaab/mattitja-analytics
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Publish the configuration file (optional):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan vendor:publish --tag=mattitja-analytics-config
|
||||||
|
```
|
||||||
|
|
||||||
|
Add your site key to your `.env` file:
|
||||||
|
|
||||||
|
```env
|
||||||
|
MATTITJA_ANALYTICS_SITE_KEY=your-site-key
|
||||||
|
MATTITJA_ANALYTICS_ENABLED=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The package automatically registers middleware for the `web` group and starts collecting analytics data immediately after installation and configuration.
|
||||||
|
|
||||||
|
### Manual middleware registration
|
||||||
|
|
||||||
|
You can also add the middleware manually for specific routes:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// In routes/web.php
|
||||||
|
Route::middleware(['mattitja-analytics'])->group(function () {
|
||||||
|
// Your routes here
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
### Basic Settings
|
||||||
|
- `MATTITJA_ANALYTICS_SITE_KEY` - Your site key from Mattitja Analytics (required)
|
||||||
|
- `MATTITJA_ANALYTICS_ENABLED` - Enable/disable tracking (default: true)
|
||||||
|
- `MATTITJA_ANALYTICS_BOTS` - Track bots (default: false)
|
||||||
|
|
||||||
|
### Privacy Settings
|
||||||
|
- `MATTITJA_ANALYTICS_RESPECT_DNT` - Respect Do Not Track header (default: true)
|
||||||
|
|
||||||
|
### Filtering
|
||||||
|
You can configure paths and IP addresses to ignore in the config file.
|
||||||
|
|
||||||
|
## Data Sent
|
||||||
|
|
||||||
|
The package sends the following data to `https://analytics.mattitja.cloud/api/send`:
|
||||||
|
|
||||||
|
- `k` - Site key
|
||||||
|
- `u` - Full URL of the visited page
|
||||||
|
- `r` - Referrer URL (if available)
|
||||||
|
- `ip` - Visitor's complete IP address
|
||||||
|
- `ua` - User agent string
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- Data is sent synchronously directly from middleware
|
||||||
|
- IP addresses are NOT anonymized (needed for geolocation)
|
||||||
|
- Bot traffic is filtered out by default
|
||||||
|
- Do Not Track header is respected by default
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Run tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer test
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"name": "mattitjaab/mattitja-analytics",
|
||||||
|
"description": "Laravel middleware package for sending analytics data to Mattitja Analytics API",
|
||||||
|
"type": "library",
|
||||||
|
"license": "MIT",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Mattitja\\Analytics\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Mattitja\\Analytics\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^8.3",
|
||||||
|
"illuminate/support": "^10.0|^11.0|^12.0",
|
||||||
|
"illuminate/http": "^10.0|^11.0|^12.0",
|
||||||
|
"guzzlehttp/guzzle": "^7.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"laravel/pint": "^1.25",
|
||||||
|
"orchestra/testbench": "^8.0|^9.0",
|
||||||
|
"pestphp/pest": "^2.0",
|
||||||
|
"pestphp/pest-plugin-laravel": "^2.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "vendor/bin/pest"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Mattitja\\Analytics\\AnalyticsServiceProvider"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"prefer-stable": true,
|
||||||
|
"config": {
|
||||||
|
"sort-packages": true,
|
||||||
|
"allow-plugins": {
|
||||||
|
"pestphp/pest-plugin": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+9503
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Analytics Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configuration for sending analytics data to Mattitja Analytics API
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'site_key' => env('MATTITJA_ANALYTICS_SITE_KEY'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Capture Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure when analytics tracking should be enabled
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'enabled' => env('MATTITJA_ANALYTICS_ENABLED', true),
|
||||||
|
'capture_bots' => env('MATTITJA_ANALYTICS_BOTS', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Filtering Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure which requests should be ignored
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'ignore_paths' => [
|
||||||
|
'horizon',
|
||||||
|
'horizon/*',
|
||||||
|
'telescope',
|
||||||
|
'telescope/*',
|
||||||
|
'_debugbar/*',
|
||||||
|
'livewire/*',
|
||||||
|
],
|
||||||
|
|
||||||
|
'ignore_ips' => [
|
||||||
|
// Add IP addresses to ignore (e.g., your own IP for development)
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Privacy Configuration
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Configure privacy settings for analytics data
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'respect_dnt' => env('MATTITJA_ANALYTICS_RESPECT_DNT', true),
|
||||||
|
];
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="https://schema.phpunit.org/10.5/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
processIsolation="false"
|
||||||
|
stopOnFailure="false"
|
||||||
|
executionOrder="random"
|
||||||
|
failOnWarning="true"
|
||||||
|
failOnRisky="true"
|
||||||
|
failOnEmptyTestSuite="true"
|
||||||
|
beStrictAboutOutputDuringTests="true"
|
||||||
|
cacheDirectory=".phpunit.cache"
|
||||||
|
backupStaticProperties="false">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="Test Suite">
|
||||||
|
<directory suffix="Test.php">./tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
<coverage>
|
||||||
|
<report>
|
||||||
|
<html outputDirectory="build/coverage"/>
|
||||||
|
<text outputFile="build/coverage.txt"/>
|
||||||
|
<clover outputFile="build/logs/clover.xml"/>
|
||||||
|
</report>
|
||||||
|
</coverage>
|
||||||
|
<logging>
|
||||||
|
<junit outputFile="build/report.junit.xml"/>
|
||||||
|
</logging>
|
||||||
|
<source>
|
||||||
|
<include>
|
||||||
|
<directory suffix=".php">./src</directory>
|
||||||
|
</include>
|
||||||
|
</source>
|
||||||
|
</phpunit>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Mattitja\Analytics\Middleware\AnalyticsMiddleware;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Http::fake();
|
||||||
|
|
||||||
|
$this->app->singleton(\Mattitja\Analytics\Services\AnalyticsClient::class, function ($app) {
|
||||||
|
return new \Mattitja\Analytics\Services\AnalyticsClient;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createTestRequest(string $url = 'https://example.com/test', string $method = 'GET'): Request
|
||||||
|
{
|
||||||
|
return Request::create($url, $method);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMiddleware(): AnalyticsMiddleware
|
||||||
|
{
|
||||||
|
return new AnalyticsMiddleware;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRequest(AnalyticsMiddleware $middleware, Request $request): Response
|
||||||
|
{
|
||||||
|
return $middleware->handle($request, function ($req) {
|
||||||
|
return new Response('Test content');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('captures analytics data for web requests', function () {
|
||||||
|
$middleware = createMiddleware();
|
||||||
|
$request = createTestRequest();
|
||||||
|
$request->headers->set('User-Agent', 'Mozilla/5.0 Chrome');
|
||||||
|
$request->headers->set('Referer', 'https://google.com');
|
||||||
|
$request->server->set('REMOTE_ADDR', '192.168.1.1');
|
||||||
|
|
||||||
|
$response = handleRequest($middleware, $request);
|
||||||
|
|
||||||
|
expect($response)->toBeInstanceOf(Response::class);
|
||||||
|
|
||||||
|
Http::assertSent(function ($request) {
|
||||||
|
$data = $request->data();
|
||||||
|
|
||||||
|
return $request->url() === 'https://analytics.mattitja.cloud/api/send' &&
|
||||||
|
$data['k'] === 'test-site-key' &&
|
||||||
|
$data['u'] === 'https://example.com/test' &&
|
||||||
|
$data['r'] === 'https://google.com' &&
|
||||||
|
$data['ip'] === '192.168.1.1' &&
|
||||||
|
$data['ua'] === 'Mozilla/5.0 Chrome';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not capture analytics when disabled', function () {
|
||||||
|
config(['mattitja-analytics.enabled' => false]);
|
||||||
|
|
||||||
|
$middleware = new AnalyticsMiddleware;
|
||||||
|
$request = Request::create('https://example.com/test', 'GET');
|
||||||
|
|
||||||
|
$response = $middleware->handle($request, function ($req) {
|
||||||
|
return new Response('Test content');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect($response)->toBeInstanceOf(Response::class);
|
||||||
|
Http::assertNothingSent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores configured paths', function () {
|
||||||
|
config(['mattitja-analytics.ignore_paths' => ['admin/*']]);
|
||||||
|
|
||||||
|
$middleware = new AnalyticsMiddleware;
|
||||||
|
$request = Request::create('https://example.com/admin/dashboard', 'GET');
|
||||||
|
|
||||||
|
$response = $middleware->handle($request, function ($req) {
|
||||||
|
return new Response('Test content');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect($response)->toBeInstanceOf(Response::class);
|
||||||
|
Http::assertNothingSent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores bot user agents when bots capture is disabled', function () {
|
||||||
|
config(['mattitja-analytics.capture_bots' => false]);
|
||||||
|
|
||||||
|
$middleware = new AnalyticsMiddleware;
|
||||||
|
$request = Request::create('https://example.com/test', 'GET');
|
||||||
|
$request->headers->set('User-Agent', 'Googlebot/2.1');
|
||||||
|
|
||||||
|
$response = $middleware->handle($request, function ($req) {
|
||||||
|
return new Response('Test content');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect($response)->toBeInstanceOf(Response::class);
|
||||||
|
Http::assertNothingSent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('respects Do Not Track header when configured', function () {
|
||||||
|
config(['mattitja-analytics.respect_dnt' => true]);
|
||||||
|
|
||||||
|
$middleware = createMiddleware();
|
||||||
|
$request = createTestRequest();
|
||||||
|
$request->headers->set('DNT', '1');
|
||||||
|
|
||||||
|
$response = handleRequest($middleware, $request);
|
||||||
|
|
||||||
|
expect($response)->toBeInstanceOf(Response::class);
|
||||||
|
Http::assertNothingSent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('captures user agent in analytics data', function () {
|
||||||
|
$middleware = createMiddleware();
|
||||||
|
$request = createTestRequest();
|
||||||
|
$request->headers->set('User-Agent', 'CustomClient/1.0 (Test)');
|
||||||
|
|
||||||
|
$response = handleRequest($middleware, $request);
|
||||||
|
|
||||||
|
Http::assertSent(function ($request) {
|
||||||
|
$data = $request->data();
|
||||||
|
|
||||||
|
return $data['ua'] === 'CustomClient/1.0 (Test)';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty user agent gracefully', function () {
|
||||||
|
$middleware = createMiddleware();
|
||||||
|
$request = createTestRequest();
|
||||||
|
$request->headers->set('User-Agent', '');
|
||||||
|
|
||||||
|
$response = handleRequest($middleware, $request);
|
||||||
|
|
||||||
|
Http::assertSent(function ($request) {
|
||||||
|
$data = $request->data();
|
||||||
|
|
||||||
|
return array_key_exists('ua', $data) && $data['ua'] === '';
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Mattitja\Analytics\Tests\TestCase;
|
||||||
|
|
||||||
|
uses(TestCase::class)->in(__DIR__);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Mattitja\Analytics\Tests;
|
||||||
|
|
||||||
|
use Mattitja\Analytics\AnalyticsServiceProvider;
|
||||||
|
use Orchestra\Testbench\TestCase as Orchestra;
|
||||||
|
|
||||||
|
class TestCase extends Orchestra
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getPackageProviders($app)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
AnalyticsServiceProvider::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEnvironmentSetUp($app)
|
||||||
|
{
|
||||||
|
config()->set('mattitja-analytics.site_key', 'test-site-key');
|
||||||
|
config()->set('mattitja-analytics.enabled', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Mattitja\Analytics\Services\AnalyticsClient;
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
Http::fake();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends analytics data to the correct endpoint', function () {
|
||||||
|
$client = new AnalyticsClient;
|
||||||
|
$data = [
|
||||||
|
'k' => 'test-site-key',
|
||||||
|
'u' => 'https://example.com/test',
|
||||||
|
'r' => 'https://google.com',
|
||||||
|
'ip' => '192.168.1.1',
|
||||||
|
'ua' => 'Mozilla/5.0 Chrome',
|
||||||
|
];
|
||||||
|
|
||||||
|
$client->send($data);
|
||||||
|
|
||||||
|
Http::assertSent(function ($request) use ($data) {
|
||||||
|
return $request->url() === 'https://analytics.mattitja.cloud/api/send' &&
|
||||||
|
$request->method() === 'POST' &&
|
||||||
|
$request->data() === $data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles API failures gracefully', function () {
|
||||||
|
Http::fake([
|
||||||
|
'analytics.mattitja.cloud/*' => Http::response('Server Error', 500),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$client = new AnalyticsClient;
|
||||||
|
$data = [
|
||||||
|
'k' => 'test-site-key',
|
||||||
|
'u' => 'https://example.com/test',
|
||||||
|
'r' => 'https://google.com',
|
||||||
|
'ip' => '192.168.1.1',
|
||||||
|
'ua' => 'Mozilla/5.0 Chrome',
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(fn () => $client->send($data))->not->toThrow(Exception::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses hardcoded timeout', function () {
|
||||||
|
$client = new AnalyticsClient;
|
||||||
|
$data = [
|
||||||
|
'k' => 'test-site-key',
|
||||||
|
'u' => 'https://example.com/test',
|
||||||
|
'r' => '',
|
||||||
|
'ip' => '192.168.1.1',
|
||||||
|
'ua' => 'Mozilla/5.0 Chrome',
|
||||||
|
];
|
||||||
|
|
||||||
|
$client->send($data);
|
||||||
|
|
||||||
|
Http::assertSent(function ($request) {
|
||||||
|
return $request->data()['k'] === 'test-site-key';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates required data fields', function () {
|
||||||
|
$client = new AnalyticsClient;
|
||||||
|
|
||||||
|
expect(fn () => $client->send([]))->toThrow(InvalidArgumentException::class)
|
||||||
|
->and(fn () => $client->send(['k' => 'test']))->toThrow(InvalidArgumentException::class)
|
||||||
|
->and(fn () => $client->send(['k' => 'test', 'u' => 'url', 'ip' => '1.1.1.1']))->toThrow(InvalidArgumentException::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters out empty referrer URLs', function () {
|
||||||
|
$client = new AnalyticsClient;
|
||||||
|
$data = [
|
||||||
|
'k' => 'test-site-key',
|
||||||
|
'u' => 'https://example.com/test',
|
||||||
|
'r' => '',
|
||||||
|
'ip' => '192.168.1.1',
|
||||||
|
'ua' => 'Mozilla/5.0 Chrome',
|
||||||
|
];
|
||||||
|
|
||||||
|
$client->send($data);
|
||||||
|
|
||||||
|
Http::assertSent(function ($request) {
|
||||||
|
$sentData = $request->data();
|
||||||
|
|
||||||
|
return ! isset($sentData['r']);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user