fix(tradein/avito): браузерный путь ходил на каждую карточку холодным (#3251) #3267

Merged
lekss361 merged 1 commit from fix/3180-avito-warm-context into main 2026-08-30 08:55:34 +00:00
Owner

Закрывает браузерную часть #3251 для Авито. Хранилище авторизованных сессий (#3179/#3180) этой правкой НЕ заменяется и не блокируется.

Что было сломано

1. Пройденный QRATOR proof-of-work выбрасывался после каждой карточки.

reuse_context во всём репозитории передаёт ЕДИНСТВЕННЫЙ вызов — domclick_detail_backfill.py:346. Значит каждый /fetch Авито шёл через browser.new_page() сайдкара, то есть новый изолированный context с пустой банкой кук.

В логе прод-сайдкара это видно прямо: PoW-челлендж снят за ~1000мс печатается на каждой успешной карточке — челлендж решается заново каждый раз, а не один раз на прогон.

Эффект той же правки у Домклика измерен и записан в domclick_detail_backfill.py:331-338: 26 последовательных фетчей = 100% блоков, те же карточки в тёплом контексте = 5/5 за ~2с.

2. Сам заход был голым.

# providers/avito/detail.py:506
html = await browser_fetcher.fetch(full_url)                                  # было
# providers/domclick/detail.py:541
html = await browser_fetcher.fetch(card_url, origin=origin, referer=origin, ) # эталон (#3247)

Написанный для Авито прогрев (warm_up_session, яндекс-referer → выдача Авито → антибот-куки) живёт только в 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(card_url): городская SERP-выдача из URL карточки, хост берётся из самого url (не хардкодится www.avito.ru), непарсящийся url → None → прежнее поведение.

Попутный дефект сайдкара, найденный при переносе

_ensure_anchor_page возвращала True на любую живую якорную вкладку, не сверяя её с запрошенным origin. А origin у обоих caller'ов выводится из URL карточки и меняется вместе с городом: у Авито — сегмент пути (/ekaterinburg/… vs /verhnyaya_pyshma/…), у Домклика — поддомен.

Значит после первой же карточки другого города Referer называл выдачу, которую этот контекст никогда не открывал: ни куки её, ни тайминга — площадка видит заявленный переход без единого следа. Ровно то, что #3258 запретил делать фолбэкам якорного поиска.

Добавлен _anchor_origins: origin сменился → вкладка переоткрывается на новый. Чинит и Домклик тоже (его detail-бэкфилл ходит по нескольким городским поддоменам).

Чего здесь сознательно НЕТ

  • Заход через поиск Яндекса для Авито не включается: BROWSER_ANCHOR_VIA_SEARCH остаётся дефолтным "domclick". Расширять список без отдельного замера запрещает комментарий у самой константы, и я это не нарушаю.
  • Хранилище кук/сессий Авито — #3179/#3180. Таблицы avito_session_cookies не существует (у Циана, Яндекса и Домклика такие есть), поэтому передавать в cookies= пока нечего. Параметр намеренно не прокидывал.
  • ?context= из выдачи#3035, отдельно.

База для сравнения (снята ДО выката)

avito_detail_backfill, доля блоков от попыток по суткам:

сутки прогонов попыток обогащено блоков доля блоков
25.08 8 228 111 107 46.9%
26.08 8 89 23 66 74.2%
27.08 10 56 1 54 96.4%
28.08 11 189 55 132 69.8%
29.08 8 658 306 348 52.9%
30.08 (3 прогона) 3 138 51 86 62.3%

Сравнивать после деплоя по доле блоков от попыток и по обогащению за сутки, а НЕ по статусу прогона: ratio-критерий обрывает каждый прогон без исключения, и banned про площадку ничего не говорит (разбиралось при калибровке #3184).

Отдельно держать в уме: часть отказов в логах — NS_ERROR_PROXY_BAD_GATEWAY / NS_ERROR_ABORT, то есть прокси, а не площадка. AvitoSidecarUnavailableError наследует AvitoRateLimitedError и потому инкрементит counters.blocked — доля блоков этим завышена. Не в scope, но цифры читать с поправкой.

Проверено

  • backend: 5150 passed, 37 skipped
  • сайдкар: 211 passed (было 206, +5 новых на переезд якоря)
  • ruff: чисто на всех изменённых файлах
  • code-reviewer: approve. Его единственный minor («стабов с фиксированной сигнатурой в тестах нет») я проверил безусловной передачей kwargs — стаб есть, test_avito_detail_backfill.py:730, тест падал. Но это довод не за условные kwargs, а за то, чтобы стаб терпел рост сигнатуры: заменил обходной путь на **_extra в стабе, kwargs теперь передаются безусловно.

Refs #3251, #3180, #3118, #3212, #3247, #3258

Закрывает браузерную часть #3251 для Авито. Хранилище авторизованных сессий (#3179/#3180) этой правкой НЕ заменяется и не блокируется. ## Что было сломано **1. Пройденный QRATOR proof-of-work выбрасывался после каждой карточки.** `reuse_context` во всём репозитории передаёт ЕДИНСТВЕННЫЙ вызов — `domclick_detail_backfill.py:346`. Значит каждый `/fetch` Авито шёл через `browser.new_page()` сайдкара, то есть новый изолированный context с пустой банкой кук. В логе прод-сайдкара это видно прямо: `PoW-челлендж снят за ~1000мс` печатается на **каждой** успешной карточке — челлендж решается заново каждый раз, а не один раз на прогон. Эффект той же правки у Домклика измерен и записан в `domclick_detail_backfill.py:331-338`: 26 последовательных фетчей = 100% блоков, те же карточки в тёплом контексте = 5/5 за ~2с. **2. Сам заход был голым.** ```python # providers/avito/detail.py:506 html = await browser_fetcher.fetch(full_url) # было # providers/domclick/detail.py:541 html = await browser_fetcher.fetch(card_url, origin=origin, referer=origin, …) # эталон (#3247) ``` Написанный для Авито прогрев (`warm_up_session`, яндекс-referer → выдача Авито → антибот-куки) живёт только в 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(card_url)`: городская SERP-выдача из URL карточки, хост берётся из самого url (не хардкодится `www.avito.ru`), непарсящийся url → `None` → прежнее поведение. ## Попутный дефект сайдкара, найденный при переносе `_ensure_anchor_page` возвращала `True` на **любую** живую якорную вкладку, не сверяя её с запрошенным `origin`. А origin у обоих caller'ов выводится из URL карточки и меняется вместе с городом: у Авито — сегмент пути (`/ekaterinburg/…` vs `/verhnyaya_pyshma/…`), у Домклика — поддомен. Значит после первой же карточки другого города Referer называл выдачу, которую этот контекст **никогда не открывал**: ни куки её, ни тайминга — площадка видит заявленный переход без единого следа. Ровно то, что #3258 запретил делать фолбэкам якорного поиска. Добавлен `_anchor_origins`: origin сменился → вкладка переоткрывается на новый. Чинит и Домклик тоже (его detail-бэкфилл ходит по нескольким городским поддоменам). ## Чего здесь сознательно НЕТ - **Заход через поиск Яндекса** для Авито не включается: `BROWSER_ANCHOR_VIA_SEARCH` остаётся дефолтным `"domclick"`. Расширять список без отдельного замера запрещает комментарий у самой константы, и я это не нарушаю. - **Хранилище кук/сессий Авито** — #3179/#3180. Таблицы `avito_session_cookies` не существует (у Циана, Яндекса и Домклика такие есть), поэтому передавать в `cookies=` пока нечего. Параметр намеренно не прокидывал. - **`?context=` из выдачи** — #3035, отдельно. ## База для сравнения (снята ДО выката) `avito_detail_backfill`, доля блоков от попыток по суткам: | сутки | прогонов | попыток | обогащено | блоков | доля блоков | |---|---:|---:|---:|---:|---:| | 25.08 | 8 | 228 | 111 | 107 | 46.9% | | 26.08 | 8 | 89 | 23 | 66 | 74.2% | | 27.08 | 10 | 56 | 1 | 54 | 96.4% | | 28.08 | 11 | 189 | 55 | 132 | 69.8% | | 29.08 | 8 | 658 | 306 | 348 | 52.9% | | 30.08 (3 прогона) | 3 | 138 | 51 | 86 | 62.3% | Сравнивать после деплоя по **доле блоков от попыток** и по обогащению за сутки, а НЕ по статусу прогона: ratio-критерий обрывает каждый прогон без исключения, и `banned` про площадку ничего не говорит (разбиралось при калибровке #3184). Отдельно держать в уме: часть отказов в логах — `NS_ERROR_PROXY_BAD_GATEWAY` / `NS_ERROR_ABORT`, то есть прокси, а не площадка. `AvitoSidecarUnavailableError` наследует `AvitoRateLimitedError` и потому инкрементит `counters.blocked` — доля блоков этим завышена. Не в scope, но цифры читать с поправкой. ## Проверено - backend: **5150 passed, 37 skipped** - сайдкар: **211 passed** (было 206, +5 новых на переезд якоря) - ruff: чисто на всех изменённых файлах - code-reviewer: approve. Его единственный minor («стабов с фиксированной сигнатурой в тестах нет») я проверил безусловной передачей kwargs — стаб есть, `test_avito_detail_backfill.py:730`, тест падал. Но это довод не за условные kwargs, а за то, чтобы стаб терпел рост сигнатуры: заменил обходной путь на `**_extra` в стабе, kwargs теперь передаются безусловно. Refs #3251, #3180, #3118, #3212, #3247, #3258
lekss361 added 1 commit 2026-08-30 08:48:06 +00:00
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
58e18e6fec
Пройденный 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
lekss361 merged commit 169121cea8 into main 2026-08-30 08:55:34 +00:00
lekss361 deleted branch fix/3180-avito-warm-context 2026-08-30 08:55:35 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3267
No description provided.