feat(tradein/scrapers): хранилище авторизованной сессии Яндекс.Недвижимости #3195

Merged
bot-backend merged 3 commits from feat/3192-yandex-session-store into main 2026-08-28 19:02:06 +00:00
Collaborator

Часть 1 из #3192 — только хранилище. Проброс кук в провайдер и извлечение телефонов — следующим PR.

Мотив (измерено, не предположение)

Прод-сайдкар, две карточки, один тракт с куками и без:

без кук с куками
"phoneNumbers" 0 / 0 2 / 2
"phones":["+7… у автора 24 / 22 27 / 25
"encryptedPhones" 54 / 54 54 / 54
"redirectId" 101 / 101 111 / 111

Структура, появляющаяся только с куками:

"author": {"category":"OWNER", "agentName":"…",
           "phones":["+7…"],
           "phoneNumbers":[{"tag":"serp","phone":"+7…","redirectId":"…"}]}

encryptedPhones одинаков в обоих режимах — зашифрованные токены отдаются всегда, различие не в них.

listings.phones (jsonb) существует в схеме и пуст у всех 51 251 активного объявления по всем четырём источникам.

Что в PR

  • 274_yandex_session_cookies.sql — форма 1:1 с работающими 027_cian_session_cookies.sql и 174_domclick_session_cookies.sql.
  • services/yandex_session.pysave/load/mark_invalid/expires_at, pgp_sym_encrypt.
  • admin.pyPOST /scrape/yandex/upload-cookies, GET /scrape/yandex/session-status.

Два отличия от образцов, оба намеренные

1. Хранится полный CDP-объект куки, не плоский dict[name→value]. У Яндекса куки на трёх доменах (.yandex.ru, .passport.yandex.ru, .realty.yandex.ru) и дублируют имена (pi ×4, yashr ×4) — плоский словарь их схлопывает. Сайдкар сегодня принимает только плоский формат (server.py:1197-1203 фабрикует домен из URL); когда контракт починят, хранилище уже готово.

2. Набор сохраняется целиком, минус аналитика (_ym_*, yabs-*, _yasc*). Список критичных кук — только проверка, что дамп похож на авторизованную сессию, не фильтр сохранения. Замер сделан на полном наборе 44 кук, какая существенна — неизвестно; сужение до allowlist сломало бы измеренное. Защищено комментариями и тестом.

Безопасность

Проверено ревью построчно: значений кук нет ни в одном logger.*, raise или return; в сообщении валидации печатается только имя куки. Роуты закрыты на двух уровнях — Caddy basic_auth снаружи и rbac_guard внутри (role=admin для /api/v1/admin/*, rbac.py:55).

Отдельно и вне этого PR: ревью нашло, что ключ шифрования и сами куки уезжают в GlitchTip через текст исключения SQLAlchemy (bind-параметры попадают в StatementError). Дефект семейный — у Циана и Домклика то же самое. Заведён #3194, лечится глобально одной строкой в app/core/db.py.

Тесты

49 зелёных: 19 сервисных + 8 роутных + 22 соседних (cian/domclick — регрессий от правки admin.py нет). ruff format --check — чисто.

Test plan

  • Локально 49/49, ruff чист
  • CI зелёный
  • После деплоя: миграция применилась, GET /scrape/yandex/session-status отвечает hasSession: false
  • Залить дамп через upload-cookies и убедиться, что load_session возвращает полный набор

Refs #3192

Часть 1 из #3192 — только хранилище. Проброс кук в провайдер и извлечение телефонов — следующим PR. ## Мотив (измерено, не предположение) Прод-сайдкар, две карточки, **один тракт** с куками и без: | | без кук | с куками | |---|---|---| | `"phoneNumbers"` | 0 / 0 | **2 / 2** | | `"phones":["+7…` у автора | 24 / 22 | **27 / 25** | | `"encryptedPhones"` | 54 / 54 | 54 / 54 | | `"redirectId"` | 101 / 101 | **111 / 111** | Структура, появляющаяся только с куками: ```json "author": {"category":"OWNER", "agentName":"…", "phones":["+7…"], "phoneNumbers":[{"tag":"serp","phone":"+7…","redirectId":"…"}]} ``` `encryptedPhones` одинаков в обоих режимах — зашифрованные токены отдаются всегда, различие не в них. `listings.phones` (jsonb) существует в схеме и **пуст у всех 51 251 активного объявления по всем четырём источникам**. ## Что в PR - `274_yandex_session_cookies.sql` — форма 1:1 с работающими `027_cian_session_cookies.sql` и `174_domclick_session_cookies.sql`. - `services/yandex_session.py` — `save/load/mark_invalid/expires_at`, `pgp_sym_encrypt`. - `admin.py` — `POST /scrape/yandex/upload-cookies`, `GET /scrape/yandex/session-status`. ## Два отличия от образцов, оба намеренные **1. Хранится полный CDP-объект куки**, не плоский `dict[name→value]`. У Яндекса куки на трёх доменах (`.yandex.ru`, `.passport.yandex.ru`, `.realty.yandex.ru`) и дублируют имена (`pi` ×4, `yashr` ×4) — плоский словарь их схлопывает. Сайдкар сегодня принимает только плоский формат (`server.py:1197-1203` фабрикует домен из URL); когда контракт починят, хранилище уже готово. **2. Набор сохраняется целиком**, минус аналитика (`_ym_*`, `yabs-*`, `_yasc*`). Список критичных кук — **только проверка**, что дамп похож на авторизованную сессию, не фильтр сохранения. Замер сделан на полном наборе 44 кук, какая существенна — неизвестно; сужение до allowlist сломало бы измеренное. Защищено комментариями и тестом. ## Безопасность Проверено ревью построчно: значений кук нет ни в одном `logger.*`, `raise` или `return`; в сообщении валидации печатается только **имя** куки. Роуты закрыты на двух уровнях — Caddy basic_auth снаружи и `rbac_guard` внутри (`role=admin` для `/api/v1/admin/*`, `rbac.py:55`). **Отдельно и вне этого PR:** ревью нашло, что ключ шифрования и сами куки уезжают в GlitchTip через текст исключения SQLAlchemy (bind-параметры попадают в `StatementError`). Дефект семейный — у Циана и Домклика то же самое. Заведён #3194, лечится глобально одной строкой в `app/core/db.py`. ## Тесты 49 зелёных: 19 сервисных + 8 роутных + 22 соседних (cian/domclick — регрессий от правки `admin.py` нет). `ruff format --check` — чисто. ## Test plan - [x] Локально 49/49, ruff чист - [ ] CI зелёный - [ ] После деплоя: миграция применилась, `GET /scrape/yandex/session-status` отвечает `hasSession: false` - [ ] Залить дамп через `upload-cookies` и убедиться, что `load_session` возвращает полный набор Refs #3192
bot-backend added 2 commits 2026-08-28 18:53:40 +00:00
fix(tradein/domclick): каждый запрос получал чистый браузерный контекст, куки реплеились протухшими
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / 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 4m49s
e2863f6be9
Сайдкар на каждый /fetch делал browser.new_page() поверх AsyncCamoufox — это новый
изолированный контекст, cookie-jar умирал сразу после ответа. Туда вливался
замороженный снимок кук из domclick_session_cookies, где qrator_jsid2 живёт ~2.5
часа, а хранится 30 дней. Первый запрос проходил с ещё живым токеном, все следующие
показывали QRATOR один и тот же протухший — он отдавал челлендж-страницу без
__SSR_STATE__. Отсюда двухнедельное attempted=4, enriched=1, blocked=3.

Замер на проде 28.08: через сайдкар 26 запросов подряд — 100% блоков, включая
свежеротированный exit-IP; те же карточки в тёплом контексте — 5 из 5 успешно,
~2 сек каждая.

Сайдкар получил переиспользуемый per-provider контекст: куки вливаются один раз при
создании, дальше jar живёт сам; страница закрывается после ответа, контекст остаётся.
Закрытие контекста подшито к _close_browser, так что relaunch и shutdown его не
теряют. Флаг opt-in: при reuse_context=False payload /fetch не получает новых ключей
вовсе, поведение остальных поставщиков не меняется.

Сброс сожжённого контекста — ровно один на обнаруженный блок, через отложенный флаг
BrowserFetcher.request_context_reset(): fetch_detail() в kit не прокидывает
reset_context, и менять этот промежуточный слой ради одного поставщика не хотелось.

NB для деплоя: правка работает только если tradein-browser пересобран вместе с
бэкендом. Старый сайдкар новые поля молча проигнорирует — не упадёт, но и не починит.

Refs #3190, #3118
feat(tradein/scrapers): хранилище авторизованной сессии Яндекс.Недвижимости
Some checks failed
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Failing after 8s
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 1m6s
CI Trade-In / backend-tests (pull_request) Successful in 4m48s
bb59caa90c
Замер через прод-сайдкар, две карточки, один тракт с куками и без:
поле author.phoneNumbers присутствует только в авторизованном режиме
(0 без кук, 2 с куками, одинаково на обеих), плюс три номера автора
сверху. Это контакты продавца с тегом канала и redirectId.
encryptedPhones при этом одинаков в обоих режимах (54) — зашифрованные
токены отдаются всегда, различие не в них.

listings.phones (jsonb) существует в схеме и пуст у всех 51 251
активного объявления по всем четырём источникам. Яндекс станет первым
источником с контактами.

Форма таблицы и сервиса повторяет работающие в проде cian_session_cookies
и domclick_session_cookies: pgp_sym_encrypt, expires_at_estimate,
last_used_at/last_invalid_at, предупреждение о протухании.

Два отличия от образцов, оба намеренные:

Хранится ПОЛНЫЙ CDP-объект куки (name/value/domain/path/httpOnly/
secure/expires), а не плоский dict name→value. У Яндекса куки живут на
трёх доменах (.yandex.ru, .passport.yandex.ru, .realty.yandex.ru) и
дублируют имена (pi ×4, yashr ×4) — плоский словарь их схлопывает.
Сайдкар сегодня принимает только плоский формат; когда его контракт
починят, хранилище уже готово.

Набор сохраняется целиком, минус очевидная аналитика (_ym_*, yabs-*,
_yasc*). Список критичных кук используется ТОЛЬКО для проверки, что
дамп похож на авторизованную сессию, и не является фильтром сохранения:
замер сделан на полном наборе 44 кук, и какая из них существенна —
неизвестно. Сужение до allowlist сломало бы измеренное; это защищено
комментариями и тестом.

Роуты закрыты на двух уровнях: Caddy basic_auth снаружи и rbac_guard
внутри (role=admin для /api/v1/admin/*).

Refs #3192
bot-backend added 1 commit 2026-08-28 18:56:28 +00:00
fix(tradein/sql): миграция 274 без lock_timeout роняла гейт #2752
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m55s
CI / changes (pull_request) Successful in 11s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Successful in 1m5s
4c3a767f45
CREATE INDEX без lock_timeout встаёт в очередь за чужой долгой сессией
и уводит за собой запросы приложения. Таблица новая и пустая, но очередь
блокировок этого не знает, а гейт check-migration-lock-timeout.py держит
правило на каждом PR — и правильно делает.

Заодно поправлена шапка миграции: там осталась моя опровергнутая версия,
будто ценность в mainPhone и одиннадцати номерах. mainPhone лежит в
паспортном блоке рядом с login/passportHost/passportPhones — это номер
НАШЕЙ учётки, а не продавца, и собирать его нельзя. Настоящее различие
между режимами — author.phoneNumbers (0 без кук, 2 с куками, одинаково
на обеих карточках); encryptedPhones в обоих режимах одинаков.

Refs #3192
bot-backend merged commit 5be64c6688 into main 2026-08-28 19:02:06 +00:00
bot-backend deleted branch feat/3192-yandex-session-store 2026-08-28 19:02:06 +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#3195
No description provided.