fix(tradein): сигналы о сбоях наконец становятся событиями, а протухание кук предупреждает заранее (#2674) #2681

Merged
bot-backend merged 2 commits from fix/2674-alerts-actually-fire into main 2026-08-05 21:56:54 +00:00
Collaborator

Почему это вообще было сломано

В контейнере скрапера GlitchTip поднят как LoggingIntegration(level=INFO, event_level=ERROR) (app/scheduler_main.py:59). Значит WARNING событием не становится — он остаётся строкой в docker-логе, которая вдобавок теряется на каждом редеплое. Любой сигнал о сбое, написанный предупреждением, невидим, сколько бы раз он ни срабатывал.

Проверено на проде (только чтение):

  • sber_freshness_monitor — 24 прогона, 9 со staleness-вердиктом (counters->>'alert' = '1'), событий ноль.
  • domclick_session_cookies.expires_at_estimate = 2026-08-03 20:10 — куки протухли, единственным следом был WARNING.

Что было и что стало

1. СберИндекс: событие — и починка ложной тревоги, которая иначе поехала бы в прод

app/tasks/sber_freshness_monitor.py, data/sql/212_sber_index_pull_weekly.sql

Сначала важная поправка к исходной посылке #2674. Те девять срабатываний, которые эпик привёл как улику застоя бенчмарка, застоем НЕ были. Разбор всех 24 прогонов монитора (read-only, 2026-08-06) показывает пилу:

13-16.07  alert=1  age 73,74,75,76   latest_month=5 (май)
17.07     alert=0  age 46            latest_month=6   ← день загрузки
18-31.07  alert=0  age 47..60        latest_month=6
01-05.08  alert=1  age 61..65        latest_month=6

sber_index_pull ходил раз в 28 дней и приносил период на месяц новее, а возраст считается от первого числа покрытого месяца. Значит в момент самой свежей загрузки возраст уже ~46 (07-17 минус 06-01), к следующей дорастает до 46+28=74, и порог 60 лежит внутри [46, 74] — тревога пересекала его каждый цикл, 14 суток из 28. Это замер нашего собственного такта, а не поведения источника. Поднять такое до ERROR и не тронуть больше ничего значило бы завести ежедневное ложное событие на две недели в месяц — ровно ту тревогу, которая приучает не читать алерты.

Выбран такт, а не порог. Рассматривались два варианта:

  • поднять lag_allowance 25 → 40 (порог 75 против потолка 74) — запас один день, ломается от любого сдвига окна на сутки, и порог продолжает кодировать наш такт. Отклонено;
  • миграция 212: такт загрузки 28 → 7 дней. Потолок возраста становится ≈53 при том же пороге 60 — запас 7 суток: один пропущенный недельный цикл поглощается, два подряд дают тревогу. Порог 60 начинает означать именно «Сбер перестал публиковать / загрузка сломалась».

Цена: pull_sber_indices делает 3 региона × 3 дашборда = 9 GET-запросов к публичному неавторизованному sberindex.ru/api/sowa, без пауз в цикле; прод-прогон 2026-07-17 занял 4 секунды (duration_sec=4, errors=0, upserted=639). Было 9 запросов / 28 дней, стало 9 / 7 дней = 36 в месяц — тот же эндпоинт, который дёргают сами дашборды Сбера при открытии страницы; за всю историю прогонов 0 ошибок, лимитов не наблюдалось. next_run_at подтянут на ближайшее окно, иначе правка начала бы действовать только после уже запланированного прогона 2026-08-14.

Побочно: у оценщика свой per-estimate guard свежести с порогом 35 дней, который пробивается всегда, потому что возраст стартует с ~46. Недельный такт снимает нашу часть задержки (≤28 суток → ≤7). Уйдёт ли возраст под 35 — зависит от того, когда Сбер реально публикует месяц (по нашим данным его лаг между 31 и 46 сутками, точнее не определить), поэтому починку этого guard'а не обещаю.

Уровни:

Ветка Было Стало Почему
данные устарели сверх порога WARNING ERROR бенчмарк участвует в сверке наших медиан; застой — сбой, а не наблюдение. Сосед по конструкции (deals_freshness_monitor) писал ERROR с самого начала — расходилась только эта джоба. Теперь, после миграции 212, тревога ещё и означает то, что написано
sber_price_index пуст/недоступен WARNING ERROR монитор не может выполнить работу вовсе. mark_failed виден только стрик-алерту (3 подряд), а монитор ходит раз в сутки — трое суток молчания
данные свежие INFO INFO рутина

2. Куки Домклика — событие по факту и предупреждение заранее

app/services/domclick_session.py, app/tasks/domclick_detail_backfill.py

Переиспользован подход #2658 (Циан), а не изобретён второй: session_expires_at(db, *, valid_only=...) + COOKIE_EXPIRY_WARN_DAYS = 5, с той же семантикой valid_only (при нескольких аккаунтах свежайшая-любая строка может быть чужой протухшей).

  • Кук нет / протухли / помечены невалидными → ERROR с машиночитаемой причиной и датой протухания (было: один общий WARNING).
  • Куки ещё рабочие, но жить им меньше 5 дней → ERROR заранее. Раньше заблаговременного предупреждения не было вовсе; сигнал по факту приходит, когда обогащение уже деградировало, а обновление кук — ручная операция (POST /scrape/domclick/upload-cookies, авто-логина нет).
  • Прогон по-прежнему не прерывается без кук: cookie-инъекция — механизм обхода QRATOR, а не жёсткое требование. Менялся сигнал, не поток управления.

3. Поллер Росреестра — разобран по веткам

app/services/rosreestr_poll.py

Ветка Было Стало Почему
каталог /data-sets/ ответил не-200 WARNING ERROR каталог — единственная опора поллера. Портал уже один раз переехал (см. «ИСТОРИЯ» в докстринге модуля), и тогда поллер молча врал кварталами
папка квартала найдена, но не открывается WARNING ERROR это не «ещё не опубликовали», а поломка портала
файл датасета найден в листинге, но HEAD не отдал zip / размер ниже порога INFO ERROR тот же класс: это буквально поведение старой Bitrix-заглушки (200 + text/html вместо архива), из-за которой поллер уже врал. Ветка может сработать легитимно — файл выложили в листинг раньше, чем докачали, — но цена асимметрична: такт 28 дней, значит ложное срабатывание стоит максимум одного события в месяц, а пропуск стоит квартала молчания
except Exception (наш баг: сменилась разметка, упал парсер href) WARNING без traceback logger.exception под WARNING собственный баг молча превращался в «квартала нет»
таймаут / сетевая ошибка WARNING WARNING транспортный блип раз в 28 дней (такт поллера) сам пройдёт; «квартал так и не приехал» ловит deals_freshness_monitor ERROR-ом по max(deal_date)
папки/файла квартала нет INFO INFO штатное состояние: квартал выходит 4 раза в год, поллер ходит 12 — большинство прогонов законно пустые
вышел новый квартал INFO INFO + явный capture_message(level="info") см. ниже

Про «хорошую новость». Событие уместно: это точный и ранний сигнал оператору запустить ручной импорт много-гигабайтного ZIP, а INFO-строка живёт до ближайшего редеплоя. Единственным он не будет: у deals_freshness_monitor порог по текущим данным (max(deal_date) = 2026-01-01 → Q1 закончился 2026-03-31, +3 месяца +45 дней) истекает 2026-08-15, и с 16 августа он начнёт писать ERROR ежедневно с тем же смыслом. Событие поллера остаётся более редким и более точным (называет конкретный квартал и ссылку), но не единственным.

Уровень оставлен info, а не поднят до error — иначе error-rate и стрик-алерты начнут врать про «сбой» там, где всё сработало как задумано. Шума не будет: такт поллера 28 дней, квартал выходит 4 раза в год, а повтор до самого импорта — это ровно то напоминание, которого просит #2670.

Что нашёл «шире»

Просмотрел все 400 вызовов logger.warning в tradein-mvp/backend/app + packages/scraper-kit. Конвенция репозитория в целом соблюдена: терминальные для прогона сбои пишутся ERROR/logger.exception, а WARNING — это per-item пропуски. Выбивались из неё вот эти.

Включено в правку (2 места сверх трёх заявленных):

  1. app/services/sber_index.py:468 — 404 датасета («slug переименован»). В комментарии написано «surface it loudly», а уровень был тише соседних веток того же except (5xx и сетевая — обе ERROR). При этом 404 — самая перманентная из трёх: 5xx и сеть пройдут сами, а переименованный slug будет 404-ить каждую неделю, пока человек не перезахватит dataset-path. Ровно тот сбой, из-за которого бенчмарк перестаёт обновляться. → ERROR. Ветка стоит внутри двух вложенных циклов (3 региона × 3 дашборда), поэтому при переименовании общего адреса один прогон даст до 9 событий; в интерфейсе они схлопнутся в одну группу по fingerprint'у, дополнительного шума нет.
  2. app/tasks/deals_freshness_monitor.py:145 — «таблица deals пуста/недоступна». Тот же класс, что и у СберИндекса, у прямого соседа по конструкции (у него соседняя ветка overdue писала ERROR с самого начала). → ERROR.

Осознанно НЕ тронуто:

  • packages/scraper-kit/.../orchestration/scheduler.py:735 — «unknown source, skip» (расписание включено, handler'а нет → источник не выполняется никогда). Это настоящий сбой, но next_run_at в этой ветке не двигается, и строка пишется каждый тик (60 с). ERROR здесь = ~1440 событий в сутки — и, как отмечено в ревью, ровно столько же строк прогонов после #2658. Заводится отдельной задачей про схлопывание, здесь не трогаю.
  • runs.py mark_done/mark_failed/mark_banned no-op «run not in running state» — дефект жизненного цикла прогона, данные от него обновляться не перестают.
  • proxy_pool.py (fallback-affinity, бан узла, «последний узел — бан не записан», таймауты health-probe) — деградация с продолжением работы; отдельная линия #2638/#2600, есть свой ERROR-путь (proxy_rotation._alert_stale_token).
  • cian_session.load_session / domclick_session.load_session «нет валидных кук» — вызывающие теперь алертят сами (_cian_pre_claim, _alert_domclick_cookies); поднимать здесь = два события на один сбой.
  • Остальные ~380 — per-item: не распарсилось одно объявление, у одного дома нет slug, геокодер не нашёл один адрес. Это рутина, ERROR превратил бы её в шум.
  • rosreestr_poll: «нет данных rosreestr в БД → fallback-базлайн» оставлен WARNING — то же состояние уже покрыто deals_freshness_monitor, который в этом PR как раз поднят до ERROR.

Честная оговорка — это НЕ решение проблемы наблюдаемости

Согласно #2673, у системы событий сейчас нет ни одного получателя: в GlitchTip-проекте нет ни правил, ни адресатов, за всю историю продукта наружу не ушло ни одного уведомления. Эта правка сама по себе никого не разбудит. Она делает так, что сигналы становятся событиями и видны в интерфейсе GlitchTip — и что, как только получатель появится (шаг №1 в #2673, на владельце), они пойдут по назначению без дополнительных доработок. Не принимайте этот PR за закрытие проблемы наблюдаемости.

Тесты

Новый tests/test_alerts_become_events.py — 16 тестов. Ключевое: проверяется факт события, а не levelno. Харнесс glitchtip_events() поднимает настоящий sentry_sdk.Client с той же интеграцией и тем же event_level=ERROR, что в проде, и собирает события через before_send (наружу ничего не уходит; клиент закрывается в finally, чтобы не оставлять фоновый поток на каждый тест). Есть мета-тест на сам харнесс (WARNING не событие, ERROR — событие), иначе зелёные проверки «событий нет» ничего не доказывали бы.

Плюс два теста инварианта такта в tests/test_sber_freshness_monitor.py: свойства миграции 212 и «пол возраста + такт загрузки < порога монитора». Второй печатает саму арифметику, если кто-то вернёт 28 дней: такт 28д даёт потолок возраста 74д при пороге 60д.

Фальсификация прогнана патч-методом: правки откатывались, тесты запускались против неисправленного кода.

  • 11 из 16 в test_alerts_become_events.py краснеют без фикса — все, что утверждают появление события (СберИндекс stale / пустой индекс, deals пуст, 404 датасета, куки Домклика протухли / отсутствуют / предупреждение заранее, SQL-запрос срока с valid_only=True, каталог Росреестра не-200, заглушка вместо zip, выход нового квартала).
  • Оба теста такта краснеют при возврате interval_days к 28.
  • 5 зелёные и без фикса, честно: мета-тест харнесса и четыре guard-теста «событий быть не должно» (свежий СберИндекс, свежие куки, квартал ещё не опубликован, таймаут поллера). Они не фальсифицируют правку — они фиксируют границу и ловят будущий регресс «сделаем всё ERROR».

Обновлены под новое поведение: tests/tasks/test_domclick_detail_backfill.py (мок сессии получил session_expires_at/COOKIE_EXPIRY_WARN_DAYS, ассерт на WARNING → на ERROR), tests/test_sber_index.py (404-тест: WARNING → ERROR).

Test plan

  • pytest tests/ -q — 3457 passed, 9 skipped. Единственный падающий тест (test_search_api.py::test_search_cache_hit, 401 вместо 200) воспроизводится на чистом origin/main — не связан с этим PR.
  • ruff check + ruff format версией из pre-commit (0.7.4) — чисто; pre-commit hooks прошли на коммитах.
  • Прод-данные, на которых основаны решения, сняты только на чтение (SELECT через docker exec tradein-postgres psql).
  • После деплоя: SELECT default_params, next_run_at FROM scrape_schedules WHERE source='sber_index_pull'interval_days = 7, next_run_at ≤ завтра 05:00 UTC.
  • После первого недельного прогона: max(period_month) в sber_price_index подтянулся (ожидается июль), возраст упал под 60 → монитор перестал алертить. Если после двух недельных прогонов возраст всё ещё >60 — это уже настоящий застой источника, и алерт будет честным.
  • После деплоя: событие в GlitchTip от domclick_detail_backfill (ночное окно 15:00-18:00 UTC, куки протухли 2026-08-03).

Схема БД не менялась: миграция 212 — только UPDATE scrape_schedules (данные расписания), DDL нет, идемпотентна.

Refs #2674, #2673

## Почему это вообще было сломано В контейнере скрапера GlitchTip поднят как `LoggingIntegration(level=INFO, event_level=ERROR)` (`app/scheduler_main.py:59`). Значит **WARNING событием не становится** — он остаётся строкой в docker-логе, которая вдобавок теряется на каждом редеплое. Любой сигнал о сбое, написанный предупреждением, невидим, сколько бы раз он ни срабатывал. Проверено на проде (только чтение): - `sber_freshness_monitor` — 24 прогона, **9** со staleness-вердиктом (`counters->>'alert' = '1'`), событий **ноль**. - `domclick_session_cookies.expires_at_estimate = 2026-08-03 20:10` — куки протухли, единственным следом был WARNING. ## Что было и что стало ### 1. СберИндекс: событие — и починка ложной тревоги, которая иначе поехала бы в прод `app/tasks/sber_freshness_monitor.py`, `data/sql/212_sber_index_pull_weekly.sql` **Сначала важная поправка к исходной посылке #2674.** Те девять срабатываний, которые эпик привёл как улику застоя бенчмарка, застоем НЕ были. Разбор всех 24 прогонов монитора (read-only, 2026-08-06) показывает пилу: ``` 13-16.07 alert=1 age 73,74,75,76 latest_month=5 (май) 17.07 alert=0 age 46 latest_month=6 ← день загрузки 18-31.07 alert=0 age 47..60 latest_month=6 01-05.08 alert=1 age 61..65 latest_month=6 ``` `sber_index_pull` ходил раз в 28 дней и приносил период на месяц новее, а возраст считается от **первого числа покрытого месяца**. Значит в момент самой свежей загрузки возраст уже ~46 (07-17 минус 06-01), к следующей дорастает до 46+28=74, и порог 60 лежит **внутри** [46, 74] — тревога пересекала его каждый цикл, 14 суток из 28. Это замер нашего собственного такта, а не поведения источника. Поднять такое до ERROR и не тронуть больше ничего значило бы завести ежедневное ложное событие на две недели в месяц — ровно ту тревогу, которая приучает не читать алерты. **Выбран такт, а не порог.** Рассматривались два варианта: - поднять `lag_allowance` 25 → 40 (порог 75 против потолка 74) — запас **один день**, ломается от любого сдвига окна на сутки, и порог продолжает кодировать наш такт. Отклонено; - **миграция 212: такт загрузки 28 → 7 дней.** Потолок возраста становится ≈53 при том же пороге 60 — запас 7 суток: один пропущенный недельный цикл поглощается, два подряд дают тревогу. Порог 60 начинает означать именно «Сбер перестал публиковать / загрузка сломалась». Цена: `pull_sber_indices` делает 3 региона × 3 дашборда = **9 GET-запросов** к публичному неавторизованному `sberindex.ru/api/sowa`, без пауз в цикле; прод-прогон 2026-07-17 занял **4 секунды** (`duration_sec=4, errors=0, upserted=639`). Было 9 запросов / 28 дней, стало 9 / 7 дней = 36 в месяц — тот же эндпоинт, который дёргают сами дашборды Сбера при открытии страницы; за всю историю прогонов 0 ошибок, лимитов не наблюдалось. `next_run_at` подтянут на ближайшее окно, иначе правка начала бы действовать только после уже запланированного прогона 2026-08-14. Побочно: у оценщика свой per-estimate guard свежести с порогом 35 дней, который пробивается всегда, потому что возраст стартует с ~46. Недельный такт снимает **нашу** часть задержки (≤28 суток → ≤7). Уйдёт ли возраст под 35 — зависит от того, когда Сбер реально публикует месяц (по нашим данным его лаг между 31 и 46 сутками, точнее не определить), поэтому починку этого guard'а не обещаю. Уровни: | Ветка | Было | Стало | Почему | |---|---|---|---| | данные устарели сверх порога | WARNING | **ERROR** | бенчмарк участвует в сверке наших медиан; застой — сбой, а не наблюдение. Сосед по конструкции (`deals_freshness_monitor`) писал ERROR с самого начала — расходилась только эта джоба. Теперь, после миграции 212, тревога ещё и означает то, что написано | | `sber_price_index` пуст/недоступен | WARNING | **ERROR** | монитор не может выполнить работу вовсе. `mark_failed` виден только стрик-алерту (3 подряд), а монитор ходит раз в сутки — трое суток молчания | | данные свежие | INFO | INFO | рутина | ### 2. Куки Домклика — событие по факту **и предупреждение заранее** `app/services/domclick_session.py`, `app/tasks/domclick_detail_backfill.py` Переиспользован подход #2658 (Циан), а не изобретён второй: `session_expires_at(db, *, valid_only=...)` + `COOKIE_EXPIRY_WARN_DAYS = 5`, с той же семантикой `valid_only` (при нескольких аккаунтах свежайшая-любая строка может быть чужой протухшей). - Кук нет / протухли / помечены невалидными → **ERROR** с машиночитаемой причиной и датой протухания (было: один общий WARNING). - Куки ещё рабочие, но жить им меньше 5 дней → **ERROR заранее**. Раньше заблаговременного предупреждения не было вовсе; сигнал по факту приходит, когда обогащение уже деградировало, а обновление кук — ручная операция (`POST /scrape/domclick/upload-cookies`, авто-логина нет). - Прогон по-прежнему **не прерывается** без кук: cookie-инъекция — механизм обхода QRATOR, а не жёсткое требование. Менялся сигнал, не поток управления. ### 3. Поллер Росреестра — разобран по веткам `app/services/rosreestr_poll.py` | Ветка | Было | Стало | Почему | |---|---|---|---| | каталог `/data-sets/` ответил не-200 | WARNING | **ERROR** | каталог — единственная опора поллера. Портал **уже один раз переехал** (см. «ИСТОРИЯ» в докстринге модуля), и тогда поллер молча врал кварталами | | папка квартала найдена, но не открывается | WARNING | **ERROR** | это не «ещё не опубликовали», а поломка портала | | файл датасета найден в листинге, но HEAD не отдал zip / размер ниже порога | INFO | **ERROR** | тот же класс: это **буквально** поведение старой Bitrix-заглушки (200 + `text/html` вместо архива), из-за которой поллер уже врал. Ветка может сработать легитимно — файл выложили в листинг раньше, чем докачали, — но цена асимметрична: такт 28 дней, значит ложное срабатывание стоит максимум одного события в месяц, а пропуск стоит квартала молчания | | `except Exception` (наш баг: сменилась разметка, упал парсер href) | WARNING без traceback | **`logger.exception`** | под WARNING собственный баг молча превращался в «квартала нет» | | таймаут / сетевая ошибка | WARNING | WARNING | транспортный блип раз в 28 дней (такт поллера) сам пройдёт; «квартал так и не приехал» ловит `deals_freshness_monitor` ERROR-ом по `max(deal_date)` | | папки/файла квартала нет | INFO | INFO | штатное состояние: квартал выходит 4 раза в год, поллер ходит 12 — большинство прогонов законно пустые | | **вышел новый квартал** | INFO | INFO + явный `capture_message(level="info")` | см. ниже | **Про «хорошую новость».** Событие уместно: это точный и ранний сигнал оператору запустить ручной импорт много-гигабайтного ZIP, а INFO-строка живёт до ближайшего редеплоя. Единственным он не будет: у `deals_freshness_monitor` порог по текущим данным (`max(deal_date) = 2026-01-01` → Q1 закончился 2026-03-31, +3 месяца +45 дней) истекает **2026-08-15**, и с 16 августа он начнёт писать ERROR **ежедневно** с тем же смыслом. Событие поллера остаётся более редким и более точным (называет конкретный квартал и ссылку), но не единственным. Уровень оставлен `info`, а не поднят до `error` — иначе error-rate и стрик-алерты начнут врать про «сбой» там, где всё сработало как задумано. Шума не будет: такт поллера 28 дней, квартал выходит 4 раза в год, а повтор до самого импорта — это ровно то напоминание, которого просит #2670. ## Что нашёл «шире» Просмотрел все **400** вызовов `logger.warning` в `tradein-mvp/backend/app` + `packages/scraper-kit`. Конвенция репозитория в целом соблюдена: терминальные для прогона сбои пишутся ERROR/`logger.exception`, а WARNING — это per-item пропуски. Выбивались из неё вот эти. **Включено в правку (2 места сверх трёх заявленных):** 1. `app/services/sber_index.py:468` — 404 датасета («slug переименован»). В комментарии написано «surface it loudly», а уровень был **тише соседних** веток того же `except` (5xx и сетевая — обе ERROR). При этом 404 — самая **перманентная** из трёх: 5xx и сеть пройдут сами, а переименованный slug будет 404-ить каждую неделю, пока человек не перезахватит dataset-path. Ровно тот сбой, из-за которого бенчмарк перестаёт обновляться. → ERROR. Ветка стоит внутри двух вложенных циклов (3 региона × 3 дашборда), поэтому при переименовании **общего** адреса один прогон даст **до 9 событий**; в интерфейсе они схлопнутся в одну группу по fingerprint'у, дополнительного шума нет. 2. `app/tasks/deals_freshness_monitor.py:145` — «таблица `deals` пуста/недоступна». Тот же класс, что и у СберИндекса, у прямого соседа по конструкции (у него соседняя ветка `overdue` писала ERROR с самого начала). → ERROR. **Осознанно НЕ тронуто:** - `packages/scraper-kit/.../orchestration/scheduler.py:735` — «unknown source, skip» (расписание включено, handler'а нет → источник не выполняется никогда). Это настоящий сбой, но `next_run_at` в этой ветке не двигается, и строка пишется **каждый тик (60 с)**. ERROR здесь = ~1440 событий в сутки — и, как отмечено в ревью, ровно столько же строк прогонов после #2658. Заводится отдельной задачей про схлопывание, здесь не трогаю. - `runs.py` `mark_done/mark_failed/mark_banned` no-op «run not in running state» — дефект жизненного цикла прогона, данные от него обновляться не перестают. - `proxy_pool.py` (fallback-affinity, бан узла, «последний узел — бан не записан», таймауты health-probe) — деградация с продолжением работы; отдельная линия #2638/#2600, есть свой ERROR-путь (`proxy_rotation._alert_stale_token`). - `cian_session.load_session` / `domclick_session.load_session` «нет валидных кук» — вызывающие теперь алертят сами (`_cian_pre_claim`, `_alert_domclick_cookies`); поднимать здесь = два события на один сбой. - Остальные ~380 — per-item: не распарсилось одно объявление, у одного дома нет slug, геокодер не нашёл один адрес. Это рутина, ERROR превратил бы её в шум. - `rosreestr_poll`: «нет данных rosreestr в БД → fallback-базлайн» оставлен WARNING — то же состояние уже покрыто `deals_freshness_monitor`, который в этом PR как раз поднят до ERROR. ## Честная оговорка — это НЕ решение проблемы наблюдаемости Согласно #2673, у системы событий **сейчас нет ни одного получателя**: в GlitchTip-проекте нет ни правил, ни адресатов, за всю историю продукта наружу не ушло ни одного уведомления. **Эта правка сама по себе никого не разбудит.** Она делает так, что сигналы становятся событиями и видны в интерфейсе GlitchTip — и что, как только получатель появится (шаг №1 в #2673, на владельце), они пойдут по назначению без дополнительных доработок. Не принимайте этот PR за закрытие проблемы наблюдаемости. ## Тесты Новый `tests/test_alerts_become_events.py` — 16 тестов. Ключевое: проверяется **факт события**, а не `levelno`. Харнесс `glitchtip_events()` поднимает настоящий `sentry_sdk.Client` с той же интеграцией и тем же `event_level=ERROR`, что в проде, и собирает события через `before_send` (наружу ничего не уходит; клиент закрывается в `finally`, чтобы не оставлять фоновый поток на каждый тест). Есть мета-тест на сам харнесс (WARNING не событие, ERROR — событие), иначе зелёные проверки «событий нет» ничего не доказывали бы. Плюс два теста инварианта такта в `tests/test_sber_freshness_monitor.py`: свойства миграции 212 и «пол возраста + такт загрузки < порога монитора». Второй печатает саму арифметику, если кто-то вернёт 28 дней: `такт 28д даёт потолок возраста 74д при пороге 60д`. Фальсификация прогнана патч-методом: правки откатывались, тесты запускались против неисправленного кода. - **11 из 16** в `test_alerts_become_events.py` **краснеют без фикса** — все, что утверждают появление события (СберИндекс stale / пустой индекс, `deals` пуст, 404 датасета, куки Домклика протухли / отсутствуют / предупреждение заранее, SQL-запрос срока с `valid_only=True`, каталог Росреестра не-200, заглушка вместо zip, выход нового квартала). - **Оба теста такта краснеют** при возврате `interval_days` к 28. - **5 зелёные и без фикса, честно:** мета-тест харнесса и четыре guard-теста «событий быть не должно» (свежий СберИндекс, свежие куки, квартал ещё не опубликован, таймаут поллера). Они не фальсифицируют правку — они фиксируют границу и ловят будущий регресс «сделаем всё ERROR». Обновлены под новое поведение: `tests/tasks/test_domclick_detail_backfill.py` (мок сессии получил `session_expires_at`/`COOKIE_EXPIRY_WARN_DAYS`, ассерт на WARNING → на ERROR), `tests/test_sber_index.py` (404-тест: WARNING → ERROR). ## Test plan - [x] `pytest tests/ -q` — 3457 passed, 9 skipped. Единственный падающий тест (`test_search_api.py::test_search_cache_hit`, 401 вместо 200) воспроизводится на чистом `origin/main` — не связан с этим PR. - [x] `ruff check` + `ruff format` версией из pre-commit (0.7.4) — чисто; pre-commit hooks прошли на коммитах. - [x] Прод-данные, на которых основаны решения, сняты только на чтение (`SELECT` через `docker exec tradein-postgres psql`). - [ ] После деплоя: `SELECT default_params, next_run_at FROM scrape_schedules WHERE source='sber_index_pull'` → `interval_days = 7`, `next_run_at` ≤ завтра 05:00 UTC. - [ ] После первого недельного прогона: `max(period_month)` в `sber_price_index` подтянулся (ожидается июль), возраст упал под 60 → монитор перестал алертить. Если после двух недельных прогонов возраст всё ещё >60 — это уже настоящий застой источника, и алерт будет честным. - [ ] После деплоя: событие в GlitchTip от `domclick_detail_backfill` (ночное окно 15:00-18:00 UTC, куки протухли 2026-08-03). Схема БД не менялась: миграция 212 — только `UPDATE scrape_schedules` (данные расписания), DDL нет, идемпотентна. Refs #2674, #2673
bot-backend added 1 commit 2026-08-05 21:31:51 +00:00
fix(tradein): сигналы о сбоях наконец становятся событиями, а протухание кук предупреждает заранее (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m58s
46bbb79881
В скрапер-контейнере GlitchTip поднят с LoggingIntegration(event_level=ERROR),
поэтому любой сигнал уровня WARNING событием не становится — сколько бы раз он
ни срабатывал. Прод это подтвердил: монитор устаревания СберИндекса отработал
24 раза, 9 из них со staleness-вердиктом, событий ноль; куки Домклика протухли
2026-08-03 и об этом никто не узнал.

Разбирали не «поменять warning на error», а по каждому сигналу: сбой, из-за
которого данные перестают обновляться — событие; рутина и ожидаемые состояния —
лог. Плюс предупреждение ЗАРАНЕЕ там, где чинить нужно руками (куки Домклика —
по образцу #2658 для Циана, переиспользован тот же подход session_expires_at +
COOKIE_EXPIRY_WARN_DAYS).

У поллера Росреестра выход нового квартала оставлен уровнем info, но получил
явный capture_message(level="info"): новость хорошая, но требует ручного импорта
оператором, а INFO-строка живёт только до ближайшего редеплоя. logger.error для
неё был бы враньём в error-rate и стрик-алертах.

Оговорка: у GlitchTip-проекта сейчас нет ни правил, ни получателей (#2673) —
события станут видны в интерфейсе, но никому не отправятся.

Refs #2674
Light1YT added 1 commit 2026-08-05 21:53:35 +00:00
fix(tradein): чинит такт загрузки СберИндекса — иначе новый ERROR стал бы ложной тревогой (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 7s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m59s
3e1b9a8b0d
Ревью PR #2681 опровергло исходную посылку по СберИндексу, и это подтвердилось
на моих же числах (все 24 прогона монитора, read-only):

  13-16.07  alert=1  age 73..76  latest=май
  17.07     alert=0  age 46      latest=июнь  ← день загрузки
  18-31.07  alert=0  age 47..60
  01-05.08  alert=1  age 61..65

Загрузка ходила раз в 28 дней и приносила период на месяц новее, возраст
считается от первого числа покрытого месяца → пол 46, потолок 74, порог 60
ВНУТРИ диапазона. Тревога срабатывала 14 суток из 28 без всякого застоя
источника: девять срабатываний были замером нашего собственного такта. Поднятие
до ERROR без этой правки завело бы ежедневное ложное событие две недели в месяц.

Миграция 212 переводит sber_index_pull на недельный такт (потолок ≈53 при пороге
60, запас 7 суток) вместо поднятия порога до 75 (запас 1 сутки — ломается от
любого сдвига окна). Цена: 9 запросов в неделю вместо 9 в 28 дней к публичному
sberindex.ru/api/sowa; прогон 4 секунды, 0 ошибок за всю историю.

Дополнительно по ревью:
- поллер Росреестра: ветка «файл найден в листинге, но HEAD не отдал zip» →
  ERROR (ровно поведение старой Bitrix-заглушки) + вписана в таблицу уровней;
- тестовый харнесс закрывает клиент событий (фоновый поток на каждый тест).

Refs #2674
bot-backend merged commit 807d586627 into main 2026-08-05 21:56:54 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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#2681
No description provided.