Кто здесь был? Веб-аналитика на Elixir без аккаунтов и журнала посещений
После запуска небольшого сайта обычно хочется ответить на несколько вопросов: сюда вообще кто-нибудь приходит? Откуда? Что люди открывают? Доходят ли до тарифов и нажимают ли на регистрацию?

Для этого необязательно знать, что посетитель №4381 уже заходил во вторник, а за неделю до этого читал другую страницу. Во многих случаях достаточно счётчиков и распределений: сколько посещений, какие источники, какие страницы и действия популярнее.
Вокруг этой задачи я сделал Who Was There: небольшую систему веб-аналитики, которая обрабатывает события, обновляет агрегаты и не сохраняет исходный журнал посещений. Без cookies, регистрации и постоянных профилей посетителей. Исходники доступны на GitHub, сервис можно развернуть у себя.
Расскажу, как это устроено и какими возможностями пришлось пожертвовать.
Подключение начинается со скрипта, а не с аккаунта
Создать сайт можно кнопкой на главной или одним запросом:
curl -s 'https://whowasthere.fyi/new?host=example.com'
В ответ приходят код счётчика и две секретные ссылки: на статистику и на управление оплатой. Код подключения выглядит так:
<script src="https://whowasthere.fyi/w.js" data-w="SITE_KEY" defer></script>
Здесь SITE_KEY — ключ приёма событий из ответа сервера. Он находится в HTML и не открывает статистику. У дашборда отдельный подписанный токен, которого в коде счётчика нет.
Получается доступ по принципу «кто владеет ссылкой, тот имеет право». Не нужно придумывать пароль и подтверждать почту, но ссылку придётся сохранить: обычного восстановления доступа через «забыли пароль?» здесь тоже нет. Переслать ссылку на дашборд — значит дать доступ к статистике.
Параметр host заранее привязывает счётчик к домену. Если его не указать, привязка происходит по первому посещению. Это помогает отсеивать события с другого сайта, хотя полноценной защитой от намеренной накрутки такую проверку считать нельзя: отправитель собственного HTTP-запроса может подделать заголовки.
Что происходит с посещением
Стек — Elixir, Phoenix, LiveView, Bandit и SQLite. Текущие данные живут в ETS: встроенных таблицах в памяти Erlang VM.
После проверки ключа и фильтрации известных ботов событие попадает в агрегатор. Он обновляет счётчики, оценку уникальных посетителей, распределения по страницам и источникам, а также короткоживущее состояние сессии.
Каждые 15 секунд изменившиеся данные сбрасываются в SQLite. Дашборд берёт сегодняшнюю статистику из памяти, прошлые дни — из базы. Отдельный сервер СУБД для этого не нужен.
В постоянном хранилище аналитики остаются дневные и почасовые счётчики и агрегированные разрезы. IP-адреса, исходные User-Agent и отдельные события туда не записываются.
Такое устройство задаёт ограничения заранее. Если через месяц захочется придумать новый отчёт и пересчитать старые события, пересчитывать будет нечего. При аварийном завершении процесса можно потерять изменения после последнего сохранения; активные сессии тоже не являются долговечным хранилищем. Для простого счётчика это допустимый компромисс, для учёта оплачиваемых операций — уже другая задача.
Логи прокси или CDN при собственном развёртывании нужно рассматривать отдельно: устройство базы аналитики само по себе не меняет настройки инфраструктуры.
Как считать уникальных без постоянного идентификатора
Здесь используется краткоживущий идентификатор: хеш от IP, User-Agent, идентификатора сайта, текущей даты и случайной соли. Соль меняется при переходе на новый день UTC. Она также создаётся заново при запуске агрегатора.
Это означает, что IP и User-Agent участвуют в обработке запроса, хотя и не сохраняются как поля посещения. Обещать, что сервис «вообще не обрабатывает IP», было бы неверно.
Хеши поступают в HyperLogLog. Вместо списка посетителей алгоритм хранит компактную структуру и оценивает число различных значений. В используемой конфигурации она занимает около 4 КБ; статистическая погрешность оценки — примерно 1,6%.
Это погрешность алгоритма, а не гарантия точности подсчёта людей. Два посетителя с одинаковыми IP и User-Agent могут склеиться. Смена сети может, наоборот, разделить одного посетителя. Перезапуск с новой солью тоже способен повторно учесть уже приходившего человека.
Ещё одно следствие: уникальные за период здесь — сумма дневных оценок. Если один человек приходил каждый день неделю, он может внести семь единиц в недельную сумму. Это не семь разных людей и не дедуплицированный недельный охват.
Долговременное узнавание посетителя в этой модели отсутствует, поэтому отчёт о возвратах через месяц из таких данных не получить.
Что остаётся в дашборде
Для повседневного наблюдения за небольшим сайтом информации всё равно достаточно:
- просмотры, сессии и дневные оценки уникальных;
- источники переходов и UTM-метки;
- популярные страницы, точки входа и выхода;
- устройства, браузеры, операционные системы;
- клики и пользовательские события;
- переходы между страницами и короткие последовательности действий.
Сессия истекает после 30 минут неактивности. Пока она открыта, в памяти хранится цепочка длиной до восьми шагов, включая клики. После завершения увеличивается счётчик соответствующей последовательности. Так можно увидеть, насколько часто встречается маршрут «главная → тарифы → клик по регистрации», без постоянного хранения карточки каждого посетителя.
У разрезов тоже есть предел: до 150 разных ключей на вид разреза, в интерфейсе — топ-20. Когда лимит заполнен, новые значения не добавляются. Поэтому сайт с огромным количеством разных URL не получит здесь полный каталог посещённых страниц.
Некоторые метрики стоит читать буквально. Отказ — сессия с одним просмотром: человек мог внимательно прочитать статью и всё равно попасть в эту категорию. Страна берётся из заголовков CDN, а при их отсутствии — из региональной части языка браузера. Последний вариант является лишь предположением о стране.
Немного о браузерной части
Счётчик учитывает обычные загрузки и изменения маршрута внутри SPA: History API, переходы назад и вперёд, hash-маршруты. Отдельно обрабатывается возвращение страницы из back-forward cache.
Клики отслеживаются делегированным обработчиком. Для понятного названия действия можно задать атрибут:
<button data-wwt="Start trial">Попробовать</button>
А завершение действия отправить явно:
window.wwt?.('signup')
Такое событие не увеличивает число просмотров. Вызывать его стоит после успешной регистрации, если измеряется именно регистрация, а не нажатие кнопки.
Для отправки используется sendBeacon, резервный вариант — fetch с keepalive. Пока вкладка видима, счётчик отправляет heartbeat. Без JavaScript доступен пиксель, но только для подсчёта просмотров.
Пути, названия событий и подписи кликов становятся ключами агрегатов. Поэтому отсутствие сырых логов не делает безопасной передачу туда любых строк: email, токен восстановления или имя пользователя не должны становиться названием события. Для динамических кнопок полезны фиксированные метки data-wwt.
У себя или как сервис
Код опубликован под AGPLv3. Для локального знакомства после получения исходников и установки окружения:
mix setup
mix phx.server
Приложение откроется на localhost:4000. В репозитории есть Dockerfile, Compose и инструкция по развёртыванию. Для собственных сайтов оператор может выдать бесплатный профиль через серверную консоль; подключать приём платежей для этого не требуется.
Размещённая версия стоит $30 в год: неограниченное число сайтов с общим лимитом 500 тысяч просмотров в месяц. Есть семь пробных дней. Оплата — картой или USDC в Solana. Каждый дополнительный пакет за $30 увеличивает месячный лимит на 500 тысяч просмотров. Email необязателен и нужен для уведомлений о сроках и расходовании лимита. Условия размещённой версии.
Who Was There рассчитан на ситуации, когда нужно понимать, как живёт сайт: читают ли документацию, откуда приходят на лендинг, открывают ли тарифы, нажимают ли нужную кнопку. Сквозная идентификация, история конкретного пользователя и произвольные исследования задним числом требуют другой модели данных.
Мне интересно обсудить именно эту границу: каких агрегированных показателей вам хватает для небольших проектов, а ради какого отчёта вы действительно готовы хранить отдельные посещения?
Войти, чтобы оценить материал
Комментарии
Войти, чтобы оставить комментарий