967 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 721ceb9876 |
fix(tradein): выборка миграции 286 повторяет гейт 1:1, правило первой точки (#3376)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
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 4m56s
Ревью нашло, что миграция и код ловили РАЗНОЕ. Миграция брала базой предыдущую СЫРУЮ строку (lag), гейт — предыдущую ОСТАВЛЕННУЮ. На 1M→10M→1M→10M (цена 1M) lag-версия удаляла честную точку, на 1M→10M→1.05M→9.9M — не была идемпотентной (второй прогон доедал 9.9M). Теперь кандидаты выбирает PL/pgSQL-цикл, пошагово повторяющий drop_decimal_slips, а правило первой точки — отдельным INSERT..SELECT уже по ОСТАВШИМСЯ строкам. Правило первой точки — из прод-разбора: 12 из 20 остатков domklik это серии вида 330 000 → 3 300 000 (текущая цена 3 300 000) и 420 000 → 4 200 000 → 4 500 000, где дефектная точка ПЕРВАЯ и базы слева у неё нет. Свидетелей по-прежнему два: ×10 ко второй точке И подтверждение второй третьей-или-текущей-ценой. Решение по первой точке принимается по kept-серии, а не по сырой, — иначе гейт теряет идемпотентность (перебор ловит 1122 таких прогона). Идемпотентность доказана НА ГЕЙТЕ: property-тест gate(gate(s)) == gate(s) по всем сериям длины 2-6 (19 525 серий × 3 текущие цены). Фальсифицирован обеими поломками — сырая база даёт 136 красных прогонов, сырые соседи первой точки 1122. Раз SQL зеркалит гейт, свойство переносится на миграцию. Ещё в 286: третий свидетель ПРОТИВ удаления (цена подтверждена триггерной строкой того же объявления — значит она реально наблюдалась в listings.price_rub) и финальный шаг |diff_percent| > 100 → NULL по всем источникам, то же правило, что validate_diff_percent на записи. Ожидаемое число удалений в шапке — 35 + ~12 из двухсвидетельского предзамера, а не 76 (то была односвидетельская цифра). Прогон обеих фаз дважды с ROLLBACK — tradein-mvp/scripts/sql/286_dryrun.sql. yandex: проводка гейта снята как мёртвая. На том пути серия из двух точек, а свидетель последней — текущая цена лота, то есть она же сама: ветка по построению не могла выбросить ничего. Оставлен честный комментарий-потолок и ссылка на follow-up (отлов требует DELETE на следующем наблюдении). cian: после выброса точки соседу пересчитывается diff_percent (было только у domclick). domclick: цена листинга берётся RETURNING'ом у UPDATE вместо отдельного SELECT по PK, пересчёт вынесен в общий recompute_diff_percent с гейтом на пустую цену (ручной ingest кладёт price_changes из JSONL без валидации). |
|||
| 0c21e522c0 |
fix(#3197): пустой пул пробрасывается из cian fetch_detail; тест через настоящий fetch_detail
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 4m58s
|
|||
| 142967d064 |
fix(tradein): гейт против сдвига разряда в offer_price_history (#3376)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 14s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m4s
В priceHistory источников встречаются точки ровно ×10/÷10 к соседям с возвратом к базе следующей же точкой — это потерянный разряд у источника, а не рынок. Прод-замер 06.09.2026: 76 таких строк у 55 объявлений (domklik 48, cian 21, yandex 6; единственная avito-строка — триггерная). Читатели колонки — медианный торг лендинга (#3223), админка, /scrapers. drop_decimal_slips живёт рядом с validate_diff_percent — на той же единой границе записи, что и гейт #3225, и подключён ко ВСЕМ писателям истории (domclick/detail.py, cian/detail.py, yandex_price_history.py). Критерий требует двух свидетелей: скачок ×10 к предыдущей точке И возврат к базе у следующей; у последней точки серии свидетель — текущая цена объявления, нет и её → точку не трогаем (без второго свидетеля ×10 может быть честной сменой цены). У domklik после выброса пересчитывается diff_percent соседа: парсер считал его от базы, которой больше нет. Миграция 286 чистит уже собранные строки тем же критерием и только у загрузчика (change_time <> recorded_at): триггерные строки — это живые смены listings.price_rub, у них другая база отсчёта (см. 285). Падает, если кандидатов больше 200. |
|||
| 8129e21b95 |
fix(tradein/cian): суточный бэкфилл истории — в прокси-пул, а не в env-прокси (#3197)
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 4m51s
BrowserFetcher(source="cian") в cian_history_backfill конструировался без proxy_provider/use_pool/environment — трёх аргументов, которые кладут "proxy" в тело POST /fetch. Сайдкар брал свой env-прокси (SCRAPER_PROXY_URL): пул из 4 узлов, его баны и ротация проходили мимо, а прод-отказ «пул пуст → не ходить на env/direct» (#2616) на этом пути был мёртв, потому что смотрит на environment. Проводка теперь как у соседей — domclick_detail_backfill и house_imv_backfill. Ожившему отказу нужен обработчик: NoProxyAvailableError ловился общим except на объявление, и батч крутил впустую весь список (пул пуст с первого — значит пуст и на 1000-м). Распознаём по цепочке причин, обрываем прогон, counters.no_proxy_stop=1 и mark_failed вместо mark_banned — отказ нашей стороны не должен записываться как бан Циана. Дома и оценки после стопа пропускаем: они идут через тот же пул. caused_by_no_proxy вынесен в scraper_kit.proxy_errors (у avito #3288 и domclick #3283 живут приватные копии — их схлопывание отдельной правкой). |
|||
| 990a59e469 |
fix(rbac): анониму на admin-префиксе отвечать 404, а не 401
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 5m6s
#3360. Периметр /trade-in/api/v1/admin/* снаружи не срезан (caddy/sites/apps.caddy: блок `handle /trade-in/api/*` стоит выше `import caddy/users.caddy.snippet`), и срезать его нельзя: admin-UI кабинета зовёт эти пути ИЗ БРАУЗЕРА (12 файлов tradein-mvp/frontend/src — scrapers/**, components/scrapers/**, admin-audit-api.ts, GuardedRoute). Значит внешний аноним доходит до rbac_guard, а после #3324 (несуществующий путь → 404 роутера) 401 на существующей ручке стал оракулом: перебором имён восстанавливался список admin-API. Все три ветки «личность не установлена» (нет X-Authenticated-User, auth_mode=db_only, подделанный заголовок без #2213-секрета) на admin-префиксе теперь отдают ровно то же, что роутер даёт на несуществующий путь. Аутентифицированные не тронуты: admin — 200, не-admin — 403 «admin only» (прятать наличие ручки от опознанного человека незачем). Смоук периметра: пара admin-путей — существующий /admin/proxies и несуществующий /admin/users — оба обязаны быть 404 снаружи. |
|||
| 7afaa12d75 |
Merge pull request 'fix(tradein/avito): брейкер добора считает долю только по отказам площадки — сайдкар/пустой пул не рвут прогон и не красят его banned' (#3367) from fix/3288-breaker-block-kind into main
Some checks failed
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 4m3s
Deploy Trade-In / build-backend (push) Successful in 1m5s
Deploy Trade-In / deploy (push) Failing after 32s
Deploy Trade-In / perimeter-smoke (push) Has been skipped
Deploy Trade-In / deploy-status (push) Failing after 1s
|
|||
| c82ec7c3af |
fix(tradein): понижать статус только у прогона, который работу сделал (#3288)
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 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 4m59s
Прошлая правка понижала banned→failed/done по одному диагнозу infra и ломала обратный контракт: нулевой прогон с infra (yandex 5xx #3196, финализатор #2764) получал 'failed' — настоящий бан площадки, опознанный как infra, прятался под «нашу поломку». Хуже исходного дефекта: 2 красных теста в полном прогоне. Понижение сужено до случая прогона 5425 — dominant='infra' И produced > 0: брейкер оборвал по доле, а карточки при этом обогащались → 'done'. Нулевой прогон остаётся 'banned' (честность несёт ban_kind), пустая перепись — тем более: dominant='unknown', статус не трогаем. Тесты: контроль на обратную ошибку (ноль результата → banned+infra) и на пустой census (→ banned+unknown); основной кейс {'infra': 20} при 10 обогащённых — не banned. |
|||
| 6ced16618c |
Merge pull request 'fix(tradein/yandex): недорендеренная карточка — отказ, не успех: структурный маркер encryptedPhones + порог размера, карточка остаётся в очереди' (#3364) from fix/3191-yandex-detail-completeness 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 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 4m2s
Deploy Trade-In / build-backend (push) Successful in 2m4s
Deploy Trade-In / deploy (push) Has been cancelled
|
|||
| 78e3da6bc3 |
fix(tradein): маркер полноты — encryptedPhones ИЛИ redirectPhones (#3191)
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 5m21s
Ревью #3364. Требуя именно encryptedPhones, отбраковывали бы вечно необмеренный класс карточек «без телефона / только чат»: в очередь они возвращаются, а ключ не появится. redirectPhones измерен тем же замером #3192 и присутствует в обеих ветках (с куками и без). Текст ABORT: consecutive_none смешанный (фетч-ошибка + parse-None + недогруз) — «N подряд без обогащения», а не «недогруженных». |
|||
| ec8213c269 |
fix(payments): обрыв соединения и Init без PaymentURL перестают запирать покупателя
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 5m11s
Три дыры в одном замке (строка без payment_url невидима для _find_live_payment, но видима предикату UNIQUE 279 → ложный 409 на 30 минут): - tbank_client ловил пару (TimeoutException, NetworkError): RemoteProtocolError, ProxyError и UnsupportedProtocol летели наружу голым httpx-типом мимо `except TBankApiError` в checkout. Ловим родителя — httpx.TransportError. - Ветка «Success:true без PaymentURL» отвечала 502, не трогая статус, — здесь банк заказ ПРИНЯЛ. Тот же терминальный статус, error_code=no_payment_url; UPDATE вынесен в общий _mark_init_failed. - _FakeDb в тестах применял статус по наличию ключа в params, а не по тексту SQL: мутант без `SET status = :status` оставался зелёным. Гейт по SQL — мутант краснит все три теста про замок. Комментарий про «возможный холд» на ветке отказа Init поправлен: Init холд не создаёт, авторизация идёт с оплаты формы, а форму покупателю не выдавали. |
|||
| 1a693192e1 |
fix(tradein): вид блока решает обрыв по доле и статус прогона (#3288 часть B)
Some checks failed
CI Trade-In / changes (pull_request) Successful in 7s
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 / openapi-codegen-check (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Failing after 5m19s
Прогон 5425 оборвался по доле блоков и получил статус banned на 48 «блоках», из которых 41 был отказом нашего сайдкара: record_block() вида не принимал, поэтому infra падал в числитель скользящего окна #3184 наравне с настоящим баном, а mark_backfill_finished считал диагноз только ради телеметрии. - record_block(kind): в числитель идёт только platform; всё остальное — в знаменатель (как record_failure), мимо серии и safety-net; - NoProxyAvailableError у avito — не блок и не отказ площадки: прогон завершается no_proxy_stop=1 + mark_failed «пул прокси пуст» (как домклик после #3283); опознаётся по цепочке __cause__, не по подстроке (#3272); - статус banned — только при доминировании platform; при infra прогон получает failed (нулевой результат) или done, с честной причиной. |
|||
| 4b5f8fb54c |
fix(tradein): недогруженная карточка Яндекса — отказ, а не успех (#3191)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / 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 5m11s
Страница на 1,8 МБ без блока контактов приходит с HTTP 200 и валидным HTML: window.INITIAL_STATE на месте, parse отрабатывает — и частичная карточка уезжала в БД с detail_enriched_at, выбывая из очереди навсегда. Единственная проверка размера (newbuilding.py, len(html) < 500) отвечала на вопрос «пришло ли хоть что-то»: 1,8 МБ проходит её в 3600 раз. Признак полноты структурный + размерный, любой из двух даёт отказ: encryptedPhones (65 вхождений у полных карточек, 0 у недогруза; отдаётся и анонимной сессии — см. yandex_session.py) и settings.yandex_detail_min_html_bytes (1 МБ). Наблюдавшийся недогруз ловит именно структурный: 1,8 МБ порог проходит. В backfill проверка стоит ДО parse: исход incomplete ⊆ failed, save не вызывается, значит detail_enriched_at не проставляется и следующий снапшот (detail_enriched_at IS NULL) возьмёт объявление снова. Серия недогрузов двигает consecutive_none — тот же брейкер, что у parse→None, поэтому вечно недогружаемая карточка обрывает прогон, а не молотится (per-listing счётчика попыток в схеме нет). Фейковые ответы в тестах-соседях (#3196/#3338) теперь при HTTP 200 выглядят полной страницей — иначе они молча стали бы кейсами про полноту. |
|||
| 5bd9c586b7 |
fix(payments): отказ банка на Init больше не запирает покупателя на 30 минут
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
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 4m52s
Строка после провального Init оставалась в NEW и без payment_url: для _find_live_payment (фильтр payment_url IS NOT NULL) её нет, а для предиката UNIQUE миграции 279 — есть. Следующий checkout ловил конфликт и получал 409 'retry shortly' до истечения _ABANDONED_AFTER_MINUTES — из-за сбоя банка, а не своего действия. Переводим такую строку в терминальный DEADLINE_EXPIRED (тот же, которым checkout уже помечает брошенные попытки) в том же UPDATE, что пишет error_code/error_message: статус вне предиката 279, пара (estimate_id, product_code) освобождается сразу. Новый статус в CHECK 233 не заводим — миграция ради ярлыка не нужна, причина и так в error_*. Refs #3323 |
|||
| bab4b4f8ff |
Merge pull request 'fix(tradein/rbac): 401-оракул закрыт — несуществующий путь под публичным префиксом отвечает так же, как любой другой мусор' (#3352) from fix/3324-rbac-401-oracle 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 15s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 2m28s
Deploy Trade-In / test (push) Has been cancelled
|
|||
| 65155e5ccd |
Merge pull request 'fix(tradein/backfill): save-False у yandex/avito больше не теряет попытку — attempted == сумма исходов у всех трёх бэкфиллов' (#3347) from fix/3338-backfill-save-false into main
Some checks failed
Deploy Trade-In / changes (push) Successful in 16s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been cancelled
Deploy Trade-In / test (push) Has been cancelled
Deploy Trade-In / build-backend (push) Has been cancelled
Deploy Trade-In / deploy (push) Has been cancelled
Deploy Trade-In / perimeter-smoke (push) Has been cancelled
Deploy Trade-In / deploy-status (push) Has been cancelled
|
|||
| b8084f6afe |
Merge pull request 'fix(tradein): секрет вебхука GlitchTip не попадает в access-log — скруббер query-секретов + приём из заголовка' (#3353) from fix/3154-glitchtip-secret-log-leak into main
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 24s
|
|||
| 8c4be70193 |
fix(rbac): честный докстринг про размен оракулов + трейлинг-слэш больше не выдаёт маршрут
All checks were successful
CI Trade-In / changes (pull_request) Successful in 16s
CI / changes (pull_request) Successful in 19s
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 5m51s
Ревью #3352: комментарий у _path_is_routed утверждал «ослабления нет» — неверно. Раньше 401 был ПРЕФИКСНЫМ оракулом (таблицу маршрутов по нему не перечислить), теперь 401/404 — оракул СУЩЕСТВОВАНИЯ маршрута, включая имена admin-ручек. Докстринг переписан честно, с проверенным по caddy/sites/apps.caddy фактом: блок handle /trade-in/api/* стоит выше import users.caddy.snippet, внешнего basic_auth у trade-in нет — значит перебор имён выполним и снаружи. Второй канал того же оракула закрыт: /api/v1/me/ не матчил ни один маршрут, guard пропускал, а роутер отвечал 307 на существующий путь. FastAPI получил redirect_slashes=False (проверено: ни одного route с трейлинг-слэшем, ни одного такого вызова во фронте; deny-правила уже на глоб-форме). Тесты: PARTIAL-кейс (POST на GET-путь анониму → 401, не 404/405), трейлинг-слэш на РЕАЛЬНОМ app.main (тест на копии был бы тавтологией), admin-гейт сверяется по тексту 'admin only' — scope-ветка отвечает тем же 403, но 'forbidden for role'. |
|||
| 659543f004 |
fix(tradein/backfill): warning save-False называет оба id (avito) и не дублирует listing_id (yandex)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 15s
CI / changes (pull_request) Successful in 17s
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 6m13s
Ревью #3347, два minor. 1. avito: WHERE в save_detail_enrichment ключуется по source_id из РАЗОБРАННОГО HTML (enrichment.item_id), а warning печатал row["id"] из снимка. При редиректе/подмене карточки строка с row-id жива, и читатель лога идёт искать несуществующую проблему не у той строки («текст ошибки называет гонца»). Печатаем оба: listing_id=%s item_id=%s. 2. yandex: listing_id стоял в строке дважды (в начале и как «id=%d» в хвосте) — убран дубль. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
|||
| 55a67e5c45 |
fix(tradein): маскировать секрет и в параметрах с префиксом (#3154)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
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) Successful in 5m38s
Ревью-minor: якорь [?&] вплотную к имени пропускал client_secret=, refresh_token=, auth_token=, webhook_secret=. Разрешаем префикс [\w.-]* перед альтернацией — маскируем имена, ОКАНЧИВАЮЩИЕСЯ на чувствительное слово. Кейс not_a_secret_name заменён на честные отрицательные: ?secretary= и ?tokens_page= (не оканчиваются на secret/token) плюс /api/v1/token-info в пути. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> |
|||
| ed94a03f73 |
fix(tradein): не логировать секрет вебхука GlitchTip в access-log (#3154)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 12s
CI / changes (pull_request) Successful in 14s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 5m9s
uvicorn печатает в access-log полный путь вместе с query, поэтому секрет вебхука (`?secret=`, он же TRADEIN_INTERNAL_AUTH_SECRET, второй рубеж rbac) уезжал в Loki открытым текстом. Скруббер #3115 в Alloy ловит только форму `user:pass@host` (DSN postgres_exporter) и такую строку не закрывает. Два слоя: - хендлер принимает секрет из заголовка X-GlitchTip-Secret; query-параметр остаётся fallback'ом, т.к. сам GlitchTip 6.1.6 (`send_webhook()`) заголовков не шлёт вовсе — убрать query можно только когда заголовок начнёт подставлять кто-то перед нами (Caddy header_up) или сменится отправитель; - app/core/log_scrub.py: logging-фильтр маскирует значения чувствительных query-параметров (secret/token/api_key/…) на uvicorn.access и на обработчиках корневого логгера — секрета нет уже в `docker logs`. Сравнение секрета и было constant-time (`secrets.compare_digest`). |
|||
| 0ef1880e55 |
fix(rbac): не отвечать 401 за несуществующий путь (#3324)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 15s
CI / changes (pull_request) Successful in 18s
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 5m36s
rbac_guard — HTTP-middleware, он отрабатывает до роутинга и потому отвечал 401 с rbac-текстом даже на пути, которых в приложении нет. Аноним получал бесплатный оракул периметра: мусор под «интересным» префиксом давал 401, а такой же мусор под публичным префиксом — 404 роутера, то есть выключенная ручка была отличима от несуществующей. Guard теперь пропускает запрос дальше, если ни один маршрут роутера не матчится (Match.NONE у всех) — 404 отдаёт тот же роутер, что и на любой другой мусор. Существующие маршруты не затронуты: Match.PARTIAL (путь есть, метод другой) по-прежнему идёт в guard, реальный закрытый маршрут анониму даёт 401, публичный — работает без идентичности. |
|||
| 3658434c03 |
fix(tradein/backfill): save-False у yandex/avito больше не теряет попытку — attempted == сумма исходов
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
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) Successful in 5m41s
Тот же дефект, что #3332 у domclick (#3335), у обоих соседей: `if save(...): enriched += 1` без else. UPDATE, не задевший строку (объявление удалено между снимком и записью), оставлял попытку без исхода — attempted переставал сходиться с суммой исходов, и расхождение читается как потерянный отказ площадки. Разбор ВСЕХ точек выхода из цикла попыток показал, что у соседей это единственная дыра: обрыва «пул прокси пуст» у них нет (resolve_proxy_url бросает ProxyPoolExhaustedError ДО цикла), а budget/SIGTERM-брейки стоят до `attempted += 1`. Исход — failed с отдельным warning про ненайденную строку. Формы тождества разные и это не описка: у yandex blocked ⊆ failed (#3196), у avito blocked/gone/failed — непересекающиеся корзины. Тесты — по значению (числа, не «не бросило»), плюс контроль на противоположную ошибку (исход не начисляется дважды). В test_3332 добавлен запрошенный ревью кейс: транспортный сбой → пустой пул даёт attempted=2 failed=2, а не 3. Closes #3338 |
|||
| aaf8119408 |
Merge remote-tracking branch 'origin/main' into HEAD
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 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 / backend-tests (pull_request) Successful in 5m1s
# Conflicts: # tradein-mvp/backend/app/services/estimator.py |
|||
| 99f112db0b |
Merge pull request 'fix(tradein/auth): единый источник ролей — БД first, YAML только legacy-fallback (закрывает эскалацию до admin и 403 своим)' (#3331) from fix/3316-role-single-source into main
All checks were successful
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / changes (push) Successful in 13s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 4m5s
Deploy Trade-In / build-backend (push) Successful in 1m47s
Deploy Trade-In / deploy (push) Successful in 2m8s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 13s
|
|||
| 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). |
||
|
|
f2f9ac941b |
fix(tradein/pdf): клиентский PDF не называет площадки-источники — «Источник N», как в веб-отчёте (#3341)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 5m7s
Веб-отчёт и лендинг с #3342 не показывают названия площадок (канон publicLabel в frontend/src/lib/source-registry.ts), а клиентский PDF по тому же /estimate/{id} печатал Avito / Циан / «Домклик · Сбер» / Я.Недвижимость / Этажи в пилюлях источников, «подтверждают Росреестр, ДомКлик…» в советах и «на Циан, Авито, Я.Недвижимости» в тарифах. Один клиент — два документа с разной нормой, и юр-риск, ради которого всё делалось, в PDF оставался открытым. - `_SOURCE_DISPLAY_NAMES` → публичные лейблы 1:1 с реестром фронта: avito → «Источник 1», cian → 2, yandex → 3, domklik → 4, etazhi → 5, rosreestr → «Росреестр»; fallback для незнакомого id — «Другой источник», а не `source.title()` (сырой id — та же утечка). - Алиасы (avito_imv, cian_valuation, yandex_valuation, domclick, etagi) канонизируются ДО выбора цвета и лейбла (`_canonical_source`), а списки источников на страницах объявлений и сделок дедуплицируются до среза [:5] (`_public_sources`): иначе `sources_used` = listing ∪ valuation давал «Источник 1, Источник 1, Источник 2, Источник 2, Источник 4» с серой точкой у алиасов и вытеснял yandex. - Цвета пилюль не тронуты: цвет — опознаватель источника, как на вебе. - Тексты: «Росреестр, сделки площадок и продажи агентств», «на основных площадках объявлений» (двойник offer-rates.ts). - Гейт `tests/test_pdf_public_source_labels.py`: видимый текст страниц (без тегов и атрибутов, href на домены площадок законны) не содержит названий площадок и сырых id, по одному кейсу на имя; дедуп алиасов; ветка совета с процентом. Не тронуто: ссылки на объявления (avito.ru/domclick.ru) — отдельное решение; `_QUALITY_SOURCE_SLOTS` — только счётчик, имена не рендерит. |
||
|
|
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 трёх подстрок на проде. |
||
| 4f4345e27c |
fix(tradein): завершить thin-market гейт IMV — Guard-1b и GET-путь (#3323)
All checks were successful
CI Trade-In / 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 / 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 5m23s
MEDIUM-1: imv_anchor_present ставился по anchor_total МИМО гейта — на тонком рынке якорь отброшен, а Guard-1b (#764) продолжал глушить квартальную поправку «потому что якорь есть»: headline не получал ни одной поправки, отброшенный якорь двигал деньги вычитанием. Теперь present = not thin_market. MEDIUM-2: trade_in.py (GET ?id= — расшаренная ссылка/PDF) — третья точка сборки карточки: market_count=0 читался как «неизвестно», thin_market не передавался вовсе → одна оценка показывала thin_market=True в POST и False при переоткрытии. |
|||
| 148da3e5a7 |
fix(tradein): thin-market IMV не двигает деньги (#3323)
All checks were successful
CI Trade-In / 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 / 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 5m6s
avito_imv_thin_market_threshold рождал только warning: IMV с market_count=1 всё равно уходил в blend (w=0.5 при A > median*1.15) и растягивал range_high. Гейт поставлен в _apply_imv_blend — единственной точке, через которую IMV влияет на деньги (обе ветки якоря, imv_anchor и imv_eval, сходятся там): market_count < threshold → no-op, якорь остаётся display-only в карточке. market_count >= threshold и market_count=None (порог не передан) — поведение прежнее. market_count=0 больше не читается как «неизвестно». Warning теперь говорит, что IMV ОТБРОШЕН, а не просто «тонкий рынок». |
|||
| 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 |
|||
| cde95aa9f6 |
test(tradein): прод-конфигурация ролей в приёмке #3316 + компромисс фолбэка
Some checks failed
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Failing after 5m8s
Review PR #3331: приёмка «роли не изменились» гонялась с ПУСТЫМ реестром, а в проде строка в БД есть у 12 из 13 юзеров и DB-роль ИНАЯ (kopylov: manager при YAML pilot, user1: employee при YAML pilot). Добавлены два кейса именно этой конфигурации: * YAML pilot + реестр employee → employee, и scope не поехал: allow/deny DB_ROLE_PATHS['employee'] сверяются со списками роли pilot из roles.yaml целиком — дрейф ЛЮБОГО из двух списков теперь красный тест, а не тихо потерянный/выданный раздел в проде; * YAML pilot + реестр manager → manager, и лишних путей на tradein-периметре нет: manager отличается от employee ровно префиксом /api/v1/team/** (вне /trade-in/**), deny-списки совпадают. Докстринг `_registry_role`: зафиксирован компромисс — при недоступном реестре фолбэк временно возвращает авторитетность roles.yaml, то есть состояние, которое фикс и лечит. Сегодня безопасно (прод-коллизий имён нет, новые закрыты 409-гвардом create_employee); появится коллизия — ветку менять на fail-closed. |
|||
| 4feb61c006 |
fix(tradein): резолвить роль из реестра, roles.yaml — только fallback (#3316)
Some checks failed
CI Trade-In / changes (pull_request) Successful in 12s
CI / changes (pull_request) Successful in 15s
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 5m27s
Роль жила в двух местах сразу: люди заводятся в БД (`tradein_users.role`),
а `get_role` читал ТОЛЬКО `auth/roles.yaml` — и никто эти два источника не
сверял. Дефект двусторонний:
* вверх: менеджер заводил сотрудника с именем, которое уже числится в
roles.yaml админом (проверялись лишь regex и уникальность в БД) — на
входе тот получал admin из YAML, то есть чтение ЛЮБОЙ чужой оценки
(admin проходит мимо ownership-check в trade_in.py) и безлимитную квоту;
* вниз: сотрудник, которого в roles.yaml нет, ловил KeyError → 403 на
СОБСТВЕННУЮ оценку.
Источник теперь один и лечится один раз — в `app.core.auth.get_role`:
реестр (`tradein_users.role` / `auth.users.role`) спрашивается первым,
roles.yaml остаётся fallback для legacy-юзеров, у которых строки в реестре
нет. Реестр недоступен → тоже fallback: падение БД не выключает legacy-вход.
Вызывающие (rbac, trade_in, team, account_quota) не менялись.
Сопутствующее, чтобы поведение существующих аккаунтов не поехало:
* rbac_guard выбирает матчер путей по РОДУ роли (роль реестра → DB_ROLE_PATHS),
иначе employee/manager на legacy-пути получил бы 403 на всё;
* get_user_scope отдаёт scope роли реестра из того же DB_ROLE_PATHS;
* право на персональный `unlimited` осталось за roles.yaml (account_quota +
_batch_quota_status) — фикс убирает эскалацию, а не раздаёт новую;
* `_batch_quota_status` берёт роли из уже прочитанных строк — иначе список
«Команды» снова стал бы N+1.
Defense-in-depth: create_employee отдаёт 409 на username, за которым в
roles.yaml числится не-employee роль.
|
|||
| 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
|
|||
|
|
d708f15019 |
fix(tradein/support): один повтор терял каждое одиннадцатое сообщение в поддержку
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
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 4m50s
Замер прода 01.09.2026 из контейнера бота: канал до api.telegram.org рвётся всплесками, доля отказов на попытку 15-38% (пять проб: 3/8, 15/40, 5/20, 3/20, 1/25), в логе long-polling'а 353 ConnectTimeout за сутки. Транспорт ни при чём — httpx и сырой сокет отваливаются одинаково (25% против 35% в чередующемся замере), и прокси не помогает, а мешает: через SCRAPER_PROXY_URL 0 из 20. Ручка веб-поддержки ходила с max_retries=1, то есть двумя попытками. При 30% отказов на попытку до пользователя доходило ~9% отказов — каждое одиннадцатое сообщение возвращало 502 «сервис недоступен». Два других числа из того же замера задают конструкцию. Успешный запрос отвечает за 0.13с (максимум из 25 проб — 0.18с), а неудачный НИКОГДА не отваливается быстро: все отказы упираются в таймаут целиком (10.02с при timeout=10.0). Значит десятисекундный таймаут не покупал ничего, кроме цены за неудачу, — снижен до 5с, это ~28-кратный запас к измеренному максимуму. И экспоненциальная пауза 2→4→8с здесь бессмысленна: отказ — неустановленное соединение, а не троттлинг, пережидать нечего; она лишь добавляла 14с к ожиданию. Правка: бюджет ручки — 3 повтора, таймаут 5с, потолок паузы 1с. Худший случай 4 попытки × 5с + 3 паузы × 1с = 23с и требует четырёх отказов подряд; типичный случай не меняется (0.13с). Расчётная потеря падает с ~9% до ~0.8%. В TelegramClient добавлен необязательный max_backoff. Воркерная политика НЕ меняется: без явного потолка откат прежний экспоненциальный до 30с, а retry_after из 429 уважается целиком — эту границу держит отдельный тест, потому что первая версия правки её сломала (капала 60с до 30с и для воркера тоже). Потолок на retry_after применяется только когда его передали явно: интерактивному пути нельзя ждать Telegram-овские 30-60с, за ним стоит открытый запрос от браузера. Тесты: 8 новых (потолок на network/429/5xx, неизменность воркерного пути, арифметика «max_retries=N → N+1 попыток», границы бюджета ручки). |
||
| 4c29b59041 |
fix(tradein/proxy): успешный fetch затирал exit_ip и latency_ms в NULL (#3283) (#3305)
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 12s
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
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
|
|||
| b7c1c4d9d4 |
fix(tradein/proxy_pool): истёкший прокси больше не выдаётся в аренду (#3303)
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 4m14s
Deploy Trade-In / build-backend (push) Successful in 1m16s
Deploy Trade-In / deploy (push) Successful in 7m32s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 12s
|
|||
| ea8943e00e |
Merge pull request 'chore(tradein/scheduler): добор карточек Яндекса и Циана шёл раз в сутки и простаивал' (#3301) from chore/cadence-yandex-cian-detail into main
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 4m17s
Deploy Trade-In / build-backend (push) Successful in 1m48s
Deploy Trade-In / deploy (push) Successful in 1m40s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 12s
|
|||
|
|
4ddb3c7196 |
chore(tradein/scheduler): добор карточек Яндекса и Циана шёл раз в сутки и простаивал
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 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 4m58s
compute_next_run_at держит суточную гранулярность (interval_days, минимум 1),
подчасовой такт делается хуком reschedule_after_minutes как post_claim. У
avito_detail_backfill он есть (180 мин), у yandex_detail_backfill и
cian_detail_backfill не было — отсюда один прогон в сутки.
Цена простоя по замеру прода 31.08:
Яндекс: 375-450 карточек за прогон, блоков НОЛЬ за неделю, очередь 11110
→ 25 суток при нынешнем такте
Циан: блоков ноль, очередь 20501
Такты разные, и это не произвол:
yandex — 180 мин (8 прогонов/сутки). Ходит через resolve_proxy_url: берёт
URL узла, но НЕ лизует его, поэтому чужие прогоны не блокирует.
cian — 360 мин (4 прогона/сутки). Ходит через BrowserFetcher и ДЕРЖИТ
lease весь прогон, то есть отнимает узел у Авито и Домклика. Пул
дефицитен (#2638), поэтому осторожнее.
Асимметрия зафиксирована комментарием у обоих хендлеров и в докстринге
миграции — иначе следующий читатель выровняет интервалы и сожжёт пул. Тест
test_cian_default_interval_is_360_minutes_not_180 ассертит именно неравенство,
чтобы выравнивание без замера покраснело.
283_scrape_schedules_cadence_yandex_cian_detail.sql — идемпотентный
UPDATE ... SET default_params = default_params || jsonb, остальные ключи
параметров не трогает.
Значения 180/360 — консервативная отправная точка по аналогии с Авито, а не
найденный оптимум: двигать вниз только по замеру нескольких суток, глядя и на
свипы тоже (тот же довод, что в комментарии у avito_detail_backfill).
Тесты (5): наличие post_claim у обоих, дефолтные интервалы, переопределение
через params. Прогон: 397 passed, 1 skipped, ruff чист.
|
||
| a256077211 |
feat(tradein/proxy_rotation): ротация IP через mobileproxy.space (aproxy.site) (#3298)
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 4m11s
Deploy Trade-In / build-backend (push) Successful in 1m5s
Deploy Trade-In / deploy (push) Successful in 7m37s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 13s
|
|||
| 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. |
||
| 8fb5587888 |
Merge branch 'main' into fix/3194-hide-sql-parameters
All checks were successful
CI Trade-In / changes (pull_request) Successful in 11s
CI / changes (pull_request) Successful in 19s
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) Successful in 3m10s
CI Trade-In / backend-tests (pull_request) Successful in 5m56s
CI / backend-tests (pull_request) Successful in 18m10s
|
|||
| 663d63aea4 |
fix(tradein): подсказки адреса — пустые части адреса выпадают, а не печатаются
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 16s
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 5m50s
Прод отдавал «г Екатеринбург, ул Фролова , д. 29, корп.»: реестровый readable_address (ЕГРН) приходит УЖЕ склеенным вместе с пустыми частями — маркер без значения печатается, лишний пробел перед запятой остаётся, и все тиры /suggest (cadastral / geoportal / houses / DaData / Nominatim) пропускали строку насквозь. Чиним склейку, а не конкретный случай: `tidy_address()` — один проход по частям (схлопнуть пробелы, выбросить пустые и маркер-без-номера: корп./стр./лит./кв./оф./пом. и пр.), склейка обратно. Применяется в `GeocodeSuggestion.__post_init__` — единственной точке, через которую проходят все тиры, включая локальные, которые питают и `geocode()`. Тест на функцию склейки, фальсифицирован снятием фильтра пустых: краснеет по значению строки (10 из 14), а не исключением. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 7e59c1e5b0 |
fix(#3194): hide_parameters=True на всех движках, include_local_variables=False у scheduler
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 / frontend-tests (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 7s
CI / openapi-codegen-check (pull_request) Successful in 3m10s
CI Trade-In / backend-tests (pull_request) Successful in 5m47s
CI / backend-tests (pull_request) Successful in 18m3s
Ключ шифрования кук и сами куки уезжали в GlitchTip: сервисы сессий передают их bind-параметрами в pgp_sym_encrypt(:cookies_json, :key), а SQLAlchemy при ошибке печатает ВСЕ параметры в тексте StatementError. Правка на уровне движка (backend + tradein-mvp: db.py, auth_db.py, alembic/env.py) кроет все сайты вызова разом, включая четвёртую копию в scraper-kit и всё будущее. scheduler_main.py был единственным из трёх sentry_sdk.init без include_local_variables=False — процесс скрейпера, в кадрах лежат прокси-креды. НЕ закрывает: текст ошибки самого драйвера (Postgres DETAIL со значением) и сырые psycopg-подключения мимо движков — отдельный класс. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
|
|
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 |
||
|
|
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). |