Контроль изменений — пятница, 21 августа
v2.3.0Это вышло на боевой сайт в пятницу, 21 августа. Страница написана накануне, по той сборке, которая должна была выйти в пятницу утром, и намеренно оставлена в этом виде: все замеры на ней сняты в четверг, одновременно с боевого сайта и с релиза, потому что после выкатки «до» пропадает и вернуть его уже нельзя. Главное исправлено. Поиск на боевом магазине падал — запрос по номеру детали занимал около семнадцати секунд, страница не дожидалась ответа, и покупатель видел ошибку. Тот же запрос теперь отвечает за секунду с небольшим. Об этом первый раздел, раньше всего остального.
На одно предупреждение с этой страницы ответ уже есть, и это как раз то предупреждение, на которое стоило ответить. Два раздела ниже говорят, что изменение уехало, не пройдя проверку: будто исправление списков схем (search#925) провалило гейт и нигде не запускалось. В четверг, когда это писалось, так и было. Гейт упал с первой попытки, его перезапустили в тот же день после обеда, и он прошёл: зелёный в 15:45 UTC в четверг, тогда же выкачен на весь трафик. Релиз, ушедший в пятницу утром, несёт именно проверенную версию, а не провалившуюся. Считано напрямую с работающего сервиса в субботу 22 августа. Больше ничего на этой странице не перепроверялось — остальное читайте как то, что было известно в четверг.
Девяносто шесть изменений с релиза 14 августа: шестьдесят два — на сайте магазина, двадцать один — в поиске, восемь — в сервисах заказов и оплаты и пять — во внутренней панели для данных по деталям. Двадцать шесть из них может заметить клиент или Google. Остальное — тесты, инструменты и удаление кода; всё это полностью перечислено внизу, по строке на каждое.
В каждой строке, которую удалось измерить, есть картина реального «до» и «после» — снимок страницы там, где изменение видно, и настоящая команда с её настоящим выводом там, где не видно. Там, где замер сделать не удалось, строка так и говорит красным, вместо того чтобы показывать число.
Что сейчас не так на боевом магазине — замеры в четверг, а не прогноз
Поиск на боевом магазине не работает, и это уже вышло за пределы поиска. Покупатель, который ищет то, чего магазин раньше не видел, получает страницу с ошибкой. Замеры в четверг в течение дня: в 07:35 боевой сервис отказал на трёх холодных запросах из трёх; к 13:00 он отвечал, но по тридцать две секунды, а его собственная проверка здоровья — самый дешёвый вопрос, который ему вообще можно задать — заняла тридцать девять секунд. Те же самые запросы к кандидату на выкатку, к той же самой базе, возвращались за четыре-шесть секунд.
Теперь это задевает и обычный просмотр каталога. Вторая и десятая страницы каталога деталей отдают ошибку, три раза из трёх. В начале недели эти страницы открывались меньше чем за секунду, и в записях, с которыми мы работали, они до сих пор такими и числятся — это больше не так, а значит, задето больше покупателей, чем описано в задаче. Первая страница и главная по-прежнему работают.
Причина в том, что боевой поисковый сервис работает на старом коде. Он отстаёт на тридцать два изменения от того, что уже собрано и проверено; починка ровно этой поломки лежит внутри этого разрыва. Сдвинуть его вперёд — это переключение, а не пересборка, но вместе с ним поедет и всё остальное из разрыва, поэтому эта работа стоит в списке, а не делается тихо.
Это же блокирует и релиз самого магазина. Сборка, из которой получается сайт, спрашивает у боевого поискового сервиса список брендов; сервис отказывает, и сборка останавливается. То есть из-за той же самой поломки ту половину этого релиза, которая про витрину, нельзя собрать, пока сначала не выкатят поиск.
Что должен решить созвон — нашли в четверг, ещё до реестра
У 7,803 из 407,302 снимаемых страниц деталей есть цена или фото — и их всё равно снимают нужно решениеэто видно клиентуfront#3700
Самое крупное изменение в этом релизе берёт 407,302 страницы деталей, на которых нет ничего — ни цены, ни фото, ни данных каталога, — и заставляет их отвечать «страница не найдена» вместо пустой страницы. Замысел верный, и делать это стоит. Но в правиле два списка, и только один из них проверяет, есть ли на странице хоть что-нибудь. 7,803 страницы, на которых что-то есть — у 4,828 из них настоящая цена, в сумме $3,888,976.50 по прайсу, — снимаются заодно.
Это нашли в четверг, просто открыв одну из них: деталь New Holland Construction, которую прод сегодня
отдаёт по $42.42 / each, со списком совместимости, восемью ссылками на схемы и кнопкой
“Свяжитесь с отделом запчастей”. На кандидате тот же адрес отвечает «страница не найдена».
Что при этом тоже верно Ни по одной из 407,302 не было заказа за двадцать четыре месяца. Ни одна не отдаётся в Google. Ни одну нельзя найти поиском по самому магазину. А схема деталей на них уже не ссылается, так что выкатка не создаёт ни одной битой ссылки и не ломает ни одного пути к покупке внутри магазина. Клиент теряет страницу с ценой и телефоном, а не возможность что-то купить.
Выбор Выкатить как есть и согласиться, что 4,828 страниц с ценой перестанут существовать, — или до пятницы перенести одно слово из одного списка в другой: это сохраняет 98.1% результата и оставляет те 7,803 страницы живыми. Сам пул-реквест называл эту правку компромиссом, на который стоит взглянуть ещё раз; её просто ни разу не проговорили вслух.
Чего это не отвечает: никто не мерил, сколько людей на самом деле открывает эти 4,828 страниц. Для этого нужна выгрузка из Search Console, а вход в облако для неё в четверг был просрочен — это недостающее измерение, а не ноль.
Остальное, что должен был решить созвон — 11 пунктов закрыть до выкатки, на один из них ответ уже есть, ещё 14 — на усмотрение
gcloud НЕ авторизован — имя ни одной ревизии Cloud Run НЕДОСТУПНО, и выдумывать его я не станунадо решить
За эту сессию я не смог прочитать ни одного имени ревизии Cloud Run, ни распределения трафика, ни переменной окружения, ни значения масштабирования на уровне сервиса. Токен просрочен; по действующему правилу просроченный токен — это ложноотрицательный результат, а не доказательство отсутствия. Чего из-за этого не хватает для отката: имени ревизии search-api, которая обслуживает трафик сейчас (это и есть реальная цель отката через --to-revisions), ответа на вопрос, совпадают ли сегодня latestReady и latestCreated на search-api, есть ли припаркованный устаревший тег трафика (он тарифицируется, и такое случалось дважды), переменных окружения search-api (подмена образа сохраняет окружение, поэтому переменная, которая нужна новому образу и которой нет на проде, = падение в цикле перезапусков), и тега развёрнутого образа по каждому сервису parts-services. Два факта у меня всё же есть, из ops/docs/evidence/2026-08-20-search-promote-before.json, снятого сегодня в 07:35:01Z, пока gcloud ещё работал: на проде search-api крутит образ search-nest:2da1a51e3d98ffc37b4656a0f00bd6c44930cdab со scalingMode MANUAL / manualInstanceCount 2; search-api-dev крутит crop-search:b92268a1a3551b8c7485ce9cf88995edce7231ed с maxInstanceCount 2. Это тег образа, а не имя ревизии — имя ревизии из него не вывести.
Что делатьВова, после gcloud auth login выполни ровно это и вставь вывод на страницу change-control: (1) gcloud run services describe search-api --region=us-east1 --format='value(status.latestReadyRevisionName,status.latestCreatedRevisionName)' (2) gcloud run services describe search-api --region=us-east1 --flatten='status.traffic[]' --format='csv[no-heading](status.traffic.revisionName,status.traffic.percent,status.traffic.tag)' — это даёт имя ревизии для отката И показывает устаревший тег, если он есть (3) gcloud run revisions list --service=search-api --region=us-east1 --format='table(name,active,creationTimestamp,spec.containers[0].image)' (4) gcloud run services describe search-api --region=us-east1 --format='yaml(spec.template.spec.containers[0].env)' — проверь IS_PUBLISHABLE_FILTER_ENABLED и все переменные, которые нужны новому модулю конфигурации (5) curl -s -H "Authorization: Bearer $(gcloud auth print-access-token)" https://run.googleapis.com/v2/projects/noted-bliss-466410-q6/locations/us-east1/services/search-api — это вызов v2 REST, потому что describe отдаёт spec.scaling как null и СКРЫВАЕТ лимит инстансов на уровне сервиса (именно поэтому ops/scripts/search-promote-check.ts ходит через REST) (6) gcloud run services list --region=us-east1 --format='table(metadata.name,status.latestReadyRevisionName,spec.template.spec.containers[0].image)' — теги образов parts-services (7) bun run ops/scripts/search-promote-check.ts, чтобы обновить инструмент «до/после», когда авторизация вернётся.
Образ для отката поиска лежит по ВЫВЕДЕННОМУ ИЗ ОБОРОТА пути в реестре — откат, который правит тег по новому пути, не находит ничегонадо решитьCROP-search#915, CROP-search#916
Путь в Artifact Registry переименовали 2026-08-19, посреди недели, между прошлой выкаткой и этой. Цепочка — из диффа #916, который я прочитал сам: search-hono-nest -> search-nest -> crop-search, всё внутри us-east1-docker.pkg.dev/noted-bliss-466410-q6/cloud-run-source-deploy/. То есть выкатка пушит crop-search:<dev sha>, а откатываться надо НА search-nest:2da1a51e3d98ffc37b4656a0f00bd6c44930cdab — это другое имя репозитория, что независимо подтверждает сегодняшний утренний JSON с замерами. В самом workflow из #916 так и написано: «откат обязан использовать тот путь, под которым этот SHA был собран, а не текущий». Раннбук delivery-flow повторяет то же самое. Ловушка в том, что подмена тега по пути crop-search на 2da1a51e не разрешается ни во что, и Cloud Build падает — ровно в тот момент, когда ты пытаешься отменить неудачную выкатку. Безопасный откат вообще не трогает образы: gcloud run services update-traffic search-api --to-revisions=<prev-revision>=100 --region=us-east1, потому что каждая ревизия остаётся доступной по имени, пока её не удалили. Для этого нужно имя ревизии, а оно НЕДОСТУПНО (см. пункт про gcloud).
Что делатьВова: до подмены сними имя ревизии, которая обслуживает трафик сейчас, и запиши на страницу change-control И имя ревизии, И полный URI старого образа (us-east1-docker.pkg.dev/noted-bliss-466410-q6/cloud-run-source-deploy/search-nest:2da1a51e3d98ffc37b4656a0f00bd6c44930cdab). Откатывайся по ревизии, а не по образу.
Прод сейчас хуже, чем на утреннем замере, и здоровый контрольный путь тоже сломалсянадо решитьCROP-search#924
Замерял сам сегодня примерно в 12:54-13:05Z, с UA краулера, запросами, которых раньше не было. Прод search-api /health — самая дешёвая ручка, какая есть, пинг базы без федеративного веера — ответил: таймаут на 40s, таймаут на 40s, потом 200 за 38.77s. Дев search-api /health ответил 200 за 0.62s, со здоровым кэшем словаря. Это не форма веера из #895/#896; /health веера не делает. Важнее другое: КОНТРОЛЬНЫЙ путь из #924 тоже отвалился — https://crop.clintontractor.net/parts?page=10 вернул 500 три раза из трёх (20.08s, 5.39s, 2.56s), тогда как в 01:15Z он отдавал 200 за 0.78-0.96s и был приведён в #924 как доказательство того, что «каталог здоров, падает только поиск». Больше это не так. Ещё замеры: / = 200 за 5.53s, /parts (без параметров) = 200 за 4.61s, карточка детали /parts/CT-NHL-100074 = 200 за 19.11s. Поэтому, по правилу из брифа, говорю прямо: сегодня я не могу отделить «поверхность выкатки» от «живой аварии», потому что авария дошла и до обычного списка каталога. Любое «до/после», которое ты снимешь в пятницу, будет мериться относительно плывущей, ухудшающейся базы — а окно «до/после» умирает в момент выкатки.
Что делатьВова: прогони bun run ops/scripts/search-promote-check.ts прямо перед выкаткой, чтобы обновить «до», а утренний JSON от 07:35Z считай устаревшим. Если на момент выкатки /health всё ещё 38s+, реши явно, можно ли вообще доверять прохождению проверки кандидата — /health на 40s не переживёт smoke-candidate.sh.
stage под этот поезд так и не срезали — там всё ещё релиз 2026-08-13надо решить
Недельная выкатка фронта — это merge stage -> main по docs/runbooks/delivery-flow.md. HEAD у stage — это e6b2f0f99, датирован 2026-08-14T06:14:24Z, сообщение 'chore(stage): re-cut release train 2026-08-13 from dev [preview]'. compare/main...stage = 2 впереди, 93 позади, 0 изменённых файлов; compare/stage...dev = 89 впереди. То есть на stage лежит поезд прошлой недели и ничего из этой. Сегодня четверг 2026-08-20, это и есть день среза, и по состоянию на 13:00Z срез не сделан. Значит, всё, что идёт после среза, тоже не сделано: приёмки QA на stage не было, а в CROP-change-control лежат только 2026-08-07.html и 2026-08-14.html — страницы под этот поезд нет, а именно она и есть материал для пятиминутного ретро с Джоном.
Что делатьВова: сегодня же срежь stage с зафиксированного SHA dev — или прямо сейчас реши, что в пятницу выкатывается только поиск, и скажи об этом вслух: фронтовую половину нельзя проверить на stage, который отстаёт от dev на 89 коммитов.
ОТВЕЧЕНО — устаревший артефакт не уехал: упавший гейт перезапустили в тот же день, и он прошёлзакрытоCROP-search#925, #920, #922, #913, #910, #918, #923
Пятничная выкатка подменяет образ на тот артефакт, который благословила проверка на DEV. HEAD dev она не благословила. Прогон 32347365459 (Deploy search-api-dev, dev sha 678b7af2 = #925, сегодня в 08:08Z): 'Build image' — успех, 'Deploy candidate revision (no traffic)' — успех, 'Smoke + warm + contract gate vs candidate' — ПАДЕНИЕ, 'Promote candidate to 100% traffic' — ПРОПУЩЕН, 'Delete un-promoted candidate on gate failure' — успех. Предыдущий прогон (f5518b6b, 07:38Z) прошёл. То есть search-api-dev сегодня отдаёт образ f5518b6b — в нём есть #920 (кэш трёх самых горячих маршрутов assemblies) и #922 (part_count для Kuhn), но НЕТ #925 (ограда для узлов CNH). Третий прогон (24b775a4, 07:38Z) тоже упал. Это ровно то тихое устаревание, которое описано в #910: workflow работает как задуман, ничто не алертит, а старая ревизия продолжает обслуживать трафик. ops/docs/2026-08-20-merchant-feed-promote.md добавляет вторую половину: smoke-candidate.sh ходит в dev-crop-db, а это и ЕСТЬ прод-PG, поэтому прогон проверки внутри окна со ~100% CPU падает на обычном просмотре каталога, и повтор в том же окне уходит впустую. Пятница, 05:00 по Нью-Йорку = 09:00 UTC: максимум 97.9% 08-17 и 94.0% 08-18.
Что делатьВова: перед подменой посмотри CPU прод-PG за предыдущие 30 минут; если он упёрт в полку — жди, а не перезапускай. Потом перезапусти деплой на dev с HEAD dev и убедись, что проверка -> выкатка -> паритет терминальной страницы зелёные все три и latestReady == latestCreated. Только после этого подменяй. И реши явно, что именно подменяешь: HEAD dev (нужна зелёная проверка) или образ f5518b6b (зелёный, но без #925).
Авария на проде идёт прямо сейчас, и она шире поиска — страница 2 каталога отдаёт 500надо решитьCROP-search#924, #874; CROP-front#3628, #3712, #3695, #3684
Замерено 2026-08-20 примерно в 13:00Z, запросами, которых раньше не было. Прод /api/search: первая проба — ответа нет за 45s, вторая — 200 за 32.4s. search-api-dev на ТОЙ ЖЕ базе: 200 за 5.67s и 4.00s. Сам /health на проде занял 39.6s; на деве /health — 0.60s. Витрина /parts?q=<fresh> — 500 за 16.8s. Новое, чего нет в #924: /parts?page=2 — 500 за 3.1s, /parts?page=10 — 500 за 2.9s и 5.3s, при этом /parts страница 1 — это 200 за 4.7s, а главная — 200. В #924 записано: «каталог здоров, /parts?page=10 отдаёт 200 за 0.81s» — это больше не так, значит видимая покупателю поверхность шире, чем говорит тикет. Страница 1 закэширована на CDN; всё, что не из кэша, падает. Отсюда же следует, что сегодня прод-500 почти на любом пути каталога — это та же авария, а не новая регрессия: разделить их на некэшированном маршруте я не смог.
Что делатьВова: лечение — это подмена образа. Добавь /parts?page=2 в критерии приёмки в search#924 (сейчас там только ?q=) и перемерь сразу после подмены. Если страница 2 не оживёт, у аварии есть вторая причина, и остальные пятничные выкатки надо остановить.
#3244 делает строгую индексацию карточек значением по умолчанию в коде, а это убирает страховку по порядку, на которую опирается #3653надо решитьCROP-front#3244, #3653
Ограничение сформулировано в теле самого #3653: «обе строки brand-hold должны быть на main ДО того, как на проде выставят PDP_INDEXING_MODE=strict — если сначала переключить переменную окружения, откроются четыре несогласованных бренда (Kuhn / Ferris / McHale / Marcrest, 358 деталей, подходящих для реестра). Сначала cherry-pick, потом переменная, именно в таком порядке, на поезде 2026-08-21.» #3244 влили днём позже, и он переключает ЗНАЧЕНИЕ ПО УМОЛЧАНИЮ на strict в lib/seo-config.ts — то есть, как только он на main, никакого шага с переменной, который мог бы придержать, уже не остаётся: strict приезжает вместе с деплоем. #3244 заодно убивает legacyBrandHold (он завязан на mode !== strict) и прямо об этом пишет. Обоснование в #3244 — «PDP_INDEXING_MODE=strict на проде и так выставлен, поэтому поведение прода не меняется». Замеры этого не подтверждают: на проде /sitemap/1.xml содержит 2,378 locs, и все 2,378 из них — CT-NHL-, ни одного FER/KUH/MCH/MAR; прод /parts/CT-FER-1608395 отвечает 200 с noindex, follow. По детектору строгого режима из самого #3625 («любой loc с не-CNH брендом означает, что удержание снято») удержание сегодня на проде ВКЛЮЧЕНО. Прочитать значение переменной в Vercel, чтобы понять, какой из двух PR прав, я не смог — значения переменных окружения отсюда не читаются.
Что делатьВова: до поезда прочитай реальное значение PDP_INDEXING_MODE в Vercel Production (vercel env ls production или в панели). Дальше либо переноси #3653 и #3244 одним набором изменений, либо придержи #3244 целиком. Один только #3244 открывает четыре бренда, которые Джон не согласовывал.
ps#615 не должен попасть на прод раньше подмены образа поиска — иначе авария оформления заказа расползётся на Express Checkoutнадо решитьCROP-parts-services#615; CROP-search#924
#615 добавляет validateCheckout в POST /payment-intent — это путь Apple Pay / Google Pay / Link — и на каждую строку корзины делает по одному GET /api/parts/:id в поисковый сервис, падая ЗАКРЫТО. Холодный поиск на проде сегодня отвечает за 32s; у fetchPartPrice в payment-service таймаут 5s. Хостовая страница оформления заказа уже падает ровно так же: в ops/docs/2026-08-20-checkout-blocked-by-pg-saturation.md записано, как crop-checkout-probe (настоящий POST /api/checkout/session по проду каждые 30 мин) провалил три запуска по расписанию 08-19 с 'Could not verify price for CROP-SYNTHETIC-PROBE (search API timeout)', политика p0, алерт сработал пять раз, не пришёл никто. Express Checkout сейчас — единственный платёжный путь, который ещё работает, ровно потому, что он не ходит в каталог. Если сначала выкатить #615, этого пути не останется.
Что делатьВова: сначала подмена образа search-api, с подтверждением зелёного на свежем холодном запросе, и только потом ps#615. Порядок не обсуждается. Если подмена сдвигается, ps#615 сдвигается вместе с ней.
У QA-приёмки из раннбука выкатки нет владельца — Денис ушёл сегоднянадо решитьwhole train; CROP-front#3565, #3562; CROP-search#891, #909; CROP-parts-services#624, #613, #615
В ops/docs/2026-08-20-merchant-feed-promote.md это записано как жёсткий барьер: «явная приёмка QA от Дениса на stage — жёсткий барьер. Нет приёмки — нет выкатки, поезд ждёт неделю». Денис уволился 2026-08-20. Семь мерджей этой недели в dev идут с Driver: @denistka, среди них два видимых покупателю изменения по наличию (front#3565, search#891) и два изменения на денежном пути (ps#615, ps#624). Никто не назначен подписывать приёмку на stage в пятницу, а барьер, который по умолчанию проходят, хуже, чем его отсутствие.
Что делатьВова: до пятницы либо назначь приёмщиком на stage себя и сам пройди проверку, либо убери этот барьер из delivery-flow.md и запиши, что приходит ему на замену. Не давай поезду уехать с галочкой, которую никто не поставил и никто не заметил.
Строгая индексация по умолчанию и удержание брендов — это два отдельных cherry-pick; если взять только один, откроются четыре несогласованных бренданадо решитьCROP-front#3244, CROP-front#3653
front#3653 (мердж 65e71f00c7, 08-18) сделал удержание брендов независимым от режима; front#3244 (мердж fa60043144, 08-19) переключил значение PDP_INDEXING_MODE по умолчанию с legacy на strict. Это отдельные коммиты, а по домашнему правилу cherry-pick оформляется отдельным PR — значит, они могут приехать порознь. Сегодня проверено на origin/main: lib/seo-config.ts по-прежнему возвращает "legacy" по умолчанию, lib/seo/pdp-index-hold.ts начинается с if (PDP_INDEXING_MODE === "strict") return false;, а app/parts/[id]/page.tsx:223 завязывает legacyBrandHold на PDP_INDEXING_MODE !== "strict". На origin/dev обоих исключений уже нет, и isBrandHeldSku работает безусловно. То есть перенести один только #3244 = карточки Kuhn/Ferris/McHale/Marcrest плюс обе карты сайта уходят в index,follow — а по #3653 это 358 деталей из реестра, которые Джон не согласовывал. Тела двух PR приходят к противоположным выводам: #3244 называет отмирание удержания "намеренным и правильным", #3653 называет это четырьмя несогласованными брендами. Ревьюер, который читает только #3244, одобрил бы его сам по себе. Вторая ловушка: строка из раннбука в #3653 — "сначала cherry-pick, потом переменная, именно в таком порядке" — теперь наполовину устарела: #3244 втянул PDP_INDEXING_MODE=strict в код, так что сам перенос и есть переключение, и барьера на переменной в этой половине не осталось. Реально остаётся один шаг с переменной — ENABLE_EQUIPMENT_TREE_NOINDEX=true (всё ещё === "true", по умолчанию выключено, проверено на dev). Ни один открытый тикет эту связку не несёт; #2912 и #3594 оба закрыты.
Что делатьВова: собери фронтовый поезд 08-21 так, чтобы lib/seo-config.ts, lib/seo/pdp-index-hold.ts и app/parts/[id]/page.tsx приехали ОДНИМ cherry-pick PR, и поправь строку про порядок в docs/seo/index-open-runbook.md, пока по ней кто-нибудь не пошёл. После выкатки дёрни curl -A crop-audit по карточке KUH/FER/MCH/MAR и убедись, что там noindex, follow.
search#924 держит фронтовый поезд сразу с двух сторон: прод отдаёт Googlebot 500 на собственных URL из карты сайта, а сборка CI на dev красная из-за тех же 429надо решитьCROP-search#924 (issue), CROP-front#3715, CROP-front#3707
Замерено сегодня с -A crop-audit по проду: /robots.txt — 200 за 0.66s, /sitemap/1.xml — 200 за 0.34s, в нём заявлено 2,378 locs карточек, / — 200 за 6.4s, но /parts?q=<never-used-string> вернул 500 за 20.2s, /parts/CT-NHL-219758 не ответил за 40s, а /parts/CT-NHL-100016 (взят прямо из той же карты сайта) отвечал 19.0s, прежде чем отдать 200. На одной пробе номер детали Ferris вернул 500 за 63s. То есть вся индексируемая поверхность, которую мы сами отдаём на индексацию, сейчас отвечает краулеру медленно или с ошибкой. Отдельно: на HEAD dev фронта (ce2513b7) падают build, lint-and-build и crawl-preview: прогон 32348291522, задача build, 08:33Z — видно, что /api/brand/{mchale,marcrest,new-holland-agriculture,ferris,kuhn,kress,landoll}/equipment-types каждый раз возвращает errorStatus 429, retry-exhausted, adaptive-backoff healthy -> recovering. Это та же зависимость от прод-поиска, ради которой заведён front#3707. Следствие, которое никто не записал: пока CI на dev красный из-за этого, ни один cherry-pick PR в main не станет зелёным, значит фронтовый поезд идёт после подмены образа, а не параллельно ей. ops/scripts/search-promote-check.ts уже есть, и окно «до» по нему снято (ops 1960680).
Что делатьВова: сначала подмена образа по search#924, проверка через bun run ops/scripts/search-promote-check.ts (сначала статус, потом задержка; запрос, которого раньше не было), затем перезапуск CI фронта на dev — и только потом сборка поезда.
Две разные фронтовые выкатки обе стоят «в очереди», и это не одно и то же изменениена усмотрениеCROP-front#3680, #3679, #3675, #3662, #3638
Маршрут A — недельный поезд из раннбука: merge stage -> main, который после свежего среза несёт compare/main...dev = 88 коммитов / 300 файлов. Это вся неделя целиком — SEO-поезд (#3653, #3700, #3661), расширение товарного фида (#3655, #3650, #3690), работа по наличию и карточкам, перестройка CI. Маршрут B — то, что реально открыто против main прямо сейчас: пять cherry-pick PR: #3680, #3679, #3675, #3662, #3638 — и каждый из них починка CI или проверки (холодный старт sitemap-seo-gate, красные прогоны по расписанию, preflight на цель stage, вычитание brand-hold, обход автоматизации Vercel). Ни один из них не трогает код, который видит покупатель. Отдельно стоит проговорить: ни у одной фронтовой починки текущей аварии — #3712 (отвечать на временный сбой поиска как на аварию, а не 500), #3684 (сбой поиска не должен читаться как снятый с продажи каталог), #3628 (деградировавший каталог доезжает до краулера как 503, а не 500) — нет открытого cherry-pick PR. Они живут только на dev. То есть на маршруте B собственная устойчивость витрины к сбою поиска до прода не доезжает.
Что делатьВова: скажи вслух до созвона, какой маршрут в пятницу. Если это маршрут B, скажи прямо, что витрина продолжит отдавать 500 при сбое поиска, потому что #3712/#3684/#3628 в него не входят.
Необратимо: 407,302 карточки деталей превращаются в 404 на CDN, и это не под флагомна усмотрениеCROP-front#3700, CROP-front#3710
Ответ на вопрос 3, первый настоящий пункт. #3700 превращает 407,302 страницы деталей в 404 на CDN, в lib/proxy/supersession-redirect.ts. Я прогрепал весь его дифф в поисках флага — process.env встречается ровно один раз, как deps.baseUrl ?? process.env.SEARCH_SERVICE_URL. Фиче-флага нет: это уезжает вместе с выкаткой. Необратимая часть — обход Google: URL, которые он обошёл и получил на них 404, пока изменение было живым, — это уже отправленный сигнал, и откат кода его не отзовёт. Ущерб ограничен двумя вещами, и обе не предположены, а измерены в самих PR. Первое: все 407,302 уже is_publishable=false и уже noindex, как написано в теле #3700, так что проиндексированное множество почти не двигается — это работа с краул-бюджетом, а не деиндексация ранжирующихся страниц. Второе: #3710 намеренно ограничивает TTL на CDN значением s-maxage=300 (иначе он унаследовал бы от /parts-not-found s-maxage=3600 + 24h stale-while-revalidate), так что откат возвращает 200 за пять минут, а не за сутки. 13,626 строк dont_list, у которых есть цена или картинка, остаются 200.
Что делатьВова: в пятницу явно прими это или придержи. Если принимаешь — запиши на странице change-control, что откат здесь это «код плюс пять минут на CDN», а не мгновенно, и что верно это только благодаря #3710: он обязан ехать в той же выкатке, что и #3700, и никогда после него.
parts-services в эту выкатку не входит, а среди его 8 невыкаченных коммитов есть два денежных путина усмотрениеCROP-parts-services#615, #624, #620, #621, #622, #618, #619, #613
compare/main...dev = 8 коммитов впереди, 56 файлов. HEAD main — be865716b от 2026-08-14T07:07:30Z (#616, поезд 08-13); HEAD dev — 38352042f от 08-18. Двое из восьми трогают деньги. #615 (3323f61c7) пересчитывает цену корзины express-checkout по каталогу до списания — он меняет services/payment/src/routes/payment-intent.ts, то есть то, сколько спишут с покупателя; #624 (38352042f) не даёт выдать ответ UPS без блока стоимости за бесплатную доставку. Сам по себе ни один из них не необратим (наружу ничего не пишется; по действующему правилу наш код никогда не ходит в Stripe за возвратами), но оба меняют то, что покупателю говорят и сколько с него берут, поэтому откат после реальных заказов оставит эти заказы посчитанными по той версии, которая была живой. #620 — это подъём Bun 1.3.9 -> 1.3.14, лекарство от утечки TLS, из-за которой четыре сервиса ловят OOM; пожалуй, именно его больше всего хочется видеть на проде. Отдельно: четыре PR на выкатку в main стоят в очереди с 08-11/08-12 и не двигаются: #600 (перебор простаивающих соединений Mongo / OOM), #596 (запись в кэш на /rates, P0), #595 (скрипт бэкфилла), #594 (brand_code при загрузке фото). Обрати внимание на предупреждение из самого раннбука: git log origin/main..origin/dev здесь завышает — он говорит 8, а git cherry -v по patch-id говорит 6.
Что делатьВова: реши, едет ли parts-services в пятницу вообще. Если да — это отдельные cherry-pick с зафиксированного SHA поезда, ни в коем случае не мердж dev->main, и #620 идёт первым, потому что он и есть лекарство от OOM. Если нет — скажи об этом: #596 и #600 стоят в очереди девять дней, и это должно быть решением, а не самотёком.
У жёсткого QA-барьера из раннбука нет владельца, и единственный барьер, который реально чист, — это отсутствие release-blockerна усмотрение
docs/runbooks/delivery-flow.md называет три барьера перед выкаткой: явная приёмка QA от Дениса на stage (жёсткий — нет приёмки, нет выкатки, поезд ждёт неделю); ноль открытых тикетов release-blocker; и никаких выкаток на прод в рабочие часы. Второй я проверил: gh issue list --label release-blocker --state open по CROP-front, CROP-search и CROP-parts-services не возвращает ничего — этот барьер чист. В CROP-search открыты два тикета класса sale-blocker: #924 (это и есть данная выкатка) и #146 (баг с фитментом в описаниях BOM, отнесён Джоном, к делу не относится). Проблема в первом барьере: за QA никто не отвечает, да и подписывать нечего — сборки на stage нет (см. пункт про stage). Прошлая выкатка уже уехала за пределы окна рабочих часов по решению Вовы и была записана в ops/docs/change-control/emergency-log.md.
Что делатьВова: реши, кто подписывает приёмку, или явно откажись от этого барьера для выкатки только поиска — и запиши отказ в emergency-log.md так же, как записали внеоконную выкатку 08-14 в прошлый раз. Жёсткий барьер без владельца, через который все тихо переступают, хуже, чем барьер, от которого отказались официально.
Подмена образа — это всё или ничего: вместе с починкой задержек едут 27 коммитовна усмотрениеCROP-search#924 (везёт #891, #904, #906, #910, #913, #920, #922, #925 и остальные)
search#924 формулирует размен своими словами: «подмена образа — это всё или ничего: выкатка везёт все 27 коммитов, а не только #896/#897. Работа по SEO-поезду, изменение товарного фида (#904) и изменение оси склада в наличии (#891) едут вместе с ней. Именно это и надо взвесить; но это не повод оставлять на проде поиск на 16.5s.» Взять одну только починку задержек нельзя. Сейчас прод search-api крутит search-nest:2da1a51e (по базовому замеру 07:35Z в ops/docs/evidence/2026-08-20-search-promote-before.json).
Что делатьВова: либо принимаешь весь набор, либо не подменяешь. Если принимаешь — пункты 7, 10 и 15 ниже это три попутчика, которые реально меняют то, что видит покупатель, и назвать их надо на встрече, а не обнаружить потом.
search#891 переворачивает наличие на схеме для группы, которую никто не посчитал, а автор просил эту цифру до мерджана усмотрениеCROP-search#891; CROP-front#3562
#891 перестаёт брать наличие для схемы деталей с выведенной из оборота оси cnh_orderability и читает сигнал склада через тот же resolveRowAvailability, что и плитка каталога. Известная группа: 127,404 публикуемых строк на схеме переезжают с зелёной плашки In Stock на Out of Stock (эта цифра — из работы по #708, а не из этого PR). В самом PR прямо сказано, что изменение ШИРЕ: любая строка BOM без показаний склада и с Material Status GREEN раньше читалась как in_stock, а теперь читается как «непонятно» — «это вторая группа, это не 127,404, и её никто не считал». Тело PR заканчивается словами «@appdev-v — пожалуйста, прогони эти [два SQL-счётчика] до мерджа». Его влили 2026-08-16 без них. front#3562 добавляет защиту на стороне витрины, чтобы кнопка «добавить всё, что в наличии» не открыла это заново, и отдельного решения не требует.
Что делатьВова или Алекс: прогони два счётчика из тела #891 по прод-PG до подмены. Именно вторую цифру (зелёная плашка -> «мы не знаем») должен услышать Джон — и именно её ни у кого нет.
#3700 отдаёт 404 на 407,302 страницах деталей, и только #3710 удерживает ошибку в рамках пяти минутна усмотрениеCROP-front#3700, #3710
Эти два работают только вместе. #3700 отдаёт 404 на CDN для всех строк no_cnh_data плюс для строк dont_list без цены и без картинки: 407,302 URL переезжают с 200 / 286 KB / 11.2s на 404 / ~104 KB / меньше секунды. Сделано намеренно, но не бесплатно: 7,804 из 75,048 строк no_cnh_data содержимое ВСЁ-ТАКИ несут (4,829 с ценой, 3,841 с картинкой) и всё равно отдают 404 — сам PR помечает это как «единственную цифру здесь, которая не выглядит очевидно бесплатной», а сделать поведение зависимым от содержимого — это правка в одну строку в WITHDRAWN_STATUSES. publish_status пересчитывается КАЖДУЮ НОЧЬ, так что URL может законно ходить 404 -> 200 -> 404 от ночи к ночи. #3710 ограничивает TTL на CDN значением s-maxage=300; без него 404 наследует s-maxage=3600 + stale-while-revalidate=86400 (замерено на dev: x-vercel-cache HIT, age 84), и деталь, которая за ночь стала продаваемой, продолжает отвечать 404 до 25 часов.
Что делатьВова: реши, будут ли 7,804 строки no_cnh_data с содержимым отдавать 404. Потом вези #3700 и #3710 одним набором изменений — или не вези ни одного. После выкатки следи за pdp.unpublishable_404: там нужен ступенчатый сдвиг, а не медленный дрейф.
#3653 — единственный PR с галочкой «уезжает на прод на этой неделе», и его эффект требует двух переменных окружения, выставленных руками после того, как код приедетна усмотрениеCROP-front#3653 (с #3244)
Всё закрыто за PDP_INDEXING_MODE=strict и новой ENABLE_EQUIPMENT_TREE_NOINDEX (по умолчанию выключена, зарегистрирована в .env.example + lib/seo-config.ts, а НЕ в lib/env.ts — значение в Vercel это часть поезда). На мердже не меняется ничего; оба переключателя сегодня выключены/в legacy. Замерено на проде сейчас: карта сайта заявляет 18,657 URL — шард 0 = 16,279 locs, из которых 16,250 это /equipment*, шард 1 = 2,378 карточек деталей. После переключения остаётся примерно 2,407 (2,378 карточек + 16 списков брендов + 2 хаба брендов + 11 статических), а 352,201 карточка + 8,126 страниц техники + 8,124 хаба схем переезжают с index,follow на noindex,follow. Все эти страницы по-прежнему отвечают 200 и по-прежнему находятся поиском по сайту. Откат — только через переменные окружения, без деплоя: PDP_INDEXING_MODE=legacy, ENABLE_EQUIPMENT_TREE_NOINDEX=false, вернуть DIAGRAM_NOINDEX_PILOT_FAMILIES, переразвернуть текущую сборку.
Что делатьВова: выставь обе переменные в Vercel Production ПОСЛЕ того, как код приедет, а не до. И скажи Джону, что число в карте сайта уходит с 18,657 -> ~2,407 ещё до того, как он увидит это в GSC, и что подготовленный список потом сам дорастает примерно до ~11,800 без единой правки в коде.
Товарный фид — это пара из двух репозиториев с жёстким порядком: сначала бэкенд, иначе фид отдаёт 503на усмотрениеCROP-search#904; CROP-front#3655, #3690, #3661
search#904 переводит источник фида с indexable_parts_v1 + merchant_eligible (там нужно clinton_on_hand > 0, отсюда и потолок в 2,684) на part_indexing_candidates_v1 + проверки по картинке/офферу/коммерции: 97,578 строк, ширина шарда 5,000 -> 25,000. front#3655 переписывает маршрут под это: 4 шарда по 25,000, id по каноническому SKU, картинки галереи >=500x500, сопоставленные категории Google, g:shipping убран. ops/docs/2026-08-20-merchant-feed-promote.md: «сначала search-api, фронт вторым. Растяжка batchSize на фронте роняет фид в 503, если фронт приедет раньше бэкенда, в который он ходит». Замерено на проде сейчас: /feed/google/0.xml = 2,632 позиции за 0.28s, /feed/google/1.xml = 503, /feed/google/inventory/0.xml = 404 — ни одна половина не живая. front#3690 чинит ещё пять дефектов с пустыми строками и кодировкой, которые внёс сам #3655, и обязан ехать вместе с ним. Если вместо поезда stage->main использовать cherry-pick хотфиксом, закрытие — это три коммита СТРОГО ПО ПОРЯДКУ: сначала b90bac5ae (он добавляет canonicalSkuPath, которого на main нет; без него два других переносятся чисто, а потом не компилируются), затем a97fb36a4, затем 4c5f8456f, плюс #3690.
Что делатьВова: сначала подмена на бэкенде с проверкой, потом фронт. Сразу после этого прогони bun run scripts/merchant-feed-audit.ts по проду. И учти: аккаунт Merchant Center так и не создан (только через консоль, это на тебе) — фид может быть идеальным и при этом не дойти ни до кого, пока аккаунта нет.
Расширенный фид выставляет цены, которые не обновлялись с апреляна усмотрениеCROP-front#3655; hub#305
#3655 говорит об этом прямо: цены — это разовая апрельская загрузка, средний возраст примерно 95 дней, и 15,345 продаваемых деталей стоят НИЖЕ живой цены CNH (ops/docs/evidence/2026-07-20-nh-price-correctness.md). Переход с 2,635 позиций на 97,578 проблему не создаёт, но умножает подверженность ей в 37x. Ведётся с Алексом как hub#305, не закрыто. Отдельно: 1,584 из 97,578 — это федеративные бренды, чьи карточки отвечают 200 с noindex, follow; 353 из них уже есть в сегодняшнем фиде, так что они остаются, но коды причин стоит проверить при первом чтении Diagnostics.
Что делатьВова: реши, уходит ли фид в бой до закрытия hub#305 или нет. Если да — назови цифру вслух на встрече: 15,345 деталей стоят дешевле живой цены CNH — чтобы это был принятый риск, а не находка.
#3568 меняет то, что Googlebot получает на каждой странице, — без флага и без возможности придержатьна усмотрениеCROP-front#3568 (с #3621, #3628)
Googlebot убирают с блокирующего бот-рендера в Next и переводят на путь через CDN. Замерено на проде: Googlebot получал x-vercel-cache BYPASS за 0.87-4.52s против HIT за 0.20-1.40s у браузера, причём head и body побайтово одинаковые — то есть блокирующий рендер не дал ничего, зато ограничивал скорость обхода. Переопределение htmlLimitedBots и workflow, который следил за его расхождением, удалены. Это изменение поведения для краулеров на всём сайте, и оно приезжает в момент деплоя кода; переключателя в переменных окружения нет. Два поддерева намеренно оставляют старый блокирующий рендер (/parts-diagrams/<brand>/<model> и /brand/<slug>), потому что их 404 существует ТОЛЬКО благодаря бот-рендеру — #3621 переносит оба на CDN и тоже лежит на dev, и именно это позволит со временем убрать исключение. Аудиторские UA, включая crop-audit, с блокирующего пути тоже уходят, так что тайминги проб до и после несравнимы.
Что делатьВова: либо принимаешь это как часть SEO-пачки, либо придерживаешь всю SEO-пачку. Если едет — считай экономию по полному расчётному дню продовых задержек «бот против человека» после выкатки, а не по превью: об этом сказано в самом #3568.
Починка карточек с 500 на 503 закрыла свой тикет на мердже в dev и на прод не приехала — а дефект там живой прямо сейчасна усмотрениеCROP-front#3712, CROP-front#3695
front#3712 влит в dev сегодня в 08:08Z: на временный сбой 429/5xx/таймаут /parts/[id] теперь рендерит ServiceUnavailable, а CDN переклеивает это краулерам в 503 + Retry-After: 300 вместо жёсткого 500. В его собственном блоке доказательств записаны три пробы по проду в 07:35Z: 500/34.9s, ответа нет/45s, 500/27.1s на CT-NHL-100016. front#3693 ЗАКРЫТ (08-20). Мои пробы четырьмя часами позже всё ещё показывают на проде 500 и ожидание 19-40s, то есть Google получает ровно то поведение, которое описано в #3693 — починка живёт только на dev, а её тикет закрыт. Сколько сегодняшних 500 приходится на аварию поиска, а сколько на что-то ещё, я разделить не могу: обе поверхности падают на одном и том же апстриме, и витрина отдаёт 500 ровно на тех запросах, которые отбивает поисковый API.
Что делатьВова: в пятницу реши, едет ли #3712 в поезде. Учти, что это эшелонированная защита — саму причину убирает подмена образа по search#924, — но только она не даст будущему всплеску 429 заново научить Google, что проиндексированные карточки отдают 500.
#2912 и #3594 закрыты, но замер, который их доказывает, явно назначен на «после выкатки», и держать его некомуна усмотрениеCROP-front#3244, CROP-front#3653
Оба тикета закрылись на своих мерджах в dev. Тело #3244: «замер схождения счётчиков карты сайта и карточек — это активность фазы проверки, уже после выкатки». Тело #3653: «числа „после“ завязаны на переключение переменной, поэтому приезжают с пятничным поездом, а не здесь». Проверяемое утверждение реально и конкретно — 352,201 карточка отвечает index,follow при карте сайта, заявляющей 2,378, и сходится к примерно 2,407 индексируемым URL — и я подтвердил, что прод-sitemap/1.xml по-прежнему несёт ровно те же 2,378 locs, что и до изменения. Эпик #2370 открыт, но ничего из этого не называет. То есть шаг «выкатить и проверить» для самого большого SEO-изменения в поезде не принадлежит ни одному тикету, и блок из четырёх curl-проверок в #3653 останется единственным местом, где это записано, как только PR уедет из поля зрения.
Что делатьВова: до поезда заведи один тикет (или переоткрой #3594) с четырьмя проверочными curl из #3653 и счётчиком locs в карте сайта, назначь его на себя и поставь дату после выкатки.
Четыре PR по parts-services стоят в очереди к main уже 8-9 дней, включая починку OOM, а единственная проверка для лекарства на Bun — непрочитанный прогон под нагрузкойна усмотрениеCROP-parts-services#620, #600, #596, #595, #594
HEAD main в parts-services всё ещё be865716 (08-14, поезд 2026-08-13). ps#600 (cherry-pick #597, половина утечки про maxIdleTimeMS) открыт с 08-12 и не уехал 08-14; в его теле написано, что алерт открыт с 08-12T09:56Z, а user-service убивают по OOM примерно раз в сутки. ps#620 (Bun 1.3.9 -> 1.3.14, остаточная утечка TLS) влит в dev 08-17, с измеренной скоростью утечки 2.87-4.43 MiB/h по user/delivery/payment/catalog и убийствами по OOM 08-15 и 08-16; заявленная для него проверка — не тест, а прогон на dev: смотреть run.googleapis.com/container/memory/utilizations на сервисах *-dev и сравнивать наклон с этой базой. Прочитал ли кто-нибудь этот прогон — НЕДОСТУПНО: gcloud в этой сессии не авторизован (токен просрочен), и я сообщаю это как «не измерено», а не как ноль. Также в очереди с 08-11: #594 (загрузка фото падает с 500 с 06-10), #595 (документы всё ещё на Mongo), #596 (расчёт доставки застрял за записью в кэш). ps#589 открыт, так что сама утечка отслеживается; не отслеживается только то, кто и к какому сроку прочитает прогон. ps#600 ещё предупреждает: держать перезапуск ревизий наготове для user/payment/delivery/catalog, пока не приедет подъём Bun.
Что делатьВова: сделай gcloud auth login и до пятницы прочитай наклон памяти на *-dev относительно базы 2.87-4.43 MiB/h. Если ровно — выкатывай #600 вместе с подъёмом Bun и снимай перезапуск ревизий; в тот же день реши, едут ли #594/#595/#596 или выпадают из очереди.
Когда ломается что-то выше по цепочке — 8 изменений, 7 из них заметны клиенту или Google
Страница каталога, которую поисковый сервис отказывается отдавать: сегодня это жёсткая ошибка, а на dev — страница, которая открывается пустойизмереноклиент это видитfront#3628, front#3695, front#3712
До — продакшен сегодняHTTP 500 примерно через 17 с, тело 57,091 байт без единой позиции в списке. Ни заголовка Retry-After, ни X-Robots-Tag. Воспроизвелось на шести номерах страниц, пять из которых раньше никто не запрашивал: страница 500 (500, 17.01 с), 641 (500, 17.02 с и 17.43 с), 733 (500, 16.95 с), 852 (500, 17.14 с), 904 (500), 1000 (500, 18.63 с).
После — кандидат сегодняHTTP 200 примерно через 9-11 с, тело 54,416 байт — тоже без единой позиции в списке (рабочая страница, ?page=1, весит 483,454 байт). По-прежнему ни Retry-After, ни X-Robots-Tag. То есть клиент перестаёт получать страницу с ошибкой, но заявленный результат front#3628 — 503 + Retry-After: 300 + X-Robots-Tag: noindex для краулера — на crop-dev.app НЕ воспроизводится. Это утверждение проверяли на превью-деплое самого PR, а не на dev. Перед выкаткой стоит принять решение: человеку тихая 200 лучше, чем 500, а для Google 200 на пустой странице — это soft-404 и это хуже, чем обещанная PR 503 с Retry-After.
Как на тот же самый сломанный запрос отвечает сам поисковый сервис — контрольный замер, который доказывает, что строка выше это работа витрины, а не аварияизмереноsearch#913 (контекст), front#3628
До — продакшен сегодняHTTP 503 за 5.44 с, retry-after: 1, cache-control: no-store, server-timing: total;dur=5003, тело {"error":"Service temporarily unavailable","message":"Search is briefly degraded — please retry.","code":"SEARCH_DEGRADED"}
После — кандидат сегодняБайт в байт то же самое: HTTP 503 за 5.49 с, retry-after: 1, total;dur=5002, то же тело SEARCH_DEGRADED. Так и задумано. Оба окружения получают от бэкенда одинаковый, корректно оформленный отказ — значит, разница между 500 и 200 в строке выше целиком в том, как витрина этот отказ обрабатывает, и ничего из этого не относится к CROP-search#874 — прод-аварии поиска.
Открытие схем деталей для машины Kuhnизмереноклиент это видитsearch#922
До — продакшен сегодняТри холодных запуска (каждый раз новый ключ кэша, x-cache: MISS на всех трёх): server-timing total;dur = 3,995 мс / 1,503 мс / 2,597 мс; по часам 5.09 с / 2.51 с / 3.21 с.
После — кандидат сегодняТе же три запуска против dev: total;dur = 58 мс / 43 мс / 32 мс; по часам 1.11 с / 1.08 с / 1.05 с. По медиане примерно в 60 раз меньше времени в базе. Ответ идентичный — все шесть ответов весят 217,121 байт с md5 20eac2b35201af17324ce24b2f6d6ade — то есть в том, что видит клиент, не изменилось ничего, изменилось только время ожидания.
Три самые нагруженные страницы каталога перестают снова и снова задавать базе один и тот же вопросизмереноклиент это видитsearch#920
До — продакшен сегодняЗаголовка x-cache на этих маршрутах нет вообще, и каждый вызов доходит до PostgreSQL. Разделы: db;dur=33 и следом db;dur=39 на двух одинаковых вызовах подряд (перепроверил позже: 41 и следом 54). Узлы модели: db;dur=304 и следом db;dur=263.
После — кандидат сегодняРазделы: первый вызов db;dur=90 с x-cache: MISS, второй такой же вызов — cache;dur=0 с x-cache: HIT. Узлы модели: первый вызов db;dur=725 MISS, второй cache;dur=0 HIT. На повторе базу никто не трогает. Тела ответов в двух окружениях побайтово одинаковые (3,807 байт у разделов, 19,635 у узлов), так что это чистое снятие нагрузки с той самой базы, которая обслуживает и оформление заказа.
Боковая панель фильтров быстро сдаётся вместо того, чтобы висеть, когда база перегруженаизмерено частичноклиент это видитsearch#913
До — продакшен сегодняHTTP 200 за 4.02 с, server-timing search_facets;dur=3360, x-cache: MISS. Зависания на 15 секунд с 500 в конце, которое зафиксировал PR (15,082 / 16,114 / 14,360 мс на этом же ключе модели), не случилось — сегодня в 12:47Z база не перегружена.
После — кандидат сегодняHTTP 200 за 5.48 с, search_facets;dur=4762, x-cache: MISS. Новый предел в 8,000 мс не сработал ни на одной стороне, потому что ни одна к нему даже не приблизилась. Изменение, ради которого всё затевалось — быстрая 503 с Retry-After вместо 15-секундной 500, а также вместо молча пустого списка фильтров — проявляется только тогда, когда PostgreSQL перегружен, а создать это состояние я мог бы только намеренно нагрузив продовую базу, чего я не делал. Сегодня 200 с обеих сторон.
Страница детали, когда поисковому сервису тяжелоизмерено частичноклиент это видитfront#3712, front#3695
До — продакшен сегодняШесть проб между 12:44Z и 12:57Z: HTTP 200 каждый раз, 2.93 с / 3.64 с / 3.97 с / 4.12 с / 4.43 с / 6.86 с, 559,047 байт. Ошибки 500 на этом же URL, пойманные PR в 07:35Z (500 за 34.9 с, ответа нет за 45.0 с, 500 за 27.1 с), сейчас НЕ воспроизводятся — за утро продовый поиск восстановился.
После — кандидат сегодняDev отвечает 200 за 3.23 с и 3.82 с, 557,270 байт. Обе стороны здоровы, поэтому само изменение — кратковременный сбой вышестоящего сервиса доходит до клиента жёсткой 500 или же страницей «временно недоступно, попробуйте чуть позже» — прямо сейчас по требованию не наблюдается. Чтобы его увидеть, пришлось бы нагружать и без того деградировавший продовый сервис, а оба PR прямо говорят, что делать так нельзя, — я и не стал. Это единственная строка, где «до» — это утреннее измерение из PR, а не то, что встреча может перезапустить сама.
Страница сохранённых деталей, которая во время аварии поиска сообщает клиенту, что его сохранённые детали сняты с производстванаблюдать не удалоськлиент это видитfront#3684
До — продакшен сегодняHTTP 307 — редирект на /sign-in?redirect_url=%2Faccount%2Fsaved-parts. Страница закрыта входом через Clerk, поэтому анонимная проба до самого поведения просто не доходит. (/garage и /saved-parts оба отдают 404 — настоящая страница это /account/saved-parts.)
После — кандидат сегодняИзмерить не удалось. Чтобы показать разницу, нужны три вещи одновременно: вошедший в аккаунт клиент, уже сохранённые у него детали и отказывающий поисковый сервис. Первые две требуют настоящей сессии, а третью нельзя безопасно создать на проде. Изменение реальное и покрыто юнит-тестами (3 из 7 тестов красные до фикса, 7 зелёных после), но снаружи пробой его не увидеть, так что на выкатку оно идёт как не проверенное end-to-end.
Эту пару сегодня наблюдать не удалось — причина в двух строках выше, и никакого числа, чтобы закрыть пробел, не выдумано.
ОТВЕЧЕНО — этот кандидат запускался: молча упавший деплой перезапустили и выкатили до релизаизмереноклиент это видитsearch#925
До — продакшен сегодняНеприменимо — на проде работает образ до выкатки, в котором этого изменения нет.
После — кандидат сегодняНа dev он тоже не работает. HEAD ветки dev — 678b7af2 (search#925), но её запуск Deploy search-api-dev упал в 08:29Z с сообщением «Gate failed — candidate tag removed». Последний успешный деплой — f5518b6b (search#922) в 07:38Z, и search-api-dev до сих пор отдаёт ревизию search-api-dev-00015-yej. То есть search#925 влит, входит в завтрашний кандидат на выкатку и при этом ни разу не обслужил ни одного запроса ни в одном окружении. Всё, что измерено в строках выше, взято только из #913 + #920 + #922; ничего из этого не включает #925.
Что этот раздел доказывает, а что нетВсе пробы сделаны 2026-08-20 между 12:32Z и 13:05Z с машины Вовы. Обе пробы витрины уходили с UA crop-audit; каждая проба crop-dev.app уходила с заголовком обхода Vercel, и я убедился, что %{url_effective} остался на crop-dev.app, а не ушёл следом на vercel.com. Значение секрета нигде не печаталось и никуда не записывалось. Что стоит с каждой стороны — проверено, а не предположено. Продовая витрина = деплой dpl_FQ8AS8w5CCA7g1JRRv3bH8QJfBHt, коммит 0ee970f4 в main, собран 2026-08-14 — то есть все пять front-PR живут только на dev, и окно «до/после» чистое. crop-dev.app = dpl_9iEJLZeLJ9JaWZtx4KVHmuemp8UZ, коммит ce2513b7 в dev, собран сегодня в 08:20Z, уже после того, как все пять front-PR были влиты. search-api-dev отдаёт ревизию search-api-dev-00015-yej = коммит f5518b6b, значит, в ней есть search#913, #920 и #922 — но НЕТ #925. Две вещи, на которые я хочу, чтобы встреча посмотрела до того, как выкатка будет одобрена. Первое — front#3628. Его заявленный критерий приёмки (деградировавшая страница списка доходит до краулера как 503 + Retry-After: 300 + noindex) проверялся на превью-деплое самого PR и на dev не воспроизводится. Dev отвечает 200. Правдоподобное объяснение, которое я НЕ проверял и обозначаю как гипотезу, а не как факт: на dev эти страницы возвращаются с x-vercel-cache: HIT и растущим заголовком age, при этом всё равно отдаются 9-11 секунд — это почерк закэшированной PPR-оболочки: статус 200 фиксируется оболочкой ещё до того, как отваливается динамическая часть страницы, поэтому никакой 5xx при рендере не остаётся и переразмечать edge-инспектору нечего. На проде на том же маршруте x-vercel-cache: BYPASS, поэтому сбой вылезает как 500. Если это так, фикс работает на холодном превью и обходится стороной на прогретом CDN — а прод после выкатки будет именно в таком состоянии. Кто-то должен подтвердить механизм до пятницы. Второе — у search#925 упал гейт деплоя и оставил на месте предыдущую ревизию, ровно тот самый известный режим молчаливого отказа. Ничто в этих измерениях это изменение не задействует. Без отдельной строки — из честности насчёт того, докуда проба вообще дотягивается: front#3643 (изоляция сбоев вышестоящих сервисов внутри границы кэша) — это поведение на сборке: раньше один кратковременный сбой поиска ронял сборку всего сайта, теперь вместо этого на главной пропадает блок популярных деталей. Рантайм-пробы, которая отличила бы одно от другого, нет: в обоих здоровых окружениях блок рисуется нормально; доказательство здесь — пара логов сборки в PR. Это видно команде, а не клиенту. gcloud в этой сессии не был авторизован, поэтому ничего здесь не взято из запросов к ревизиям Cloud Run или к метрикам — факты о деплоях взяты из логов GitHub Actions и из Vercel API, и то и другое можно перезапустить. Утром продовый поиск был в известном деградировавшем состоянии (CROP-search#874); к моменту проб он восстановился достаточно, чтобы страница детали шесть раз подряд ответила 200 — поэтому строку 6 не удалось измерить как «до/после», о чём в ней и сказано.
Скорость поиска — 6 изменений, 6 из них заметны покупателю или Google
Поиск по фразе, которую никто раньше не искал, больше не ждёт сломанный запрос по брендамизмереноэто видит покупательsearch#896, search#897
До — продакшен сегодня7 поисковых строк, которые ещё ни разу не запрашивались, на продакшене. Общее время по данным сервера: 2,137 / 15,051 / 15,173 / 15,199 / 15,272 / 15,285 / 15,454 мс — шесть из семи упёрлись ровно в 15-секундный таймаут базы, независимо от запроса. По секундомеру 4.7-17.4 с. В заголовке с таймингами на продакшене вообще нет сегмента federated_equipment, поэтому эти 15 секунд выглядят как необъяснимый провал.
После — кандидат сегодня7 новых строк на dev: 1,312 / 1,347 / 1,680 / 1,747 / 2,138 / 2,571 / 5,140 мс. По секундомеру 1.8-5.7 с. Никто не упирается в стену 15 с. В каждом ответе dev есть federated_equipment;dur=0 — у этого шага теперь свой сегмент (#896), и стоит он ноль, потому что неудачный снимок по брендам запоминается на пять минут, а не запрашивается заново (#897).
Страница поиска в витрине отдаёт результат, а не ошибкуизмереноэто видит покупательsearch#896, search#897 (сквозной путь через crop.clintontractor.net)
До — продакшен сегодня7 новых запросов на продакшене: 6 вернули HTTP 500 через 16.7-17.2 секунды, 1 вернул 200 через 6.2 с. Это и есть тот живой симптом для покупателя, из-за которого заведён CROP-search#924.
После — кандидат сегодня7 новых запросов на dev: 7 из 7 вернули HTTP 200, за 4.4-8.6 секунды. Оговорка прямым текстом: на dev ещё и более новый код витрины, поэтому эта строка — весь путь целиком, а не только поисковый сервис; изолированное измерение сервиса это строка 1 выше.
Три самых нагруженных маршрута схем отдаются из кэша, а не ходят в базу каждый разизмереноэто видит покупательsearch#920
До — продакшен сегодняПродакшен на этих трёх маршрутах вообще не отдаёт заголовок x-cache, а db;dur= стоит в каждом вызове, включая немедленный повтор того же самого URL: сборки модели 124 и затем 139 мс, разделы 40 и затем 80 мс, карточка сборки 13 и затем 14 мс. Каждый запрос доходит до Postgres. (На /api/search и на маршрутах брендов продакшен x-cache всё-таки отдаёт, то есть заголовок не срезается — эти маршруты просто никогда не подключали.)
После — кандидат сегодняDev на первом вызове отдаёт x-cache: MISS с db;dur=, а на втором — x-cache: HIT с cache;dur=0 — и так на всех трёх маршрутах. Сознательное исключение работает как задумано: у /api/assembly-search на dev по-прежнему нет x-cache и запрос по-прежнему выполнился дважды (db;dur=970 и затем 409 мс).
Открытие списка схем машины Kuhn больше не пересчитывает все 15.5 миллиона строк деталейизмереноэто видит покупательsearch#922
До — продакшен сегодняПродакшен, общее время по данным сервера, кэш сознательно обойдён свежим nonce, 3-4 замера на модель: модель 2273 = 1,252 / 1,305 / 1,315 / 1,379 мс; модель 15661 = 1,222 / 1,229 / 1,266 мс; модель 10363 = 1,250 / 1,257 / 2,427 мс; модель 159 = 135 / 144 / 210 мс.
После — кандидат сегодняDev, те же модели, тот же метод: модель 2273 = 29 / 34 / 38 / 50 мс; модель 15661 = 85 / 99 / 100 мс; модель 10363 = 29 / 35 / 41 мс; модель 159 = 21 / 23 / 69 мс. Примерно в 30 раз быстрее на трёх крупных моделях. Ответ при этом не изменился: полный ответ модели 2273 из 916 строк побайтово совпадает на продакшене и на dev, если исключить поле с таймингом.
Когда база перегружена, боковая панель фильтров быстро отказывает и просит повторить запрос, а не висит пятнадцать секунд и падает с ошибкойизмереноэто видит покупательsearch#913
До — продакшен сегодня14 холодных вызовов на продакшене по самому тяжёлому шагу: 1,135 / 2,025 / 2,388 / 2,483 / 2,546 / 2,793 / 2,901 / 3,052 / 3,245 / 3,820 / 6,596 / 7,777 / 11,095 мс. Все ответили 200. Потолка нет — вызов на 11.1 секунды это продакшен, который просто ждёт, а после 15 с это превращается в 500 или в молча пустую панель.
После — кандидат сегодня16 холодных вызовов на dev, тот же шаг: от 1,497 до 7,628 мс ответили 200, а один вызов, дошедший до нового бюджета в 8 секунд, вернул HTTP 503 с Retry-After: 1 при total;dur=8061. Поймано вживую и сопоставлено с вызовом на продакшене в ту же секунду, который всё ещё ждал на 7,777 мс без всякого ограничения. Ограничение срабатывает ровно там, где заявлено в PR.
Списки схем CNH: ограничить фильтр по модели и прекратить перетасовку одинаковых строк между страницамиизмерить нельзяэто видит покупательsearch#925
До — продакшен сегодняДефект реальный и воспроизводится сегодня. Три одинаковых запроса страницы 1 списка схем модели ABC3335398 вернули на dev-бэкенде три разные страницы — 16 и 19 строк из 50 поменяли позицию, и наборы строк были даже не одни и те же. Продакшен на те же три вызова во время проверки ответил одинаково просто потому, что сейчас у него стабильный план запроса; исправления нет ни на одной стороне, значит ни одна от этого не застрахована.
После — кандидат сегодняИзмерить нельзя. Изменение влилось в dev сегодня в 08:08Z, деплой на dev отработал, ревизия-кандидат была собрана и развёрнута без трафика, контрактный гейт не прошёл, шаг выкатки был пропущен, а ревизию-кандидат удалили. Dev отдаёт предыдущий коммит. То есть search#925 не работает ни на одном из сервисов, и если до завтра свежий деплой на dev не пройдёт свой гейт, в пятничную подмену образа он тоже не попадёт.
Эту пару сегодня измерить не удалось — причина в двух строках выше, и ни одна цифра не была придумана, чтобы закрыть пробел.
Что этот раздел доказывает, а что нетЧТО РЕАЛЬНО ОБСЛУЖИВАЕТ ЗАПРОСЫ — от этого зависит, что унесёт завтрашняя выкатка: - На продакшене search-api работает на образе search-nest:2da1a51e (дайджест снят в 07:35Z в /Users/vova/Code/CROP/ops/docs/evidence/2026-08-20-search-promote-before.json). Это на 31 коммит позади образа, который отдаёт dev, crop-search:f5518b6b. Все пять измеренных PR — #896, #897, #913, #920, #922 — попадают в этот разрыв, поэтому у каждой строки выше есть настоящее «до» и настоящее «после». - search#925 НЕ входит в образ, который отдаёт dev (dev ровно на один коммит позади него). Его гейт при деплое не прошёл, и workflow удалил невыкаченного кандидата. Стоит минуты на встрече: выкатка подменяет образ, прошедший гейт на dev, поэтому PR, который гейт так и не прошёл, вместе с ней не поедет. - В этой сессии gcloud не авторизован, поэтому сегодня я не смог перечитать живые дайджесты образов и имена ревизий Cloud Run. Дайджест выше — из утренних сохранённых свидетельств; то, что кода на продакшене по-прежнему нет, подтверждено вместо этого поведением: нет сегмента тайминга federated_equipment, нет заголовка x-cache на маршрутах сборок и нет 8-секундного потолка у фильтров. Без gcloud считайте дайджест снимком на 07:35Z, а не на сейчас. КАК ПЕРЕЗАПУСТИТЬ И НЕ ПОЛУЧИТЬ ЛОЖНЫЙ ЗЕЛЁНЫЙ: - Никогда не переиспользуйте поисковую строку. Повтор отдаётся из single-flight-кэша меньше чем за секунду, и сломанная система измеряется как здоровая. Каждая команда выше генерирует новую строку через openssl rand. - На кэшируемых маршрутах добавляйте уникальный параметр nonce=, чтобы принудительно промахнуться мимо кэша — ключ кэша это весь URL. Проверено: лишний параметр ответ не меняет. - Всегда отправляйте -A crop-audit и всегда отправляйте заголовок обхода Vercel на crop-dev.app, иначе проба уйдёт по 302 на страницу входа и отрапортует 200. - Готовый инструмент /Users/vova/Code/CROP/ops/scripts/search-promote-check.ts делает всё это сквозным прогоном и сохраняет JSON-снимок, но для дайджестов образов он вызывает gcloud auth print-access-token, поэтому в этой сессии запуститься не смог. Командам curl выше не нужна никакая авторизация, кроме секрета обхода. УСЛОВИЯ ВО ВРЕМЯ ОКНА (12:33-13:20Z): - Шторм отказов 429 из 07:35Z не повторился — все 40 с лишним вызовов API на продакшене ответили 200. Ошибки 500 на продакшене в строке 2 — это витрина, а не отказы сервиса. - Насыщение Postgres в течение окна то приходило, то уходило. Это видно в цифрах: один вызов фильтров на продакшене на 11,095 мс и один вызов на dev, упёршийся в потолок 8 с, при том что всё остальное по обе стороны укладывается в 2-3 секунды. Поэтому строки 1, 4 и 5 дают разброс, а не одно значение, и поэтому сравнения dev против прода в строке 4 гонялись подряд, а не с разницей в часы. - Все пробы были только читающими GET-запросами. Продакшен-Postgres обслуживает ещё и оформление заказа, поэтому на дорогих шагах число замеров сознательно держали небольшим.
Каталог, фильтры и статус «В наличии» — 8 изменений, 4 из них заметны покупателю или Google
Клик по категории больше не тащит старые фильтры в адресную строкуизмеренопокупатель это видитfront#3590
До — продакшен сегодняАдресная строка становится /parts?view=counter&pageSize=100&sortBy=name_asc&hasImage=true&categoryId=FASTENERS — каждый параметр, который там уже был, едет следом
После — кандидат на выкатку сегодняАдресная строка становится /parts?categoryId=FASTENERS — проверено сегодня на живой странице crop-dev.app, а не взято из описания PR
Страница, на которую раньше вёл клик по категории, — та самая, которую мы просим Google выбросить из индексаизмерено частичноfront#3590
До — продакшен сегодняСклеенный URL отвечает <meta name="robots" content="noindex, follow"> и вообще не несёт canonical. Чистый /parts?categoryId=FASTENERS отвечает "index, follow" и указывает canonical сам на себя. То есть на проде каждый клик по категории тратил ссылочный вес на страницу, которую мы сами попросили Google выбросить.
После — кандидат на выкатку сегодняНа dev тот же склеенный URL теперь отдаёт canonical на чистый /parts?categoryId=FASTENERS. Значение robots на dev сравнить нельзя — там весь сайт намеренно отвечает "noindex, nofollow", — поэтому видимая покупателю половина этого пункта находится в строке выше: клик больше не создаёт noindex-адрес.
Одно место с категориями вместо двух, которые никогда не совпадалиизмеренопокупатель это видитfront#3590
До — продакшен сегодня1 — полоса «таблеток» под строкой поиска висит на странице рядом со списком категорий деталей в боковой панели, отсортирована иначе и может показывать другой набор категорий
После — кандидат на выкатку сегодня0 — полосы «таблеток» больше нет; боковая панель — единственный элемент управления категориями
Поисковый сервис теперь считает, что останется после включения фильтра «да/нет»измереноsearch#892
До — продакшен сегодня{"total":8,"toggleCounts":null} — на проде такого поля нет, поэтому страница никак не может узнать, что фильтр опустошит выдачу
После — кандидат на выкатку сегодня{"total":8,"toggleCounts":{"shipsSameDay":0,"hasImage":3}} — dev это измеряет: из 8 тормозных деталей Versatile ни одна не отгружается в день заказа
Фильтр «отгрузка в день заказа» по-прежнему ведёт на пустую страницу — и там, и тамизмеренопокупатель это видитfront#3592, search#892
До — продакшен сегодняГалочка активна; по клику попадаем на ...&shipsSameDay=true, и страница пишет «No results match your filters»
После — кандидат на выкатку сегодняНа dev без изменений: галочка всё так же активна и всё так же приводит к «No results match your filters», хотя собственный /api/filters на dev отдаёт shipsSameDay: 0 ровно для этой комбинации. Серверная половина приехала; боковая панель в этом случае на неё не реагирует. Этот тупик выкаткой НЕ закрывается.
Схема деталей перестаёт предлагать детали, которые мы не можем отгрузитьизмеренопокупатель это видитsearch#891, front#3562
До — продакшен сегодня52 детали на схеме Battery: 49 показывают in_stock, 2 — call_us, 1 — непонятно. Деталь 48126818 показывает in_stock, хотя показание склада по ней RED, а страница каталога по той же детали говорит, что её нет в наличии. Кнопка «Add all in-stock» добавила бы все 49.
После — кандидат на выкатку сегодняТе же 52 детали: 34 показывают in_stock, 18 — call_us, 0 — непонятных. Деталь 48126818 теперь показывает call_us. 15 строк уходят из кнопки «Add all in-stock», и схема сходится с каталогом.
«В наличии» над кнопкой «Запросить цену» — живого примера не нашлосьизмерено частичноfront#3565
До — продакшен сегодняПрод и так показывает всё правильно в самом близком живом случае, который удалось найти: CT-AGC-302047 (онлайн-цены нет, 50 штук на нашем складе) показывает «Out of Stock» и «Ask for a price», при этом «In Stock» не встречается ни разу, и «usually ships same day» — тоже ни разу.
После — кандидат на выкатку сегодняНа dev то же самое. Мне не удалось найти ни одной живой детали в том состоянии, на которое нацелена правка: в рабочей базе каждая публикуемая деталь с ценой, но недоступная к покупке, и так показывает out_of_stock, и ни у одной из них нет остатка на складе. То есть это едет как страховка от состояния, которого сегодня ни у одной детали нет, а не как видимое исправление — пожалуйста, не описывайте это Джону как убранную живую неправду, пока кто-нибудь не найдёт такую деталь.
Тест-страховка, чтобы кнопка «Добавить всё в наличии» снова тихо не взяла запрещённую детальнаблюдать нельзяfront#3562
До — продакшен сегодняНичто не проверяло состав самой кнопки от начала до конца; поломка соответствия в снапшоте роняла всего одну не связанную с этим проверку.
После — кандидат на выкатку сегодняСнаружи наблюдать нельзя — это изменение только в тестах, у него нет ничего отрисованного на странице. И в этой сессии тест не запускался: рабочего дерева CROP-front здесь нет, а общий чекаут стоит на устаревшей ветке, так что запуск измерил бы не тот код.
Эту пару сегодня наблюдать не удалось — причина в двух строках выше, и никакая цифра не выдумана, чтобы закрыть пробел.
Что этот раздел доказывает, а что нетС поправкой на известный сбой поиска на проде (search#924): все проверки листинга здесь — это режим просмотра каталога (categoryId / manufacturer), а не /parts?q=, и прод на всех ответил 200 за 1.3-1.9 с. Ни одна строка выше не искажена штормом 429 / 17 с. Единственное место, где устаревший прод действительно виден, — строка про схему деталей, и это как раз выкатка делает свою работу. Половина front#3592 про фильтр по отзывам — настоящая, но покупателю не видна, поэтому отдельной строкой она не идёт: на проде такого элемента управления нет вообще (grep 'filter-hasReviews' по HTML продовой /parts возвращает 0 — при NEXT_PUBLIC_APP_ENV=prod он вырезается на сборке), а dev рисует его неактивным, с подсказкой «The review demo ignores other filters — clear them to browse reviewed parts». Тупик, который она закрывает, за пределами команды никто никогда не видел. В строке про «отгрузку в день заказа» объяснение, почему галочка всё ещё кликается, — это предположение, а не измерение: список брендов в боковой панели на dev для BRAKES показывает New Holland, Ford, Fiat, Versatile, Ford/Dearborn и Hesston, то есть фасеты считаются с отброшенным фильтром по бренду, а /api/filters?categoryId=BRAKES сам по себе возвращает shipsSameDay: 42. Счётчик, посчитанный без бренда, никогда не бывает 0, и это объясняло бы, почему элемент остаётся активным. В коде я это не подтвердил — стоит завести одну задачу, прежде чем кто-то отчитается, что этот тупик закрыт. gcloud в этой сессии не авторизован (истёк токен), поэтому фактов про ревизии Cloud Run и SHA образов здесь нет. Это ложноотрицательный результат, а не доказательство отсутствия. Гигиена проверок: curl-запросы на dev шли с заголовком обхода защиты Vercel, и каждый проверялся через -w '%{url_effective}', чтобы убедиться, что он остался на crop-dev.app, а не ушёл следом на страницу входа; все проверки статусов шли с -A crop-audit. Проверки в браузере выполнялись в локальном Chrome, где уже есть сессия Vercel для crop-dev.app, так что клики на dev — это настоящие действия на странице. Значение секрета обхода нигде не печаталось, не логировалось и не записывалось. Пометка для тех, кто будет повторять: dev намеренно отвечает "noindex, nofollow" по всему сайту — никогда не читайте это как регрессию. Идентификаторы деталей и узлов, использованные здесь (CT-AGC-302047, 48126818, узел ABC5920397), найдены запросом только на чтение к рабочей базе, а не угаданы.
Адреса, которые вели в никуда — 7 изменений, 5 из них заметны покупателю или Google
Детали строительного дивизиона открываются, а не упираются в тупикизмереноэто видно покупателюfront#3657
До — прод сегодняТупик. /parts/069526 делает один переход 308 на /parts/CT-NHL-069526, и этот адрес отвечает 404. То же самое на всех трёх проверенных строительных деталях (069526, A14858, 1014009C1) — 3 из 3 упираются в тупик, итоговый код 404.
После — кандидат сегодняРаботает. Один переход 308 на /parts/CT-NHC-069526, который отвечает 200. 3 из 3 попадают на живую страницу за один переход. У всех проверенных деталей publish_status='sellable', так что ничто другое их не скрывает.
Адреса строительных деталей, которые Google уже обошёл, перестают отдавать 404измереноэто видно покупателюfront#3657
До — прод сегодня404 сразу же, вообще без редиректа (hops=0). То же для CT-NHL-A14858 и CT-NHL-1014009C1. Каждый из этих адресов уже есть в индексе Google с прошлых обходов, и каждый — глухой тупик.
После — кандидат сегодняПереход 308 на /parts/CT-NHC-069526, который отвечает 200 (hops=1). Все три уже обойдённых адреса выводят на рабочую страницу, а не умирают.
Ссылки на схемы со страницы детали ведут прямо на страницу, а не через лишний переходизмереноfront#3658, search#905, search#906
До — прод сегодняСтраница выдаёт 6 ссылок на схемы, 6 из 6 в старом виде — с сырым кодом (например .../parts-diagrams/5FC8CAAF-B8BF-E111-9FCE-005056875BD6?highlight=21). Если по ней перейти, это 308 на .../parts-diagrams/75-200-10-spike-tooth-harrow и только потом 200 — каждая ссылка на схему стоит лишнего похода на сервер.
После — кандидат сегодняТе же 6 ссылок, 6 из 6 уже в читаемом виде (например .../parts-diagrams/75-200-10-spike-tooth-harrow?highlight=21). При переходе отвечает 200 с hops=0 — без лишнего скачка.
Поисковый сервис теперь отдаёт витрине собственный адрес каждой схемыизмереноsearch#905, search#906
До — прод сегодняВозвращается 6 схем, поле assemblySlug встречается 0 раз. Витрине было не из чего строить ссылку, кроме сырого кода — именно поэтому состояние «до» в строке 3 вообще существует.
После — кандидат сегодняТе же 6 схем, assemblySlug встречается 6 раз (например "assemblySlug":"75-200-10-spike-tooth-harrow"), а отдельное поле "slug" по-прежнему несёт slug модели, без изменений. Это та половина, которая всё включает — подтверждена вживую на search-api-dev, так что front#3658 есть откуда читать настоящие данные.
Номера деталей со слешем указывают на настоящую страницуизмереноэто видно покупателюfront#3659
До — прод сегодняСама страница открывается (200), но адрес, который она объявляет своим официальным, — https://crop.clintontractor.net/parts/CT-NHL-ESH6/L3/48 — слеш оставлен как есть и режет один номер детали на три куска пути. Если запросить этот объявленный адрес напрямую, он отвечает 404. То есть страница говорит Google, что её настоящий дом — битая ссылка.
После — кандидат сегодняТа же страница объявляет https://crop-dev.app/parts/CT-NHL-ESH6%2FL3%2F48 — слеш закодирован, адрес указывает на неё саму. Настоящий номер детали из прод PG (ESH6/L3/48, sellable, $192.91), а не выдуманный пример.
Разные версии одной машины получают разные заголовки страницизмереноэто видно покупателюfront#3660
До — прод сегодняДве из трёх версий POWERSTAR-90 отдают побайтово одинаковый заголовок: 'New Holland POWERSTAR-90 ...4 B (na) - 20 chapters | CROP' — и для страницы Mechanical, и для Power Shuttle. Слова, которые их различают, обрезаны; остался только общий хвост.
После — кандидат сегодняВсе три разные: '...POWERSTAR-90 Mechanical... - 20 chapters', '...POWERSTAR-90 Power... - 20 chapters', '...POWERSTAR-90 Dual... - 20 chapters'. 3 из 3 уникальны на той же команде.
Заголовок на странице схем машины говорит, какую версию вы смотритеизмереноэто видно покупателюfront#3660
До — прод сегодняОбе страницы версий показывают один и тот же заголовок: 'New Holland POWERSTAR-90'. Покупатель, попав на любую из них, не может по заголовку понять, схемы какой версии он смотрит.
После — кандидат сегодня'New Holland POWERSTAR-90 Mechanical Tractor - Tier 4 B (na)' и 'New Holland POWERSTAR-90 Power Shuttle Tractor - Tier 4 B (na)'. Теперь заголовок несёт то, что раньше стояло только в абзаце под ним.
Что этот раздел доказывает, а что нетВсе семь строк измерены вживую сегодня, 2026-08-20, прод против dev. Ничего здесь не взято из описания PR; каждое число вернулось из проверки, которую я запустил. ЛОВУШКА, В КОТОРУЮ ПОПАДЁТ ЛЮБОЙ, КТО ПОВТОРИТ ПРИМЕР ИЗ САМИХ PR. Главный пример в front#3657 — деталь 87438210. На dev эта деталь отвечает 404 в любом виде — в голом, с CT-NHL- и с CT-NHC-. Это НЕ поломка исправления по строительному дивизиону. У 87438210 publish_status='no_cnh_data', а более ПОЗДНЕЕ изменение на dev (front#3698, оно не входит в семь PR этой темы) снимает весь этот класс статусов на границе и намеренно отдаёт по нему 404 в ответ. На этой детали исправление по строительному дивизиону уже нельзя показать. Я перешёл на три строительные детали со статусом publish_status='sellable' — 069526, A14858, 1014009C1 — где действует только это исправление. Если кто-то на встрече вставит команду из самого PR и увидит 404, вот почему. DEV ОПЕРЕЖАЕТ РЕЛИЗНЫЙ ПОЕЗД. Сейчас crop-dev.app отдаёт деплой dpl_9iEJLZeLJ9JaWZtx4KVHmuemp8UZ, коммит ce2513b75 = сегодняшний origin/dev HEAD. Мерджи поезда легли 2026-08-19 (front#3661 как 0b5ea5ba, search#906) и являются его предками, проверено через git merge-base --is-ancestor. То есть на dev лежат семь PR этой темы плюс примерно шесть более поздних коммитов. Колонка «после» — это «что dev отвечает сегодня», а не «что делают эти семь PR сами по себе»; взаимодействие строительного дивизиона с no_cnh_data выше — единственное место, где это различие реально поменяло ответ. DEV КЭШИРУЕТ 404-е ответы. У ответов dev стоит cdn-cache-control s-maxage=300 — и переписывание на страницу «не найдено» тоже кэшируется (x-matched-path: /parts-not-found, x-vercel-cache: HIT). Самая первая моя проверка по строительной детали на dev вернула устаревший закэшированный 404 и дала бы неверную строку «без изменений». Именно поэтому каждая команда для dev выше добавляет ?cb=$RANDOM — сохраняйте это при повторе. СБОЙ ПОИСКА НА ПРОДЕ ЗДЕСЬ НИ ПРИ ЧЁМ. Ни одна из этих проверок не трогает /parts?q=, так что известная деградация CROP-search#924 (429 и медленный поиск) не может их испортить. Рядом снят контроль: прод /parts/CT-NHL-84071139 ответил 200 за 2.2s, и каждая проверенная здесь прод-страница детали и техники ответила 200 или настоящим 404, ни разу 500. СВЕРКА ЧИСЛА НОМЕРОВ СО СЛЕШЕМ для front#3659, на случай если цифру 2,545 поставят под сомнение. В public.parts_public всего 185 номеров деталей со слешем (86 публикуемых) — это только CNH. Одиннадцать схем брендовых каталогов добавляют ещё 2,524 номера. То есть 2,545 из PR сходится, если посчитать брендовые каталоги; не цитируйте 185, это только срез по CNH. gcloud в этой сессии не авторизован (токен истёк, нужен интерактивный вход Вовы). Ни одной из этих семи строк он не понадобился — факты по поисковому сервису получены прямыми HTTPS-проверками обоих сервисов Cloud Run, а факты по данным — из psql только на чтение к прод PG. Одну строку я намеренно не делал: кодирование <loc> в карте сайта, это тоже часть front#3659. Карта сайта по деталям разбита на части, и я не смог определить, в какой части лежит ESH6/L3/48 без широкого обхода, поэтому вместо этого я измерил канонический тег на странице детали — тот же дефект, то же исправление, одна команда. Если на встрече захотят, чтобы половина про карту сайта тоже была доказана, это отдельная последующая проверка, а не то, что я отчитываю как сделанное.
Что Google разрешено индексировать — 8 изменений, 3 из них заметны покупателю или Google
Снятые с продажи детали больше не отдают индексируемую страницу, а отвечают 404измереноэто видит покупательfront#3700
До — продакшен сегодняВсе три отвечают 200. CT-NHL-01111895 (нет цены, нет изображения): 200, 288,341 B, 2.19 s. CT-NHL-0145155 (нет данных CNH): 200, 338,531 B, 2.93 s. CT-NHL-100074 (контрольная — снятая с продажи деталь, у которой изображение есть): 200, 289,938 B, 2.34 s. Продакшен-база PG, запрос напрямую сегодня: у 75,048 строк нет данных CNH, ещё 332,254 сняты с продажи и не имеют ни цены, ни изображения — 407,302 URL такого вида.
После — кандидат сегодняДве мёртвые отвечают 404 объёмом 101,413 B за 0.51 s каждая — треть секунды вместо двух с половиной и треть байтов. Контрольная не тронута: 200, 285,347 B — те 13,626 снятых с продажи деталей, у которых всё ещё есть цена или изображение, свою страницу сохраняют.
Этот 404 больше не кешируется сутки, и вернувшаяся деталь снова доступна в течение пяти минутизмереноэто видит покупательfront#3710
До — продакшен сегодня404 на продакшене отдаётся с cdn-cache-control: public, s-maxage=3600, stale-while-revalidate=86400 и cache-control: s-maxage=31536000 — час свежий, потом ещё сутки отдаётся устаревшим. Доступность детали к покупке пересчитывается ночью, поэтому вернувшаяся деталь могла продолжать отвечать 404 до двадцати пяти часов.
После — кандидат сегодня404 на dev отдаётся с cdn-cache-control: public, s-maxage=300 без stale-while-revalidate и с cache-control: public, max-age=0, must-revalidate. Подтверждено и на реальной снятой с продажи детали: /parts/CT-NHL-20348S отвечает на dev 404 с s-maxage=300 и x-vercel-cache: HIT при age 184.
Мёртвая страница бренда или модели перестаёт выглядеть живой для человека — раньше 404 был только для Googleизмереноэто видит покупательfront#3621
До — продакшен сегодняНа продакшене все три мёртвых URL отвечают 200 браузеру и 404 для Googlebot. /brand/zzz-not-a-brand, /parts-diagrams/new-holland/ZZZ-NOT-A-MODEL и /parts-diagrams/mchale/ZZZ-NOT-A-MODEL/ZZZ-NOT-AN-ASM: chrome=200, googlebot=404 у каждого. Живая контрольная /brand/new-holland-agriculture отвечает 200 обоим.
После — кандидат сегодняНа dev все три отвечают 404 обоим. Живая контрольная по-прежнему отвечает 200 обоим. Статус больше не зависит от того, кто спрашивает.
Googlebot получает ту же закешированную страницу, что и покупатель, а не отдельный рендер каждый разизмереноfront#3568
До — продакшен сегодняНа продакшене Googlebot получает x-vercel-cache: BYPASS на /parts/CT-NHL-100016 — дважды подряд, 2.67 s и 4.68 s. Браузер, который в ту же минуту запрашивает тот же самый URL, получает x-vercel-cache: HIT. Каждый обход оплачивал свежий рендер, который никто не сохранял.
После — кандидат сегодняНа dev Googlebot получает x-vercel-cache: HIT — то же состояние кеша, что и браузер, на том же URL. Как выигрыш по времени это здесь не измеряется: dev — другой, более холодный деплой, поэтому секунды тут шум. Факт — это состояние кеша.
Параметры отображения в URL больше не размножаются по всем страницам спискаизмерено частичноfront#3577
До — продакшен сегодняПродакшен отдаёт три индексируемые ссылки, которые тащат параметры отображения дальше: /parts?view=counter&pageSize=100&page=2, &page=3 и &page=1000. Отдельно: /parts?view=grid отдаёт index, follow с canonical, указывающим на /parts, а /parts?pageSize=100 — такой же по сути параметр на той же странице — отдаёт noindex, follow вообще без canonical.
После — кандидат сегодняDev отдаёт /parts?page=2, /parts?page=3 и /parts?page=1000 — параметры отображения из ссылок исчезли. Ту половину изменения, которая про тег robots, на dev отделить нельзя: dev по настройке окружения отдаёт noindex, nofollow на каждой странице и выдаёт canonical даже на /parts?pageSize=100 — там, где правило продакшена его убирает. До пятницы чисто измеримы только ссылки.
Глубокие страницы списка бренда перестают выдавать себя за первую страницуизмерено частичноfront#3586
До — продакшен сегодняНа продакшене /parts/brand/new-holland-agriculture/type/balers?page=7 отвечает index, follow и называет своим canonical https://crop.clintontractor.net/parts/brand/new-holland-agriculture/type/balers — первую страницу, у которой нет ни одной детали с седьмой. /parts/brand/new-holland-agriculture?page=7 отвечает index, follow с заголовком New Holland Parts | Clinton Tractor — Page 7.
После — кандидат сегодняНа dev страница пресс-подборщиков называет своим canonical саму себя: .../type/balers?page=7. Заголовок бренда читается как New Holland Parts — Page 7 | Clinton Tractor: номер страницы перед названием магазина, как на /parts. Третью часть этого изменения — глубокие страницы бренда отвечают noindex после пятой — на dev прочитать нельзя, потому что dev в любом случае отвечает noindex на каждой странице.
Сокращаем индексируемую поверхность до реестра товаров и убираем дерево техники из индексанаблюдать нельзяfront#3653
До — продакшен сегодняИзмерено сегодня на продакшене. Sitemap заявляет 18,657 URL (16,279 + 2,378), и 16,250 из них — 87 процентов — это дерево техники по моделям и его хабы схем. Только 2,378 из них — товарные страницы. При этом /parts/CT-NHL-219758, которого в sitemap нет вообще, отвечает 200 с index, follow, и то же самое делают /equipment/attachments-rotary-broom-aad86496ef и его хаб /parts-diagrams. В продакшен-базе PG 579,518 строк деталей помечены как публикуемые — против тех самых 2,378 заявленных: тег robots и sitemap живут по двум разным правилам.
После — кандидат сегодняНа dev наблюдать нельзя, да и не было бы видно, даже если бы было можно. Три отдельные причины, каждая проверена: robots.txt на dev — это User-Agent: * / Disallow: /, и каждая страница по настройке окружения отвечает noindex, nofollow, так что ни один тег robots не отличить; /sitemap.xml, /sitemap-index.xml, /sitemap/0.xml, /sitemap/1.xml и /sitemap-images/0.xml на dev отвечают 404 — считать попросту нечего; и оба переключателя, которые добавляет этот PR, по замыслу поставляются выключенными, так что изменение бездействует, пока после выкатки в продакшене не выставят значения окружения. Цифра 2,407 URL в PR — это прогноз, а не измерение.
Эту пару сегодня наблюдать не удалось — причина в двух строках выше, и никакая цифра не выдумана, чтобы закрыть пробел.
Глубокая страница списка, которая просит Google сбавить темп обхода, должна отвечать «зайди позже», а не «сломано»наблюдать нельзяfront#3628
До — продакшен сегодняПродакшен отвечает 500 на обеих, без Retry-After и без заголовка noindex: /parts?page=500 за 17.0 s и /parts/brand/new-holland-agriculture?page=300 за 19.3 s. Именно повторяющиеся 500 говорят Google обходить сайт реже.
После — кандидат сегодняНа dev наблюдать нельзя. Dev отвечает 200 на обоих этих URL — 11.6 s и 14.9 s — пустым списком без единой ссылки на товар, потому что поисковый сервис на dev не отказывает глубокой странице так, как это делает продакшен. Изменение всего лишь переобозначает страницу, которая упала, а на dev ничего не падает — значит, и переобозначать нечего. Поиск на продакшене сегодня к тому же деградировал (CROP-search#924, выкатка стоит в очереди), а это значит, что даже на стороне продакшена я не могу отделить задуманный отказ на глубокой странице от этой аварии.
Эту пару сегодня наблюдать не удалось — причина в двух строках выше, и никакая цифра не выдумана, чтобы закрыть пробел.
Что этот раздел доказывает, а что нетВсе восемь PR влиты в dev. Два самых свежих (#3700 в 01:27Z и #3710 в 02:45Z сегодня) вживую видны на dev, так что сайт dev несёт все восемь — это проверено, а не предположено. Главное, что определяет всю эту тему: dev велит Google держаться от него подальше совсем. Его robots.txt — это «Disallow: /», каждая страница несёт noindex, nofollow, и sitemap нет ни по одному адресу. Это правильно и сделано намеренно — dev не должен индексироваться никогда — но из-за этого сам тег robots и оказывается тем единственным, что dev показать не может. Всё в этой теме, что сводится к тегу robots, читается на dev одинаково — работает изменение или нет. Что dev показать МОЖЕТ и на что опираются шесть измеренных строк: HTTP-статус, которым отвечает URL, заголовки ответа, теги canonical и title и ссылки, которые отдаёт страница. Они не зависят от окружения, и каждый из них считан живой пробой. Поэтому front#3653 — самое крупное из восьми изменений по количеству URL — единственная строка, у которой в колонке «после» ничего нет. Это тег robots и sitemap, а на dev нет ни того, ни другого. Зато его «до» полностью измерено на продакшене, и именно эту цифру стоит нести на встречу: sitemap заявляет 18,657 URL, 16,250 из них — страницы дерева техники, и только 2,378 — товарные страницы; при этом 579,518 строк деталей в продакшен-базе помечены как публикуемые, а страницы вне sitemap всё равно отвечают «индексируй меня». Эти две половины никогда не сходились. Две цифры здесь получены прямым запросом к продакшен-PostgreSQL, а не взяты из описания PR: 75,048 деталей без данных CNH плюс 332,254 снятых с продажи детали без цены и без изображения — в сумме те самые 407,302 URL из первой строки, и те 13,626 деталей, которые свою страницу сохраняют. Заявленное в front#3700 они воспроизводят независимо. gcloud в этой сессии не авторизован. Ни одной строке он не понадобился, так что из-за этого ничего не потеряно. Поиск на продакшене сегодня деградировал (CROP-search#924). Это касается ровно одной строки — той, что про глубокий список — и я так там и написал, вместо того чтобы тихо списать 500 на само изменение.
Товарный фид для Google — 7 изменений, 0 из которых заметны покупателю или Google
Сколько товаров может нести фид для Google и на сколько файлов он делитсяизмереноsearch#904
До — продакшен сегодня{"total":2681,"batchSize":5000,"boundaries":[]} — 2,681 товар проходит в фид, одним файлом. Всё ограничивал фильтр clinton_on_hand > 0, то есть рекламировать можно было только те детали, которые физически лежат на собственном складе Clinton.
После — кандидат сегодня{"total":97619,"batchSize":25000,"boundaries":["NHL|47886103","NHL|84071622","NHL|87419908"]} — 97,619 товаров в 4 файлах по 25,000. Это в 36 раз больше товаров и 4 источника данных в Merchant Center вместо 20.
Фотография товара, на которую фид указывает Google, действительно открываетсяизмереноsearch#904
До — продакшен сегодняПродакшен выдаёт URL фотографии без префикса бакета parts/ — https://media.clintontractor.net/ct/bns/1650347SM/gallery/1650347SM-1.jpg. Такой вид у 405 из 2,681 строк на проде, и 10 из 10 проверенных отдают 404.
После — кандидат сегодняDev выдаёт https://media.clintontractor.net/parts/ct/bns/1650347SM/gallery/1650347SM-1.jpg, и 10 из 10 проверенных отдают 200. Та же фотография, та же деталь: URL с прода отдаёт 404 там, где dev-URL рядом отдаёт 200 в том же прогоне.
Фотография товара в фиде достаточно большая для Google Shoppingизмерено частичноfront#3655, search#904
До — продакшен сегодняЖивой фид подставляет миниатюру из каталога CNH у 2,278 из 2,632 позиций (86%). Все 12 из 12 проверенных ровно 510x287 — 0 из 12 проходят порог Merchant Center 500x500 по размеру, то есть такие позиции будут отклонены. Аудит в ops оценил то же самое как 4 из 24.
После — кандидат сегодняСтроки merchant, которые читает новый фид, несут вместо неё фотографию из галереи: 12 из 12 проверенных >=500x500 (обычно 1024x1024 и одна 1612x1209), и у 25,000 из 25,000 строк фотография вообще есть. Честная оговорка: это измерено на источнике, который читает новый фид, а не на самом файле фида, потому что /feed/google/*.xml отдаёт 404 вне продакшена. Собственный g:image_link фида можно будет измерить только после пятничной выкатки.
Идентификаторы позиций, категория товара и доставка в самом файле фидане наблюдаемоfront#3655, front#3690
До — продакшен сегодняИзмерено на проде сегодня: 1 шард, 2,632 позиции (шарды 1-3 отдают 404). Критерий аудита 1 НЕ ПРОЙДЕН — 0 из 2,632 g:id несут канонический SKU вида CT-VENDOR-PN; там голые номера деталей вроде 1650347SM, то есть Merchant Center привяжет статистику по товарам к идентификатору, который мы вот-вот поменяем. g:google_product_category равен 888 (голый корень Vehicles & Parts) у всех 2,632 позиций; g:product_type — у 0 позиций; g:shipping — у 2,256 (85%), фиксированные 9.00 USD, которые противоречат правилу «бесплатно от $75»; g:description побайтово совпадает с заголовком у 2,306 (87%). Критерии 2 и 4 сегодня проходят: ссылки 40/40 отдают 200 без редиректа, совпадение 20/20 фид == JSON-LD карточки детали.
После — кандидат сегодняИЗМЕРИТЬ НЕ УДАЛОСЬ. /feed/google/0.xml и далее до 3.xml — все отдают 404 на crop-dev.app: вне продакшена маршрут намеренно закрывается по IS_INDEXING_ENABLED, так что 404 здесь — правильное поведение, а не поломка. Прогон аудита с --base https://crop-dev.app показывает 0 шардов / 0 позиций именно поэтому, а не потому, что фид пустой. Этот 404 — настоящий 404 и не стена входа Vercel: тот же заголовок обхода на /parts/CT-FER-1608395 отдаёт 200 при итоговом URL всё том же crop-dev.app. Первое наблюдаемое «после» — та же команда аудита, прогнанная по проду после пятничной выкатки.
Эту пару сегодня наблюдать не удалось — причина в двух строках выше, и никакого числа, чтобы закрыть пробел, не выдумано.
Что страница детали сообщает Google о наличии, цене и названии товараизмереноfront#3645
До — продакшен сегодняПрод. Из 12 проверенных страниц деталей 9 сообщают Google наличие, а 3 вообще не отдают availability внутри Offer — CT-AGC-000-1158, CT-AGC-000-1182, CT-FER-1608395. У CT-AGC-000-1158 в её же серверном HTML отрисован бейдж "Out of Stock", а Google при этом не сообщается вообще ничего. У CT-FER-1608395: name = "Spring-Extn 1.040ODX 04.000LG .105WIRE D... 1608395" (буквальное многоточие из-за обрезки в 70 символов, с приклеенным номером детали); description = "Genuine Ferris spring-extn, part 1608395. $13.99 at Clinton Tractor." (цена лежит внутри описания товара, поэтому устаревает); category = "Ferris" — это бренд, а не категория; itemCondition стоит на Product; seller — анонимная Organization; @id нет ни у одного узла.
После — кандидат сегодняDev. 12 из 12 проверенных страниц отдают availability. Те три, что прод пропускал, читаются как OutOfStock / OutOfStock / InStock, и у CT-AGC-000-1158 JSON-LD и отрисованный сервером бейдж теперь говорят одно и то же — оба Out of Stock. У CT-FER-1608395: name = "Spring-Extn 1.040ODX 04.000LG .105WIRE D" (без многоточия и без приклеенного номера детали); description = "Genuine Ferris spring-extn, part 1608395." — цена ушла; category не ошибочная, а просто отсутствует; itemCondition переехал в Offer; у seller есть @id .../#org, а у Product — @id .../#product, так что Google сводит всё к одному продавцу вместо анонимного.
Каждая цена, которую страница каталога показывает Google, действительно напечатана на страницеизмереноfront#3629
До — продакшен сегодняprod {"numberOfItems":40,"entries":40,"priced":40,"pricesVisibleInHtml":18,"entriesWithImage":0} — Google сообщают 40 цен, а найти в собственном HTML страницы можно только 18 из них (18 — это ровно бюджет немедленного рендера; остальные 22 карточки уходят заглушкой без цены). Ни одна из 40 записей не несёт картинку. Размеченный контент, которого не видно на странице, — это нарушение правил Google по структурированным данным, и применяют его сразу ко всему сайту, а не к отдельному URL.
После — кандидат сегодняdev {"numberOfItems":40,"entries":40,"priced":40,"pricesVisibleInHtml":40,"entriesWithImage":40} — все 40 размеченных цен присутствуют в HTML страницы вне скриптов, и все 40 записей несут картинку.
Страница со схемой деталей отдаётся один раз вместо двухизмереноfront#3630
До — продакшен сегодняprod: 516,737 байт, 64 ссылки /parts/ на 16 уникальных деталей — ссылка каждой детали отдаётся четыре раза, потому что всё поддерево спецификации рендерилось дважды (по разу на брейкпоинт), а каждая строка сверху складывала вторую копию ссылки на свой номер детали.
После — кандидат сегодняdev: 360,590 байт (-30.2%), 16 ссылок /parts/ на те же 16 уникальных деталей. Число уникальных ссылок с обеих сторон одинаково, то есть ни одна ссылка не потерялась — ушли только дубли. Выигрыш растёт вместе с числом строк в схеме; здесь страница на 16 строк.
Что этот раздел доказывает, а что нетКОНТРОЛЬ НА ИЗВЕСТНЫЙ СБОЙ ПОИСКА НА ПРОДЕ. Он воспроизводится точно и не задел ничего из того, что я измерял. На строке запроса, которая раньше нигде не использовалась: прод /parts?q=zqx7bromegear20260820 отдал 500 через 16.8s, dev отдал 200 через 8.1s. Но ни одна из семи строк выше не идёт через путь запроса. Поверхности, которые я действительно измерял, на проде в момент проверки были здоровы: /parts (без запроса) 200 за 1.43s, /parts/CT-FER-1608395 200 за 1.15s, /feed/google/0.xml 200 за 0.35s. Так что ни одна строка выше не искажена CROP-search#924.
АВТОРИЗАЦИЯ НА DEV БЫЛА ПРОВЕРЕНА, А НЕ ПРИНЯТА НА ВЕРУ. Каждая проверка на dev отправляла заголовок обхода, и я убедился, что %{url_effective} возвращается как crop-dev.app, а не vercel.com. Страница детали на dev под этим заголовком отдаёт 200 — именно это и делает 404 у фида на dev осмысленным: это маршрут закрывается по IS_INDEXING_ENABLED, а не стена входа. Значение секрета нигде не печаталось, не логировалось и не записывалось.
СТРОКА 4 — ТА САМАЯ, КОТОРУЮ ИЗМЕРИТЬ НЕЛЬЗЯ, И ОНА ЖЕ ГЛАВНОЕ ИЗМЕНЕНИЕ. Сам файл фида существует только в продакшене. То есть у самого крупного пункта этой темы — 2,632 позиции превращаются в ~97,578, голые номера деталей — в канонические SKU, появляется настоящая категория товара, исчезает призрачная доставка за $9 — есть полностью измеренное «до» и никакого наблюдаемого «после» до выкатки. Вместо этого я измерил, что мог, на шаг выше по цепочке (строки 1-3, на поисковом сервисе, который читает новый фид). Предлагаю на странице релиза напечатать «после» для строки 4 красным, как «измерить до выкатки не удалось», приложив команду для перезапуска, и чтобы в пятницу кто-нибудь заново прогнал bun run scripts/merchant-feed-audit.ts по проду и закрыл этот пункт. Эта одна команда оценивает все четыре критерия приёмки и завершается с ненулевым кодом при провале.
НИЧЕГО ИЗ ЭТОЙ ТЕМЫ НА САЙТЕ НЕ ВИДНО. Каждая строка обращена к машинам: к тому, что читает краулер Google и что забирает Merchant Center. В описании front#3629 прямо сказано «invisible to customers», а в front#3630 — что на экране ничего не меняется; сам я в браузере это отдельно не перепроверял. Отдача придёт позже — в том, как товары выглядят в результатах поиска и в Shopping, а не в чём-то, что покупатель увидит завтра. Об этом стоит прямо сказать на встрече, чтобы никто не ждал видимых изменений.
ОДНО УТВЕРЖДЕНИЕ ИЗ PR, КОТОРОЕ БОЛЬШЕ НЕ ВОСПРОИЗВОДИТСЯ. В таблице «до» из front#3629 записано, что ?pageSize=80 объявлял 80 позиций при 50 записях. Я проверил это сегодня: прод отвечает numberOfItems 50 при 50 записях, ровно как dev — дефект на проде больше не наблюдается, поэтому я не заявлял его как изменение. Ещё две мелкие заметки в том же духе: эндпоинт boundaries на проде сообщает 2,681 подходящую позицию, тогда как файл фида на проде несёт 2,632 позиции — разница это строки, которым шаг обогащения не может проставить ни цену, ни наличие; и в листинге ?pageSize=80 на dev картинку несут 48 из 50 записей, а не 50, и это задокументированное правило: строка без разрешимой картинки убирает свой узел Product, а не отдаёт заглушку.
front#3690 влили сегодня утром в 08:20Z, и он трогает только путь фида (пять дефектов с пустыми строками и URL-кодированием). Поскольку этот путь на dev отдаёт 404 в ответ, он целиком попадает в ненаблюдаемое окно строки 4 — нет проверки, которая отделила бы его от front#3655 до самой выкатки.
gcloud в этой сессии не авторизован, так что ничего здесь на нём не держится. Он и не понадобился: каждое число выше получено через публичную HTTP-поверхность — ту же самую, которую видит Google. Один факт со стороны прода, который я НЕ проверял и не утверждаю: забирает ли Merchant Center этот фид вообще прямо сейчас. Из документа по настройке в front#3655 следует, что конфигурация аккаунта должна произойти до первой выгрузки, — поэтому я пометил все строки как customer_visible=false, но это вывод из PR, а не измерение, и Джону или Вове стоит это подтвердить, прежде чем кто-то назовёт нынешний фид живым для покупателей.
Что осталось для тех, кто захочет перепроверить: снимок продового фида в /tmp/prod-feed-0.xml (2,632 позиции), дампы merchant-строк с прода и с dev в /tmp/prod-merch5k.json и /tmp/dev-merch.json, а также парсеры /tmp/pdp.js, /tmp/itemlist.js и /tmp/jsonld.mjs. Это временные файлы, никуда не закоммиченные.
Оформление заказа, деньги и доставка — 7 изменений, 4 из них заметны покупателю или Google
Покупателю, который вставляет в оформление заказа адрес в Нью-Мексико, больше не отвечают, что мы туда не возимизмереноэто видно покупателюfront#3642
До — продакшен сегодняorigin/main (то, из чего собран продакшен) отвечает: REJECT: International addresses are not supported. We currently ship to US only. Американская улица, названная в честь страны, ломается точно так же — '456 Mexico St, Springfield, IL 62701' тоже отклоняется. Если ввести 'NM' — работает; если вставить полный адрес — нет.
После — кандидат сегодняorigin/dev отвечает: OK state=NM. '456 Mexico St, Springfield, IL 62701' -> OK state=IL. Настоящий международный адрес по-прежнему отклоняется: 'Calle Reforma 100, Mexico City, DF 01000' -> REJECT: International addresses are not supported.
Подтверждение, что исправление по Нью-Мексико лежит в том JavaScript, который оба сайта реально отдают, а не только в веткеизмереноfront#3642
До — продакшен сегодняБандл, который отдаёт продакшен: replace(/\s+/g," ");if(t=s,[/\b(?:canada|uk|united — проверка на страну стоит ПЕРВЫМ выражением в разборе адреса, поэтому она срабатывает до того, как начнётся сам разбор, и 'New Mexico' попадает под слово 'mexico'.
После — кандидат сегодняБандл, который отдаёт dev: ngs:s})}(t,i,a);return l?l:[/\b(?:canada|uk|united — та же проверка на страну теперь выполняется только после того, как стратегии разбора ничего не вернули. Проверено, что запрос к dev остался на crop-dev.app (url_effective=https://crop-dev.app/checkout/shipping, http=200), а не ушёл на страницу входа Vercel.
Express Checkout (Apple Pay / Google Pay / Link) перед списанием заново сверяет цены с каталогомизмерено частичноэто видно покупателюps#615
До — продакшен сегодняorigin/main: 0 — маршрут Express Checkout не проверяет цену вообще. Живой payment-service последний раз выкатывали 2026-08-14 из main@be865716, значит именно это работает в продакшене сегодня: unitPrice, присланный самим браузером, уходит в Stripe без проверки, тогда как обычная страница оформления заказа (hosted Checkout) ровно ту же корзину отклоняет.
После — кандидат сегодняorigin/dev: 2 упоминания — та же защита, которой пользуется обычная страница оформления заказа. Dev-овский payment-service выкатили 2026-08-18 из dev@d626f353, и в нём исправление есть (проверено через git merge-base --is-ancestor). В рантайме намеренно НЕ проверяли: чтобы воспроизвести «до», пришлось бы подделать корзину и дёрнуть POST /payment-intent, а на продакшене это создаёт настоящий PaymentIntent в боевом режиме. Сознательно не стали.
Деньги, которые записываются в оплаченный заказ (налог, доставка, скидка, итог, цены строк), теперь закреплены тестамиизмерено частичноps#613
До — продакшен сегодняorigin/main: 0. Ни один тест в платёжном сервисе не смотрит, с чем вызывается orderRepo.create — все наборы тестов по вебхукам проверяли только сам факт вызова. Налог, доставку, скидку, итог, валюту и цены за единицу в каждой строке можно было переписать в любом оплаченном заказе, а сборка осталась бы зелёной.
После — кандидат сегодняorigin/dev: 1 набор тестов, 7 кейсов, 17 буквальных expect( в файле (в PR указан 21 вызов проверки, когда набор запускает bun) — покрыты оба пути записи заказа: обычная страница Checkout и Express Checkout. В этот заход я набор не запускал: ему нужен свежий worktree и полный bun install, поэтому строка измеряет код, который есть, а не зелёный прогон.
Ответ UPS без блока со стоимостью отбрасывается, а не превращается в бесплатный вариант доставкиизмерено частичноэто видно покупателюps#624
До — продакшен сегодняorigin/main, строка 188: cost: Number.parseFloat(total.MonetaryValue ?? '0') — тариф без цены превращается ровно в 0.00 и показывается покупателю как настоящий бесплатный вариант, который можно выбрать, а реальный счёт перевозчика остаётся на нас. Продакшеновский delivery-service выкатили 2026-08-14 из main@be865716, так что это живёт в продакшене сегодня.
После — кандидат сегодняorigin/dev, строка 178: Number.parseFloat(total.MonetaryValue ?? '') плюс отбрасывание с предупреждением, если значение не конечное число больше нуля; если отброшены все тарифы, расчёт падает, а не уходит в бесплатный. Dev-овский delivery-service выкатили 2026-08-18 из dev@38352042, и это И ЕСТЬ merge-коммит #624 — сам симптом снаружи не спровоцировать: только UPS решает, когда ответить без блока со стоимостью, — поэтому «до» вживую не воспроизводили, и вся гарантия держится на 9 новых тестах (6 из них падали до исправления).
Реальный расчёт доставки на адрес в Нью-Мексико по-прежнему даёт одинаковые цены по всем тарифам на обоих сайтахизмереноэто видно покупателюps#624
До — продакшен сегодняПродакшен возвращает 5 вариантов за 1.1s: Clinton Tractor In-Store Pickup $0, Clinton Tractor FREE shipping $0, UPS Ground $14.02, UPS 2nd Day Air $33.57, UPS Next Day Air $53.93.
После — кандидат сегодняDev возвращает те же 5 вариантов по тем же ценам за 1.5s. Это защита от перегиба: наши собственные два намеренных тарифа по $0.00 (самовывоз и бесплатная доставка) остались нетронутыми, потому что новая проверка живёт только внутри провайдера UPS, а все тарифы UPS по-прежнему возвращаются с ценой.
Какую сборку на самом деле крутит каждый сервис доставки (и что dev-овский этого сказать не может)измереноps#624
До — продакшен сегодняПродакшен отвечает ready=true gitSha=be865716b8f668d3245e4595b82d3377cd1e5133 — он называет собственную сборку, и этот коммит — HEAD origin/main от 2026-08-14.
После — кандидат сегодняDev отвечает ready=true gitSha=unknown. Dev-овский workflow деплоя вообще не передаёт сборочный аргумент GIT_SHA (его передаёт только delivery-deploy.yml, в строке 103), поэтому dev-сервис не может назвать себя, и его сборку приходится брать из записи о деплое в GitHub Actions. Стоит починить: проверка в день выкатки, идущая по dev, не может сама подтвердить, что именно она только что проверила.
Что этот раздел доказывает, а что нетВсе четыре изменения сегодня живут только на dev. Оба продакшен-сервиса из этой темы (платежи и доставка) последний раз выкатывали 2026-08-14 из main@be865716 — они отстают на шесть дней, — а бандл продакшен-витрины по-прежнему несёт старый, доисправленный разбор адреса. Все четыре сдвинет завтрашняя выкатка. Два из четырёх сценариев отказа снаружи не спровоцировать, и я их не подделывал. Чтобы воспроизвести подмену цены в Express Checkout (ps#615), пришлось бы создать на продакшене настоящий PaymentIntent в боевом режиме; тариф UPS за $0.00 (ps#624) требует, чтобы UPS ответил без блока со стоимостью, а это не в нашей власти. В обоих случаях я измерил, какой код крутится в каждой среде, — это подтверждено родословной коммита относительно выкаченного образа — плюс тесты, которые добавили эти PR. Три вещи, по которым на встрече надо именно принять решение, а не просто отметить: 1. ps#615 меняет поведение для покупателей в Express Checkout так, что они это почувствуют: если цена в каталоге ВЫРОСЛА больше чем на 5% после того, как корзина была записана, заказ теперь отклоняется с «Price mismatch». Это устаревшая корзина, а не подмена. Обычная страница оформления заказа всегда вела себя именно так, и Express теперь ей соответствует — но сегодня такие покупатели проходят насквозь и платят старую, более низкую цену. После пятницы их остановят, и корзину придётся перезагрузить. Починить это только на одном пути — значит вернуть ту самую асимметрию, которую закрывает PR, так что это одно продуктовое решение сразу на оба. 2. ps#624 падает в закрытую: любая стоимость от UPS, равная нулю или меньше, считается непригодной, так что по-настоящему бесплатный тариф UPS, если такой когда-нибудь существовал, был бы отброшен, а не показан. Автор PR отметил, что это решение можно и отменить. 3. ps#613 закрепляет число, которое никто не выбирал: итог по строке хранится без округления, поэтому 7 x $8.15 записывается как 57.050000000000004. Новый набор тестов проверяет это намеренно — чтобы зафиксировать сегодняшнее поведение, а не спрятать его. Для покупателя симптома нет: списываемый итог приходит из Stripe, а не из суммы строк, — но это реальное свойство записей о заказах, и автор попросил вынести его в отдельное изменение. Заметки по методу для тех, кто будет это перезапускать: gcloud в этой сессии не аутентифицирован, поэтому ревизии Cloud Run и дайджесты образов НЕДОСТУПНЫ — то, какая сборка выкачена, взято из собственного /ready/deep сервисов и из записей о деплое в GitHub Actions. Деградация поиска на продакшене (search#924) ни одну проверку здесь не задела: /checkout/shipping на продакшене ответил 200 и сервис доставки дёргали напрямую, а не через витрину. Dev-овским командам нужен заголовок обхода Vercel — он берётся из ops/.env.local и никогда не печатается; отдельно подтверждено, что запрос к dev заканчивается на crop-dev.app, а не на странице входа Vercel. Одна ловушка bash, стоившая мне двух прогонов: origin/$b:lib/... коверкает двоеточие (оно раскрывается в "origin/mainib/..."); пишите вместо этого origin/${b}:lib/... .
Всё, что вошло в этот релиз — 96 изменений, и у каждого есть краткое описание в одну строку, записанное прямо в самом изменении
Сайт магазина62 изменения 17 из них заметны покупателю или Google
| Изменение | Что это | Было и стало | Сделал |
|---|---|---|---|
| front#3715 | Сборка отправляла по одному запросу на каждый бренд одновременно в поисковый сервис, у которого всего двадцать слотов; шаг с картинками для соцсетей теперь переиспользует ограничитель из карты сайта — не больше двух запросов в полёте. | ~10 одновременных запросов таксономии брендов -> не больше 2 одновременно; один общий помощник вместо своей копии; красную сборку это НЕ чинит, её причина внешняя (CROP-search#874) | Вова |
| front#3712 | Когда поисковому сервису тяжело, страница детали отдавала жёсткую ошибку; теперь она показывает ту же страницу «временно недоступно, попробуйте чуть позже», которую уже показывали страницы схем. | кратковременные 429/5xx/таймаут: жёсткий 500 (замеры 34.9s / 27.1s на /parts/CT-NHL-100016) -> тело ServiceUnavailable, переразмечено в 503 + Retry-After: 300 для краулеров | Вова |
| front#3711 | Тест, прерванный во время ввода текста, продолжал печатать уже в следующий тест, и посторонние тесты падали на искажённом вводе; теперь набор падает и называет настоящего виновника. | тихая утечка нажатий между тестами -> afterEach роняет тест, оставивший работать цикл userEvent; 5 новых защитных тестов, набор 1002 файла / 10906 тестов зелёный | Вова |
| front#3710 | Ответ «не найдено» для снятой детали кешировался до суток, и деталь, вернувшаяся за ночь, всё равно выглядела отсутствующей; теперь этот кеш ограничен пятью минутами. | TTL кеша CDN для 404 на границе: s-maxage=3600 + stale-while-revalidate=86400 -> s-maxage=300 в обоих заголовках Vercel-CDN-Cache-Control и CDN-Cache-Control | Вова |
| front#3706 | Тесты адреса при оформлении заказа печатали так медленно, что выходили за лимит времени и портили следующий тест, окрашивая в красное посторонние изменения; для этого файла задержка ввода убрана. | 3-5 падений в unit-tests-shards (1) -> 9/9 проходят, время файла 2.28/2.09/2.08s -> 1.27/1.26/1.23s (~41% быстрее) | Вова |
| front#3703 | Наша собственная проверка здоровья повторяла всё, что ответил поисковый сервис, поэтому когда поиск начинал ограничивать запросы, каждый прогон тестов объявлял приложение мёртвым; теперь приложение и поиск отчитываются отдельно. | /api/health проксировал 429/503 от бэкенда -> всегда 200 плюс X-Upstream-Status; проверка запуска больше не падает, пока Next рапортует готовность за 197ms | Вова |
| front#3701 | Проверка времени падала всякий раз, когда общая машина сборки была занята, и блокировала постороннее изменение; теперь берётся самый быстрый из пяти прогонов, а не один замер. | один замер (6.13ms на загруженном шарде, ~0.05ms на свободном) -> лучший из пяти против того же порога 5ms; настоящий полный обход по-прежнему краснеет на 9.81ms | Вова |
| front#3700 | 407,302 страницы деталей без цены, картинки и данных каталога всё равно открывались как настоящие страницы; теперь они отдают честное «не найдено» и больше не появляются ссылками. | снятая когорта (все no_cnh_data плюс dont_list без цены и без картинки): 200, 286 KB, 11.2s -> на границе 404, ~104 KB, меньше секунды; 13,626 строк dont_list с содержимым остаются 200 | Вова |
| front#3695 | Когда поисковый сервис переполнен, он отклоняет запросы; повторные попытки съедали 10.7s из 16.7s неудачной загрузки страницы, поэтому живой трафик теперь падает сразу, а повторяют только сборки. | 429 в рантайме: 2 попытки / ~10.7s из 16.7s отказа -> 1 попытка, без повторов; повтор при 429 на этапе сборки сохранён | Вова |
| front#3694 | Две проверки дизайн-системы печатали четыре тысячи замечаний и «PASSED» на каждый коммит, поэтому их никто не читал; большинство замечаний были багами самих проверок — их починили и вынесли в один общий модуль. | замечаний о захардкоженных цветах 3,779 -> 498 и 225 -> 66; убранные 3,281 — строгое подмножество, 0 новых замечаний; ни одно правило не ослаблено, ни один файл интерфейса не тронут | Вова |
| front#3692 | Тестовый поисковый сервис переименовали, убрав из названия имя фреймворка, поэтому проверку и две тестовые фикстуры, всё ещё смотревшие на старый адрес, обновили до того, как он исчезнет. | search-nest -> search-api-dev в SEARCH_SERVICE_URL проверки brand-pdp-gate и в двух тестовых фикстурах прокси (3 строки) | Вова |
| front#3691 | Каждая выкатка витрины на тестовый сайт падала на одном и том же шаге, когда поисковый сервис ненадолго ограничивал запросы; теперь сборка повторяет попытки достаточно долго, чтобы пройти. | повтор на этапе сборки 2 попытки / 50ms -> 4 попытки / ~500-1500-4500ms; тестовый сайт отставал на пять влитых изменений, путь в рантайме намеренно не тронут | Вова |
| front#3690 | Пять способов, которыми пустое значение или «#» в номере детали могли выбросить или испортить позицию в фиде Google, починили до того, как фид пошёл в работу. | 5 дефектов краснели на origin/dev -> здесь зелено (5 failed | 1 passed -> 6 passed); исполняемых строк в offer-mapping.ts 309 -> 309 после сжатия комментариев | Вова |
| front#3689 | Две ночные задачи вызывали инструменты, которых на их машинах никогда не было: одна умирала за 90 секунд, вторая вообще не могла отправить своё оповещение; обе теперь используют то, что реально установлено. | e2e-stage gh api -> bun -e (в пуле crop нет gh); проверка карты сайта получает actions/checkout, что прекращает её эскалации с кодом выхода 127 | Вова |
| front#3688 | Три ночные проверки сайта обвиняли сайт в недоступности, хотя это была всего лишь страница входа для превью; теперь сообщение называет защиту Vercel и ветку, которую надо проверить. | 302/401/403 читались как «цель недоступна» -> называются защитой развёртывания Vercel, плюс страховка, закрепляющая заголовок обхода во всех трёх проверках | Вова |
| front#3684 | Во время сбоя поиска гараж сообщал покупателям, что их сохранённые детали сняты с производства, и предлагал удалить их одним кликом; теперь «пропала» означает только настоящее «не найдено». | любая неудача запроса отвечала success: true с пустым списком -> всё, кроме 404 — это отказ, и показывается уже существующее состояние «Could not load your saved parts»; 3 из 7 случаев краснели до починки -> 7 passed | Денис |
| front#3678 | Ночная проверка карты сайта отваливалась по таймауту на спящем поисковом сервисе и рапортовала это как обвал каталога; теперь она повторяет попытки и правильно называет недоступный сервис. | --max-time 60 без повторов -> 120s с --retry 3 --retry-all-errors, против измеренного холодного старта в 85s; сервис всё это время держал 2736 деталей | Вова |
| front#3674 | Две быстрые задачи, которые ничего не собирают, занимали дефицитные Linux-машины сборки и подвешивали за собой целые конвейеры; теперь они идут на простаивающем локальном пуле Mac. | changes + seo-smoke на Linux-пуле crop -> vars.CI_LIGHT_RUNNER; замерено 7 простаивающих Mac-раннеров рядом с 11/11 занятыми Linux-раннерами и 7 задачами в очереди | Вова |
| front#3671 | Две тестовые линии на stage засчитывали страницу входа Vercel за живой сайт, а потом ждали 40 минут сборку, которая уже упала; теперь обе останавливаются и говорят, что именно случилось. | выход при мёртвой сборке 2400s -> 1s; предварительная проверка теперь шлёт заголовок обхода и отвергает посадку на vercel.com | Вова |
| front#3670 | Сегодня ничего не сломано, но следующее автоматическое обновление зависимостей затянуло бы библиотеку для работы с базой, из-за которой четыре наших скрипта дозаливки данных падают сразу при старте. | bson не закреплён (разрешался в 7.2.0) -> "bson": "7.2.0" в overrides; 2 строки, установка без изменений — 922 установки на 985 пакетов | Вова |
| front#3669 | Отложенный тест был помечен как проваливающий свои проверки, и одно расследование ушло искать несуществующий баг; теперь в записи указана настоящая причина — таймаут. | причина "slug assertions fail (4 of 8 cases)" -> выход за бюджет 30s на 100 последовательных запросов к живому проду, во всех 14 опрошенных прогонах | Вова |
| front#3661 | Собирает пять исправлений из августовского разбора обхода в одну проверенную партию: мёртвые URL деталей строительной техники, незакодированные слеши, дублирующиеся заголовки машин и ссылки на схемы, уходящие через редирект. | 5 дочерних PR, 68 файлов, +2386/-823; CI 19/19 зелёный, аудит fallow — 0 новых замечаний | Вова |
| front#3660 | Разные версии одной машины делили один заголовок страницы и один заголовок на ней, поэтому их нельзя было отличить друг от друга; теперь у большинства есть слова, которые их разделяют, и ни одна не стала хуже. | дублирующихся заголовков 872 -> 526 (parts-diagrams) и 1,111 -> 620 (equipment), 0 ухудшений | Вова |
| front#3659 | Номера деталей со слешем указывали на битый адрес в canonical-тегах, картах сайта и ссылках на схемы; теперь слеш везде кодируется процентом. | /parts/CT-BRL-1-1/2 -> /parts/CT-BRL-1-1%2F2 во всех пяти местах генерации; слеш есть у 2,545 публикуемых строк | Вова |
| front#3658 | Ссылки на схемы со страницы детали использовали адрес старого вида, который проходит через редирект; все шесть построителей ссылок теперь берут прямой, когда сервис его даёт. | на выборочной странице детали 6 из 6 ссылок были в перенаправляющей форме с кодом -> все шесть мест генерации предпочитают assemblies.slug | Вова |
| front#3657 | Детали строительного подразделения упирались в тупик: простой URL с номером детали перенаправлял на адрес, который отвечает 404; теперь такие URL ведут на рабочий адрес, который сервис уже называет. | /parts/87438210: 308 -> CT-NHL- -> 404 превращается в один 308 -> CT-NHC- -> 200; проверено 11 живых строк, URL подразделения AG не сдвинулись | Вова |
| front#3656 | Наш чек-лист открытия бренда для Google ни разу не спрашивал, работают ли вообще URL этого бренда; теперь его закрывают пять исполняемых проверок, а таблица флагов переписана по тому, что показывает живой замер на проде. | пунктов в чек-листе 6 -> 11, таблица флагов исправлена по живому замеру на проде (2026-08-18) | Вова |
| front#3655 | Наш фид Google Shopping перечислял всего 2,635 деталей, потому что требовал наличия на нашем собственном складе; теперь он предлагает 97,578 с правильными идентификаторами, картинками и категориями. | 2,635 позиций в 1 шарде -> 97,578 в 4 шардах по 25,000; захардкоженная категория 888 -> сопоставленная (17 из 26 категорий до листа); превью 510x287 -> картинка из галереи >=500x500; постоянная доставка 9.00 USD убрана | Вова |
| front#3653 | Наши страницы приглашали Google проиндексировать 352,201 товар, тогда как карта сайта предлагала всего 2,378; переключатель сокращает приглашение до ~2,407 подготовленных страниц, и все они остаются живыми. | за двумя флагами окружения, выключено на момент слияния: 352,201 индексируемая страница детали + 8,126 страниц техники + 8,124 хаба схем -> ~2,407 URL, тег robots и карта сайта по одному правилу реестра | Вова |
| front#3652 | Зависимости витрины сильно отстали от выпущенных версий; 34 подтянуты до актуальных внутри своих мажорных версий, а фреймворк и линтер оставлены на отдельный разбор. | 52 из 85 объявленных зависимостей отставали от последних -> 34 обновлены; набор юнит-тестов без изменений — 992 файла / 10693 теста, пять почтовых снапшотов теряют подсказку preload | Вова |
| front#3645 | Страницы деталей сообщали Google обрезанное название, бренд вместо категории и цену без состояния наличия; теперь разметка и хлебные крошки совпадают со страницей. | наличие в Offer отсутствовало у 227,461 детали с ценой (44% из 512,404) -> указывается всегда, с падением до OutOfStock; Product.name не совпадал с h1 на 47 из 52 проверенных страниц деталей -> теперь ровно тот же h1 без многоточия | Вова |
| front#3643 | Короткий сбой поискового сервиса раньше убивал всю сборку сайта; теперь вместо этого просто тихо пропадают блок популярных деталей на главной и список узлов у детали. | один кратковременный таймаут /api/parts/popular: next build код выхода 1 -> код 0 с отсутствующим блоком, урезанная запись кешируется 5s, а не 24h | Вова |
| front#3642 | Адреса штата Нью-Мексико отклонялись как международные, потому что «Mexico» находилось внутри названия; проверка страны теперь идёт последней, поэтому настоящий штат США с индексом всегда побеждает. | 6 из 306 сгенерированных адресов отклонялись, все — Нью-Мексико -> 0; недостижимая стратегия разбора удалена; 16 тестов -> 22 | Вова |
| front#3636 | Задачи браузерных тестов постоянно боролись с машиной за блокировку пакетного менеджера и умирали на старте; безопасная настройка теперь стоит по умолчанию, а не флагом, который каждая задача должна помнить. | 6 из 15 браузерных задач наследовали умолчание apt-get и гонялись за блокировкой (код выхода 100) -> умолчание переключено на "false", 9 лишних флагов и их продублированные комментарии удалены (9 добавленных строк, 43 удалённых) | Вова |
| front#3630 | Страницы деталей узла отправляли один и тот же список деталей четыре раза за одну загрузку, раздувая страницу для краулеров и телефонов; теперь он отправляется один раз. | страница узла 516,789 -> 362,021 байт (-29.9%), тело без скриптов 392,507 -> 240,899 (-38.6%), ссылок /parts/ 64 -> 16 при тех же 16 уникальных деталях | Вова |
| front#3629 | Google сообщалось 39 цен для одной страницы списка, хотя на самой странице их было только 16; теперь каждая заявленная цена видна, а у каждой предложенной строки есть картинка. | цен, видимых в HTML без скриптов, 16 -> 39 из 39 предложений, записей с картинкой 0 -> 40 из 40, ?pageSize=80 объявлял 80 позиций из 50 -> 50 из 50 | Вова |
| front#3628 | Глубокие страницы списка, которые поисковый сервис отклоняет, сообщались Google как ошибка сервера; теперь краулер на этих маршрутах получает 503 «попробуйте позже» на любую неудачную отрисовку. | /parts?page=184 по ?page=1000 (817 URL) для краулера: 500 -> 503 с Retry-After: 300 и X-Robots-Tag: noindex; путь для человека не тронут | Вова |
| front#3627 | Оповещение о том, что у шести брендов в живом магазине нет контента, было сформулировано как упавшая сборка, и читатели уходили искать сломанный конвейер; теперь оно называет настоящую находку. | дважды в день "CI gate FAILED: brand content gate on prod - 6 brand(s)" -> "Brand content missing: prod - 6 brand(s)", при этом остальные четыре вызова скрипта оповещений не изменились ни на байт | Вова |
| front#3626 | Пять непокрытых файлов на пути доставки и заказа сначала закрепили новыми тестами, а потом 12 функций ужали настолько, что их 16 отговорок про сложность удалось удалить совсем. | 27 -> 11 подавлений проверки сложности, ни одного добавленного; 5 новых тестовых файлов, набор юнит-тестов 10,565 -> 10,684 теста; отрисованный DOM побайтово совпадает с dev на обоих изменённых публичных маршрутах | Вова |
| front#3625 | Пять ночных тестовых линий днями стояли красными по причинам, которые никто не проверял; каждую причину замерили и починили, так что красный ночной прогон снова что-то значит. | e2e-deep красный в 4 прогонах из 4 подряд, e2e-mobile в 3 из 3, проверка карты сайта в 3 из 4 при расхождении 13% -> гонка за блокировкой apt обойдена (playwright-with-deps: false), 4 мобильных спека в карантине, карта сайта ожидала 2736 - 358 придержанных = 2378 против 2378 живых (расхождение 0%) | Вова |
| front#3621 | Мёртвые URL брендов и схем отвечали браузерам так, будто страница существует, и только Googlebot получал 404; теперь граница отдаёт настоящий 404 всем. | промахи /brand/{slug} и /parts-diagrams/{brand}/{model}[/{assembly}]: браузер 200 / бот 404 -> 404 для обоих UA на 5 замеренных мёртвых путях, 5 живых контрольных не изменились | Вова |
| front#3618 | Аудит хлебных крошек был направлен на URL, который уводит редиректом, поэтому шаблон вариантов техники получал зелёный, ни разу не будучи загруженным. Перенаправлен на модель, которая его действительно отрисовывает. | 1 ложное нарушение -> 0, шаблон теперь покрыт по-настоящему | Вова |
| front#3616 | Восемь функций в шести файлах несли пометки, оправдывающие их сложность; их разбивали, пока пометки не стали ненужными, ни один тестовый файл не менялся, а разбор адресов проверили случай за случаем. | 38 -> 27 подавлений проверки сложности, ни одного добавленного; сравнительный стенд разбора адресов cases=201 mismatches=0; локальный Playwright теперь стартует на том порту, который называет baseURL | Вова |
| front#3614 | Подавления линтера прятали настоящие дефекты, включая элемент управления в списке деталей, до которого нельзя было добраться с клавиатуры; 25 сняты починкой кода, а ложные обоснования у остальных исправлены. | 199 -> 174 подавления (noExplicitAny 7 -> 0, noSvgWithoutTitle 5 -> 0); +210 характеризующих тестов на модель представления страницы детали; элемент управления строкой «все детали» теперь настоящая кнопка | Вова |
| front#3613 не выпущено | Закрыт без слияния — админ-страница, сверяющая 38 обещаний, данных покупателю, с тем, что реально собирают прогоны тестов, взамен ручного реестра; брошена, когда её автор ушёл из команды. | не влит; +3,454 строки в 16 файлах; замерено: 20 из 38 обещаний доказаны до слияния, 12 только в ночных прогонах, 1 полностью пропущен, 5 без покрытия; строк в юнит-тестах 72.50% | Денис |
| front#3610 | Каждый pull request блокировала проверка ссылок, объявлявшая медленные страницы битыми; страница, вышедшая за таймаут, теперь получает более долгую повторную попытку, прежде чем считаться мёртвой. | 3 ложные мёртвые ссылки (все с status: 0) на каждом PR -> страница по таймауту ждёт 3s и повторяет с утроенным бюджетом в 15s | Вова |
| front#3608 | Внутренняя ручка метрик принимала запись без аутентификации в счётчики, которые всегда были нулевыми; эти обработчики удалены, а девять копий одного блока проверки сотрудника стали одним общим помощником. | 89 замечаний аудита -> 13 применены; итого -140 строк в 19 файлах (из них -52 — маршрут метрик), POST/DELETE без аутентификации на /api/internal/metrics убраны, одно подавление проверки сложности удалено, а не переформулировано | Вова |
| front#3606 | Две тестовые задачи на одной машине делили порт 3000, поэтому одна тестировала сборку другой; теперь у каждой задачи свой порт, а лог её сервера сохраняется. | задачи, поднимающие приложение и пересёкшиеся с соседней, падали в 20 случаях из 41 (49%) против 1 из 14 в одиночку (7%) на 25 прогонах -> порт на каждый раннер, мёртвый сервер краснеет за секунду, и артефакт с логом приложения при каждом падении | Вова |
| front#3605 | После отключённых функций осталось 24 модуля, которые ничто не запускало; 11 были не видны проверке мёртвого кода, потому что собственный тест каждого модуля делал его на вид живым, и их удаление вскрыло остальные 13. | удалены 24 модуля + 14 тестовых файлов + 8 никем не читаемых переменных окружения, +14/-6,086 строк в 42 файлах; dead:check пуст -> пуст и после каскада | Вова |
| front#3602 | Более ранний перенос файлов оставил проверку названий брендов смотрящей в удалённую папку, поэтому каждая сборка на общей ветке падала; теперь проверка смотрит туда, где эти файлы действительно лежат. | dev красный с 4 блокирующими замечаниями и шестью красными задачами -> glob списка разрешений перенаправлен с lib/parts-schematics/ на lib/brands/, содержимое то же, ничего нового не заглушено | Вова |
| front#3600 | От отключённого продукта осталось четыре живых боевых веб-маршрута, к которым никто не обращался: один открывал доступ к бакетам хранилища, другой держал сервисный ключ; все четыре теперь удалены. | убраны 4 мёртвых API-маршрута и 5 неиспользуемых переменных окружения, -756/+49 в 33 файлах; dead:check 1 замечание -> пустой отчёт | Вова |
| front#3596 | Ничто не мешало длинным блокам комментариев накапливаться в коде; теперь правка файла, в котором есть блок комментариев длиннее восьми строк, роняет проверку сборки. | проверки длины комментариев не было -> задача линтера краснит любой изменённый файл с блоком длиннее 8 строк; 1331 старый блок в 892 файлах из 2715 остаются зелёными, пока их не тронут | Вова |
| front#3592 | Фильтр, который опустошил бы сетку, оставался кликабельным; переключатели «отгрузка в тот же день» и «с фото» теперь отключаются при замеренном нуле, а при неизвестном количестве остаются доступными. | в 6 из 45 пар категория x бренд детали были, но с отгрузкой в тот же день — 0 -> там эти переключатели отключаются; демо для ревью отключает их везде, где список сужен другим фильтром | Вова |
| front#3590 | Два конкурирующих элемента выбора категории расходились между собой, и каждый клик по категории тащил старые параметры сортировки и вида на страницу, которую мы просим Google выбросить; теперь один элемент собирает чистый URL. | ?view=counter&pageSize=100&categoryId=FASTENERS (noindex, без canonical) -> ?categoryId=FASTENERS (index, canonical на себя), итого -919 строк | Вова |
| front#3586 | Списки брендов предлагали Google все 1,000 страниц, а 509 страниц типов утверждали, что их детали лежат на первой странице; теперь глубокие страницы скрыты и каждая страница указывает на саму себя. | списки брендов и типов брендов дальше страницы 5: index,follow -> noindex,follow; canonical хаба типов с голого пути -> на себя ?page=N (пресс-подборщики NH: 20,388 деталей на 510 страницах) | Вова |
| front#3577 | Настройка вида, влияющая только на отображение, превращала каждый номер страницы в новый URL, который Google мог индексировать; теперь такие варианты помечены как неиндексируемые и убраны из ссылок пагинации. | списки с ?view=: index,follow + canonical на /parts -> noindex,follow, canonical на чужой URL убран; ссылки пагинации перестают выдавать ?view=counter&pageSize=100&page=1000 | Вова |
| front#3568 | Краулер Google загонялся в медленную сборку страницы, которую кеш никогда не сохранял, и это ограничивало скорость индексации сайта; теперь он получает ту же закешированную страницу, что и покупатели. | Googlebot: некешированный BYPASS 0.87-4.52s -> CDN HIT с теми же байтами, что получает браузер (браузерный HIT 0.20-1.40s); переопределение htmlLimitedBots и его сценарий отслеживания расхождений удалены | Вова |
| front#3565 | Деталь без цены всё равно показывала зелёную плашку In Stock и обещание отгрузки в тот же день над кнопкой «Ask for a price»; теперь там написано Out of Stock. | 4 из 42 ячеек матрицы падали на старом поведении -> 42 passed; плашка, строка отгрузки и кнопка теперь выводятся из одного решения, которому продаваемость передаётся обязательным входом | Денис |
| front#3562 | У кнопки, добавляющей в корзину всю схему целиком, не было теста, доказывающего, что она пропускает детали, которые мы не можем отгрузить; теперь это поведение закреплено пятью случаями. | теста на саму сборку кнопки не было -> 5 случаев, 20 passed / 0 failed (2 краснеют, если намеренно сломать snapshotToDisplayState) | Денис |
| front#3483 не выпущено | Закрыт без слияния — тестовая линия, измеряющая, может ли покупатель найти детали, загруженные от поставщика; была оставлена красной в ожидании списка поставщиков, а потом брошена, когда её автор ушёл. | не влит; +246 строк в 3 новых файлах; фикстура закоммичена пустой, поэтому набор падает по замыслу; классификатор совпал с ручным замером на 3 поставщиках | Денис |
| front#3350 не выпущено | Закрыт без слияния — черновик доработок админского сборщика фотографий, включая поиск картинок для моделей New Holland Construction, которых инструмент не видел; брошен, когда его автор ушёл из команды. | не влит; 8 коммитов, 79 файлов от устаревшей точки ответвления (в описании сказано, что настоящая разница с dev — 29 файлов); вершина ветки записана на случай возобновления | Денис |
| front#3244 | Карта сайта и сама страница решали вопрос индексируемости по двум разным правилам; более строгое правило, которое их согласует, теперь стоит по умолчанию в коде, а не в настройке панели. | PDP_INDEXING_MODE по умолчанию legacy -> strict, обе поверхности на одном предикате реестра; отмечен побочный эффект: безусловная задержка для брендов-адаптеров (Ferris, McHale) перестаёт действовать, поэтому такие страницы деталей индексируются, когда так говорит реестр | Вова |
Поисковый сервис21 изменение 7 из них заметит покупатель или Google
| Изменение | Что это | До и после | Кто сделал |
|---|---|---|---|
| search#925 | Списки схем для самых востребованных машин перебирали 62,153 записи, чтобы заполнить одну страницу из 50 строк; теперь запрос начинается от модели, а строки с одинаковым порядком перестали перескакивать между страницами. | самая востребованная модель: 377.6ms -> 35.5ms, парный подсчёт 23.2ms -> 0.6ms, буферы 355,529 -> 16,691; 14/14 результатов совпадают по md5 | Vova |
| search#923 | Цикл прогрева сдавался через 25 секунд, а холодный просмотр каталога занимает 33, поэтому проверка перед выкаткой всё равно считала эту страницу холодной; теперь попыток меньше, но каждая дольше. | цикл прогрева 6 x 25s, ни разу не дожидался просмотра длиной 32.92s и тратил 175s на адрес -> 3 x 45s, 145s в худшем случае | Vova |
| search#922 | Открытие схем деталей машины Kuhn пересчитывало все 15.5 миллиона строк деталей ради нескольких сотен номеров; теперь считаются только показанные схемы. | модель 2273: 16,132ms -> 109ms (-99.3%), буферы 3,140,327 -> 8,908 (-99.7%); результат совпадает по md5 на четырёх моделях | Vova |
| search#920 | Три самых нагруженных адреса каталога — около трёх четвертей всего трафика — ходили в базу на каждый запрос и держали её на полной загрузке процессора; теперь они кэшируются. | разделы / узлы модели / карточка узла: 761 из ~1000 запросов в час (~76%) шли напрямую в PG -> отдаются из кэша результатов; /api/assembly-search оставлен без кэша | Vova |
| search#918 | Проверка перед выкаткой прогревала дешёвые запросы, а оценивала холодными дорогие, поэтому падала каждый раз на разных тестах; теперь прогреваются и те четыре дорогих запроса, которые набор тестов действительно проверяет. | 4 проверяемых запроса без прогрева (7.15s -> 0.43s, 7.73s -> 0.40s, 6.26s -> 1.30s, 2.36s -> 0.42s на втором вызове) -> добавлены в список прогрева, ~15s к прогону | Vova |
| search#917 | Последний пример для копирования в репозитории всё ещё указывал на старый dev-адрес поиска, который перестанет открываться, как только этот сервис удалят; теперь там новый. | одна строка с примером, search-nest-... -> search-api-dev-...; два оставшихся упоминания оставлены намеренно — как происхождение образа и как история | Vova |
| search#916 | Dev-сервис поиска был назван по фреймворку, который в нём просто оказался и который со временем меняется; сам сервис и его сценарий выкатки переименованы по роли. | сервис search-nest -> search-api-dev, сценарий deploy-nest.yml -> deploy-search-api-dev.yml, образ search-nest -> crop-search; боевой search-api не тронут | Vova |
| search#914 | Проверочный скрипт объявлял фильтр каталога сломанным, когда с ним всё было в порядке, а тест по Kuhn использовал 150,000 и как нижнюю, и как верхнюю границу; теперь оба сравнивают живые числа. | скрипт падал на 1,140,643 строках при ожидаемом коридоре 200K-400K и указывал на сервис, удалённый в #839 -> сравнение живого с живым, публикуемых NHL 572,922 < нефильтрованных 975,998, exit 0 | Vova |
| search#913 | У боковой панели фильтров не было ограничения по времени, поэтому при загруженной базе она висела пятнадцать секунд, а потом отдавала ошибку или молча показывала пустой результат; теперь она сразу отвечает отказом. | все девять веток /api/filters ограничены 8000ms: ~15s (замеры 15,082 / 16,114 / 14,360ms) и 500 или молчаливый total: 0 -> 503 + Retry-After | Vova |
| search#910 | Одна из двух проверок, роняющих выкатку, — зашитый в код предел размера каталога, который отстал от реального роста; теперь она сравнивает два живых счётчика и устареть больше не может, а вторая причина падений приедет отдельно. | фиксированный потолок 1,000,000 против 1,140,643 публикуемых строк -> живая точка отсчёта 1,567,259 через catalogTotal=true, который считает только количество; отданные строки совпадают 20/20 | Vova |
| search#909 | Никто не выполнял правило в базе, которое решает, есть ли деталь в наличии, поэтому фильтр по наличию и значок наличия могли молча расходиться; теперь оба прогоняются по одной матрице из 26 строк. | CASE по наличию проверялся только через toBeDefined() -> настоящий CASE и фильтр выполняются на контейнере Postgres в CI; две подсаженные ошибки дали 3 и 4 падения | Denis |
| search#906 | Поисковая половина поезда правок для обхода роботами, в ней одно изменение: список схем теперь возвращает собственный веб-адрес каждой схемы рядом с адресом модели. | 1 дочерний PR, 5 файлов, +90/-0; lint, typecheck, 2,736 тестов и 176 офлайн-тестов зелёные | Vova |
| search#905 | Список схем по детали никогда не возвращал собственный веб-адрес каждой схемы, поэтому витрина могла ссылаться только на старую форму адреса, которая перенаправляет; теперь адрес возвращается. | /api/parts/{pn}/assemblies получил assemblySlug, null для брендовых каталогов; в боевой PG slug есть у 465,099 из 465,099 узлов | Vova |
| search#904 | Фид деталей мог предлагать только то, что лежит на нашем собственном складе, поэтому в него попадали 2,684 детали; теперь в нём 97,578 деталей, которые можно продать, с фото и ценой. | источник indexable_parts_v1 + merchant_eligible -> part_indexing_candidates_v1 + проверки картинки, предложения и продажи, 2,684 -> 97,578 строк; размер части 5,000 -> 25,000 (20 -> 4 источника данных Merchant Center); адреса картинок ct/ 404 -> 200 | Vova |
| search#902 | Плановое обновление неосновных зависимостей поискового сервиса, включая форматтер кода: его новое правило нашло проверки в тестах, которые упали бы с исключением вместо честного провала. | biome 2.5.2 -> 2.5.8 (схема выровнена), Anthropic SDK 0.106 -> 0.117.1, yaml 2.8.1 -> 2.9.0, postgres 3.4.8 -> 3.4.9; 2734 модульных и 176 офлайн-тестов зелёные | Vova |
| search#900 | Нигде не было записано, зачем каждому необязательному запросу нужен лимит по времени, поэтому то же пятнадцатисекундное зависание можно было вернуть заново; теперь правила и ловушки записаны в руководстве по сервису. | не описано -> 34 строки в CLAUDE.md: лимиты запросов, три ловушки тихого пропуска ошибки и как проверить кандидата на выкатку до релиза | Vova |
| search#897 | За этим ограничением сломанный запрос по бренду всё равно повторялся на каждом обращении и жёг ресурсы базы; теперь неудача запоминается на пять минут, а не повторяется бесконечно. | падающий снимок по бренду повторялся ~240 раз в час -> 12; q=ford 15,330ms -> 5,050ms на проверке перед выкаткой; устаревший потолок просмотра 700K -> 1,000,000 | Vova |
| search#896 | Поиск всего, чего кэш ещё не видел, ждал до пятнадцати секунд на сломанном запросе по бренду; теперь этот запрос сдаётся через 1.5 секунды. | холодный /api/search в сумме 15,661ms (по часам 17.36s) -> веер запросов по брендам ограничен 1500ms, и у этой ветки наконец появился свой сегмент Server-Timing | Vova |
| search#894 | После перехода на другой фреймворк в коде поиска остались дублирующиеся места для настроек, базы, логов и телеметрии, а два из четырёх правил о границах охраняли папки, которых никогда не существовало; исправлено и то, и другое. | lint:arch 2 из 4 правил мертвы -> 6 из 6 проверены вживую подсаженными нарушениями; bun test src 2704 прошло / 0 упало до и после | Vova |
| search#892 | У фильтров «отгрузка в тот же день» и «с фото» не было счётчика, поэтому никто не знал, что они ничего не вернут; теперь API фильтров считает оба, и витрина может выключить фильтр, ведущий в тупик. | счётчиков переключателей не было -> toggleCounts в режиме просмотра /api/filters за один дополнительный проход, null (не 0) при ошибке; у 6 из 45 проверенных пар категория x бренд 0 деталей с отгрузкой в тот же день | Vova |
| search#891 | Схема деталей могла показывать деталь как «В наличии», а её собственная страница каталога — как «Нет в наличии»; теперь оба места берут один и тот же сигнал со склада через общее правило. | строки спецификации определялись только по устаревшей оси возможности заказа -> собственный resolveRowAvailability плитки каталога; проверка на совпадение: раньше падало 8 случаев из 19 -> 19 проходят | Denis |
Сервисы заказов и платежей8 изменений 2 заметны покупателю или Google
| Изменение | Что это | Было и стало | Кто сделал |
|---|---|---|---|
| ps#624 | Ответ UPS без цены превращался в вариант бесплатной доставки, который покупатель мог выбрать, а реальный счёт перевозчика ложился на нас; теперь такие варианты отбрасываются, а не попадают в расчёт. | вариант без цены показывался за $0.00 -> отбрасывается и пишется в лог, расчёт падает, если отброшены все; у parseRates было 0 тестов -> 9 (6 падали до исправления) | Денис |
| ps#622 | Пять почти одинаковых тестовых задач, и каждая оплачивала минимальную минуту тарификации ради нескольких секунд тестов; теперь это одна задача с тем же покрытием. | 5 задач, 75 с реальной работы / 300 с в счёте -> 1 задача, те же 24 файла тестов; примерно 420 ubuntu-минут в месяц | Вова |
| ps#621 | Плановое обновление стороннего кода во всех сервисах плюс одна намеренная фиксация версии, которая не пускает версию библиотеки для базы данных, несовместимую с Bun. | 13 пакетов обновлены внутри мажорной версии, biome 2.3.15 -> 2.5.8; bson без фиксации -> закреплён на 7.0.0 (на 7.3.2 падали 10 тестов доставки, набор documents вообще не стартовал) | Вова |
| ps#620 | Четыре бэкенд-сервиса текут по памяти не от нагрузки, а просто со временем — один терял 2.87 MiB/ч, не обслуживая вообще ни одного запроса, — и его дважды в неделю убивал OOM; среда выполнения переезжает на выпуск Bun, где исправлена сама утечка в TLS. | Bun 1.3.9 -> 1.3.14 в 25 файлах; базовая утечка 2.87-4.43 MiB/ч, проверка — длительный прогон на dev | Вова |
| ps#619 | В руководстве по репозиторию теперь записана ловушка с моками в тестах, из-за которой сборка целый день была красной, — чтобы следующий не открывал её заново той же дорогой ценой. | раздел про тестирование: 3 пункта без единого слова про моки -> +16 строк с обоими сценариями поломки, формой исправления и расхождением между счётчиками пропущенных и упавших тестов | Вова |
| ps#618 | Заглушки из одного файла тестов протекали во все остальные, скрывая 16 тестов и держа гейт на слияние красным целый день; теперь заглушки полные и после теста восстанавливаются. | Test - documents красный на dev, 41 тест в 7 файлах -> 58 в 7 файлах, все зелёные | Вова |
| ps#615 | Express Checkout брал цену из браузера, поэтому подделанная корзина могла купить деталь за $289 за один цент; теперь цены сначала перепроверяются по каталогу. | вызовов validateCheckout в payment-intent.ts: 0 -> 1, та же проверка, что и на размещённой странице оплаты; новый набор тестов 6 проходят / 4 падают -> 10 проходят / 0 падают | Денис |
| ps#613 | Суммы, которые записываются в оплаченный заказ, не проверял никто, поэтому налог, доставка, скидка или итог могли поменяться незаметно; новый тест фиксирует точные цифры на обоих путях оплаты. | ноль проверок объекта заказа (create).toHaveBeenCalledWith не встречалось в платёжном сервисе нигде) -> 7 случаев, 17 вызовов expect(), оба пути записи заказа | Денис |
Внутренняя диспетчерская по данным о деталях5 изменений 0 из них заметны покупателю или Google
| Изменение | Что это | До и после | Кто сделал |
|---|---|---|---|
| crm#22 | Фид поставщика, уверенно отвечающий неверными данными, прошёл бы все проверки; теперь при каждой оценке здоровья заново проверяются двадцать деталей с заранее известным ответом — по четырём фидам. | только проверки распределения -> 20 эталонных деталей (по 5 на Briggs, Kuhn, Ventrac, CNH) добавлены в проверку plausible; эталонная деталь, которую ни разу не прочитали, считается нарушением | Alex |
| crm#21 | На плитках дашборда теперь есть динамика день ко дню, свежесть оценивается по каждому бренду отдельно, а не по общим 48 часам, и пропущенный ночной свод теперь поднимает алерт. | одно окно свежести 48 ч для всех брендов -> у каждого бренда свой freshness_threshold_hours; на плитках появились дельта и 30-дневный спарклайн; проверка пропущенного свода по расписанию */30 9-12 UTC | Alex |
| crm#20 | Аудит тишины закрыл три слепые зоны: семь заданий, о неявке которых никто не узнавал, счётчики изменений, мерившие объём, а не новости, и страницы брендов, где никогда не показывалось время обновления данных. | 7 линий, для которых незапуск ничего не значил -> под наблюдением; eparts отдаёт реальную разницу по BOM/хотспотам вместо числа переписанных строк; три периферийных фида проставляют source_max_checked_at | Alex |
| crm#19 | Фиды Kuhn, Ferris и Ventrac каждую ночь сообщали о нуле изменений, хотя наличие у них менялось, поэтому дашборды показывали ложное спокойствие, а здоровые бренды шли к ложным тревогам. | changes_found застрял на 0 на всех трёх фидах -> считаются реальные смены состояний (KUH 512, FER 24, VNT 59 за 14 дней проходили как «изменений нет») | Alex |
| crm#18 | Без ручных запросов никто не видел, что происходит с данными о деталях; теперь есть дашборды с размером каталога, свежестью и ежедневными изменениями по каждому бренду. | проверить здоровье каталога можно было только запросами вручную -> страницы /dash/ отдаются из ночного свода DailyBrandMetric в 07:30 UTC, 69 новых тестов | Alex |
Что работало до и что работает сейчас — то самое, к чему откатываться
Левая половина каждой пары снята с работающих систем в четверг 20 августа, до релиза. Правая — с тех же систем в субботу 22 августа, после него. Откатываться означает вернуться к левой, а строка, которой в четверг не хватало — имя ревизии для отката поиска — заполнена ниже.
| Витрина | 0ee970f4b → 137e97ed2 | Работает с пятницы, со второй половины дня. Vercel хранит предыдущую сборку, так что откат — это одна кнопка. |
|---|---|---|
| Поиск | search-nest:2da1a51e → crop-search:678b7af2 | Подменили ночью перед релизом, раньше витрины. Одним шагом уехали тридцать два изменения. Старый образ лежит по пути в реестре, который переименовали в середине недели, поэтому адрес, к которому откатываться, — не тот, куда пишет новая сборка; он выписан здесь целиком, и это теперь единственное место, где он записан. |
| Сервисы | be865716b → d35bec865 | Сервисы заказов и оплаты. Изменения, которые ждали очереди — четыре из них с 11 августа, — уехали вместе. |
| Имя ревизии для отката | search-api-00019-kxl | Это та строка, где в четверг
стояло «не считано»: истёк логин в облако, и имя не выдумывали. Считано напрямую сегодня: поиск отдаёт
всё с ревизии search-api-00042-fim, а та, что выше, — то, что она заменила. Одно, что
надо знать до отката: поиск теперь берёт себе до двадцати машин, тогда как в четверг был зажат на двух,
и возврат старого образа этого не возвращает. |
| База данных | без изменений | Ни в работе по витрине, ни в работе по поиску на этой неделе миграций не было, и с релизом ни одна не выполнялась. Дозаливка данных по деталям, которая в живую базу действительно писала, прошла за несколько дней до этого и уже была на месте. |