169 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| dab2b43efc |
imv: HTTP 429 больше не приговаривает дом навечно
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-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 / changes (pull_request) Successful in 11s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m50s
Алерты 01.09 (house_imv: RuntimeError('HTTP 429') / ('HTTP 400')) вскрыли
дыру в классификаторе: _raise_for_status_categorized разбирает 401/403 и 5xx,
а ВЕСЬ остальной 4xx проваливается в resp.raise_for_status() и приезжает в
house_imv_backfill голым RuntimeError. Бэкфилл типизирует только
IMV*-исключения — дом получает ТЕРМИНАЛЬНЫЙ imv_status='error' и выпадает из
повторных пакетов навсегда.
429 — канонический ВРЕМЕННЫЙ отказ (rate limit), и механика повтора для таких
существует (transient_error + retry-lane + лимит попыток #2674). Замер на
проде: 19 домов заперты в error с причиной «HTTP 429» — ретраебельный отказ
стал вечным приговором.
429 и 408 теперь IMVTransientError. 400 НАМЕРЕННО оставлен терминальным: тело
безликое {"code":400,"message":"Bad Request"}, оснований считать его
временным нет, а ретраебельный 400 значил бы вечно долбить дома с реально
кривыми параметрами. Тест держит границу С ОБЕИХ СТОРОН — и «429 transient»,
и «400 НЕ transient».
Фальсификация: снятие ветки 408/429 даёт 2 failed по значению
(IMVTransientError не поднят), не ImportError. 9 passed, ruff чисто.
Ремонт уже запертых строк — отдельным шагом после мержа: UPDATE 19 домов
error→transient_error (починка разбора не чинит строки сама).
|
|||
| 72cb410b05 |
fix(tradein/avito): бан-страница площадки уезжала в транспортные сбои и не банила узел (#3283) (#3304)
Some checks are pending
Deploy Trade-In / test (push) Blocked by required conditions
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / build-frontend (push) Blocked by required conditions
Deploy Trade-In / build-browser (push) Blocked by required conditions
Deploy Trade-In / deploy (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 13s
Co-authored-by: lekss361 <lekss361@gendsgn.local> Co-committed-by: lekss361 <lekss361@gendsgn.local> |
|||
| 9219ab6507 |
feat(tradein/avito): ротация exit-IP по счётчику попыток в доборе карточек (#3302)
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) Has been skipped
Deploy Trade-In / test (push) Successful in 4m8s
Deploy Trade-In / build-backend (push) Successful in 1m56s
Deploy Trade-In / deploy (push) Successful in 1m43s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
|
|||
|
|
5c10aee4c2 |
fix(tradein/avito): browser-путь добора не смотрел на HTTP-статус — отказ площадки уходил в ошибку парсинга (#3297)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
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 5m4s
CI Trade-In / browser-tests (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
Замер прода 31.08.2026 (24 попытки через 3 узла) поймал три отказа, которые
доезжали до parse_detail_html и падали ValueError("Cannot extract item_id"):
страница 8172 байта, статус 439, title «Доска объявлений от частных лиц и
компаний на Авито». Отказ ПЛОЩАДКИ записывался generic-ошибкой разбора, узел
не ротировался и не банился, брейкер по доле его не видел.
Две независимые дыры, обе в browser-ветке fetch_detail:
1. last_response_status не читался вовсе. Curl-ветка того же файла статус
проверяет (`if sc in (403, 439) or is_firewall`), browser-ветка смотрела
только на HTML. Прод ходит именно browser-путём.
2. Маркер витрины-заглушки протух: искали «объявления на сайте авито», а
фактический title — «доска объявлений от частных лиц и компаний на авито»,
подстрока в нём не встречается.
Правка:
- новые константы _AVITO_DETAIL_BROWSER_BLOCK_STATUSES = {403, 439} и
_AVITO_DETAIL_BROWSER_RATELIMIT_STATUS = 429, источник каждого статуса
назван комментарием;
- проверка стоит ПОСЛЕ _is_detail_not_found (404 остаётся
AvitoListingGoneError) и ДО parse_detail_html;
- статус None (сайдкар старой версии, goto без статуса) отказом НЕ считается —
поведение прежнее, фолбэк на html-эвристики;
- 429 разведён с блокирующими статусами и поднимает AvitoRateLimitedError.
Разница не косметическая: на AvitoBlockedError оркестратор один раз за прогон
зовёт request_context_reset (#3251) и выбрасывает пройденный QRATOR-PoW. При
rate-limit контекст цел, сбрасывать его — значит проходить проверку заново с
того же IP. Зеркалит curl-ветку, где 429 тоже не блок;
- старый title-маркер не удалён, а дополнен снятым вживую: площадка может
отдавать обе формы.
Тесты (9): каждый статус по отдельности, None-статус не ломает разбор и не
подавляет html-эвристики, 404 побеждает блокирующий статус (порядок проверок),
оба title-маркера опознаются, 429 не является AvitoBlockedError.
Фальсификация: без правки detail.py 5 из 9 новых тестов падают.
Прогон: 397 passed, 1 skipped (-k "avito or cadence or scheduler"), ruff чист.
Замер намеренно жёстче прода (без прогрева сессии и органического перехода из
выдачи), поэтому доля таких отказов в проде из него НЕ следует — её покажет
счётчик после правки.
|
||
|
|
657fdbd1ba |
Merge remote-tracking branch 'forgejo/main' into fix/3267-block-marker-in-listing-text
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 4m51s
|
||
| 169121cea8 |
Merge pull request 'fix(tradein/avito): браузерный путь ходил на каждую карточку холодным (#3251)' (#3267) from fix/3180-avito-warm-context 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 2m19s
Deploy Trade-In / test (push) Successful in 4m11s
Deploy Trade-In / build-backend (push) Successful in 2m9s
Deploy Trade-In / deploy (push) Successful in 1m50s
Deploy Trade-In / deploy-status (push) Successful in 2s
Deploy Trade-In / perimeter-smoke (push) Successful in 13s
|
|||
|
|
aec05280fa |
fix(tradein/domclick): «система защиты от протечек» в объявлении читалась как блок QRATOR
Some checks failed
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 12s
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) Failing after 7m5s
_extract_json искал маркеры блока подстрокой по ВСЕМУ телу ответа — до
разбора JSON, то есть и по пользовательским описаниям объявлений.
Прод 30.08, прогон 5363: продавец написал в описании квартиры «🔹 Система
защиты от протечек». Подстрока совпала с маркером «система защиты», ответ
на 107 183 байта с двадцатью валидными офферами был объявлен блок-
страницей, свип оборвал все комнатные корзины и забанил живой узел пула
на 6 часов. Совпасть так же могут «captcha» и «qrator» — в тексте
объявления, в имени агентства, в ссылке.
Порядок перевёрнут: сначала разбор JSON, маркеры — только если разбор не
удался. Разобранный JSON нужной формы блок-страницей быть не может,
QRATOR отдаёт HTML, так что валидный разбор сам по себе доказывает
отсутствие блока. Тот же порядок давно применён в detail.py — там
маркеры смотрят только когда __SSR_STATE__ не найден; свип был
единственным местом с обратной логикой.
Мусор без маркеров теперь ValueError, а не блок: за неразобранный ответ
неизвестной природы узел банить нельзя.
Тест проверен мутацией: со старым порядком 8 проверок из 11 краснеют,
включая дословный фрагмент описания из прогона 5363.
|
||
|
|
58e18e6fec |
fix(tradein/avito): браузерный путь ходил на каждую карточку холодным (#3251)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
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 / browser-tests (pull_request) Successful in 1m28s
CI Trade-In / backend-tests (pull_request) Successful in 4m58s
Пройденный QRATOR proof-of-work выбрасывался после каждой карточки: `reuse_context` во всём репозитории передавал ЕДИНСТВЕННЫЙ вызов — domclick_detail_backfill.py:346. Значит каждый /fetch Авито шёл через sidecar'овский browser.new_page(), то есть новый изолированный context с пустой банкой кук. В логе прод-сайдкара это видно прямо: «PoW-челлендж снят за ~1000мс» печатается на КАЖДОЙ успешной карточке — челлендж решается заново каждый раз, а не один раз на прогон. Эффект той же правки у Домклика измерен и записан в domclick_detail_backfill.py:331 — 26 последовательных фетчей сайдкара дали 100% блоков, те же карточки в тёплом контексте 5/5 за ~2с. Второй холод — сам заход: providers/avito/detail.py звал fetch(full_url) голым, без origin и без Referer, тогда как Домклик (#3247) идёт fetch(card_url, origin=SERP, referer=origin). Существующий для Авито прогрев (warm_up_session) живёт только в curl-пути и с 22.08 в проде мёртв — AVITO_DETAIL_BACKFILL_USE_CURL выставлен в false. Что сделано: - avito_detail_backfill: reuse_context=True + request_context_reset() РОВНО один раз за прогон и только на AvitoBlockedError. Не на AvitoSidecarUnavailableError (подтип AvitoRateLimitedError — отказ нашего тракта, не бан площадки) и не на AvitoListingGoneError. Зеркалит #3212: сброс на каждый блок сам себя поддерживает — пропуск живёт в context'е, сброс его выбрасывает, повторная проверка с того же IP снова блокируется, одна осечка даёт каскад. - fetch_detail: необязательные origin/browser_referer (имя referer уже занято под Referer curl-пути, это разные фетчеры и разные поля). Дефолт None → payload и поведение city_sweep/pipeline/admin не меняются. - _serp_origin_for: городская SERP из URL карточки, хост берётся из самого url. Попутно — дефект якорной вкладки сайдкара, найденный при переносе. _ensure_anchor_page отдавала True на ЛЮБУЮ живую вкладку, не сверяя её с запрошенным origin. А origin у обоих caller'ов выводится ИЗ URL карточки и меняется вместе с городом (Авито — сегмент пути, Домклик — поддомен). После первой же карточки другого города Referer называл выдачу, которую этот контекст никогда не открывал: ни куки её, ни тайминга, площадка видит заявленный переход без единого следа. Ровно то, что #3258 запретил делать фолбэкам якорного поиска. Добавлен _anchor_origins: origin сменился — вкладка переоткрывается. Чинит и Домклик тоже. Заход через поиск Яндекса (BROWSER_ANCHOR_VIA_SEARCH) для Авито НЕ включается — расширять этот список без отдельного замера запрещает комментарий у самой константы. Хранилище авторизованных сессий Авито (#3179/#3180) этой правкой не заменяется. База для сравнения снята ДО выката и записана в #3251: доля блоков от попыток 46.9% / 74.2% / 96.4% / 69.8% / 52.9% / 62.3% по суткам 25-30.08. Сравнивать после деплоя по доле блоков и обогащению за сутки, а НЕ по статусу прогона: ratio-критерий обрывает КАЖДЫЙ прогон, и статус banned про площадку ничего не говорит. Тесты: backend 5150 passed / 37 skipped, сайдкар 211 passed (было 206 + 5 новых на переезд якоря), ruff чист. Refs #3251, #3180, #3118, #3212, #3247, #3258 |
||
|
|
5baf07d6e6 |
fix(tradein/domclick): свип брал BFF навигацией браузера — теперь подзапросом (#3264)
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 1m28s
CI Trade-In / backend-tests (pull_request) Successful in 4m53s
BFF-ручка Домклика не страница, а JSON-эндпоинт SPA. Сайдкар умел ровно одно — page.goto(url), — и навигацией браузера на API-хост мы делали то, чего настоящий клиент не делает никогда. Перехват сети на живой выдаче 30.08 это и показал: офферы приезжают в SSR-документе, а к BFF ходят XHR'ы за гео, районами и метро. Через мобильные узлы пула такая навигация упиралась в ChallengeTimeout: прогоны 5330 и 5351 — 9 из 9 и 6 из 6 запросов зависли на челлендже, 0 лотов. Карточки через те же узлы в те же минуты шли. Замер трёх режимов на одном узле, один и тот же ресурс: BFF navigate ChallengeTimeout, 55с BFF subresource HTTP 200, 52 байта, 6с BFF page_fetch ошибка Подзапрос проверен вширь: все шесть комнатных корзин HTTP 200, суммарно 6359 офферов против 6367, снятых напрямую с резидентного IP (расхождение — дрейф фонда за пару часов); список офферов отдаётся на всех четырёх узлах пула, по 20 штук, 120 КБ. Сайдкар получил fetch_mode: navigate (дефолт, прежнее поведение), subresource (context.request.get из прогретого контекста) и page_fetch (fetch из страницы). Третий оставлен, потому что теоретически он ближе всего к настоящему XHR, но в замере отказал — выбор сделан измерением, а не рассуждением. Прогрев origin обязателен: рукопожатие QRATOR попадает в куки контекста именно при заходе на страницу, поэтому свип теперь передаёт origin и referer, которых раньше не передавал вовсе. _decode_body распаковывает gzip по магическим байтам — карта офферов приезжает Content-Type: application/gzip, и без этого вызывающий получил бы бинарь в поле "html". Сама карта, к слову, подзапросом всё равно не берётся (279 байт — загрузчик QRATOR), но BFF полнее: 100% фонда против 62% у карты. fetch_mode кладётся в payload только когда он не дефолтный — сайдкар прежней версии не должен получать незнакомый ключ (тот же приём, что с referer в #3247). Тесты: сайдкар 212 passed (новый test_server_fetch_mode.py — 6 проверок), backend 316 passed (новый test_3264_sweep_subresource_mode.py — 5). Три тестовых дублёра _do_fetch/_post_fetch знали старую сигнатуру — обновлены. |
||
|
|
44633b0df4 |
fix(tradein/domclick): свип ходил на QRATOR без кук и вис на PoW каждым запросом (#3264)
All checks were successful
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 / 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 4m53s
serp.py не передавал куки сессии вообще — слова cookie в файле не было. Свип приходил к QRATOR с чистым браузером и был обязан решать proof-of-work с нуля на каждый запрос. Прод, прогон 5330 (30.08 03:49-03:55): 9 запросов к bff-search-web.domclick.ru, все 9 зависли на челлендже (перезагрузка 1/2, 2/2, отказ), status=failed, 0 лотов. Прокси при этом ротировался (узел 9 → 10) — узел тут ни при чём. Добор с тем же сайдкаром и тем же пулом в ту же ночь взял 37 карточек из 37 без единого блока. Разница ровно в куках. Чинить это стало возможно только сейчас: свип ходит не на ekaterinburg.domclick.ru, а на отдельный хост bff-search-web.domclick.ru, и до правки _cookie_domain (PR #3262) куки легли бы на .bff-search-web.domclick.ru, не совпав с сессией площадки. Теперь оба хоста схлопываются в общий .domclick.ru. Снимок приходит параметром снаружи, а не читается внутри kit: kit не импортирует app.* (strangler-инвариант #2133). Поэтому джоба domclick_city_sweep переопределена продуктовым Handler'ом — build_registry это прямо допускает («последнее слово за продуктом»), а БД читает только app-сторона. Отсутствие сессии не авария: load_session вернул None → свип идёт как раньше, без инъекции, факт логируется один раз. Цена решения — override повторяет вызов kit-джобы целиком и может тихо с ней разойтись. Добавлен тест, который зовёт оба джоба одинаково и сравнивает наборы kwargs, допуская расхождение ровно в cookies. Проверен мутацией: с искусственно добавленным в kit-версию аргументом краснеет, без него зелёный. Тесты: 245 passed, 1 skipped (domclick + parity). |
||
|
|
ff0a15d443 |
feat(tradein/browser): ходить по Домклику как человек — с Referer и с перезагрузкой зависшего рукопожатия
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 12s
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 1m28s
CI Trade-In / backend-tests (pull_request) Successful in 5m1s
Добор карточек Домклика упирался в отказ на 11-й карточке: прогон 5298 дал attempted=13, enriched=10, blocked=3. Ручные прогоны в живом браузере брали 91 и 40 карточек без единого отказа. Разбор нашёл два отличия, и оба оказались нашими, а не площадки. 1. Referer не отправлялся НИКОГДА. playwright'овский goto() по умолчанию этот заголовок не шлёт, а параметр `referer`, который он принимает, мы не передавали. Площадка видела десяток появлений подряд прямо на URL карточки, без источника перехода — так не ходит ни один человек. Комментарии в коде при этом уверяли про «органическую навигацию с реальным Referer»; они врали, теперь исправлены. Сайдкар принимает `referer` в теле /fetch и ставит его ТОЛЬКО на целевую навигацию; на origin и якорную вкладку не ставит — туда приходят «сами». Добор Домклика передаёт страницу выдачи, чем переход и является по смыслу. 2. Зависшее рукопожатие не перезагружалось. _wait_out_pow_challenge построен на допущении «страница перезагрузит себя сама после решения PoW»; ручная сессия 29.08 через узел 10 это опровергла — выдача осталась на 401, и пропуск qrator_jsid2 выдался только после ДВУХ перезагрузок, сделанных руками: 102.7с GET → 401, 115.1с GET → 401, 118.4с GET → 200, следом кука-пропуск, и карточка за 6 секунд. Пока мы только опрашивали content(), такая страница жила до таймаута, а бэкфилл засчитывал это в блоки. Теперь после BROWSER_CHALLENGE_RELOAD_AFTER_MS (8с) сайдкар перезагружает сам, не больше BROWSER_CHALLENGE_MAX_RELOADS (2) раз за фетч. Оба пути безопасны на откат: без поля `referer` в теле поведение прежнее, BROWSER_CHALLENGE_RELOAD_AFTER_MS=0 возвращает прежний опрос без навигаций, упавшая перезагрузка не роняет фетч — опрос продолжается в том же бюджете. Тесты: 191 passed в сайдкаре (было 182) — 4 на Referer, 5 на перезагрузку, в том числе «страница ожила сама → лишней навигации нет» и «висит вечно → не больше лимита». Точечные backend-тесты домклика и scraper_kit — 144 passed. |
||
| 3d51c08e44 |
Merge pull request 'fix(tradein/domclick): подтверждённый отказ площадки уехал в ветку «сбой транспорта» и перестал банить узел' (#3241) from fix/3239-sidecar-ban-page-reaches-ban-path 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 3m0s
Deploy Trade-In / test (push) Successful in 4m5s
Deploy Trade-In / build-backend (push) Successful in 1m36s
Deploy Trade-In / deploy (push) Successful in 8m32s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
|
|||
|
|
c5784e85bc |
fix(tradein/domclick): подтверждённый отказ площадки уехал в ветку «сбой транспорта» и перестал банить узел
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
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 / browser-tests (pull_request) Successful in 1m17s
CI Trade-In / backend-tests (pull_request) Successful in 4m54s
#3237 научил сайдкар опознавать статический отказ Домклика самостоятельно — это правильно и работает, но вместе с распознаванием ban-сигнал переехал не туда. Отказ стал приезжать обычной 500-кой: browser_fetcher обнуляет на ней last_response_status, и в detail.py срабатывает ветка except Exception, которая по построению НЕ зовёт report_ban («не подтверждённый маркер-бан, а сбой транспорта», #2600 п.4). Итог на проде (прогон 5287): ban_kinds сменился с platform на unknown, и при шести «статический отказ площадки» подряд в логах сайдкара в scrape_proxy_source_bans не появилось НИ ОДНОЙ записи. Это не косметика счётчиков — platform единственный диагноз, запускающий ротацию IP, поэтому мы продолжали бы долбиться в отказавший узел вместо перехода на свободный. Правка возвращает отказ на ban-путь, сохраняя разделение, ради которого #3237 и делался: - сайдкар кладёт в тело ошибки структурный признак ban_page и апстрим-статус. HTTP-код НЕ меняем: на 500 завязана classify_browser_probe; - SidecarBanPageError — подкласс httpx.HTTPStatusError, поэтому ловля у прочих поставщиков и retry-политика fetch() не замечают нового типа; - detail.py различает две ветки: подтверждённый отказ → report_ban + статус из исключения, транспортный сбой — как раньше. Статус несём отдельным полем, а не через last_response_status: на error-пути fetch() его обнуляет, а у Домклика отказ приходит с 401, без которого классификатор ставит unknown. Подстрокой в тексте исключения признак искать нельзя — _raise_for_sidecar_status обрезает тело до 300 символов, и формулировка отказа менялась дважды за месяц. Тесты держат обе ветки раздельно на всех трёх уровнях: сайдкар (признак есть у бан-страницы, отсутствует у транспортной ошибки), фетчер (тип и upstream_status, включая ловушку bool-как-int из #3196), detail.py (report_ban зовётся / не зовётся, статус доезжает). Closes #3239 |
||
|
|
3059f09e53 |
docs(tradein/domclick): два комментария описывали пул, которого нет с миграции 253
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 4m48s
Оба места утверждали, что у Домклика один выделенный резидентный прокси и пула нет. Это перестало быть правдой ещё в #2800 (миграция 253 сняла резервацию узла), но текст остался — и именно на него опирался тикет #3189, поставленный под «калибровку» ограничения, которого не существует. Свип к тому же ходит через пул давно (serp.py:359), а бэкфилл подключён к нему в PR #3222. Заодно докстринг называл не тот ограничитель: свип кладёт не счётчик провалов lease, а break по первому DomClickBlockedError (#2854) — до ротации дело не доходит ни при каком счётчике, отсюда buckets_completed=0. Только комментарии, поведение не меняется. Миграция 175 уже применена, а _schema_migrations трекает по имени файла без checksum — правка текста её не перезапустит. |
||
| 195f9f3697 |
Merge pull request 'chore(tradein): две ручки ротации без читателей и врущий комментарий над ними' (#3232) from chore/3212-dead-rotation-knobs 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 / changes (push) Successful in 11s
Deploy Trade-In / test (push) Successful in 4m2s
Deploy Trade-In / build-backend (push) Successful in 1m40s
Deploy Trade-In / deploy (push) Successful in 1m35s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
|
|||
|
|
1141035899 |
chore(tradein): две ручки ротации, которых не осталось читателей, и врущий комментарий над ними
All checks were successful
CI Trade-In / 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 / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
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 5m8s
proxy_rotate_attempts / proxy_rotate_attempt_timeout_s тюнили ретраи changeip-GET. Сам changeip снят в #2616 шаг 2 (аккаунт mobileproxy закрыт, ссылки нет), и с тех пор ручки живут пламбингом: Settings -> property адаптера -> поле протокола ScraperConfig -> и всё. Ни одного потребителя, только четыре теста, которые заполняют их при сборке конфига. Комментарий над ними в contracts.py оправдывал их сохранение так: «оставлены как budget-верхняя-граница для app.tasks.avito_detail_backfill wait_for» — но wait_for там берёт СОСЕДНЕЕ поле, avito_proxy_rotate_settle_s (avito_detail_backfill.py:249). То есть комментарий приписывал этим двум полям работу третьего и тем самым прикрывал их мёртвость. Соседние ручки проверены и ОСТАВЛЕНЫ, они действительно читаются: * avito/cian/yandex_proxy_max_rotations — pipeline._max_rotations; * avito_proxy_rotate_settle_s — asyncio.wait_for в avito_detail_backfill. Заодно сжат комментарий в config.py: перечисление истории changeip заменено на то, что нужно знать сейчас — кто читает оставшиеся две ручки и где живая ротация (ASOCKS_API_TOKEN / proxy_rotation, #2611). ruff clean, 4974 passed / 37 skipped. |
||
|
|
4902d1e29e |
chore(tradein): четыре символа, на которые нет ни одной ссылки
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
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 5m12s
Найдено сверкой двух независимых источников: подсчёт упоминаний по всем
python-файлам (включая тесты) дал имена, встречающиеся ровно один раз — в
собственном объявлении; каждое затем подтверждено serena find_referencing_symbols
(LSP, видит и косвенные ссылки) и проверено grep'ом по yml/sql/ts/md на случай
ссылки строкой.
Удалено:
* _qr_code_data_url (trade_in_pdf) — QR как SVG data URL, не вызывался
ниоткуда; вместе с ним ушёл осиротевший import segno. `io` ОСТАВЛЕН —
io.BytesIO используется дальше по файлу (строка 774);
* _local_name (gar_flats_loader) — снятие namespace с имени тега;
* AvitoParseError — класс никто не поднимает и не ловит;
* IMVCityMismatchError — то же.
Что НЕ тронуто, хотя фильтр их показал:
* ~200 обработчиков FastAPI и celery-задач — их поднимает декоратор, по имени
их действительно никто не зовёт;
* refresh_ddu_price_indicator — точка ручного обслуживания, задокументирована
в комментарии к матвью (data/sql/152_mv_ddu_price_indicator.sql: «Refresh:
... (не в beat)»);
* schemas/parcel.py::MarketPrice — половина контракта, фронт использует
(frontend/src/types/site-finder.ts: market_price?: MarketPrice);
* JobSetting — SQLAlchemy-модель, живёт через metadata Base.
segno остался в backend/pyproject.toml и больше нигде не используется — снятие
зависимости требует пересборки лока, поэтому отдельным PR.
ruff clean, 4974 passed / 37 skipped.
|
||
|
|
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 не исполняют. |
||
| 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
|
|||
| bdb9b64b03 |
Merge pull request 'fix(tradein/domclick): исчерпание пула прокси помечалось как отказ сбора (#3118)' (#3174) from fix/3118-domclick-no-proxy into main
All checks were successful
Deploy Trade-In / test (push) Successful in 4m0s
Deploy Trade-In / changes (push) Successful in 13s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 1m35s
Deploy Trade-In / deploy (push) Successful in 1m16s
Deploy Trade-In / deploy-status (push) Successful in 2s
Deploy Trade-In / perimeter-smoke (push) Successful in 11s
|
|||
|
|
ade1a065d6 |
fix(tradein/domclick): исчерпание пула прокси свипа теперь infra-бан, не отказ сбора
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 4m42s
NoProxyAvailableError поднимается из BrowserFetcher.__aenter__ (_acquire_lease)
ДО первого HTTP-запроса, когда пул прокси пуст — это НАША инфраструктура, не
блокировка площадкой. У run_avito_full_load/run_cian_full_load/run_yandex_full_load
уже есть выделенный except NoProxyAvailableError -> mark_banned(ban_kind='infra'),
у run_domclick_city_sweep его не было: исключение проваливалось в общий except
Exception внутри SERP-фазы, _scraper_ref оставался пустым, и честный статус ниже
видел "0 лотов + errors>0" -> mark_failed("fetch errors — 0 listings") с
ban_kind=NULL. Прод-факт: run 5023 (27.08) умер за 51 мс, errors_count=1,
ban_kind=NULL — неотличимо от честного отказа сбора площадкой.
Добавлен except NoProxyAvailableError перед generic except Exception (порядок
важен: класс — подкласс RuntimeError). Обработчик зеркалит avito/cian/yandex:
mark_banned + ban_kind_of_exception(exc) (даёт BAN_KIND_INFRA), и сохраняет
унаследованный чекпоинт (skip_buckets) вместо потери его на нашем же отказе.
Тест test_3118_domclick_no_proxy.py проверен на обратимость: без обработчика
падает (mark_failed вместо mark_banned), с обработчиком — проходит.
|
||
|
|
dc793e8701 |
fix(tradein/yandex): чекпоинт combo ставился до save_listings, не после
All checks were successful
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m40s
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
_on_combo в run_yandex_city_sweep делал done_combos.add(combo_label) ДО вызова save_listings. Отказ save_listings перехватывается (осознанно — одна упавшая единица не должна ронять весь sweep) и логируется, но combo уже был отмечен пройденным и уходил в heartbeat done_buckets. Следующий resume брал skip_combos из done_buckets (членство в множестве — само по себе корректно, не трогал) и пропускал этот combo навсегда: молча, прогон завершался штатно, просто сегмент выдачи не собирался никогда. Тот же инвариант "отмечаем пройденным только после успешного save", что уже есть у страницы в run_avito_newbuilding_sweep (_saved_ok), якоря в run_avito_city_sweep (_anchor_ok) и бакета в run_cian_full_load (_mark_bucket) — применил к combo. Heartbeat пишется в любом случае (и при отказе save тоже), иначе reap_zombies посчитает живой прогон мёртвым. Тесты: test_3170_yandex_combo_checkpoint.py — combo с упавшим save не попадает в done_buckets, успешный (включая пустую выдачу) — попадает. Обратимость проверена: с возвращённым дефектом (git stash) первый тест красный, со снятым — зелёный вместе с существующим test_3074_yandex_ sweep_checkpoint.py (6/6). |
||
|
|
5a410687ac |
feat(tradein/scraper): чекпоинты для avito_newbuilding_sweep — страница как единица (#3074)
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 / 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 4m41s
Последний длинный свип без возобновления: при обрыве прогон начинался с первой страницы, а собранное терялось целиком — save_listings вызывался один раз на весь sweep. Единица возобновления — страница выдачи, по образцу якорей в city sweep. `_paginate_sweep`/`fetch_newbuildings` получили `start_page` (уже собранные страницы не запрашиваются) и колбэк `on_page`, который вызывается только после того, как страница пройдена до конца. Сохранение стало постраничным, номера пройденных страниц копятся в `scrape_runs.counters.done_buckets` мержем через `update_heartbeat`. Два инварианта, без которых фича вредна: 1. В чекпоинт попадает только страница, чьи лоты СОХРАНЕНЫ. Отказ save_listings перехвачен и прогон продолжается, но отметить такую страницу пройденной значило бы, что следующий прогон её пропустит и объявления оттуда не соберутся никогда — молча, потому что прогон завершится штатно. 2. Подхват начинается с ПЕРВОЙ несобранной страницы, а не с max+1. Дыра в чекпоинте возможна ровно из-за п.1, и max+1 перепрыгнул бы её навсегда. Страницы после дыры перечитаются — это дешевле потери и безопасно, повторная запись схлопывается по dedup_hash. Оба инварианта закрыты тестами, которые падают при их нарушении. |
||
|
|
26cd8dbe34 |
fix(tradein/avito): повтор после блока терял прокси и уходил с адреса машины
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 Trade-In / backend-tests (pull_request) Successful in 4m52s
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Обе ветки повтора в `fetch_detail` — 403/firewall и 429 — пересоздавали
эфемерную сессию вызовом `_build_detail_session()` БЕЗ `config`. Прокси
kit-версия читает только из `config.scraper_proxy_url`, поэтому такая сессия
уходила напрямую.
Замысел ветки прямо обратный, он записан в её же комментарии: «эфемерная
свежая сессия (новый CONNECT-туннель = свежий exit-IP)». Ветка вообще
исполняется только при backconnect=True, а он вычисляется как «задан
config.scraper_proxy_url» — то есть в момент вызова достоверно известно, что
прокси есть, и он терялся.
ПОЧЕМУ НЕ БЫЛО ВИДНО. Пока адрес самой машины не был заблокирован, прямой
повтор часто срабатывал, и подмена канала выглядела как успех. Замер 27.08 из
прод-контейнера, один и тот же URL Авито:
через прокси — 200, 3.3 МБ страницы
напрямую — 429, «доступ ограничен», firewall
С этого момента каждый повтор после блока обречён. Обогащение
avito_detail_backfill по суткам: 21-25.08 — 178/129/147/138/111, 26.08 — 23,
27.08 — 0 при 25 блоках. Обвал начинается ровно с окна, в котором сменился
адрес машины.
Тот же класс ошибки чинили в #2330 для build_warmed_session; в пути повтора он
оставался.
Три теста: обе ветки на месте (страховка от проверки пустоты), ни одна не
строит сессию без config, и отдельно доказано, что без config прокси в сессии
действительно нет. Проверил красноту на старом коде — падает с точным текстом.
Прогон: 107 тестов scrapers зелёные, ruff чист.
Refs #3034, #3045
|
||
| ede5653aba |
feat(tradein/domclick): чекпоинты для city_sweep — корзина как единица возобновления (#3118, #3074)
All checks were successful
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 / changes (pull_request) Successful in 10s
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 4m43s
Прод-факты #3118: QRATOR рубит свип внутри 1-2-й корзины при любом старте (buckets_completed ≤ 1 из 6), banned-прогоны собирают 59–1815 лотов. Сдвиг #2854 лишь распределяет потери; чекпоинт превращает случайную ротацию в систематический обход — шесть прогонов закрывают шесть корзин. Моё раннее «домклику чекпоинтить нечего» (замер в #3074 по средним 3 мин) опровергнуто данными #3118 — этот коммит исправляет тот вывод кодом. Зеркально yandex-чекпоинту (#3074): провайдер — skip_buckets + имена завершённых корзин + гард «цепочка накопила все 6» (честный no-op); buckets_total сжимается до объёма ЭТОГО прогона; пайплайн — done_buckets = унаследованное ∪ завершённое, heartbeat с мержем jsonb до финализации; планировщик — generic _pick_resume (#2845). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 3c315b5105 |
fix(tradein/scheduler): boot-reap виден и при нуле — молчащий механизм неотличим от неподключённого (#3122)
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 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 4m45s
Первая живая приёмка #3122: деплоевский startup-reap успел снять зомби раньше, boot-reap отработал с нулём — и не оставил НИКАКОГО следа исполнения. Ноль — штатный исход, но «сторож, молчащий при нуле» — это слепая зона по построению: оборванную проводку не отличить от чистого прода. INFO-строка при нуле, warning при снятых — как было. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 63956f0891 |
feat(tradein/scheduler): boot-reap — прогоны предыдущего контейнера снимаются на старте (#3122)
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 4m40s
Прод-факт 27.08 (после ночного офлайна #3119): 4 прогона 'running' со стартами до старта контейнера блокировали свои источники через has_running_run до 6-часового порогового reap'а — до пяти часов слепоты на источник ровно после простоя, когда догон нужнее всего. Критерий — started_at < старт процесса планировщика (минус минута на дрейф), пульс не участвует: ложные срабатывания класса #2702 (редкий пульс длинных прогонов) невозможны по построению — живой прогон этого процесса не может быть старше самого процесса. Маркер counters.boot_reaped=true открывает boot-зомби подхват чекпоинта (_resume_decision): у порогового zombie процесс может быть жив (движущаяся точка — причина исключения 'zombie' из _RESUME_STATUSES), у boot-зомби — гарантированно мёртв. Пороговый zombie без маркера по-прежнему отвергается (закреплено тестом-инвариантом). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 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> |
|||
|
|
68daac2a30 |
feat(tradein/scraper): чекпоинт по якорям для cian_city_sweep (#3074)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / browser-tests (pull_request) Has been skipped
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 / changes (pull_request) Successful in 9s
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m43s
Третий шард после yandex (#3098) и avito (#3112). Выбран по замеру за 60 дней: 65 прогонов, среднее 35 минут, максимум 72, две отмены деплоем. Пятиминутного дренажа (#3029) на такие прогоны не хватает - убитый на 35-й минуте сбор начинался заново с первого якоря. Ключ чекпоинта - ИМЯ якоря, а не индекс: состав списка зависит от city_slug (областные свипы идут по своим наборам), позиция между городами не устойчива. По той же причине гарда по числу якорей не нужна - в отличие от combo-чекпоинта яндекса, где ключ якоря не содержал. Отличие от avito-шарда: там успех и неудача якоря сходились в одной строке и потребовался отдельный флаг _anchor_ok. У циана граница уже проведена самим потоком управления - все ветки отказа делают return или continue и до записи чекпоинта не доходят. Добавлять флаг значило бы дублировать то, что уже выражено структурой; достаточно писать чекпоинт в единственной точке успеха. Тест сторожит эту границу отдельно, потому что рефакторинг, сливающий ветки, сломал бы её незаметно. Пропущенный якорь двигает anchors_done - чтобы счётчик продолжал означать "докуда дошли по списку", а не "сколько собрал именно этот прогон". Тесты (4) поведенческие, с подменой CianScraper и save_listings: якорь из чекпоинта не опрашивается вовсе; пройденный дописывается поверх унаследованных; без чекпоинта обходятся все; упавший в чекпоинт не попадает. Двойнику пришлось добавить счётчики state_extraction_* - конвейер читает их после каждого якоря (#2625), и без них падал бы сам двойник, а не проверяемая логика. Фальсификация: на исходном коде краснеют все 4. Весь набор #3074 (yandex, avito, cian, claim) - 14 passed. |
||
|
|
0586951772 |
feat(tradein/scraper): чекпоинт по якорям для avito_city_sweep (#3074)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m47s
Продолжение после yandex-свипа. Выбор источника — по замеру, а не «для
полноты»: за 60 дней avito_city_sweep дал 68 прогонов, 25 банов и 3 отмены
деплоем при среднем времени 15 мин и максимуме 97.
Прод-факт, который решает дело. Типичный итог свипа:
{"anchors_done": 1, "anchors_total": 5, ...,
"enrichment_abort_note": "detail enrichment aborted (Avito detail
firewall/soft-block ...)"}
Прогон срывается блокировкой на ПЕРВОМ из пяти якорей. Без чекпоинта
следующий прогон снова идёт в первый якорь, упирается в ту же стену, и якоря
2-5 не собираются никогда.
Ключ чекпоинта — ИМЯ якоря, а не индекс: состав списка зависит от city_slug,
позиция в нём между городами не устойчива. По той же причине здесь не нужна
гарда по числу якорей, которая есть у combo-чекпоинта яндекса: там ключ
якоря не содержал, здесь якорь и есть ключ.
Инвариант, ради которого отдельный флаг _anchor_ok: в чекпоинт попадает
только якорь, пройденный до конца. Ветка блокировки делает return и до записи
не доходит, а generic-except доходит — якорь упал, но цикл продолжается.
Записать такой якорь пройденным значило бы, что следующий прогон пропустит
его навсегда, причём молча: прогон завершится штатно, просто часть города не
соберётся. Флаг сбрасывается на каждой итерации, иначе один упавший якорь
заразил бы все последующие.
Пропущенный якорь двигает anchors_done — чтобы счётчик продолжал означать
«докуда дошли по списку», а не «сколько собрал именно этот прогон».
domclick_city_sweep намеренно НЕ трогаю: 54 прогона, ноль отмен деплоем,
среднее время 3 минуты — чекпоинт там не окупается. cian_city_sweep (среднее
35 мин, 2 отмены) — следующий шард.
Тесты (4) поведенческие: якорь из чекпоинта не опрашивается вовсе; пройденный
дописывается поверх унаследованных; без чекпоинта обходятся все; упавший в
чекпоинт НЕ попадает. Оговорка: на исходном коде они падают по сигнатуре
(unexpected keyword argument), то есть доказывают отсутствие параметра, а не
поведение — поведенческую часть держат сами проверки. Весь набор #3074 —
10 passed.
|
||
| 01ccf4f133 |
Merge pull request 'feat(tradein/yandex): is_pro_seller из агентского блока + бэкфилл (#3063 п.2)' (#3100) from fix/3063-pro-seller-derive into main
Some checks failed
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 / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m48s
Deploy Trade-In / deploy (push) Has been cancelled
Deploy Trade-In / build-backend (push) Successful in 1m48s
|
|||
| 993b44c7e0 |
Merge pull request 'feat(tradein): чекпоинты #3074 — наследование при claim + yandex_city_sweep' (#3098) from fix/3074-checkpoint-survives-claim into main
Some checks failed
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 11s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Successful in 1m33s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m46s
Deploy Trade-In / deploy (push) Has been cancelled
|
|||
| 1aaa8b88a1 |
feat(tradein/yandex): is_pro_seller выводится из агентского блока + бэкфилл 4469 строк (#3063 п.2)
All checks were successful
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m29s
Detail-обогащение извлекало agency_name, но признак «продаёт профи» не выводило — 4 469 активных листингов с известным агентством стояли с is_pro_seller=NULL (признак эрозирован SERP-затиранием до PR #3067, а заново не появлялся). Признак идёт в оценщик trade-in. - save_detail_enrichment: is_pro_seller=TRUE при известном agency_name, fill-only COALESCE; отсутствие блока НЕ доказывает «частник» (п.3 задачи — отдельное решение), в ту сторону ничего не пишем; - миграция 271: одноразовый бэкфилл уже существующих строк всех источников (правило источник-независимо), идемпотентна. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 307055232e |
feat(tradein/yandex): чекпоинты для yandex_city_sweep — combo как единица возобновления (#3074)
All checks were successful
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 Trade-In / changes (pull_request) Successful in 7s
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 4m35s
Из таблицы убитых деплоем: yandex_city_sweep 15.08 прожил 65 мин, 12.08 — 2ч33м; оба потеряны целиком — у свипа не было чекпоинтов вовсе. Единица чекпоинта — combo (сегмент × комнатность × ценовой диапазон), ровно то, чем цикл обхода уже итерируется. Три слоя: - провайдер: skip_combos (ни одного HTTP по собранным) + on_combo для каждого ПРОЙДЕННОГО combo, включая пустые — иначе пустой combo не попадал бы в чекпоинт и перечитывался бы вечно; оборванный отказом combo (gate failure) on_combo по-прежнему не вызывает; - пайплайн: done_combos → heartbeat с done_buckets (мерж jsonb, финализаторы не затирают); подхват гейтится единственным якорем — combo_label не содержит якоря, multi-anchor подхват пропускал бы чужие якоря; - планировщик: resume_run_id=_pick_resume(...) в диспатче (generic-механизм #2845 — params-идентичность, свежесть точки, потолок цепочки — бесплатно). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| e4b3c6cc2b |
fix(tradein/scheduler): чекпоинт предшественника переживает обрыв до первой новой корзины (#3074)
All checks were successful
CI / changes (pull_request) Successful in 10s
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 / 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 4m35s
Прод-факт: прогон 4707 (23.08) подхватил 42 корзины у 4117, был убит деплоем на 26-й минуте до завершения первой НОВОЙ корзины — и не успел ни разу написать heartbeat с done_buckets. Его собственный чекпоинт пуст: следующий кандидат увидел бы no_checkpoint, цепочка оборвалась бы с потерей 42 корзин. _resume_decision при вердикте 'ok' теперь кладёт done_buckets предшественника в counters-заготовку нового прогона — она персистится при claim, до старта пайплайна. Heartbeat мержит jsonb: первый настоящий bucket-heartbeat перезапишет ключ надмножеством, двойной записи нет. Отказные вердикты чекпоинт не наследуют (прогон с нуля не должен врать о собранном). 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> |
|||
| 7c916082e8 |
feat(tradein/domklik): ЖК в свою колонку вместо только raw_payload (#3076)
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 / build-backend (push) Successful in 1m47s
Deploy Trade-In / deploy (push) Successful in 1m55s
Deploy Trade-In / perimeter-smoke (push) Successful in 9s
Deploy Trade-In / test (push) Successful in 3m46s
Deploy Trade-In / deploy-status (push) Successful in 1s
|
|||
| e73cde7ad3 |
fix(scraper-kit): бедный re-scrape больше не стирает признаки продавца (#3067)
Some checks failed
Deploy Trade-In / test (push) Blocked by required conditions
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / build-frontend (push) Blocked by required conditions
Deploy Trade-In / build-browser (push) Blocked by required conditions
Deploy Trade-In / deploy (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) Has been cancelled
|
|||
| 4aaa021b62 |
feat(tradein/domklik): пробросить isRosreestrApproved в колонку (#3068)
Some checks failed
Deploy Trade-In / deploy (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 / build-backend (push) Blocked by required conditions
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) Has been cancelled
|
|||
| 3b6bca80c0 |
fix(avito): считать HTTP 439 блокировкой, а не концом выдачи (#3065)
Some checks failed
Deploy Trade-In / build-backend (push) Blocked by required conditions
Deploy Trade-In / build-frontend (push) Blocked by required conditions
Deploy Trade-In / deploy-status (push) Blocked by required conditions
Deploy Trade-In / test (push) Blocked by required conditions
Deploy Trade-In / build-browser (push) Blocked by required conditions
Deploy Trade-In / deploy (push) Blocked by required conditions
Deploy Trade-In / perimeter-smoke (push) Blocked by required conditions
Deploy Trade-In / changes (push) Has been cancelled
|
|||
|
|
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 штук: бюджет ЦИАН, клиентские координаты, аудит) зелёные.
|
||
|
|
4c9f5b6e9f |
Merge remote-tracking branch 'forgejo/main' into fix/avito-detail-fields
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 4m42s
|
||
| f34edeb512 |
chore(tradein/avito): ссылки на реальный issue #3047 вместо выдуманного номера
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 11s
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
Воркер сослался на #3086, которого не существует — тест с таким именем остался бы загадкой для следующего читателя. Заведён настоящий issue #3047 с замерами покрытия и разбором дефекта; все ссылки и имя файла приведены к нему. Refs #3047 |
|||
|
|
efce189c8b |
fix(tradein/avito): починить чтение параметров, добавить metro/is_homeowner/days_on_market
Разобрались по эталонной разметке (2 живые detail-карточки, 2026-08-21), что реально
парсится и что нет:
- sale_type: item-view/item-params — Avito отдаёт "О квартире" и "О доме" как ДВА
ОТДЕЛЬНЫХ <div> с ОДНИМ И ТЕМ ЖЕ маркером, а не один div с двумя <ul>, как
предполагал старый код. css_first брал только ПЕРВЫЙ div — sale_type это не
задевало (он в первом блоке), но house_type/total_floors_house/лифты из второго
блока терялись молча через мёртвую ветку `len(ul_els) >= 2`. Теперь читаем <ul>
из ВСЕХ блоков с этим маркером — устойчиво к порядку блоков.
- metro_stations: раньше только эвристика по тексту описания (28 строк из 54 855).
Основной источник теперь — структурная разметка (#item-view-address, иконка
"Пешком до метро"), покрывает станции без ограничения на суффикс имени
(METRO_RE ловил только "-ская"/"-инская"). Описание — фолбэк.
- is_homeowner: не парсился вовсе. [data-marker='seller-info/label'] — "Частное
лицо" -> True, "Агентство" -> False. Подтверждено разными значениями на двух
эталонах.
- days_on_market: на странице явно нет, но есть publish_date, из которого честно
выводится. Заодно чинит сам publish_date — искали дату ВНУТРИ item-id-блока, а
она в СОСЕДНЕМ [data-marker='item-view/item-date'] ("сегодня в HH:MM").
- cadastral_number: подтверждено отсутствие на странице (не парсим, не выдумываем).
Тесты на реальной разметке (backend/tests/test_avito_detail_fields_3086.py) на
обеих эталонных фикстурах (большие embedded JS-блобы вырезаны из фикстур —
не используются DOM-based парсером, экономят место). estimator.py не тронут —
ни одно поле не влияет на цену.
|
||
|
|
b3a39699d1 |
Merge remote-tracking branch 'forgejo/main' into fix/3034-avito-fingerprint
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
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 4m34s
|
||
| bebeb0c8f0 |
feat(tradein/avito): путь вторички для якорного и citywide-обхода (#3033, follow-up) (#3042)
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m41s
Deploy Trade-In / build-backend (push) Successful in 1m40s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 9s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / deploy (push) Successful in 2m21s
|
|||
| 17a6dae3a4 |
feat(tradein/avito): house-поля с детальной страницы доезжают в houses (#3036) (#3040)
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 3m37s
Deploy Trade-In / build-backend (push) Successful in 2m55s
Deploy Trade-In / deploy (push) Successful in 3m20s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
|
|||
| 6ffcd78d35 |
fix(tradein/avito): страница SERP отдаёт 60 карточек, а не 50
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 11s
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 4m27s
Замер живьём 2026-08-21 через tradein-browser (camoufox, JS исполняется):
59-60 уникальных `data-item-id` на странице выдачи. «Лишние» сверх 50 —
обычные объявления с платным продвижением (`vas-icon_type-promoted`), они
лежат в том же списке под `page-title/count`, а не отдельным рекламным
блоком, и собираются наравне с остальными.
Направление эффекта важно понимать правильно. Константа участвует ТОЛЬКО
в `ceil(total / PAGE)`, поэтому занижение размера страницы ЗАВЫШАЛО
расчётное число страниц, а не занижало:
- запрашивали примерно на 17 % страниц больше, чем нужно; при доле банов
37-83 % по avito-заданиям лишние запросы — основная цена ошибки;
- `tail_loss` считался как `total - cap * 50` и завышал потерю;
- флаг `complete` в пагинации листа чаще ложно показывал «неполно».
Тихой потери данных НЕ было: условия «страница вернула меньше PAGE
карточек, значит последняя» в коде нет, пагинация ограничена только
`max_pages`. Тест закрепляет и значение, и направление арифметики, чтобы
неверная трактовка не вернулась при следующем рефакторинге.
Найдено при разборе Авито сверкой живого браузера со скраппером; полный
разбор — в волте `research/Avito_Live_Browser_Recon_0821.md`.
Refs #3033
|
|||
|
|
fc2930fbb6 |
Merge remote-tracking branch 'forgejo/main' into fix/3034-avito-fingerprint
All checks were successful
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 / 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 4m28s
|
||
| 7382ae123d |
feat(tradein/avito): полный обход качает вторичку родным фильтром пути (#3033) (#3039)
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 3m42s
Deploy Trade-In / build-backend (push) Successful in 1m48s
Deploy Trade-In / deploy (push) Successful in 2m28s
Deploy Trade-In / deploy-status (push) Successful in 2s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
|