0% прочитано

Laravel Analytics без збереження сирого User-Agent: як я визначаю сімейства ботів без зайвих персональних даних

Для аналітики ICanUp мені важливо бачити не тільки Human і Bot, а й розуміти, які відомі сімейства ботів обходять сайт. Але для цього не потрібно роками зберігати повний User-Agent кожного запиту. Показую, як сире значення використовується лише під час класифікації, а в історію потрапляють обмежені нормалізовані ознаки: is_bot, crawler_family, browser family, OS і тип пристрою.

20 вересня 2026 р. 11 хв читанняЛаравель

Після того як у внутрішній аналітиці ICanUp з’явився поділ Human і Bot, виникло наступне питання: а які саме боти створюють цей трафік?

Один загальний показник Bot уже не давав достатньо інформації. Для SEO важливо відрізняти пошукові роботи, для попереднього перегляду посилань корисно бачити окремі сервіси, а після появи AI-краулерів хотілося розуміти і їхню частку.

Найпростішим рішенням було б зберігати повний User-Agent кожного перегляду й розбирати його пізніше.

Я свідомо пішла іншим шляхом.

Для довготривалої аналітики мені потрібна відповідь "до якого відомого класу належить запит", а не повна копія технічного заголовка кожного відвідувача.
Laravel analytics privacy flow where raw User-Agent is classified transiently into Human Bot crawler family browser OS and device dimensions while raw value is discarded
Сирий User-Agent використовується на вході для класифікації. В історію аналітики потрапляють лише обмежені нормалізовані ознаки.

User-Agent корисний на вході, але не обов’язково в історії

Коли HTTP-запит приходить до Laravel, User-Agent містить технічну інформацію про клієнта.

З нього можна спробувати визначити:

  • тип пристрою;
  • сімейство браузера;
  • сімейство операційної системи;
  • чи схожий запит на автоматизований;
  • чи належить бот до відомого сімейства.

Для цього сире значення справді потрібне.

Але воно потрібне під час обробки запиту. Із цього не випливає, що його потрібно зберігати як аналітичний вимір.

Я розділила отримання сигналу і його збереження

Класифікувати, а не зберігати сире значення
HTTP request    |    vraw User-Agent    |    vserver-side classification    |    +--> device_type    +--> browser_family    +--> os_family    +--> is_bot    +--> crawler_family    |    vstore normalized dimensions

Це стало базовим правилом для моєї аналітики: сирі дані запиту можуть бути потрібні для визначення ознак, але в історію повинна потрапляти лише інформація, яка справді потрібна для звітів.

Перший рівень дуже простий: Human або Bot

Основним поділом залишається ознака is_bot.

Базовий поділ трафіку
Human request:is_bot = false Automated request:is_bot = true

Цього достатньо, коли я хочу побачити загальну частку людей і автоматизованого трафіку.

Але для аналізу самих ботів одного boolean-поля вже мало.

crawler_family деталізує тільки ботів

Другий рівень класифікації я використовую тільки для запитів, які вже визначені як Bot.

Умовно результат може виглядати так:

Обмежені сімейства автоматизованого трафіку
is_bot = truecrawler_family = googlebot is_bot = truecrawler_family = bingbot is_bot = truecrawler_family = meta_preview is_bot = truecrawler_family = telegram_preview is_bot = truecrawler_family = ai_crawler is_bot = true
Назви тут показують принцип нормалізованої класифікації. Важливо, що набір значень обмежений і контролюється системою, а не створюється безпосередньо з довільного User-Agent.

Для людини crawler_family взагалі не потрібен

Людина не отримує штучного сімейства бота
Human: is_bot = falsecrawler_family = null

Я не хочу створювати зайві групи на кшталт human усередині crawler_family.

Для цього вже є is_bot. Другий вимір має сенс тільки тоді, коли перший показує Bot.

Чому не варто зберігати повний User-Agent

Повний User-Agent здається привабливим, бо залишає максимум інформації "на майбутнє".

Але для звичайної контентної аналітики це створює кілька проблем.

Сире значення

Нормалізований вимір

Може мати дуже багато унікальних варіантів

Має обмежений набір значень

Містить технічні деталі, які не потрібні звіту

Зберігає лише потрібну класифікацію

Збільшує кількість груп у сховищі

Тримає кількість агрегатів контрольованою

Збільшує поверхню для профілювання

Краще відповідає мінімізації даних

Висока кардинальність особливо небезпечна для агрегатів

Моя аналітика зберігає агреговані дані за днем і вимірами.

Кожне нове унікальне значення виміру може створити окрему комбінацію агрегату.

Обмежений вимір проти довільного рядка
Good: crawler_family- googlebot- bingbot- ai_crawler- other_bot  Bad: raw_user_agent- thousands of arbitrary strings- versions- platform fragments- custom scanners

Якщо зберігати повний User-Agent, сховище починає групуватися за деталями, які я все одно не збираюся показувати в основному звіті.

Невідомий бот не повинен створювати нову категорію сам

Особливо важливий випадок - автоматизований клієнт, сигнатура якого не входить до підтримуваного списку.

Я не хочу брати його назву з User-Agent і створювати нове значення аналітики.

Стабільний запасний варіант
Unknown automated client        |        vis_bot = true        |        vcrawler_family = other_bot

Так навіть дивний або спеціально сформований заголовок не розширює словник аналітики довільним значенням.

Сімейство бота не є доказом його справжньої особи

Є ще одна принципова межа.

User-Agent надсилає сам клієнт. Його можна підмінити.

Якщо запит назвав себе Googlebot, це ще не доводить, що він справді належить Google.

Класифікація не дорівнює перевіреній ідентичності
User-Agent says:Googlebot Analytics classification:looks like Googlebot Security conclusion:NOT automatically verified Googlebot

Для звичайної статистики така класифікація все одно корисна.

Але для розслідування підозрілої активності потрібні інші джерела й окрема перевірка.

Саме тому аналітика й журнали сервера вирішують різні задачі

Коли мені потрібно зрозуміти загальну картину за місяць, достатньо нормалізованих агрегатів.

Коли раптом приходять тисячі підозрілих запитів, уже важливі технічні деталі конкретних HTTP-запитів.

Аналітика

Операційні журнали

Довготривалі тенденції

Розслідування конкретної події

Нормалізовані категорії

Точні технічні запити

Обмежена кількість значень

Може містити детальніший технічний контекст

Контентні та продуктові питання

Діагностика й безпека

Я не хочу перетворювати довготривалу контентну аналітику на архів серверних журналів лише тому, що іноді потрібне розслідування.

Браузер, ОС і пристрій я нормалізую за тим самим принципом

Підхід із ботами не є винятком.

Із технічного заголовка запиту я також можу отримати обмежені значення для інших звітів:

JSON - Приклад нормалізованих ознак
{    "device_type": "mobile",    "browser_family": "Chrome",    "os_family": "Android",    "is_bot": false,    "crawler_family": null}

Для відповіді на питання "скільки переглядів було з Chrome?" мені не потрібна точна версія кожної збірки браузера, якщо вона не має окремої аналітичної цінності.

Raw IP мені теж не потрібна для цієї аналітики

Той самий принцип мінімізації я застосовую до географії.

Для звіту за країнами достатньо нормалізованого дволітерного коду країни, отриманого через довірений інфраструктурний шлях.

Зберігати результат класифікації, а не сирий ідентифікатор
Needed for report: country_code = UA  Not needed in analytics history: raw client IP

Не кожне технічне значення варте нового виміру

Перед додаванням нового поля в аналітику я намагаюся відповісти на три питання.

  • Яке конкретне рішення я зможу прийняти за цим показником?
  • Чи можна зберегти нормалізовану категорію замість сирого значення?
  • Чи не створить новий вимір неконтрольовану кількість комбінацій?

Якщо на перше питання немає відповіді, саме зберігання "про всяк випадок" для мене не є достатньою причиною.

AI crawler теж залишається Bot

Для AI-краулерів я не створюю третій базовий тип відвідувача поруч із Human і Bot.

З погляду основної класифікації це автоматизований трафік.

AI-краулер як деталізація Bot
is_bot = true crawler_family:AI crawler family

Це дозволяє на верхньому рівні порівнювати Human і Bot, а всередині Bot окремо аналізувати пошукові, AI та інші відомі сімейства.

Відсоток crawler family потрібно рахувати від Bot traffic

Якщо я дивлюся блок сімейств ботів, відсоток має відповідати саме популяції ботів.

Правильний знаменник для сімейств ботів
All views: 10,000 Human: 8,000Bot:   2,000 Googlebot: 1,000 Googlebot share of bot traffic:1,000 / 2,000 = 50%

Якщо поділити 1000 на всі 10000 переглядів, вийде інша метрика й інший зміст.

Фільтри повинні працювати разом

crawler_family корисний не тільки як один загальний список.

Я хочу мати можливість застосувати ті самі часові та контентні фільтри, що й для решти аналітики.

Один вимір усередині спільної аналітики
Period:Last 30 days Locale:uk Content:Posts Visitor:Bot Crawler family:Googlebot

Для цього не потрібне окреме сховище ботів. Нормалізоване сімейство стає ще одним підтримуваним виміром у спільному механізмі.

Не потрібно створювати окрему таблицю для кожного нового звіту

Я не хочу мати одну таблицю для Googlebot, другу для AI-краулерів і третю для інших ботів.

Класифікація повинна проходити через загальний етап підготовки аналітичних даних перед збереженням.

Одна точка підготовки вимірів
eligible public request        |        vanalytics enrichment        |        +--> locale        +--> country        +--> device        +--> browser        +--> OS        +--> Human/Bot        +--> crawler family        +--> traffic source        |        vshared aggregate persistence

Помилка класифікації не повинна ламати сторінку

Аналітика є допоміжною функцією публічного сайту.

Якщо дивний або пошкоджений User-Agent не вдалося розпізнати, користувач усе одно повинен отримати сторінку.

Для аналітики краще використати передбачуване запасне значення або пропустити необов’язковий вимір, ніж перетворити помилку класифікатора на HTTP 500.

Класифікатор має бути централізованим

Список підтримуваних сімейств не повинен розростатися випадковими перевірками в контролерах.

Концептуально мені потрібна одна точка, яка перетворює сирий сигнал на обмежений результат.

PHP - Концепція централізованого визначення сімейства
final class CrawlerFamilyResolver{    public function resolve(        string $userAgent,        bool $isBot,    ): ?string {        if (! $isBot) {            return null;        }         return $this->knownFamily($userAgent)            ?? 'other_bot';    }}
Це ілюстрація контракту, а не твердження про точну назву класу чи методів у поточному коді ICanUp.

Не можна перетворювати назву з User-Agent безпосередньо на значення dimension

PHP - Обмежити словник значень
// Bad idea:$crawlerFamily = $parsedBotName; // Better contract:$crawlerFamily = match ($knownSignature) {    'google' => 'googlebot',    'bing' => 'bingbot',    'meta_preview' => 'meta_preview',    'telegram_preview' => 'telegram_preview',    'ai' => 'ai_crawler',    default => 'other_bot',};

У другому варіанті кількість значень визначає застосунок, а не зовнішній клієнт.

Тести повинні перевіряти не тільки відомих ботів

Сценарій

Очікування

Звичайний браузер

is_bot = false, crawler_family = null

Відомий пошуковий бот

Стабільне нормалізоване сімейство

Відомий preview bot

Стабільне нормалізоване сімейство

Підтримуваний AI-краулер

Bot із нормалізованим сімейством

Невідомий автоматизований клієнт

other_bot, а не довільне значення

Порожній або пошкоджений User-Agent

Немає помилки публічної сторінки

Збережена аналітика

Не містить повного сирого User-Agent

Окремо тестую приватність самого сховища

Функціональний тест "Googlebot потрапив у правильну групу" для мене недостатній.

Потрібно також перевірити, що сире значення не просочилося в історичний запис.

Перевірка межі збереження
Persisted analytics may contain: is_botcrawler_familydevice_typebrowser_familyos_familycountry_code  Persisted analytics must not contain: full raw User-Agentraw IParbitrary crawler name

Other bot корисний, але має свою межу

Нормалізований other_bot добре захищає сховище від необмеженої кількості значень.

Його недолік очевидний: якщо саме ця група раптом створює більшість трафіку, з агрегованого звіту не видно конкретного джерела.

Але я не хочу вирішувати цю проблему автоматичним довічним збереженням сирих заголовків кожного запиту.

Для аномалії потрібен окремий діагностичний шлях

Коли агрегована аналітика показує аномалію, наприклад різкий ріст other_bot, це вже сигнал для технічної діагностики.

Тоді можна перейти до серверних журналів або іншого контрольованого операційного джерела й дослідити конкретний період.

Від агрегату до окремого розслідування
Analytics: other_bot spike detected        |        voperational investigation        |        vinspect detailed server evidence        |        vdecide whether classifier should change

Якщо новий бот справді стабільний, корисний для звітів і має надійну сигнатуру, його можна додати до контрольованого класифікатора.

Якщо це одноразовий сканер, аналітика не повинна отримувати нову довготривалу категорію лише через один інцидент.

Мінімізація даних не означає відмову від корисної аналітики

Мені не потрібно вибирати між двома крайнощами: або нічого не знати про трафік, або зберігати кожну технічну деталь запиту.

Між ними є практична модель:

  • отримати сирий сигнал під час запиту;
  • перетворити його на потрібну категорію;
  • зберегти тільки нормалізований результат;
  • агрегувати його для звітів;
  • детальні технічні розслідування залишити окремому операційному шару.

Що я не зберігаю в основній аналітиці

  • Повний сирий User-Agent кожного перегляду.
  • Raw IP як аналітичний вимір.
  • Довільну назву бота, витягнуту з заголовка.
  • Повний URL реферера з довільними параметрами.
  • Постійний профіль неавторизованого відвідувача.
  • Технічні поля лише тому, що браузер або HTTP-запит дозволяє їх отримати.

Мій короткий checklist для privacy-safe bot analytics

  • Визначити, яке бізнесове або технічне питання має вирішувати новий вимір.
  • Використовувати сирий User-Agent тільки як вхідний сигнал для класифікації.
  • Зберігати Human/Bot як окрему базову ознаку.
  • Деталізувати тільки Bot через bounded crawler_family.
  • Для Human залишати crawler_family порожнім.
  • Не створювати нове значення з довільного тексту User-Agent.
  • Невідомих ботів зводити до стабільного fallback.
  • Не трактувати User-Agent як підтверджену ідентичність бота.
  • Тримати основну аналітику окремо від операційних журналів.
  • Не зберігати raw IP лише заради групування трафіку.
  • Тестувати не тільки класифікацію, а й відсутність сирих значень у сховищі.
  • Контролювати кардинальність кожного нового виміру.

Для аналітики мені потрібне не все, що браузер повідомив про себе. Мені потрібна мінімальна нормалізована інформація, яка відповідає на конкретне питання.

Висновок

У ICanUp я хочу бачити, скільки трафіку створюють люди, скільки боти і які відомі сімейства ботів обходять публічний контент.

Для цього не потрібно зберігати повний User-Agent кожного перегляду.

Сирий заголовок використовується як тимчасовий вхід для класифікації. Після цього в історію потрапляють обмежені значення: is_bot, crawler_family, тип пристрою, сімейство браузера та ОС.

Невідомий бот не створює довільну нову категорію, а потрапляє в передбачуваний fallback. Людина взагалі не отримує crawler_family.

А коли потрібне справжнє розслідування конкретного сканера або підозрілої активності, для цього існують операційні журнали, а не довготривалі контентні агрегати.

Мій головний принцип тут простий: зберігати не максимальну кількість доступних даних, а мінімальну кількість даних, достатню для корисної відповіді.