Коли я почала будувати аналітику для ICanUp, мені не хотілося просто вставити ще один сторонній script у браузер і відправляти всю поведінку відвідувачів у зовнішній сервіс.
Мені потрібні були значно конкретніші відповіді: які Posts, Pages, Series, News і Tips переглядають, якою мовою, з яких країн приходить трафік, які дивайси і бравзери використовуються, звідки приходять переходи та як усе це змінюється по днях і годинах.
При цьому аналітика не повинна перетворюватися на архів необроблених запитів.
У результаті вийшла server-side система з двома основними принципами: зберігати агрегати замість нескінченної історії сирих подій і зберігати обмежені розміри замість необмежених значень запиту.
Аналітика починається з визначення реального публічного перегляду
Навіть серверна аналітика може легко рахувати перегляди неправильно, якщо збільшувати лічильник на кожен запит.
Одна публічна сторінка може створювати кілька технічних запитів: попереднє завантаження, часткове оновлення через Inertia або підвантаження наступної порції даних під час нескінченного прокручування. Той самий матеріал може відкриватися і в адмінпанелі.
Усе це - технічні запити, але вони не повинні автоматично зараховуватися як перегляди відвідувачів.
Тому PublicContentViewTracker має окремі умови, за яких запит можна врахувати як перегляд: це має бути GET-запит, не попереднє завантаження, не частковий запит Inertia, не підвантаження даних для нескінченного прокручування і не запит з адмінпанелі.
private function isPublicViewRequest( Request $request,): bool { if ( ! $request->isMethod('GET') || $request->prefetch() ) { return false; } if ( $request->headers->has( Header::PARTIAL_COMPONENT ) || $request->headers->has( Header::PARTIAL_ONLYОдне вимірювання проходить через спільний конвеєр аналітики
Після того як запит визначено як реальний перегляд публічного контенту, трекер не записує дані безпосередньо в окрему таблицю аналітики дописів.
Замість цього він створює універсальний AnalyticsMeasurement, у якому зберігаються метрика, об’єкт аналітики, локаль, додаткові параметри та час події.
Для контенту зараз використовуються дві основні метрики: views і deduplicated_views. Обидві працюють з однаковим набором параметрів, тому на панелі аналітики їх можна порівнювати в тих самих зрізах.
$measurement = new AnalyticsMeasurement( metric: ContentMetricEnum::VIEWS ->metric(), dimensions: AnalyticsDimensions::forSubject( $subject, $locale, $this ->requestDimensions ->forRequest($request), ), occurredAt: CarbonImmutable::now(),Тип контенту теж є одним з аналітичних параметрів
Я не створювала окрему історію для кожного виду public content.
Subject | Entity identity |
|---|---|
Home | Немає окремої entity |
Category | Category ID |
Post | Post ID |
Page | Page ID |
Series | Post Series ID |
News | News ID |
Tip | Tip ID |
Новий content type не потребує нової Analytics architecture. Він підключається як ще один supported subject.
Перед збереженням параметри аналітики доповнюються додатковими даними
Сам URL або необроблений запит не є моделлю аналітики.
Спочатку запит доповнюється потрібними даними, а вже після цього у measurement залишаються нормалізовані параметри, які використовуються для побудови звітів.
Серед них: локаль, код країни, тип пристрою, сімейство браузера, сімейство операційної системи, тип відвідувача, ознака Human/Bot, канал трафіку, джерело трафіку та домен переходу.
Частину параметрів не завжди вдається визначити. У такому разі система не підставляє вигадані значення, а залишає дані невизначеними.
Public page view | vEligibility boundary | vRequest enrichment | +-- locale +-- subject type +-- entity identity +-- country +-- device type +-- browser family +-- OS family +-- viewer typeДля мене аналітика з повагою до приватності означає не збирати якомога більше даних, а спочатку визначити, на які запитання має відповідати система, і зберігати лише той обмежений набір сигналів, який справді для цього потрібен.
Daily aggregates стали основою історії
Для панелі аналітики мені не потрібен список із мільйонів необроблених подій перегляду сторінок, щоб щоразу заново обчислювати загальні показники.
Основна історія зберігається в analytics_daily_aggregates. Кожен запис визначається модулем, метрикою, датою звіту та уніфікованим набором аналітичних параметрів.
Завдяки цьому через одну модель агрегації можна отримувати динаміку за днями, розподіл за локалями й типами контенту, історію конкретного матеріалу та інші потрібні зрізи.
analytics_daily_aggregates modulemetricanalytics_datelocalesubject_typeentity_identity_id_hashadditional_dimensionsdimensions_hashvaluecreated_atupdated_atСам механізм безпечного збільшення агрегованого значення за конкурентних запитів я тут не дублюю. Його окремо розібрано в окремій статті про atomic additive upsert.
Hourly history не замінює daily history
Коли з’явилася потреба бачити, як трафік розподіляється протягом дня, я не стала змінювати вже наявну модель добової аналітики.
Поруч із нею з’явилася analytics_hourly_aggregates. Вона використовує той самий набір метрик і параметрів, але додатково групує дані за годинами.
Добова історія залишається зручною для аналізу тривалих періодів. Погодинна відповідає на інше запитання: коли саме протягом доби відбувалася активність?
analytics_hourly_aggregates modulemetric reporting_datereporting_hour visitor_local_datevisitor_local_hour localesubject_typeentity_identity_id_hash Час формування звітів і місцевий час відвідувача - це дві різні часові осі.
Погодинна таблиця окремо зберігає reporting_date/reporting_hour і visitor_local_date/visitor_local_hour.
Час звітності дає системі єдину й стабільну часову шкалу для побудови звітів. Місцевий час відвідувача можна визначити лише тоді, коли є безпечний з погляду приватності сигнал про зміщення від UTC.
Я не використовую країну як заміну часового поясу. Країна та місцевий час відвідувача - це різні аналітичні параметри з різним рівнем точності.
Основні та додаткові параметри аналітики живуть разом
Не всі аналітичні параметри однаково важливі для структури даних.
Дата, година, локаль, тип об’єкта та його ідентифікатор мають окремі колонки в базі даних. Інші нормалізовані параметри зберігаються в additional_dimensions.
Репозиторій агрегації приховує цю різницю від прикладних запитів. Для коду, який звертається до аналітики, країна, браузер або джерело трафіку залишаються звичайними параметрами аналітики.
Завдяки цьому аналітику можна розширювати новими зрізами без створення окремої таблиці для кожного з них.
return match ( $dimension->value()) { AnalyticsDimensionKey::DATE => $query->granularity() === AnalyticsGranularity::DAILY ? 'analytics_date' : ( $query->timeBasis() === AnalyticsTimeBasis::REPORTING_TIME ? 'reporting_date' : 'visitor_local_date' ), AnalyticsDimensionKey::HOUR => $query->timeBasis()Один рівень запитів працює і з добовими, і з погодинними агрегованими даними.
Панель аналітики не знає, як саме зберігаються дані для кожного звіту.
AnalyticsAggregationQuery описує, яку метрику потрібно отримати, з якою деталізацією та часовою шкалою, які фільтри застосувати і за якими параметрами згрупувати результат. На основі цього репозиторій сам обирає добові або погодинні агреговані дані.
Той самий механізм використовується для підрахунку загальних показників, побудови динаміки та розподілів за типами контенту, локалями, країнами, пристроями, браузерами, операційними системами, каналами й джерелами трафіку, а також доменами переходів.
$builder = $this->connection ->table( $query->granularity() === AnalyticsGranularity::DAILY ? 'analytics_daily_aggregates' : 'analytics_hourly_aggregates' ) ->where( 'module', $query ->metric() ->module() ->value(), ) ->where( 'metric',Чітко визначені аналітичні параметри важливіші за кількість даних.
Загальна історія аналітики не зберігає необроблені дані запиту як довготривалу інформацію про відвідувача.
Наприклад, для аналізу переходів зберігається нормалізований зовнішній домен, а не повний Referer URL разом зі шляхом і параметрами запиту. Країна зберігається як нормалізований код країни лише тоді, коли система має достатньо надійні географічні дані.
Тип пристрою, браузер і операційна система зберігаються у вигляді узагальнених категорій і сімейств, а не як необроблені значення HTTP-заголовків.
Визначення Human/Bot і сімейств пошукових роботів я тут також не повторюю. Цю частину детально розібрано в …
Повнота даних теж є частиною правдивої картини.
Якщо країну вдалося визначити лише для частини переглядів, показувати відсотки так, ніби дані є для всіх переглядів, було б оманливо.
Тому кожен розподіл повертає не лише самі результати, а й інформацію про повноту даних: загальну кількість переглядів, кількість переглядів із визначеним значенням, кількість переглядів без нього та частку покриття.
Те саме стосується аналітичних параметрів, які були додані пізніше. Старі записи можуть цілком коректно залишатися без значень для нового параметра.
Збій у системі аналітики не повинен призводити до помилки на публічній сторінці
Ще одна важлива архітектурна межа полягає в тому, що аналітика виконує допоміжну роль.
Якщо збагачення даних або їх збереження тимчасово не працює, публічний допис чи сторінка все одно мають залишатися доступними.
PublicContentViewTracker перехоплює таку помилку, записує попередження в журнал і повертає керування коду, який його викликав. Окремі тести перевіряють, що коректний публічний контент продовжує відкриватися навіть у разі збою аналітики.
Що я отримала без стороннього трекера
На основі одного набору агрегованих даних адмінпанель аналітики тепер може відповідати на різні запитання:
Скільки було звичайних і унікальних переглядів за вибраний період?
Як змінювалася кількість переглядів за днями?
У які години спостерігалася найбільша активність?
Які типи контенту отримують найбільше переглядів?
Які саме дописи, сторінки, серії, новини та поради читають відвідувачі?
Як перегляди розподіляються за локалями?
Для якої частини трафіку вдалося визначити країну?
Які пристрої, браузери та операційні системи використовують відвідувачі?
Які канали й нормалізовані джерела трафіку приводять відвідувачів?
З яких зовнішніх доменів приходять переходи?
І для кожного такого звіту мені не потрібно окремо зберігати журнал усіх необроблених подій перегляду.
Архітектурний контракт, який залишився
Спочатку визначити, яка подія справді має враховуватися в аналітиці.
Не змішувати технічні запити з реальними переглядами публічного контенту.
Використовувати універсальне вимірювання замість окремих лічильників для кожного типу контенту.
Використовувати однаковий набір аналітичних параметрів для звичайних і унікальних переглядів.
Зберігати довготривалу історію у вигляді агрегованих даних.
Добова та погодинна деталізація мають відповідати на різні запитання.
Не створювати штучні погодинні дані або значення нових параметрів для старої історії.
Зберігати обмежений набір нормалізованих параметрів замість необроблених значень запиту.
Показувати повноту даних, якщо певний параметр вдалося визначити не для всіх переглядів.
Збій аналітики не повинен заважати роботі публічного контенту.
Власна аналітика стала справді корисною не тоді, коли я почала збирати більше даних, а тоді, коли для кожного збереженого показника стало зрозуміло, навіщо він потрібен і де проходить межа допустимого з погляду приватності.
Висновок
У ICanUp аналітика поступово виросла з простого лічильника переглядів у власну архітектуру звітності.
Публічний запит спочатку проходить перевірку, чи взагалі його можна враховувати як перегляд. Потім він перетворюється на AnalyticsMeasurement, доповнюється обмеженим набором нормалізованих параметрів і потрапляє до добової та погодинної історії агрегованих даних. Після цього один рівень запитів використовує ту саму історію для загальних показників, динаміки, аналізу контенту, аудиторії та джерел трафіку.
Найважливіше для мене те, чого в цій архітектурі немає: немає потреби зберігати кожен необроблений запит лише тому, що колись він може знадобитися, і немає потреби передавати всю поведінку аудиторії сторонньому трекеру в браузері.
Приватність тут не є додатковою можливістю панелі аналітики. Вона починається з самої моделі даних і з рішення про те, які дані взагалі варто зберігати.



