Skip to content

ТЗ — Google-сервисы (cms/integration-google)

Слой: 🟡 функц-модуль · Зрелость доноров: ★ · Донор: — Статус: ТЗ к разработке

Назначение и возможности

Серверные интеграции с сервисами Google: Maps (данные для блока карты), reCAPTCHA (защита форм ядра), серверные события Google Analytics 4 (Measurement Protocol), Search Console (индексация) и Merchant Center (товарный фид). Клиентская вставка GA4-счётчика — в cms/analytics, здесь только серверные вызовы API.

  • Коннектор Google Maps — геокодирование адресов для блока карты;
  • provides: captcha-provider — reCAPTCHA для защиты форм ядра;
  • Серверные события GA4 (Measurement Protocol) — дополнение к клиентской GA, с явным гейтом по Consent Mode v2 (analytics_storage) через cms/cookie-consent;
  • Search Console: подтверждение владения сайтом, ping sitemap и проверка статуса индексации URL при публикации (через SitemapRegistry/previous_slug ядра);
  • Google Merchant Center: выгрузка и предварительная валидация товарного фида по расписанию — невалидные позиции отклоняются с отчётом, не половинчатая заливка;
  • Google Ads: офлайн-конверсии по LeadCreated (аналог Директа у Яндекса), OAuth с рефреш-токеном — Ads API не поддерживает сервис-аккаунты;
  • Кеширование результатов геокодирования — не бьёт в API повторно на тот же адрес;
  • Единая точка настройки сервис-аккаунта и OAuth-подключений Google-сервисов.

Зависимости и выключение

requires: cms/integrations-bus · suggests: cms/analytics, cms/cookie-consent (гейт Consent Mode v2), cms/utm (обогащение атрибуции) · provides: captcha-provider

Поведение при выключении: блок карты работает на последнем закешированном геокодировании; формы ядра переключаются на резервный captcha-provider (если подключён) или работают без капчи с усиленным rate-limit; фид Merchant перестаёт обновляться (последняя валидная версия остаётся доступной); GA4-события Measurement Protocol и офлайн-конверсии Ads не отправляются — лид/заказ создаётся штатно, форма не блокируется; sitemap-пинг Search Console не выполняется — переиндексация идёт по обычному краулингу Google.

Модель данных

ТаблицаКлючевые поляПримечание
cms_integration_google_geocode_cacheid, address_hash, coordinates (json), cached_atкеш геокодирования адресов
cms_integration_google_merchant_feed_logid, status, items_count, rejected_count, generated_atжурнал выгрузки товарного фида
cms_integration_google_service_accountid, key_ref, scopes (json), status (enum), verified_atссылка на JSON-ключ сервис-аккаунта (.env) + проверенные scope
cms_integration_google_ads_oauthid, access_token (encrypted), refresh_token (encrypted), expires_at, status (enum), connected_by, connected_atOAuth-подключение Google Ads (единственное, сервис-аккаунт для Ads не поддерживается)
cms_integration_google_ads_conversions_logid, lead_id (FK, nullable), gclid, goal_code, status (enum), sent_atжурнал офлайн-конверсий Ads

Индексы: уникальный address_hash; coordinatesjson() + cast array; status/goal_code не Enum (goal_code — строка, whitelist на уровне валидации); status — PHP Enum; key_ref/access_token/refresh_tokenencrypted cast; lead_id в ads_conversions_logconstrained('cms_leads')->nullOnDelete(); журнальные таблицы (merchant_feed_log, ads_conversions_log) — append-only, BRIN по *_at.

ПДн-паспорт: gclid — идентификатор рекламного клика Google, не ПДн (сопоставление с личностью — на стороне Google); в Ads уходят только gclid + goal_code, не email/телефон/ФИО лида. Товарный фид Merchant — характеристики товара (название, цена, наличие, картинка), не данные покупателя. key_ref/токены — секреты подключения, не ПДн, шифруются и не подлежат экспорту в «выгрузить всё по субъекту».

Входные и выходные данные

Входы — всё, что не перечислено ниже, отвергается (whitelist-принцип §11 стандарта):

ИсточникДанные/поляЧем валидируется
GET /api/v1/integration-google/geocodeaddress (строка)FormRequest: длина, whitelist символов, rate-limit по IP
Filament: настройка модуляфлаги включения, geocode_cache_ttl_days, geocode_timeout_ms, GA4 Measurement ID/API secret (.env-ссылки)FormRequest, числовые поля — диапазоны
Загрузка JSON-ключа сервис-аккаунта (Filament)файл ключапроверка формата JSON + тестовый вызов API (Search Console/Merchant) при сохранении, не «сохранили и надеемся»
OAuth callback Ads (GET .../ads/oauth/callback)code, state от Google OAuthstate сверяется с ранее выданным (CSRF-защита)
Событие ядра CookieConsentGiven (слушается, от cms/cookie-consent)categories[]Контракт события cms/cookie-consent; используется для Consent Mode v2
Событие ядра LeadCreated (слушается)стандартный payload + опциональное скрытое поле формы _gclidКонтракт события ядра; _gclid не гарантирован (см. «Крайние случаи»)
Событие ядра PagePublished/ContentEntrySaved/PageDeleted (слушается)slug, previous_slug (nullable)Контракт события ядра (ревизия 14.07.2026)
Admin API: генерация фида Merchant— (без тела)Idempotency-Key обязателен (мутация с внешним эффектом, §7 стандарта)
Импорт / вебхукНе применимо: входящих вебхуков от Google нет, внешние вызовы — только исходящие через cms/integrations-bus

Выходы

ПотребительДанныеФормат
Блок карты (посетитель)coordinates (или fallback/meta.degraded)JSON {data, meta}
Filament: журнал фида/конверсийзаписи журналовkeyset-пагинация {data, meta.next_cursor}
GA4 (Measurement Protocol)событие конверсии + consent_stateформат Google MP, через cms/integrations-bus
Google Merchant Centerвалидированный товарный фидContent API формат, через cms/integrations-bus
Google Search Consolesitemap ping / статус индексации URLSearch Console API, через cms/integrations-bus
Google Ads (Offline Conversions)gclid, goal_code, datetimeформат Ads API, через cms/integrations-bus
Подписчики событийGoogleMerchantFeedGenerated, GoogleMerchantFeedRejectedItems, GoogleGeocodeFailed, GoogleServiceAccountRevoked, GoogleAdsOAuthTokenExpired, GoogleAdsConversionSent/Failed, GoogleSearchConsolePinged/Failedpayload по таблице в «События и обмен»

Настройки (группа integration-google)

КлючТипДефолтaffectsPageCacheОписание
integration-google.maps_enabledbooltrueдаИспользовать Google Maps в блоке карты
integration-google.recaptcha_enabledboolfalseнетВключить reCAPTCHA на формах ядра
integration-google.merchant_feed_enabledboolfalseнетKill-switch генерации фида Merchant
integration-google.ga4_server_events_enabledboolfalseнетKill-switch серверных событий Measurement Protocol
integration-google.search_console_ping_enabledboolfalseнетKill-switch sitemap-пинга Search Console
integration-google.ads_conversions_enabledboolfalseнетKill-switch офлайн-конверсий Ads
integration-google.geocode_cache_ttl_daysint90нетСрок жизни кеша геокодирования
integration-google.geocode_timeout_msint1500нетТаймаут синхронного вызова геокодера (обязан быть ≤2000 мс — §9 стандарта)
integration-google.geocode_daily_quotaint2500нетИнформационный лимит бесплатной квоты Maps Platform — сверх неё оплата по счётчику
integration-google.consent_gate_modeenum(strict/cookieless_ping)strictнетПри отсутствии analytics_storage: strict — событие не отправляется, cookieless_ping — отправляется в denied-режиме без идентификаторов (для моделирования Google)
integration-google.merchant_feed_log_retention_daysint180нетХранение журнала выгрузки фида
integration-google.ads_conversions_log_retention_daysint90нетХранение журнала офлайн-конверсий Ads

Секреты (ключ API Maps, ключ reCAPTCHA, GA4 Measurement ID + API secret, JSON-ключ сервис-аккаунта, client_id/client_secret OAuth-приложения Ads) — только .env. OAuth access_token/refresh_token Ads-подключения — в БД зашифрованы.

API

МетодПутьДоступНазначение
GET/api/v1/integration-google/geocoderate-limitГеокодирование адреса для блока карты (с кешем)
GET/api/v1/admin/integration-google/merchant-feedadmin (integration-google.view)Журнал выгрузки фида (включая отчёт по отклонённым позициям)
POST/api/v1/admin/integration-google/merchant-feed/generateadmin (integration-google.manage), Idempotency-KeyРучной запуск генерации фида
GET/api/v1/admin/integration-google/service-accountadmin (integration-google.view)Статус сервис-аккаунта (scope, дата проверки)
POST/api/v1/admin/integration-google/service-accountadmin (integration-google.oauth-manage)Загрузка/замена JSON-ключа сервис-аккаунта
GET/api/v1/admin/integration-google/ads/oauthadmin (integration-google.view)Статус OAuth-подключения Ads
POST/api/v1/admin/integration-google/ads/oauth/connectadmin (integration-google.oauth-manage)Инициировать OAuth-подключение Ads
GET/api/v1/admin/integration-google/ads/oauth/callbackadmin, state-валидацияCallback OAuth, сохранение токена
GET/api/v1/admin/integration-google/ads/conversionsadmin (integration-google.view)Журнал офлайн-конверсий Ads

Все журналы — keyset-пагинация, не OFFSET. Ответ /geocode при недоступности геокодера — 200 с meta.degraded: true и закешированным/пустым результатом (конвенция деградации, ревизия ядра 14.07.2026 п.2 — применяется ко всем provides-провайдерам и внешним вызовам).

Компоненты

Блоки: карта с точками (геокодирование через сервис). Filament: журнал выгрузки фида Merchant (с отчётом об отклонённых позициях), панель сервис-аккаунта (scope, дата проверки), панель OAuth-подключения Ads, журнал офлайн-конверсий. Команды: cms:integration-google:generate-feed --json, cms:integration-google:refresh-ads-token --json, cms:integration-google:verify-service-account --json, cms:integration-google:warm-geocode-cache --json.

Демо-контент: сидер создаёт записи geocode_cache (типовые адреса) и запись в merchant_feed_log (в т.ч. один демо-пример отклонённой позиции) — блок карты и журнал фида видны в /_gallery без ручного ввода ключей API.

Фронтенд-бюджет: блок карты — ленивая загрузка JS API Maps по видимости (IntersectionObserver), не в критическом пути первой отрисовки; резерв размеров контейнера — без CLS. Источник домена Maps JS API регистрируется в CSP через канонический фильтр FilterBus security.csp (ревизия №2 ядра, п. 5); инлайн-код инициализации — с nonce ядра.

События и обмен

СобытиеКогдаPayload
GoogleMerchantFeedGeneratedфид Merchant сгенерирован и принятitems_count, rejected_count, generated_at
GoogleMerchantFeedRejectedItemsчасть позиций фида не прошла валидациюrejected_count, errors[] (по позиции: причина отклонения)
GoogleGeocodeFailedгеокодирование адреса завершилось ошибкой/таймаутомaddress_hash, error
GoogleServiceAccountRevokedсервис-аккаунт отозван/недоступенscope — требуется загрузка нового ключа
GoogleAdsOAuthTokenExpiredрефреш Ads-токена невозможен— требуется ручная переавторизация
GoogleAdsConversionSent / GoogleAdsConversionFailedофлайн-конверсия принята/отклонена Adslead_id, goal_code[, error]
GoogleSearchConsolePinged / GoogleSearchConsolePingFailedsitemap-пинг отправлен/не удалсяsitemap_url[, error]

Реализует provides: captcha-provider (reCAPTCHA). Слушает CookieConsentGiven (гейт GA4), LeadCreated (офлайн-конверсии Ads), PagePublished/ContentEntrySaved (sitemap-пинг с учётом previous_slug). Весь вызов внешних API Google — через cms/integrations-bus (канал 4).

Таблица взаимодействий

Сущность/модульКаналНаправлениеЧто происходит
Ядро: формы (лиды/комментарии/отзывы)provides-контракт captcha-provider (3)inЯдро резолвит reCAPTCHA через DI при recaptcha_enabled=true
cms/cookie-consent (suggests)событие CookieConsentGiven (1)inГейт отправки GA4 Measurement Protocol по категории analytics (Consent Mode v2)
Ядро: LeadCreatedсобытие (1)inТонкий слушатель кладёт job офлайн-конверсии Ads в очередь, если есть _gclid
Ядро: PagePublished/ContentEntrySaved/PageDeletedсобытие (1)inJob sitemap-пинга Search Console (использует SitemapRegistry ядра, не собственный список URL)
cms/integrations-bus (requires)сервис-вызов (4)outВсе HTTP-вызовы к API Google (Maps, reCAPTCHA, GA4, Search Console, Merchant, Ads)
cms/utm (suggests)сервис-вызов (4, опционально)inОбогащение атрибуции офлайн-конверсии данными визита
cms/healthсобытие (1)outGoogleServiceAccountRevoked/GoogleAdsOAuthTokenExpired/квоты на исходе → алерт
cms/audit (если включён)событие (1)outФиксирует смену сервис-аккаунта, подключение Ads, генерацию фида

Фоновая работа

Очередь integration-google:

  • GenerateMerchantFeed — по расписанию, валидирует каждую позицию перед отправкой (обязательные поля, схема, доступность картинки) — невалидные позиции отклоняются с построчным отчётом, валидные отправляются, частичная заливка невалидных запрещена;
  • RefreshAdsOAuthToken — по расписанию, аналогично Yandex-механизму (см. «Крайние случаи»);
  • VerifyServiceAccount — по расписанию, тестовый вызов Search Console/Merchant API для раннего обнаружения отзыва прав (не ждать провала боевой отправки фида);
  • SendGA4Event — из слушателя LeadCreated/OrderCompleted (если cms/analytics издаёт свои цели), гейт по CookieConsentGiven;
  • SendAdsConversion — из слушателя LeadCreated, идемпотентна по lead_id;
  • PingSearchConsole — из слушателей публикации/удаления, идемпотентна по url.

Геокодирование — единственный синхронный внешний вызов, с таймаутом geocode_timeout_ms (≤2000 мс) и graceful fallback. Ретраи с backoff и circuit breaker — на уровне cms/integrations-bus.

Эксплуатация (ранбук): метрики integration_google_geocode_requests_total, integration_google_geocode_cache_hit_ratio, integration_google_merchant_rejected_ratio, integration_google_ads_token_ttl_seconds. Алерты: GoogleServiceAccountRevoked, GoogleAdsOAuthTokenExpired, доля отклонённых позиций фида выше порога, доля GoogleGeocodeFailed выше порога за час.

СимптомЧто проверить / команда
Карта не показывает новые адресаmaps_enabled, ключ API Maps в .env, доля GoogleGeocodeFailed за сутки
Фид Merchant не обновляется / много отклоненийmerchant_feed_enabled, cms:integration-google:generate-feed --json, отчёт по отклонённым позициям в журнале
GA4-события не долетаютga4_server_events_enabled, статус согласия analytics в cms/cookie-consent, consent_gate_mode
Ads-конверсии не долетаютстатус OAuth Ads (cms:integration-google:refresh-ads-token --json), наличие _gclid в форме
Search Console не индексируетcms:integration-google:verify-service-account --json, статус сервис-аккаунта, доступность SitemapRegistry-источников

Бэкап/рестор: все таблицы модуля — в бэкапе. geocode_cache не требует пересоздания после рестора (не критично, только для скорости — прогревается трафиком или warm-geocode-cache); журналы — исторические данные, восстанавливаются как есть.

Производительность и кеш

Ожидаемые объёмы: типичный сайт — геокодирование единиц-десятков адресов при сохранении блока карты, публичный /geocode не гоняет геокодер на каждый визит (координаты уже сохранены); каталог с калькулятором доставки — до нескольких сотен геокодирований в сутки. Бесплатная квота Maps Platform — порядка 2500 запросов/сутки на метод в рамках месячного кредита (geocode_daily_quota, информационная настройка), сверх неё — оплата по счётчику. GA4-событий — по числу конверсий с согласием на аналитику. Merchant-фид — по размеру каталога (сотни–десятки тысяч товаров), генерация — батчами, не построчными вызовами API.

Горячие пути и бюджет запросов: GET /geocode — синхронный внешний вызов только при промахе кеша (0 внешних вызовов при попадании, 1 индексный SELECT); GA4-событие/ Ads-конверсия/sitemap-пинг — асинхронно из очереди, не в горячем пути публикации/лида (слушатель тонкий).

Почему синхронный вызов геокодера допустим (§9 стандарта: внешний HTTP на горячем пути запрещён, кроме синхронных подсказок с таймаутом и graceful fallback): вызов ограничен geocode_timeout_ms (дефолт 1500 мс, обязан быть ≤2000 мс), при таймауте — немедленный fallback на кеш/пустой результат. Все остальные внешние вызовы — строго из очереди integration-google.

Критичные индексы: уникальный address_hash на geocode_cache; lead_id на ads_conversions_log под идемпотентность job; BRIN по *_at на журнальных таблицах.

Что кешируется: тег integration-google:geocode — результаты геокодирования (TTL geocode_cache_ttl_days, инвалидация по истечению TTL). Статус сервис-аккаунта/OAuth и настройки — стандартным кешем группы (SettingsStore ядра, §6 стандарта). Журналы — не кешируются (низкочастотные admin-запросы с keyset-пагинацией).

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

  • reCAPTCHA реализует контракт captcha-provider, проверка токена на сервере перед обработкой формы — токен не проверяется на клиенте; домен виджета (google.com/gstatic.com) регистрируется модулем через канонический фильтр FilterBus security.csp, инлайн-инициализация — с nonce ядра, не собственным;
  • сервис-аккаунт vs OAuth: сервис-аккаунт (JSON-ключ) — для Search Console API и Merchant Content API (server-to-server, права выдаются вручную владельцем аккаунта Google одноразово при настройке); OAuth с рефреш-токеном — только для Google Ads API (Ads API не поддерживает сервис-аккаунты, требует пользовательского согласия);
  • отзыв сервис-аккаунта на стороне Google (владелец убрал права) обнаруживается джобой VerifyServiceAccount, не только по факту провала боевого вызова;
  • GA4 Measurement Protocol-события не отправляются без согласия analytics_storage (см. «Крайние случаи», Consent Mode v2) — публичный /api/v1/analytics/event не относится к этому модулю (владелец — cms/analytics), здесь только серверные MP-вызовы;
  • валидация фида Merchant перед отправкой — обязательные поля (id, title, price, availability, image_link), схема, доступность картинки по URL; невалидные позиции отклоняются с построчным отчётом, заливка валидных не блокируется отклонёнными;
  • секреты (ключи API, JSON-ключ сервис-аккаунта, client_id/client_secret Ads) — только .env; OAuth-токены Ads — encrypted cast, маскируются в журнале cms/integrations-bus.

Права: integration-google.view, integration-google.manage, integration-google.oauth-manage.

Матрица ролей:

ДействиеАдминистраторМенеджерРедакторStudio
Просмотр журналов (фид/конверсии) — view
Ручной запуск генерации фида — manage
Загрузка ключа сервис-аккаунта / подключение Ads OAuth — oauth-manage
Правка ключей API (.env)✅ (доступ к серверу)
Kill-switch (ads_conversions_enabled, ga4_server_events_enabled, merchant_feed_enabled, search_console_ping_enabled)

UX-требования

Админ: ключи API/сервис-аккаунт не заданы → понятная заглушка «интеграция не настроена — обратитесь к разработчику»; отозванный сервис-аккаунт → баннер «Сервис- аккаунт Google отклонён, загрузите новый ключ»; истёкший OAuth Ads → баннер «Требуется переавторизация Google Ads» с кнопкой «Переподключить»; отчёт по отклонённым позициям фида — скачиваемый список с причиной по каждой позиции, не просто счётчик; массовое действие в журнале конверсий — повторная отправка выбранных failed; подтверждение при замене JSON-ключа сервис-аккаунта («Действия от имени старого ключа прекратятся немедленно»).

Посетитель: недоступность карты (ключ невалиден/лимит/таймаут) → последний закешированный результат либо адрес текстом со ссылкой «открыть в Google Maps», не пустой прямоугольник; недоступность reCAPTCHA → форма отправляется без капчи с усиленным rate-limit (деградация §11 ядра), без видимой посетителю ошибки; отсутствие согласия на аналитику → сайт не показывает ошибок, GA4-события просто не уходят.

Крайние случаи и типовые баги

  • Consent Mode v2: нет согласия на analytics_storage → GA4 Measurement Protocol- события не отправляются (consent_gate_mode=strict, дефолт) либо отправляются в denied-режиме без идентификаторов посетителя (cookieless_ping) — выбор явная настройка, не хардкод одного поведения; событие AnalyticsGoalTriggered от cms/analytics при этом всё равно фиксируется на сервере (агрегированная статистика), только внешняя отправка в GA4 гейтится;
  • Отсутствие cms/cookie-consent (suggests не установлен) → без сигнала о согласии ga4_server_events_enabled фактически не может быть безопасно true: Filament требует явного подтверждения при включении без установленного cms/cookie-consent («вы подтверждаете правовое основание для отправки без баннера согласия»);
  • Сервис-аккаунт отозван (владелец Google-аккаунта убрал права) → VerifyServiceAccount обнаруживает это плановой проверкой, не только по провалу боевой генерации фида; GoogleServiceAccountRevoked, Search Console/Merchant приостанавливаются с понятным сообщением, geocoding и reCAPTCHA не затронуты (используют отдельные API-ключи, не сервис-аккаунт);
  • Истёк refresh_token Ads → аналогично Яндекс.Директу: GoogleAdsOAuthTokenExpired, офлайн-конверсии приостанавливаются без ошибки в остальном сайте, нужна ручная переавторизация;
  • Невалидные позиции в фиде Merchant (нет gtin/битая ссылка на картинку/цена ≤ 0) → отклоняются с построчным отчётом (GoogleMerchantFeedRejectedItems), валидные позиции отправляются — частичная заливка без предупреждения запрещена;
  • LeadCreated без _gclid (лид не с рекламы Google) → офлайн-конверсия не отправляется — ожидаемое поведение, не ошибка;
  • Квота GA4 Measurement Protocol / Search Console API исчерпана → job откладывается до следующего окна через cms/integrations-bus, не теряется молча;
  • Смена схемы ответа Geocoder API (Google меняет структуру ответа) → парсер валидирует ожидаемую структуру перед извлечением координат; неожиданная — GoogleGeocodeFailed, fallback на кеш/пустой результат, не необработанное исключение;
  • Гонка: двойной сабмит формы с одним gclid → уникальность (lead_id) в ads_conversions_log предотвращает дубль job;
  • Выключен cms/integrations-bus (requires) → ни один внешний вызов не работает; geocoding деградирует на кеш, остальные функции приостанавливаются с понятным сообщением (§3 стандарта);
  • Измерение city/site отсутствует → кеш геокодирования и офлайн-конверсии не завязаны на city_id/site_id, один код-путь в обоих режимах;
  • ⚠️ Противоречие: канонический payload LeadCreated не гарантирует _gclid — аналогично _yclid у Яндекса (см. /cms-v2/modules/integration-yandex). Разрешение: требование к конкретной форме (скрытое поле через движок полей), не к событию ядра; оба модуля документируют один и тот же паттерн независимо, чтобы не вводить общую зависимость ради одного поля;
  • ⚠️ Противоречие: исходное ТЗ не задавало таймаут синхронного вызова геокодера (нарушение §9). Разрешение: настройка geocode_timeout_ms (дефолт 1500 мс, ≤2000 мс).

Донорский код

Донор: — (новая разработка).

Легаси-импорт (§16 стандарта): не применимо — интеграция не хранит доменных данных для миграции с донора. Полезный batch-сценарий — прогрев кеша геокодирования по уже существующим адресам каталога командой cms:integration-google:warm-geocode-cache --source=<профиль> --dry-run после переезда клиента, чтобы блок карты не делал массовых синхронных вызовов геокодера в первые дни.

Тесты и приёмка

  • [ ] Мок API Google Maps: геокодирование с кешированием и graceful fallback на таймаут geocode_timeout_ms (дефолт ≤2000 мс);
  • [ ] reCAPTCHA реализует captcha-provider, проверяется мок-токеном в тесте формы;
  • [ ] Генерация фида Merchant валидирует позиции перед отправкой: невалидные отклоняются с построчным отчётом, валидные отправляются — частичная заливка без отчёта невозможна (тест на смешанный набор позиций);
  • [ ] GA4-событие не отправляется без согласия analytics_storage при consent_gate_mode=strict; при cookieless_ping отправляется без идентификаторов;
  • [ ] VerifyServiceAccount обнаруживает отзыв прав плановой проверкой, издаёт GoogleServiceAccountRevoked, не роняет боевую генерацию фида необработанным сбоем;
  • [ ] Рефреш OAuth Ads-токена запускается заранее; неудачный рефреш переводит подключение в expired и издаёт GoogleAdsOAuthTokenExpired, не роняя очередь;
  • [ ] Офлайн-конверсия Ads отправляется только при _gclid в лиде; повторная постановка job идемпотентна (тест на дубль);
  • [ ] При выключении модуля формы ядра работают без капчи либо с резервным провайдером;
  • [ ] Кеш геокодирования снижает число вызовов API при повторных запросах адреса;
  • [ ] Неожиданная схема ответа геокодера не роняет запрос — трактуется как GoogleGeocodeFailed;
  • [ ] Права integration-google.manage/.oauth-manage разграничивают запуск генерации фида и загрузку ключа сервис-аккаунта/подключение Ads;
  • [ ] Контрактный набор cms-testing зелёный, testbench-изоляция, feature-тест на каждый роут; тестовая БД только integration_google_test, migrate:fresh/refresh/reset запрещены.

Внутренняя база знаний студии