168 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 126608e1fc |
Merge pull request 'fix(tradein/domclick): обрыв «пул пуст» и save-False больше не теряют попытку — attempted == сумма исходов' (#3335) from fix/3332-domclick-counter-identity into main
Some checks failed
Deploy Trade-In / build-backend (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
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
|
|||
|
|
76b24e9f4d |
fix(mera-public): подписи метрик снижения цены и бенчмарк отчёта не называют площадку
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 / 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 1m12s
CI Trade-In / backend-tests (pull_request) Successful in 5m5s
Замечание юриста (Сайт_МЕРА_v2, п.3): публичный измеримый контент не должен строиться на названных данных площадок. - landing_stats.py: note метрик price_cut_share_pct / price_cut_median_pct_per_month «Только Домклик (…)» → «Только одна из площадок (…)». Текст персистится в landing_stats.note и рендерится на лендинге в блоке «цена ошибки» — после деплоя нужен пересчёт (refresh_landing_stats), деплой кода note не перепишет. - HeroSummary.tsx / IMVBenchmark.tsx: убраны видимые «IMV» и title «Information Market Value» / «Индекс Market Value» — бренд-термин оценочной модели площадки; подпись — «оценочная модель» + sourcePublicLabel. - public-copy-no-platform-names.test.ts: «IMV», «Information Market Value» добавлены в BANNED (гейт 34/34). |
||
|
|
5541aa9d37 |
fix(mera-public): публичная копия — сервис не называет себя оценщиком, площадки-источники не названы
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 / 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 5m2s
Три юр-правки по документу владельца продукта «Сайт_МЕРА_v2» (31.08.2026), сделаны локально 31.08, но не были закоммичены — на meraocenka.ru всё оставалось по-старому. 1. «Оценщик» про собственный алгоритм. Витрина сделок (`landing_showcase_deals.py`, REJECTION_RULE/NOTE) и сноска статьи «Как оценить квартиру» теперь говорят «расчёт МЕРЫ»: самоназвание обесценивало дисклеймер «не официальный отчёт оценщика». Комментарии и докстринги не тронуты — посетитель их не видит. 2. «Путь 2»: «стоимость услуг фиксированная и известна заранее» — читалось как фиксированная цена квартиры. 3. Названия площадок убраны из видимой копии лендинга и веб-отчёта. Канон — `publicLabel` + `sourcePublicLabel()` в `source-registry.ts`: одна площадка = один номер «Источник N» и один цвет точки (цвет остаётся опознавателем между блоками), Росреестр под своим именем, неизвестный id → «Другой источник» (раньше fallback отдавал сырой id). Три параллельные реализации `sourceLabel` сведены к одной; `sourceLabel()` с реальными именами живёт для админки. Backend: якорь confidence_explanation «по оценке Avito IMV» → «по оценочной модели площадки». Подсказки геокодера «Yandex / Nominatim» и «по Яндексу» сняты из клиентских форм. Гейт `public-copy-no-platform-names.test.ts` сканирует mera-public/** и components/trade-in/** без комментариев, по Unicode-границе слова; список запретов регистрозависимый намеренно (строчные id `"avito"` законны), поэтому капс-варианты и словоформы перечислены явно — «6202 ОБЪЯВЛЕНИЯ ДОМКЛИК» в статье именно так проходил первую версию гейта. Не закрыто здесь: клиентский PDF (#3341) и ссылки на объявления на доменах площадок. Текст витрины хранится в БД (`landing_showcase_runs.rejection_rule`, `landing_showcase_deals.note`), планировщика у пересчёта нет — после деплоя нужен ручной пересчёт или UPDATE трёх подстрок на проде. |
||
| e2e7e17f95 |
fix(domclick): каждая попытка добора получает исход (#3332)
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 / openapi-codegen-check (pull_request) Has been skipped
CI / changes (pull_request) Successful in 12s
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 5m15s
Обрыв «пул прокси пуст» уходил из цикла между attempted++ и записью исхода, поэтому тождество attempted = enriched + failed + blocked ломалось ровно на 1 (прод: 5 прогонов с diff=1). Исход честно failed, не blocked: к площадке не ходили, отказала наша инфраструктура — тот же разряд, что у транспортных сбоев (#3283); причина прогона по-прежнему в no_proxy_stop=1 + mark_failed. Та же дыра закрыта у save_detail_enrichment(...) is False: карточка разобрана, но строки уже нет — попытка была, исхода не было. Closes #3332 |
|||
| add974db65 |
feat(tradein/avito): бан площадки не менял IP — прогон добивался в тот же забаненный адрес (#3283) (#3313)
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 4m10s
Deploy Trade-In / build-backend (push) Successful in 1m7s
Deploy Trade-In / deploy (push) Successful in 7m47s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 13s
|
|||
| 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
|
|||
| e45b05845e |
fix(mera-landing): наличие схемы улицы входит в полноту строки витрины
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 13s
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 5m14s
Первой строкой витрины и первым раундом игры «Проверьте себя» стояла единственная из двадцати сделка БЕЗ улицы: у неё вместо схемы улиц рисовался полигон района. Причина — `completeness()` считала район, этаж и этажность, но не улицу, хотя именно она решает, будет ли у строки карта. Схема улицы — такое же ВИДИМОЕ поле, как район: строка, которой нечем нарисовать карту, полнее строки с картой быть не может. Признак берётся из уже загружаемого индекса улиц (`StreetIndex.lookup`, поиск в памяти); индекс поднят выше отбора, дорогие пространственные запросы остались в `_schemes_for` и по-прежнему считаются только для показанных строк. Отбор по ВЕЛИЧИНЕ ОШИБКИ не введён и введён быть не может: строка с отклонением +75,7% остаётся в витрине, просто больше не открывает её. Прежнее правило «наличие схемы на отбор не влияет» в докстринге `_schemes_for` заменено с разбором, почему оно давало этот дефект. Тест двусторонний: при прочих равных строка со схемой выше строки без неё, а строка без улицы остаётся в витрине. Фальсифицирован — снятие `row.has_street` из `completeness` даёт красное ПО ЗНАЧЕНИЮ ([9, 8] вместо [8, 9]), не по исключению. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
|
|
1e02badbe9 |
fix(tradein/domclick): бан сайдкара наследуется от httpx-ошибки и уезжал в транспортные сбои (#3283)
All checks were successful
CI Trade-In / 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 / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m5s
Правка #3286 разделила отказы площадки и сбои нашей стороны по природе исключения: httpx-ошибка в цепочке причин = транспорт. Прод показал дыру на первом же прогоне (5406): СБОЙ #1/400 (подряд=1/10, http=401, kind=platform, не блок площадки): DomClick detail: sidecar detected platform refusal Заголовок «не блок площадки» стоит над записью, где kind=platform и код 401, то есть над самым что ни на есть отказом площадки. Причина в иерархии: SidecarBanPageError объявлен как подкласс httpx.HTTPStatusError, поэтому проверка на httpx, стоявшая первой, забирала генуинный бан себе. Порядок проверок перевёрнут: бан сайдкара отсекается ДО общей httpx-ветки. Остальное поведение #3286 не тронуто — обычная 500 от сайдкара по-прежнему транспортный сбой. Тесты: +4, включая закрепление самой предпосылки (issubclass проверяется явно, чтобы смена иерархии не отключила проверку молча) и страховку, что обычная 500 осталась сбоем. Проверено мутацией: снятие порядка роняет тест и печатает самопротиворечивый лог «10 сбоев подряд без единого отказа площадки, диагнозы: {'platform': 10}». Набор целиком — 5210 passed. |
||
|
|
42913d2843 |
fix(tradein/domclick): таймаут сайдкара и пустой пул считались отказами площадки (#3283)
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 4m56s
Прогон 5399 умер по правилу «3 блока подряд», не обогатив ни одной карточки
из 400. Отказом Домклика не был ни один из трёх: два — HTTP 500 от сайдкара
(таймаут навигации), третий — пустой пул, при котором запрос к площадке не
отправлялся вовсе. Лог абортa сам это печатал: диагнозы {'unknown': 3}.
Различать по HTTP-статусу нельзя: настоящий QRATOR-челлендж приходит без
статуса и неотличим от таймаута — на этом и стояли прежние тесты. Различима
природа исключения, причём разделение уже проведено в fetch_detail: генуинный
бан приезжает через SidecarBanPageError или маркеры в parse_detail_html, а
транспорт — через с , то есть с
httpx-ошибкой в цепочке причин.
Три правки:
* пустой пул (NoProxyAvailableError в цепочке) — не блок, а остановка прогона:
продолжать бессмысленно, каждая следующая карточка упрётся в то же. Прогон
уходит в failed, а не banned — запись не должна утверждать про площадку то,
чего не было;
* транспортные сбои идут в failed, туда же, куда давно идёт DomClickParseError
(«schema drift, not a block»), и счётчик блоков не трогают;
* отдельный сторож на молчаливый отказ: max_consecutive_soft_failures=10, иначе
снятие хайртриггера оставило бы прогон без тормоза вовсе.
Тесты: 10 проверок, включая дословный сценарий 5399 и страховку «генуинные
блоки по-прежнему рвут прогон после трёх». Проверено мутациями: возврат пустого
пула в блоки роняет 2, возврат транспорта — 6, снятие сторожа — 2. Четыре
существовавших теста на генуинный блок продолжают проходить без правки — это и
показывает, что разделение проведено по верному признаку. Набор целиком —
5206 passed, 37 skipped.
|
||
|
|
5f9dc5d512 |
feat(tradein/cian): у Циана не было добора карточек — только побочный эффект задачи про историю (#3284)
All checks were successful
CI Trade-In / 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) Has been skipped
CI / changes (pull_request) Successful in 12s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m2s
Карточки Циана доставались побочным эффектом cian_history_backfill, а её выборка ключуется по offer_price_history. Следствия на 30.08: карточка есть у 4494 из 25222 объявлений (17.8% — последнее место при втором месте по объёму), 19046 без истории при квоте 100/сутки (190 дней на остаток, тогда как очередь растёт вдвадцатеро быстрее), и 1697 объявлений с историей и без карточки, которые исторической выборке недостижимы в принципе. Фетчер при этом исправен: прогоны 5154/5240/5328 дали 100/100, 99/100, 100/100. Чинить нечего — не выдана мощность. Добавлен второй режим выборки (listings_pending="detail", по detail_enriched_at, свежие первыми) и второе расписание поверх ТОГО ЖЕ тела: машинерия работает, дублировать её новым модулем незачем. Историческая выборка оставлена побайтово — по ней живёт суточный прогон. batch_size=400 не на глаз: замеренный темп ~28с на объявление, порог reap_zombies 6ч по heartbeat, бюджетного сторожа у задачи нет — 400×28с≈3.1ч проходит, 800 как у Яндекса (≈6.2ч) убивало бы жнецом. Расписание засеяно enabled=false, как domclick_detail_backfill в миграции 175: это третий круглосуточный добор на общий пул из четырёх узлов, влияние на соседей надо посмотреть, а не предположить. Тесты: 13 проверок, ключ выборки / порядок / неизменность прежнего режима / проводка параметров через посредника / регистрация обоих source. Проверено мутациями: снятие ORDER BY, молчаливый дефолт вместо ValueError и потеря listings_pending в посреднике роняют по 2-3 теста каждая. Набор целиком — 5196 passed, 37 skipped. |
||
|
|
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 |
||
| c467584de3 |
fix(mera/landing): «Цена ДКП» — цена договора, а не произведение; экспозиция считает и Домклик
Витрина показывала fact_rub = price_per_m2 * area_m2, хотя deals.price_rub лежит в той же строке и не использовалась. price_per_m2 в базе integer, поэтому под подписью «Цена ДКП» ехала реконструкция: 4 799 995 вместо 4 800 000, 3 649 995 вместо 3 650 000 (прод, сделки 5777343 и др.). Теперь price_rub едет из выборки (DealSample.price_rub) и показывается как есть; err_pct считается от той же величины. Строка без price_rub НЕ показывается — подставлять реконструкцию в одну строку из двадцати значило бы спрятать тот же дефект (на проде price_rub заполнен у 33 555 из 33 555 сделок выборки витрины). Вторая находка аудита (listing_date якобы «когда увидели МЫ», экспозиция занижена втрое) НЕ ПОДТВЕРДИЛАСЬ. listing_date пишут cian (added_ts), yandex (creationDate) и avito (дата карточки выдачи) — это дата публикации у источника. Там, где заполнены и listing_date, и publish_date, они совпадают: yandex 10 761 из 10 903, avito 474 из 569, медиана разницы 0 дней. 75 дней у аудитора — эффект другой ВЫБОРКИ: publish_date есть у 15 058 активных строк (yandex + Домклик, оба старые), listing_date — у 25 982 (плюс cian с медианой 17 дней и 87% avito с медианой 19). Настоящий дефект рядом: по одному listing_date Домклик выпадал целиком (0 из 3061 активной строки), метрика считалась по 83.6% активных объявлений, и подпись об этом молчала. COALESCE(listing_date, publish_date) → охват 95.2% (29 568 из 31 068), медиана та же — 26 дней; охват теперь назван в note. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 2390eec740 |
fix(mera/b2c): вернуть координаты сделки, снесённые моим же коммитом про студию
Коммит |
|||
| ca3f073b0e |
feat(mera/b2c): улица сделки доезжает до витрины готовой схемой, а не геометрией
Карта в карточке игры показывала полигон района — единственную геометрию, до которой у tradein был доступ. Улицы живут в базе gendesign, foreign table и гранта на них не было. Мост по образцу соседей (v_tradein_cad_buildings / v_tradein_osm_poi_ekb): gendesign 195 — вьюха v_tradein_osm_roads_ekb (highway + water) с GRANT В ТОМ ЖЕ ФАЙЛЕ (после #3227 грант отдельной миграцией теряется при пересоздании); tradein 281 — foreign table gendesign_osm_roads_ekb плюс колонки street_name / street_scheme в landing_showcase_deals. Пересчёт витрины кладёт в строку УЖЕ СПРОЕЦИРОВАННЫЕ SVG-пути окна 840x840 м вокруг центра улицы. Проекция — та же равнопромежуточная с cos(широты), что в export_ekb_districts_svg.py; второй в проекте нет. Замер 2026-08-29: GeoJSON того же окна 7-8 КБ на строку, схема — 2.8-2.9 КБ. ЧТО ДАННЫЕ ВЫДЕРЖИВАЮТ, И НИ СЛОВОМ БОЛЬШЕ · Это УЛИЦА, а не дом: deals.address уровня улицы, номер дома у 2.7% сделок. В схеме намеренно НЕТ координат окна и констант проекции — точку дома по ней нельзя поставить даже случайно. Это замок, а не забывчивость. · Зданий нет: cad_buildings — 18 307 контуров на город, в плотном центре 51 здание на радиус 450 м, где их в разы больше. Нарисованная застройка заявляла бы полноту, которой в данных нет. · Улицы — фильтрованная выгрузка «источников шума»: именованные покрыты хорошо, дворовые и служебные проезды отсутствуют. · Улица сматчилась у 550 названий из 654 — 31 410 сделок из 34 021 (92.3%). Остальным street_scheme = NULL, и это штатно: фронт показывает район, который для этого и оставлен. · Перекрёсток («Челюскинцев/Шейнкмана») берём первой улицей: таких адресов три на 34 021 сделку, и обе улицы одинаково верны на уровне улицы. · Наличие схемы НА ОТБОР СТРОК НЕ ВЛИЯЕТ — то же правило, что запрещает отбор по величине ошибки: иначе витрина показывала бы не работу оценщика, а те 92% адресов, что удобно легли на OSM. Тесты (каждый сломан вручную и покраснел): нормализация на реальных адресах включая ё/е и «8 Марта»; недоступная вьюха и упавший запрос дают None, а не исключение; схема не раздувается — потолок в байтах на реальной плотности плюс прямая проверка округления до 0.1. |
|||
| 4e3279d448 |
fix(mera/b2c): студия подписывалась как «0-к» на витрине сделок
Увидел на скриншоте карточки игры: «0-к, 25,9 м²». Замер на проде — 3 строки витрины из 20 имеют rooms = 0, то есть каждая шестая карточка так и выглядит. «0-к» читается как ошибка выгрузки, а не как тип квартиры. Студией её называет и наш собственный бэктест (per_rooms.label в scripts/backtest_estimator.py), и рынок. Тест двусторонний и фальсифицирован: возврат «0-к» для rooms=0 красит его по значению, обратная правка — снова зелено. |
|||
| f3401badd0 |
feat(mera/b2c): координата сделки доезжает до витрины — с записанной границей честности
Карта лэндинга не может показать точку, пока её нет в витрине: район, комнаты, площадь и квартал в `landing_showcase_deals` есть, координаты не было. Что сделано: миграция 280 добавляет lat/lon (double precision, NULLABLE), задача пересчёта переносит их из `deals`, ручка /showcase отдаёт их как Optional[float]. ГРАНИЦА ЧЕСТНОСТИ, записанная в трёх местах (COMMENT колонок, `note` каждой строки, докстринг задачи), а не только в голове автора: это ЦЕНТРОИД УЛИЦЫ, а не дом. Замер на проде 2026-08-29 по той самой выборке, из которой набирается витрина (deals, city='Екатеринбург', deal_date >= '2025-01-01'): 34 021 сделка, 34 017 с координатой, но РАЗЛИЧНЫХ точек всего 991 — ≈34 сделки в одной точке, при 2.7% известных номеров дома. Точка верна на масштабе района и улицы и неверна на масштабе дома; `note` едет на фронт вместе с числами, поэтому следующий, кто возьмётся зумить карту, об этом споткнётся. NULLABLE и без отбраковки: строка без координаты остаётся на витрине с lat=lon=None. Выбрасывать сделку за отсутствие точки — отбор по признаку, не связанному с качеством оценки, то есть та же порча витрины, которую здесь уже чинили (порог по величине ошибки). Тесты двусторонние, каждый проверен фальсификацией — краснеет по ЗНАЧЕНИЮ: * перестановка lat/lon в build_row → 60.6055 == 56.8386 * `if lat is None: return None` → None is not None * перестановка lat/lon в ручке → 60.6055 == 56.8386 * фильтр строк без координат в ручке → len([]) == 1 Перестановку широты и долготы не ловит ни схема, ни тип (обе float), поэтому в фикстурах намеренно непохожие величины: 56.8386 против 60.6055. Фронтенд не тронут — его делает следующий шаг. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| abe559cf8f |
fix(mera): витрина больше не отсеивает промахи оценщика, счётчики едут на фронт
Ревью MAJOR по честности, два пункта. 1. Убран MAX_ABS_ERR_PCT = 40 из build_row. Докстринг модуля сам запрещает отбор по величине ошибки, но запрет был реализован только в _sort_key, а фильтр — тот же отбор ступенькой раньше, и злее: строка не попадала даже в кандидаты. Обоснование «отклонение >40% — почти всегда занижение ДКП ради налога» не держится: _load_sample уже режет выборку санитарным диапазоном ₽/м² (для ЕКБ это глобальные PPM2_MIN=30k / PPM2_MAX=600k — город намеренно не заведён в deal_city_price_bands), то есть грубые занижения вырезаны выше по потоку и ПО СВОЙСТВУ САМОЙ СДЕЛКИ. Всё, что после этого дало большую ошибку, — работа оценщика, и посетитель обязан её видеть. Честность про заниженные ДКП перенесена в note каждой строки. Заодно убраны MIN_FACT_PPM2=30k (дублировал уже применённый фильтр) и MAX_FACT_PPM2=1.2M (недостижим при потолке выборки 600k): из трёх отбраковок в проде срабатывала ровно одна — та, что льстила витрине, а два мёртвых порога читались как работающие. Осталась только структурная отбраковка «нет прогноза / квартала / площади». 2. Счётчики прогона выведены в ответ ручки. Итог пересчёта пишется в landing_showcase_runs (миграция 277) и уезжает в ShowcaseResponse.stats вместе с правилом отбраковки: показано 20 из N годных, рассмотрено M сделок. Отдельная таблица, а не колонки в строках, — иначе в самом важном случае (показывать нечего) счётчики исчезли бы вместе со строками. Ручка теперь берёт и строки, и числа ИЗ ОДНОГО прогона: иначе пустой прогон показал бы вчерашние строки под сегодняшними счётчиками. Тесты двусторонние и проверены на сломанном коде: возврат любого порога по ошибке → красный с величиной отклонения в сообщении; возврат любой границы ₽/м² → красная своя половина; stats=None при живом прогоне → красный. |
|||
| b72dbc5da3 |
feat(mera): витрина лэндинга на реальных ДКП-сделках вместо выдуманных
Лента «МЕРА сказала X — продали за Y» жила на константах в marketing-v3.ts. Здесь появляется её настоящий источник: сделки Росреестра по ЕКБ, прогнанные через тот же спайн оценщика, что и боевой расчёт (backtest_estimator). Отбор строк идёт по полноте данных и свежести квартала и НЕ смотрит на величину ошибки: отбор по малой ошибке дал бы формально работающий код и врущую витрину — показанные строки перестали бы быть выборкой из работы оценщика. Свойство закреплено двусторонним тестом. Витрина не показывает адреса (номер дома есть у 2.7% сделок) и не показывает дня сделки (deal_date — первое число квартала). Каждая строка несёт note о том, что замер не point-in-time. Заниженные ради налога ДКП отбрасываются по |отклонению| > 40% и ₽/м² вне [30k; 1.2M], счётчик отброшенного — в лог. |
|||
| e9a2fff0b3 |
test(mera/b2c): гейт на сам SQL доли снижений + чистка протухших метрик
All checks were successful
CI Trade-In / changes (pull_request) Successful in 13s
CI / changes (pull_request) Successful in 16s
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 5m32s
Ревью: гейт охранял не то место. Подмена знаменателя красила три теста, но дефект «84.8% вместо 48.1%» живёт в SQL — во включении однострочных записей истории (у domklik одна запись = «цену не менял») в знаменатель. Ревьюер вернул дефект условием n_rows >= 2 в CTE moved, и все 25 тестов остались зелёными: текстовые пины держали только span_days и max_abs_pct. Новый пин держит обе половины: однострочные попадают в moved веткой CASE со значением 0, и нигде в запросе нет фильтра по числу записей истории (ни в WHERE, ни HAVING). Живой прогон на подготовленных строках не заведён намеренно: DATABASE_URL в тестовой джобе — заглушка, Postgres там нет, и тест по образцу test_purge_expired_trade_in_data.py молча скипался бы, то есть не гейтил бы ничего. Фальсифицировано руками — с n_rows >= 2 тест красный и называет причину. Второе: метрика, у которой пропал вход, больше не доживает в таблице со старым computed_at (ручка отдавала её неотличимо от свежей). Строки вне сегодняшнего набора удаляются в той же транзакции. На ПУСТОМ наборе чистка не ходит: разом отвалившиеся все входы — признак поломки прогона, а не пяти одновременных «данных больше нет». Оба поведения покрыты тестами, оба проверены на сломанном коде. |
|||
| b5645ec1bc |
feat(mera/b2c): витринные метрики лэндинга считаются по проду
All checks were successful
CI Trade-In / 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 / changes (pull_request) Successful in 13s
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 5m13s
Числа на публичном лэндинге лежали литералами во фронте
(mera-public/marketing-v3.ts) — то есть были выдуманы и не имели срока
годности. Теперь их считает ночная задача и отдаёт публичная ручка,
вместе с размером выборки и описанием того, что именно измерено.
Что считается: число расчётов и период работы, медиана аналогов на
расчёт, медианная ЭКСПОЗИЦИЯ активного объявления по ЕКБ (не срок
продажи — так и написано в note), доля снижавших цену и медианное
снижение за 30 дней, сделки Росреестра по ЕКБ за 12 месяцев.
Ценовые метрики берут ТОЛЬКО domklik: у avito/yandex триггер не пишет
стартовую цену, а yandex вдобавок сеет синтетическую пару со сдвигом в
сутки — на такой смеси «снизил» и «не снижал» неразличимы. Знаменатель
доли — все объявления, наблюдавшиеся от 14 дней, включая не менявшие
цену; считая только по менявшим, получили бы 85% вместо честных 48%.
Метрика без входных данных строку НЕ пишет: подставленный ноль читался
бы как измеренный ноль. Пустая таблица — валидные {} и 200, а не 500.
«Точность прогноза» и «срок продажи» здесь не считаются намеренно —
таких величин в данных нет.
|
|||
|
|
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 |
|||
|
|
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 |
||
| 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
|
|||
|
|
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 |
||
| 3d0541b2e2 | Merge origin/main into fix/2656-anchor-ratio-freshness — конфликты: метки #2656→#2661 (main), импорт-union, baseline unrecorded=0 (перезахваченная фикстура) | |||
| 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 гарантированно шли по одному срезу. |
||
|
|
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> |
|||
|
|
e00dcac177 |
fix(tradein/deactivate): resolve migration 264 renumber collision on merge with main
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / 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 4m27s
fix/tradein-ttl-effective-cap (PR #2907, merged into main while this branch was in flight) already claimed 264/265 for deactivate_stale_avito_cap_mult / deactivate_stale_yandex_cap_mult. Renumbers this branch's 264_seed_deactivate_stale_null_segment_yandex_cian.sql -> 266_seed_... (git mv + _manifest_applied.txt entry moved after 264/265 + self-references in the migration header and in test_deactivate_stale_listings.py's _MIGRATION_264 constant/test names). Merges main's bool-guard (ttl_days/cap_mult reject bool) + CAP_MULT ceiling + per-source cap_mult calibration with this branch's null_segment_only kwarg (explicit `listing_segment IS NULL` predicate, since ANY(:segments) never matches NULL) -- both features apply to the same deactivate_stale_listings() call site in product_handlers.py and the same function signature/docstring in deactivate_stale_avito.py, so every conflict was signature/docstring-level, not logic-level (git already auto-merged the function body correctly since the two features touch disjoint lines below the signature). Also reconciled the module docstring's "known gap" note (main) to reflect that the NULL-segment slice it measured (cian 211 / yandex 523 rows >60d) is now closed by this migration -- novostroyki stays open, unrelated to this branch. |
||
|
|
08cff706ec |
docs(tradein/deactivate): убрать неверное число из обоснования потолка
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / 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 4m33s
В шапке модуля, в комментарии миграции 264 и в докстринге теста стояло «23 687 из 44 744 avito-объявлений не подтверждались >7 суток» под заголовком «ЗАМЕР НА ПРОДЕ». Число реальное, но приписано не тому. Перепроверено запросом 2026-08-15: это ВСЕ источники вместе, и две трети — новостройки, которых оценщик не берёт (он фильтрует listing_segment IS NULL OR = 'vtorichka'). У самого avito просроченных строк ноль: 8 663 активных, максимальный возраст 10 суток. Оставлять это в коде нельзя: следующий человек прочитает «avito раздут вдвое», проверит и не найдёт — а заодно потеряет доверие к остальным числам в том же абзаце, которые верны и сверены с scrape_runs.counters. Заодно явно записано, чего потолок НЕ делает: он не сжимает пул (0 деактиваций замерено на всех четырёх джобах), а защищает от разгона пола и от опечатки в расписании. Настоящий раздутый срез — строки с пустым сегментом, они чинятся отдельной джобой. |
||
|
|
cfb4c159ab |
fix(tradein/scraper): deactivate stale yandex/cian listings with NULL segment
544 yandex + 224 cian active rows carry listing_segment=NULL (legacy rows predating migration 011, plus a small trickle that can never self-heal since the upsert ON CONFLICT never rewrites listing_segment on re-scrape). 94-97% of them are frozen at ~86 days old, yet the estimator's Tier A (same-building) and Tier C (micro-radius) anchor queries filter only is_active=true -- no freshness column -- so these stale asking prices anchor live valuations. deactivate_stale_yandex/_cian (migration 115) already run at TTL=30 but scope segments=['vtorichka'] only: `= ANY(CAST(:segments AS text[]))` never matches NULL, so the NULL bucket was invisible to both existing jobs and to avito/domklik/n1 (which are source-blanket or vtorichka-only respectively). Adds null_segment_only kwarg to deactivate_stale_listings() building an explicit `listing_segment IS NULL` predicate (confirmations/revisit-floor builders extended in parallel for correctness, though both gates are kept off for this slice -- population too small for thresholds calibrated on a full vtorichka sweep, would permanently skip as unhealthy). Two new scrape_schedules rows (migration 264) run it per source, untouched novostroyki/vtorichka jobs unaffected. TTL=60d (vs 30d for vtorichka): revisit-floor is not computable here (rows out of dedicated sweep scope have no revisit-gap history), so the margin is folded into the TTL directly -- 2.26x/1.4x over the measured p99 revisit gaps already on file for cian/yandex vtorichka (26.6d/43.0d). Bimodal age distribution means this costs almost nothing in coverage (30d vs 60d: 744 vs 734 deactivated). First run: 734 of 768 NULL rows deactivated (211 cian, 523 yandex), 34 remain (fresher than 60d, still incidentally re-touched). Comparable pool (is_active AND segment IN (NULL, vtorichka)) after: cian -2.7% (7739->7528), yandex -10.2% (5133->4610) -- yandex crosses the 10% flag threshold. All 734 removed rows already had scraped_at frozen >60d, i.e. already excluded from Tier S/H (which do filter freshness, 14-60d window) -- the drop is real for is_active headcount but zero-impact there; it only prunes Tier A/C, where it removes stale prices rather than live comps. Separate finding (not fixed here): base.py's ON CONFLICT DO UPDATE omits listing_segment from SET entirely, so a legacy NULL row can never heal even though cian/yandex SERP always compute segment deterministically on re-scrape. |
||
|
|
19c9da8119 |
fix(tradein/deactivate): bool guard hole + unpinned test + yandex cap_mult gap (TTL-CAP round 3)
Три остатка ревью TTL-CAP: 1. cap_mult < 1 пропускал bool: jsonb true -> True < 1 ложно -> потолок = ttl_days*True = ttl_days -> пол молча отключается без ValueError. Тот же класс дыры возможен и через ttl_days=true (TTL молча = 1). Оба параметра теперь явно отклоняют bool ДО числового сравнения; воспроизведено на HEAD и закрыто тестами (True/False на обоих параметрах). 2. test_avito_prod_floor_is_capped_by_calibrated_cap_mult хардкодил cap_mult=6 как вход -- мутация миграции 264 (6 -> 2) оставляла набор зелёным. Тест теперь читает cap_mult ИЗ ФАЙЛА миграции regex'ом, ожидаемый результат (потолок 60) остаётся зафиксированным числом -- дрейф калибровки в SQL теперь ломает тест. 3. Текст миграции 264 утверждал "yandex 43.0 -> потолок 60, запас есть" по статическому p99. Живые полы из scrape_runs.counters (08-10..08-15: 75/75/75/39/52/54) и live-замер сегодня (79.2, n=1961) выше потолка 60 -- тот же false-kill класс, что у avito. Откалибровал yandex отдельной миграцией 265 (cap_mult=3 -> потолок 90, тот же запас ~14%, что у avito), поправил таблицу в 264 на живые числа и пиннящий тест по образцу avito. Численный эффект (live-замер 2026-08-15, до и после): next-run deactivated=0 на всех четырёх джобах что до, что после -- ветка по-прежнему НЕ сжимает пул (avito/cian: живой пол уже ниже потолка, cap не участвует; yandex: 0 активных строк старше 39 суток вообще, калибровка убирает будущий риск, не текущее число; domklik: блокирован гейтом здоровья, confirmations 94 < 200). Ветка остаётся тем, чем и была: защита от опечатки в расписании + калибровка, не сжатие пула. 4508 backend-тестов зелёные (uv run pytest tests/), ruff чист на изменённых файлах. |
||
|
|
772ae116b5 |
fix(tradein/deactivate): validate cap_mult, calibrate avito, document scope gap
Round-2 review (MAJOR) left three items open: 1. cap_mult was threaded through as a jsonb default_params parameter but never validated, reproducing the exact ttl_days<=0 hole the earlier guard closed. Verified live: cap_mult=0 -> effective_ttl=0 -> whole active pool of the source would deactivate; cap_mult=0.5 pushes the ceiling BELOW the operator- configured ttl_days. Added `if cap_mult < 1: raise ValueError` next to the ttl_days guard (same fail-fast contract, before any SQL). Non-numeric values (e.g. a stringly-typed "6" from a typo in default_params) already fail safe via TypeError on the comparison, caught by the same except-block -> mark_failed. Covered with 5 new tests (zero/negative/<1/non-numeric/mark_failed routing). 2. The mechanical part of cap_mult (parameter + wiring) was merged but never calibrated for avito on prod -- no migration shipped, so prod default_params for deactivate_stale_avito still lacked "cap_mult" and ran with the module default (CAP_MULT=2, ceiling=20d), which is BELOW avito's own p99 revisit gap (42.1d) and below the observed prod peak (floor=52, three runs 08-10..08-12). Added data/sql/264_deactivate_stale_avito_cap_mult.sql (idempotent, same pattern as 219) setting cap_mult=6 for deactivate_stale_avito only (ceiling 60d, matching the order of magnitude already used for cian/yandex). cian/ yandex/domklik keep the CAP_MULT=2 default -- their p99 gaps (26.6/43.0/3.1) sit comfortably under their default ceilings (60/60/28), no override needed. Pinned the calibration with a dedicated test (test_avito_prod_floor_is_capped_by_calibrated_cap_mult) instead of leaving the avito slice skipped in the false-kill coverage test. 3. Confirmed (SSH read-only, prod counts): active rows aged >60d that this PR cannot touch regardless of cap_mult -- cian/novostroyki 9483, cian/NULL 211, yandex/NULL 523 (0 inside the jobs' actual scope: cian/vtorichka, yandex/vtorichka). deactivate_stale_cian/_yandex are scoped to segments=['vtorichka'] by a deliberate, documented DECISION (blanket TTL on novostroyki risks killing live inventory cian/yandex don't fully sweep). Widening that scope is a separate, riskier investigation and is out of scope here -- documented the gap directly in the module docstring next to the existing DECISION so it isn't lost. Verification (SSH read-only against prod, 2026-08-15): recomputed the exact per-source formula the next scheduled run will use. In-scope next-run deactivation is currently 0 for all four sources -- the active pool has already self-corrected to be consistent with each source's own recent effective TTL (yesterday's yandex run used effective=54, so no active row is older than that yet). This matches the round-2 reviewer's own conclusion: the cap is a preventative guardrail, not a retroactive cleanup, and isn't expected to fire on the exact day it's calibrated. It is not idle, though -- live recompute of yandex/vtorichka's raw (uncapped) floor right now is 78.2d, already above its 60d ceiling; the trailing 6-day counters show the identical loop (floor=75, deactivated=0, three days straight) already recurred twice without this cap in place. The mechanism will bind the moment the pool ages past the ceiling, which is exactly the recurrence it exists to stop. Tests: 106 passed (test_deactivate_stale_ttl_cap.py, test_deactivate_stale_revisit_floor.py, test_deactivate_stale_health_gate.py, test_deactivate_stale_listings.py, test_migrations_manifest.py). ruff clean. scripts/check-migration-lock-timeout.py: pass (UPDATE-only migration, no blocking DDL, no SET LOCAL needed). |
||
|
|
cb79c67bfc |
fix(tradein/deactivate): make TTL-cap multiplier configurable per source
Review of
|
||
|
|
3a1e29a7da |
fix(tradein/deactivate): cap effective TTL floor at 2x configured value
Revisit-floor (#2659) raises effective TTL via max(ttl_days, floor) with no upper bound -- a positive feedback loop confirmed on prod: slow crawl raises the floor, a high floor keeps stale listings marked active longer than a fresh sweep needs to return, the "active" pool bloats with rot, and the next floor measurement on that bloated pool comes out even higher. Yandex counters sat at ttl_days_effective=75/75/75/39/52/54 for six runs straight with deactivated=0; 23,687/44,744 "active" avito listings hadn't been confirmed in >7 days, cian 10,572/19,514 and yandex 7,178/15,790 were >30 days stale, the oldest "active" row hadn't been seen in 86 days. CAP_MULT=2 caps the floor's upward push without disabling it -- the floor still protects against premature deactivation during genuinely slow (but alive) crawl cycles, it just can no longer grow unbounded. Beyond 2x, a persistently low crawl rate is better handled by the existing health gate (min_confirmations), which disables deactivation outright instead of stretching TTL forever. When the cap binds, counters gain ttl_floor_capped=1 + ttl_days_floor_raw (the uncapped value) so it's visible in the run-history dashboard, not just logs -- counters are stored as-is in scrape_runs.counters. Single fix point: all four sources (avito/yandex/cian/domklik) route through this one deactivate_stale_listings() via the product_handlers wildcard "deactivate_stale_*" handler, so no other task file needed the change. |
||
| 01b5e73ea4 |
feat(tradein): вся Свердловская область — 40 городов в city-sweep (#2879)
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 3m33s
Deploy Trade-In / build-backend (push) Successful in 1m41s
Deploy Trade-In / deploy (push) Successful in 1m38s
|
|||
| d7017ebfee |
Merge remote-tracking branch 'origin/main' into HEAD
Some checks failed
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 9s
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 4m21s
|
|||
| d6c000eddb |
docs(tradein): снять устаревшее обоснование обрыва прогона Домклика (#2854) (#2859)
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 3m30s
Deploy Trade-In / build-backend (push) Successful in 1m7s
Deploy Trade-In / deploy (push) Successful in 1m36s
|
|||
| 3cd7e0a9c4 |
fix(tradein/sber): сторож мерит отставание загрузки, а не календарь (#2846) (#2849)
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 3m32s
Deploy Trade-In / build-backend (push) Successful in 1m2s
Deploy Trade-In / deploy (push) Successful in 2m3s
|