From 74a8f717c9e2d9b299f73f199ea8dcb8de3bdcea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Mon, 13 Jul 2026 18:17:21 +0300 Subject: [PATCH] =?UTF-8?q?docs(specs):=20=D1=81=D0=BE=D0=B1=D1=81=D1=82?= =?UTF-8?q?=D0=B2=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9=20=D1=83=D1=87=D1=91=D1=82?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=81=D0=B5=D1=82=D0=B8=D1=82=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D0=B9=20+=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=20=C2=AB?= =?UTF-8?q?=D0=9F=D0=BE=D1=81=D0=B5=D1=82=D0=B8=D1=82=D0=B5=D0=BB=D0=B8?= =?UTF-8?q?=C2=BB=20=D0=B2=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Точное разделение робот/браузер/человек (отклик страницы, а не запрос), сквозная склейка лендинг→портал через куку на .liderra.ru, воронка и каналы в админке. Метрику и Вебвизор не трогаем. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-07-13-visitors-analytics-design.md | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-13-visitors-analytics-design.md diff --git a/docs/superpowers/specs/2026-07-13-visitors-analytics-design.md b/docs/superpowers/specs/2026-07-13-visitors-analytics-design.md new file mode 100644 index 00000000..e79bf710 --- /dev/null +++ b/docs/superpowers/specs/2026-07-13-visitors-analytics-design.md @@ -0,0 +1,200 @@ +# Собственный учёт посетителей и раздел «Посетители» в админке + +**Дата:** 13.07.2026 +**Статус:** согласовано владельцем (брейншторм 13.07.2026) +**Автор:** Claude Code (сессия «откуда заходят на liderra.ru») + +--- + +## 1. Зачем + +13.07.2026 владелец разослал смс со ссылкой на liderra.ru и одновременно опубликовал +вакансию на hh.ru. Ни Яндекс.Метрика, ни логи nginx не дали ответа «какой канал сработал»: + +- **Метрика врёт в большую сторону.** За 13.07 показала 11 посетителей; разбор логов показал, + что живых людей ~6–7, остальное — сканеры (FlowIQLabsBot с трёх адресов, GPTBot, облачный фон + Tencent/AWS), которых её фильтр не отсёк. +- **Ссылки без меток.** Переход из смс и ручной набор адреса с вакансии одинаково выглядят как + «прямой заход» — разделить их нельзя. +- **Цепочка рвётся между сайтом и порталом.** Лендинг (`liderra.ru`) и кабинет (`lk.liderra.ru`) — + разные хосты; связать «пришёл по смс» → «зарегистрировался» → «вошёл» сейчас невозможно. +- **Внешних переходов почти нет.** За 7 дней жизни лендинга (07–13.07) — 5 переходов из Яндекса, + 0 живых с hh.ru (единственная запись `krasnoyarsk.hh.ru` — робот GPTBot). + +Нужен собственный счётчик, который **точно** отделяет людей от роботов и показывает **сквозной путь** +от первого показа страницы до входа в кабинет. + +## 2. Цель и границы + +**Цель:** админ Лидерры за 10 секунд видит, какой канал приносит не клики, а клиентов, и на каком +шаге люди отваливаются. + +**В объёме:** +- Учёт на лендинге `liderra.ru` и в портале `lk.liderra.ru` (включая страницу регистрации и вход). +- Сквозная склейка пути одного гостя между двумя доменами. +- Точное разделение «робот / браузер / живой человек». +- Раздел «Посетители» в админке: воронка, каналы, список визитов, активность в кабинете. +- Город и оператор посетителя по офлайн-базе, с честной пометкой «через VPN — город недостоверен». +- Имя домена (`$host`) в логах nginx. + +**Вне объёма (не делаем):** +- Яндекс.Метрика и Вебвизор **не трогаем** — работают как есть, параллельно. +- Плашка «сайт использует куки» — владелец решил обойтись без неё (осознанный риск, п. 9). +- Содержимое действий клиента в кабинете (какие лиды/сделки смотрел) — только имена экранов. +- A/B-тесты, тепловые карты, записи экрана. + +## 3. Как отличаем робота от человека + +Роботы не исполняют JavaScript. Поэтому считаем **не запрос к серверу, а отклик страницы**. + +| Ступень | Признак | Что отсекает | +|---|---|---| +| 1. Запрос HTML | строка в логе nginx | ничего — сюда попадают все, включая сканеры | +| 2. **Браузер** | страница выполнила JS и отправила событие `view` | FlowIQLabsBot, GPTBot, облачные сканеры, curl/python | +| 3. **Человек** | было взаимодействие: скролл / касание / движение мыши / клик → событие `alive` | редкие роботы-рендереры | +| 4. **Действие** | нажал CTA, открыл регистрацию, зарегистрировался, вошёл | — | + +Дополнительный фильтр: если IP принадлежит хостингу/дата-центру (по ASN из офлайн-базы) — гость +помечается `is_datacenter=true`. Это и «через VPN», и маскирующийся робот. В отчёте такие видны +отдельно, в основные цифры воронки не входят. + +**В базу пишутся только гости со ступени 2 и выше.** Роботы ступени 1 в базу не попадают вовсе — +их число берём из счётчика nginx-логов и показываем как «отсеяно роботов: N». + +## 4. Метка гостя и склейка пути + +- Первый отклик страницы (`POST /api/track`, событие `view`) получает от сервера куку + **`lid_vid`** — UUID гостя. +- Кука ставится **сервером** (`Set-Cookie`), домен **`.liderra.ru`**, срок 1 год, + `SameSite=Lax; Secure; HttpOnly` — читать её из JS не нужно, браузер шлёт её сам, а закрытая + от JS кука не утечёт через чужой скрипт. +- Домен `.liderra.ru` — родительский и для лендинга, и для `lk.liderra.ru`, поэтому кука видна + обоим: путь «лендинг → кабинет» склеивается сам. +- При успешной регистрации и при входе backend связывает `lid_vid` с `tenant_id` / `user_id`. + С этого момента в отчёте видно: «этот клиент пришёл 13.07 по смс». + +## 5. События + +| Событие | Кто пишет | Когда | +|---|---|---| +| `view` | JS лендинга / портала | страница загрузилась (браузер подтверждён) | +| `alive` | JS | первое взаимодействие (скролл/касание/мышь), один раз за визит | +| `cta_click` | JS лендинга | нажата «Попробовать бесплатно» / «Вход в личный кабинет» | +| `register_open` | JS портала | открыт экран регистрации | +| `register_done` | backend | регистрация успешна (`POST /api/auth/register`) | +| `login_done` | backend | вход успешен (`POST /api/auth/login`) | +| `portal_screen` | JS портала | открыт экран кабинета — пишем только имя маршрута | + +Backend-события пишутся **в коде**, не через JS: их подделать нельзя, и они не теряются. + +## 6. Данные + +### 6.1. Таблица `site_visitors` — гость + +| Поле | Тип | Смысл | +|---|---|---| +| `id` | uuid PK | = значение куки `lid_vid` | +| `first_seen_at` / `last_seen_at` | timestamptz | первый и последний контакт | +| `channel` | text | канал: `sms`, `hh`, `search`, `direct`, `referral`, … (правило — п. 7) | +| `utm_source/medium/campaign/content` | text NULL | метки из ссылки, как пришли | +| `referrer` | text NULL | откуда пришёл (заголовок или `document.referrer`) | +| `landing_path` | text | страница входа | +| `device` | text | `phone` / `desktop` / `tablet` | +| `user_agent` | text | как есть | +| `ip` | inet | **сырой IP** (решение владельца, см. п. 9) | +| `city` / `region` / `asn_org` | text NULL | из офлайн-базы | +| `is_datacenter` | bool | хостинг/VPN → город недостоверен | +| `is_human` | bool | было событие `alive` | +| `tenant_id` / `user_id` | NULL | проставляются при регистрации/входе | + +### 6.2. Таблица `site_events` — шаги + +`id`, `visitor_id` (FK), `event` (перечисление из п. 5), `occurred_at`, `host` +(`liderra.ru` / `lk.liderra.ru`), `path`, `screen` (для `portal_screen`), `meta` jsonb. + +**Партиционирование** — помесячно (как принято в проекте; нарезчик партиций уже есть, +`--behind=1` учтён). **Срок хранения — 180 дней**, старые партиции удаляются джобом. + +**RLS:** обе таблицы — глобальные, не клиентские. Доступ только ролям SaaS-админа; +клиентские роли (`crm_app_user`) не видят ничего. Запись — через приложение. + +**ПДн:** `ip`, `user_agent`, `city` попадают под маскирование `pg_anonymizer` в дампах +(правило добавляется рядом с существующими). + +## 7. Как определяем канал + +Правило по убыванию приоритета: + +1. **Метка в ссылке** (`utm_source`) — самый надёжный. Готовим ссылки: смс — `?utm_source=sms`, + вакансия — `?utm_source=hh`, Телеграм — `?utm_source=tg`. +2. **Откуда пришёл** (`referrer`): домен поисковика → `search`; `hh.ru` → `hh`; прочий сайт → `referral`. +3. Иначе — **`direct`** (прямой заход: набрал руками, из закладок, из мессенджера без метки). + +Пока метки не разосланы, смс-трафик неотличим от прямого — это честно пишем в интерфейсе +подсказкой: «прямые заходы включают переходы из смс и мессенджеров». + +## 8. Раздел «Посетители» в админке + +Новый пункт меню рядом с «Клиентами» и «Лидами». Стиль — как у Командного центра: +переключатель периода (сегодня / 7 / 30 дней), Forest-палитра, Vuetify 3. + +**Сверху — воронка одной полосой** (числа за период, проценты перехода, красным подсвечен шаг +с наибольшим отвалом): + +``` +Открыли сайт 42 → Живых 30 (71%) → Нажали кнопку 8 (27%) → Открыли регистрацию 5 (63%) +→ Зарегистрировались 2 (40%) → Вошли в кабинет 2 (100%) +``` + +**Плашка честности:** «Роботов отсеяно: 137» — чтобы цифрам можно было верить. + +**Таблица каналов:** канал | людей | нажали кнопку | регистраций | входов | конверсия в клиента. + +**Список визитов:** время, канал, телефон/компьютер, город (или «через VPN»), IP, цепочка шагов +(«сайт → кнопка → регистрация»); если гость стал клиентом — ссылка на его карточку. + +**Активность в кабинете:** кто из клиентов заходил за период и какие экраны открывал (топ экранов). + +**Пустое состояние:** «Пока никто не заходил» — без пустых таблиц с нулями. + +## 9. Персональные данные и риски + +- **IP хранится как есть** — решение владельца (13.07.2026). Следствия закрываем: маскирование + в дампах (`pg_anonymizer`), срок хранения 180 дней, доступ только SaaS-админу. +- **Плашки о куках нет** — решение владельца. Формально 152-ФЗ/ФЗ «О рекламе» рекомендуют + уведомление; практика штрафов за отсутствие куки-баннера в РФ пока не сложилась. Возврат к + плашке — отдельная получасовая задача. +- **Задним числом не работает.** Данные пойдут с момента выката; прошлую неделю восстановить нельзя. +- **VPN искажает город** — по словам владельца, до 2/3 гостей через VPN. Поэтому город + показывается с пометкой, а не как факт. + +## 10. Изменения в инфраструктуре + +- **nginx:** в формат лога добавить `$host` (сейчас лендинг от портала отличается только размером + ответа — так отчёты не построишь). Одна строка в `nginx.conf`, откат мгновенный. +- **CSP:** маячок стучится на свой же домен (`/api/track` отдаёт то же приложение) — проверить, + что `connect-src 'self'` разрешён в обоих nginx-блоках (liderra.ru и lk). +- **Гео-база:** офлайн-файл (IP2Location LITE / DB-IP), обновление раз в месяц, никаких обращений + к внешним сервисам (IP наружу не уходит). + +## 11. Тесты + +- **Pest:** запись событий, идемпотентность (двойной `view` не удваивает гостя), связка + `register_done` → `tenant_id`, отсев по ступеням, RLS (клиентская роль не видит таблиц), + правило определения канала (таблица случаев: utm / referrer / прямой). +- **Vitest:** раздел «Посетители» — воронка считает проценты, пустое состояние, таблица каналов. +- **Ручная проверка на проде после выката:** зайти с телефона и с компьютера, убедиться, что + гость появился, шаги записались, робот-запрос `curl` в базу не попал. + +## 12. Порядок работ + +1. Миграция: две таблицы + партиции + RLS + правило анонимизации. +2. Backend: `POST /api/track`, кука гостя, определение канала/устройства/гео, события регистрации и входа. +3. Лендинг: маячок (`view` / `alive` / `cta_click`). +4. Портал: маячок в роутере (`view`, `register_open`, `portal_screen`). +5. Админ-API + раздел «Посетители». +6. nginx `$host`, гео-база, джоб удаления старых партиций. +7. Выкат на боевой (только с разрешения владельца), проверка живым заходом. + +Работа ведётся в отдельной ветке/воркстри — параллельно идут ещё 3 сессии с крупными фичами, +пересечений по файлам быть не должно.