docs(specs): собственный учёт посетителей + раздел «Посетители» в админке
Точное разделение робот/браузер/человек (отклик страницы, а не запрос), сквозная склейка лендинг→портал через куку на .liderra.ru, воронка и каналы в админке. Метрику и Вебвизор не трогаем. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 сессии с крупными фичами,
|
||||
пересечений по файлам быть не должно.
|
||||
Reference in New Issue
Block a user