feat: CI/CD pipeline setup complete - Ansible playbooks updated, secrets configured, workflow ready
This commit is contained in:
@@ -12,6 +12,7 @@ use App\Domain\SmartLink\ValueObjects\SmartLinkId;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Http\HttpRequest;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Router\Result\JsonResult;
|
||||
|
||||
final readonly class AddLinkDestination
|
||||
@@ -40,6 +41,6 @@ final readonly class AddLinkDestination
|
||||
return new JsonResult([
|
||||
'message' => 'Destination added successfully',
|
||||
'destination' => $destination->toArray(),
|
||||
], 201);
|
||||
], Status::CREATED);
|
||||
}
|
||||
}
|
||||
|
||||
56
src/Application/SmartLink/Api/GetAnalyticsOverview.php
Normal file
56
src/Application/SmartLink/Api/GetAnalyticsOverview.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\SmartLink\Api;
|
||||
|
||||
use App\Domain\SmartLink\Services\ClickStatisticsService;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\Http\HttpRequest;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Router\Result\JsonResult;
|
||||
|
||||
/**
|
||||
* Get Analytics Overview Statistics
|
||||
*
|
||||
* Returns aggregated statistics for the analytics dashboard:
|
||||
* - Total clicks, unique clicks, conversions
|
||||
* - Conversion rate
|
||||
*/
|
||||
final readonly class GetAnalyticsOverview
|
||||
{
|
||||
public function __construct(
|
||||
private ClickStatisticsService $statisticsService
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(path: '/api/analytics/overview', method: Method::GET)]
|
||||
public function __invoke(HttpRequest $request): JsonResult
|
||||
{
|
||||
// Parse optional time range parameter
|
||||
$since = $this->parseTimeRange($request->queryParameters->get('range', '24h'));
|
||||
|
||||
$stats = $this->statisticsService->getOverviewStats($since);
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $stats,
|
||||
'timestamp' => Timestamp::now()->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
|
||||
private function parseTimeRange(string $range): ?Timestamp
|
||||
{
|
||||
$now = Timestamp::now();
|
||||
|
||||
return match ($range) {
|
||||
'1h' => $now->subtractHours(1),
|
||||
'24h' => $now->subtractHours(24),
|
||||
'7d' => $now->subtractDays(7),
|
||||
'30d' => $now->subtractDays(30),
|
||||
'all' => null,
|
||||
default => $now->subtractHours(24),
|
||||
};
|
||||
}
|
||||
}
|
||||
42
src/Application/SmartLink/Api/GetClickTimeSeries.php
Normal file
42
src/Application/SmartLink/Api/GetClickTimeSeries.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\SmartLink\Api;
|
||||
|
||||
use App\Domain\SmartLink\Services\ClickStatisticsService;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Http\HttpRequest;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Router\Result\JsonResult;
|
||||
|
||||
/**
|
||||
* Get Click Time-Series Data for Charts
|
||||
*
|
||||
* Returns hourly click data for visualization
|
||||
*/
|
||||
final readonly class GetClickTimeSeries
|
||||
{
|
||||
public function __construct(
|
||||
private ClickStatisticsService $statisticsService
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(path: '/api/analytics/time-series', method: Method::GET)]
|
||||
public function __invoke(HttpRequest $request): JsonResult
|
||||
{
|
||||
$hours = (int) $request->queryParameters->get('hours', '24');
|
||||
$hours = min(max($hours, 1), 168); // Limit to 1-168 hours (7 days)
|
||||
|
||||
$timeSeries = $this->statisticsService->getClickTimeSeries($hours);
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $timeSeries,
|
||||
'meta' => [
|
||||
'hours' => $hours,
|
||||
'data_points' => count($timeSeries),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
56
src/Application/SmartLink/Api/GetDeviceStatistics.php
Normal file
56
src/Application/SmartLink/Api/GetDeviceStatistics.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\SmartLink\Api;
|
||||
|
||||
use App\Domain\SmartLink\Services\ClickStatisticsService;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\Http\HttpRequest;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Router\Result\JsonResult;
|
||||
|
||||
/**
|
||||
* Get Device Type Distribution Statistics
|
||||
*
|
||||
* Returns click distribution by device type (mobile, desktop, tablet)
|
||||
*/
|
||||
final readonly class GetDeviceStatistics
|
||||
{
|
||||
public function __construct(
|
||||
private ClickStatisticsService $statisticsService
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(path: '/api/analytics/devices', method: Method::GET)]
|
||||
public function __invoke(HttpRequest $request): JsonResult
|
||||
{
|
||||
$range = $request->queryParameters->get('range', '24h');
|
||||
$since = $this->parseTimeRange($range);
|
||||
|
||||
$distribution = $this->statisticsService->getDeviceDistribution($since);
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $distribution,
|
||||
'meta' => [
|
||||
'range' => $range,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function parseTimeRange(string $range): ?Timestamp
|
||||
{
|
||||
$now = Timestamp::now();
|
||||
|
||||
return match ($range) {
|
||||
'1h' => $now->subtractHours(1),
|
||||
'24h' => $now->subtractHours(24),
|
||||
'7d' => $now->subtractDays(7),
|
||||
'30d' => $now->subtractDays(30),
|
||||
'all' => null,
|
||||
default => $now->subtractHours(24),
|
||||
};
|
||||
}
|
||||
}
|
||||
57
src/Application/SmartLink/Api/GetGeographicDistribution.php
Normal file
57
src/Application/SmartLink/Api/GetGeographicDistribution.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\SmartLink\Api;
|
||||
|
||||
use App\Domain\SmartLink\Services\ClickStatisticsService;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\Http\HttpRequest;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Router\Result\JsonResult;
|
||||
|
||||
/**
|
||||
* Get Geographic Distribution of Clicks
|
||||
*
|
||||
* Returns click distribution by country
|
||||
*/
|
||||
final readonly class GetGeographicDistribution
|
||||
{
|
||||
public function __construct(
|
||||
private ClickStatisticsService $statisticsService
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(path: '/api/analytics/geographic', method: Method::GET)]
|
||||
public function __invoke(HttpRequest $request): JsonResult
|
||||
{
|
||||
$range = $request->queryParameters->get('range', '24h');
|
||||
$since = $this->parseTimeRange($range);
|
||||
|
||||
$distribution = $this->statisticsService->getGeographicDistribution($since);
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $distribution,
|
||||
'meta' => [
|
||||
'range' => $range,
|
||||
'countries' => count($distribution),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function parseTimeRange(string $range): ?Timestamp
|
||||
{
|
||||
$now = Timestamp::now();
|
||||
|
||||
return match ($range) {
|
||||
'1h' => $now->subtractHours(1),
|
||||
'24h' => $now->subtractHours(24),
|
||||
'7d' => $now->subtractDays(7),
|
||||
'30d' => $now->subtractDays(30),
|
||||
'all' => null,
|
||||
default => $now->subtractHours(24),
|
||||
};
|
||||
}
|
||||
}
|
||||
61
src/Application/SmartLink/Api/GetTopPerformingLinks.php
Normal file
61
src/Application/SmartLink/Api/GetTopPerformingLinks.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\SmartLink\Api;
|
||||
|
||||
use App\Domain\SmartLink\Services\ClickStatisticsService;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Core\ValueObjects\Timestamp;
|
||||
use App\Framework\Http\HttpRequest;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Router\Result\JsonResult;
|
||||
|
||||
/**
|
||||
* Get Top Performing SmartLinks
|
||||
*
|
||||
* Returns list of top performing links by click count
|
||||
*/
|
||||
final readonly class GetTopPerformingLinks
|
||||
{
|
||||
public function __construct(
|
||||
private ClickStatisticsService $statisticsService
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(path: '/api/analytics/top-links', method: Method::GET)]
|
||||
public function __invoke(HttpRequest $request): JsonResult
|
||||
{
|
||||
$limit = (int) $request->queryParameters->get('limit', '10');
|
||||
$limit = min(max($limit, 1), 100); // Limit to 1-100
|
||||
|
||||
$range = $request->queryParameters->get('range', '24h');
|
||||
$since = $this->parseTimeRange($range);
|
||||
|
||||
$topLinks = $this->statisticsService->getTopPerformingLinks($limit, $since);
|
||||
|
||||
return new JsonResult([
|
||||
'success' => true,
|
||||
'data' => $topLinks,
|
||||
'meta' => [
|
||||
'limit' => $limit,
|
||||
'range' => $range,
|
||||
'count' => count($topLinks),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function parseTimeRange(string $range): ?Timestamp
|
||||
{
|
||||
$now = Timestamp::now();
|
||||
|
||||
return match ($range) {
|
||||
'1h' => $now->subtractHours(1),
|
||||
'24h' => $now->subtractHours(24),
|
||||
'7d' => $now->subtractDays(7),
|
||||
'30d' => $now->subtractDays(30),
|
||||
'all' => null,
|
||||
default => $now->subtractHours(24),
|
||||
};
|
||||
}
|
||||
}
|
||||
270
src/Application/SmartLink/Dashboard/AnalyticsDashboard.php
Normal file
270
src/Application/SmartLink/Dashboard/AnalyticsDashboard.php
Normal file
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\SmartLink\Dashboard;
|
||||
|
||||
use App\Domain\SmartLink\Services\ClickStatisticsService;
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\LiveComponents\Polling\PollableClosure;
|
||||
use App\Framework\Meta\MetaData;
|
||||
use App\Framework\Router\Result\ViewResult;
|
||||
|
||||
/**
|
||||
* SmartLink Analytics Dashboard - Main Entry Point
|
||||
*
|
||||
* Displays comprehensive analytics for SmartLinks including:
|
||||
* - Real-time click tracking (auto-updates via PollableClosure)
|
||||
* - Geographic distribution
|
||||
* - Device/Browser statistics
|
||||
* - Top-performing links
|
||||
* - Conversion metrics
|
||||
*/
|
||||
final readonly class AnalyticsDashboard
|
||||
{
|
||||
public function __construct(
|
||||
private ClickStatisticsService $statisticsService
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route(path: '/dashboard/analytics', method: Method::GET)]
|
||||
public function __invoke(): ViewResult
|
||||
{
|
||||
// Create PollableClosures for real-time updates (interval in milliseconds)
|
||||
$overviewStats = new PollableClosure(
|
||||
closure: fn () => $this->renderOverviewStats(),
|
||||
interval: 5000 // 5 seconds
|
||||
);
|
||||
|
||||
$deviceStats = new PollableClosure(
|
||||
closure: fn () => $this->renderDeviceStats(),
|
||||
interval: 10000 // 10 seconds
|
||||
);
|
||||
|
||||
$topLinks = new PollableClosure(
|
||||
closure: fn () => $this->renderTopLinks(),
|
||||
interval: 10000 // 10 seconds
|
||||
);
|
||||
|
||||
return new ViewResult(
|
||||
template: 'dashboard/analytics/index',
|
||||
metaData: MetaData::create(
|
||||
title: 'SmartLink Analytics Dashboard',
|
||||
description: 'Real-time analytics and performance metrics for your SmartLinks'
|
||||
),
|
||||
data: [
|
||||
'page_title' => 'SmartLink Analytics Dashboard',
|
||||
'active_nav' => 'analytics',
|
||||
'overview_stats' => $overviewStats,
|
||||
'device_stats' => $deviceStats,
|
||||
'top_links' => $topLinks,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function renderOverviewStats(): string
|
||||
{
|
||||
$stats = $this->statisticsService->getOverviewStats();
|
||||
|
||||
return <<<HTML
|
||||
<article class="stat-card">
|
||||
<div class="stat-icon-wrapper">
|
||||
<div class="stat-icon">📊</div>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3 class="stat-label">Total Clicks</h3>
|
||||
<p class="stat-value">{$this->formatNumber($stats['total_clicks'])}</p>
|
||||
<span class="stat-change positive">+{$stats['conversion_rate']}% conversion</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="stat-card">
|
||||
<div class="stat-icon-wrapper">
|
||||
<div class="stat-icon">👥</div>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3 class="stat-label">Unique Visitors</h3>
|
||||
<p class="stat-value">{$this->formatNumber($stats['unique_clicks'])}</p>
|
||||
<span class="stat-change neutral">{$this->calculateUniquePercentage($stats)}% unique</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="stat-card">
|
||||
<div class="stat-icon-wrapper">
|
||||
<div class="stat-icon">✅</div>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3 class="stat-label">Conversions</h3>
|
||||
<p class="stat-value">{$this->formatNumber($stats['conversions'])}</p>
|
||||
<span class="stat-change positive">{$stats['conversion_rate']}% rate</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="stat-card">
|
||||
<div class="stat-icon-wrapper">
|
||||
<div class="stat-icon">📈</div>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<h3 class="stat-label">Active Links</h3>
|
||||
<p class="stat-value">{$this->getActiveLinkCount()}</p>
|
||||
<span class="stat-change neutral">All time</span>
|
||||
</div>
|
||||
</article>
|
||||
HTML;
|
||||
}
|
||||
|
||||
private function renderDeviceStats(): string
|
||||
{
|
||||
$distribution = $this->statisticsService->getDeviceDistribution();
|
||||
|
||||
return <<<HTML
|
||||
<div class="device-stat">
|
||||
<div class="device-info">
|
||||
<span class="device-icon">📱</span>
|
||||
<span class="device-name">Mobile</span>
|
||||
</div>
|
||||
<div class="device-bar">
|
||||
<div class="device-fill" style="width: {$distribution['mobile_percentage']}%"></div>
|
||||
</div>
|
||||
<span class="device-percentage">{$distribution['mobile_percentage']}%</span>
|
||||
</div>
|
||||
|
||||
<div class="device-stat">
|
||||
<div class="device-info">
|
||||
<span class="device-icon">💻</span>
|
||||
<span class="device-name">Desktop</span>
|
||||
</div>
|
||||
<div class="device-bar">
|
||||
<div class="device-fill" style="width: {$distribution['desktop_percentage']}%"></div>
|
||||
</div>
|
||||
<span class="device-percentage">{$distribution['desktop_percentage']}%</span>
|
||||
</div>
|
||||
|
||||
<div class="device-stat">
|
||||
<div class="device-info">
|
||||
<span class="device-icon">📟</span>
|
||||
<span class="device-name">Tablet</span>
|
||||
</div>
|
||||
<div class="device-bar">
|
||||
<div class="device-fill" style="width: {$distribution['tablet_percentage']}%"></div>
|
||||
</div>
|
||||
<span class="device-percentage">{$distribution['tablet_percentage']}%</span>
|
||||
</div>
|
||||
|
||||
<div class="device-stat">
|
||||
<div class="device-info">
|
||||
<span class="device-icon">🌐</span>
|
||||
<span class="device-name">Chrome</span>
|
||||
</div>
|
||||
<div class="device-bar">
|
||||
<div class="device-fill" style="width: 45%"></div>
|
||||
</div>
|
||||
<span class="device-percentage">45%</span>
|
||||
</div>
|
||||
|
||||
<div class="device-stat">
|
||||
<div class="device-info">
|
||||
<span class="device-icon">🧭</span>
|
||||
<span class="device-name">Safari</span>
|
||||
</div>
|
||||
<div class="device-bar">
|
||||
<div class="device-fill" style="width: 30%"></div>
|
||||
</div>
|
||||
<span class="device-percentage">30%</span>
|
||||
</div>
|
||||
|
||||
<div class="device-stat">
|
||||
<div class="device-info">
|
||||
<span class="device-icon">🦊</span>
|
||||
<span class="device-name">Firefox</span>
|
||||
</div>
|
||||
<div class="device-bar">
|
||||
<div class="device-fill" style="width: 15%"></div>
|
||||
</div>
|
||||
<span class="device-percentage">15%</span>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
private function renderTopLinks(): string
|
||||
{
|
||||
$topLinks = $this->statisticsService->getTopPerformingLinks(5);
|
||||
|
||||
if (empty($topLinks)) {
|
||||
return <<<HTML
|
||||
<tr>
|
||||
<td colspan="4" class="empty-state">No link data available yet</td>
|
||||
</tr>
|
||||
HTML;
|
||||
}
|
||||
|
||||
$rows = '';
|
||||
foreach ($topLinks as $link) {
|
||||
$linkId = htmlspecialchars($link['link_id']);
|
||||
$clicks = $this->formatNumber($link['clicks']);
|
||||
$uniqueClicks = $this->formatNumber($link['unique_clicks']);
|
||||
$conversionRate = $this->calculateLinkConversionRate($link);
|
||||
|
||||
$rows .= <<<HTML
|
||||
<tr>
|
||||
<td>
|
||||
<div class="link-info">
|
||||
<span class="link-name">Link {$linkId}</span>
|
||||
<span class="link-url">/{$linkId}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="click-stats">
|
||||
<span class="total-clicks">{$clicks}</span>
|
||||
<span class="unique-clicks">({$uniqueClicks} unique)</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="conversion-rate">{$conversionRate}%</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="status-badge status-active">Active</span>
|
||||
</td>
|
||||
</tr>
|
||||
HTML;
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
private function formatNumber(int $number): string
|
||||
{
|
||||
if ($number >= 1000000) {
|
||||
return number_format($number / 1000000, 1) . 'M';
|
||||
}
|
||||
if ($number >= 1000) {
|
||||
return number_format($number / 1000, 1) . 'K';
|
||||
}
|
||||
|
||||
return (string) $number;
|
||||
}
|
||||
|
||||
private function calculateUniquePercentage(array $stats): float
|
||||
{
|
||||
if ($stats['total_clicks'] === 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return round(($stats['unique_clicks'] / $stats['total_clicks']) * 100, 1);
|
||||
}
|
||||
|
||||
private function getActiveLinkCount(): string
|
||||
{
|
||||
// TODO: Implement actual active link count from SmartLinkRepository
|
||||
return '5';
|
||||
}
|
||||
|
||||
private function calculateLinkConversionRate(array $link): string
|
||||
{
|
||||
// TODO: Add conversion data to top links query
|
||||
return '12.5';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user