fix(tradein): сигналы о сбоях наконец становятся событиями, а протухание кук предупреждает заранее (#2674) #2681
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2681
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/2674-alerts-actually-fire"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Почему это вообще было сломано
В контейнере скрапера 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) показывает пилу:
sber_index_pullходил раз в 28 дней и приносил период на месяц новее, а возраст считается от первого числа покрытого месяца. Значит в момент самой свежей загрузки возраст уже ~46 (07-17 минус 06-01), к следующей дорастает до 46+28=74, и порог 60 лежит внутри [46, 74] — тревога пересекала его каждый цикл, 14 суток из 28. Это замер нашего собственного такта, а не поведения источника. Поднять такое до ERROR и не тронуть больше ничего значило бы завести ежедневное ложное событие на две недели в месяц — ровно ту тревогу, которая приучает не читать алерты.Выбран такт, а не порог. Рассматривались два варианта:
lag_allowance25 → 40 (порог 75 против потолка 74) — запас один день, ломается от любого сдвига окна на сутки, и порог продолжает кодировать наш такт. Отклонено;Цена:
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'а не обещаю.
Уровни:
deals_freshness_monitor) писал ERROR с самого начала — расходилась только эта джоба. Теперь, после миграции 212, тревога ещё и означает то, что написаноsber_price_indexпуст/недоступенmark_failedвиден только стрик-алерту (3 подряд), а монитор ходит раз в сутки — трое суток молчания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(при нескольких аккаунтах свежайшая-любая строка может быть чужой протухшей).POST /scrape/domclick/upload-cookies, авто-логина нет).3. Поллер Росреестра — разобран по веткам
app/services/rosreestr_poll.py/data-sets/ответил не-200text/htmlвместо архива), из-за которой поллер уже врал. Ветка может сработать легитимно — файл выложили в листинг раньше, чем докачали, — но цена асимметрична: такт 28 дней, значит ложное срабатывание стоит максимум одного события в месяц, а пропуск стоит квартала молчанияexcept Exception(наш баг: сменилась разметка, упал парсер href)logger.exceptiondeals_freshness_monitorERROR-ом поmax(deal_date)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 места сверх трёх заявленных):
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'у, дополнительного шума нет.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.pymark_done/mark_failed/mark_bannedno-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); поднимать здесь = два события на один сбой.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д.Фальсификация прогнана патч-методом: правки откатывались, тесты запускались против неисправленного кода.
test_alerts_become_events.pyкраснеют без фикса — все, что утверждают появление события (СберИндекс stale / пустой индекс,dealsпуст, 404 датасета, куки Домклика протухли / отсутствуют / предупреждение заранее, SQL-запрос срока сvalid_only=True, каталог Росреестра не-200, заглушка вместо zip, выход нового квартала).interval_daysк 28.Обновлены под новое поведение:
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 — это уже настоящий застой источника, и алерт будет честным.domclick_detail_backfill(ночное окно 15:00-18:00 UTC, куки протухли 2026-08-03).Схема БД не менялась: миграция 212 — только
UPDATE scrape_schedules(данные расписания), DDL нет, идемпотентна.Refs #2674, #2673