first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
<?php
namespace Common\Admin\Analytics\Actions;
interface BuildAnalyticsReport
{
/**
* Get data for admin area analytics page from active provider.
* (Demo or Google Analytics currently)
*/
public function execute(array $params): array;
}

View File

@@ -0,0 +1,136 @@
<?php
namespace Common\Admin\Analytics\Actions;
use App\Models\User;
use Common\Database\Metrics\MetricDateRange;
use Illuminate\Support\Arr;
class BuildDemoAnalyticsReport implements BuildAnalyticsReport
{
protected MetricDateRange $dateRange;
public function execute(array $params): array
{
$this->dateRange = $params['dateRange'];
return [
'pageViews' => $this->buildPageviewsMetric(),
'browsers' => [
'granularity' => $this->dateRange->granularity,
'datasets' => [
[
'label' => __('Sessions'),
'data' => $this->buildBrowsersMetric(),
],
],
],
'locations' => [
'granularity' => $this->dateRange->granularity,
'datasets' => [
[
'label' => __('Sessions'),
'data' => $this->buildLocationsMetric(),
],
],
],
'devices' => [
'granularity' => $this->dateRange->granularity,
'datasets' => [
[
'label' => __('Sessions'),
'data' => $this->buildDevicesMetric(),
],
],
],
'platforms' => [
'granularity' => $this->dateRange->granularity,
'datasets' => [
[
'label' => __('Sessions'),
'data' => $this->buildPlatformsMetric(),
],
],
],
];
}
public function buildPageviewsMetric(): array
{
$current = (new DemoTrend(
User::query(),
dateRange: $this->dateRange,
))->count();
$previous = (new DemoTrend(
User::query(),
dateRange: $this->dateRange,
))->count();
return [
'granularity' => $this->dateRange->granularity,
'total' => array_sum(Arr::pluck($current, 'value')),
'datasets' => [
[
'label' => __('Current period'),
'data' => $current,
],
[
'label' => __('Previous period'),
'data' => $previous,
],
],
];
}
public function buildBrowsersMetric(): array
{
return [
['label' => 'Chrome', 'value' => random_int(300, 500)],
['label' => 'Firefox', 'value' => random_int(200, 400)],
['label' => 'IE', 'value' => random_int(100, 150)],
['label' => 'Edge', 'value' => random_int(100, 200)],
['label' => 'Safari', 'value' => random_int(200, 300)],
];
}
public function buildDevicesMetric(): array
{
return [
['label' => 'Mobile', 'value' => random_int(300, 500)],
['label' => 'Tablet', 'value' => random_int(200, 400)],
['label' => 'Desktop', 'value' => random_int(100, 150)],
];
}
public function buildPlatformsMetric(): array
{
return [
['label' => 'Windows', 'value' => random_int(300, 500)],
['label' => 'Linux', 'value' => random_int(200, 400)],
['label' => 'iOS', 'value' => random_int(100, 150)],
['label' => 'Android', 'value' => random_int(100, 150)],
];
}
public function buildLocationsMetric(): array
{
$data = [
['label' => 'United States', 'code' => 'US', 'value' => random_int(300, 500)],
['label' => 'India', 'code' => 'IN', 'value' => random_int(100, 300)],
['label' => 'Russia', 'code' => 'RU', 'value' => random_int(250, 400)],
['label' => 'Germany', 'code' => 'DE', 'value' => random_int(200, 500)],
['label' => 'France', 'code' => 'FR', 'value' => random_int(150, 300)],
['label' => 'Japan', 'code' => 'JP', 'value' => random_int(150, 300)],
['label' => 'United Kingdom', 'code' => 'GB', 'value' => random_int(300, 400)],
['label' => 'Canada', 'code' => 'CA', 'value' => random_int(100, 150)],
];
$total = array_sum(Arr::pluck($data, 'value'));
return array_map(function($item) use($total) {
$item['percentage'] = round($item['value'] / $total * 100, 2);
return $item;
}, $data);
}
}

View File

@@ -0,0 +1,331 @@
<?php
namespace Common\Admin\Analytics\Actions;
use Carbon\Carbon;
use Common\Database\Metrics\MetricDateRange;
use Common\Database\Metrics\Traits\GeneratesTrendResults;
use Google\Analytics\Data\V1beta\BetaAnalyticsDataClient;
use Google\Analytics\Data\V1beta\DateRange;
use Google\Analytics\Data\V1beta\Dimension;
use Google\Analytics\Data\V1beta\Metric;
use Google\Analytics\Data\V1beta\OrderBy;
use Google\Analytics\Data\V1beta\OrderBy\DimensionOrderBy;
use Google\Analytics\Data\V1beta\OrderBy\MetricOrderBy;
use Google\Analytics\Data\V1beta\Row;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
class BuildGoogleAnalyticsReport implements BuildAnalyticsReport
{
use GeneratesTrendResults;
protected BetaAnalyticsDataClient $client;
protected MetricDateRange $dateRange;
public function __construct()
{
$this->client = new BetaAnalyticsDataClient([
'credentials' => storage_path('laravel-analytics/certificate.json'),
]);
}
public function execute(array $params = []): array
{
$this->dateRange = $params['dateRange'] ?? new MetricDateRange();
return $this->buildReport();
}
protected function buildReport(): array
{
return [
'pageViews' => [
'datasets' => $this->buildPageViewsMetric(),
'granularity' => $this->dateRange->granularity,
],
'browsers' => [
'granularity' => $this->dateRange->granularity,
'datasets' => [
[
'label' => __('Sessions'),
'data' => $this->buildTopBrowsersMetric(),
],
],
],
'locations' => [
'granularity' => $this->dateRange->granularity,
'datasets' => [
[
'label' => __('Sessions'),
'data' => $this->buildTopLocationsMetric(),
],
],
],
'devices' => [
'granularity' => $this->dateRange->granularity,
'datasets' => [
[
'label' => __('Sessions'),
'data' => $this->buildTopDevicesMetric(),
],
],
],
'platforms' => [
'granularity' => $this->dateRange->granularity,
'datasets' => [
[
'label' => __('Sessions'),
'data' => $this->buildTopPlatformsMetric(),
],
],
],
];
}
protected function buildTopBrowsersMetric(int $maxResults = 10): Collection
{
$rows = $this->performQuery($this->dateRange, [
'dimensions' => ['browser'],
'metrics' => ['sessions'],
'sort' => ['direction' => 'desc', 'metric' => 'sessions'],
]);
$topBrowsers = $rows->map(function (Row $row) {
return [
'label' => $row->getDimensionValues()[0]->getValue(),
'value' => $row->getMetricValues()[0]->getValue(),
];
});
if ($topBrowsers->count() <= $maxResults) {
return $topBrowsers;
}
return $topBrowsers->take($maxResults - 1)->push([
'label' => __('Others'),
'value' => $topBrowsers->splice($maxResults - 1)->sum('value'),
]);
}
private function buildTopLocationsMetric(): Collection
{
$maxResults = 6;
$rows = $this->performQuery($this->dateRange, [
'metrics' => ['sessions'],
'dimensions' => ['country'],
'sort' => [
'direction' => 'desc',
'metric' => 'sessions',
],
]);
$locations = $rows->map(function (Row $row) {
return [
'label' => $row->getDimensionValues()[0]->getValue(),
'value' => $row->getMetricValues()[0]->getValue(),
];
});
$total = $locations->sum('value');
$locations = $locations->map(function ($location) use ($total) {
$location['percentage'] = round(
(100 * $location['value']) / $total,
1,
);
return $location;
});
if ($locations->count() <= $maxResults) {
return $locations;
}
return $locations->take($maxResults - 1)->push([
'label' => __('Other'),
'value' => $locations->splice($maxResults - 1)->sum('value'),
]);
}
protected function buildTopDevicesMetric(int $maxResults = 10): Collection
{
$rows = $this->performQuery($this->dateRange, [
'dimensions' => ['deviceCategory'],
'metrics' => ['sessions'],
'sort' => ['direction' => 'desc', 'metric' => 'sessions'],
]);
$devices = $rows->map(function (Row $row) {
return [
'label' => __(
ucfirst($row->getDimensionValues()[0]->getValue()),
),
'value' => $row->getMetricValues()[0]->getValue(),
];
});
if ($devices->count() <= $maxResults) {
return $devices;
}
return $devices->take($maxResults - 1)->push([
'label' => __('Others'),
'value' => $devices->splice($maxResults - 1)->sum('value'),
]);
}
protected function buildTopPlatformsMetric(int $maxResults = 10): Collection
{
$rows = $this->performQuery($this->dateRange, [
'dimensions' => ['operatingSystem'],
'metrics' => ['sessions'],
'sort' => ['direction' => 'desc', 'metric' => 'sessions'],
]);
$platforms = $rows->map(
fn(Row $row) => [
'label' => $row->getDimensionValues()[0]->getValue(),
'value' => $row->getMetricValues()[0]->getValue(),
],
);
if ($platforms->count() <= $maxResults) {
return $platforms;
}
return $platforms->take($maxResults - 1)->push([
'label' => __('Others'),
'value' => $platforms->splice($maxResults - 1)->sum('value'),
]);
}
private function buildPageViewsMetric(): array
{
$this->getPageViews($this->dateRange->getPreviousPeriod());
return [
[
'label' => __('Current period'),
'data' => $this->getPageViews($this->dateRange),
],
[
'label' => __('Previous period'),
'data' => $this->getPageViews(
$this->dateRange->getPreviousPeriod(),
),
],
];
}
private function getPageViews(MetricDateRange $dateRange): Collection
{
$format = $this->dateRange->getGroupingFormat();
$results = $this->fetchVisitorsAndPageViews($dateRange)
// Google Analytics will return views at one minute granularity always.
// Group these by date format based on selected granularity instead.
// e.g. "day" granularity will equal "YYYY-MM-DD" => ["value" => 1]
->groupBy(fn($item) => $item['date']->format($format))
// reduce each granularity group to a single value
->map(function (Collection $dateGroup) {
return $dateGroup->reduce(
function ($result, $item) {
$result['value'] += $item['value'];
return $result;
},
[
'date' => $dateGroup[0]['date'],
'value' => 0,
],
);
})
->mapWithKeys(
fn($item) => [
$item['date']->format($format) => $this->formatTrendResult(
$dateRange->granularity,
$item['date'],
$item['value'],
),
],
)
->sortKeys()
->all();
$mergedResults = array_replace(
$this->getAllPossibleDateResults($dateRange),
$results,
);
return collect($mergedResults)->values();
}
protected function fetchVisitorsAndPageViews(
MetricDateRange $dateRange,
): Collection {
$rows = $this->performQuery($dateRange, [
'dimensions' => ['dateHourMinute', 'pageTitle'], // YYYYMMDDHHMM
'metrics' => ['totalUsers', 'screenPageViews'],
]);
return $rows->map(function (Row $dateRow) {
return [
'date' => Carbon::createFromFormat(
'YmdHi',
$dateRow->getDimensionValues()[0]->getValue(),
),
'pageTitle' => $dateRow->getDimensionValues()[1]->getValue(),
'visitors' => (int) $dateRow->getMetricValues()[0]->getValue(),
'value' => (int) $dateRow->getMetricValues()[1]->getValue(),
];
});
}
protected function performQuery(
MetricDateRange $dateRange,
array $options,
): Collection {
$orderBy = null;
if (isset($options['sort'])) {
$sortOptions = [];
if (Arr::get($options, 'sort.direction') == 'desc') {
$sortOptions['desc'] = true;
}
if ($metricName = Arr::get($options, 'sort.metric')) {
$sortOptions['metric'] = (new MetricOrderBy())->setMetricName(
$metricName,
);
}
if ($dimensionName = Arr::get($options, 'sort.dimensions')) {
$sortOptions[
'dimension'
] = (new DimensionOrderBy())->setDimensionName($dimensionName);
}
$orderBy = new OrderBy($sortOptions);
}
$propertyId = config('services.google.analytics_property_id');
$response = $this->client->runReport([
'property' => "properties/$propertyId",
'dateRanges' => [$this->getGoogleDateRange($dateRange)],
'dimensions' => array_map(
fn($name) => new Dimension(['name' => $name]),
$options['dimensions'],
),
'metrics' => array_map(
fn($name) => new Metric(['name' => $name]),
$options['metrics'],
),
'orderBys' => $orderBy ? [$orderBy] : null,
]);
return collect($response->getRows() ?: []);
}
protected function getGoogleDateRange(MetricDateRange $range): DateRange
{
return (new DateRange())
->setStartDate($range->start->toDateString())
->setEndDate($range->end->toDateString());
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Common\Admin\Analytics\Actions;
class BuildNullAnalyticsReport implements BuildAnalyticsReport
{
public function execute(array $params): array
{
$dateRange = $params['dateRange'];
return [
'pageViews' => [
'granularity' => $dateRange->granularity,
'datasets' => [
[
'label' => __('Current period'),
'data' => [],
],
[
'label' => __('Previous period'),
'data' => [],
],
],
],
'browsers' => [
'granularity' => $dateRange->granularity,
'datasets' => [
[
'label' => __('Sessions'),
'data' => [],
],
],
],
'locations' => [
'granularity' => $dateRange->granularity,
'datasets' => [
[
'label' => __('Sessions'),
'data' => [],
],
],
],
'devices' => [
'granularity' => $dateRange->granularity,
'datasets' => [
[
'label' => __('Sessions'),
'data' => [],
],
],
],
'platforms' => [
'granularity' => $dateRange->granularity,
'datasets' => [
[
'label' => __('Sessions'),
'data' => [],
],
],
],
];
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Common\Admin\Analytics\Actions;
use Common\Database\Metrics\Trend;
class DemoTrend extends Trend
{
protected function aggregate(string $function): array
{
$data = array_map(function($item) {
$item['value'] = random_int(100, 500);
return $item;
}, $this->getAllPossibleDateResults($this->dateRange));
return array_values($data);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Common\Admin\Analytics\Actions;
interface GetAnalyticsHeaderDataAction
{
/**
* Get analytics header data.
*/
public function execute(array $params): array;
}

View File

@@ -0,0 +1,72 @@
<?php namespace Common\Admin\Analytics;
use Carbon\CarbonImmutable;
use Common\Admin\Analytics\Actions\BuildAnalyticsReport;
use Common\Admin\Analytics\Actions\BuildNullAnalyticsReport;
use Common\Admin\Analytics\Actions\GetAnalyticsHeaderDataAction;
use Common\Core\BaseController;
use Common\Database\Metrics\MetricDateRange;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class AnalyticsController extends BaseController
{
public function __construct(
protected Request $request,
protected BuildAnalyticsReport $getDataAction,
protected GetAnalyticsHeaderDataAction $getHeaderDataAction,
) {
}
public function report()
{
$this->authorize('index', 'ReportPolicy');
$types = explode(',', $this->request->get('types', 'visitors,header'));
$dateRange = $this->getDateRange();
$cacheKey = sprintf(
'%s-%s',
$dateRange->getCacheKey(),
implode(',', $types),
);
$response = [];
$reportParams = ['dateRange' => $dateRange];
if (in_array('visitors', $types)) {
try {
$response['visitorsReport'] = Cache::remember(
"adminReport.main.$cacheKey",
CarbonImmutable::now()->addDay(),
fn() => $this->getDataAction->execute($reportParams),
);
} catch (Exception $e) {
$response['visitorsReport'] = app(
BuildNullAnalyticsReport::class,
)->execute($reportParams);
}
}
if (in_array('header', $types)) {
$response['headerReport'] = Cache::remember(
"adminReport.header.$cacheKey",
CarbonImmutable::now()->addDay(),
fn() => $this->getHeaderDataAction->execute($reportParams),
);
}
return $this->success($response);
}
protected function getDateRange(): MetricDateRange
{
$startDate = $this->request->get('startDate');
$endDate = $this->request->get('endDate');
$timezone = $this->request->get('timezone', config('app.timezone'));
return new MetricDateRange(
start: $startDate,
end: $endDate,
timezone: $timezone,
);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Common\Admin\Analytics;
use Common\Admin\Analytics\Actions\BuildAnalyticsReport;
use Common\Admin\Analytics\Actions\BuildDemoAnalyticsReport;
use Common\Admin\Analytics\Actions\BuildGoogleAnalyticsReport;
use Common\Admin\Analytics\Actions\BuildNullAnalyticsReport;
use Illuminate\Support\ServiceProvider;
class AnalyticsServiceProvider extends ServiceProvider
{
/**
* Register bindings in the container.
*
* @return void
*/
public function register()
{
$this->app->singleton(BuildAnalyticsReport::class, function () {
if (config('common.site.demo')) {
return new BuildDemoAnalyticsReport();
} else {
return $this->getGoogleAnalyticsData();
}
});
}
private function getGoogleAnalyticsData()
{
try {
return new BuildGoogleAnalyticsReport();
} catch (\Exception $e) {
return new BuildNullAnalyticsReport();
}
}
}