Сайт: посетители по логам nginx

Дашборд «Сайт: посетители» считает посещаемость сайта по access-логу nginx (или apache): люди, просмотры, источники, реклама, воронка до заявки, битые ссылки. Счётчика на страницах не нужно, данные не уходят третьим лицам и хранятся у вас.

Что нужно

Необязательно, но полезно — дописать в конец log_format имя сайта и время ответа. Тогда можно разделить несколько сайтов одного nginx и увидеть медленные страницы:

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                '$status $body_bytes_sent "$http_referer" "$http_user_agent" '
                'rt=$request_time vhost=$host';
access_log /var/log/nginx/access.log main;

Поля после стандартной строки агент 1.6.1+ разбирает в любом порядке.

Как считаются цифры

С Яндекс Метрикой цифры совпадут по порядку, но не точно: офис за NAT — это один IP, телефон в мобильной сети может сменить IP посреди визита. Сайт-одностраничник (SPA) виден только по первой загрузке.

Настройка дашборда

Все параметры — переменные вверху дашборда. Значения по умолчанию подходят большинству сайтов; поменяйте нужные и сохраните дашборд или ссылку на него.

ПеременнаяПо умолчаниюЧто задаёт
Хост с nginx сайтапервый в спискесервер, где работает nginx сайта
Сайт (регэксп)пусто — все сайтыимя сайта из поля vhost=, точку писать как [.]: ^(www[.])?example[.]ru$
Источник логовnginx в Docker и файлы *access*сузить: container:nginx или file:access.log
Что считать страницей/, пути на /, .html, .php и одно слово без точки (/about)регэксп пути страницы — не картинки и не API
Адрес формы/feedback, /contact, /order, /lead, /callback, /form, /send (и то же под /api/)куда форма отправляет заявку
Шаг 2 и шаг 3 воронкидемо или цены; контактыстраницы промежуточных шагов и их подписи на панели «Воронка»

Что на дашборде

Свёрнутые разделы не запрашивают данные, пока их не раскроют, — дашборд открывается быстро даже на большом сайте.

Время на сайте: маячок страниц

По одному access-логу нельзя понять, сколько человек читал страницу. Для этого на страницы добавляется маленький скрипт: он считает активное время (вкладка видна и человек что-то делал за последнюю минуту) и раз в 15 секунд сообщает его запросом /e?…. Запрос попадает в тот же access-лог — дашборд считает по нему визиты, медиану и среднее время, отказы (визит из одной страницы короче 15 секунд) и глубину просмотра, как в отчёте Метрики «Время на сайте».

1. nginx — ответить на маячок пустым ответом (в server сайта):

location = /e {
    expires -1;
    return 204;      # запрос пишется в access-лог сайта, его и читает агент
}

2. Скрипт — перед </body> на всех страницах:

<script>
(function () {
  if (navigator.webdriver) return;
  var now = function () { return Date.now(); };
  var rnd = function () { return Math.random().toString(36).slice(2, 10); };
  var visit;
  try {
    var s = JSON.parse(sessionStorage.getItem('vt_visit') || 'null');
    visit = s && s.id && now() - s.at < 1800000 ? s.id : rnd();
  } catch (e) { visit = rnd(); }
  function touch() { try { sessionStorage.setItem('vt_visit', JSON.stringify({ id: visit, at: now() })); } catch (e) {} }
  touch();
  var view = rnd(), path = location.pathname, active = 0, lastInput = now(), tick = now(), sent = -1, closed = false;
  function engaged() { return document.visibilityState === 'visible' && now() - lastInput < 60000; }
  function account() { var t = now(); if (engaged()) active += t - tick; tick = t; }
  function send(ev) {
    var a = Math.round(active / 1000);
    if (ev === 'ping' && a === sent) return;
    sent = a;
    var url = '/e?v=' + visit + '&p=' + view + '&e=' + ev + '&a=' + a + '&u=' + encodeURIComponent(path);
    try {
      if (!(navigator.sendBeacon && navigator.sendBeacon(url)))
        fetch(url, { method: 'POST', keepalive: true, credentials: 'omit' })['catch'](function () {});
    } catch (e) {}
  }
  ['pointermove', 'pointerdown', 'keydown', 'scroll', 'wheel', 'touchstart'].forEach(function (t) {
    addEventListener(t, function () { if (!engaged()) tick = now(); lastInput = now(); touch(); }, { passive: true });
  });
  send('load');
  var timer = setInterval(function () {
    account();
    if (active > 1800000) { clearInterval(timer); return; }   // 30 минут на странице — дальше это забытая вкладка
    if (engaged()) send('ping');
  }, 15000);
  document.addEventListener('visibilitychange', function () {
    if (document.visibilityState === 'hidden') { account(); send('hide'); } else { tick = now(); lastInput = now(); }
  });
  addEventListener('pagehide', function () { if (closed) return; closed = true; account(); send('leave'); });
})();
</script>
Маячок не ставит cookie и не собирает персональные данные: в запросе только случайный номер визита (живёт в sessionStorage вкладки, обнуляется после 30 минут бездействия), случайный номер просмотра, адрес страницы и число секунд. Упомяните его в политике конфиденциальности сайта рядом с серверными логами. Если на сайте строгая CSP, разрешите скрипт так же, как остальные свои скрипты.