From 5fbacf7ebd29ebfb3f2f5a5a934a91fafcae6090 Mon Sep 17 00:00:00 2001 From: "maxim.gorbatyuk" Date: Mon, 15 Jun 2026 10:17:24 +0500 Subject: [PATCH 1/2] feat: detect visitor type and report human/crawler/AI traffic to GA4 Classify each request (human / search-crawler / AI agent) from its User-Agent in the SSR server and expose it via res.locals + the X-Visitor-Type header. Report a visitor_hit GA4 event per page render so all three buckets land in one comparable dimension: humans client-side via VisitorAnalyticsService, bots via the GA4 Measurement Protocol (env-gated on GA_MP_API_SECRET, bot User-Agent omitted so GA4's bot filter does not drop the hit). Measurement is scoped to Angular page renders, not static/well-known/sitemap requests. The MP secret is injected through the deploy pipeline (GitHub Actions secret -> ssh-action envs -> docker-compose substitution). --- .github/workflows/deploy.yml | 3 + README.md | 10 ++ docker-compose.frontend.yml | 1 + docs/ga-ssr-visitor-type.md | 133 ++++++++++++++++++ docs/geo.md | 27 +++- docs/interactions.md | 11 ++ src/app/app.component.ts | 3 + src/app/services/visitor-analytics.service.ts | 46 ++++++ src/server.ts | 38 +++++ .../ga-measurement-protocol.util.spec.ts | 51 +++++++ src/server/ga-measurement-protocol.util.ts | 90 ++++++++++++ src/server/visitor-type.util.spec.ts | 78 ++++++++++ src/server/visitor-type.util.ts | 66 +++++++++ 13 files changed, 556 insertions(+), 1 deletion(-) create mode 100644 docs/ga-ssr-visitor-type.md create mode 100644 src/app/services/visitor-analytics.service.ts create mode 100644 src/server/ga-measurement-protocol.util.spec.ts create mode 100644 src/server/ga-measurement-protocol.util.ts create mode 100644 src/server/visitor-type.util.spec.ts create mode 100644 src/server/visitor-type.util.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0b5aae38..db9d3b0c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -41,11 +41,14 @@ jobs: target: "/home/devops/deploy" - name: Execute commands uses: appleboy/ssh-action@v1.0.3 + env: + GA_MP_API_SECRET: ${{ secrets.GA_MP_API_SECRET }} with: host: ${{ secrets.IP }} username: devops key: ${{ secrets.SSH_KEY_PRIVATE }} port: ${{ secrets.PORT }} + envs: GA_MP_API_SECRET script_stop: true script: | cd ~/deploy diff --git a/README.md b/README.md index 033bb737..69f1d8c1 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,16 @@ npm run serve:ssr:interviewer Opens at `http://localhost:4000` +### Runtime environment variables (SSR server) + +| Variable | Default | Purpose | +|---|---|---| +| `PORT` | `4000` | SSR server listening port | +| `GA_MP_API_SECRET` | _(unset)_ | GA4 Measurement Protocol API secret. When set, the SSR server sends a `visitor_hit` event for crawler / AI-agent requests so bot traffic shows in Google Analytics. Unset = no-op (dev/local). See [`docs/ga-ssr-visitor-type.md`](docs/ga-ssr-visitor-type.md). | +| `GA_MEASUREMENT_ID` | `G-6GS9Y7GJD3` | Optional override for the GA4 measurement ID used by the Measurement Protocol send. | + +In production the secret is stored as the `GA_MP_API_SECRET` GitHub Actions secret and forwarded into the deploy SSH session (`appleboy/ssh-action` `envs:` in `.github/workflows/deploy.yml`), where `docker-compose` substitutes it into `${GA_MP_API_SECRET}` in `docker-compose.frontend.yml`. It never enters the repo or image. + ## Docker ```sh diff --git a/docker-compose.frontend.yml b/docker-compose.frontend.yml index ba73976a..2d714b97 100644 --- a/docker-compose.frontend.yml +++ b/docker-compose.frontend.yml @@ -14,5 +14,6 @@ services: - 8080:4000 environment: - NODE_ENV=production + - GA_MP_API_SECRET=${GA_MP_API_SECRET} networks: - interviewer-network diff --git a/docs/ga-ssr-visitor-type.md b/docs/ga-ssr-visitor-type.md new file mode 100644 index 00000000..67128c85 --- /dev/null +++ b/docs/ga-ssr-visitor-type.md @@ -0,0 +1,133 @@ +# Сбор visitor_type в Google Analytics (SSR + Measurement Protocol) + +Инструкция по ручной настройке на стороне Google Analytics (GA4) для фичи, которая +различает посетителей сайта: человек / поисковый краулер / AI-агент и шлёт это в +аналитику. + +## Зачем это нужно + +Классификация посетителя происходит на **сервере** (Express SSR), а обычный GA4 +работает в **браузере** и срабатывает только когда исполняется JavaScript. Боты и +AI-агенты JS не исполняют, поэтому в браузерный GA они не попадают. Чтобы видеть их +статистику, сервер отправляет события напрямую через **GA4 Measurement Protocol**. + +Для этого нужно вручную сделать две вещи в интерфейсе GA4: + +1. получить секретный ключ `GA_MP_API_SECRET`; +2. зарегистрировать специальный параметр `visitor_type`. + +Названия пунктов меню даны на русском, в скобках — на английском. + +--- + +## Задача 1. Получить значение `GA_MP_API_SECRET` + +1. Зайти на **https://analytics.google.com** под своим аккаунтом. +2. Вверху слева убедиться, что выбран нужный **ресурс (property)** — тот, у которого + ID `G-6GS9Y7GJD3` (сайт techinterview.space). +3. Внизу левого меню нажать на шестерёнку **«Администратор» (Admin)**. +4. В столбце **«Ресурс» (Property)** найти раздел **«Сбор и изменение данных» + (Data collection and modification)** → нажать **«Потоки данных» (Data streams)**. +5. В списке нажать на свой **веб-поток (Web)** — там будет домен techinterview.space. +6. Прокрутить панель с деталями потока **вниз до конца** → пункт **«Секреты API + Measurement Protocol» (Measurement Protocol API secrets)**. Нажать на него. +7. Нажать **«Создать» (Create)**. +8. Ввести понятное название (nickname), например `ssr-server`. Нажать **«Создать»**. +9. Появится строка **«Секретное значение» (Secret value)** — длинный набор символов. + **Скопировать его.** Это и есть `GA_MP_API_SECRET`. + +> Это секрет. Не публиковать, не коммитить в гит. Куда вставить на сервере — см. раздел +> «Куда положить секрет на сервере» ниже. + +На той же панели вверху виден **Measurement ID** = `G-6GS9Y7GJD3` — это вторая часть, +она уже известна, отдельно ничего делать не нужно. + +--- + +## Задача 2. Настроить сбор `visitor_type` (специальный параметр) + +Без этого шага GA будет получать значение `visitor_type`, но **не покажет его в +отчётах**. + +1. В **«Администратор» (Admin)**, столбец **«Ресурс» (Property)**. +2. Раздел **«Отображение данных» (Data display)** → **«Специальные определения» + (Custom definitions)**. +3. Вкладка **«Специальные параметры» (Custom dimensions)**. +4. Кнопка **«Создать специальный параметр» (Create custom dimension)** справа вверху. +5. Заполнить поля: + + | Поле | Значение | + |---|---| + | **Название параметра** (Dimension name) | `visitor_type` | + | **Область действия** (Scope) | **«Событие» (Event)** | + | **Описание** (Description) | `human / search-crawler / ai-agent` | + | **Параметр события** (Event parameter) | `visitor_type` (точно так же, маленькими буквами) | + +6. Нажать **«Сохранить» (Save)**. + +> Поле **«Параметр события»** должно быть строго `visitor_type` — так же, как +> отправляет код. Опечатка или другой регистр — данные не свяжутся. + +--- + +## Важные нюансы + +- **Данные не собираются задним числом.** Параметр начнёт собираться только **после** + создания, поэтому создать его нужно до выкатки кода на прод. +- **В обычных отчётах появится через 24–48 часов** — это нормально. +- **Проверить сразу** можно в **«Отчёты» → «В реальном времени» (Realtime)** или в + **«Администратор» → DebugView** — там событие `visitor_hit` видно через пару минут. +- Само событие `visitor_hit` отдельно регистрировать не нужно — GA подхватывает новые + события автоматически. + +--- + +## Куда положить секрет на сервере + +Сервер читает ключ из переменной окружения `GA_MP_API_SECRET` во время работы +(runtime), а не из Angular `environment.ts` (те значения вшиваются в бандл на этапе +сборки и утекли бы в публичный код). + +Фронтенд на проде запускается через `docker-compose.frontend.yml` на Droplet +(DigitalOcean), а деплой идёт через GitHub Actions (`.github/workflows/deploy.yml`). +Секрет прокидывается через пайплайн: + +1. В GitHub репозитории добавить секрет: **Settings → Secrets and variables → + Actions → New repository secret** → имя `GA_MP_API_SECRET`, значение — из Задачи 1. + +2. В `docker-compose.frontend.yml` уже есть ссылка на переменную (без самого секрета): + + ```yaml + environment: + - NODE_ENV=production + - GA_MP_API_SECRET=${GA_MP_API_SECRET} + ``` + +3. В шаге деплоя (`appleboy/ssh-action`) секрет пробрасывается в SSH-сессию через + `envs`, и `docker-compose` подставляет его в `${GA_MP_API_SECRET}`: + + ```yaml + - name: Execute commands + uses: appleboy/ssh-action@v1.0.3 + env: + GA_MP_API_SECRET: ${{ secrets.GA_MP_API_SECRET }} + with: + # ... + envs: GA_MP_API_SECRET + ``` + +Так секрет хранится только в GitHub Secrets (рядом с остальными — `DO_TOKEN`, +`SSH_KEY_PRIVATE`) и не попадает в гит, образ или файл на сервере. Ротация: поменять +секрет в GitHub и передеплоить. + +**Локально:** если переменная не задана — код ничего не отправляет (no-op). Чтобы +проверить отправку локально: `GA_MP_API_SECRET=xxx npm run serve:ssr:interviewer`. + +--- + +## Как смотреть статистику + +Когда данные накопятся: **«Исследования» (Explore)** → отчёт «Свободная форма» +(Free-form) → параметр (dimension) **`visitor_type`**, показатель (metric) +**«Количество событий» (Event count)**, при желании фильтр по событию `visitor_hit`. +Получится сравнение: люди / поисковые краулеры / AI-агенты. diff --git a/docs/geo.md b/docs/geo.md index dfc2753a..00ddbe40 100644 --- a/docs/geo.md +++ b/docs/geo.md @@ -242,6 +242,28 @@ The response also sets: Implementation: `src/server.ts` (the final `app.use(...)` SSR handler). The turndown instance strips `