Laravel аналитика. Зачем и как я сделал свой пакет

Источник: «Laravel analytics - how and why I made my own analytics package»
Я использовал Google Analytics в течение нескольких лет, и она работала хорошо. Возникает вопрос, зачем я написал свой пакет аналитики.

Для этого было несколько причин:

Требования

База данных

Как я упоминал ранее, цель состояла в том, чтобы всё было очень просто, поэтому база данных состоит только из одной таблицы с именем laravel_analytics_page_views, где префикс laravel_analytics_ настраивается в файле конфигурации для предотвращения потенциальных конфликтов с таблицами базы данных приложения.

Схема структуры/миграции выглядит следующим образом:

$tableName = config('laravel-analytics.db_prefix') . 'page_views';

Schema::create($tableName, function (Blueprint $table) {
$table->id();
$table->string('session_id')->index();
$table->string('path')->index();
$table->string('user_agent')->nullable();
$table->string('ip')->nullable();
$table->string('referer')->nullable()->index();
$table->string('county')->nullable()->index();
$table->string('city')->nullable();
$table->string('page_model_type')->nullable();
$table->string('page_model_id')->nullable();
$table->timestamp('created_at')->nullable()->index();
$table->timestamp('updated_at')->nullable();

$table->index(['page_model_type', 'page_model_id']);
});

Мы отслеживаем уникальных посетителей по session_id, что конечно не идеально и не на 100% точно, но работает.

Мы создаём полиморфное отношение с page_model_type и page_model_id, если для отслеживаемой страницы есть релевантная модель, мы сохраняем тип и идентификатор для использования в будущем, если это необходимо. Также создан комбинированный индекс для этих двух полей, так как они чаще всего запрашиваются вместе при использовании полиморфных отношений.

Middleware

Я хотел универсальное решение, а не добавление аналитики ко всем контроллерам, создал middleware, которое может обрабатывать отслеживание. Middleware можно добавить ко всем маршрутам или к определённой группе/группам маршрутов.

Само middleware довольно простое, оно отслеживает только запросы на получение и пропускает вызовы ajax. Поскольку отслеживать трафик ботов не имеет смысла, я использовал пакет https://github.com/JayBizzle/Crawler-Detect для обнаружения сканеров и ботов. Когда сканер обнаруживается, он просто пропускается отслеживанием, таким образом, мы можем избежать бесполезных данных в таблице.

Было несколько сложно получить связанную модель для URL-адреса универсальным способом. Итоговое решение не является полностью универсальным, поскольку оно предполагает, что приложение использует привязку модели маршрута, и предполагает, что первая привязка имеет отношение к этой странице. Опять, это не идеально, но соответствует минималистичному подходу, которому я следовал при разработке этого пакета.

Вот код middleware:

public function handle(Request $request, Closure $next)
{
$response = $next($request);

try {
if (!$request->isMethod('GET')) {
return $response;
}

if ($request->isJson()) {
return $response;
}

$userAgent = $request->userAgent();

if (is_null($userAgent)) {
return $response;
}

/** @var CrawlerDetect $crawlerDetect */
$crawlerDetect = app(CrawlerDetect::class);

if ($crawlerDetect->isCrawler($userAgent)) {
return $response;
}

/** @var PageView $pageView */
$pageView = PageView::make([
'session_id' => session()->getId(),
'path' => $request->path(),
'user_agent' => Str::substr($userAgent, 0, 255),
'ip' => $request->ip(),
'referer' => $request->headers->get('referer'),
]);

$parameters = $request->route()?->parameters();
$model = null;

if (!is_null($parameters)) {
$model = reset($parameters);
}

if (is_a($model, Model::class)) {
$pageView->pageModel()->associate($model);
}

$pageView->save();

return $response;
} catch (Throwable $e) {
report($e);
return $response;
}
}

Маршруты

При разработке Laravel пакетов можно настроить сервис провайдер пакетов, чтобы сказать приложению использовать маршруты из пакета. Обычно я не использую этот подход, потому что после этого у вас не будет полного контроля над маршрутами в приложении. Например, вы не сможете добавить префикс, поместить их в группу или добавить к ним middleware.

Мне нравиться создавать класс со статическим методом route, где я определяю маршруты.

public static function routes()
{

Route::get(
'analytics/page-views-per-days',
[AnalyticsController::class, 'getPageViewsPerDays']
);

Route::get(
'analytics/page-views-per-path',
[AnalyticsController::class, 'getPageViewsPerPaths']
);
}

Таким образом я мог бы легко поместить маршруты пакета, например, в часть /admin в моём приложении.

Фронтенд компоненты

Фронтенд часть состоит из двух компонентов vue: один для диаграммы посетителей, а другой содержит простую таблицу наиболее посещаемых страниц. Для диаграммы я использовал Vue библиотеку chartjs.

<template>
<div>
<div><strong>Visitors: {{chartData.datasets[0].data.reduce((a, b) => a + b, 0)}}</strong></div>
<div>
<LineChartGenerator
:chart-options="chartOptions"
:chart-data="chartData"
:chart-id="chartId"
:dataset-id-key="datasetIdKey"
:plugins="plugins"
:css-classes="cssClasses"
:styles="styles"
:width="width"
:height="height"
/>

</div>
</div>
</template>

<script>

import { Line as LineChartGenerator } from 'vue-chartjs/legacy'
import {
Chart as ChartJS,
Title,
Tooltip,
Legend,
LineElement,
LinearScale,
CategoryScale,
PointElement
} from 'chart.js'

ChartJS.register(
Title,
Tooltip,
Legend,
LineElement,
LinearScale,
CategoryScale,
PointElement
)

export default {
name: 'VisitorsPerDays',
components: { LineChartGenerator },
props: {
'initialData': Object,
'baseUrl': String,
chartId: {
type: String,
default: 'line-chart'
},
datasetIdKey: {
type: String,
default: 'label'
},
width: {
type: Number,
default: 400
},
height: {
type: Number,
default: 400
},
cssClasses: {
default: '',
type: String
},
styles: {
type: Object,
default: () => {}
},
plugins: {
type: Array,
default: () => []
}
},
data() {
return {
chartData: {
labels: Object.keys(this.initialData),
datasets: [
{
label: 'Visitors',
backgroundColor: '#f87979',
data: Object.values(this.initialData)
}
]
},
chartOptions: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
ticks: {
precision: 0
}
}
}
}
}
},
mounted() {
},

methods: {

},
}
</script>

Заключение

Это был довольно забавный и интересный проект, и после месяца его использования и анализа результатов, похоже, он работает нормально. Если вас интересует код или вы хотите попробовать пакет, он размещён на GitHub wdev-rs/laravel-analytics.

Дополнительные материалы

Предыдущая Статья

Value Object /Объект-Значение в Laravel

Следующая Статья

Laravel: Gate и Policy