[site-finder][scrapers] domrf_kn FLATS loader сломан с ~17.05 (DOM.РФ WAF): flats 376k→~0, best-layouts supply пустой 5 недель #1945

Closed
opened 2026-06-27 06:08:13 +00:00 by bot-backend · 4 comments
Collaborator

Симптом

Последние снимки domrf_kn_flatsчастичные/пустые. count(*) по snapshot_date:

snapshot flats
2026-05-17 376604 (последний полный)
2026-05-24 2296
2026-06-03 3670
2026-06-10 0
2026-06-22 9

Т.е. ~5 недель свежих flats-данных практически нет.

Корень: per-object FLATS extraction сломан, objects грузятся нормально

kn_scrape_runs (последние): status='done', progress_obj_index = total_obj_count = 1548 (обход ВСЕХ объектов завершён), objects_count ≈ 1548 стабильно — но flats_count = 9 / 0 / 3670 / 0. Объекты тянутся (1548), квартиры из каждого объекта — нет.

Совпадает с DOM.РФ WAF: коммит #503 «bypass WAF + update renamed endpoints» (2026-05-24 10:53) был РЕАКЦИЕЙ на это (снимок 05-24 06:14 до #503 = 2296 flats, 05-24 12:01 после = 0). WAF-bypass частично помог объектам, но per-object flats-эндпоинт остаётся заблокирован/изменён → flats≈0. После #503 правок именно flats-extraction не было (только status-badge-regex / concurrency).

Impact

  • Best-layouts supply: _SUPPLY_BATCH_SQL берёт MAX(snapshot_date) = 06-22 (9 flats) → supply_units_in_radius ≈ 0, sold_pct_of_supply = None, is_oversold = False для всех участков. Проверено live: POST /best-layouts → supply=0 по всем строкам.
  • НЕ затронут headline: ТЗ-рекомендация (unit-mix) и PDF считаются из sum_deals (objective velocity), не из domrf_kn_flats. Поэтому регресс был тихим (вторичная колонка).
  • Возможно затронуты др. потребители latest domrf_kn_flats (проверить competitors-pricing/market-metrics, если используют последний снимок flats).

Что сделать (scraper-infra / Лёха — нужен live DOM.РФ + браузер per .claude/rules/backend.md)

  1. Прозондировать per-object flats-эндпоинт DOM.РФ под WAF (playwright, не curl): что отдаёт сейчас (403/JS-challenge/изменённый формат?).
  2. Расширить #503 WAF-bypass на flats-эндпоинт (или перевести per-object flats fetch на browser-fetcher/прокси, как cian/avito).
  3. После починки — full re-load: свежий snapshot должен вернуть ~376k flats.

Связанное / уже сделано

  • #1944 (смержён): best-layouts supply-query исправлен (objects-first + fan-out fix). Корректен и готов — заработает в полную силу, как только flats-loader вернёт полные снимки. Т.е. этот тикет — про ДАННЫЕ (loader), #1944 — про запрос.
  • Класс — как cian #1781 / avito #197068c / browser-hang #1940 (анти-бот источника).

Найдено в ходе perf/data-health прогона 2026-06-27 (chase best-layouts seq-scan → supply=0 → loader root-cause).

## Симптом Последние снимки `domrf_kn_flats` — **частичные/пустые**. `count(*)` по snapshot_date: | snapshot | flats | |---|---| | 2026-05-17 | **376604** (последний полный) | | 2026-05-24 | 2296 | | 2026-06-03 | 3670 | | 2026-06-10 | **0** | | 2026-06-22 | **9** | Т.е. **~5 недель** свежих flats-данных практически нет. ## Корень: per-object FLATS extraction сломан, objects грузятся нормально `kn_scrape_runs` (последние): `status='done'`, `progress_obj_index = total_obj_count = 1548` (обход ВСЕХ объектов завершён), `objects_count ≈ 1548` стабильно — но `flats_count` = 9 / 0 / 3670 / 0. Объекты тянутся (1548), **квартиры из каждого объекта — нет**. Совпадает с DOM.РФ **WAF**: коммит #503 «bypass WAF + update renamed endpoints» (2026-05-24 10:53) был РЕАКЦИЕЙ на это (снимок 05-24 06:14 до #503 = 2296 flats, 05-24 12:01 после = 0). WAF-bypass частично помог объектам, но **per-object flats-эндпоинт остаётся заблокирован/изменён** → flats≈0. После #503 правок именно flats-extraction не было (только status-badge-regex / concurrency). ## Impact - **Best-layouts `supply`**: `_SUPPLY_BATCH_SQL` берёт `MAX(snapshot_date)` = 06-22 (9 flats) → `supply_units_in_radius ≈ 0`, `sold_pct_of_supply = None`, `is_oversold = False` для всех участков. Проверено live: POST /best-layouts → supply=0 по всем строкам. - **НЕ затронут headline**: ТЗ-рекомендация (unit-mix) и PDF считаются из `sum_deals` (objective velocity), не из domrf_kn_flats. Поэтому регресс был тихим (вторичная колонка). - Возможно затронуты др. потребители latest `domrf_kn_flats` (проверить competitors-pricing/market-metrics, если используют последний снимок flats). ## Что сделать (scraper-infra / Лёха — нужен live DOM.РФ + браузер per .claude/rules/backend.md) 1. Прозондировать per-object flats-эндпоинт DOM.РФ под WAF (playwright, не curl): что отдаёт сейчас (403/JS-challenge/изменённый формат?). 2. Расширить #503 WAF-bypass на flats-эндпоинт (или перевести per-object flats fetch на browser-fetcher/прокси, как cian/avito). 3. После починки — full re-load: свежий snapshot должен вернуть ~376k flats. ## Связанное / уже сделано - **#1944** (смержён): best-layouts supply-query исправлен (objects-first + fan-out fix). Корректен и готов — заработает в полную силу, как только flats-loader вернёт полные снимки. Т.е. этот тикет — про ДАННЫЕ (loader), #1944 — про запрос. - Класс — как cian #1781 / avito #197068c / browser-hang #1940 (анти-бот источника). _Найдено в ходе perf/data-health прогона 2026-06-27 (chase best-layouts seq-scan → supply=0 → loader root-cause)._
bot-backend added the
bug
data
needs-human
priority/p1
scope/backend
labels 2026-06-27 06:08:23 +00:00
Author
Collaborator

Уточнения + превентивный мониторинг (2026-06-27)

Было частично замечено в коде (но не затрекано как issue и без root-cause):

  • analytics_queries.py:2043 — аналитика уже мигрирована с domrf_kn_flats («stale 2026-01»).
  • competitors.py:519 — комментарий «на ПОСЛЕДНЕМ snapshot domrf_kn_flats.price_per_m2 = 0 строк» (замер 2026-06).
    Этот тикет добавляет: root-cause (DOM.РФ WAF, kn_scrape_runs flats=0 при objects=1548), таймлайн (полный снимок 05-17 → 9 flats 06-22), и impact на best-layouts supply (#1944-запрос корректен, но данных нет).

Почему 5 недель тихо → починил мониторинг (#1947, в CI): freshness-монитор (compute_freshness) классифицировал по recency прогона, а kn-источник смотрел objects_count (здоров ~1548), НЕ flats_count (=0). Т.е. «прогон done + свежий» = fresh, хотя flats не извлекались. #1947 добавляет zero-output check + источник kn_flats (work_col=flats_count, critical) → теперь recent-success-но-0-выхлопа → status=failed → GlitchTip-алёрт. Этот класс регрессий больше не будет тихим ~5 недель (поймается за ~8-14д).

Сам loader-фикс (WAF на per-object flats-эндпоинте) остаётся за scraper-infra (этот тикет).

## Уточнения + превентивный мониторинг (2026-06-27) **Было частично замечено в коде** (но не затрекано как issue и без root-cause): - `analytics_queries.py:2043` — аналитика уже мигрирована с `domrf_kn_flats` («stale 2026-01»). - `competitors.py:519` — комментарий «на ПОСЛЕДНЕМ snapshot domrf_kn_flats.price_per_m2 = 0 строк» (замер 2026-06). Этот тикет добавляет: root-cause (DOM.РФ WAF, kn_scrape_runs flats=0 при objects=1548), таймлайн (полный снимок 05-17 → 9 flats 06-22), и impact на best-layouts supply (#1944-запрос корректен, но данных нет). **Почему 5 недель тихо → починил мониторинг (#1947, в CI):** freshness-монитор (`compute_freshness`) классифицировал по **recency прогона**, а `kn`-источник смотрел `objects_count` (здоров ~1548), НЕ `flats_count` (=0). Т.е. «прогон done + свежий» = fresh, хотя flats не извлекались. #1947 добавляет zero-output check + источник `kn_flats` (work_col=flats_count, critical) → теперь recent-success-но-0-выхлопа → `status=failed` → GlitchTip-алёрт. **Этот класс регрессий больше не будет тихим ~5 недель** (поймается за ~8-14д). Сам loader-фикс (WAF на per-object flats-эндпоинте) остаётся за scraper-infra (этот тикет).
Author
Collaborator

Монитор свежести теперь ловит эту поломку (детект-сторона закрыта)

PR #1948 смёржен и задеплоен на прод (main d87641a). Loader-фикс (DOM.РФ WAF) остаётся открытым в этом тикете — но «тихо 5+ недель» больше не повторится.

Прод-verify GET /api/v1/admin/scrape/freshness сразу после деплоя:

overall_status: failed
kn_flats   failed   recent_output=9   reason="low-output: done-прогоны за 8d дали SUM(flats_count)=9 < порога 50000"
objective  ok       recent_output=946264
kn         ok       recent_output=0    (флаг off — не алёртит)

kn_flats критичный → overall=failed → на следующем beat-прогоне scrape_freshness_check шлёт error-level алёрт в GlitchTip.

Ground truth, подтверждающий поломку (domrf_kn_flats snapshots):

snapshot_date flats
2026-06-22 9
2026-06-03 3670
2026-05-24 2296
2026-05-17 376604 ← последний здоровый

Хронология фикса монитора (#1947#1948):

  1. #1947 (zero-output upd_7d==0) — прод-проверка показала: (а) пропускал flats_count=9 (не ровно 0), (б) ложно-срабатывал на недельном прогоне 7–8d (окно 7d vs fresh_days=8).
  2. Первый редизайн (single-newest done-прогон) — адверсариал-ревью зарубило: цикл загрузки = несколько done-прогонов (objective пишет на каждую из 4 групп; kn — на регион + resume-хвост), одиночный прогон врёт.
  3. #1948 (итог) — floor сравнивается с SUM(work_col) по всем done-прогонам цикла в окне fresh_days: агрегирует цикл (objective 946k, kn_flats 376k) И не ложит на 7–8d.

Что останется доделать здесь — собственно loader-фикс flats (DOM.РФ WAF, live-браузер). Детект-инфра готова и проверена.

## Монитор свежести теперь ловит эту поломку (детект-сторона закрыта) PR #1948 смёржен и **задеплоен на прод** (main `d87641a`). Loader-фикс (DOM.РФ WAF) остаётся открытым в этом тикете — но «тихо 5+ недель» больше не повторится. **Прод-verify `GET /api/v1/admin/scrape/freshness` сразу после деплоя:** ``` overall_status: failed kn_flats failed recent_output=9 reason="low-output: done-прогоны за 8d дали SUM(flats_count)=9 < порога 50000" objective ok recent_output=946264 kn ok recent_output=0 (флаг off — не алёртит) ``` `kn_flats` критичный → `overall=failed` → на следующем beat-прогоне `scrape_freshness_check` шлёт error-level алёрт в GlitchTip. **Ground truth, подтверждающий поломку (`domrf_kn_flats` snapshots):** | snapshot_date | flats | |---|---| | 2026-06-22 | **9** | | 2026-06-03 | 3670 | | 2026-05-24 | 2296 | | **2026-05-17** | **376604** ← последний здоровый | **Хронология фикса монитора** (#1947 → #1948): 1. #1947 (zero-output `upd_7d==0`) — прод-проверка показала: (а) пропускал `flats_count=9` (не ровно 0), (б) ложно-срабатывал на недельном прогоне 7–8d (окно 7d vs `fresh_days=8`). 2. Первый редизайн (single-newest done-прогон) — адверсариал-ревью зарубило: цикл загрузки = несколько done-прогонов (objective пишет на каждую из 4 групп; kn — на регион + resume-хвост), одиночный прогон врёт. 3. #1948 (итог) — floor сравнивается с `SUM(work_col)` по всем done-прогонам цикла в окне `fresh_days`: агрегирует цикл (objective 946k, kn_flats 376k) И не ложит на 7–8d. Что останется доделать здесь — собственно **loader-фикс flats** (DOM.РФ WAF, live-браузер). Детект-инфра готова и проверена.
Author
Collaborator

Loader восстановлен — #2050 валидирован на проде

Валидирующий прогон (run 33, region 66, on-demand, extras=false fetch_flats=true):

snapshot flats объектов requests
2026-06-10 (run 30) 0 16976
2026-06-22 (run 32) 9 1 15268
2026-06-28 (run 33) 193 519 703 1559

Loader снова грузит flats в масштабе — даже лучшие старые прогоны крыли лишь 4–8 объектов (05-24: 2296/4, 06-03: 3670/8). #2050 (extras-isolation: flats тянутся отдельным чистым Playwright-сессионным проходом, extras — best-effort с recycle-на-WAF-403 и early-abort) разблокировал WAF И на порядок эффективнее: 1559 запросов вместо ~16–17к (bulk per-object flats вместо пагинации с WAF-ретраями). Ноль WAF-ошибок в run 33.

Честная оговорка по качеству данных (НЕ регресс): из 193 519 flats цена/статус есть лишь у 506 (status='free'). Остальные 193 013 — metadata-only (площадь/комнаты/этаж). Это природа эндпойнта /portal/table — он отдаёт price=null для проданных/забронированных и объектов без per-flat цен (задокументировано в domrf_kn.py:416-418). Цена м² деривируется из price_rub/total_area когда API даёт price_rub.

Downstream: #1956 supply (срок продажи/%продано) теперь имеет свежие данные по 703 объектам (был занижен из-за этого лоадера). Монитор #1948 чист (snapshot=today, 193k). Следующий недельный cron подтвердит steady-state (тот же #2050-код).

Закрываю — больше не Лёha-zone. Если следующий cron-прогон деградирует, #1948 заалёртит.

## ✅ Loader восстановлен — #2050 валидирован на проде **Валидирующий прогон (run 33, region 66, on-demand, `extras=false fetch_flats=true`):** | snapshot | flats | объектов | requests | |---|---|---|---| | 2026-06-10 (run 30) | **0** | — | 16976 | | 2026-06-22 (run 32) | **9** | 1 | 15268 | | **2026-06-28 (run 33)** | **193 519** | **703** | **1559** | Loader снова грузит flats в масштабе — даже лучшие старые прогоны крыли лишь 4–8 объектов (05-24: 2296/4, 06-03: 3670/8). #2050 (extras-isolation: flats тянутся отдельным чистым Playwright-сессионным проходом, extras — best-effort с recycle-на-WAF-403 и early-abort) разблокировал WAF И на порядок эффективнее: **1559 запросов вместо ~16–17к** (bulk per-object flats вместо пагинации с WAF-ретраями). Ноль WAF-ошибок в run 33. **Честная оговорка по качеству данных (НЕ регресс):** из 193 519 flats цена/статус есть лишь у **506** (status='free'). Остальные 193 013 — metadata-only (площадь/комнаты/этаж). Это **природа эндпойнта `/portal/table`** — он отдаёт `price=null` для проданных/забронированных и объектов без per-flat цен (задокументировано в `domrf_kn.py:416-418`). Цена м² деривируется из `price_rub/total_area` когда API даёт price_rub. **Downstream:** #1956 supply (срок продажи/%продано) теперь имеет свежие данные по 703 объектам (был занижен из-за этого лоадера). Монитор #1948 чист (snapshot=today, 193k). Следующий недельный cron подтвердит steady-state (тот же #2050-код). Закрываю — больше не Лёha-zone. Если следующий cron-прогон деградирует, #1948 заалёртит.
Author
Collaborator

Покрытие vs 05-17 baseline (проверка, что recovery полный, а не половинчатый): хотя total flats 193k < 376k (05-17), это НЕ регресс покрытия — 05-17 «полный» снимок крыл лишь 417 объектов, а run 33 — 703 (overlap 415, потеряно только 2 объекта, +288 новых). Меньше total flats = меньше flats/объект (медиана 223, avg 275, max 1570 — здоровое распределение), т.к. текущий /portal/table отдаёт leaner per-flat payload (см. оговорку выше). Т.е. охват объектов сегодня шире, чем на пике до поломки. Recovery полный.

**Покрытие vs 05-17 baseline (проверка, что recovery полный, а не половинчатый):** хотя total flats 193k < 376k (05-17), это НЕ регресс покрытия — 05-17 «полный» снимок крыл лишь **417 объектов**, а run 33 — **703** (overlap 415, потеряно только **2** объекта, +288 новых). Меньше total flats = меньше flats/объект (медиана **223**, avg 275, max 1570 — здоровое распределение), т.к. текущий `/portal/table` отдаёт leaner per-flat payload (см. оговорку выше). Т.е. охват объектов сегодня **шире**, чем на пике до поломки. Recovery полный.
Sign in to join this conversation.
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#1945
No description provided.