Мера: о новой заявке узнаёт ответственный, а на дашборде видно, сколько оценок доходит до заявки #3551

Merged
bot-backend merged 1 commit from fix/lead-funnel into main 2026-09-17 09:21:48 +00:00
Collaborator

Что было

В #1971 было четыре пункта DoD. Два уже сделали раньше: POST /api/v1/trade-in/lead с сохранением и доказательством согласия (#2376, #2497, #2511) и форма вместо mailto (#2377). Два оставались несделанными:

  1. Ответственному не приходит уведомление. В docstring lead.py было прямо написано «Notification (Telegram/email) — вне scope», а колонку notified_at оставили на будущее. На проде 17.09: в trade_in_leads 4 строки, notified_at не заполнен ни у одной, последняя заявка 12.07, за 30 дней ни одной. За те же 30 дней посчитано 189 оценок. Все от вошедших пользователей, 7 разных аккаунтов.
  2. Конверсию «оценка → заявка» нигде не считают. Счётчики mera_estimates_total и mera_leads_total есть (#3471), но доли заявок от оценок нет ни на одной панели.

Почему этого не сделали сразу: в разборе 04.07 записали, что интеграции с Telegram в коде нет. Сейчас это уже неверно. Появились общий клиент app/services/tgbot/shared.py:get_telegram_client и веб-чат поддержки (api/v1/support.py), который пишет в support-топик. На проде у tradein-backend заданы TELEGRAM_BOT_TOKEN, TELEGRAM_SUPPORT_CHAT_ID и TELEGRAM_SUPPORT_TOPIC_ID=2 (проверено через docker inspect 17.09).

Что сделано

Уведомление (tradein-mvp/backend/app/api/v1/lead.py):

  • Лид сохраняется и коммитится как раньше. Потом, если бот настроен (переиспользован support._bot_configured), в BackgroundTasks ставится _notify_new_lead. Ответ клиенту не ждёт Telegram.
  • Задача шлёт сообщение в support-топик через общий клиент. Ретраи обычные воркерные, без интерактивного лимита, как в app/tasks/glitchtip_alert_retry.py. После успешной отправки выполняется UPDATE trade_in_leads SET notified_at = now() в отдельной сессии.
  • Если Telegram не ответил, клиент всё равно получает 200 и лид остаётся в базе. Ошибка пишется в лог, notified_at остаётся пустым, и по нему видно, что заявка не дошла.
  • Телефона в сообщении нет. Механизм удаления ПДн (data_erasure.py) не стирает копию в Telegram, поэтому в сообщение идут только id заявки, имя пользователя (его уже показывает зеркало поддержки) и id оценки. Телефон оператор смотрит в trade_in_leads по id.

Конверсия (ops/metrics/grafana/dashboards/product.json): новая панель «Конверсия оценка → заявка, скользящие 7 суток» с выражением
sum(increase(mera_leads_total[7d])) / sum(increase(mera_estimates_total{outcome="ok"}[7d])).
В знаменателе только outcome="ok": на /trade-in/v2 LeadForm рендерится только при hasEstimate = … && !insufficient.

Тесты

  • tests/test_trade_in_lead.py: добавлено 3 теста, всего в файле 17 passed.
    • Бот настроен: сообщение уходит в chat_id/message_thread_id из настроек, в тексте есть id лида и пользователь. Цифр телефона 9123456789 нет даже среди цифр текста. UPDATE ушёл с {"id": <лид>}, выполнены commit и close.
    • send_message бросает TelegramError: ответ 200 с тем же id, лид закоммичен, UPDATE не выполнялся.
    • telegram_support_chat_id=0: отправки нет.
  • Весь сьют МЕРА backend: DATABASE_URL=… uv run python -m pytest tests/ -q -p no:cacheprovider: 6228 passed, 42 skipped, rc=0.
  • uv run ruff check app tests: All checks passed. ruff format --check по изменённым файлам: already formatted.
  • Панель проверена на значениях с боевого Prometheus (только чтение, /api/v1/query). Скрипт берёт expr из JSON и отдельно считает числитель и знаменатель: panel value: [0.0] numerator: [0.0] denominator: [91.00765993265993], то есть 91 успешная оценка за неделю и 0 заявок.

Фальсификация

Каждый раз ломал копию исходника, запускал проверку, восстанавливал файл и сверял diff -q.

Поломка Результат
return перед UPDATE (notified_at не пишется) FAILED test_lead_notifies_topic_without_phone_and_sets_notified_at: AttributeError: 'NoneType' object has no attribute 'args', rc=1
телефон попадает в текст AssertionError: assert '9123456789' not in '00000000000...179123456789', rc=1
отказ Telegram не перехватывается FAILED test_lead_telegram_failure_keeps_200_and_notified_at_empty: app.services.tgbot.client.TelegramError: boom, rc=1
снята проверка «бот настроен» FAILED test_lead_bot_not_configured_skips_notification: AssertionError: Expected mock to not have been awaited. Awaited 1 times., rc=1
в панели outcome="OK" AssertionError: панель пустая при живых оценках — выражение не матчит ряды, rc=1
в панели mera_lead_total то же, rc=1

Деплой

  • Backend-образ Меры пересобирается, tradein-backend и tradein-scraper будут пересозданы. Деплой сам ждёт, пока scrape_runs.status='running' станет 0 (до 5 мин). Перед мержем стоит проверить, что прямо сейчас не идёт долгий обход. Миграций нет: notified_at на проде уже есть.
  • Панель доедет через deploy-metrics.yml (Beget). Каталог dashboards смонтирован целиком, Grafana перечитывает его раз в 30 с.

Приёмка на проде (после деплоя, до 24.09.2026)

  1. Первая же заявка после деплоя (или тестовая от владельца) появляется в support-топике (topic 2) сообщением «Новая заявка на трейд-ин» с id. Проверка: SELECT id, notified_at FROM trade_in_leads ORDER BY created_at DESC LIMIT 1 показывает заполненный notified_at. Если заявка есть, а notified_at пуст, ищем trade_in_lead: уведомление не доставлено в docker logs tradein-backend.
  2. В Grafana, дашборд «Продуктовые метрики», есть панель «Конверсия оценка → заявка». При живых оценках она показывает число (сейчас 0 %), а не «No data».

Что не сделано

  • Если процесс перезапустится между ответом клиенту и отправкой, уведомление потеряется (BackgroundTasks не переживают рестарт). Лид при этом сохранится с пустым notified_at. Повторная досылка неуведомлённых заявок не сделана: при 4 заявках за 2 месяца это лишнее.
  • Сообщения идут в тот же топик, что и поддержка. Если нужен отдельный топик для заявок, это одна настройка, но решать это владельцу.
  • Конверсия считается по событиям, а не по людям: одна заявка после трёх пересчётов того же адреса даёт 33 %.

Closes #1971

🤖 Generated with Claude Code

## Что было В #1971 было четыре пункта DoD. Два уже сделали раньше: `POST /api/v1/trade-in/lead` с сохранением и доказательством согласия (#2376, #2497, #2511) и форма вместо mailto (#2377). Два оставались несделанными: 1. **Ответственному не приходит уведомление.** В docstring `lead.py` было прямо написано «Notification (Telegram/email) — вне scope», а колонку `notified_at` оставили на будущее. На проде 17.09: в `trade_in_leads` 4 строки, `notified_at` не заполнен ни у одной, последняя заявка 12.07, за 30 дней ни одной. За те же 30 дней посчитано 189 оценок. Все от вошедших пользователей, 7 разных аккаунтов. 2. **Конверсию «оценка → заявка» нигде не считают.** Счётчики `mera_estimates_total` и `mera_leads_total` есть (#3471), но доли заявок от оценок нет ни на одной панели. Почему этого не сделали сразу: в разборе 04.07 записали, что интеграции с Telegram в коде нет. Сейчас это уже неверно. Появились общий клиент `app/services/tgbot/shared.py:get_telegram_client` и веб-чат поддержки (`api/v1/support.py`), который пишет в support-топик. На проде у `tradein-backend` заданы `TELEGRAM_BOT_TOKEN`, `TELEGRAM_SUPPORT_CHAT_ID` и `TELEGRAM_SUPPORT_TOPIC_ID=2` (проверено через `docker inspect` 17.09). ## Что сделано **Уведомление** (`tradein-mvp/backend/app/api/v1/lead.py`): - Лид сохраняется и коммитится как раньше. Потом, если бот настроен (переиспользован `support._bot_configured`), в `BackgroundTasks` ставится `_notify_new_lead`. Ответ клиенту не ждёт Telegram. - Задача шлёт сообщение в support-топик через общий клиент. Ретраи обычные воркерные, без интерактивного лимита, как в `app/tasks/glitchtip_alert_retry.py`. После успешной отправки выполняется `UPDATE trade_in_leads SET notified_at = now()` в отдельной сессии. - Если Telegram не ответил, клиент всё равно получает 200 и лид остаётся в базе. Ошибка пишется в лог, `notified_at` остаётся пустым, и по нему видно, что заявка не дошла. - **Телефона в сообщении нет.** Механизм удаления ПДн (`data_erasure.py`) не стирает копию в Telegram, поэтому в сообщение идут только id заявки, имя пользователя (его уже показывает зеркало поддержки) и id оценки. Телефон оператор смотрит в `trade_in_leads` по id. **Конверсия** (`ops/metrics/grafana/dashboards/product.json`): новая панель «Конверсия оценка → заявка, скользящие 7 суток» с выражением `sum(increase(mera_leads_total[7d])) / sum(increase(mera_estimates_total{outcome="ok"}[7d]))`. В знаменателе только `outcome="ok"`: на `/trade-in/v2` `LeadForm` рендерится только при `hasEstimate = … && !insufficient`. ## Тесты - `tests/test_trade_in_lead.py`: добавлено 3 теста, всего в файле 17 passed. - Бот настроен: сообщение уходит в `chat_id`/`message_thread_id` из настроек, в тексте есть id лида и пользователь. Цифр телефона `9123456789` нет даже среди цифр текста. UPDATE ушёл с `{"id": <лид>}`, выполнены commit и close. - `send_message` бросает `TelegramError`: ответ 200 с тем же id, лид закоммичен, UPDATE не выполнялся. - `telegram_support_chat_id=0`: отправки нет. - Весь сьют МЕРА backend: `DATABASE_URL=… uv run python -m pytest tests/ -q -p no:cacheprovider`: **6228 passed, 42 skipped, rc=0**. - `uv run ruff check app tests`: All checks passed. `ruff format --check` по изменённым файлам: already formatted. - Панель проверена на значениях с боевого Prometheus (только чтение, `/api/v1/query`). Скрипт берёт `expr` из JSON и отдельно считает числитель и знаменатель: `panel value: [0.0] numerator: [0.0] denominator: [91.00765993265993]`, то есть 91 успешная оценка за неделю и 0 заявок. ## Фальсификация Каждый раз ломал копию исходника, запускал проверку, восстанавливал файл и сверял `diff -q`. | Поломка | Результат | |---|---| | `return` перед UPDATE (`notified_at` не пишется) | `FAILED test_lead_notifies_topic_without_phone_and_sets_notified_at`: `AttributeError: 'NoneType' object has no attribute 'args'`, rc=1 | | телефон попадает в текст | `AssertionError: assert '9123456789' not in '00000000000...179123456789'`, rc=1 | | отказ Telegram не перехватывается | `FAILED test_lead_telegram_failure_keeps_200_and_notified_at_empty`: `app.services.tgbot.client.TelegramError: boom`, rc=1 | | снята проверка «бот настроен» | `FAILED test_lead_bot_not_configured_skips_notification`: `AssertionError: Expected mock to not have been awaited. Awaited 1 times.`, rc=1 | | в панели `outcome="OK"` | `AssertionError: панель пустая при живых оценках — выражение не матчит ряды`, rc=1 | | в панели `mera_lead_total` | то же, rc=1 | ## Деплой - Backend-образ Меры пересобирается, `tradein-backend` и `tradein-scraper` будут пересозданы. Деплой сам ждёт, пока `scrape_runs.status='running'` станет 0 (до 5 мин). Перед мержем стоит проверить, что прямо сейчас не идёт долгий обход. Миграций нет: `notified_at` на проде уже есть. - Панель доедет через `deploy-metrics.yml` (Beget). Каталог `dashboards` смонтирован целиком, Grafana перечитывает его раз в 30 с. ## Приёмка на проде (после деплоя, до 24.09.2026) 1. Первая же заявка после деплоя (или тестовая от владельца) появляется в support-топике (topic 2) сообщением «Новая заявка на трейд-ин» с id. Проверка: `SELECT id, notified_at FROM trade_in_leads ORDER BY created_at DESC LIMIT 1` показывает заполненный `notified_at`. Если заявка есть, а `notified_at` пуст, ищем `trade_in_lead: уведомление не доставлено` в `docker logs tradein-backend`. 2. В Grafana, дашборд «Продуктовые метрики», есть панель «Конверсия оценка → заявка». При живых оценках она показывает число (сейчас 0 %), а не «No data». ## Что не сделано - Если процесс перезапустится между ответом клиенту и отправкой, уведомление потеряется (`BackgroundTasks` не переживают рестарт). Лид при этом сохранится с пустым `notified_at`. Повторная досылка неуведомлённых заявок не сделана: при 4 заявках за 2 месяца это лишнее. - Сообщения идут в тот же топик, что и поддержка. Если нужен отдельный топик для заявок, это одна настройка, но решать это владельцу. - Конверсия считается по событиям, а не по людям: одна заявка после трёх пересчётов того же адреса даёт 33 %. Closes #1971 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 1 commit 2026-09-17 07:30:51 +00:00
fix(mera/lead): о новой заявке узнаёт ответственный, конверсия «оценка → заявка» на дашборде
All checks were successful
CI Trade-In / changes (pull_request) Successful in 17s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 21s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 3m53s
CI Trade-In / backend-tests (pull_request) Successful in 6m56s
CI / backend-tests (pull_request) Successful in 8m25s
56f0cdbdf7
#1971, два невыполненных пункта DoD.

1. Уведомления не было. Заявка с результата оценки только ложилась в
trade_in_leads (docstring прямо называл уведомление «вне scope»). На проде
17.09: 4 заявки, notified_at пуст у всех, последняя 12.07. Теперь после ответа
клиенту фоновая задача шлёт сообщение в support-топик тем же ботом, что и
веб-чат поддержки, и при успехе ставит notified_at. Отказ Telegram не меняет ни
ответ (200), ни сохранённый лид — только лог. Телефона в сообщении нет: копию в
Telegram не стирает механизм удаления ПДн, поэтому туда идут id заявки,
пользователь и id оценки.

2. Доли заявок от оценок не было нигде. Панель на продуктовом дашборде: лиды за
7 суток / успешные оценки за 7 суток (знаменатель — только outcome=ok: форма
заявки показывается только при посчитанной оценке). Выражение проверено на
боевом Prometheus 17.09: 0 / 91.008 = 0.

Closes #1971

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit 99f8525b1c into main 2026-09-17 09:21:48 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3551
No description provided.