2525 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| a7ca0e9ee8 |
fix(tradein): вернуть фотографии подсказок Avito IMV задним числом (#2674) (#2693)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m54s
Deploy Trade-In / build-backend (push) Successful in 28s
Deploy Trade-In / deploy (push) Successful in 1m7s
|
|||
| e1c26c212a |
chore(tradein): догнать _manifest_applied.txt до факта прода (31 имя) (#2692)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m54s
Deploy Trade-In / build-backend (push) Successful in 28s
Deploy Trade-In / deploy (push) Successful in 1m6s
|
|||
| fb5ec56a54 |
Merge pull request 'chore(tradein): разбор мёртвого кода — подключить, удалить или задокументировать (#2674)' (#2689) from chore/2674-dead-code-sweep into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Successful in 2m10s
Deploy Trade-In / test (push) Successful in 3m1s
Deploy Trade-In / build-backend (push) Successful in 1m36s
Deploy Trade-In / deploy (push) Successful in 2m19s
|
|||
| 3d38d589d0 |
fix(tradein): панорама для страниц без истории, точные счётчики, окно без гонки (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
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 3m0s
Правки по ревью PR #2689. Признак панорамы был недостижим примерно для десятой части страниц. Вызов стоял после раннего возврата по пустой истории размещений, поэтому идеально отрисованная страница без единого объявления до записи не доходила: на проде 1519 оценок против 1360 домов с историей. Резолв дома и запись панорамы подняты выше возврата — гейт честности не тронут. Цена: match_or_create_house теперь вызывается и для таких страниц (может создать дом), но это тот же вызов с тем же адресом, который уже отрабатывает на остальных 90%. Числа в комментариях к схеме были оценками планировщика, а не точным счётом: listings 142 569 против реальных 93 408 (раздув мёртвыми кортежами на 53%), house_sources 46 813 против 49 502. На безопасность удаления это не влияло — нули там точные, — но оценка уезжала в постоянный комментарий к схеме, в PR, тезис которого «каждое утверждение несёт число с прода». Пересчитано точным count(*). Окно расписания ДОМ.РФ 03:00-04:00 совпадало с refresh_search_matview — то есть ровно с тем заданием, которое переносит year_built в поиск. Планировщик берёт случайный момент внутри окна и гоняет источники параллельно, так что порядок был подбрасыванием монеты. Перенесено на 01:00-02:00; в комментарии честно сказано, что гарантии всё равно нет и при аномально долгом прогоне возможно отставание на цикл. Тесты, адресовавшие вызовы по позиции (db.execute.call_args_list[0]), переведены на фильтр по SQL — это и была причина, по которой добавление второго execute ломало шесть чужих тестов разом. То же для side_effect в тесте отката батча: исключение доставалось бы записи панорамы, которая свои ошибки глотает, и тест молча проверял бы не тот путь. В test_save_history_items_inserts_each возвращено утверждение о числе коммитов (было удалено вместо обновления). Refs #2674 |
|||
| f76485781b |
Merge pull request 'fix(tradein/matching): честность тиров сопоставления домов (#2674)' (#2688) from fix/2674-matching-tiers-honesty into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m50s
Deploy Trade-In / build-backend (push) Successful in 1m37s
Deploy Trade-In / deploy (push) Successful in 1m45s
|
|||
|
|
1a577fe748 |
fix(tradein/matching): снять слияние по ГАР-GUID, починить приёмник кадастра и keeper (#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 / openapi-codegen-check (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m54s
Ревью PR #2688 нашло, что расширение ключа дедупа было неверным. Снимаю его полностью и добавляю три правки, которых не хватало. СНЯТО: слияние 781 дома по COALESCE(house_fias_id, gar_house_guid). Аргумент «общий UUID здания есть независимая идентичность» оказался круговым. gar_flats_loader проставляет gar_house_guid предикатом WHERE tradein_canon_addr(COALESCE(h.short_address, h.full_address, h.address)) = gp.canon — левая часть побайтово равна ключу канон-прохода, то есть guid является детерминированной функцией канон-адреса, а не вторым наблюдением. Проход шёл с выключенным гео-стражем, значит #2187 обходился боковой дверью: канон-проход отказывается слить два дома в 6 км, а этот сливал их же за «общий UUID», выданный за тот же адрес. Плюс gar_pick берёт DISTINCT ON (canon) — одна ГАР-строка на канон, а 20.3% канонов накрывают несколько зданий, и ЕКБ-фильтр стоит только на стороне ГАР. Кедровка/Советская 17 уехала бы в ЕКБ. Нужен ключ, независимый от канона, либо включённый гео-страж — это другая задача. Приёмник кадастра сужен до кадастра ЗДАНИЯ. Параметр cadastral_number (кадастр КВАРТИРЫ) убран из match_or_create_house, Protocol HouseMatcher, RealMatcherAdapter и обоих вызывающих; `cad` больше не падает на него фолбэком. Мина была отложенной: начни Циан отдавать offer["cadastralNumber"], который парсер уже читает, — у каждой квартиры свой номер, Tier 0 не сматчил бы никогда, New-house INSERT записал бы номер квартиры в houses.cadastral_number и попутно снял P1-страж «безномерный адрес без кадастра не создаём». Две квартиры одного дома дали бы два дома — то самое дробление. В listings оба поля пишутся как раньше. Keeper: listing_cnt DESC NULLS LAST. Счётчик приходит из LEFT JOIN, у дома без объявлений он NULL, а DESC в Postgres — NULLS FIRST, поэтому пустая запись обгоняла запись со 192 объявлениями вопреки задокументированному правилу. Дефект предсуществующий и живой для канон-прохода. Сторож границы вызова для живого ФИАС-тира. Прежние проверки были структурными — видели имя параметра в сигнатуре. Уберут аргумент на настоящей границе (estimator.estimate_quality -> match_house_readonly) — сигнатура цела, тесты зелёные, тир снова мёртв. Новый тест смотрит на сам вызов. Заявление «тест ловит неуловимый класс» из прошлого описания снято как преувеличение: структурная проверка ловит подслучай, и building_cadastral_number её проходит при нуле срабатываний из 49 502. Остаётся из первого захода: снятый фильтр поиска has_kadastr, разделение ФИАС-тира (удалён в пути создания, оставлен в read-only), поправка ложного утверждения в шапке cadastral_geo_match.py. Refs #2674 |
||
| f5b39e6fc9 |
chore(tradein): разбор мёртвого кода — подключить, удалить или задокументировать (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 2m57s
Восемь находок «написано, покрыто тестами, ни разу не сработало» разведены на три разных диагноза. Две из восьми оказались не мёртвым кодом, а оборванной проводкой. ПОДКЛЮЧЕНО Загрузчик ДОМ.РФ. Loader и CLI существуют с #2013, а Handler'а в product_handlers и строки в scrape_schedules не было — вызвать его было нечем. На проде 29 978 строк staging с ОДНИМ loaded_at (2026-07-12), то есть ровно один ручной запуск, 24 дня без обновления. Отсюда кормятся houses.year_built/material_walls/total_floors и дальше listings.year_built — когортный фильтр эстиматора. Недельный такт, окно 03:00-04:00 UTC (до импорта ДКП и дневных агрегатов). filters_hash. Парсер читал estimation.sale.data.filtersHash, а Циан кладёт ключ уровнем выше — estimation.sale.filtersHash. Колонка пуста 0/1658, при том что в сохранённых сырых ответах хеш есть у 139/139 и все значения различны. Путь исправлен, 139 строк восстановлены бэкфиллом из raw_payload. has_panorama. Разбирался парсером, лежал в карте приоритетов, обещан публичным контрактом market.v_houses — и не попадал в houses ни одной строкой кода (0 из 9366). Пишется там, где yandex_valuation уже держит и house_id, и мету. Гейт честности: парсер отдаёт bool, а не bool|None, поэтому false пишем только при подтверждённо отрисованной странице (есть год или этажность) — иначе NULL, а не выдуманный false. УДАЛЕНО Дедуп-обёртки эстиматора _phys_dedup_key / _extract_street_token: 25 ссылок, все из тестов. Хуже, чем просто мёртвые — _phys_dedup_key утверждала правило «ключ = кадастр ИЛИ улица», которого в боевом дедупе нет (_union_find_phys_dedup держит оба композита и сливает по любому совпадению). Тесты переведены на живые функции. Тиерные коэффициенты выкупа asking_to_sold_ratios_tiered + asking_to_sold_tier_bounds: ноль читателей и писателей, флага tier_aware_ratio_enabled не существует. Посчитаны один раз при накатке 098 (computed_at 2026-06-27) — тогда как живая asking_to_sold_ratios обновляется ежедневно (2026-08-05). Методика сохранена в 098. Колонки без писателя: listings.merged_into (113 уже называла её мёртвой) и house_sources.raw_payload вместе с GIN-индексом по всегда-NULL колонке. v_data_quality.price_disagreements_count: у всех 89 699 объявлений ровно один источник, показатель структурно не мог быть ненулевым, а ноль читался как «расхождений нет». ЗАДОКУМЕНТИРОВАНО BROWSER_BLOCK_RESOURCES выставлен во всех трёх прод-контейнерах, а код перестал его читать в #1812. Блокировка при этом не ослабла (image глушит camoufox block_images, font/media — дефолт списка типов), мёртв только выключатель. Сервис теперь говорит об этом на старте: молча игнорируемая ручка опаснее отсутствующей. v_price_divergence / v_cross_source_health оставлены как задел, но в COMMENT написано, почему они пусты структурно: боевой путь загрузки зовёт upsert_listing_source напрямую и не зовёт match_or_create_listing. house_sources.ext_url пуст 46 813/46 813, но входит в публичный контракт market.v_house_sources — оставлен и подписан. Refs #2674 |
|||
|
|
3fd6550a16 |
fix(tradein/matching): честность тиров сопоставления домов (#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 2m54s
Три находки эпика #2674 про верхние тиры матчинга домов. Замеры — прод tradein-postgres, 2026-08-05/06. Кадастр от площадок не приходит вообще. listings.cadastral_number (кадастр КВАРТИРЫ) — 0 из 93 408; единственный писатель, парсер Циана, читает offer["cadastralNumber"], которого в ответе нет. Все 28 504 заполненных building_cadastral_number на 100% пришли из локального гео-зеркала ЕГРН (tasks/cadastral_geo_match.py, KNN <=50 м) — проверено джойном к cad_buildings_local. Поэтому снят фильтр поиска has_kadastr: предикат `cadastral_number IS NOT NULL` мог вернуть только пустую выдачу. Колонка и писатель оставлены — заработают сами, если площадка начнёт отдавать кадастр. Tier 0 cadastr_exact оставлен, но не подключён к гео-кадастру. Он достижим по построению (ScrapedLot -> адаптер -> матчер), просто данных нет; подать туда KNN-заполнение НЕЛЬЗЯ: как ключ здания оно не инъективно — 656 из 3 260 значений накрывают >1 здание ГАР (20.1%), 751 из 2 864 зданий получают >1 значение (26.2%). Это был бы over-merge с confidence 1.0. Заодно исправлено ложное утверждение в шапке cadastral_geo_match.py, будто Tier 0 трактует эту колонку как подсказку. Tier 0.5 fias_exact удалён из match_or_create_house. Параметра house_fias_id не было ни в Protocol scraper_kit.contracts.HouseMatcher, ни в RealMatcherAdapter, ни у двух прямых вызывающих — передать его было некому. В match_house_readonly тир оставлен: у estimate-пути источник ФИАС есть (payload.target_fias_id / DaData). Что чинит сопоставление на самом деле: ключ идентичности в house_dedup_merge расширен с house_fias_id до COALESCE(house_fias_id, gar_house_guid). Это один и тот же UUID здания в ГАР (3 666 совпадений из 3 667 домов, где заполнены оба), но заполняют его разные источники, и половина в проход не входила. Read-only прогон отрендеренного mapping-SQL на проде: старый ключ — 0 пар, новый — 781 (8.3% таблицы houses, 6 389 объявлений на них). Канон-проход эти дома узнаёт (900 пар из 919 имеют один канон-адрес), но блокирует гео-стражем: 356 пар с NULL geom, 457 дальше 250 м (максимум 5 065 км — битый геокод). Ровно аргумент #2187: общий UUID здания старше близости. Качество сопоставления сейчас: 0 из 49 502 строк house_sources сматчены верхними тирами; fingerprint 58.97%, new 22.65%, geo_proximity 18.36%. Тесты: новый tests/test_matching_tier_reachability_2674.py сверяет параметры матчера с границей вызова (Protocol + адаптер) — ловит класс «ветка есть, передать некому», который обычный тест не видит, потому что зовёт функцию напрямую. Удалены два теста мёртвого fias-тира: они были зелёными ровно потому, что обходили границу вызова. Refs #2674 |
||
| 5e92810d72 |
Merge pull request 'fix(tradein/avito): окно ретроспективы под недельный такт + диагноз полного обхода (#2674)' (#2685) from fix/2674-avito-full-load-coverage into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m49s
Deploy Trade-In / build-backend (push) Successful in 1m32s
Deploy Trade-In / deploy (push) Successful in 1m39s
|
|||
| 6d76328168 |
fix(tradein/avito): верное объяснение границы окна и критерий приёмки по глубине (#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 2m52s
Разбор ревью PR #2685. Выбор окна в 7 суток подтверждён непредвзятым замером по одному прогону (run 2990, exhaustive 02.08, 2184 датированных строки): W=2 -> 147, W=6 -> 446, W=7 -> 1275, W=12 -> 1278. Шестёрка теряет две трети семёрки, а 7..12 — плато в +3 лота, то есть семёрка стоит на самой дешёвой его точке. Потеря окна 2 занижена мной в первом заходе: не 3.26x, а 8.7x. Механизм объяснён неверно. Пик на возрасте ровно 7 — не недельный авто-подъём Авито, а квантование нашего же парсера относительных дат: «неделю назад» -> ровно today-7, «две недели назад» -> today-14, возрасты 8..13 по этому пути недостижимы. То самое плато (3 лота из 2184) это и доказывает: при реальном подъёме полоса 8..13 была бы заполнена. Вывод от этого только крепнет — шестёрка режет не по пику распределения, а по границе квантования и теряет бакет «неделю назад» целиком, а внутри него реальный возраст от 7 до 13 суток. 51% не воспроизводится: 1212 из 3714 датированных наблюдений — 32.6%. Пятьдесят один получается только на знаменателе, урезанном возрастами 0-13. Цена по запросам описана неверно и в опасную сторону. Рост не пропорционален лотам: стоимость бакета — ceil(свежих/50) страниц с полом 1-2, при окне 7 на бакет выходит ~15-20 свежих (1275 на 77 бакетов), то есть меньше страницы. Большинство бакетов как стояло на 1-2 страницах, так и останется. Верхняя граница честная и продом пережитая: полный обход без отсечки — 6 ч 59 мин (run 295) и 2 ч 34 мин (run 2990). Отсюда же переписан критерий приёмки: ждать «9-10 тысяч собранных лотов» нельзя, это уведёт в ложный вывод. Прогон с окном 2 уже собирал 2804 лота, потому что первые страницы всё равно полные — объём почти не сдвинется, сдвинется глубина. Считать надо лоты с listing_date в полосе [D-7, D-3] и число страниц из лог-строки paginated=. Впечатана мина на случай отката такта: ни миграция (GREATEST только расширяет), ни планировщик (расширяет до такта, не сужает) окно не сузят, поэтому interval_days 7 -> 1 при окне 7 даст восьмикратный охват каждый день. Такт и окно менять вместе. int(params.get("interval_days", 1)) падал на значении null в jsonb — соседний параметр строкой выше обрабатывался через явную проверку на None, этот нет. |
|||
| 0ed0f97ae2 |
Merge pull request 'fix(tradein/admin): убрать показатели, которые не могут быть ненулевыми, и брать список источников из данных (#2674)' (#2684) from fix/2674-admin-metrics-honesty into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m2s
Deploy Trade-In / test (push) Successful in 2m55s
Deploy Trade-In / build-backend (push) Successful in 1m32s
Deploy Trade-In / deploy (push) Successful in 2m5s
|
|||
| 3c5f535e6c |
fix(tradein/admin): гейт отмены по источнику, честный комментарий view, лимит 50 (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 8s
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 / frontend-checks (pull_request) Successful in 1m2s
CI Trade-In / backend-tests (pull_request) Successful in 2m58s
Ревью PR #2684 — четыре MINOR. 1. Починка фильтра открыла кнопку отмены на все 53 источника. Раньше таблица была пуста на каждой вкладке, поэтому кнопка не рендерилась НИ РАЗУ и дыра не проявлялась: ручки отмены source не проверяют вовсе. Оператор на вкладке Авито мог бы «отменить» refresh_search_matview — задача продолжила бы работать под статусом 'cancelled' (ещё один врущий статус ровно в тот день, когда их вычищаем), а has_running_run перестал бы держать single-run guard, который существует из-за инцидента с двойным свипом и баном (2026-05-31). Гейт поставлен на общем узле всех пяти ручек — scrape_runs.honors_cancel + отказ в mark_cancelled, — а не в UI: иначе ручной POST по-прежнему снимал бы guard. Флаг cancellable отдаётся в строке, UI по нему прячет кнопку. Состав набора выведен из call-site'ов runs.is_cancelled: city-sweep'ы (все площадки и города), full-load'ы, avito_newbuilding_sweep, rosreestr_dkp_import. Правило НЕ «любой *_sweep»: yandex_newbuilding_sweep отмену не опрашивает. 2. Комментарий пересозданного v_data_quality утверждал, что его обновляет /api/v1/admin/data-quality. Читателей у view нет ни одного — живая ручка строит свой запрос. PR с тезисом «ложный показатель хуже отсутствующего» не имеет права переносить в прод ложное утверждение о читателе. 3. Лимит выдачи 20 → 50: первые 20 строк по started_at на три четверти — сердцебиение proxy_healthcheck (1631 из 3245), часовой сбор мог не поместиться. Привязка к вкладке НЕ возвращается. 4. Тест «действующее определение view» искал маркер подстрокой с OR REPLACE — миграция с обычным CREATE VIEW или парой DROP+CREATE была бы невидима, и тест проверял бы 214, пока показатель уже вернулся. Заменено регуляркой на обе формы. Фальсификация трёх новых тестов патч-методом — все три красные. Полный прогон 3490 passed / 9 skipped, tsc --noEmit чистый. |
|||
| e4ac0365cf |
fix(tradein/avito): окно ретроспективы под недельный такт + диагноз полного обхода (#2674)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI Trade-In / 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 2m53s
Окно и такт разъехались. Каденс avito_full_load задаёт default_params.interval_days, глубину обхода — incremental_days (since = today - N, ранняя остановка по listing_date). Это два независимых литерала, обязанных совпадать: 129 поставил окно 2 при ежедневном такте (перекрытие было), 206 перевёл такт на 7 суток и окно не тронул. Прогон видит [D-2, D] = 3 суток из 7; дни D+1..D+4 не попадают ни в один прогон. Числа с прода (tradein, read-only 2026-08-06). 2026-06-21 — единственный день, когда оба обхода отработали: инкрементальный run 297 — 2804 unique, exhaustive run 295 — 9992 unique, то есть окно в 2 суток достаёт 28.1% инвентаря. Когорта run 295, не виденная после 23.06 (listing_date заморожен): полоса [D-2, D] — 189 лотов, полоса [D-7, D-3] — ещё 427, расширение окна берёт в 3.26 раза больше. Гистограмма (last_seen_at::date - listing_date) за 20 суток: возраст 0-2 — 594, возраст ровно 7 — 1212 (51% датированных наблюдений) — у Avito недельный авто-подъём, sortTimeStamp сдвигается кратно 7. Структурный минимум бездырочного покрытия — 6, но 6 режет ровно по этому пику; 7 = такт, полосы соседних прогонов смыкаются с суточным перехлёстом под дрейф расписания (замер: last_run 03.08 13:37 -> next_run_at 10.08 14:16). Цена: страниц примерно втрое больше на прогон, но прогон недельный. До 206 система платила ~150-370 страниц семь раз в неделю; после правки — ~500-1200 в неделю, всё ещё примерно вдвое дешевле, чем до 206. Чиню в двух местах: миграция 215 выводит окно из фактического interval_days строки (GREATEST — не сужает окно шире такта), scheduler расширяет его на лету и пишет warning, чтобы расхождение не вернулось следующей правкой каденса. Полный обход: причина не в площадке. Все пять banned-прогонов avito_full_load_exhaustive (05.07-02.08) несут один текст — "browser-sidecar error: browser unavailable (proxy may be down)", то есть 503 от своего же сайдкара, у которого не поднялся камуфокс. В те же дни avito_city_sweep (20 done), avito_newbuilding_sweep (21) и avito_detail_backfill (63) работали. Корень — мёртвый BROWSER_PROXY_AVITO (ard.mobileproxy.space) в env сайдкара, куда полный обход проваливался, потому что строил свой BrowserFetcher без пула; починено не здесь, а #2637 (02.08, пул для браузерного пути Авито) и #2616 шаг 2 (05.08, снос мёртвых env). Прод подтверждает: 0 лотов 05/12/19/26.07, 2334 и 362 в двух прогонах после 02.08. Остаток, который чинится кодом, здесь: 503 сайдкара классифицировался как soft-ban и уходил в бюджет IP-ротации, а ротация снята (#2616 шаг 2, max_rot=0) — условие rot_done < max_rot ложно всегда, а бюджет коротких backoff-retry стоял в else и был для soft-ban недостижим. Ни одного ретрая на самую частую ошибку: один блип сайдкара стоил бакета, четыре подряд — всего прогона. Бюджет backoff теперь общий для обеих причин; сайдкар на таком 503 сам поднимает фоновый retry launch'а, поэтому повтор через пару секунд обычно проходит. Статус banned на такой ошибке остаётся ложью (площадка не банила) — это же чтение легло в основание 206. Здесь не трогаю: набор status ограничен CHECK-констрейнтом, а mark_banned в отличие от mark_failed сохраняет чекпоинт done_buckets. |
|||
| 4d0795ae7a |
fix(tradein/admin): убрать показатели, которые не могут быть ненулевыми, и брать список источников из данных (#2674)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI Trade-In / changes (pull_request) Successful in 8s
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 / frontend-checks (pull_request) Successful in 1m1s
CI Trade-In / backend-tests (pull_request) Successful in 2m57s
Четыре находки одного класса: админка показывает числа, которые никогда не бывают ненулевыми, и подаёт это как результат. Ноль читается оператором как «всё чисто», а не как «мы это не считаем» — такой показатель хуже отсутствующего. 1. «Помечено выбросов» (v_data_quality.outliers_flagged) — УБРАН вместе с колонкой listings.is_outlier. Механизм не «не доделан»: «выброс» у эстиматора вычисляется Tukey-фильтром по КОНКРЕТНОЙ подборке аналогов и живёт один запрос — один и тот же лот выброс для одной оценки и нормальный аналог для соседней. Persist-флаг на объявлении такое отношение выразить не может, реализовать пометку нечем. 2. http_requests / http_errors / returning_count / disappeared_count — УБРАНЫ. HTTP-запросы не считает ни один фетчер (заполнить нечем без сквозной инструментации). Ошибки и «пропало/вернулось» уже считает тот, кто их знает, и кладёт в counters jsonb: errors_count у pipeline, deactivated/revived у deactivate_stale_*. Отдельные колонки были бы вторым определением того же. 3. run_type — УБРАН из API, из таблицы админки и из схемы. Ни одно место кода его не задавало; DEFAULT из 051 подписывал 'city_sweep' даже proxy_healthcheck. Колонка «Тип» в UI заменена на «Источник» — там осмысленное значение. 4. Фильтр источников — теперь из данных (GET /scrape/runs/sources, SELECT DISTINCT source). Захардкоженная тройка не просто была неполной: сравнение точное, а строк с source='avito'/'cian'/'yandex' в таблице нет вообще, то есть каждый пункт фильтра давал пустую выдачу, и пустой выбор («Все») тоже — он молча подставлял source вкладки. Новый источник появляется в списке сам. Числа с прода (tradein-postgres, 2026-08-06): is_outlier=true у 0 из 93 408 listings (NULL у 0 — только DEFAULT); четыре счётчика = 0 во всех 3244 прогонах с миграции 015; run_type — одно значение на 3244 строки; 53 реальных источника, 2466 прогонов (76%) вне трёх площадок, включая весь Домклик. Миграция 214 идемпотентна; v_data_quality пересоздан тем же DDL минус outliers_flagged (порядок DROP VIEW → DROP COLUMN → CREATE как в 095). |
|||
| 673c02e5d6 |
Merge pull request 'fix(tradein): писатели наконец пишут то, что обещает схема — фото подсказок, статус «снято», события объявлений (#2674)' (#2682) from fix/2674-writers-honor-schema into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m50s
Deploy Trade-In / build-backend (push) Successful in 1m32s
Deploy Trade-In / deploy (push) Successful in 6m36s
|
|||
| 77336d351c |
chore(tradein): перенумеровать миграцию 212 -> 213 (коллизия с #2681)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 2m55s
PR #2681 смержен, пока ветка была в работе, и принёс 212_sber_index_pull_weekly.sql. Номер 212 занят — беру 213 (свободен, проверено git ls-tree по origin/main после fetch). Почему локальный гейт молчал: test_new_files_do_not_reuse_prefix сравнивает префиксы файлов В ОДНОМ ДЕРЕВЕ, а смерженный 212_sber в этой ветке отсутствует. Проверено симуляцией (копия data/sql + stub 212_sber): с моим 212 тест КРАСНЫЙ («212 уже у нового 212_sber»), с 213 — зелёный. Кросс-ветковым реестром занятых номеров служит _manifest_applied.txt, но он отстал на 27 имён (171, 187-188, 189-211, 213), поэтому префикс 212 нигде не числился занятым. Про долг — отдельно, в этом PR манифест не трогаю. Apply after в шапке обновлён на 212_sber_index_pull_weekly.sql. |
|||
| ab01f7cc48 |
fix(tradein): убрать невыводимые события, развести «снято» и «протухло» (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 9s
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 3m1s
Ревью PR #2682 нашло контрольную группу в наших же данных. Перепроверено собственными запросами к проду — сходится, местами хуже заявленного. 1. delisted/relisted УБРАНЫ из писателя событий. Покрытие обхода за 14-18.07: domklik 99.9-100%, yandex 34-43%, cian 21-27%, avito 1.6-3.4%. Переходы за те же дни: domklik — снятий 1/2/0/2/4 в сутки и возвратов РОВНО 0 все пять суток; yandex — снятий 343-433 в сутки. Тот же обход, тот же день, разница только в покрытии: событие рождается тем, что скрейпер снова дошёл, а не тем, что объявление вернулось. Подтверждения: avito 13.07 (день остановки обхода) — 3023 «снятия» за сутки против контрольной ставки 1-4 (точность ≈4%); 4705 возвратов из 5493 за 12 дней (85.7%) — это 2-3.08, два дня после возобновления обхода. Сужение окна свежести сделало бы хуже (больше флапаний). Журнал из догадок хуже пустого журнала — не пишем. is_active убран из запроса целиком. Гейт-тест ослаблен до трёх типов + новый гейт «невыводимые НЕ пишутся». 2. TTL-путь пишет 'stale', а не 'closed'. Прогон по домклику 02.08 деактивировал 6131 объявление за раз (TTL 14 суток против 12 суток простоя обхода) — под общим статусом это 6131 фальшивая «дата продажи» одной датой. 'closed' остаётся только за 404: там ответила площадка. CHECK на колонке нет, миграция 212 обновляет только COMMENT. 3. change_time усечён до суток (date_trunc). С now() UNIQUE(source, change_time, type) работал только внутри прогона: второй прогон в те же сутки (2 августа их было два) давал дубли. Теперь заявленная идемпотентность действительно работает. 4. Комнатность в разборе заголовка стала необязательной: 1991 заголовок из 25 055 (7.9%) — «Квартира-студия, 34,2 м², 9/10 эт.», обязательная группа роняла match и обнуляла все четыре поля. Чинит обоих писателей сразу (house_suggestions + house_placement_history, там 8.8% без площади). Студия → rooms=0 по конвенции kit'а, а не None. Фальсификация: вернуть delisted — 1 красный; 'closed' на TTL-пути — 6; обязательная комнатность — 2; now() вместо date_trunc — 1. |
|||
| 807d586627 |
Merge pull request 'fix(tradein): сигналы о сбоях наконец становятся событиями, а протухание кук предупреждает заранее (#2674)' (#2681) from fix/2674-alerts-actually-fire into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m53s
Deploy Trade-In / build-backend (push) Successful in 1m4s
Deploy Trade-In / deploy (push) Successful in 7m53s
|
|||
| 3e1b9a8b0d |
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
Ревью 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 |
|||
| 43aaf91b97 |
fix(tradein): писатели наконец пишут то, что обещает схема — фото подсказок, статус «снято», события объявлений (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
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 2m56s
Три находки одного класса из эпика: колонка есть, писатель есть, тест на писателя зелёный — а данные не появляются. Тестами это не ловится по построению, только сверкой с продом. 1. house_suggestions: парсер выбрасывал imageLink, а INSERT не перечислял image_link + area_m2/rooms/floor/total_floors. 25 055 строк с NULL во всех пяти колонках, ~74 дня с миграции 064. Метрики парсятся из title тем же _parse_title, что и у placementHistory. 2. listings_snapshots.status: 'active' у всех 394 299 строк при 55 448 реально неактивных объявлений. Оба места вызова с литералом 'active' честны — там объявление действительно видели; не писал никто ветку «снято». Теперь оба места деактивации пишут снимок 'closed' в ТОЙ ЖЕ транзакции: TTL-задача (data-modifying CTE, все 4 источника через один deactivate_stale_listings) и 404 из avito_detail_backfill. Дата снятия перестаёт быть догадкой. 3. listing_source_events: схема знает 5 типов, писался 1 (price_change, 8288 строк). Дописаны ветки delisted/relisted/edited/first_seen в тот же set-based statement — данные для них уже лежат в снимке. JOIN → LEFT JOIN LATERAL, иначе first_seen недостижим по построению; план #2607 (per-row index point-lookup по idx_lss_source_date) сохранён, проверено EXPLAIN на проде. Счётчики прогона теперь по типам, все пять всегда присутствуют — ровно они показали бы четыре нуля из пяти. Миграция не нужна: все колонки и CHECK уже существуют. Тесты: tests/test_2674_writers_honor_schema.py. Гейты сверяют писателя со СХЕМОЙ (колонки INSERT против CREATE TABLE 064, типы событий против CHECK 079), поэтому ловят и следующую забытую колонку. Фальсификация патч-методом: без фикса 1 — 6 красных, без фикса 2 — 6, без фикса 3 — 4. |
|||
| 46bbb79881 |
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
В скрапер-контейнере 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 |
|||
| 9f9086fa4d |
Merge pull request 'fix(tradein/imv): домовая оценка перестаёт врать про ремонт и тип дома (#2674)' (#2675) from fix/2674-house-imv-params into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 9s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m47s
Deploy Trade-In / build-backend (push) Successful in 1m1s
Deploy Trade-In / deploy (push) Successful in 1m27s
|
|||
| 4b4ab8b34c |
fix(tradein/imv): счётчики прогона в total_seen/new_count + лог дрейфа ремонта (#2674)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
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 2m51s
По ревью PR #2675. 1. counters прогона не заполняли выделенные колонки. _column_counts (scrape_runs.py) берёт total_seen из ключей total_seen|lots_fetched и new_count из new_count|lots_inserted — ни одного из них в дикте не было, поэтому все 39 прогонов этого source лежат в БД с total_seen=0. А mark_done по этой же колонке шлёт алерт «3 подряд done с нулевым результатом» (#2625): даже идеальный прогон с 50 сохранёнными считался бы нулевым и через три дня выстрелил бы ложной тревогой про капчу. Добавлены total_seen=checked и new_count=saved. Трейд-офф назван в комментарии: на исчерпанной очереди checked=0 три дня подряд тоже даст алерт — но пустая очередь при ежедневном расписании это и правда сигнал. 2. _map_renovation_type молча схлопывал в 'cosmetic' любое незнакомое непустое значение. Сегодня в проде ровно четыре канонических, живого эффекта нет, но дрейф вокабуляра реален (70950 строк listings с пустым нормализованным ремонтом). Добавлен logger.debug на случай «непустое, но не в карте» — паритет с house_type_normalizer, который такой лог уже пишет. 3. Обоснование дефолта 'cosmetic' в докстринге заменено на более сильное по данным: это одновременно МОДА и МЕДИАННАЯ категория популяции (standard 7984 / good 7116 / needs_repair 4738 / excellent 2562; кумулятивно needs_repair 21.2%, +standard 56.8%), то есть наилучшая одиночная догадка, а не просто «не край шкалы». Там же названа асимметрия: поштучный путь эстиматора при неизвестном ремонте IMV вообще не зовёт, а домовой дефолтит — решение осознанное (иначе теряем ещё ~32% домов очереди), чтобы следующий читатель не принял это за недосмотр. Refs #2674 |
|||
| 0815319e1c |
fix(tradein/imv): домовая оценка перестаёт врать про ремонт и тип дома (#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 2m51s
Три дефекта в house_imv_backfill, найденные системным поиском (эпик #2674/#2673).
1. Тип ремонта был захардкожен литералом 'cosmetic' — все 2685 запросов ушли
как «косметический ремонт», хотя мода repair_state по объявлениям тех же
домов другая: standard 4564 / good 4118 / needs_repair 2279 / excellent 1631
(косметика лишь 36%). Теперь renovation_type берётся из mode(repair_state)
в том же агрегате, что уже считает медианы комнат/площади/этажа, и проходит
через существующий estimator._IMV_REPAIR_MAP (ленивый импорт — estimator
тянет scraper_adapters, а тот импортирует этот модуль). Второго словаря не
заводим. Неизвестный ремонт (498 домов из 2685) остаётся 'cosmetic': это
середина порядковой шкалы required < cosmetic < euro < designer, а не край,
системного сдвига в одну сторону не даёт.
2. Неизвестный тип дома молча становился 'panel' — и когда типа нет вовсе, и
когда он есть, но не совпал со словарём. Панель почти самый дешёвый класс
(медиана по нашим же 2685 оценкам: block 122.6k < panel 128.8k <
brick 131.1k < monolithic 145.9k руб/м2), то есть дефолт систематически
занижал. На проде так уехали 363 дома совсем без типа и 75 домов с
camelCase-типом из Циана (56 из них monolithBrick — минус 11.7% против
monolithic). Теперь сырое значение прогоняется через общий
scraper_kit.house_type_normalizer.normalize_house_type (знает monolithBrick /
gasSilicateBlock / aerocreteBlock / stalin и SCREAMING-вокабуляр Яндекса),
дефолт 'panel' убран: тип не распознан → house_type=None → дом помечается
no_params ('unknown house_type') и запрос к площадке не тратится. 'other' и
'wireframe' намеренно НЕ маппятся — честного соответствия у них нет.
3. Прогон не умел падать: 31 прогон подряд с saved=0 и ~35 ошибками из 50
помечен 'done'. Тот же класс, что #2670/#2657 — успех определялся как «не
поймали известное исключение». Теперь saved=0 при errors>0 → mark_failed.
Ноль сохранённых БЕЗ ошибок (всё отфильтровано в skipped) остаётся done.
Балкон/лоджия оставлены константами намеренно: покрытие listings.has_balcony
13.8%, listings.balcony_loggia 9.4%, и колонки противоречат друг другу (по
has_balcony «есть» у 62%, а по balcony_loggia самый частый случай — loggia
5650 против balcony 2794). Мода по одному-двум объявлениям на таком покрытии —
шум, а не данные.
Причина, по которой бэкфилл не сохранил НИ ОДНОЙ оценки за 34 дня, — вне этого
модуля и здесь не чинится (детали и числа в описании PR): 1240 домов легли на
отказе браузерного сайдкара «нет прокси» (гейт #2616, 05.07-02.08), а после
возврата прокси 05.08 — 23 на Page.evaluate «Execution context was destroyed»
в tradein-browser и 12 на 403 Авито.
Refs #2674
|
|||
| 4e9e4f558e |
Merge pull request 'fix(tradein): гейт правдоподобия на «медианный торг» — не показывать артефакт пейринга как рыночный факт (#2666)' (#2671) from fix/2666-discount-plausibility-gate into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m8s
Deploy Trade-In / test (push) Successful in 2m51s
Deploy Trade-In / build-backend (push) Successful in 1m0s
Deploy Trade-In / deploy (push) Successful in 1m16s
|
|||
| 77ae08f207 |
fix(tradein): отказ гейта — факт про выборку вместо обещания надёжности (#2666)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 7s
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 / frontend-checks (pull_request) Successful in 1m6s
CI Trade-In / backend-tests (pull_request) Successful in 2m46s
Правки по ревью PR #2671. Текст «надёжная медиана начинается от 10» обещал то, чего мы гарантировать не можем: пары — псевдореплики (одно объявление переиспользуется на многих сделках, на живом кейсе Космонавтов 2-комн. 42 пары стоят на 2 различных объявлениях), и 10 пар надёжности не дают. Теперь отказ сообщает факт: сколько пар есть и что на такой выборке медиана гуляет на десятки п.п. Формулировка диапазонной ветки укорочена: она дублировала street_only- дисклеймер, который идёт следующим блоком. Проверено скриншотом отрендеренной карточки — две формулировки подряд читались как стена текста; теперь три однострочных хинта, на 820px — по две строки, переполнения нет. В шапку секции добавлен потолок гейта, найденный ревью: бутстрап пересэмплировал ПАРЫ, т.е. мерил дисперсию со стороны сделок, а доминирует дисперсия со стороны ОБЪЯВЛЕНИЙ (джекнайф p90 17.3 п.п., max 63.8); 22 из 64 переживших групп стоят на одном объявлении. Плюс нижняя граница оказалась слишком мягкой, а не строгой: 26 из 64 показываемых значений ниже −23.8%, самое глубокое −58.5%. Оба пункта — отдельная задача, здесь только зафиксированы, чтобы порог не перечитали как гарантию. Тесты: пустое утверждение "1" in explanation (всегда истинно из-за "10") заменено на «всего 1 —». Добавлены два недостающих — отсутствие пар со скидкой даёт explanation=None, и порядок проверок (3 пары по +80% отчитываются «мало пар», а не «вне диапазона»). |
|||
| ef8609d725 |
Merge pull request 'fix(tradein/cian): читать bti из offerData — BTI-персист в houses писал ноль строк (#2435)' (#2668) from feat/2435-cian-house-enrichment into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m50s
Deploy Trade-In / build-backend (push) Successful in 1m33s
Deploy Trade-In / deploy (push) Successful in 1m55s
|
|||
| 301fbed0d7 |
Merge pull request 'fix(tradein/scraper): блок QRATOR у Домклика больше не помечает прогон успешным (#2657)' (#2667) from fix/2657-domclick-block-not-done into main
Some checks failed
Deploy Trade-In / changes (push) Successful in 9s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m51s
Deploy Trade-In / build-backend (push) Successful in 1m44s
Deploy Trade-In / deploy (push) Has been cancelled
|
|||
| b88535425e |
fix(tradein): гейт правдоподобия на «медианный торг» — не показывать артефакт пейринга как рыночный факт (#2666)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 8s
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 / frontend-checks (pull_request) Successful in 1m10s
CI Trade-In / backend-tests (pull_request) Successful in 2m51s
/sales-vs-listings отдавал median_discount_pct без всякой проверки: после сегментного гарда #2660 по `%Космонавтов%` 2-комн. значение уехало с −11.9% на +36.4%, то есть пользователю написали бы «продали на 36% дороже, чем просили». Корень унаследованный — пейринг ДКП↔объявление идёт по улице без номера дома (ADR #721), так что на длинной улице в пару попадают квартиры разных ценовых классов. Пейринг здесь не чиним, перестаём показывать число, которому нельзя верить. Пороги подобраны по проду (симуляция эндпоинта на 238 реальных пользовательских запросах из trade_in_estimates, 128 дали хотя бы одну пару): - MIN_PAIRS = 10 — бутстрап по 12 плотным группам: p90 отклонения медианы подвыборки от полной 18.8 п.п. при k=5, 12.0 при k=10, 9.9 при k=15. Кривая ломается на 10; совпадает с уже принятым в продукте sell_time_sensitivity_min_n_lots. - Санитарный диапазон [−60%, +20%] — асимметричный. Сверху распределение разорвано (…+16.9, пусто, +33.7…+103.1), отсечка попадает в разрыв; ни один городской бакет asking_to_sold_ratios не даёт плюса вообще (max 0.9132). Снизу разрыва нет (у большого минуса есть механизм — занижение цены в ДКП), граница грубая «заведомо не рынок»: 2.5× худшего бакета (студии, −23.8%). Форма отказа — не пустота: новое поле median_discount_explanation по образцу confidence_explanation оценщика, фронт рендерит его вместо числа. Гаснет ровно строка «медианный торг»: сделки, медиана ₽/м², диапазон, linkage_rate_pct и per-pair discount_pct не трогаются. |
|||
| c232772e70 |
fix(tradein/cian): читать bti из offerData — BTI-персист в houses писал ноль строк (#2435)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 2m43s
#2435 (PR #2437) завёл запись BTI-полей дома через match_or_create_house, но на проде она не дала ни одной строки: 9361 дом, 0 с series_name/entrances/flat_count/ is_emergency/heat_supply_type/gas_supply_type/overlap_type — при 628 detail- обогащённых Cian-листингах за Jul 5-22 и 5188 домах, которых Cian вообще касался. Причина: bti читался только как соседний с defaultState ключ контейнера frontend-offer-card, а Cian отдаёт его ВНУТРИ defaultState — offerData.bti. extract_all_states() исправно возвращает 143 ключа, но bti среди них нет, поэтому bti_data всегда оставался None и весь write-path был мёртвым. Существующие тесты этого не ловили: они кормят bti_data прямо в save_detail_enrichment, минуя fetch_detail. Новый тест гоняет реальный сохранённый HTML (fixtures/cian_flat_330982715.html) через настоящий fetch_detail — без фикса краснеет. Старое место оставлено фоллбэком. |
|||
| 9b9f299922 |
fix(tradein/scraper): блок QRATOR у Домклика больше не помечает прогон успешным (#2657)
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 2m43s
Honest-status в run_domclick_city_sweep требовал ОДНОВРЕМЕННО блок И ноль лотов, поэтому распознанный QRATOR-блок после первых собранных лотов уходил в `done`. На проде это 13 из 13 прогонов с blocked=1 (39-464 лота вместо ~6300) — ни один распознанный блок ни разу не дал не-`done` статус. Домклик структурно отличается от cian/yandex (#2625/#2642): там независимые anchor'ы и провал одного среди успешных — не бан (анти-флап). Здесь anchor'ов нет, sweep линейный по ROOM_BUCKETS, и первый же блок делает break — оставшиеся бакеты не пробуются вовсе. Значит блок = прогон оборван, сколько бы лотов он ни успел взять до этого. Теперь: blocked → mark_banned (external constraint, не наш баг; тот же статус, что #2642 дал cian/yandex — доступен как триггер ротации IP #2611, сама ротация не вызывается). Ноль лотов с fetch-ошибками, но БЕЗ блока → по-прежнему failed. Честная пустота → по-прежнему done. Пометка прокси-пула (fetcher.report_ban, #2600 п.1) не тронута — живёт в providers/domclick/serp.py и срабатывает раньше и независимо от статуса прогона. Refs #2657 |
|||
| c9f71da484 |
Merge pull request 'feat(tradein/auth): глобальный потолок попыток входа на имя пользователя (#2571)' (#2663) from feat/2571-login-throttle into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m49s
Deploy Trade-In / build-backend (push) Successful in 1m4s
Deploy Trade-In / deploy (push) Successful in 1m10s
|
|||
| 96d62e418b |
Merge pull request 'fix(tradein/scraper): пропуск расписания пишет строку прогона со статусом skipped (#2658)' (#2662) from fix/2658-loud-skip-status into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 9s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m6s
Deploy Trade-In / test (push) Successful in 2m42s
Deploy Trade-In / build-backend (push) Successful in 1m33s
Deploy Trade-In / deploy (push) Successful in 2m11s
|
|||
| 63ea44fdd2 |
Merge pull request 'fix(tradein): сегментный гард в «медианном торге», свежесть в индексе локации, честные админ-счётчики (#2660)' (#2664) from fix/2660-display-freshness-segment into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m38s
Deploy Trade-In / build-backend (push) Successful in 1m3s
Deploy Trade-In / deploy (push) Successful in 1m23s
|
|||
| 40fdf11f19 |
fix(tradein/auth): не ронять и не занимать пул на замедлении входа (#2571)
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 2m56s
Ревью нашло два способа положить сервис ровно под той нагрузкой, ради которой писалась защита. Первый: `min()` вычисляет оба аргумента, поэтому `float(2 ** (excess - 1))` при 1045 неудачах по имени за окно падал с OverflowError. Счётчик ничем не ограничен сверху — `record()` только копит метки и на лимит не смотрит. С этой попытки и до конца окна вход отдавал 500 мгновенно, без задержки и без записи в аудит: терялись обе ценности PR, и трение, и сигнал. Показатель степени зажат; 2**16 заведомо выше любого разумного потолка, поэтому видимое поведение не меняется. Второй: сон шёл внутри области жизни сессии БД. В дефолтном режиме `get_identity_db` отдаёт ту же сессию, что `get_db`, а SELECT в `get_user_by_username` оставляет её в открытой транзакции — соединение висело занятым все восемь секунд. Пятнадцати одновременных неудач хватало, чтобы выбрать QueuePool целиком и уронить любой другой эндпоинт по pool_timeout. Отказ в обслуживании против всех сразу — хуже той блокировки учётки, ради ухода от которой замедление и выбиралось. Соединение теперь возвращается в пул перед сном. Заодно: длина имени ограничена 64 (верх CHECK'а реестра) — сырое имя становится ключом обоих лимитеров, а их словарь при часовом окне не подчищается; и явно записано, что `limit` у счётчика на имя не порог. |
|||
| d173163025 |
fix(tradein): тест ловит копию константы, а не equality; честный комментарий про вклад свежести (#2660)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 2m44s
По ревью PR #2664. 1. test_freshness_window_is_the_estimator_constant_not_a_copy проверял `lc.LISTINGS_FRESH_DAYS is estimator.LISTINGS_FRESH_DAYS` — CPython кэширует малые int, поэтому скопированный литерал `LISTINGS_FRESH_DAYS = 14` тест бы ПРОШЁЛ, хотя докстринг обещает ловить ровно это. Прошлая фальсификация срабатывала лишь потому, что откат удалял имя целиком (AttributeError). Теперь проверяем исходник через inspect.getsource — фальсифицировано подстановкой копии литерала вместо импорта: тест краснеет. 2. Комментарий в location_index.py приписывал свежести чужую заслугу. Прод-разложение: из −14.8% сдвига городской медианы −14.7 п.п. даёт сегментный гард и лишь −0.18 п.п. свежесть. Для этой метрики свежесть — не коррекция смещения, а страховка на будущее, оплаченная третью пула (3 504 вторичных строки, из них 2 724 живые) и ростом дисперсии: на центре ЕКБ n 423 → 86, индекс гуляет по выбору окна на 12-14 п.п. Размен верный, но он должен быть написан как размен. Там же задокументирован новый режим отказа: свежесть связала витрину со здоровьем сбора — встанет скрейпинг на 14 дней, и insufficient_data прилетит всем пользователям разом. Учитывая, что #2574 это месяц молчаливой поломки сбора, сценарий не гипотетический. Окно свежести не меняю — вопрос вынесен отдельно. Refs #2660 |
|||
| b800760c24 |
fix(tradein/scraper): фильтр skipped в админке + освежение схлопнутой строки (#2658)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 7s
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 / frontend-checks (pull_request) Successful in 1m3s
CI Trade-In / backend-tests (pull_request) Successful in 2m46s
Правки по ревью PR #2662. Фильтр статуса. `GET /admin/scrape/runs?status=skipped` отдавал 422 — 'skipped' не было в Literal, а во фронте не было чипа. Строки рисовались, но задать вопрос «что сейчас пропускается» на единственной поверхности, построенной ровно для этого, было нельзя. Добавлено в оба места (translateStatus «пропущено» и нейтральный бейдж уже умели). Схлопывание освежает строку. UPDATE двигал только finished_at/heartbeat_at, из-за чего живой стрик замерзал: списки прогонов сортируют ORDER BY started_at DESC и берут limit=20, поэтому 37-дневный пропуск утонул бы под свежими прогонами других источников — след в базе есть, на экране нет. Теперь started_at = NOW(), а начало стрика переезжает в counters.first_skip_at; сортировку общего списка не трогаем (она про все источники, чинить надо было одну строку). Там же обновляется counters.detail — иначе в строке 37 дней висел текст «протухли 1 день назад», хотя именно эта цифра и есть предмет issue. jsonb_set заменён на `||` + jsonb_build_object: три вложенных jsonb_set читать в 3 ночи невозможно, а NULL в jsonb_set обнуляет весь counters. Поиск последней строки. `ORDER BY id DESC` не ложится на индекс (source, started_at DESC) из миграции 015 — для unknown_source (тикает каждые 60 с бессрочно) это отбор всех строк источника с сортировкой раз в минуту. Теперь ORDER BY started_at DESC, id DESC. session_expires_at получил valid_only: предупреждение «скоро протухнут» считает срок ИМЕННО той записи, которую взял load_session — при нескольких аккаунтах свежайшая-любая может быть чужой протухшей строкой. Диагностика после None по-прежнему смотрит на свежайшую любую (валидных там нет по определению). Запись пропуска намеренно НЕ обёрнута в свой try/except: если db.execute падает, то падает и claim следующего расписания в этом же тике — тик срывается в любом случае, а глушить исключение здесь значило бы вернуть ровно тот немой пропуск, ради которого заведён #2658. Самовосстановление через 60 с. |
|||
| 837ad8cfd4 |
fix(tradein): сегментный гард в «медианном торге», свежесть в индексе локации, честные админ-счётчики (#2660)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 6s
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 2m42s
Пользовательская половина разбора #2574: витрины читают listings без сегмента и без свежести, поэтому показывают числа, посчитанные не по тому пулу. 1. Миграция 211 — гард #1186 в window_listings у street_sales_vs_listings(). 27.3% кандидатов на пару «ДКП ↔ объявление» были новостройками, и девелоперский прайс (который не торгуется) формировал показываемый процент торга. is_active здесь по-прежнему НЕ фильтруется — осознанно: функция намеренно смотрит и снятые объявления, иначе к сделке нечего подставить. Сигнатура не меняется, значит CREATE OR REPLACE — замена, а не вторая перегрузка (грабли #2627 закрыты тестом-сравнением сигнатур с м.205). 2. location_index — предикат свежести + сегментный гард в обоих запросах медианы, симметрично _COMMON_WHERE эстиматора. Витрина обязана смотреть на тот же пул, на котором считается цена; окно свежести берётся импортом LISTINGS_FRESH_DAYS, второго определения константы не заводим. 3. /scraper/data-quality и /cache-stats — «активно» не прячем, а разделяем: рядом отдаётся «из них не виделись N дней» (+ сам порог N в ответе). Именно слепой count(*) WHERE is_active заставлял #2574 месяц выглядеть как «всё собирается». Refs #2660 |
|||
| 7d154de1f7 |
feat(tradein/auth): глобальный потолок попыток входа на имя пользователя (#2571)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 6s
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 2m50s
Лимит на логине ключевался парой (username, IP), поэтому распределённый перебор одного имени с тысячи адресов получал по 5 попыток с каждого источника и не упирался ни во что. После снятия Caddy basic_auth с /trade-in (#2558) POST /auth/login — единственная ручка, доступная из интернета без кредов, так что дыра открыта прямо сейчас. Поверх существующего per-IP лимита добавлен глобальный счётчик неудач на ИМЯ, без IP в ключе. Превышение порога не блокирует учётку, а растит задержку ответа (удвоение от 1с до потолка): блокировка по имени была бы вектором отказа в обслуживании против конкретного человека — не зная пароля, злоумышленник гарантированно выключал бы чужой вход. Задержка применяется по ПРИСЛАННОМУ имени, без проверки его в реестре, и из одного места — общего хвоста всех отказов по кредам. Иначе «быстрый 401» для несуществующего имени стал бы оракулом существования учётки, то есть ровно той user-enumeration, от которой уже защищают одинаковый generic-ответ и безусловный bcrypt. |
|||
| 0b54b96984 |
fix(tradein/scraper): пропуск расписания пишет строку прогона со статусом skipped (#2658)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI Trade-In / changes (pull_request) Successful in 8s
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 / frontend-checks (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 2m41s
Пропуск наступившего окна был немым: logger + сдвиг next_run_at, ни строки в scrape_runs, ни изменения last_run_at. cian_history_backfill так простоял 37 дней на протухших куках Циана и снаружи выглядел работающим — next_run_at исправно двигался вперёд, а docker-логи с warning'ом терялись на каждом редеплое. Статус 'skipped' заведён ещё миграцией 015 и локализован во фронте («пропущено»), но в проде имел 0 строк — механизм построен и ни разу не использован. Задействуем его во всех пяти местах, где расписание пропускалось без следа: kit `_claim_run` (already_running / concurrent_claim / running_appeared_under_lock), kit `scheduler_loop` (unknown_source) и продуктовый cian `pre_claim`. Причина — слаг в `error`, по нему «нет кук» отличается от «уже бежит» запросом, а не грепом логов. Подряд идущие одинаковые пропуски схлопываются в одну строку со счётчиком `counters.skips`: «уже бежит» и «неизвестный source» не двигают next_run_at и иначе плодили бы строку каждый тик (60 с). Алерт про куки жил в недостижимой ветке: он стоял там, где verify_session вернул None, а на протухших куках load_session сам фильтрует expires_at_estimate > NOW() и отдаёт None ещё в первой, немой ветке. Теперь алерт в обеих ветках и через logger.error — в scraper-контейнере GlitchTip поднят с LoggingIntegration (event_level=ERROR), поэтому прежний capture_message(level="warning") событием не становился. Плюс предупреждение ЗАРАНЕЕ (COOKIE_EXPIRY_WARN_DAYS=5) в том же pre_claim: обновление кук — ручная операция, алерт по факту протухания приходит, когда сбор уже встал. Монитор нулевых прогонов (#2625) не трогаем: обе alert-выборки отбирают failed/banned/done/cancelled, поэтому 'skipped' в стрик не попадает и его не прерывает — пропуск не «прогон вернул ноль лотов», смешивать нельзя. |
|||
| 0a001ee3f7 |
Merge pull request 'fix(tradein/geocode): прошить city_hint в deals-скрипт + развести счётчики гейта (#2603)' (#2655) from fix/2603-geocode-city-hint-tails into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 9s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m37s
Deploy Trade-In / build-backend (push) Successful in 59s
Deploy Trade-In / deploy (push) Successful in 1m52s
|
|||
| 17fcf746f7 |
Merge remote-tracking branch 'origin/main' into fix/2603-geocode-city-hint-tails
All checks were successful
CI / changes (pull_request) Successful in 7s
CI Trade-In / 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 2m41s
# Conflicts: # tradein-mvp/backend/app/api/v1/admin.py |
|||
| 5ecd5361fd |
fix(tradein/geocode): гейт мусорного города вынести в общий хелпер и прошить в admin-путь (#2603)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI Trade-In / 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 2m42s
Первый коммит починил только scripts/geocode_deals_nominatim.py — ручной скрипт. Тот же дефект оставался на живом пути: POST /admin/geocode-missing?target=deals отдавал сырой row["city"] в city_hint, а deals.city росреестровое и в хвосте распределения содержит не-города («Бессонова», «Билейский рыбопитомник»). Любой не-ЕКБ хинт жёстко закрывает EKB-локальные тиры и уезжает префиксом в запрос провайдеру, то есть мусорный хинт хуже отсутствия хинта. Гейт вынесен в geocoder.known_city_hint (сверка с SVERDLOVSK_OBLAST_CITIES — тем же набором, который уже питает _names_non_ekb_city / _ekb_local_tiers_allowed) и переиспользуется всеми тремя потребителями city_hint: скриптом, admin-ручкой и задачей geocode_missing. Копий функции нет — четвёртый потребитель, если появится, получит гейт сам. Тесты: мусорный город -> хинт не передаётся, валидный -> передаётся; проверено фальсификацией (без фикса все три новых теста краснеют). |
|||
| fcaa7c6364 |
Merge pull request 'feat(tradein/proxy): здоровье прокси по паре «узел × источник» (#2600 п.2)' (#2654) from feat/2600-per-source-proxy-health into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m39s
Deploy Trade-In / build-backend (push) Successful in 1m33s
Deploy Trade-In / deploy (push) Successful in 2m3s
|
|||
| 00bc07a55f |
fix(tradein/proxy): backup-узел должен быть пригоден + рычаг снятия бана (#2600 п.2)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
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 2m45s
Правки по deep-review PR #2654. MEDIUM. Внутренний EXISTS считал backup'ом любой enabled-узел affinity. До п.2 это было эквивалентно «пригоден», потому что бан выключал узел глобально; теперь узел бывает enabled и одновременно забанен СВОИМ же источником. Fallback мог увести последний реально рабочий узел выделенной affinity (два domclick-узла, один забанен domclick'ом → второй уходит под avito → domclick без прокси). Добавлено требование, что backup не забанен своим источником — в acquire и зеркально в защите mark_banned. MEDIUM. У оператора не осталось способа снять бан: в п.1 ложное срабатывание лечилось PATCH enabled=true (он обнулял disabled_reason), теперь бан живёт в отдельной таблице и истекает только по таймеру, до 72ч при эскалации. Добавлен proxy_pool.clear_source_bans; зовётся из patch_proxy при ручном включении и после УСПЕШНОЙ ротации exit-IP (бан привязан к proxy_id, а банился IP — после смены адреса строка держала бы узел вне выдачи без причины). LOW. Тест защиты дублировал логику вместо её проверки: ban-предикаты в фейксессии теперь гейтятся по подстрокам боевого SQL (как в acquire-ветке) — проверено мутацией, тесты краснеют при удалении NOT EXISTS из запроса. LOW. Конверсия в миграции 210 матчила disabled_reason по LIKE 'banned:%' и могла отменить ручное выключение оператора (формат подсказан комментарием 209-й) — сужено до точного списка значений домена provider_affinity. LOW. Docstring report_ban в browser_fetcher описывал старую модель (enabled=false); формула в COMMENT ON COLUMN была на шаг мимо (срок ТЕКУЩЕГО бана, не следующего). Расхождение с acquire по leased_by зафиксировано в докстринге как осознанное. Refs #2600 |
|||
| ec17886d60 |
fix(tradein/geocode): прошить city_hint в deals-скрипт + развести счётчики гейта (#2603)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 2m46s
Хвосты после #2601 (замыкание петли «город → геокодер»). 1. scripts/geocode_deals_nominatim.py — непрошитый sibling-caller. Скрипт группировал `GROUP BY address` и звал `geocode(address, db)` без города, хотя deals.city (миграция 177) заполнена на 100%: один и тот же текст адреса из разных городов схлопывался в одну группу, один geocode-вызов и один UPDATE по тексту адреса. Теперь — та же форма, что в #2601: группировка по паре (address, city), city_hint в geocode(), UPDATE и mark-tried через `city IS NOT DISTINCT FROM` (обычное `=` не ловит NULL-город → NULL-группа не обновлялась бы вовсе). Хинт передаётся ТОЛЬКО для значений из geocoder.SVERDLOVSK_OBLAST_CITIES: deals.city росреестровое, в хвосте лежит мусор («Бессонова», «Бердюгина», «Билейский рыбопитомник»), а любой не-ЕКБ хинт жёстко закрывает EKB-локальные тиры и подставляется в запрос провайдеру — мусорный хинт хуже отсутствия хинта. Словарь переиспользован, а не заведён свой: тот же набор уже питает гейты самого геокодера (_names_non_ekb_city / _ekb_local_tiers_allowed) и estimator._resolve_target_city. 2. tasks/backfill_listings_coords_geoportal.py — наблюдаемость городского гейта. Добавлен skipped_non_ekb_by_column (+ в to_counters и в DONE-логи): колоночный гейт стоит перед парсером адреса, поэтому по мере раскатки областных развёрток (#2598) строки потекут из no_address в skipped_non_ekb и общий счётчик поменяет смысл ровно тогда, когда по нему валидируют раскатку. Старый счётчик не тронут — остаётся суммой обоих гейтов, вклад текстового считается разностью. 3. tests: test_admin_geocode_missing_passes_city_hint параметризован на target="deals" (колонка city есть в обеих таблицах, ветка была не покрыта). 4. tasks/geocode_missing.py: dry-run лог печатает city — он с #2594 часть ключа группы, без него две строки dry-run неотличимы. Refs #2603 |
|||
| 964867a943 |
feat(tradein/proxy): здоровье прокси по паре «узел × источник» (#2600 п.2)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
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 2m43s
Бан площадкой был глобальным: п.1 на распознанный бан выключал узел целиком (enabled=false, disabled_reason='banned:<source>'). Реальность другая — Авито банит IP, а Яндекс через тот же IP ходит чисто, поэтому один забаненный источник выкидывал живой узел из пула для всех и худил пул быстрее, чем его пополняют (#2638). Плюс такое состояние не самолечилось: ipify площадку не эмулирует, бан не видит, а non-NULL disabled_reason блокирует авто-воскрешение (#2610) — нужен был ручной PATCH. Теперь бан — свойство ПАРЫ (proxy_id, source) в scrape_proxy_source_bans: acquire(source) не выдаёт узел только этому источнику, для остальных узел первосортный; снимается сам по времени. Срок эскалирует 6ч → 12 → 24 → 48 → 72 (потолок) на повторных банах той же пары; ban_count сбрасывается purge'ем истёкших строк через 7 суток — поэтому purge намеренно отложенный, а не по banned_until < now(). Защита последнего узла сохранена, но считается по источнику: если после бана у acquire(source) не останется кандидатов — бан не пишется, WARNING зовёт пополнять пул. Миграция 210 конвертирует прод-остатки п.1 (enabled=false + disabled_reason LIKE 'banned:%') в 6-часовые per-source баны и возвращает узлы в строй — иначе они висели бы выключенными вечно. Оператору активные баны видны в GET/PATCH /admin/proxies (source_bans) — без этого «узел включён, но не выдаётся» необъяснимо. Refs #2600 |
|||
| d362b16d7c |
fix(tradein/proxy): доводить сигнал бана площадки до пула (#2600 п.1) (#2653)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m39s
Deploy Trade-In / build-backend (push) Successful in 1m35s
Deploy Trade-In / deploy (push) Successful in 1m40s
|
|||
| aa5bb76822 |
fix(tradein/proxy): отличать ручное выключение узла от авто-выключения (#2610) (#2652)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 2m35s
Deploy Trade-In / build-backend (push) Successful in 58s
Deploy Trade-In / deploy (push) Successful in 1m17s
|
|||
| 5162659277 |
fix(tradein/ui): чистить TanStack Query cache при смене identity (#2567) (#2651)
All checks were successful
Deploy Trade-In / changes (push) Successful in 9s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m2s
Deploy Trade-In / deploy (push) Successful in 1m9s
|