- Use the correct endpoint (analytics.mattitja.se instead of the non-existent .cloud domain) - Authenticate with the site ingest secret as a Bearer token so the API trusts the forwarded visitor IP and user agent - Forward the visitor Accept-Language header (required by bot detection) - Only track successful GET requests rendering HTML - Short timeouts and a file-based circuit breaker that pauses sending after connection failures or 5xx responses, so no queue, worker or cache is needed and a down API never slows the host site - Log rejected requests instead of failing silently
240 lines
6.6 KiB
PHP
240 lines
6.6 KiB
PHP
<?php
|
|
|
|
use Illuminate\Support\Facades\Http;
|
|
use Mattitja\Analytics\Data\PageviewData;
|
|
use Mattitja\Analytics\Services\AnalyticsClient;
|
|
|
|
beforeEach(function () {
|
|
Http::fake();
|
|
@unlink(storage_path('framework/mattitja-analytics.paused'));
|
|
});
|
|
|
|
afterEach(function () {
|
|
@unlink(storage_path('framework/mattitja-analytics.paused'));
|
|
});
|
|
|
|
function testPageview(): PageviewData
|
|
{
|
|
return new PageviewData(
|
|
siteKey: 'testsiteky',
|
|
url: 'https://example.com/test',
|
|
ip: '192.168.1.1',
|
|
userAgent: 'Mozilla/5.0 Chrome',
|
|
);
|
|
}
|
|
|
|
it('sends analytics data to the correct endpoint', function () {
|
|
$client = new AnalyticsClient;
|
|
$data = new PageviewData(
|
|
siteKey: 'testsiteky',
|
|
url: 'https://example.com/test',
|
|
ip: '192.168.1.1',
|
|
userAgent: 'Mozilla/5.0 Chrome',
|
|
referrer: 'https://google.com',
|
|
);
|
|
|
|
$client->send($data);
|
|
|
|
Http::assertSent(function ($request) {
|
|
return $request->url() === 'https://analytics.mattitja.se/api/send' &&
|
|
$request->method() === 'POST' &&
|
|
$request->data() === [
|
|
'k' => 'testsiteky',
|
|
'u' => 'https://example.com/test',
|
|
'ip' => '192.168.1.1',
|
|
'ua' => 'Mozilla/5.0 Chrome',
|
|
'r' => 'https://google.com',
|
|
];
|
|
});
|
|
});
|
|
|
|
it('handles API failures gracefully', function () {
|
|
Http::fake([
|
|
'analytics.mattitja.se/*' => Http::response('Server Error', 500),
|
|
]);
|
|
|
|
$client = new AnalyticsClient;
|
|
$data = new PageviewData(
|
|
siteKey: 'testsiteky',
|
|
url: 'https://example.com/test',
|
|
ip: '192.168.1.1',
|
|
userAgent: 'Mozilla/5.0 Chrome',
|
|
referrer: 'https://google.com',
|
|
);
|
|
|
|
expect(fn () => $client->send($data))->not->toThrow(Exception::class);
|
|
});
|
|
|
|
it('sends data with correct site key', function () {
|
|
$client = new AnalyticsClient;
|
|
$data = new PageviewData(
|
|
siteKey: 'testsiteky',
|
|
url: 'https://example.com/test',
|
|
ip: '192.168.1.1',
|
|
userAgent: 'Mozilla/5.0 Chrome',
|
|
);
|
|
|
|
$client->send($data);
|
|
|
|
Http::assertSent(function ($request) {
|
|
return $request->data()['k'] === 'testsiteky';
|
|
});
|
|
});
|
|
|
|
it('skips sending when required fields are empty', function () {
|
|
$client = new AnalyticsClient;
|
|
|
|
$client->send(new PageviewData(
|
|
siteKey: '',
|
|
url: 'https://example.com',
|
|
ip: '192.168.1.1',
|
|
userAgent: 'Mozilla/5.0',
|
|
));
|
|
|
|
$client->send(new PageviewData(
|
|
siteKey: 'testsiteky',
|
|
url: '',
|
|
ip: '192.168.1.1',
|
|
userAgent: 'Mozilla/5.0',
|
|
));
|
|
|
|
$client->send(new PageviewData(
|
|
siteKey: 'testsiteky',
|
|
url: 'https://example.com',
|
|
ip: '',
|
|
userAgent: 'Mozilla/5.0',
|
|
));
|
|
|
|
Http::assertNothingSent();
|
|
});
|
|
|
|
it('skips sending when URL exceeds 512 characters', function () {
|
|
$client = new AnalyticsClient;
|
|
$longUrl = 'https://example.com/'.str_repeat('a', 500);
|
|
|
|
$client->send(new PageviewData(
|
|
siteKey: 'testsiteky',
|
|
url: $longUrl,
|
|
ip: '192.168.1.1',
|
|
userAgent: 'Mozilla/5.0 Chrome',
|
|
));
|
|
|
|
Http::assertNothingSent();
|
|
});
|
|
|
|
it('excludes empty referrer from payload', function () {
|
|
$client = new AnalyticsClient;
|
|
$data = new PageviewData(
|
|
siteKey: 'testsiteky',
|
|
url: 'https://example.com/test',
|
|
ip: '192.168.1.1',
|
|
userAgent: 'Mozilla/5.0 Chrome',
|
|
referrer: '',
|
|
);
|
|
|
|
$client->send($data);
|
|
|
|
Http::assertSent(function ($request) {
|
|
return ! isset($request->data()['r']);
|
|
});
|
|
});
|
|
|
|
it('excludes null referrer from payload', function () {
|
|
$client = new AnalyticsClient;
|
|
$data = new PageviewData(
|
|
siteKey: 'testsiteky',
|
|
url: 'https://example.com/test',
|
|
ip: '192.168.1.1',
|
|
userAgent: 'Mozilla/5.0 Chrome',
|
|
referrer: null,
|
|
);
|
|
|
|
$client->send($data);
|
|
|
|
Http::assertSent(function ($request) {
|
|
return ! isset($request->data()['r']);
|
|
});
|
|
});
|
|
|
|
it('sends the ingest secret as bearer token and forwards Accept-Language', function () {
|
|
(new AnalyticsClient)->send(new PageviewData(
|
|
siteKey: 'testsiteky',
|
|
url: 'https://example.com/test',
|
|
ip: '192.168.1.1',
|
|
userAgent: 'Mozilla/5.0 Chrome',
|
|
acceptLanguage: 'sv-SE,sv;q=0.9',
|
|
));
|
|
|
|
Http::assertSent(function ($request) {
|
|
return $request->hasHeader('Authorization', 'Bearer test-secret')
|
|
&& $request->hasHeader('Accept-Language', 'sv-SE,sv;q=0.9')
|
|
&& ! array_key_exists('acceptLanguage', $request->data());
|
|
});
|
|
});
|
|
|
|
it('logs a warning when the API rejects the pageview', function () {
|
|
Http::swap(new \Illuminate\Http\Client\Factory);
|
|
Http::fake(['*' => Http::response('', 401)]);
|
|
\Illuminate\Support\Facades\Log::spy();
|
|
|
|
(new AnalyticsClient)->send(new PageviewData(
|
|
siteKey: 'testsiteky',
|
|
url: 'https://example.com/test',
|
|
ip: '192.168.1.1',
|
|
userAgent: 'Mozilla/5.0 Chrome',
|
|
));
|
|
|
|
\Illuminate\Support\Facades\Log::shouldHaveReceived('warning')
|
|
->withArgs(fn ($message, $context) => str_contains($message, 'rejected') && $context['status'] === 401)
|
|
->once();
|
|
});
|
|
|
|
it('pauses sending after a connection failure', function () {
|
|
Http::swap(new \Illuminate\Http\Client\Factory);
|
|
$attempts = 0;
|
|
Http::fake(function () use (&$attempts) {
|
|
$attempts++;
|
|
|
|
throw new \Illuminate\Http\Client\ConnectionException('timeout');
|
|
});
|
|
|
|
$client = new AnalyticsClient;
|
|
$client->send(testPageview());
|
|
$client->send(testPageview());
|
|
|
|
expect($client->isPaused())->toBeTrue()
|
|
->and($attempts)->toBe(1);
|
|
});
|
|
|
|
it('pauses sending after a server error', function () {
|
|
Http::swap(new \Illuminate\Http\Client\Factory);
|
|
Http::fake(['*' => Http::response('', 503)]);
|
|
|
|
$client = new AnalyticsClient;
|
|
$client->send(testPageview());
|
|
$client->send(testPageview());
|
|
|
|
expect($client->isPaused())->toBeTrue();
|
|
Http::assertSentCount(1);
|
|
});
|
|
|
|
it('does not pause on client errors', function () {
|
|
Http::swap(new \Illuminate\Http\Client\Factory);
|
|
Http::fake(['*' => Http::response('', 401)]);
|
|
|
|
$client = new AnalyticsClient;
|
|
$client->send(testPageview());
|
|
|
|
expect($client->isPaused())->toBeFalse();
|
|
});
|
|
|
|
it('resumes sending after the pause expires', function () {
|
|
touch(storage_path('framework/mattitja-analytics.paused'), time() - 120);
|
|
|
|
$client = new AnalyticsClient;
|
|
$client->send(testPageview());
|
|
|
|
Http::assertSentCount(1);
|
|
expect(file_exists(storage_path('framework/mattitja-analytics.paused')))->toBeFalse();
|
|
});
|