254 lines
9.2 KiB
PHP
254 lines
9.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Analytics\AnalyticsCategory;
|
|
use App\Framework\Analytics\AnalyticsCollector;
|
|
use App\Framework\Analytics\Storage\AnalyticsStorage;
|
|
use App\Framework\Http\ServerEnvironment;
|
|
use App\Framework\Performance\Contracts\PerformanceCollectorInterface;
|
|
use App\Framework\Random\RandomGenerator;
|
|
|
|
describe('AnalyticsCollector', function () {
|
|
beforeEach(function () {
|
|
// Mock dependencies
|
|
$this->performanceCollector = Mockery::mock(PerformanceCollectorInterface::class);
|
|
$this->storage = Mockery::mock(AnalyticsStorage::class);
|
|
$this->random = Mockery::mock(RandomGenerator::class);
|
|
|
|
// Create real ServerEnvironment with test data (final class, can't be mocked)
|
|
$this->serverEnvironment = new ServerEnvironment([
|
|
'REMOTE_ADDR' => '127.0.0.1',
|
|
'HTTP_USER_AGENT' => 'Test-Agent/1.0',
|
|
'REQUEST_URI' => '/test',
|
|
'HTTP_REFERER' => 'https://example.com',
|
|
]);
|
|
|
|
// Allow any performance collector calls (framework internal)
|
|
$this->performanceCollector
|
|
->shouldReceive('recordMetric')
|
|
->zeroOrMoreTimes();
|
|
|
|
$this->performanceCollector
|
|
->shouldReceive('increment')
|
|
->zeroOrMoreTimes();
|
|
|
|
// Allow storage aggregated calls (framework internal)
|
|
$this->storage
|
|
->shouldReceive('storeAggregated')
|
|
->zeroOrMoreTimes();
|
|
|
|
// Allow random float calls for sampling (may or may not be called)
|
|
$this->random
|
|
->shouldReceive('float')
|
|
->with(0, 1)
|
|
->zeroOrMoreTimes()
|
|
->andReturn(0.5);
|
|
|
|
// Allow random bytes calls for session ID generation (may or may not be called)
|
|
$this->random
|
|
->shouldReceive('bytes')
|
|
->with(16)
|
|
->zeroOrMoreTimes()
|
|
->andReturn(str_repeat('a', 16));
|
|
|
|
// Default: tracking enabled, 100% sampling for tests
|
|
$this->collector = new AnalyticsCollector(
|
|
performanceCollector: $this->performanceCollector,
|
|
storage: $this->storage,
|
|
random: $this->random,
|
|
serverEnvironment: $this->serverEnvironment,
|
|
enabled: true,
|
|
samplingRate: 1.0
|
|
);
|
|
});
|
|
|
|
afterEach(function () {
|
|
Mockery::close();
|
|
});
|
|
|
|
describe('trackAction', function () {
|
|
it('tracks user action with category and properties', function () {
|
|
// Expect raw data storage with flexible array matcher
|
|
$this->storage
|
|
->shouldReceive('storeRawData')
|
|
->once()
|
|
->with(
|
|
Mockery::on(function ($data) {
|
|
return is_array($data)
|
|
&& $data['category'] === 'user_behavior'
|
|
&& $data['action'] === 'button_click'
|
|
&& isset($data['session_id'])
|
|
&& isset($data['timestamp'])
|
|
&& isset($data['button_id'])
|
|
&& $data['button_id'] === 'submit-btn';
|
|
}),
|
|
1.0
|
|
);
|
|
|
|
// Track action
|
|
$this->collector->trackAction(
|
|
action: 'button_click',
|
|
category: AnalyticsCategory::USER_BEHAVIOR,
|
|
properties: ['button_id' => 'submit-btn']
|
|
);
|
|
});
|
|
|
|
it('does not track when analytics disabled', function () {
|
|
// Create collector with analytics disabled
|
|
$disabledCollector = new AnalyticsCollector(
|
|
performanceCollector: $this->performanceCollector,
|
|
storage: $this->storage,
|
|
random: $this->random,
|
|
serverEnvironment: $this->serverEnvironment,
|
|
enabled: false, // Disabled
|
|
samplingRate: 1.0
|
|
);
|
|
|
|
// Storage should NOT be called
|
|
$this->storage->shouldNotReceive('storeRawData');
|
|
|
|
// Track action (should be ignored)
|
|
$disabledCollector->trackAction('click', AnalyticsCategory::USER_BEHAVIOR);
|
|
});
|
|
|
|
it('respects sampling rate', function () {
|
|
// Create new Random mock for this test
|
|
$randomMock = Mockery::mock(RandomGenerator::class);
|
|
|
|
// Random returns 0.6 (above 0.5 threshold) -> should NOT track (0.6 > 0.5)
|
|
$randomMock->shouldReceive('float')->with(0, 1)->andReturn(0.6);
|
|
|
|
// Create collector with 50% sampling
|
|
$sampledCollector = new AnalyticsCollector(
|
|
performanceCollector: $this->performanceCollector,
|
|
storage: $this->storage,
|
|
random: $randomMock,
|
|
serverEnvironment: $this->serverEnvironment,
|
|
enabled: true,
|
|
samplingRate: 0.5
|
|
);
|
|
|
|
// Storage should NOT be called (sampled out)
|
|
$this->storage->shouldNotReceive('storeRawData');
|
|
|
|
// Track action (should be sampled out)
|
|
$sampledCollector->trackAction('click', AnalyticsCategory::USER_BEHAVIOR);
|
|
});
|
|
});
|
|
|
|
describe('trackPageView', function () {
|
|
it('tracks page view with path and title', function () {
|
|
// Expect raw data storage with flexible matcher
|
|
$this->storage
|
|
->shouldReceive('storeRawData')
|
|
->once()
|
|
->with(
|
|
Mockery::on(function ($data) {
|
|
return is_array($data)
|
|
&& $data['path'] === '/dashboard'
|
|
&& $data['title'] === 'Dashboard'
|
|
&& isset($data['timestamp'])
|
|
&& isset($data['session_id']);
|
|
}),
|
|
1.0
|
|
);
|
|
|
|
// Track page view
|
|
$this->collector->trackPageView(
|
|
path: '/dashboard',
|
|
title: 'Dashboard'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('trackError', function () {
|
|
it('tracks error with type and message', function () {
|
|
// trackError only logs to performance collector, not storage
|
|
// Storage expectations are handled by global mocks
|
|
|
|
// Track error
|
|
$this->collector->trackError(
|
|
errorType: 'ValidationException',
|
|
message: 'Invalid email format'
|
|
);
|
|
|
|
// Test passes if no exceptions are thrown
|
|
expect(true)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('trackBusinessEvent', function () {
|
|
it('tracks business event with value and currency', function () {
|
|
// trackBusinessEvent only logs to performance collector, not storage
|
|
// Storage expectations are handled by global mocks
|
|
|
|
// Track business event
|
|
$this->collector->trackBusinessEvent(
|
|
event: 'purchase_completed',
|
|
value: 99.99,
|
|
currency: 'EUR'
|
|
);
|
|
|
|
// Test passes if no exceptions are thrown
|
|
expect(true)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('trackApiCall', function () {
|
|
it('tracks API call with endpoint and metrics', function () {
|
|
// trackApiCall only logs to performance collector, not storage
|
|
// Storage expectations are handled by global mocks
|
|
|
|
// Track API call
|
|
$this->collector->trackApiCall(
|
|
endpoint: '/api/users',
|
|
method: 'GET',
|
|
responseCode: 200,
|
|
responseTime: 0.125
|
|
);
|
|
|
|
// Test passes if no exceptions are thrown
|
|
expect(true)->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('edge cases', function () {
|
|
it('handles zero sampling rate', function () {
|
|
// Create new Random mock for this test
|
|
$randomMock = Mockery::mock(RandomGenerator::class);
|
|
|
|
// Random float will be called and return value > 0.0 (will fail sampling)
|
|
$randomMock->shouldReceive('float')->with(0, 1)->andReturn(0.1);
|
|
|
|
// Create collector with 0% sampling (no tracking)
|
|
$noSamplingCollector = new AnalyticsCollector(
|
|
performanceCollector: $this->performanceCollector,
|
|
storage: $this->storage,
|
|
random: $randomMock,
|
|
serverEnvironment: $this->serverEnvironment,
|
|
enabled: true,
|
|
samplingRate: 0.0
|
|
);
|
|
|
|
// Storage should NOT be called
|
|
$this->storage->shouldNotReceive('storeRawData');
|
|
|
|
// Track action (should be sampled out)
|
|
$noSamplingCollector->trackAction('click', AnalyticsCategory::USER_BEHAVIOR);
|
|
});
|
|
|
|
it('handles full sampling rate', function () {
|
|
// With 100% sampling, float() should NOT be called (early return)
|
|
// Expect storage to be called
|
|
$this->storage
|
|
->shouldReceive('storeRawData')
|
|
->once()
|
|
->with(Mockery::type('array'), 1.0);
|
|
|
|
// Track action (should be tracked)
|
|
$this->collector->trackAction('click', AnalyticsCategory::USER_BEHAVIOR);
|
|
});
|
|
});
|
|
});
|