842 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fd95c962bb |
fix(tradein/domclick): backfill ходил в сайдкар мимо прокси-пула (#3197 ч.1)
Some checks failed
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
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) Failing after 4m53s
BrowserFetcher(source="domclick", reuse_context=True) конструировался без proxy_provider/use_pool/environment -- тела POST /fetch не несли "proxy", сайдкар брал свой env-прокси, и прогон шёл мимо пула целиком: ни выбора узла по affinity, ни scrape_proxy_source_bans, ни ротации при блоке. Тот же дефект уже чинили на avito_detail_backfill/house_imv_backfill (#2698) -- этот call site оставался последним непочиненным. environment обязателен: без него отказ «пул пуст» на этом пути мёртв (#2616 шаг 1). reuse_context=True сохранён без изменений. Заодно поправлен устаревший комментарий над конструктором: ссылался на scrape_proxies.provider_affinity='domclick' и миграцию 173 -- на проде такого больше нет (миграция 253 сняла резервацию узла, #2800), все четыре включённых узла (id 1/9/10/11) имеют provider_affinity='any'. test_3118_domclick_warm_context.py обновлён под новую сигнатуру вызова (assert_called_once_with -> точечная проверка нужных kwargs). |
||
|
|
09bdd7888b |
fix(tradein/domclick): перезапуск браузера на карточку уничтожал пропуск QRATOR (#3212)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 19s
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 / browser-tests (pull_request) Successful in 1m10s
CI Trade-In / backend-tests (pull_request) Successful in 5m23s
ДомКлик закрыт QRATOR с proof-of-work: первый запрос отдаёт 401 и заглушку с задачей, браузер её решает, дёргает /__qrator/validate и получает пропуск в куках (qrator_jsid2 + qrator_jsr). Пропуск живёт в cookie jar, то есть в browser context'е — а прод брал каждую карточку в новом контексте, выбрасывая его. Повторная валидация с того же IP получала 403 и страницу bot-mitigation. Замер (прод, прод-прокси, по 6 карточек): общий контекст, одна вкладка 6/6, validate не вызывался ни разу общий контекст, вкладка на карточку 6/6, validate не вызывался ни разу новый контекст на карточку (= прод) 1/6, validate = [304, 403] на каждой боевой путь /fetch с reuse_context 6/6 при 0 перезапусков и 1 контексте Что правится: * снят код-дефолт domclick=1 из #3205: перезапуск процесса гарантированно уничтожает контекст, то есть лечил симптом, который сам же и создавал. Ручка per-provider и domclick как отдельный провайдер остаются; * сброс контекста в бэкфилле был на КАЖДЫЙ блок — стал один раз за прогон. Это и объясняет провал #3193: сброс выбрасывал пропуск, следующий фетч блокировался гарантированно, что снова вызывало сброс. Приёмка тогда дала ровно 1 успех из 10; * снята неверная формулировка «отказ, а не челлендж» из #3204/#3205 — 26 624 байта это РЕЗУЛЬТАТ проваленного PoW, а не статика вместо него. Ошибка вышла из метода: HTML читали на 4.5-й секунде и не смотрели в сеть. Замер 6/6, которым обосновывали #3205, был испорчен: в логах сайдкара после каждой страницы стоит «recycle threshold (1) достигнут, перезапуск браузера». Тесты: два кодировали domclick=1 — переписаны через подставной словарь, чтобы уровень «код-дефолт поставщика» продолжал проверяться, а не исчез вместе с записью. Тест сброса требует ровно одну попытку за прогон. 159 passed (сайдкар), 4972 passed / 37 skipped (backend). |
||
|
|
663f426d4c |
fix(tradein/domclick): отказ с кодом 401 классифицировался как причина неизвестной природы (#3196)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
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 4m55s
Замер на проде 2026-08-29: страница отказа ДомКлика при HTTP 401 — РОВНО 26 624 байта, байт в байт та же, что снималась 28.08 под кодом 403. Ни PoW, ни QRATOR, ни капчи. Приходит одинаково и с валидной сохранённой сессией (16 куков из domclick_session), и полностью анонимно — значит это не «сессия отвергнута», а WAF-отказ с подменённым кодом ответа. В таблице классификатора 401 не было (403/429 → platform, 5xx → infra), поэтому все три пробы подряд дали ban_kind='unknown' — ровно то, что #3196 и должен был убрать. Механика диагноза при этом рабочая: статус доезжает от page.goto до исключения целым, шов blocked.status = status подтверждён живым 401 на проде. Правка доменная — в _ban_kind_of_block домкликового таска, а не в общей scraper_kit.browser_fetcher.ban_kind_from_status: у других поставщиков 401 обычно значит «наша сессия протухла», это наша сторона, и метка 'platform' там зря запустила бы ротацию IP (#2611). |
||
| cdcb152d76 |
Merge pull request 'fix(tradein/scrapers): ABORT-лог называл серию блоков, хотя рвал прогон по доле' (#3201) from fix/3184-abort-log-names-wrong-criterion into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 14s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Successful in 43s
Deploy Trade-In / test (push) Successful in 4m10s
Deploy Trade-In / build-backend (push) Successful in 1m35s
Deploy Trade-In / deploy (push) Successful in 2m32s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
Reviewed-on: #3201 |
|||
| 79e469de1c |
Merge pull request 'fix(tradein/scrapers): диагноз блока из HTTP-статуса, а не из маркеров чужой площадки (#3196)' (#3202) from fix/3196-ban-kind-http-status into main
Some checks failed
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / perimeter-smoke (push) Blocked by required conditions
Deploy Trade-In / deploy-status (push) Blocked by required conditions
Deploy Trade-In / changes (push) Successful in 12s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / test (push) Has been cancelled
Deploy Trade-In / build-browser (push) Has been cancelled
Reviewed-on: #3202 |
|||
|
|
bf3214b9e4 |
fix(tradein/scrapers): диагноз блока брался из текстовых маркеров чужой площадки, а не из HTTP-статуса (#3196)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
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 / browser-tests (pull_request) Successful in 1m7s
CI Trade-In / backend-tests (pull_request) Successful in 4m57s
Сайдкар вообще не читал код ответа page.goto: страница классифицировалась только по маркерам, снятым с Авито. Домклик отдаёт статическую `403 | Домклик` на 26 624 байта, где нет ни одного такого маркера (замер прода 28.08.2026) — она уезжала наверх как валидный HTML, парсер не находил состояние, и прогон получал блок неизвестной природы. За 14 дней все 14 прогонов домклика легли с ban_kind='unknown'; у Яндекса счётчика blocked не было вовсе, поэтому ветка перевода прогона в 'banned' была недостижима по построению — ноль банов. - browser/server.py: статус целевой навигации сохраняется per-provider и доезжает в тело /fetch аддитивным ключом "status" (ключ "html" не тронут); 403/429 с маркерами челленджа больше не ждут PoW — ждать нечего, статическая страница сама себя не перезагрузит. Наверх идёт BanPageDetectedError, а не заглушка: вернув её контентом, воскресили бы #3045. - scraper_kit/browser_fetcher.py: BrowserFetcher.last_response_status + ban_kind_from_status (403/429 → platform, 5xx → infra, прочее → None). Поток управления не менялся: fetch() по-прежнему отдаёт str. - domclick: DomClickBlockedError несёт .status — один тип исключения на маркер-детект и на сбой фетча разводится без размножения типов; прогон передаёт перепись диагнозов в mark_backfill_finished. - yandex: появился счётчик blocked, оживляющий ветку бана. Серии блоков и промахов парсера считаются РАЗДЕЛЬНО: иначе четыре промаха плюс один 403 пятым давали 'banned' с переписью {platform: 1}. - cian: ban_kinds наполняется только диагностируемым статусом. HTTP 200 с пустым разбором — дрейф разметки на нашей стороне, а не отказ площадки; записав его блоком, мы бы штамповали фиктивные баны у здорового источника (13 done против 1 banned за 14 дней). Инвариант: непустой ban_kinds ⟺ виден ответ 403/429/5xx. Значения остаются в пределах CHECK scrape_runs.ban_kind. Известный пробел: шов providers/domclick/detail.py `blocked.status = status` тестами не покрыт — существующие домкликовые тесты подают исключение готовым моком и боевой fetch_detail не исполняют. |
||
|
|
8176e8d167 |
fix(tradein/scrapers): ABORT-лог называл серию блоков, хотя рвал прогон по доле
All checks were successful
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / browser-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 4m49s
Прод-прогон 5210 оборвался по ratio-критерию — 14 блоков из 20, ровно порог 0.7 — и отчитался строкой «ABORT -- 1 consecutive blocks». Число верное: последняя серия в тот момент действительно равнялась единице (19-я попытка успех, 20-я блок). Величина не та. Читатель лога видит цифру, по которой обрыва быть не могло, и идёт искать несуществующий баг в брейкере. Причина: #3184 заменил критерий обрыва на долю в скользящем окне, а текст лога остался от прежнего критерия «N подряд» — то есть ровно та же болезнь, которую #3178 лечил у соседней строки (литерал «IP rate-limited» вместо измеренной причины). - BlockRatioBreaker.abort_reason() возвращает "ratio" / "safety_net" / None; should_abort() выражен через него, поведение не меняется. - abort_explanation() даёт текст с той величиной, по которой обрыв и произошёл: доля печатает «доля блоков 14/20 в окне (порог 70%)», safety-net — «5 блоков подряд без единого успеха (снапшот 5 короче окна 20)». - counters["abort_reason"] — чтобы причина обрыва читалась SQL-запросом по scrape_runs, а не грепом контейнера. Ключа нет, если прогон не обрывался. Тесты (проверено мутацией источника — на прежнем сообщении оба падают): - ratio-обрыв на раскладке прогона 5210 (серия на обрыве = 1) требует «14/20» в логе и отсутствия слова consecutive; - safety-net требует «5 блоков подряд» и отсутствия «доля блоков» — без этого зеркала первый тест проходил бы и у сообщения, всегда печатающего долю; - прогон без обрыва (13/20) не пишет abort_reason в counters. Refs #3184, #3178 |
||
|
|
c995af73c2 |
docs(tradein/scrapers): в репозитории лежало опровергнутое обоснование хранилища сессий Яндекса
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m44s
#3195 замержен с утверждением, что авторизованная сессия раскрывает контакты продавца. Утверждение неверно, а лежит оно в двух местах, которые читают в первую очередь: докстринг app/services/yandex_session.py и шапка миграции 274. Повторный замер (#3192, 2026-08-28) сделан на ПРОД-транспорте — curl_cffi + прокси из пула, тот же путь, что у yandex_detail_backfill, — а не на сайдкаре, как первый: - offerCard.card.author у целевой карточки не несёт phones/phoneNumbers ни в одном из 12 случайных объявлений (6 AGENCY, 6 DEVELOPER), одинаково с куками и без; только encryptedPhones (1 токен) и redirectPhones; - phoneNumbers во всём INITIAL_STATE встречается только под offerCard.visitedOffers[*].author — истории просмотров НАШЕЙ учётки; анонимно список пуст, с куками в нём 9-10 записей; - первый замер («0 → 3,4,5,6») считал рост именно этой истории: +1 на каждый фетч; - authorStats.phones (коммутатор застройщика) отдаётся анонимно — тот же номер в обеих ветках. Правка только текстовая: ни схема, ни поведение не меняются. Шапку применённой миграции правлю сознательно — файл повторно не выполняется (учёт по имени в _schema_migrations), а неверное описание пережило бы любой следующий разбор. Таблицу не трогаю: она пуста, но DROP без явного решения владельца делать нельзя. Судьба #3195 — на владельце, #3192 помечен needs-human. Refs #3192, #3195 |
||
| 5be64c6688 |
fix(tradein/scrapers): хранилище авторизованной сессии Яндекс.Недвижимости (#3195)
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 2m18s
Deploy Trade-In / test (push) Successful in 4m4s
Deploy Trade-In / build-backend (push) Successful in 1m36s
Deploy Trade-In / deploy (push) Successful in 2m0s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
|
|||
| 49b70a67f3 |
fix(tradein/scrapers): обрыв по серии блоков рвал каждый прогон, включая здоровые (#3188)
All checks were successful
Deploy Trade-In / changes (push) Successful in 13s
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 4m2s
Deploy Trade-In / build-backend (push) Successful in 1m10s
Deploy Trade-In / deploy (push) Successful in 1m37s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
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
|
|||
| e685f96107 |
fix(tradein/scrapers): диагноз блока терялся при схлопывании, а в алерт шла непроверенная причина (#3183)
All checks were successful
Deploy Trade-In / changes (push) Successful in 13s
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 3m59s
Deploy Trade-In / build-backend (push) Successful in 1m7s
Deploy Trade-In / deploy (push) Successful in 1m32s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
|
|||
|
|
51027d8b02 |
fix(tradein/ingest): rosreestr_dkp_import курсор переживает рестарт (#3168)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m45s
last_id жил только в памяти процесса (import_rosreestr_dkp, scheduler.py):
heartbeat писал его в scrape_runs.counters каждый батч (комментарий рядом
прямо называл это чекпоинтом), но при старте last_id всегда инициализировался
литералом 0 — обрыв (деплой/OOM/рестарт хоста) откатывал прогресс и заставлял
пере-сканировать источник с начала.
Разведка: из пяти backfill-циклов issue (avito_detail_backfill,
house_imv_backfill, cian_history_backfill, yandex_detail_backfill,
geocode_missing_listings) ни один не имеет этого дефекта — все устроены как
WHERE ... IS NULL/NOT EXISTS ... LIMIT, естественно резюмируемы без курсора.
Единственный код, буквально описанный в issue (строки/SQL/комментарий),
это шестой, не входящий в таблицу backfill — rosreestr_dkp_import.
Фикс — _resume_dkp_cursor(db, run_id):
- кандидат — последний прогон source='rosreestr_dkp_import';
- резюмится только незавершённый штатно прогон: status running/zombie,
либо done с counters.interrupted=1 (SIGTERM-drain — эта ветка раньше
считала последующий full rescan штатным поведением, теперь помечает
себя как прерванную и резюмится наравне с zombie);
- потолок возраста чекпоинта — 24ч, старше — 'checkpoint_stale', старт с 0;
- чистый 'done' (полный проход) не резюмится — иначе ON CONFLICT DO UPDATE
перестанет ловить правки уже импортированных сделок при следующем проходе.
Вердикт и per-batch чекпоинт пишутся через kit_runs.update_heartbeat (merge
`counters || :counters`) вместо локального runs_mod.update_heartbeat (полная
замена) — иначе resume-вердикт стирался первым же heartbeat'ом батча.
Тесты: tests/test_3168_backfill_cursor_resume.py — резюм с сохранённого
last_id, резюм после SIGTERM-drain, отказ резюмить чистый done, отказ
резюмить протухший (>24ч) чекпоинт, merge не стирает посторонние ключи.
Обратимость проверена вручную (временный откат _resume_dkp_cursor красил
6 из 8 тестов).
|
||
|
|
c01ec805df |
fix(tradein/tgbot): в логе сетевого сбоя не было причины — только пустота после двоеточия
All checks were successful
CI / changes (pull_request) Successful in 10s
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m51s
Замер на проде 27.08: `getUpdates` падает 23 раза в сутки, 14 из них за один
час. Ретрай почти всегда чинит с первой попытки, поэтому сообщений не теряется
— теряется возможность понять, что происходит:
network error (попытка 1/3): — retry через 2s
После двоеточия пусто. У httpx.ReadError и httpx.ConnectError `str(exc)` пуст,
а тип исключения в строку не попадал. По такому логу не отличить таймаут от
обрыва соединения от сброса TLS, то есть 23 события в сутки не дают ни одной
зацепки. Сеть при этом цела: сырой TLS до Telegram проходит 6 из 6 попыток
за ~0.16s.
Тип добавляется к тексту, а не вместо него: на исключениях с внятным
сообщением диагностика не должна стать беднее прежней. Оба конца закреплены
тестами — с пустым текстом и с непустым.
Closes #3156
|
||
|
|
8cdb195e18 |
fix(tradein/geocode): бюджет прогона не был потолком — проверялся только между батчами
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (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 4m45s
CI / backend-tests (pull_request) Has been skipped
`run_geocode_missing_listings` рекламирует `budget_sec` как максимальное время прогона, но проверка стояла после возврата из батча. Внутри батча цикл шёл по всем 200 адресам и часов не смотрел, то есть фактический потолок был `budget_sec + один полный батч`. Замер: прогон 5017 (27.08, `budget_sec=1800`) шёл 3150 с — 175 % бюджета, и вышел не по бюджету, а по дренажу: бюджетная ветка за 52 минуты не выполнилась ни разу. Пока адрес стоил ~1.9 с это терялось в шуме. После общего ограничителя темпа Nominatim (#2953) средняя цена 4.5 с, а на трудном хвосте (tier-1 + до 4 typo-вариантов под паузой 1 с, плюс retry×3) — до 33 с. Полный батч из таких адресов уезжает на ~110 минут поверх бюджета, при окне расписания 06:00–09:00. Дедлайн теперь передаётся В батч и проверяется на каждом адресе. Оборванный батч — штатный исход: `geocode_tried_at` проставлен только у обработанных пар, остальные попадут в выборку следующего прогона. Отдельный флаг `budget_exhausted` нужен потому, что `addresses_total` на оборванном батче равен размеру ВЫБОРКИ (== batch_size) — ветка дренажа `addresses_total < batch_size` не сработала бы, и обёртка крутила бы цикл дальше. Тест на это падает без флага (проверено снятием ветки). Тесты: 5 новых, все три несущие проверки падают без соответствующей правки. 37 passed локально. Closes #3151 |
||
|
|
dd3a79b475 |
style(tradein): уложить перенесённые строки в лимит 100 символов
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m48s
Ruff E501 на трёх строках, которые удлинились от замены литерала на `DEFAULT_IMPERSONATE` в докстроках. Абзацы перевёрстаны целиком, а не разорваны по месту переполнения — рваный перенос читался бы как опечатка. Прогон: `ruff check app tests` — All checks passed. Refs #3148 |
||
|
|
cf3d4850e2 |
fix(tradein/scrapers): три прод-пути остались на chrome120, пока kit ушёл на chrome146
Some checks failed
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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) Failing after 1m3s
#3034 свёл impersonate к единственной константе внутри scraper_kit и поднял профиль до chrome146. Сторож литерала сканирует только пакет, а его докстрока объявила остальное «отдельным периметром вне scope», сославшись на #2361 F4a. Периметр не спящий — он ходит в сеть каждый день, а #2361 к тому моменту был закрыт, то есть отсылка вела в никуда. На chrome120 оставались: app/services/cian_session.py:164 верификация куки Циана app/services/yandex_address_backfill.py:153 бэкфилл адресов app/tasks/yandex_detail_backfill.py:303 detail-бэкфилл Разрыв в 31 мажорную версию живёт в TLS-отпечатке (JA3/JA4), а не в строке User-Agent, поэтому сменой прокси он не лечится. ПРО ЦИАН ОТДЕЛЬНО. По #2673 оценка Циана мертва с 29 июня — «куки протухли, ни одной новой строки 37 дней». Путь, которым проверяется живость этих куки, всё это время представлялся площадке браузером двухлетней давности. Причину этим не объявляю: утверждаю, что при таком отпечатке отличить «куки протухли» от «нас узнали по рукопожатию» нечем. ТЕСТ ЗАКРЕПЛЯЛ ДЕФЕКТ. test_cian_session прибивал chrome120 гвоздём: подъём профиля в kit ронял бы этот тест, а «починкой» выглядел бы возврат к устаревшему профилю. Теперь тест сверяется с DEFAULT_IMPERSONATE. Сторож литерала расширен на backend/app — без этого периметр возвращается молча, что уже один раз и произошло. Намеренно НЕ входят tests/fixtures/** (номер профиля там — часть записи о том, чем снят фикстур-HTML) и scripts/** (разовые инструменты, в прод-путях не участвуют). Исторические замеры в комментариях сохранены как замеры: «curl_cffi с kit-профилем (на момент замера — Chrome 120)» вместо переписывания истории. Проверено: сканер сторожа на дереве даёт ноль нарушителей, на подсаженном литерале краснеет; все изменённые модули компилируются. Refs #3148 |
||
|
|
d66a52d492 |
fix(tradein/avito): браузерный бэкфилл ходил мимо пула прокси
Some checks failed
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 / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Failing after 4m53s
Ветка browser_mode в avito_detail_backfill конструировала BrowserFetcher без proxy_provider/use_pool/environment. Без них фетчер не кладёт "proxy" в тело POST /fetch, сайдкар берёт свой env-прокси, и прогон уходит мимо пула целиком: ни выбора узла по affinity, ни учёта scrape_proxy_source_bans, ни ротации при блоке. В логе это ровно `proxy_lease_id=None`. Ровно этот дефект чинили рядом — #2698 в house_imv_backfill, где он держал 35 отказов из 35 попыток в каждом прогоне полтора месяца, пока соседние свипы через ТОТ ЖЕ сайдкар тянули сотни объявлений. Здесь он остался. Замер 27.08, прогон 5098: mode=browser, proxy_lease_id=None BLOCKED #1..#5 подряд — firewall/soft-block (browser-mode) ABORT — 5 consecutive blocks, enriched=0 attempted=5 При этом пул здоров — 4 узла, все ok, ни один не занят, браузерная проверка пройдена в то же утро. А тот же URL Авито через прокси отдаёт 200 и 3.3 МБ страницы. То есть площадка нас пускала, запрос шёл не оттуда. Это объясняет, почему предыдущая правка (#3143, прокси в повторах curl-пути) не восстановила сбор: боевой режим бэкфилла — browser, и он до curl-веток вообще не доходит. Три теста: конструктор на месте (страховка от проверки пустоты), все три аргумента проводки передаются, use_pool читается из конфига а не зашит константой (зашитый True отнял бы у владельца выключатель, зашитый False вернул бы дефект незаметно). Проверил красноту на коде без проводки. Прогон: 113 тестов зелёные, ruff чист. Refs #3045, #3034, #2698 |
||
| 01b36f623e |
Merge pull request 'fix(tradein): срок жизни оценки 24ч → 720ч — отчёты исчезали раньше, чем ими успевали воспользоваться' (#3133) from fix/tradein-retention-720h into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m29s
Deploy Trade-In / test (push) Successful in 4m3s
Deploy Trade-In / build-backend (push) Successful in 1m5s
Deploy Trade-In / deploy (push) Successful in 1m29s
Deploy Trade-In / deploy-status (push) Successful in 5s
Deploy Trade-In / perimeter-smoke (push) Successful in 13s
|
|||
|
|
34f8f506c0 |
fix(tradein): срок жизни оценки 24ч → 720ч (30 суток) — отчёты исчезали раньше, чем ими успевали воспользоваться
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 13s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m50s
27.08, пилот «Практика». У пользователя ВСЕ 75 оценок оказались недоступны, включая позавчерашние: список показывал их со статусом «устарел», а открытие отдавало «Ссылка устарела, отчёт удалён или у вас нет к нему доступа». Выглядело как пропажа данных — на деле строки целы, истёк expires_at = created_at + 24ч. Совпадение двух вещей и создало впечатление аварии: сутки не работал вход (упало право CONNECT на базу auth), и ровно за это время истекли последние живые отчёты. Люди зашли и увидели, что не открывается ничего. Обоснование «оценка живёт сессию клиента, не архив» писалось под анонимный B2C. «Практика» — пилот-юрлицо: менеджер возвращается к оценке через день-два, когда клиент перезванивает. Суточный срок для такого сценария означает, что работа исчезает раньше, чем её успевают использовать. Настройка одна на оба контура, и это осознанно НЕ маскируется: юридический мотив 152-ФЗ относится к анонимному B2C, разделение сроков по контурам — отдельная задача. Здесь поднят общий срок, а не сделан вид, что контуры уже разведены. |
||
|
|
b9654d01c0 |
fix(tradein/auth): лимит попыток входа 5 → 20 на адрес — офис за одним NAT запирал сам себя
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 14s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m55s
27.08, инцидент с пилотом «Практика». После починки доступа к базе auth сотрудники начали получать «Слишком много попыток» — при том, что многие ещё вообще не пробовали войти. Причина в форме лимита, а не в его величине. Пилот — ОФИС: все выходят из-под одного NAT, и пять попыток за пять минут делились на всю компанию сразу. Одного человека, перепутавшего пароль, хватало, чтобы запереть остальных. Защита от перебора не ослабевает. Настоящий предохранитель — счёт по ЛОГИНУ (login_username_fail_threshold, 20 за час), он не тронут. Лимит по адресу существует против всплеска с одной машины, а не против офиса, и двадцать попыток за пять минут эту роль выполняют. |
||
| 27fdd55aae |
feat(tradein/matching): region_code у houses — вывод, а не выдумка (#3051 п.4)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m32s
Миграция 272: колонка + бэкфилл ТОЛЬКО по bbox региона 66 (значения байт-в-байт из реестра, синхронизацию держит тест). Карантин NULL: 620 домов без geom, 23 порченых (ЕКБ-адреса с чужими координатами — «Вильгельма де Геннина» на Байкале, «Крауля» под Москвой, «Учителей» в Таллине, с живыми ссылками листингов) — им регион не присваивается, включая 3 дома с координатами в bbox Москвы (порча, не переезд). NOT NULL из постановки — отдельной миграцией, когда карантин опустеет. Запись: новый дом наследует регион РАЗВЁРТКИ (base.py → контракт → адаптер → matching), только если координаты не противоречат; координаты другого региона → NULL-карантин + warning. Вне-bbox координаты при живой развёртке наследуют её регион (адрес и развёртка согласны, координатам веры нет) — семантика закреплена тестом, чтобы смена была осознанной. Матчинг-запросы НЕ тронуты — гард по региону это #3052. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 0529bb249c |
feat(tradein/geo): реестр регионов — единственный источник гео-границ (#3051, шард 1a)
Some checks failed
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Failing after 4m39s
Границы покрытия лежали литералами в трёх файлах (location_index / geocoder / matching.normalize), и каждая молча отвергла бы Москву. Новый модуль app.services.regions — лист дерева импортов — держит per-регион bbox'ы (tight/wide/region/product_core), города, city_token и набор доступных тиров обогащения; потребители держат прежние имена как алиасы на объекты реестра (identity закреплена тестом — копии, разъезжающиеся при правке, невозможны). Регион 66 — байт-в-байт прежние литералы (закреплено тестом: этот PR только переносит границы, менять их = отдельное решение). Регион 77 (Москва): МКАД- ядро + генеральный bbox с Новой Москвой и Зеленоградом; тиров обогащения НЕТ ни одного — и это явный факт реестра с готовой формулировкой (unsupported_tier_reason), а не молчаливое «посчитаем без источника». Приёмка #3051: точка 55.75/37.62 больше не out_of_coverage — location_index узнаёт регион 77 и считает в его ядре (сегодня листингов Москвы нет → честный insufficient_data). Область 50 отложена по решению в #2996. Не здесь (следующие шарды): city_fias_id сквозняком (п.2), doc_type в deals (п.3), region_code у houses (п.4), депромоут описаний (п.5), параметры загрузчиков (п.6). Гейт ЕКБ-тиров геокодера (#2582) уже деградирует правильно для Москвы — fail-closed открывает их только при подтверждённом ЕКБ. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 3d0541b2e2 | Merge origin/main into fix/2656-anchor-ratio-freshness — конфликты: метки #2656→#2661 (main), импорт-union, baseline unrecorded=0 (перезахваченная фикстура) | |||
|
|
124cfb3d5d |
feat(observability): /metrics в обоих бэкендах — счётчики, задержка, дашборд
All checks were successful
CI / backend-tests (pull_request) Successful in 17m30s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m22s
CI Trade-In / backend-tests (pull_request) Successful in 4m51s
Третья часть #3078 и единственная, трогающая прод-код. До неё числовых рядов у приложений не было вовсе: только логи и исключения в GlitchTip. Класс отказов «отвечает, но медленно» и «отдаёт 401 потоком» в такой картине невидим — исключения нет, строка в логе выглядит обычной, а продукт при этом не работает. Метка route — ШАБЛОН маршрута, а не путь запроса. Это несущее решение, а не деталь: кадастровый номер или идентификатор заявки в метке даёт новый временной ряд на каждую сущность, а ряд у Prometheus стоит памяти постоянно, а не в момент запроса. Самый известный способ уронить мониторинг тем самым мониторингом. Незаматченные пути (404, сканеры) сведены в одну метку, иначе тот же взрыв устроит любой бот, перебирающий адреса. Оба свойства сторожатся тестами, а не комментарием: тест бьёт тремя разными идентификаторами и требует ОДИН ряд. Слой регистрируется последним и потому оказывается самым внешним. Изнутри RBAC-гварда не видно ни отказов авторизации, ни времени, которое он тратит на резолв сессии в БД auth, — а именно этот путь уже давал инцидент с блокирующим I/O в middleware (#1202). Упавший исключением запрос считается как 500 в finally: без этого он просто отсутствовал бы в счётчике, то есть ровно тогда, когда метрики нужнее всего. Путь публичен ВНУТРИ и закрыт СНАРУЖИ — это два разных периметра. Скрейп идёт из docker-сети, где заголовка X-Authenticated-User нет ни у кого, поэтому /metrics внесён в _PUBLIC_PATHS обоих бэкендов; иначе агент получал бы 401 и метрик не было бы вовсе. Наружу путь не открывается ни через gendsgn.ru, ни через meraocenka.ru, и вдобавок закрыт явным respond 404 в обоих site-блоках — чтобы закрытость осталась решением, а не следствием текущего порядка директив. Ограничитель частоты и аудит «Меры» не трогались: оба смотрят только на пути под /api/, скрейп под них не попадает. Проверено тестом, а не чтением. Прод-поведение не меняется ничем, кроме нового публичного пути: ни один существующий обработчик, гвард или маршрут не тронут. Refs #3078 |
||
| 929eff11d7 |
Merge pull request 'feat(tradein/estimate): потолок одновременных оценок — 4 слота, быстрый 429 (#3082)' (#3097) from fix/3082-estimate-concurrency into main
All checks were successful
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 3m42s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-backend (push) Successful in 1m16s
Deploy Trade-In / deploy (push) Successful in 1m25s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
|
|||
| 4b245b6605 |
feat(tradein/estimate): потолок одновременных оценок — 4 слота, быстрый 429 (#3082)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m41s
Рейт-лимит меряет частоту (300/60с вправе стартовать в одну секунду), квота — счётная и помесячная: параллелизм /estimate не ограничивал никто. Оценка 0.8–2.4с держит соединение общего пула SQLAlchemy (5+10) и внешние тиры — пила одновременных оценок выедала пул и тормозила весь /api/v1/*. Семафор по образцу public/mera.py::_suggest_slots: acquire после дешёвых отказов (рейт-лимит, квота) с ожиданием 5с ≈ две длительности оценки, timeout → 429 с Retry-After; release в finally сразу после дорогой части. 4+4 слота (estimate+suggest) = 8 удерживаемых соединений из 15 пула. Семафор в памяти процесса — при переходе на несколько воркеров (#3083) лимит умножится на их число; задачи согласовывать (о чём комментарий на месте). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 0a85fa7202 |
fix(tradein/observability): результат detail-бэкфиллов доезжает до колонки total_seen (#3044)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
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 4m37s
Прод-факт: замер по avito_detail_backfill прочитал «обогащено 0 за 7 дней» при реальных 801 — джобы пишут результат только ключом 'enriched', которого _column_counts не знал, и колонка total_seen оставалась 0 у всех прогонов. 'enriched'-фолбэк добавлен именно в _column_counts (витринная колонка), а НЕ в _RESULT_COUNTER_KEYS: тот список кормит zero-result-стрик, где догнавший очередь бэкфилл стал бы непрерываемым измеренным нулём — ловушка, за которую ревью уже выкинуло из списка 'rows_inserted' (#2703). Контракт стрик-сторожа закреплён инвариант-тестом. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| d409e31f00 |
feat(mera/b2c): анти-абуз для анонимного трафика — этап 2 из 8 (#2546)
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 3m50s
Deploy Trade-In / build-backend (push) Successful in 4m23s
Deploy Trade-In / deploy (push) Successful in 2m32s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
|
|||
| 9810ae350f |
feat(tradein/deactivate-stale): страховочные рельсы объёма снятия (#3066)
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / test (push) Successful in 3m47s
Deploy Trade-In / build-backend (push) Successful in 1m43s
Deploy Trade-In / deploy (push) Successful in 2m1s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
|
|||
|
|
fb38d657ad |
fix(tradein/deactivate-stale): пол переобхода — LATERAL вместо equality-join по дате
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
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 4m41s
Правка меняет две вещи разом, обе намеренно. 1. Равенство по дате -> «последняя строка не позже якоря». listing_source_snapshots переходит на модель «строка на изменение». В ней equality-join теряет 95.6% пар (на 2026-08-20 изменениями являются 4458 строк из 101795): выборка для percentile_disc схлопывается до n=3-4, квантиль вырождается в максимум из трёх чисел, TTL обваливается — domklik 28->14 (под снятие сразу 128 активных строк), yandex 44->30, avito 13->10. Дыры в суточной истории ломали equality-join и до перехода: на проде 03-14.06 (12 суток подряд), 03-04.07, 12.07, 26.07, 30-31.07, 01.08 — там n_pairs=0, floor_days=NULL и TTL молча оставался как задан. 2. Глобальный max(snapshot_date) -> максимум внутри источника. Старый подзапрос брал максимум по ВСЕЙ таблице, не скоупленный по listing_source_id: источник, чья история короче общей, выпадал из выборки целиком. LATERAL ищет предшественника по строке. Замер на живом проде 2026-08-23 (health_window_days=3): обе формы дают побитово одинаковый результат на всех четырёх источниках — avito n=765 пол=49.7, cian n=1226 пол=81.6, domklik n=565 пол=35.7, yandex n=3856 пол=87.3. Сегодня суточная джоба пишет строку для каждого источника каждый день, поэтому глобальный максимум совпадает с максимумом каждого источника, и пункт 2 — no-op на текущих данных. Расхождение проявится только на дырах и после перехода на change-only. Плюс floor_n_pairs в counters — наблюдательность для будущего гейта деградации пола, сейчас ничего не блокирует. FROM..WHERE вынесен в _revisit_floor_from_where_sql, чтобы count(*) и percentile_disc гарантированно шли по одному срезу. |
||
|
|
5b447ec33d |
feat(tradein/estimate): внешние оценки не ждутся в запросе — 9 секунд превращаются в 1
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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 4m42s
Замер на проде 2026-08-22, разбивка одного расчёта по логам:
10.659 старт
10.827 дом найден 0.17 с
11.565 аналоги, 49 кандидатов 0.74 с
11.579 ДКП-коридор 0.01 с
17.576 yandex_valuation ← 6.0 с
19.159 cian_valuation ← 1.5 с
19.252 готово итого 8.6 с
Семь с половиной секунд из восьми с половиной — ожидание чужих HTTP. Наша база
и сам расчёт укладываются в секунду. По уже виденному адресу (кэш 24 ч) — 0.4-0.8 с,
по новому — 8-12.4 с. Геокодинг ни при чём: с готовыми координатами те же 7.5-10.8.
Публикация в РБК 30.08 приведёт аудиторию на НОВЫЕ адреса, то есть мимо кэша.
Масштабирование контейнеров тут не помогает: время уходит на ожидание чужого
ответа, а не на наши вычисления.
Что сделано: у обоих источников появился режим «только кэш» (fetch_on_miss).
При включённом ESTIMATE_EXTERNAL_SOURCES_BACKGROUND запрос делает лишь чтение
кэша (локальный запрос на миллисекунды), а свежая загрузка уходит в фон и
наполняет кэш к следующему обращению по тому же адресу.
Почему это безопасно: деградация источника в None — НЕ новое состояние ответа.
Ровно так же ведёт себя таймаут estimate_*_valuation_timeout_s, и этот путь
работает в проде сегодня. Контракт API не меняется.
Фоновая задача берёт СВОЮ сессию: сессия запроса закрывается вместе с ответом,
а обе функции источников делают внутри себя db.commit() — переиспользование
чужой сессии зафиксировало бы её незавершённую работу. По той же причине
отвергнут наивный asyncio.gather двух источников на одной сессии.
Очередь догрузки ограничена восемью задачами. Без потолка всплеск по новым
адресам — ровно тот случай, ради которого режим и сделан — породил бы сотни
параллельных задач с сессиями и HTTP-клиентами при max_connections 100 и
mem_limit 768m у backend, то есть отказ вместо ускорения.
Дефолт в коде False: поведение других окружений не меняется. На проде режим
включён через docker-compose.prod.yml у сервиса backend.
Отдельно НЕ сделано, хотя предлагалось: снижение таймаутов до 4 с. Замер
показал, что свежий запрос к Яндексу занимает 6 с — таймаут 4 обрывал бы его
почти всегда, кэш бы не наполнялся, и источник оказался бы тихо отключён.
Таймаут здесь страховка от патологии, а не регулятор задержки.
Тесты: 7 штук на режим «только кэш», собственную сессию, гашение ошибок,
удержание ссылки на задачу и потолок очереди. Фальсифицированы — на неизменённом
коде падают 5 из 7 (проходит только сторож неизменности дефолта). Смежные
тесты оценщика (29 штук: бюджет ЦИАН, клиентские координаты, аудит) зелёные.
|
||
|
|
fa1399d59e |
fix(tradein/avito): бэкфилл простаивал 23 часа из 24 — каденс был суточным
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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 4m47s
Замер 2026-08-22, уже после починки транспорта (#3049): обогащение работает, но очередь не разбирается. За сутки 238 карточек при 9 951 активном объявлении. Причина в планировщике, а не в скрапинге. `compute_next_run_at` имеет суточную гранулярность по построению — `interval_days = max(1, int(...))`, целевая дата `now + interval_days`. Меньше суток не выражается. Бэкфилл при этом умирает по бану через 17-83 минуты, то есть работал около часа в сутки, а остальное время расписание ждало следующего дня. Механизм sub-hourly каденса уже был написан — `reschedule_after_minutes` (#2162, сделан для proxy_healthcheck). Его просто не подключили к бэкфиллу. Хук ставит next_run_at = now() + interval_minutes сразу после claim и сам себя тормозит: пока прогон идёт, has_running_run в _claim_run возвращает None. 180 минут — осознанно консервативная отправная точка, НЕ найденный оптимум. Данных для подбора нет, и имеющиеся два прогона противоречат наивному ожиданию: 4562 дал 175 карточек за 83 минуты, а 4586 через 4.3 часа — когда пул прокси был давно чист — умер за 17 минут с 42 карточками. Значит память Авито длиннее часов, и учащение может ухудшить выход. Отдельно держать в голове при подборе: те же 4 прокси обслуживают SERP-свипы, то есть первичный сбор. Сжечь их на обогащении хуже, чем медленно обогащать. Двигать интервал вниз только по замеру нескольких суток, глядя и на свипы. Подбор — через default_params расписания, правка кода для этого не нужна. Тесты закрепляют подключённость хука и коридор дефолта, а не конкретное значение: 180 будет двигаться, а вот утрата хука вернёт суточный простой молча. Проверено фальсификацией — на неизменённом коде все три падают. |
||
|
|
b61123d7a6 |
fix(tradein/avito): detail_backfill ходил curl'ом, который не решает PoW-челлендж
All checks were successful
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 / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m32s
Замер на проде 2026-08-21: Авито за QRATOR отдаёт JavaScript proof-of-work
челлендж (startPow → кука pow_solved → self-reload через window.location).
curl_cffi не исполняет JS и пройти его не может в принципе.
Что это давало в цифрах (counters прогонов avito_detail_backfill):
run 4508: enriched 3 / attempted 46
run 4394: enriched 2 / attempted 47
run 4348: enriched 4 / attempted 53
~6% успеха. Браузерный путь на том же проде — ~74% (17/23 в инлайн-обогащении
свипа, 2/3 в точечной проверке после #3048). То есть задание работало почти
вхолостую, а поля дома, которые чинил #3048, до карточек просто не доходили.
Исходное обоснование AVITO_DETAIL_BACKFILL_USE_CURL=true (browser открывает
десятки коннектов и превышает cap прокси-аккаунта auv) больше не действует:
браузер сериализован BROWSER_CONCURRENCY=1 и ходит через тот же backconnect
SCRAPER_PROXY_URL, что и curl-путь.
Переключение сделано в docker-compose.prod.yml (environment перекрывает
env_file), а не правкой рантайм-env на хосте — чтобы изменение осталось в
репозитории и пережило deploy без дрейфа конфигурации. Дефолт в config.py
оставлен True: другие окружения не трогаем вслепую, но устаревшее обоснование
там помечено.
|
||
|
|
fe628a5e17 |
fix(tradein/scraper-kit): actualize avito fingerprint - impersonate/Sec-Fetch-Site/referer/cookie-check
Живой замер браузера 2026-08-21 разошёлся с тем, что шлёт прогретая сессия:
- impersonate="chrome120" был захардкожен в 9+ местах (avito/{serp,detail,imv,
houses}.py, pipeline.py x4, cian/valuation.py, yandex/valuation.py) вместо
единой DEFAULT_IMPERSONATE (providers/_base.py). Обновление до chrome146
(макс. доступный профиль curl_cffi 0.15.0; alias "chrome" НЕ используется -
едет сам при апгрейде библиотеки без ревью) теперь меняется в одном месте.
Guard-тест backend/tests/test_impersonate_single_source.py грепает всё
дерево scraper_kit на литерал "chrome120".
- DOCUMENT_HEADERS ставил Sec-Fetch-Site="none" на уровне сессии, а Referer
добавлялся per-request (curl_cffi мёржит per-request headers поверх
session-level) - живой Referer с "none" рядом не бывает у настоящего
Chrome. Добавлен referer_headers() (Sec-Fetch-Site="cross-site") для
caller'ов с чужедоменным Referer; avito warm-up (yandex/ya.ru -> avito)
теперь его использует. Внутренний avito-search -> avito-detail Referer
(fetch_detail, same-origin) НЕ тронут - отдельный явный комментарий почему.
- Referer прогрева заменён с yandex.ru на ya.ru (живой переход из выдачи
даёт короткий домен). ysclid сознательно не добавлен - значение выпускает
Яндекс, подделка хуже отсутствия.
- warm_up_session/research_in_session считали прогрев успешным по HTTP-
статусу и отсутствию firewall-маркеров, не проверяя антибот-cookies
(__zzatw-*/cfidsw-*, сняты с живого браузера) - detail-батч на такой
"прогретой" сессии сжигал прокси на обречённых 403. Теперь поднимают
AvitoWarmupCookiesMissingError (наследник AvitoBlockedError - существующие
except-блоки/ban_kind/proxy-ротация в pipeline и backfill ловят без
изменений).
Полный backend pytest suite зелёный (4672 passed), ruff check чист.
Refs #3034
|
||
| cdf493f345 |
chore(format): нормализация под ruff 0.15.20 — 161 файл, только формат (#2864) (#3022)
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy Trade-In / changes (push) Successful in 13s
Deploy / build-frontend (push) Has been skipped
Deploy / deploy-caddy (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy / build-backend (push) Successful in 2m23s
Deploy Trade-In / test (push) Successful in 3m56s
Deploy / build-worker (push) Successful in 4m16s
Deploy Trade-In / build-backend (push) Successful in 1m19s
Deploy / deploy (push) Successful in 1m49s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 12s
Deploy Trade-In / deploy (push) Successful in 2m25s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
|
|||
| 507c8e0611 |
fix(tradein): неудача резолва yandex_jk_slug помечает дом — очередь не упирается в одни и те же пять (#2924)
Some checks failed
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Failing after 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m25s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
(полное описание — в PR) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 6cdf56820d |
fix(tradein): апсерт не переписывает неизменившуюся строку — гейт IS DISTINCT FROM + МСК-день (#2992)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m15s
(полное описание — в PR) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 865e64b783 |
feat(tradein/estimator): штраф за неизвестный год/тип дома — флагом, default OFF (#2936)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m14s
В SQL-формуле relevance_score кандидат без year_built получает штраф 0 — столько же, сколько точное попадание в год, и лучше, чем кандидат с известным годом, отличающимся на 24 (2.0). То же с house_type. Отсутствие данных выигрывает у знания и возвышает источник с худшей полнотой. Флаг estimate_unknown_attr_penalty_enabled (default OFF): кандидат с NULL получает МЕДИАННЫЙ по пулу штраф того же признака среди тех, у кого он известен — не наказание и не награда; считается по тем же термам, что SQL (abs(Δyear)/12.0, 1.5 за несовпадение типа). Лежит в Python-слое после SQL, рядом с kitchen/ceiling (#2012), по тому же контракту: включать — только по бэктесту. Без чего флаг был бы мёртв (и был в первой редакции): _ANALOG_SELECT_COLS не выбирал year_built/house_type — SQL считал по ним CASE, но в словарь кандидата колонки не попадали, Python-слой видел None у ВСЕХ и не штрафовал никого по построению. Probe-лог в прод-оверлее: pool=30 null_year=30. Добавлены в _ANALOG_SELECT_COLS, во внешние SELECT тиров H/W и во внутренний base Tier W (он строится явным списком). Контроль: флаг OFF с колонками и без — метрики бэктеста идентичны до сотых. На этот инвариант стоит тест по исходнику запросов. Живой A/B (бэктест в прод-контейнере, оверлей /tmp/ab, 300 сделок ЕКБ Q2 2026, одна и та же выборка в обоих прогонах — проверено по deal_id): состав топ-50: сменилось 96 слотов из 5 915 (1.6 %), 27 сделок из 300 источники: avito 55.7→55.3 %, cian 24.5→24.8 %, yandex 12.7→12.6 % цена: MAPE 16.70→16.70, bias −4.52→−4.52, coverage 84.46→84.46 — идентично до сотых по всем срезам (сегменты, комнатность). Нижний Тагил (300 сделок, пулы 21/p90 40): 0 из 5 666 слотов сменилось. Почему эффект в разы меньше замера задачи (×0.20 avito): тот замер шёл по SQL тира H без стратификации. В боевом пути 54.9 % слотов топ-50 — гарантированная квота MIN_ANALOGS_PER_SOURCE=5, раздаётся ДО сортировки остатка; и у avito в топ-50 NULL-год лишь у 25.7 % (задача мерила 56 % по всем активным объявлениям). Обе величины измерены по фикстурам A/B. Что это значит: артефакт в формуле есть, флаг его корректно лечит, но на итоговую цену он не влияет измеримо. Включать по умолчанию оснований нет — и это и есть ответ, ради которого флаг заводился вместо правки. pytest tradein-mvp/backend: test_2936 7 passed; гейт фикстуры и roundtrip 8 passed; -k "estimat or analog" 735 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| a003fdd37c |
feat(tradein): ручка «дома, уехавшие от собственных объявлений» (#2996)
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
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 3m27s
Deploy Trade-In / build-backend (push) Successful in 1m4s
Deploy Trade-In / deploy (push) Successful in 2m10s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
Часть Находки 3 из #2996. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| fab0bc84c1 |
feat(tradein): ручка «дома, уехавшие от собственных объявлений» (#2996)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m49s
Находка 3 задачи («23 дома лежат вне области 66, гарда на приёме нет») подтвердилась, и к ней добавились масштаб и причина. Масштаб: к этим домам привязано 591 объявление. Адреса екатеринбургские («Ул. 8 Марта», «Крауля», «Амундсена»), координаты — Варшава, Белград, Таллин, Владивосток, Ижевск. Причина: 22 из 23 несут в raw_payload след разового бэкфилла `full_backfill_2026-05-27`, который звал Yandex-геокодер напрямую, без резолва города. Живой путь при этом ЧИСТ — 0 записей вне региона в geocode_cache из 10 448 и 2 из 88 544 у listings; скрипта в репозитории нет. Это исторический осадок, а не текущая утечка, и гард нужен не столько живому пути, сколько следующему разовому скрипту. 17 из 22 помечены геокодером `precision: "exact"`. Точность отвечает на «нашёлся ли номер дома», а не «в том ли городе», и критерием приёмки быть не может — поэтому проверяется ПРИНАДЛЕЖНОСТЬ. Инвариант нарочно не географический: «дом рядом со своими объявлениями», а не «дом внутри рамки области». Рамка сломалась бы при выходе в Москву — ровно то, ради чего заведена #2996. Он же строго сильнее: ловит 25 домов против 23 у рамки, и оба лишних проверены («Ул. Белинского/Фурманова» за 207 км, 34 объявления). Порог 100 км не подобран на глаз. Замер по 9 052 домам: ближе 1 км — 8 914 (98.5 %), 25-100 км — 9 настоящих пригородов (Сарапулка, Кедровка, Чусовское Озеро, Ревда, Первоуральск; самый дальний 46.7 км), дальше 100 км — 25 (ближайший 119.2, максимум 5 079). Между 46.7 и 119.2 км нет НИ ОДНОГО дома: порог лежит в середине пустого промежутка. Первая редакция клала счётчик полем в /scraper/data-quality. Замер это остановил: запрос стоит ~445 мс на тёплом кэше, а обе существующие выборки той ручки вместе — 27 мс, при опросе фронтом каждые 120 с. То есть 17-кратное удорожание ради числа, которое меняется раз в месяцы. Проверка вынесена в отдельную ручку по требованию, и на возврат в горячий путь поставлен контроль-тест. Ручка отдаёт не только счётчик, но и масштаб (список домов + сколько объявлений привязано) — по одному числу «25» решение об очистке не принять. Двусторонне: против origin/main три теста красные, краснота везде по значению — ни одного ImportError/AttributeError. Контроль test_check_stays_out_of_the_polled_endpoint зелёный с обеих сторон. pytest tradein-mvp/backend — 4642 passed, 23 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| b9cfdaa040 |
feat(tradein): secondary_only — параметр расписания, выброшенное считается (#1781)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / browser-tests (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 4m27s
`run_cian_full_load` передавал `secondary_only=True` жёстко, поэтому включить новостройки в полный обход можно было только деплоем. Теперь это параметр с ТЕМ ЖЕ дефолтом `True` — поведение прода не меняется ни на строку, но решение становится правкой одной ячейки `scrape_schedules.default_params`, а не выкаткой кода. Откат — тем же движением. Почему это важно именно здесь. Новостройки НЕ пропускаются при запросе: они скачиваются, разбираются и выбрасываются последним шагом (`cian/serp.py`), потому что SERP-параметр `object_type=1` у Cian ненадёжен (~5 % выдачи) и фильтруют по authoritative `listing_segment` после парсинга. Проба бакета берёт `totalOffers` из Redux-состояния SERP и считает `pages_needed = ceil(totalOffers / offers_per_page)`, а `totalOffers` включает ОБЕ категории — то есть страницы с новостройками уже скачаны, лимит страниц и антибан-бюджет за них уже заплачены. Включение стоит ноль дополнительных запросов. Заодно `dropped_novostroyki` сохраняется в counters прогона. Счётчик логировался (`dropped_nb=`), но не персистился, и ответить «сколько инвентаря выбрасывает полный обход» задним числом было нечем: логи за 17.08 уже ротировались — `docker logs --since 120h` не находит ни строки «cian:» ни в одном контейнере. Тот же довод, по которому рядом заведён `partial_buckets`. Копится в атрибуте инстанса, а не аргументом `on_bucket`: у колбэка есть внешние реализации, менять его сигнатуру ради счётчика нельзя. Сброс на каждый прогон — инстанс переиспользуется. Замер, ради которого это делается (прод 21.08): месячный охват свипа cian/novostroyki — 11.7 % против 100 % у cian/vtorichka и avito/novostroyki; 11 993 активные строки, медианный возраст 81 сутки, 10 585 старше 30 суток. Подробности и оговорки — в #1781 и #2994. Двусторонне: против origin/main пять тестов красные, и краснота везде по значению, а не по отсутствию символа — ни одного KeyError. Сообщения перечисляют фактическое состояние («параметра нет в сигнатуре; параметры: [...]», «поля нет в запросе; поля: [...]»). Контроли зелёные с обеих сторон: дефолт остаётся True (иначе правка тихо включила бы сбор новостроек на проде — это отдельное решение с замером); фильтр при `secondary_only=True` остаётся на месте и по-прежнему зависит от флага. pytest tradein-mvp/backend — 4644 passed, 23 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 3b40ba09f7 |
fix(tradein/geocoder): темп Nominatim сдерживается перед запросом, а не после успеха (#2953) (#2954)
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 3m32s
Deploy Trade-In / build-backend (push) Successful in 1m20s
Deploy Trade-In / deploy (push) Successful in 1m36s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 9s
|
|||
| cf48e6d6c8 |
feat(tradein): позиция квартиры внутри когорты аналогов — перцентиль (#2899) (#2926)
All checks were successful
Deploy Trade-In / changes (push) Successful in 15s
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 3m54s
Deploy Trade-In / build-backend (push) Successful in 1m7s
Deploy Trade-In / deploy (push) Successful in 2m6s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
|
|||
| d0071c57bc |
fix(tradein): фильтр выдачи Яндекса мёртв — slug ЖК ищем по странице объекта (#2860) (#2923)
All checks were successful
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 9s
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m50s
Deploy Trade-In / build-backend (push) Successful in 1m41s
Deploy Trade-In / deploy (push) Successful in 1m42s
|
|||
| 215b78a348 |
Merge pull request 'fix(tradein/estimate): залипший anchor_tier больше не глушит четыре механизма разом' (#2920) from fix/2656-anchor-tier-reset into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 12s
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 3m45s
Deploy Trade-In / build-backend (push) Successful in 1m10s
Deploy Trade-In / deploy (push) Successful in 1m57s
Deploy Trade-In / deploy-status (push) Successful in 1s
|
|||
|
|
42b0ebe338 |
fix(tradein/estimate): залипший anchor_tier молча глушил IMV-blend и quarter-index
All checks were successful
CI Trade-In / browser-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 / 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 Trade-In / backend-tests (pull_request) Successful in 4m32s
Флаг anchor_tier оставался равным anchor_tier_fetched ("A"/"C"), когда якорь
фактически НЕ строился — сброс делал только low-conf гейт (#audit-1), но не
Tier C corridor-гейт (#1795) и не сама _compute_same_building_anchor, когда
она отклоняет кандидата (комплов меньше estimate_sb_min_comps). Дальше по коду
залипший флаг читается как «headline построил якорь» и молча глушит IMV/Yandex
blend (#651, гейт `anchor_tier is None`) и quarter-index correction (#764
Guard-1a) — притом что радиусный headline их не получал.
Замер: 154 из 996 сделок теряют tier-флаг этой правкой, и у всех 154 изменение
цены ровно 0.000% — чинится именно залипший ФЛАГ, не ценообразование (баланс
метрик бэктеста подтверждает: единственная дельта в baseline — новая канарейка
unrecorded_lookup_calls, все остальные метрики побитово те же).
- estimator.py: сброс `anchor_tier = None` единой веткой `if anchor is None`
после всех трёх гейтов (Tier C / low-conf / _compute_same_building_anchor);
display-only IMV-карточка больше не гейтится по `anchor_tier is not None`
(иначе терялась в щели «тир добыт, якорь не построен, headline подавлен»).
- backtest_estimator.py: quarter_index_lookup/quarter_indexes_lookup в реплее
отвечают «промах» (None/{}), если сброс флага открыл путь, которого не было
в замороженной фикстуре, вместо падения с RuntimeError; счётчик таких промахов
уходит в baseline как unrecorded_lookup_calls (точное целое, канарейка на
расхождение реплея с захватом). Заодно пиннится estimate_dedup_analogs_enabled
= False внутри replay_fixture (было только в самом гейте) — иначе штатная
регенерация baseline (--from-fixture --update-baseline) писала baseline,
который тест не совпадал бы никогда.
- backtest_baseline.json: перегенерирован штатным путём, unrecorded_lookup_calls=0.
Выделено из #2656/PR #2661 — фильтры свежести (scraped_at) в якоре дома и в
знаменателе коэффициента выкупа остаются в исходном PR как отдельная, более
спорная правка (двигает деньги: знаменатель просаживается на ~1.3% по бакетам).
|
||
|
|
c66047683c |
Merge remote-tracking branch 'forgejo/main' into local/pr2836
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 4m36s
|
||
|
|
73c4c487ed |
fix(mera/b2c): житель Серова получал «вы вне области», а подсказки игнорировали выбранный город
All checks were successful
CI / changes (pull_request) Successful in 10s
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 4m50s
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Successful in 48s
Два дефекта, найденных прогоном сценария глазами посетителя на живом домене. ## 1. Город предлагали выбрать, но отвечать по нему не умели Дропдаун на сайте (`OBLAST_CITIES`, city-registry.ts) и списки покрытия (`COVERAGE_GREEN/YELLOW_CITIES`, trade_in.py) — одно множество, записанное в двух местах. Они разошлись в обе стороны: предлагали, но не отвечали: Серов отвечали, но не предлагали: Берёзовский, Среднеуральск, Ревда Житель Серова выбирал СВОЙ город из НАШЕГО дропдауна и получал: «Этот адрес вне области, по которой мы собираем данные. Сейчас это Свердловская область: Екатеринбург целиком и ещё несколько городов вокруг.» Про город в той же самой области. Серов при этом покрыт данными: 363 активных объявления в радиусе 15 км, все свежие (замер по проде). Поэтому добавлен в жёлтый тир, а не убран из дропдаунa; три недостающих города добавлены на фронт. Шапка city-registry.ts этот риск прямо предсказывала — «перед добавлением 7-го города сверить оба списка вручную, теста на это пока нет». Теперь тест есть: бэкендовый сьют читает TS-реестр и требует РАВЕНСТВА множеств. Плюс проверка, что у каждого города с порогом есть центроид, — иначе порог мёртвый, город по координатам не резолвится. ## 2. Подсказки не слушались выбранного города `city_hint` доезжает до геокодера, но на выдачу не влияет: его смотрит только екатеринбургский кадастровый тир (как признак «речь не про ЕКБ, тир пропускаем»), а DaData-тир ограничен регионом целиком и хинта не принимает. Замер: выбран Серов, введено «Ленина 1» → первой подсказкой «Невьянский р-н, пгт Верх-Нейвинский». Человек выбирает верхний вариант и считает чужой дом — ровно баг #2576, ради которого город и спрашивают. Публичная ручка теперь подставляет город в саму строку запроса. Проверено на проде: «Серов Ленина 1» даёт серовскую выдачу целиком. Для Екатеринбурга подстановка безвредна — три разных адреса дали тот же результат с префиксом и без, поэтому правило одно на все города, без исключения для основного трафика. Чинится в публичной ручке, а не в геокодере: там от `city_hint` зависит поведение закрытого контура (`target_city_ambiguous`). ## Фикстура теста `_FAR_AWAY_CITY` стояла в 21 км от центра Серова и работала как «далеко от всех» лишь потому, что Серов не был поддержан. Переехала в Тавду — 271 км до ближайшего центроида. ## Мутации убрать Серов из покрытия (состояние прода) → падает сверка списков не подставлять город в строку → падает проверка ручки откат → 21 passed Плюс backend 75 passed, vitest 56 passed, tsc, lint, build, isolation guard. `city-registry.ts` добавлен в paths-фильтр БЭКЕНДОВОГО лэйна: сверку списков делает бэкендовый тест, и без этой строки правка одного лишь дропдауна её бы не запускала — то есть ровно тот путь, которым списки и разошлись. |
||
|
|
56194c606a |
fix(mera/b2c): семь дефектов публичного периметра, найденных состязательным ревью
Some checks failed
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Failing after 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 / browser-tests (pull_request) Successful in 46s
CI Trade-In / frontend-checks (pull_request) Successful in 1m3s
CI Trade-In / backend-tests (pull_request) Successful in 4m42s
Ревью четырьмя независимыми линзами (периметр, семантика Caddy, политика ПДн
против кода, фронт) + по два проверяющих на каждую находку. Ниже — то, что
пережило проверку и воспроизведено на живом коде, а не выведено из чтения.
## Caddy: открытый редирект и потерянные ссылки
Захват хвоста регекспом (`^/trade-in/mera-public/(.+)$` → `redir /{re…1}`) —
открытый редирект. Захват берётся из РАСКОДИРОВАННОГО пути, поэтому
`/trade-in/mera-public/%5Cevil.example/pay` даёт цель `/\evil.example/pay`, а
браузеры трактуют `/\` как `//` — Location уводит на чужой хост. Готовая
фишинговая заготовка с домена, который напечатан внутри оферты и уходит
модератору эквайера. Заменено поимённым списком путей: такой адрес просто не
матчится.
Адреса со слэшем на конце (`/oferta/`, и длинные `…/oferta/`) отдавали 404 —
ровно те ссылки, ради сохранности которых редирект и делался. Добавлена
нормализация, цепочка замкнута (проверено: 2 перехода → 200).
Query-строка терялась: размещённые ссылки с UTM приходили бы в аналитику как
прямой заход. `uri strip_prefix` + `{uri}` переносит её. Обёртка `route`
обязательна — без неё `redir` выполняется раньше `uri` и Location равен
исходному адресу (бесконечный цикл, поймано на стенде).
`/v3` — черновое превью с маркетинговыми плейсхолдерами — было открыто на
боевом домене молча. Теперь названо вслух и запинено тестом.
## Гейты, которых не было
`caddy validate` не звал НИ ОДИН workflow, а deploy применяет конфиг не через
`reload` (тот отказался бы принять битый), а через `up -d --force-recreate` —
опечатка уводит контейнер в crash-loop и роняет ВСЕ домены. Добавлен гейт в
ci.yml, тем же образом caddy:2, что и на проде.
Проверка «роут ↔ Caddy» была односторонней и пропускала обратную ошибку —
путь, открытый наружу, о котором приложение не знает. Так и уехал `/v3`.
Теперь двусторонняя, плюс проверка, что для каждой страницы есть 301.
## Бюджет внешнего геокодера
Per-IP окна ограничивают одного клиента, но не сумму: 40/мин с адреса — это
57 600 в сутки при бесплатном тире DaData в 10 000, ОБЩЕМ с закрытым контуром.
Подтверждено на проде: достаточно упомянуть не-екатеринбургский город, чтобы
локальный тир отключился и запрос гарантированно ушёл во внешний сервис. То
есть один скрипт оставлял без подсказок платящих пилотов.
Per-IP снижен до 20/мин, добавлен общий суточный потолок 2000 и потолок
одновременных подсказок (4): кадастровый тир уходит в FDW-скан чужой базы,
держит соединение около секунды, а пул общий с B2B — полтора десятка
параллельных публичных запросов клали бы закрытый контур.
## «Адрес нигде не сохраняется» — теперь правда целиком
Две утечки, обе воспроизведены:
1. ЖУРНАЛЫ. Геокодер печатает введённую строку открытым текстом на каждый
вызов, прод пишет stdout в persistent journald — адрес ложился на диск
рядом с IP того же запроса в access-логе Caddy. Закрыто фильтром логов на
время публичного запроса (contextvar, переживает await и to_thread).
Закрытый контур логи сохраняет: они нужны для разбора жалоб пилотов.
2. МОНИТОРИНГ. sentry_sdk кладёт в событие ПОЛНОЕ тело запроса — а тело
публичной ручки это ровно `{"q": "<адрес>"}`; `send_default_pii=False` тут
не гейт, он про куки. Плюс брэдкрамб httpx несёт адрес в query геокодера.
Закрыто `scrub_public_address`.
Текст п. 5.4 политики расширен до «ни в журналы веб-сервера, ни в технические
журналы, ни в мониторинг» — ровно то, что теперь обеспечено кодом.
## Фронт
- Отмена запроса подсказок откладывалась внутрь следующего debounce-такта и
не наступала вовсе, если человек переставал печатать: ответ по старой строке
долетал и ложился в список. Контроллер создаётся сразу, отменяется в cleanup.
- Список схлопывался на каждое нажатие — клик по намеченному пункту
промахивался. Старая выдача висит, пока не пришла новая.
- «Комнат» с лэндинга — свободный текст: «студия» не совпадала ни с одним
option, селект показывал пустоту, parseInt давал NaN, на сервер уходил
rooms: null → 422 с текстом «сломалось на нашей стороне». Нормализация
вынесена чистой функцией и покрыта тестами.
- У пробы покрытия не было ни таймаута, ни отмены: оборванное соединение
оставляло кнопку в «Смотрим данные…» навсегда. 15 с + понятный текст.
- Ошибка подсказок глушилась в пустой список — тупик без объяснения.
- Комбобокс: Tab проваливался в кнопки подсказок, список не закрывался по
уходу фокуса и перекрывал поля, Escape оставлял висячий aria-activedescendant.
## Проверено
Локальный стенд (реальный site-блок Caddy + заглушка): 18 маршрутов, включая
`%5C`, `//`, `%2F` — все три теперь 404. vitest 55 passed, backend 17 passed по
публичному API, tsc, lint, build, isolation guard 41 файл, caddy validate.
Мутации: снять редакцию логов → падает тест журналов; не вырезать тело запроса
→ падает тест мониторинга; убрать /estimate из Caddy → падает тест маршрутов.
|
||
|
|
b1fb7bb055 |
Merge remote-tracking branch 'forgejo/main' into feat/mera-public-api
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Successful in 46s
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 1m7s
CI Trade-In / backend-tests (pull_request) Successful in 4m45s
# Conflicts: # tradein-mvp/backend/app/core/rbac.py |