Laravel аналитика. Зачем и как я сделал свой пакет
Для этого было несколько причин:
- В последнее время Google Analytics стала довольно сложным и медленным. Особенно с введением нового Google Analytics 4 он стал сложнее, и я понял, что не использую даже 0,1% его возможностей. Этот блог и другие веб-сайты, которые я разрабатывал как сторонние проекты, нуждаются только в простых вещах, таких как количество посетителей в определённый период и просмотры страниц для наиболее посещаемых страниц. Вот и всё!
- Я хотел максимально избавиться от сторонних файлов cookie.
- Сторонние инструменты аналитики в основном блокируются блокировщиками рекламы, поэтому я вижу меньшие цифры, а не количество реальных посетителей.
Требования
- Это должен быть Laravel, так как я хочу использовать его в нескольких проектах.
- Будь проще, только базовый функционал.
- Отслеживание посещений страниц по
uri
, а также по релевантным идентификаторам моделей, если применимо (например,id
сообщения в блоге илиid
продукта). - Хранение
UserAgents
для возможного дальнейшего анализа устройств посетителей (настольных и мобильных) и для фильтрации трафика ботов. - Хранение
IP
адреса для запланированной функции: сегментация пользователей по странам и городам. Внутреннее
решение, отслеживание данных в собственной базе данных приложения.- Только бэкенд функции для отслеживания, без фронтенд отслеживания.
- Создание диаграммы посетителей за последние 28 дней и наиболее посещаемых страниц за тот же период.
- Отслеживание посещений страниц по
- Создание MVP и отложить все дополнительные функции, такие как:
- Агрегировать данные в отдельные таблицы вместо того, чтобы запрашивать таблицу
page_view
(я создам её, когда запросы станут медленными). - Добавить базу данных
geoip
и сохранить страну и город пользователя на основе его IP-адреса. - Добавить возможность изменить период времени отображаемый на графиках.
- Агрегировать данные в отдельные таблицы вместо того, чтобы запрашивать таблицу
База данных
Как я упоминал ранее, цель состояла в том, чтобы всё было очень просто, поэтому база данных состоит только из одной таблицы с именем 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.